Capabilities: MCP, your own keys, outside services
The Capabilities engine lets a space reach outside Zvessa, and lets anything outside build it. Three parts, all on the server, all under the same rule as everything else: changes are proposals a person approves, and every approved change is a version that can be undone.
People get the screens by adding the Connections kit (HQ → Library). It adds a Connections page with one system block, hq.connections. The engine itself works without the kit.
1. Build from anywhere (the MCP server) #
Any MCP client (Claude Desktop, Claude Code, Cursor, another app's AI) can build a space with the same tools as the in-app AI.
- Address:
{CONVEX_SITE_URL}/mcp(self-hosted defaulthttp://127.0.0.1:3211/mcp; put it behind HTTPS before using it from another machine). - Auth: an access token made on the Connections page:
Authorization: Bearer mbk_…. Tokens end after 90 days (1–365 when made) and allow 120 calls a minute. For clients that only take a URL, a token can be made as a URL token ({CONVEX_SITE_URL}/mcp/t/mbk_…); URLs end up in logs, so those can only read and propose. Batches are capped at 20 messages. - Transport: Streamable HTTP, stateless, JSON responses. Checked with the official MCP SDK client.
- A token belongs to one person in one space. It can do what both its scopes and the person's current role allow, so changing someone's role changes their tokens at once. Only a hash is stored; the token is shown once. Revoking stops it at once.
claude mcp add --transport http zvessa https://your-backend/mcp --header "Authorization: Bearer mbk_…"
| Scope | Tools | Needs role permission |
|---|---|---|
read | get_space, read_blueprint, describe_library, list_proposals, list_versions | view (proposals, versions: manage) |
propose | propose_change, add_kit, remove_kit | manage |
approve | approve_proposal, discard_proposal, undo_to_version | manage |
data.read | list_records | view |
data.write | add_record, update_record, delete_record | add (others' records: edit) |
connections | list_connections, call_connection | view / manage |
New tokens get read + propose: the client proposes, a person approves in the app. Give a token approve only for a client you'd let change the space on its own (it can still be undone; History shows "approved from <token name>"). Kids spaces can't have approving tokens.
The tools are described once in convex/lib/capabilities/tools.ts. The in-app AI gets the same ones (minus the outside-only reads it already has in its prompt, and minus approving). How to build, in words for an AI, is convex/lib/guide.ts: the in-app AI's instructions and the describe_library tool both come from it.
2. Your own keys (the vault) #
- A space's keys live in the
secretstable, sealed with AES-256-GCM under the platform's vault key and tied to their space and name. No query returns a value; only server code about to send a request opens one (openSecretinconvex/capabilities/vault.ts). - Each key has scopes: what it's for in this space (
ai: the AI chat's model key;connections: connections may send it) and the hosts it may go to (api.example.comor*.example.com). A key is refused for any other host, whatever a connection says. - Your AI key: a key with use
ai(hostapi.anthropic.com) makes the space's AI chat run on that key. Without one, the space's AI asks a manager to connect a key (the "ai.setup" block), unless the platform offers its own key (optional, off for now). - Only people who can manage the space add, change or remove keys. Changing a key's hosts or uses can only narrow them; widening needs the key pasted again, so nobody can send a key they don't have to a new place.
- Wildcard hosts can't cover shared suffixes (
*.co.uk,*.vercel.app,*.github.io…).
3. Outside services (connections) #
A connection is a setting in the blueprint, so the AI can propose one and a person approves it:
"connections": [
{ "id": "github", "name": "GitHub", "kind": "mcp", "url": "https://api.githubcopilot.com/mcp/",
"auth": { "secret": "github", "as": "bearer" }, "tools": ["search_repositories", "create_issue"] },
{ "id": "weather", "name": "Weather", "kind": "http", "url": "https://api.example.com/v1",
"auth": { "secret": "weather", "as": "query", "name": "key" } }
]
mcp: a remote MCP server (Streamable HTTP; event-stream answers are fine). Only tools the server marks read-only can be called, unless the person lists them intools.http: any HTTPS API. Calls stay underurl; onlyGETunless the person listsmethods.auth.secretnames a vault key; the server adds it as a bearer token, a header or a query parameter. Keys are scrubbed from results and errors.- Every call leaves through
convex/capabilities/egress.ts(a Node action). The host must be public HTTPS on port 443; it is resolved, every address must be public (no loopback, private, link-local, CGNAT, NAT64/6to4/Teredo, IPv4-compatible or reserved ranges), and the request is pinned to that address, so a name can't switch inside between the check and the call. No redirects, 2 MB and 30 s caps. For more depth, also firewall the backend's outbound traffic away from your private network. - Header values in the blueprint can't look like keys (
X-API-Key,Authorization…): those go in the vault. Only people who manage a space see its connections. - The in-app AI can only read through connections (GET, read-only MCP tools), and what comes back is marked as outside data, so a page or tool result can't make it send anything out. Changing calls run from an MCP client with the
connectionsscope. - Local (stdio) MCP servers can't run on the backend; connect the remote version of a server.
Files #
convex/capabilities/:tables.ts(schema),vault.ts,tokens.ts,mcp.ts+routes.ts(the MCP server),connections.ts(calling outside services),builder.ts(tool server side),run.ts(one place that checks scopes and roles and runs a tool)convex/lib/capabilities/:tools.ts(the tool list and scopes),connections.ts(connection and key checks),mcpClient.ts(calling MCP servers),crypto.ts(sealing, hashing)convex/lib/guide.ts: how to build, for any AIsrc/components/capabilities/ConnectionsPanel.tsx: thehq.connectionsblocktests/capabilities.test.ts: vault, tokens, MCP server, connections