Whether you’re implementing a chat bot, or some complicated business analytical tool, AI is a game changer in the ability to provide natural language responses.
What is MCP
MCP stands for Model Context Protocol, and was developed and released as open source by Anthropic in 2024. It is a standard around providing a set of tools that an AI can call, enhancing the capabilities of the underlying model.
You can think of MCP as being the following things all in one:
- An AI phrasebook of things that will help it do the things you want
- An encyclopedia of very specific information, to improve the AI’s accuracy
- A toolbox filled with tools for specific jobs, that an AI can use as needed
What is the Protocol in MCP
In MCP there is a strict flow that AI agents need to follow to be able to communicate with an MPC server, it is all communicated via a JSON-RPC format where both the client and server declare their capabilities ahead of time.
The basic flow is as follows:
- AI sends an initialize message to the MCP “Hey, I’m an AI, I am capable of doing the following things: …”
- MCP responds with a message acknowledging the initialization, along with its own capabilities, a session ID and some instructions on what its purpose is. “Hey, I’m an MCP server for providing some specific tool to your purpose.”
- The AI then replies back to the MCP agreeing with an initialized response.
- The AI then can request a list of tools, which the MCP will respond with all tools it has available to it, any arguments it needs, and a natural language description of the tools purpose.
More formally the flow looks like the following:
sequenceDiagram
participant AI
participant MCP
AI->>MCP: Initialize Request With Capabilities
activate MCP
MCP->>AI: Initialize Response With Capabilities and Session ID
AI->>MCP: Initialize Acknowledgement
deactivate MCP
AI->>MCP: Request tool List
activate MCP
MCP->>AI: List of Tools
deactivate MCP
AI->>MCP: Call tool
activate MCP
create participant Tool
MCP->>Tool: Calls tool with arguments
destroy Tool
Tool->>MCP: Returns Result
MCP->>AI: Returns tool response
deactivate MCP
Lets Talk Layers
MCP is fundamentally made up of 2 separate layers:
- The Data Layer
- The Transport Layer
The Transport layer is effectively wrapping up the data layer of an MCP server, this means to get to the good stuff inside, we first need to figure out how we’re going to communicate with the MCP server.
The Data Layer
The data layer is built up of three core components:
- Prompts
- Resources
- Tools
Prompts are a way for AI tools to use a template to generate a prompt to solve a specific task. These are designed to often be triggered by a user interaction, such as tab completion within an IDE.
Resources are a way for the MCP to share files to the client. They are typically controlled by the application, in terms of how they are used, but the MCP protocol itself does not specify how they should be used by clients.
Tools allow the MCP server to do things with remote systems, this could be via calling APIs, querying databases or even running local applications such as git. This is one of the most powerful parts of an MCP server, and the one we’ll be focusing on later in this article.
The Transport Layer
The transport for MCP can occur in two separate ways, either locally via STDIO, or via streamable HTTP requests, for client/server communication. For most systems, we’re likely to need to call remote MCP servers to perform tasks we cannot usually do with our LLM, however for tools such as local developer tools to be integrated with VS Code, it would be more appropriate, and more performant for us to implement STDIO streams.
Historically remote MCP implementations used an SSE format, but as of the 2025-06-18 version of the specification, remote MCP servers use Streamable HTTP.
An Example: PokéMCP Server
So let’s do something practical. We could empower our AI with knowledge of Pokémon, this is something that an agent may already have some understanding of, but we can guarantee it has the correct information.
A solid starting point would be PokéAPI this is a free to use RESTful API for Pokémon.
Prerequisites
The below example assumes the following:
- You are using a modern version of node (NOTE: The MCP SDK requires at least node 18)
- You have initialized an npm project
- You have setup TypeScript
First our Tool
Our initial tool will allow us to look up a Pokémon by either name or Pokédex number. Using the pokedex-promise-v2 package, this is fairly straightforward, as it nicely wraps up PokéAPI’s API and response formats.
// src/tools/getPokemon.ts
import Pokedex from "pokedex-promise-v2";
const pokedex = new Pokedex();
export async function getPokemon({
identifier,
}: {
identifier: string | number;
}) {
const pokemon = await pokedex.getPokemonByName(identifier);
return pokemon;
}
Setting up the MCP Server
Next we’ll go ahead and set up the MCP server itself, for this we’ll be using the official MCP TypeScript SDK.
Additionally we’ll be using Zod to handle our schema validation for inputs.
There are two parts here, first we define our MCP server, including its name and version number.
Next we register our tool, this includes defining the parameter we’ll be calling it with and then serializing the results so they can be correctly sent back to the client.
Of note is that you can see that we return JSON as text.
Additionally the below code would benefit from some improved error handling, but for the sake of clarity this has been omitted.
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
/**
* Factory to build and configure the PokéMCP server instance.
* Separating this allows embedding in alternate runtimes/tests without
* invoking transports directly.
*/
export function getMcpServer() {
const server = new McpServer({
name: "poke-mcp",
version: "1.0.0",
});
server.registerTool(
"getPokemon",
{
title: "Get Pokémon Info",
description: "Get information about a Pokémon by name or ID",
inputSchema: { identifier: z.union([z.string(), z.number()]) },
},
async ({ identifier }) => {
console.log("Fetching Pokémon:", identifier);
const { getPokemon } = await import("./tools/getPokemon.js");
const pokemon = await getPokemon({ identifier });
return {
content: [
{
type: "text" as const,
text: JSON.stringify(pokemon),
},
],
};
},
);
return server;
}
Implementation STDIO transport
The next logical step here is to enable calling the MCP server via STDIO. The implementation here is fairly straightforward and doesn’t require too much thought.
// src/transports/stdio.ts
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
/**
* Start the MCP server using stdio transport.
* Logs lifecycle events to stderr to avoid interfering with protocol stdout.
*/
export async function startStdio(server: McpServer) {
console.error("Starting PokéMCP server (stdio transport)...");
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("PokéMCP server running on stdio transport");
}
Wire up the entrypoint for STDIO
Finally we need make this all accessible via a CLI app, which we have used commander to build.
We’ll also do a little extra legwork here in preparation for implementing the Streamable HTTP transport later, but for now if we try to call it with http, we’ll just throw a Not Implemented Error.
// src/index.ts
import { Command } from "commander";
import { getMcpServer } from "./server.js";
import { startStdio } from "./transports/stdio.js";
const server = getMcpServer();
async function main() {
const program = new Command();
program
.name("poke-mcp")
.description("PokéMCP server")
.option(
"-t, --transport <mode>",
"Transport mode to use (stdio|http)",
process.env.TRANSPORT || "stdio",
)
.option(
"-p, --port <number>",
"Port for HTTP transport (when using --transport http)",
process.env.PORT || "3000",
);
program.parse(process.argv);
const opts = program.opts<{
transport: string;
port: string;
}>();
const mode = opts.transport === "http" ? "http" : "stdio";
if (mode === "http") {
throw new Error("Not Implemented");
} else {
await startStdio(server);
}
}
main().catch((error) => {
console.error("Server failed to start:", error);
process.exit(1);
});
Testing out STDIO
Run the following:
npx tsc --rootDir src --outDir distnpx @modelcontextprotocol/inspector node dist/index.js- Click connect
- Click List Tools
- Click
getPokemon - Enter identifier
25orpikachu - Tool Result shows success with Pikachu’s data loaded
Adding support for Streamable HTTP Transport
This is more involved, as we require a working HTTP Server, so we’ll use express in this example, however you could wire this up to pretty much anything.
// src/transports/http.ts
import { randomUUID } from "crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import express from "express";
/**
* Start the MCP server using the experimental streaming HTTP transport.
* Exposes a single POST /mcp endpoint. Manages session state internally.
*/
export interface HttpTransportOptions {
port?: number;
}
export async function startHttp(
server: McpServer,
options: HttpTransportOptions = {},
) {
console.error("Starting Pokémon MCP server (HTTP transport)...");
const app = express();
app.use(express.json());
const transports = new Map<string, StreamableHTTPServerTransport>();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport | undefined;
if (sessionId) {
transport = transports.get(sessionId);
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
transports.set(sid, transport as StreamableHTTPServerTransport);
},
});
transport.onclose = () => {
const sessionId = transport?.sessionId;
if (sessionId && transports.has(sessionId)) {
transports.delete(sessionId);
}
};
await server.connect(transport);
}
if (!transport) {
res.status(500).send("Invalid or missing MCP session ID");
return;
}
await transport.handleRequest(req, res, req.body);
});
const port = options.port ?? 3000;
app.listen(port, () => {
console.log(`PokéMCP server running via HTTP on port ${port} (POST /mcp)`);
});
}
Update the index file with the following, this will allow us to run the MCP with the HTTP transport instead of via STDIO.
// src/index.ts
...
if (mode === 'http') {
const portNum = parseInt(opts.port, 10) || 3000;
await startHttp(server, { port: portNum });
} else {
await startStdio(server);
}
...
You can now rebuild the server using npx tsc --rootDir src --outDir dist and then modify the connection within the MCP Inspector and connect using your new address http://localhost:3000/mcp
All the previous steps should still work and you should be able to still fetch pokemon information.
Making Your Agent Truly Smart
MCP transforms AI agents from impressive conversationalists into capable assistants that can actually do things. By standardizing how agents access external tools and data, MCP eliminates the complexity of building custom integrations for every new capability.
The PokéMCP example we built demonstrates the core pattern: wrap an API or service in a simple tool interface, and suddenly your AI can access real-time, accurate data. Scale this approach across databases, file systems, APIs, and local tools, and you’ve built an agent that’s genuinely useful rather than just impressive.
Your Next Steps
- Start small: Pick one API or tool your current project needs and wrap it in MCP
- Explore existing servers: Check the MCP server directory before building from scratch
- Stack capabilities: Combine multiple MCP servers to create powerful, specialized agents
The ecosystem is rapidly growing, with official SDKs for TypeScript, Python, and Go, plus community implementations emerging for other languages. Whether you’re building a customer service bot that needs to check order status, or an analytics agent that queries your data warehouse, MCP provides the standardized foundation to make it happen.
Ready to make your AI agent actually smart? The tools are here, the protocol is stable, and the only limit is your imagination.