Model Context Protocol

Last update: 2022-03-25 05:36:01 +0800 +0800

MCP just say that: here is a jsonrpc-based schema, please follow this for the http request-response if the communication parties happened to be an LLM-using program (agent) and an arbitrary http server that is meant to provide arbitrary abilities.

Model Context Protocol, usually abbreviated as MCP, is an open protocol for connecting AI applications to tools and contextual data. It gives an AI host one consistent way to discover and invoke capabilities provided by local programs, remote services, databases, and other systems.

Without MCP, every integration tends to be custom. A chat application might use one proprietary plug-in format for GitHub, another adapter for a database, and special application code for a local file system. Each AI host and each external system must understand the other side’s API. MCP puts a protocol between them: an MCP server publishes capabilities, and an MCP client consumes those capabilities on behalf of an AI application.

MCP does not replace the external system’s API or business logic. An MCP server is normally an adapter over those existing components. For example, a shop may already have an OrderService used by REST controllers and batch jobs. An MCP server can expose selected methods of that same service as tools suitable for an AI client.

The Problem MCP Solves #

Large language models can generate text, but useful applications often need information or actions outside the model:

  • Read a document whose contents were not in the model’s training data.
  • Query a database for the current state of an order.
  • Create an issue, send an email, or deploy an application.
  • Ask a user to approve a risky operation.
  • Use another model through the host application.

An application can implement each integration with ordinary function calling, REST clients, and custom prompts. That works, but it couples the integration to one application and often to one model SDK. MCP standardizes discovery, schemas, invocation, results, notifications, and lifecycle management. An MCP server written for one conforming host can therefore be reused by other hosts.

The value is similar to a database driver or Language Server Protocol: standardization does not remove the underlying complexity, but it prevents every consumer and provider from inventing a different connection contract.

Architecture #

MCP uses a host-client-server architecture.

Host

The host is the AI application the user interacts with, such as a desktop assistant, an IDE, or a Spring AI application. It controls model access, permissions, user consent, and the overall conversation. A host may connect to many MCP servers.

Client

An MCP client is the protocol participant inside the host. It maintains a connection to one server, negotiates capabilities, sends requests, and receives responses and notifications. A host normally creates a separate client for each server connection so that security boundaries and server state remain isolated.

Server

An MCP server publishes tools, resources, prompts, or other protocol capabilities. It may be a local subprocess connected through standard input and output, or a remote service reached over HTTP. The server can wrap a narrow utility, an existing REST application, or an entire business system, but it should expose a deliberately limited and well-descr ibed interface.

Consider an AI coding assistant connected to three MCP servers:

text
                         +------------------+
                         |  Git MCP server  |
                         +------------------+
                                  ^
                                  | MCP
+--------+     +-------------+    |
|  User  | --> | Host + LLM  | ---+----> Database MCP server
+--------+     +-------------+    |
                                  | MCP
                                  v
                         +------------------+
                         | Docs MCP server  |
                         +------------------+

The model does not open arbitrary network connections to these servers. The host decides which MCP capabilities to present to the model, sends protocol requests when appropriate, and can require user approval before a call.

Protocol Lifecycle #

MCP messages use JSON-RPC 2.0. Requests expect responses, notifications do not, and errors use JSON-RPC error objects. MCP adds a lifecycle and typed methods on top of this message format.

A stateful connection normally proceeds as follows:

  1. The client sends initialize with the MCP version it supports, client information, and client capabilities.
  2. The server chooses a mutually supported protocol version and returns its server information and capabilities.
  3. The client sends an initialized notification.
  4. The two parties exchange normal MCP requests and notifications.
  5. The transport is closed when the session ends.

This negotiation matters because MCP is extensible. A server must not assume that every client supports sampling, elicitation, or list-change notifications. Likewise, a client should call only the server features advertised during initialization.

MCP is stateful by default: initialization establishes a logical session and both sides can send requests during that session. Streamable HTTP also defines stateless operation for servers that do not need server-to-client requests, server-initiated notifications, or session state. Stateless mode is useful for simple horizontally scaled tool servers, but it is a constrained deployment choice rather than a description of MCP as a whole.

Server Capabilities #

Tools #

A tool is an operation the model can ask the host to invoke. The server publishes a name, description, JSON input schema, and optionally an output schema. Examples include:

  • get_order for a read-only lookup.
  • cancel_order for a state-changing operation.
  • search_knowledge_base for retrieval.
  • run_test_suite for a long-running task.

The client discovers tools with tools/list and invokes one with tools/call. Servers can notify clients when the available list changes.

Tool descriptions and schemas are part of the application’s control plane. They should state important preconditions and effects, use unambiguous parameter names, and validate every argument. Tool annotations can provide hints such as whether a tool is read-only, destructive, idempotent, or open-world. These are hints for clients, not security controls; the server must still authenticate, authorize, and validate every call.

