@ frontmcp/plugin - 核准
一个FrontMCP插件,拒绝运行一个工具,直到为呼叫者录制一个批准. 用来做工具
不应该在没有别人说的情况下跑,比如删除机票或者退款。 标记工具为 approval, (中文(简体) ),
直到你的代码批准之前 每一个电话都被拒绝 this.approval 或插件的商店.
每一次批准批准的记录,可以撤销或者规定失效. 你决定一个人如何答应:
窗体从 this.elicit(),或采用外部审批系统,插件会向您询问
网点 或通过 投票。 。 。 .
安装
npm install @frontmcp/plugin-approval @frontmcp/sdk
需要节点.js 24或后. 保留 @frontmcp/sdk 上。 核准
保存在内存中,除非您配置 storage重新获得支持ioredis)包括;对于其他商店,安装
他们自己的客户:
@vercel/kv,可选的同伴依赖,用于storage: { type: 'vercel-kv' }。 。 。 .@upstash/redis页:1storage: { type: 'upstash' }。 。 。 .
此插件不属于 @frontmcp/plugins 元包装.
使用量
注册插件于 @FrontMcp打开每个应用程序的工具,或者打开一个 @App标出需要批准的工具,
并给用户一个批准的方法:
import { ApprovalPlugin, userGrantor } from '@frontmcp/plugin-approval';
import { App, FrontMcp, Tool, ToolContext, z } from '@frontmcp/sdk';
@Tool({
name: 'delete_ticket',
description: 'Delete a support ticket for good. Needs approval: call approve_delete first.',
inputSchema: { id: z.string() },
approval: {
approvalMessage: 'delete_ticket needs the user to approve it. Call approve_delete first.',
category: 'delete',
riskLevel: 'high',
},
})
class DeleteTicket extends ToolContext {
async execute({ id }: { id: string }) {
return { id, deleted: true };
}
}
@Tool({ name: 'approve_delete', description: 'Ask the user to allow delete_ticket for this session', inputSchema: {} })
class ApproveDelete extends ToolContext {
async execute() {
// The user answers this form in the client. The model can't.
const answer = await this.elicit(
'Allow the assistant to delete tickets in this session?',
z.object({ allow: z.boolean() }),
);
if (answer.status !== 'accept' || !answer.content?.allow) return { approved: false };
await this.approval.grantSessionApproval('help-desk:delete_ticket', {
grantedBy: userGrantor(this.auth.user.sub),
});
return { approved: true };
}
}
@App({ id: 'help-desk', name: 'Help Desk', tools: [DeleteTicket, ApproveDelete] })
class HelpDeskApp {}
@FrontMcp({
info: { name: 'help-desk', version: '1.0.0' },
apps: [HelpDeskApp],
elicitation: { enabled: true },
plugins: [ApprovalPlugin.init()],
})
export default class Server {}
- 工具标识是
<app id>:<tool name>一样help-desk:delete_ticket给予'delete_ticket'记录 a 批准什么也没有检查. - 批准属于打电话者,因此使用已签名用户的插件。 匿名电话可以换个新的 然后就永远不适用于他们的下一次通话.
- 别让模特儿批准自己的电话. 一个不问人就提供工具 就能保护大门 没什么 如上所述,在表格后给予用户答复,或给予更多权利的呼叫者答复.
- 字段需要它的插件 . 一个工具所在的服务器
approval没有ApprovalPlugin到达它不会开始: 失败UnenforcedMetadataError。 。 。 . ApprovalPlugin.init()没有争论plugins: [ApprovalPlugin],使用默认值.
客户所看到的
拒绝调用是模型可以读取的工具错误: isError: true, (中文(简体) ), _meta.code: 'APPROVAL_REQUIRED'文本时
该工具的 approvalMessage在生产过程中(批准错误扩大) PublicMcpError) (中文(简体) ). 默认
留言, Tool "<id>" requires approval to execute. Allow?不要告诉模特儿下一步该做什么,所以写你
拥有的。 这个工具 category, (中文(简体) ), riskLevel 其他字段留在服务器上.
这个 approval 打开 @Tool
approval: true 表示 { required: true, defaultScope: ApprovalScope.SESSION }。任何物体都需要批准,除非
它说 required: false。 。 。 .
| 外地 | 它会做什么 |
|---|---|
required | false 把门关了 默认 true。 。 。 . |
skipApproval | true 把门也关了. |
alwaysPrompt | 每次批准都允许一个呼叫运行,并被使用. |
approvalMessage | 拒书之文. |
allowedScopes | 批准这一工具的范围。 补助金通过 this.approval 其他范围抛出 ApprovalScopeNotAllowedError。 。 。 . |
maxTtlMs | 这个工具的核准时间最长. |
preApprovedContexts | 让一个电话在未经批准的情况下运行 authInfo.extra.approvalContext 火柴。 比赛。 FrontMCP的认证模式没有设置,所以你自己的代码必须设置. |
category, (中文(简体) ), riskLevel, (中文(简体) ), defaultScope | 锁定错误 details客户看不到的. |
this.approval
该插件添加 this.approval 给它注册的应用程序的工具、资源和提示。 它的作用为
当前调用器,并在 <app id>:<tool name> 形式.
| 方法 | 它会做什么 |
|---|---|
isApproved(toolId, context?) | 门是否让呼叫者运行工具 . |
grantSessionApproval(toolId, options?) | 批准召会. |
grantUserApproval(toolId, options?) | 批准在每届会议上召集人. |
grantTimeLimitedApproval(toolId, ttlMs, options?) | 核准: ttlMs 毫秒长. |
grantContextApproval(toolId, context, options?) | 在一个方面核准, { type, identifier } (见 preApprovedContexts) (中文(简体) ). |
revokeApproval(toolId, options?) | 删去呼叫人对工具的认可,保留副本24小时. |
getRevocations(toolId) | 调用者对该工具的认可在后24小时内被撤销. |
getApproval, (中文(简体) ), getSessionApprovals, (中文(简体) ), getUserApprovals, (中文(简体) ), queryApprovals, (中文(简体) ), clearSessionApprovals | 读取或清取来电所录. |
赠款 options 为: { grantedBy?, reason?, metadata? }选项 { revokedBy?, reason? }没有
grantedBy,一个赠款记录了与 method: 'implicit'上面写着没人问:
{ source: 'user', identifier: <user id>, method: 'implicit' },或 { source: 'user', method: 'implicit' } 没有
注册用户。 在用户同意后,通过一个设保人说:
import { policyGrantor, userGrantor } from '@frontmcp/plugin-approval';
// { source: 'user', identifier: 'user-123', displayName: 'Jane Doe', method: 'interactive' }
await this.approval.grantUserApproval('ops:deploy', { grantedBy: userGrantor('user-123', 'Jane Doe') });
// { source: 'policy', identifier: 'safe-list', method: 'implicit' }
await this.approval.grantSessionApproval('ops:deploy', { grantedBy: policyGrantor('safe-list') });
设保人的其他帮助者是: adminGrantor, (中文(简体) ), systemGrantor, (中文(简体) ), agentGrantor, (中文(简体) ), apiGrantor, (中文(简体) ), oauthGrantor, (中文(简体) ),
testGrantor 财务报告和审定财务报表 customGrantor,与匹配的撤销器助手(userRevoker, (中文(简体) ), adminRevoker, (中文(简体) ), policyRevoker,以及
页:1.
要批准或撤销除打电话者以外的人,如批准队友的领先者,使用商店:
this.get(ApprovalStoreToken) 返回,使用 grantApproval({ toolId, scope, userId?, sessionId?, ttlMs?, grantedBy?, ... })
财务报告和审定财务报表 revokeApproval(...)。 。 。 .
范围
ApprovalScope 是字符串音符 :
| 范围 | 数值 | 它会做什么 |
|---|---|---|
SESSION | 'session' | 核准本届会议. |
USER | 'user' | 在每届会议上为用户核准. |
TIME_LIMITED | 'time_limited' | 需要a ttlMs,并批准至到期. |
CONTEXT_SPECIFIC | 'context_specific' | 仅核准发出这方面的呼吁,因为 authInfo.extra.approvalContext 携带. |
TOOL_SPECIFIC | 'tool_specific' | 标签 插件中没有任何内容授予它. |
根据MCP协议2026-07-28,客户端没有会话,因此每个用户都会保持会话批准,并持续到完成 撤销或过期。 在有需要时,给予有时限的批准.
选项
ApprovalPlugin.init({
storage: { type: 'redis', redis: { url: process.env.REDIS_URL } },
});
| 选项 | 默认 | 说明 |
|---|---|---|
storage | { type: 'auto' } | 在保留批准的情况下: 'memory', (中文(简体) ), 'redis', (中文(简体) ), 'vercel-kv', (中文(简体) ), 'upstash', (中文(简体) ), 'cloudflare-kv' 或者说 'auto'。 。 。 . 'auto' 从其环境变量中选择 Upstash, Vercel KV 或 Redis, 否则则选择内存 . |
storageInstance | 一个你做的存储器 createStorage() 从 @frontmcp/utils胜利结束 storage。 。 。 . | |
namespace | 'approval' | 插件密钥的前缀 . |
cleanupIntervalSeconds | 60 | 过期核准被删除的频率如何. 0 关掉. |
mode | 'recheck' | 'recheck' 民意调查 recheck.url 当它被设置的时候; 'webhook' 登记a ChallengeService 并向 webhook.url 当它被设置。 没有 URL , 既不接触外部系统 . |
recheck | 见 重新检查模式。 。 。 . | |
webhook | 见 Webhook 模式。 。 。 . webhook.challengeTtl (默认) 300)是挑战持续多久,以秒计. | |
enableAudit | false | 日志 a 审计线索。 。 。 . |
maxDelegationDepth | 3 | 最深处 授权链 授权批准可以指名,0至64个. |
在浏览器构建中( I)create() 在网页上,WebMCP, storage: { type: 'redis' } 或者说 { type: 'sqlite' } 掷出
BrowserStoreUnavailableError 从 init()名称 storage: { type: 'memory' }: 一页没有 TCP 套接字, 没有
文档. { type: 'auto' } 在那里选择记忆.
审计线索
与 enableAudit: true,插件日志 1 info 线条( 浏览器) ApprovalAudit每个事件。 讯息是
<event> <toolId>,第一个论点是:
| 活动 | 何时 |
|---|---|
approval.requested | 一通电话没有得到任何认可 它可以使用. state数字 : pending, (中文(简体) ), expired, (中文(简体) ), denied,或 approved 由复检系统进行; via数字 : webhook 或者说 recheck。 。 。 . |
approval.granted | 核准记录如下: this.approval,商店,一个webhook召回或重新检查答案. |
approval.denied | 一份否认书被记录在案. |
approval.revoked | 一项批准被撤销(revokedBy, (中文(简体) ), reason),或会议批准被批准(count) (中文(简体) ). |
approval.expired | 清理删除了过期的审批或拒绝. |
字段是工具、范围、用户编号、上下文、设保人或撤销人、理由和 expiresAt。一条线从不携带
会话 id、 符号或记录 metadata。一个过期的后端,例如Redis,可以删除已过期的
记录在清理之前; approval.granted 携带其 expiresAt。 。 。 .
默认关闭:最多1.11.2项选项无所作为(其记录的默认为 true,并打开它添加
线条,在日志中带有用户编号。 以木质运输方式追踪他们.
代表团链
授权批准姓名,授权: grantedBy.delegationContext, (中文(简体) ), { delegatorId, delegateId, purpose?, constraints?, chain? }代表可进一步授权: chain 代表团名单
授权人的第一个,和 extendDelegation() 建立下个.
import { agentGrantor, extendDelegation } from '@frontmcp/plugin-approval';
const toPlanner = { delegatorId: 'user-123', delegateId: 'planner-agent', purpose: 'release' };
const toDeployer = extendDelegation(toPlanner, 'deploy-agent', { purpose: 'deploy step' });
// { delegatorId: 'planner-agent', delegateId: 'deploy-agent', purpose: 'deploy step',
// chain: [{ delegatorId: 'user-123', delegateId: 'planner-agent', purpose: 'release' }] }
await this.approval.grantSessionApproval('ops:deploy', { grantedBy: agentGrantor('deploy-agent', toDeployer) });
maxDelegationDepth (默认) 3限制链条,计算代表团本身: toPlanner 一个深,
toDeployer 2. 其链条更深、断裂(不是前一名代表的委托人)或当事人名称的赠款
两次抛出 ApprovalOperationError,而存储的批准 这样不允许一个电话通过。 拒绝保留
无论他们的链子是什么. maxDelegationDepth: 1 仅允许直接授权,以及 0 没有授权核准.
delegationDepth(context) 财务报告和审定财务报表 delegationProblem(context, maxDepth) 自己检查一个上下文。 最多1.11.2选项
什么都没做.
Webhook 模式
与 mode: 'webhook' 财务报告和审定财务报表 webhook.url电话需要批准 它不会发出签名的批准请求
并被拒绝作为 pending. 系统
由 POSTING 返回到 webhook.callbackPath,而打电话者的下一个呼叫运行,或被拒绝作为 denied。 。 。 .
ApprovalPlugin.init({
storage: { type: 'redis', redis: { url: process.env.REDIS_URL } },
mode: 'webhook',
webhook: {
url: 'https://approvals.example.com/webhook',
secret: process.env.APPROVAL_WEBHOOK_SECRET, // required with url, at least 24 bytes
challengeTtl: 300,
callbackPath: '/approval/callback',
},
});
| 选项 | 默认 | 说明 |
|---|---|---|
webhook.url | 请求被搁置的地方. https://· ; http://localhost, (中文(简体) ), 127.0.0.1 或者说 [::1] 仅在 NODE_ENV 没有 production没有证书. | |
webhook.secret | 签名每个请求并验证每个召回。 要求为 urlA级 whsec_ secret (Standard Webhooks) 是前缀后的基础64. | |
webhook.includeJwt | false | 在请求中添加调用符 jwt. 只有当审批制度必须充当调用人时才设置. |
webhook.challengeTtl | 300 | 驳回请求等待裁决 驳回请求没有 ttlMs 持续时间. |
webhook.callbackPath | '/approval/callback' | 服务器作出决定的路径( T)POST) (中文(简体) ). |
请求,每个呼叫者和工具发送一次,直至决定或 challengeTtl 通行证 :
{
"type": "approval.requested",
"codeChallenge": "…",
"codeVerifier": "…",
"toolId": "ops:delete_ticket",
"toolName": "delete_ticket",
"category": "delete",
"riskLevel": "high",
"approvalMessage": "…",
"requestedScope": "session",
"userId": "user-123",
"callbackPath": "/approval/callback",
"callbackUrl": "https://mcp.example.com/approval/callback",
"expiresAt": 1700000300000,
"timestamp": 1700000000000
}
它从来不给会话命名. callbackUrl 有没有当 FRONTMCP_PUBLIC_URL 设置; allowedScopes 财务报告和审定财务报表 maxTtlMs
当工具限制它们时; jwt 仅使用 includeJwt: true保留 codeVerifier 秘密:决定
请求。 失败的请求( 错误状态, 重定向, 10 秒内没有答案) 由下一个调用再次发送 .
这一决定,被选为: callbackPath 并以同一秘密签署:
{
"codeVerifier": "…",
"approved": true,
"scope": "session",
"ttlMs": 3600000,
"grantedBy": { "source": "user", "identifier": "lead-7" },
"reason": "ok"
}
- 被记录下来的,是给打电话的人的 要求,从来没有一个尸体的名字
allowedScopes财务报告和审定财务报表maxTtlMs(单位:千美元)scope默认为requestedScope),与grantedBy财务报告和审定财务报表origin: 'webhook'A级 拒绝(approved: false)记录在呼叫者的会话中,并持续ttlMs,或challengeTtl没有它. - 每个
codeVerifier工作过一次,直到challengeTtl通过。 重播或迟到的决定毫无作用. - 每一个签名的,造型良好的召回
202 { "status": "received" },无论它是否符合请求,所以 路由从未说明是否等待批准。 未签名或错误签名的回调,或一个以上签名的回调 几分钟前,得到401一个不是决定的身体400; 1 超过 16 KB413。 。 。 .
请求和回调与HMAC-SHA256在 标准网络呼号
格式,所以任何标准 Webhooks 库检查它们:页眉 webhook-id, (中文(简体) ), webhook-timestamp (Unix 秒)和 (韩语)
webhook-signature: v1,<base64>结束 <webhook-id>.<webhook-timestamp>.<body>检查时间戳在你这边,
并拒绝一个 webhook-id 你已经看到了.
this.get(ChallengeServiceToken) 返回插件 ChallengeService,这些一次性 PKCE (Proof Key) 的存储器
代码交换,RFC 7636挑战: createChallenge(...), (中文(简体) ), consume(codeVerifier) (请求一次,或
undefined和 verifyAndConsume(codeVerifier) (请求一次,或 ChallengeValidationError) (中文(简体) ).
重新检查模式
与 mode: 'recheck' (默认)和 recheck.url需要你同意的电话 没有民调
批准API直到它决定.
ApprovalPlugin.init({
recheck: {
url: 'https://approvals.example.com/status',
auth: 'bearer',
token: process.env.APPROVALS_API_TOKEN,
interval: 2000,
maxAttempts: 5,
},
});
| 选项 | 默认 | 说明 |
|---|---|---|
recheck.url | 状态 URL, 规则与 webhook.url。 。 。 . | |
recheck.auth | 'none' | 'bearer' 发送 recheck.token· ; 'jwt' 转发呼叫者自己的访问信使(没有传票的呼叫); 'custom' 发送信头 recheck.headers(request) 返回. |
recheck.token | 标志 auth: 'bearer' 发送. | |
recheck.headers | (request) => headers,用于 auth: 'custom'。 。 。 . | |
recheck.secret | 在每张民意测验上签名时, 请签名 . | |
recheck.interval | 2000 | 民调之间的相差100到60000秒. |
recheck.maxAttempts | 5 | 每通电话1到60. interval × (maxAttempts − 1) 最多两分钟. |
每份民意调查 { type: 'approval.status', requestId, attempt, toolId, toolName, userId?, requestedScope, ... }
(单位:千美元)requestId 对一个打电话者和工具的每一次民意测验都是一样的,姓名也不相同),并读作:
{ approved, pending?, scope?, ttlMs?, grantedBy?, denialReason? }数字 :
{ "approved": true }记录了批准情况,因为网上核准记录了批准情况,但与origin: 'recheck',则 呼叫运行。 一个alwaysPrompt工具的批准让它单独通过 并且没有存储.{ "approved": false, "pending": true }投票之后interval; 之后maxAttempts民意调查 电话被拒绝pending。 。 。 .{ "approved": false }拒绝调用为denied. 无所存: 下一次民调.- 一个错误状态,一个方向调整,一个不是这样的答案的身体,超过64 KB,或者10秒内没有答案拒绝
调用为
pending,并登录一个只列出 URL 来源的警告.
有记录的拒绝在任何信息被传票或发出之前,以两种方式拒绝打电话.
启动检查
ApprovalPlugin.init() 掷出 ApprovalConfigurationError (代码) APPROVAL_CONFIGURATION),服务器不
启动, 对于无法工作的选项 : webhook.url 无 webhook.secret,或24字节以下的秘密; webhook.*
(不含 challengeTtl页:1 mode: 'webhook' 或无 webhook.url· ; recheck 内 mode: 'webhook' 或者说
无 recheck.url; 不是一个 URL https:// (或外部生产回放),或在其中有资质;
auth: 'bearer' 无 token, (中文(简体) ), auth: 'custom' 无 headers,或为另一个设定 auth; 一个
interval 或者说 maxAttempts a 区域外;和 callbackPath 这不是一条平坦的道路,服务器保留了
MCP 端点, /oauth/*, (中文(简体) ), /.well-known/*),或另一个 ApprovalPlugin 服务。 最多为1.11.2项选择
被接受和忽视.
文档
相关软件包
@frontmcp/sdk: 此插件扩展的框架@frontmcp/plugin-feature-flags: 隐藏和块 特性标记后的工具@frontmcp/plugin-remember数字 :this.remember,一个记忆 用于工具
捐款
资料来源: plugins/plugin-approval问题
欢迎抽取请求;见 捐款额。 。 。 .
许可证
阿帕奇2.0 (英语). 见 自由主义。 。 。 .