MCP has a client-server architecture. The client is a protocol client that has a one to one section with a server. The server enables you to use MCP to connect the LLM to your database to take out actions. MCP includes many SDKs that you can check out here. For this post I’ll use the TypeScript SDK, you can get the starter code I use here.
Model
This refers to the AI model/LLM you are using.
Context
Context refers to what you are trying to send to the model for the response you want to get back.
Protocol
Protocol is what you use to get this response.
LLMs are able to serve large datasets but do not have the ability to serve this data to an application, this is where MCP server comes in.
npm install @modelcontextprotocol/sdk
Handles the connect, protocol and message routing.
const server = new McpServer({
name: "My App",
version: "1.0.0",
});
This is how you expose the data within the LLM.
server.resource("config", "config://app", async (uri) => ({
contents: [
{
uri: uri.href,
text: "App configuration here",
},
],
}));
Tools perform computation (unlike resources) and can have side effects. This is how you can take actions through the server. Below is example code that looks like a GET request from a RESTful API via a fetch on the front-end.
server.tool("fetch-weather", { city: z.string() }, async ({ city }) => {
const response = await fetch(`https://api.weather.com/${city}`);
const data = await response.text();
return {
content: [{ type: "text", text: data }],
};
});
Reusable templates that enable the LLM to interact with the server.
server.prompt("review-code", { code: z.string() }, ({ code }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`,
},
},
],
}));
import express, { Request, Response } from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const server = new McpServer({
name: "example-server",
version: "1.0.0",
});
// ... set up server resources, tools, and prompts ...
const app = express();
// to support multiple simultaneous connections we have a lookup object from
// sessionId to transport
const transports: { [sessionId: string]: SSEServerTransport } = {};
app.get("/sse", async (_: Request, res: Response) => {
const transport = new SSEServerTransport("/messages", res);
transports[transport.sessionId] = transport;
res.on("close", () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
app.post("/messages", async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).send("No transport found for sessionId");
}
});
app.listen(3001);
# Create project directory
mkdir mcp-client-typescript
cd mcp-client-typescript
# Initialize npm project
npm init -y
# Install dependencies
npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
# Install dev dependencies
npm install -D @types/node typescript
# Create source file
touch index.ts
In package.json:
{
"type": "module",
"scripts": {
"build": "tsc && chmod 755 build/index.js"
}
}
Create a tsconfig.json
file:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["index.ts"],
"exclude": ["node_modules"]
}
Setting up API Key
Create a .env
file: echo "ANTHROPIC_API_KEY=<your key here>" > .env
Add .env
to your .gitignore
: echo ".env" >> .gitignore
In index.ts
:
import { Anthropic } from "@anthropic-ai/sdk";
import {
MessageParam,
Tool,
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import readline from "readline/promises";
import dotenv from "dotenv";
dotenv.config();
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
throw new Error("ANTHROPIC_API_KEY is not set");
}
class MCPClient {
private mcp: Client;
private anthropic: Anthropic;
private transport: StdioClientTransport | null = null;
private tools: Tool[] = [];
constructor() {
this.anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
});
this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
}
// methods will go here
}
Connect to the MCP server:
async connectToServer(serverScriptPath: string) {
try {
const isJs = serverScriptPath.endsWith(".js");
const isPy = serverScriptPath.endsWith(".py");
if (!isJs && !isPy) {
throw new Error("Server script must be a .js or .py file");
}
const command = isPy
? process.platform === "win32"
? "python"
: "python3"
: process.execPath;
this.transport = new StdioClientTransport({
command,
args: [serverScriptPath],
});
this.mcp.connect(this.transport);
const toolsResult = await this.mcp.listTools();
this.tools = toolsResult.tools.map((tool) => {
return {
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
};
});
console.log(
"Connected to server with tools:",
this.tools.map(({ name }) => name)
);
} catch (e) {
console.log("Failed to connect to MCP server: ", e);
throw e;
}
}
Running the client with any MCP server
# Build TypeScript
npm run build
# Run the client
node build/index.js path/to/server.py # python server
node build/index.js path/to/build/index.js # node server
There are many MCP servers, from GitHub to Anthropic. Pulse MCP has some good examples of both clients and servers
Jack Herrington’s video was a nice overview and the mcp documentation is very clear and easy to follow.