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..c18dcab16 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,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 + + +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 + +```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 +// ❌ 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** + +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)