- Biweekly Engineering
- Posts
- Fantastic MCP Servers and How to Build Them | Biweekly Engineering - Episode 41
Fantastic MCP Servers and How to Build Them | Biweekly Engineering - Episode 41
From what is MCP to an actual implementation of an MCP server!
Disclaimer: Gmail might clip the episode partially so visit Biweekly Engineering to read the full piece.
One of the more recent hypes around AI was MCP, aka Model Context Protocol. In simple terms, it’s a standard developed by Anthropic to make LLMs seamlessly interact with outside world.
Soon after MCP was made public, the AI community embraced it with open arms. Many engineers and enthusiasts started to build MCP servers on their own. There are now thousands of MCP servers on Github and other platforms. And there are even MCP markets where you pay for using MCP built by someone else!
Examples of platforms where you can find already implemented MCP servers are smithery.ai, mcpmarket.com and mcp.so.
But what is MCP? And how can one build an MCP server like others? This episode takes you on a trip to MCP.
Let’s go! 🚞

A bus turned into coffee shop - Hella, Iceland
MCP - A Quick Primer
What is MCP?
As mentioned before, MCP (Model Context Protocol) is a standardized protocol developed by Anthropic. It allows AI models like Claude or Gemini to connect to and interact with external data sources and tools.
MCP acts as a bridge between the AI model and various services, databases, or applications.
Think of MCP as a universal translator and connector. Instead of AI models being limited to their training data or requiring custom integrations for each service, MCP provides a standardized way for them to access live information and perform actions across different platforms.
How Does MCP Work?
Conceptually, the MCP server acts as an intermediary that sits between a model and an external service.
When you ask a model to do something that requires external data or actions, the model communicates with the MCP server using the standardized protocol. The MCP server then translates these requests into the appropriate format for the target service, retrieves the information or performs the action, and sends the results back to the model.
For example, imagine you're working on a project and you ask Gemini: "Can you check my Google Calendar for next week and then create a summary document in my Google Drive with my upcoming meetings?"
With an MCP server configured for Google services, here's what happens:
Gemini sends a request to the MCP server asking for calendar data
The MCP server authenticates with your Google Calendar, retrieves next week's events, and sends the data back to Gemini
Gemini processes this information and creates a summary
Gemini then sends another request to the MCP server to create a document in Google Drive
The MCP server handles the document creation and confirms completion
Key Terminologies behind MCP
MCP Server: A service that implements the MCP protocol and provides access to specific resources, tools, or data sources. Servers expose capabilities that MCP clients can discover and use.
MCP Client: An application (like Claude Desktop or other AI assistants) that connects to MCP servers to access external resources and capabilities through the standardized protocol.
Resources: External data sources or content that MCP servers make available to clients, such as files, databases, APIs, or real-time data feeds.
Tools: Specific functions or actions that MCP servers expose to clients, allowing the AI to perform operations like running code, making API calls, or manipulating data.
Prompts: Pre-defined prompt templates that MCP servers can provide to clients, offering structured ways to interact with specific resources or perform common tasks.
Transport Layer: The underlying communication mechanism that connects MCP clients and servers, which can be stdio, HTTP, or other protocols.
Capabilities: The specific features and functionalities that an MCP server advertises to clients during the connection handshake, defining what resources and tools are available.
Schema: The structured format definitions that describe how data and messages are formatted when exchanged between MCP clients and servers.
Session: The active connection between an MCP client and server, maintaining state and context throughout the interaction.

Diagram by Anthropic
Building an MCP Server
While I was aware of the concept behind MCP, I never went hands on. So recently, I started to look up more details about MCP and decided to implement something myself. After all, what's better than getting your hands dirty?
Let’s discuss more.
Deciding What to Build
First step was to decide what kind of MCP server you want to build. There is no rule here, really. You just pick up something that you think will be useful for you or for the community. Or something that is just fun.
I looked for useful services and decided to build something that potentially doesn’t exist in one form or another. Finally, I landed on Google Scholar because I could only find MCP server implementations that were in Python or implemented in stdio
protocol.
Transport Protocol
The first decision I took was the transport protocol to use. As mentioned in the previous section, MCP has three transport protocols:
stdio (Standard Input/Output): This is a simple communication method. Here, the MCP server runs as a separate process and communicates with the client through standard input and output streams.
The client launches the server process and exchanges JSON messages through stdin/stdout pipes. This is lightweight and works well for local integrations where the server runs on the same machine as the client. It's commonly used for desktop applications and local development scenarios.
Since the first use-case of MCP was for Claude Desktop app, this was the protocol initially proposed.HTTP-SSE (HTTP Server-Sent Events): This is the first web-based communication protocol where the MCP server runs as an HTTP server and uses Server-Sent Events for real-time, bidirectional communication.
SSE allows the server to push data to the client over a persistent HTTP connection. So it means, this protocol is essentially enabling real-time updates and streaming responses.
This approach works well for web-based clients and remote server deployments. It offers better scalability and network traversal compared to stdio.Streamable HTTP: This is the second and the latest transport protocol. It was proposed as a variant of HTTP communication that supports streaming responses, allowing data to be sent and received in chunks rather than waiting for complete messages.
This transport protocol enables more efficient handling of large datasets or long-running operations by processing data incrementally as it arrives.
It's particularly useful when dealing with large files, real-time data feeds, or operations that generate substantial amounts of output over time.
In short, if you want to build something that’s local and it’s required that model interacts with the server running on a different process but on the same node, you should opt for stdio
. But since internet is distributed and it’s more common to have things communicate over network, Streamable HTTP is the way to go.
After Streamable HTTP was published, HTTP-SSE has become legacy. Yes, things are moving that fast!
I decided to use Streamable HTTP since this is the recommended standard right now.
Single Endpoint with POST
and GET
In Streamable HTTP, there is single endpoint /mcp
with POST
and GET
methods. MCP clients call GET
to receive data over HTTP stream and POST
to execute a typical HTTP request-response.
const MCP_ENDPOINT = "/mcp";
router.post(MCP_ENDPOINT, async (req: Request, res: Response) => {
await server.handlePostRequest(req, res);
});
router.get(MCP_ENDPOINT, async (req: Request, res: Response) => {
await server.handleGetRequest(req, res);
});
Handling GET
Requests
The following code is an example of how to handle GET
requests in Streamable HTTP.
async handleGetRequest(req: Request, res: Response) {
console.log("Received GET request");
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !this.transports[sessionId]) {
res
.status(400)
.json(
this.createErrorResponse("Bad Request: invalid session ID or method.")
);
return;
}
console.log(`Establishing SSE stream for session ${sessionId}`);
const transport = this.transports[sessionId];
await transport.handleRequest(req, res);
await this.streamMessages(transport);
}
As you can see, the GET
request is basically establishing a connection between server and client to stream response from the server. Note that this stream is sent over SSE.
Handling POST
Requests
POST /mcp
is used to communicate using request-response HTTP between the client and the server.
async handlePostRequest(req: Request, res: Response) {
const sessionId = req.headers[SESSION_ID_HEADER_NAME] as string | undefined;
let transport: StreamableHTTPServerTransport;
console.log('Executing request for with sessionId:', sessionId);
try {
// reuse existing transport
if (sessionId && this.transports[sessionId]) {
transport = this.transports[sessionId];
await transport.handleRequest(req, res, req.body);
return;
}
// create new transport
if (!sessionId && this.isInitializeRequest(req.body)) {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await this.server.connect(transport);
await transport.handleRequest(req, res, req.body);
// session ID will only be available (if in not Stateless-Mode)
// after handling the first request
const sessionId = transport.sessionId;
if (sessionId) {
this.transports[sessionId] = transport;
}
return;
}
res.status(400).json(
this.createErrorResponse("Bad Request: invalid session ID or method.")
);
} catch (error) {
console.error("Error handling MCP request:", error);
res.status(500).json(this.createErrorResponse("Internal server error."));
}
}
The core idea from the code snippet above is the using the transport
to call handleRequest
which propagates the request to correct handler. This is handled by the MCP SDK internally.
Session Management
Both the code snippets above have session handling. Streamable HTTP also defines how session should be handled.
The basic idea can be summarized as below:
When an MCP client connects to a server, it sends a
POST InitializeRequest
. The MCP server returns asessionId
which is later passed asMcp-Session-Id
header.This is handled by the code snippet above in the second
if
block where it checks if the request is anInitializeRequest
without anysessionId
set in the header.If client wants to send a simple HTTP request to the server, it passes the
Mcp-Session-Id
in its header in aPOST
request. The server returns a response as usual.To establish an SSE-stream connection, a
GET /mcp
request is also sent to the server and client should set theMcp-Session-Id
header. Upon receiving this, server opens the SSE stream with the client. Later, server may use this connection to send streaming response.
Note that in Streamable HTTP, there is freedom between the client and server on how to communicate. A POST
request could mean server send back a typical response (e.g application/json
) or a stream text/stream
). If server sends a text/stream
response, then the already established SSE-stream connection is used.
Standard HTTP Response Format
HTTP/1.1 200 OK
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": "request-id",
"result": {
// Response data
}
}
SSE Response Format
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"jsonrpc": "2.0", "id": "request-id", "result": {...}}
data: {"jsonrpc": "2.0", "method": "notifications/progress", "params": {...}}
Tool Setup
During the initialization of the MCPServer
object, a setupTools
function is invoked.
private setupTools() {
// Define available tools
const setToolSchema = () =>
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: googleScholarTools,
};
});
setToolSchema();
// handle tool calls
this.server.setRequestHandler(
CallToolRequestSchema,
async (request, extra) => {
const args = request.params.arguments;
const toolName = request.params.name;
console.log("Received request for tool with argument:", toolName, args);
if (!args) {
throw new Error("arguments undefined");
}
if (!toolName) {
throw new Error("tool name undefined");
}
/* Handle tool call here */
if (toolName === 'search_google_scholar') {
return await callSearchGoogleScholarTool(args);
}
throw new Error("Tool not found");
}
);
}
The setRequestHandler
sets the handler implementation for different schemas. As you can see, there are two schema types here: ListToolsRequestSchema
and CallToolRequestSchema
. These are types supported by the MCP SDK. When you set the handlers on these schema types, the SDK under the hood takes care of invoking the right handler for the right scenario.
The crux of this snippet is the following:
if (toolName === 'search_google_scholar') {
return await callSearchGoogleScholarTool(args);
}
If a new tool is added, simply invoke it based on the new tool name.
The actual implementation of tools and tool calls can be found here. This file defines the tools and their implementations. After the initial scaffolding of an MCP server, developers are expected to mostly write business logic as implementation of new tools or enhancements of the existing ones.
MCP Client
The last piece of the puzzle is an MCP client. This is the process that will communicate with an MCP server and possibly interface with LLMs. In my implementation, notice the following flow:
MCP client initiates a
chatLoop
Upon a user query, an API call to Gemini is made
In Gemini’s response, there could be one or more tool calls
MCP client determines the tool calls and actually calls the tools
const toolResult = await this.mcp.callTool({
name: toolCall.name,
arguments: toolCall.args,
});
The response from the tool calls are again fed back to the model to receive the final response.
Note that this is a very basic interaction showcasing how MCP servers can come into use. In production systems, this will most likely be supported by a framework that under the hood takes care of tool calls and model re-invocation. And this could as well happen in a loop until the model doesn’t need any more tool calls.
Lastly, as you can guess, the MCP client also uses the same Streamable HTTP transport to establish the connection with the server.
// Initialize transport and connect to server
const url = new URL(serverUrl);
this.transport = new StreamableHTTPClientTransport(url);
await this.mcp.connect(this.transport);
this.setUpTransport();
With transport initiation on both client and server, the connection is successfully established.
Further Reading
The technology is new and there is a lot to discover. Below are some potential sources you can look into to learn more.
Okay, so finally we are done! This was a long one and longer than my usual posts. But I wanted to be a bit more hands-on for my readers to get comfortable with MCP and its implementation. Hope this helps, at least some of you!
See you all in the next episode! 🌊
Reply