Skip to content

P13 — AI Connections (MCP pairing): connection-bundle service + UI #65

Description

@ehsan6sha

P13 — AI Connections (pairing)

Add an "AI Connections" feature to FxFiles that lets a user pair an AI client (via the Model Context Protocol, MCP) with their encrypted AI workspace. Pairing produces a one-time connection bundle the user copies into their AI client.

Output contract

The bundle JSON must match the MCP's CapabilityBundle wire contract (fula-mcp/src/capability.rs, CapabilityBundleJson):

{
  "endpoint": "<S3 gateway URL>",
  "jwt": "<scoped gateway JWT>",
  "workspace_secret_b64": "<base64 of 32 bytes>",
  "mcp_secret_b64": "<base64 of 32 bytes>",
  "owner_public_b64": "<base64 of 32 bytes>",
  "user_id": "<FxFiles userId>",
  "storage_api_url": "<credit host>"
}

Scope

  • New module lib/features/ai_connections/ (Riverpod Notifier<State> + NotifierProvider, models/services/providers/screens).
  • AiConnectionService:
    • generateMcpKeypair() — fresh X25519 keypair (32 secure-random bytes = secret; public derived via FFI).
    • deriveWorkspaceSecret()blake3DeriveKey('fula:ai-workspace-secret:v1', KEK) (one-way; the MCP cannot recover the KEK).
    • ownerPublicKey() — the user's X25519 sharing public key (AuthService.getPublicKey()).
    • mintScopedJwt()POST /api/mcp/tokens (P11 issuer) with the session JWT as bearer; parse token.
    • buildBundleJson() — pure builder of the exact contract above.
    • createConnection({label}) — orchestrates the above, persists ONLY the non-secret record, returns the bundle once.
  • UI: AiConnectionsScreen (list saved connections + "Create" → one-time copyable bundle dialog) + a Settings entry.

Security model

FxFiles persists only the non-secret record (MCP public key + label + createdAt). The bundle's secrets (mcp_secret_b64, workspace_secret_b64, scoped jwt) are never persisted — the bundle is shown ONCE for the user to copy. Losing it means re-pairing.

Tests

test/unit/core/services/ai_connection_service_test.dart: bundle JSON has exactly the contract keys; each *_b64 decodes to 32 bytes; the mint POST sends the right body + parses the JWT (injected http.Client); the persisted record carries no secrets.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions