Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add SSE transport support for MCP protocol
- Create MCP server with executeQuery and listGraphs tools
- Add GET /api/sse endpoint for SSE connections
- Add POST /api/messages endpoint for client messages
- Update documentation with SSE configuration example

Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com>
  • Loading branch information
Claude and gkorland committed Feb 7, 2026
commit 0becdf3a53a471c5628b2fd215217a0b25e3e6df
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,41 @@ npm start

## API Endpoints

### REST API Endpoints

* `GET /api/mcp/metadata`: Get metadata about the FalkorDB instance and available capabilities
* `POST /api/mcp/context`: Execute queries against FalkorDB
* `GET /api/mcp/health`: Check server health
* `GET /api/mcp/graphs`: Returns the list of Graphs
*

### MCP Protocol Endpoints (SSE Transport)

* `GET /api/sse`: Server-Sent Events endpoint for MCP protocol connections
* `POST /api/messages`: Message endpoint for MCP protocol communication
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The POST /api/messages endpoint requires a sessionId query parameter (as implemented in sse.routes.ts). Document the required sessionId and note that clients should use the message URL provided/constructed by the SSE handshake (rather than calling /api/messages directly without parameters).

Suggested change
* `POST /api/messages`: Message endpoint for MCP protocol communication
* `POST /api/messages?sessionId={sessionId}`: Message endpoint for MCP protocol communication. This endpoint requires a `sessionId` query parameter; clients should use the message URL provided or constructed as part of the SSE handshake rather than calling `/api/messages` directly without parameters.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2c480e4. Updated README to document that sessionId is required and clients should use the URL provided by the SSE handshake.


## MCP Configuration

To use this server with MCP clients, you can add it to your MCP configuration:
### Using SSE Transport (Recommended for Remote Servers)

For MCP clients that support HTTP/SSE transport (like Cline), configure as follows:

```json
{
"mcpServers": {
"falkordb": {
"disabled": false,
"autoApprove": [],
"timeout": 60,
"url": "http://localhost:3000/api/sse",
"transportType": "http"
}
}
}
```

### Using Docker with stdio Transport

To use this server with MCP clients via stdio:

```json
{
Expand All @@ -95,7 +121,9 @@ To use this server with MCP clients, you can add it to your MCP configuration:
}
```

For client-side configuration:
### REST API Configuration

For client-side REST API configuration:

