Skip to main content
Glama

fsm-voice-mcp

One finite state machine, exposed as voice-driven MCP tools and resources — designed to be compatible with Alexa+'s MCP Toolkit (spec 2025-11-25+, Streamable HTTP) and any other MCP client.

Most voice apps hand-write a pile of intents that duplicate the state graph a UI already encodes. This library goes the other way: define the state machine once, in an XState-compatible config shape, and generate the MCP tool/resource surface from it. A UI-schema generator from the same config is on the roadmap — the point is one source of truth for "what can happen next," spoken or tapped.

Install

npm install fsm-voice-mcp

Node 20+, ESM only.

Related MCP server: state-machine

Quick example

import { z } from "zod";
import { createFsmMcpBridge } from "fsm-voice-mcp";
import type { FsmConfig } from "fsm-voice-mcp";

interface CoffeeContext {
  drink?: string;
}

const coffeeMachine: FsmConfig<CoffeeContext> = {
  id: "coffee",
  initial: "idle",
  context: {},
  states: {
    idle: {
      meta: { prompt: "Ready to take your order." },
      on: {
        ORDER: {
          target: "ordering",
          actions: (_ctx, event) => ({ drink: event.drink as string }),
        },
      },
    },
    ordering: {
      meta: { prompt: "Got it. Ready to confirm or cancel." },
      on: { CONFIRM: "brewing", CANCEL: "idle" },
    },
    brewing: {
      meta: { prompt: "Brewing your drink now." },
      on: { READY: "done" },
    },
    done: { final: true, meta: { prompt: "Your drink is ready. Enjoy!" } },
  },
};

const bridge = createFsmMcpBridge(coffeeMachine, {
  eventSchemas: {
    ORDER: z.object({ drink: z.string().optional() }),
  },
});

// bridge.tools    -> [{ name: "coffee_order", ... }, { name: "coffee_confirm", ... }, ...]
// bridge.resources -> [{ uri: "fsm://state", ... }]  (current state + context, as JSON)

bridge.tools is framework-agnostic — each entry is { name, description, inputSchema, handler } where inputSchema is a Zod object and handler returns { content: [{ type: "text", text }] }. Wire it into any MCP server by hand, or use the bundled adapter:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerFsmMcpBridge } from "fsm-voice-mcp";

const server = new McpServer({ name: "coffee-shop", version: "0.1.0" });
registerFsmMcpBridge(server, bridge);

See examples/server.ts for a full Streamable HTTP server you can point MCP Inspector or an Alexa+ add-on at:

npm install
npm run example:server
# MCP endpoint: http://127.0.0.1:3100/mcp

