How to Build MCP Servers in Node.js for AI Agents (2026 Guide)
The way AI agents interact with APIs is fundamentally changing. In 2026, the Model Context Protocol (MCP) has become the standard for exposing APIs to AI models and autonomous agents. This guide shows you how to build production-ready MCP servers in Node.js that any AI agent can consume directly.
What is MCP and Why It Matters in 2026
The Model Context Protocol (MCP) is an open standard that enables AI models to discover and interact with external tools and services through a standardized interface. Think of it as a universal adapter that lets AI agents call your API functions without needing custom integrations.
According to the Postman State of the API Report, 82% of organizations have adopted API-first approaches, and 2026 marks the year where APIs are no longer just for developers—they're directly consumable by AI agents.
Why MCP is Essential for API Developers
Traditional REST APIs require developers to write custom integration code for each AI model. MCP changes this by providing:
- Standardized Tool Definitions: AI agents automatically discover available functions
- Type-Safe Interfaces: Strong typing through TypeScript type hints and docstrings
- Universal Compatibility: Any MCP-compatible client can consume your API
- Reduced Integration Work: One endpoint serves all AI agents
Setting Up Your MCP Server Project
Let's build a complete MCP server for a sample API—in this case, a weather API that AI agents can query.
Prerequisites
- Node.js 20+ (or Bun/Deno)
- npm or yarn
- Basic TypeScript knowledge
Initialize the Project
mkdir weather-mcp-server && cd weather-mcp-server
npm init -y
npm install @modelcontextprotocol/server @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
Create the MCP Server
Create a file called src/server.ts:
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';
// Define tool input schemas using Zod
const GetWeatherInput = z.object({
location: z.string().describe('City name or coordinates'),
units: z.enum(['celsius', 'fahrenheit']).optional().default('celsius'),
});
const GetForecastInput = z.object({
location: z.string(),
days: z.number().min(1).max(7).optional().default(3),
});
// Mock weather data (replace with real API calls)
const weatherData: Record<string, any> = {
'new york': { temp: 18, condition: 'Partly Cloudy', humidity: 65 },
'london': { temp: 12, condition: 'Rainy', humidity: 82 },
'tokyo': { temp: 22, condition: 'Sunny', humidity: 55 },
'saigon': { temp: 32, condition: 'Hot', humidity: 78 },
};
class WeatherMCPServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: 'weather-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_current_weather',
description: 'Get current weather for a specific location',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'City name (e.g., "New York", "London", "Tokyo")',
},
units: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Temperature units',
default: 'celsius',
},
},
required: ['location'],
},
},
{
name: 'get_forecast',
description: 'Get weather forecast for multiple days',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'City name',
},
days: {
type: 'number',
description: 'Number of days (1-7)',
default: 3,
},
},
required: ['location'],
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_current_weather':
return await this.getCurrentWeather(args);
case 'get_forecast':
return await this.getForecast(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
});
}
private async getCurrentWeather(args: unknown) {
const input = GetWeatherInput.parse(args);
const locationLower = input.location.toLowerCase();
const weather = weatherData[locationLower];
if (!weather) {
return {
content: [
{
type: 'text',
text: `Weather data not found for "${input.location}". Try: New York, London, Tokyo, or Saigon.`,
},
],
};
}
const temp = input.units === 'fahrenheit'
? (weather.temp * 9/5) + 32
: weather.temp;
const unitSymbol = input.units === 'fahrenheit' ? '°F' : '°C';
return {
content: [
{
type: 'text',
text: JSON.stringify({
location: input.location,
temperature: `${temp}${unitSymbol}`,
condition: weather.condition,
humidity: `${weather.humidity}%`,
}, null, 2),
},
],
};
}
private async getForecast(args: unknown) {
const input = GetForecastInput.parse(args);
const locationLower = input.location.toLowerCase();
const weather = weatherData[locationLower];
if (!weather) {
return {
content: [
{
type: 'text',
text: `Location "${input.location}" not found.`,
},
],
};
}
// Generate mock forecast
const forecast = [];
for (let i = 0; i < input.days; i++) {
const dayTemp = weather.temp + Math.floor(Math.random() * 6) - 3;
forecast.push({
day: i + 1,
temp: `${dayTemp}°C`,
condition: weather.condition,
});
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
location: input.location,
forecast,
}, null, 2),
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Weather MCP Server running on stdio');
}
}
const server = new WeatherMCPServer();
server.run().catch(console.error);
Configure Package.json
{
"name": "weather-mcp-server",
"type": "module",
"scripts": {
"start": "tsx src/server.ts",
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/server": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
Add MCP Configuration
Create mcp.json for clients to discover your server:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/path/to/weather-mcp-server/dist/server.js"],
"env": {}
}
}
}
Testing Your MCP Server
Using the MCP Inspector
npx @modelcontextprotocol/inspector node dist/server.js
This opens a web interface where you can test your tools interactively.
Testing with a Simple Client
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
async function test() {
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/server.js'],
});
const client = new Client(
{ name: 'test-client', version: '1.0.0' },
{ capabilities: {} }
);
await client.connect(transport);
// List available tools
const tools = await client.request(
{ method: 'tools/list' },
{ method: 'tools/list', params: {} }
);
console.log('Available tools:', tools);
// Call a tool
const result = await client.request(
{ method: 'tools/call' },
{
method: 'tools/call',
params: {
name: 'get_current_weather',
arguments: { location: 'Tokyo', units: 'celsius' },
},
}
);
console.log('Weather result:', result);
}
test();
Exposing Your MCP Server Over HTTP
While MCP traditionally uses stdio, you can expose it over HTTP for remote AI agents:
import express from 'express';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { HttpServerTransport } from '@modelcontextprotocol/sdk/server/http.js';
const app = express();
app.use(express.json());
const mcpServer = new Server({ name: 'api-mcp', version: '1.0.0' }, {
capabilities: { tools: {} },
});
// ... define tools as shown above
app.post('/mcp', async (req, res) => {
const transport = new HttpServerTransport(req, res);
await mcpServer.connect(transport);
});
app.listen(3000, () => {
console.log('MCP HTTP Server running on port 3000');
});
Best Practices for Production MCP Servers
1. Use Type-Safe Tool Definitions
Always define input schemas with Zod or TypeScript for compile-time validation:
const CreateUserInput = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'guest']).default('user'),
});
2. Implement Proper Error Handling
Return structured errors that AI agents can understand:
try {
// Your tool logic
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true,
};
}
3. Add Comprehensive Descriptions
The description field is critical—AI agents use it to understand when to call your tools:
{
name: 'create_order',
description: 'Create a new order. Use this when a customer wants to purchase products. Requires authenticated session.',
inputSchema: {
type: 'object',
properties: {
items: {
type: 'array',
description: 'Array of {productId, quantity} objects'
},
shippingAddress: {
type: 'string',
description: 'Full shipping address including city and postal code',
},
},
required: ['items', 'shippingAddress'],
},
}
4. Secure Your MCP Endpoints
Add authentication for production servers:
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token || !validateToken(token)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
app.post('/mcp', authMiddleware, async (req, res) => {
// Handle MCP requests
});
Connecting MCP to 1xAPI
You can expose your 1xAPI endpoints as MCP tools, making them accessible to AI agents:
import fetch from 'node-fetch';
const TranslateTool = {
name: 'translate_text',
description: 'Translate text between languages using 1xAPI',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text to translate' },
from: { type: 'string', description: 'Source language code (e.g., en, my)' },
to: { type: 'string', description: 'Target language code' },
},
required: ['text', 'from', 'to'],
},
};
async function translate(args: any) {
const response = await fetch('https://api.1xapi.com/translate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.1XAPI_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
const result = await response.json();
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
}
Conclusion
The Model Context Protocol is transforming how AI agents interact with APIs. By exposing your 1xAPI as MCP servers, you enable AI agents to consume your services natively—without custom integration code for each agent.
Key takeaways:
- MCP provides a standardized way for AI agents to discover and call your API functions
- Use Zod for type-safe input validation
- Add comprehensive descriptions to help AI agents understand when to use your tools
- Implement proper authentication for production deployments
- Consider HTTP transport for remote AI agent access
As AI agents become more prevalent, MCP will likely become as essential as REST APIs were for web services. Start building your MCP servers today to stay ahead of this shift.
Related Links:
Top comments (0)