AI Integration · BraivIQ AI Engineering Playbook
MCP Integration After The September 2026 SDK Advisories: Pinned Issuers, Audience Checks And Bounded Sessions In Python
Secure MCP integration now means more than upgrading the SDK. Between 28 September and 5 October 2026 the Model Context Protocol project published ten security advisories across its Python and TypeScript SDKs, covering OAuth credentials sent to a server-chosen authorisation server, tokens accepted for the wrong service, unbounded sessions and request bodies, and cross-origin redirects. The fixes are in mcp 2.2.0 and @modelcontextprotocol/sdk 1.32.0, but two of them do nothing until you change your configuration. This playbook shows the server and client settings in tested Python, and why it matters now that vendors such as Bloomberg are putting licensed data behind MCP.
Published · Updated · 12 min read · By BraivIQ Engineering
Key takeaways
- The MCP project published six advisories for the Python SDK and four for the TypeScript SDK between 28 September and 5 October 2026. Python fixes are in mcp 1.30.0 and 2.2.0, and the current release is 2.3.0 from 2 October 2026.
- Two fixes are off by default. The client only refuses a rogue authorisation server if you pass
issuer=, and the server only refuses tokens minted for another service if you setvalidate_token_resource=Trueand return the token's audience. - Streamable HTTP servers now reclaim idle sessions after 1,800 seconds and cap sessions at 10,000 and request bodies at 4 MiB by default. Tune all three to your own load.
- Langflow's CVE-2026-105697, rated 9.9, showed what an unrestricted MCP stdio configuration can do: run any command on the host. Treat MCP server configuration as code execution.
- Bloomberg's Enterprise MCP, announced on 29 September 2026, validates a firm's Data License entitlements on every agent request. Your own MCP servers should be as strict about who is asking.
10 - MCP SDK security advisories published between 28 September and 5 October 2026 (six Python, four TypeScript) · 2 - Fixes that stay off until you change configuration: issuer pinning and token audience checks · 9.9 - CVSS score of Langflow's CVE-2026-105697, arbitrary commands through an MCP stdio configuration · 2.3.0 - Current Python mcp release, 2 October 2026
Upgrading your MCP SDK is necessary and it is not enough. The ten advisories published between 28 September and 5 October 2026 were fixed in mcp 2.2.0 for Python and @modelcontextprotocol/sdk 1.32.0 for TypeScript, but two of the most serious fixes only take effect when you change configuration. A client must name the authorisation server it trusts, and a server must refuse tokens that were minted for another service. This playbook shows both in tested Python, along with the session and request limits that close the denial-of-service issues.
It is written for engineers who own MCP integration at banks, brokers, asset managers and fintechs, where MCP servers increasingly front client data, reference data and internal workflow systems.
Which MCP advisories landed in September and October 2026?
Ten advisories in eight days, all published by the Model Context Protocol project on GitHub. For the Python SDK there were six. On 28 September, an OAuth client that could send credentials to an authorisation server chosen by the MCP server, and a client that fetched server-chosen $ref URLs while validating tool output. On 30 September, Streamable HTTP sessions that were never reclaimed (CVE-2026-59951) and request bodies read with no size limit. On 2 October, cross-origin redirects that carried custom headers and bodies. On 5 October, bearer authentication that accepted tokens issued for a different resource server.
The TypeScript SDK had four. The same OAuth credential issue (CVE-2026-104850, rated 7.5) on 30 September, experimental tasks that were not tied to the session that created them and the redirect issue on 2 October, and the same audience issue on 5 October.
The Python fixes are in mcp 1.30.0 for the 1.x line and 2.2.0 for the 2.x line, apart from the body limit, which arrived in 1.29.1 and 2.1.0. The current release is 2.3.0, published on 2 October 2026. On npm, @modelcontextprotocol/sdk 1.32.0 covers the four TypeScript issues and 1.32.1 is current.
Two related items from the same fortnight explain why this matters. On 7 October 2026 the GitHub Advisory Database published Langflow's CVE-2026-105697, rated 9.9: before version 1.10.3, Langflow's MCP stdio transport launched whatever command a user put in an MCP server configuration. And the OWASP GenAI Security Project's roundup of 8 October described an MCP server "that appears benign when approved" but "can later change its tool metadata".
Why does MCP integration matter more for financial firms this quarter?
Because licensed and client data is moving behind MCP. On 29 September 2026 Bloomberg announced Enterprise MCP, an access layer for its Data License Plus offering covering more than 100 million securities and over 50,000 fields. Bloomberg says it "validates a firm's entitlements before returning data on any agent request", that requests resolve against its Operational Datastore, and that tool-level limits cap the securities and fields returned in one call. Bloomberg hosts the server. Clients control the agents, models and prompts that connect to it.
That is the right model for your own servers too. A vendor checks entitlements on every request because it cannot trust the caller. Your internal security master, client records and booking systems deserve the same treatment, and the September advisories show where the SDK defaults fell short. Our blog covered the business side of Bloomberg's Enterprise MCP for asset managers.
How do you harden an MCP server in Python?
The first sample is an internal reference-data server built on mcp 2.3.0. We ran it on Python 3.14.4 and 3.11.14, it passes mypy --strict, and a test drives it over real HTTP with Starlette's test client. The security master is sample data.
refdata_server.py"""Hardened MCP server for internal reference data, after the September 2026 SDK advisories.
Python 3.11+, mcp 2.3.0 (the settings below need 2.2.0 or later). PyJWT and uvicorn
arrive as dependencies of mcp. The security master below is sample data.
"""
from __future__ import annotations
import json
import logging
from typing import Any
import jwt
from mcp.server.auth.provider import AccessToken
from mcp.server.auth.settings import AuthSettings
from mcp.server.mcpserver import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
from pydantic import AnyHttpUrl
from starlette.applications import Starlette
HOST = "mcp.refdata.internal.example.co.uk"
RESOURCE = f"https://{HOST}/mcp"
ISSUER = "https://login.internal.example.co.uk"
audit = logging.getLogger("mcp.audit")
SECURITY_MASTER: dict[str, dict[str, str]] = {
"GB00SAMPLE001": {"name": "Sample Gilt 4% 2031", "currency": "GBP", "type": "government bond"},
}
class AudienceCheckingVerifier:
"""Checks signature, issuer, expiry and audience. A token minted for another service gets None."""
def __init__(self, issuer_public_key_pem: bytes) -> None:
self._key = issuer_public_key_pem
async def verify_token(self, token: str) -> AccessToken | None:
try:
claims = jwt.decode(
token, self._key, algorithms=["ES256"], audience=RESOURCE, issuer=ISSUER,
options={"require": ["exp", "iss", "aud", "sub"]},
)
except jwt.PyJWTError as exc:
audit.warning(json.dumps({"event": "token_refused", "reason": type(exc).__name__}))
return None
return AccessToken(
token=token,
client_id=str(claims.get("client_id", claims["sub"])),
scopes=str(claims.get("scope", "")).split(),
expires_at=int(claims["exp"]),
resource=RESOURCE, # lets validate_token_resource check it as well
subject=str(claims["sub"]),
claims={"iss": claims["iss"]},
)
def build_app(issuer_public_key_pem: bytes) -> Starlette:
server: MCPServer[Any] = MCPServer(
name="refdata",
token_verifier=AudienceCheckingVerifier(issuer_public_key_pem),
auth=AuthSettings(
issuer_url=AnyHttpUrl(ISSUER),
resource_server_url=AnyHttpUrl(RESOURCE),
required_scopes=["refdata.read"],
validate_token_resource=True, # off unless set, even on patched versions
),
)
@server.tool()
def get_instrument(isin: str) -> dict[str, str]:
"""Static reference data for one ISIN from the security master. Read-only."""
record = SECURITY_MASTER.get(isin.upper())
audit.info(json.dumps({"event": "tool_call", "tool": "get_instrument", "isin": isin, "found": bool(record)}))
return record or {"error": "unknown ISIN"}
return server.streamable_http_app(
max_request_body_size=512 * 1024, # bounded bodies (GHSA-fmmv-w9g8-j3gc)
session_idle_timeout=300, # idle sessions reclaimed (CVE-2026-59951)
max_sessions=200, # and a hard cap on live sessions
transport_security=TransportSecuritySettings(allowed_hosts=[HOST], allowed_origins=[f"https://{HOST}"]),
host="0.0.0.0",
)
if __name__ == "__main__":
from pathlib import Path
import uvicorn
uvicorn.run(build_app(Path("issuer_public_key.pem").read_bytes()), host="0.0.0.0", port=8080)The verifier checks signature, issuer, expiry and audience with PyJWT, which mcp already depends on, and returns the audience as AccessToken.resource so the SDK's own validate_token_resource check applies as well. That is deliberate belt and braces: if someone later swaps the verifier, the SDK still refuses a token for another service. The three limits on streamable_http_app replace the defaults of 4 MiB, 1,800 seconds and 10,000 sessions with numbers sized for an internal service, and TransportSecuritySettings rejects requests whose Host header is not the server's own name.
Our test sends five requests. No token gets 401. A valid token minted for the payments service gets 401. A valid token for this server opens a session and returns an mcp-session-id. A 600 KiB body gets 413. A request with a foreign Host header is refused.
How should an MCP client connect to servers you did not write?
The second sample covers the client side: a pinned OAuth issuer, and a pinned definition for every tool your security review approved. If a server changes a tool's description or schema after approval, the client refuses to use it. Tools the review never saw are dropped before the model can see them.
pinned_client.py"""MCP client for servers you did not write: a pinned OAuth issuer and pinned tool definitions.
Python 3.11+, mcp 2.3.0 (issuer pinning needs 2.2.0 or later).
"""
from __future__ import annotations
import hashlib
import json
import httpx2
from mcp import types
from mcp.client.auth.extensions.client_credentials import ClientCredentialsOAuthProvider
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
EXPECTED_ISSUER = "https://login.internal.example.co.uk"
class MemoryTokenStorage:
"""Keeps tokens in memory. Use your secrets store in production."""
def __init__(self) -> None:
self._tokens: OAuthToken | None = None
self._client: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self._tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self._tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self._client
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self._client = client_info
def oauth_http_client(server_url: str, client_id: str, client_secret: str) -> httpx2.AsyncClient:
provider = ClientCredentialsOAuthProvider(
server_url=server_url,
storage=MemoryTokenStorage(),
client_id=client_id,
client_secret=client_secret,
scope="refdata.read",
issuer=EXPECTED_ISSUER, # without this, the GHSA-qx49 fix changes nothing
)
return httpx2.AsyncClient(auth=provider, timeout=30)
def fingerprint(tool: types.Tool) -> str:
"""Hash of everything the model reads about a tool. A changed description is a changed tool."""
body = json.dumps(
{"name": tool.name, "description": tool.description,
"input_schema": tool.input_schema, "output_schema": tool.output_schema},
sort_keys=True,
)
return hashlib.sha256(body.encode()).hexdigest()
class ToolDriftError(RuntimeError):
pass
async def approved_tools(session: ClientSession, approved: dict[str, str]) -> list[types.Tool]:
"""Return only tools whose definitions match what your security review approved."""
tools: list[types.Tool] = []
cursor: str | None = None
while True:
page = await session.list_tools(params=types.PaginatedRequestParams(cursor=cursor))
tools.extend(page.tools)
cursor = page.next_cursor
if cursor is None:
break
changed = [t.name for t in tools if t.name in approved and approved[t.name] != fingerprint(t)]
if changed:
raise ToolDriftError(f"tool definitions changed since approval: {changed}")
return [t for t in tools if t.name in approved] # unreviewed tools never reach the model
async def lookup_instrument(url: str, http: httpx2.AsyncClient, approved: dict[str, str], isin: str) -> types.CallToolResult:
"""Errors raised inside the transport arrive wrapped in an ExceptionGroup: catch with `except* ToolDriftError`."""
async with http, streamable_http_client(url, http_client=http) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
names = {t.name for t in await approved_tools(session, approved)}
if "get_instrument" not in names:
raise ToolDriftError("get_instrument is not an approved tool on this server")
return await session.call_tool("get_instrument", {"isin": isin})We ran this client against the hardened server in the same process, over the SDK's real Streamable HTTP transport. With the approved fingerprint it initialises, filters the tools and gets the sample gilt back. With a wrong fingerprint it raises ToolDriftError. One detail surprised us: because the transport runs in an anyio task group, an exception raised inside it reaches the caller wrapped in an ExceptionGroup, so callers should catch it with except* ToolDriftError.
What breaks in production?
- **Configuration that never got changed.** The issuer and audience checks are the two that matter most and both are opt-in. Add a test that a token for another audience gets 401, and run it in CI.
- **Old OAuth registrations.** The Python issuer advisory notes that registrations stored before the fix stay unbound to any issuer. Clear stored client information once so clients register again, and rotate secrets for any client that may have talked to an untrusted server.
- **Redirects that used to work.** After the redirect fix, a client that reached its server through a redirect to another host stops connecting. Point it at the final URL rather than turning redirects back on.
- **Limits that are too tight.** A capped server answers 503 when sessions run out and 413 when a body is too large. Load-test with your real document sizes and agent concurrency before you lower the defaults.
- **Vendors changing tools legitimately.** Pinning will flag a vendor's harmless update as drift. Make re-approval a quick, logged step with a named reviewer, not a reason to switch the check off.
- **Exceptions that arrive grouped.** Code that catches
ToolDriftErrorwith a plainexceptwill miss it. Useexcept*, which needs Python 3.11 or later.
Should you run MCP servers stateful or stateless, and what are the alternatives?
Each choice trades convenience for something you will have to defend in a review.
| Choice | Pick it when | Cost |
|---|---|---|
| Stateful Streamable HTTP with caps (as above) | Tools need per-session context or streaming | You must size idle timeout and session caps |
Stateless HTTP (stateless_http=True) | Each tool call stands alone | No per-session state, so no session resumption |
| Local JWT verification | Your identity provider issues signed JWTs | Revocation waits for token expiry, so keep tokens short |
| Token introspection at the IdP | You need instant revocation | One more network call per request |
| Hash pinning of tool definitions | Third-party servers you cannot audit | Re-approval work when vendors change tools |
| A gateway in front of all MCP servers | Many teams and servers | Another component to run, and a single point to protect |
For servers that act on records, the pinning and audience checks are the minimum. Pair them with the pre-tool gate from our featured playbook on agentic AI architecture with approval gates, and with the provenance rules in prompt injection defence in code, because a pinned tool can still return text an attacker wrote. Our earlier guide to MCP tool and skill supply-chain security covers signed skill archives and scoped sessions.
How can an AI Agency UK team help you audit existing MCP integrations?
Start with an inventory: every MCP server your agents reach, the SDK version behind each one, whether the issuer and audience checks are on, and which tools each agent can see. Most firms we speak to find at least one server on an old SDK and one client that follows whatever the server advertises. We run that audit and fix the gaps inside a 14-day Proof Run, alongside one live workflow, and it is the first step in every AI Automation London engagement our AI Agency UK team takes on. The Workflow Automation Agency work described on our services page runs on the same hardened servers.
Frequently asked questions
What is MCP integration?
It is connecting an AI agent to tools and data through the Model Context Protocol, an open standard in which an MCP server describes its tools and a client inside the agent calls them. The current specification version is 2026-07-28. In a financial firm the integration work is mostly about identity, scope and audit rather than the protocol itself.
Is MCP secure?
The protocol can be run securely, but the defaults matter. The September and October 2026 advisories showed SDK clients sending OAuth credentials where a malicious server pointed them, servers accepting tokens issued for other services and sessions that were never reclaimed. Upgrade, then turn on the issuer and audience checks, cap sessions and request sizes, and pin the definitions of the tools you approved.
Which MCP SDK versions fix the September 2026 advisories?
For Python, mcp 1.30.0 on the 1.x line and 2.2.0 on the 2.x line fix all six advisories, and 2.3.0 is current. For TypeScript, @modelcontextprotocol/sdk 1.32.0 fixes the four advisories, with @modelcontextprotocol/client 2.3.0 and @modelcontextprotocol/server 2.3.0 for the 2.x packages. Check each advisory for the exact range that applies to your package.
Do I need to change code after upgrading the MCP SDK?
Yes, for two of the fixes. Pass issuer= to ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider, and on servers set validate_token_resource=True in AuthSettings with your token verifier returning the token's audience. The advisories say plainly that upgrading alone changes nothing for these two.
References
- Model Context Protocol (GitHub), "OAuth client could send credentials to an authorization server chosen by the MCP server (Python SDK, GHSA-qx49-fqc8-xw99)", 28 September 2026. https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99
- Model Context Protocol (GitHub), "Streamable HTTP sessions were never reclaimed by default and kept their initialize payloads (CVE-2026-59951)", 30 September 2026. https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-84m7-p3x7-pcfv
- Model Context Protocol (GitHub), "HTTP server transports and OAuth endpoints read request bodies with no size limit (GHSA-fmmv-w9g8-j3gc)", 30 September 2026. https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-fmmv-w9g8-j3gc
- Model Context Protocol (GitHub), "Server bearer-token authentication accepts tokens issued for a different resource server (GHSA-w4fh-qvv9-3v23)", 5 October 2026. https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-w4fh-qvv9-3v23
- Model Context Protocol (GitHub), "HTTP client transports followed cross-origin redirects carrying custom headers and bodies (GHSA-5h93-6whr-6q8j)", 2 October 2026. https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-5h93-6whr-6q8j
- Model Context Protocol (GitHub), "TypeScript SDK: OAuth client could send credentials to an authorization server chosen by the MCP server (CVE-2026-104850)", 30 September 2026. https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6qxp-vccf-f47h
- GitHub Advisory Database, "Langflow: OS command injection (RCE) via arbitrary command in MCP stdio server configuration (CVE-2026-105697)", 7 October 2026. https://github.com/advisories/GHSA-w794-rj3p-xv45
- OWASP GenAI Security Project, "GenAI and Agentic AI Exploit Roundup Q3 2026", 8 October 2026. https://genai.owasp.org/2026/10/08/genai-and-agentic-ai-exploit-roundup-q3-2026/
- Bloomberg (PR Newswire), "Bloomberg Launches Enterprise MCP to Seamlessly Connect Bloomberg Data with Clients' Enterprise AI Applications", 29 September 2026. https://www.prnewswire.com/news-releases/bloomberg-launches-enterprise-mcp-to-seamlessly-connect-bloomberg-data-with-clients-enterprise-ai-applications-302891331.html
- Python Package Index, "mcp 2.3.0", 2 October 2026. https://pypi.org/project/mcp/2.3.0/