fix(ai): MCP bridge dropped the API path prefix on relative URLs#348
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug — MCP bridge dropped the
/apiprefix (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
TextStackApiClientbuilt relative request URLs with a leading slash (/search,/me/highlights/{id},/books/{id}/ask,/auth/device/*) whileHttpClient.BaseAddresshad no trailing slash. .NET resolves a leading-slash relative URI from the host root, dropping the/apiprefix → the bridge hithttps://textstack.app/search(the SPA →301/HTML) instead ofhttps://textstack.app/api/search→JsonException→ the clean "upstream unavailable" IsError.Why it hid: unit tests use a fake
HttpMessageHandlerthat ignoresBaseAddress, and the prod Docker deployment setsTEXTSTACK_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/apicase.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 typedTextStackApiClientand 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_booksagainst prod returns real results (Dracula/ Bram Stoker).Regression test —
tests/TextStack.UnitTests/McpApiPrefixTests.cs(9)A capturing handler wired through the real
TextStackApiClientwith a prefixedBaseAddress(http://localhost/api/) asserts the resolved absoluteRequestUripreserves the prefix:search_books→http://localhost/api/search?...list_my_highlights→http://localhost/api/me/highlights/{id}ask_book→http://localhost/api/books/{id}/askhttp://localhost/api/auth/device/codeBaseUrihelper: 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.UnitTests→ 629 pass (+9); existing MCP tests + StudyBuddy set-equality greendotnet build/dotnet format(whitespace+style) → clean🤖 Generated with Claude Code