Module 8 — Model Context Protocol (MCP)

Module 8 — Model Context Protocol (MCP)


1. MCP Fundamentals — How Models Access Tools, Resources, and Context

The problem MCP solves, stated precisely: Module 4 covered tool calling entirely within one application — tools declared and invoked inside the same JVM process calling the model. MCP generalizes this into a standardized protocol for how any AI application can discover and invoke tools, retrieve resources, and access context exposed by a separate service — regardless of what language or framework either side is written in.

The three core primitives MCP standardizes:

Primitive What it represents Analogy
Tools Invokable actions with defined inputs/outputs — the same concept as Module 4’s @Tool methods, just exposed across a process boundary A remote procedure call — same idea as a REST endpoint, but described in a way a model can discover and reason about
Resources Addressable, retrievable content — files, database rows, documents — that a client can read without necessarily invoking “logic” Closer to a REST GET on a resource URI — read access to data, not an action
Prompts Reusable, parameterized prompt templates that a server can expose for clients to use A shared prompt-engineering library, served over the protocol rather than copy-pasted into every consuming application

Why standardization matters here specifically: before MCP, every team building “let an AI agent use my service” built a bespoke integration — a custom tool schema, a custom invocation mechanism, no portability between one team’s agent framework and another’s. MCP is explicitly trying to be the equivalent of what HTTP/REST did for web APIs — a shared, well-understood contract so a tool built by one team, in one framework, can be consumed by any MCP-compliant client, including agentic CLIs and IDEs built by entirely different vendors (Claude Code, Gemini CLI, and others, as referenced in the Module 9 territory from your table of contents).

Talking point: “MCP is doing for AI tool access what REST/HTTP did for web services twenty years ago — replacing N bespoke integrations with one shared contract. The architectural payoff is the same in both cases: a service exposed via MCP doesn’t need to know or care what’s consuming it — Claude Code, Gemini CLI, a custom internal agent — the same way a well-designed REST API doesn’t need to know whether it’s being called by a browser, a mobile app, or another backend service.”


2. Spring AI’s First-Class MCP Support — Boot Starters, MCP Java Annotations

Boot Starters — the same auto-configuration philosophy from every other Spring AI integration, applied to MCP specifically:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<!-- or, for building a client that consumes external MCP servers -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

MCP Java Annotations — declarative tool/resource exposure, the same @Tool-style ergonomics from Module 4, now exposing functionality across the MCP boundary rather than just within one application:

@Component
public class InventoryMcpTools {

    @Autowired
    private InventoryRepository inventoryRepository;

    @McpTool(description = "Checks current stock level for a given product SKU")
    public String checkStock(@McpToolParam(description = "Product SKU to check") String sku) {
        return inventoryRepository.findBySku(sku)
            .map(item -> "In stock: " + item.getQuantity() + " units")
            .orElse("SKU not found");
    }

    @McpResource(uri = "inventory://catalog", description = "Full product catalog")
    public String getCatalog() {
        return inventoryRepository.findAll().toString();   // simplified — real impl would serialize properly
    }
}

The deliberate parallel to @Tool from Module 4 is the point, not a coincidence: “The annotation style here is intentionally familiar — if you’ve already internalized @Tool and @ToolParam from in-process tool calling, @McpTool and @McpToolParam apply the identical mental model, just registering the method as discoverable and invokable by external MCP clients rather than only your own ChatClient calls. Spring AI is deliberately not asking you to learn a second, unrelated annotation philosophy for what’s conceptually the same capability exposed at a different boundary.”


3. Transport Options — STDIO, SSE, Streamable-HTTP

MCP defines the protocol’s content (tools, resources, prompts) independent of how bytes actually move between client and server — that’s the transport layer, and Spring AI supports three:

Transport Mechanism Best fit when…
STDIO Server runs as a local subprocess; communication via standard input/output streams Local development tooling, CLI-invoked agents, IDE integrations where the MCP server is launched and managed as a child process on the same machine — no network involved at all
SSE (Server-Sent Events) HTTP-based, server pushes events over a long-lived connection Network-deployed MCP servers where the server needs to push updates/streaming results to a remote client over standard HTTP infrastructure
Streamable-HTTP A more modern, bidirectional-friendly HTTP-based transport (the protocol’s evolution beyond plain SSE) The current recommended approach for production, network-deployed MCP servers — better suited to bidirectional and more complex interaction patterns than SSE alone

