Skip to content

docs: MCP SSE custom server guide — express.json() pitfall and autoApprove troubleshooting#526

Open
willtwilson wants to merge 2 commits intoLibreChat-AI:mainfrom
willtwilson:docs/mcp-sse-server-guide
Open

docs: MCP SSE custom server guide — express.json() pitfall and autoApprove troubleshooting#526
willtwilson wants to merge 2 commits intoLibreChat-AI:mainfrom
willtwilson:docs/mcp-sse-server-guide

Conversation

@willtwilson
Copy link

@willtwilson willtwilson commented Mar 8, 2026

Summary

This PR adds two practical troubleshooting sections to the MCP Servers Object Structure documentation, covering two issues that together caused zero MCP tools to load in a production LibreChat SSE deployment.


Part 1 — Express.json() causes HTTP 400 on every MCP initialize call

Root cause

Express.json() uses body-parser internally, which reads and drains the Node.js IncomingMessage request body stream. SSEServerTransport.handlePostMessage() in @modelcontextprotocol/sdk also reads this same stream to parse incoming MCP protocol messages.

When Express.json() is registered as middleware (which always runs first), the stream is exhausted before the MCP SDK can read it. This results in HTTP 400 errors on every MCP initialize handshake — silently preventing all tools from loading in LibreChat with no obvious error message in the UI.

What this PR adds

  • A prominent explaining why Express.json() must not be used on the same Express instance as MCP SSE transport
  • A correct TypeScript/Express pattern showing the right way to implement a custom SSE server
  • An incorrect pattern annotated to show exactly what goes wrong

Part 2 — Agent generates placeholder text instead of calling tools

Root cause

When AutoApprove is not configured, LibreChat shows a confirmation dialog before each tool call. In some agent configurations this dialog is never presented or resolved, causing the agent to stall and instead generate placeholder text like [Fetching weather data...].

What this PR adds

  • A troubleshooting section explaining the symptom and both root causes
  • The �utoApprove configuration snippet needed to bypass the confirmation dialog
  • A cross-reference to the Express.json() section above for MCP initialization failures

Impact

Both issues are subtle and hard to diagnose from LibreChat logs alone. These two sections document what I found after debugging a working MCP SSE server that appeared configured correctly but loaded zero tools.

… 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>
Copilot AI review requested due to automatic review settings March 8, 2026 21:42
@vercel
Copy link

vercel bot commented Mar 8, 2026

@willtwilson is attempting to deploy a commit to the LibreChat's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds troubleshooting guidance to the MCP Servers object-structure documentation to help diagnose “no tools loaded” scenarios in LibreChat SSE deployments.

Changes:

  • Documented an Express express.json() / request-stream consumption pitfall when implementing custom MCP SSE servers.
  • Added a troubleshooting section for agents generating placeholder text instead of invoking tools, including autoApprove configuration guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

<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
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.

```

**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
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.


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
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.

});
```

### 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
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

- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants