Introduction

An MCP server does not have to be a terminal endpoint. It can also act as an MCP client, connecting to other servers to compose capabilities. This pattern enables middleware servers that add cross-cutting concerns like authentication, logging, and caching, as well as aggregator servers that present a unified tool surface from multiple backends. Composability is what transforms MCP from a point-to-point protocol into a full architecture.

Key Concepts

  • Server-as-Client: An MCP server can instantiate an MCP Client to connect to downstream MCP servers. This creates a chain: Host connects to your server, and your server connects to other servers. Your server acts as both server (to the host) and client (to downstream servers).
  • Middleware Servers: Servers that intercept calls, apply cross-cutting logic, and forward to downstream servers. Common middleware patterns include authentication (validate tokens before forwarding), logging (record all tool invocations), caching (return cached results for identical inputs), and rate limiting.
  • Dynamic Tool Registration: Servers can register and unregister tools at runtime, then notify clients via notifications/tools/list_changed. This enables plugin architectures where new capabilities are added without restarting the server.
  • Tool Versioning: As tools evolve, use semver-inspired naming (e.g., query_v2) and deprecation patterns to maintain backward compatibility while introducing new behavior.

Real World Context

A fintech company runs three MCP servers: one for market data, one for trade execution, and one for compliance checks. Instead of connecting the host to all three directly, they build a gateway MCP server that connects to all three as a client. This gateway adds authentication (validates API keys), audit logging (records every trade tool call), and policy enforcement (blocks trades that fail compliance checks) — all transparently. The host sees a single server with a curated set of tools.

Deep Dive

Building a Server That Is Also a Client

An MCP server can create Client instances to connect to downstream servers. Here is a server that wraps another server and adds logging:

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { z } from "zod";

const gateway = new McpServer({ name: "gateway", version: "1.0.0" });

// Connect to a downstream MCP server as a client
const downstreamTransport = new StdioClientTransport({
  command: "node",
  args: ["./downstream-server.js"]
});
const downstreamClient = new Client(
  { name: "gateway-client", version: "1.0.0" }
);
await downstreamClient.connect(downstreamTransport);

// Discover downstream tools
const { tools } = await downstreamClient.listTools();

// Re-expose downstream tools with added logging
for (const tool of tools) {
  gateway.tool(
    tool.name,
    tool.description || "",
    tool.inputSchema ?? {},
    async (args) => {
      console.error(`[gateway] Forwarding call to ${tool.name}`);
      const start = performance.now();

      const result = await downstreamClient.callTool({
        name: tool.name,
        arguments: args
      });

      const duration = performance.now() - start;
      console.error(`[gateway] ${tool.name} completed in ${Math.round(duration)}ms`);

      return result;
    }
  );
}

const transport = new StdioServerTransport();
await gateway.connect(transport);

The gateway discovers all tools from the downstream server, re-registers them on itself with added logging, and forwards every call. The host sees the gateway as a regular MCP server — the composition is transparent.

Middleware Patterns

Common middleware patterns you can implement with composable servers:

text
Pattern           What It Does
────────────────  ─────────────────────────────────────────
Auth middleware   Validates bearer tokens before forwarding
Cache middleware   Returns cached results for identical inputs
Rate limiter      Throttles calls per tool per time window
Audit logger      Records every invocation with timestamp
Circuit breaker   Opens circuit after N consecutive failures

Each middleware server wraps a downstream server and adds a single concern. You can stack them: Host -> Auth -> RateLimiter -> AuditLog -> Actual Server.

Dynamic Tool Registration

Servers can register new tools at runtime and notify connected clients:

typescript
// Register a new tool dynamically
server.tool(
  "new_analysis_tool",
  "Run advanced analysis",
  { dataset: z.string() },
  async ({ dataset }) => {
    // ... implementation
    return { content: [{ type: "text", text: "Analysis complete" }] };
  }
);

// Notify the client that the tool list has changed
server.server.sendNotification({
  method: "notifications/tools/list_changed"
});

The client will re-fetch the tool list and see the newly registered tool. This enables plugin architectures where administrators can add capabilities to a running server.

Tool Versioning Strategies

As tools evolve, maintain backward compatibility:

text
Strategy              Example
────────────────────  ─────────────────────────────────────
Suffix versioning     query_v1, query_v2
Deprecation flag      Mark old tools as deprecated in description
Input schema growth   Add optional fields, never remove required ones
Parallel deployment   Run v1 and v2 side by side during migration

Never break existing tool signatures that clients depend on. Add new versions alongside old ones and deprecate gracefully.

Common Pitfalls

  1. Circular dependencies — Server A connects to Server B, which connects to Server A. This creates an infinite loop during initialization. Design your server topology as a directed acyclic graph (DAG).
  2. Losing error context in forwarding — When a middleware server catches an error from a downstream server and re-throws it, the original error context (stack trace, error code) can be lost. Preserve error details when forwarding.
  3. Capability mismatch — A gateway that advertises tool capabilities but does not forward resource or prompt capabilities from downstream servers creates a confusing experience. Explicitly decide which capabilities to expose.

Best Practices

  1. Keep middleware servers stateless — Middleware servers should not maintain business state. They add cross-cutting concerns (auth, logging, caching) and forward to stateful downstream servers. This makes them easy to scale and replace.
  2. Document the server topology — When you have multiple layers of MCP servers, maintain a diagram showing the flow from host to final server. Future developers need to understand the composition chain.
  3. Test composition end-to-end — Unit-test each server individually, but also run integration tests through the full composition chain to catch issues that only manifest when servers interact.

Summary

  • An MCP server can instantiate an MCP Client to connect to downstream servers, creating composable server chains.
  • Middleware servers add cross-cutting concerns (auth, caching, rate limiting, audit logging) transparently.
  • Dynamic tool registration with notifications/tools/list_changed enables plugin architectures.
  • Version tools with suffixes or parallel deployment to maintain backward compatibility.
  • Design server topologies as directed acyclic graphs to avoid circular dependencies.

Code Examples

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

// An MCP server acting as a client to another server
const transport = new StdioClientTransport({
  command: "node",
  args: ["./downstream-server.js"]
});
const client = new Client({ name: "gateway", version: "1.0.0" });
await client.connect(transport);

// Discover and forward tools
const { tools } = await client.listTools();
const result = await client.callTool({
  name: tools[0].name,
  arguments: { query: "SELECT 1" }
});
✓ Completed