Skip to main content

Overview

xmcp allows your MCP server to act as a client to other MCP servers. This enables:
  • Service Composition - Combine multiple MCP servers into unified tools
  • Tool Aggregation - Access tools from external services (Playwright, Context7, etc.)
  • Data Integration - Pull data from remote MCP sources
  • Proxy Patterns - Create gateway servers that route to multiple backends
xmcp provides two client creation functions:
packages/xmcp/src/index.ts
export {
  createHTTPClient,
  createSTDIOClient,
  listSTDIOClientTools,
  callSTDIOClientTool,
  disconnectSTDIOClient,
} from "./client";

Client Configuration

Define client connections in src/clients.ts:
examples/external-clients/src/clients.ts
import { ClientConnections } from "xmcp";

export const clients: ClientConnections = {
  context: {
    url: "https://mcp.context7.com/mcp",
    headers: [
      {
        name: "CONTEXT7_API_KEY",
        env: "CONTEXT7_API_KEY",
      },
    ],
  },
  playwright: {
    npm: "@playwright/mcp",
  },
};
xmcp automatically generates typed client code for all defined connections.

HTTP Clients

Connect to remote MCP servers over HTTP/HTTPS.

Creating HTTP Clients

packages/xmcp/src/client/index.ts
export async function createHTTPClient({
  url,
  headers,
}: HttpClientOptions): Promise<Client<Request, Notification, Result>> {
  const client = new Client<Request, Notification, Result>(
    CLIENT_IDENTITY,
    CLIENT_CAPABILITIES
  );

  const headersRecord = headers
    ? headersToRecord(headers)
    : ({} as Record<string, string>);

  const serverUrl = new URL(url);

  const transportOptions: StreamableHTTPClientTransportOptions = {
    ...(headers ? { requestInit: { headers: headersRecord } } : {}),
    reconnectionOptions: {
      maxReconnectionDelay: 30000,
      initialReconnectionDelay: 1000,
      reconnectionDelayGrowFactor: 1.5,
      maxRetries: 2,
    },
  };

  const transport = new StreamableHTTPClientTransport(
    serverUrl,
    transportOptions
  );

  await client.connect(transport);

  return client;
}

HTTP Client Configuration

