Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Callout type="warning" title="Critical: Do NOT use express.json() with MCP SSE 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.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

The statement "as middleware always does" is misleading in Express: middleware runs in registration order, and can be scoped to specific paths. Consider rephrasing to clarify the real failure mode (e.g., when express.json() is applied to /messages or registered before the MCP routes), and mention the safe alternative of scoping JSON parsing to non-MCP routes (e.g., app.use('/api', express.json())).

Suggested change
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 on the same routes as your MCP transport (for example on `/messages`) and runs before the MCP SDK handler, it will consume the request body stream so it 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.
To safely combine JSON APIs with MCP SSE transport in the same Express app, scope JSON parsing only to your non-MCP routes, for example: `app.use("/api", express.json());`.

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

Choose a reason for hiding this comment

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

You're right that 'as middleware always does' was too absolute — updated in 4c855a0. The revised text correctly scopes the failure: it only occurs when express.json() is registered globally or specifically on /messages before the MCP SDK handler runs. I've also added the safe alternative pattern (app.use('/api', express.json())) so users who need JSON APIs alongside MCP SSE transport in the same Express app can do so correctly.

</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
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"
Copy link

Copilot AI Mar 8, 2026

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, transport is 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 where transport comes from (as in the correct pattern) or renaming/commenting it as an existing transport instance.

Suggested change
// but stream is already empty — HTTP 400 "stream is not readable"
// but stream is already empty — HTTP 400 "stream is not readable"
// Assume `transport` is an existing SSEServerTransport instance, as in the correct pattern above.

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

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 transport in the 'Incorrect Pattern' snippet refers to an existing SSEServerTransport instance as defined in the correct pattern above. Without that context it was genuinely confusing.

await transport.handlePostMessage(req, res);
});
```

### librechat.yaml configuration for a custom SSE server
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

The heading "librechat.yaml configuration for a custom SSE server" uses sentence-case, while nearby headings in this file use Title Case (e.g., "MCP Server with OAuth Authentication", "Correct Pattern"). Consider capitalizing it for consistency.

Suggested change
### librechat.yaml configuration for a custom SSE server
### librechat.yaml Configuration for a Custom SSE Server

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

Choose a reason for hiding this comment

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

Fixed in 4c855a0 — updated to Title Case to match surrounding headings: ### 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**

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

This cross-reference anchor looks broken: the target is a <Callout ... title="...">, which typically does not generate a markdown heading id/anchor. Consider linking to an actual heading (e.g., the "Building a Custom MCP SSE Server" section) or adding a dedicated heading for the express.json() pitfall that can be reliably linked to.

Suggested change
## 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.

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

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.

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)
Expand Down
Loading