MCP
Paddock is an MCP host: the served model calls tools exposed by MCP servers, and Paddock executes those calls server-side in an agent loop, over both the OpenAI and Anthropic APIs.
Overview
MCP servers are registered once and reused across requests. Registered configs are inert; Paddock connects lazily on first use, keeps one pooled client per server, and shares it across conversations. Two transports are supported: stdio (a child process) and streamable HTTP, with OAuth for remote HTTP servers. Tool calls are made by any served model through its normal function-calling dialect; Paddock routes them to the right server, executes them, and feeds the results back.
Server Registry
| Endpoint | Description |
|---|---|
GET /api/mcp | Lists registered servers, secrets redacted. |
POST /api/mcp | Registers a server; labels must be unique. |
GET /api/mcp/{id} | Fetches one server config, secrets redacted. |
PUT /api/mcp/{id} | Updates a config; masked secrets keep their stored values, so an edit never wipes credentials. |
DELETE /api/mcp/{id} | Removes the server and drops any pooled connection. |
GET /api/mcp/{id}/tools | Lazily connects and lists the server's tools. This is where a stdio child process spawns. |
POST /api/mcp/{id}/test | Connection test; returns ok with a tool count, or the error. |
POST /api/mcp/{id}/authorize | Starts the OAuth flow for a remote HTTP server; returns the consent URL. |
GET /api/mcp-oauth/callback | The browser redirect target that completes the OAuth code exchange. |
POST /api/mcp-approvals/{id} | Resolves a pending tool-call approval in a streaming agent loop. |
Server Configuration
A registration carries a unique label, an enabled flag, a transport, and a per-server require-approval switch:
{
"label": "filesystem",
"enabled": true,
"transport": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/data"],
"env": {}
},
"requireApproval": false
}HTTP transports use "type": "http" with url and optional headers. Secrets are write-only: environment values, header values, OAuth client secrets, and tokens are stored server-side but always masked in API responses, and OAuth tokens never leave the server at all; clients only see an authorized flag and the token expiry.
The Agent Loop
When a request enables MCP servers, Paddock connects them lazily, gathers their tools, and injects them into the model's tool set, namespaced by server label so two servers cannot collide. Then it generates, executes the model's MCP tool calls, appends the results, and generates again, up to 8 rounds. A loop that does not converge ends with an explicit error rather than looping forever. Calls on approval-gated servers pause for a human decision; a pending approval auto-denies after 5 minutes, and a denied call feeds a denial message back to the model instead of a result.
Tool Disclosure at Scale
Injecting every tool schema degrades tool selection and can overflow a small context. Above 16 tools, or when the schemas would exceed roughly a third of the context window, Paddock switches to search-mode disclosure: the model gets two synthetic tools, mcp_search_tools (returns matching tools with their input schemas inline) and mcp_call_tool (a generic executor for a discovered tool), and the catalog is ranked with BM25 lexical retrieval. No embedding model is required for tool routing.
The OpenAI mcp Tool
On POST /v1/responses, an mcp tool entry enables servers per request. It can reference a registered server by label (any transport, including stdio) or define an inline HTTP server:
{
"model": "gpt-oss-20b",
"input": "List the files in the project root.",
"tools": [
{ "type": "mcp", "server_label": "filesystem", "require_approval": "never" }
]
}Inline servers add server_url, optional headers, and optional authorization (a bearer token for the server). allowed_tools filters the tool list, as an array of names or as an object with tool_names and read_only (matching the tool's readOnlyHint annotation). require_approval takes "always", "never", or per-tool objects, and falls back to the registered server's stored switch when omitted. OpenAI-hosted connectors (connector_id) are not supported and return a clear error.
The response carries the spec output items: one mcp_list_tools item per server before generation, then an mcp_call item per executed call, and an mcp_approval_request item when a gated call needs a decision. Streaming emits the matching events, including response.mcp_list_tools.in_progress and .completed, response.mcp_call.in_progress, response.mcp_call_arguments.done, and response.mcp_call.completed or .failed.
Approvals have two paths. In a streaming response, approve or deny the pending call with POST /api/mcp-approvals/{id} and a body of {"approve": true}; the paused loop resumes in place. In a non-streaming response, the response ends with the mcp_approval_request item, and the client resumes by sending a new request with previous_response_id and mcp_approval_response input items, the OpenAI spec approval flow.
Anthropic mcp_servers
On POST /v1/messages, the mcp_servers field (the MCP connector, current beta mcp-client-2025-11-20; the deprecated 2025-04-04 shape still works) enables remote HTTP servers inline, with per-server tool configuration through mcp_toolset entries in tools:
{
"model": "qwen3.5-9b",
"max_tokens": 1024,
"mcp_servers": [
{
"type": "url",
"url": "https://mcp.example.com",
"name": "example",
"authorization_token": "TOKEN",
"tool_configuration": { "enabled": true, "allowed_tools": ["search"] }
}
],
"messages": [{"role": "user", "content": "Search for the latest filing."}]
}Tool calls and their results are reported as mcp_tool_use and mcp_tool_result content blocks, in both non-streaming and streaming responses, and the same 8-round agent loop applies. On this surface servers are defined inline per request; by-label reuse of registered servers is a feature of the OpenAI Responses surface.
OAuth for Remote Servers
Remote HTTP servers that require OAuth are authorized once through the registry. POST /api/mcp/{id}/authorize discovers the provider's metadata (RFC 8414 and RFC 9728), establishes a client identity, either a pre-registered client_id and client_secret or RFC 7591 Dynamic Client Registration, and returns a PKCE (S256) consent URL. The user's browser completes consent and lands on GET /api/mcp-oauth/callback, which is exempt from bearer auth and protected by the unguessable CSRF state instead; it exchanges the code and persists the tokens. Pending authorizations expire after 10 minutes.
On connect, the access token rides as an Authorization: Bearer header on the existing HTTP transport; an explicit Authorization header set on the server config takes precedence. Tokens are write-only and are never returned by the API.
Security
Registering an MCP server is a security decision: stdio servers spawn processes on your machine, and HTTP servers make outbound requests. On the default loopback bind the registry is open; on a network bind the API key is required before any MCP endpoint can be reached. By default tools run without confirmation, bounded by the round cap and the approval timeout; turn on a server's approval switch to route every call through the approve-or-deny flow. Secret values are redacted in logs and in every /api/mcp response, but they are stored in plaintext in the local SQLite database.