Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/
6 changes: 3 additions & 3 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",
"argocd-mcp-http": {
"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);
};