diff --git a/.gitignore b/.gitignore index 443c595..169f8a4 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# ide +.idea/ diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 15a4e97..855c55d 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,8 +1,8 @@ { "servers": { - "argocd-mcp-sse": { - "type": "sse", - "url": "http://localhost:3000/sse", + "argocd-mcp-http": { + "type": "http", + "url": "http://localhost:3000/mcp", "headers": { "x-argocd-base-url": "", "x-argocd-api-token": "" diff --git a/README.md b/README.md index 960ad98..955cb3c 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,11 @@ pnpm install 3. Start the development server with hot reloading enabled: ```bash -# For SSE mode with hot reloading +# For HTTP mode with hot reloading pnpm run dev + +# For SSE mode with hot reloading +pnpm run dev-sse ``` Once the server is running, you can utilize the MCP server within Visual Studio Code or other MCP client. diff --git a/package.json b/package.json index 1165530..31f49f7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "LICENSE" ], "scripts": { - "dev": "tsx watch src/index.ts sse", + "dev": "tsx watch src/index.ts http", + "dev-sse": "tsx watch src/index.ts sse", "lint": "eslint src/**/*.ts --no-warn-ignored", "lint:fix": "eslint src/**/*.ts --fix", "build": "tsup", diff --git a/src/cmd/cmd.ts b/src/cmd/cmd.ts index 6fef467..4d6b2c2 100644 --- a/src/cmd/cmd.ts +++ b/src/cmd/cmd.ts @@ -1,6 +1,10 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { connectStdioTransport, connectSSETransport } from '../server/transport.js'; +import { + connectStdioTransport, + connectHttpTransport, + connectSSETransport +} from '../server/transport.js'; export const cmd = () => { const exe = yargs(hideBin(process.argv)); @@ -24,5 +28,17 @@ export const cmd = () => { ({ port }) => connectSSETransport(port) ); + exe.command( + 'http', + 'Start ArgoCD MCP server using Http Stream.', + (yargs) => { + return yargs.option('port', { + type: 'number', + default: 3000 + }); + }, + ({ port }) => connectHttpTransport(port) + ); + exe.demandCommand().parseSync(); }; diff --git a/src/server/transport.ts b/src/server/transport.ts index 8f5aeee..8461a17 100644 --- a/src/server/transport.ts +++ b/src/server/transport.ts @@ -3,6 +3,9 @@ import express from 'express'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { logger } from '../logging/logging.js'; import { createServer } from './server.js'; +import { randomUUID } from 'node:crypto'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; export const connectStdioTransport = () => { const server = createServer({ @@ -45,3 +48,80 @@ export const connectSSETransport = (port: number) => { logger.info(`Connecting to SSE transport on port: ${port}`); app.listen(port); }; + +export const connectHttpTransport = (port: number) => { + const app = express(); + app.use(express.json()); + + const httpTransports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + + app.post('/mcp', async (req, res) => { + const sessionIdFromHeader = req.headers['mcp-session-id'] as string | undefined; + let transport: StreamableHTTPServerTransport; + + if (sessionIdFromHeader && httpTransports[sessionIdFromHeader]) { + transport = httpTransports[sessionIdFromHeader]; + } else if (!sessionIdFromHeader && isInitializeRequest(req.body)) { + const argocdBaseUrl = (req.headers['x-argocd-base-url'] as string) || ''; + const argocdApiToken = (req.headers['x-argocd-api-token'] as string) || ''; + + if (argocdBaseUrl == '' || argocdApiToken == '') { + res + .status(400) + .send('x-argocd-base-url and x-argocd-api-token must be provided in headers.'); + return; + } + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + httpTransports[newSessionId] = transport; + } + }); + + transport.onclose = () => { + if (transport.sessionId) { + delete httpTransports[transport.sessionId]; + } + }; + + const server = createServer({ + argocdBaseUrl, + argocdApiToken + }); + + await server.connect(transport); + } else { + const errorMsg = sessionIdFromHeader + ? `Invalid or expired session ID: ${sessionIdFromHeader}` + : 'Bad Request: Not an initialization request and no valid session ID provided.'; + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: errorMsg + }, + id: req.body?.id !== undefined ? req.body.id : null + }); + return; + } + + await transport.handleRequest(req, res, req.body); + }); + + const handleSessionRequest = async (req: express.Request, res: express.Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !httpTransports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + const transport = httpTransports[sessionId]; + await transport.handleRequest(req, res); + }; + + app.get('/mcp', handleSessionRequest); + app.delete('/mcp', handleSessionRequest); + + logger.info(`Connecting to Http Stream transport on port: ${port}`); + app.listen(port); +};