Skip to content

Remote contracts

A contract describes the protocol shared by a host and an implementation. It contains no implementation location. Select that location in the host entry.

Describe each service that crosses the boundary with Contract.service: its key, an Effect RpcGroup of its calls, and two functions.

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 runs where the service is implemented, and fromClient where it is used. Map RpcClientError in fromClient, so a broken connection reaches callers as an error they already handle. Contract.make collects the descriptions into the contract:

Field Meaning
name The plugin’s name.
config Config schema. Must encode to JSON.
error Schema of the plugin’s declared failures. Defaults to Schema.Never.
inject Host services the implementation uses.
provides Services the implementation publishes to the host.
  • No schema in a contract may need services to encode or decode.
  • Service keys are unique within a contract. RPC names may repeat across services.
  • Export the contract as contract or as the default export.
  • Both sides import the same built contract, so keys and RPC schemas match.

Each RPC in a service has an immutable name@version, assigned with Contract.operation.

Rule Meaning
Name and version A nonempty name without whitespace and a positive integer version.
Protocol changes Change the operation version when its schema, encoding, or meaning changes.
Wire tag The Effect RPC tag is unchanged; it is not the operation identity.
Service metadata service.operations lists the identities in its RPC group. Unnamed operations and duplicate identities in one service are rejected.
Compatibility Contract.compareOperations(required, provided) checks exact set inclusion. Additional provided operations are allowed. A newer version does not satisfy an older version.

Comparison is pure metadata inspection. It neither compares schemas nor checks a running implementation. Package versions and digests are deployment facts, not operation compatibility.

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 }] })

Declare the service descriptors in Plugin.make({ provides }), then publish their values with Graft.provide inside apply. overlap: true declares that serving is safe alongside another attempt; its default is false. These declarations do not enable overlap transitions or change restart behavior.

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}!`),
}),
})

The MCP projection exposes explicitly annotated operations through injected services. Contract.identity(rpc) reads an operation identity without interpreting its schemas. MCP JSON Schema describes tool input only; it never participates in provenance or compatibility.