MCP Library updated 12 min read

Building Advanced MCP Servers: Custom Integrations for Any Service

Go beyond basics to build production-ready MCP servers. Learn advanced patterns, error handling, streaming, authentication, and publishing to the MCP ecosystem.

RP

Rajesh Praharaj

Jul 21, 2025 · Updated Dec 26, 2025

Building Advanced MCP Servers: Custom Integrations for Any Service

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:

  1. ✅ Completed Building Your First MCP Server
  2. ✅ Familiar with TypeScript or Python
  3. ✅ Understand async programming
  4. ✅ Have an API or service to integrate

2025 Enterprise Options

DeploymentDescription
LocalTraditional npx/node setup
AWS ManagedEKS/ECS managed MCP servers (Nov 2025)
DockerContainerized deployment
KubernetesScalable orchestration

Project Structure

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

  1. 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"]
}
  1. 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.
  1. Publish:
npm publish

Submit to MCP Registry

  1. Go to mcp.so
  2. Submit your server details
  3. 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.

Was this page helpful?

Let us know if you found what you were looking for.