关于 Tenant Context
mcp-tenant-context 是供 Node.js 服务使用的 TypeScript 库,可在异步处理链中传递租户与操作者身份。它借助 AsyncLocalStorage 减少函数间显式传参,并提供中间件和回调绑定工具。
主要功能
- 01传递租户与操作者身份
- 02在异步调用中读取上下文
- 03为脱离作用域的回调绑定上下文
- 04用中间件包装处理器
- 05创建 OAuth、API 密钥和匿名操作者
适用场景
- 01按租户范围执行数据库查询
- 02记录写入操作的操作者
- 03在事件监听器中保留上下文
MCP 相关
该库面向 MCP 服务设计,上下文可包含对应 mcp-session-id 请求头的 sessionId。它本身不是 MCP 服务,也可用于其他 Node.js 请求服务。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
属于 StudioMeyer MCP Stack —— 源自马略卡岛 🌴 · 如果你在使用,请 ⭐
mcp-tenant-context
通过 AsyncLocalStorage 在处理程序堆栈中传递每个请求的租户 + 操作者身份,这样 tenantSlug 和请求操作者就不必出现在每个函数签名中。零运行时依赖,仅使用 Node 标准库。
Node: 22+ · 许可证: MIT · 类型: 库(无服务、无命令行)
我们想说的话
过去两年里,我们一直在为自己构建工具和系统。这个仓库规模小、星标少,并不是因为它刚刚诞生,而是因为我们最近才决定分享已经构建好的东西。这些代码真实可靠,已在生产环境中运行,问题也会得到解答。
我们热爱构建和分享,但不喜欢增长黑客,也不热衷于追逐星标。所以这个仓库规模不大。请根据代码来评判。如果它对你有帮助,一个星标、一个测试或一个问题都能帮助我们。如果你用它构建了什么,欢迎告诉我们:[email protected]——这真的会让我们开心。
来自马略卡岛帕尔马的一间小工作室。
为什么要开发它
每个多租户服务都必须在处理程序堆栈中传递两类每个请求的身份信息:
tenantSlug— 请求所属的租户。它决定每个受限范围的数据库查询(WHERE tenant_slug = $1),也是跨租户隔离的核心。actor— 触发请求的人类(OAuth 用户)或服务主体(API 密钥)。它决定审计日志条目和写入操作的归属信息。
在每个处理程序签名中传递这些信息属于样板代码,漏掉一个参数就会造成静默错误。AsyncLocalStorage 能让接口保持简洁:在入口点设置一次上下文,然后在调用堆栈中的任意位置读取。这个库将这种模式封装起来——约 115 行代码、零依赖、完全类型化。
本库面向 MCP 服务开发(TenantContext 包含一个可选的 sessionId,与 mcp-session-id 标头匹配),但它并非 MCP 专用,可用于任何按请求管理上下文的 Node 服务。
安装
npm install mcp-tenant-context
需要 Node 22+。
快速开始
import {
runWithTenantContext,
getTenantContext,
oauthActor,
} from "mcp-tenant-context";
// At the transport boundary — wrap every dispatch in a tenant context.
app.post("/mcp", async (req, res) => {
const slug = await authenticateBearer(req);
const email = await verifyOAuth(req);
await runWithTenantContext(
{ tenantSlug: slug, actor: oauthActor(email) },
async () => {
const result = await dispatchTool(req.body);
res.json(result);
},
);
});
// Anywhere inside a tool handler — no ctx threading needed.
async function listPages(args: { limit: number }) {
const ctx = getTenantContext(); // throws if no ctx is active
return db.query(
"SELECT * FROM pages WHERE tenant_slug = $1 LIMIT $2",
[ctx.tenantSlug, args.limit],
);
}
公共 API
| 导出项 | 用途 |
|---|---|
runWithTenantContext<T>(ctx, fn) | 在安装了 ctx 的情况下运行 fn。保留同步/异步返回类型。 |
getTenantContext() | 当前的 ctx;若不存在,则抛出 NoTenantContextError。 |
getTenantContextOrUndefined() | 当前的 ctx;若不存在,则返回 undefined。 |
bindTenantContext<F>(fn) | 将当前活动的 ctx 快照绑定到 fn,使分离的 EventEmitter/流回调仍能保留它。输入和输出签名相同。 |
withTenantContextHandler<TArgs, TResult>(handler) | 用于提取 ctx 并将其作为第一个参数传入的中间件。 |
oauthActor(email) | 基于 OAuth 的角色。显示名称由电子邮件地址的本地部分生成。 |
apiKeyActor(tenantSlug) | 合成的 service-account@<slug>.invalid 角色(RFC 2606 保留的顶级域名)。 |
anonymousActor() | 用于未通过身份验证路径的空电子邮件角色。 |
TenantContext(类型) | { tenantSlug, actor, traceId?, sessionId? }。 |
ActorIdentity(类型) | { email, name, source }。 |
ActorSource(类型) | "oauth" | "api_key" | "anonymous"。 |
NoTenantContextError(类) | 特定的、可捕获的错误子类。 |
中间件
withTenantContextHandler 将 getTenantContext() 的样板代码提取出来:
import { withTenantContextHandler } from "mcp-tenant-context";
export const listPages = withTenantContextHandler(
async (ctx, args: { limit: number }) => {
return db.query(
"SELECT * FROM pages WHERE tenant_slug = $1 LIMIT $2",
[ctx.tenantSlug, args.limit],
);
},
);
// Dispatch:
await runWithTenantContext(ctx, () => listPages({ limit: 10 }));
在 runWithTenantContext 外部调用处理器时会被拒绝,并产生
NoTenantContextError(作为被拒绝的 Promise,因此所有调用方都可以统一地对它使用 .catch())。
使用方职责
此库不会验证或清理输入——这是有意为之,以保持通用性。安全相关部分由调用方负责:
- 在构建上下文前验证
tenantSlug(非空、符合预期字符集,例如^[a-z0-9][a-z0-9-]{0,62}$)。将空值或攻击者控制的 slug 传给WHERE tenant_slug = $1存在跨租户风险。 - 对 SQL 使用参数化查询——
tenantSlug是普通字符串,切勿将其插入 SQL。 - 在身份层验证
email,并在调用oauthActor(email)前将其转换为小写。 - 在需要租户的处理器层拒绝匿名角色。
此库只保证一件事:上下文不会在并发作用域之间泄漏。上下文之上的一切都由你负责。
回调和事件发送器中的上下文丢失
AsyncLocalStorage 会自动跟踪 async/await、Promise、setTimeout、
setInterval、setImmediate、process.nextTick、queueMicrotask 以及
Promise.all / race / allSettled 组合器(测试套件均已覆盖)。但它不会跟踪在不同异步上下文中注册的回调——典型情况是 EventEmitter 监听器
(stream.on("data", ...)、req.on("close", ...))在租户作用域外部绑定。在此类回调中,getTenantContext() 会抛出异常。
遇到这种情况时,请在注册回调时使用 bindTenantContext 将其绑定到当前上下文:
import { runWithTenantContext, bindTenantContext, getTenantContext } from "mcp-tenant-context";
runWithTenantContext(ctx, () => {
// Captured now → still resolves when `data` fires later, outside the scope.
stream.on("data", bindTenantContext((chunk: Buffer) => {
const { tenantSlug } = getTenantContext(); // resolves to ctx
audit(tenantSlug, chunk.length);
}));
});
bindTenantContext(fn) 返回一个与 fn 签名相同的函数(参数和返回值原样传递),并会快照整个环境异步上下文,因此租户、跟踪信息以及任何其他 AsyncLocalStorage 都会一并恢复。它基于 Node 的 AsyncLocalStorage.bind 构建。如果绑定时没有活动上下文,绑定后的函数行为与未绑定时完全相同(因此 getTenantContext() 仍会抛出异常)——绑定不会带来坏处。
上下文仍不会传播到 worker_threads——按照设计,启动的 worker 会从空存储开始。请显式跨 worker 边界传递 tenantSlug。
性能
AsyncLocalStorage 会为每次 await 增加少量固定的记账开销。对于任何有实际工作的处理器(例如数据库往返、大模型调用),这点开销可以忽略不计——延迟主要由 I/O 决定,而不是上下文查询。Node 将该实现描述为“高性能且内存安全”。只有在路径极其高频且对分配敏感时,才需要测量其开销。
模块实例
该存储是模块级单例:同一进程中每次导入 mcp-tenant-context 都会共享一个存储(这是预期行为——上下文是进程级的)。如果依赖树中最终包含此包的两个 不同 已安装副本,它们将不会共享存储,因此当多个依赖使用此包时,请确保只保留一个版本(npm dedupe)。
测试套件覆盖的边界情况
- 嵌套调用
runWithTenantContext——内层上下文会覆盖外层,恢复后会还原外层,包括在await之后以及跨并发子作用域时。 - 并发的同级运行——并行上下文之间不会相互泄漏。
- 异步迭代器透传——ctx 在
for await边界之后仍然保留。 - 每种定时器和微任务边界——
setTimeout(resolve 和 reject 路径)、setInterval、setImmediate、process.nextTick、queueMicrotask。 - Promise 组合器——
Promise.all、Promise.race、Promise.allSettled会在每个分支中以及合并后保留 ctx。 - 分离的
EventEmitter/ 流监听器——ctx 会按预期缺失(这是AsyncLocalStorage无法跟踪的边界),且此处调用getTenantContext()会抛出异常;bindTenantContext会恢复 ctx,即使run()已退出,或事件是在另一个仍有效的上下文中发出的,也一样。 - 同步与异步
fn的返回类型——会保留。 - 可选的
traceId+sessionId字段。 oauthActor遇到异常输入时:本地部分为空(@host)、完全没有@、本地部分仅有分隔符,以及本地部分过长(派生名称会截断,电子邮件会原样保留)——每种情况都会返回安全且不会抛出异常的值。
4 个文件中共有 47 项测试。
版本管理
严格遵循 SemVer。未经主版本升级并在 CHANGELOG.md 中附上迁移说明,不得引入破坏性变更。请使用插入符固定版本("mcp-tenant-context": "^0.2.0")。
相关项目
属于 StudioMeyer MCP 技术栈——构建具有纵深防御的多租户 MCP 服务时,建议一并安装:
mcp-tenant-pair— 为面向消费者的 MCP 服务提供多用户租户支持(伴侣、家庭、小团体)。mcp-stdio-shellguard— MCP 服务中针对exec/spawn的默认拒绝防护。mcp-rce-guard— 为 MCP 子进程提供进程隔离和 CVE 重放防护。
参与贡献
欢迎提交问题和 PR——请参阅 CONTRIBUTING.md。安全问题请按照 SECURITY.md 中的流程报告。
许可证
MIT © Matthias Meyer(StudioMeyer)
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。