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: nano-vm-mcp
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
Give any MCP-compatible AI assistant a builder for live, hosted web tools and workflows.
Build, validate, deploy — HTTP APIs, cron jobs, webhooks and MCP tools — from your AI client.
Human-input bridge for AI agents with voice-first answer links, MCP tools, and HTTP APIs.
MCP facade over the Nebelus Construction API. ~48 tools give full agent build parity: create/update/probe agents, edit graphs, attach knowledge and vector stores, wire connectors, set governance policies and locked guardrails, enable grounding-trace, and read deployment wiring. Purpose-built for regulated industries: data residency is enforced per region (EU / GCC-KSA), with PII controls and an audit trail. Agents are created as drafts — no deploy tool is exposed over MCP by design; publishing happens in the Nebelus console.
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.57 npm22MIT
- AlicenseAqualityAmaintenanceGoverned agent execution gateway for LLM workflows, providing deterministic FSM-based execution, audit trails, and idempotency guarantees via MCP.5MIT
- AlicenseNot gradedqualityCmaintenanceTurns any YAML manifest into a fully-functional MCP server without hosting infrastructure, enabling custom tools, resources, and prompts via declarative configuration.5 npmMIT
- -licenseNot gradedqualityNot gradedmaintenanceGoal-oriented narrative state machine for AI agents, exposing live world/scene context as MCP tools and resources.-