Moxo MCP server

Connect Claude Desktop, Cursor, Continue, or any Model Context Protocol client to your Moxo workspace. Live tool catalogue rendered from the same registry the server dispatches against — what's listed here is what the protocol returns.

What is MCP?

The Model Context Protocol is an open standard that lets LLM clients (Claude Desktop, Cursor, Continue, Windsurf, …) call tools, render prompts, and read resources from external systems via a standard JSON-RPC 2.0 wire format. Moxo exposes one MCP server: every authenticated workspace becomes addressable from any compatible AI client without writing custom integration code.

Quick install

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "moxo": {
      "command": "npx",
      "args": ["-y", "@moxo/mcp-server@1.1.3"],
      "env": { "MOXO_API_TOKEN": "mxo_pat_..." }
    }
  }
}

Cursor

Same JSON block in ~/.cursor/mcp.json.

Continue (VS Code)

mcpServers:
  - name: moxo
    command: npx
    args: ["-y", "@moxo/mcp-server@1.1.3"]
    env:
      MOXO_API_TOKEN: mxo_pat_...

Restart your client. The Moxo tools appear in its tool drawer.

The proxy package — @moxo/mcp-server

A thin stdio ↔ HTTPS proxy your local MCP client spawns via npx. Holds the Bearer token in process memory only — never writes it to disk, never logs it. The full tool catalogue lives on the backend, so new tools appear in your client without reinstalling the proxy.

Version: 1.1.3 — npm

Environment variables

VariableRequiredDefaultNotes
MOXO_API_TOKEN✓—Bearer token. PAT (`mxo_pat_*`) accepted unconditionally; org key (`mxo_sk_*`) requires `MOXO_ALLOW_ORG_KEY=1`.
MOXO_BASE_URL—https://api.moxo.comHostname only — `/mcp-server` is auto-appended. Non-loopback `http://` is refused (plaintext would leak the Bearer token).
MOXO_ALLOW_ORG_KEY——Set to `1` to permit an org API key as `MOXO_API_TOKEN`. Default refuses org keys — PAT is the right shape for personal installs.
MOXO_REQUEST_TIMEOUT_MS—120000Per-request deadline (milliseconds). Synthesizes a JSON-RPC error if the remote takes longer; matches typical MCP-client tool-call timeouts.

Self-hosted / on-prem

{
  "mcpServers": {
    "moxo": {
      "command": "npx",
      "args": ["-y", "@moxo/mcp-server@1.1.3"],
      "env": {
        "MOXO_API_TOKEN": "mxo_pat_...",
        "MOXO_BASE_URL": "https://moxo.your-company.internal"
      }
    }
  }
}

Why a proxy?

Claude Desktop's stable MCP integration path is the stdio JSON config; modern builds also support remote URLs but only via OAuth-shaped servers. Moxo's MCP server uses Bearer-token auth (PAT / org key), so a stdio adapter is the universal install path across every current MCP client.

Direct HTTPS integration (no proxy)

Use this path when you're integrating from your own backend or any tool that already speaks HTTP — custom LLM agents, Slack / Teams bots, n8n / Zapier / Make nodes, server-to-server analytics jobs, smoke-test scripts. The proxy package above exists for stdio-only clients (Claude Desktop, Cursor, Continue); everything else should talk to the Streamable HTTP endpoint directly.

The endpoint

POST  https://api.moxo.com/mcp-server            (production)
POST  https://api.staging.moxo.com/mcp-server    (staging)
HeaderValue
AuthorizationBearer mxo_pat_<token> (PAT) or Bearer mxo_sk_<token> (org key)
Acceptapplication/json, text/event-stream
Content-Typeapplication/json
Mcp-Session-IdThe value returned in the mcp-session-id response header of initialize — required on every subsequent request

Wire protocol: Streamable HTTP per the MCP spec. JSON-RPC 2.0 payloads. POST for requests, GET for server-initiated SSE pushes, DELETE to tear down a session.

Shape 1 — Official MCP SDK

The cleanest path. Anthropic's MCP SDKs handle Streamable HTTP, session management, and reconnection automatically.

TypeScript / Node 20+

npm install @modelcontextprotocol/sdk

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(
  new URL('https://api.moxo.com/mcp-server'),
  {
    requestInit: {
      headers: { Authorization: `Bearer ${process.env.MOXO_API_TOKEN!}` },
    },
  },
);