```json
{
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from 'express';
import { config } from './config';
import { mcpRoutes } from './routes/mcp.routes';
import { sseRoutes } from './routes/sse.routes';
import { authenticateMCP } from './middleware/auth.middleware';
import { falkorDBService } from './services/falkordb.service';

Expand All @@ -14,6 +15,9 @@ app.use(express.urlencoded({ extended: true }));
// Apply authentication to MCP routes
app.use('/api/mcp', authenticateMCP, mcpRoutes);

// SSE routes for MCP protocol (no authentication for SSE transport)
app.use('/api', sseRoutes);
Comment on lines 15 to +19
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mounting SSE MCP endpoints without authentication makes the new executeQuery tool remotely callable by anyone who can reach the server, which can expose/modify database data. If unauthenticated access is required for a specific client, consider adding an opt-in config (e.g., env flag) and/or a lightweight shared-secret check (header or query param) for /api/sse and /api/messages to reduce exposure in non-local deployments.

Suggested change
// Apply authentication to MCP routes
app.use('/api/mcp', authenticateMCP, mcpRoutes);
// SSE routes for MCP protocol (no authentication for SSE transport)
app.use('/api', sseRoutes);
// Lightweight auth for SSE MCP endpoints
const requireSseAuth = (req: any, res: any, next: any) => {
const nodeEnv = config.server.nodeEnv;
const allowUnauthenticated = process.env.ALLOW_UNAUTHENTICATED_SSE === 'true';
// Allow unauthenticated SSE only in development or when explicitly opted-in
if (nodeEnv === 'development' || allowUnauthenticated) {
return next();
}
const sharedSecret = process.env.SSE_SHARED_SECRET;
if (!sharedSecret) {
console.warn('SSE_SHARED_SECRET is not set; rejecting SSE request in non-development environment.');
return res.status(503).json({ error: 'SSE is not configured' });
}
const headerSecret = req.headers['x-sse-shared-secret'] as string | undefined;
const querySecret = (req.query && (req.query.sse_shared_secret as string | undefined)) || undefined;
const providedSecret = headerSecret || querySecret;
if (providedSecret === sharedSecret) {
return next();
}
return res.status(401).json({ error: 'Unauthorized' });
};
// Apply authentication to MCP routes
app.use('/api/mcp', authenticateMCP, mcpRoutes);
// SSE routes for MCP protocol (protected by lightweight auth)
app.use('/api', requireSseAuth, sseRoutes);

Copilot uses AI. Check for mistakes.

// Basic routes
app.get('/', (req, res) => {
res.json({
Expand Down
72 changes: 72 additions & 0 deletions src/routes/sse.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Router, Request, Response } from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { createMCPServer } from '../services/mcp.server';

const router = Router();

// Store active SSE transports by session ID
const transports: { [sessionId: string]: SSEServerTransport } = {};
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a plain object as a session map is more error-prone than Map (key collisions with inherited properties, less explicit APIs). Consider switching to const transports = new Map<string, SSEServerTransport>() and using set/get/delete for clearer and safer session management.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2c480e4. Replaced plain object with Map<string, SSEServerTransport> for clearer and safer session management.


/**
* SSE endpoint for MCP connections
* This establishes the Server-Sent Events connection for the MCP protocol
*/
router.get('/sse', async (req: Request, res: Response) => {
try {
// Create a new SSE transport
const transport = new SSEServerTransport('/api/messages', res);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message endpoint is hard-coded to /api/messages, which will break if the server is mounted under a different base path (or behind a reverse proxy path prefix). Consider deriving the endpoint from the incoming request (e.g., using req.baseUrl/req.originalUrl) so the transport advertises a correct, environment-agnostic message URL.

Suggested change
const transport = new SSEServerTransport('/api/messages', res);
const baseUrl = (req.baseUrl || '').replace(/\/$/, '');
const messagesEndpoint = `${baseUrl}/messages`;
const transport = new SSEServerTransport(messagesEndpoint, res);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2c480e4. The message endpoint is now derived from req.baseUrl to handle different base paths and reverse proxy configurations.

const sessionId = transport.sessionId;

// Store the transport
transports[sessionId] = transport;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a plain object as a session map is more error-prone than Map (key collisions with inherited properties, less explicit APIs). Consider switching to const transports = new Map<string, SSEServerTransport>() and using set/get/delete for clearer and safer session management.

Copilot uses AI. Check for mistakes.

// Clean up when the connection closes
res.on('close', () => {
delete transports[sessionId];
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a plain object as a session map is more error-prone than Map (key collisions with inherited properties, less explicit APIs). Consider switching to const transports = new Map<string, SSEServerTransport>() and using set/get/delete for clearer and safer session management.

Copilot uses AI. Check for mistakes.
console.log(`SSE connection closed for session: ${sessionId}`);
});

// Create a new MCP server instance for this connection
const server = createMCPServer();

// Connect the server to the transport
await server.connect(transport);

console.log(`SSE connection established for session: ${sessionId}`);
} catch (error) {
console.error('Error establishing SSE connection:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Failed to establish SSE connection' });
}
}
});

/**
* POST endpoint for MCP messages
* This receives messages from the MCP client
*/
router.post('/messages', async (req: Request, res: Response) => {
try {
const sessionId = req.query.sessionId as string;

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Express, req.query.sessionId can be string | string[] | undefined. Casting to string can cause incorrect behavior (e.g., an array becomes truthy but not a valid session key). Parse/validate the query param type explicitly (reject arrays or pick the first value) before using it as a lookup key.

Suggested change
const sessionId = req.query.sessionId as string;
const rawSessionId = req.query.sessionId;
let sessionId: string | undefined;
if (Array.isArray(rawSessionId)) {
sessionId = rawSessionId[0];
} else if (typeof rawSessionId === 'string') {
sessionId = rawSessionId;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2c480e4. Added explicit type checking to handle array, string, and undefined cases for the sessionId query parameter.

if (!sessionId) {
return res.status(400).json({ error: 'sessionId query parameter is required' });
}

const transport = transports[sessionId];
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a plain object as a session map is more error-prone than Map (key collisions with inherited properties, less explicit APIs). Consider switching to const transports = new Map<string, SSEServerTransport>() and using set/get/delete for clearer and safer session management.

Copilot uses AI. Check for mistakes.

if (!transport) {
return res.status(404).json({ error: 'No active session found for the provided sessionId' });
}

// Handle the POST message
await transport.handlePostMessage(req, res, req.body);
} catch (error) {
console.error('Error handling MCP message:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Failed to handle message' });
}
}
});

export const sseRoutes = router;
77 changes: 77 additions & 0 deletions src/services/mcp.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { falkorDBService } from './falkordb.service';

/**
* Create and configure the MCP server with FalkorDB tools
*/
export function createMCPServer(): McpServer {
const server = new McpServer({
name: 'FalkorDB MCP Server',
version: '1.0.0',
});

// Tool: Execute a Cypher query on a graph
server.tool(
'executeQuery',
{
graphName: z.string().describe('The name of the graph to query'),
query: z.string().describe('The Cypher query to execute'),
params: z.record(z.any()).optional().describe('Optional parameters for the query'),
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using z.any() removes meaningful validation and can mask unexpected inputs. Prefer z.unknown() (or a more specific schema if possible) for safer parsing, e.g. z.record(z.unknown()).optional().

Suggested change
params: z.record(z.any()).optional().describe('Optional parameters for the query'),
params: z.record(z.unknown()).optional().describe('Optional parameters for the query'),

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2c480e4. Changed from z.any() to z.record(z.string(), z.unknown()) for safer parameter validation.

},
async ({ graphName, query, params }) => {
try {
const result = await falkorDBService.executeQuery(graphName, query, params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error executing query: ${error.message}`,
},
],
isError: true,
};
Comment on lines +34 to +46
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning raw error.message to the MCP client can leak sensitive internal details (DB error strings, connection info, stack-adjacent messages). Consider logging the full error server-side and returning a more generic client-facing message (optionally with a request/session correlation id).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2c480e4. Error messages now log full details server-side while returning generic messages to clients to avoid leaking sensitive database information.

}
}
);

// Tool: List all available graphs
server.tool(
'listGraphs',
{},
async () => {
try {
const graphs = await falkorDBService.listGraphs();
return {
content: [
{
type: 'text',
text: JSON.stringify({ graphs }, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error listing graphs: ${error.message}`,
},
],
isError: true,
};
}
}
);

return server;
}