MCP Fusion/Core concepts/FSM state gating
FSM state gating
Temporal anti-hallucination: bind tools to workflow states so an out-of-order action physically disappears from the tool list instead of being rejected after the agent tries it.
The most expensive hallucination in an agent workflow is not a wrong value: it is calling step four before step three. The agent sees a tool, so it calls it, and the server rejects it, and the turn is burned. MCP Fusion removes the step from the menu instead.
Bind a tool to a state
export default f.action('invoice.discharge')
.describe('Discharge an approved invoice')
.bindState('approved', 'DISCHARGE')
.handle(async (input, ctx) => discharge(input.id));bindState(states, transition) means: this tool appears in tools/list only while the workflow is in one of the given states, and on a successful call it fires transition. The states come from a machine you configure once:
const fsm = f.fsm({
id: 'invoice',
initial: 'draft',
states: {
draft: { on: { SUBMIT: 'review' } },
review: { on: { APPROVE: 'approved', REJECT: 'draft' } },
approved: { on: { DISCHARGE: 'discharged' } },
discharged: { type: 'final' },
},
});The shape is XState v5 compatible. XState is an optional peer: when it is installed the gate runs a real actor; without it the gate drives a built-in manual transition table, so the feature degrades gracefully instead of failing to boot.
What the agent experiences
- In
draft,invoice.dischargeis not in the list. The agent cannot call what it cannot see. - On the tool that leads to
approved, the successful call moves the machine and the server sendsnotifications/tools/list_changed. - The client refreshes the list, and now
invoice.dischargeappears, with its own description.
This is stronger than a guard: a guard rejects after the attempt, gating removes the temptation. Validation errors about ordering disappear from your logs because the ordering conflict cannot be expressed.
Gating is enforced on the call path too, not only in the list. A client that guesses a tool name still gets FORBIDDEN with the allowed actions listed, so security never depends on the client refreshing its menu.
Progressive disclosure in the schema
An FSM-bound connector can also shrink its own context. compactDescription() gives a tool its short description while the machine sits in the initial state, and the full .describe() arrives for later states. The agent's menu in draft is smaller and sharper than the same tool in approved, without a second deployment.
Serverless and stateless state
A state machine is durable state, and MCP 2.0 stateless transports have no session to hang it on. Three mechanisms cover that:
stateHandleKey: the tool call may carry an explicit handle argument the model passes back, the spec-recommended pattern for stateless flows- session id: when the transport has one (
Mcp-Session-Id), the gate stores the state per session - per-attachment fallback: a single-tenant server without either key uses an in-process identity
Persistence is a FsmStateStore interface you implement:
interface FsmStateStore {
load(handle: string): Promise<{ state: string; updatedAt: string } | undefined>;
save(handle: string, snapshot: { state: string; updatedAt: string }): Promise<void>;
}Ship the in-memory store for a single process, or back it with Redis, DynamoDB, an edge KV, a database row. The framework clones and restores the gate per request, so concurrent requests never share a machine by accident.
When to reach for it
- Approval and review flows (nothing is dischargeable before approval)
- Payment and fulfillment (capture after authorization, never before)
- Multi-step migrations and destructive maintenance windows
- Anything where a wrong order costs money or data, and a rejected call still costs a turn
FSM gating composes with Tool exposition (the visible surface changes with state) and with Routing (state-bound tools live in their own files). The handlers do not change: they were always written for one state of the world.
Next steps
- Tools: the builder methods around
bindState - State sync: the other temporal problem, stale data
- Token economics: why the smaller menu pays
