← Blog

MCP for agent integrations

Published Oct 2025 · Revised Sep 2026 · 9 min read

Takeaways

  • MCP is the Model Context Protocol: an open standard over JSON-RPC 2.0 in which a server exposes tools, resources and prompts and a client inside the agent discovers and calls them.
  • It is not a prompting technique and does not replace function calling; it standardises the wiring between the model's tool call and the system that executes it.
  • Two transports: stdio for a subprocess you launch, Streamable HTTP for a server elsewhere. Since revision 2026-07-28 every HTTP request is self-contained.
  • A server is a dozen lines with the official Python SDK. The client side is one Client object, list_tools() and call_tool().
  • In a bank the protocol is the easy part. The work is the perimeter: allow-listed servers, OAuth on HTTP, a log line per tools/call, least privilege per tool.

What MCP is, and is not

MCP stands for Model Context Protocol. Anthropic published it in November 2024 as an open standard, now developed in the open at modelcontextprotocol.io; the current specification revision is 2026-07-28, read on 10 September 2026. The site's own definition: "an open-source standard for connecting AI applications to external systems."

Three roles. A host is the LLM application, say an agent runtime or an IDE. Inside it, a client holds one connection to one server, a small process or service that owns access to something: a database, a ticketing system, a file tree. Messages are JSON-RPC 2.0. The model itself is outside the protocol; the host decides which model it talks to.

The contrast with plain function calling is the point most people miss. Function calling is the model's side: the API returns a structured request to call account_balance with these arguments. What executes it, where the definitions come from, how a second agent reuses the integration: none of that is defined. MCP fills the gap. The client asks a server tools/list, receives JSON Schema definitions, hands them to the model as function-calling tools, and when the model picks one, sends tools/call and returns the result. Build the integration once as a server and every MCP-capable host can use it.

Three primitives, two transports

A server offers any of three things. Tools are functions the model may execute, each with a name, description and input schema. Resources are data addressed by URI, read with resources/read: context the host pulls in, not actions. Prompts are templated messages a server publishes for users, fetched with prompts/get. Most integration work is tools; resources earn their place when the agent needs to read a policy document rather than call something.

Transports carry the messages without changing their meaning. stdio: the client launches the server as a subprocess and exchanges newline-delimited JSON-RPC over its standard streams, which is why a stdio server must never write anything but protocol messages to stdout; a stray print() corrupts the stream. Streamable HTTP: the server exposes one endpoint, conventionally /mcp, and every JSON-RPC request is its own HTTP POST, answered with a single JSON object or a Server-Sent Events stream scoped to that request. The 2026-07-28 revision removed protocol-level sessions, so each request carries its protocol version and client capabilities in _meta and mirrors them into headers: MCP-Protocol-Version, Mcp-Method and Mcp-Name. That detail matters for audit.

A minimal server in Python

The official SDK is the mcp package, installed with uv add "mcp[cli]" (2.0 or later). Type hints and the docstring become the tool's schema and description.

# ledger_server.py
from mcp.server import MCPServer

mcp = MCPServer("ledger")

@mcp.tool()
async def account_balance(iban: str) -> str:
    """Return the booked balance for an account the caller may see.

    Args:
        iban: IBAN of the account, e.g. CH93 0076 2011 6238 5295 7
    """
    # Look this up in your own system. Never print() in a stdio server.
    return "CHF 12'340.55"

if __name__ == "__main__":
    mcp.run(transport="stdio")

Run it with uv run ledger_server.py. To serve the same code to remote clients, change the last line to mcp.run(transport="streamable-http", port=3001) and clients connect to http://127.0.0.1:3001/mcp. The SDK builds a Starlette app and serves it with uvicorn; to mount it inside an existing ASGI application you build the app yourself.

Connecting an agent to it

StdioServerParameters describes the subprocess, stdio_client() turns it into a transport, and Client opens it inside an async with. Entering the block launches the server and agrees a protocol version; leaving it shuts the subprocess down.

import asyncio
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main() -> None:
    params = StdioServerParameters(command="python", args=["ledger_server.py"])
    async with Client(stdio_client(params)) as client:
        tools = await client.list_tools()
        print([t.name for t in tools.tools])   # ['account_balance']
        result = await client.call_tool(
            "account_balance", {"iban": "CH93 0076 2011 6238 5295 7"}
        )
        print(result.content, result.is_error)

