TL;DR - Advanced MCP Development
Build production-ready MCP servers with professional patterns.
🆕 2025: Nov 2025 spec version with async operations, AWS managed MCP servers for EKS/ECS, and MCP Registry for discovery! $10.3B market projected. For an introduction to MCP, see the MCP Introduction guide.
What You’ll Learn:
- 🔒 Authentication: Handle API keys and OAuth
- ⚡ Streaming: Long-running operations
- 🧪 Testing: Unit and integration tests
- 📊 Pagination: Handle large datasets
- 🚨 Errors: Proper error handling
- 📦 Publishing: Share with the community
Quick Reference:
// Advanced MCP Server Structure
src/
├── index.ts // Server entry point
├── tools/ // Tool implementations
│ ├── query.ts
│ └── action.ts
├── services/ // External API clients
│ └── api-client.ts
├── utils/ // Helpers
│ ├── auth.ts
│ └── pagination.ts
└── types/ // TypeScript types
└── index.ts
Prerequisites
Before building advanced MCPs:
- ✅ Completed Building Your First MCP Server
- ✅ Familiar with TypeScript or Python
- ✅ Understand async programming
- ✅ Have an API or service to integrate
2025 Enterprise Options
| Deployment | Description |
|---|---|
| Local | Traditional npx/node setup |
| AWS Managed | EKS/ECS managed MCP servers (Nov 2025) |
| Docker | Containerized deployment |
| Kubernetes | Scalable orchestration |
Project Structure
Recommended Layout
my-mcp-server/
├── src/
│ ├── index.ts # Entry point
│ ├── server.ts # Server setup
│ ├── tools/ # Tool implementations
│ │ ├── index.ts # Tool exports
│ │ ├── search.ts # Search tool
│ │ └── create.ts # Create tool
│ ├── resources/ # Resource implementations
│ │ └── items.ts
│ ├── services/ # External API clients
│ │ └── api-client.ts
│ ├── utils/
│ │ ├── auth.ts # Authentication helpers
│ │ ├── pagination.ts # Pagination utilities
│ │ └── errors.ts # Error handling
│ └── types/
│ └── index.ts # Type definitions
├── tests/
│ ├── tools.test.ts # Unit tests
│ └── integration.test.ts # Integration tests
├── package.json
├── tsconfig.json
└── README.md
For patterns on building your first AI application, also see the Building Your First AI Application guide.
Package.json Setup
{
"name": "mcp-server-myservice",
"version": "1.0.0",
"description": "MCP server for MyService API",
"main": "dist/index.js",
"bin": {
"mcp-server-myservice": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"test": "jest",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"jest": "^29.0.0"
}
}
Authentication Patterns
Authentication is critical for production MCP servers. For related concepts about API security, see the Building Your First AI Application guide.
Pattern 1: API Key from Environment
// src/utils/auth.ts
export function getApiKey(): string {
const apiKey = process.env.MYSERVICE_API_KEY;
if (!apiKey) {
throw new Error(
"MYSERVICE_API_KEY environment variable is required"
);
}
return apiKey;
}
// Usage in API client
import { getApiKey } from "./utils/auth";
export class ApiClient {
private apiKey: string;
constructor() {
this.apiKey = getApiKey();
}
async request(endpoint: string, options: RequestInit = {}) {
return fetch(`https://api.myservice.com${endpoint}`, {
...options,
headers: {
"Authorization": `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
}
}
Pattern 2: OAuth Token Refresh
// src/utils/auth.ts
interface TokenData {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
class AuthManager {
private tokenData: TokenData | null = null;
async getAccessToken(): Promise<string> {
if (!this.tokenData) {
throw new Error("Not authenticated");
}
// Refresh if expires in next 5 minutes
if (Date.now() > this.tokenData.expiresAt - 300000) {
await this.refreshToken();
}
return this.tokenData.accessToken;
}
private async refreshToken(): Promise<void> {
const response = await fetch("https://api.service.com/oauth/token", {
method: "POST",
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: this.tokenData?.refreshToken,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
});
const data = await response.json();
this.tokenData = {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
}
}
export const authManager = new AuthManager();
Pattern 3: Multiple Auth Methods
// src/utils/auth.ts
type AuthMethod = "api_key" | "oauth" | "basic";
interface AuthConfig {
method: AuthMethod;
credentials: Record<string, string>;
}
export function getAuthHeaders(config: AuthConfig): Record<string, string> {
switch (config.method) {
case "api_key":
return { "X-API-Key": config.credentials.apiKey };
case "oauth":
return { "Authorization": `Bearer ${config.credentials.token}` };
case "basic":
const encoded = Buffer.from(
`${config.credentials.username}:${config.credentials.password}`
).toString("base64");
return { "Authorization": `Basic ${encoded}` };
}
}
Error Handling
Proper error handling makes your MCP server reliable and debuggable. For more on debugging AI tools, see the CLI Tools for AI guide.
Structured Error Types
// src/utils/errors.ts
export class McpError extends Error {
constructor(
message: string,
public code: string,
public statusCode?: number,
public details?: Record<string, unknown>
) {
super(message);
this.name = "McpError";
}
}
export class AuthenticationError extends McpError {
constructor(message: string) {
super(message, "AUTHENTICATION_ERROR", 401);
}
}
export class RateLimitError extends McpError {
constructor(public retryAfter: number) {
super(`Rate limited. Retry after ${retryAfter} seconds`, "RATE_LIMITED", 429);
}
}
export class NotFoundError extends McpError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, "NOT_FOUND", 404);
}
}
Error Handling in Tools
// src/tools/query.ts
import { McpError, NotFoundError, RateLimitError } from "../utils/errors";
export async function queryTool(params: QueryParams): Promise<ToolResult> {
try {
const result = await apiClient.query(params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
if (error instanceof NotFoundError) {
return {
content: [{ type: "text", text: `Not found: ${error.message}` }],
isError: true,
};
}
if (error instanceof RateLimitError) {
return {
content: [{
type: "text",
text: `Rate limited. Please wait ${error.retryAfter} seconds.`,
}],
isError: true,
};
}
// Unknown error
console.error("Unexpected error:", error);
return {
content: [{ type: "text", text: "An unexpected error occurred" }],
isError: true,
};
}
}
API Response Error Handling
// src/services/api-client.ts
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const body = await response.text();
switch (response.status) {
case 401:
throw new AuthenticationError("Invalid or expired credentials");
case 403:
throw new McpError("Access forbidden", "FORBIDDEN", 403);
case 404:
throw new NotFoundError("Resource", "unknown");
case 429:
const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
throw new RateLimitError(retryAfter);
default:
throw new McpError(
`API error: ${body}`,
"API_ERROR",
response.status
);
}
}
return response.json();
}
Pagination Handling
Cursor-Based Pagination
// src/utils/pagination.ts
interface PaginatedResponse<T> {
items: T[];
nextCursor?: string;
hasMore: boolean;
}
export async function fetchAllPages<T>(
fetchPage: (cursor?: string) => Promise<PaginatedResponse<T>>,
maxPages = 10
): Promise<T[]> {
const allItems: T[] = [];
let cursor: string | undefined;
let pageCount = 0;
do {
const response = await fetchPage(cursor);
allItems.push(...response.items);
cursor = response.nextCursor;
pageCount++;
} while (cursor && pageCount < maxPages);
return allItems;
}
Paginated Tool Implementation
// src/tools/list.ts
export const listItemsTool: Tool = {
name: "list_items",
description: "List items with pagination",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", description: "Items per page", default: 20 },
cursor: { type: "string", description: "Pagination cursor" },
},
},
async handler(params: ListParams): Promise<ToolResult> {
const { items, nextCursor, total } = await api.listItems({
limit: params.limit || 20,
cursor: params.cursor,
});
let response = `Found ${total} items.\n\n`;
response += items.map(item => `- ${item.name} (${item.id})`).join("\n");
if (nextCursor) {
response += `\n\nMore items available. Use cursor: ${nextCursor}`;
}
return { content: [{ type: "text", text: response }] };
},
};
Streaming Responses
Streaming is essential for long-running operations. For more on handling large contexts, see the Tokens, Context Windows & Parameters guide.
Long-Running Operations
// src/tools/process.ts
import { PassThrough } from "stream";
export const processDataTool: Tool = {
name: "process_large_data",
description: "Process large dataset with progress updates",
async *handler(params: ProcessParams): AsyncGenerator<Partial<ToolResult>> {
const items = await fetchLargeDataset(params.datasetId);
const total = items.length;
let processed = 0;
// Initial progress
yield {
content: [{ type: "text", text: `Processing ${total} items...` }],
};
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
processed++;
// Progress update every 10%
if (processed % Math.ceil(total / 10) === 0) {
yield {
content: [{
type: "text",
text: `Progress: ${Math.round(processed / total * 100)}%`,
}],
};
}
}
// Final result
yield {
content: [{ type: "text", text: `Completed! Processed ${total} items.` }],
};
},
};
Input Validation
Zod Schema Validation
// src/utils/validation.ts
import { z } from "zod";
export const SearchParamsSchema = z.object({
query: z.string().min(1, "Query is required").max(500),
limit: z.number().int().min(1).max(100).default(20),
filters: z.object({
status: z.enum(["active", "archived", "all"]).optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional(),
}).optional(),
});
export type SearchParams = z.infer<typeof SearchParamsSchema>;
export function validateInput<T>(
schema: z.ZodSchema<T>,
input: unknown
): T {
const result = schema.safeParse(input);
if (!result.success) {
throw new McpError(
`Validation error: ${result.error.message}`,
"VALIDATION_ERROR",
400
);
}
return result.data;
}
Validated Tool
// src/tools/search.ts
import { SearchParamsSchema, validateInput } from "../utils/validation";
export const searchTool: Tool = {
name: "search",
description: "Search with validated parameters",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
limit: { type: "number", description: "Max results" },
filters: { type: "object", description: "Optional filters" },
},
required: ["query"],
},
async handler(rawParams: unknown): Promise<ToolResult> {
const params = validateInput(SearchParamsSchema, rawParams);
const results = await api.search(params);
return formatResults(results);
},
};
Caching Strategies
Caching improves performance and reduces API costs. For more on cost optimization, see the Running LLMs Locally guide.
Simple In-Memory Cache
// src/utils/cache.ts
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
class SimpleCache<T> {
private cache = new Map<string, CacheEntry<T>>();
constructor(private defaultTtl = 300000) {} // 5 minutes
get(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}
set(key: string, value: T, ttl = this.defaultTtl): void {
this.cache.set(key, {
value,
expiresAt: Date.now() + ttl,
});
}
invalidate(pattern?: string): void {
if (!pattern) {
this.cache.clear();
} else {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
}
export const cache = new SimpleCache();
Cached API Calls
// src/services/api-client.ts
import { cache } from "../utils/cache";
export async function getItem(id: string): Promise<Item> {
const cacheKey = `item:${id}`;
// Check cache
const cached = cache.get<Item>(cacheKey);
if (cached) return cached;
// Fetch and cache
const item = await fetchItem(id);
cache.set(cacheKey, item);
return item;
}
Testing
Testing ensures your MCP server works correctly. For more on AI workflows, see the AI-Powered Workflows guide.
Unit Tests
// tests/tools.test.ts
import { searchTool } from "../src/tools/search";
describe("searchTool", () => {
test("returns results for valid query", async () => {
const result = await searchTool.handler({ query: "test" });
expect(result.content).toBeDefined();
expect(result.isError).toBeFalsy();
});
test("handles empty query", async () => {
const result = await searchTool.handler({ query: "" });
expect(result.isError).toBe(true);
});
test("respects limit parameter", async () => {
const result = await searchTool.handler({ query: "test", limit: 5 });
const parsed = JSON.parse(result.content[0].text);
expect(parsed.length).toBeLessThanOrEqual(5);
});
});
Integration Tests
// tests/integration.test.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { createServer } from "../src/server";
describe("MCP Server Integration", () => {
let server: Server;
beforeAll(async () => {
server = createServer();
});
test("lists available tools", async () => {
const tools = await server.listTools();
expect(tools.length).toBeGreaterThan(0);
expect(tools.map(t => t.name)).toContain("search");
});
test("executes search tool", async () => {
const result = await server.callTool("search", { query: "test" });
expect(result).toBeDefined();
});
});
MCP Inspector Testing
# Install the inspector
npm install -g @modelcontextprotocol/inspector
# Run your server with the inspector
npx mcp-inspector node dist/index.js
Publishing Your MCP
Ready to share your MCP with the community? For an overview of the MCP ecosystem, see the MCP Introduction guide.
Prepare for Publishing
- Update package.json:
{
"name": "mcp-server-myservice",
"version": "1.0.0",
"description": "MCP server for MyService",
"keywords": ["mcp", "myservice", "ai-tools"],
"repository": {
"type": "git",
"url": "https://github.com/yourname/mcp-server-myservice"
},
"license": "MIT",
"files": ["dist", "README.md"]
}
- Create comprehensive README:
# MCP Server for MyService
Connect MyService to AI assistants via MCP.
## Installation
\`\`\`bash
npm install mcp-server-myservice
\`\`\`
## Configuration
Add to your MCP config:
\`\`\`json
{
"mcpServers": {
"myservice": {
"command": "npx",
"args": ["-y", "mcp-server-myservice"],
"env": {
"MYSERVICE_API_KEY": "your-api-key"
}
}
}
}
\`\`\`
## Available Tools
| Tool | Description |
|------|-------------|
| `search` | Search for items |
| `create` | Create new item |
| `update` | Update existing item |
## Security
Requires API key with read permissions.
- Publish:
npm publish
Submit to MCP Registry
- Go to mcp.so
- Submit your server details
- Include: npm package name, GitHub repo, description
Production Checklist
Before Deployment
☐ All errors handled gracefully
☐ Input validation on all tools
☐ Rate limiting considered
☐ Authentication secure
☐ Sensitive data not logged
☐ README comprehensive
☐ Unit tests passing
☐ Integration tests passing
Security Review
☐ No credentials in code
☐ Environment variables documented
☐ Minimal required permissions
☐ Error messages don't leak info
☐ Dependencies updated
For broader security considerations when building AI tools, also see the Understanding AI Safety, Ethics, and Limitations guide.
Performance
☐ Responses reasonably sized
☐ Pagination for large datasets
☐ Caching where appropriate
☐ Streaming for long operations
Summary
Building advanced MCP servers requires:
- ✅ Proper auth handling - API keys, OAuth, refresh
- ✅ Error handling - Structured, informative errors
- ✅ Pagination - Handle large datasets gracefully
- ✅ Validation - Validate all inputs
- ✅ Testing - Unit and integration tests
- ✅ Documentation - Clear usage instructions
2025 Enterprise Options:
- AWS Managed MCP Servers (EKS/ECS preview Nov 2025)
- Docker containerization for MCP components
- Kubernetes orchestration for scaling
- MCP Registry for discovery (Sept 2025 preview)
Key patterns:
- Separate concerns (tools, services, utils)
- Use TypeScript for type safety
- Cache expensive operations
- Stream long-running tasks
- Handle rate limits gracefully
For combining multiple MCP servers, see the MCP Combinations guide.
Ready to publish?
- Complete documentation
- Comprehensive tests
- Submit to MCP Registry
🎉 Congratulations on completing the MCP Library!
Questions about building MCPs? Check the official docs or the MCP Registry.