From 5c5751287285306a44f34937d096655d91c8b319 Mon Sep 17 00:00:00 2001 From: willtwilson Date: Sun, 8 Mar 2026 21:41:45 +0000 Subject: [PATCH 1/2] docs: add MCP SSE server guide, express.json warning, and autoApprove troubleshooting - Add 'Building a Custom MCP SSE Server' section with a prominent warning callout explaining why express.json() must not be used with MCP SSE transport: body-parser drains the IncomingMessage stream before SSEServerTransport can read it, causing HTTP 400 on every initialize handshake and preventing tools from loading. - Include correct and incorrect Express/TypeScript patterns with clear comments. - Add 'Troubleshooting: Agent generates placeholder text instead of calling tools' section explaining the two root causes: missing autoApprove (stalled confirmation dialog) and MCP server not connected / tools not loading. These two issues together caused zero MCP tools to load in a production LibreChat deployment. Documenting them here to help other self-hosters avoid the same pitfall. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../object_structure/mcp_servers.mdx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx b/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx index 72ea750a7..5ed0faa0a 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx +++ b/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx @@ -634,6 +634,105 @@ 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, **do not add `express.json()` middleware** to the same Express instance that handles MCP transport. + + +`express.json()` uses body-parser internally, which reads and drains the Node.js `IncomingMessage` request body stream. `SSEServerTransport.handlePostMessage()` in the `@modelcontextprotocol/sdk` also reads this same stream to parse MCP protocol messages. + +If `express.json()` runs first (as middleware always does), the stream is already exhausted when the MCP SDK tries to read it — resulting in **HTTP 400 errors on every MCP `initialize` handshake**, which silently prevents all tools from loading in LibreChat. + + +### 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(); + +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 +const app = express(); +app.use(express.json()); // ❌ This drains the request stream! + +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); +}); +``` + +### 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** + +Ensure the MCP server is connected and tools are loading. Check the LibreChat logs for MCP initialization errors. If you see HTTP 400 errors on `/messages`, see the [express.json() warning](#critical-do-not-use-expressjson-with-mcp-sse-transport) above. + +--- + ## References - [Model Context Protocol (MCP) Documentation](https://github.com/modelcontextprotocol) From 4c855a01f74a45e9f0ad97a9c1be00317500ce2b Mon Sep 17 00:00:00 2001 From: Will Wilson Date: Sun, 8 Mar 2026 22:08:55 +0000 Subject: [PATCH 2/2] docs: address review feedback on MCP SSE express.json guide - Nuance express.json() warning (scoped middleware is safe) - Fix broken callout anchor with proper heading - Add transport variable context to incorrect pattern - Fix heading capitalisation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../object_structure/mcp_servers.mdx | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx b/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx index 5ed0faa0a..c18dcab16 100644 --- a/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx +++ b/content/docs/configuration/librechat_yaml/object_structure/mcp_servers.mdx @@ -636,12 +636,27 @@ The `mcpServers` configurations allow LibreChat to dynamically interact with var ## Building a Custom MCP SSE Server -When implementing a custom MCP SSE server in Node.js/Express to use with LibreChat, **do not add `express.json()` middleware** to the same Express instance that handles MCP transport. +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. - -`express.json()` uses body-parser internally, which reads and drains the Node.js `IncomingMessage` request body stream. `SSEServerTransport.handlePostMessage()` in the `@modelcontextprotocol/sdk` also reads this same stream to parse MCP protocol messages. +### Critical: Do Not Use express.json() With MCP SSE Transport -If `express.json()` runs first (as middleware always does), the stream is already exhausted when the MCP SDK tries to read it — resulting in **HTTP 400 errors on every MCP `initialize` handshake**, which silently prevents all tools from loading in LibreChat. + +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 +``` ### Correct Pattern @@ -679,17 +694,19 @@ 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()); // ❌ This drains the request stream! +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); + await transport.handlePostMessage(req, res); // stream already exhausted → HTTP 400 }); ``` -### librechat.yaml configuration for a custom SSE server +### librechat.yaml Configuration for a Custom SSE Server ```yaml filename="librechat.yaml" mcpServers: