Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "genesys-cloud-architect",
"source": "./",
"description": "Create, test, and debug Genesys Cloud Architect flows",
"version": "2.0.2"
"version": "2.0.3"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "genesys-cloud-architect",
"description": "Create, test, and debug Genesys Cloud Architect flows",
"version": "2.0.2",
"version": "2.0.3",
"author": {
"name": "Lucas Woodward",
"url": "https://makingchatbots.com/"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This plugin allows you to:
* `Architect > Flow Instance > View`
* `Architect > Flow Instance > Search`
* `Architect > Flow Instance Execution Data > View`
* `Architect > User Prompt > View`

## Usage

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genesys-cloud-architect",
"version": "2.0.2",
"version": "2.0.3",
"private": true,
"packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d",
"author": {
Expand Down
110 changes: 55 additions & 55 deletions servers/genesys-cloud-architect-mcp.js

Large diffs are not rendered by default.

26 changes: 25 additions & 1 deletion skills/interpret-flow-ir/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: interpret-flow-ir
description: Use when interpreting the JSON returned by the flow_ir, flow_action, search_in_flow, find_flow_execution or flow_execution_data tools, or when the user asks about a deployed Genesys Cloud Architect flow, including one named only by flow name (resolve with find_flow first) or a queue named only by queue name (resolve with find_queue first). Covers structural questions ("analyse this flow", "trace the path", "find dead logic", "does it loop"), semantic questions ("what does this decision check", "what does pressing 2 do"), search questions ("which actions reference this queue", "which use Flow.DNIS"), and execution questions about a real conversation or test run ("what did this conversation do", "why did the customer loop", "did the flow error", "is it working"). Use it to answer control-flow questions from the IR rather than the raw configuration JSON, to find the actions worth inspecting, to fetch per-action settings the IR omits, and to read one run's execution data against the IR.
description: Use when interpreting the JSON returned by the flow_ir, flow_action, search_in_flow, find_flow_execution or flow_execution_data tools, or when the user asks about a deployed Genesys Cloud Architect flow, including one named only by flow name (resolve with find_flow first) or a queue named only by queue name (resolve with find_queue first). Covers structural questions ("analyse this flow", "trace the path", "find dead logic", "does it loop"), semantic questions ("what does this decision check", "what does pressing 2 do"), search questions ("which actions reference this queue", "which use Flow.DNIS"), prompt questions ("what does the caller hear here", "what does Prompt.Welcome say"; resolve with get_prompts), and execution questions about a real conversation or test run ("what did this conversation do", "why did the customer loop", "did the flow error", "is it working"). Use it to answer control-flow questions from the IR rather than the raw configuration JSON, to find the actions worth inspecting, to fetch per-action settings the IR omits, and to read one run's execution data against the IR.
---

# Interpreting Flow IRs
Expand Down Expand Up @@ -211,6 +211,30 @@ matching `paths[]` entry supplies the outcome *name*, and the IR branch-output
node `<actionId>::<outputId>` supplies where that outcome leads. Use
`paths[].nextActionId` for nothing.

## Prompt content: the `get_prompts` tool

Audio-playing actions (Play Audio, Communicate, Menu, Collect Input) reference
reusable *user prompts* by name, as `Prompt.<name>`, so the raw config returned by
`flow_action` tells you *which* prompt plays but not what the caller hears or for
how long. `get_prompts` resolves that. Three ways to ask, combinable in one call:

- `flowId` — every user prompt the flow's latest configuration references, in
encounter order. Reach for this first when the question is about a flow's whole
script; one call replaces a `flow_action` sweep.
- `promptNames` — names exactly as the config shows them (the `Prompt.` prefix is
accepted and stripped). Matching is exact, not a search.
- `promptIds` — ids from the USERPROMPT entries of `flow_dependencies`.

**Attributing a hang-up to a prompt.** `durationTotals` sums recorded durations
per language over the prompts *in the order returned*, and `sequence` gives each
prompt's `startSeconds`. The offsets mean something only if that is the play
order: use `flowId` alone, or list `promptIds`/`promptNames` in play order.
Encounter order follows one path through a branching flow, so confirm the path
against the IR before reading off which prompt was playing N seconds in.

Inline `ToAudioTTS("...")` text and `PromptSystem.<name>` system prompts are not
user prompts; the former is already in the raw config, the latter is out of scope.

## Content search: the `search_in_flow` tool

`search_in_flow` answers "which actions reference X" in **one call**. It matches
Expand Down
8 changes: 8 additions & 0 deletions src/mcp-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { flowAction } from "./tools/flow-action.ts";
import { flowDependencies } from "./tools/flow-dependencies.ts";
import { flowExecutionData } from "./tools/flow-execution-data.ts";
import { flowIr } from "./tools/flow-ir.ts";
import { getPrompts } from "./tools/get-prompts.ts";
import { searchInFlow } from "./tools/search-in-flow.ts";
import { testBotFlow } from "./tools/test-bot-flow.ts";

Expand Down Expand Up @@ -87,6 +88,13 @@ server.registerTool(
flowExecutionDataTool.handler,
);

const getPromptsTool = getPrompts({ architectApi });
server.registerTool(
"get_prompts",
getPromptsTool.config,
getPromptsTool.handler,
);

const deployFlowTool = deployFlow({
region: envVars.GENESYS_REGION,
clientId: envVars.GENESYS_CLIENT_ID,
Expand Down
21 changes: 21 additions & 0 deletions src/mcp-server/tools/api-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ export function toApiError(err: unknown): ApiError {
return { status, code, message };
}

/**
* Guidance to append to an auth failure's message. `permission` names the
* Genesys Cloud permission the tool needs, e.g. "Routing > Queue > View
* (routing:queue:view)". Empty for any other status.
*/
export function authHint(
status: number | undefined,
permission: string,
): string {
if (status === 401) {
return (
" The access token is missing or expired; re-authenticate " +
"(restart the MCP server) and retry."
);
}
if (status === 403) {
return ` The OAuth client needs the '${permission}' permission.`;
}
return "";
}

export function formatApiError(err: unknown): string {
const { status, code, message } = toApiError(err);
const parts: string[] = [];
Expand Down
13 changes: 13 additions & 0 deletions src/mcp-server/tools/entity-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** A reference to another Genesys Cloud entity, as the API embeds them. */
export interface EntityRef {
id: string;
name?: string;
}

/** Null when the API gave no reference or one without an id. */
export function toEntityRef(
ref: { id?: string; name?: string } | undefined,
): EntityRef | null {
if (!ref?.id) return null;
return { id: ref.id, ...(ref.name ? { name: ref.name } : {}) };
}
10 changes: 4 additions & 6 deletions src/mcp-server/tools/find-flow-execution.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ArchitectApi, Models } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { formatApiError, toApiError } from "./api-error.ts";
import { authHint, formatApiError, toApiError } from "./api-error.ts";
import type { ToolFactory } from "./types.ts";

/** The API's own ceiling on results per query, per the help centre. */
Expand Down Expand Up @@ -240,11 +240,9 @@ export const findFlowExecution: ToolFactory<ToolConfig, typeof inputSchema> = ({
} catch (err) {
const { status } = toApiError(err);
const hint =
status === 403
? " The OAuth client needs the 'Architect > Flow Instance > Search' permission."
: status === 400
? " The query was rejected; check that the conversationId and flowId are well-formed Genesys Cloud ids."
: "";
status === 400
? " The query was rejected; check that the conversationId and flowId are well-formed Genesys Cloud ids."
: authHint(status, "Architect > Flow Instance > Search");
return {
isError: true,
content: [
Expand Down
27 changes: 8 additions & 19 deletions src/mcp-server/tools/find-queue.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type platformClient from "purecloud-platform-client-v2";
import type { RoutingApi } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { formatApiError, toApiError } from "./api-error.ts";
import { authHint, formatApiError, toApiError } from "./api-error.ts";
import { type EntityRef, toEntityRef } from "./entity-ref.ts";
import { isExactNameMatch, moveExactMatchToTop } from "./name-match.ts";
import type { ToolFactory } from "./types.ts";

const MAX_RETURNED_QUEUES = 50;
const MAX_FETCHED_QUEUES = 200;

interface EntityRef {
id: string;
name?: string;
}

interface QueueSummary {
id: string;
name: string;
Expand Down Expand Up @@ -46,21 +42,14 @@ function toWildcardName(name: string): string {
return `*${fragment}*`;
}

function toEntityRef(
ref: { id?: string; name?: string } | undefined,
): EntityRef | undefined {
if (!ref?.id) return undefined;
return { id: ref.id, ...(ref.name ? { name: ref.name } : {}) };
}

function toQueueSummary(queue: platformClient.Models.Queue): QueueSummary {
const queueFlow = toEntityRef(queue.queueFlow);
const emailInQueueFlow = toEntityRef(queue.emailInQueueFlow);
const messageInQueueFlow = toEntityRef(queue.messageInQueueFlow);
return {
id: queue.id ?? "",
name: queue.name ?? "",
division: toEntityRef(queue.division) ?? null,
division: toEntityRef(queue.division),
memberCount: queue.memberCount ?? null,
joinedMemberCount: queue.joinedMemberCount ?? null,
dateModified: queue.dateModified ?? null,
Expand Down Expand Up @@ -182,16 +171,16 @@ export const findQueue: ToolFactory<ToolConfig, typeof inputSchema> = ({
content: [{ type: "text", text: JSON.stringify(result) }],
};
} catch (err) {
const permissionHint =
toApiError(err).status === 403
? " The OAuth client needs the 'Routing > Queue > View' permission."
: "";
const hint = authHint(
toApiError(err).status,
"Routing > Queue > View",
);
return {
isError: true,
content: [
{
type: "text",
text: `Failed to search for queues: ${formatApiError(err)}${permissionHint}`,
text: `Failed to search for queues: ${formatApiError(err)}${hint}`,
},
],
};
Expand Down
16 changes: 6 additions & 10 deletions src/mcp-server/tools/flow-execution-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@makingchatbots/genesys-cloud-architect-diagram-lib";
import type { ArchitectApi, Models } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { formatApiError, toApiError } from "./api-error.ts";
import { authHint, formatApiError, toApiError } from "./api-error.ts";
import { fetchFlowConfiguration } from "./fetch-flow-configuration.ts";
import type { ToolFactory } from "./types.ts";

Expand All @@ -24,6 +24,7 @@ const DEFAULT_MAX_WAIT_MS = 45_000;

/** How long Genesys Cloud keeps execution data, per the help centre. */
const RETENTION_DAYS = 10;
const PERMISSION = "Architect > Flow Instance > View";

type JobResult = Models.GetFlowExecutionDataJobResult;

Expand Down Expand Up @@ -135,8 +136,8 @@ function describeEntityFailure(
case "403":
return (
`Not authorised to read execution data for "${executionId}" (403): the ` +
"OAuth client needs the 'Architect > Flow Instance > View' permission " +
"and division access to the flow."
`OAuth client needs the '${PERMISSION}' permission and division ` +
"access to the flow."
);
default:
return (
Expand All @@ -151,13 +152,8 @@ function describeApiFailure(executionId: string, err: unknown): string {
if (err instanceof ExecutionDataError) return err.message;
const { status } = toApiError(err);
if (status === 404) return describeNotFound(executionId);
if (status === 403) {
return (
`Not authorised to read execution data (403): the OAuth client needs the ` +
"'Architect > Flow Instance > View' permission."
);
}
return `Failed to retrieve execution data for "${executionId}": ${formatApiError(err)}`;
const hint = authHint(status, PERMISSION);
return `Failed to retrieve execution data for "${executionId}": ${formatApiError(err)}.${hint}`;
}

/** Unwrap the downloaded file, `{ "flow": {...} }`, tolerating a bare flow. */
Expand Down
4 changes: 2 additions & 2 deletions src/mcp-server/tools/flow-ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import type { ArchitectApi } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { fetchFlowConfiguration } from "./fetch-flow-configuration.ts";
import { isExactNameMatch } from "./name-match.ts";
import type { ToolFactory } from "./types.ts";

/**
Expand All @@ -20,8 +21,7 @@ function findTask(
if (byId) {
return { match: byId };
}
const lowered = query.toLowerCase();
const byName = tasks.filter((t) => t.name.toLowerCase() === lowered);
const byName = tasks.filter((t) => isExactNameMatch(t, query));
if (byName.length > 1) {
return { ambiguous: byName };
}
Expand Down
Loading
Loading