Model Context Protocol (MCP) Libraries for Node.js 2026
TL;DR
Model Context Protocol (MCP) is Anthropic's open standard for connecting AI models to data sources — and it's growing fast. In 2026, MCP lets you expose tools, resources, and prompts to any MCP-compatible AI client (Claude Desktop, Cursor, your own apps). The official @modelcontextprotocol/sdk is the foundation; FastMCP (fastmcp) adds a developer-friendly layer on top. Building an MCP server is genuinely useful when you want your tools usable across multiple AI contexts — not just in your app, but in Claude Desktop, Cursor, and future clients.
Key Takeaways
@modelcontextprotocol/sdk: official Anthropic SDK, stdio + SSE transports, ~200K weekly downloads and growing fastfastmcp: developer-friendly wrapper — less boilerplate, Zod schemas, built-in authentication- MCP vs tool calling: tool calling is per-request; MCP servers are persistent, reusable across clients
- Three primitives: Tools (actions), Resources (readable data), Prompts (reusable templates)
- Transport: stdio (local processes), SSE/WebSocket (remote servers), HTTP (stateless)
- Use cases: database query tools, file system access, API wrappers, knowledge base search
MCP Download Trends
| Package | Weekly Downloads | Trend |
|---|---|---|
@modelcontextprotocol/sdk | ~210K | ↑ Growing fast |
fastmcp | ~45K | ↑ Growing |
@anthropic-ai/sdk | ~800K | ↑ Growing |
MCP is early-stage but growing rapidly as Claude Desktop and Cursor adoption increases.
What is MCP?
MCP (Model Context Protocol) is a standard that lets AI models connect to external data sources and tools. Instead of hardcoding API calls inside your app, you build an MCP server that exposes:
- Tools: callable functions (like API calls, database queries)
- Resources: readable data (files, documents, database records)
- Prompts: reusable prompt templates with parameters
Any MCP-compatible client can then use your server. Build once, use everywhere.
Official SDK: @modelcontextprotocol/sdk
Building a Basic MCP Server
// npm install @modelcontextprotocol/sdk zod
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
const server = new Server(
{
name: 'pkgpulse-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Register available tools:
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_package_stats',
description: 'Get npm package download stats, health score, and metadata',
inputSchema: {
type: 'object',
properties: {
packageName: { type: 'string', description: 'npm package name (e.g., "react")' },
},
required: ['packageName'],
},
},
{
name: 'compare_packages',
description: 'Compare two npm packages by downloads, bundle size, and health score',
inputSchema: {
type: 'object',
properties: {
packageA: { type: 'string' },
packageB: { type: 'string' },
},
required: ['packageA', 'packageB'],
},
},
],
}));
// Handle tool calls:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'get_package_stats': {
const { packageName } = args as { packageName: string };
// Fetch from npm API:
const [downloads, metadata] = await Promise.all([
fetch(`https://api.npmjs.org/downloads/point/last-week/${packageName}`).then(r => r.json()),
fetch(`https://registry.npmjs.org/${packageName}`).then(r => r.json()),
]);
return {
content: [
{
type: 'text',
text: JSON.stringify({
name: packageName,
weeklyDownloads: downloads.downloads,
latestVersion: metadata['dist-tags']?.latest,
description: metadata.description,
license: metadata.license,
lastPublished: metadata.time?.[metadata['dist-tags']?.latest],
}, null, 2),
},
],
};
}
case 'compare_packages': {
const { packageA, packageB } = args as { packageA: string; packageB: string };
const [statsA, statsB] = await Promise.all([
fetchPackageStats(packageA),
fetchPackageStats(packageB),
]);
return {
content: [
{
type: 'text',
text: JSON.stringify({ packageA: statsA, packageB: statsB }, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start the server:
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server running on stdio');
Registering Resources
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// Expose database records as readable resources:
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'pkgpulse://packages/trending',
name: 'Trending npm Packages',
description: 'Top 50 trending npm packages this week',
mimeType: 'application/json',
},
{
uri: 'pkgpulse://categories',
name: 'Package Categories',
description: 'All package categories with counts',
mimeType: 'application/json',
},
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'pkgpulse://packages/trending') {
const trending = await db.package.findMany({
orderBy: { weeklyDownloadChange: 'desc' },
take: 50,
});
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(trending, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
FastMCP: Developer-Friendly Alternative
FastMCP wraps the official SDK with better ergonomics — Zod schemas, cleaner tool definition, built-in auth:
// npm install fastmcp zod
import { FastMCP } from 'fastmcp';
import { z } from 'zod';
const mcp = new FastMCP({
name: 'pkgpulse-mcp',
version: '1.0.0',
});
// Define tools with Zod schemas (no manual JSON Schema):
mcp.addTool({
name: 'get_package_stats',
description: 'Get npm package download stats and health score',
parameters: z.object({
packageName: z.string().describe('npm package name, e.g. "react"'),
includeBundleSize: z.boolean().default(false),
}),
execute: async ({ packageName, includeBundleSize }) => {
const stats = await fetchPackageStats(packageName);
if (includeBundleSize) {
const bundle = await fetch(`https://bundlephobia.com/api/size?package=${packageName}`)
.then(r => r.json());
return { ...stats, gzipSize: bundle.gzip, parseTime: bundle.assets?.[0]?.js?.parse };
}
return stats;
},
});
mcp.addTool({
name: 'search_packages',
description: 'Search npm packages by keyword',
parameters: z.object({
query: z.string(),
category: z.enum(['framework', 'testing', 'orm', 'ui', 'all']).default('all'),
limit: z.number().min(1).max(20).default(10),
}),
execute: async ({ query, category, limit }) => {
return searchPackages(query, { category, limit });
},
});
// Add resources:
mcp.addResource({
uri: 'pkgpulse://trending',
name: 'Trending Packages',
description: 'This week\'s top trending npm packages',
mimeType: 'application/json',
load: async () => {
const trending = await getTrendingPackages();
return JSON.stringify(trending, null, 2);
},
});
// Start (stdio for local tools, HTTP for remote):
mcp.run({ transport: 'stdio' });
// or: mcp.run({ transport: 'sse', port: 3001 });
Connecting MCP to Claude Desktop
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"pkgpulse": {
"command": "node",
"args": ["/path/to/pkgpulse-mcp/dist/index.js"],
"env": {
"DATABASE_URL": "postgresql://..."
}
}
}
}
Now Claude Desktop can call your tools directly: "Compare React vs Vue download trends" → Claude calls compare_packages → returns live data.
MCP vs Tool Calling: When to Use Each
Use Tool Calling (Vercel AI SDK / OpenAI function calling) when:
→ Building app-specific features
→ Tools are only needed in your app
→ Simple, stateless tool execution
→ Don't need cross-client reuse
Use MCP when:
→ Want tools available in Claude Desktop, Cursor, etc.
→ Building a data connector (database, API) for AI
→ Team uses multiple AI clients and wants shared tools
→ Building an AI-native product that exposes data
→ Creating developer tools with AI integration
Explore MCP libraries and AI SDK download trends on PkgPulse.