Introduction
The Model Context Protocol (MCP) represents a significant shift in how AI applications interact with external data sources, tools, and services. As large language models become more capable, the need for standardized ways to connect these models to real-world systems has grown dramatically. MCP addresses this challenge by providing an open protocol that enables AI assistants to securely access local and remote resources through a consistent interface.
For TypeScript developers, MCP opens new possibilities for building AI-powered applications that can interact with databases, file systems, APIs, and other services in a structured, predictable manner. Whether you are building an AI coding assistant, a data analysis tool, or an enterprise automation system, understanding how to implement MCP servers is becoming an essential skill.
This guide walks you through the complete process of building MCP servers with TypeScript. You will learn the core concepts behind the protocol, understand the architecture of MCP servers and clients, implement a production-ready server using Server-Sent Events transport, and explore real-world examples that demonstrate practical applications. By the end of this article, you will have the knowledge and code needed to deploy MCP servers in your own projects.
Table of Contents
- Core Concepts
- Architecture Overview
- Step-by-Step Guide
- Real-World Examples
- Production Code Examples
- Comparison Table
- Best Practices
- Common Mistakes
- Performance Tips
- Security Considerations
- Deployment Notes
- Debugging Tips
- FAQ
- Conclusion
Core Concepts
The Model Context Protocol is built around a client-server architecture where MCP clients (typically AI applications or hosts) connect to MCP servers that expose specific capabilities. Understanding the fundamental components of this protocol is essential before diving into implementation.
MCP Server
An MCP server is a program that exposes resources, tools, and prompts to MCP clients. Servers can provide access to local files, database records, API endpoints, or any other data source that can be programmatically accessed. Each server runs independently and communicates with clients using a standardized protocol.
MCP Client
An MCP client initiates connections to MCP servers and sends requests on behalf of the user or AI model. Clients are typically integrated into AI applications, IDEs, or desktop tools that want to leverage external capabilities through the MCP interface.
Resources
Resources are data sources that MCP servers expose to clients. These can include files, database rows, API responses, or any structured data that an AI model might need to reference. Resources are identified by URIs and can be subscribed to for real-time updates.
Tools
Tools are executable functions that MCP servers expose. Unlike resources, which provide data, tools perform actions. Examples include running database queries, sending emails, creating files, or calling external APIs. Tools are defined with input schemas that validate parameters before execution.
Prompts
Prompts are templated interactions that servers can offer to clients. These help standardize common AI interactions by providing predefined prompt structures that clients can use when communicating with language models.
Transport
Transport defines how MCP servers and clients communicate. The protocol supports multiple transport mechanisms, including standard input/output (stdio) for local processes and Server-Sent Events (SSE) for remote connections over HTTP.
Architecture Overview
An MCP server architecture consists of several layers that work together to handle protocol communication, capability exposure, and business logic execution. Understanding this architecture helps you design servers that are maintainable, secure, and performant.
Transport Layer
The transport layer handles the low-level communication between clients and servers. For TypeScript implementations, this typically involves setting up an HTTP server with SSE support or managing stdio communication for local processes. The transport layer is responsible for parsing incoming messages, routing them to the appropriate handler, and sending responses back to clients.
Protocol Layer
The protocol layer implements the MCP specification, handling message types such as initialize, list_resources, list_tools, call_tool, and read_resource. This layer validates incoming requests against the MCP schema, manages server capabilities, and ensures compliance with the protocol specification.
Capability Layer
The capability layer defines what your server exposes to clients. This includes registering resources with their URIs, defining tools with their input schemas, and providing prompts. This layer acts as the interface between the protocol implementation and your business logic.
Business Logic Layer
The business logic layer contains the actual implementation of your server's functionality. This might include database connections, API clients, file system operations, or any other code that performs the real work when clients request resources or invoke tools.
Error Handling Layer
Error handling is critical in MCP servers because clients depend on consistent error responses to handle failures gracefully. Your server should catch exceptions, log errors appropriately, and return structured error responses that clients can interpret and act upon.
Step-by-Step Guide
Building an MCP server with TypeScript involves several distinct steps, from project setup to server deployment. This guide walks through each phase with practical details you can follow.
Step 1: Project Initialization
Start by creating a new Node.js project and installing the necessary dependencies. You will need TypeScript, Node.js types, and the MCP SDK for TypeScript. Initialize your project with npm and configure TypeScript for strict type checking.
Step 2: Server Setup
Create your MCP server instance with basic configuration including server name, version, and capabilities. Set up the transport layer based on your deployment needs—stdio for local integrations or SSE for network-accessible servers.
Step 3: Resource Registration
Define the resources your server will expose. Each resource needs a unique URI, a name, an optional description, and a MIME type. Implement handlers that fetch data when clients request specific resources.
Step 4: Tool Definition
Create tools that perform actions on behalf of clients. Define input schemas using JSON Schema to validate parameters, implement the execution logic, and register each tool with your server instance.
Step 5: Transport Configuration
Configure your chosen transport mechanism. For SSE, set up an HTTP server with appropriate endpoints for connection establishment and message streaming. For stdio, configure process input and output handling.
Step 6: Error Handling
Implement comprehensive error handling throughout your server. Catch errors at the transport level, protocol level, and business logic level. Return structured error responses that help clients understand what went wrong.
Step 7: Testing
Test your server with an MCP client to verify that resources are accessible, tools execute correctly, and errors are handled properly. Test edge cases such as invalid parameters, missing resources, and transport failures.
Step 8: Deployment
Prepare your server for production deployment. This includes environment variable configuration, logging setup, health check endpoints, and process management. Consider containerization with Docker for consistent deployment across environments.
Real-World Examples
MCP servers can be applied to many practical scenarios. These examples demonstrate common use cases and how to approach implementation for each type of server.
Database MCP Server
A database MCP server exposes database tables and records as resources, while providing tools for executing queries, inserting records, and updating data. This allows AI assistants to interact with your database through natural language while maintaining security through parameterized queries and access controls.
File System MCP Server
A file system MCP server provides access to local files and directories. Resources represent individual files or directory listings, while tools enable reading, writing, and managing files. Security is critical here to prevent unauthorized access to sensitive files.
API Gateway MCP Server
An API gateway MCP server acts as a bridge between AI assistants and external APIs. Resources represent API endpoints and their responses, while tools enable making authenticated API calls. This pattern is useful for integrating AI with SaaS platforms, payment processors, or external services.
Monitoring MCP Server
A monitoring MCP server exposes system metrics, logs, and health status as resources. Tools can trigger health checks, retrieve recent logs, or restart services. This enables AI-powered observability and incident response workflows.
Production Code Examples
The following code examples demonstrate a complete MCP server implementation with TypeScript. These examples are designed for production use and include proper error handling, type safety, and best practices.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';interface ServerConfig { name: string; version: string;}interface Resource { uri: string; name: string; description?: string; mimeType: string;}class MCPServer { private server: Server; private resources: Map = new Map(); constructor(config: ServerConfig) { this.server = new Server( { name: config.name, version: config.version, }, { capabilities: { resources: {}, tools: {}, }, } ); this.setupHandlers(); } private setupHandlers(): void { this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: Array.from(this.resources.values()), }; }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params as { uri: string }; const resource = this.resources.get(uri); if (!resource) { throw new Error(`Resource not found: ${uri}`); } const content = await this.fetchResourceContent(uri); return { contents: [ { uri: resource.uri, mimeType: resource.mimeType, text: content, }, ], }; }); } registerResource(resource: Resource): void { this.resources.set(resource.uri, resource); } private async fetchResourceContent(uri: string): Promise { throw new Error('fetchResourceContent must be implemented by subclass'); } async start(): Promise { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`${this.server.name} MCP server started on stdio`); }}export { MCPServer, type ServerConfig, type Resource }; import { MCPServer } from './base-server.js';interface DatabaseConfig { host: string; port: number; user: string; password: string; database: string;}class DatabaseMCPServer extends MCPServer { private dbConfig: DatabaseConfig; constructor(config: DatabaseConfig) { super({ name: 'database-mcp-server', version: '1.0.0', }); this.dbConfig = config; this.registerResource({ uri: 'db://tables/users', name: 'Users Table', description: 'Access to user records in the database', mimeType: 'application/json', }); this.registerResource({ uri: 'db://tables/orders', name: 'Orders Table', description: 'Access to order records in the database', mimeType: 'application/json', }); } private async fetchResourceContent(uri: string): Promise { const tableName = uri.replace('db://tables/', ''); try { const connection = await this.createConnection(); const rows = await connection.query( `SELECT * FROM ${tableName} LIMIT 100` ); await connection.end(); return JSON.stringify(rows, null, 2); } catch (error) { throw new Error(`Failed to fetch ${tableName}: ${error}`); } } private async createConnection(): Promise { // Database connection implementation // This would use a database client like pg, mysql2, or similar return { query: async (sql: string) => [], end: async () => {}, }; }}export { DatabaseMCPServer }; import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';import { z } from 'zod';const QueryToolSchema = z.object({ table: z.string().min(1), conditions: z.record(z.string()).optional(), limit: z.number().min(1).max(1000).optional().default(100),});type QueryToolParams = z.infer;class DatabaseMCPServer extends MCPServer { constructor(config: DatabaseConfig) { super({ name: 'database-mcp-server', version: '1.0.0', }); this.dbConfig = config; this.setupToolHandlers(); } private setupToolHandlers(): void { this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'query_database') { return this.handleQueryDatabase(args as QueryToolParams); } throw new Error(`Unknown tool: ${name}`); }); } private async handleQueryDatabase(params: QueryToolParams) { const validated = QueryToolSchema.safeParse(params); if (!validated.success) { throw new Error(`Invalid parameters: ${validated.error.message}`); } const { table, conditions, limit } = validated.data; try { const connection = await this.createConnection(); let sql = `SELECT * FROM ${table}`; if (conditions && Object.keys(conditions).length > 0) { const whereClauses = Object.entries(conditions) .map(([key, value]) => `${key} = '${value}'`) .join(' AND '); sql += ` WHERE ${whereClauses}`; } sql += ` LIMIT ${limit}`; const rows = await connection.query(sql); await connection.end(); return { content: [ { type: 'text', text: JSON.stringify(rows, null, 2), }, ], }; } catch (error) { throw new Error(`Query failed: ${error}`); } }}export { DatabaseMCPServer }; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';import express from 'express';import { MCPServer } from './base-server.js';class SSEDatabaseMCPServer extends MCPServer { private app: express.Application; private transports: Map = new Map(); constructor(config: DatabaseConfig) { super({ name: 'database-mcp-server-sse', version: '1.0.0', }); this.dbConfig = config; this.app = express(); this.app.use(express.json()); this.setupRoutes(); } private setupRoutes(): void { this.app.get('/sse', async (req, res) => { const transport = new SSEServerTransport('/messages', res); this.transports.set(transport.sessionId, transport); await this.server.connect(transport); }); this.app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId as string; const transport = this.transports.get(sessionId); if (transport) { await transport.handlePostMessage(req, res); } else { res.status(404).json({ error: 'Session not found' }); } }); this.app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy' }); }); } async start(port: number = 3000): Promise { this.app.listen(port, () => { console.error(`MCP SSE server listening on port ${port}`); }); }}export { SSEDatabaseMCPServer }; Comparison Table
| Feature | Stdio Transport | SSE Transport | HTTP Streamable Transport |
|---|---|---|---|
| Connection Type | Local process | Remote HTTP | Remote HTTP |
| Best For | IDE integrations, local tools | Web applications, network services | Production MCP servers |
| Setup Complexity | Low | Medium | Medium |
| Scalability | Limited to single process | High with load balancer | High with load balancer |
| Authentication | Process-level | HTTP headers, tokens | HTTP headers, tokens |
| Browser Compatible | No | Yes | Yes |
| MCP SDK Support | Stable | Stable | Beta/Evolving |
Best Practices
Building production MCP servers requires attention to security, performance, and maintainability. These best practices will help you create servers that are reliable and safe to deploy.
Input Validation
Always validate input parameters using schema validation libraries like Zod or Joi. Never trust data received from MCP clients without validation. This prevents injection attacks and ensures your server behaves predictably.
Resource URI Design
Design resource URIs that are descriptive, hierarchical, and consistent. Use patterns like "db://tables/users" or "file:///documents/report.pdf" to make resources easy to identify and organize.
Error Handling
Implement structured error handling at every layer of your server. Return meaningful error messages that help clients understand failures without exposing sensitive internal information.
Logging
Set up comprehensive logging for all incoming requests, executed tools, and errors. Use structured logging formats like JSON to make logs easier to analyze and monitor in production.
Rate Limiting
Implement rate limiting to prevent abuse and protect your backend resources. This is especially important for servers that expose database access or external API calls.
Versioning
Version your MCP server and document breaking changes. This helps clients adapt to updates and maintain compatibility across server versions.
Common Mistakes
Developers new to MCP often encounter these issues. Understanding them early can save significant debugging time.
Missing Error Handling
Failing to handle errors properly can cause your server to crash or return unhelpful responses. Always wrap tool executions and resource reads in try-catch blocks and return structured error responses.
Insecure Resource Access
Exposing file system resources without proper path validation can lead to directory traversal attacks. Always validate and sanitize resource URIs before accessing underlying systems.
Poor Type Safety
Using "any" types extensively in TypeScript defeats the purpose of type safety. Define proper interfaces and types for all request parameters, response bodies, and internal data structures.
Transport Misconfiguration
Choosing the wrong transport for your use case can limit your server's usability. Stdio is appropriate for local integrations, while SSE is needed for network-accessible servers.
Missing Health Checks
Production servers should expose health check endpoints for monitoring and load balancer integration. Without health checks, it is difficult to detect server failures automatically.
Performance Tips
MCP server performance directly affects the responsiveness of AI applications that depend on it. These tips will help you optimize your server for production workloads.
Connection Pooling
Use connection pooling for database connections to avoid the overhead of creating new connections for each request. This significantly improves throughput for database-backed MCP servers.
Caching
Implement caching for frequently accessed resources. Cache database query results, API responses, and file contents with appropriate TTL values to reduce backend load.
Lazy Loading
Only load resources and initialize connections when they are first requested. This reduces startup time and memory usage for servers with many optional capabilities.
Async Processing
Use async/await throughout your server to avoid blocking the event loop. This is especially important for I/O operations like database queries and external API calls.
Response Compression
Enable response compression for HTTP-based transports to reduce bandwidth usage and improve response times for clients on slower connections.
Security Considerations
MCP servers often provide access to sensitive data and powerful capabilities. Security must be a primary concern throughout design and implementation.
Authentication
Implement authentication for SSE-based MCP servers using API keys, OAuth tokens, or JWT validation. Stdio-based servers rely on operating system-level access controls.
Authorization
Implement role-based access control to restrict which clients can access specific resources or invoke particular tools. Not all clients should have access to all capabilities.
Input Sanitization
Sanitize all input parameters, especially those used in database queries, file paths, or command execution. Use parameterized queries and avoid string concatenation for SQL statements.
Audit Logging
Log all tool invocations and resource access for security auditing. Include client identity, timestamp, action performed, and result status in audit logs.
Network Security
Use HTTPS for SSE transports in production. Configure CORS policies appropriately and consider running MCP servers behind a reverse proxy with additional security controls.
Deployment Notes
Deploying MCP servers to production requires careful planning around process management, environment configuration, and monitoring.
Environment Variables
Store configuration such as database credentials, API keys, and server ports in environment variables. Never commit secrets to version control.
Process Management
Use process managers like PM2, systemd, or Kubernetes to manage your MCP server processes. Configure automatic restarts on failure and set appropriate resource limits.
Containerization
Docker is recommended for deploying MCP servers. Create multi-stage builds that minimize image size and include only production dependencies.
Monitoring
Set up monitoring for server health, response times, error rates, and resource usage. Configure alerts for critical metrics to detect issues before they affect users.
Load Balancing
For SSE-based servers, use load balancers to distribute traffic across multiple instances. Ensure session affinity is configured if your transport requires it.
Debugging Tips
Debugging MCP servers can be challenging due to the distributed nature of client-server communication. These techniques will help you identify and resolve issues efficiently.
Enable Protocol Logging
Configure your MCP server to log all incoming and outgoing protocol messages. This helps you verify that messages are formatted correctly and identify where communication breaks down.
Use an MCP Inspector
Use MCP inspector tools to connect to your server and test resources and tools manually. This isolates server issues from client integration problems.
Check Transport Logs
Examine transport-level logs to verify that connections are established correctly and messages are being transmitted without corruption.
Validate JSON Schemas
Ensure that your tool input schemas and resource definitions comply with JSON Schema specifications. Invalid schemas can cause client rejection or unexpected behavior.
Test Edge Cases
Test your server with invalid parameters, missing resources, and network interruptions. Verify that error handling works correctly in all failure scenarios.
FAQ
What is the Model Context Protocol?
The Model Context Protocol is an open standard that enables AI applications to connect to external data sources, tools, and services through a consistent interface. It defines how clients and servers communicate to exchange resources, execute tools, and manage prompts.
Why use TypeScript for MCP servers?
TypeScript provides type safety, better developer experience, and catch errors at compile time rather than runtime. Since MCP involves structured messages and schemas, TypeScript's type system helps ensure protocol compliance and reduces bugs.
Can MCP servers access local files?
Yes, MCP servers can access local files through stdio transport when running on the same machine as the client. However, you should implement strict path validation to prevent unauthorized file access.
How does MCP differ from traditional APIs?
MCP is specifically designed for AI-to-tool communication, with standardized message formats and capability discovery. Traditional APIs are typically designed for general-purpose application integration and lack the standardized resource and tool definitions that MCP provides.
Can multiple clients connect to one MCP server?
Yes, especially with SSE transport, multiple clients can connect to the same MCP server simultaneously. Each client maintains its own session and can independently request resources and invoke tools.
Is MCP suitable for production applications?
Yes, MCP is designed for production use with proper error handling, authentication, and transport options. Many organizations are already deploying MCP servers for AI integration in production environments.
How do I secure an MCP server?
Secure MCP servers through authentication (API keys, OAuth, JWT), authorization (role-based access control), input validation, and network security (HTTPS, CORS policies, reverse proxies).
What tools are available for MCP development?
The official MCP SDK provides libraries for TypeScript and Python. Additional tools include MCP Inspector for testing, various client implementations for AI applications, and community-built servers for common services.
Conclusion
Building MCP servers with TypeScript enables you to create powerful integrations between AI applications and your existing systems. By following the patterns and practices outlined in this guide, you can build servers that are secure, performant, and maintainable.
The Model Context Protocol is still evolving, but it represents a fundamental shift in how AI systems interact with the world. Whether you are building internal tools, customer-facing applications, or enterprise automation systems, MCP provides a standardized foundation for AI integration.
Start by implementing a simple MCP server that exposes a single resource or tool. As you become comfortable with the protocol, expand to more complex servers with multiple capabilities, authentication, and production-grade deployment configurations.
Ready to build your first MCP server? Begin with the TypeScript examples in this guide, adapt them to your specific use case, and deploy your server to start connecting AI applications to your data and services.