fix(mcplocal): serve OpenAI-safe tool names on the wire #124

Merged
michal merged 2 commits from fix/wire-safe-tool-names into main 2026-08-25 19:23:20 +00:00
Owner

Problem

chat.ad.itaz.eu (LibreChat + deepseek-v4-flash) intermittently returned "This tool's MCP server is temporarily unavailable" for fetch_content / web_url_read while every backend was healthy.

Root cause (confirmed via LibreChat logs + the mcpd AuditEvent table): the proxy serves slash-namespaced tool names (websearch/fetch_content), which are invalid OpenAI-style function names. LibreChat forwards them verbatim to the LLM; the model intermittently emits only the tail (fetch_content), LibreChat's registry lookup fails locally, and the call never reaches mcplocal. Claude Code and the pi extension only dodge this because they sanitize names client-side.

Fix

WireNameCodec at the HTTP boundary (both /mcp and /projects/:name/mcp):

  • tools/list responses rewritten to wire-safe names (/_), exact-match reverse map, deterministic _2 suffix on collision
  • tools/call names mapped back to the canonical presented name before routing
  • unknown inbound names pass through unchanged → legacy slash-name clients and virtual tools (begin_session) keep working
  • everything inside the proxy (routing maps, plugins, favourites config pins, audit events) keeps canonical server/tool names
  • codecs keyed per project, outliving the router-cache TTL

Claude Code wire names change from all/websearch/fetch_content to all_websearch_fetch_content — which is exactly what Claude Code already displayed after its own sanitization, so nothing changes shape there.

Not in scope (follow-ups)

  • gate/begin_session prose still lists canonical slash names in its tool inventory; inbound slash names keep routing, so this is cosmetic
  • the librechat serverInstructions in kubernetes-deployment mention searxng/web_url_read in prose — worth aligning after this deploys

Tests

  • tests/wire-names.test.ts — 13 unit tests (sanitize, round-trip, collision, passthrough, no-mutation)
  • tests/mcp-endpoint-wire-names.test.ts — end-to-end over the real Streamable HTTP transport: lists websearch_fetch_content, routes the call back as websearch/fetch_content
  • full mcplocal suite: 779/779 green, tsc clean, eslint clean on touched files

🤖 Generated with Claude Code

https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir

## Problem chat.ad.itaz.eu (LibreChat + deepseek-v4-flash) intermittently returned *"This tool's MCP server is temporarily unavailable"* for `fetch_content` / `web_url_read` while every backend was healthy. Root cause (confirmed via LibreChat logs + the mcpd `AuditEvent` table): the proxy serves slash-namespaced tool names (`websearch/fetch_content`), which are invalid OpenAI-style function names. LibreChat forwards them verbatim to the LLM; the model intermittently emits only the tail (`fetch_content`), LibreChat's registry lookup fails locally, and the call never reaches mcplocal. Claude Code and the pi extension only dodge this because they sanitize names client-side. ## Fix `WireNameCodec` at the HTTP boundary (both `/mcp` and `/projects/:name/mcp`): - `tools/list` responses rewritten to wire-safe names (`/` → `_`), exact-match reverse map, deterministic `_2` suffix on collision - `tools/call` names mapped back to the canonical presented name before routing - unknown inbound names pass through unchanged → legacy slash-name clients and virtual tools (`begin_session`) keep working - everything inside the proxy (routing maps, plugins, favourites config pins, audit events) keeps canonical `server/tool` names - codecs keyed per project, outliving the router-cache TTL Claude Code wire names change from `all/websearch/fetch_content` to `all_websearch_fetch_content` — which is exactly what Claude Code already displayed after its own sanitization, so nothing changes shape there. ## Not in scope (follow-ups) - gate/begin_session prose still lists canonical slash names in its tool inventory; inbound slash names keep routing, so this is cosmetic - the librechat `serverInstructions` in kubernetes-deployment mention `searxng/web_url_read` in prose — worth aligning after this deploys ## Tests - `tests/wire-names.test.ts` — 13 unit tests (sanitize, round-trip, collision, passthrough, no-mutation) - `tests/mcp-endpoint-wire-names.test.ts` — end-to-end over the real Streamable HTTP transport: lists `websearch_fetch_content`, routes the call back as `websearch/fetch_content` - full mcplocal suite: 779/779 green, tsc clean, eslint clean on touched files 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir
michal added 1 commit 2026-08-25 19:17:49 +00:00
fix(mcplocal): serve OpenAI-safe tool names on the wire
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m22s
CI/CD / lint (pull_request) Successful in 2m40s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / smoke (pull_request) Failing after 2m3s
CI/CD / build (pull_request) Successful in 4m58s
CI/CD / publish (pull_request) Has been skipped
065ce02a60
The proxy namespaces tools as `server/tool` (and favourite-index presents
`favourite/<tool>` / `all/<server>/<tool>`). A `/` is not a valid character
in an OpenAI-style function name, so hosts that forward MCP tool names
verbatim as LLM function names depend on the model faithfully echoing an
illegal name. LibreChat did exactly that: deepseek-v4-flash intermittently
dropped the `websearch/` prefix, LibreChat's registry lookup failed, and it
reported "This tool's MCP server is temporarily unavailable" while nothing
was down — the calls never reached mcplocal at all (confirmed against the
AuditEvent table, 2026-08-25). Claude Code and the pi extension only dodge
this because they sanitize names client-side.

Fix at the HTTP boundary only: a WireNameCodec rewrites tools/list responses
to wire-safe names (`/` -> `_`, exact-match reverse map, deterministic
suffix on collision) and maps tools/call names back before routing. Wired
into both /mcp and /projects/:name/mcp. Everything inside the proxy —
routing maps, plugins, favourites config, audit events — keeps canonical
names, and unknown inbound names (legacy clients echoing slash names,
virtual tools) pass through unchanged, so existing clients keep working.

Codecs are keyed per project and outlive the router cache TTL so a client
can call a tool it listed minutes earlier; after a restart the client's
initialize-time tools/list repopulates the map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir
michal added 1 commit 2026-08-25 19:23:12 +00:00
test(mcplocal): end-to-end wire-name coverage on the project endpoint
Some checks failed
CI/CD / lint (pull_request) Successful in 1m18s
CI/CD / typecheck (pull_request) Successful in 1m21s
CI/CD / test (pull_request) Successful in 3m42s
CI/CD / smoke (pull_request) Failing after 3m5s
CI/CD / build (pull_request) Successful in 2m26s
CI/CD / publish (pull_request) Has been skipped
eb0e97e76e
Drives /projects/:name/mcp over the real Streamable HTTP transport with a
fake websearch upstream: tools/list must serve `websearch_fetch_content`
(every name matching the OpenAI function-name charset), calling that wire
name must reach the upstream as bare `fetch_content`, and a legacy client
echoing the canonical `websearch/fetch_content` must still route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JaFvfHrQyUKCGv6o3N2Wir
michal merged commit 7fbb827aa5 into main 2026-08-25 19:23:20 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michal/mcpctl#124