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 .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.20.0
24
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": "0.2",
"name": "Genesys Cloud MCP Server",
"version": "1.0.3",
"version": "1.0.4",
"description": "Interact with Genesys Cloud's Platform API",
"long_description": "This extension allows Claude to connect to Genesys Cloud's Platform API via a local MCP server. It provides tools for querying queue volumes, retrieving conversation samples, analyzing sentiment and voice quality, accessing transcripts, and more.\n\nThis project is not affiliated with Genesys.",
"author": {
Expand Down
2,563 changes: 1,435 additions & 1,128 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@makingchatbots/genesys-cloud-mcp-server",
"version": "1.0.3",
"version": "1.0.4",
"description": "A Model Context Protocol (MCP) server exposing Genesys Cloud tools for LLMs, including sentiment analysis, conversation search, topic detection and more.",
"bin": "./dist/cli.js",
"type": "module",
Expand Down Expand Up @@ -37,22 +37,22 @@
"test:pack": "npm run build && npm pack --pack-destination ./dist"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.2",
"@modelcontextprotocol/sdk": "^1.29.0",
"date-fns": "^4.1.0",
"lru-cache": "^11.2.4",
"purecloud-platform-client-v2": "^239.0.0",
"zod": "^3.23.8"
"lru-cache": "^11.2.7",
"purecloud-platform-client-v2": "^249.1.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@anthropic-ai/mcpb": "^2.0.1",
"@biomejs/biome": "2.2.4",
"@google/genai": "^1.12.0",
"@types/node": "^18.19.130",
"dotenv": "^17.2.2",
"lint-staged": "^16.2.7",
"tsx": "^4.20.6",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
"@anthropic-ai/mcpb": "^2.1.2",
"@biomejs/biome": "2.4.10",
"@google/genai": "^1.47.0",
"@types/node": "^25.5.0",
"dotenv": "^17.3.1",
"lint-staged": "^16.4.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2",
"vitest": "^4.1.2"
},
"lint-staged": {
"src/*.ts": [
Expand Down
4 changes: 2 additions & 2 deletions src/auth/OAuthClientCredentialsWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ export const OAuthClientCredentialsWrapper = (
configRetriever: ConfigRetriever,
apiClient: ApiClientClass,
) => {
return <Schema extends z.Schema = z.Schema>(
return <Schema extends z.ZodType = z.ZodType>(
call: ToolCall<Schema>,
): ToolCall<Schema> =>
async (input: Schema) => {
async (input: z.output<Schema>) => {
if (!isAuthenticated) {
const authResult = await authenticate(apiClient, configRetriever);
if (authResult.authenticated) {
Expand Down
7 changes: 3 additions & 4 deletions src/createConfigRetriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ export type Result<T> = SuccessResult<T> | ErrorResult;

const genesysAuthConfigSchema = z.object({
GENESYSCLOUD_REGION: z.string({
required_error: "Missing environment variable: GENESYSCLOUD_REGION",
error: "Missing environment variable: GENESYSCLOUD_REGION",
}),
GENESYSCLOUD_OAUTHCLIENT_ID: z.string({
required_error: "Missing environment variable: GENESYSCLOUD_OAUTHCLIENT_ID",
error: "Missing environment variable: GENESYSCLOUD_OAUTHCLIENT_ID",
}),
GENESYSCLOUD_OAUTHCLIENT_SECRET: z.string({
required_error:
"Missing environment variable: GENESYSCLOUD_OAUTHCLIENT_SECRET",
error: "Missing environment variable: GENESYSCLOUD_OAUTHCLIENT_SECRET",
}),
});

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const withAuth = OAuthClientCredentialsWrapper(

const server: McpServer = new McpServer({
name: "Genesys Cloud",
version: "1.0.3", // Same version as version in package.json
version: "1.0.4", // Same version as version in package.json
});

const cache = new LRUCache<string, OAuthClientUsageResponse>({
Expand Down
32 changes: 20 additions & 12 deletions src/tools/conversationSentiment/conversationSentiment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpError } from "@modelcontextprotocol/sdk/types.js";
import type {
CallToolResult,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";

import type { MockedObjectDeep } from "@vitest/spy";
import { beforeEach, describe, expect, test, vi } from "vitest";
import {
Expand Down Expand Up @@ -52,6 +56,7 @@ describe("Conversation Sentiment Tool", () => {
annotations: { title: "Conversation Sentiment" },
description:
"Retrieves sentiment analysis scores for one or more conversations. Sentiment is evaluated based on customer phrases, categorized as positive, neutral, or negative. The result includes both a numeric sentiment score (-100 to 100) and an interpreted sentiment label.",
execution: { taskSupport: "forbidden" },
inputSchema: {
type: "object",
properties: {
Expand All @@ -60,6 +65,8 @@ describe("Conversation Sentiment Tool", () => {
items: {
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)$",
description:
"A UUID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)",
},
Expand All @@ -70,7 +77,6 @@ describe("Conversation Sentiment Tool", () => {
},
},
required: ["conversationIds"],
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
},
});
Expand All @@ -84,11 +90,13 @@ describe("Conversation Sentiment Tool", () => {
conversationIds: [],
},
}),
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("conversationId") &&
error.message.includes("Array must contain at least 1 element(s)"),
).resolves.toSatisfy(
(result: CallToolResult) =>
result.isError === true &&
(result.content[0] as TextContent).text.includes("conversationId") &&
(result.content[0] as TextContent).text.includes(
"Too small: expected array to have >=1 items",
),
);
});

Expand All @@ -100,11 +108,11 @@ describe("Conversation Sentiment Tool", () => {
conversationIds: ["invalid-uuid"],
},
}),
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("conversationIds") &&
error.message.includes("Invalid uuid"),
).resolves.toSatisfy(
(result: CallToolResult) =>
result.isError === true &&
(result.content[0] as TextContent).text.includes("conversationIds") &&
(result.content[0] as TextContent).text.includes("Invalid UUID"),
);
});

Expand Down
20 changes: 13 additions & 7 deletions src/tools/conversationTopics/conversationTopics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpError } from "@modelcontextprotocol/sdk/types.js";
import type {
CallToolResult,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";

import type { MockedObjectDeep } from "@vitest/spy";
import { beforeEach, describe, expect, test, vi } from "vitest";
import {
Expand Down Expand Up @@ -56,18 +60,20 @@ describe("Conversation Topics Tool", () => {
annotations: { title: "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.",
execution: { taskSupport: "forbidden" },
inputSchema: {
type: "object",
properties: {
conversationId: {
description:
"A UUID for a conversation. (e.g., 00000000-0000-0000-0000-000000000000)",
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)$",
type: "string",
},
},
required: ["conversationId"],
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
},
});
Expand All @@ -81,11 +87,11 @@ describe("Conversation Topics Tool", () => {
conversationId: "",
},
}),
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("conversationId") &&
error.message.includes("Invalid uuid"),
).resolves.toSatisfy(
(result: CallToolResult) =>
result.isError === true &&
(result.content[0] as TextContent).text.includes("conversationId") &&
(result.content[0] as TextContent).text.includes("Invalid UUID"),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,20 @@ describe("Conversation Transcription Tool", () => {
annotations: { title: "Conversation Transcript" },
description:
"Retrieves a structured transcript of the conversation, including speaker labels, utterance timestamps, and sentiment annotations where available. The transcript is formatted as a time-aligned list of utterances attributed to each participant (e.g., customer or agent)",
execution: { taskSupport: "forbidden" },
inputSchema: {
type: "object",
properties: {
conversationId: {
description:
"The UUID of the conversation to retrieve the transcript for (e.g., 00000000-0000-0000-0000-000000000000)",
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)$",
type: "string",
},
},
required: ["conversationId"],
additionalProperties: false,

$schema: "http://json-schema.org/draft-07/schema#",
},
Expand Down
20 changes: 13 additions & 7 deletions src/tools/oauthClientUsage/oauthClientUsage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpError } from "@modelcontextprotocol/sdk/types.js";
import type {
CallToolResult,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";

import type { MockedObjectDeep } from "@vitest/spy";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { oauthClientUsage, type ToolDependencies } from "./oauthClientUsage.js";
Expand Down Expand Up @@ -50,12 +54,15 @@ describe("OAuth Client Usage", () => {
annotations: { title: "OAuth Client Usage" },
description:
"Retrieves the usage of an OAuth Client for a given period. It returns the total number of requests and a breakdown of Platform API endpoints used by the client.",
execution: { taskSupport: "forbidden" },
inputSchema: {
properties: {
oauthClientId: {
description:
"The UUID of the OAuth Client to retrieve the usage for (e.g., 00000000-0000-0000-0000-000000000000)",
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)$",
type: "string",
},
endDate: {
Expand All @@ -71,7 +78,6 @@ describe("OAuth Client Usage", () => {
},
required: ["oauthClientId", "startDate", "endDate"],
type: "object",
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
},
});
Expand All @@ -87,11 +93,11 @@ describe("OAuth Client Usage", () => {
endDate: "2024-01-02T00:00:00Z",
},
}),
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("oauthClientId") &&
error.message.includes("Invalid uuid"),
).resolves.toSatisfy(
(result: CallToolResult) =>
result.isError === true &&
(result.content[0] as TextContent).text.includes("oauthClientId") &&
(result.content[0] as TextContent).text.includes("Invalid UUID"),
);
});

Expand Down
4 changes: 2 additions & 2 deletions src/tools/oauthClients/formatOAuthClientJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type RoleId = string;
type DivisionId = string;

function combineRolesAndDivisions(
client: Models.OAuthClientListing,
client: Models.OAuthClient,
availableDivisions: Models.AuthzDivision[],
availableRoles: Models.DomainOrganizationRole[],
): RoleToDivisionsAssociation[] {
Expand Down Expand Up @@ -52,7 +52,7 @@ function combineRolesAndDivisions(
}

export function formatOAuthClientJson(
client: Models.OAuthClientListing,
client: Models.OAuthClient,
availableDivisions: Models.AuthzDivision[],
availableRoles: Models.DomainOrganizationRole[],
): OAuthClientResponse {
Expand Down
2 changes: 1 addition & 1 deletion src/tools/oauthClients/oauthClients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ describe("List OAuth Clients", () => {
},
description:
"Retrieves a list of all OAuth clients, including their associated roles and divisions. This tool is useful for auditing and managing OAuth clients in the Genesys Cloud organization.",
execution: { taskSupport: "forbidden" },
inputSchema: {
properties: {},
type: "object",
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
},
});
Expand Down
22 changes: 15 additions & 7 deletions src/tools/queryQueueVolumes/queryQueueVolumes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { McpError } from "@modelcontextprotocol/sdk/types.js";
import type {
CallToolResult,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";

import type { MockedObjectDeep } from "@vitest/spy";
import { beforeEach, describe, expect, test, vi } from "vitest";
import {
Expand Down Expand Up @@ -54,6 +58,7 @@ describe("Query Queue Volumes Tool", () => {
annotations: { title: "Query Queue Volumes" },
description:
"Returns a breakdown of how many conversations occurred in each specified queue between two dates. Useful for comparing workload across queues. MAX 300 queue IDs.",
execution: { taskSupport: "forbidden" },
inputSchema: {
type: "object",
properties: {
Expand All @@ -62,6 +67,8 @@ describe("Query Queue Volumes Tool", () => {
items: {
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)$",
description:
"A UUID for a queue. (e.g., 00000000-0000-0000-0000-000000000000)",
},
Expand All @@ -81,7 +88,6 @@ describe("Query Queue Volumes Tool", () => {
},
},
required: ["queueIds", "startDate", "endDate"],
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
},
});
Expand All @@ -97,11 +103,13 @@ describe("Query Queue Volumes Tool", () => {
endDate: "2024-01-02T00:00:00Z",
},
}),
).rejects.toSatisfy(
(error: McpError) =>
error.name === "McpError" &&
error.message.includes("queueId") &&
error.message.includes("Array must contain at least 1 element(s)"),
).resolves.toSatisfy(
(result: CallToolResult) =>
result.isError === true &&
(result.content[0] as TextContent).text.includes("queueId") &&
(result.content[0] as TextContent).text.includes(
"Too small: expected array to have >=1 items",
),
);
});

Expand Down
Loading
Loading