kflows). For 90% of API integrations, tools alone satisfy the requirement. The following implementation demonstrates a production-ready TypeScript server that wraps a hypothetical inventory management REST API.
Architecture Decisions & Rationale
- Intent-shaped tool grouping: Instead of mirroring every REST endpoint, we group related operations into semantic actions. This prevents agent paralysis and aligns with how models reason about tasks.
- Strict JSON Schema validation: MCP relies on OpenAPI-style schemas for parameter definition. We enforce type safety, required fields, and explicit enums to prevent malformed calls.
- Server-side auth isolation: API tokens, OAuth flows, and signing logic remain in the server environment. The model never handles secrets.
- Normalized error contracts: Failures return structured messages with actionable guidance, enabling automatic retry or fallback strategies.
- Stateless execution model: Each tool call is independent. Pagination and cursors are handled internally, returning truncated, context-safe payloads.
Implementation (TypeScript)
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
const API_BASE = process.env.INVENTORY_API_BASE ?? "https://api.internal.example.com/v2";
const API_TOKEN = process.env.INVENTORY_API_TOKEN ?? "";
const server = new Server(
{ name: "inventory-mcp-bridge", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Define tools with strict schemas
const TOOLS: Tool[] = [
{
name: "search_inventory",
description: "Query stock levels by keyword, category, or SKU. Returns up to 20 matching items. Use the 'next_cursor' field for pagination.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search term for name, SKU, or category" },
category: { type: "string", enum: ["electronics", "apparel", "hardware"], description: "Filter by product category" },
limit: { type: "integer", minimum: 1, maximum: 20, default: 20, description: "Max results to return" },
cursor: { type: "string", description: "Pagination token from previous response" },
},
required: ["query"],
},
},
{
name: "get_product_spec",
description: "Retrieve detailed specifications for a single product. Includes dimensions, weight, warranty terms, and supplier data.",
inputSchema: {
type: "object",
properties: {
product_id: { type: "string", description: "Unique product identifier, e.g., 'prod_8f3a2c'" },
},
required: ["product_id"],
},
},
{
name: "adjust_stock_level",
description: "Increment or decrement inventory count for a specific SKU. Requires explicit reason for audit logging. Returns updated balance.",
inputSchema: {
type: "object",
properties: {
sku: { type: "string", description: "Stock keeping unit identifier" },
delta: { type: "integer", description: "Positive to add, negative to remove" },
reason: { type: "string", description: "Business justification for adjustment" },
},
required: ["sku", "delta", "reason"],
},
},
];
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "search_inventory": {
const params = { q: args.query, cat: args.category, limit: args.limit, cursor: args.cursor };
const res = await axios.get(`${API_BASE}/products/search`, {
headers: { Authorization: `Bearer ${API_TOKEN}` },
params,
timeout: 8000,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
case "get_product_spec": {
const res = await axios.get(`${API_BASE}/products/${args.product_id}/specs`, {
headers: { Authorization: `Bearer ${API_TOKEN}` },
timeout: 5000,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
case "adjust_stock_level": {
const payload = { sku: args.sku, delta: args.delta, reason: args.reason };
const res = await axios.post(`${API_BASE}/inventory/adjust`, payload, {
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
timeout: 6000,
});
return {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
// Normalize errors for agent consumption
const status = error.response?.status ?? 500;
const message = error.response?.data?.message ?? error.message;
const guidance = status === 404
? "Verify the identifier format. Product IDs follow 'prod_' prefix."
: status === 429
? "Rate limit exceeded. Wait 30 seconds before retrying."
: "Contact platform team if issue persists.";
return {
content: [{ type: "text", text: `Error ${status}: ${message}. ${guidance}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Inventory MCP server running on stdio");
}
main().catch(console.error);
Why these choices matter:
- The
@modelcontextprotocol/sdk handles protocol serialization, JSON-RPC framing, and stdio communication. You focus on business logic.
- Tool schemas use explicit
enum and minimum/maximum constraints. Models respect these boundaries better than freeform text.
- Error handling returns
isError: true with actionable guidance. This enables the agent to adjust parameters or pause execution rather than hallucinating a fix.
- Timeouts are set per-tool based on expected latency. Search operations get 8s, spec fetches get 5s, writes get 6s. This prevents context window exhaustion from hanging requests.
- Authentication is injected at the HTTP layer, never exposed to the model. The server acts as a secure proxy.
Pitfall Guide
Explanation: Creating one MCP tool per REST endpoint quickly balloons to 100+ tools. Models struggle with selection accuracy beyond 20-25 options, leading to paralysis or incorrect routing.
Fix: Group by intent. Replace GET /users, GET /users/:id, GET /users?email=, and GET /users?role= with a single find_users(query, filters) tool.
2. Marketing-Style Descriptions
Explanation: Descriptions like "Advanced inventory management" provide zero operational signal. Models rely on parameter docs to decide when and how to call a tool.
Fix: Write execution-focused descriptions. Specify limits, pagination behavior, required formats, and known edge cases. Example: "Returns max 20 items. Use 'next_cursor' for subsequent pages. Fails if query length < 3."
3. Opaque Error Responses
Explanation: Returning raw HTTP status codes or generic JSON like {"error": "bad_request"} forces the model to guess corrections. Retry loops become unstable.
Fix: Normalize errors into human-readable, agent-actionable messages. Include expected formats, rate limit windows, and fallback suggestions. Always set isError: true.
4. Exposing Destructive Operations Blindly
Explanation: Tools like DELETE /accounts or POST /transfers executed by autonomous agents can cause irreversible damage. Models may trigger them during exploratory queries.
Fix: Default to read-only exposure. Wrap destructive operations in confirmation gates, require explicit reason fields, or route them through human-in-the-loop approval workflows.
5. Ignoring Context Window Limits
Explanation: Returning full database rows or untruncated lists floods the agent's context. Subsequent reasoning degrades, and token costs spike.
Fix: Enforce strict payload limits. Return summary fields, truncate long text, and use cursor-based pagination. Document limits explicitly in tool descriptions.
6. Hardcoding Secrets in Source
Explanation: Embedding API keys or tokens directly in the server code creates version control leaks and deployment risks.
Fix: Use runtime environment variables. Validate presence at startup. Rotate credentials via CI/CD pipelines. Never log or echo secrets in responses.
7. Overcomplicating with Prompts & Resources
Explanation: Teams often overuse MCP prompts and resources early, adding complexity without measurable agent benefit. Most workflows only need tools.
Fix: Start with tools. Add resources only for static reference data (pricing tables, changelogs). Use prompts sparingly for multi-step templates that agents cannot reliably chain themselves.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal dev tooling | Read-only MCP server with 10-15 intent tools | Fast iteration, low risk, immediate agent productivity gains | Low (single server, minimal infra) |
| Public agent marketplace | Curated tool set with rate limiting & usage telemetry | Prevents abuse, enables billing, maintains SLA compliance | Medium (monitoring, auth gateway, scaling) |
| High-security finance/healthcare | Strict read-only exposure + human approval gates for writes | Compliance requirements, audit trails, zero-trust auth | High (HSM integration, approval workflows, logging) |
| Rapid prototyping / MVP | Single-file MCP server with stdio transport | Zero deployment overhead, immediate testing in local AI editors | Low (dev-only, no production hardening) |
Configuration Template
{
"mcpServers": {
"inventory-bridge": {
"command": "node",
"args": ["dist/mcp-server.js"],
"env": {
"INVENTORY_API_BASE": "https://api.internal.example.com/v2",
"INVENTORY_API_TOKEN": "${VAULT_INVENTORY_TOKEN}",
"NODE_ENV": "production"
},
"disabled": false,
"autoApprove": []
}
}
}
Note: Use ${VAULT_...} syntax with your secret manager. Never commit plaintext tokens. The autoApprove array controls which tools bypass confirmation prompts in supported clients.
Quick Start Guide
- Initialize the project: Run
npm init -y && npm install @modelcontextprotocol/sdk axios typescript @types/node. Create tsconfig.json with module: commonjs and target: es2020.
- Write the server: Copy the TypeScript implementation above into
src/mcp-server.ts. Replace API_BASE and API_TOKEN with your environment variables.
- Build and test: Run
npx tsc, then execute node dist/mcp-server.ts in a terminal. Verify it outputs Inventory MCP server running on stdio without crashing.
- Connect to an AI client: Open your MCP configuration file (e.g.,
claude_desktop_config.json or Cursor's MCP settings). Paste the JSON template from the Configuration Template section. Restart the client.
- Validate with smoke tests: Ask the agent to perform three tasks: search for a known SKU, fetch product specifications, and attempt an invalid ID. Confirm responses match expected schemas and error guidance.
Shipping an MCP endpoint transforms your API from a reference document into an executable interface. The protocol handles the plumbing; you control the semantics. Teams that treat agent consumption as a first-class architectural concern will see measurable gains in workflow reliability, security posture, and developer velocity.