Why the transport choice matters architecturally, not just as a config flag: STDIO fundamentally assumes co-location (same machine, parent-child process relationship) — it’s not a deployment option for a server meant to be consumed by remote clients across a network at all. SSE and Streamable-HTTP are the options for anything actually deployed as a network service, with Streamable-HTTP representing the protocol’s own evolution toward better handling more complex interaction patterns than plain SSE supports.

# Server-side transport configuration
spring:
  ai:
    mcp:
      server:
        transport: STREAMABLE_HTTP   # or SSE, or STDIO for local-only use

Talking point: “STDIO is the right choice exactly once — when you’re building something meant to run as a local tool invoked by an IDE or CLI agent on the same machine, like a local development assistant. The moment a server needs to serve remote clients over a network — which is the normal case for an enterprise service exposing internal capabilities to an AI ecosystem — STDIO isn’t even on the table, and the real decision is SSE versus Streamable-HTTP, where I’d lean toward Streamable-HTTP as the more modern, actively-recommended choice unless there’s a specific compatibility reason to stay on SSE.”


4. Building an MCP Server vs. an MCP Client

MCP Server — exposing your own Spring-based services to the AI ecosystem. This is the direction most enterprise architecture conversations will actually be about: “how do we let agentic tools (internal or external) safely use our existing business logic.”

@Configuration
public class McpServerConfig {

    @Bean
    public List<ToolCallback> inventoryTools(InventoryMcpTools tools) {
        return ToolCallbacks.from(tools);   // exposes @McpTool-annotated methods as MCP-discoverable tools
    }
}

The architectural framing worth holding onto: an MCP server is, conceptually, a new front door onto existing business logic — much like building a REST API on top of an existing service layer. The actual business logic (inventory lookups, order processing) doesn’t need to be rewritten; MCP is an additional, standardized way of exposing it, parallel to how the same service might already be exposed via a REST controller for human-facing clients.

MCP Client — consuming external MCP servers from within your own application, the inverse direction:

@Bean
McpSyncClient mcpClient(McpClientTransport transport) {
    McpSyncClient client = McpClient.sync(transport).build();
    client.initialize();
    return client;
}

// Tools discovered from the external server can be registered directly 
// with a ChatClient, bridging MCP-discovered tools into the same 
// tool-calling mechanism from Module 4
List<ToolCallback> externalTools = client.listTools().stream()
    .map(McpToolUtils::toToolCallback)
    .toList();

chatClient.prompt()
    .user("What's the current weather-related shipping delay risk for our EU warehouse?")
    .tools(externalTools)
    .call()
    .content();

The genuinely important architectural insight tying this back to Module 4: from the ChatClient‘s perspective, a tool discovered via an MCP client and a tool declared locally via @Tool look the same once registered — both become ToolCallback instances participating in the same request/response loop covered in Module 4. MCP doesn’t introduce a parallel tool-calling mechanism; it’s a discovery and transport layer that feeds the same underlying tool-calling architecture.

Talking point: “Once a tool is discovered through an MCP client, it rejoins the exact same tool-calling pipeline from Module 4 — same request/response loop, same need for the tool’s own authorization logic, same ToolContext discipline. MCP’s job is purely getting a tool definition and an invocation channel from a remote service into your application; everything covered in Module 4 about treating the model as an untrusted caller of that tool still applies in full, and arguably applies with more force here, since an MCP-discovered tool’s implementation lives in code you don’t control at all.”


5. MCP Security — Enterprise Authentication and Compliance

This extends Module 4’s tool-security discussion directly into the cross-organization-boundary case, where the trust assumptions are meaningfully weaker.

