Build a Custom MCP Server: Developer Guide
Complete developer guide for building a custom MCP (Model Context Protocol) server. Covers the MCP specification, tool and resource definitions, TypeScript SDK, testing with Inspector, and deployment to Claude and other clients.
- The Model Context Protocol (MCP) is an open standard that defines how AI models connect to external tools, data sources, and systems. Building an MCP server makes your tools accessible to any MCP-compatible client including Claude Desktop, Cursor, and custom agents.
- MCP servers expose three primitives: tools (actions the model can invoke with parameters), resources (data the model can read), and prompts (reusable prompt templates). Most custom servers start with tools and add resources later.
- The TypeScript MCP SDK provides a Server class with decorators for registering tools, resources, and prompts. A minimal MCP server with one tool requires about 40 lines of TypeScript including schema definitions and error handling.
- MCP Inspector is the essential testing tool during development. It connects to your server locally, displays available tools and resources, lets you invoke them with test inputs, and shows the raw protocol messages for debugging.
- Production MCP servers need input validation (Zod schemas), error handling (structured error responses, not crashes), rate limiting, and logging. The server should be stateless so it can be restarted without losing context.
Understanding the Model Context Protocol
The Model Context Protocol (MCP) is an open standard created by Anthropic that defines how AI models connect to external tools and data sources. Before MCP, every AI application had its own proprietary way of integrating tools: OpenAI had function calling, LangChain had its tool abstraction, and every chatbot platform had its own plugin format. MCP standardizes this into a single protocol that any client and any server can implement, creating an ecosystem where tools are built once and work everywhere.
Think of MCP like USB for AI tools. Before USB, every peripheral device had its own proprietary connector. USB standardized the interface, and suddenly any device worked with any computer. MCP does the same for AI tools: build an MCP server for your database, and it works with Claude Desktop, Cursor, Windsurf, custom LangGraph agents, and any future client that implements the MCP protocol. No per-client integration work, no proprietary APIs, no vendor lock-in.
The protocol defines three primitives that a server can expose: Tools are actions the model can invoke — they have a name, description, input schema (JSON Schema), and a handler function that executes the action and returns a result. Think of tools as functions the AI can call: query_database, send_email, create_ticket. Resources are data the model can read — they have a URI, a name, and content that can be text or binary. Think of resources as files or documents: database schemas, configuration files, documentation pages. Prompts are reusable prompt templates that the model can use — they have a name, description, and parameterized template text.
Most custom MCP servers start with tools because they provide the most immediate value. A tool that queries your production database gives Claude the ability to answer questions about your data in real time. A tool that creates Jira tickets lets Claude turn conversation into action. A tool that calls your internal API gives Claude access to your business logic. Each tool you add expands what AI can do in your specific environment.
The MCP architecture is client-server. The client (Claude Desktop, Cursor, or your custom application) discovers available servers, connects to them, and presents their tools and resources to the AI model. The server (what you are building) implements the tools and resources and responds to client requests. Communication happens over stdio (the client spawns the server as a subprocess and communicates via stdin/stdout) or HTTP with SSE (the client connects to the server over HTTP for remote deployments). Stdio is simpler and more secure for local development; HTTP is necessary for remote servers.
Before building, decide what your MCP server will do. The best MCP servers solve a specific integration need: connecting an AI model to a particular system (your database, your CRM, your monitoring stack) or exposing a particular capability (code execution, file manipulation, data visualization). Avoid building a "do everything" server — focused servers are easier to develop, test, and maintain. Our MCP server tutorial for AI agents covers additional server patterns and architectures. If you are building MCP servers for agentic workflows, our LangGraph tutorial shows how to consume MCP tools from LangGraph agents.
Setting Up Your MCP Server Project
MCP servers can be built in TypeScript or Python. This guide uses TypeScript because the TypeScript SDK is more mature, the type safety catches schema mismatches at compile time, and most MCP servers in the ecosystem are TypeScript-based. The Python SDK is a valid alternative if your team is Python-first — the concepts are identical, only the syntax differs.
Initialize a new TypeScript project. Create a directory for your server, initialize it with npm init -y, and install the MCP SDK: npm install @modelcontextprotocol/sdk. Install TypeScript and Zod for schema validation: npm install -D typescript @types/node and npm install zod. Create a tsconfig.json with "target": "ES2022", "module": "Node16", and "strict": true. These settings ensure compatibility with the MCP SDK and enable full type checking.
The project structure for an MCP server is simple. You need: src/index.ts (the server entry point that initializes the server and registers tools), src/tools/ (a directory with one file per tool, each exporting the tool definition and handler), and src/resources/ (optional, for resource definitions). This separation keeps your server organized as you add more tools. A single-tool server can put everything in index.ts, but separate files become essential once you have 3+ tools.
Create the server entry point in src/index.ts. Import McpServer from @modelcontextprotocol/sdk/server/mcp.js and StdioServerTransport from @modelcontextprotocol/sdk/server/stdio.js. Instantiate the server with a name and version: const server = new McpServer({ name: "my-server", version: "1.0.0" }). The name appears in client UIs so users know which server provides which tools. The version helps with debugging and updates.
Connect the transport layer at the bottom of your entry point: const transport = new StdioServerTransport() followed by await server.connect(transport). This starts the server and listens for MCP protocol messages on stdin, sending responses on stdout. The stdio transport is the standard for local MCP servers — Claude Desktop and Cursor both spawn servers as child processes and communicate via stdio. For remote servers, use SSEServerTransport instead, which starts an HTTP server.
Add the build and run scripts to package.json: a "build" script that compiles TypeScript (tsc) and a "start" script that runs the compiled output (node dist/index.js). Also add a "dev" script for development: tsx src/index.ts (using the tsx package for direct TypeScript execution without a build step). Install tsx as a dev dependency: npm install -D tsx.
Set the "type": "module" field in package.json to enable ES module imports. The MCP SDK uses ES modules, and mixing CommonJS and ESM causes import errors. Also add a "bin" field pointing to your compiled entry point: "bin": { "my-server": "dist/index.js" }. This allows the server to be installed globally with npm install -g and run by name, which is the expected deployment pattern for MCP servers used with Claude Desktop.
Verify the setup by running npm run dev. The server should start without errors and wait for input on stdin. You will not see any output because the server is waiting for MCP protocol messages, not human input. If you see errors about missing modules or import failures, check that your tsconfig.json module settings match the SDK's requirements (Node16 or NodeNext). We will test properly with MCP Inspector in a later section.
Defining Tools: Schemas, Handlers, and Best Practices
Tools are the core of most MCP servers. Each tool has four components: a name (how the AI model refers to the tool), a description (how the model decides when to use the tool), an input schema (what parameters the model needs to provide), and a handler (the function that executes when the tool is called). Getting all four right is essential for a tool that works reliably.
Register a tool on your server using the server.tool() method. The first argument is the tool name (use snake_case, be descriptive: query_orders not q). The second argument is the description — this is the most important field because it is what the AI model reads when deciding whether to call the tool. Write it like documentation for a developer: what the tool does, when to use it, what it returns, and any important constraints. The third argument is the input schema as a Zod object. The fourth argument is the async handler function.
The input schema uses Zod for validation and type inference. Define the schema with z.object() containing the tool's parameters. Each parameter needs a type and a description. For a database query tool: z.object({ table: z.string().describe("The database table to query"), filter: z.string().optional().describe("SQL WHERE clause without the WHERE keyword"), limit: z.number().default(10).describe("Maximum rows to return, default 10") }). The descriptions on each field help the model provide correct values — without them, the model guesses based on field names alone, which often leads to errors.
The handler function receives the validated input object and must return a result in MCP's content format. The most common return format is text content: return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }. You can also return images (type: "image" with base64 data) or embedded resources (type: "resource" with a URI). Always return structured data (JSON) rather than prose when the tool produces data — the model can interpret JSON more reliably than free-form text.
Implement error handling in every handler. If the tool encounters an error (database connection failed, API returned 500, invalid parameters that passed Zod validation but failed business logic), return a structured error rather than throwing an exception. MCP defines an isError field for tool results: return { content: [{ type: "text", text: "Error: Database connection timeout. Please try again." }], isError: true }. The isError: true flag tells the AI model that the tool failed, so it can inform the user or try a different approach rather than treating the error message as a successful result.
Tool design best practices from production MCP servers: make tools focused — one tool does one thing. A get_customer tool and a update_customer tool are better than a single manage_customer tool with a mode parameter. Focused tools are easier for the model to select correctly. Include examples in descriptions — "Query orders for a customer. Example: {customer_id: 'cust_123', status: 'pending'}" helps the model format parameters correctly. Return metadata alongside data — include the query that was run, the total result count, and the timestamp so the model can provide context in its response.
For tools that modify data (write, update, delete), add a confirmation pattern. The first call returns a preview of what will change: "This will update 15 customer records to status 'inactive'. Confirm?" The model presents this to the user, and only on confirmation does it call the tool again with a confirm: true parameter. This prevents the AI from making unintended data changes. Implement this with a dryRun parameter that defaults to true — the tool runs in preview mode unless explicitly told to execute.
For performance-sensitive tools, add caching and timeout logic in the handler. If the tool queries an external API, cache responses for a configurable TTL (30 seconds for real-time data, 5 minutes for slowly-changing data). If the tool runs a database query, set a query timeout (5 seconds is reasonable) and return a timeout error rather than hanging. The MCP protocol does not have built-in timeouts, so the server must enforce them in tool handlers.
Exposing Resources and Prompt Templates
Resources are the second MCP primitive. While tools let the model take actions, resources let the model read data. A resource has a URI (like db://schema/customers), a name, a MIME type, and content. Resources are useful for giving the model access to reference data that does not change per query: database schemas, API documentation, configuration files, code templates, and business rules documents. The model can read resources to ground its understanding before using tools.
Register resources using server.resource(). There are two patterns: static resources that return the same content every time, and dynamic resources that generate content based on a URI template. A static resource for a database schema: register it with a fixed URI like db://schema/main, and the handler returns the current schema definition. A dynamic resource for database table contents: register it with a URI template like db://tables/{tableName}, and the handler reads the table name from the URI and returns a description of that table's columns and sample data.
Resource content can be text (SQL schemas, documentation, configuration) or binary (images, PDFs). Text resources use { uri, mimeType: "text/plain", text: "..." }. Binary resources use { uri, mimeType: "image/png", blob: base64String }. For most MCP servers, text resources are sufficient. Binary resources are useful for servers that expose dashboards, charts, or generated documents.
The resource list endpoint lets clients discover available resources. When a client connects, it calls resources/list and receives a list of all available resource URIs with descriptions. The model (or the user through the client UI) can then select which resources to read. This discovery mechanism means you do not need to hardcode resource URIs in your prompts — the model can browse available resources and choose the relevant ones. Implement the resource list by registering resources with descriptive names: "Customers table schema", "Product catalog schema", "API rate limits configuration".
Resource templates (using URI templates like db://tables/{name}) are powerful for parameterized resources. Instead of registering a separate resource for each database table, register one template that accepts the table name as a parameter. The client presents the template to the model, which fills in the parameter. This pattern scales to hundreds of resources without hundreds of registrations. Templates use standard URI template syntax (RFC 6570): {variableName} for required parameters.
Prompts are the third MCP primitive. They are reusable prompt templates that clients can present to users. A prompt has a name, description, optional arguments, and a template that generates messages. Prompts are useful for standardized workflows: "Analyze this dataset" (with a dataset argument), "Review this code" (with a file path argument), "Generate a report on" (with a topic argument). The prompt template returns a list of messages that prime the conversation for the specific task.
Register prompts with server.prompt(). The handler receives the prompt arguments and returns an array of messages. For a "code review" prompt: the handler takes a filePath argument, reads the file (or fetches it via a resource), and returns messages like [{ role: "user", content: "Please review the following code for bugs, security issues, and style: [file contents]" }]. The client inserts these messages into the conversation, and the model responds with the code review. Prompts standardize how users interact with your server's capabilities, reducing the chance of poorly-phrased requests. For more on how MCP servers integrate into agentic architectures, see our MCP server tutorial for AI agents.
A practical pattern: combine resources and prompts. Register a resource that contains your company's coding standards, and a prompt that instructs the model to review code against those standards. The prompt template includes a message that says "Read the coding standards from the resource [URI], then review the following code." This grounds the model's review in your actual standards rather than its generic training data. The combination of resources (context) and prompts (instructions) produces significantly better results than either alone.
Testing with MCP Inspector and Integration Testing
Testing MCP servers requires a client that speaks the protocol. Building a test client from scratch is possible but tedious. MCP Inspector is the official testing tool that connects to your server, discovers its capabilities, and lets you invoke tools and read resources interactively. It is the single most important tool in your MCP development workflow.
Install and run MCP Inspector with npx @modelcontextprotocol/inspector. It launches a web UI where you configure your server connection. For a stdio server, set the command to your server's entry point: node dist/index.js or tsx src/index.ts. Click "Connect" and the Inspector spawns your server as a child process and establishes the MCP connection. If the connection succeeds, you see your server's name, version, and a list of its tools, resources, and prompts.
Use the Inspector to test each tool. Select a tool, fill in the input parameters using the auto-generated form (built from your JSON Schema), and click "Run". The Inspector shows the raw MCP request, the raw response, and a formatted view of the result. Test with: valid inputs that exercise the happy path, edge case inputs (empty strings, very large numbers, special characters), invalid inputs that should trigger validation errors, and inputs that should trigger your error handling (database connection failures, API timeouts). Verify that every case returns a sensible result or a clear error message.
The Inspector's protocol log shows every MCP message exchanged between the client and server. This is invaluable for debugging: if a tool call fails, the protocol log shows exactly what request was sent and what response came back. Common issues visible in the protocol log: schema mismatches (the client sends a parameter the server does not expect), encoding errors (binary data not properly base64-encoded), and timeout issues (the server takes too long to respond).
For automated testing, write integration tests using the MCP SDK's client. Create a test file that instantiates your server, connects a client, and calls tools programmatically. Assert on the tool responses: correct data, correct format, correct error handling. Run these tests in CI to catch regressions. A minimal test framework: use Node.js test runner (node --test) or Jest, instantiate your server with stdio transport, connect a client, and call client.callTool({ name: "query_orders", arguments: { customer_id: "test-123" } }). Assert that the response contains the expected order data.
Test resource endpoints similarly. Use the Inspector to browse available resources, read each one, and verify the content is correct and well-formatted. For dynamic resources with URI templates, test with various parameter values including edge cases. Verify that the resource list endpoint returns all expected resources with correct descriptions.
Test your server with Claude Desktop before shipping. Add your server to Claude's MCP configuration file (claude_desktop_config.json on macOS/Linux, stored in the Claude Desktop config directory). The configuration specifies the server command and any environment variables: {"mcpServers": {"my-server": {"command": "node", "args": ["/path/to/dist/index.js"], "env": {"DB_URL": "..."}}}}. Restart Claude Desktop, and your server's tools appear in the tool list. Ask Claude to use each tool and verify the results match your expectations. This end-to-end test catches issues that unit tests miss: schema interpretation differences between Inspector and Claude, tool selection behavior, and result formatting.
Create a test data set for your server. If your server queries a database, set up a test database with known data. If it calls an API, use a mock server (like Prism or WireMock) that returns deterministic responses. Deterministic test data makes your tests reliable and repeatable. Avoid testing against production systems — test data changes, rate limits apply, and failed tests can have side effects.
Deployment, Security, and Production Best Practices
Deploying an MCP server to production means making it available to MCP clients reliably and securely. The deployment approach depends on your transport: stdio servers are distributed as npm packages or binaries that clients install and run locally, while HTTP/SSE servers are deployed as web services that clients connect to over the network.
For stdio deployment (the most common pattern for developer tools), publish your server as an npm package. Users install it globally (npm install -g your-mcp-server) and add it to their Claude Desktop or Cursor configuration. The package should include the compiled JavaScript, a proper "bin" entry in package.json, and a shebang line (#!/usr/bin/env node) at the top of the entry point. Document the required environment variables clearly — users need to know what API keys or connection strings to provide.
For HTTP/SSE deployment (necessary for remote servers or multi-user setups), deploy your server as a web service on any cloud provider. Use the SSEServerTransport from the MCP SDK, which starts an HTTP server with SSE endpoints for the MCP protocol. Deploy behind a reverse proxy (nginx, Cloudflare) with SSL termination. Add authentication — the MCP protocol does not include authentication, so you must implement it at the transport layer. API key authentication via HTTP headers is the simplest approach: the client includes an Authorization header, and your server validates it before processing requests.
Security is critical because MCP servers often have access to sensitive systems: databases, APIs, internal tools. Follow these security practices: Principle of least privilege — give the server read-only database access unless write operations are explicitly needed. Use a dedicated database user with minimal permissions. Input sanitization — even though Zod validates parameter types, validate parameter values against business rules. A tool that queries by customer ID should verify the ID format, not just that it is a string. SQL injection prevention — never concatenate user input into SQL queries. Use parameterized queries exclusively. Secret management — never hardcode API keys or database passwords. Use environment variables and document them in your README.
For tools that perform write operations (creating records, sending emails, modifying data), implement defense in depth: the dry-run-by-default pattern (covered in the tools section), rate limiting (no more than 10 write operations per minute), and audit logging (record every write operation with timestamp, parameters, and result). These guardrails prevent runaway AI models from making excessive or unintended changes. An AI model that calls send_email 500 times due to a loop bug can cause significant damage without rate limiting.
Add observability to your production server. Log every tool call with: timestamp, tool name, input parameters (redacted of sensitive values), response size, execution time, and success/failure status. Structured JSON logs work best for aggregation in logging platforms. Monitor: tool call frequency (identify the most-used tools for optimization), error rates by tool (catch failing integrations), response latency (identify slow tools that degrade user experience), and unique client count (understand usage patterns).
Versioning and updates require care because MCP clients cache server capabilities. When you add a new tool or change a tool's schema, clients may not immediately see the changes. For stdio servers, users need to restart their client (Claude Desktop, Cursor) to pick up changes. For HTTP servers, the client re-discovers capabilities on each connection, so changes take effect on the next session. When making breaking changes (renaming a tool, changing required parameters), bump the server version and document the migration path. Avoid removing tools that users depend on — deprecate them (keep them working but update the description to say "Deprecated, use X instead") and remove them in a future major version.
For teams maintaining multiple MCP servers, consider a monorepo structure with shared utilities: a common error handling module, shared authentication logic, and reusable database connection management. This reduces code duplication and ensures consistent behavior across servers. Use npm workspaces or turborepo to manage the monorepo. Each server is a separate package that can be published and versioned independently. For teams building MCP servers as part of a larger agent architecture, our AI agent implementation guide covers how MCP fits into enterprise deployment strategies.
Real-World MCP Server Examples and Architecture Patterns
Concrete examples clarify how MCP servers work in practice. Here are five production-grade MCP server architectures, each solving a different integration need, that you can use as templates for your own servers.
Database Explorer Server: This server gives AI models read access to a PostgreSQL database. Tools: list_tables (returns table names and row counts), describe_table (returns column names, types, and constraints for a specific table), query (runs a read-only SQL query with a 5-second timeout and 100-row limit). Resources: db://schema (the full database schema as SQL DDL). The query tool uses a read-only database user and wraps all queries in a transaction that rolls back, ensuring the tool cannot modify data even if the AI crafts an INSERT or UPDATE statement. This server is the most common MCP use case because it turns any database into an AI-queryable knowledge base.
API Gateway Server: This server wraps your internal REST API as MCP tools. Each API endpoint becomes a tool with parameters matching the endpoint's query parameters or request body. A tool for GET /api/customers/{id} takes a customer_id parameter and returns the customer record. A tool for POST /api/tickets takes title, description, and priority parameters and creates a support ticket. The server handles authentication (adds your API key to requests), error mapping (translates HTTP errors to MCP error responses), and response formatting (extracts relevant fields from API responses to reduce token usage). This pattern lets you expose your entire API to AI without modifying the API itself.
File System Server: This server provides controlled file system access within a defined directory scope. Tools: list_files (lists files in a directory with metadata), read_file (reads file contents), write_file (writes content to a file, with confirmation), search_files (grep-like search across files). The critical security measure: all file paths are resolved relative to a configured root directory, and the server rejects any path that would escape the root (path traversal attacks using ../). This server is useful for coding assistants that need to read and modify project files.
Monitoring Dashboard Server: This server gives AI models access to your infrastructure monitoring data. Tools: get_metrics (fetches metrics from Prometheus/Grafana for a time range), get_alerts (lists active alerts with severity and description), get_logs (searches logs in Elasticsearch/Loki with a query). Resources: monitoring://dashboards (list of available dashboards), monitoring://runbooks/{alertName} (runbook documentation for each alert type). This server turns your AI assistant into an on-call copilot that can investigate incidents by querying the same data sources your engineers use.
Multi-Service Orchestrator Server: For complex workflows that span multiple services, build a server with tools that combine multiple API calls into a single operation. A process_refund tool that checks the order in the order service, creates a refund in the payment service, updates the ticket in the support system, and sends a confirmation email — all in one tool call. This is more reliable than having the AI model call four separate tools in sequence because the server handles the orchestration logic, error handling, and rollback. If the payment refund fails, the server rolls back the other changes rather than leaving the system in an inconsistent state.
Architecture patterns to follow across all servers: stateless handlers (each tool call is independent, no shared state between calls), idempotent operations (calling the same tool with the same parameters produces the same result), graceful degradation (if a downstream service is unavailable, return a clear error rather than hanging), and structured responses (return JSON objects with consistent schemas rather than free-form text). These patterns make your servers predictable, testable, and reliable. For broader context on how MCP servers fit into AI agent ecosystems, our AI agents for small business guide covers the business case for tool-connected AI assistants.
FAQ
Which language should I use to build an MCP server?
TypeScript is recommended for most servers because the SDK is more mature, the type safety catches bugs early, and most existing MCP servers are TypeScript-based. Use Python if your server wraps Python-specific tools (machine learning models, data science libraries) or your team is Python-first. Both SDKs implement the full MCP specification.
Can I use MCP with OpenAI models?
MCP is an open protocol, and any client can implement it. Currently, the primary MCP clients are Claude Desktop, Cursor, Windsurf, and Zed. OpenAI does not natively support MCP, but you can use MCP servers with OpenAI models by building a custom client that translates between MCP tools and OpenAI function calling. LangChain provides MCP tool adapters for this purpose.
How do I handle authentication in MCP servers?
For stdio servers (local), authentication is handled by the host system — if the user can run the server, they have access. For HTTP/SSE servers (remote), implement API key authentication at the transport layer: require an Authorization header and validate it before processing requests. The MCP protocol itself does not define authentication, so the server must handle it.
What is the difference between MCP tools and resources?
Tools are actions the model can invoke with parameters (query a database, send an email, create a record). Resources are data the model can read (database schemas, documentation, configuration files). Tools are for doing things; resources are for knowing things. Most servers start with tools and add resources for reference data that helps the model use tools more effectively.
How do I debug MCP protocol issues?
Use MCP Inspector — it shows the raw protocol messages between client and server. Common issues: JSON parsing errors (malformed responses), schema mismatches (client expects different parameter types), and transport errors (process crashes, stdin/stdout buffering issues). Enable verbose logging in your server to capture every request and response.