const client = new Client({ name: 'my-app', version: '1.0.0' }, { capabilities: {} });
await client.connect(transport);

const { tools } = await client.listTools();
const result = await client.callTool({
  name: 'list_flows',
  arguments: { status: 'IN_PROGRESS', limit: 10 },
});

await client.close();

Python 3.10+

pip install mcp

import os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client(
    "https://api.moxo.com/mcp-server",
    headers={"Authorization": f"Bearer {os.environ['MOXO_API_TOKEN']}"},
) as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()
        result = await session.call_tool(
            "list_flows",
            {"status": "IN_PROGRESS", "limit": 10},
        )

Shape 2 — Native HTTP MCP client config

Some MCP clients accept a remote URL + headers directly in their config (no proxy install). Check your client's documentation for the exact key names — the shape is typically:

# Exact keys vary by client (`url` / `endpoint` / `remote.url`)
mcpServers:
  moxo:
    url: https://api.moxo.com/mcp-server
    headers:
      Authorization: Bearer mxo_pat_...

Shape 3 — Raw JSON-RPC over HTTPS

No SDK dependency. Useful for scripts, debugging, and one-shot calls from tools that already do HTTP (n8n, Zapier, Make).

#!/usr/bin/env bash
TOKEN='mxo_pat_...'
BASE='https://api.moxo.com/mcp-server'

# 1. Initialize — capture session id from response header
SID=$(curl -sD - -X POST "$BASE" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"my-script","version":"1.0"}}}' \
  | grep -i 'mcp-session-id:' | awk '{print $2}' | tr -d '\r\n')

# 2. List tools
curl -X POST "$BASE" \
  -H "Authorization: Bearer $TOKEN" -H "Mcp-Session-Id: $SID" \
  -H 'Accept: application/json, text/event-stream' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 3. Call a tool
curl -X POST "$BASE" \
  -H "Authorization: Bearer $TOKEN" -H "Mcp-Session-Id: $SID" \
  -H 'Accept: application/json, text/event-stream' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_flows","arguments":{"status":"IN_PROGRESS","limit":5}}}'

# 4. Optional cleanup
curl -X DELETE "$BASE" \
  -H "Authorization: Bearer $TOKEN" -H "Mcp-Session-Id: $SID"

Which shape to use

Building…Use
Custom LLM agent or bot in your codebaseShape 1 (Official SDK + direct URL)
Slack / Discord / Teams botShape 1
n8n / Zapier / Make nodeShape 3 (raw HTTP, one call per workflow)
Server-to-server analytics pullShape 1 with an org key (mxo_sk_*), not a PAT
MCP client that documents URL + headers configShape 2
Claude Desktop / Cursor / Continuenpm proxy (above) — these only speak stdio
Quick debugging / smoke testShape 3 (curl)

Authentication

MCP uses the same Bearer auth surface as the REST API — same token, same scopes, same tenant boundaries. Mint a token in the REST API docs and use the returned mxo_pat_* (Personal Access Token) or mxo_sk_* (organization API key) value as MOXO_API_TOKEN.

Scopes are filtered server-side: a token with flows:read but not templates:read sees only the flow-shaped tools in tools/list — the others aren't advertised, so the client doesn't surface buttons that 403.

Tools (25)

Auto-generated from the live registry. Tool order alphabetical for stability across renders.

archive_flow flows:write

Archive a completed or cancelled flow. The flow stays visible via `lifecycle: 'archived'` queries but disappears from the default active flow list. Returns an error envelope if the flow is still in progress (use cancel_flow first) or already archived. Idempotent against the second case.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "flowId"
  ],
  "additionalProperties": false
}
cancel_flow flows:write

Cancel an active flow. All non-terminal steps cascade to CANCELLED in a single transaction; SKIPPED / COMPLETED / FAILED steps keep their original outcome. Returns INVALID_STATE if the flow is already COMPLETED or CANCELLED. Side effects: cancels scheduled jobs for active steps, queues participant notifications, and (for sub-flows) marks the parent SUB_FLOW step FAILED. Distinct from archive_flow — cancellation stops execution; archive only hides it from the active list.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    },
    "reason": {
      "description": "Free-text cancellation reason. Defaults to \"Manual cancellation\".",
      "type": "string",
      "maxLength": 500
    }
  },
  "required": [
    "flowId"
  ],
  "additionalProperties": false
}
complete_step flows:write

