{
  "name": "Nopaque",
  "kind": "product",
  "version": "0.1.0",
  "description": "IVR mapping and testing platform - map phone-tree menus, run scripted mission tests, and orchestrate load tests against any phone number with a workspace API key.",
  "icon": "https://www.nopaque.co.uk/mark.svg",
  "url": "https://mcp.nopaque.co.uk",
  "serverUrl": "https://mcp.nopaque.co.uk",
  "transport": "streamable-http",
  "websiteUrl": "https://www.nopaque.co.uk",
  "documentation": "https://www.nopaque.co.uk/docs",
  "categories": [
    "voice",
    "telecom",
    "testing",
    "contact-centre",
    "compliance"
  ],
  "capabilities": {
    "tools": true,
    "resources": false
  },
  "auth": {
    "type": "apiKey",
    "in": "header",
    "name": "x-api-key"
  },
  "tools": [
    {
      "name": "createMapping",
      "description": "Create an IVR mapping job for a phone number"
    },
    {
      "name": "getMapping",
      "description": "Get the current state and run metadata for a specific IVR mapping job.\n\n    USE WHEN:\n    - User asks about a specific mapping job by ID\n    - User wants to know if a mapping run has finished\n    - User wants run-level metadata (stats, in-flight calls, start/completion times)\n    - You need a job's currentRun.id to feed into getMappingTree or REST /mapping/{id}/runs\n\n    QUERY PARAMS (optional):\n    - version: Run number (1-indexed) — fetches that specific historical run. Default: latest run.\n\n    JOB vs RUN STATUS (critical):\n    Mapping JOBS cycle status `running` ↔ `idle` and **never reach `completed`** between runs.\n    Only RUNS reach `completed` / `failed` / `limited`. To detect a finished run, read\n    `currentRun.status === 'completed'`. **Do NOT poll for `status === 'completed'` at the\n    job level — that state never arrives.** See getMappingTree for the discovered IVR tree,\n    available once `currentRun.status === 'completed'`.\n\n    RESPONSE FIELDS:\n    - id: Job UUID.\n    - status: Job-level status (always `idle` between runs; `running` during an active run).\n    - tags: String labels for grouping (set via createMapping or update).\n    - config: mappingMode, maxDepth, maxCalls, probeMode.\n    - currentRun: The run this response describes — the active/latest run by default, or the run\n      identified by `version` when supplied. Contains: id, status, runNumber, stats, inFlightCount,\n      limitReason, startedAt, completedAt. **Prefer `currentRun.status` over the flat `status`.**\n    - status / runNumber / stats / inFlightCount / limitReason / startedAt / completedAt: Flat-merged\n      duplicates of currentRun fields, kept for back-compat. New code should read currentRun.*.\n\n    USAGE PATTERNS:\n    - \"Is mapping <id> finished?\" → getMapping { id } then read `currentRun.status`\n    - \"Show me run 2 of mapping <id>\" → getMapping { id, version: 2 }\n    - \"What's the in-flight call count for mapping <id>?\" → getMapping { id } then read `currentRun.inFlightCount`\n\n    RELATED TOOLS:\n    - listMappings — find a job by phoneNumber/status/tag/createdAfter (use this before getMapping if you don't have the id)\n    - getMappingTree — discovered IVR tree once `currentRun.status === 'completed'` (use `?format=flat` for leaf-count / depth aggregation)\n    - cancelMapping — **DESTRUCTIVE** — stop an in-progress run; only call when explicitly asked\n    - createMapping — **REAL CALL** — starts a new mapping run (initiates external phone call); only call when explicitly asked\n    - REST GET /mapping/{id}/runs — run history (REST-only today; may surface as a future MCP tool if eval shows demand)\n\n    ERRORS:\n    - 404: job does not exist or belongs to another workspace (cross-workspace 404 by design — do not retry)\n    - 400: invalid version (non-integer or < 1)\n    - 404: requested version not found in this job's run history (response message lists available versions)"
    },
    {
      "name": "listMappings",
      "description": "List IVR mapping jobs in your workspace. Supports filtering and cursor pagination.\n\n    FILTERS (all optional):\n    - status: Filter by the latest run's status. Values: idle | running | completed | failed | limited.\n      IMPORTANT: Mapping JOBS cycle between status \\`running\\` (active mapping) and \\`idle\\` (between runs)\n      and NEVER reach \\`completed\\`. Only RUNS reach \\`completed\\`/\\`failed\\`/\\`limited\\`. The \\`status\\` filter\n      compares against the latest run's status — so \\`status=completed\\` returns jobs whose most recent\n      run finished, and \\`status=running\\` returns jobs with an in-flight run.\n    - tag: Filter to jobs with this tag (exact, lowercase alphanumeric + hyphens, e.g. 'compliance-eu').\n    - phoneNumber: Filter by exact E.164 phone number (e.g. '+441234567890').\n    - name: Case-insensitive substring match against the job name.\n    - profileId: Filter to jobs using a specific mapping profile (UUID).\n    - createdAfter / createdBefore: ISO-8601 datetime strings (e.g. '2026-05-01T00:00:00Z'). Efficient\n      DDB range filter — both, either, or neither.\n    - limit: Page size, 1-100, default 50.\n\n    SORTING:\n    - sortDir: 'asc' or 'desc' (default 'desc' = newest first). Sort is always by createdAt.\n\n    PAGINATION:\n    - Pass \\`cursor\\` (from a previous response's \\`nextCursor\\`) to get the next page.\n    - When \\`nextCursor\\` is absent from the response, you are on the last page.\n    - LOSSY-PAGINATION CAVEAT: When filtering by \\`status\\`, paged results may underfill (return fewer\n      than \\`limit\\` items) because the status filter is applied after the DDB query. Always follow\n      \\`nextCursor\\` until it is absent to be sure you have all matches.\n\n    RESULT FIELDS:\n    - id: UUID — use with \\`getMapping\\` for full detail (run state, step counts) or \\`getMappingTree\\`\n      for the discovered IVR tree (once the latest run has status=\\`completed\\`).\n    - status: The latest run's status; \\`idle\\` when the job has no active run.\n    - tags: String labels for grouping. Set via \\`createMapping\\` / mapping update.\n    - config: Includes mappingMode, maxDepth, maxCalls, probeMode.\n    - runNumber: The run number of the latest run, if any.\n\n    USAGE PATTERNS:\n    - \"List my 5 most recent mappings\" → { limit: 5, sortDir: 'desc' }\n    - \"Show running mapping jobs\" → { status: 'running' }\n    - \"Mappings tagged compliance-eu\" → { tag: 'compliance-eu' }\n    - \"Mappings created this week\" → { createdAfter: '<start-of-week ISO>' }\n    - \"Find runs for +44... in detail\" → call listMappings { phoneNumber: '+44...' } then getMapping(id)"
    },
    {
      "name": "cancelMapping",
      "description": "Cancel an in-progress mapping job"
    },
    {
      "name": "getMappingTree",
      "description": "Get the discovered IVR tree structure for a mapping job — every node visited, the DTMF/voice paths between them, audio recordings, and (if enrichment ran) IVR menu transcripts and probe classifications.\n\n    USE WHEN:\n    - User asks for the discovered tree / map / structure of an IVR\n    - User wants leaf-node count, depth, or aggregate tree statistics (use `format=flat`)\n    - User wants to inspect specific tree-node fields (voice prompts, probe classifications, audio URLs)\n    - User wants to compare two historical runs (call twice with different `version`)\n\n    BEST CALLED: after `currentRun.status === 'completed'` (from getMapping). If you call earlier and\n    the run is still active, the response will document that via `reason: 'in_progress'` (see below).\n\n    QUERY PARAMS (optional):\n    - format: `tree` (default) = hierarchical nesting; `flat` = array of nodes with depth/path fields\n      for aggregation. Use `flat` for \"how many leaves?\" / \"how deep?\" / \"list all voice prompts\" questions.\n    - version: Run number (1-indexed) — fetches the tree of that historical run. Default: latest run.\n\n    RESPONSE — SUCCESS (tree built from steps):\n    - jobId, runId, runNumber, status, stats: identifiers + run-level summary.\n    - tree (when `format=tree`): a hierarchical TreeNode with .children[] recursion.\n    - steps (when `format=flat`): an array of TreeNode (each with empty .children, depth, path).\n\n    TreeNode fields (each is OPTIONAL — omitted when absent; never null per the AJV contract):\n    - stepId, digit, label, depth, path, status, transcript, isTerminal, children, duration\n    - stepType: 'dtmf' | 'voice' — interaction type (Phase 30 voice-agent)\n    - voicePrompt: spoken prompt text at this node (voice nodes only)\n    - menuLabel: semantic tag emitted per bot turn (e.g. 'greeting', 'capabilities_listed', 'balance_captured'); from Phase 55 voice-agent enrichment\n    - spokenResponse: what the agent said back at this turn (Phase 55 voice-agent)\n    - probeCategory / probeClassification / probeRationale: probe-enrichment fields (Phase 31). Only present\n      after probe enrichment ran. Trigger probes via REST POST /mapping/{id}/runs/{runId}/probe (not exposed\n      via MCP today).\n    - audioUrl: presigned S3 URL for the recorded audio of this step. **Valid for approximately 1 hour.\n      Do not cache or store** — re-call getMappingTree to refresh URLs for long sessions.\n    - inputRequired: per-step UX hint when a prompt requires user input. Contains type, description,\n      formatHint, terminator, startTimeMs.\n\n    RESPONSE — EMPTY-STATE ENVELOPE (200 OK, tree: null):\n    The handler returns 200 with `tree: null` and a `reason` discriminator in three cases:\n    - `reason: 'no_runs'` — the job has never been started. Message suggests POST /mapping/{id}/start.\n    - `reason: 'no_steps'` — a run started but no steps recorded yet. May still be initialising; retry shortly.\n    - `reason: 'in_progress'` — the latest run is still active and only the root step has been recorded.\n      Don't treat the current tree as complete. Retry shortly OR ask getMapping for `currentRun.status` to confirm.\n\n    All three empty-state cases return 200 OK with the same envelope shape; only `reason` + `message` differ.\n\n    USAGE PATTERNS:\n    - \"Show me the IVR tree for mapping <id>\" → getMappingTree { id }\n    - \"How many leaves does mapping <id> have?\" → getMappingTree { id, format: 'flat' } then filter steps where children.length === 0 OR isTerminal === true\n    - \"How deep is the tree?\" → getMappingTree { id, format: 'flat' } then max(steps[].depth)\n    - \"What voice prompts did mapping <id> discover?\" → getMappingTree { id, format: 'flat' } then filter where stepType === 'voice' or voicePrompt is present\n    - \"Show me run 1's tree\" → getMappingTree { id, version: 1 }\n    - \"What probe classifications fired?\" → getMappingTree { id } then walk tree filtering nodes where probeClassification is present\n\n    RELATED TOOLS:\n    - getMapping — job + run metadata, including `currentRun.status` (check this before deciding the tree is \"done\")\n    - listMappings — find jobs by phoneNumber/status/tag if you don't have the id\n    - REST GET /mapping/{id}/runs — run history (REST-only today; useful for picking a `version` for historical tree fetches)\n\n    ERRORS:\n    - 404: job does not exist or belongs to another workspace (cross-workspace 404 by design — do not retry)\n    - 400: invalid version (non-integer or < 1) OR invalid format (not 'tree' or 'flat')\n    - 404: requested version not found\n    - 200 + reason: 'no_runs' | 'no_steps' | 'in_progress' — NOT errors; empty-state envelope (see above)"
    },
    {
      "name": "runMissionTest",
      "description": "Launch a mission test run from a saved config"
    },
    {
      "name": "getMissionTestRun",
      "description": "Get a mission-test run's status and verdict"
    },
    {
      "name": "listMissionTestConfigs",
      "description": "List saved mission-test configs in your workspace with rich filters. Each saved config is a reusable mission-test definition (name, phone, sector, persona/mission, acceptance criterion, profile, optional tags). Filter by: `name` (substring), `phoneNumber` (E.164 exact), `sector` (e.g. `financial-services`), `profileId`, `tag` (lowercase exact, single tag), `createdAfter` / `createdBefore` (ISO8601). Sort by `createdAt` or `name`, asc/desc (default `createdAt desc`). Paginate via `cursor` + `limit` (1..100, default 50). Use when: the user wants to find or list configs by attribute (\"configs for compliance-EU\", \"configs for phone +44…\", \"configs tagged X\", \"configs created this month\"). Returns a slim projection — for full mission/acceptance text use getMissionTestConfig once you have identified the config. Mission configs are immutable today — there is no separate \"modified\" timestamp. To find configs newly added in a time window, filter by createdAfter / createdBefore. Cross-tool: for the run history launched from a specific config, use listTestRuns?configId=X. For pass-rate-by-config, use aggregateTestRuns?groupBy=configId. Mission configs are workspace-scoped; cross-workspace returns 404."
    },
    {
      "name": "getMissionTestConfig",
      "description": "Get a single saved mission-test config by ID. Returns the full row: name, description, phoneNumber, sector, mission, acceptance, profileId, tags, createdAt, updatedAt. Use when: you have a config ID (typically from listMissionTestConfigs or from a run.configId field) and want to inspect the persona / mission / acceptance criterion text — for example before launching a new run or to explain to the user how a previous run was configured. Cross-tool: for run history launched from this config, use listTestRuns?configId=X. For the run that produced a specific verdict, use getMissionTestRun. Mission configs are immutable today — updatedAt always equals createdAt. Cross-workspace returns 404."
    },
    {
      "name": "getRunResults",
      "description": "Get any test run's status and per-step results"
    },
    {
      "name": "listLoadTests",
      "description": "List load-test runs in your workspace"
    },
    {
      "name": "listComplianceCatalogue",
      "description": "List available regulatory compliance tests"
    },
    {
      "name": "runComplianceTest",
      "description": "Run a regulatory compliance test against a phone number"
    },
    {
      "name": "getWorkspaceUsage",
      "description": "Get current workspace usage and entitlement balances"
    },
    {
      "name": "listTestRuns",
      "description": "List test runs (mission/compliance/standard/param) with filters: runType, outcome, phoneNumber, configId, date range; cursor pagination"
    },
    {
      "name": "aggregateTestRuns",
      "description": "Aggregate test-run counts; groupBy outcome/runType/configId/catalogueTestId/phoneNumber; optional timeBucket day/week/month"
    }
  ]
}
