跳到內容

Remote 契約

契約描述宿主與實作共用的協定,不包含實作位置。實作位置由宿主項目指定。

用 Contract.service 描述每個跨越邊界的服務:它的鍵、列出各個呼叫的 Effect RpcGroup,以及兩個函式。

src/contract.ts
import { Context, Effect, Schema } from "effect"
import { Rpc, RpcGroup } from "effect/rpc"
import { Contract } from "@graftland/plugin-remote"
export class GreetingUnavailable extends Schema.TaggedError<
GreetingUnavailable,
{ readonly brand: unique symbol }
>()("GreetingUnavailable", { cause: Schema.Defect() }) {}
export class Greeter extends Context.Service<
Greeter,
{
readonly greet: (name: string) => Effect.Effect<string, GreetingUnavailable>
}
>()("app/Greeter") {}
const api = RpcGroup.make(
Contract.operation(
"@app/greeter/Greet",
1,
Rpc.make("Greet", {
payload: { name: Schema.String },
success: Schema.String,
error: GreetingUnavailable,
}),
),
)
const greeter = Contract.service(Greeter, api, {
toHandlers: (service) => ({ Greet: ({ name }) => service.greet(name) }),
fromClient: (client) => ({
greet: (name) =>
client
.Greet({ name })
.pipe(
Effect.catchTag("RpcClientError", (cause) =>
GreetingUnavailable.make({ cause }),
),
),
}),
})
export const contract = Contract.make({
name: "app/greeter",
config: Schema.Struct({ greeting: Schema.String }),
provides: [greeter],
})

toHandlers 在實作服務的一側執行,fromClient 在使用服務的一側執行。請在 fromClient 中轉換 RpcClientError,讓連線中斷以呼叫端本來就會處理的錯誤出現。Contract.make 再把這些描述彙整成契約:

欄位 意義
name 外掛名稱。
config 設定 schema,必須能編碼為 JSON。
error 外掛宣告的失敗的 schema,預設為 Schema.Never。
inject 實作使用的宿主服務。
provides 實作發布給宿主的服務。
  • 契約中的 schema 都不能需要服務才能編碼或解碼。
  • 服務鍵在同一份契約內必須唯一;RPC 名稱可以在不同服務間重複。
  • 以 contract 或預設匯出的形式匯出契約。
  • 雙方匯入同一份建置後的契約,所以鍵與 RPC schema 一致。

服務中的每個 RPC 都透過 Contract.operation 取得不可變的 name@version 識別。

規則 意義
名稱與版本 名稱不得為空或含空白,版本必須是正整數。
協定變更 schema、編碼或語意改變時,提升該操作的版本。
傳輸標籤 Effect RPC 的標籤保持不變,它不是操作識別。
服務中繼資料 service.operations 列出 RPC 群組中的操作識別。缺少識別或同一服務內識別重複會被拒絕。
相容性 Contract.compareOperations(required, provided) 檢查精確的集合包含關係。提供方可以有額外操作;新版本不能滿足舊版本的要求。

比較是純粹的中繼資料檢查,不比較 schema,也不檢查執行中的實作。套件版本與摘要屬於部署資訊,不決定操作相容性。

import { Array } from "effect"
import { Contract } from "@graftland/plugin-remote"
import { contract } from "./src/contract.ts"
const required = Array.flatMap(
contract.provides,
(service) => service.operations,
)
Contract.compareOperations(required, required) // => ({ _tag: "Compatible" })
Contract.compareOperations(required, []) // => ({ _tag: "Incompatible", missing: [{ name: "@app/greeter/Greet", version: 1 }] })

在 Plugin.make({ provides }) 中宣告服務描述,再在 apply 中用 Graft.provide 發布服務值。overlap: true 宣告服務可以安全地與另一次服務嘗試並行執行,預設為 false。這些宣告不會啟用重疊轉換,也不改變重新啟動行為。

src/greeter.ts
import { Effect } from "effect"
import { Graft, Plugin } from "@graftland/graft"
import { contract, Greeter } from "./contract.ts"
export default Plugin.make({
name: "app/greeter",
config: contract.config,
provides: contract.provides,
overlap: true,
apply: ({ greeting }) =>
Graft.provide(Greeter, {
greet: (name) => Effect.succeed(`${greeting}, ${name}!`),
}),
})

MCP 投影透過注入的服務公開明確註解的操作。Contract.identity(rpc) 讀取操作身分,不解釋 schema。MCP JSON Schema 只描述工具輸入,絕不參與來源紀錄或相容性。