intermediate~4h

Model Context Protocol — Architecture

Module 12's tools lived inside your own application. What if you want to expose tools as a standalone service other AI applications (not just yours) can use — or consume someone else's tools without writing a custom integration for every one? That's what MCP is for.

Learning objectives

  • Beginner: Explain what problem MCP solves that plain in-app tool calling doesn't.
  • Intermediate: Build a minimal MCP client that connects to an existing MCP server using Spring AI.
  • Advanced: Build and expose your own MCP server, and reason about when STDIO vs. a remote transport is the right choice.

◆ The problem

Module 12's @Tool methods live inside your Spring Boot application, coupled to that one codebase. If a second, unrelated application (maybe not even written in Java) wants to use the same GitHub-querying tool, or a database-querying tool, it has to reimplement that integration from scratch — there's no standard way for an AI application to discover and use tools built by someone else.

MCP standardizes this: any MCP-compliant client can talk to any MCP-compliant server, regardless of what language either is written in, the same way any HTTP client can talk to any HTTP server. A tool exposed via an MCP server becomes usable by any MCP client that connects to it — Spring AI applications, other IDEs' AI assistants, anything implementing the protocol.

One MCP client (your app) can connect to any number of independent MCP servers, each exposing its own tools/resources/prompts, over a standardized transport.

ConceptRole
HostThe end-user-facing application (your Spring AI app, an IDE, a chat UI).
ClientThe component inside the host that speaks MCP to one specific server, managing that connection's lifecycle.
ServerAn independent process exposing Tools, Resources, and/or Prompts (Module 14) over MCP.
TransportHow it worksGood for
STDIOClient launches the server as a local subprocess and communicates over its stdin/stdoutLocal tools, CLI-style integrations, no network exposure
Streamable HTTP (remote)Client connects to a server over the network, receiving streamed responses over a single HTTP-based transportShared/hosted tool servers multiple clients or machines need to reach

▲ Version note

Standalone "HTTP+SSE" was the original remote transport in early MCP versions but is now deprecated in favor of Streamable HTTP — if you see "HTTP/SSE" as a current transport option in older material, treat it as superseded.

◆ Under the hood — built-in transport logic

Spring AI's MCP starter handles transport negotiation and message framing for you — your code interacts with a client/server API, not raw stdin/stdout bytes or SSE event parsing. Switching a client from STDIO to remote Streamable HTTP is, once again, primarily a configuration change rather than a rewrite — the same portability philosophy seen throughout this site.

spring: ai: mcp: client: stdio: connections: github: command: npx args: ["-y", "@modelcontextprotocol/server-github"]
@Bean public ChatClient chatClient(ChatClient.Builder builder, List<McpSyncClient> mcpClients) { return builder .defaultTools(new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks()) .build(); }

▲ Version note

.defaultToolCallbacks(...) (and the older .defaultFunctions(...)) are deprecated — .defaultTools(...) is the current method.

Once wired up, tools discovered from the MCP server are handed to the model exactly like Module 12's local @Tool methods — the model doesn't distinguish "local Java tool" from "remote MCP tool"; both arrive as tool descriptions with schemas.

💻 Code example

spring: ai: mcp: client: stdio: connections: github: command: npx args: ["-y", "@modelcontextprotocol/server-github"]
@Component public class OrderMcpTools { @McpTool(name = "lookup_order", description = "Look up an order by ID") public Order lookupOrder(@McpToolParam(description = "the order ID") String orderId) { return orderRepository.findById(orderId).orElseThrow(); } }

spring.ai.mcp.server.name=order-tools-server spring.ai.mcp.server.version=1.0.0

▲ Common mistake

Assuming Module 12's @Tool/@ToolParam-annotated methods can be exposed as an MCP server unchanged. They can't — an MCP server uses its OWN, separate annotation family: @McpTool/@McpToolParam, not @Tool/@ToolParam. The two annotation sets look similar and solve a similar problem (describing a Java method as an invokable tool), but they're not interchangeable — a class written for local ChatClient tool-calling needs its annotations changed, not just its Spring configuration, to become an MCP server tool.

◆ Under the hood

The underlying idea IS shared — describe a method, its parameters, and let the framework generate the tool schema and dispatch calls to it — Spring AI just implements that idea with two separate annotation families depending on whether the method is being exposed locally (@Tool) or via an MCP server (@McpTool), since MCP servers have protocol-specific concerns (structured content, resource links, request context) local tool-calling doesn't.

💻 Code example

@Component public class OrderMcpTools { @McpTool(name = "lookup_order", description = "Look up an order by ID") public Order lookupOrder(@McpToolParam(description = "the order ID") String orderId) { return orderRepository.findById(orderId).orElseThrow(); } }

The MCP Inspector is a standalone debugging UI that connects to any MCP server (yours or a third party's) and lets you manually browse its exposed tools/resources/prompts and invoke them directly — the MCP equivalent of testing a REST API with Postman before wiring up real client code against it.

◆ The problem

A real MCP server (e.g. GitHub's official one) can expose dozens of tools. Handing a model all of them on every call bloats the prompt with tool schemas the current question has nothing to do with, and — worse — measurably increases the odds of the model picking the wrong tool among too many similar-looking options.

Spring AI supports filtering which tools from a connected MCP server are actually exposed to a given ChatClient call — narrowing "everything this server offers" down to "what's relevant for this specific feature/endpoint."

List<ToolCallback> filtered = new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks().stream() .filter(tc -> tc.getToolDefinition().name().startsWith("github_issue")) .toList(); chatClient.prompt().user(question).toolCallbacks(filtered).call().content();

▲ Pitfall

"Give the model every tool it might conceivably need" feels safer than filtering, but empirically tends to reduce reliability once the tool count grows past roughly a dozen or so — treat tool surface area for a given call as something to deliberately minimize, the same way you'd minimize an API surface for clarity, not maximize it for completeness.

✓ Quick recap

What problem does MCP solve that Module 12's local @Tool methods don't? Portability — an MCP server's tools can be used by any compliant client, not just the one application they were written inside. What are the two transport options, and when would you pick each? STDIO for local subprocess tools with no network exposure; Streamable HTTP for a remote, shared tool server multiple clients need to reach. Why can giving a model too many tools at once hurt reliability? It bloats the prompt with irrelevant tool schemas and increases the chance of the model selecting the wrong tool among too many similar options.

💻 Code example

List<ToolCallback> filtered = new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks().stream() .filter(tc -> tc.getToolDefinition().name().startsWith("github_issue")) .toList(); chatClient.prompt().user(question).toolCallbacks(filtered).call().content();

Want a visual for this concept?

Generate a diagram tailored to “Model Context Protocol — Architecture” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to MCP Advanced — Sampling, Elicitation, Resources, Prompts← Back to all Spring AI chapters