feat: accept an OAuth bearer token, not only an API key - #17
Merged
Conversation
The client could only send `X-API-Key`. The API has accepted `Authorization: Bearer` for a while — it is how the ChatGPT path authenticates — so the SDK was the piece that could not speak a credential the server already understood. That gap blocks anything acting on a user's behalf. A remote MCP server receives the caller's OAuth token and should hand it straight to the API, so the token is verified in one place instead of the crypto and the JWKS living in two. Without this it cannot: putting a JWT in `api_key` sends it as `X-API-Key`, where it is looked up as a key, misses, and 401s before the bearer path is reached. It fails in a way that points at the wrong thing. Both clients take `bearer_token=`, and exactly one credential is required. Refusing both rather than picking a winner: passing an api_key and a token together has no obvious right answer, and silently preferring one is how a service ends up authenticating as itself when it meant to act for a user. `core` and `user_id` are unchanged and orthogonal — brain selection has nothing to do with which credential opened the door. Ten tests pin the header each credential produces, because the difference is invisible until a request fails somewhere unrelated. The existing 35 still pass; nothing about `api_key` moved.
hugo8xx
added a commit
that referenced
this pull request
Aug 26, 2026
…led from (#18) khwan-mcp installs khwan from PyPI, not from this repository, so merging #17 changed nothing it can reach: the MCP layer still cannot call `Khwan(bearer_token=…)` until a version carrying it is published. The same shape as the engine pin between khwan-api and khwan-worker that came up twice today — merged is not deployed, and the boundary is a package registry rather than a git ref. Additive and backwards compatible: `api_key` behaves exactly as before, and the existing 35 tests pass untouched.
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.
The client could only send
X-API-Key. The API has acceptedAuthorization: Bearerfor a while — it is how the ChatGPT path authenticates — so the SDK was the piece that could not speak a credential the server already understood.Why it blocks the remote transport
A remote MCP server receives the caller's OAuth token and should hand it straight to the API, so the token is verified in one place rather than the crypto and the JWKS living in two.
Without this it cannot. Putting a JWT in
api_keysends it asX-API-Key, where the engine looks it up as a key, misses, and 401s — before the bearer path is ever reached. It fails pointing at the wrong thing.After
Both clients, sync and async.
coreanduser_idare unchanged and orthogonal — brain selection has nothing to do with which credential opened the door.Exactly one is required. Passing both is refused rather than resolved: there is no obvious winner, and silently preferring one is how a service ends up authenticating as itself when it meant to act for a user.
Tests
10 new, pinning the header each credential produces — that difference is invisible until a request fails somewhere unrelated. Including the one that matters most: a bearer client sends no
X-API-Keyat all.The existing 35 pass unchanged; nothing about
api_keymoved.README gains a short "Two ways to authenticate" section, since a credential nobody knows about is half-shipped.
Unblocks the
streamable_http_app()mount in khwan-mcp — the last piece of Phase 1.