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
6 changes: 5 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Development

Test the plugin locally:
Test the plugin locally in the Claude Code CLI:

```shell
CLAUDE_PLUGIN_ROOT=$(pwd) claude --plugin-dir .
Expand All @@ -21,3 +21,7 @@ npx skills add https://github.com/anthropics/claude-plugins-official/tree/main/p
Claude Code plugin for developing plugins

https://github.com/anthropics/claude-code/tree/main/plugins/plugin-dev

## Claude Desktop

Logs for Claude Desktop can be found: `~/Library/Logs/Claude/claude.ai-web.log`
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.5.7",
"@makingchatbots/genesys-cloud-architect-diagram-lib": "^1.3.2",
"@makingchatbots/genesys-cloud-architect-diagram-lib": "^1.4.0",
"@types/node": "^25.8.0",
"esbuild": "^0.28.2",
"husky": "^9.1.7",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ allowBuilds:

minimumReleaseAgeExclude:
- purecloud-flow-scripting-api-sdk-javascript@0.68.3
- '@makingchatbots/genesys-cloud-architect-diagram-lib@1.3.2'
- '@makingchatbots/genesys-cloud-architect-diagram-lib@1.3.5'
- '@makingchatbots/genesys-cloud-architect-diagram-lib@1.4.0'
96 changes: 48 additions & 48 deletions servers/genesys-cloud-architect-mcp.js

Large diffs are not rendered by default.

90 changes: 84 additions & 6 deletions 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: This skill should be used when interpreting the JSON returned by the flow_ir or flow_action tools, or when the user asks questions about a deployed Genesys Cloud Architect flow. Structural questions such as "analyse this flow", "trace the path through the flow", "what happens when the customer says X", "why is this task unreachable", "check the flow for missing error handling", "find dead logic", or "does this flow loop". Semantic questions such as "what does this decision check", "what prompt does it play", "what does this data action send", or "what does pressing 2 do". Use it to answer control-flow questions from the IR instead of guessing from the flow's raw configuration JSON, and to fetch the per-action settings the IR omits.
description: This skill should be used when interpreting the JSON returned by the flow_ir, flow_action or search_in_flow tools, or when the user asks questions about a deployed Genesys Cloud Architect flow. Structural questions such as "analyse this flow", "trace the path through the flow", "what happens when the customer says X", "why is this task unreachable", "check the flow for missing error handling", "find dead logic", or "does this flow loop". Semantic questions such as "what does this decision check", "what prompt does it play", "what does this data action send", or "what does pressing 2 do". Search questions such as "find every action that references this queue", "which actions use Flow.DNIS", "where does this flow mention that data action", or "which actions play a prompt containing X". Use it to answer control-flow questions from the IR instead of guessing from the flow's raw configuration JSON, to find the actions worth looking at without fetching them all, and to fetch the per-action settings the IR omits.
---

# Interpreting Flow IRs
Expand All @@ -11,9 +11,12 @@ Branches, loops, IVR menu choices, and cross-task jumps are already resolved
into edges. Answer structural questions from this IR, never by re-deriving
control flow from the flow's raw configuration JSON.

The two tools are a pair: `flow_ir` owns **structure** — what connects to what —
and `flow_action` owns **semantics** — what an individual action is configured to
do (see "Action semantics").
The tools are a trio. `search_in_flow` owns **discovery** — which actions mention a
given name, expression, or phrase (see "Content search"). `flow_ir` owns
**structure** — what connects to what. `flow_action` owns **semantics** — what an
individual action is configured to do (see "Action semantics"). The usual order
runs the same way: search to find the ids worth caring about, trace to see how
they connect, then inspect only those.

## Tool output shape

Expand Down Expand Up @@ -151,8 +154,12 @@ where that outcome *leads* from the IR's branch-output node, never from

**Branch-output ids.** `<actionId>::<outputId>` is accepted: the suffix is
stripped, the underlying action is returned, and `notes` flags that it happened.
Prefer passing plain action GUIDs. `<taskId>::start` is a task marker rather than
an action, so it can never match.
Ids in one call that strip to the same GUID collapse to a single lookup,
reported only under whichever form was requested first — the other requested id
appears in neither `found` nor `notFound`, so never send both forms of one
action and never treat the absent form as a missing action. Prefer passing
plain action GUIDs. `<taskId>::start` is a task marker rather than an action,
so it can never match.

**`menuChoice`.** Present only when the action sits inside an IVR menu choice.
Its `digit` and `name` are the choice's *presentation* — the keypress and the
Expand Down Expand Up @@ -192,6 +199,77 @@ 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.

## Content search: the `search_in_flow` tool

`search_in_flow` answers "which actions reference X" in **one call**. It matches
text across every action's raw configuration in a flow and returns only the
actions containing it, each with the path and a short excerpt of what matched.

**When to reach for it.** Before any bulk `flow_action` sweep. "Which actions use
the BookSeller module", "what references `Flow.DNIS`", "which queues does this flow
transfer to", "where is that prompt wording" are each one search, not dozens of
action fetches — and a sweep pulls back whole subtrees for actions that turn out
to be irrelevant. Search to find the ids, trace them in `flow_ir`, then fetch
only the ones that matter with `flow_action`.

**Parameters.** `pattern` is a literal substring unless `regex: true`, in which
case it is a JavaScript regular expression. `caseSensitive` defaults to false in
both modes. Prefer a distinctive
literal (a queue name, a variable reference, a phrase of prompt wording) over a
regex when either would answer the question.

**The envelope.**
`{ flowId, pattern, totalMatchedActions, matchedActions: [{ actionId, actionType?, name?, taskId, taskName, menuChoice?, matchedPaths: [{ path, excerpt }], truncated? }], notes? }`

A search matching nothing is a **success**, not an error: `totalMatchedActions:
0` and an empty `matchedActions`. That is a real answer — the flow does not
contain the text — so report it rather than retrying variations of the pattern.

**The join.** `actionId` is the Architect GUID, so it is simultaneously the id of
the `flow_ir` node whose `kind` is `"action"` and an id `flow_action` accepts
verbatim. `taskId` joins to the IR's `<taskId>::start` node, which is how a
scatter of hits becomes "these three tasks are involved". An action occurring
more than once in a flow yields one entry per occurrence — and occurrences can
share both `actionId` *and* `taskId`, since a duplicate may sit inside a single
task (one in its action list, one under a menu choice). `menuChoice` and each
entry's `matchedPaths` are what tell such occurrences apart; group by
`actionId`, exactly as with `flow_action`.

**Excerpts may be clipped.** A short matched value — up to 160 characters, the
common case for queue names, variable references, and short expressions — comes
back **whole**, with no markers: it is safe to quote as that leaf's value
without re-fetching. A longer value is a window around the match, with `…`
marking whichever ends were cut, and the response carries a note whenever any
excerpt was clipped. So the `…` markers are the tell: an unmarked excerpt is the
complete leaf; a marked one is enough to judge relevance but never enough to
quote. Either way an excerpt is one leaf, not the action — when the surrounding
configuration matters, fetch the action with `flow_action` and read `action`
there.

**Truncation is reported in two places.** `totalMatchedActions` is the count
before any capping, so it stays accurate even when `matchedActions` is shorter;
compare the two before asserting "exactly N actions". A per-entry
`truncated: true` means that action matched in more places than `matchedPaths`
lists, so a path's absence from that entry proves nothing. `notes`, when present,
is advisory prose about the search itself — read it, but key no logic off its
wording.

**What a match does and does not mean.** Matching is against **string values
only**, never key names, so every hit is real content. But:

- A hit in a wiring field (`nextAction`, `paths[].nextActionId`) locates the text
of an id; it is **not** a control-flow claim. The scoping rule from
`flow_action` applies unchanged — the IR owns wiring.
- Content search is not reachability. A match inside a disabled branch, an
unreachable action, or a task nothing ever calls is still reported. Before
concluding "this flow transfers to queue X", check the matched actions against
the IR's `reachable` flag and any `DISABLED_BRANCH` warning.
- `path` (e.g. `outputs.0.value.text`) is a legible pointer, not a parseable
grammar: keys containing dots are not escaped. Use it to say where a match
sits, never to reconstruct ids from.
- A match is text, not structure. `search_in_flow` finding a queue name in six
actions says those six mention it, not that six transfers exist.

## Analysis recipes

**Trace "what happens when..."**: walk successors from the entry task-start,
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 @@ -6,6 +6,7 @@ import { deployFlow } from "./tools/deploy-flow.ts";
import { flowAction } from "./tools/flow-action.ts";
import { flowDependencies } from "./tools/flow-dependencies.ts";
import { flowIr } from "./tools/flow-ir.ts";
import { searchInFlow } from "./tools/search-in-flow.ts";
import { testBotFlow } from "./tools/test-bot-flow.ts";

const envResults = z
Expand Down Expand Up @@ -54,6 +55,13 @@ server.registerTool(
flowActionTool.handler,
);

const searchInFlowTool = searchInFlow({ architectApi });
server.registerTool(
"search_in_flow",
searchInFlowTool.config,
searchInFlowTool.handler,
);

const deployFlowTool = deployFlow({
region: envVars.GENESYS_REGION,
clientId: envVars.GENESYS_CLIENT_ID,
Expand Down
76 changes: 76 additions & 0 deletions src/mcp-server/tools/fetch-flow-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { ArchitectApi } from "purecloud-platform-client-v2";

export type FetchFlowConfigurationResult =
| { ok: true; configuration: unknown }
| { ok: false; message: string };

export async function fetchFlowConfiguration(
architectApi: ArchitectApi,
flowId: string,
): Promise<FetchFlowConfigurationResult> {
try {
return {
ok: true,
configuration:
await architectApi.getFlowLatestconfiguration(flowId),
};
} catch (err) {
return { ok: false, message: describeFailure(flowId, err) };
}
}

function describeFailure(flowId: string, err: unknown): string {
const status =
err !== null &&
typeof err === "object" &&
"status" in err &&
typeof err.status === "number"
? err.status
: undefined;
const detail =
err !== null &&
typeof err === "object" &&
"message" in err &&
typeof err.message === "string" &&
err.message.length > 0
? err.message
: undefined;

if (status === 404) {
return `Flow "${flowId}" not found. Check the flow id.`;
}
if (status === 401) {
return (
`Not authenticated with Genesys Cloud (401) fetching flow "${flowId}": ` +
"the access token is missing or expired. The flow may well exist — " +
"re-authenticate (restart the MCP server) and retry rather than " +
"concluding anything about the flow."
);
}
if (status === 403) {
return (
`Not authorised to read flow "${flowId}" (403): the OAuth client lacks ` +
"permission or division access to it. The flow exists but this client " +
"cannot see it."
);
}
if (status === 429) {
return (
`Rate limited by Genesys Cloud (429) fetching flow "${flowId}". ` +
"Transient — wait and retry; this says nothing about the flow."
);
}
if (status !== undefined && status >= 500) {
return (
`Genesys Cloud returned ${status} fetching flow "${flowId}". ` +
"Transient server-side error — retry; this says nothing about the flow."
);
}
return (
`Failed to fetch flow "${flowId}"` +
(status !== undefined ? ` (HTTP ${status})` : "") +
(detail ? `: ${detail}` : ".") +
" Likely transient (network or gateway) — retry rather than concluding " +
"the flow does not exist."
);
}
17 changes: 5 additions & 12 deletions src/mcp-server/tools/flow-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from "@makingchatbots/genesys-cloud-architect-diagram-lib";
import type { ArchitectApi } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { fetchFlowConfiguration } from "./fetch-flow-configuration.ts";
import type { ToolFactory } from "./types.ts";

export interface ToolConfig {
Expand Down Expand Up @@ -87,26 +88,18 @@ export const flowAction: ToolFactory<ToolConfig, typeof inputSchema> = ({
inputSchema,
},
handler: async ({ flowId, actionIds }) => {
let configuration: unknown;
try {
configuration =
await architectApi.getFlowLatestconfiguration(flowId);
} catch {
const fetched = await fetchFlowConfiguration(architectApi, flowId);
if (!fetched.ok) {
return {
isError: true,
content: [
{
type: "text",
text: `Flow "${flowId}" not found or not accessible.`,
},
],
content: [{ type: "text", text: fetched.message }],
};
}

const { lookupIds, requestedFor, strippedAny } = planLookups(actionIds);
// `findRawActions` never throws and always answers for every id, so a
// batch where nothing matched is still a successful lookup.
const lookup = findRawActions(configuration, lookupIds);
const lookup = findRawActions(fetched.configuration, lookupIds);
const asRequested = (entry: RawActionLookup): RawActionLookup => ({
...entry,
actionId: requestedFor.get(entry.actionId) ?? entry.actionId,
Expand Down
17 changes: 5 additions & 12 deletions src/mcp-server/tools/flow-ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from "@makingchatbots/genesys-cloud-architect-diagram-lib";
import type { ArchitectApi } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { fetchFlowConfiguration } from "./fetch-flow-configuration.ts";
import type { ToolFactory } from "./types.ts";

/**
Expand Down Expand Up @@ -64,23 +65,15 @@ export const flowIr: ToolFactory<ToolConfig, typeof inputSchema> = ({
inputSchema,
},
handler: async ({ flowId, task }) => {
let configuration: unknown;
try {
configuration =
await architectApi.getFlowLatestconfiguration(flowId);
} catch {
const fetched = await fetchFlowConfiguration(architectApi, flowId);
if (!fetched.ok) {
return {
isError: true,
content: [
{
type: "text",
text: `Flow "${flowId}" not found or not accessible.`,
},
],
content: [{ type: "text", text: fetched.message }],
};
}

const result = parseFlow(configuration);
const result = parseFlow(fetched.configuration);

if (!result.ok) {
return {
Expand Down
Loading
Loading