Authorising ferrlabs-track and ferrlabs-growth over the standard OAuth flow succeeds, but every authenticated tool call then fails. The token is minted by api.ferrlabs.com (the authorization server named in each /.well-known/oauth-protected-resource), relayed by the MCP as x-api-token + Authorization: Bearer, and rejected by the product API.
Reproduction
Authorize the server, then call any authenticated tool:
ferrlabs-track -> list_projects -> 401
ferrlabs-growth -> list_sites -> 401
Both surface as the UnauthorizedError raised in packages/mcp-core/src/api-client.ts:50-56:
Stored token rejected by the FerrLabs API (likely revoked, expired, or issued
under an incompatible format). The token has been cleared - retry the call to
trigger a fresh OAuth login.
Retrying does not help: a fresh authorization yields another token of the same format, which is rejected the same way.
What is not the cause
- Not a client misconfiguration. The stored credential is a valid 61-character access token with a
clientId, written by a completed authorization-code flow.
- Not an outage.
api.ferrtrack.com/health and api.ferrgrowth.com/health both return 200.
- Not the MCP transport.
ferrlabs against api.ferrlabs.com works on the same flow and returns real data, and ferrlabs-fleet gets past auth to a 404, so the bearer relay itself is correct.
Likely root cause
FerrTrack and FerrGrowth verify sessions with a shared HS256 secret rather than the format the FerrLabs authorization server issues. FerrVault is the EdDSA reference implementation. The authorization server and the product APIs therefore disagree on the token format, and the OAuth flow can never produce a token these APIs accept.
That makes the current sub-MCP OAuth path unusable for both products regardless of client configuration.
Scope
The fix is unlikely to land in this repo. It belongs to the session-verification code shared through Kit and to the product APIs. Filing here because this is where the failure surfaces and where the sub-MCPs are documented as working. Verify ferrlabs-vault too, it was not exercised.
Note
Each 401 also triggers #239, which clears the server-side persisted token even though the credential came from a per-request bearer. Reproducing this issue reproduces that one.
Authorising
ferrlabs-trackandferrlabs-growthover the standard OAuth flow succeeds, but every authenticated tool call then fails. The token is minted byapi.ferrlabs.com(the authorization server named in each/.well-known/oauth-protected-resource), relayed by the MCP asx-api-token+Authorization: Bearer, and rejected by the product API.Reproduction
Authorize the server, then call any authenticated tool:
ferrlabs-track->list_projects-> 401ferrlabs-growth->list_sites-> 401Both surface as the
UnauthorizedErrorraised inpackages/mcp-core/src/api-client.ts:50-56:Retrying does not help: a fresh authorization yields another token of the same format, which is rejected the same way.
What is not the cause
clientId, written by a completed authorization-code flow.api.ferrtrack.com/healthandapi.ferrgrowth.com/healthboth return 200.ferrlabsagainstapi.ferrlabs.comworks on the same flow and returns real data, andferrlabs-fleetgets past auth to a 404, so the bearer relay itself is correct.Likely root cause
FerrTrack and FerrGrowth verify sessions with a shared HS256 secret rather than the format the FerrLabs authorization server issues. FerrVault is the EdDSA reference implementation. The authorization server and the product APIs therefore disagree on the token format, and the OAuth flow can never produce a token these APIs accept.
That makes the current sub-MCP OAuth path unusable for both products regardless of client configuration.
Scope
The fix is unlikely to land in this repo. It belongs to the session-verification code shared through Kit and to the product APIs. Filing here because this is where the failure surfaces and where the sub-MCPs are documented as working. Verify
ferrlabs-vaulttoo, it was not exercised.Note
Each 401 also triggers #239, which clears the server-side persisted token even though the credential came from a per-request bearer. Reproducing this issue reproduces that one.