Mark a step COMPLETED on behalf of the calling user (PAT-only: step completion is a participant action with audit-trail implications, so org keys are rejected). Confirms any draft uploads, runs the per-step-type completion gate (CHECKLIST/FILE_REQUEST/DECISION/APPROVAL/FORM/QUESTIONNAIRE), and advances the flow. Multi-participant group steps honor the configured ONE/MAJORITY/ALL completion mode. Returns INSUFFICIENT_SCOPE for org-key callers; NOT_FOUND for cross-org or unknown step ids; and a per-gate INVALID_STATE-shaped error if the resultData fails the step-type's validation. For a choice question on a form step, answer with an option's `value` from `get_flow_step` (an array for a multi-select); a label or id is also accepted and stored as that option's value.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    },
    "stepExecutionId": {
      "type": "string",
      "minLength": 1
    },
    "resultData": {
      "default": {},
      "description": "Step-type-specific completion payload. CHECKLIST: { items: { [itemId]: boolean } }. DECISION: { outcomeId }. APPROVAL: { decision: 'APPROVED' }. FORM/QUESTIONNAIRE: **keyed by `fieldId`, not by label** — call `get_flow_step` first to discover the field IDs (the `formFields[]` array on the response). Label-keyed payloads are auto-translated when the label uniquely matches one field; ambiguous or unknown labels are rejected with VALIDATION_ERROR. The workspace UI reads `resultData[fieldId]` directly, so the right keys matter for display, not just for completion. The completion gate validates + enriches per step-type before advancing.",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {}
    }
  },
  "required": [
    "flowId",
    "stepExecutionId",
    "resultData"
  ],
  "additionalProperties": false
}
create_external_user external_users:write

Create a new external user in the bound org. Returns the new user's id, name, email, phone, title, primaryChannel, and accountId. Soft-deleted (INACTIVE) duplicates on email/phone are reactivated (returns `reactivated: true`); ACTIVE duplicates return a DUPLICATE error. Emails belonging to an existing team member or pending team invite are rejected (EMAIL_IS_TEAM_MEMBER / EMAIL_HAS_PENDING_INVITE). Does NOT send a portal invitation — invite them from the orchestrator portal for that. The successful user is always ACTIVE; no status field is surfaced.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 255
    },
    "email": {
      "type": "string",
      "format": "email",
      "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "phone": {
      "type": "string"
    },
    "primaryChannel": {
      "type": "string",
      "enum": [
        "email",
        "sms",
        "email_and_sms"
      ]
    },
    "accountId": {
      "description": "Attach the user to an existing account.",
      "type": "string"
    },
    "personaId": {
      "description": "Assign a persona at creation time (defaults to org default persona).",
      "type": "string"
    },
    "portalId": {
      "description": "Bind the user directly to a portal (otherwise derived from persona).",
      "type": "string"
    },
    "title": {
      "description": "Job title on this person in this org (max 100 characters). Omit to leave an existing title unchanged when reactivating a removed person. Null or blank clears it. A new name is added to the org's title list; a name the org already has is reused, and the stored label is what comes back.",
      "anyOf": [
        {
          "type": "string",
          "maxLength": 100
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "name",
    "email"
  ],
  "additionalProperties": false
}
get_dashboard_summary reports:read

Top-level metrics for the bound org: count of active flows, overdue flows, flows completed today, and top 3 templates by active flow count. Use as the lead-in for daily standup or briefing prompts. PATs see only their permission-filtered view; org-keys see the whole org.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_external_user external_users:read

Get an external user by ID: name, email, phone, title, primary channel. Returns null if not found or belongs to a different organization. Pass the `id` exactly as returned by `list_external_users` or `search` — it is an opaque per-org identifier (an `usr_…` string); pass it back unchanged. `title` is the job title on this person's profile in this org, or null when unset.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "userId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "userId"
  ],
  "additionalProperties": false
}
get_flow flows:read

Return the LLM-friendly detail of a single flow by id: status, progress, current step + assignee names, flat list of all steps, top-level roles, and accounts. Use after `list_flows` to drill into one flow. Returns null if the flow does not exist or belongs to a different organization (cross-org access is silently 404).

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "flowId"
  ],
  "additionalProperties": false
}
get_flow_step flows:read

