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
29 changes: 29 additions & 0 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,32 @@ Required Permissions:
Platform API endpoint used:

- [GET /api/v2/speechandtextanalytics/conversations/{conversationId}](https://developer.genesys.cloud/analyticsdatamanagement/speechtextanalytics/#get-api-v2-speechandtextanalytics-conversations--conversationId-)

## Conversation Topics

Retrieves Speech and Text Analytics topics detected for a specific conversation. Topics
represent business-level intents (e.g. cancellation, billing enquiry) inferred from recognised
phrases in the customer-agent interaction.

Read more [about programs, topics, and phrases](https://help.mypurecloud.com/articles/about-programs-topics-and-phrases/).

[Source file](/src/tools/conversationTopics.ts).

### Input

- `conversationId`
- A UUID ID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)

### Security

Required Permissions:

- `speechAndTextAnalytics:topic:view`
- `analytics:conversationDetail:view`
- `analytics:speechAndTextAnalyticsAggregates:view`

Platform API endpoints used:

- [GET /api/v2/speechandtextanalytics/topics](https://developer.genesys.cloud/analyticsdatamanagement/speechtextanalytics/#get-api-v2-speechandtextanalytics-topics)
- [GET /api/v2/analytics/conversations/{conversationId}/details](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#get-api-v2-analytics-conversations--conversationId--details)
- [POST /api/v2/analytics/transcripts/aggregates/query](https://developer.genesys.cloud/analyticsdatamanagement/analytics/analytics-apis#post-api-v2-analytics-transcripts-aggregates-query)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@makingchatbots/genesys-cloud-mcp-server",
"version": "0.0.6",
"version": "0.0.7",
"description": "A MCP server for connecting LLMs to Genesys Cloud's Platform API",
"exports": "./dist/index.js",
"type": "module",
Expand Down
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { sampleConversationsByQueue } from "./tools/sampleConversationsByQueue.j
import { queryQueueVolumes } from "./tools/queryQueueVolumes.js";
import { voiceCallQuality } from "./tools/voiceCallQuality.js";
import { conversationSentiment } from "./tools/conversationSentiment.js";
import { conversationTopics } from "./tools/conversationTopics.js";

const configResult = loadConfig(process.env);
if (!configResult.success) {
Expand Down Expand Up @@ -90,6 +91,21 @@ server.tool(
),
);

const conversationTopicsTool = conversationTopics({
speechTextAnalyticsApi,
analyticsApi,
});
server.tool(
conversationTopicsTool.schema.name,
conversationTopicsTool.schema.description,
conversationTopicsTool.schema.paramsSchema.shape,
withAuth(
conversationTopicsTool.call,
config.genesysCloud,
platformClient.ApiClient.instance,
),
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.log("Started...");
25 changes: 13 additions & 12 deletions src/tools/conversationSentiment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { randomUUID } from "node:crypto";
import { McpError } from "@modelcontextprotocol/sdk/types.js";

describe("Conversation Sentiment Tool", () => {
let toolDeps: MockedObjectDeep<ToolDependencies>;
Expand Down Expand Up @@ -80,13 +81,12 @@ describe("Conversation Sentiment Tool", () => {
conversationIds: [],
},
}),
).rejects.toMatchObject({
name: "McpError",
code: -32602,
message: expect.stringContaining(
"Array must contain at least 1 element(s)",
) as string,
});
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("conversationId") &&
error.message.includes("Array must contain at least 1 element(s)"),
);
});

test("errors when conversation ID not UUID", async () => {
Expand All @@ -97,11 +97,12 @@ describe("Conversation Sentiment Tool", () => {
conversationIds: ["invalid-uuid"],
},
}),
).rejects.toMatchObject({
name: "McpError",
code: -32602,
message: expect.stringContaining("Invalid uuid") as string,
});
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("conversationIds") &&
error.message.includes("Invalid uuid"),
);
});

