← All articles

Model Context Protocol (MCP) – A Deep Dive

MCP gives an AI agent one standard way to discover and call the tools it needs, instead of a custom integration for each. We explain how it works, then build an MCP server in Spring AI and cover tool design and security.

An AI application with a single MCP port, linked through one MCP connection to three tool servers: a database, a CI pipeline and a ticketing system. AI APPLICATION MCP DATABASE CI PIPELINE TICKETS
One standard port between an AI application and every tool it uses.

Introduction

An AI agent is an autonomous software system that can plan, make independent decisions and take multi-step actions towards a specific goal. The large language model (LLM) inside it does the planning, but the agent still needs a way to reach the outside world: the databases, APIs and other systems where the work actually happens.

In our Agentic AI blog, we introduced the Model Context Protocol (MCP) as the standard way for an AI application to connect to these tools.

To keep things concrete, we will use an IT service help desk as the running example. Employees raise tickets when a laptop breaks or when they need access to a system, and the IT service team triages, assigns and resolves those tickets within the times set by its service level agreements (SLAs). An MCP server lets both sides work with the help desk from an AI assistant, in plain English, instead of filling in forms and searching through queues.

In this blog, we will look at what makes up an agent, the problem MCP solves and how it works. Then we will build a small MCP server for the help desk in Spring AI, and see how to design its tools, where to put the server and how to secure it.

What Makes Up an AI Agent?

An agent is made of three parts:

  • LLM (the brain): It understands the goal, breaks it into smaller steps and decides which tool to use for each step.
  • Tools (the hands and legs): They let the agent act on the world. A tool can be a RAG lookup over your documents, a web search, a call to a REST API, a database query, or a function that runs code in Python, Java, JavaScript, .NET or Scala.
  • Memory: It keeps track of what has happened so far, so the agent can use the result of one step in the next.

In simple words, LLM + Tools + Memory = Agent.

LLM plus tools plus memory equals an agent: the LLM plans, the tools act, and the memory remembers. LLM plans TOOLS act MEMORY remembers AGENT
An agent is an LLM that plans, the tools it acts through, and the memory that carries results from one step to the next.

Let’s take the trip planner from our earlier blog as an example. If you ask an agent to plan a trip to Dubai, the LLM first breaks the goal into smaller tasks. It checks which dates are good to travel, since summer there is too hot, and then looks for the cheapest flights. After that, it books a hotel, plans the itinerary for the length of the stay and books tickets for the attractions.

For each of these tasks, the agent calls a tool: a travel booking site for the flights and the hotel, an attractions site for the tickets, and a weather service’s REST API for the forecast. The LLM does all the planning, but without access to these tools, it cannot finish the job.

What Was the Problem Before MCP?

Before MCP, every AI application connected to its tools in its own way. Each one had a custom implementation, its own prompt logic to describe the tools to the model, its own code to call them, and its own way of reaching the data behind them.

This caused two problems:

  • Fragmented development: With M AI applications and N tools, you need M × N separate integrations. Three applications that each use a database, a file system and a calculator need nine integrations, and every new tool adds one more for each application.
  • Brittle integrations: When a tool’s interface changes, every application that calls it has to change as well. The more integrations there are, the more of them break with each change.

What is MCP?

The Model Context Protocol (MCP) is an open standard, introduced by Anthropic in November 2024, that defines how AI applications connect to and work with tools and data. Two comparisons help to explain it:

  • A USB-C port for AI applications: Any laptop with a USB-C port can use any USB-C accessory. In the same way, any AI application that supports MCP can use any MCP server, without custom code for each pair.
  • REST for AI applications: REST APIs gave web applications an industry-standard way to talk to each other. MCP does the same for the interaction between an AI application and the systems it works with.

MCP uses JSON-RPC 2.0 for its messages. A server that runs on your own machine usually talks to the application over standard input and output (STDIO), while a remote server uses HTTP, through a transport called Streamable HTTP.

An MCP server can offer three kinds of capabilities: tools that the model can call, resources that it can read, and prompts that the user can pick. Tools are by far the most used of the three, and they are the focus of this blog.

Before you build a server of your own, it is worth checking whether one already exists. The official MCP Registry and GitHub’s MCP page list ready-made servers for databases, developer tools, cloud platforms and many SaaS products.

How Does MCP Work?

MCP has three main parts:

  • Host: The AI application the user works in, such as Claude Desktop, Claude Code or an IDE with an AI assistant.
  • Client: A connector inside the host that keeps a connection to one MCP server. A host that uses three servers runs three clients.
  • Server: A program that exposes the tools of one system, such as a database, a CRM or a version control platform.