Return the full detail of one step execution by id: status, result data, participants (executor / preparer / reviewer), and timestamps. Use after `list_flow_steps` to drill into a specific step. Returns null if the step does not exist or its flow is not in the current org. For FORM and QUESTIONNAIRE steps the response also includes `formFields[]`: the field definitions (id, label, type, required, and a choice question's `options` as `{ label, value }`) the step expects. Use these `fieldId`s as the keys in `complete_step`'s `resultData` (label-keyed payloads are silently dropped in the app). For a choice question, answer with one of its `options` values (an array for a multi-select); that is what `resultData` holds, and each value names exactly one option. The `label` is what people see.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    },
    "stepExecutionId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "flowId",
    "stepExecutionId"
  ],
  "additionalProperties": false
}
get_org (no scope required)

Return the identity (id, name, slug, companyName) of the organization the current MCP session is bound to. Use this first when starting a new session to confirm the connection and surface the org name to the user.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
get_team_member team:read

Fetch a single team member of the bound org by userId. Returns the same shape as one entry of list_team_members: id, name, email, role (ADMIN | MEMBER), permission group, join date. NOT_FOUND if the userId is not a member of this org (no existence leak across orgs).

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "userId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "userId"
  ],
  "additionalProperties": false
}
get_template templates:read

Return the LLM-friendly definition of a single template by id: roles, custom fields, and a flat list of steps with their type, completion mode, and assignee role names. Use after `list_templates` to read a specific template, or before `start_flow` to inspect what role assignments + kickoff data are required. Returns null on cross-org access (silently 404).

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "templateId": {
      "type": "string",
      "minLength": 1
    },
    "version": {
      "description": "Reserved for future use; ignored today.",
      "type": "string"
    }
  },
  "required": [
    "templateId"
  ],
  "additionalProperties": false
}
invite_team_member team:write

Invite an email to the bound org as a team member (ADMIN or MEMBER). Sends the invitation email. Returns DUPLICATE for already-members, EMAIL_IS_CONTACT for emails already used as a contact, ADMIN_INVITE_REQUIRES_ADMIN for non-admin callers inviting an ADMIN, ADMIN_CANNOT_HAVE_PERMISSION_GROUP for ADMIN+permissionGroupId combos, and PERMISSION_GROUP_NOT_FOUND for unknown group ids. Org API keys are NOT supported — invitations require a human inviter for the audit trail; use a Personal Access Token bound to an admin user.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "name": {
      "type": "string",
      "maxLength": 255
    },
    "phone": {
      "type": "string"
    },
    "primaryChannel": {
      "type": "string",
      "enum": [
        "email",
        "sms",
        "email_and_sms"
      ]
    },
    "smsConsent": {
      "type": "boolean"
    },
    "role": {
      "type": "string",
      "enum": [
        "ADMIN",
        "MEMBER"
      ]
    },
    "permissionGroupId": {
      "description": "Assign a permission group at invite time. MEMBER role only — ADMINs cannot have permission groups.",
      "type": "string"
    }
  },
  "required": [
    "email",
    "role"
  ],
  "additionalProperties": false
}
list_attention_items flows:read

PAT-only. Return the active steps where the calling user is the current executor — i.e. things they need to do now. Each item carries the flowId so the AI can drill in via `get_flow` or the `moxo://flows/{id}` resource. Empty array for org-key actors (no human participant identity).

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
list_external_users external_users:read

List external users in the bound org with filters (optional substring search on name, email, or phone). Returns id, name, email, phone, title, primaryChannel. The returned `id` is an opaque per-org identifier (an `usr_…` string) — pass it back unchanged to `start_flow`, `get_external_user`, etc. `title` is the job title on this person's profile in this org, or null when unset.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "search": {
      "description": "Substring match against name, email, or phone (case-insensitive).",
      "type": "string"
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "offset": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "additionalProperties": false
}
list_flow_messages chat:read

Return the in-flow chat of a single flow — the messages participants (team members, external clients, AI agents) exchanged inside it, newest-first with threaded replies. Use to review or summarize a flow's conversation. PATs see chat only for flows the user manages, owns, or participates in (view-only access excludes chat); org-keys see every flow's chat in the org. internal_only messages are internal staff notes. Reads ONLY in-flow chat — not group conversations. Requires the `chat:read` scope.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1,
      "description": "The flow whose chat to read."
    },
    "cursor": {
      "description": "Opaque pagination cursor from a prior call's nextCursor. Omit for the first page.",
      "type": "string"
    },
    "limit": {
      "description": "Max root messages to return (default 100, max 200). Threaded replies are nested and not counted against this.",
      "type": "integer",
      "minimum": 1,
      "maximum": 200
    }
  },
  "required": [
    "flowId"
  ],
  "additionalProperties": false
}
list_flow_steps flows:read

