# Tool and MCP security **Status:** DOCUMENTED (xAI `mcp` tool LIVE_VERIFIED against a public MCP server on 2026-09-19 — `require_approval: "never"` silently accepted, no approval item ever produced; Gemini `mcpServers` on generateContent UNVERIFIED) **Sources:** https://developers.openai.com/api/docs/guides/tools-connectors-mcp (Approvals, Filtering tools, Authentication, Risks and safety) · https://developers.openai.com/api/docs/guides/secure-mcp-tunnels · https://platform.claude.com/docs/en/agents-and-tools/mcp-connector · https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers · https://platform.claude.com/docs/en/api/messages · xAI: https://docs.x.ai/developers/tools/remote-mcp (`server_url`, `server_label`, `allowed_tools`, `authorization`, `headers`; "require_approval, connector_id not supported"), https://docs.x.ai/developers/tools/overview, https://docs.x.ai/developers/tools/function-calling, https://docs.x.ai/developers/tools/advanced-usage (`max_turns`, `parallel_tool_calls`), `sources/xai/openapi/openapi.json` (`ModelTool` mcp variant) · Gemini: https://ai.google.dev/gemini-api/docs/function-calling (#model-context-protocol-mcp, `toolConfig.functionCallingConfig`), https://ai.google.dev/api/generate-content (#Tool `mcpServers[]`, #McpServer, #StreamableHttpTransport, #FunctionCallingConfig `VALIDATED`), https://ai.google.dev/api/interactions (`mcp_server{name, url, headers, allowed_tools}`), python-genai / js-genai READMEs (`mcpToTool`, `automatic_function_calling.maximum_remote_calls`) **Last verified:** 2026-09-19 ## Two very different trust models | | Client (function) tools | Remote MCP / connectors | |---|---|---| | Who executes | **your code** — the model only proposes `name` + arguments (Gemini: `functionCall.args` object; xAI/OpenAI: JSON string) | the **provider's infrastructure** calls a third-party server; results flow back into the model without touching your code — OpenAI `mcp`, Anthropic `mcp_servers`, xAI `mcp`, Gemini `tools[].mcpServers` / Interactions `mcp_server`. Exception: **Gemini's documented MCP path is SDK-side** — the MCP client runs in *your* process and the wire only sees ordinary function calls | | What can go wrong | your executor trusts model arguments (injection into shell/SQL/paths) | data you send is shared with a third party; the server can return injections; tool behaviour can change silently; **your bearer tokens are forwarded by the provider** (OpenAI `authorization`, Anthropic `authorization_token`, xAI `authorization`/`headers`, Gemini `headers`) | | Provider controls | schema `strict` (OpenAI/Anthropic; xAI implicit), `tool_choice` (Gemini `functionCallingConfig.mode` incl. `VALIDATED`), `parallel_tool_calls` / `disable_parallel_tool_use` | OpenAI `require_approval`, `allowed_tools`, `authorization`; Anthropic `tool_configuration.{enabled, allowed_tools}`, https-only; xAI `allowed_tools` only (**no approval flow**); Gemini SDK `automatic_function_calling.disable` / `maximum_remote_calls`, Interactions `allowed_tools` | ## xAI Responses — `{"type":"mcp", …}` tool | Field | Security meaning | |---|---| | `server_url` (required; Streamable HTTP or SSE) · `server_label` (required; prefixes tool names in usage) · `server_description` | identify the server; only trusted servers — xAI executes the call and injects the result into the model inside the same request | | `allowed_tools` | string[]; **empty = every tool the server lists is injected into context** (docs). Always set it | | `authorization` · `headers` | bearer token / extra headers **forwarded to the MCP server by xAI**: scope minimally, rotate, never reuse your xAI key here | | `require_approval`, `connector_id` | docs: not supported; live: `require_approval: "never"` was silently accepted. There is **no `mcp_approval_request` item** — every listed tool can run without a human step. Gate side-effecting tools by *not* allowlisting them (or expose them as client `function` tools you execute) | | `defer_loading` | alpha, 403 | | Output | `mcp_call {name, server_label, arguments, output, error}` in `output[]`; `output` always present (no `include` needed) — treat as untrusted text (docs/tools/xai/mcp.md) | | Also | usable inside Batch and Speech-to-Speech requests — same trust model applies to voice agents; bound the agentic loop with `max_turns` | ## Gemini — SDK-side MCP vs server-side `mcpServers` - **SDK-side (documented, experimental):** `config.tools=[mcp_session]` (Python) / `mcpToTool(client)` (JS). The SDK lists the server's tools, converts them to `functionDeclarations`, and on each `functionCall` **calls the MCP server from your process** (`automatic_function_calling`, default `maximum_remote_calls` 10). Credentials never reach Google; the injection surface is the tool list/descriptions and results (same as any client tool). Disable AFC (`automatic_function_calling.disable=True`) to inspect calls before execution; tools only (no MCP resources/prompts); the Live API has no automatic tool handling. - **Server-side `tools[].mcpServers[]{name, streamableHttpTransport{url, headers, timeout, sseReadTimeout, terminateOnClose}}`:** present in the REST reference and discovery only; `headers` (your tokens) would be forwarded from Google's side; **UNVERIFIED** on `generateContent` in this atlas — do not rely on it. The **Interactions API** documents `tools:[{"type":"mcp_server","name","url","headers","allowed_tools"}]` (Streamable HTTP only, names without `-`); set `allowed_tools`, use HTTPS, and remember Interactions store state by default (`store: true`). - Function calling controls: `toolConfig.functionCallingConfig.mode` `AUTO | ANY | NONE | VALIDATED` (+ `allowedFunctionNames` — a per-request tool allowlist); reset `ANY` to `AUTO` after the forced turn or the model calls forever; `parametersJsonSchema` for plain JSON Schema; `behavior: NON_BLOCKING` is Live-only. - Gemini 3 **thought signatures**: replay the model turn verbatim; a tampered signature → 400 "Corrupted thought signature." (integrity check on your side of the loop — do not "fix" histories by hand). ## OpenAI Responses — `{"type":"mcp", …}` tool | Field | Security meaning | |---|---| | `server_label`, `server_url` **or** `connector_id` | identify the server; only connect to servers you trust ("It is very important that developers trust any remote MCP server they use") | | `allowed_tools` | array of tool names the model may call — deny-by-default filtering at list time | | `require_approval` | `"always"` (**default**), `"never"`, or `{ "never": {"tool_names":[…]} }` — with approvals, the model emits an `mcp_approval_request` output item; you reply with `{"type":"mcp_approval_response","approval_request_id":…,"approve":true|false}` in the next request. Skip approvals only for read-only tools on trusted servers | | `authorization` | OAuth access token forwarded to the server. Documented behaviour: OpenAI does not store it, and it is not logged in stored responses — but **you** must scope it minimally and rotate it | | `server_description` | free text seen by the model; keep it factual | | Logging | with `store: true` (default), data sent to MCP servers is retained 30 days unless ZDR — the docs *recommend* logging shared data on your side and reviewing it | | ZDR / data residency | MCP is compatible, but data leaving to the MCP server is governed by *that server's* policies — your responsibility | Secure MCP Tunnels (`secure-mcp-tunnels.md`) let you expose a private MCP server to OpenAI without opening it to the Internet. ## Anthropic Messages — MCP connector (beta) ```json "mcp_servers": [{ "type": "url", "url": "https://mcp.example.com/sse", "name": "example", "authorization_token": "", "tool_configuration": {"enabled": true, "allowed_tools": ["read_issue", "search"]} }] ``` Header `anthropic-beta: mcp-client-2025-11-20` (legacy `mcp-client-2025-04-04` still accepted). Rules from the docs: `url` **must start with `https://`**; OAuth tokens are obtained by *you* (the API consumer handles the OAuth flow — the MCP Inspector "Quick OAuth Flow" is the documented helper) and passed per request; `allowed_tools` restricts what the model can call; `enabled: false` disables a server without removing it. Results arrive as `mcp_tool_use` / `mcp_tool_result` blocks (`is_error` on failure). There is **no server-side approval flow** equivalent to OpenAI's `require_approval` — gate side-effecting MCP tools by *not* allowlisting them, or route them as client tools you execute yourself. ## Client (function) tools — all four providers - **Schema strictness**: OpenAI `tools[].strict: true`; Anthropic `tools[].strict: true`; xAI: tool schemas are **always strictly enforced** (the `strict` flag is accepted and ignored) and `additionalProperties` defaults to false; Gemini `mode: VALIDATED` validates calls against the declaration, otherwise best-effort. Strictness guarantees *shape*, not *safety* — a valid `{"path": "../../etc/passwd"}` is still valid. - **Tool choice**: force or forbid tools per request (`tool_choice: "none"` / `{type:"none"}` / Gemini `mode: NONE`) when reading untrusted content; per-request allowlists via OpenAI/xAI `{type:"function", name}` or Gemini `allowedFunctionNames`. - **Parallel calls**: OpenAI/xAI `parallel_tool_calls: false`, Anthropic `disable_parallel_tool_use: true` when tools have ordering-dependent side effects; Gemini parallel calls arrive as several `functionCall` parts — answer all of them in one Content, and note only the first carries a `thoughtSignature`. - **Agentic loop bounds**: xAI `max_turns` (server-side tools run inside one request — a runaway loop is billed per call: web_search/x_search/code_interpreter $5/1k), Gemini SDK `maximum_remote_calls`, your own `max_rounds` in the shared tool loop. - **Anthropic programmatic tool calling**: `allowed_callers: ["code_execution_20250825"]` — decide explicitly whether a tool may be invoked from generated code. Gemini 3 "tool combination" similarly lets server tools and your functions interleave (`includeServerSideToolInvocations`). - **Descriptions are part of your prompt**: keep them precise; never embed secrets (API URLs with tokens) in descriptions — on xAI the MCP `server_description` is also model-visible. - **Validate in your executor** (`schema-validation.md`, `command-and-path-injection.md`) and return errors with `is_error: true` (Anthropic) / explicit status text (OpenAI, xAI) / `{"error": …}` object (Gemini `functionResponse.response`) (`untrusted-tool-outputs.md`). ## Checklist - [ ] Every MCP server is inventoried with owner, URL, auth scope, and reviewed tool list. - [ ] OpenAI: `allowed_tools` set; `require_approval` left at `always` except for reviewed read-only tools; approval UI shows the exact arguments. - [ ] Anthropic: `tool_configuration.allowed_tools` set; https URLs only; beta header pinned; side-effecting MCP tools excluded or executed client-side with a human gate. - [ ] xAI: `allowed_tools` **always** set (empty = everything); only read-only tools on remote servers because there is no approval flow; `authorization`/`headers` scoped to that server; `max_turns` bounded. - [ ] Gemini: prefer SDK-side MCP (credentials stay local); disable automatic function calling when a human must approve; Interactions `mcp_server.allowed_tools` set; server-side `mcpServers` on generateContent not used until verified. - [ ] OAuth tokens for MCP scoped minimally, short-lived, rotated; never logged; never a provider API key. - [ ] Data sent to MCP servers logged on your side (per OpenAI recommendation) and reviewed periodically; xAI/Gemini MCP outputs treated as untrusted text. - [ ] Tool descriptions and lists pinned; alert on change. - [ ] Client tools: strict schemas (`VALIDATED` on Gemini) + executor-side validation + idempotency per call id. - [ ] `tool_choice: none` / `mode: NONE` (or no tools) when the request's only job is to read untrusted content.