asyncio.run(main())

An agent loop sits between those two calls. Each entry in tools.tools has name, description and input_schema, the shape a chat API's tool definition wants. When the model returns a tool use, the loop calls call_tool and feeds result.content back. A tool that raises does not raise in your code; it answers with is_error set, so pass the flag on and let the model read the message. A desktop host registers the same server under its mcpServers config key with a command and args.

Inside a bank: what changes

The specification is candid that it cannot enforce its own security principles; hosts and servers have to. Five concerns cover most of a review.

Allow-list servers. The servers an agent may connect to are configuration, reviewed and versioned, never discovered at runtime. Pin the command for stdio servers and the exact URL for HTTP ones. Tool descriptions "should be considered untrusted, unless obtained from a trusted server", so an unreviewed server is prompt injection with a schema.

Authentication on Streamable HTTP. Authorization is optional in the protocol, but for HTTP transports the spec defines it as OAuth 2.1: the MCP server is a resource server, the token travels in Authorization: Bearer on every request, and the server must validate that the token was issued for it as audience (RFC 8707). Servers must not accept or pass through any other tokens. Servers must also validate the Origin header and, when local, bind to 127.0.0.1. For stdio servers the guidance is the opposite: no OAuth, credentials come from the environment, so the process that launches the server owns the secret.

Audit logging of tool calls. Every action the agent takes is one tools/call message with a name and arguments. Log it at the client, where you know the user and conversation, and at the server, where you know what ran. On Streamable HTTP the Mcp-Method and Mcp-Name headers let a gateway log and route without parsing the body; the server must reject any request where header and body disagree, which is what makes that log trustworthy.

Keep data inside the perimeter. Run servers next to the systems they wrap and keep the transport internal. Whatever a tool returns becomes model context and leaves for the model provider unless the model is hosted inside, so return the minimum: a balance, not the customer record. The spec's own principle is that hosts must not transmit resource data elsewhere without user consent.

Least privilege per tool. One server per system, and within it only the tools the use case needs. Scope tokens per tool where the authorization server allows it; the spec supports a scope challenge in WWW-Authenticate for this. It also says hosts must obtain explicit user consent before invoking any tool. In an unattended agent that consent becomes policy: a written list of tools that may run without a human, and a human step for the rest.

Where this fits with Spring AI and Java

Java is a first-class option. The official Java SDK lives at modelcontextprotocol/java-sdk and Spring AI builds its MCP support on it. Spring AI 2.0 ships starters for both sides: spring-ai-starter-mcp-server for a stdio server, WebMVC and WebFlux variants for Streamable HTTP, and spring-ai-starter-mcp-client for the agent. The server quickstart annotates a service method with @Tool and exposes it through a ToolCallbackProvider; spring.ai.mcp.server.protocol=STREAMABLE switches a web server to Streamable HTTP. For a Spring Boot estate with security filters and observability already in place, this is the shortest path to an MCP server that passes the same reviews as every other service.

The skeleton in this repo

The MCP agent skeleton is a small Python project for reading and adapting, not for production. It is a hand-written stdio client with no SDK dependency, so the JSON-RPC traffic is visible: it launches each server listed in config/servers.json as a subprocess, sends initialize, discovers tools with tools/list and runs them with tools/call. An interactive loop in agent.py accepts list tools and run <server> <tool> {json}; llm.py holds a stub model to replace. Two caveats: it targets the 2024-11-05 revision with the older initialize handshake, and it frames messages with Content-Length headers rather than the newline delimiting the current stdio binding specifies. Use it to understand the flow, then build with the official SDK.

Practical takeaways

  1. Start with one server and one tool over stdio; prove list_tools and call_tool before adding a model.
  2. Write the tool docstring as if it were the API contract, because to the model it is.
  3. Move to Streamable HTTP only when the server must run elsewhere, and add OAuth, Origin validation and a gateway log at the same time.
  4. Keep a versioned allow-list of servers and, per server, the tools an unattended agent may call.
  5. Log every tools/call with user, conversation, tool name and arguments; that log is what the auditor will ask for.

Sources, read 10 September 2026: MCP specification revision 2026-07-28 (overview, transports, Streamable HTTP, authorization), the build-server and build-client guides on modelcontextprotocol.io, the Python SDK reference, and the Spring AI MCP overview for Spring AI 2.0.1.