Return the step executions for one flow as a flat list (id, name, type, status, timestamps). Lighter than `get_flow` for callers that only need the step view — e.g. "what step is the ACME flow stuck on?". Returns an empty array if the flow does not exist in this org.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "flowId"
  ],
  "additionalProperties": false
}
list_flows flows:read

List flows in the bound organization with rich filters: status, role, failure presence, time-range, template version. Returns id, name, status, template info, startedAt, and dueAt for each flow. Use this to find a flow before drilling into it. PATs see only flows the user can see (admin = all in org); org-keys see every non-sandbox flow in the org.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "status": {
      "description": "Filter to flows in this status. `NOT_STARTED` means running but not yet acted on. Omit for all flows.",
      "type": "string",
      "enum": [
        "IN_PROGRESS",
        "COMPLETED",
        "CANCELLED",
        "NOT_STARTED"
      ]
    },
    "roleName": {
      "description": "Restrict to flows that have the given role assigned (e.g. \"Client\", \"Approver\").",
      "type": "string"
    },
    "hasFailed": {
      "description": "Only return flows with at least one FAILED step execution.",
      "type": "boolean"
    },
    "dateRange": {
      "description": "Only return flows started within the trailing window — convenience tokens for common spans. For finer windows (\"last 2 days\", \"last hour\", \"today since 9am\") use `startedAfter` instead.",
      "type": "string",
      "enum": [
        "7d",
        "30d",
        "90d",
        "180d"
      ]
    },
    "startedAfter": {
      "description": "Only return flows whose startedAt is on or after this ISO-8601 timestamp. Use this when `dateRange` does not have a token that fits — anything shorter than 7 days, anything with hour/minute granularity, or anything anchored to an absolute calendar time. Compute as `new Date(Date.now() - N * 86400000).toISOString()` for \"last N days\" or `new Date(Date.now() - N * 3600000).toISOString()` for \"last N hours\". If passed alongside `dateRange`, the later of the two cutoffs wins.",
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
    },
    "templateVersionId": {
      "description": "Restrict to flows spawned from a specific template version.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "templateId": {
      "description": "Restrict to flows from a specific template (any version).",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "accountId": {
      "description": "Restrict to flows tied to a specific account.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "limit": {
      "description": "Max flows to return (default 50, max 100).",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "offset": {
      "description": "Pagination offset (default 0).",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "additionalProperties": false
}
list_overdue_flows flows:read

List active flows whose due date is in the past, sorted by most-overdue first. Use this for daily triage and standup summaries. PATs see only flows the user is involved in (per the standard /v1/flows visibility filter); org-keys see every overdue flow in the org.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "limit": {
      "description": "Max overdue flows to return (default 50).",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "additionalProperties": false
}
list_team_members team:read

List the team members of the bound org: id, name, email, role (ADMIN | MEMBER), permission group, join date. Use this to identify who can be assigned to a step or who owns a flow. Pending invites are not included.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
list_templates templates:read

List all templates in the bound organization. Returns id, name, description, version, status, and stepCount. Use this to discover what workflows the org has defined before starting a flow (M2 adds the start-flow tool).

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
query_audit_trail reports:read

Query the org audit trail — recent events such as flow start/complete, step transitions, role assignments. Use to answer "what happened on this flow?" or "what did actor X do recently?". Returns the newest events first, capped at 100 per call. PATs see only audit events for flows the user can see; org-keys see every audit event in the org.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "description": "Restrict to audit events on a specific flow (by flow id).",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "eventType": {
      "description": "Restrict to a specific event type (e.g. \"flow.started\", \"step.completed\", \"flow.role_assigned\").",
      "type": "string"
    },
    "actorId": {
      "description": "Restrict to events caused by a specific actor (user id).",
      "type": "string"
    },
    "dateRange": {
      "description": "Restrict to events occurring within the trailing window — convenience tokens for common spans. For finer windows (\"last 2 days\", \"last hour\", \"since 9am today\") use `since` instead.",
      "type": "string",
      "enum": [
        "7d",
        "30d",
        "90d",
        "180d"
      ]
    },
    "since": {
      "description": "Restrict to events that occurred on or after this ISO-8601 timestamp. Use this when `dateRange` does not have a token that fits — anything shorter than 7 days, anything with hour/minute granularity, or anything anchored to an absolute calendar time. If passed alongside `dateRange`, the later of the two cutoffs wins.",
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
    },
    "limit": {
      "description": "Max events to return (default 25, max 100). Newest first.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "additionalProperties": false
}
query_steps flows:read

Org-wide step query — list step executions across every flow you can see, filtered by flow / status / overdue / reopened / due-by. Use to answer "what is overdue org-wide?" or "what steps named approval are open right now?". Newest first, capped at 100 per call. PATs see only steps for flows the user can see; org-keys see every step in the org.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "description": "Restrict to step executions belonging to a specific flow.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "stepName": {
      "description": "Restrict to steps whose name contains this substring (case-insensitive prefix-and-substring match).",
      "type": "string"
    },
    "status": {
      "description": "Restrict to steps in this execution status.",
      "type": "string",
      "enum": [
        "PENDING",
        "IN_PROGRESS",
        "PREPARING",
        "WAITING_FOR_ASSIGNEE",
        "REVIEWING",
        "COMPLETED",
        "FAILED",
        "CANCELLED",
        "SKIPPED",
        "SUPERSEDED"
      ]
    },
    "isOverdue": {
      "description": "Only return steps whose dueAt has passed and are still in an active phase.",
      "type": "boolean"
    },
    "hasReopens": {
      "description": "Only return steps that have been reopened at least once.",
      "type": "boolean"
    },
    "dueBefore": {
      "description": "Only return steps due on or before this ISO-8601 timestamp.",
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
    },
    "startedAfter": {
      "description": "Only return steps that started on or after this ISO-8601 timestamp. Symmetric to `dueBefore`. Use for fine-grained windows of step activity (\"steps that started in the last 2 hours\").",
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
    },
    "accountId": {
      "description": "Restrict to steps on flows tied to a specific account.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "limit": {
      "description": "Max steps to return (default 25, max 100). Newest first by startedAt.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "additionalProperties": false
}
search flows:read templates:read external_users:read

Universal search across flows, templates, and contacts in the bound org. Substring match (case-insensitive) on name fields. Returns up to `limit` results across all three categories combined (default 30). Use this as the entry-point when the user mentions a name like 'ACME Onboarding' and you don't know whether it's a flow, a template, or a contact. Hits include an `id` and a `type` (flow|template|contact); pass each id back to downstream tools (`start_flow`, `get_flow`, `get_external_user`, etc.) exactly as returned (contact ids are opaque per-org `usr_…` strings).

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 2
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    }
  },
  "required": [
    "query"
  ],
  "additionalProperties": false
}
send_reminder flows:write

