> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/grittyninja/umcp/llms.txt
> Use this file to discover all available pages before exploring further.

# Transport Bridging

> Learn how UMCP bridges different MCP transport protocols into a unified interface

## Overview

UMCP acts as a **transport bridge**, allowing you to connect to MCP providers that use different transport protocols and expose them all through a single unified interface. This means downstream clients only need to connect to UMCP using one transport type, while UMCP handles the complexity of connecting to providers via their native transports.

## Supported Transports

UMCP supports three MCP transport protocols:

1. **stdio** - Standard input/output (process-based)
2. **sse** - Server-Sent Events (HTTP-based)
3. **streamable-http** - Streamable HTTP (modern HTTP-based)

From `config.ts:11`:

```typescript theme={null}
const transportKindSchema = z.enum(["stdio", "sse", "streamable-http"]);
```

## How Transport Bridging Works

### Upstream Connections

Each provider in your configuration specifies its transport type. UMCP creates the appropriate client connection based on the `transport` field:

```jsonc theme={null}
{
  "categories": {
    "example": {
      "providers": [
        {
          "name": "local_tool",
          "transport": "stdio",
          "command": "node",
          "args": ["./tool-server.js"]
        },
        {
          "name": "remote_tool",
          "transport": "streamable-http",
          "url": "https://api.example.com/mcp"
        },
        {
          "name": "legacy_tool",
          "transport": "sse",
          "url": "https://legacy.example.com/mcp"
        }
      ]
    }
  }
}
```

### Downstream Interface

UMCP exposes a single downstream interface that clients connect to. You choose the downstream transport when starting UMCP:

```bash theme={null}
# Serve via stdio
umcp serve

# Serve via HTTP
umcp serve --transport http --host 0.0.0.0 --port 3000 --path /mcp
```

## Transport-Specific Configuration

### stdio Transport

For `stdio` providers, you must specify a `command` and optionally `args`. From `config.ts:46-52`:

```typescript theme={null}
if (provider.transport === "stdio" && !provider.command) {
  ctx.addIssue({
    code: z.ZodIssueCode.custom,
    message: "provider.command is required when transport is stdio",
    path: ["command"]
  });
}
```

**Example configuration:**

```jsonc theme={null}
{
  "name": "filesystem",
  "transport": "stdio",
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
```

### HTTP Transports (sse & streamable-http)

For HTTP-based transports, you must specify a `url`. From `config.ts:54-61`:

```typescript theme={null}
if ((provider.transport === "sse" || provider.transport === "streamable-http") && !provider.url) {
  ctx.addIssue({
    code: z.ZodIssueCode.custom,
    message: "provider.url is required when transport is sse or streamable-http",
    path: ["url"]
  });
}
```

**Example configuration:**

```jsonc theme={null}
{
  "name": "cloud_service",
  "transport": "streamable-http",
  "url": "https://api.example.com/mcp"
}
```

## Downstream Server Implementation

UMCP's `serve.ts` module handles both stdio and HTTP downstream connections.

### stdio Server

The stdio server uses `StdioServerTransport` from the MCP SDK. From `serve.ts:109-114`:

```typescript theme={null}
async function runStdioServe(server: McpServer, logger: Logger): Promise<void> {
  const transport = new StdioServerTransport();
  await server.connect(transport as any);
  logger.info("server.started", "umcp started with stdio transport");
  await waitForShutdownSignal(logger, true);
}
```

### HTTP Server

The HTTP server uses `StreamableHTTPServerTransport` and handles multiple HTTP methods. From `serve.ts:116-177`:

```typescript theme={null}
async function runHttpServe(
  server: McpServer,
  options: { host: string; port: number; path: string; logger: Logger }
): Promise<void> {
  const { host, port, logger } = options;
  const endpointPath = normalizeEndpointPath(options.path);
  const mcpTransport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined
  });

  await server.connect(mcpTransport as any);

  const httpServer = createServer(async (req, res) => {
    try {
      const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
      if (requestUrl.pathname !== endpointPath) {
        res.statusCode = 404;
        res.end("Not Found");
        return;
      }

      const method = req.method ?? "GET";
      if (method !== "POST" && method !== "GET" && method !== "DELETE") {
        res.statusCode = 405;
        res.end("Method Not Allowed");
        return;
      }

      if (method === "POST") {
        const parsedBody = await readJsonBody(req);
        await mcpTransport.handleRequest(req, res, parsedBody);
      } else {
        await mcpTransport.handleRequest(req, res);
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      logger.error("server.http_error", "HTTP transport request failed", { message });
      if (!res.headersSent) {
        res.statusCode = 400;
        res.setHeader("content-type", "application/json");
        res.end(JSON.stringify({ error: message }));
      }
    }
  });

  await new Promise<void>((resolve, reject) => {
    httpServer.once("error", reject);
    httpServer.listen(port, host, () => resolve());
  });

  logger.info("server.started", "umcp started with streamable-http transport", {
    host,
    port,
    endpointPath
  });

  await waitForShutdownSignal(logger, false);

  await new Promise<void>((resolve) => {
    httpServer.close(() => resolve());
  });
}
```

## Server Setup Flow

The main `runServe` function orchestrates the entire server setup. From `serve.ts:179-209`:

