This morning we made two calls to two public APIs. The first asks the repository of the Model Context Protocol Python SDK —the piece that connects AI agents to the systems they work on— how many security advisories it has published: it returns twelve. The second asks the same of OSV, the public database feeding half the dependency-scanner industry: it returns six. The six that are missing are the six most recent ones.
They were published between 28 September and 5 October 2026: eight days, six advisories. Three score 7.5 out of 10. Today, 12 October, none of the six exists in OSV, none exists in GitHub's global advisory database, none comes back from the PyPI API, and the only one with a CVE assigned —CVE-2026-59951— is not in the NVD either. They exist in exactly one place: the repository's security tab.
The sentence "the scanner did not flag anything" carries an assumption that almost never gets looked at squarely: that what is known about a flaw and what reaches your inventory are the same thing. Here they are not, and the difference can be measured. The project, incidentally, has done its part; we will come back to that at the end.
The check, so you can repeat it
Two unauthenticated calls, no account and no tooling. The first asks the repository; the second asks OSV:
# Advisories published on the repository
curl -s "https://api.github.com/repos/modelcontextprotocol/python-sdk/security-advisories" \
| jq -r '.[] | "\(.published_at[0:10]) \(.ghsa_id) CVE:\(.cve_id // "-")"' | sort
# Advisories OSV knows about for the package mcp
curl -s -X POST "https://api.osv.dev/v1/query" \
-d '{"package":{"name":"mcp","ecosystem":"PyPI"}}' | jq -r '.vulns[].id' | grep GHSA | sort
Twelve against six. The grep in the second call is not a trick to make the number come out: OSV returns twelve entries, but they are those same six advisories listed twice, once under their GHSA identifier and once under their PYSEC alias —PYSEC-2026-1616 and friends, each carrying the old GHSA in its aliases field. Six distinct vulnerabilities, all of them from before September. And here is the control that turns this into data rather than an anecdote: ask PyPI about an old version and about a recent one. The PyPI API is also the source pip-audit queries by default —its own documentation lists --vulnerability-service as (choices: osv, pypi, esms) (default: pypi)—.
$ curl -s https://pypi.org/pypi/mcp/1.12.0/json | jq '.vulnerabilities | length'
6
$ curl -s https://pypi.org/pypi/mcp/1.29.1/json | jq '.vulnerabilities | length'
0
The 2025 version returns six vulnerabilities. 1.29.1, the last of the 1.x line before the fix, returns zero. And five of the six new advisories name it as affected in their version ranges. The same holds for 2.1.1 on the 2.x line: zero vulnerabilities according to PyPI, five of the six advisories include it. The package is in the feeds; these six advisories are not.
The explanation is boring, which is why it gets missed: GitHub distinguishes between an advisory a project publishes on its repository and one that is also published to the global database. Only the second is replicated to OSV and fires alerts: the Dependabot documentation says it in one line —"Only advisories reviewed by GitHub trigger alerts". All six are in the first state. Neither side has done anything odd. The outcome, even so, is that a correct scanner, properly configured and up to date, returns zero.
What is in the six
They are worth reading because they are not laboratory flaws, and because they split evenly: three of the six only affect you if your code acts as an MCP client —if you have an agent connecting to servers you do not fully control— and the other three only if you are the one running the server. Each advisory draws the line in its Am I affected? section, and the server-side ones say plainly "Not affected: MCP clients".
- 28 Sep · 7.5 ·
GHSA-qx49-fqc8-xw99. The MCP server you connect to got to decide which authorization server your client sent its credentials to. It walks away with theclient_secret, the authorization code and the PKCEcode_verifier. - 28 Sep · 6.5 ·
GHSA-rwrf-2pqf-9j8j. While validating a tool result, the client resolved schema$refreferences by opening the URL. A server could make it fetch an internal address or read a local file, and hang it: "the fetch ran synchronously on the event loop with no timeout". - 30 Sep · 7.5 ·
GHSA-84m7-p3x7-pcfv(CVE-2026-59951). Streamable HTTP sessions were never reclaimed: they did not expire, aDELETEleft the entry registered, and each session kept itsinitializeparameters. Only a restart freed the memory. The advisory is honest about its own fix: "This bounds retention rather than eliminating it". - 30 Sep · 7.5 ·
GHSA-fmmv-w9g8-j3gc. The server read the whole body of every request into memory before checking its size, including on the OAuth endpoints and with no credentials needed. This is the only one of the six whose fix is older: it landed in1.29.1on 24 August. - 2 Oct · 5.9 ·
GHSA-5h93-6whr-6q8j. The HTTP transports followed redirects to any host carrying your custom headers —the advisory gives anX-API-Keyas its example— and, on a 307 or 308, the request body too. A lapsed domain at the end of an old redirect is enough. - 5 Oct · 6.8 ·
GHSA-w4fh-qvv9-3v23. The server's token authentication never checked who the token had been issued for. If your authorization server issues tokens for several services, another service's token got in too.
Two of the six are not fixed by upgrading
Here is the part that actually changes somebody's work, and it is the part that travels worst through any automated feed, because no feed has a field for "you also have to edit code". Two of the six say so in plain words.
The other-service-token one, GHSA-w4fh-qvv9-3v23, opens its fix section like this: "Upgrading alone changes nothing. The patched versions add a validate_token_resource setting that is off unless you set it.". And it warns about doing it halfway: you must switch the setting on and return the token audience from verify_token(); with only the first, "every request gets 401". So: either you do all of it, or you do none of it, or you take your own service down.
The OAuth-credentials one, GHSA-qx49-fqc8-xw99, the 7.5, says the same for two of its providers: "upgrading changes nothing until you also pass issuer=". Without that parameter "they still follow whichever authorization server the MCP server advertises". And how do you find out it is missing? Through a warning the same text switches off in the same sentence: "On 1.30.0 it is a DeprecationWarning, which Python hides by default". The signal exists, it ships disabled, and the advisory admits it in writing.
That same advisory adds two tasks no package manager performs: clear the stored OAuth registration, because "a registration stored without an issuer stays unbound", and rotate the secret and revoke the tokens if that client may have connected to a server you do not control. A scanner that told you "upgrade to 1.30.0" and said nothing else would leave you feeling finished.
The fix is from 7 September; the advisory, from the 28th
The versions closing five of the six —1.30.0 and 2.2.0— were uploaded to PyPI on 7 September 2026. The advisories explaining what they closed arrived twenty-one days later for the first and twenty-eight for the last. During those three or four weeks the information existed, but under a different name: the release notes for both versions describe the changes under the heading "Behaviour changes", not under "security". In 1.30.0 you read, verbatim, "HTTP client redirects are only followed within the endpoint's origin", "Idle Streamable HTTP sessions now expire" and "The OAuth client checks the authorization server's issuer"; in 2.2.0 the last two carry an extra tail about the legacy path. The same three flaws, told as behaviour tweaks.
This is not a reproach: a project may fix first and write the advisory afterwards, and that order is better than the reverse. What it exposes is the real sequence in which people find out. Whoever upgrades out of habit was covered on 7 September without knowing what for. Whoever reads the changelog before upgrading read "behaviour change" and filed it under things that are not urgent. And whoever waits for an alert to fire is still waiting today.
The score goes up when nobody is watching
There is a detail in two of the advisories we had not seen before: the same flaw carries two scores. Two of the three client-side advisories are rated twice, depending on whether a person is there approving. The $ref one puts it this way: "The rating assumes a person added the server and approves tool calls. For unattended agents, scored without user interaction, it is 7.5". The credentials one runs the other way: it scores 7.5 for the unattended providers and clarifies that "with the interactive OAuthClientProvider a person has to start the sign-in; scored that way it is 6.5".
Translated into your inventory: the same flaw, in the same package, is worth one thing on a developer's laptop where every call gets approved and quite another in the process that runs on its own at three in the morning. And the one running on its own at three in the morning is precisely what you build once the automation starts working well. Human supervision is not an implementation detail: it is a variable in the score, written by the people who know the code. That was also the point of The AI agent will not be in your log: whoever lent it the token will: the agent inherits the permissions of whoever lent them, and the log names the wrong person.
What changes in practice
A dependency scanner answers one very specific question: "is there an advisory in the public feeds naming this version?". The question you care about is a different one, "is anything known about this component?", and nobody answers that for you. Three things you can actually do this week:
- 1List the components that automate your company, with their versions. The orchestrator, the SDKs, the MCP servers you stood up, the connector somebody wrote in March. If it is not on a list with a name and a version, it is in no inventory, and what is in no inventory does not get patched: it gets discovered.
- 2For each one, write down where its advisories get published. Not "in my scanner": the actual address. On GitHub that is the repository's
/security/advisories, queried with the call above from a weekly cron. Note there is not even a shortcut: GitHub serves an Atom feed for a repository's releases, but asking it for the advisories one returns a 406. The inventory is built once; the query runs itself afterwards. - 3After upgrading, read whether the fix also requires a code change. It is the checkbox no dashboard has. Here it is two out of six, with the sentence "upgrading alone changes nothing" written by the authors themselves. If your patching process ends when the version number changes, in those two cases it ends before anything is fixed.
We apply it to our own setup, which is small and therefore makes a fair example: our internal automation runs on self-hosted n8n, and we treat it as what it is —a production service: inventoried, with an owner and an update window— rather than a script somebody stood up on a Friday. That is the whole difference between a component that gets patched and a component that gets discovered the day it breaks. It is the same gap we described in Your patching policy has no entry for "no patch", seen from the other side: there was an advisory and no patch; here there is a patch and no advisory.
What we are not going to tell you
- ✗That the project did anything wrong. The six advisories are among the best we have read this year: exact version ranges, an "Am I affected?" section that rules cases out, mitigations for those who cannot upgrade, and an explicit warning that upgrading is not enough. Plenty of outfits with bigger budgets publish a great deal less.
- ✗That your scanner is broken or needs replacing. It is doing its job properly with the data it is given. The gap is not in the tool, it is between publication and feed, and no tool closes it on its own.
- ✗How many other projects are in the same position. We do not know: we looked at one, carefully, and we report what we measured. If you want to know whether your own critical dependencies are the same, the check above takes a minute per package and the answer is yours, not ours.
Last month this package was downloaded two hundred and thirty-five million times, according to PyPI's public statistics —a figure that includes continuous integration, mirrors and reinstalls, so it is not two hundred and thirty-five million live installations. Divide it by whatever you like and you are still left with an enormous number of places where somebody answered "the scanner said nothing" and was right.
Sources (checked 12 Oct 2026): the counts of twelve advisories on the repository and six in OSV, the 404s for the six recent ones in both api.osv.dev and GitHub's global database, the zero results for CVE-2026-59951 in the NVD, and the PyPI API figures are ours, obtained with the public calls reproduced in the article. Quoted passages are verbatim from the repository advisories: modelcontextprotocol/python-sdk · Security Advisories. Release notes for 1.30.0 and 2.2.0 (7 Sep 2026): the project releases. pip-audit default source: pypa/pip-audit README. Dependabot alert condition: About Dependabot alerts. Downloads last month: pypistats.org/packages/mcp. If you repeat the calls weeks after this date the numbers may have changed: that would be good news, and the method for checking is the same.
Do you know which version of which component drives your automation right now?
If the answer takes more than a minute, the inventory is the work, not the patch. That is what we do in AI and automation —giving every component that works on its own an owner, a version and a window— what keeps it alive afterwards is IT maintenance, and what limits the damage when a credential ends up where it should not is Zero Trust: making sure the agent's token opens no more doors than it needs.
Talk to everyWAN