Skip to content

MCP

Polo uses the Model Context Protocol in both directions, and the two are separate modules:

  • Polo as a client (api/src/modules/mcp/): a workspace registers an external MCP server, and agents may use the tools an administrator granted.
  • Polo as a server (api/src/modules/mcp-server/): Claude Code, Codex, Cursor or any MCP client can work with a workspace through an access token.

The rule for the first direction is the one to remember: connecting a server enables none of its tools. Discovery and permission to execute are separate steps.

An administrator registers a server on the Tools page, which calls POST /api/mcp/servers with registerMcpSchema (application/contracts.ts):

Field Meaning
name A display name
endpoint A fixed URL. Credentials, query parameters and fragments in the URL are refused.
token Optional bearer token. Sealed before it is stored.
oauth Optional, instead of a token: issuer, client registration, approved scopes

McpService.register discovers the catalog first and stores the server only if discovery succeeds. A server registered with oauth is stored first and waits for consent; its authorization state is one of awaiting_consent, connected, refreshing or reconnect_required (McpOAuthStatus).

discoverCatalog (application/catalog.ts) asks the server for its tools and turns the answer into a stored catalog:

  • Each tool gets a stable ID: tool- plus the first 16 hex characters of the SHA-256 of its name.
  • Each tool gets a fingerprint computed from its name, title, description and schemas (mcpFingerprint).
  • A catalog with more than 128 tools, or with two tools that collide on an ID, is refused.
  • A catalog that echoes the server’s own authorization token back is refused.

The catalog is stored in the server’s ServerScope with a revision. POST /api/mcp/servers/:id/refresh discovers again; if anything differs, the catalog is replaced and the revision goes up by one. Every write to a server sends expected_revision, so two administrators cannot overwrite each other.

Nothing is usable yet. An administrator now chooses tools with PUT /api/mcp/servers/:id/grants (grantsMcpSchema). Each grant is:

{ tool_id, mode: 'read' | 'write', fingerprint, verifiers: tool_id[] }

McpService.grants enforces three things:

  • The fingerprint must be current. The grant names the fingerprint the administrator reviewed. If the tool’s schema changed since, the grant is refused: “The approved MCP tool schema changed. Review the current fingerprint”.
  • A write tool needs a verifier. Every write grant must list at least one other tool that is granted as read. That read tool is how Polo checks the write afterwards.
  • A read tool has no verifiers.

A grant keeps working only while its fingerprint matches. activeGrant in domain/mcp.ts compares them every time, so if a later refresh brings a changed schema, the old grant silently stops applying until someone reviews the new one. A server cannot widen a tool after it was approved.

Server grants decide which tools exist as far as Polo is concerned. Agents are a second layer. McpProvider (infrastructure/mcp.provider.ts) exposes each actively granted tool as a target of the server’s connection, with two capabilities:

Capability Mode Arguments
mcp.read read target, schema_sha256, arguments
mcp.call write the same, plus verification

An agent’s profile must be granted mcp.read or mcp.call on the specific tool. A write tool whose verifiers are no longer active is not offered at all.

The model does not get every tool’s schema in its prompt. It sees the granted tools by name and asks for a schema with mcp.describe; the runtime keeps at most two schemas in context (MAX_MCP_SCHEMAS in runtime/operation_tools.py).

  • A read runs immediately and returns a receipt.
  • A call is a proposed operation and follows the operation contract. Its arguments must include a verification block: which read tool to call afterwards, with which arguments, which result_path to look at, and the expected value. The reviewer sees and approves all of it.
  • schema_sha256 pins the call to the schema the model read. At dispatch the client checks the live tool’s fingerprint again and refuses if it changed.
  • McpProvider sets requiresDispatchReceipt. A call whose outcome cannot be confirmed becomes unknown: “The MCP mutation outcome is uncertain. Verify the recorded operation before another write.”

Because an MCP call is fully described by its arguments and carries its own verifier, it is the one capability an administrator can pre-authorize today (preauthorizableFor).

DELETE /api/mcp/servers/:id disables the server. It is refused while the server has an approved mutation that is still unresolved, so an uncertain write cannot be orphaned.

Polo serves MCP over Streamable HTTP at <public URL>/mcp (McpEndpointController, routes POST /mcp and GET /mcp).

A member creates a token under Settings → MCP access (/api/access-tokens). The model is in domain/access.ts:

  • A token has an access level, read or act, and a separate approvals switch.
  • Each tool has a level: read, act or approve. allows(identity, level) decides. A member with the viewer role can only read, whatever the token says.
  • Polo stores only a SHA-256 hash of the token and shows the token once.
  • The member’s role is read each time the token is presented, so demoting a member takes effect at once.

Each tool calls Polo’s own HTTP route as the token’s owner (infrastructure/api-caller.ts). There is no second permission system: the member’s role, the approval rules and every validation apply exactly as in the app.

Level Tools
read whoami, list_agents, list_tasks, get_task, wait_for_task, list_approvals, get_approval_rules, list_connections, list_services, list_cloud_accounts, list_cloud_resources, get_cloud_resource, generate_terraform, list_incidents, list_monitors, get_monitor, list_schedules, search_knowledge, list_knowledge, list_reports, get_report, get_usage, list_models
act ask_agent, answer_question, follow_up, cancel_task, set_agent_rules, scan_cloud_account, create_monitor, set_monitor_enabled, create_schedule, set_schedule_enabled, add_knowledge
approve approve_change, reject_change

No tool reaches sign-in and passwords, team members and roles, the vault, model keys or the access tokens themselves.

The plungo command line (ui/cli/plungo.js) is a client of this server, so it can do exactly what the token allows.

What Where
Client, OAuth and operations api/test/mcp/
Runtime path with a fixture server tests/test_runtime_mcp.py, tests/test_runtime_mcp_catalog.py
Polo’s server, with a real MCP client over HTTP api/test/mcp-server/mcp-server.test.ts
The command line as a real process api/test/mcp-server/cli.test.ts

docs/CAPABILITIES.md rates the MCP client family as verified locally: it runs end to end against a local fixture server. It has not been run against a third-party MCP server in a recorded test.