Open-source TypeScript alpha · WebMCP today
Add control and recovery to actions—not model responses.
Define an application action once. Remy adds policy, approval, receipts, and recovery, then exposes it through WebMCP today and other adapters later.
Five-minute quickstart
Install the public alpha packages from npm. Remy accepts Standard Schema validators; this example uses Zod.
npm install @remy-ai/core@alpha @remy-ai/webmcp@alpha zodCreate a client around the service your application already owns, wrap one function, register the action, then expose it through WebMCP. The complete version lives in the typechecked document example.
import { createRemy, succeed } from "@remy-ai/core"
import { registerWebMCP } from "@remy-ai/webmcp"
import { z } from "zod"
const remy = createRemy({ context: () => documentService })
const renameDocument = remy.defineAction({
name: "rename_document",
title: "Rename document",
description: "Change the open document title.",
kind: "write",
input: z.strictObject({ title: z.string().min(1).max(120) }),
risk: "low",
preview: ({ input, context }) => ({
summary: `Rename document to ${input.title}.`,
changes: [{ label: "Title", before: context.title, after: input.title }],
recovery: { title: context.title },
}),
execute: ({ input, context }) => {
context.rename(input.title)
return succeed({ title: input.title })
},
recovery: {
kind: "exact",
execute: ({ receipt, context }) => {
context.rename(receipt.recovery.title)
return succeed({ title: receipt.recovery.title })
},
},
})
remy.register(renameDocument)
await registerWebMCP(remy, { signal })Remy wraps host functions. Keep authentication, authorisation, state, and durable side effects authoritative in your application.
To run the Morrow demo or contribute to Remy, clone the repository and start the local Next.js application:
git clone https://github.com/MustafaK99/Remy.git
cd Remy
npm ci
npm run devCore concepts
Remy wraps the functions and services an application already uses. Your application keeps its data, authentication, authorisation, editor history, side effects, and interface.
Define an action
Call remy.defineAction() so the context, schema input, execution output, and recovery material are inferred. Pass one Standard Schema V1 validator. Zod 4 provides runtime validation and WebMCP JSON Schema from that same object.
import { z } from "zod"
import { createRemy, succeed } from "@remy-ai/core"
const remy = createRemy({
context: () => documentService,
})
const renameDocument = remy.defineAction({
name: "rename_document",
title: "Rename document",
description: "Change the open document title.",
kind: "write",
input: z.strictObject({
title: z.string().trim().min(1).max(120),
}),
risk: "low",
preview: ({ input, context }) => ({
summary: `Rename the document to “${input.title}”.`,
resources: ["document:title"],
changes: [{
label: "Document title",
before: context.getTitle(),
after: input.title,
}],
recovery: { previousTitle: context.getTitle() },
}),
execute: async ({ input, context }) => {
await context.setTitle(input.title)
return succeed({ title: context.getTitle() })
},
recovery: {
kind: "exact",
execute: async ({ receipt, context }) => {
await context.setTitle(receipt.recovery.previousTitle)
return succeed({ title: context.getTitle() })
},
},
})
remy.register(renameDocument)const result = await remy.run(
renameDocument,
{ title: "Launch notes" },
{
actor: "agent",
transport: "webmcp",
runId: "run-42",
taskId: "edit-launch-doc",
},
)
if (!result.ok) {
console.error(result.code, result.error)
}inputOne strict schema. Use jsonSchema only when the validator cannot expose a representable schema.previewDescribe bounded resources and human-readable changes without mutating state.executeCall the existing application function and return succeed(value) or fail(code, message).redactInputExplicitly allowlist compact receipt fields; raw input is private by default.exposeOutputExplicitly select the JSON-safe output returned through protocol adapters.Authentication, authorisation, prices, permissions, and durable idempotency must still be enforced by the authoritative server.
Policy and capabilities
Built-in preview, ask, reversible, and trusted modes are presets. Supply a custom policy when the application needs different rules. Capabilities are generic strings such asdocuments.publish orcommerce.purchase.
import type { Policy } from "@remy-ai/core"
export const documentPolicy: Policy = ({ action, controls }) => {
if (controls.paused) {
return { outcome: "deny", reason: "Agent changes are paused." }
}
const missingGrant = action.requiredGrants.find(
(grant) => !controls.grants.includes(grant),
)
if (missingGrant) {
return { outcome: "require_approval", reason: "A grant is missing." }
}
return { outcome: "allow", reason: "Application policy allows it." }
}Agent principals include an assurance level. The WebMCP identity tool creates self-reported attribution and never grants authority. Authenticated or verified principals must come from a trusted host integration.
Compilable source: examples/policy.ts
Exact recovery
Exact recovery restores the previous state. The preview returns typed private recovery material; the recovery handler consumes it. Remy checks declared resource versions first and appends a linked receipt instead of deleting the original.
In an editor, a native history token can be the recovery material. The host editor still owns and validates that history.
Compensation
Some effects cannot be rewound. A compensating recovery performs a new corrective action, such as cancelling a booking.
recovery: {
kind: "compensating",
execute: async ({ output, context }) => {
const cancellation = await context.cancelBooking(output.id)
return cancellation.ok
? succeed({ bookingId: output.id, cancelled: true })
: fail("CANCEL_FAILED", cancellation.message)
},
}Label this as cancellation or compensation in the interface—not “undo.” Both the original and corrective receipts remain visible.
Compilable source: examples/action-variants.ts
Irreversible actions
Irreversible definitions expose no recovery handler. TypeScript rejects a fake undo on this branch of the action union. Use an explicit approval for genuinely consequential work.
const publishDocument = remy.defineAction({
name: "publish_document",
title: "Publish document",
description: "Publish the current document publicly.",
kind: "write",
input: z.strictObject({}),
risk: "high",
approval: "always",
preview: () => ({
summary: "Publish this document publicly.",
changes: [{ label: "Visibility", before: "Draft", after: "Public" }],
}),
execute: async ({ context }) => succeed(await context.publishDocument()),
recovery: { kind: "irreversible" },
})Compilable source: examples/action-variants.ts
WebMCP adapter
The headless adapter imports only public core contracts. It feature detects document.modelContext, converts the action schema, registers each tool once, validates input again at execution, and reports partial registration failures. Approval-gated tool promises remain pending by default, while the protocol-neutral receipt stays authoritative in core.
import { registerWebMCP } from "@remy-ai/webmcp"
const lifecycle = new AbortController()
const registration = await registerWebMCP(remy, {
signal: lifecycle.signal,
approvalTimeoutMs: 120_000,
})
if (registration.status === "partial") {
console.error(registration.failures)
}
// Unregister every tool when this integration unmounts.
lifecycle.abort()get_remy_statusRead controls and attribution without changing application state.identify_assistantAttach self-reported identity for receipts; never authorisation.request_remy_controlsRestrictions apply; requests for more authority wait for the person.get_action_historyReturn compact agent-action receipts, not browsing or transcripts.revert_actionRequest version-safe exact or compensating recovery.Unsupported browsers return unsupported. The application and manual controls continue to work normally.
React integration
React is optional. useRemySnapshot(remy)uses the engine as a proper external store with stable subscriptions and referentially stable snapshots. Build any accessible approval surface your product needs.
"use client"
import type { RemyClient } from "@remy-ai/core"
import { useRemySnapshot } from "@remy-ai/react"
export function AgentActivity({
remy,
}: {
readonly remy: RemyClient<unknown>
}) {
const snapshot = useRemySnapshot(remy)
const agentReceipts = snapshot.receipts.filter(
(receipt) => receipt.actor === "agent" || receipt.reversesReceiptId,
)
return (
<ol aria-label="Agent activity">
{agentReceipts.map((receipt) => (
<li key={receipt.id}>
{receipt.summary} — {receipt.status}
</li>
))}
</ol>
)
}The Morrow Action Center is demo UI, not a required Remy design. Agent activity excludes ordinary user clicks; developer run summaries belong in developer tooling, not the end-user action panel.
Compilable source: examples/react-action-center.tsx
Persistence and privacy
The application and Remy have separate persistence boundaries. Remy stores schema-versioned semantic receipts and journal events; the Morrow demo stores its fictional shop state separately.
Use createMemoryJournalStore() or the safe, namespaced createBrowserJournalStore(). Browser restore validates version 1 data and fails closed on corrupt or incompatible payloads. Durable recovery needs an explicit, redacted application serializer in a future store integration.
Testing
Run the same checks used in CI:
npm run lint
npm run typecheck
npm run test:run
npm run build
npm run verify:packagesProduction checklist
- Authenticate and authorise each state-changing operation at the server or owning service.
- Recompute totals, eligibility, permissions, and versions from authoritative state.
- Use strict schemas and allowlist only receipt fields safe for operators and users.
- Keep credentials, payment details, prompts, transcripts, and application snapshots out of the journal.
- Apply CSRF protection to cookie-authenticated writes and rate-limit abuse-prone operations.
- Enforce idempotency at the real side-effect boundary, not only inside a browser tab.
- Fail closed when approval context, recovery material, or resource versions are stale.
- Set retention, export, and deletion rules before connecting durable storage.
- Treat self-reported WebMCP identity as attribution only.
API reference
These alpha exports are curated from @remy-ai/core, @remy-ai/webmcp, and @remy-ai/react.
createRemy(options)Create a generic client around a host-owned context, policy, resources, and journal.remy.defineAction(definition)Infer context, input, output, and recovery types and validate configuration early.remy.register(action)Register one definition. Duplicate action names throw an actionable error.remy.run(action, input, meta?)Typed application execution. Input and success output follow the action.remy.runByName(name, unknown, meta?)Runtime-validated string dispatch for protocol adapters.remy.waitForAction(id, options?)Wait for any adapter's action record to settle; timeout or cancellation preserves the receipt.remy.approve(id) / reject(id)Resolve staged or waiting work after validating its current state.remy.revert(id, meta?)Run exact or compensating recovery and append a linked receipt.registerWebMCP(remy, options?)Register generic tools and return ready, partial, unsupported, or error status.useRemySnapshot(remy)Subscribe to cached external-store snapshots from a client component.summarizeActionRun(receipts)Derive action-only developer metrics; it is not required end-user UI.Troubleshooting
Roadmap and versioning
Receipts carry schemaVersion: 1; action definitions have their own version. The source API is alpha and may still change between alpha releases. The boundary is intentionally stable: host services → semantic actions → Remy core → adapters and optional UI.
Ship agents users aren't afraid to trust.
Try policy, approval, receipts, and recovery in the Morrow demo.