Skip to content

Transport

air supports three MCP transport types. Set via the transport config.

stdio

Standard input/output. Claude Desktop launches your server as a child process and communicates via stdin/stdout.

typescript
defineServer({
  transport: { type: 'stdio' },
});

No port needed. Logs are sent to stderr (stdout is reserved for MCP protocol).

SSE (Server-Sent Events)

HTTP-based remote connection. A separate MCP server instance is created per session.

typescript
defineServer({
  transport: { type: 'sse', port: 3510 },
});

SSE endpoints

MethodPathDescription
GET/sseOpen SSE stream (creates new session)
POST/message?sessionId=xxxSend message
GET/Server status JSON
OPTIONS*CORS preflight (auto-handled)

Client connection flow:

  1. GET /sse → establishes SSE connection, assigns session ID
  2. POST /message?sessionId=xxx → sends MCP messages
  3. On disconnect, session is automatically cleaned up

Terminal output:

[air] SSE server listening on port 3510
[air] SSE client connected (session: a1b2c3d4-...)
[air] SSE client disconnected (session: a1b2c3d4-...)

CORS

SSE transport sets CORS headers automatically:

  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Methods: GET, POST, OPTIONS
  • Access-Control-Allow-Headers: Content-Type

Use corsPlugin for custom configuration.

Streamable HTTP

Latest MCP transport spec. Single endpoint handles all communication.

typescript
defineServer({
  transport: { type: 'http', port: 3510 },
});
MethodDescription
POSTMCP message handling
GETServer status JSON
DELETESession termination

Each session gets a unique ID via crypto.randomUUID().

Cloudflare Workers

Edge deployment transport. No Node.js http server — instead, air exposes a fetch() handler compatible with the Workers runtime. MCP protocol is handled as JSON-RPC 2.0 directly (no SDK dependency needed at runtime).

typescript
import { defineServer, defineTool } from '@airmcp-dev/core';

const server = defineServer({
  name: 'my-edge-server',
  transport: { type: 'workers' },
  tools: [
    defineTool('hello', {
      params: { name: 'string' },
      handler: async ({ name }) => `Hello, ${name}!`,
    }),
  ],
});

export default {
  async fetch(request: Request, env: any): Promise<Response> {
    const url = new URL(request.url);

    // MCP endpoint
    if (request.method === 'POST' && url.pathname === '/') {
      return server.fetch!(request);
    }

    // Status
    if (request.method === 'GET' && url.pathname === '/') {
      return Response.json(server.status());
    }

    return new Response('Not Found', { status: 404 });
  },
};

server.fetch handles:

  • initialize → server info + capabilities
  • tools/list → tool schemas with annotations
  • tools/call → executes through the full middleware chain
  • resources/list, prompts/list → if registered
  • notifications/initialized → 204 No Content

TIP

Workers transport doesn't call server.start(). The server is ready immediately — just call server.fetch(request) in your Workers fetch handler.

WARNING

stdio and sse transports are not available in Workers. Only workers (and http for Node.js) support the Streamable HTTP protocol.

auto (default)

Omit type or set 'auto' to auto-detect based on environment:

typescript
defineServer({
  transport: { type: 'auto', port: 3510 },
});

Detection order:

  1. MCP_TRANSPORT env variable overrides everything (stdio, http, sse, workers)
  2. Workers environment detected (globalThis.caches exists, no process.stdin) → workers
  3. stdin is not TTY → stdio (MCP client spawned the process)
  4. stdin is TTY → http (developer running directly)
bash
# Force via env variable
MCP_TRANSPORT=sse node dist/index.js

# Piped → auto-detects stdio
echo '{}' | node dist/index.js

# Direct terminal → auto-detects http
node dist/index.js

TransportConfig

typescript
interface TransportConfig {
  type?: 'stdio' | 'sse' | 'http' | 'workers' | 'auto';  // Default: 'auto'
  port?: number;                               // HTTP/SSE port
  host?: string;                               // Default: 'localhost'
  basePath?: string;                           // Default: '/'
}

Port resolution order

Port is determined by:

  1. transport.port (explicit)
  2. dev.port (dev mode setting)
  3. 3100 (hardcoded default)
typescript
// transport.port takes priority
defineServer({ transport: { type: 'sse', port: 3510 } });  // → 3510

// Falls back to dev.port
defineServer({ transport: { type: 'sse' }, dev: { port: 4000 } });  // → 4000

// Falls back to 3100
defineServer({ transport: { type: 'sse' } });  // → 3100

Which to use?

TransportUse caseClient
stdioLocal tools, Claude Desktop directClaude Desktop, mcp-cli
sseRemote servers, existing MCP infra, mcp-proxy compatCursor, VS Code, mcp-proxy
httpNew deployments, Streamable HTTP spec, behind reverse proxyLatest MCP clients
workersCloudflare Workers edge deployment, serverlessPlayMCP, any MCP client via remote URL

TIP

When deploying behind a reverse proxy (Nginx, Cloudflare), use http transport. SSE requires special proxy config for long-lived connections (proxy_read_timeout 86400).

INFO

With stdio transport, all console.log output can mix into the MCP protocol stream. air's builtin logger automatically redirects to stderr.

Released under the Apache-2.0 License.