Skip to main content

MCP Tools

An MCP server offers tools over a standard protocol. Give the agent a server and its tools sit beside your local tools: the model sees one set of tools, and the runtime routes each call to where it lives. By the end of this page you will have written an MCP server, connected an agent to it, and read which server answered each call. OmniCoreAgent uses the official MCP Python SDK 2.x (mcp>=2.2.0,<3, installed with it) and negotiates protocol version 2025-11-25.
Every output on this page is what the code printed when it was run. The model’s wording will differ on your run.

Your first MCP server and agent

1

Write a server

A stdio server is one file. Save it as notes_server.py:
In mcp 2.x the server class is MCPServer; 1.x called it FastMCP.
2

Connect an agent

Save this next to it and run it from that folder:
Each call names its provider (mcp) and its server. The last line is from the run’s header, which records every configured server’s state when the run started.
await agent.connect_mcp_servers() is required. run() does not connect servers itself; without it the agent runs with no MCP tools.

How it works

  • Connecting. connect_mcp_servers() starts every stdio server and connects every HTTP server at once. Each does the MCP handshake and lists all its tools (every page) within connect_timeout. A server that fails does not stop the others: its reason is recorded, and the agent runs without its tools. OmniServe connects on startup.
  • The name is the identity. The name you configure is what routing, governance, telemetry and readiness use. What a server reports about itself (its name and version) is kept as metadata only, since the server controls it. A server without a name gets a stable one derived from its command or host, such as python_c51c4f; naming each server is clearer.
  • Calling. A call goes to the server that offered the tool, under call_timeout if you set one and always under the agent’s tool_call_timeout. If two servers offer a tool with the same name, the model is shown each under its own unambiguous alias.
  • Closing. agent.cleanup() (or cleanup_mcp_servers()) closes every connection and stops stdio server processes.

What the model receives

  • Structured content, when the server returns it, is the result. A text block that only repeats it is dropped; other blocks, such as an image, are kept beside it. A scalar the server wrapped as {"result": value} is returned as the value. A Python server sends structured content when the tool’s return type is typed (a TypedDict, a Pydantic model, dict[str, int], int); note_stats above is annotated as a bare dict, so the SDK sent it as JSON text instead.
  • A single text block is returned as text; several blocks as a list, with their MCP field names (such as mimeType).
  • A tool that reports failure (isError) is an error result.
  • A protocol error is an error result that keeps its JSON-RPC code, such as MCP error -32602: ... for invalid arguments, or MCP error -32001 when call_timeout expires. The code is also in the result’s data as mcp_error.
Every result, structured content included, passes through the agent’s guardrail before the model sees it.

When a connection drops

If a session drops mid-run (a stdio server exits, or an HTTP server restarts and no longer knows the session), the next call reconnects once and is retried. If reconnecting fails, the call returns an error naming both causes, and the server is marked disconnected with its last error. Each reconnect is recorded on the tool call it affected.

Remote servers: Streamable HTTP and SSE

The same server can serve HTTP. Replace the last line of notes_server.py with server.run("streamable-http", port=8765), start it, and point the agent at its URL:
After connect_mcp_servers(), its entry in the run’s header read:
streamable-http, the spelling the MCP SDK uses, is accepted too. An SSE server takes the same shape with "transport_type": "sse", its URL (usually ending in /sse), and optionally sse_read_timeout.

OAuth

With "auth": {"method": "oauth"} on an HTTP server, the MCP SDK runs the OAuth authorization code flow with PKCE: server metadata discovery, dynamic client registration, and token exchange. When a login is needed, OmniCoreAgent starts a small callback server on 127.0.0.1, opens the authorization URL in the browser, and waits for the redirect without blocking other work.
When the authorization server includes its issuer (iss) in the redirect, it is checked, so a redirect claiming a different authorization server is rejected. Tokens are kept in memory for the life of the process.

Options

Each server’s settings are checked when the agent is created. A setting that does nothing for its transport, an unknown setting, a wrong type or a non-positive timeout raises a ValueError naming the server and the setting. mcp_tools is an argument of OmniCoreAgent itself; see the OmniCoreAgent reference for the agent’s MCP methods.

Governance

With governance on, a server is authorized by its configured name before any process starts or connection opens (capability mcp.server.start for stdio, mcp.server.connect for HTTP), and each call is authorized as tool.mcp.call with the target mcp_server and tool_name. What the built-in profiles do with MCP: Connecting happens outside any run, so an ask there cannot pause: it fails with ApprovalRequiredError. An ask also outranks an allow rule you add. For a server you trust, remove those asks and allow it by name:
Every capability and each profile’s rules are in the policy reference; how a policy asks a person is in the tour.

Readiness and telemetry

  • The run’s header (trajectory["harness"]["mcp_servers"]) lists every configured server with its status (connected, disconnected, failed or not_connected), reported name and version, protocol version, tool count, reconnects and last error. It never includes commands, arguments, environment, URLs or headers, which can carry credentials.
  • Each MCP call in the trajectory names its provider (mcp) and server, and lists any reconnect. See Observability.
  • OmniServe’s GET /ready reports mcp_connected (every configured server connected) and mcp_servers, each server’s status and last error, so one failed server among several is visible (OmniServe).

When things go wrong

Either connect_mcp_servers() was not called, or the server failed to connect. connect_mcp_servers() does not raise for a server that fails; the run goes on without its tools, and its header says why. With a misspelt script name (notez_server.py), Python’s own can't open file ... No such file or directory went to stderr, and printing the status, the response and trajectory["harness"]["mcp_servers"] gave:
Run the server’s command yourself to see why it exits. A relative path in args is resolved from the working directory, or from cwd if you set it.
A setting is missing, unknown, or does not apply to the transport. These are raised when the agent is created:
Two servers with the same name raise ValueError: Duplicate MCP tool names: notes.
Governance is on with interactive-dev, which asks before starting a server, and there is no run to pause while connecting, so connect_mcp_servers() raises. Allow the server by name, as in Governance.
A stdio server does not inherit your environment: it gets a small safe baseline. Pass what it needs explicitly with "env": {"NAME": value}.

Next

Local tools

Your Python functions, beside the server’s tools.

Many tools

Servers with many tools: let the model search them.

Security model

What a policy can decide, and what it cannot.

Serve it

Connect servers at startup, and report them in /ready.