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.
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.
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.
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:
- Discovery: The client sends a
tools/listrequest, 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. - 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.
- Invocation: The client sends a
tools/callrequest with the tool’s name and arguments, and the server runs the tool. - 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:
{
"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.
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.
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:
<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.
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.
@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.
| Hint | What it tells the client | Default |
|---|---|---|
readOnlyHint | The tool does not modify its environment, such as a lookup or a report. | false |
destructiveHint | The tool may make destructive updates, such as deleting data. It only matters when the tool is not read-only. | true |
idempotentHint | Calling the tool again with the same arguments has no additional effect. It only matters when the tool is not read-only. | false |
openWorldHint | The 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.
@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:
| Tool | What it does | Read-only |
|---|---|---|
create_ticket | Raises a ticket for the user | No |
get_ticket | Returns the status, priority and assignee of one ticket | Yes |
list_tickets | Lists tickets, filtered by requester, queue, priority or SLA status | Yes |
update_ticket | Changes a ticket’s status, priority or assignee, or adds a comment | No |
search_knowledge_base | Finds articles that describe a known fix | Yes |
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 module | Inside the web service module | |
|---|---|---|
| How it works | A separate module that calls the web service and exposes its features as MCP tools | The MCP tools sit in their own package inside the existing web service |
| Advantages | Clean architecture, with the AI code kept out of the core service, and a separate deployment | Nothing new to deploy, and the tools can call the service layer directly |
| Things to consider | One more component to deploy, run and monitor | The 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.
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:
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_.
- 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.
- 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.
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.