Send a manual reminder to the participant(s) of an active step execution. Mirrors the workspace's 'Remind' button — picks email or SMS per the participant's primaryChannel, honors per-role mutes, bumps `reminderCount`, and writes a notification_log row. Use `stepExecutionId` from `list_flow_steps` to target the correct execution (a single stepId may have multiple executions in looped or repeated flows). Returns INVALID_STATE for terminal steps (COMPLETED/CANCELLED/FAILED) or PENDING steps with no participant yet. `manage` access required.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "flowId": {
      "type": "string",
      "minLength": 1
    },
    "stepExecutionId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "flowId",
    "stepExecutionId"
  ],
  "additionalProperties": false
}
start_flow flows:write

Start a new flow from a published template. Caller supplies templateId, optional name (auto-generated if omitted), optional fieldValues, and a name-keyed roleAssignments map. Each role assignment is a tagged `{ kind: 'user' | 'contact', id: <uuid> }` reference (or array of references for group roles) — `kind` is required because a UUID alone can't disambiguate a team member from a contact, and a wrong guess produces a FK violation on insert. Only a published (ACTIVE) template can start — a template shown by list_templates may be DRAFT or archived: NOT_FOUND for cross-org templates, INVALID_STATE for a non-ACTIVE template (publish a DRAFT, or restore-and-publish an archived one, first), VALIDATION_ERROR for unknown role names. Substrate-aware: emits flow.started inside the same tx as the flow row commit; runs post-commit side effects (notifications, magic-link mint, DDR context, webhook dispatch, usage metering). Test-mode is NOT supported — use the template builder.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "templateId": {
      "type": "string",
      "minLength": 1
    },
    "name": {
      "type": "string",
      "maxLength": 200
    },
    "fieldValues": {
      "description": "Flow-start field values, keyed by the field `key` (or `fieldId`). Required inputs must be present.",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {}
    },
    "roleAssignments": {
      "description": "Map of role *name* (label as it appears on the template) to participant reference(s). Each reference is `{ kind: \"user\" | \"contact\", id: <id> }` — single object for a single participant, array for multi-participant group roles. Unknown role names return VALIDATION_ERROR. Pass each `id` exactly as returned by `list_external_users`, `list_team_members`, or `search` (an opaque per-org `usr_…` string). Example: `{ \"Client\": { \"kind\": \"contact\", \"id\": \"usr_7Kd2mQxWnR4vT9pLcYb3Ae\" }, \"Reviewer\": { \"kind\": \"user\", \"id\": \"usr_2DNenpAmc5c5LNsmpZRUrO\" } }`.",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "user",
                  "contact"
                ],
                "description": "Whether `id` references a team member (user) or an external participant (contact). Required — the resolver cannot disambiguate from the UUID alone."
              },
              "id": {
                "type": "string",
                "minLength": 1,
                "description": "Opaque per-org id of the user or contact (an `usr_…` string) — pass exactly as returned by `list_team_members` / `list_external_users` / `search`."
              }
            },
            "required": [
              "kind",
              "id"
            ],
            "additionalProperties": false
          },
          {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "user",
                    "contact"
                  ],
                  "description": "Whether `id` references a team member (user) or an external participant (contact). Required — the resolver cannot disambiguate from the UUID alone."
                },
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "description": "Opaque per-org id of the user or contact (an `usr_…` string) — pass exactly as returned by `list_team_members` / `list_external_users` / `search`."
                }
              },
              "required": [
                "kind",
                "id"
              ],
              "additionalProperties": false
            }
          }
        ]
      }
    },
    "connectionBundleKey": {
      "description": "The stable key of the connection bundle this flow acts through, when the template uses a connected app that resolves per connection bundle. Ignored otherwise. Find keys via the connection bundles list.",
      "type": "string"
    }
  },
  "required": [
    "templateId"
  ],
  "additionalProperties": false
}

