Short answer: give each user or tenant their own connection and their own token, and keep the choice of whose connection to use out of the model’s hands. ragleap-integrations 0.3.0 does this with McpHub: one owner-level configuration and one Connection per Scope. The scope is fixed in code when the connection is made and is never a tool argument, so the model cannot ask for another person’s connection.
The comic that started this post
The idea was drawn as a manga-style comic by @nj85c (Comic News) on X. Its caption, in Japanese with an English translation shown by X, says that when several people use an AI the connections should be separated, that each call should get the key of the person themselves, and that the AI should not be able to choose another person’s connection. The comic names ragleap-integrations 0.3.0 as the tool that does the separating. The artwork is theirs, so it is linked here, not copied. This post checks the idea against the package’s own changelog and README.
The problem
An AI agent that calls tools through MCP (Model Context Protocol) needs credentials. With one user, one token is fine. When several people share one agent, a single shared token means every call runs with the same access, and a mistake or a manipulated prompt could use someone else’s access. The fix is structural: the credentials belong to the person, and the code, not the model, decides which person a call is for.
What McpHub does in 0.3.0
- One owner-level
McpConfigand oneConnectionperScope(a tenant and a subject). Each scope has its own client, tool snapshot and lock. The scope is closed over by the tool handlers and is never a tool argument. token_provider(scope, server)is called for every request and the package caches nothing. If it returnsNone, no token is sent (the static token is not used then); a failing provider gives the constant text “MCP token is unavailable”.allow_for(scope)can only narrow the owner’s allowlist.policy_for(scope)picks a per-tenantMcpPolicy, and itsapprove_scopedwins overapprove; without a scope it denies.RateLimitis per tenant and is checked before approval and before any network call. Events carry the tenant and the scope, never arguments or results, and anauditobject (anything withappend(event, data, scope)) receives each one.hub.connect()needs an exactScope; a string, a dict,Noneor a subclass raisesScopeError.- Code that does not use
McpHubbehaves as in 0.2.0, per the changelog.
Install
pip install ragleap-integrations pip install "ragleap-integrations[security]"
With the security extra the scope is ragleap-security’s Scope; without it a small built-in Scope with the same fields and key format is used. The package needs Python 3.10 or newer and is marked alpha in its metadata.
The example from the README
from ragleap_integrations import McpConfig, McpHub, McpServerConfig, RateLimit, Scope
hub = McpHub(
McpConfig(servers=[McpServerConfig("crm", "https://mcp.example.com/mcp")],
allowed_tools=["crm.get_order", "crm.update_order"]),
token_provider=lambda scope, server: vault.token_for(scope.tenant, server),
allow_for=lambda scope: ["crm.get_order"] if scope.tenant == "trial" else None,
limit=RateLimit(60, 60), # 60 calls a minute per tenant
audit=audit_log, # anything with append(event, data, scope)
)
conn = hub.connect(Scope(tenant="acme", subject="u1"))
conn.toolkit.call(name_from_model, arguments_from_model) # same Connection as connect()
Copied from the ragleap-integrations 0.3.0 README. It refers to names you supply yourself, such as vault and audit_log. We have not run this snippet for this post.
How it was checked, and what was not
By our count the package has 201 test functions in 6 test files at the 0.3.0 tag (parametrised cases count once). The changelog also records a live check on 2026-10-26, read-only, using the repository source of the 0.3.0 branch against a third party’s public mock MCP auth server. In it two tenants each called a tool with their own token and all four recorded requests carried only that tenant’s token (compared by hash); a made-up token was refused with HTTP 401 while the real tenants kept working; after one tenant’s token was changed only that tenant got 401; RateLimit(2, 60) allowed two calls and denied the third locally; nine events carried a tenant and none a token.
The limit of that check: the mock server cannot tell tenants apart, so separation is shown from the requests that were sent, not from a server that really separates tenants’ data. The changelog lists as not verified: approve_scoped, allow_for and policy_for against a real server, a real ragleap_security.AuditLog fed by a live run, concurrent tenants against a real server, and the published wheel. Update 2026-10-31: the 0.3.0 GitHub release notes say the published wheel and sdist were checked from PyPI on 2026-10-27: hashes identical to the TestPyPI upload, installed files equal to the repository source, the smoke test passing with and without the extra, and the sdist tests passing.
What it does not do
- It does not decide who the caller is. Tenant isolation is only as strong as the
Scopeyou pass and the token your provider returns. - Rate windows are kept per tenant until
hub.forget(tenant=...), the limit is per process, andallow_forandpolicy_forare read once per scope. - Server-supplied text (tool descriptions, schemas and results) is untrusted and is not screened for prompt injection.
- stdio and OAuth are not supported in this version; its README has the full list.
- This is a library feature. RagLeap Core v0.12.0 is single-tenant by its own release notes, and this post is about the package, not a core feature.
Related
The ragleap-integrations library page, the ragleap-integrations guide, ragleap-security (the optional Scope and AuditLog) and ragleap-integrations on PyPI.