From 9df8ab5af8b6bd3377e385490d47af3b8da34037 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Tue, 8 Sep 2026 19:09:59 +0000 Subject: [PATCH] Per-session Komodo creds from MCP client headers, remove baked-in env vars --- docker-compose.yml | 3 -- src/index.ts | 68 ++++++++++++++++++++++++++------------------ src/komodo-client.ts | 20 +++++++++---- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a0a5563..bca8702 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,5 @@ services: ports: - "9999:9800" environment: - - KOMODO_BASE_URL=${KOMODO_BASE_URL:-http://10.10.2.114:9120} - - KOMODO_API_KEY=${KOMODO_API_KEY} - - KOMODO_API_SECRET=${KOMODO_API_SECRET} - PORT=9800 restart: unless-stopped diff --git a/src/index.ts b/src/index.ts index b2fd693..f6451d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { randomUUID } from "node:crypto"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; -import { KomodoClient } from "./komodo-client.js"; +import { KomodoClient, type KomodoCredentials } from "./komodo-client.js"; import { listInputSchema, handleList } from "./tools/list.js"; import { getInputSchema, handleGet } from "./tools/get.js"; import { createInputSchema, handleCreate } from "./tools/create.js"; @@ -18,13 +18,28 @@ import { copyInputSchema, handleCopy } from "./tools/copy.js"; import { renameInputSchema, handleRename } from "./tools/rename.js"; const PORT = parseInt(process.env.PORT || "9800", 10); -const client = new KomodoClient(); -const transports: Record = {}; -function createServerInstance(): McpServer { +interface Session { + transport: StreamableHTTPServerTransport; + client: KomodoClient; +} +const sessions: Record = {}; + +function extractCredentials(req: IncomingMessage): KomodoCredentials { + const h = (name: string) => (req.headers[name] as string) || undefined; + return { + baseUrl: h("x-komodo-base-url"), + apiKey: h("x-komodo-api-key"), + apiSecret: h("x-komodo-api-secret"), + username: h("x-komodo-username"), + password: h("x-komodo-password"), + }; +} + +function createServerInstance(client: KomodoClient): McpServer { const server = new McpServer({ name: "komodo-mcp-server", - version: "2.1.0", + version: "2.2.0", }); server.registerTool("komodo_list", { @@ -41,7 +56,7 @@ function createServerInstance(): McpServer { server.registerTool("komodo_create", { description: - "Create a new Komodo resource. resource_type: stack, build, server, procedure, deployment, alerter, image_registry_account, sync_resource, tag, action, repo, builder, swarm, variable, user_group, git_provider_account. Provide params object with Create* endpoint fields.", + "Create a new Komodo resource. resource_type: stack, build, server, procedure, deployment, alerter, image_registry_account, sync_resource, user, tag, action, repo, builder, swarm, variable, user_group, git_provider_account. Provide params object with Create* endpoint fields.", inputSchema: createInputSchema, }, async (args) => handleCreate(args, client)); @@ -125,12 +140,11 @@ function parseBody(req: IncomingMessage): Promise { } const httpServer = createServer(async (req, res) => { - // CORS headers res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.setHeader( "Access-Control-Allow-Headers", - "Content-Type, Authorization, mcp-session-id, X-Api-Key, X-Api-Secret", + "Content-Type, Authorization, mcp-session-id, X-Komodo-Base-Url, X-Komodo-Api-Key, X-Komodo-Api-Secret, X-Komodo-Username, X-Komodo-Password", ); if (req.method === "OPTIONS") { @@ -139,47 +153,47 @@ const httpServer = createServer(async (req, res) => { return; } - // Health check if (req.method === "GET" && req.url === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "ok", name: "komodo-mcp-server", version: "2.1.0" })); + res.end(JSON.stringify({ status: "ok", name: "komodo-mcp-server", version: "2.2.0" })); return; } - // MCP endpoint if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) { const sessionId = req.headers["mcp-session-id"] as string | undefined; if (req.method === "POST") { const body = await parseBody(req); - // Check if this is an initialize request (no existing session) const isInitialize = !sessionId || - !transports[sessionId] || + !sessions[sessionId] || (body && typeof body === "object" && "method" in body && body.method === "initialize"); - if (isInitialize && (!sessionId || !transports[sessionId])) { + if (isInitialize && (!sessionId || !sessions[sessionId])) { + const creds = extractCredentials(req); + const client = new KomodoClient(creds); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sid) => { - transports[sid] = transport; + sessions[sid] = { transport, client }; }, }); transport.onclose = () => { const sid = transport.sessionId; - if (sid) delete transports[sid]; + if (sid) delete sessions[sid]; }; - const server = createServerInstance(); + const server = createServerInstance(client); await server.connect(transport); await transport.handleRequest(req, res, body); return; } - if (sessionId && transports[sessionId]) { - const transport = transports[sessionId]; + if (sessionId && sessions[sessionId]) { + const { transport } = sessions[sessionId]; await transport.handleRequest(req, res, body); return; } @@ -195,13 +209,13 @@ const httpServer = createServer(async (req, res) => { return; } - if (req.method === "GET" && sessionId && transports[sessionId]) { - await transports[sessionId].handleRequest(req, res); + if (req.method === "GET" && sessionId && sessions[sessionId]) { + await sessions[sessionId].transport.handleRequest(req, res); return; } - if (req.method === "DELETE" && sessionId && transports[sessionId]) { - await transports[sessionId].handleRequest(req, res); + if (req.method === "DELETE" && sessionId && sessions[sessionId]) { + await sessions[sessionId].transport.handleRequest(req, res); return; } @@ -223,15 +237,15 @@ const httpServer = createServer(async (req, res) => { httpServer.listen(PORT, "0.0.0.0", () => { console.log(`Komodo MCP Server listening on http://0.0.0.0:${PORT}/mcp`); console.log(`Health check: http://0.0.0.0:${PORT}/health`); - console.log(`Komodo URL: ${process.env.KOMODO_BASE_URL || process.env.KOMODO_URL || "http://10.10.2.114:9120"}`); + console.log(`Komodo credentials: from client headers (X-Komodo-*) or env vars`); }); process.on("SIGINT", async () => { console.log("\nShutting down..."); - for (const sid of Object.keys(transports)) { + for (const sid of Object.keys(sessions)) { try { - await transports[sid].close(); - delete transports[sid]; + await sessions[sid].transport.close(); + delete sessions[sid]; } catch {} } httpServer.close(); diff --git a/src/komodo-client.ts b/src/komodo-client.ts index e3151ef..6c0f9d1 100644 --- a/src/komodo-client.ts +++ b/src/komodo-client.ts @@ -9,6 +9,14 @@ type Update = Types.Update; type Route = "read" | "write" | "execute"; +export interface KomodoCredentials { + baseUrl?: string; + apiKey?: string; + apiSecret?: string; + username?: string; + password?: string; +} + export class KomodoClient { private baseUrl: string; private username: string; @@ -18,13 +26,13 @@ export class KomodoClient { private token: string | null = null; private useApiKey: boolean; - constructor() { + constructor(creds?: KomodoCredentials) { this.baseUrl = - process.env.KOMODO_BASE_URL || process.env.KOMODO_URL || "http://10.10.2.114:9120"; - this.username = process.env.KOMODO_USERNAME || process.env.KOMODO_INIT_ADMIN_USERNAME || ""; - this.password = process.env.KOMODO_PASSWORD || process.env.KOMODO_INIT_ADMIN_PASSWORD || ""; - this.apiKey = process.env.KOMODO_API_KEY || null; - this.apiSecret = process.env.KOMODO_API_SECRET || null; + creds?.baseUrl || process.env.KOMODO_BASE_URL || process.env.KOMODO_URL || "http://10.10.2.114:9120"; + this.username = creds?.username || process.env.KOMODO_USERNAME || process.env.KOMODO_INIT_ADMIN_USERNAME || ""; + this.password = creds?.password || process.env.KOMODO_PASSWORD || process.env.KOMODO_INIT_ADMIN_PASSWORD || ""; + this.apiKey = creds?.apiKey || process.env.KOMODO_API_KEY || null; + this.apiSecret = creds?.apiSecret || process.env.KOMODO_API_SECRET || null; this.useApiKey = !!(this.apiKey && this.apiSecret); }