Skip to content

fix(ai): MCP bridge dropped the API path prefix on relative URLs#348

Merged
mrviduus merged 1 commit into
mainfrom
ai-049-fix-api-prefix
Jun 16, 2026
Merged

fix(ai): MCP bridge dropped the API path prefix on relative URLs#348
mrviduus merged 1 commit into
mainfrom
ai-049-fix-api-prefix

Conversation

@mrviduus

Copy link
Copy Markdown
Owner

Bug — MCP bridge dropped the /api prefix (caught while dogfooding)

Building the local MCP server and pointing it at the public API (TEXTSTACK_API_URL=https://textstack.app/api), every tool failed with "... failed: upstream unavailable".

Root cause: the bridge's TextStackApiClient built relative request URLs with a leading slash (/search, /me/highlights/{id}, /books/{id}/ask, /auth/device/*) while HttpClient.BaseAddress had no trailing slash. .NET resolves a leading-slash relative URI from the host root, dropping the /api prefix → the bridge hit https://textstack.app/search (the SPA → 301/HTML) instead of https://textstack.app/api/searchJsonException → the clean "upstream unavailable" IsError.

Why it hid: unit tests use a fake HttpMessageHandler that ignores BaseAddress, and the prod Docker deployment sets TEXTSTACK_API_URL=http://api:8080 (no path prefix), so the dropped-prefix never bit. It only surfaces against a prefixed base URL — exactly the local-dogfood / any reverse-proxied /api case.

Fix

  • McpBridgeCore.BaseUri(apiBaseUrl) — trailing-slash-normalizes the base so the prefix segment isn't treated as a file and replaced. Used by both the typed TextStackApiClient and the device-flow named client.
  • TextStackApiClient.Relative(url) — strips the leading slash on every relative request URI (PublicRequest + AuthorizedRequestAsync).
  • DeviceFlowTokenProvider — its 3 URLs (auth/device/code, auth/device/token, auth/refresh-mobile) lose their leading slash too.

Verified live: after the fix, search_books against prod returns real results (Dracula / Bram Stoker).

Regression test — tests/TextStack.UnitTests/McpApiPrefixTests.cs (9)

A capturing handler wired through the real TextStackApiClient with a prefixed BaseAddress (http://localhost/api/) asserts the resolved absolute RequestUri preserves the prefix:

  • search_bookshttp://localhost/api/search?...
  • list_my_highlightshttp://localhost/api/me/highlights/{id}
  • ask_bookhttp://localhost/api/books/{id}/ask
  • device flow → http://localhost/api/auth/device/code
  • BaseUri helper: appends one trailing slash, idempotent.

Proven to catch the bug: temporarily reverting the fix makes 7/9 fail with the exact signature (http://localhost/me/highlights/... vs expected .../api/...).

Verify

  • dotnet test tests/TextStack.UnitTests629 pass (+9); existing MCP tests + StudyBuddy set-equality green
  • dotnet build / dotnet format (whitespace+style) → clean

🤖 Generated with Claude Code

The bridge built relative request URLs with a LEADING slash (/search,
/me/highlights/{id}, /books/{id}/ask, /auth/device/*) and set
HttpClient.BaseAddress WITHOUT a trailing slash. When TEXTSTACK_API_URL
carries a path prefix (e.g. https://textstack.app/api), .NET resolves a
leading-slash relative URI from the host ROOT and drops the prefix → the
bridge hit https://textstack.app/search (the SPA, 301→HTML) instead of
.../api/search → JsonException → 'upstream unavailable'.

Silently fine in unit tests (fake handler ignores BaseAddress) and in the
prod-internal deployment (TEXTSTACK_API_URL=http://api:8080, no prefix) —
surfaced only when dogfooding Claude Desktop against the public /api host.

Fix: McpBridgeCore.BaseUri() trailing-slash-normalizes the base; the API
client's Relative() strips the leading slash on every relative URI; the
device-flow client + its 3 URLs get the same treatment. 9 regression
tests assert the resolved ABSOLUTE RequestUri preserves the /api prefix
(verified 7/9 fail without the fix). 629 unit green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mrviduus mrviduus merged commit 37e3c17 into main Jun 16, 2026
5 checks passed
@mrviduus mrviduus deleted the ai-049-fix-api-prefix branch June 16, 2026 23:22
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.

1 participant