A host such as Claude Code runs three MCP clients. One client talks to a local server over STDIO, which reads files; the other two talk to remote servers over Streamable HTTP, in front of a database and a CRM. HOST e.g. Claude Code MCP CLIENT MCP CLIENT MCP CLIENT STDIO STREAMABLE HTTP STREAMABLE HTTP LOCAL SERVER REMOTE SERVER REMOTE SERVER FILES DATABASE CRM
One host, one client per server, and the transport each server uses.

What makes MCP different from a plain API is dynamic discovery. The client does not need to know in advance what a server can do. Instead, it asks the server when it connects. Here’s how it works:

  1. Discovery: The client sends a tools/list request, which is the protocol’s way of asking the server “What capabilities do you offer?”. The server replies with the name of each tool, a description and a JSON schema for its input.
  2. Selection: The host passes these descriptions to the LLM. When the user asks a question, the LLM picks the tool that fits and works out the arguments.
  3. Invocation: The client sends a tools/call request with the tool’s name and arguments, and the server runs the tool.
  4. Result: The server returns the result, and the LLM uses it to answer the user or to decide on the next step.

Below is a shortened example of a server’s reply to tools/list, with one tool:

JSON
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "get_ticket",
        "description": "Get the status, priority and assignee of a ticket",
        "inputSchema": {
          "type": "object",
          "properties": {
            "ticketId": {
              "type": "string",
              "description": "Ticket number, such as HD-10482"
            }
          },
          "required": ["ticketId"]
        }
      }
    ]
  }
}

Because the tools are discovered at run time, you can add a new tool to the server, and every connected application can use it without changing its own code. A server can also notify its clients when its list of tools changes, so they fetch the new list.

Dynamic discovery as a sequence between the LLM, the MCP client and the MCP server. Discovery: the client sends tools/list and the server returns names, descriptions and schemas. Selection: the client passes the tool descriptions to the LLM, which picks get_ticket. Invocation: the client sends tools/call. Result: the server returns the result, which goes back to the LLM. 1 DISCOVERY 2 SELECTION 3 INVOCATION 4 RESULT LLM MCP CLIENT MCP SERVER tools/list names, descriptions, schemas tool descriptions use get_ticket tools/call result result
The client learns what a server can do by asking it, so a new tool needs no change on the client side.

Why MCP Matters?

MCP turns the M × N problem into an M + N problem. Each AI application implements an MCP client once, and each system exposes an MCP server once. After that, any application can use any server.

Without MCP, three AI applications wired directly to a database, a file system and a calculator need nine integrations (M times N). With MCP, the same applications and tools connect through one MCP hub with six links (M plus N). WITHOUT MCP WITH MCP MCP AI APPS TOOLS AI APPS TOOLS M × N = 9 links M + N = 6 links
Without MCP, every application needs its own integration with every tool. With MCP, each side implements the protocol once.

This helps in three ways:

  • For teams building AI applications: You get access to every MCP server without writing a new integration for each one.
  • For teams that own a product or its data: You build one MCP server, and your users can work with your product from the AI assistant they already use, such as Claude, ChatGPT or GitHub Copilot.
  • For maintenance: When a system’s interface changes, only its MCP server has to change, not every application that uses it.

Creating Your Own MCP Server

MCP has official SDKs for many languages, including TypeScript, Python, Java, C# and Go. For a Java application built on Spring, Spring AI’s MCP server support is the quickest route. Suppose our help desk is such an application. Adding a basic MCP server to it takes three steps.

Step 1. Add the Dependency

Below is the dependency for Spring AI’s MCP server starter, for an application that uses Spring MVC:

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

Step 2. Choose the Transport

Set the protocol to Streamable HTTP in application.properties, so that clients can reach the server over the network. By default, the server listens on the /mcp endpoint.

Properties
spring.ai.mcp.server.protocol=STREAMABLE

Step 3. Annotate Your Methods

Add the @McpTool annotation, with a name and a description, to a method of a Spring bean. Spring AI registers the method as an MCP tool and generates its input schema from the method’s parameters.

Java
@Component
public class TicketTools {

    private final TicketService ticketService;

    public TicketTools(TicketService ticketService) {
        this.ticketService = ticketService;
    }

    @McpTool(name = "get_ticket",
             description = "Get the status, priority and assignee of a ticket")
    public TicketSummary getTicket(
            @McpToolParam(description = "Ticket number, such as HD-10482",
                          required = true) String ticketId) {
        return ticketService.getSummary(ticketId);
    }
}

Once the application is running, any MCP client can connect to it and discover the get_ticket tool. That is all it takes to have a working server. The harder part is designing tools that an LLM can use well, which is what the next section covers.

Best Practices for Designing MCP Tools

The LLM decides which tool to call, and with which arguments, based only on the names, descriptions and schemas that the server publishes. Most of the best practices below follow from that.

Describe Every Parameter

Use @McpToolParam on every parameter of a tool, with these two attributes:

  • required: Whether the LLM must supply a value, or can leave it out.
  • description: What the value means and the format it takes, for example “Ticket number, such as HD-10482” or “Priority, from P1 (most urgent) to P4”.

A clear description saves the LLM from guessing, and a wrong guess usually shows up as a failed call or a wrong answer.

Add Tool Hints

MCP lets a server describe how each tool behaves, through four hints. Clients can use them, for example, to decide when to ask the user for confirmation before calling a tool.

HintWhat it tells the clientDefault
readOnlyHintThe tool does not modify its environment, such as a lookup or a report.false
destructiveHintThe tool may make destructive updates, such as deleting data. It only matters when the tool is not read-only.true
idempotentHintCalling the tool again with the same arguments has no additional effect. It only matters when the tool is not read-only.false
openWorldHintThe tool may interact with external entities, such as a web search. False means it works within a closed domain.true

In Spring AI, you set the hints on the @McpTool annotation. Below is a tool that raises a ticket. It changes state, so it is not read-only, but it does not destroy anything. It is not idempotent either, because calling it twice raises two tickets.

Java
@McpTool(name = "create_ticket",
         description = "Raise a ticket for the user and return its number",
         annotations = @McpTool.McpAnnotations(
             readOnlyHint = false,
             destructiveHint = false,
             idempotentHint = false,
             openWorldHint = false))
public String createTicket(
        @McpToolParam(description = "Short summary of the problem",
                      required = true) String summary,
        @McpToolParam(description = "Category, such as Hardware or Access",
                      required = true) String category) {
    return ticketService.create(summary, category);
}

Note that the hints are only hints. The MCP specification asks clients to treat them as untrusted unless they come from a trusted server, so they never replace access control on the server itself.

Fewer Tools, the Better

It is tempting to expose every endpoint of your API as a tool. In practice, the fewer tools a server has, the better the agent performs, for three reasons:

  • Context bloat: The name, description and schema of every tool are sent to the LLM, and they take up space in its context window before the user has asked anything.
  • Cognitive overload and confusion: With many similar tools to choose from, the LLM is more likely to pick the wrong one, or to chain several calls where one would do.
  • Performance: Every extra call adds a round trip to the server and more tokens for the model to process, so answers take longer and cost more.

Design Tools Around Workflows

A few principles help when deciding which tools to build:

  • Avoid direct API wrappers: A tool that mirrors one REST endpoint leaves the LLM to join the data itself. Answering “Which of my team’s tickets are close to breaching their SLA?” through raw endpoints might take one call to list the team’s open tickets. Then it takes one call per ticket to get its SLA target, and another per ticket to get its history.
  • Build workflow-centric tools: Design each tool around a task the user wants done, such as finding the tickets that are close to an SLA breach, and do the joins and filtering inside the server. The LLM then makes one call instead of dozens.
  • Use flexible parameters: Let a tool accept the different ways people refer to things. For example, a tool that lists tickets can take the requester as an employee ID, an email address or a name, which saves the LLM a separate lookup call.
  • Curate the surface area: Decide what the server exposes the way you would design a public API. Many tiny, single-purpose tools (micro) are hard for the LLM to choose between, and one large tool that does everything (monolith) is hard for it to call correctly. Aim for a small set of tools, each covering one complete task.

For the help desk, five tools cover most requests:

ToolWhat it doesRead-only
create_ticketRaises a ticket for the userNo
get_ticketReturns the status, priority and assignee of one ticketYes
list_ticketsLists tickets, filtered by requester, queue, priority or SLA statusYes
update_ticketChanges a ticket’s status, priority or assignee, or adds a commentNo
search_knowledge_baseFinds articles that describe a known fixYes

The SLA filter on list_tickets is what makes it workflow-centric: it answers the SLA question above in one call.

Where Should the MCP Server Live?

If you are adding MCP to an existing web application, such as the help desk, there are two ways to structure it. Both work, and this table shows what each one gives you:

New MCP or AI moduleInside the web service module
How it worksA separate module that calls the web service and exposes its features as MCP toolsThe MCP tools sit in their own package inside the existing web service
AdvantagesClean architecture, with the AI code kept out of the core service, and a separate deploymentNothing new to deploy, and the tools can call the service layer directly
Things to considerOne more component to deploy, run and monitorThe MCP code is released together with the web service

A package inside the web service is the simpler start when the tools are thin and the same team owns both. A separate module suits an MCP server that combines several services, or one that needs to be released and scaled on its own.

Two ways to place the MCP server. Left: a new MCP module, deployed on its own, that calls the web service over its REST API. Right: an MCP package inside the web service that calls the service layer directly. AI ASSISTANT MCP MCP MODULE REST API WEB SERVICE SERVICE LAYER NEW MCP MODULE AI ASSISTANT MCP WEB SERVICE MCP PACKAGE DIRECT CALL SERVICE LAYER INSIDE THE WEB SERVICE
The MCP server as a module of its own, or as a package inside the web service.

