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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions