mcp-sdk
v1.1.0
Published
MCP developer toolkit | consume, build, proxy, and aggregate SSE MCP servers
Maintainers
Readme
mcp-sdk
Consume MCP servers in your app — or from the terminal.
npm install mcp-sdkimport { MCP } from "mcp-sdk";
const mcp = await MCP.connect("https://example.com/mcp");
const text = await mcp.callText("search_web", { query: "hello" });
await mcp.disconnect();What is this?
Model Context Protocol (MCP) lets AI models call tools hosted on remote servers. Most MCP tooling is aimed at building servers. This package is aimed at using them.
mcp-sdk is two things in one:
| | What it is | Who it's for | |---|---|---| | SDK | A TypeScript library for connecting to MCP servers in your app | App developers | | CLI | A terminal tool that proxies remote servers to AI clients | AI power users |
If you're building an AI app that needs tools, you want the SDK. If you're wiring up Claude Desktop or Cursor to a remote MCP server, you want the CLI.
SDK
Installation
npm install mcp-sdkQuick start
import { MCP } from "mcp-sdk";
const mcp = await MCP.connect("https://example.com/mcp");
// List available tools
const tools = await mcp.tools();
console.log(tools.map(t => t.name));
// Call a tool — returns raw MCP result
const result = await mcp.call("search_web", { query: "hello world" });
console.log(result.content); // [{ type: "text", text: "..." }]
// Convenience: get the first text block as a plain string
const text = await mcp.callText("search_web", { query: "hello world" });
console.log(text); // "The search returned..."
// Get ALL text blocks joined by newline (for multi-block responses)
const full = await mcp.callAllText("list_items", {});
await mcp.disconnect();Transport
By default, mcp-sdk auto-detects the transport protocol — it tries Streamable HTTP (MCP spec 2025-03-26+) first and falls back to SSE automatically. You can also be explicit:
// Auto-detect (default)
const mcp = await MCP.connect("https://example.com/mcp");
// Force Streamable HTTP (newer servers)
const mcp = await MCP.connect("https://example.com/mcp", { transport: "http" });
// Force SSE (legacy servers)
const mcp = await MCP.connect("https://example.com/sse", { transport: "sse" });Authentication
Pass headers as part of options:
const mcp = await MCP.connect("https://api.example.com/mcp", {
headers: {
Authorization: `Bearer ${process.env.MCP_TOKEN}`,
},
});Middleware
Add middleware to intercept every tool call. Middleware is composable and runs in the order added.
import { MCP, logger, cache, retry } from "mcp-sdk";
const mcp = await MCP.connect("https://example.com/sse", {
middleware: [
retry(3), // retry failed calls up to 3 times (exponential backoff)
cache(), // cache successful results for 60 seconds
logger(), // log every call with timing and outcome
],
});Or add middleware after connecting — returns this so it's chainable:
const mcp = (await MCP.connect(url)).use(logger()).use(cache());Built-in middleware:
| Middleware | What it does |
|---|---|
| logger() | Logs tool name, args, response time, outcome to stderr |
| cache(ttlMs?) | Caches successful results in memory. Default TTL: 60s |
| retry(n, backoffMs?) | Retries failures with exponential backoff |
You can write your own:
import type { MiddlewareFn } from "mcp-sdk";
const myMiddleware: MiddlewareFn = async (ctx, next) => {
console.log("calling", ctx.toolName, "on", ctx.serverUrl);
const result = await next();
console.log("done");
return result;
};Multiple servers
Connect to multiple servers at once. Tools are automatically namespaced by hostname so names never collide.
import { MCP } from "mcp-sdk";
const mcp = await MCP.aggregate([
"https://github-mcp.com/sse",
"https://linear-mcp.com/sse",
"https://notion-mcp.com/sse",
]);
// Tools are prefixed: "github__search_repos", "linear__create_issue", etc.
const repos = await mcp.callText("github__search_repos", { q: "mcp" });
const issue = await mcp.call("linear__create_issue", { title: "Fix bug" });
const page = await mcp.call("notion__create_page", { title: "Notes" });
await mcp.disconnect();Per-server auth with shared middleware:
const mcp = await MCP.aggregate(
[
{ url: "https://github-mcp.com/sse", headers: { Authorization: `Bearer ${GH_TOKEN}` } },
{ url: "https://linear-mcp.com/sse", headers: { Authorization: `Bearer ${LIN_TOKEN}` } },
],
{
middleware: [logger(), retry(3)],
}
);Framework integrations
React
npm install mcp-sdk reactSingle server — useMCP:
import { useMCP } from "mcp-sdk/react";
function SearchWidget() {
const { call, callText, tools, loading, error } = useMCP(
"https://example.com/mcp",
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
if (loading) return <p>Connecting...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<p>{tools.length} tools available</p>
<button onClick={() => callText("search_web", { query: "hello" })}>
Search
</button>
</div>
);
}Multiple servers — useAggregateMCP:
import { useAggregateMCP } from "mcp-sdk/react";
function MultiWidget() {
const { call, tools, loading } = useAggregateMCP([
{ url: "https://github-mcp.com/mcp", headers: { Authorization: `Bearer ${GH_TOKEN}` } },
{ url: "https://linear-mcp.com/mcp", headers: { Authorization: `Bearer ${LIN_TOKEN}` } },
]);
if (loading) return <p>Connecting...</p>;
// Tools are namespaced: "github__search_repos", "linear__create_issue"
return <p>{tools.map(t => t.name).join(", ")}</p>;
}Both hooks reconnect automatically when URLs change and disconnect on unmount. Wrap options in useMemo if it contains dynamic values to avoid unnecessary reconnects.
Next.js (App Router)
Create a route handler that proxies tool calls to a remote MCP server. A new connection is made per request — works correctly in serverless environments.
npm install mcp-sdk// app/api/mcp/route.ts
import { createMCPHandler } from "mcp-sdk/next";
export const POST = createMCPHandler("https://example.com/sse", {
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
});Call it from your frontend:
const res = await fetch("/api/mcp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tool: "search_web", args: { query: "hello" } }),
});
const result = await res.json();Express
npm install mcp-sdk expressStateless (one connection per request — simple, works anywhere):
import { mcpHandler } from "mcp-sdk/express";
app.use(express.json());
app.post("/mcp", mcpHandler("https://example.com/sse"));Persistent (one connection reused across requests — better for production):
import { MCP, logger, cache } from "mcp-sdk";
const mcp = await MCP.connect("https://example.com/sse", {
middleware: [logger(), cache()],
});
app.post("/mcp/:tool", async (req, res, next) => {
try {
const result = await mcp.call(req.params.tool, req.body);
res.json(result);
} catch (err) {
next(err);
}
});Full API reference
MCP.connect(url, options?)
Connect to a single SSE MCP server. Returns a connected MCPClient.
const mcp = await MCP.connect(url, {
headers?: Record<string, string>,
middleware?: MiddlewareFn[],
});MCP.aggregate(servers, options?)
Connect to multiple servers. Each entry can be a URL string or a ServerConfig object for per-server options. Returns a connected MCPClient.
const mcp = await MCP.aggregate(
[
"https://server-a.com/sse",
{ url: "https://server-b.com/sse", headers: { ... } },
],
{
headers?: Record<string, string>, // applied to all servers
middleware?: MiddlewareFn[],
}
);MCPClient
| Method | Description |
|---|---|
| .call(tool, args?) | Call a tool. Returns CallToolResult (raw MCP type). |
| .callText(tool, args?) | Call a tool, return the first text block as a string. |
| .callAllText(tool, args?) | Call a tool, return all text blocks joined by \n. |
| .tools() | List all available tools. |
| .resources() | List all available resources. |
| .prompts() | List all available prompts. |
| .use(middleware) | Add middleware. Returns this. |
| .disconnect() | Close all connections. |
Errors
import { MCPConnectionError, MCPToolError } from "mcp-sdk";
try {
const mcp = await MCP.connect("https://example.com/mcp");
} catch (err) {
if (err instanceof MCPConnectionError) {
console.error("Could not reach server:", err.serverUrl);
}
}
try {
await mcp.callText("search_web", { query: "hello" });
} catch (err) {
if (err instanceof MCPToolError) {
console.error("Tool failed:", err.toolName);
}
}CLI
The CLI is built on the same SDK. Use it to wire remote MCP servers into AI clients or debug them from the terminal.
Install
npm install -g mcp-sdk
# or just use npx — no install needed
npx mcp-sdk --helpserve (default)
Proxy one or more SSE MCP servers as a local stdio MCP server. This is what you put in your AI client config.
mcp-sdk <url...> [options]
mcp-sdk serve <url...> [options] # explicit| Option | Description |
|---|---|
| --no-log | Disable call logging to stderr |
| --cache | Cache identical tool call results |
| --cache-ttl <ms> | Cache TTL (default: 60000) |
| --retry <n> | Retry failures up to N times |
| -H <key=value> | Add an HTTP header (repeatable) |
# Single server
mcp-sdk https://example.com/sse
# Multiple servers — auto-namespaced
mcp-sdk https://github-mcp.com/sse https://linear-mcp.com/sse
# With auth and middleware
mcp-sdk https://api.example.com/sse \
-H "Authorization=Bearer $TOKEN" \
--cache --retry 3inspect
Print everything a server exposes — tools, resources, prompts — without starting a proxy.
mcp-sdk inspect <url> [-H <key=value>]Server: https://example.com/sse
────────────────────────────────────────────────────────────
TOOLS (3)
search_web(query: string, limit?: number)
Search the web and return results
get_weather(location: string)
Get current weather for a location
summarize(url: string)
Fetch and summarize a URL
RESOURCES (1)
file:///{path} — Local file system
PROMPTS (0)
nonecall
Call a tool directly and print the result. Useful for testing.
mcp-sdk call <url> <tool> [json-args] [-H <key=value>]mcp-sdk call https://example.com/sse search_web '{"query":"hello"}'
mcp-sdk call https://api.example.com/sse my_tool '{}' -H "Authorization=Bearer $TOKEN"Wiring into AI clients
Claude Desktop — edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-sdk", "https://example.com/sse", "-H", "Authorization=Bearer YOUR_TOKEN"]
}
}
}Multiple servers in one entry:
{
"mcpServers": {
"everything": {
"command": "npx",
"args": [
"mcp-sdk",
"https://github-mcp.com/sse",
"https://linear-mcp.com/sse",
"https://slack-mcp.com/sse"
]
}
}
}Cursor — edit .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-sdk", "https://example.com/sse"]
}
}
}Server
Build your own MCP server — without the boilerplate of the official SDK.
Define tools individually with defineTool, then pass them to createServer. Each tool is a self-contained unit: a name, a description, a Zod input schema, and a run function.
npm install mcp-sdk zodQuick start
import { defineTool, createServer } from "mcp-sdk/server";
import { z } from "zod";
const getUser = defineTool({
name: "get_user",
description: "Fetch a user by ID",
input: z.object({
id: z.string().describe("The user's ID"),
}),
run: async ({ id }, { headers }) => {
const res = await fetch(`https://api.example.com/users/${id}`, { headers });
return res.json();
},
});
const server = createServer({
tools: [getUser],
auth: { type: "bearer", token: process.env.API_TOKEN! },
});
await server.listen(); // stdio — ready for Claude Desktop, Cursor, etc.Define tools in separate files
Tools are plain objects — split them across files however makes sense for your project.
// tools/search-users.ts
import { defineTool } from "mcp-sdk/server";
import { z } from "zod";
export const searchUsers = defineTool({
name: "search_users",
description: "Search users by name or email",
input: z.object({
query: z.string().describe("Name or email fragment"),
limit: z.number().default(10).describe("Max results"),
}),
run: async ({ query, limit }, { headers }) => {
const res = await fetch(
`https://api.example.com/users?q=${query}&limit=${limit}`,
{ headers }
);
return res.json();
},
});// index.ts
import { createServer } from "mcp-sdk/server";
import { searchUsers } from "./tools/search-users.js";
import { createIssue } from "./tools/create-issue.js";
const server = createServer({
tools: [searchUsers, createIssue],
auth: { type: "bearer", token: process.env.API_TOKEN! },
});
await server.listen();Auth
Auth is configured once on the server and injected into every tool as ctx.headers — ready to drop into fetch. Tools never handle credentials directly.
// Bearer token
createServer({ auth: { type: "bearer", token: process.env.TOKEN! }, tools });
// API key header
createServer({ auth: { type: "apikey", header: "X-API-Key", value: process.env.KEY! }, tools });
// HTTP Basic
createServer({ auth: { type: "basic", username: "user", password: process.env.PASS! }, tools });
// No auth (default)
createServer({ tools });Inside a tool, use it directly:
run: async ({ id }, { headers }) => {
return fetch(`https://api.example.com/users/${id}`, { headers }).then(r => r.json());
}Return anything
run can return a string, number, object, or array — the SDK serializes it to MCP content automatically. Throw to signal a tool error.
run: async () => "pong" // string → text content
run: async ({ id }) => ({ name: "Sam" }) // object → JSON text content
run: async () => { throw new Error("oops") } // throw → isError resultTest tools without a server
server.call() invokes a tool directly — no MCP protocol, no client needed. Use it in unit tests.
const server = createServer({ tools: [getUser], auth: { ... } });
const result = await server.call("get_user", { id: "123" });
console.log(result.content[0].text); // the tool's outputWire into Claude Desktop
{
"mcpServers": {
"my-api": {
"command": "node",
"args": ["/path/to/your/index.js"]
}
}
}Full API
defineTool(definition)
| Field | Type | Description |
|---|---|---|
| name | string | Tool name shown to the AI |
| description | string | What the tool does — the AI uses this to decide when to call it |
| input | z.ZodObject | Zod schema for the tool's arguments. Use .describe() on fields for per-param descriptions |
| run | (args, ctx) => unknown | Tool handler. Return any value; throw to signal failure |
createServer(options)
| Field | Type | Description |
|---|---|---|
| tools | ToolDefinition[] | Tools to expose |
| auth | AuthConfig? | Auth injected into ctx.headers in every tool |
| name | string? | Server name shown to MCP clients. Default: "mcp-server" |
| version | string? | Server version. Default: "1.0.0" |
MCPServer
| Method | Description |
|---|---|
| .listen() | Start the server over stdio |
| .call(toolName, args?) | Call a tool directly — useful for testing |
How it compares
| | mcp-sdk | mcp-remote | @modelcontextprotocol/sdk |
|---|---|---|---|
| Role | Consume + build MCP servers | CLI proxy only | Build servers/clients (low-level) |
| Single-server proxy | ✓ | ✓ | manual |
| Multi-server aggregation | ✓ | ✗ | ✗ |
| Middleware (log, cache, retry) | ✓ | ✗ | ✗ |
| React / Next.js / Express | ✓ | ✗ | ✗ |
| Build a server (Zod tools) | ✓ | ✗ | boilerplate |
| inspect and call commands | ✓ | ✗ | ✗ |
| OAuth | soon | ✓ | manual |
Requirements
- Node.js 18+
- Zod 3 or 4 (only required for
mcp-sdk/server)
License
MIT
