Skip to content

Commit 3fa5d9f

Browse files
committed
chore: bump version to 1.5.0 and document stateless HTTP mode
1 parent 4b3a566 commit 3fa5d9f

8 files changed

Lines changed: 55 additions & 135 deletions

File tree

LLM_GUIDANCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,6 @@ For application development, use the `mcp` and `google-genai` libraries to conne
355355

356356
Setup: `pip install google-genai mcp`
357357

358-
Implementation: Use an `SSEClientTransport` to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
358+
Implementation: Use a streamable HTTP transport in JSON response mode (e.g. `enableJsonResponse: true`) to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
359359

360360
This MCP enables powerful neuroscience research by providing programmatic access to one of the most comprehensive neuroanatomical databases available.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# VFB3-MCP Server
22

3-
A Model Context Protocol (MCP) server for interacting with VirtualFlyBrain (VFB) APIs. This server provides tools to query VFB data, run queries, and search for terms.
3+
A Model Context Protocol (MCP) server for interacting with VirtualFlyBrain (VFB) APIs. This server provides tools to query VFB data, run queries, and search for terms. In HTTP mode it runs statelessly (no session tracking), so any replica can handle any request and standard load balancing works.
44

55
## 🚀 Quick Start
66

@@ -111,7 +111,7 @@ For application development, use the `mcp` and `google-genai` libraries to conne
111111

112112
Setup: `pip install google-genai mcp`
113113

114-
Implementation: Use an `SSEClientTransport` to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
114+
Implementation: Use a streamable HTTP transport in JSON response mode (e.g. `enableJsonResponse: true`) to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
115115

116116
#### Testing the Connection
117117

TECHNICAL.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ The VFB3-MCP server supports two operational modes:
1414
- Compatible with Claude Desktop local MCP configuration
1515

1616
#### HTTP Mode (Production)
17-
- Express.js server with Server-Sent Events (SSE) for bidirectional communication
18-
- RESTful endpoints for MCP protocol
17+
- Express.js server with stateless JSON-over-HTTP request/response (no SSE)
18+
- RESTful endpoints for MCP protocol (POST / for MCP requests, GET/DELETE return 405 unless requesting HTML)
1919
- OAuth 2.0 metadata endpoints (returns 404 - no authentication required)
2020
- CORS enabled for web client access
2121

2222
### MCP Protocol Implementation
2323

2424
- Built using the official `@modelcontextprotocol/sdk`
25-
- Express transport for HTTP mode with SSE
25+
- Express transport for HTTP mode using stateless JSON-over-HTTP (no SSE)
2626
- Stdio transport for local development
27-
- Session management with UUID-based session IDs
27+
- Stateless HTTP mode (no session tracking / no session IDs)
28+
- GA4 analytics use a stable server-side client ID in HTTP mode (no per-session IDs)
2829

2930
## Infrastructure
3031

@@ -35,9 +36,9 @@ The VFB3-MCP server supports two operational modes:
3536
The production deployment runs on VFB's Rancher/Cattle Kubernetes infrastructure with:
3637

3738
- **Protocol**: HTTPS with automatic SSL certificate management
38-
- **Transport**: Server-Sent Events (SSE) for real-time bidirectional communication
39+
- **Transport**: Stateless JSON-over-HTTP request/response (no SSE)
3940
- **Authentication**: Open server (no authentication required)
40-
- **Load Balancing**: Kubernetes service with automatic scaling
41+
- **Load Balancing**: Kubernetes service with automatic scaling (stateless; no sticky sessions required)
4142
- **Resource Limits**: 512Mi memory, 500m CPU
4243
- **Security**: Non-root user (UID 1000), read-only filesystem
4344
- **MCP Endpoint**: `/` (root path)
@@ -227,7 +228,7 @@ For application development, use the `mcp` and `google-genai` libraries to conne
227228

228229
Setup: `pip install google-genai mcp`
229230

230-
Implementation: Use an `SSEClientTransport` to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
231+
Implementation: Use a streamable HTTP transport in JSON response mode (e.g. `enableJsonResponse: true`) to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
231232

232233
## Security
233234

@@ -255,7 +256,7 @@ While the server includes OAuth metadata endpoints for MCP SDK compatibility, au
255256
- **Console Output**: Structured logging to stdout/stderr
256257
- **Debug Mode**: Verbose logging with `MCP_DEBUG=true`
257258
- **Error Handling**: Comprehensive error logging with context
258-
- **Request Tracking**: Session and request ID logging
259+
- **Request Tracking**: Request ID logging (no session IDs in HTTP mode)
259260

260261
### Infrastructure Monitoring
261262