Resources #

A resource is data that a server makes readable through a URI. Resources are appropriate for contextual information rather than actions:

text
order://12345
config://shipping-policy
repo://acme/payments/README.md

The server may publish concrete resources and URI templates. Clients can list resources, read their contents, and, when supported, subscribe to updates. Resources can contain text or binary data and declare a MIME type.

The distinction between a resource and a tool is primarily semantic. Reading a known document is naturally a resource; performing a search with several parameters is usually a tool. A server need not turn every underlying REST GET endpoint into a resource.

Prompts #

A prompt is a reusable, server-provided interaction template. A server can publish a prompt such as investigate_failed_order that accepts an order ID and returns one or more messages instructing the model how to perform the investigation.

Prompts help a domain owner package a recommended workflow near its tools and resources. They are generally user-selectable templates, whereas tools are operations the model may choose during inference.

Client Capabilities #

MCP is bidirectional. A server may use capabilities offered by the client, but only after negotiation.

Sampling allows a server to request a model completion through the client. The host remains in control of model selection, permissions, and user consent. This lets a server use an LLM without owning model credentials, although hosts should guard against recursive or unexpectedly expensive call chains.

Elicitation allows a server to ask the user for additional structured information. For example, a deployment tool may request a target environment or confirmation that downtime is acceptable. Sensitive values should not be requested unless the host and protocol flow are designed to handle them safely.

Roots let a client tell a server which file-system roots or other URI roots are in scope. They help a local server understand the intended workspace, but they do not replace operating-system sandboxing or file permissions.

Clients may also handle server logging messages, progress notifications, and notifications that capability lists changed.

The specification also defines experimental Tasks for work that cannot finish within one request. A task has durable status that can be polled and may eventually produce a result. Because Tasks are experimental, implementations must negotiate support and should expect the API to evolve.

Transports #

MCP separates protocol messages from their transport. The two standard transports are stdio and Streamable HTTP.

Standard Input and Output #

With stdio, the client launches the MCP server as a subprocess. The client writes JSON-RPC messages to the process’s standard input, and the server writes responses and notifications to standard output.

This transport is a good fit for local tools because it requires no listening port and the subprocess naturally shares the user’s machine and file-system permissions. The launch configuration supplies the executable, arguments, and environment variables.

Standard output is reserved for MCP protocol traffic. A server that writes ordinary log lines to standard output corrupts the message stream, so it must write logs to standard error instead.

Streamable HTTP #

Streamable HTTP is designed for remote or independently deployed servers. A server exposes one MCP endpoint that accepts HTTP POST; it may also accept GET to open a Server-Sent Events stream. A response may be a single JSON message or an SSE stream, depending on the request and negotiated behavior. Sessions can be associated with an Mcp-Session-Id.

Streamable HTTP replaces the older HTTP+SSE transport. New applications should prefer Streamable HTTP unless interoperability with an older client requires the deprecated transport.

Because the endpoint can receive requests from browsers and local networks, a server must validate the HTTP Origin header to mitigate DNS rebinding. A remote server should use TLS, authenticate every protected request, validate that tokens were issued for it, and avoid accepting credentials intended for an upstream API.

MCP, REST, and A2A #

MCP and REST operate at different layers. REST is an application interface between HTTP services. MCP is an AI integration protocol that adds capability discovery, JSON schemas, prompts, model-oriented tool results, negotiation, and bidirectional interactions.

An MCP server often calls REST APIs internally, or it can be hosted inside the same Spring Boot application as existing REST controllers. MCP should not replace a stable REST API used by mobile applications, web front ends, or service-to-service integrations. Instead, it presents a curated AI-facing surface over the same business logic.

Agent2Agent (A2A) addresses a different boundary. MCP connects an AI application to tools and resources; A2A connects independent agents that have their own goals, identity, conversation state, and potentially long-lived tasks. An order agent might use MCP to query an order database while using A2A to delegate fraud review to a separate fraud agent.

Using MCP in a Spring Boot REST Application #

Spring AI provides MCP client and server starters. The following example adds a Streamable HTTP MCP endpoint to an existing Spring MVC order application. It uses Spring AI 2.0 APIs.

Add the Server Starter #

Import the Spring AI bill of materials and add the WebMVC MCP server starter:

xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
</dependencies>

The WebMVC starter is appropriate when the application already uses spring-boot-starter-web. Spring AI also provides WebFlux and WebMVC-less server variants for other deployment models.

Configure the server:

yaml
spring:
  ai:
    mcp:
      server:
        name: order-service
        version: 1.0.0
        protocol: STREAMABLE
        type: SYNC
        streamable-http:
          mcp-endpoint: /mcp

