跳转到内容

契约工具

投影通过现有的工具目录公开选定的契约操作。契约仍然是 Effect 值。

  • 本地服务与受管 Remote 门面使用相同的绑定。
  • 每个工具显式声明元数据和输出处理方式。
  • 更新视图保留传输连接与已准入的调用。
import { Context, Effect, Schema } from "effect"
import { Rpc } from "effect/rpc"
import { Plugin } from "@graftland/graft"
import * as Projection from "@graftland/plugin-mcp-server/Projection"
import { Contract } from "@graftland/plugin-remote"
class Forecast extends Context.Service<
Forecast,
{
readonly get: (city: string) => Effect.Effect<string>
}
>()("example/Forecast") {}
const Get = Contract.operation(
"example/forecast/Get",
1,
Rpc.make("Get", {
payload: { city: Schema.String },
success: Schema.String,
}),
).annotate(Projection.Annotation, {
name: "forecast",
title: "City forecast",
description: "Read a city forecast",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
output: "json",
})
export default Plugin.make({
name: "forecast-tools",
inject: [Forecast],
apply: () =>
Projection.offer("forecast-tools", [
Projection.operation(Forecast, Get, (forecast, { city }) =>
forecast.get(city),
),
]).pipe(Effect.asVoid),
})

使用契约包导出的服务键和 RPC。调用使用贡献插件注入的服务,因此受管部署通过 fork 持有的门面调用。注解可以放在契约包中,也可以放在宿主显式声明的 MCP 视图中。

声明 规则
name、title、description 全部必填。工具名称由 1–128 个字母、数字、下划线、点或连字符组成。
四个提示 全部必填;直接映射到 MCP 注解,不做推断。
output: "json" 用可移植编解码器编码成功值,将 { value } 作为结构化内容和文本返回。
output: "void" 丢弃成功值。
服务与操作 每个组合在视图中只出现一次;视图中的工具名称唯一。

输入 schema 从操作载荷的可移植编解码器生成,采用 MCP 默认的 JSON Schema 2020-12 方言。它只是外部客户端视图,绝不参与契约身份、来源记录或兼容性检查。运行时仍以 Effect 解码为准。

ProjectionError 指明服务、操作和原因。缺少身份或注解、流式操作、RPC 中间件、递归或不透明载荷,以及没有对象根的 schema,都会在发布前被拒绝。Schema 不得需要额外的解码或编码服务。这些是投影的限制,不是契约的限制;普通契约操作在 MCP 之外仍然有效。

结果 MCP 行为
工具参数无效 返回输入无效的 isError 结果;不调用服务。
已声明的业务错误 返回带 kind: "business" 和可移植错误值的 isError。
系统失败、未声明错误或缺陷 返回经过清理的 isError,带 kind: "system"。
输出编码失败 返回经过清理的系统结果。
取消 中断受管调用并等待其清理完成。
格式错误的协议请求 MCP 协议错误。

日志保留完整失败 cause,并标注服务键和操作身份,包括编码失败。业务错误值会有意公开;不要在其中放置凭据。

调用方取消或贡献插件释放时,即使调用或编码器的清理也失败,取消仍然优先。日志保留完整的混合 cause,不会将其转换为业务或系统工具结果。服务端发起的取消以协议错误返回客户端。

Projection.offer 返回控制器。只修改宿主视图时使用 controller.update。视图属于部署变更时,使用 controller.updateAfter(action, bindings, accepted):将 Graft.transition 或发布 apply 作为 action,仅接受其 Accepted 结局。新视图在 action 开始前完成验证。Action 与后续发布归贡献插件的 scope 持有,因此取消调用方不会丢失已接受的更新。

并发的控制器 action 会在 action 开始前以 ProjectionError 失败,不会排队。注册与历史记录原子发布。

宿主必须提供新的带注解视图。仅改变 worker 代码或发布物,不会将新注解导入宿主。失败的 action 保留旧视图。移除贡献插件会取消其持有的工作,并先撤下工具,再向已连接的客户端发送列表变更通知。

发布会向支持通知的 MCP 会话发出 notifications/tools/list_changed。 Projection.Changed 记录修订号及变更前后的元数据,包括服务键和操作身份。 controller.history 保留最近 32 个修订;需要持久审计时,由应用持久化该事件。通知和记录不授予批准,也不实现重新批准。

集成网关示例 使用独立的 MCP 客户端检查工具列表、调用、描述变更通知,以及已准入的 v1 调用跨越已接受的 v2 转换而无需重连。

通过 stdio 或支持通知订阅的传输发布视图变更。Effect 4.1.2 的旧版 Streamable HTTP 适配器不提供空闲 GET 事件流(405);此路径尚未实现主动发送工具列表变更通知。使用该路径的客户端仍可无需重连地列出和调用工具。上游跟踪:Effect #10436。

对于受策略管理的条目,投影元数据属于精确的批准声明。发布变更后,新调用会被阻止,直到重新批准;缓存旧工具列表的客户端也不例外。列表变更通知不是批准,已接纳的调用仍使用原来的 serving attempt。

投影验证 MCP 字段,并以 mcp/tools 标签发布批准数据。撤下贡献者会移除其在线工具,但保留批准事实。重新挂载相同元数据无需再次批准;发布不同元数据会在暴露变更前阻止准入。

给 Projection.offer 传入稳定名称,例如 "public/forecast-tools"。每个名称在当前 Graft 运行时标识一个逻辑视图。重复在线所有者返回 ProjectionConflict;释放时撤销所有权,但保留最新批准事实。更新只替换该视图的事实。还原元数据会恢复匹配的批准。发布审计日志保存修订,在线声明仅保留最新值。审计及撤销规则见信任与隔离。