@@ -271,7 +272,7 @@ While the server includes OAuth metadata endpoints for MCP SDK compatibility, au
271272
- **API Caching**: VFB provides cached endpoints for performance
272273
- **Connection Pooling**: Axios configuration for efficient HTTP requests
273274
- **Memory Management**: Node.js memory limits and garbage collection
274-
- **Concurrent Requests**: Support for multiple simultaneous MCP sessions
275+
- **Concurrent Requests**: Support for multiple simultaneous MCP requests (stateless)
275276

276277
### Scalability
277278

examples.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ For application development, use the `mcp` and `google-genai` libraries to conne
149149

150150
Setup: `pip install google-genai mcp`
151151

152-
Implementation: Use an `SSEClientTransport` to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
152+
Implementation: Use a streamable HTTP transport in JSON response mode (e.g. `enableJsonResponse: true`) to connect to the VFB URL, list its tools, and pass their schemas to the Gemini model as Function Declarations.
153153

154154
## Docker Usage
155155
```bash

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vfb3-mcp",
3-
"version": "1.4.4",
3+
"version": "1.5.0",
44
"description": "MCP server for VirtualFlyBrain API integration",
55
"main": "index.js",
66
"scripts": {

server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "org.virtualflybrain/vfb3-mcp",
44
"title": "VirtualFlyBrain",
55
"description": "MCP server for Drosophila neuroscience data from VirtualFlyBrain",
6-
"version": "1.4.1",
6+
"version": "1.5.0",
77
"websiteUrl": "https://virtualflybrain.org",
88
"repository": {
99
"url": "https://github.com/Robbie1977/VFB3-MCP",

src/index.ts

Lines changed: 37 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import {
66
ErrorCode,
77
ListToolsRequestSchema,
88
McpError,
9-
isInitializeRequest,
109
} from '@modelcontextprotocol/sdk/types.js';
1110
import axios from 'axios';
1211
import cors from 'cors';
1312
import express from 'express';
1413
import { randomUUID } from 'node:crypto';
1514

16-
const VERSION = '1.4.4';
15+
const VERSION = '1.5.0';
1716

1817
// GA4 Analytics configuration
1918
const GA_MEASUREMENT_ID = process.env.GA_MEASUREMENT_ID || 'G-K7DDZVVXM7';
@@ -636,16 +635,14 @@ function getHtmlPage(): string {
636635

637636
async function runHttpMode() {
638637
const port = process.env.PORT || '3000';
639-
console.error(`MCP Debug: Starting VFB3-MCP server v${VERSION} in HTTP mode on port ${port}`);
638+
console.error(`MCP Debug: Starting VFB3-MCP server v${VERSION} in STATELESS HTTP mode on port ${port}`);
640639
console.error(`MCP Debug: GA4 analytics ${GA_ENABLED ? 'enabled' : 'disabled (set GA_MEASUREMENT_ID and GA_API_SECRET to enable)'}`);
640+
console.error('MCP Debug: Stateless mode — no session tracking, safe for multi-replica deployment');
641641

642642
const app = express();
643643
app.use(cors());
644644
app.use(express.json());
645645

646-
// Store active transports by session ID
647-
const transports: Record<string, StreamableHTTPServerTransport> = {};
648-
649646
// MCP Registry HTTP authentication endpoint
650647
app.get('/.well-known/mcp-registry-auth', (_req: any, res: any) => {
651648
const authProof = process.env.MCP_REGISTRY_AUTH;
@@ -656,101 +653,46 @@ async function runHttpMode() {
656653
}
657654
});
658655

659-
// Handle GET requests: browser HTML page or SSE streams
656+
// Handle GET requests: browser HTML page (SSE not supported in stateless mode)
660657
app.get('/', async (req: any, res: any) => {
661658
// Serve HTML page for browser requests
662659
if (req.headers.accept && req.headers.accept.includes('text/html')) {
663660
res.send(getHtmlPage());
664661
return;
665662
}
666663

667-
// SSE stream for existing MCP sessions
668-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
669-
if (!sessionId) {
670-
res.status(400).json({
671-
jsonrpc: '2.0',
672-
error: { code: -32000, message: 'Bad Request: Missing session ID' },
673-
id: null,
674-
});
675-
return;
676-
}
677-
if (!transports[sessionId]) {
678-
// Session not found — return 404 per MCP spec so client re-initializes
679-
res.status(404).json({
680-
jsonrpc: '2.0',
681-
error: { code: -32001, message: 'Session not found' },
682-
id: null,
683-
});
684-
return;
685-
}
686-
687-
const transport = transports[sessionId];
688-
await transport.handleRequest(req, res);
664+
// SSE streams are not supported in stateless mode
665+
res.writeHead(405).end(JSON.stringify({
666+
jsonrpc: '2.0',
667+
error: { code: -32000, message: 'Method not allowed. SSE streams are not supported in stateless mode.' },
668+
id: null,
669+
}));
689670
});
690671

691-
// Handle POST requests: MCP JSON-RPC messages
672+
// Handle POST requests: stateless — fresh server + transport per request
692673
app.post('/', async (req: any, res: any) => {
693-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
694-
695674
try {
696-
if (sessionId && transports[sessionId]) {
697-
// Existing session — reuse transport
698-
const transport = transports[sessionId];
699-
await transport.handleRequest(req, res, req.body);
700-
return;
701-
}
702-
703-
// Session ID provided but not found — return 404 per MCP spec so client re-initializes
704-
if (sessionId && !transports[sessionId]) {
705-
res.status(404).json({
706-
jsonrpc: '2.0',
707-
error: { code: -32001, message: 'Session not found' },
708-
id: null,
709-
});
710-
return;
711-
}
712-
713-
// Check for initialization request (handles both single and batch messages)
714-
const body = req.body;
715-
const hasInitRequest = Array.isArray(body)
716-
? body.some((msg: unknown) => isInitializeRequest(msg))
717-
: isInitializeRequest(body);
718-
719-
if (hasInitRequest) {
720-
// New initialization request — create transport and server
721-
const sessionIdHolder: { id?: string } = {};
722-
const transport = new StreamableHTTPServerTransport({
723-
sessionIdGenerator: () => randomUUID(),
724-
onsessioninitialized: (sid: string) => {
725-
console.error(`MCP Debug: Session initialized: ${sid}`);
726-
transports[sid] = transport;
727-
sessionIdHolder.id = sid;
728-
},
729-
});
730-
731-
transport.onclose = () => {
732-
const sid = transport.sessionId;
733-
if (sid && transports[sid]) {
734-
console.error(`MCP Debug: Session closed: ${sid}`);
735-
delete transports[sid];
736-
}
737-
};
738-
739-
// Connect a new MCP server to this transport
740-
const server = createServer(sessionIdHolder);
741-
await server.connect(transport);
675+
// Create a fresh server and transport for every request.
676+
// sessionIdGenerator: undefined = stateless mode — no session ID is
677+
// generated, returned, or validated. Any replica can handle any request.
678+
const server = createServer();
679+
const transport = new StreamableHTTPServerTransport({
680+
sessionIdGenerator: undefined,
681+
enableJsonResponse: true,
682+
});
742683

743-
// Handle the initialization request
744-
await transport.handleRequest(req, res, req.body);
745-
return;
746-
}
684+
transport.onerror = (error) => {
685+
console.error('MCP transport error:', error);
686+
};
747687

748-
// Invalid request — no valid session and not an initialize request
749-
res.status(400).json({
750-
jsonrpc: '2.0',
751-
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
752-
id: null,
688+
// Clean up when the HTTP connection closes
689+
res.on('close', () => {
690+
transport.close();
691+
server.close();
753692
});
693+
694+
await server.connect(transport);
695+
await transport.handleRequest(req, res, req.body);
754696
} catch (error) {
755697
console.error('MCP Debug: Error handling POST request:', error);
756698
if (!res.headersSent) {
@@ -763,36 +705,13 @@ async function runHttpMode() {
763705
}
764706
});
765707

766-
// Handle DELETE requests: session termination
767-
app.delete('/', async (req: any, res: any) => {
768-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
769-
if (!sessionId) {
770-
res.status(400).json({
771-
jsonrpc: '2.0',
772-
error: { code: -32000, message: 'Bad Request: Missing session ID' },
773-
id: null,
774-
});
775-
return;
776-
}
777-
if (!transports[sessionId]) {
778-
// Session not found — return 404 per MCP spec so client re-initializes
779-
res.status(404).json({
780-
jsonrpc: '2.0',
781-
error: { code: -32001, message: 'Session not found' },
782-
id: null,
783-
});
784-
return;
785-
}
786-
787-
try {
788-
const transport = transports[sessionId];
789-
await transport.handleRequest(req, res);
790-
} catch (error) {
791-
console.error('MCP Debug: Error handling DELETE request:', error);
792-
if (!res.headersSent) {
793-
res.status(500).send('Error processing session termination');
794-
}
795-
}
708+
// DELETE not supported in stateless mode (no sessions to terminate)
709+
app.delete('/', async (_req: any, res: any) => {
710+
res.writeHead(405).end(JSON.stringify({
711+
jsonrpc: '2.0',
712+
error: { code: -32000, message: 'Method not allowed. Session termination is not supported in stateless mode.' },
713+
id: null,
714+
}));
796715
});
797716

798717
app.listen(parseInt(port), () => {
@@ -814,4 +733,4 @@ if (mode === 'http') {
814733
runHttpMode().catch(console.error);
815734
} else {
816735
runStdioMode().catch(console.error);
817-
}
736+
}

0 commit comments

Comments
 (0)