test("error from Genesys Cloud's Platform SDK returned", async () => {
Expand Down
14 changes: 1 addition & 13 deletions src/tools/conversationSentiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
SpeechTextAnalyticsApi,
Models,
} from "purecloud-platform-client-v2";
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { errorResult } from "./utils/errorResult.js";

export interface ToolDependencies {
readonly speechTextAnalyticsApi: Pick<
Expand All @@ -29,18 +29,6 @@ const paramsSchema = z.object({
.describe("A list of up to 100 conversation IDs to retrieve sentiment for"),
});

function errorResult(errorMessage: string): CallToolResult {
return {
isError: true,
content: [
{
type: "text",
text: errorMessage,
},
],
};
}

function interpretSentiment(score?: number): string {
if (score === undefined) return "Unknown";
if (score > 55) return "Positive";
Expand Down
137 changes: 137 additions & 0 deletions src/tools/conversationTopics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { conversationTopics, ToolDependencies } from "./conversationTopics.js";
import { MockedObjectDeep } from "@vitest/spy";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { randomUUID } from "node:crypto";
import { McpError } from "@modelcontextprotocol/sdk/types.js";

describe("Conversation Topics Tool", () => {
let toolDeps: MockedObjectDeep<ToolDependencies>;
let client: Client;
let toolName: string;

beforeEach(async () => {
toolDeps = {
speechTextAnalyticsApi: {
getSpeechandtextanalyticsTopics: vi.fn(),
},
analyticsApi: {
getAnalyticsConversationDetails: vi.fn(),
postAnalyticsTranscriptsAggregatesQuery: vi.fn(),
},
};

const toolDefinition = conversationTopics(toolDeps);
toolName = toolDefinition.schema.name;

const server = new McpServer({ name: "TestServer", version: "test" });
server.tool(
toolDefinition.schema.name,
toolDefinition.schema.description,
toolDefinition.schema.paramsSchema.shape,
toolDefinition.call,
);

const [serverTransport, clientTransport] =
InMemoryTransport.createLinkedPair();

await server.connect(serverTransport);

client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(clientTransport);
});

test("schema describes tool", async () => {
const tools = await client.listTools();
expect(tools.tools[0]).toStrictEqual({
name: "conversation_topics",
description:
"Retrieves Speech and Text Analytics topics detected for a specific conversation. Topics represent business-level intents (e.g. cancellation, billing enquiry) inferred from recognised phrases in the customer-agent interaction.",
inputSchema: {
type: "object",
properties: {
conversationId: {
description:
"A UUID ID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)",
format: "uuid",
type: "string",
},
},
required: ["conversationId"],
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
},
annotations: undefined,
});
});

test("errors when no conversation ID provided", async () => {
await expect(
client.callTool({
name: toolName,
arguments: {
conversationId: "",
},
}),
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("conversationId") &&
error.message.includes("Invalid uuid"),
);
});

test("sentiment returned for single conversation", async () => {
const conversationId = randomUUID();

toolDeps.analyticsApi.getAnalyticsConversationDetails.mockResolvedValue({
conversationStart: "2025-05-19T20:00:07.395Z",
conversationEnd: "2025-05-19T21:00:52.686Z",
});
toolDeps.analyticsApi.postAnalyticsTranscriptsAggregatesQuery.mockResolvedValue(
{
results: [
{ group: { topicId: "test-topic-id-1" } },
{ group: { topicId: "test-topic-id-2" } },
],
},
);
toolDeps.speechTextAnalyticsApi.getSpeechandtextanalyticsTopics.mockResolvedValue(
{
entities: [
{
name: "Test Topic 1",
description: "Test Topic 1 Desc",
},
{
name: "Test Topic 2",
description: "Test Topic 2 Desc",
},
],
},
);

const result = await client.callTool({
name: toolName,
arguments: {
conversationId: conversationId,
},
});

expect(result).toStrictEqual({
content: [
{
type: "text",
text: `
Conversation ID: ${conversationId}
Detected Topics:
• Test Topic 1: Test Topic 1 Desc
• Test Topic 2: Test Topic 2 Desc
`.trim(),
},
],
});
});
});
Loading