Why MCP specifically needs its own dedicated security layer, beyond what Module 4 already established: Module 4’s tool security assumed the calling model was yours — orchestrated by your own application, with prompts you control. An MCP server, by design, may be consumed by external clients you don’t control at all — a different team’s agent, a third-party agentic CLI, potentially a different organization entirely. The authorization question changes from “is this tool call appropriate given the conversation” to “should this caller be allowed to invoke this tool at all, and with what scope.”

The concerns MCP Security addresses, as enterprise authentication and compliance requirements:

  • Authentication of the calling client — an MCP server exposed over a network needs real auth (OAuth, API keys, mTLS — whatever fits the existing enterprise identity infrastructure), not an assumption that anything speaking the MCP protocol correctly is automatically trustworthy.
  • Authorization scoped per-tool, per-client — a given MCP client might be permitted to call checkStock but not cancelOrder, requiring the same kind of fine-grained, role-based access control discussed in the BI/DAX module’s RLS section, just applied to tool invocation rather than row visibility.
  • Audit logging of every tool invocation across the MCP boundary — since a remote, less-trusted caller is now executing code inside your system, a complete record of who called what, when, and with what arguments is a baseline compliance requirement, not optional observability.
  • Input validation at the MCP boundary independent of the model’s “intent” — exactly Module 4’s “never trust that the model only calls a tool when it’s supposed to,” reinforced here because the calling model is now potentially controlled by an entirely separate organization with separate prompt-engineering practices you have zero visibility into.

Talking point that synthesizes this with the Module 4 security framing and your AWS/IAM background directly: “MCP Security is the same authorization discipline as any externally-exposed API — think of an MCP server the way you’d think about exposing a service through API Gateway with IAM-backed authorization rather than a fully open internal endpoint. The fact that the caller happens to be an AI agent rather than a traditional client application doesn’t change the security architecture at all — it just means the request arriving at your authorization boundary was assembled by a process you have even less visibility into and even less ability to predict than a traditional API consumer, which argues for being more conservative with scope and audit logging at this boundary, not less.”


6. Recent Breaking Changes — MCP Annotation Package Renames, Transport Artifact Relocations

What changed, concretely (from the 2.0.0-M3 release flagged in Module 1’s versioning section): MCP annotation packages were renamed, and MCP transport artifacts were relocated — meaning code written against an earlier 2.0 milestone’s MCP support may not compile cleanly against a later milestone without import/dependency adjustments.

Why this is worth treating as a standing practice, not a one-time migration note: MCP is the area of Spring AI experiencing the most active architectural churn right now — it’s genuinely new ground for the framework, being actively shaped in real time alongside the broader MCP specification itself (which is also evolving externally, independent of Spring AI’s own release cycle). This compounds the model-churn and infrastructure-churn discipline from Modules 3 and 7: here, the protocol-integration layer itself is the moving target, on top of everything else that can change.

The defensive practice, extending the same pattern established twice already in this track:

  • Treat any MCP-related code written against a 2.0.0-Mx milestone as explicitly migration-prone — re-check imports and artifact coordinates on every milestone bump, don’t assume a minor version bump means no breaking changes (Module 1 already established that milestones don’t carry that guarantee).
  • Isolate MCP annotation usage and transport configuration behind your own thin wrapper layer where practical, so a package rename touches one file rather than every tool definition across the codebase — the same “isolate the volatile dependency behind a seam” discipline from Module 3’s model-deprecation handling.
  • Given that MCP is simultaneously evolving as an external specification and as a Spring AI integration, budget explicit time for tracking both — a Spring AI release note might reflect a change Spring AI made on its own, or a change forced by an upstream MCP specification revision, and the remediation differs depending on which it is.

Talking point that closes the module: “MCP is the part of this whole curriculum where I’d be most explicit in an interview that the ground is still moving — annotation package renames and transport relocations between milestones aren’t a sign of a poorly-run project, they’re the normal cost of building against a protocol that’s itself still being actively defined, on a framework that’s racing to keep pace with it. I’d treat any MCP integration built today as something to revisit deliberately at each Spring AI milestone bump, with the same isolation-behind-a-seam discipline as model deprecations — not because the team building it did anything wrong, but because volatility here is structural to where this technology currently sits, not a quality problem to be engineered away.”