url
string
required
Full MCP server base URL (e.g., https://host.tld/mcp)
headers
CustomHeaders
Optional array of HTTP headers for authentication

Custom Headers

Use headers for API keys and authentication tokens:
packages/xmcp/src/client/headers.ts
export interface StaticHeader {
  name: string;
  value: string;
}

export interface EnvHeader {
  name: string;
  env: string; // Environment variable name
}

export type CustomHeader = StaticHeader | EnvHeader;
export type CustomHeaders = CustomHeader[];
Use env for sensitive values like API keys. The string is the environment variable name, and xmcp reads process.env[env] at runtime.

Example HTTP Client Usage

examples/external-clients/src/tools/get-library-docs.ts
import { InferSchema, type ToolMetadata } from "xmcp";
import { generatedClients } from "../generated/client.index";
import { z } from "zod";

export const schema = {
  libraryName: z.string().describe("The name of the library to get docs for"),
};

export const metadata: ToolMetadata = {
  name: "get-library-docs",
  description: "Get the docs for a library",
};

export default async function handler({
  libraryName,
}: InferSchema<typeof schema>) {
  const libraryDocs = await generatedClients.context.getLibraryDocs({
    context7CompatibleLibraryID: libraryName,
  });

  const result = (libraryDocs.content as any)[0].text;

  return `Library docs: ${result}`;
}

STDIO Clients

Connect to local MCP servers that communicate via stdin/stdout.

Creating STDIO Clients

packages/xmcp/src/client/index.ts
export async function createSTDIOClient(
  options: StdioClientOptions
): Promise<StdioClientConnection> {
  const { onStderrData, ...serverParams } = options;

  const client = new Client<Request, Notification, Result>(
    CLIENT_IDENTITY,
    CLIENT_CAPABILITIES
  );

  const transport = new StdioClientTransport({
    ...serverParams,
    stderr: serverParams.stderr ?? "pipe",
  });

  const stderrStream = transport.stderr;
  if (stderrStream) {
    const stderrHandler =
      onStderrData ??
      ((data: Buffer) => {
        console.error(`[STDIO MCP Server stderr]: ${data}`);
      });
    stderrStream.on("data", stderrHandler);
  }

  await client.connect(transport);

  return {
    client,
    transport,
  };
}

STDIO Client Configuration

command
string
Command to execute (e.g., "node", "python")
args
string[]
Command arguments (e.g., ["server.js"])
npm
string
NPM package to execute (alternative to command)
env
Record<string, string>
Environment variables for the subprocess
cwd
string
Working directory for the subprocess
stderr
'pipe' | 'inherit' | 'ignore'
default:"pipe"
How to handle stderr from the subprocess
onStderrData
(chunk: Buffer) => void
Custom handler for stderr data

STDIO Helper Functions

Convenience functions for STDIO clients:
packages/xmcp/src/client/index.ts
export async function listSTDIOClientTools(
  connection: StdioClientConnection
): Promise<string[]> {
  try {
    const result = await connection.client.listTools();
    return result.tools.map((tool) => tool.name);
  } catch (error) {
    console.error("Failed to list stdio MCP tools:", error);
    return [];
  }
}

export async function callSTDIOClientTool(
  connection: StdioClientConnection,
  toolName: string,
  args: Record<string, unknown>
): Promise<unknown> {
  try {
    return await connection.client.callTool({
      name: toolName,
      arguments: args,
    });
  } catch (error) {
    console.error(`Failed to call stdio MCP tool "${toolName}":`, error);
    throw error;
  }
}

export async function disconnectSTDIOClient(
  connection: StdioClientConnection
): Promise<void> {
  try {
    await connection.transport.close();
    await connection.client.close();
  } catch (error) {
    console.error("Error disconnecting from stdio MCP server:", error);
    throw error;
  }
}

Example STDIO Client Usage

examples/external-clients/src/tools/browser-navigate.ts
import { InferSchema, type ToolMetadata } from "xmcp";
import { generatedClients } from "../generated/client.index";
import { z } from "zod";

export const schema = {
  url: z.string().describe("The URL to navigate to"),
};

export const metadata: ToolMetadata = {
  name: "browser-navigate",
  description: "Navigate to a URL",
};

export default async function handler({ url }: InferSchema<typeof schema>) {
  await generatedClients.playwright.browserNavigate({
    url,
  });

  return `Navigated to: ${url}`;
}

Generated Client Code

xmcp generates typed client wrappers for all connections:
examples/external-clients/src/generated/client.index.ts
function proxyPromise<T extends object>(clientPromise: Promise<T>): T {
  return new Proxy({} as T, {
    get(_target, prop) {
      return async (...args: unknown[]) => {
        const client = await clientPromise;
        const method = (client as Record<string | symbol, unknown>)[prop];
        if (typeof method === "function") {
          return method.call(client, ...args);
        }
        throw new Error(`Property "${String(prop)}" is not a function`);
      };
    },
  });
}

export const generatedClients = {
  "context": proxyPromise<ClientContextClient>(clientContext),
  "playwright": proxyPromise<ClientPlaywrightClient>(clientPlaywright),
} as const;
Generated clients are automatically typed and provide full IntelliSense support.

Client Types

packages/xmcp/src/client/types.ts
export type HttpClientConfig = {
  name?: string;
  type?: "http";
  url: string;
  headers?: CustomHeaders;
};

export type StdioClientConfig = {
  name?: string;
  type?: "stdio";
  command?: string;
  args?: string[];
  npm?: string;
  npmArgs?: string[];
  env?: Record<string, string>;
  cwd?: string;
  stderr?: StdioIOStrategy;
};

export type ClientConnectionEntry =
  | string
  | HttpClientConfig
  | StdioClientConfig
  | ClientDefinition;

export type ClientConnections =
  | Record<string, ClientConnectionEntry>
  | ClientConnectionEntry[];

Project Configuration

Enable client generation in your xmcp config:
examples/external-clients/xmcp.config.ts
import { XmcpConfig } from "xmcp";

const config: XmcpConfig = {
  http: true,
  paths: {
    tools: "./src/tools",
    prompts: false,
    resources: false,
  },
  typescript: {
    skipTypeCheck: true,
  },
};

export default config;

Use Cases

Documentation Lookup

Query Context7 or other doc services from your tools

Browser Automation

Control Playwright for web scraping and testing

Service Mesh

Create gateway servers that route to specialized backends

Tool Aggregation

Combine tools from multiple MCP servers into one interface

Best Practices

Use Environment Variables

Store API keys and secrets in env vars, not in code

Handle Errors

Always wrap client calls in try-catch for network failures

Connection Pooling

Reuse generated clients instead of creating new connections

Timeout Configuration

Set appropriate timeouts for remote calls to prevent hangs

Client Identity

All xmcp clients identify themselves to servers:
packages/xmcp/src/client/index.ts
const packageJson = require("../../package.json");

export const CLIENT_IDENTITY = {
  name: packageJson.name,
  version: packageJson.version,
};

const CLIENT_CAPABILITIES = {
  capabilities: {
    sampling: {},
    elicitation: {},
    roots: { listChanged: true },
  },
} as const;

Next Steps

Tools

Learn how to create tools that use client connections

Transports

Understand HTTP vs STDIO transports

Authentication

Secure client connections with auth headers

API Reference

Detailed client API documentation

Build docs developers (and LLMs) love