```typescript theme={null}
export async function runServe(options: ServeOptions): Promise<void> {
  const logger = options.logger;
  const configPath = resolveConfigPath(options.configPath);
  const { config } = await loadConfig({ configPath, logger });

  const envPool = createRoundRobinEnvPool(logger);
  const providerManager = createProviderManager({ config, envPool, logger });
  const server = new McpServer({
    name: "umcp",
    version: "0.1.0"
  });

  try {
    const bindings = await discoverUnifiedTools(config, providerManager, logger);
    registerUnifiedTools(server, bindings, providerManager, logger);

    if (options.transport === "http") {
      await runHttpServe(server, {
        host: options.host,
        port: options.port,
        path: options.path,
        logger
      });
    } else {
      await runStdioServe(server, logger);
    }
  } finally {
    await providerManager.close();
    await server.close();
  }
}
```

**Key steps:**

1. Load configuration
2. Create environment pool (for round-robin rotation)
3. Create provider manager (handles upstream connections)
4. Create MCP server
5. Discover and register tools
6. Start appropriate transport (stdio or HTTP)

## HTTP Request Handling

The HTTP server supports GET, POST, and DELETE methods, matching the MCP protocol requirements:

* **GET**: Typically used for session management
* **POST**: Used for tool calls and other operations
* **DELETE**: Used for session cleanup

From `serve.ts:138-149`:

```typescript theme={null}
const method = req.method ?? "GET";
if (method !== "POST" && method !== "GET" && method !== "DELETE") {
  res.statusCode = 405;
  res.end("Method Not Allowed");
  return;
}

if (method === "POST") {
  const parsedBody = await readJsonBody(req);
  await mcpTransport.handleRequest(req, res, parsedBody);
} else {
  await mcpTransport.handleRequest(req, res);
}
```

## Request Body Parsing

For POST requests, UMCP reads and parses JSON bodies with size limits. From `serve.ts:30-51`:

```typescript theme={null}
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
  const chunks: Buffer[] = [];
  let size = 0;
  const maxSize = 1_048_576;  // 1MB limit

  for await (const chunk of req) {
    const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    chunks.push(bufferChunk);
    size += bufferChunk.length;

    if (size > maxSize) {
      throw new Error("Request body too large");
    }
  }

  if (chunks.length === 0) {
    return undefined;
  }

  const body = Buffer.concat(chunks).toString("utf8");
  return JSON.parse(body);
}
```

## Path Normalization

HTTP endpoint paths are automatically normalized to start with a forward slash. From `serve.ts:26-28`:

```typescript theme={null}
function normalizeEndpointPath(rawPath: string): string {
  return rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
}
```

## Use Cases

### Local Development

Connect to local stdio tools and expose them via HTTP:

```bash theme={null}
umcp serve --transport http --port 3000
```

Now local AI clients can connect to `http://localhost:3000/mcp` instead of managing multiple stdio processes.

### Centralized Gateway

Create a single HTTP endpoint that aggregates tools from multiple sources:

* Local stdio tools (file system, shell commands)
* Remote HTTP services (cloud APIs)
* Legacy SSE providers

### Cloud Deployment

Deploy UMCP as an HTTP service that bridges to stdio tools running in containers:

```jsonc theme={null}
{
  "categories": {
    "data": {
      "providers": [
        {
          "name": "postgres",
          "transport": "stdio",
          "command": "/usr/local/bin/postgres-mcp",
          "env": {
            "DATABASE_URL": "postgresql://..."
          }
        }
      ]
    }
  }
}
```

Start UMCP with:

```bash theme={null}
umcp serve --transport http --host 0.0.0.0 --port 8080
```

## Best Practices

1. **Choose the right downstream transport**: Use stdio for local development and HTTP for networked access
2. **Secure HTTP deployments**: Use reverse proxies (nginx, Caddy) to add TLS and authentication
3. **Monitor provider health**: Watch logs for upstream connection failures
4. **Set appropriate timeouts**: Consider network latency when connecting to remote HTTP providers

## Graceful Shutdown

UMCP handles shutdown signals to cleanly close all connections. From `serve.ts:53-81`:

```typescript theme={null}
function waitForShutdownSignal(logger: Logger, includeStdinClose = true): Promise<string> {
  return new Promise((resolve) => {
    let settled = false;

    const finish = (reason: string) => {
      if (settled) {
        return;
      }
      settled = true;
      process.off("SIGINT", onSigInt);
      process.off("SIGTERM", onSigTerm);
      if (includeStdinClose) {
        process.stdin.off("close", onStdinClose);
      }
      logger.info("shutdown.signal", "Shutdown signal received", { reason });
      resolve(reason);
    };

    const onSigInt = () => finish("SIGINT");
    const onSigTerm = () => finish("SIGTERM");
    const onStdinClose = () => finish("stdin-close");

    process.on("SIGINT", onSigInt);
    process.on("SIGTERM", onSigTerm);
    if (includeStdinClose) {
      process.stdin.on("close", onStdinClose);
    }
  });
}
```

**Signals handled:**

* **SIGINT**: Ctrl+C in terminal
* **SIGTERM**: Standard termination signal
* **stdin close**: When stdin pipe closes (stdio mode only)
