Toolception–动态MCP工具库
](https://www.npmjs.com/package/toolception) 
目录
- createMcpServer - createPermissionBasedMcpServer
何时以及为什么使用Toolception
使用数十或数百个工具构建MCP服务器通常会损害LLM性能和开发人员体验:
- 工具太多,无法选择:较大的工具列表会增加混淆和误选率。
- 令牌和模式膨胀:长工具目录会增加提示和延迟。
- 名称冲突和歧义:跨域的类似工具名称会导致失败和脆弱的集成。
- 运营开销:预先加载每个工具都会浪费资源;许多工具都是针对特定任务的。
Toolception通过将工具分组到工具集中,并允许您在需要时只公开所需的内容来解决这个问题。
何时使用Toolception
- 大型或多域目录:您有>20-50个工具或多个域(例如搜索、数据、计费),不想一次全部暴露出来。
- 特定任务工作流:您希望客户端/代理仅启用与当前任务相关的工具。
- 多租户或政策需求:不同的用户/租户需要不同的工具访问权限或限制。
- 每个会话上下文:对于传递给模块加载程序的每个客户端会话,您需要不同的上下文值(API令牌、用户ID)。
- 基于权限的访问控制:您需要强制执行特定于客户端的工具集权限,以实现安全性、合规性或多租户隔离。每个客户端都应该只查看和访问他们有权使用的工具集,并使用服务器端或基于头的权限强制。
- 碰撞安全命名:您需要可预测的、命名空间的工具名称以避免冲突。
- 延迟加载:有些工具很重,应该按需装载。
为什么Toolception有帮助
- 工具集:将相关工具分组,并为每个任务公开最小、连贯的子集。
- 动态模式(运行时控制):
- 通过元工具按需启用工具集(enable_toolset, disable_toolset, list_toolsets, describe_toolset, list_tools). - 减少提示/工具表面积→ 更好的工具选择和更低的延迟。 - 延迟加载模块仅在需要时生成工具;通行证共享 context 安全装载。 - 支持 tools.listChanged 通知,以便客户端可以对更新的工具列表做出反应。
- 静态模式(可预测启动):
- 对于固定管道和更简单的环境,在启动时预加载已知的工具集(或ALL)。 - 仅保留部署足迹所需的集合。
- 风险敞口政策:
- maxActiveToolsets:同时限制活动集以防止膨胀。 - 全科医生/牙科医生:强制启用哪些工具集。 - 命名空间工具带设置键:默认打开;将工具注册为 set.tool 以避免冲突并明确意图。
- 运行安全:
- 中央 ToolRegistry 验证名称并防止冲突。 - ModuleLoaders 对于可重复运行和缓存来说是确定性的/幂等的。
选择模式
- 更喜欢动态 当工具需求因任务而异时,您需要更严格的提示,或者需要运行时门控和延迟加载。
- 选择静态 当您的工具需求稳定且较小时,或者当您的客户端无法(或不应该)执行运行时启用/禁用操作时。
典型流量
- 发现优先(动态):客户电话
list_toolsets→ 启用一组→ 调用命名空间工具(例如。,core.ping). - 固定管道(静态):服务器在启动时预加载命名工具集(或ALL);客户来电
list_tools并像往常一样调用。
入门指南
步骤1:安装
npm i toolception步骤2:导入工具概念
import { createMcpServer } from "toolception";步骤3:定义工具集目录
const catalog = {
quotes: { name: "Quotes", description: "Market quotes", modules: ["quotes"] },
};步骤4:定义一个工具
const quoteTool = {
name: "price",
description: "Return a fake price",
inputSchema: {
type: "object",
properties: { symbol: { type: "string" } },
required: ["symbol"],
},
handler: async ({ symbol }: { symbol: string }) => ({
content: [{ type: "text", text: `${symbol}: 123.45` }],
}),
} as const;步骤5:提供模块加载器
const moduleLoaders = {
quotes: async () => [quoteTool],
};步骤6:(可选)配置模式
const configSchema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
REQUIRED_PARAM: { type: "string", title: "Required Param" },
OPTIONAL_PARAM: { type: "string", title: "Optional Param" },
},
required: ["REQUIRED_PARAM"],
} as const;步骤7:创建MCP SDK服务器并启动Toolception
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// You own the SDK server; pass a factory into Toolception (required in DYNAMIC mode)
const createServer = () =>
new McpServer({
name: "my-mcp-server",
version: "0.0.0",
capabilities: { tools: { listChanged: true } },
});
const { start, close } = await createMcpServer({
catalog,
moduleLoaders,
startup: { mode: "DYNAMIC" },
http: { port: 3000 },
createServer,
// configSchema, // uncomment to expose at /.well-known/mcp-config
});
await start();步骤8:优雅关机
process.on("SIGINT", async () => {
await close();
process.exit(0);
});
process.on("SIGTERM", async () => {
await close();
process.exit(0);
});静态启动
在引导时启用部分或全部工具集。在STATIC模式下,创建一个服务器实例并为所有客户端重用:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const staticCatalog = {
search: { name: "Search", description: "Search tools", modules: ["search"] },
quotes: { name: "Quotes", description: "Market quotes", modules: ["quotes"] },
};
// Load specific toolsets
createMcpServer({
catalog: staticCatalog,
startup: { mode: "STATIC", toolsets: ["search", "quotes"] },
http: { port: 3001 },
createServer: () =>
new McpServer({
name: "static-1",
version: "0.0.0",
capabilities: { tools: { listChanged: false } },
}),
});
// Load ALL toolsets
createMcpServer({
catalog: staticCatalog,
startup: { mode: "STATIC", toolsets: "ALL" },
http: { port: 3002 },
createServer: () =>
new McpServer({
name: "static-2",
version: "0.0.0",
capabilities: { tools: { listChanged: false } },
}),
});基于权限的入门指南
使用 createPermissionBasedMcpServer 当您需要强制执行特定于客户端的工具集权限时。这对于多租户应用程序、安全敏感环境或不同客户端应具有不同访问级别时非常理想。
步骤1:安装
npm i toolception步骤2:导入工具概念
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";步骤3:定义工具集目录
const catalog = {
admin: {
name: "Admin Tools",
description: "Administrative operations",
modules: ["admin"],
},
user: {
name: "User Tools",
description: "Standard user operations",
modules: ["user"],
},
};步骤4:定义工具
const adminTool = {
name: "delete_user",
description: "Delete a user account",
inputSchema: {
type: "object",
properties: {
userId: { type: "string", description: "User ID to delete" },
},
required: ["userId"],
},
handler: async ({ userId }: { userId: string }) => ({
content: [{ type: "text", text: `User ${userId} deleted` }],
}),
} as const;
const userTool = {
name: "get_profile",
description: "Get user profile information",
inputSchema: {
type: "object",
properties: {
userId: { type: "string", description: "User ID" },
},
required: ["userId"],
},
handler: async ({ userId }: { userId: string }) => ({
content: [{ type: "text", text: `Profile for ${userId}: {...}` }],
}),
} as const;步骤5:提供模块加载器
const moduleLoaders = {
admin: async () => [adminTool],
user: async () => [userTool],
};步骤6:选择许可方法
您有两个管理权限的选项:
基于标头的权限:
- 当您有身份验证网关/代理时使用
- 通过HTTP标头传递的权限
- 适用于动态、频繁更改的权限
- 需要对标头进行外部验证
基于配置的权限:
- 当您需要服务器端控制时使用
- 服务器配置中定义的权限
- 更好的安全性(没有客户端提供权限数据)
- 有利于稳定的许可结构
步骤7:创建基于权限的MCP服务器
选项A:基于标头的权限
const createServer = () =>
new McpServer({
name: "permission-header-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
const { start, close } = await createPermissionBasedMcpServer({
catalog,
moduleLoaders,
permissions: {
source: "headers",
headerName: "mcp-toolset-permissions", // optional, this is default
},
http: { port: 3000 },
createServer,
});
await start();选项B:基于配置的权限(静态映射)
const createServer = () =>
new McpServer({
name: "permission-config-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
const { start, close } = await createPermissionBasedMcpServer({
catalog,
moduleLoaders,
permissions: {
source: "config",
staticMap: {
"admin-client-id": ["admin", "user"],
"user-client-id": ["user"],
},
defaultPermissions: [], // unknown clients get no toolsets
},
http: { port: 3000 },
createServer,
});
await start();选项C:基于配置的权限(解析器函数)
const createServer = () =>
new McpServer({
name: "permission-resolver-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
const { start, close } = await createPermissionBasedMcpServer({
catalog,
moduleLoaders,
permissions: {
source: "config",
resolver: (clientId: string) => {
// Your custom permission logic
if (clientId.startsWith("admin-")) {
return ["admin", "user"];
}
if (clientId.startsWith("user-")) {
return ["user"];
}
return [];
},
defaultPermissions: [],
},
http: { port: 3000 },
createServer,
});
await start();步骤8:优雅关机
process.on("SIGINT", async () => {
await close();
process.exit(0);
});
process.on("SIGTERM", async () => {
await close();
process.exit(0);
});权限配置方法
基于标头的权限设置
当您拥有验证和设置权限标头的身份验证网关或代理时,请使用基于标头的权限。这种方法对于动态权限很灵活,但需要外部标头验证。
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const createServer = () =>
new McpServer({
name: "permission-header-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
const { start, close } = await createPermissionBasedMcpServer({
catalog: {
admin: {
name: "Admin",
description: "Admin tools",
modules: ["admin"],
},
user: {
name: "User",
description: "User tools",
modules: ["user"],
},
},
moduleLoaders: {
admin: async () => [
/* admin tools */
],
user: async () => [
/* user tools */
],
},
permissions: {
source: "headers",
headerName: "mcp-toolset-permissions", // optional, this is default
},
http: { port: 3000 },
createServer,
});
await start();何时使用:
- 您有一个验证请求的身份验证网关/代理
- 权限经常更改或根据请求计算
- 您可以确保标头经过加密签名或验证
- 您的身份验证系统位于MCP服务器外部
基于配置的权限设置(静态映射)
当您有一组具有已知权限的固定客户端时,请使用静态映射。这提供了服务器端控制和更好的安全性。
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const createServer = () =>
new McpServer({
name: "permission-config-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
const { start, close } = await createPermissionBasedMcpServer({
catalog: {
admin: {
name: "Admin",
description: "Admin tools",
modules: ["admin"],
},
user: {
name: "User",
description: "User tools",
modules: ["user"],
},
},
moduleLoaders: {
admin: async () => [
/* admin tools */
],
user: async () => [
/* user tools */
],
},
permissions: {
source: "config",
staticMap: {
"admin-client-id": ["admin", "user"],
"user-client-id": ["user"],
},
defaultPermissions: [], // clients not in map get no toolsets
},
http: { port: 3000 },
createServer,
});
await start();何时使用:
- 你有一组固定的已知客户
- 权限相对稳定
- 你想要最高的安全级别
- 您希望避免信任客户端提供的数据
基于配置的权限设置(解析器功能)
当您需要自定义逻辑来确定权限时,例如从数据库查找或应用复杂规则,请使用解析器函数。
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const createServer = () =>
new McpServer({
name: "permission-resolver-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
const { start, close } = await createPermissionBasedMcpServer({
catalog: {
admin: {
name: "Admin",
description: "Admin tools",
modules: ["admin"],
},
user: {
name: "User",
description: "User tools",
modules: ["user"],
},
},
moduleLoaders: {
admin: async () => [
/* admin tools */
],
user: async () => [
/* user tools */
],
},
permissions: {
source: "config",
resolver: (clientId: string) => {
// Custom logic - could check database, config file, etc.
if (clientId.startsWith("admin-")) {
return ["admin", "user"];
}
if (clientId.startsWith("user-")) {
return ["user"];
}
return [];
},
staticMap: {
// optional fallback
"special-client": ["admin"],
},
defaultPermissions: [],
},
http: { port: 3000 },
createServer,
});
await start();何时使用:
- 您需要自定义权限逻辑
- 权限是根据客户端ID模式或属性计算的
- 您想与现有的权限系统集成
- 您需要使用staticMap进行回退行为
注: 解析器函数必须同步。如果需要从外部源获取权限,请在创建服务器之前执行此操作并缓存结果。
自定义HTTP端点
Toolception支持自定义HTTP端点和MCP协议端点,通过Zod验证和类型推断实现类似REST的API。
基本用途
import { createMcpServer, defineEndpoint } from "toolception";
import { z } from "zod";
const { start } = await createMcpServer({
// ... standard options
http: {
port: 3000,
customEndpoints: [
defineEndpoint({
method: "GET",
path: "/api/users",
querySchema: z.object({
limit: z.coerce.number().int().positive().default(10),
role: z.enum(["admin", "user"]).optional(),
}),
responseSchema: z.object({
users: z.array(z.object({ id: z.string(), name: z.string() })),
}),
handler: async (req) => {
// req.query is typed: { limit: number, role?: "admin" | "user" }
// req.clientId is available from mcp-client-id header
return { users: [{ id: "1", name: "Alice" }] };
},
}),
],
},
});验证模式
- querySchema:验证URL查询参数(使用
z.coerce用于类型转换) - bodySchema 的:验证请求体(POST/PUT/PATCH)
- paramsSchema:验证路径参数(例如。,
/users/:userId) - 响应方案:验证处理程序响应(防止无效数据泄漏)
请求上下文
处理程序接收键入的请求对象:
{
body: TBody, // Validated from bodySchema
query: TQuery, // Validated from querySchema
params: TParams, // Validated from paramsSchema
headers: Record,
clientId: string, // From mcp-client-id header or auto-generated
}权限感知端点
使用 definePermissionAwareEndpoint 在基于权限的服务器中访问客户端权限:
import { createPermissionBasedMcpServer, definePermissionAwareEndpoint } from "toolception";
const { start } = await createPermissionBasedMcpServer({
// ... permission config
http: {
customEndpoints: [
definePermissionAwareEndpoint({
method: "GET",
path: "/api/me",
responseSchema: z.object({
clientId: z.string(),
allowedToolsets: z.array(z.string()),
isAdmin: z.boolean(),
}),
handler: async (req) => {
// req.allowedToolsets and req.failedToolsets are available
return {
clientId: req.clientId,
allowedToolsets: req.allowedToolsets,
isAdmin: req.allowedToolsets.includes("admin-tools"),
};
},
}),
],
},
});错误处理
验证失败返回标准化错误响应:
- 400验证错误:请求验证失败(正文、查询或参数)
- 500内部错误:处理程序抛出异常
- 500响应_验证_错误:响应验证失败
错误响应示例:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed for query",
"details": [
{
"code": "invalid_type",
"path": ["limit"],
"message": "Expected number, received string"
}
]
}
}预留路径
自定义端点不能覆盖内置MCP路径:
/mcp-MCP JSON-RPC端点/healthz-健康检查/tools-工具列表/.well-known/mcp-config-配置架构
完整示例
看 examples/custom-endpoints-demo.ts 关于GET、POST、PUT、DELETE端点、分页和权限感知处理程序的完整工作示例。
每个会话上下文
使用 sessionContext 选项,启用从查询参数中提取的每个客户端上下文值。这对于每个客户端需要传递给模块加载程序的不同配置(API令牌、用户ID等)的多对映体场景非常有用。
基本用途
import { createMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const { start } = await createMcpServer({
catalog: { /* ... */ },
moduleLoaders: { /* ... */ },
context: { baseValue: 'shared' }, // Base context for all sessions
sessionContext: {
enabled: true,
queryParam: {
name: 'config',
encoding: 'base64',
allowedKeys: ['API_TOKEN', 'USER_ID'], // Security: always specify
},
merge: 'shallow',
},
createServer: () => new McpServer({
name: "my-server",
version: "1.0.0",
capabilities: { tools: { listChanged: true } },
}),
http: { port: 3000 },
});
await start();客户端连接
# Encode session config as base64
CONFIG=$(echo -n '{"API_TOKEN":"user-secret-token","USER_ID":"123"}' | base64)
# Connect with session config
curl -X POST "http://localhost:3000/mcp?config=$CONFIG" \
-H "mcp-client-id: my-client" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize",...}'模块加载器接收合并的上下文
const moduleLoaders = {
tenant: async (ctx: any) => {
// ctx = { baseValue: 'shared', API_TOKEN: 'user-secret-token', USER_ID: '123' }
return [/* tools using ctx.API_TOKEN */];
},
};自定义上下文解析器
对于高级用例,提供自定义解析器函数:
sessionContext: {
enabled: true,
queryParam: { allowedKeys: ['tenant_id'] },
contextResolver: (request, baseContext, parsedConfig) => ({
...baseContext,
...parsedConfig,
clientId: request.clientId,
timestamp: Date.now(),
}),
}安全注意事项
- 始终指定
allowedKeys:如果没有白名单,则接受查询配置中的任何键 - 故障保护:无效编码会自动回退到基础上下文
- 不记录值:会话配置值从不记录
- 无声过滤:过滤不允许的密钥,不显示错误消息
API
createMcpServer(选项)
将您的MCP SDK服务器连接到动态/静态工具管理和Fastify HTTP传输。
需求
createServer必须提供。- 在DYNAMIC模式下,每个客户端通过以下方式创建一个新的服务器实例
createServer. - 在STATIC模式下,通过以下方式创建一个服务器实例
createServer并为所有客户端重复使用。
options.catalog(必填)
Record
- 定义要公开的可用工具集。每个项目包括
name,description,可选内联tools,可选modules(适用于惰性加载器),可选decisionCriteria.
options.moduleLoader(可选)
Record
- 将模块密钥映射到返回的异步加载器
McpToolDefinition[]工具集通过引用modules: [key].
使用和行为
| 特性 | 详细信息 |
|---|---|
| 键命名 | 对象键是中引用的模块标识符 catalog[toolset].modules示例: { ext: async () => [...] } 和 modules: ["ext"]. |
| 加载器签名 | (context?: unknown) => Promise 或 McpToolDefinition[] |
| 调用 | STATIC模式时:启动时(用于指定的工具集或ALL)。DYNAMIC模式:当通过元工具启用工具集时。 |
| 返回值 | 要注册的工具数组。每个工具集的工具名称应该是唯一的;如果 namespaceToolsWithSetKey 如果是真的,名字会在注册时加前缀。 |
| 错误 | 抛出会拒绝该工具集的启用/预加载流,并向调用者显示错误。 |
| Idempotency | 加载器可能在运行/客户端之间被多次调用。保持它们的确定性/幂等性。如果执行昂贵的I/O,则实现内部缓存 |
示例
const moduleLoaders = {
ext: async (ctx?: unknown) => [
{
name: "echo",
description: "Echo back provided text",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
handler: async ({ text }: { text: string }) => ({
content: [{ type: "text", text }],
}),
},
],
};
const catalog = {
ext: { name: "Extensions", description: "Extra tools", modules: ["ext"] },
};选项.启动(可选)
{ mode?: "DYNAMIC" | "STATIC"; toolsets?: string[] | "ALL" }
- 控制启动行为。在静态模式下,预加载特定的工具集(或全部)。在DYNAMIC中,注册元工具并按需加载。
启动优先级和验证
| 输入 | 生效模式 | 工具集处理 | 结果/注释 |
|---|---|---|---|
startup.mode = "DYNAMIC" (是否存在工具集) | DYNAMIC | startup.toolsets 忽略 | 通过元工具在运行时管理工具集;如果发生以下情况,则记录警告 toolsets 提供 |
startup.mode = "STATIC", toolsets = "ALL" | STATIC | 从预加载所有工具集 catalog 好的 | |
startup.mode = "STATIC", toolsets = [names] | STATIC | 根据验证名称 catalog | 无效名称警告;如果没有剩余有效→ 错误 |
没有 startup.mode, toolsets = "ALL" | 静态 | 预加载所有工具集 | 确定 |
没有 startup.mode, toolsets = [names] | STATIC | 根据验证名称 catalog | 无效名称警告;如果没有剩余有效→ 错误 |
没有 startup.mode,没有 toolsets | DYNAMIC | 无预加载 | 默认行为;通过元工具在运行时管理工具集 |
options.registerMetaTools(可选)
boolean (默认值:DYNAMIC模式为true;STATIC模式为false)
- 是否注册元工具进行工具集管理。
- 在DYNAMIC模式下:注册所有元工具(
enable_toolset,disable_toolset,list_toolsets,describe_toolset,list_tools). - 在静态模式下:仅注册
list_tools(其他元工具不适用,因为工具集在启动时是固定的)。
options.exposurePolicy(可选)
ExposurePolicy
- 控制可以激活哪些工具集以及注册时如何命名工具。
| 字段 | 类型 | 用途 | 示例 |
|---|---|---|---|
maxActiveToolsets | number | 限制一次可以激活的工具集数量。防止工具膨胀。 | { maxActiveToolsets: 1 } 启用第二个工具集的块 |
namespaceToolsWithSetKey | boolean | 注册时用工具集键作为工具名称的前缀,以避免名称冲突。 | 与 true,使 core 寄存器 core.ping 而不是 ping |
allowlist | string[] | 只能启用这些工具集。其他人则被否认。 | { allowlist: ["core"] } 阻止启用 ext |
denylist | string[] | 无法启用这些工具集。 | { denylist: ["ext"] } 块 ext |
onLimitExceeded | (attempted, active) => void | 回调时 maxActiveToolsets 将被超越。 | 日志或遥测挂钩 |
备注
- 策略在启用时强制执行(通过元工具或静态预加载)。
- 如果两者都有
allowlist和denylist如果存在,则条目必须在allowlist而不是在denylist通过。 - 名称间距在注册时应用一致,并反映在
GET /tools.
options.context(可选)
unknown
- 任意上下文传递给
moduleLoaders在工具解析期间。
| 字段 | 类型 | 用途 | 示例 |
|---|---|---|---|
context | unknown | 每个人都可以获得额外的数据/注射剂 ModuleLoader(context) 解析工具时调用。 | { db, cache, apiClients } 在装载机内部用于制造工具 |
备注
- 仅
moduleLoaders接收context.中内联定义的直接工具catalog不要。 - 不通过HTTP向客户端公开;它在服务器上处于进程内。
- 保持轻便稳定;更喜欢传递句柄(例如db-client)而不是巨大的数据blob。
- STATIC模式:加载程序在启动时以相同的方式调用
context. - DYNAMIC模式:在启用时调用加载器,并使用相同的
context.
示例
const moduleLoaders = {
ext: async (ctx: any) => [
{
name: "echo",
description: "Echo using a backing service",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
handler: async ({ text }: { text: string }) => {
const result = await ctx.apiClients.echoService.send(text);
return { content: [{ type: "text", text: result }] } as any;
},
},
],
};options.sessionContext(可选)
SessionContextConfig
从查询参数中提取每个会话上下文的配置。启用多租户用例,其中每个客户端会话都可以有自己的上下文值传递给模块加载器。看 每个会话上下文 查看详细的使用示例。
| 字段 | 类型 | 默认值 | 描述 | |
|---|---|---|---|---|
enabled | boolean | true | 是否启用会话上下文提取 | |
queryParam.name | string | 'config' | 查询参数名称 | |
queryParam.encoding | `'base64' \ | 'json'` | 'base64' | 编码格式 |
queryParam.allowedKeys | string[] | - | 允许的密钥白名单(出于安全考虑,建议使用) | |
contextResolver | function | - | 自定义上下文解析器函数 | |
merge | `'shallow' \ | 'deep'` | 'shallow' | 如何与基础上下文合并 |
备注
- 根据请求提取会话上下文并将其与库合并
context选项 - 每个唯一的会话配置都会生成一个不同的缓存密钥,从而启用每个租户的模块缓存
- 无效的编码或解析错误会自动回退到基础上下文(故障安全)
- 仅适用于DYNAMIC模式服务器;STATIC模式使用单个共享服务器实例
options.http(可选)
{ host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean; customEndpoints?: CustomEndpointDefinition[] }
- 加快运输配置。默认值:主机
0.0.0.0,港口3000,basePath/,CORS已启用,记录器已禁用。 customEndpoints:可选的自定义HTTP端点数组,与MCP协议端点一起注册。看 自定义HTTP端点 了解详情。
options.createServer(可选)
() => McpServer
创建SDK服务器实例所需的工厂。
options.configSchema(可选)
object
- JSON模式在
GET /.well-known/mcp-config用于客户端发现。
createPermissionBasedMcpServer(选项)
创建一个权限感知的MCP服务器,每个客户端只接收他们有权访问的工具集。此函数为基于权限的场景提供了一个单独的API,同时维护与 createMcpServer.
需求
createServer必须提供permissions必须提供配置- 服务器在每个客户端的STATIC模式下运行(工具集由权限决定)
- 每个客户端都会获得一个带有其特定工具集的隔离服务器实例
选项.权限(必填)
PermissionConfig
定义如何解析和执行客户端权限。
权限源类型
| 来源 | 描述 | 用例 | 安全级别 |
|---|---|---|---|
headers | 从请求标头读取权限 | 在经过身份验证的代理/网关后面 | 中等(需要外部验证) |
config | 服务器端权限查找 | 直接服务器控制 | 高(服务器控制) |
基于标头的配置
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
source | 'headers' | 必需 | 表示基于标头的权限 |
headerName | string | 'mcp-toolset-permissions' | 包含逗号分隔的工具集列表的标题名称 |
基于配置的配置
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
source | 'config' | yes | 表示基于配置的权限 |
staticMap | Record | staticMap或解析器之一 | 将客户端ID映射到工具集数组 |
resolver | (clientId: string) => string[] | staticMap或解析器之一 | 为客户端返回工具集数组的函数 |
defaultPermissions | string[] | no | 未知客户端的回退权限(默认值: []) |
备注
- 对于基于配置的权限,至少有一个
staticMap或resolver必须提供 - 如果两者都提供,
resolver先尝试,然后staticMap那么defaultPermissions - 解析器函数必须是同步的,并返回字符串数组
- 在服务器创建过程中,权限中的无效工具集名称会被过滤掉
options.catalog(必填)
同 createMcpServer -看 选项目录.
options.moduleLoader(可选)
同 createMcpServer -看 options.moduleLoader.
options.exposurePolicy(可选)
ExposurePolicy (部分支持)
基于权限的服务器仅支持 namespaceToolsWithSetKey。其他策略字段被忽略,因为工具集访问由权限控制:
| 现场 | 支持 |
|---|---|
namespaceToolsWithSetKey | ✅ 支持(默认值:true) |
allowlist | ⚠️ 忽略(由客户端权限决定) |
denylist | ⚠️ 忽略(改用权限) |
maxActiveToolsets | ⚠️ 忽略(由权限计数决定) |
onLimitExceeded | ⚠️ 忽略(未强制执行工具集限制) |
注: 如果您提供忽略选项,服务器将在启动时记录警告以提醒您。
options.http(可选)
同 createMcpServer -看 选项.html.
options.createServer(必需)
同 createMcpServer -看 options.createServer.
options.configSchema(可选)
同 createMcpServer -看 options.configSchema.
options.context(可选)
同 createMcpServer -看 options.context.
options.sessionContext(可选)
SessionContextConfig
会话上下文在基于权限的服务器中可用,但支持有限。由于基于权限的服务器在连接时根据权限确定工具集,因此会话上下文不会影响加载哪些工具集。但是,合并的上下文仍然会传递给模块加载器。
注: 如果出现以下情况,则发出警告 sessionContext 与一起使用 createPermissionBasedMcpServer。要使用每会话工具集缓存获得完整的会话上下文支持,请使用 createMcpServer 采用动态模式。
元工具
元工具按模式注册:
动态模式 (默认注册,或当 registerMetaTools 是真的):
enable_toolset-按名称启用工具集disable_toolset-按名称禁用工具集(仅限状态;工具仍保持注册状态)list_toolsets-列出处于活动状态的可用工具集describe_toolset-描述一个具有定义和工具的特定工具集list_tools-列出当前注册的工具名称
静态模式 (当 registerMetaTools 是真的):
list_tools-列出当前注册的工具名称
注:在静态模式下, enable_toolset, disable_toolset, list_toolsets,以及 describe_toolset 由于工具集在启动时是固定的,因此不可用。
基于权限的客户端集成
使用基于标头的权限
当使用基于标头的权限连接到基于权限的服务器时,请包括 mcp-toolset-permissions 带有逗号分隔的工具集列表的标题:
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const clientId = "my-client-id";
const allowedToolsets = ["user", "reports"]; // determined by your auth system
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
{
requestInit: {
headers: {
"mcp-client-id": clientId,
"mcp-toolset-permissions": allowedToolsets.join(","),
},
},
}
);
const client = new Client({ name: "example-client", version: "1.0.0" });
await client.connect(transport);
// Client can only access tools from 'user' and 'reports' toolsets
const tools = await client.listTools();
console.log(tools); // Only shows user.* and reports.* tools
await client.close();重要提示: 您的应用层必须验证并可能对权限标头进行签名/加密,以防止篡改。MCP服务器按原样信任标头值。
使用基于配置的权限
当连接到具有基于配置的权限的基于权限的服务器时,只需提供 mcp-client-id 头球服务器在内部查找权限:
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const clientId = "admin-client-id"; // matches server's staticMap or resolver
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
{
requestInit: {
headers: {
"mcp-client-id": clientId,
// No permission header needed - server looks up permissions
},
},
}
);
const client = new Client({ name: "example-client", version: "1.0.0" });
await client.connect(transport);
// Client receives toolsets based on server configuration
const tools = await client.listTools();
console.log(tools); // Shows tools based on server's permission config
await client.close();安全: 基于配置的权限提供了更好的安全性,因为客户端无法影响自己的权限。确保您的客户端ID在到达MCP服务器之前经过身份验证和确认。
客户端ID生命周期
- 什么:客户通过
mcp-client-id每个请求上的HTTP标头。 - 谁生成的:客户。使用稳定的标识符(例如,本地持久的UUID)。
- 如果省略:MCP协议端点(
POST /mcp,GET /mcp,DELETE /mcp)返回400错误。自定义端点仍然接受具有自动生成ID的匿名客户端。
示例(MCP官方客户)
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
// Create a stable client id (persist it for reuse across runs)
const clientId = "my-stable-client-id"; // e.g., from disk/env
// Transport manages HTTP, including SSE and JSON-RPC framing
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
{
requestInit: { headers: { "mcp-client-id": clientId } },
}
);
// High-level MCP client
const client = new Client({ name: "example-client", version: "1.0.0" });
// Connect negotiates capabilities and establishes a session. Transport handles session id.
await client.connect(transport);
// Call a tool (example)
const res = await client.listTools();
console.log(res);
// Close when done
await client.close();会话ID生命周期
- 什么:服务器在初始化时返回的每个会话标识符。
- 谁生成的:初始化期间的服务器。客户端必须从初始化响应标头中读取它,并在后续请求中通过以下方式将其发送回
mcp-session-id. - 用于:跟进JSON-RPC请求(POST
/mcp),SSE流(GET/mcp),以及终止(删除/mcp).
示例(MCP官方客户)
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const clientId = "my-stable-client-id";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
{
requestInit: { headers: { "mcp-client-id": clientId } },
}
);
const client = new Client({ name: "example-client", version: "1.0.0" });
await client.connect(transport);
// Session id is handled by the transport. No need to manually set mcp-session-id.
// Call tools
await client.callTool({ name: "enable_toolset", arguments: { name: "core" } });
const ping = await client.callTool({ name: "core.ping", arguments: {} });
console.log(ping);
// When finished
await client.close();基于权限的安全最佳实践
何时使用每种方法
在以下情况下使用基于标头的权限:
- 您有一个验证和设置标头的身份验证网关/代理
- 您需要经常更改的动态权限
- 您的身份验证系统位于MCP服务器外部
- 您可以确保标头经过加密签名或验证
在以下情况下使用基于配置的权限:
- 您希望服务器端控制权限
- 权限相对稳定
- 您需要最高的安全级别
- 您希望避免信任客户端提供的数据
身份验证和授权模式
基于标头的模式:
Client → Auth Gateway → MCP Server
(validates,
sets headers)身份验证网关必须:
- 对客户端进行身份验证
- 确定授权工具集
- 集
mcp-toolset-permissions头球 - 可选地对标头进行签名/加密,以防止篡改
基于配置的模式:
Client → MCP Server → Permission Lookup
(validates (staticMap or
client-id) resolver)MCP服务器:
- 接收客户端id
- 在内部查找权限
- 不信任客户端提供的权限数据
标头验证和签名
如果使用基于标头的权限,请执行验证以防止篡改:
import crypto from "crypto";
// Example: Using HMAC to sign permission headers
function signPermissions(
clientId: string,
toolsets: string[],
secret: string
): string {
const data = `${clientId}:${toolsets.join(",")}`;
const signature = crypto
.createHmac("sha256", secret)
.update(data)
.digest("hex");
return `${toolsets.join(",")};sig=${signature}`;
}
function verifyPermissions(
clientId: string,
headerValue: string,
secret: string
): string[] {
const [toolsetsStr, sigPart] = headerValue.split(";sig=");
const expectedSig = crypto
.createHmac("sha256", secret)
.update(`${clientId}:${toolsetsStr}`)
.digest("hex");
if (sigPart !== expectedSig) {
throw new Error("Invalid permission signature");
}
return toolsetsStr.split(",").map((s) => s.trim());
}
// In your auth gateway:
const clientId = "user-123";
const allowedToolsets = ["user", "reports"];
const signedHeader = signPermissions(clientId, allowedToolsets, SECRET_KEY);
// Forward to MCP server with signed header
fetch("http://mcp-server:3000/mcp", {
headers: {
"mcp-client-id": clientId,
"mcp-toolset-permissions": signedHeader,
},
});安全注意事项
基于标头的权限:
- 风险: 如果没有正确保护,客户端可能会操纵标头
- 缓解措施: 始终验证/签署应用层中的标头
- 建议: 仅在经过身份验证的反向代理或网关后使用
- 最佳实践: 使用HMAC或JWT实现标头签名
基于配置的权限:
- 好处: 服务器端权限存储提供了更强的安全性
- 建议: 首选生产环境
- 最佳实践: 在客户端ID到达MCP服务器之前对其进行身份验证
- 注: 无客户端权限数据泄露
一般安全:
- 权限缓存: 每个客户端会话都会缓存权限。权限更改时使会话无效。
- 客户端隔离: 每个客户端都有一个独立的服务器实例。无跨客户端权限泄漏。
- 错误消息: 服务器避免在错误响应中暴露未经授权的工具集名称。
- 客户端ID验证: 在请求到达MCP服务器之前,始终在应用层中验证和验证客户端ID。
错误处理和信息披露
当客户端试图访问未经授权的工具集时:
- 服务器返回一个通用的“拒绝访问”错误
- 未经授权的工具集名称不会在错误消息中显示
- 这可以防止有关可用工具集的信息泄露
- 客户只能看到他们通过授权访问的工具
listTools()
基于权限的常见模式
多租户服务器设置
创建一个服务器,每个租户都可以访问自己的工具集和共享工具:
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const { start, close } = await createPermissionBasedMcpServer({
catalog: {
"tenant-a-tools": {
name: "Tenant A",
description: "Tools for tenant A",
modules: ["tenant-a"],
},
"tenant-b-tools": {
name: "Tenant B",
description: "Tools for tenant B",
modules: ["tenant-b"],
},
"shared-tools": {
name: "Shared",
description: "Shared tools",
modules: ["shared"],
},
},
moduleLoaders: {
"tenant-a": async () => [
/* tenant A specific tools */
],
"tenant-b": async () => [
/* tenant B specific tools */
],
shared: async () => [
/* shared tools */
],
},
permissions: {
source: "config",
resolver: (clientId: string) => {
const [tenant] = clientId.split("-");
if (tenant === "tenantA") {
return ["tenant-a-tools", "shared-tools"];
}
if (tenant === "tenantB") {
return ["tenant-b-tools", "shared-tools"];
}
return ["shared-tools"]; // unknown tenants get only shared tools
},
},
http: { port: 3000 },
createServer: () =>
new McpServer({
name: "multi-tenant-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
}),
});
await start();与外部认证系统集成
通过预加载权限与外部身份验证系统集成:
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Pre-load permissions from your auth system
// This should be done before server creation and cached
const permissionCache = new Map();
async function loadPermissionsFromAuthSystem() {
// Fetch permissions from your auth system
// This is just an example - implement according to your system
const users = await authSystem.getAllUsers();
for (const user of users) {
const permissions = await authSystem.getUserPermissions(user.id);
permissionCache.set(user.id, permissions.allowedToolsets);
}
}
// Load permissions at startup
await loadPermissionsFromAuthSystem();
// Optionally refresh permissions periodically
setInterval(loadPermissionsFromAuthSystem, 5 * 60 * 1000); // every 5 minutes
const { start, close } = await createPermissionBasedMcpServer({
catalog: {
/* your toolsets */
},
moduleLoaders: {
/* your loaders */
},
permissions: {
source: "config",
resolver: (clientId: string) => {
// Synchronous lookup from pre-loaded cache
return permissionCache.get(clientId) || [];
},
defaultPermissions: ["public"], // unauthenticated users get public tools
},
http: { port: 3000 },
createServer: () =>
new McpServer({
name: "auth-integrated-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
}),
});
await start();基于角色的访问控制(RBAC)
使用预定义的角色到工具集映射实现基于角色的访问控制:
import { createPermissionBasedMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Define role-to-toolset mappings
const rolePermissions = {
admin: ["admin-tools", "user-tools", "reports", "analytics"],
manager: ["user-tools", "reports", "analytics"],
user: ["user-tools", "reports"],
guest: ["public-tools"],
};
// Map client IDs to roles (could come from database, JWT claims, etc.)
function getRoleForClient(clientId: string): string {
// Example: extract role from client ID or look up in database
if (clientId.startsWith("admin-")) return "admin";
if (clientId.startsWith("manager-")) return "manager";
if (clientId.startsWith("user-")) return "user";
return "guest";
}
const { start, close } = await createPermissionBasedMcpServer({
catalog: {
"admin-tools": {
name: "Admin",
description: "Admin tools",
modules: ["admin"],
},
"user-tools": {
name: "User",
description: "User tools",
modules: ["user"],
},
reports: {
name: "Reports",
description: "Reporting tools",
modules: ["reports"],
},
analytics: {
name: "Analytics",
description: "Analytics tools",
modules: ["analytics"],
},
"public-tools": {
name: "Public",
description: "Public tools",
modules: ["public"],
},
},
moduleLoaders: {
admin: async () => [
/* admin tools */
],
user: async () => [
/* user tools */
],
reports: async () => [
/* report tools */
],
analytics: async () => [
/* analytics tools */
],
public: async () => [
/* public tools */
],
},
permissions: {
source: "config",
staticMap: {
// Known admin users
"admin-user-1": rolePermissions.admin,
"admin-user-2": rolePermissions.admin,
// Known managers
"manager-user-1": rolePermissions.manager,
// Known regular users
"regular-user-1": rolePermissions.user,
"regular-user-2": rolePermissions.user,
},
resolver: (clientId: string) => {
// Dynamic role lookup for clients not in static map
const role = getRoleForClient(clientId);
return rolePermissions[role] || rolePermissions.guest;
},
defaultPermissions: rolePermissions.guest,
},
http: { port: 3000 },
createServer: () =>
new McpServer({
name: "rbac-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
}),
});
await start();工具类型
- 直接工具:内联定义
catalog[toolset].tools并在启用该工具集时注册。 - 模块生产工具:返回
moduleLoaders[moduleKey]()并在启用引用的工具集时进行注册modules: [moduleKey].
使用直接工具进行简单/本地实用程序;使用模块生成的工具跨多个工具集共享工具,或延迟加载较重的定义。
关于动态模式的说明:支持直接工具和模块生成工具。模块生成的工具通过在启用时启用按需加载来帮助最大限度地减少启动占用空间。
启动模式
服务器以两种主要模式之一运行:
- 动态模式 (
startup.mode = "DYNAMIC")
- 已注册的所有元工具: enable_toolset, disable_toolset, list_toolsets, describe_toolset, list_tools - 通过元工具调用按需加载工具 - 每个客户端都有一个独立的服务器实例 - 最适合工具需要更改的灵活、特定任务的工作流程
- 静态模式 (
startup.mode = "STATIC")
- 启动时预加载特定工具集(toolsets 数组或“全部”) - 仅 list_tools 元工具可用(运行时无法更改工具集) - 单个服务器实例可供所有客户端重用 - 最适合已知、一致的工具要求
许可证
阿帕奇-2.0。看 LICENSE 了解详情。
