es:** Errors should be returned as structured text content rather than thrown exceptions, providing the agent with actionable feedback to correct its approach.
Implementation Guide
The following example demonstrates a TypeScript MCP server for an inventory management system. This implementation includes schema validation, idempotency checks, rate limiting, and structured error handling.
Step 1: Project Setup
Initialize the project and install the MCP SDK and validation dependencies.
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Step 2: Define the Server and Tools
Create inventory-server.ts. This server exposes tools for adjusting stock levels and retrieving product details.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Initialize the MCP server instance
const server = new McpServer({
name: "inventory-service",
version: "1.0.0",
});
// Tool 1: Adjust Stock Level
// Requires an idempotency key to prevent duplicate adjustments on retry.
server.tool(
"adjust_stock_level",
"Modifies the inventory count for a specific SKU. Returns the new stock level.",
{
sku: z.string().regex(/^[A-Z0-9]{6,12}$/, "SKU must be 6-12 alphanumeric characters"),
adjustment: z.number().int().min(-1000).max(1000),
idempotency_key: z.string().uuid("Required to prevent duplicate operations"),
},
async ({ sku, adjustment, idempotency_key }) => {
// Simulate idempotency check against a database
const processed = await checkIdempotency(idempotency_key);
if (processed) {
return {
content: [
{
type: "text",
text: `Operation already processed. Current stock for ${sku} is ${processed.current_stock}.`,
},
],
};
}
try {
// Simulate database update
const newStock = await updateDatabase(sku, adjustment);
await recordIdempotency(idempotency_key, { current_stock: newStock });
return {
content: [
{
type: "text",
text: `Stock updated successfully. SKU: ${sku}, New Level: ${newStock}.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Database error while updating ${sku}. Please retry with a new idempotency key.`,
},
],
isError: true,
};
}
}
);
// Tool 2: Get Product Details
// Demonstrates handling of external API calls with timeout management.
server.tool(
"get_product_details",
"Retrieves metadata for a product based on its SKU.",
{
sku: z.string().regex(/^[A-Z0-9]{6,12}$/),
},
async ({ sku }) => {
try {
const details = await fetchProductMetadata(sku);
return {
content: [
{
type: "text",
text: JSON.stringify(details, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to fetch details for ${sku}. The product catalog service may be unavailable.`,
},
],
isError: true,
};
}
}
);
// Helper functions simulating backend logic
async function checkIdempotency(key: string) {
// In production, query your database for the key
return null;
}
async function recordIdempotency(key: string, data: any) {
// In production, insert into idempotency table
}
async function updateDatabase(sku: string, adjustment: number) {
// Simulate DB latency
await new Promise((resolve) => setTimeout(resolve, 50));
return Math.floor(Math.random() * 100);
}
async function fetchProductMetadata(sku: string) {
// Simulate external API call
return { sku, name: `Product ${sku}`, category: "Electronics" };
}
// Start the server using stdio transport
async function main() {
const transport = new StdioTransport();
await server.connect(transport);
console.error("Inventory MCP server running on stdio");
}
main().catch(console.error);
Step 3: Run the Server
Execute the server to begin listening for JSON-RPC requests.
npx ts-node inventory-server.ts
The server now accepts tool calls via standard input/output, validating inputs against the Zod schemas and returning structured responses.
Pitfall Guide
Building reliable MCP servers requires anticipating failure modes specific to AI agent interactions. The following pitfalls are common in production environments.
Explanation: LLMs frequently hallucinate parameter formats, such as sending "tomorrow" instead of an ISO date, or "two" instead of the integer 2. Relying solely on the schema description is insufficient.
Fix: Implement strict runtime validation using libraries like Zod. Return specific, actionable error messages that guide the agent to correct the input format.
Explanation: Agents may retry tool calls due to timeouts or ambiguous responses. Without idempotency, a retry can trigger duplicate side effects, such as double-charging a user or creating duplicate records.
Fix: Mandate an idempotency_key parameter for all write operations. The server must check this key against a persistent store before executing the action and return the previous result if the key has already been processed.
3. Unbounded Rate Limiting
Explanation: Agents can generate bursts of tool calls, potentially exhausting API quotas or overloading backend services. Standard rate limiters may not account for the bursty nature of agent loops.
Fix: Implement token bucket or sliding window rate limiters within the MCP server. Return a structured error indicating rate limit status, allowing the agent to back off gracefully.
4. Context Window Overflow
Explanation: Returning large payloads from tools can consume significant context window space, increasing costs and degrading model performance.
Fix: Truncate tool responses to essential information. Implement pagination for list operations and summarize data where possible. Monitor response sizes and enforce limits.
5. Blocking the Event Loop
Explanation: In asynchronous environments like Node.js, performing synchronous I/O or heavy computation can block the event loop, causing the server to become unresponsive to other requests.
Fix: Ensure all I/O operations are non-blocking. Offload CPU-intensive tasks to worker threads or external services. Use async/await patterns consistently.
6. Schema Drift Between Client and Server
Explanation: If the server updates a tool's schema without notifying the client, the agent may continue sending outdated parameters, leading to validation failures.
Fix: Version your tools and implement dynamic schema updates. Use the MCP tools/list endpoint to allow clients to refresh available tools and schemas periodically.
7. Inadequate Error Context
Explanation: Generic error messages like "Internal Server Error" provide no value to the agent, causing it to repeat the same failed action or abandon the task.
Fix: Return detailed error messages that explain the cause of the failure and suggest corrective actions. Use the isError flag in the response to clearly indicate failure states.
Production Bundle
Action Checklist
Decision Matrix
Selecting the appropriate transport and deployment strategy depends on the operational requirements.
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Local Development | Stdio Transport | Simple setup, no network overhead, easy debugging. | Low |
| Remote Agent Access | SSE over HTTP | Enables network communication, supports multiple clients. | Medium (Infrastructure) |
| High-Volume Tools | Dedicated Server + Rate Limiting | Isolates tool logic, prevents resource exhaustion. | Medium (Compute) |
| Sensitive Data | Local Server + Stdio | Keeps data within the agent's environment, reduces exposure. | Low |
Configuration Template
Use this template to bootstrap a production-ready MCP server configuration with rate limiting and logging.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Rate limiter configuration
const RATE_LIMIT = { maxCalls: 30, windowMs: 60000 };
const callCounts = new Map<string, { count: number; resetAt: number }>();
function checkRateLimit(toolName: string): boolean {
const now = Date.now();
const entry = callCounts.get(toolName);
if (!entry || now > entry.resetAt) {
callCounts.set(toolName, { count: 1, resetAt: now + RATE_LIMIT.windowMs });
return true;
}
if (entry.count >= RATE_LIMIT.maxCalls) {
return false;
}
entry.count++;
return true;
}
const server = new McpServer({ name: "production-tool-server", version: "1.0.0" });
server.tool(
"process_order",
"Processes a new order. Requires idempotency key.",
{
order_id: z.string().uuid(),
items: z.array(z.object({ sku: z.string(), qty: z.number() })),
idempotency_key: z.string().uuid(),
},
async ({ order_id, items, idempotency_key }) => {
if (!checkRateLimit("process_order")) {
return {
content: [{ type: "text", text: "Rate limit exceeded. Please retry later." }],
isError: true,
};
}
// Business logic here...
return {
content: [{ type: "text", text: `Order ${order_id} processed successfully.` }],
};
}
);
export { server };
Quick Start Guide
- Initialize Project: Run
npm init -y and install @modelcontextprotocol/sdk and zod.
- Create Server: Define your MCP server instance and register tools with Zod schemas.
- Implement Logic: Add handlers for each tool, including validation, idempotency checks, and error handling.
- Configure Transport: Choose
StdioTransport for local use or SSETransport for remote access.
- Deploy: Run the server and connect your MCP-compatible agent client to begin tool interactions.