ol = toolSchema.extend({
name: "warehouse/inventory_check",
description: "Retrieve stock levels and reorder thresholds for a given SKU",
inputSchema: z.object({
sku: z.string().min(3).max(20).describe("Stock keeping unit identifier"),
location_code: z.string().optional().describe("Optional warehouse zone filter"),
include_reserved: z.boolean().default(false).describe("Include allocated but unshipped stock")
})
});
### Step 2: Implement the MCP Server
The server registers tools, handles incoming requests, and manages transport. We use `stdio` for local development and `sse` for networked deployments.
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSETransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { InventoryLookupTool } from "./tools/inventory.js";
import { InventoryService } from "../services/inventory.js";
export class WarehouseMcpServer {
private server: McpServer;
private inventorySvc: InventoryService;
constructor() {
this.server = new McpServer({
name: "warehouse-tool-server",
version: "1.2.0"
});
this.inventorySvc = new InventoryService(process.env.DB_CONNECTION_STRING!);
this.registerTools();
}
private registerTools(): void {
this.server.tool(
InventoryLookupTool.name,
InventoryLookupTool.description,
InventoryLookupTool.inputSchema.shape,
async (params) => {
try {
const result = await this.inventorySvc.getStock(params.sku, {
location: params.location_code,
includeReserved: params.include_reserved
});
return {
content: [{ type: "text", text: JSON.stringify(result) }],
isError: false
};
} catch (err) {
return {
content: [{ type: "text", text: `Inventory lookup failed: ${err.message}` }],
isError: true
};
}
}
);
}
async start(transportType: "stdio" | "sse"): Promise<void> {
const transport = transportType === "stdio"
? new StdioTransport()
: new SSETransport({ port: 3001, host: "0.0.0.0" });
await this.server.connect(transport);
console.log(`[MCP] Server listening via ${transportType}`);
}
}
Step 3: Build the Client Adapter
Clients should never assume tool availability. They must query capabilities, validate schemas, and handle transport failures gracefully.
import { McpClient } from "@modelcontextprotocol/sdk/client/mcp.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export class LlmToolClient {
private client: McpClient;
constructor(serverCommand: string[]) {
const transport = new StdioClientTransport({
command: serverCommand[0],
args: serverCommand.slice(1)
});
this.client = new McpClient({ name: "llm-consumer", version: "1.0.0" });
this.client.connect(transport);
}
async discoverTools(): Promise<string[]> {
const capabilities = await this.client.listTools();
return capabilities.tools.map(t => t.name);
}
async invokeInventoryCheck(sku: string, options?: { location?: string; includeReserved?: boolean }) {
const available = await this.discoverTools();
if (!available.includes("warehouse/inventory_check")) {
throw new Error("Required tool not exposed by server");
}
const response = await this.client.callTool("warehouse/inventory_check", {
sku,
location_code: options?.location,
include_reserved: options?.includeReserved ?? false
});
if (response.isError) {
throw new Error(`Tool execution failed: ${response.content[0].text}`);
}
return JSON.parse(response.content[0].text);
}
}
Architecture Decisions & Rationale
- Transport Separation:
stdio is used for local/CI environments where latency and process isolation are prioritized. sse enables remote deployment and multi-client access. Choosing the wrong transport for your deployment topology introduces unnecessary network overhead or limits scalability.
- Explicit Error Mapping: MCP returns structured error objects. Mapping these to typed exceptions at the client layer prevents silent failures and enables retry logic in agent frameworks.
- Schema-First Validation: Using Zod to define input schemas ensures that malformed requests are rejected before hitting business logic. This reduces runtime exceptions and improves observability.
- Capability Discovery: Clients must query
tools/list before invocation. Hardcoding tool names creates brittle integrations that break during server updates.
Pitfall Guide
1. Treating MCP as an Orchestration Layer
Explanation: Teams expect MCP to chain tools, manage state, or handle multi-step reasoning. It does none of these. MCP is a transport and discovery protocol.
Fix: Use dedicated agent frameworks (LangGraph, CrewAI, or custom state machines) for orchestration. Keep MCP servers stateless and focused on single-tool execution.
2. Ignoring Transport Latency in Real-Time Loops
Explanation: JSON-RPC serialization and transport handshakes add 2-15ms per call. In voice agents, trading systems, or real-time control loops, this accumulates and breaches p99 SLAs.
Fix: Benchmark your latency budget. If sub-50ms round trips are required, bypass MCP and use direct function calls. Reserve MCP for batch, async, or human-in-the-loop workflows.
3. Schema Drift Without Versioning
Explanation: Tool parameters evolve. Clients built against v1 schemas break when v2 removes or renames fields. MCP does not enforce backward compatibility automatically.
Fix: Version tool names (warehouse/inventory_check/v1) and maintain deprecated schemas alongside new ones. Use JSON Schema additionalProperties: false to catch unexpected inputs early.
4. Over-Provisioning for Single-Consumer Apps
Explanation: Adding MCP to a monorepo where one app calls two internal tools introduces network calls, process management, and deployment complexity for zero portability gain.
Fix: Keep direct function calls until you have at least two distinct LLM clients or cross-team tool ownership. Migrate only when the N×M complexity curve becomes unsustainable.
5. Mishandling Authentication Boundaries
Explanation: MCP servers often run as separate processes. Hardcoding credentials or sharing parent process tokens violates least-privilege principles and creates audit gaps.
Fix: Inject scoped credentials via environment variables or secret managers. Implement token refresh logic inside the server. Never pass raw secrets through MCP payloads.
6. Silent Failure Propagation
Explanation: JSON-RPC errors can be swallowed by client libraries if not explicitly checked. This leads to models receiving empty responses and hallucinating fallback behavior.
Fix: Validate isError flags on every response. Implement exponential backoff for transient transport failures. Log structured error codes for downstream monitoring.
7. Skipping Health & Capability Discovery
Explanation: Clients assume tools exist and remain stable. Servers crash, networks partition, and capabilities change. Without health checks, integrations fail silently.
Fix: Implement ping or health endpoints. Validate tool availability at client startup. Use circuit breakers to prevent cascading failures when servers become unresponsive.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single app, 1-2 internal tools | Direct Function Calling | No multi-consumer demand; MCP adds unnecessary transport overhead | Lower infrastructure cost, faster iteration |
| Multiple LLM clients (Desktop, Code, API) | MCP Architecture | Linearizes integration complexity; shared contract reduces duplication | Higher initial setup, lower long-term maintenance |
| Ecosystem data sources (GitHub, Postgres, Slack) | Pre-built MCP Servers | Avoids reinventing authentication, rate limiting, and schema mapping | Minimal dev cost, predictable vendor SLAs |
| Real-time voice/trading loops | Direct Function Calling | Transport hop breaches sub-50ms latency budgets | Higher performance, tighter coupling |
| Cross-team tool ownership | MCP Architecture | Decouples schema, auth, and error handling from app teams | Clear boundaries, reduced cross-team friction |
Configuration Template
{
"mcp": {
"servers": {
"warehouse-inventory": {
"command": "node",
"args": ["./dist/warehouse-server.js"],
"env": {
"DB_CONNECTION_STRING": "${DB_CONNECTION_STRING}",
"MCP_LOG_LEVEL": "info",
"TRANSPORT_TYPE": "stdio"
},
"healthCheck": {
"endpoint": "/health",
"interval": 30,
"timeout": 5
},
"retryPolicy": {
"maxAttempts": 3,
"backoffMs": 1000,
"backoffMultiplier": 2
}
}
},
"client": {
"timeoutMs": 5000,
"schemaValidation": true,
"errorMapping": "strict"
}
}
}
Quick Start Guide
- Initialize the project: Run
npm init -y && npm install @modelcontextprotocol/sdk zod to install the official SDK and schema validation library.
- Create the server file: Copy the
WarehouseMcpServer implementation into src/server.ts. Replace the mock service with your actual data source or API client.
- Start the transport: Execute
node src/server.ts to launch the server over stdio. Verify it responds to tools/list requests using the MCP Inspector CLI.
- Connect a client: Instantiate
LlmToolClient in your application, call discoverTools(), and invoke invokeInventoryCheck() with valid parameters. Monitor logs for schema validation and transport latency.
- Deploy to production: Switch
TRANSPORT_TYPE to sse, configure environment secrets, and expose the server behind a reverse proxy with TLS termination. Add health checks and circuit breakers before routing live traffic.