Either way, keep the MCP code in its own module or package, with its own tests, so that it is easy to find and to change.

How Do You Secure an MCP Server?

An MCP server gives an AI assistant access to your system, so it needs the same care as any other API. When the server is exposed to the outside world, OAuth is the preferred option, and the MCP specification describes how clients and servers should use it.

For an internal application with a known group of users, such as the help desk, a simpler option works well: a bearer token in the request header. In Claude Code, for example, each user adds the server once with their own token:

Shell
claude mcp add --transport http --scope user \
  helpdesk https://helpdesk.example.com/mcp \
  --header "Authorization: Bearer <YOUR_PERSONAL_ACCESS_TOKEN>"

The --scope user option makes the server available in all of that user’s projects.

Why Not Use a JWT?

If the web application already issues JSON Web Tokens (JWT) for its user interface, it is tempting to reuse them for MCP. There are four reasons not to:

  • Short-lived: JWTs are designed to expire within minutes or hours. An MCP connection needs a token that lasts much longer, because users configure it once in their AI assistant and expect it to keep working.
  • Signing key in memory: If the application generates its JWT signing key at startup and holds it in memory, every issued token stops working when the application restarts.
  • Revocation: A JWT stays valid until it expires, and there is no simple way to revoke one token if it leaks.
  • Stale claims: The claims inside a JWT, such as the user’s roles, are fixed when it is issued. A long-lived JWT would keep carrying permissions that may no longer be true.

A personal access token (PAT) avoids all four problems.

How Does a Personal Access Token Work?

A PAT is a long random string that identifies one user. A common format is a fixed prefix followed by 32 random bytes, encoded in base64url. The prefix lets the server recognize a PAT at a glance, before doing any lookup. GitHub’s personal access tokens, for example, start with ghp_ or github_pat_.

How a personal access token works. Issue: a user requests a new PAT, which is shown to them once; its SHA-256 hash is stored in the token table. Validate: the PAT filter picks up the bearer PAT, hashes it with SHA-256, looks up the hash in the token table, checks the token is valid, and sets the security context. ISSUE VALIDATE TOKEN TABLE shown once store hash look up USERNEW PATSHA-256 BEARER PATPAT FILTERSHA-256VALID? SECURITY CONTEXT
The server never stores the token itself, only its hash.
  1. Issue the token: A logged-in user requests a PAT. The server generates it and shows it to the user once. It stores only the token’s SHA-256 hash, in a table that holds the user ID, the hash, a revoked flag, the time the token was last used and the date it was created.
  2. Validate it on every request: A PAT filter runs ahead of the JWT filter and picks up any bearer token that starts with the PAT prefix. It hashes the token, looks up the hash in the table and confirms that the token is valid, i.e. not revoked and not expired. If it is, the filter sets the security context with the user’s account, and the request carries on with that user’s permissions.

Because only the hash is stored, the table holds nothing that can be used as a token. Revoking a token is a single update to its row.

What Can Users Ask?

With the server in place, employees and the IT service team work with the help desk in plain English. Below are some of the questions they can ask:

  • “My laptop won’t connect to the VPN. Raise a ticket for me.”
  • “What is the status of my access request?”
  • “Is there a known fix for Outlook not syncing?”
  • “Which P1 tickets are open right now?”
  • “Which of my team’s tickets are close to breaching their SLA?”
  • “Assign HD-10482 to the network team and add a note that the switch was replaced.”

Each question maps to one or two tool calls. The LLM picks the tool from its description, fills in the arguments from the question, and turns the result into an answer. Questions that change something, such as raising or assigning a ticket, are where the tool hints matter: the client can ask the user to confirm before it calls the tool.

A member of the IT service team asks an AI assistant "Which of my team’s tickets are close to breaching their SLA?". The assistant makes one call to list_tickets, one of the help desk MCP server’s five tools, and answers with the tickets at risk. AI ASSISTANT Which of my team’s tickets are close to breaching their SLA? list_tickets MCP SERVER create_ticketget_ticketupdate_ticketsearch_knowledge_base list_tickets
A question in plain English, answered through the MCP server’s tools.

The Bottom Line

MCP gives AI applications one standard way to discover and call tools, so a system needs one MCP server instead of a separate integration for every AI assistant. Building a basic server takes a dependency, a property and an annotation. Most of the effort goes into the design: a few workflow-centric tools with clear descriptions, the right hints, and authentication that suits long-lived connections.

In our next blog, we will look at Claude Skills, which build on an MCP server like this one to turn common requests into repeatable tasks, and at how to share them across teams with a plugin marketplace.