STREAMABLE selects Streamable HTTP, and /mcp is the default endpoint. SYNC, also the default, registers synchronous annotated methods. An ASYNC server registers asynchronous methods instead; it does not automatically turn blocking service calls into nonblocking work.

Although /mcp uses HTTP, it is not an ordinary REST endpoint. An MCP client must initialize the protocol and send MCP JSON-RPC messages rather than making application-specific GET and POST requests.

Share the Application Service #

Keep transport adapters thin. Both the REST controller and MCP provider should delegate to the same application service:

java
public record OrderView(
        long id, String customerName, String status, BigDecimal total) {
}

public record CancelOrderCommand(long orderId, String reason) {
}

@Service
public class OrderService {

    private final OrderRepository orders;

    public OrderService(OrderRepository orders) {
        this.orders = orders;
    }

    @Transactional(readOnly = true)
    public OrderView find(long id) {
        return orders.findViewById(id)
                .orElseThrow(() -> new OrderNotFoundException(id));
    }

    @Transactional
    public OrderView cancel(CancelOrderCommand command) {
        Order order = orders.findById(command.orderId())
                .orElseThrow(() -> new OrderNotFoundException(command.orderId()));
        order.cancel(command.reason());
        return order.toView();
    }
}

The existing REST adapter remains conventional:

java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orders;

    public OrderController(OrderService orders) {
        this.orders = orders;
    }

    @GetMapping("/{id}")
    OrderView get(@PathVariable long id) {
        return orders.find(id);
    }

    @PostMapping("/{id}/cancellation")
    OrderView cancel(
            @PathVariable long id,
            @RequestBody CancelOrderRequest request) {
        return orders.cancel(new CancelOrderCommand(id, request.reason()));
    }
}

Do not make the MCP method call this controller over HTTP. That introduces serialization, routing, and authentication overhead inside one process and makes business behavior depend on a transport adapter. Sharing OrderService keeps validation and transaction boundaries consistent.

Publish Tools #

Spring AI scans Spring beans for MCP annotations by default:

java
@Component
public class OrderMcpTools {

    private final OrderService orders;

    public OrderMcpTools(OrderService orders) {
        this.orders = orders;
    }

    @McpTool(
            name = "get_order",
            description = "Get the current details of an order by its numeric ID",
            generateOutputSchema = true,
            annotations = @McpTool.McpAnnotations(
                    readOnlyHint = true,
                    destructiveHint = false,
                    idempotentHint = true,
                    openWorldHint = false))
    public OrderView getOrder(
            @McpToolParam(
                    description = "Numeric order ID",
                    required = true)
            long orderId) {
        return orders.find(orderId);
    }

    @McpTool(
            name = "cancel_order",
            description = "Cancel an order that has not shipped",
            generateOutputSchema = true,
            annotations = @McpTool.McpAnnotations(
                    destructiveHint = true,
                    idempotentHint = true,
                    openWorldHint = false))
    public OrderView cancelOrder(
            @McpToolParam(description = "Numeric order ID", required = true)
            long orderId,
            @McpToolParam(
                    description = "Business reason for cancellation",
                    required = true)
            String reason) {
        return orders.cancel(new CancelOrderCommand(orderId, reason));
    }
}

The names and descriptions are visible to the model, so they should explain behavior rather than mirror Java implementation details. The generated input schema describes the annotated parameters, while generateOutputSchema describes the returned record.

A destructive hint can help a host decide to ask for confirmation, but it does not enforce confirmation. The host and server must implement their own approval and authorization policy.

Publish a Resource and Prompt #

The same server can expose order data as a URI-addressable resource:

java
@Component
public class OrderMcpResources {

    private final OrderService orders;
    private final ObjectMapper objectMapper;

    public OrderMcpResources(
            OrderService orders,
            ObjectMapper objectMapper) {
        this.orders = orders;
        this.objectMapper = objectMapper;
    }

    @McpResource(
            uri = "order://{id}",
            name = "Order",
            description = "Current order details",
            mimeType = "application/json")
    public String order(String id) throws JsonProcessingException {
        return objectMapper.writeValueAsString(
                orders.find(Long.parseLong(id)));
    }
}

A prompt can package the domain’s preferred investigation procedure:

java
@Component
public class OrderMcpPrompts {

    @McpPrompt(
            name = "investigate_order",
            description = "Investigate why an order has not progressed")
    public GetPromptResult investigate(
            @McpArg(
                    name = "orderId",
                    description = "Numeric order ID",
                    required = true)
            String orderId) {
        String text = """
                Investigate order %s. Read its current details, identify the
                blocking state, and explain the safest next action. Do not
                cancel or modify the order without explicit user approval.
                """.formatted(orderId);

        return GetPromptResult.builder(List.of(new PromptMessage(
                        Role.USER,
                        TextContent.builder(text).build())))
                .description("Order investigation workflow")
                .build();
    }
}

