So you need an MCP tool, now what?
In this post I look at some things related to creating an MCP server from scratch which, as it turns out, is not too bad. I also balance curiosity with caution. You can (and should) read more about MCP here and while you are there, narrow your focus to "server developers" content for what follows here.
Full disclosure, I don't know if MCP will be evergreen or a passing fad. I also don't know if MCP will end up bringing about world peace or somehow creating newer, more expensive footguns. If you use MCP, use it responsibly!
I am just a plain old software engineer who heard a buzzword and went to see what all the hype was about. So let us dive in.
What is MCP and what are MCP tools?
I mentioned this in the tl/dr above but it is worth repeating here. MCP is a light-weight protocol that attempts to address a specific problem of how to allow 3rd parties (you, me, or anyone) to provide relevant prompts or context to an LLM and to provide AI clients with tools to that enable LLMs to complete well-defined tasks (essentially plugins for AI).
MCP servers provide a few different capabilities, but I only explore "tools" here. Therefore, the diagram above is necessarily ignorant of other capabilities (i.e. prompts and content) and I only focus on MCP tools above and below.
When to not use MCP?
This is just an opinion. Your mileage may vary and of course, as with any speculation of AI topics, this opinion could age poorly.
Given advances in the field of AI, I do not think MCP is solving problems for the average LLM use cases I have observed thus far. For example, if asking questions or generating responses about a static input meets your requirement then MCP is not going to add value.
Imagine working with an LLM trained on data from a fixed point in time (N months / years ago). Apart from hallucinations, that model will have no reference to updated information. What happens if you want explanations about recent topics?
Please explain the "vibe coding" meme to me and cite relevant sources from across popular online websites in your response. (See Vibe Coding)
In recent days vendors have released AI clients that support extended research capabilities beyond chat, such as searching for sources or spending more cycles to get more accurate analysis of a given topic.
Here are examples, in no order:
- https://openai.com/index/introducing-deep-research/
- https://gemini.google/overview/deep-research/?hl=en
- https://www.anthropic.com/research/visible-extended-thinking
New AI client features can make a static LLM trained at a point in time feel a bit more dynamic or up to date. (Although "extended thinking" is more about accuracy and less about recent events, I think its important to keep in mind.)
Depending on your technology stack you could consider taking the AI models closer to your data. Retrieval Augmented Generation (RAG) is one such approach that gives an LLM access to relevant and up to date information. Oracle describes this technique using various implementations. Here is an example:
Thus, we have several options when it comes to providing relevant context to an AI model or extending it beyond static training data. So what about MCP?
When to use MCP?
An AI model, as of the date of this post, would have no standardized way of interacting with the outside world apart from what an AI client was designed to support. This theoretical interaction with the outside world is where MCP servers (and specifically MCP tools) come into the picture.
With MCP you do not need a vendor to waste GPU cycles downloading data and preparing additional context. You also do not need to expose proprietary systems directly to the vendor of any given LLM.
An AI client can offload queries for information from proprietary systems. The LLM can request execution of arbitrary tasks related to the context of a given interaction. With MCP, the LLM does not need to know the specifics of authentication or even communication protocols.
The MCP server abstracts away the details.
Do you want an LLM to inspect your database design using a live database connection? Maybe an LLM should review two systems and analyze differences. Do you want an AI client to be able to document (diagram?) or manage resources in a cloud tenancy?
In theory, the sky is the limit. In practice, the context and computing capacity are still a limiting factor.
This was simultaneously the "aha!" and "uh oh!" moment for me with MCP and where I think we should start planning for how to manage arbitrary execution of code by an LLM, especially in the context of any proprietary system. Safety is one thing you do not get for free with MCP.
About MCP Safety
You can read more about user interactions here:
As of early 2025 the MCP specification included the following callout:
Applications SHOULD: ... Present confirmation prompts to the user for operations, to ensure a human is in the loop.
Safety is a strong recommendation but that does not mean every MCP client server will act accordingly.
If we do not proceed with some safety in mind, headlines months or days from now may turn up citing attack vectors that employed malicious or negligent MCP tools.
MCP is shiny and cool. I have personally seen amazing and practical demos from real people and believe they have exciting potential. Yet I imagine people using AI clients to habitually approve requests from an LLM that execute an MCP tool. Then, somewhere along the way, the LLM hallucinates a statement that drops a production table.
Perhaps this is far-fetched, but I don't really know what to expect at this point. For corporations adopting MCP tools, risk mitigation and disaster recovery plans should be updated.
As the hype around MCP grows, more and more 3rd party vendors are supporting MCP tools. You may already be experimenting with some of them. My advice in the short term is to be selective and to take time to review what the tools are doing and how they manage safety and security.
All that said, we should understand how MCP works and from there eventually begin to understand what risks to mitigate (an exercise I leave to the reader).
Getting Started with MCP Tools
Getting started with MCP from scratch means understanding the fundamental components of the thing. The basic communication sequences are well documented here:
It makes the most sense to start with the user manual and there is no reason to duplicate that content here. Having built a couple of MCP servers, I will make the following suggestions:
- Learn about sending and receiving JSON-RPC 2.0 messages and the specification for JSON-RPC is a small pre-requisite.
- Start with STDIO in your exploration of MCP servers before you expose the same over HTTP, and with good design practice, organize your MCP server such that you can add different transport protocols later.
- If you are building a proof of concept, focus on "initialize", "tools/list" and "tools/call" messages at first.
I also recommend developing your MCP server in your language or framework of choice. MCP clients that prefer or require specific frameworks will be a short-lived requirement. Clients that allow configuration of any arbitrary command as the MCP server using standard I/O will proliferate in terms of flexibility as well as developer choice.
Step 1: Building a JSON-RPC server
The first step is to get the JSON-RPC basics out of the way. The very first MCP method call your MCP server should receive is a request with the "initialize" method. We need a representation of JSON-RPC requests and a responses.
The structure of those objects is well defined. Here is an example of a JSON-RPC request in Java without all the typical getter/setter methods.
public class Request {
@JsonProperty("jsonrpc")
private String version;
private String method;
private String id;
private Map<String, Object> params;
...
}
Example Java class that represents a JSON-RPC request object
Notice that I am mapping version
to jsonrpc
using a Jackson annotation. This is not required, and you could just live with member variables like "jsonrpc" and similarly named getter/setter methods but that is totally up to you and your approach to dealing with JSON serialization.
Here is an example representation of a JSON-RPC response in Java, again without all the typical scaffolding, and again using annotations from Jackson.
@JsonPropertyOrder({"jsonrpc", "result", "error", "id"})
public class Response {
// jsonrpc (version) is handled by via getter
@JsonInclude(Include.NON_NULL)
private final Object result;
@JsonInclude(Include.NON_NULL)
private final Error error;
private final String id;
...
@JsonProperty("jsonrpc")
public String getVersion() {
return RPC.SUPPORTED_VERSION;
}
}
There is nothing super tricky about this code.
When Jackson serializes responses into JSON format the order I specified is maintained (useful for unit tests) and when certain fields are null
, they are not included in the response. The JSON-RPC spec covers which fields are required, or under which circumstances they are optional.
In STDIO mode, an MCP server should expect a line of input from STDIN to represent a single request and similarly, a line of output to STDOUT should represent one response or notification. Thus, all messages are line-feed delimited and there should be no embedded line feeds in request or response payloads.
Somewhere in your STDIO-mode MCP server you will have a loop scanning for input and inside that loop, a call to convert lines of input from strings (JSON) to a request object to which your server can interpret.
Using ObjectMapper
from Jackson you could have code such as:
var input = in.readLine();
...
try {
return mapper.readValue(input, Request.class);
} catch (JsonMappingException e) {
System.err.println(e.getMessage());
...
}
Step 2: Handling initialization
Once you can parse requests you need to handle them. Some kind of request router or controller make sense here but use your own design instincts.
At this point you may also start adding higher-level abstractions of the domain concepts from MCP so that the server is modular, cohesive, and easy to reason about. In addition to the JSON-RPC classes, I tend to have classes like InitializationResult
(or ...Response
) that get serialized to as responses.
Request routing of messages to the proper handler is not super interesting or important. You may have something simple that just does the job, such as:
public Response handleRequest(Request request) throws Exception {
return switch (request.getMethod()) {
case "initialize" -> initialize(request);
//case "tools/list" -> listTools(request);
//case "tools/call" -> invokeTool(request);
//...
default -> {
// To error or to ignore? That's a good question!
yield null;
}
};
}
Here is an example handling server initialization when requested by the MCP client. Below is an example that generates the appropriate response.
public Response initialize(Request request) {
// is your initialize method idempotent?
...
initialized = true;
return new Response(initializationResult = new InitializationResult(), request.getId());
}
What should you send back in response to the "initialize" message from a client? The specification covers the initialization request / response payloads so you should read more about those details here:
At a minimum you will want to respond with a capability that indicates to the client than your MCP server supports tools. There is also a detailed schema for the various types of messages here:
I find both of these to be good references while developing MCP servers.
Step 3: Handling MCP tool requests
Once you have a working request / response JSON-RPC mechanic running it becomes much easier to iterate on the rest.
After initialization it makes sense to get a basic tool configured as well as a list operation. If your MCP server supports tools an MCP client will want to list them to understand what tools are available and what parameters (the input schema) are required for the tool to function properly.
For my implementations I have used something like a tool manifest that describes the tool and then I respond to the MCP client with a JSON array of those. Here is an example tool manifest class in Java without all the scaffolding:
public abstract class ToolManifest implements Callable<Response> {
private String name;
private String description;
private Object inputSchema;
protected Request request;
...
}
Here I implement java.util.concurrent.Callable
and return a Response
object defined within my project. i.e. ToolSubclass::call()
method is executed, and a Response object is created with the appropriate response data or errors.
Given the above abstract class, I can implement a simple "Hello, World" tool by doing something like this.
public class GreetingTool extends ToolManifest {
public GreetingTool() {
setName("greeting");
setDescription("A tool that says hello");
setInputSchema(new ToolInputSchema());
}
@Override
public Response call() {
var greeting = new ToolResultTextContent("Hello, World!");
var result = new ToolResult(List.of(greeting));
return new Response(result, null, request.getId());
}
@Override
public GreetingTool build(Request _request) {
var tool = new GreetingTool();
tool.request = _request;
return tool;
}
}
In this case, the tool doesn't do anything interesting except help us validate that an LLM is able to call a tool in our server and receive the response. Speaking of validation, the MCP documentation has some helpful notes about debugging MCP servers and I find the MCP inspector tool to be pretty helpful here.
Here is an example running the MCP inspector from the command line to test out the MCP server we are working on.
$ bunx --bun @modelcontextprotocol/inspector java.exe -jar ./target/mcp-server-1.0-SNAPSHOT.jar
Starting MCP inspector...
⚙️ Proxy server listening on port 6277
🔍 MCP Inspector is up and running at http://127.0.0.1:6274 🚀
And in the browser, we might see something like this user interface where we can connect to the STDIO-based MCP server, list tools and execute them.
Step 4: Testing the MCP server in an AI Client!
Once you have an MCP server developed, and everything appears to be working correctly in the MCP inspector (highly recommended) you can configure your server in an AI client. I've tried various integrated developer environment (IDE) plugins as well as stand-alone AI clients such as Claude for Desktop.
{
"mcpServers": {
"tdd": {
"command": "java",
"args": [
"-jar",
"path/to/mcp-server-1.0-SNAPSHOT.jar"
]
}
}
}
Example configuration of an MCP tool in Claude for Desktop.
Here is an example confirming that Claude can use one of my Hello, World tools to execute a tool called "greeting".
Note, although Claude warns us that we are about to execute a tool from an MCP server we don't have much detail on what the request will actually do behind the scenes. This is one example of what I mean about considering the safety aspects of using arbitrary MCP servers with your AI client of choice. Make sure you know what it is doing.
In this case, we are writing our own servers so there is no concern proceeding with the request.
Huzzah!
Hopefully, it is obvious that I left out a lot of scaffolding code in the example above. The point here was to look at the process of creating an MCP server from scratch and not really to provide step by step code samples.
I leave that as an exercise to the reader.
I hope this overview of creating MCP servers and MCP tools was helpful for you. If I goofed or got anything wrong or you want to discuss MCP servers, feel free to hit me up on social media, catch me at a meetup, or chat at a conference.
Thanks for reading and I will see you next time.
Cheers!