Skip to content

Commit dd2dfc9

Browse files
authored
feat: add http stream support (#37)
* add http support * revise to separate endpoints * Rename sse to http for http example
1 parent 1dfcf3b commit dd2dfc9

File tree

6 files changed

+109
-6
lines changed

6 files changed

+109
-6
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,6 @@ dist
137137
.yarn/build-state.yml
138138
.yarn/install-state.gz
139139
.pnp.*
140+
141+
# ide
142+
.idea/

.vscode/mcp.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"servers": {
3-
"argocd-mcp-sse": {
4-
"type": "sse",
5-
"url": "http://localhost:3000/sse",
3+
"argocd-mcp-http": {
4+
"type": "http",
5+
"url": "http://localhost:3000/mcp",
66
"headers": {
77
"x-argocd-base-url": "<argocd_url>",
88
"x-argocd-api-token": "<argocd_token>"

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,11 @@ pnpm install
152152

153153
3. Start the development server with hot reloading enabled:
154154
```bash
155-
# For SSE mode with hot reloading
155+
# For HTTP mode with hot reloading
156156
pnpm run dev
157+
158+
# For SSE mode with hot reloading
159+
pnpm run dev-sse
157160
```
158161
Once the server is running, you can utilize the MCP server within Visual Studio Code or other MCP client.
159162

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"LICENSE"
3737
],
3838
"scripts": {
39-
"dev": "tsx watch src/index.ts sse",
39+
"dev": "tsx watch src/index.ts http",
40+
"dev-sse": "tsx watch src/index.ts sse",
4041
"lint": "eslint src/**/*.ts --no-warn-ignored",
4142
"lint:fix": "eslint src/**/*.ts --fix",
4243
"build": "tsup",

src/cmd/cmd.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import yargs from 'yargs';
22
import { hideBin } from 'yargs/helpers';
3-
import { connectStdioTransport, connectSSETransport } from '../server/transport.js';
3+
import {
4+
connectStdioTransport,
5+
connectHttpTransport,
6+
connectSSETransport
7+
} from '../server/transport.js';
48

59
export const cmd = () => {
610
const exe = yargs(hideBin(process.argv));
@@ -24,5 +28,17 @@ export const cmd = () => {
2428
({ port }) => connectSSETransport(port)
2529
);
2630

31+
exe.command(
32+
'http',
33+
'Start ArgoCD MCP server using Http Stream.',
34+
(yargs) => {
35+
return yargs.option('port', {
36+
type: 'number',
37+
default: 3000
38+
});
39+
},
40+
({ port }) => connectHttpTransport(port)
41+
);
42+
2743
exe.demandCommand().parseSync();
2844
};

src/server/transport.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import express from 'express';
33
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
44
import { logger } from '../logging/logging.js';
55
import { createServer } from './server.js';
6+
import { randomUUID } from 'node:crypto';
7+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
69

710
export const connectStdioTransport = () => {
811
const server = createServer({
@@ -45,3 +48,80 @@ export const connectSSETransport = (port: number) => {
4548
logger.info(`Connecting to SSE transport on port: ${port}`);
4649
app.listen(port);
4750
};
51+
52+
export const connectHttpTransport = (port: number) => {
53+
const app = express();
54+
app.use(express.json());
55+
56+
const httpTransports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
57+
58+
app.post('/mcp', async (req, res) => {
59+
const sessionIdFromHeader = req.headers['mcp-session-id'] as string | undefined;
60+
let transport: StreamableHTTPServerTransport;
61+
62+
if (sessionIdFromHeader && httpTransports[sessionIdFromHeader]) {
63+
transport = httpTransports[sessionIdFromHeader];
64+
} else if (!sessionIdFromHeader && isInitializeRequest(req.body)) {
65+
const argocdBaseUrl = (req.headers['x-argocd-base-url'] as string) || '';
66+
const argocdApiToken = (req.headers['x-argocd-api-token'] as string) || '';
67+
68+
if (argocdBaseUrl == '' || argocdApiToken == '') {
69+
res
70+
.status(400)
71+
.send('x-argocd-base-url and x-argocd-api-token must be provided in headers.');
72+
return;
73+
}
74+
75+
transport = new StreamableHTTPServerTransport({
76+
sessionIdGenerator: () => randomUUID(),
77+
onsessioninitialized: (newSessionId) => {
78+
httpTransports[newSessionId] = transport;
79+
}
80+
});
81+
82+
transport.onclose = () => {
83+
if (transport.sessionId) {
84+
delete httpTransports[transport.sessionId];
85+
}
86+
};
87+
88+
const server = createServer({
89+
argocdBaseUrl,
90+
argocdApiToken
91+
});
92+
93+
await server.connect(transport);
94+
} else {
95+
const errorMsg = sessionIdFromHeader
96+
? `Invalid or expired session ID: ${sessionIdFromHeader}`
97+
: 'Bad Request: Not an initialization request and no valid session ID provided.';
98+
res.status(400).json({
99+
jsonrpc: '2.0',
100+
error: {
101+
code: -32000,
102+
message: errorMsg
103+
},
104+
id: req.body?.id !== undefined ? req.body.id : null
105+
});
106+
return;
107+
}
108+
109+
await transport.handleRequest(req, res, req.body);
110+
});
111+
112+
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
113+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
114+
if (!sessionId || !httpTransports[sessionId]) {
115+
res.status(400).send('Invalid or missing session ID');
116+
return;
117+
}
118+
const transport = httpTransports[sessionId];
119+
await transport.handleRequest(req, res);
120+
};
121+
122+
app.get('/mcp', handleSessionRequest);
123+
app.delete('/mcp', handleSessionRequest);
124+
125+
logger.info(`Connecting to Http Stream transport on port: ${port}`);
126+
app.listen(port);
127+
};

0 commit comments

Comments
 (0)