Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# ide
.idea/
4 changes: 2 additions & 2 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"servers": {
"argocd-mcp-sse": {
"type": "sse",
"url": "http://localhost:3000/sse",
"type": "http",
"url": "http://localhost:3000/mcp",
"headers": {
"x-argocd-base-url": "<argocd_url>",
"x-argocd-api-token": "<argocd_token>"
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 17 additions & 1 deletion src/cmd/cmd.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand All @@ -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();
};
80 changes: 80 additions & 0 deletions src/server/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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);
};