-
Notifications
You must be signed in to change notification settings - Fork 363
docs: MCP SSE custom server guide — express.json() pitfall and autoApprove troubleshooting #526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -634,6 +634,122 @@ The `mcpServers` configurations allow LibreChat to dynamically interact with var | |||||||||||
| - OAuth2 flow is supported for secure authentication with MCP servers | ||||||||||||
| - Users will be prompted to authenticate via OAuth before the MCP server can be used | ||||||||||||
|
|
||||||||||||
| ## Building a Custom MCP SSE Server | ||||||||||||
|
|
||||||||||||
| When implementing a custom MCP SSE server in Node.js/Express to use with LibreChat, take care with `express.json()` middleware on the routes that handle MCP transport. | ||||||||||||
|
|
||||||||||||
| ### Critical: Do Not Use express.json() With MCP SSE Transport | ||||||||||||
|
|
||||||||||||
| <Callout type="warning" title="express.json() and MCP SSE transport conflict"> | ||||||||||||
| If `express.json()` is registered globally (or on the `/messages` route specifically) and runs | ||||||||||||
| before the MCP SDK handler, it will consume the request body stream — leaving it exhausted when | ||||||||||||
| `SSEServerTransport.handlePostMessage()` tries to read it. This causes **HTTP 400 "stream is not | ||||||||||||
| readable"** errors on every MCP `initialize` handshake, silently preventing all tools from loading. | ||||||||||||
|
|
||||||||||||
| The safe pattern for Express apps that need JSON APIs alongside MCP SSE transport is to scope | ||||||||||||
| JSON parsing only to non-MCP routes: | ||||||||||||
|
|
||||||||||||
| ```typescript | ||||||||||||
| // ✅ Safe: JSON parsing scoped to /api routes only | ||||||||||||
| app.use('/api', express.json()); | ||||||||||||
|
|
||||||||||||
| // MCP routes — no body parser registered here | ||||||||||||
| app.get('/sse', handleSseConnection); | ||||||||||||
| app.post('/messages', handleMcpMessage); // MCP SDK reads raw stream | ||||||||||||
| ``` | ||||||||||||
| </Callout> | ||||||||||||
|
|
||||||||||||
| ### Correct Pattern | ||||||||||||
|
|
||||||||||||
| ```typescript | ||||||||||||
| import express from "express"; | ||||||||||||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||||||||||||
| import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; | ||||||||||||
|
|
||||||||||||
| const app = express(); | ||||||||||||
| // Do NOT add: app.use(express.json()); | ||||||||||||
| // Do NOT add: app.use(bodyParser.json()); | ||||||||||||
|
|
||||||||||||
| const transports = new Map<string, SSEServerTransport>(); | ||||||||||||
|
|
||||||||||||
| app.get("/sse", async (req, res) => { | ||||||||||||
| const transport = new SSEServerTransport("/messages", res); | ||||||||||||
| transports.set(transport.sessionId, transport); | ||||||||||||
| const server = buildMcpServer(); // your McpServer setup | ||||||||||||
| await server.connect(transport); | ||||||||||||
| res.on("close", () => transports.delete(transport.sessionId)); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| app.post("/messages", async (req, res) => { | ||||||||||||
| const id = req.query.sessionId as string; | ||||||||||||
| const transport = transports.get(id); | ||||||||||||
| if (!transport) { res.status(404).json({ error: "Session not found" }); return; } | ||||||||||||
| await transport.handlePostMessage(req, res); // reads raw stream internally | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| app.get("/health", (_req, res) => res.json({ status: "ok" })); | ||||||||||||
| app.listen(8932); | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ### Incorrect Pattern (causes HTTP 400 on all MCP calls) | ||||||||||||
|
|
||||||||||||
| ```typescript | ||||||||||||
| // ❌ Incorrect: express.json() consumes the body stream | ||||||||||||
| // (transport = an existing SSEServerTransport instance) | ||||||||||||
| const app = express(); | ||||||||||||
| app.use(express.json()); // drains IncomingMessage stream globally | ||||||||||||
|
|
||||||||||||
| app.post("/messages", async (req, res) => { | ||||||||||||
| // SSEServerTransport.handlePostMessage() tries to read stream here | ||||||||||||
| // but stream is already empty — HTTP 400 "stream is not readable" | ||||||||||||
| await transport.handlePostMessage(req, res); // stream already exhausted → HTTP 400 | ||||||||||||
| }); | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ### librechat.yaml Configuration for a Custom SSE Server | ||||||||||||
|
|
||||||||||||
| ```yaml filename="librechat.yaml" | ||||||||||||
| mcpServers: | ||||||||||||
| my-server: | ||||||||||||
| type: sse | ||||||||||||
| url: http://localhost:8932/sse | ||||||||||||
| # Optional: pre-approve tools to skip confirmation dialogs | ||||||||||||
| autoApprove: | ||||||||||||
| - tool_name_1 | ||||||||||||
| - tool_name_2 | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| --- | ||||||||||||
|
|
||||||||||||
| ## Troubleshooting: Agent generates placeholder text instead of calling tools | ||||||||||||
|
|
||||||||||||
| **Symptom:** When you ask the agent to use a tool (e.g. "check the weather"), instead of calling the tool it generates descriptive text like `[Fetching weather data...]` or `[Calling API...]` and stops. | ||||||||||||
|
|
||||||||||||
| **Root cause:** The agent model "sees" the tool described in its system prompt but the tool is not wired into the agent's actual tool execution context. The model generates confirmatory text as a placeholder instead of making a real tool call. | ||||||||||||
|
|
||||||||||||
| **Fix — Two required conditions:** | ||||||||||||
|
|
||||||||||||
| **Condition 1: `autoApprove` in MCP server config** | ||||||||||||
|
|
||||||||||||
| Without `autoApprove`, LibreChat shows a confirmation dialog before each tool call. In some agent configurations this dialog is never resolved, causing the agent to stall. Add the tool names to `autoApprove`: | ||||||||||||
|
|
||||||||||||
| ```yaml filename="librechat.yaml" | ||||||||||||
| mcpServers: | ||||||||||||
| my-server: | ||||||||||||
| type: sse | ||||||||||||
| url: http://localhost:8932/sse | ||||||||||||
| autoApprove: | ||||||||||||
| - search_web | ||||||||||||
| - get_weather | ||||||||||||
| - list_files | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| **Condition 2: Tools must be available in the agent session** | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| ## Critical: Do not use express.json() with MCP SSE transport | |
| When using an MCP server over SSE, do not apply `express.json()` (or other body-parsing middleware) to the SSE endpoint route, as it can break the streaming connection and cause HTTP 400 errors on `/messages`. Ensure your SSE route is defined before JSON body parsers or is excluded from them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 4c855a0 — replaced the Callout cross-reference (which doesn't generate an anchor) with a proper ## heading so it can be linked to reliably. The section is now anchored as ## Critical: Do Not Use express.json() With MCP SSE Transport.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the "Incorrect Pattern" snippet,
transportis referenced but never defined/initialized in the example. This makes the snippet harder to follow and can confuse readers trying to reproduce the failure; consider either showing wheretransportcomes from (as in the correct pattern) or renaming/commenting it as an existing transport instance.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 4c855a0 — added a comment making clear that
transportin the 'Incorrect Pattern' snippet refers to an existing SSEServerTransport instance as defined in the correct pattern above. Without that context it was genuinely confusing.