From 0341c29432d6309aba9e25fb3caa0ea8b32f0900 Mon Sep 17 00:00:00 2001 From: Atul Kanswal Date: Tue, 3 Mar 2026 16:58:12 +0530 Subject: [PATCH] Recent Codex/CLI startup failures showed MCP initialize handshakes failing when the server was expected to run over `stdio` but actually started in `http` mode. --- .env.example | 4 ++-- Dockerfile | 4 +--- README.md | 25 ++++++++++++++-------- docker-compose.yml | 6 +++--- lib/transport-mode.js | 35 ++++++++++++++++++++++++++++++ mcpServer.js | 20 ++++++++++++------ tests/transport-mode.test.js | 41 ++++++++++++++++++++++++++++++++++++ tools-manifest.json | 5 +++-- 8 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 lib/transport-mode.js create mode 100644 tests/transport-mode.test.js diff --git a/.env.example b/.env.example index a2a6880..311d89c 100644 --- a/.env.example +++ b/.env.example @@ -13,9 +13,9 @@ WEBEX_API_BASE_URL=https://webexapis.com/v1 # User Information (for reference) WEBEX_USER_EMAIL=your-email@company.com -# Optional: Server Mode (defaults to STDIO) +# Optional: Transport mode (defaults to stdio) # Set to 'http' for HTTP mode, 'sse' for SSE mode, or leave blank for STDIO -MCP_MODE= +TRANSPORT= # Optional: Port for HTTP/SSE mode (defaults to 3001) # Only used when running with --http or --sse flag diff --git a/Dockerfile b/Dockerfile index 3fc3ae0..0ba28a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,8 +24,6 @@ COPY --chown=mcp:nodejs . . # Set environment variables ENV NODE_ENV=production -# Set transport mode to HTTP for Smithery deployment -ENV TRANSPORT=http # Expose port for HTTP mode (configurable via PORT env var) EXPOSE $PORT @@ -37,5 +35,5 @@ USER mcp HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node -e "console.log('Health check passed')" || exit 1 -# Start the server (transport mode controlled by TRANSPORT env var) +# Start the server (transport mode controlled by env/CLI; default is STDIO) CMD ["node", "mcpServer.js"] diff --git a/README.md b/README.md index 4e941a7..3807c11 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,8 @@ Tools are organized by functionality: 1. **Build and run:** ```bash docker build -t webex-mcp-server . - docker run -i --rm --env-file .env webex-mcp-server + # STDIO transport (for Codex/Claude Desktop) + docker run -i --rm --env-file .env -e TRANSPORT=stdio webex-mcp-server ``` 2. **Using docker-compose:** @@ -132,7 +133,13 @@ Tools are organized by functionality: | `WEBEX_API_BASE_URL` | No | Webex API base URL | `https://webexapis.com/v1` | | `WEBEX_USER_EMAIL` | No | Your Webex email (for reference) | - | | `PORT` | No | Port for HTTP mode | `3001` | -| `MCP_MODE` | No | Transport mode (`stdio` or `http`) | `stdio` | +| `TRANSPORT` | No | Transport mode (`stdio`, `http`, or `sse`) | `stdio` | + +Transport precedence in `mcpServer.js` is: +1. CLI args (`--http`, `--sse`, `--stdio`, or bare values) +2. `TRANSPORT` + +Legacy env vars `MCP_MODE` and `MODE` are deprecated and ignored. ### Getting a Webex API Token @@ -162,6 +169,8 @@ Add to your Claude Desktop configuration: "WEBEX_USER_EMAIL", "-e", "WEBEX_API_BASE_URL", + "-e", + "TRANSPORT=stdio", "webex-mcp-server" ], "env": { @@ -196,7 +205,7 @@ The server supports MCP 2025-06-18 protocol with StreamableHTTP transport, inclu For STDIO mode: ```bash -docker run -i --rm --env-file .env webex-mcp-server +docker run -i --rm --env-file .env -e TRANSPORT=stdio webex-mcp-server ``` For HTTP mode: @@ -268,14 +277,12 @@ docker run -p 3001:3001 --rm --env-file .env webex-mcp-server --http ## Transport Modes -### STDIO Mode (Default) -The default transport mode for MCP clients like Claude Desktop: +### STDIO Mode +For MCP clients like Claude Desktop/Codex, use STDIO transport: ```bash # Start in STDIO mode node mcpServer.js -# or -npm start ``` ### HTTP Mode (StreamableHTTP) @@ -296,7 +303,7 @@ node mcpServer.js --http - **Protocol**: MCP 2025-06-18 with StreamableHTTP transport **Environment Variables:** -- `MCP_MODE=http` - Force HTTP mode +- `TRANSPORT=http` - Force HTTP mode - `PORT=3001` - Custom port (default: 3001) ### Smithery Integration @@ -404,4 +411,4 @@ MIT License - see LICENSE file for details - **Issues**: Report bugs and feature requests via GitHub issues - **Documentation**: See SETUP-COMPLETE.md for detailed setup instructions -- **Community**: Join discussions in the MCP community channels \ No newline at end of file +- **Community**: Join discussions in the MCP community channels diff --git a/docker-compose.yml b/docker-compose.yml index bbe57a8..ce6d636 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: # Server Configuration - NODE_ENV=production - PORT=${PORT:-3001} - - MCP_MODE=${MCP_MODE:-} + - TRANSPORT=${TRANSPORT:-stdio} ports: # Only needed for HTTP mode - uses PORT env var with fallback to 3001 - "${PORT:-3001}:${PORT:-3001}" @@ -19,7 +19,7 @@ services: tty: true restart: unless-stopped - # Example for HTTP mode using MCP_MODE + # Example for HTTP mode using TRANSPORT webex-mcp-server-http: build: . container_name: webex-mcp-server-http @@ -29,7 +29,7 @@ services: - WEBEX_USER_EMAIL=${WEBEX_USER_EMAIL} - NODE_ENV=production - PORT=${PORT:-3001} - - MCP_MODE=http + - TRANSPORT=http ports: - "${PORT:-3001}:${PORT:-3001}" restart: unless-stopped diff --git a/lib/transport-mode.js b/lib/transport-mode.js new file mode 100644 index 0000000..32f6f10 --- /dev/null +++ b/lib/transport-mode.js @@ -0,0 +1,35 @@ +const SUPPORTED_MODES = new Set(["stdio", "http", "sse"]); + +function normalizeMode(value) { + if (typeof value !== "string") { + return null; + } + + const normalized = value.trim().toLowerCase().replace(/^--/, ""); + if (!normalized) { + return null; + } + + return SUPPORTED_MODES.has(normalized) ? normalized : null; +} + +/** + * Resolve transport mode with deterministic precedence: + * CLI > TRANSPORT > stdio(default) + */ +export function resolveTransportMode(args = [], env = process.env) { + for (const arg of args) { + const mode = normalizeMode(arg); + if (mode) { + return { mode, source: "cli" }; + } + } + + const transportMode = normalizeMode(env.TRANSPORT); + + if (transportMode) { + return { mode: transportMode, source: "TRANSPORT" }; + } + + return { mode: "stdio", source: "default" }; +} diff --git a/mcpServer.js b/mcpServer.js index 618db58..4a924ec 100644 --- a/mcpServer.js +++ b/mcpServer.js @@ -8,6 +8,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { discoverTools } from "./lib/tools.js"; +import { resolveTransportMode } from "./lib/transport-mode.js"; import { randomUUID } from "crypto"; import { z } from "zod"; @@ -141,19 +142,24 @@ async function createMcpServer() { } async function run() { - // Transport mode detection following MCP 2025-06-18 patterns + // Transport mode detection with deterministic precedence const args = process.argv.slice(2); - const modeFromEnv = (process.env.TRANSPORT || process.env.MCP_MODE || process.env.MODE)?.toLowerCase(); - const isHTTP = args.includes('--http') || modeFromEnv === 'http'; - const isSSE = args.includes('--sse') || modeFromEnv === 'sse'; + const { mode: resolvedMode, source } = resolveTransportMode(args, process.env); - const mode = isHTTP ? 'HTTP' : 'STDIO'; - console.error(`[MCP Server] Mode: ${mode}`); + if (typeof process.env.MCP_MODE === "string" || typeof process.env.MODE === "string") { + console.error("[MCP Server] Deprecated env vars detected (MCP_MODE/MODE). They are ignored. Use TRANSPORT instead."); + } + + const isSSE = resolvedMode === 'sse'; + const isHTTP = resolvedMode === 'http' || isSSE; + + const mode = isSSE ? 'SSE (deprecated)' : (isHTTP ? 'HTTP' : 'STDIO'); + console.error(`[MCP Server] Mode: ${mode} (source: ${source})`); // Deprecation warning for SSE if (isSSE) { console.error('WARNING: SSE mode is deprecated in MCP 2025-06-18. Use StreamableHTTP instead.'); - console.error('Use --http flag or MCP_MODE=http for HTTP mode.'); + console.error('Use --http or TRANSPORT=http for HTTP mode.'); } if (isHTTP) { diff --git a/tests/transport-mode.test.js b/tests/transport-mode.test.js new file mode 100644 index 0000000..b68f06f --- /dev/null +++ b/tests/transport-mode.test.js @@ -0,0 +1,41 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { resolveTransportMode } from "../lib/transport-mode.js"; + +describe("Transport mode resolution", () => { + it("defaults to stdio when no mode is configured", () => { + const result = resolveTransportMode([], {}); + assert.strictEqual(result.mode, "stdio"); + assert.strictEqual(result.source, "default"); + }); + + it("gives CLI args highest precedence", () => { + const result = resolveTransportMode(["--http"], { + TRANSPORT: "stdio", + }); + + assert.strictEqual(result.mode, "http"); + assert.strictEqual(result.source, "cli"); + }); + + it("uses TRANSPORT when set", () => { + const result = resolveTransportMode([], { + TRANSPORT: "http", + }); + + assert.strictEqual(result.mode, "http"); + assert.strictEqual(result.source, "TRANSPORT"); + }); + + it("ignores legacy MCP_MODE and defaults to stdio when TRANSPORT is absent", () => { + const result = resolveTransportMode([], { MCP_MODE: "http" }); + assert.strictEqual(result.mode, "stdio"); + assert.strictEqual(result.source, "default"); + }); + + it("normalizes values and accepts bare CLI values", () => { + const result = resolveTransportMode(["STDIO"], { TRANSPORT: "http" }); + assert.strictEqual(result.mode, "stdio"); + assert.strictEqual(result.source, "cli"); + }); +}); diff --git a/tools-manifest.json b/tools-manifest.json index b9d1e10..0531534 100644 --- a/tools-manifest.json +++ b/tools-manifest.json @@ -118,13 +118,14 @@ "WEBEX_API_BASE_URL", "WEBEX_USER_EMAIL", "PORT", - "MCP_MODE", + "TRANSPORT", "ENABLED_TOOLS" ] }, "transport_modes": [ "stdio", - "http" + "http", + "sse" ], "features": { "tool_filtering": true,