Tools, resources, and prompts are complementary. A client can read the order resource, invoke get_order, or start from the investigation prompt according to its user experience.

Secure the MCP Endpoint #

Treat MCP as a privileged application API, not as a harmless chat feature. A tool call can carry model-generated input and may mutate production data.

At minimum:

  • Use TLS for remote servers.
  • Authenticate the MCP client and authorize each capability.
  • Apply the same tenant and row-level authorization used by REST endpoints.
  • Derive user and tenant identity from trusted authentication context, never from a model-supplied tool argument.
  • Validate inputs in the application service.
  • Require approval for destructive or high-impact actions.
  • Rate-limit expensive operations and record security audit events.
  • Return only the fields the model needs; do not expose whole entities by convenience.

For an internal resource server using bearer tokens, the existing Spring Security policy can protect /mcp:

java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -> csrf.ignoringRequestMatchers("/mcp"))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/mcp")
                    .hasAuthority("SCOPE_mcp.invoke")
                    .requestMatchers("/api/orders/**")
                    .hasAuthority("SCOPE_orders")
                    .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt())
            .build();
}

Ignoring CSRF is appropriate here only because /mcp uses bearer authentication rather than browser cookies. If the application authenticates with cookies, keep CSRF protection and design the client flow accordingly.

MCP’s HTTP authorization specification includes OAuth metadata discovery and resource indicators in addition to token validation. A generic resource-server configuration protects the endpoint but does not by itself implement every part of that MCP authorization flow. Confirm the capabilities of the Spring AI version and identity provider before claiming standards-compliant public MCP authorization.

Consuming an MCP Server with Spring AI #

A Spring AI application can also be an MCP client. For production Streamable HTTP connections, use the WebFlux client starter:

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>

Configure one or more named servers:

yaml
spring:
  ai:
    mcp:
      client:
        type: SYNC
        request-timeout: 20s
        streamable-http:
          connections:
            orders:
              url: https://orders.example.com
              endpoint: /mcp

The starter initializes an MCP client for each connection and, by default, adapts discovered MCP tools to Spring AI ToolCallback objects. They can be given to a ChatClient:

java
@RestController
@RequestMapping("/assistant")
public class AssistantController {

    private final ChatClient chatClient;

    public AssistantController(
            ChatClient.Builder builder,
            SyncMcpToolCallbackProvider mcpTools) {
        this.chatClient = builder
                .defaultTools(mcpTools)
                .build();
    }

    @PostMapping
    String assist(@RequestBody AssistantRequest request) {
        return chatClient.prompt()
                .user(request.message())
                .call()
                .content();
    }
}

The flow for a question such as “Why is order 123 blocked?” is:

  1. The client discovers get_order and gives its schema to the chat model.
  2. The model emits a tool call with orderId set to 123.
  3. Spring AI sends tools/call to the order MCP server.
  4. The server invokes OrderService and returns structured content.
  5. Spring AI gives the tool result to the model.
  6. The model writes the final answer.

The model chooses when to request a tool, but application code executes the call and controls which tools are available. In a high-risk application, do not register every discovered tool as a default. Filter tools by user, conversation, and operation, and put an approval step between model intent and destructive execution.

For local subprocess servers, use spring-ai-starter-mcp-client and configure spring.ai.mcp.client.stdio.connections. The command, arguments, and environment form the subprocess launch configuration. Keep credentials out of source control and remember that the subprocess receives the operating-system permissions of the user running the host.

Design Guidelines #

An effective MCP server is not merely an automatic wrapper around every controller method.

Expose use cases, not tables. prepare_refund_quote is safer and easier for a model to use than unrestricted CRUD over payment rows.

Keep tools narrow. A tool should have one understandable effect and a bounded result. Very broad “execute request” tools defeat schema validation and permission controls.

Make effects explicit. Put read, write, external communication, and irreversibility in descriptions and annotations. Use separate tools for preview and commit when possible.

Prefer structured results. Stable records with generated output schemas are easier for clients to validate and for models to interpret than prose assembled inside the server.

Design for retries. Networks fail and models may retry. Use idempotency keys for state-changing operations where duplicate execution would be harmful.

Bound context. Paginate lists, truncate large documents deliberately, and offer focused retrieval. Sending an entire database result into a model is expensive and increases the chance of disclosing unrelated data.

Treat all content as untrusted. Tool output and resources may contain prompt-injection text. The host should keep data separate from trusted instructions and should not let retrieved content expand its own permissions.

Observe both protocol and domain behavior. Record request IDs, tool names, latency, result size, authenticated identity, and outcome, but redact prompts, arguments, and results when they may contain secrets or personal data.

Further Reading #

Comments

Read or comment on GitHub (sign-in required to comment)