diff --git a/README.md b/README.md index 870eea2..fe935e4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ A Model Context Protocol (MCP) server for Genesys Cloud's Platform API. ## Usage with Claude Desktop +### MCP Bundle + +This MCP Server provides an [MCP Bundle](https://github.com/anthropics/mcpb) (.mcpb file) along with each [release](https://github.com/MakingChatbots/genesys-cloud-mcp-server/releases), +which is a single-click installable package for Claude Desktop. To use it: + +1. Download the `.mcpb` file from the [latest release](https://github.com/MakingChatbots/genesys-cloud-mcp-server/releases) +2. In Claude Desktop navigate to Settings > Extensions. +3. Open the .mcpb file with Claude +4. Configure the Region and OAuth Client for the extension + +The extension will now be available in your conversations. + ### NPX Add this to your `claude_desktop_config.json`: @@ -43,19 +55,6 @@ Add this to your `claude_desktop_config.json`: } ``` -### MCP Bundle - -This MCP Server provides an [MCP Bundle](https://github.com/anthropics/mcpb) (.mcpb file) along with each [release](https://github.com/MakingChatbots/genesys-cloud-mcp-server/releases), -which is a single-click installable package for Claude Desktop. To use it: - -1. Download the `.mcpb` file from the [latest release](https://github.com/MakingChatbots/genesys-cloud-mcp-server/releases) -2. In Claude Desktop navigate to Settings > Extensions. -3. Browse to, or drag in the .mcpb file downloaded -4. Click "Install" -5. Configure the Region and OAuth Client for the extension - -The extension will now be available in your conversations. - ## Usage with Gemini CLI Add below to your `.gemini/settings.json` file. You can read more about the [setup from the official guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/tutorials.md#configure-the-mcp-server-in-settingsjson). diff --git a/manifest.json b/manifest.json index bcc75d6..6c4c583 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": "0.2", "name": "Genesys Cloud MCP Server", - "version": "0.0.16", + "version": "1.0.2", "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": { @@ -55,6 +55,14 @@ { "name": "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)." + }, + { + "name": "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." + }, + { + "name": "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 requests per organization." } ], "user_config": { diff --git a/package-lock.json b/package-lock.json index e1289fe..35148d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@makingchatbots/genesys-cloud-mcp-server", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makingchatbots/genesys-cloud-mcp-server", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.18.2", "date-fns": "^4.1.0", + "lru-cache": "^11.2.4", "purecloud-platform-client-v2": "^239.0.0", "zod": "^3.23.8" }, @@ -3044,6 +3045,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", diff --git a/package.json b/package.json index 8197cc2..f2a41cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makingchatbots/genesys-cloud-mcp-server", - "version": "1.0.1", + "version": "1.0.2", "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", @@ -39,19 +39,20 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.18.2", "date-fns": "^4.1.0", + "lru-cache": "^11.2.4", "purecloud-platform-client-v2": "^239.0.0", "zod": "^3.23.8" }, "devDependencies": { - "dotenv": "^17.2.2", "@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", - "@biomejs/biome": "2.2.4" + "vitest": "^3.2.4" }, "lint-staged": { "src/*.ts": [ diff --git a/src/index.ts b/src/index.ts index 49c937a..b7f4078 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { LRUCache } from "lru-cache"; import platformClient from "purecloud-platform-client-v2"; import { OAuthClientCredentialsWrapper } from "./auth/OAuthClientCredentialsWrapper.js"; import { createConfigRetriever } from "./createConfigRetriever.js"; @@ -7,7 +8,10 @@ import { conversationSentiment } from "./tools/conversationSentiment/conversatio import { conversationTopics } from "./tools/conversationTopics/conversationTopics.js"; import { conversationTranscription } from "./tools/conversationTranscription/conversationTranscription.js"; import { oauthClients } from "./tools/oauthClients/oauthClients.js"; -import { oauthClientUsage } from "./tools/oauthClientUsage/oauthClientUsage.js"; +import { + type OAuthClientUsageResponse, + oauthClientUsage, +} from "./tools/oauthClientUsage/oauthClientUsage.js"; import { queryQueueVolumes } from "./tools/queryQueueVolumes/queryQueueVolumes.js"; import { sampleConversationsByQueue } from "./tools/sampleConversationsByQueue/sampleConversationsByQueue.js"; import { searchQueues } from "./tools/searchQueues.js"; @@ -21,7 +25,17 @@ const withAuth = OAuthClientCredentialsWrapper( const server: McpServer = new McpServer({ name: "Genesys Cloud", - version: "1.0.1", // Same version as version in package.json + version: "1.0.2", // Same version as version in package.json +}); + +const cache = new LRUCache({ + max: 500, + ttl: 1000 * 60 * 5, // 5 minutes + + allowStale: false, + + updateAgeOnGet: false, + updateAgeOnHas: false, }); const routingApi = new platformClient.RoutingApi(); @@ -147,6 +161,7 @@ server.registerTool( ); const oauthClientUsageTool = oauthClientUsage({ + cache, oauthApi, }); server.registerTool( diff --git a/src/tools/oauthClientUsage/oauthClientUsage.ts b/src/tools/oauthClientUsage/oauthClientUsage.ts index a7646e8..8e9ffcd 100644 --- a/src/tools/oauthClientUsage/oauthClientUsage.ts +++ b/src/tools/oauthClientUsage/oauthClientUsage.ts @@ -1,3 +1,4 @@ +import type { LRUCache } from "lru-cache"; import type { Models, OAuthApi } from "purecloud-platform-client-v2"; import { z } from "zod"; import { createTool, type ToolFactory } from "../utils/createTool.js"; @@ -6,12 +7,24 @@ import { isUnauthorisedError } from "../utils/genesys/isUnauthorisedError.js"; import { waitFor } from "../utils/waitFor.js"; const MAX_ATTEMPTS = 10; +const TOOL_CACHE_KEY = "oauthClientUsage"; + +export interface OAuthClientUsageResponse { + startDate: string; + endDate: string; + totalRequest: number; + requestsPerOrganisation: { + organisationId?: string; + requests?: number; + }[]; +} export interface ToolDependencies { readonly oauthApi: Pick< OAuthApi, "postOauthClientUsageQuery" | "getOauthClientUsageQueryResult" >; + readonly cache?: LRUCache; } const paramsSchema = z.object({ @@ -36,7 +49,7 @@ const paramsSchema = z.object({ export const oauthClientUsage: ToolFactory< ToolDependencies, typeof paramsSchema -> = ({ oauthApi }) => +> = ({ oauthApi, cache }) => createTool({ schema: { name: "oauth_client_usage", @@ -59,6 +72,20 @@ export const oauthClientUsage: ToolFactory< to.setTime(now.getTime()); } + const cacheName = `${TOOL_CACHE_KEY}.${oauthClientId}-${from.getTime()}-${to.getTime()}`; + + const cachedResult = cache?.get(cacheName); + if (cachedResult) { + return { + content: [ + { + type: "text", + text: JSON.stringify(cachedResult), + }, + ], + }; + } + let result: Models.UsageExecutionResult; try { result = await oauthApi.postOauthClientUsageQuery(oauthClientId, { @@ -66,7 +93,6 @@ export const oauthClientUsage: ToolFactory< metrics: ["Requests"], groupBy: ["OrganizationId"], }); - console.log(result); } catch (error: unknown) { const errorMessage = isUnauthorisedError(error) ? "Failed to retrieve usage of OAuth client: Unauthorised access. Please check API credentials or permissions" @@ -118,24 +144,31 @@ export const oauthClientUsage: ToolFactory< ); } + const toolResult: OAuthClientUsageResponse = { + startDate, + endDate, + totalRequest: (apiUsageQueryResult?.results ?? []).reduce( + (acc, curr) => acc + (curr.requests ?? 0), + 0, + ), + requestsPerOrganisation: (apiUsageQueryResult?.results ?? []).map( + (result) => ({ + organisationId: result.organizationId, + requests: result.requests, + }), + ), + }; + + cache?.set( + `${TOOL_CACHE_KEY}.${oauthClientId}-${from.getTime()}-${to.getTime()}`, + toolResult, + ); + return { content: [ { type: "text", - text: JSON.stringify({ - startDate, - endDate, - totalRequest: (apiUsageQueryResult?.results ?? []).reduce( - (acc, curr) => acc + (curr.requests ?? 0), - 0, - ), - requestsPerOrganisation: (apiUsageQueryResult?.results ?? []).map( - (result) => ({ - organisationId: result.organizationId, - requests: result.requests, - }), - ), - }), + text: JSON.stringify(toolResult), }, ], }; diff --git a/tests/integration/serverRuns.test.ts b/tests/integration/serverRuns.test.ts index c59a6c0..5c85b1d 100644 --- a/tests/integration/serverRuns.test.ts +++ b/tests/integration/serverRuns.test.ts @@ -81,7 +81,7 @@ describe("Server Runs", () => { client = new Client({ name: "test-client", - version: "1.0.1", + version: "1.0.2", }); await client.connect(transport);