Skip to content

feat(mcp): MCP gateway, MVP: registry, /mcp proxy, access per key, accounting - #136

Merged
coyl merged 2 commits into
mainfrom
feat/mcp-gateway
Oct 27, 2026
Merged

coyl merged 2 commits into
mainfrom
feat/mcp-gateway

Conversation

@coyl

@coyl coyl commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

MCP clients (Claude Code, Cursor, agents) reach MCP servers registered in Keystone through /mcp with a KTMS key. Task: ai-process/docs#183.

  • Registry: mcp_server / mcp_credential tables; UpsertMCPServer, DeleteMCPServer, SetMCPCredential, DeleteMCPCredential, ListMCPTools; GetConfig lists servers and credential fingerprints. ktms mcp-servers, mcp_servers / mcp_credentials file sections.
  • Proxy: /mcp (all allowed servers, tools server__tool) and /mcp/{server}, stateless Streamable HTTP on HTTP_ADDR; upstreams Streamable HTTP or SSE, one shared session per server, rebuilt when spec or credential changes. MCP_GATEWAY_ENABLED (chart mcpGatewayEnabled).
  • Access: new mcp scope; allowed_mcp (server or server/tool) on key policy and team, intersected; others hidden, calls refused with -32003 without reaching the upstream.
  • Credentials: encrypted with the vendor keyring, re-encrypted by RotateEncryption, injected by the gateway, never returned, logged or in usage events.
  • Security: private/loopback/link-local/metadata addresses refused at registration and on every dial (resolved address is the one dialed) unless allow_private; proxy env ignored; timeout_ms, max_response_bytes.
  • Accounting: tool_rpm per key and tool (Redis); usage event per call (vendor mcp, model server__tool, additive ToolCall in usage.v1); ktms_mcp_tool_calls_total, _duration_seconds, _response_bytes.
  • Marked experimental in API stability.

Verified: go vet, go test ./... -race with Postgres, buf lint, buf breaking against main. Live smoke on the built binary: two upstream servers, Claude Code claude mcp add connects (caught and fixed an empty cacheScope it rejects), only allowed tools listed, a denied call never reaches the upstream, a generate-only key gets 403, metrics move, credential absent from logs, events land in light-tokenmeter (ai-process/light-tokenmeter PR, tool_call stored).

…counting

MCP clients such as Claude Code reach the MCP servers registered in Keystone
at /mcp (every server the key may use, tools named server__tool) and
/mcp/{server}, with a KTMS key of the new mcp scope. The gateway is
stateless Streamable HTTP on HTTP_ADDR; upstreams speak Streamable HTTP or
SSE through one shared session per server, rebuilt when the spec or
credential changes.

- mcp_server and mcp_credential tables; UpsertMCPServer, DeleteMCPServer,
  SetMCPCredential, DeleteMCPCredential and ListMCPTools RPCs; GetConfig
  lists servers and credential fingerprints, never values.
- Credentials are encrypted with the vendor keys' keyring, re-encrypted by
  RotateEncryption, added by the gateway and never returned, logged or
  passed to clients.
- allowed_mcp ("server" or "server/tool") on key policies and teams; other
  tools are hidden and refused (-32003) without calling the upstream.
- Private, loopback, link-local and metadata addresses are refused at
  registration and at every connection unless allow_private is set.
- tool_rpm per key and tool (Redis), timeout_ms, max_response_bytes.
- Each call is a usage event (vendor mcp, model server__tool, ToolCall) and
  counted in ktms_mcp_tool_calls_total, _duration_seconds, _response_bytes.
- ktms mcp-servers, the mcp_servers and mcp_credentials file sections,
  --allowed-mcp, MCP_GATEWAY_ENABLED and the chart's mcpGatewayEnabled.

docs#183
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedOct 27, 2026, 7:51 PM

- Upstream redirects are refused: the credential is set by the transport,
  so a followed redirect carried it to the target host.
- A tools/call is no longer retried on a closed connection, which does not
  say whether it ran; only an unknown session is. A replaced or removed
  upstream is retired and never reconnects with the old credential.
- A reply cut at max_response_bytes is a too_large tool error: the cut is
  marked on the call's context, as the SDK drops the read error.
- Names the server does not list are unknown tools, refused before access,
  limits and metrics, so labels and rate keys stay bounded.
- A server with no tools lists none instead of being unavailable, and the
  empty list is cached.
- Tool call events cut server and tool to light-tokenmeter's limits.
- A rotation that cannot read an MCP credential says so and names
  SetMCPCredential.
@coyl
coyl merged commit 5ed6beb into main Oct 27, 2026
13 of 14 checks passed
@coyl
coyl deleted the feat/mcp-gateway branch October 27, 2026 21:17
@coyl coyl mentioned this pull request Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant