Model Context Protocol (MCP) is Anthropic's open standard for connecting AI models to external tools and data sources. Think of it as a USB-C standard for AI integrations — instead of every AI assistant implementing its own custom plugin system, MCP gives you one protocol that any compliant client (Claude Desktop, Cursor, Continue, and others) can speak. I have been building MCP servers for internal tools at Commsult Indonesia since early 2025, and the productivity gain from exposing our ERP data to Claude through a proper MCP server has been significant. This guide covers the full lifecycle: server setup, tool definition, authentication, error handling, and deployment.
MCP defines a client-server protocol over stdio or HTTP/SSE. The AI model (client) discovers available tools and resources from your MCP server, then calls those tools during conversations. Your server handles tool execution and returns results. The protocol is JSON-RPC 2.0 based, stateful (the client maintains a session with the server), and supports three primitives: Tools (executable functions), Resources (readable data), and Prompts (reusable prompt templates). For most use cases, you will primarily implement Tools — these are what enable Claude to take actions in your systems.
Anthropic provides official SDKs for TypeScript and Python. The TypeScript SDK is the most mature and what I use in production. Install with npm install @modelcontextprotocol/sdk. The SDK handles protocol negotiation, session management, and serialization. You focus on defining your tools and implementing their handlers. The server can run as a standalone process communicating over stdio (for Claude Desktop integration) or as an HTTP server with SSE for remote deployments.
┌─────────────────────────────────────────────────────────────┐
│ MCP Architecture Flow │
│ │
│ Claude Desktop / Cursor / Continue (MCP Client) │
│ │ │
│ │ JSON-RPC 2.0 over stdio or HTTP/SSE │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Your MCP Server │ │
│ │ │ │
│ │ tools/list → returns Tool[] │ │
│ │ tools/call → executes handler │ │
│ │ resources/* → returns data │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Your Systems: Database / APIs / File System │
└─────────────────────────────────────────────────────────────┘From my experience building MCP servers for ERP integrations: define your tool schemas strictly using Zod validation. The AI model relies on your JSON Schema descriptions to understand what parameters to pass. Vague descriptions lead to incorrect tool calls. Write descriptions as if explaining to a smart junior developer who has never seen your system — be specific about data formats, required fields, and expected value ranges.
A complete MCP server with a real tool that queries an ERP database. This example implements a tool to look up invoice status — a common ERP integration use case that demonstrates authentication, database queries, and structured response formatting.
Each tool needs a name, description, input schema (JSON Schema), and a handler function. The handler receives validated inputs and returns a content array. Return structured data as JSON strings inside a text content block — this gives the AI model machine-readable data while maintaining protocol compatibility. Always include error handling: return error content rather than throwing exceptions so the model can understand what went wrong and communicate it to the user.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { z } from "zod"
const server = new McpServer({
name: "erp-mcp-server",
version: "1.0.0",
})
// Define a tool with Zod schema validation
server.tool(
"get_invoice_status",
"Look up the status of an invoice by ID or invoice number",
{
invoice_id: z.string().optional().describe("UUID of the invoice"),
invoice_number: z.string().optional().describe("Human-readable invoice number like INV-2025-001"),
include_line_items: z.boolean().default(false).describe("Whether to include line item details"),
},
async ({ invoice_id, invoice_number, include_line_items }) => {
if (!invoice_id && !invoice_number) {
return {
content: [{ type: "text", text: JSON.stringify({ error: "Either invoice_id or invoice_number is required" }) }],
isError: true,
}
}
try {
const invoice = await db.invoice.findFirst({
where: invoice_id ? { id: invoice_id } : { invoiceNumber: invoice_number },
include: { lineItems: include_line_items, client: true },
})
if (!invoice) {
return {
content: [{ type: "text", text: JSON.stringify({ error: "Invoice not found" }) }],
isError: true,
}
}
return {
content: [{ type: "text", text: JSON.stringify(invoice) }],
}
} catch (err) {
return {
content: [{ type: "text", text: JSON.stringify({ error: "Database error", retry_suggested: true }) }],
isError: true,
}
}
}
)
// Start server with stdio transport (for Claude Desktop)
const transport = new StdioServerTransport()
await server.connect(transport)Remote MCP servers must implement authentication. The MCP spec recommends OAuth 2.0 with PKCE for client authentication. For internal tools, a simpler approach is bearer token authentication validated per request. I implement a middleware layer that extracts the Authorization header, validates the token against our user database, and attaches user context to the request before tool handlers execute. This ensures tool calls are scoped to the authenticated user's permissions — critical for ERP data where different users can see different records.
MCP servers running locally via stdio have access to your full user account permissions. A malicious MCP server (or a legitimate server that gets compromised through indirect prompt injection) can read files, execute commands, and make network requests as your user. Always review MCP server code before installing it. For production remote MCP servers, implement authentication (OAuth 2.0 is the MCP spec recommendation), enforce HTTPS, and apply the principle of least privilege — the server should only have access to the specific resources its tools need.
For Claude Desktop integration, your MCP server runs as a local stdio process — configure it in claude_desktop_config.json. For team or multi-client deployments, run your server as an HTTP+SSE endpoint behind nginx with TLS. Use PM2 or systemd for process management. Implement health checks, structured logging with request IDs, and rate limiting per authenticated user. Monitor tool call latency — if a tool consistently takes more than 5 seconds, the user experience degrades significantly. Cache expensive lookups (database queries, API calls) with appropriate TTLs.
At Commsult Indonesia, our production MCP server exposes tools for: querying sales orders and invoices, checking inventory levels, running approval status queries, and generating report summaries. The server runs behind an nginx reverse proxy with Let's Encrypt TLS, authenticated via JWT tokens tied to employee accounts. Latency for database-backed tools averages 200-400ms which is acceptable. The biggest productivity win has been Claude being able to answer specific ERP questions without employees needing to navigate the UI — a query like 'what invoices are overdue for client X' that used to take 3 clicks and 30 seconds now happens in one conversation turn.
Sources & Further Reading