Point a curated MCP server only at a credential of its own kind - #231
Open
beardthelion wants to merge 1 commit into
Open
Point a curated MCP server only at a credential of its own kind#231beardthelion wants to merge 1 commit into
beardthelion wants to merge 1 commit into
Conversation
beardthelion
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
August 24, 2026 18:37
Adding a server by URL checks which credential it is being pointed at. Adding one from the catalogue took the same field from the same request and stored it unread, so a credential of any kind could be attached to a curated server and spent by the refresh that runs before the add returns. The reach is narrower than the path beside it and worth saying so. The column is a foreign key, so an id naming nothing was already refused by the database, and the one entry in the catalogue is reached with each person's own account, whose OAuth client is registered through its own call and sent to a pinned address. What was reachable is a credential of the wrong kind being accepted and spent on behalf of somebody who never agreed to it, a malformed id arriving as a database error where a refusal belongs, and the whole shape returning with the first deployment-bearer entry a fork re-adds, which the catalogue invites. Which kind an entry takes is decided beside the entry, because it is a property of the vendor's auth rather than of the request. Both add paths then ask one function the same question, so a credential that does not exist and one of the wrong kind are still refused in the same words and the endpoint cannot be asked which ids are real. The curated route maps that refusal to a 400 rather than letting it surface as a 500. Re-adding a curated server no longer clears the credential it points at. That column holds the OAuth client registering one put there, and a re-add to change an instance host said nothing about it while clearing it anyway, leaving the row orphaned and everybody who had connected told there is no client registered.
beardthelion
force-pushed
the
fix/curated-server-credential-kind
branch
from
August 24, 2026 18:42
3cbba90 to
515d826
Compare
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.
Closes #229.
#214 gave
addCustomServera rule about which credential it may be pointed at.addServer, the curated path immediately above it, took the same field from the same admin request and stored it unread.The reach is narrower than the path beside it and worth stating rather than dressing up.
mcp_servers.credential_idis a real foreign key, so an id naming nothing was already refused by the database, and the one entry in the catalogue isuser-oauth, so what it decrypts goes to a pinned vendor address rather than one a caller chose. There is no exfiltration path here of the kind #214 closed. What was reachable is a credential of the wrong kind being accepted and spent on behalf of somebody who never agreed to it, a malformed id reaching auuidcolumn so the caller gets a database error where a refusal belongs, and the whole shape returning with the firstdeployment-bearerentry a fork re-adds, whichcatalogue.tsinvites in its own comment.Which kind an entry takes is decided beside the entry, since it is a property of the vendor's auth rather than of the request. Both add paths then ask one function the same question, so "wrong kind" and "no such credential" are still refused in the same words and the endpoint cannot be asked which ids are real. The curated route maps that refusal to a 400 rather than letting it surface as a 500.
One thing found while reading it, fixed here because the change would otherwise make it unfixable. The upsert set
credential_idunconditionally, andregisterOAuthClientkeeps the client it minted in that column, so re-adding a server to change an instance host cleared it: the credential row was orphaned with nothing to revoke it, and everybody who had connected was told the deployment has no client registered. Passing the id back was the only repair, and this PR refuses that on auser-oauthentry, so the pointer now survives a re-add that names no credential.Verification. The refusals were observed failing first. Three mutations, all red: deleting the guard call from
addServerfails three tests, restoring the clobbering upsert fails the OAuth-client test, and neutering the shared kind check fails all five of the custom-path tests from #214, so the extraction did not weaken code that was already reviewed. A route test covers 400 for a refusal, 400 for an unknown key, 500 for a failure that is not a refusal, and 403 for a non-admin. An integration test drivesPOST /api/plugins/serversthrough the real store against Postgres with nothing stubbed in between, and both mutations bite through that path too. Full suite 1437 pass, 0 fail, run twice for cross-run residue; build, typecheck and lint clean.Not covered, deliberately. A curated entry whose auth is
deployment-bearer, pointed at a wrong-kind credential, cannot be produced while the catalogue is frozen with oneuser-oauthentry, and I did not add a test-only seam tocreatePluginStoreto reach it. It is covered as two halves instead:serverCredentialKindis unit-tested for that branch, and the lookup itself is proven load-bearing by mutation on the custom path.Separate, not fixed here.
requireCredentialOfKindchecks the credential's kind but not itsprovider, so anymcpcredential works for any server. On the custom path, where the URL is caller-chosen, that is the sharper version of the problem. It is pre-existing and closing it would refuse configurations some deployments may already run, so it wants its own issue and its own call.