How it maps the machine to voice

  • One MCP tool per event type. Every event named in any state's on block becomes a tool (default name: <machine id>_<event, snake_case>). Calling it sends that event to the machine.

  • Guards are enforced at call time, not hidden from the tool list. A tool exists even if the current state can't take it right now; calling it then explains why not and lists what is available — the same shape a voice UI needs ("You can't confirm right now. Try: order.").

  • State drives the spoken response. Each state's meta.prompt (or a generated default) becomes the tool's reply after a successful transition, plus a list of the next available actions.

  • Current state is also an MCP resource (fsm://state by default) — JSON snapshot of { value, context, availableEvents, done }, so a client can ground itself without guessing from tool replies.

  • Sessions are pluggable. By default all calls share one machine instance; pass getSessionId to key a separate instance per caller (e.g. per Alexa+ conversation).

Importing a workflow from a task-management system

Most task boards (Jira, Trello, Linear, GitHub Projects) already expose their workflow as "from this status, these are the allowed next statuses" — an adjacency graph. fsmFromStatusGraph turns that directly into an FsmConfig, so you don't hand-write states for a workflow that already exists elsewhere:

import { fsmFromStatusGraph, createFsmMcpBridge } from "fsm-voice-mcp";

interface TicketContext {
  prUrl?: string;
}

const ticketWorkflow = fsmFromStatusGraph<TicketContext>({
  id: "ticket",
  initial: "todo",
  context: {},
  graph: {
    todo: ["in_progress"],
    in_progress: ["to_review"],
    to_review: ["approved", "todo"], // approve, or reject back to todo
    approved: ["done"],
    done: [], // no outgoing edges -> final
  },
  prompts: {
    to_review: "In review. A draft pull request is up.",
  },
  onEnter: {
    // Runs once, on any transition landing on "to_review" — awaited
    // before the voice reply is built, so a real side effect (not just a
    // context update) can happen on entry to a state.
    to_review: async () => {
      const pr = await pushDraftPullRequest();
      return { prUrl: pr.url };
    },
  },
});

const bridge = createFsmMcpBridge(ticketWorkflow);
// -> ticket_move_to_in_progress, ticket_move_to_to_review,
//    ticket_move_to_approved, ticket_move_to_todo (the reject path),
//    ticket_move_to_done

One event (and MCP tool) is generated per destination status, valid from every status that lists it as reachable — so "approved (or back to todo)" from to_review becomes two ordinary generated tools, not special cased. See examples/task-workflow.ts for the full runnable version.

actions (and so onEnter) may be async — it's awaited before the transition's result (and voice reply) is produced, so entering a state can genuinely call an API (create a ticket, push a draft PR) rather than only update in-memory context.

What "XState-compatible" means here

FsmConfig mirrors the shape of an XState config — initial, context, states: { on: { EVENT: { target, guard, actions } } } — closely enough that reading one is reading the other. It is deliberately a subset:

  • Flat states only — no nested/parallel/history states.

  • actions is a single function returning a partial context to merge (a simplified assign), not XState's full action/effect model.

  • No invoked services, delays, or always transitions.

The interpreter is hand-rolled and has zero runtime dependencies beyond zod — it does not require the xstate package. If your app already uses real XState machines, adapt machine.config (or the relevant subset) into an FsmConfig at the boundary; a direct adapter for createMachine-produced machines is on the roadmap.

API

  • createFsmMcpBridge(config, options?) -> { tools, resources, getSnapshot, reset }

  • createFsmActor(config) -> { getSnapshot, send, reset } — the interpreter, if you want to drive the machine directly (e.g. from a UI) without going through MCP. send is async.

  • collectEventTypes(config) -> Map<eventType, stateNames[]>

  • fsmFromStatusGraph(input) -> FsmConfig — build a config from a status adjacency graph instead of hand-writing states (see above).

  • registerFsmMcpBridge(server, bridge, options?) — adapter for any MCP server exposing registerTool / registerResource (works with @modelcontextprotocol/sdk and the Alexa+ MCP Toolkit SDK).

See src/types.ts for full type definitions and test/ for executable usage examples.

Roadmap

  • UI-schema generation from the same FsmConfig — a JSON screen description or React component tree keyed by state, so the voice tools and the UI never drift apart.

  • Voice prompt/utterance generation separate from raw tool descriptions (sample utterances per event, for VUI design/testing).

  • Nested and parallel states.

  • Mermaid/diagram export for docs and demos.

  • A direct adapter from real xstate createMachine instances.

Issues and PRs welcome — this started as the open-source half of a Build, Ship, Shape: Amazon Developer Hackathon Alexa+ project and is meant to stand on its own.

License

MIT

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • -
      license
      Not graded
      quality
      Not graded
      maintenance
      Goal-oriented narrative state machine for AI agents, exposing live world/scene context as MCP tools and resources.
      -
    • F
      license
      Not graded
      quality
      C
      maintenance
      Exposes declarative Prompt flow workflows as individual MCP tools, allowing generalist agents to invoke deterministic, isolated, and permission-bound operations without custom code.
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables Alexa+ conversational applications to compile conversational contracts into revisioned state rules, guard effects against stale or ambiguous state, and generate adversarial multi-turn attacks through MCP tools.
      Apache 2.0