fsm-voice-mcp
Targets Amazon's Alexa+ MCP Toolkit (Streamable HTTP, spec 2025-11-25+): exposes a finite state machine's events as MCP tools and its current state/context as an MCP resource, so an Alexa+ add-on can drive a voice conversation through the state graph — speaking each state's prompt, listing the next available actions, and enforcing guards (e.g. "You can't confirm right now. Try: order.") when a transition isn't valid. Sessions can be keyed per Alexa+ conversation, allowing separate machine instances per caller.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fsm-voice-mcpI'd like to order a latte"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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-mcpNode 20+, ESM only.
Related MCP server: narrative-runtime
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/mcpHow it maps the machine to voice
One MCP tool per event type. Every event named in any state's
onblock 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://stateby 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
getSessionIdto 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_doneOne 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.
actionsis a single function returning a partial context to merge (a simplifiedassign), not XState's full action/effect model.No invoked services, delays, or
alwaystransitions.
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.sendis 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 exposingregisterTool/registerResource(works with@modelcontextprotocol/sdkand 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
xstatecreateMachineinstances.
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
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Hosted MCP runtime where your agent publishes its own tools by tool call and keeps working.
Hosted MCP memory and agent control plane for durable conversations, jobs, and operations.
Patterns for designing and reviewing AI skills, agents, and multi-agent workflows. Find guidance on context economy, delegation, verification, and tool design; inspect claims, worked examples, maturity labels, and source references. Five read-only tools let agents discover relevant patterns, compare concise cards, read specific sections, and explore relationships. Hosted Streamable HTTP at https://agentic-atlas.dev/mcp/ — no installation, account, or API key required. Browse the atlas at https://agentic-atlas.dev/.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables orchestration of MCP tool calls through declarative YAML-defined directed graphs with data transformation, conditional routing, and observable execution flows.6 npm22MIT
- -licenseNot gradedqualityNot gradedmaintenanceGoal-oriented narrative state machine for AI agents, exposing live world/scene context as MCP tools and resources.-
- AlicenseNot gradedqualityAmaintenanceEnables validating, simulating, and analyzing finite state machines via MCP tools for step execution, path finding, and schema inspection.Apache 2.0
- FlicenseNot gradedqualityCmaintenanceExposes declarative Prompt flow workflows as individual MCP tools, allowing generalist agents to invoke deterministic, isolated, and permission-bound operations without custom code.-