Prompts (3)

Slash-commands the server advertises. The client's LLM does the orchestration; the prompt text references tools by name.

/flow_activity_summary flows:read reports:read

Standup-style summary of recent flow activity, grouped by template. Highlights new escalations vs lingering debt.

Arguments

  • since — ISO-8601 timestamp for the start of the window. Defaults to 24 hours ago.
/show_my_attention flows:read PAT-only

Triage the caller's active work: what needs your action now, what's waiting on others, what's past due. PAT-only.

Arguments

No arguments.
/start_flow_for flows:write templates:read external_users:read

Start a flow for a person or account. The prompt resolves the target via list_external_users, suggests a matching template, and confirms before starting.

Arguments

  • target (required) — Free-form description of the person or account to start the flow for.

Resources (3)

URI-addressable read-only documents. Subscribable resources push notifications/resources/updated when the underlying state changes (M4 wiring).

moxo://attention flows:read subscribable PAT-only

Active executor steps for the calling user. PAT-only.

MIME type: application/json

moxo://flows/{flowId} flows:read subscribable

Single Moxo flow by id, including steps, role assignments, and template metadata.

MIME type: application/json

moxo://templates/{templateId}@{version} templates:read

Single Moxo template, version-pinned. Use `@latest` for the active version.

MIME type: application/json

Example prompts

Natural-language prompts that route through this server. Try these in your MCP client's chat:

Troubleshooting

Missing MOXO_API_TOKEN
Your MCP client didn't pass the env block through. Confirm env: is set in your client config.
MOXO_API_TOKEN looks like an organization API key
You passed an mxo_sk_* token without setting MOXO_ALLOW_ORG_KEY=1. PATs are the recommended kind for personal installs.
plaintext http:// is only allowed for loopback
Your MOXO_BASE_URL is http:// on a non-loopback host. Use https:// — sending a Bearer token over an unencrypted connection would expose it on the network path.
401 Unauthorized
Token is valid syntax but rejected. Either it was revoked / expired, or you're pointing at the wrong MOXO_BASE_URL for that tenant.
Tools don't appear after restart
Confirm node >= 20 is on your MCP client's PATH. Claude Desktop in particular launches with a stripped environment; if you use nvm, use an absolute path to npx in the command field.