@indemn/cli 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +9 -4
- package/.github/workflows/ci.yml +10 -0
- package/.github/workflows/publish.yml +2 -2
- package/.mcp.json +1 -4
- package/CONTRIBUTING.md +3 -1
- package/README.md +69 -176
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +226 -18
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/analytics.d.ts +3 -0
- package/dist/cli/analytics.d.ts.map +1 -0
- package/dist/cli/analytics.js +133 -0
- package/dist/cli/analytics.js.map +1 -0
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +62 -17
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/billing.d.ts +3 -0
- package/dist/cli/billing.d.ts.map +1 -0
- package/dist/cli/billing.js +23 -0
- package/dist/cli/billing.js.map +1 -0
- package/dist/cli/campaigns.d.ts +3 -0
- package/dist/cli/campaigns.d.ts.map +1 -0
- package/dist/cli/campaigns.js +34 -0
- package/dist/cli/campaigns.js.map +1 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +69 -102
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +71 -27
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/conversations.d.ts +3 -0
- package/dist/cli/conversations.d.ts.map +1 -0
- package/dist/cli/conversations.js +129 -0
- package/dist/cli/conversations.js.map +1 -0
- package/dist/cli/distribution.d.ts +3 -0
- package/dist/cli/distribution.d.ts.map +1 -0
- package/dist/cli/distribution.js +122 -0
- package/dist/cli/distribution.js.map +1 -0
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/eval.js +315 -71
- package/dist/cli/eval.js.map +1 -1
- package/dist/cli/functions.d.ts.map +1 -1
- package/dist/cli/functions.js +125 -35
- package/dist/cli/functions.js.map +1 -1
- package/dist/cli/index.js +63 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +27 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +152 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/kb.d.ts.map +1 -1
- package/dist/cli/kb.js +141 -15
- package/dist/cli/kb.js.map +1 -1
- package/dist/cli/link.d.ts +21 -0
- package/dist/cli/link.d.ts.map +1 -0
- package/dist/cli/link.js +112 -0
- package/dist/cli/link.js.map +1 -0
- package/dist/cli/org.d.ts.map +1 -1
- package/dist/cli/org.js +79 -67
- package/dist/cli/org.js.map +1 -1
- package/dist/cli/platform.d.ts +3 -0
- package/dist/cli/platform.d.ts.map +1 -0
- package/dist/cli/platform.js +133 -0
- package/dist/cli/platform.js.map +1 -0
- package/dist/cli/tickets.d.ts +3 -0
- package/dist/cli/tickets.d.ts.map +1 -0
- package/dist/cli/tickets.js +125 -0
- package/dist/cli/tickets.js.map +1 -0
- package/dist/cli/utils/output.d.ts +15 -9
- package/dist/cli/utils/output.d.ts.map +1 -1
- package/dist/cli/utils/output.js +53 -46
- package/dist/cli/utils/output.js.map +1 -1
- package/dist/cli/voice.d.ts +3 -0
- package/dist/cli/voice.d.ts.map +1 -0
- package/dist/cli/voice.js +206 -0
- package/dist/cli/voice.js.map +1 -0
- package/dist/core/diff.d.ts +16 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +57 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/org-resolver.d.ts +37 -0
- package/dist/core/org-resolver.d.ts.map +1 -0
- package/dist/core/org-resolver.js +73 -0
- package/dist/core/org-resolver.js.map +1 -0
- package/dist/core/runtime.d.ts +36 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +62 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/scope.d.ts +33 -0
- package/dist/core/scope.d.ts.map +1 -0
- package/dist/core/scope.js +37 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/mcp/registry.d.ts +67 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +195 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +87 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts +21 -0
- package/dist/mcp/tool-meta.d.ts.map +1 -0
- package/dist/mcp/tool-meta.js +203 -0
- package/dist/mcp/tool-meta.js.map +1 -0
- package/dist/mcp/tools.d.ts +2 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1462 -215
- package/dist/mcp/tools.js.map +1 -1
- package/dist/sdk/agent-bundle.d.ts +99 -0
- package/dist/sdk/agent-bundle.d.ts.map +1 -0
- package/dist/sdk/agent-bundle.js +211 -0
- package/dist/sdk/agent-bundle.js.map +1 -0
- package/dist/sdk/agents.d.ts +54 -1
- package/dist/sdk/agents.d.ts.map +1 -1
- package/dist/sdk/agents.js +59 -5
- package/dist/sdk/agents.js.map +1 -1
- package/dist/sdk/analytics.d.ts +158 -0
- package/dist/sdk/analytics.d.ts.map +1 -0
- package/dist/sdk/analytics.js +185 -0
- package/dist/sdk/analytics.js.map +1 -0
- package/dist/sdk/auth.d.ts +34 -12
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +60 -77
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/billing.d.ts +25 -0
- package/dist/sdk/billing.d.ts.map +1 -0
- package/dist/sdk/billing.js +61 -0
- package/dist/sdk/billing.js.map +1 -0
- package/dist/sdk/campaigns.d.ts +35 -0
- package/dist/sdk/campaigns.d.ts.map +1 -0
- package/dist/sdk/campaigns.js +59 -0
- package/dist/sdk/campaigns.js.map +1 -0
- package/dist/sdk/chat.d.ts +25 -2
- package/dist/sdk/chat.d.ts.map +1 -1
- package/dist/sdk/chat.js +37 -10
- package/dist/sdk/chat.js.map +1 -1
- package/dist/sdk/client.d.ts +93 -5
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +362 -100
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.d.ts +74 -0
- package/dist/sdk/config-change.d.ts.map +1 -0
- package/dist/sdk/config-change.js +117 -0
- package/dist/sdk/config-change.js.map +1 -0
- package/dist/sdk/config.d.ts.map +1 -1
- package/dist/sdk/config.js +2 -1
- package/dist/sdk/config.js.map +1 -1
- package/dist/sdk/conversations.d.ts +101 -0
- package/dist/sdk/conversations.d.ts.map +1 -0
- package/dist/sdk/conversations.js +155 -0
- package/dist/sdk/conversations.js.map +1 -0
- package/dist/sdk/credentials.d.ts +25 -0
- package/dist/sdk/credentials.d.ts.map +1 -0
- package/dist/sdk/credentials.js +44 -0
- package/dist/sdk/credentials.js.map +1 -0
- package/dist/sdk/distribution.d.ts +94 -0
- package/dist/sdk/distribution.d.ts.map +1 -0
- package/dist/sdk/distribution.js +100 -0
- package/dist/sdk/distribution.js.map +1 -0
- package/dist/sdk/eval.d.ts +81 -0
- package/dist/sdk/eval.d.ts.map +1 -1
- package/dist/sdk/eval.js +117 -0
- package/dist/sdk/eval.js.map +1 -1
- package/dist/sdk/functions.d.ts +14 -1
- package/dist/sdk/functions.d.ts.map +1 -1
- package/dist/sdk/functions.js +48 -5
- package/dist/sdk/functions.js.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/jarvis-chat.d.ts +28 -0
- package/dist/sdk/jarvis-chat.d.ts.map +1 -0
- package/dist/sdk/jarvis-chat.js +117 -0
- package/dist/sdk/jarvis-chat.js.map +1 -0
- package/dist/sdk/kb.d.ts +69 -1
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +111 -24
- package/dist/sdk/kb.js.map +1 -1
- package/dist/sdk/orgs.d.ts +13 -0
- package/dist/sdk/orgs.d.ts.map +1 -0
- package/dist/sdk/orgs.js +59 -0
- package/dist/sdk/orgs.js.map +1 -0
- package/dist/sdk/platform.d.ts +62 -0
- package/dist/sdk/platform.d.ts.map +1 -0
- package/dist/sdk/platform.js +95 -0
- package/dist/sdk/platform.js.map +1 -0
- package/dist/sdk/session.d.ts +25 -0
- package/dist/sdk/session.d.ts.map +1 -0
- package/dist/sdk/session.js +69 -0
- package/dist/sdk/session.js.map +1 -0
- package/dist/sdk/skills.d.ts +46 -0
- package/dist/sdk/skills.d.ts.map +1 -0
- package/dist/sdk/skills.js +203 -0
- package/dist/sdk/skills.js.map +1 -0
- package/dist/sdk/test-chat.d.ts +52 -0
- package/dist/sdk/test-chat.d.ts.map +1 -0
- package/dist/sdk/test-chat.js +134 -0
- package/dist/sdk/test-chat.js.map +1 -0
- package/dist/sdk/tickets.d.ts +98 -0
- package/dist/sdk/tickets.d.ts.map +1 -0
- package/dist/sdk/tickets.js +91 -0
- package/dist/sdk/tickets.js.map +1 -0
- package/dist/sdk/types.d.ts +91 -6
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/voice.d.ts +147 -0
- package/dist/sdk/voice.d.ts.map +1 -0
- package/dist/sdk/voice.js +236 -0
- package/dist/sdk/voice.js.map +1 -0
- package/package.json +22 -3
- package/scripts/check-skills.mjs +25 -0
- package/skills/agent-builder/SKILL.md +89 -252
- package/skills/evaluations/SKILL.md +23 -234
- package/skills/evaluations/references/analysis.md +95 -0
- package/skills/evaluations/references/cli-reference.md +28 -0
- package/skills/evaluations/references/json-formats.md +100 -0
- package/src/cli/agents.ts +202 -22
- package/src/cli/analytics.ts +138 -0
- package/src/cli/auth.ts +56 -18
- package/src/cli/billing.ts +25 -0
- package/src/cli/campaigns.ts +33 -0
- package/src/cli/chat.ts +66 -115
- package/src/cli/config.ts +65 -31
- package/src/cli/conversations.ts +133 -0
- package/src/cli/distribution.ts +116 -0
- package/src/cli/eval.ts +254 -70
- package/src/cli/functions.ts +122 -36
- package/src/cli/index.ts +58 -55
- package/src/cli/init.ts +154 -0
- package/src/cli/kb.ts +120 -17
- package/src/cli/link.ts +117 -0
- package/src/cli/org.ts +81 -75
- package/src/cli/platform.ts +128 -0
- package/src/cli/tickets.ts +130 -0
- package/src/cli/utils/output.ts +51 -42
- package/src/cli/voice.ts +204 -0
- package/src/core/diff.ts +52 -0
- package/src/core/org-resolver.ts +113 -0
- package/src/core/runtime.ts +73 -0
- package/src/core/scope.ts +64 -0
- package/src/mcp/registry.ts +236 -0
- package/src/mcp/server.ts +84 -2
- package/src/mcp/tool-meta.ts +241 -0
- package/src/mcp/tools.ts +1529 -265
- package/src/sdk/agent-bundle.ts +245 -0
- package/src/sdk/agents.ts +74 -5
- package/src/sdk/analytics.ts +220 -0
- package/src/sdk/auth.ts +87 -107
- package/src/sdk/billing.ts +59 -0
- package/src/sdk/campaigns.ts +60 -0
- package/src/sdk/chat.ts +44 -10
- package/src/sdk/client.ts +373 -102
- package/src/sdk/config-change.ts +148 -0
- package/src/sdk/config.ts +2 -1
- package/src/sdk/conversations.ts +199 -0
- package/src/sdk/credentials.ts +53 -0
- package/src/sdk/distribution.ts +132 -0
- package/src/sdk/eval.ts +140 -0
- package/src/sdk/functions.ts +52 -6
- package/src/sdk/index.ts +2 -0
- package/src/sdk/jarvis-chat.ts +112 -0
- package/src/sdk/kb.ts +116 -29
- package/src/sdk/orgs.ts +62 -0
- package/src/sdk/platform.ts +115 -0
- package/src/sdk/session.ts +90 -0
- package/src/sdk/skills.ts +188 -0
- package/src/sdk/test-chat.ts +151 -0
- package/src/sdk/tickets.ts +120 -0
- package/src/sdk/types.ts +88 -6
- package/src/sdk/voice.ts +262 -0
- package/test/cli/entry.test.ts +34 -0
- package/test/cli/init.test.ts +66 -0
- package/test/cli/link.test.ts +57 -0
- package/test/core/org-resolver.test.ts +92 -0
- package/test/core/runtime.test.ts +30 -0
- package/test/core/scope.test.ts +93 -0
- package/test/harness.test.ts +24 -0
- package/test/helpers/config-home.ts +22 -0
- package/test/helpers/fake-middleware.ts +56 -0
- package/test/helpers/fetch-recorder.ts +75 -0
- package/test/helpers/jwt.ts +9 -0
- package/test/helpers/mcp-pair.ts +38 -0
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
- package/test/mcp/askers.test.ts +60 -0
- package/test/mcp/elicit-timeout.test.ts +29 -0
- package/test/mcp/registry.test.ts +169 -0
- package/test/mcp/shaping.test.ts +41 -0
- package/test/mcp/tools-snapshot.test.ts +42 -0
- package/test/sdk/agents.test.ts +232 -0
- package/test/sdk/analytics.test.ts +89 -0
- package/test/sdk/auth.test.ts +70 -0
- package/test/sdk/billing.test.ts +47 -0
- package/test/sdk/campaigns.test.ts +43 -0
- package/test/sdk/client.test.ts +80 -0
- package/test/sdk/config-change.test.ts +142 -0
- package/test/sdk/conversations.test.ts +80 -0
- package/test/sdk/credentials.test.ts +33 -0
- package/test/sdk/distribution.test.ts +55 -0
- package/test/sdk/eval.test.ts +155 -0
- package/test/sdk/kb-upload.test.ts +50 -0
- package/test/sdk/kb.test.ts +95 -0
- package/test/sdk/orgs.test.ts +48 -0
- package/test/sdk/platform.test.ts +89 -0
- package/test/sdk/renewal.test.ts +115 -0
- package/test/sdk/session.test.ts +60 -0
- package/test/sdk/skills.test.ts +235 -0
- package/test/sdk/test-chat.test.ts +112 -0
- package/test/sdk/tickets.test.ts +42 -0
- package/test/sdk/voice.test.ts +147 -0
- package/vitest.config.ts +11 -0
package/src/mcp/tools.ts
CHANGED
|
@@ -1,15 +1,58 @@
|
|
|
1
1
|
import { z } from 'zod/v3';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import type { IndemnClient } from '../sdk/client.js';
|
|
4
|
-
import { AgentsSDK } from '../sdk/agents.js';
|
|
4
|
+
import { AgentInsightsSDK, AgentsSDK } from '../sdk/agents.js';
|
|
5
|
+
import {
|
|
6
|
+
createAgentFromSkeleton, exportAgent, importAgent, missingSkeletonFields, parseModel, type AgentBundle, type AgentSkeleton,
|
|
7
|
+
} from '../sdk/agent-bundle.js';
|
|
8
|
+
import { getAskers } from '../core/scope.js';
|
|
9
|
+
import { proposedSkills, skillConfiguration } from '../sdk/skills.js';
|
|
10
|
+
import { VoiceSDK } from '../sdk/voice.js';
|
|
11
|
+
import { DistributionSDK } from '../sdk/distribution.js';
|
|
12
|
+
import { AnalyticsSDK, AuditSDK, OrgSettingsSDK } from '../sdk/analytics.js';
|
|
13
|
+
import { TicketsSDK } from '../sdk/tickets.js';
|
|
14
|
+
import { BillingSDK } from '../sdk/billing.js';
|
|
15
|
+
import { CampaignsSDK } from '../sdk/campaigns.js';
|
|
16
|
+
import { PlatformSDK } from '../sdk/platform.js';
|
|
17
|
+
import { jarvisChat } from '../sdk/jarvis-chat.js';
|
|
18
|
+
import { applyConfigChange, describePlan, liveText, mayBeLive, modelArg, planConfigChange, type ConfigChange } from '../sdk/config-change.js';
|
|
5
19
|
import { ConfigSDK } from '../sdk/config.js';
|
|
6
20
|
import { FunctionsSDK } from '../sdk/functions.js';
|
|
7
21
|
import { KBSDK } from '../sdk/kb.js';
|
|
8
22
|
import { RubricsSDK } from '../sdk/rubrics.js';
|
|
9
23
|
import { TestSetsSDK } from '../sdk/test-sets.js';
|
|
10
|
-
import { EvalSDK } from '../sdk/eval.js';
|
|
11
|
-
import {
|
|
24
|
+
import { EvalSDK, gateVerdict } from '../sdk/eval.js';
|
|
25
|
+
import type { EvaluationTriggerInput } from '../sdk/types.js';
|
|
26
|
+
import { exchange, openTestChat } from '../sdk/test-chat.js';
|
|
27
|
+
import { ConversationsSDK, conversationToTestSet, conversationUrl, isCliConversation, isTestConversation, projectOfAgent } from '../sdk/conversations.js';
|
|
12
28
|
import { exportEvalMarkdown, exportEvalReport, exportAgentCard } from '../report/export.js';
|
|
29
|
+
import { createRegistry, type Registry } from './registry.js';
|
|
30
|
+
import { OrgsSDK } from '../sdk/orgs.js';
|
|
31
|
+
import { whoami } from '../sdk/auth.js';
|
|
32
|
+
import { NeedsInputError, SessionExpiredError } from '../core/runtime.js';
|
|
33
|
+
|
|
34
|
+
// A REST skill's authentication as flat fields: for a nested object Gemini writes calls Google rejects.
|
|
35
|
+
const skillAuthFields = {
|
|
36
|
+
auth_type: z.enum(['no-auth', 'api-key', 'basic-auth', 'bearer-token']).optional().describe('rest_api: how the API is called (default no-auth)'),
|
|
37
|
+
auth_token: z.string().optional().describe('bearer-token: the token'),
|
|
38
|
+
auth_key: z.string().optional().describe('api-key: the header name, e.g. x-api-key'),
|
|
39
|
+
auth_value: z.string().optional().describe('api-key: the key'),
|
|
40
|
+
auth_username: z.string().optional().describe('basic-auth: user name'),
|
|
41
|
+
auth_password: z.string().optional().describe('basic-auth: password'),
|
|
42
|
+
};
|
|
43
|
+
type AuthFields = { auth_type?: string; auth_token?: string; auth_key?: string; auth_value?: string; auth_username?: string; auth_password?: string };
|
|
44
|
+
|
|
45
|
+
// Helper: the flat auth fields as the dashboard's skill form saves them (configurationSchema.authorization)
|
|
46
|
+
function withAuthorization<T extends AuthFields & { configuration?: Record<string, unknown> }>(args: T): T {
|
|
47
|
+
if (!args.auth_type) return args;
|
|
48
|
+
const fields: Record<string, string[]> = { 'api-key': ['key', 'value'], 'basic-auth': ['username', 'password'], 'bearer-token': ['token'], 'no-auth': [] };
|
|
49
|
+
const authorization: Record<string, string> = { type: args.auth_type };
|
|
50
|
+
for (const k of fields[args.auth_type] ?? []) {
|
|
51
|
+
const v = (args as Record<string, unknown>)[`auth_${k}`];
|
|
52
|
+
if (typeof v === 'string' && v) authorization[k] = v;
|
|
53
|
+
}
|
|
54
|
+
return { ...args, configuration: { ...(args.configuration ?? {}), authorization } };
|
|
55
|
+
}
|
|
13
56
|
|
|
14
57
|
// Helper: format a successful MCP tool result
|
|
15
58
|
function success(data: unknown) {
|
|
@@ -18,66 +61,295 @@ function success(data: unknown) {
|
|
|
18
61
|
|
|
19
62
|
// Helper: format an error MCP tool result
|
|
20
63
|
function error(err: unknown) {
|
|
64
|
+
if (err instanceof NeedsInputError) {
|
|
65
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ needs_input: true, message: err.message, choices: err.choices, to_proceed: 'Ask the user which organization to use, then call again with org_id set to its id.' }, null, 2) }] };
|
|
66
|
+
}
|
|
21
67
|
const message = err instanceof Error ? err.message : String(err);
|
|
22
68
|
return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true };
|
|
23
69
|
}
|
|
24
70
|
|
|
25
|
-
|
|
71
|
+
// Helper: the org's pages in the dashboard, so the person gets a link instead of an id
|
|
72
|
+
async function orgPages(client: IndemnClient): Promise<string> {
|
|
73
|
+
const base = client.getHosts().copilot.replace(/\/api\/?$/, '');
|
|
74
|
+
return `${base}/#/organization/${await client.currentOrgId()}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Test items and rubric rules as the evaluations service defines them (indemn_evals models).
|
|
78
|
+
// Typed fields, not open records: models such as Gemini cannot fill an object with no
|
|
79
|
+
// declared fields. Other fields the service accepts still pass through.
|
|
80
|
+
// Test items and rubric rules with flat fields (a nested object makes Gemini write calls Google rejects);
|
|
81
|
+
// mapped to the evaluations service's shape (inputs / expected, evaluation_criteria) before saving.
|
|
82
|
+
const testItemSchema = z.object({
|
|
83
|
+
type: z.enum(['single_turn', 'scenario', 'voice_simulation', 'transcript']).describe('single_turn: one message; scenario: a simulated chat; voice_simulation: a simulated phone call'),
|
|
84
|
+
name: z.string().describe('Short name of what is tested'),
|
|
85
|
+
message: z.string().optional().describe('single_turn: the message sent to the agent'),
|
|
86
|
+
persona: z.string().optional().describe('scenario / voice_simulation: who the simulated customer is'),
|
|
87
|
+
initial_message: z.string().optional().describe('scenario / voice_simulation: their first message'),
|
|
88
|
+
goal: z.string().optional().describe('voice_simulation: what the caller wants'),
|
|
89
|
+
max_turns: z.number().int().optional().describe('scenario / voice_simulation: turns before stopping (default 5)'),
|
|
90
|
+
success_criteria: z.array(z.string()).optional().describe('Plain-language checks that must all pass (needed)'),
|
|
91
|
+
expected_outcome: z.string().optional(),
|
|
92
|
+
tool_present: z.array(z.string()).optional().describe('Skills that must be used'),
|
|
93
|
+
tool_absent: z.array(z.string()).optional().describe('Skills that must not be used'),
|
|
94
|
+
tags: z.array(z.string()).optional(),
|
|
95
|
+
priority: z.string().optional(),
|
|
96
|
+
}).passthrough();
|
|
97
|
+
|
|
98
|
+
const rubricRuleSchema = z.object({
|
|
99
|
+
id: z.string().describe('Short rule id, e.g. STAYS_ON_TOPIC'),
|
|
100
|
+
name: z.string(),
|
|
101
|
+
severity: z.string().describe('high, medium or low'),
|
|
102
|
+
category: z.string().describe('e.g. response_quality, accuracy, safety'),
|
|
103
|
+
description: z.string(),
|
|
104
|
+
pass_conditions: z.array(z.string()).optional().describe('What passing looks like'),
|
|
105
|
+
fail_conditions: z.array(z.string()).optional().describe('What failing looks like'),
|
|
106
|
+
}).passthrough();
|
|
107
|
+
|
|
108
|
+
const defined = (o: Record<string, unknown>) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
109
|
+
|
|
110
|
+
// Helper: a flat test item in the service's shape; inputs / expected given as objects (other clients) are kept.
|
|
111
|
+
function toTestItem(item: Record<string, unknown>): Record<string, unknown> {
|
|
112
|
+
const { message, persona, initial_message, goal, max_turns, success_criteria, expected_outcome, tool_present, tool_absent, inputs, expected, ...rest } = item;
|
|
113
|
+
return {
|
|
114
|
+
...rest,
|
|
115
|
+
inputs: { ...((inputs as Record<string, unknown>) ?? {}), ...defined({ message, persona, initial_message, goal, max_turns }) },
|
|
116
|
+
expected: { ...((expected as Record<string, unknown>) ?? {}), ...defined({ success_criteria, expected_outcome, tool_present, tool_absent }) },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Helper: a flat rubric rule in the service's shape (evaluation_criteria).
|
|
121
|
+
function toRubricRule(rule: Record<string, unknown>): Record<string, unknown> {
|
|
122
|
+
const { pass_conditions, fail_conditions, evaluation_criteria, ...rest } = rule;
|
|
123
|
+
const criteria = { ...((evaluation_criteria as Record<string, unknown>) ?? {}), ...defined({ pass_conditions, fail_conditions }) };
|
|
124
|
+
return Object.keys(criteria).length ? { ...rest, evaluation_criteria: criteria } : rest;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Objects whose fields a model cannot see (Gemini) arrive as JSON text: accept both.
|
|
128
|
+
const parseJsonText = (v: unknown) => {
|
|
129
|
+
if (typeof v !== 'string') return v;
|
|
130
|
+
try { return JSON.parse(v); } catch { return v; }
|
|
131
|
+
};
|
|
132
|
+
const jsonObject = () => z.preprocess(parseJsonText, z.record(z.any()));
|
|
133
|
+
|
|
134
|
+
// A skill for bulk_create_skills with flat fields (the configuration is built as create_function builds it);
|
|
135
|
+
// a proposal from generate_skills_from_doc keeps its own configurationSchema.
|
|
136
|
+
const skillDefinition = z.preprocess(parseJsonText, z.object({
|
|
137
|
+
name: z.string(),
|
|
138
|
+
description: z.string().optional(),
|
|
139
|
+
type: z.string().describe('e.g. rest_api, transfer_call'),
|
|
140
|
+
url: z.string().optional().describe('rest_api: endpoint URL; {{parameter}} placeholders allowed'),
|
|
141
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional().describe('rest_api'),
|
|
142
|
+
payload: z.string().optional().describe('rest_api: request body as JSON text with {{parameter}} placeholders'),
|
|
143
|
+
...skillAuthFields,
|
|
144
|
+
response_field: z.string().optional().describe('rest_api: dot-path of the answer to use'),
|
|
145
|
+
output_instructions: z.string().optional().describe('What the agent says with the answer'),
|
|
146
|
+
transfer_to: z.string().optional().describe('transfer_call: phone number (E.164) or SIP address'),
|
|
147
|
+
parameters: z.array(z.object({
|
|
148
|
+
name: z.string(), type: z.string().describe('string, number, boolean or list'),
|
|
149
|
+
description: z.string().optional(), required: z.boolean().optional(),
|
|
150
|
+
}).passthrough()).optional(),
|
|
151
|
+
}).passthrough());
|
|
152
|
+
|
|
153
|
+
export function registerTools(server: McpServer, client: IndemnClient): Registry {
|
|
154
|
+
const reg = createRegistry(server, {
|
|
155
|
+
context: () => client.getResultContext(),
|
|
156
|
+
// Each call decides its own org; an answer given once is not reused for the next call.
|
|
157
|
+
resolveOrg: (explicit) => client.resolveOrg(explicit, { cache: false }),
|
|
158
|
+
// The person approves a change by names: look up the ids the call names.
|
|
159
|
+
// An id that names nothing gets no card: the registry answers "not found" instead.
|
|
160
|
+
displayNames: async (args, orgId) => {
|
|
161
|
+
const names: Record<string, string> = {};
|
|
162
|
+
const missing: string[] = [];
|
|
163
|
+
const notFound = /not found|is not on agent|404/i;
|
|
164
|
+
const add = (id: unknown, what: string | null, lookup: (id: string) => Promise<{ name?: unknown } | undefined>) =>
|
|
165
|
+
typeof id === 'string' && id
|
|
166
|
+
? lookup(id).then(
|
|
167
|
+
(x) => {
|
|
168
|
+
if (typeof x?.name === 'string' && x.name) names[id] = x.name;
|
|
169
|
+
else if (what && !x) missing.push(`${what} ${id}`);
|
|
170
|
+
},
|
|
171
|
+
(err) => { if (what && notFound.test(err instanceof Error ? err.message : String(err))) missing.push(`${what} ${id}`); })
|
|
172
|
+
: undefined;
|
|
173
|
+
await Promise.all([
|
|
174
|
+
add(args.agent_id ?? args.bot_id, 'agent', (id) => agents.getAgent(id)),
|
|
175
|
+
typeof args.agent_id === 'string' ? add(args.function_id, 'skill', (id) => skillOf(String(args.agent_id), id)) : undefined,
|
|
176
|
+
add(args.kb_id, 'knowledge base', (id) => kb.getKB(id)),
|
|
177
|
+
add(args.test_set_id, 'test set', (id) => testSets.getTestSet(id)),
|
|
178
|
+
add(args.rubric_id, 'rubric', (id) => rubrics.getRubric(id)),
|
|
179
|
+
add(orgId, null, (id) => client.copilotGet<{ organization?: { name?: string } }>(`/organization/${id}`).then((r) => r.organization)),
|
|
180
|
+
]);
|
|
181
|
+
return { names, missing };
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const orgs = new OrgsSDK(client);
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Login and organization tools
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
reg.tool(
|
|
191
|
+
'whoami',
|
|
192
|
+
'Show the platform (dev or production), the logged-in user, the session expiry, the orgs you belong to, and whether this folder is linked to one org. Call this first when unsure which org to use.',
|
|
193
|
+
{},
|
|
194
|
+
async () => {
|
|
195
|
+
try {
|
|
196
|
+
const info = whoami();
|
|
197
|
+
if (!info) throw new SessionExpiredError('Not logged in. Run `indemn login` in a terminal.');
|
|
198
|
+
const link = client.getLink();
|
|
199
|
+
return success({ ...info, linked_folder: link ? { org_id: link.org_id, org_name: link.org_name, agents: link.agents, path: link.path } : null });
|
|
200
|
+
} catch (err) { return error(err); }
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
reg.tool(
|
|
205
|
+
'list_organizations',
|
|
206
|
+
'List the organizations you belong to on this platform. Pass one of these ids as org_id to other tools.',
|
|
207
|
+
{},
|
|
208
|
+
async () => {
|
|
209
|
+
try { return success(await orgs.listOrganizations()); } catch (err) { return error(err); }
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
reg.tool(
|
|
214
|
+
'list_org_members',
|
|
215
|
+
'List the owners, admins and agents of an organization, with their roles.',
|
|
216
|
+
{},
|
|
217
|
+
async () => {
|
|
218
|
+
try { return success(await orgs.listMembers()); } catch (err) { return error(err); }
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
reg.tool(
|
|
223
|
+
'list_channels',
|
|
224
|
+
'List the channels an agent can be published on, such as WEB or VOICE.',
|
|
225
|
+
{},
|
|
226
|
+
async () => {
|
|
227
|
+
try { return success(await orgs.listChannels()); } catch (err) { return error(err); }
|
|
228
|
+
},
|
|
229
|
+
);
|
|
26
230
|
const agents = new AgentsSDK(client);
|
|
27
231
|
const config = new ConfigSDK(client);
|
|
28
232
|
const functions = new FunctionsSDK(client);
|
|
233
|
+
// A skill of an agent by its id.
|
|
234
|
+
const skillOf = async (agentId: string, id: string) => {
|
|
235
|
+
const skill = (await functions.listFunctions(agentId)).find((f) => f._id === id);
|
|
236
|
+
if (!skill) throw new Error(`Skill ${id} is not on agent ${agentId}.`);
|
|
237
|
+
return skill;
|
|
238
|
+
};
|
|
29
239
|
const kb = new KBSDK(client);
|
|
30
240
|
const rubrics = new RubricsSDK(client);
|
|
31
241
|
const testSets = new TestSetsSDK(client);
|
|
32
242
|
const evals = new EvalSDK(client);
|
|
243
|
+
// The evaluations service checks nothing about orgs: touch a rubric or test set only when its agent is in this org.
|
|
244
|
+
const rubricInOrg = async (id: string) => {
|
|
245
|
+
const r = await rubrics.getRubric(id);
|
|
246
|
+
if (r.agent_id) await evals.assertAgentInOrg(String(r.agent_id));
|
|
247
|
+
return r;
|
|
248
|
+
};
|
|
249
|
+
const testSetInOrg = async (id: string) => {
|
|
250
|
+
const t = await testSets.getTestSet(id);
|
|
251
|
+
if (t.agent_id) await evals.assertAgentInOrg(String(t.agent_id));
|
|
252
|
+
return t;
|
|
253
|
+
};
|
|
33
254
|
|
|
34
255
|
// ---------------------------------------------------------------------------
|
|
35
256
|
// Agent tools
|
|
36
257
|
// ---------------------------------------------------------------------------
|
|
37
258
|
|
|
38
|
-
|
|
259
|
+
reg.tool(
|
|
39
260
|
'list_agents',
|
|
40
|
-
'List
|
|
41
|
-
{
|
|
261
|
+
'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.',
|
|
262
|
+
{ channel: z.string().optional().describe('Only agents on this channel, e.g. web, voice, email') },
|
|
42
263
|
async (args) => {
|
|
43
264
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
265
|
+
const pages = await orgPages(client);
|
|
266
|
+
const wanted = args.channel?.trim().toUpperCase();
|
|
267
|
+
// What a person needs, not the stored record (a page of full records ran to 73k characters).
|
|
268
|
+
const rows = (await agents.listAgents()).map((a) => ({
|
|
269
|
+
_id: a._id,
|
|
270
|
+
name: a.name,
|
|
271
|
+
...(a.description ? { description: a.description } : {}),
|
|
272
|
+
// The list returns channel records, not the names the type declares.
|
|
273
|
+
channels: ((a.channels ?? []) as unknown[]).map((c) => (typeof c === 'string' ? c : (c as { name?: string })?.name ?? '')).filter(Boolean),
|
|
274
|
+
...((a as { connected_phone?: string }).connected_phone ? { phone: (a as { connected_phone?: string }).connected_phone } : {}),
|
|
275
|
+
updated_at: a.updatedAt,
|
|
276
|
+
url: `${pages}/ai-studio/bot/${a._id}/configurations`,
|
|
277
|
+
}));
|
|
278
|
+
return success(wanted ? rows.filter((a) => a.channels.some((c) => c.toUpperCase() === wanted)) : rows);
|
|
47
279
|
} catch (err) { return error(err); }
|
|
48
280
|
},
|
|
49
281
|
);
|
|
50
282
|
|
|
51
|
-
|
|
283
|
+
reg.tool(
|
|
52
284
|
'get_agent',
|
|
53
|
-
'Get
|
|
285
|
+
'Get one agent: name, description, channels and project. For what it does, use agent_summary; for its prompt and model, get_config.',
|
|
54
286
|
{ agent_id: z.string().describe('The agent ID') },
|
|
55
287
|
async (args) => {
|
|
56
288
|
try {
|
|
57
289
|
const result = await agents.getAgent(args.agent_id);
|
|
58
|
-
return success(result);
|
|
290
|
+
return success({ ...result, url: `${await orgPages(client)}/ai-studio/bot/${args.agent_id}/configurations` });
|
|
59
291
|
} catch (err) { return error(err); }
|
|
60
292
|
},
|
|
61
293
|
);
|
|
62
294
|
|
|
63
|
-
|
|
295
|
+
reg.tool(
|
|
64
296
|
'create_agent',
|
|
65
|
-
'Create a
|
|
297
|
+
'Create an agent with its basics: name, channels, what it is for, system prompt and model. Missing basics are asked from the user when the host can ask; otherwise the result lists them (pass minimal: true to create with only a name). Refuses a name that already exists in the org.',
|
|
66
298
|
{
|
|
67
|
-
name: z.string().describe('Agent name'),
|
|
68
|
-
channels: z.array(z.string()).optional().describe('Channels
|
|
299
|
+
name: z.string().optional().describe('Agent name'),
|
|
300
|
+
channels: z.array(z.string()).optional().describe('Channels, e.g. ["WEB"] (see list_channels)'),
|
|
301
|
+
description: z.string().optional().describe('What the agent is for'),
|
|
302
|
+
system_prompt: z.string().optional().describe('System prompt'),
|
|
303
|
+
model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
|
|
304
|
+
allow_duplicate: z.boolean().optional().describe('Create even if an agent with this name exists'),
|
|
305
|
+
minimal: z.boolean().optional().describe('Create with only the fields given, without asking for the rest'),
|
|
69
306
|
},
|
|
70
307
|
async (args) => {
|
|
71
308
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
309
|
+
const skeleton: AgentSkeleton = {
|
|
310
|
+
name: args.name ?? '',
|
|
311
|
+
channels: args.channels,
|
|
312
|
+
description: args.description,
|
|
313
|
+
system_prompt: args.system_prompt,
|
|
314
|
+
model: args.model ? parseModel(args.model) : undefined,
|
|
315
|
+
};
|
|
316
|
+
let missing = missingSkeletonFields(skeleton);
|
|
317
|
+
if (missing.length && !args.minimal) {
|
|
318
|
+
const titles: Record<string, string> = { name: 'Agent name', description: 'What is this agent for?', system_prompt: 'System prompt' };
|
|
319
|
+
const answers = await getAskers().form?.('Fill in the basics for the new agent.', missing.map((key) => ({ key, title: titles[key] ?? key, required: key === 'name' })));
|
|
320
|
+
if (answers) {
|
|
321
|
+
if (answers.name) skeleton.name = answers.name;
|
|
322
|
+
if (answers.description) skeleton.description = answers.description;
|
|
323
|
+
if (answers.system_prompt) skeleton.system_prompt = answers.system_prompt;
|
|
324
|
+
}
|
|
325
|
+
missing = missingSkeletonFields(skeleton);
|
|
326
|
+
if (missing.includes('name') || (missing.length && !answers)) {
|
|
327
|
+
throw new NeedsInputError(
|
|
328
|
+
`Before creating the agent, provide: ${missing.join(', ')}. Or pass minimal: true to create it with only a name.`,
|
|
329
|
+
missing.map((m) => ({ id: m, label: titles[m] ?? m })),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const result = await createAgentFromSkeleton(client, skeleton, { allowDuplicate: args.allow_duplicate });
|
|
334
|
+
return success({ agent: result.agent, set: result.set, not_set: missingSkeletonFields(skeleton) });
|
|
74
335
|
} catch (err) { return error(err); }
|
|
75
336
|
},
|
|
76
337
|
);
|
|
77
338
|
|
|
78
|
-
|
|
339
|
+
// Lifecycle lives on the V2 platform; "unknown" when it cannot be read (treated as published: mayBeLive).
|
|
340
|
+
const lifecycleOf = (id: string) => new AgentInsightsSDK(client).detail(id).then((d) => d.lifecycle ?? 'prototype', () => 'unknown');
|
|
341
|
+
// A change to an agent that may be live asks first; the approval names the fields, never their values (skill auth).
|
|
342
|
+
const liveChange = (what: string, skip: string[]) => ({
|
|
343
|
+
guardWhen: async (a: Record<string, unknown>) => mayBeLive(await lifecycleOf(String(a.agent_id))),
|
|
344
|
+
preview: async (a: Record<string, unknown>) => {
|
|
345
|
+
const fields = Object.keys(a).filter((k) => a[k] !== undefined && !['agent_id', 'confirm', 'org_id', ...skip].includes(k));
|
|
346
|
+
return `This will change ${what} of agent ${a.agent_id}: ${fields.join(', ') || 'nothing'}. The agent ${liveText(await lifecycleOf(String(a.agent_id)))}.`;
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
reg.tool(
|
|
79
351
|
'update_agent',
|
|
80
|
-
'
|
|
352
|
+
'Change an agent\'s name, description or webhook. For the prompt, greeting or model use set_config.',
|
|
81
353
|
{
|
|
82
354
|
agent_id: z.string().describe('The agent ID'),
|
|
83
355
|
name: z.string().optional().describe('New agent name'),
|
|
@@ -95,28 +367,117 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
95
367
|
return success(result);
|
|
96
368
|
} catch (err) { return error(err); }
|
|
97
369
|
},
|
|
370
|
+
liveChange('the name, description or webhook', []),
|
|
98
371
|
);
|
|
99
372
|
|
|
100
|
-
|
|
373
|
+
reg.tool(
|
|
101
374
|
'delete_agent',
|
|
102
|
-
'Delete an agent',
|
|
103
|
-
{
|
|
104
|
-
|
|
375
|
+
'Delete an agent. It is removed from the dashboard and its knowledge base links are dropped.',
|
|
376
|
+
{
|
|
377
|
+
agent_id: z.string().describe('The agent ID to delete'),
|
|
378
|
+
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
379
|
+
},
|
|
380
|
+
async (args, meta) => {
|
|
105
381
|
try {
|
|
382
|
+
const agent = await agents.getAgent(args.agent_id);
|
|
383
|
+
const lifecycle = await lifecycleOf(args.agent_id);
|
|
384
|
+
if (mayBeLive(lifecycle) && !meta.confirmedByPerson && args.confirm_published !== true) {
|
|
385
|
+
throw new Error(`"${agent.name}" ${liveText(lifecycle)}. Show the user; only if they explicitly agree, call again with confirm: true and confirm_published: true.`);
|
|
386
|
+
}
|
|
106
387
|
await agents.deleteAgent(args.agent_id);
|
|
107
|
-
return success({ deleted: true, agent_id: args.agent_id });
|
|
388
|
+
return success({ deleted: true, agent_id: args.agent_id, name: agent.name });
|
|
108
389
|
} catch (err) { return error(err); }
|
|
109
390
|
},
|
|
391
|
+
{
|
|
392
|
+
preview: async (a) => {
|
|
393
|
+
const agent = await agents.getAgent(String(a.agent_id));
|
|
394
|
+
const lifecycle = await lifecycleOf(String(a.agent_id));
|
|
395
|
+
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored from the CLI.`;
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const insights = new AgentInsightsSDK(client);
|
|
401
|
+
|
|
402
|
+
reg.tool(
|
|
403
|
+
'agent_summary',
|
|
404
|
+
'What an agent does: one paragraph (default) or its skills and knowledge sources (full: true). Read this before changing an agent.',
|
|
405
|
+
{ agent_id: z.string().describe('The agent ID'), full: z.boolean().optional().describe('List skills and knowledge sources') },
|
|
406
|
+
async (args) => {
|
|
407
|
+
try { return success(await insights.summary(args.agent_id, { text: !args.full })); } catch (err) { return error(err); }
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
reg.tool(
|
|
412
|
+
'agent_stats',
|
|
413
|
+
'Conversation metrics for one agent (test conversations excluded). Dates are YYYY-MM-DD; default is the last 30 days.',
|
|
414
|
+
{
|
|
415
|
+
agent_id: z.string().describe('The agent ID'),
|
|
416
|
+
date_from: z.string().optional().describe('YYYY-MM-DD'),
|
|
417
|
+
date_to: z.string().optional().describe('YYYY-MM-DD'),
|
|
418
|
+
channel: z.enum(['web', 'voice', 'all']).optional(),
|
|
419
|
+
},
|
|
420
|
+
async (args) => {
|
|
421
|
+
try { return success(await insights.stats(args.agent_id, { from: args.date_from, to: args.date_to, channel: args.channel })); } catch (err) { return error(err); }
|
|
422
|
+
},
|
|
110
423
|
);
|
|
111
424
|
|
|
112
|
-
|
|
425
|
+
reg.tool(
|
|
426
|
+
'set_agent_lifecycle',
|
|
427
|
+
'Move an agent between prototype, beta and published. Only the owner or an org admin may; the V2 platform enforces it.',
|
|
428
|
+
{ agent_id: z.string().describe('The agent ID'), lifecycle: z.enum(['prototype', 'beta', 'published']) },
|
|
429
|
+
async (args) => {
|
|
430
|
+
try { return success(await insights.setLifecycle(args.agent_id, args.lifecycle)); } catch (err) { return error(err); }
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
reg.tool(
|
|
435
|
+
'transfer_agent_owner',
|
|
436
|
+
'Make another user the owner of an agent. Only the current owner or an org admin may; the V2 platform enforces it.',
|
|
437
|
+
{ agent_id: z.string().describe('The agent ID'), owner_id: z.string().describe('User id of the new owner (see list_org_members)') },
|
|
438
|
+
async (args) => {
|
|
439
|
+
try { return success(await insights.transferOwner(args.agent_id, args.owner_id)); } catch (err) { return error(err); }
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
reg.tool(
|
|
113
444
|
'clone_agent',
|
|
114
|
-
'
|
|
115
|
-
{
|
|
445
|
+
'Copy an agent of this org into a new agent in the same org.',
|
|
446
|
+
{
|
|
447
|
+
agent_id: z.string().describe('The agent ID to copy'),
|
|
448
|
+
name: z.string().optional().describe('Name of the copy'),
|
|
449
|
+
channels: z.array(z.string()).optional().describe('Channels of the copy'),
|
|
450
|
+
},
|
|
116
451
|
async (args) => {
|
|
117
452
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
453
|
+
return success(await agents.cloneAgent(args.agent_id, { name: args.name, channels: args.channels }));
|
|
454
|
+
} catch (err) { return error(err); }
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
reg.tool(
|
|
459
|
+
'export_agent',
|
|
460
|
+
'Export an agent as a bundle: prompt, greeting, model, skills with parameters, linked knowledge base ids. Secrets in skill settings are redacted unless include_secrets is true.',
|
|
461
|
+
{ agent_id: z.string().describe('The agent ID'), include_secrets: z.boolean().optional() },
|
|
462
|
+
async (args) => {
|
|
463
|
+
try { return success(await exportAgent(client, args.agent_id, { includeSecrets: args.include_secrets })); } catch (err) { return error(err); }
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
reg.tool(
|
|
468
|
+
'import_agent',
|
|
469
|
+
'Create a new agent in this org from a bundle made by export_agent (possibly from another org or platform). Lists skills that failed and secrets to fill in.',
|
|
470
|
+
{
|
|
471
|
+
bundle: z.record(z.unknown()).describe('The bundle returned by export_agent'),
|
|
472
|
+
name: z.string().optional().describe('Name for the new agent (default: the bundle name)'),
|
|
473
|
+
link_knowledge_bases: z.boolean().optional().describe('Link the same knowledge base ids if they exist in this org'),
|
|
474
|
+
allow_duplicate: z.boolean().optional(),
|
|
475
|
+
},
|
|
476
|
+
async (args) => {
|
|
477
|
+
try {
|
|
478
|
+
return success(await importAgent(client, args.bundle as unknown as AgentBundle, {
|
|
479
|
+
name: args.name, linkKnowledgeBases: args.link_knowledge_bases, allowDuplicate: args.allow_duplicate,
|
|
480
|
+
}));
|
|
120
481
|
} catch (err) { return error(err); }
|
|
121
482
|
},
|
|
122
483
|
);
|
|
@@ -125,9 +486,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
125
486
|
// Config tools
|
|
126
487
|
// ---------------------------------------------------------------------------
|
|
127
488
|
|
|
128
|
-
|
|
489
|
+
reg.tool(
|
|
129
490
|
'get_config',
|
|
130
|
-
'
|
|
491
|
+
'An agent\'s configuration: system prompt, greeting (first message), LLM model and knowledge settings.',
|
|
131
492
|
{ agent_id: z.string().describe('The agent ID') },
|
|
132
493
|
async (args) => {
|
|
133
494
|
try {
|
|
@@ -137,28 +498,44 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
137
498
|
},
|
|
138
499
|
);
|
|
139
500
|
|
|
140
|
-
|
|
501
|
+
const applyChange = async (agentId: string, change: ConfigChange, confirmedByPerson: boolean, confirmPublished?: boolean) => {
|
|
502
|
+
const plan = await planConfigChange(client, agentId, change);
|
|
503
|
+
if (plan.changes.length === 0) return success({ changed: [], message: 'Nothing to change: the agent already has these values.' });
|
|
504
|
+
if (mayBeLive(plan.lifecycle) && !confirmedByPerson && confirmPublished !== true) {
|
|
505
|
+
throw new NeedsInputError(
|
|
506
|
+
`Agent ${agentId} ${liveText(plan.lifecycle)}. Show the user the change; only if they explicitly agree, call again with confirm: true and confirm_published: true.`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const result = await applyConfigChange(client, plan, change);
|
|
510
|
+
return success({ changed: result.applied, not_saved: result.mismatched, lifecycle: plan.lifecycle });
|
|
511
|
+
};
|
|
512
|
+
const previewChange = (argsToChange: (a: Record<string, unknown>) => ConfigChange) => async (a: Record<string, unknown>) =>
|
|
513
|
+
describePlan(await planConfigChange(client, String(a.agent_id), argsToChange(a)));
|
|
514
|
+
const configArgs = (a: Record<string, unknown>): ConfigChange => ({
|
|
515
|
+
system_prompt: a.system_prompt as string | undefined,
|
|
516
|
+
greeting: a.first_message as string | undefined,
|
|
517
|
+
model: a.model ? modelArg(String(a.model)) : undefined,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
reg.tool(
|
|
141
521
|
'set_config',
|
|
142
|
-
'
|
|
522
|
+
'Change an agent\'s prompt, greeting and/or model. The confirmation shows the prompt diff and whether the agent is published; published agents need confirm_published too. The result says what was saved (read back from the platform).',
|
|
143
523
|
{
|
|
144
524
|
agent_id: z.string().describe('The agent ID'),
|
|
145
|
-
system_prompt: z.string().optional().describe('
|
|
146
|
-
first_message: z.string().optional().describe('
|
|
525
|
+
system_prompt: z.string().optional().describe('New system prompt (whole text)'),
|
|
526
|
+
first_message: z.string().optional().describe('New greeting shown to users'),
|
|
527
|
+
model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
|
|
528
|
+
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
147
529
|
},
|
|
148
|
-
async (args) => {
|
|
149
|
-
try {
|
|
150
|
-
const input: Record<string, unknown> = {};
|
|
151
|
-
if (args.system_prompt !== undefined) input.system_prompt = args.system_prompt;
|
|
152
|
-
if (args.first_message !== undefined) input.first_message = { text: args.first_message };
|
|
153
|
-
const result = await config.setConfig(args.agent_id, input);
|
|
154
|
-
return success(result);
|
|
155
|
-
} catch (err) { return error(err); }
|
|
530
|
+
async (args, meta) => {
|
|
531
|
+
try { return await applyChange(args.agent_id, configArgs(args), meta.confirmedByPerson, args.confirm_published); } catch (err) { return error(err); }
|
|
156
532
|
},
|
|
533
|
+
{ preview: previewChange(configArgs) },
|
|
157
534
|
);
|
|
158
535
|
|
|
159
|
-
|
|
536
|
+
reg.tool(
|
|
160
537
|
'get_prompt',
|
|
161
|
-
'
|
|
538
|
+
'An agent\'s system prompt (the instructions it follows).',
|
|
162
539
|
{ agent_id: z.string().describe('The agent ID') },
|
|
163
540
|
async (args) => {
|
|
164
541
|
try {
|
|
@@ -168,28 +545,27 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
168
545
|
},
|
|
169
546
|
);
|
|
170
547
|
|
|
171
|
-
|
|
548
|
+
reg.tool(
|
|
172
549
|
'set_prompt',
|
|
173
|
-
'
|
|
550
|
+
'Replace an agent\'s system prompt. The confirmation shows the diff; published agents need confirm_published too.',
|
|
174
551
|
{
|
|
175
552
|
agent_id: z.string().describe('The agent ID'),
|
|
176
|
-
prompt: z.string().describe('The system prompt
|
|
553
|
+
prompt: z.string().describe('The whole new system prompt'),
|
|
554
|
+
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
177
555
|
},
|
|
178
|
-
async (args) => {
|
|
179
|
-
try {
|
|
180
|
-
const result = await config.setPrompt(args.agent_id, args.prompt);
|
|
181
|
-
return success(result);
|
|
182
|
-
} catch (err) { return error(err); }
|
|
556
|
+
async (args, meta) => {
|
|
557
|
+
try { return await applyChange(args.agent_id, { system_prompt: args.prompt }, meta.confirmedByPerson, args.confirm_published); } catch (err) { return error(err); }
|
|
183
558
|
},
|
|
559
|
+
{ preview: previewChange((a) => ({ system_prompt: String(a.prompt) })) },
|
|
184
560
|
);
|
|
185
561
|
|
|
186
562
|
// ---------------------------------------------------------------------------
|
|
187
563
|
// Function tools
|
|
188
564
|
// ---------------------------------------------------------------------------
|
|
189
565
|
|
|
190
|
-
|
|
566
|
+
reg.tool(
|
|
191
567
|
'list_functions',
|
|
192
|
-
'List
|
|
568
|
+
'List an agent\'s skills (the actions it can take: REST calls, handoff, FAQ retriever, transfer call, ...), with their parameters.',
|
|
193
569
|
{ agent_id: z.string().describe('The agent ID') },
|
|
194
570
|
async (args) => {
|
|
195
571
|
try {
|
|
@@ -199,84 +575,139 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
199
575
|
},
|
|
200
576
|
);
|
|
201
577
|
|
|
202
|
-
|
|
578
|
+
reg.tool(
|
|
203
579
|
'create_function',
|
|
204
|
-
'
|
|
580
|
+
'Add a skill to an agent. rest_api skills need a configurationSchema (endpoint, method, auth, payload). Names too close to the built-in FAQ retriever are refused.',
|
|
205
581
|
{
|
|
206
582
|
agent_id: z.string().describe('The agent ID'),
|
|
207
583
|
type: z.string().describe('Function type (e.g. "rest_api")'),
|
|
208
584
|
description: z.string().optional().describe('What the function does'),
|
|
209
585
|
name: z.string().optional().describe('Function name'),
|
|
210
|
-
url: z.string().optional().describe('
|
|
211
|
-
method: z.string().optional().describe('HTTP method (GET, POST, etc.)'),
|
|
586
|
+
url: z.string().optional().describe('rest_api: endpoint URL'),
|
|
587
|
+
method: z.string().optional().describe('rest_api: HTTP method (GET, POST, etc.)'),
|
|
588
|
+
payload: z.union([z.string(), z.record(z.any())]).optional().describe('rest_api: request body (JSON); {{parameter}} placeholders are filled from the skill parameters'),
|
|
589
|
+
response_field: z.string().optional().describe('rest_api: dot-path of the response to use (e.g. data.result)'),
|
|
590
|
+
output_instructions: z.string().optional().describe('What the agent says with the answer; {{response.field}} reads the API answer'),
|
|
591
|
+
craft_response: z.boolean().optional().describe('true: the agent phrases the answer itself (default for rest_api without output_instructions); false: the answer, or output_instructions, goes to the customer as is'),
|
|
592
|
+
...skillAuthFields,
|
|
593
|
+
configuration: jsonObject().optional().describe('Other configurationSchema fields (an object, or JSON text); the fields above win'),
|
|
594
|
+
transfer_to: z.string().optional().describe('transfer_call: phone number (E.164, e.g. +15551234567) or SIP address (sip:...) calls are transferred to'),
|
|
212
595
|
},
|
|
213
596
|
async (args) => {
|
|
214
597
|
try {
|
|
215
|
-
const { agent_id,
|
|
216
|
-
const
|
|
217
|
-
|
|
598
|
+
const { agent_id, type, name, description } = args;
|
|
599
|
+
const configurationSchema = skillConfiguration(type, withAuthorization(args));
|
|
600
|
+
// copilot sends a REST answer straight to the customer unless craftResponse is on; with nothing to say
|
|
601
|
+
// how to present it, let the agent phrase it.
|
|
602
|
+
if (type === 'rest_api' && configurationSchema && configurationSchema.craftResponse === undefined && !configurationSchema.outputInstructions) {
|
|
603
|
+
configurationSchema.craftResponse = true;
|
|
604
|
+
}
|
|
605
|
+
if (type === 'rest_api' && !(configurationSchema?.endpoint && configurationSchema?.method)) {
|
|
606
|
+
throw new Error('A rest_api skill needs url and method.');
|
|
607
|
+
}
|
|
608
|
+
if (type === 'transfer_call' && !(configurationSchema?.phoneNumber || configurationSchema?.sipUri)) {
|
|
609
|
+
throw new Error('A transfer_call skill needs transfer_to: a phone number (E.164, e.g. +15551234567) or a SIP address (sip:...).');
|
|
610
|
+
}
|
|
611
|
+
// A retry must not leave copies: one skill per name on an agent.
|
|
612
|
+
const same = name ? (await functions.listFunctions(agent_id)).find((f) => (f.name ?? '').trim().toLowerCase() === name.trim().toLowerCase()) : undefined;
|
|
613
|
+
if (same) throw new Error(`The agent already has a skill named "${name}" (function_id ${same._id}). Change it with update_function instead of creating another.`);
|
|
614
|
+
// copilot turns configurationSchema into the executable form only on update: create, then configure.
|
|
615
|
+
const created = await functions.createFunction(agent_id, { type, name, description });
|
|
616
|
+
if (!configurationSchema) return success(created);
|
|
617
|
+
try {
|
|
618
|
+
return success(await functions.updateFunction(agent_id, created._id, { type, description: description ?? '', configurationSchema }));
|
|
619
|
+
} catch (err) {
|
|
620
|
+
// Leave no half-made skill behind: remove the empty one, then say why nothing was saved.
|
|
621
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
622
|
+
try {
|
|
623
|
+
await functions.deleteFunction(agent_id, created._id);
|
|
624
|
+
} catch {
|
|
625
|
+
throw new Error(`Skill ${created._id} was created, but its configuration was not saved (${why}). Remove it with delete_function, or fix it with update_function.`);
|
|
626
|
+
}
|
|
627
|
+
throw new Error(`Nothing was saved: the skill's configuration was refused (${why}). Fix it and call create_function again.`);
|
|
628
|
+
}
|
|
218
629
|
} catch (err) { return error(err); }
|
|
219
630
|
},
|
|
220
631
|
);
|
|
221
632
|
|
|
222
|
-
|
|
633
|
+
reg.tool(
|
|
223
634
|
'update_function',
|
|
224
|
-
'
|
|
635
|
+
'Change a skill of an agent (name, description, configurationSchema).',
|
|
225
636
|
{
|
|
226
637
|
agent_id: z.string().describe('The agent ID'),
|
|
227
638
|
function_id: z.string().describe('The function ID'),
|
|
228
639
|
description: z.string().optional().describe('Updated description'),
|
|
229
640
|
name: z.string().optional().describe('Updated function name'),
|
|
230
|
-
url: z.string().optional().describe('
|
|
231
|
-
method: z.string().optional().describe('
|
|
641
|
+
url: z.string().optional().describe('rest_api: new endpoint URL'),
|
|
642
|
+
method: z.string().optional().describe('rest_api: new HTTP method'),
|
|
643
|
+
payload: z.union([z.string(), z.record(z.any())]).optional().describe('rest_api: new request body (JSON) with {{parameter}} placeholders'),
|
|
644
|
+
response_field: z.string().optional().describe('rest_api: new response dot-path'),
|
|
645
|
+
output_instructions: z.string().optional().describe('New text the agent says with the answer; {{response.field}} reads the API answer'),
|
|
646
|
+
craft_response: z.boolean().optional().describe('true: the agent phrases the answer; false: it goes to the customer as is'),
|
|
647
|
+
...skillAuthFields,
|
|
648
|
+
configuration: jsonObject().optional().describe('Other configurationSchema fields to change (an object, or JSON text); the rest of the saved configuration is kept'),
|
|
649
|
+
transfer_to: z.string().optional().describe('transfer_call: new phone number (E.164) or SIP address; replaces the saved destination'),
|
|
232
650
|
},
|
|
233
651
|
async (args) => {
|
|
234
652
|
try {
|
|
235
|
-
const { agent_id
|
|
236
|
-
|
|
237
|
-
|
|
653
|
+
const { agent_id } = args;
|
|
654
|
+
// copilot needs type and description on every update and replaces the configuration whole.
|
|
655
|
+
const current = await skillOf(agent_id, args.function_id);
|
|
656
|
+
const function_id = current._id;
|
|
657
|
+
const input: { type: string; description: string; name?: string; configurationSchema?: Record<string, unknown> } = {
|
|
658
|
+
type: current.type, description: args.description ?? current.description ?? '',
|
|
659
|
+
};
|
|
660
|
+
if (args.name) input.name = args.name;
|
|
661
|
+
const configurationSchema = skillConfiguration(current.type, withAuthorization(args), current.configurationSchema ?? {});
|
|
662
|
+
if (configurationSchema) input.configurationSchema = configurationSchema;
|
|
663
|
+
return success(await functions.updateFunction(agent_id, function_id, input));
|
|
238
664
|
} catch (err) { return error(err); }
|
|
239
665
|
},
|
|
666
|
+
liveChange('a skill', ['function_id']),
|
|
240
667
|
);
|
|
241
668
|
|
|
242
|
-
|
|
669
|
+
reg.tool(
|
|
243
670
|
'delete_function',
|
|
244
|
-
'
|
|
671
|
+
'Remove a skill from an agent.',
|
|
245
672
|
{
|
|
246
673
|
agent_id: z.string().describe('The agent ID'),
|
|
247
674
|
function_id: z.string().describe('The function ID to delete'),
|
|
248
675
|
},
|
|
249
676
|
async (args) => {
|
|
250
677
|
try {
|
|
251
|
-
await
|
|
252
|
-
|
|
678
|
+
const skill = await skillOf(args.agent_id, args.function_id);
|
|
679
|
+
await functions.deleteFunction(args.agent_id, skill._id);
|
|
680
|
+
return success({ deleted: true, agent_id: args.agent_id, function_id: skill._id, name: skill.name });
|
|
253
681
|
} catch (err) { return error(err); }
|
|
254
682
|
},
|
|
255
683
|
);
|
|
256
684
|
|
|
257
|
-
|
|
685
|
+
reg.tool(
|
|
258
686
|
'test_function',
|
|
259
|
-
'
|
|
687
|
+
'Run a skill once with sample parameters and return what its API answered. Calls the real endpoint.',
|
|
260
688
|
{
|
|
261
689
|
agent_id: z.string().describe('The agent ID'),
|
|
262
690
|
function_id: z.string().describe('The function ID to test'),
|
|
263
|
-
params:
|
|
691
|
+
params: jsonObject().optional().describe('Sample values for the skill\'s {{parameter}} placeholders (an object, or JSON text)'),
|
|
264
692
|
},
|
|
265
693
|
async (args) => {
|
|
266
694
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
695
|
+
// copilot's test route runs the configuration it is sent (not the saved one) and sends the body as is:
|
|
696
|
+
// send the saved configuration with the sample values in place of its placeholders.
|
|
697
|
+
const current = await skillOf(args.agent_id, args.function_id);
|
|
698
|
+
const params = (args.params ?? {}) as Record<string, unknown>;
|
|
699
|
+
const fill = (v: unknown) => (typeof v === 'string'
|
|
700
|
+
? v.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (m, k: string) => (k in params ? String(params[k]) : m))
|
|
701
|
+
: v);
|
|
702
|
+
const config = Object.fromEntries(Object.entries(current.configurationSchema ?? {}).map(([k, v]) => [k, fill(v)]));
|
|
703
|
+
return success(await functions.testFunction(args.agent_id, current._id, config));
|
|
273
704
|
} catch (err) { return error(err); }
|
|
274
705
|
},
|
|
275
706
|
);
|
|
276
707
|
|
|
277
|
-
|
|
708
|
+
reg.tool(
|
|
278
709
|
'export_functions',
|
|
279
|
-
'Export
|
|
710
|
+
'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions).',
|
|
280
711
|
{ agent_id: z.string().describe('The agent ID') },
|
|
281
712
|
async (args) => {
|
|
282
713
|
try {
|
|
@@ -286,7 +717,7 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
286
717
|
},
|
|
287
718
|
);
|
|
288
719
|
|
|
289
|
-
|
|
720
|
+
reg.tool(
|
|
290
721
|
'import_functions',
|
|
291
722
|
'Import functions into an agent from a URL (e.g. OpenAPI spec)',
|
|
292
723
|
{
|
|
@@ -301,24 +732,65 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
301
732
|
},
|
|
302
733
|
);
|
|
303
734
|
|
|
304
|
-
|
|
735
|
+
reg.tool(
|
|
736
|
+
'generate_skills_from_doc',
|
|
737
|
+
'Propose skills from an API document (file path on this machine) or pasted text. Creates nothing: review the proposal, then call bulk_create_skills.',
|
|
738
|
+
{
|
|
739
|
+
agent_id: z.string().describe('The agent ID'),
|
|
740
|
+
file_path: z.string().optional().describe('Path of a document on this machine'),
|
|
741
|
+
text: z.string().optional().describe('Text to read instead of a file'),
|
|
742
|
+
},
|
|
743
|
+
async (args) => {
|
|
744
|
+
try {
|
|
745
|
+
if (!args.file_path && !args.text) throw new Error('Give file_path or text.');
|
|
746
|
+
const proposal = await functions.generateFromDoc(args.agent_id, { filePath: args.file_path, text: args.text });
|
|
747
|
+
return success({ proposed: proposedSkills(proposal), raw: proposedSkills(proposal) ? undefined : proposal });
|
|
748
|
+
} catch (err) { return error(err); }
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
reg.tool(
|
|
753
|
+
'bulk_create_skills',
|
|
754
|
+
'Create several skills on an agent at once, for example the proposal from generate_skills_from_doc. Names are checked first; a failure part-way can leave some created.',
|
|
755
|
+
{
|
|
756
|
+
agent_id: z.string().describe('The agent ID'),
|
|
757
|
+
functions: z.array(skillDefinition).min(1).describe('Skill definitions'),
|
|
758
|
+
},
|
|
759
|
+
async (args) => {
|
|
760
|
+
try {
|
|
761
|
+
const skills = (args.functions as Array<Record<string, unknown> & AuthFields>).map((f) => {
|
|
762
|
+
const { url, method, payload, response_field, output_instructions, transfer_to, auth_type, auth_token, auth_key, auth_value, auth_username, auth_password, ...rest } = f;
|
|
763
|
+
const built = skillConfiguration(String(f.type), withAuthorization({
|
|
764
|
+
url: url as string | undefined, method: method as string | undefined, payload, response_field: response_field as string | undefined,
|
|
765
|
+
output_instructions: output_instructions as string | undefined, transfer_to: transfer_to as string | undefined,
|
|
766
|
+
auth_type, auth_token, auth_key, auth_value, auth_username, auth_password,
|
|
767
|
+
configuration: rest.configurationSchema as Record<string, unknown> | undefined,
|
|
768
|
+
}));
|
|
769
|
+
return built ? { ...rest, configurationSchema: built } : rest;
|
|
770
|
+
});
|
|
771
|
+
return success(await functions.bulkCreate(args.agent_id, skills));
|
|
772
|
+
} catch (err) { return error(err); }
|
|
773
|
+
},
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
reg.tool(
|
|
305
777
|
'list_function_parameters',
|
|
306
|
-
'
|
|
778
|
+
'The parameters a skill collects from the customer before it runs.',
|
|
307
779
|
{
|
|
308
780
|
agent_id: z.string().describe('The agent ID'),
|
|
309
781
|
function_id: z.string().describe('The function ID'),
|
|
310
782
|
},
|
|
311
783
|
async (args) => {
|
|
312
784
|
try {
|
|
313
|
-
const result = await functions.listParameters(args.agent_id, args.function_id);
|
|
785
|
+
const result = await functions.listParameters(args.agent_id, (await skillOf(args.agent_id, args.function_id))._id);
|
|
314
786
|
return success(result);
|
|
315
787
|
} catch (err) { return error(err); }
|
|
316
788
|
},
|
|
317
789
|
);
|
|
318
790
|
|
|
319
|
-
|
|
791
|
+
reg.tool(
|
|
320
792
|
'add_function_parameter',
|
|
321
|
-
'Add a parameter
|
|
793
|
+
'Add a parameter the skill should collect (name, type, description, required).',
|
|
322
794
|
{
|
|
323
795
|
agent_id: z.string().describe('The agent ID'),
|
|
324
796
|
function_id: z.string().describe('The function ID'),
|
|
@@ -326,19 +798,20 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
326
798
|
type: z.string().describe('Parameter type (string, number, boolean)'),
|
|
327
799
|
description: z.string().optional().describe('Parameter description'),
|
|
328
800
|
required: z.boolean().optional().describe('Whether the parameter is required'),
|
|
801
|
+
allowed_values: z.array(z.string()).optional().describe('Fixed choices the customer picks from (shown as buttons); omit for free text'),
|
|
329
802
|
},
|
|
330
803
|
async (args) => {
|
|
331
804
|
try {
|
|
332
|
-
const { agent_id, function_id, ...param } = args;
|
|
333
|
-
const result = await functions.addParameter(agent_id, function_id, param);
|
|
805
|
+
const { agent_id, function_id, allowed_values, ...param } = args;
|
|
806
|
+
const result = await functions.addParameter(agent_id, (await skillOf(agent_id, function_id))._id, { ...param, allowedValues: allowed_values });
|
|
334
807
|
return success(result);
|
|
335
808
|
} catch (err) { return error(err); }
|
|
336
809
|
},
|
|
337
810
|
);
|
|
338
811
|
|
|
339
|
-
|
|
812
|
+
reg.tool(
|
|
340
813
|
'update_function_parameter',
|
|
341
|
-
'
|
|
814
|
+
'Change a skill parameter.',
|
|
342
815
|
{
|
|
343
816
|
agent_id: z.string().describe('The agent ID'),
|
|
344
817
|
function_id: z.string().describe('The function ID'),
|
|
@@ -347,19 +820,20 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
347
820
|
type: z.string().optional().describe('Updated type'),
|
|
348
821
|
description: z.string().optional().describe('Updated description'),
|
|
349
822
|
required: z.boolean().optional().describe('Updated required flag'),
|
|
823
|
+
allowed_values: z.array(z.string()).optional().describe('New fixed choices; [] removes them. Anything not given stays as it is'),
|
|
350
824
|
},
|
|
351
825
|
async (args) => {
|
|
352
826
|
try {
|
|
353
|
-
const { agent_id, function_id, parameter_id, ...param } = args;
|
|
354
|
-
const result = await functions.updateParameter(agent_id, function_id, parameter_id, param);
|
|
827
|
+
const { agent_id, function_id, parameter_id, allowed_values, ...param } = args;
|
|
828
|
+
const result = await functions.updateParameter(agent_id, (await skillOf(agent_id, function_id))._id, parameter_id, { ...param, allowedValues: allowed_values });
|
|
355
829
|
return success(result);
|
|
356
830
|
} catch (err) { return error(err); }
|
|
357
831
|
},
|
|
358
832
|
);
|
|
359
833
|
|
|
360
|
-
|
|
834
|
+
reg.tool(
|
|
361
835
|
'delete_function_parameter',
|
|
362
|
-
'
|
|
836
|
+
'Remove a skill parameter.',
|
|
363
837
|
{
|
|
364
838
|
agent_id: z.string().describe('The agent ID'),
|
|
365
839
|
function_id: z.string().describe('The function ID'),
|
|
@@ -367,27 +841,28 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
367
841
|
},
|
|
368
842
|
async (args) => {
|
|
369
843
|
try {
|
|
370
|
-
await functions.deleteParameter(args.agent_id, args.function_id, args.parameter_id);
|
|
844
|
+
await functions.deleteParameter(args.agent_id, (await skillOf(args.agent_id, args.function_id))._id, args.parameter_id);
|
|
371
845
|
return success({ deleted: true });
|
|
372
846
|
} catch (err) { return error(err); }
|
|
373
847
|
},
|
|
374
848
|
);
|
|
375
849
|
|
|
376
|
-
|
|
850
|
+
reg.tool(
|
|
377
851
|
'list_master_functions',
|
|
378
|
-
'
|
|
379
|
-
{},
|
|
380
|
-
async () => {
|
|
852
|
+
'Built-in skill types an agent can use as starting points (pass agent_id to get the ones for its channels, e.g. voice).',
|
|
853
|
+
{ agent_id: z.string().optional().describe('Agent whose channels (WEB, VOICE) decide which templates apply') },
|
|
854
|
+
async (args) => {
|
|
381
855
|
try {
|
|
382
|
-
const
|
|
856
|
+
const channels = args.agent_id ? ((await agents.getAgent(args.agent_id)).channels as unknown[] | undefined) : undefined;
|
|
857
|
+
const result = await functions.listMasterFunctions(channels?.map((c) => (typeof c === 'string' ? c : String((c as { type?: unknown }).type ?? ''))).filter(Boolean));
|
|
383
858
|
return success(result);
|
|
384
859
|
} catch (err) { return error(err); }
|
|
385
860
|
},
|
|
386
861
|
);
|
|
387
862
|
|
|
388
|
-
|
|
863
|
+
reg.tool(
|
|
389
864
|
'list_llm_models',
|
|
390
|
-
'
|
|
865
|
+
'LLM providers and models an agent can run on (use provider/model with set_config).',
|
|
391
866
|
{},
|
|
392
867
|
async () => {
|
|
393
868
|
try {
|
|
@@ -401,34 +876,35 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
401
876
|
// Knowledge base tools
|
|
402
877
|
// ---------------------------------------------------------------------------
|
|
403
878
|
|
|
404
|
-
|
|
879
|
+
reg.tool(
|
|
405
880
|
'list_knowledge_bases',
|
|
406
|
-
'List
|
|
407
|
-
{
|
|
408
|
-
async (
|
|
881
|
+
'List the org\'s knowledge bases (what agents can look answers up in), with the agents each is linked to.',
|
|
882
|
+
{},
|
|
883
|
+
async () => {
|
|
409
884
|
try {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
885
|
+
// A website base lists every crawled page (100k characters for a large site); get_knowledge_base has them.
|
|
886
|
+
const kbs = (await kb.listKBs()) as unknown as Array<{ fileWebSources?: unknown[]; kbSampleFaqs?: unknown; [k: string]: unknown }>;
|
|
887
|
+
const pages = await orgPages(client);
|
|
888
|
+
return success(kbs.map(({ fileWebSources, kbSampleFaqs: _faqs, ...rest }) => ({ ...rest, sources: fileWebSources?.length ?? 0, url: `${pages}/ai-studio/knowledge-base/${rest._id}` })));
|
|
413
889
|
} catch (err) { return error(err); }
|
|
414
890
|
},
|
|
415
891
|
);
|
|
416
892
|
|
|
417
|
-
|
|
893
|
+
reg.tool(
|
|
418
894
|
'get_knowledge_base',
|
|
419
|
-
'
|
|
895
|
+
'One knowledge base with its website and file sources and their crawl status.',
|
|
420
896
|
{ kb_id: z.string().describe('The knowledge base ID') },
|
|
421
897
|
async (args) => {
|
|
422
898
|
try {
|
|
423
899
|
const result = await kb.getKB(args.kb_id);
|
|
424
|
-
return success(result);
|
|
900
|
+
return success({ ...result, url: `${await orgPages(client)}/ai-studio/knowledge-base/${args.kb_id}` });
|
|
425
901
|
} catch (err) { return error(err); }
|
|
426
902
|
},
|
|
427
903
|
);
|
|
428
904
|
|
|
429
|
-
|
|
905
|
+
reg.tool(
|
|
430
906
|
'create_knowledge_base',
|
|
431
|
-
'Create a
|
|
907
|
+
'Create a knowledge base. type QNA (question/answer pairs), URL (websites) or FILE (documents).',
|
|
432
908
|
{
|
|
433
909
|
name: z.string().describe('Knowledge base name'),
|
|
434
910
|
type: z.string().describe('Knowledge base type'),
|
|
@@ -441,9 +917,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
441
917
|
},
|
|
442
918
|
);
|
|
443
919
|
|
|
444
|
-
|
|
920
|
+
reg.tool(
|
|
445
921
|
'update_knowledge_base',
|
|
446
|
-
'
|
|
922
|
+
'Rename a knowledge base.',
|
|
447
923
|
{
|
|
448
924
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
449
925
|
name: z.string().optional().describe('Updated name'),
|
|
@@ -460,9 +936,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
460
936
|
},
|
|
461
937
|
);
|
|
462
938
|
|
|
463
|
-
|
|
939
|
+
reg.tool(
|
|
464
940
|
'delete_knowledge_base',
|
|
465
|
-
'Delete a knowledge base',
|
|
941
|
+
'Delete a knowledge base with all its sources. It must be unlinked from every agent first.',
|
|
466
942
|
{ kb_id: z.string().describe('The knowledge base ID to delete') },
|
|
467
943
|
async (args) => {
|
|
468
944
|
try {
|
|
@@ -472,9 +948,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
472
948
|
},
|
|
473
949
|
);
|
|
474
950
|
|
|
475
|
-
|
|
951
|
+
reg.tool(
|
|
476
952
|
'list_kb_data',
|
|
477
|
-
'
|
|
953
|
+
'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base).',
|
|
478
954
|
{
|
|
479
955
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
480
956
|
page: z.number().optional().describe('Page number'),
|
|
@@ -491,7 +967,7 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
491
967
|
},
|
|
492
968
|
);
|
|
493
969
|
|
|
494
|
-
|
|
970
|
+
reg.tool(
|
|
495
971
|
'add_kb_data',
|
|
496
972
|
'Add a data source entry to a knowledge base. For URL scraping use scrape_url instead. For file uploads use upload_kb_file.',
|
|
497
973
|
{
|
|
@@ -514,7 +990,7 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
514
990
|
},
|
|
515
991
|
);
|
|
516
992
|
|
|
517
|
-
|
|
993
|
+
reg.tool(
|
|
518
994
|
'scrape_url',
|
|
519
995
|
'Scrape a website URL and add it as a data source to a knowledge base. Initiates async crawling.',
|
|
520
996
|
{
|
|
@@ -530,9 +1006,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
530
1006
|
},
|
|
531
1007
|
);
|
|
532
1008
|
|
|
533
|
-
|
|
1009
|
+
reg.tool(
|
|
534
1010
|
'upload_kb_file',
|
|
535
|
-
'Upload a
|
|
1011
|
+
'Upload a document from this machine into a knowledge base; it is read in the background (see kb_scrape_status).',
|
|
536
1012
|
{
|
|
537
1013
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
538
1014
|
file_path: z.string().describe('Path to the file to upload'),
|
|
@@ -545,9 +1021,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
545
1021
|
},
|
|
546
1022
|
);
|
|
547
1023
|
|
|
548
|
-
|
|
1024
|
+
reg.tool(
|
|
549
1025
|
'update_kb_data',
|
|
550
|
-
'
|
|
1026
|
+
'Change a question/answer pair of a knowledge base.',
|
|
551
1027
|
{
|
|
552
1028
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
553
1029
|
source_id: z.string().describe('The data source ID'),
|
|
@@ -565,9 +1041,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
565
1041
|
},
|
|
566
1042
|
);
|
|
567
1043
|
|
|
568
|
-
|
|
1044
|
+
reg.tool(
|
|
569
1045
|
'delete_kb_data',
|
|
570
|
-
'Delete a
|
|
1046
|
+
'Delete a source of a knowledge base.',
|
|
571
1047
|
{
|
|
572
1048
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
573
1049
|
source_id: z.string().describe('The data source ID to delete'),
|
|
@@ -580,9 +1056,65 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
580
1056
|
},
|
|
581
1057
|
);
|
|
582
1058
|
|
|
583
|
-
|
|
1059
|
+
reg.tool(
|
|
1060
|
+
'kb_chunks',
|
|
1061
|
+
'The text chunks the agent actually retrieves from one knowledge base source. Use it to check what was learned from a page or file.',
|
|
1062
|
+
{ kb_id: z.string(), source_id: z.string() },
|
|
1063
|
+
async (args) => {
|
|
1064
|
+
try { return success(await kb.chunks(args.kb_id, args.source_id)); } catch (err) { return error(err); }
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
reg.tool(
|
|
1069
|
+
'reimport_kb_source',
|
|
1070
|
+
'Crawl a website source again after the site changed. Runs in the background; follow it with kb_scrape_status.',
|
|
1071
|
+
{ kb_id: z.string(), source_id: z.string() },
|
|
1072
|
+
async (args) => {
|
|
1073
|
+
try { return success(await kb.reimport(args.kb_id, args.source_id)); } catch (err) { return error(err); }
|
|
1074
|
+
},
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
reg.tool(
|
|
1078
|
+
'kb_scrape_status',
|
|
1079
|
+
'Progress of website crawls and file imports of a knowledge base (per source: status, job status, error).',
|
|
1080
|
+
{ kb_id: z.string() },
|
|
1081
|
+
async (args) => {
|
|
1082
|
+
try { return success(await kb.scrapeStatus(args.kb_id)); } catch (err) { return error(err); }
|
|
1083
|
+
},
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
reg.tool(
|
|
1087
|
+
'kb_gaps',
|
|
1088
|
+
'Questions the agents could not answer, to decide what to add to the knowledge base. themes: true groups them by topic.',
|
|
1089
|
+
{
|
|
1090
|
+
agent_id: z.string().optional(),
|
|
1091
|
+
kb_id: z.string().optional(),
|
|
1092
|
+
themes: z.boolean().optional(),
|
|
1093
|
+
days: z.number().int().min(1).max(365).optional().describe('With themes: look back this many days'),
|
|
1094
|
+
include_test: z.boolean().optional(),
|
|
1095
|
+
page: z.number().int().min(0).optional().describe('Starting at 0'),
|
|
1096
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1097
|
+
},
|
|
1098
|
+
async (args) => {
|
|
1099
|
+
try {
|
|
1100
|
+
if (args.themes) return success(await kb.gapClusters({ days: args.days, page: args.page, limit: args.limit }));
|
|
1101
|
+
return success(await kb.gaps({ agentId: args.agent_id, kbId: args.kb_id, includeTest: args.include_test, page: args.page, limit: args.limit }));
|
|
1102
|
+
} catch (err) { return error(err); }
|
|
1103
|
+
},
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
reg.tool(
|
|
1107
|
+
'kb_check',
|
|
1108
|
+
'Could the org\'s knowledge bases answer this question? Returns whether it is answerable, the top score and the matching chunks.',
|
|
1109
|
+
{ question: z.string() },
|
|
1110
|
+
async (args) => {
|
|
1111
|
+
try { return success(await kb.check(args.question)); } catch (err) { return error(err); }
|
|
1112
|
+
},
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
reg.tool(
|
|
584
1116
|
'link_kb',
|
|
585
|
-
'
|
|
1117
|
+
'Let an agent answer from these knowledge bases.',
|
|
586
1118
|
{
|
|
587
1119
|
agent_id: z.string().describe('The agent ID'),
|
|
588
1120
|
kb_ids: z.array(z.string()).describe('Array of knowledge base IDs to link'),
|
|
@@ -595,9 +1127,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
595
1127
|
},
|
|
596
1128
|
);
|
|
597
1129
|
|
|
598
|
-
|
|
1130
|
+
reg.tool(
|
|
599
1131
|
'unlink_kb',
|
|
600
|
-
'
|
|
1132
|
+
'Stop an agent from using a knowledge base.',
|
|
601
1133
|
{
|
|
602
1134
|
agent_id: z.string().describe('The agent ID'),
|
|
603
1135
|
kb_id: z.string().describe('The knowledge base ID to unlink'),
|
|
@@ -610,9 +1142,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
610
1142
|
},
|
|
611
1143
|
);
|
|
612
1144
|
|
|
613
|
-
|
|
1145
|
+
reg.tool(
|
|
614
1146
|
'export_kb',
|
|
615
|
-
'Export knowledge base
|
|
1147
|
+
'Export the question/answer pairs of a knowledge base as a CSV or XLSX download link.',
|
|
616
1148
|
{
|
|
617
1149
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
618
1150
|
format: z.string().optional().describe('Export format (e.g. "json", "csv")'),
|
|
@@ -625,9 +1157,9 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
625
1157
|
},
|
|
626
1158
|
);
|
|
627
1159
|
|
|
628
|
-
|
|
1160
|
+
reg.tool(
|
|
629
1161
|
'import_kb',
|
|
630
|
-
'Import
|
|
1162
|
+
'Import websites or files into a knowledge base; processed in the background (see kb_scrape_status).',
|
|
631
1163
|
{
|
|
632
1164
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
633
1165
|
data: z.record(z.unknown()).describe('Data to import'),
|
|
@@ -644,65 +1176,627 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
644
1176
|
// Chat tool
|
|
645
1177
|
// ---------------------------------------------------------------------------
|
|
646
1178
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1179
|
+
const conversations = new ConversationsSDK(client);
|
|
1180
|
+
|
|
1181
|
+
reg.tool(
|
|
1182
|
+
'list_conversations',
|
|
1183
|
+
'List conversations of one agent in the org, newest first, including test ones, with whether each is a test and whether the CLI opened it.',
|
|
650
1184
|
{
|
|
651
|
-
agent_id: z.string().describe('
|
|
652
|
-
|
|
653
|
-
|
|
1185
|
+
agent_id: z.string().describe('Agent whose conversations to list'),
|
|
1186
|
+
limit: z.number().int().min(1).max(100).optional().describe('Rows per page (default 50, at most 100)'),
|
|
1187
|
+
page: z.number().int().min(0).optional().describe('Page, starting at 0'),
|
|
1188
|
+
live_only: z.boolean().optional().describe('Hide test conversations'),
|
|
1189
|
+
cli_only: z.boolean().optional().describe('Only conversations opened by the CLI'),
|
|
654
1190
|
},
|
|
655
1191
|
async (args) => {
|
|
656
1192
|
try {
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
// Track streaming state to distinguish greeting from response
|
|
661
|
-
let receiving = false;
|
|
662
|
-
let messageCount = 0;
|
|
663
|
-
|
|
664
|
-
chat.on('stream', (data: { isFirstChunk?: boolean; streamFinished?: boolean }) => {
|
|
665
|
-
if (data.isFirstChunk) receiving = true;
|
|
666
|
-
if (data.streamFinished) receiving = false;
|
|
1193
|
+
const res = await conversations.list(args.agent_id, {
|
|
1194
|
+
limit: args.limit, page: args.page, includeTest: !args.live_only, cliOnly: args.cli_only,
|
|
667
1195
|
});
|
|
1196
|
+
return success(res.conversations.map((c) => ({
|
|
1197
|
+
conversation_id: c.request_id, created_at: c.created_at ?? c.createdAt, test: isTestConversation(c),
|
|
1198
|
+
cli: isCliConversation(c), first_message: c.first_text,
|
|
1199
|
+
})));
|
|
1200
|
+
} catch (err) { return error(err); }
|
|
1201
|
+
},
|
|
1202
|
+
);
|
|
668
1203
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1204
|
+
reg.tool(
|
|
1205
|
+
'get_conversation',
|
|
1206
|
+
'Get one conversation and its messages (who said what, oldest first). format "testset" returns it as an evaluation test set with one scenario item to replay.',
|
|
1207
|
+
{
|
|
1208
|
+
agent_id: z.string().describe('Agent the conversation belongs to'),
|
|
1209
|
+
conversation_id: z.string().describe('Conversation (request) id'),
|
|
1210
|
+
format: z.enum(['messages', 'testset']).optional().describe('messages (default) or testset'),
|
|
1211
|
+
},
|
|
1212
|
+
async (args) => {
|
|
1213
|
+
try {
|
|
1214
|
+
const projectId = await projectOfAgent(client, args.agent_id);
|
|
1215
|
+
const [conversation, messages] = await Promise.all([
|
|
1216
|
+
conversations.get(projectId, args.conversation_id),
|
|
1217
|
+
conversations.messages(projectId, args.conversation_id, args.agent_id),
|
|
1218
|
+
]);
|
|
1219
|
+
if (args.format === 'testset') return success(conversationToTestSet(conversation, messages, args.agent_id));
|
|
1220
|
+
return success({
|
|
1221
|
+
conversation_id: conversation.request_id, test: conversation.isTestMode === true,
|
|
1222
|
+
url: conversationUrl(client, projectId, conversation.request_id),
|
|
1223
|
+
messages: messages.map((m) => ({ role: m.role, text: m.text, at: m.createdAt })),
|
|
673
1224
|
});
|
|
1225
|
+
} catch (err) { return error(err); }
|
|
1226
|
+
},
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
reg.tool(
|
|
1230
|
+
'mark_conversation_test',
|
|
1231
|
+
'Mark conversations as test, as the dashboard does: removed from analytics and usage billing reversed.',
|
|
1232
|
+
{
|
|
1233
|
+
agent_id: z.string().describe('Agent the conversations belong to'),
|
|
1234
|
+
conversation_ids: z.array(z.string()).min(1).max(100).describe('Conversation (request) ids'),
|
|
1235
|
+
},
|
|
1236
|
+
async (args) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const projectId = await projectOfAgent(client, args.agent_id);
|
|
1239
|
+
const done: string[] = [];
|
|
1240
|
+
for (const id of args.conversation_ids) {
|
|
1241
|
+
await conversations.markTest(projectId, id);
|
|
1242
|
+
done.push(id);
|
|
1243
|
+
}
|
|
1244
|
+
return success({ marked_test: done });
|
|
1245
|
+
} catch (err) { return error(err); }
|
|
1246
|
+
},
|
|
1247
|
+
);
|
|
674
1248
|
|
|
675
|
-
|
|
1249
|
+
const voice = new VoiceSDK(client);
|
|
676
1250
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1251
|
+
reg.tool(
|
|
1252
|
+
'list_voices',
|
|
1253
|
+
'Text-to-speech voices of a provider (cartesia default, or elevenlabs), filterable by language, gender and name.',
|
|
1254
|
+
{
|
|
1255
|
+
provider: z.enum(['cartesia', 'elevenlabs']).optional(),
|
|
1256
|
+
language: z.string().optional(),
|
|
1257
|
+
gender: z.enum(['masculine', 'feminine', 'gender_neutral']).optional(),
|
|
1258
|
+
search: z.string().optional(),
|
|
1259
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1260
|
+
},
|
|
1261
|
+
async (args) => {
|
|
1262
|
+
try { return success(await voice.listVoices({ provider: args.provider, language: args.language, gender: args.gender, q: args.search, limit: args.limit })); } catch (err) { return error(err); }
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
684
1265
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1266
|
+
reg.tool(
|
|
1267
|
+
'list_voice_models',
|
|
1268
|
+
'Speech provider and model catalog: stt (speech to text) or tts (text to speech).',
|
|
1269
|
+
{ kind: z.enum(['stt', 'tts']) },
|
|
1270
|
+
async (args) => {
|
|
1271
|
+
try { return success(await voice.voiceModels(args.kind)); } catch (err) { return error(err); }
|
|
1272
|
+
},
|
|
1273
|
+
);
|
|
688
1274
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1275
|
+
reg.tool(
|
|
1276
|
+
'get_agent_voice',
|
|
1277
|
+
'Voice settings of an agent (text-to-speech voice, speech-to-text, noise cancellation, interruption). They live on its phone number; an agent without one has none.',
|
|
1278
|
+
{ agent_id: z.string() },
|
|
1279
|
+
async (args) => {
|
|
1280
|
+
try { return success(await voice.getAgentVoice(args.agent_id)); } catch (err) { return error(err); }
|
|
1281
|
+
},
|
|
1282
|
+
);
|
|
694
1283
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1284
|
+
reg.tool(
|
|
1285
|
+
'set_agent_voice',
|
|
1286
|
+
'Change the text-to-speech voice of an agent. Take voice_id, name, gender, language and country_code from list_voices.',
|
|
1287
|
+
{
|
|
1288
|
+
agent_id: z.string(),
|
|
1289
|
+
provider: z.enum(['cartesia', 'elevenlabs']).optional(),
|
|
1290
|
+
voice_id: z.string(),
|
|
1291
|
+
name: z.string(),
|
|
1292
|
+
gender: z.string(),
|
|
1293
|
+
language: z.string(),
|
|
1294
|
+
country_code: z.string(),
|
|
1295
|
+
model: z.string().optional(),
|
|
1296
|
+
speed: z.number().optional(),
|
|
1297
|
+
},
|
|
1298
|
+
async (args) => {
|
|
1299
|
+
try {
|
|
1300
|
+
return success(await voice.setAgentVoice(args.agent_id, {
|
|
1301
|
+
provider: args.provider ?? 'cartesia', voice_id: args.voice_id, name: args.name, gender: args.gender, language: args.language,
|
|
1302
|
+
countryCode: args.country_code, ...(args.model ? { model: args.model } : {}), ...(args.speed ? { settings: { speed: args.speed } } : {}),
|
|
1303
|
+
}));
|
|
1304
|
+
} catch (err) { return error(err); }
|
|
1305
|
+
},
|
|
1306
|
+
);
|
|
699
1307
|
|
|
700
|
-
|
|
701
|
-
|
|
1308
|
+
reg.tool(
|
|
1309
|
+
'set_agent_transcriber',
|
|
1310
|
+
'Change how a voice agent hears callers (speech-to-text): provider and model from list_voice_models (kind stt), language, and key terms to listen for (names, products).',
|
|
1311
|
+
{
|
|
1312
|
+
agent_id: z.string(),
|
|
1313
|
+
provider: z.string(),
|
|
1314
|
+
model: z.string(),
|
|
1315
|
+
language: z.string(),
|
|
1316
|
+
keyterms: z.array(z.string()).optional().describe('Up to 50 words or phrases, 100 characters each'),
|
|
1317
|
+
},
|
|
1318
|
+
async (args) => {
|
|
1319
|
+
try { return success(await voice.setAgentTranscriber(args.agent_id, { provider: args.provider, model: args.model, language: args.language, keyterms: args.keyterms })); } catch (err) { return error(err); }
|
|
1320
|
+
},
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
reg.tool(
|
|
1324
|
+
'list_agent_numbers',
|
|
1325
|
+
'Phone numbers attached to an agent, or (without agent_id) every number the org owns.',
|
|
1326
|
+
{ agent_id: z.string().optional() },
|
|
1327
|
+
async (args) => {
|
|
1328
|
+
try { return success(args.agent_id ? await voice.agentNumbers(args.agent_id) : await voice.orgNumbers()); } catch (err) { return error(err); }
|
|
1329
|
+
},
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
reg.tool(
|
|
1333
|
+
'attach_phone_number',
|
|
1334
|
+
'Attach a phone number the org already owns to an agent. Never buys a number.',
|
|
1335
|
+
{ agent_id: z.string(), phone_number: z.string() },
|
|
1336
|
+
async (args) => {
|
|
1337
|
+
try { return success(await voice.attachNumber(args.agent_id, args.phone_number)); } catch (err) { return error(err); }
|
|
1338
|
+
},
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
reg.tool(
|
|
1342
|
+
'search_phone_numbers',
|
|
1343
|
+
'Numbers available to buy for a voice agent (US, CA, GB, AU; local or toll-free). Free: nothing is bought. First ask the person what kind of number they want (local with which area code, or toll-free), then search and show 4-5 numbers with their location, and buy only the one they choose.',
|
|
1344
|
+
{
|
|
1345
|
+
area_code: z.string().optional().describe('Three-digit area code, e.g. 331'),
|
|
1346
|
+
contains: z.string().optional().describe('Digits the number should contain'),
|
|
1347
|
+
country: z.enum(['US', 'CA', 'GB', 'AU']).optional().describe('Default US'),
|
|
1348
|
+
toll_free: z.boolean().optional(),
|
|
1349
|
+
limit: z.number().int().min(1).max(20).optional().describe('How many (default 5)'),
|
|
1350
|
+
},
|
|
1351
|
+
async (args) => {
|
|
1352
|
+
try { return success(await voice.searchNumbers({ areaCode: args.area_code, contains: args.contains, country: args.country, tollFree: args.toll_free, limit: args.limit })); } catch (err) { return error(err); }
|
|
1353
|
+
},
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
reg.tool(
|
|
1357
|
+
'get_phone_number',
|
|
1358
|
+
'The phone number of a voice agent: number, location, type, and a summary of its voice and transcriber.',
|
|
1359
|
+
{ agent_id: z.string() },
|
|
1360
|
+
async (args) => {
|
|
1361
|
+
try { return success(await voice.numberDetails(args.agent_id)); } catch (err) { return error(err); }
|
|
1362
|
+
},
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
reg.tool(
|
|
1366
|
+
'buy_phone_number',
|
|
1367
|
+
'Buy a number from search_phone_numbers and attach it to a voice agent (costs the org money every month; no undo here). Only after the person chose that exact number. The host asks the person to approve; without an approval form this is refused.',
|
|
1368
|
+
{
|
|
1369
|
+
agent_id: z.string(),
|
|
1370
|
+
phone_number: z.string().describe('The number the person chose, as search_phone_numbers returned it'),
|
|
1371
|
+
country: z.enum(['US', 'CA', 'GB', 'AU']).optional(),
|
|
1372
|
+
toll_free: z.boolean().optional(),
|
|
1373
|
+
},
|
|
1374
|
+
async (args, meta) => {
|
|
1375
|
+
try {
|
|
1376
|
+
// A purchase needs the person, not the model: confirm: true from a host without approval forms is not enough.
|
|
1377
|
+
if (!meta.confirmedByPerson) throw new Error('Buying a number needs a person to approve it in an approval form, and this host cannot show one. Nothing was bought. Buy it in the dashboard, or with `indemn agents numbers buy` in a terminal.');
|
|
1378
|
+
return success(await voice.buyNumber(args.agent_id, args.phone_number, { country: args.country, tollFree: args.toll_free }));
|
|
1379
|
+
} catch (err) { return error(err); }
|
|
1380
|
+
},
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
reg.tool(
|
|
1384
|
+
'detach_phone_number',
|
|
1385
|
+
'Detach a phone number from an agent; the org keeps owning it.',
|
|
1386
|
+
{ agent_id: z.string(), phone_number: z.string() },
|
|
1387
|
+
async (args) => {
|
|
1388
|
+
try { return success(await voice.detachNumber(args.agent_id, args.phone_number)); } catch (err) { return error(err); }
|
|
1389
|
+
},
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
reg.tool(
|
|
1393
|
+
'voice_preview',
|
|
1394
|
+
'A browser link for a test voice call with an agent (marked as test, valid 1 hour). Give the join_url to the user; do not share it further.',
|
|
1395
|
+
{ agent_id: z.string() },
|
|
1396
|
+
async (args) => {
|
|
1397
|
+
try { return success(await voice.voicePreview(args.agent_id)); } catch (err) { return error(err); }
|
|
1398
|
+
},
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
const distribution = new DistributionSDK(client);
|
|
1402
|
+
|
|
1403
|
+
reg.tool('list_distributions', 'Website paths that open agents (web widget) and the org\'s embed code.', {}, async () => {
|
|
1404
|
+
try { return success(await distribution.list()); } catch (err) { return error(err); }
|
|
1405
|
+
});
|
|
702
1406
|
|
|
703
|
-
|
|
1407
|
+
reg.tool('get_embed_code', 'The embed code and the website paths that open one agent.', { agent_id: z.string() }, async (args) => {
|
|
1408
|
+
try { return success(await distribution.embed(args.agent_id)); } catch (err) { return error(err); }
|
|
1409
|
+
});
|
|
704
1410
|
|
|
705
|
-
|
|
1411
|
+
reg.tool(
|
|
1412
|
+
'create_distribution',
|
|
1413
|
+
'Open an agent on a website path. Visitors of that page see it once the embed code is on the site.',
|
|
1414
|
+
{ agent_id: z.string(), host: z.string(), path: z.string(), path_expression: z.string().optional(), greeting: z.string().optional(), allow_upload: z.boolean().optional() },
|
|
1415
|
+
async (args) => {
|
|
1416
|
+
try {
|
|
1417
|
+
return success(await distribution.create({ agentId: args.agent_id, host: args.host, path: args.path, pathExpression: args.path_expression, greeting: args.greeting, allowUpload: args.allow_upload }));
|
|
1418
|
+
} catch (err) { return error(err); }
|
|
1419
|
+
},
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
reg.tool(
|
|
1423
|
+
'update_distribution',
|
|
1424
|
+
'Change a website distribution of this org: which agent it opens, its greeting or page pattern.',
|
|
1425
|
+
{ distribution_id: z.string(), agent_id: z.string().optional(), greeting: z.string().optional(), path_expression: z.string().optional() },
|
|
1426
|
+
async (args) => {
|
|
1427
|
+
try {
|
|
1428
|
+
const changes: Record<string, unknown> = {};
|
|
1429
|
+
if (args.agent_id) changes.id_bot = args.agent_id;
|
|
1430
|
+
if (args.greeting) changes.greetingMessage = args.greeting;
|
|
1431
|
+
if (args.path_expression) changes.pathExpression = args.path_expression;
|
|
1432
|
+
return success(await distribution.update(args.distribution_id, changes));
|
|
1433
|
+
} catch (err) { return error(err); }
|
|
1434
|
+
},
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
reg.tool('list_templates', 'Agent templates, shared by every org on the platform.', {}, async () => {
|
|
1438
|
+
try { return success(await distribution.listTemplates()); } catch (err) { return error(err); }
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
reg.tool(
|
|
1442
|
+
'create_template',
|
|
1443
|
+
'Publish an agent of this org as a template that every org can copy.',
|
|
1444
|
+
{ agent_id: z.string(), name: z.string(), description: z.string().optional(), category: z.array(z.string()).optional(), tags: z.array(z.string()).optional() },
|
|
1445
|
+
async (args) => {
|
|
1446
|
+
try { return success(await distribution.createTemplate({ agentId: args.agent_id, name: args.name, description: args.description, category: args.category, tags: args.tags })); } catch (err) { return error(err); }
|
|
1447
|
+
},
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
reg.tool('delete_template', 'Remove a template made from an agent of this org.', { template_id: z.string() }, async (args) => {
|
|
1451
|
+
try { return success(await distribution.deleteTemplate(args.template_id)); } catch (err) { return error(err); }
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
reg.tool(
|
|
1455
|
+
'create_agent_from_template',
|
|
1456
|
+
'Create a new agent in this org copied from a template (prompt, model, skills, knowledge bases; not phone numbers, voice settings or distributions).',
|
|
1457
|
+
{ template_id: z.string(), name: z.string().describe('Name of the new agent'), channels: z.array(z.string()).optional() },
|
|
1458
|
+
async (args) => {
|
|
1459
|
+
try {
|
|
1460
|
+
if ((await agents.findByName(args.name)).length) throw new Error(`An agent named "${args.name}" already exists in this org.`);
|
|
1461
|
+
return success(await distribution.createFromTemplate(args.template_id, { name: args.name, channels: args.channels }));
|
|
1462
|
+
} catch (err) { return error(err); }
|
|
1463
|
+
},
|
|
1464
|
+
);
|
|
1465
|
+
|
|
1466
|
+
reg.tool('list_solutions', 'Solutions of the org with their connected systems.', {}, async () => {
|
|
1467
|
+
try { return success(await distribution.listSolutions()); } catch (err) { return error(err); }
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
const analytics = new AnalyticsSDK(client);
|
|
1471
|
+
const orgSettings = new OrgSettingsSDK(client);
|
|
1472
|
+
const range = {
|
|
1473
|
+
date_from: z.string().optional().describe('YYYY-MM-DD, default 30 days ago'),
|
|
1474
|
+
date_to: z.string().optional().describe('YYYY-MM-DD, default today'),
|
|
1475
|
+
channel: z.enum(['web', 'voice', 'all']).optional(),
|
|
1476
|
+
agent_id: z.string().optional().describe('Only this agent'),
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
reg.tool(
|
|
1480
|
+
'get_analytics',
|
|
1481
|
+
'Dashboard analytics (test conversations excluded): metrics, model-performance, model-performance/bots, agent-volume, handoff-precursors, critical-feedback.',
|
|
1482
|
+
{ kind: z.enum(['metrics', 'model-performance', 'model-performance/bots', 'agent-volume', 'handoff-precursors', 'critical-feedback']), ...range },
|
|
1483
|
+
async (args) => {
|
|
1484
|
+
try { return success(await analytics.get(args.kind, { from: args.date_from, to: args.date_to, channel: args.channel, agentId: args.agent_id })); } catch (err) { return error(err); }
|
|
1485
|
+
},
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
reg.tool(
|
|
1489
|
+
'list_analytics_conversations',
|
|
1490
|
+
'The conversations behind an analytics number, filtered by a dimension (outcome, feedback, sentiment, precursor, reason, critical_feedback_theme, attribute:<key>).',
|
|
1491
|
+
{ ...range, dimension: z.string().optional(), value: z.string().optional(), search: z.string().optional(), page: z.number().int().min(1).optional(), page_size: z.number().int().min(1).max(200).optional() },
|
|
1492
|
+
async (args) => {
|
|
1493
|
+
try {
|
|
1494
|
+
return success(await analytics.conversations({ from: args.date_from, to: args.date_to, channel: args.channel, agentId: args.agent_id, dimension: args.dimension, value: args.value, search: args.search, page: args.page, pageSize: args.page_size }));
|
|
1495
|
+
} catch (err) { return error(err); }
|
|
1496
|
+
},
|
|
1497
|
+
);
|
|
1498
|
+
|
|
1499
|
+
reg.tool('list_custom_charts', 'Custom charts configured for the org.', {}, async () => {
|
|
1500
|
+
try { return success(await analytics.charts()); } catch (err) { return error(err); }
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
reg.tool('get_custom_chart', 'Data of one custom chart.', { chart_id: z.string(), ...range }, async (args) => {
|
|
1504
|
+
try { return success(await analytics.chart(args.chart_id, { from: args.date_from, to: args.date_to, channel: args.channel, agentId: args.agent_id })); } catch (err) { return error(err); }
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
reg.tool('get_org_settings', 'Org settings: timezone, business hours, holidays, handoff settings (read-only) and feature switches.', {}, async () => {
|
|
1508
|
+
try { return success(await orgSettings.get()); } catch (err) { return error(err); }
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
reg.tool('set_org_timezone', 'Set the org timezone (business hours are read in it).', { timezone: z.string().describe('For example America/New_York') }, async (args) => {
|
|
1512
|
+
try { return success(await orgSettings.setTimezone(args.timezone)); } catch (err) { return error(err); }
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
reg.tool(
|
|
1516
|
+
'set_business_hours',
|
|
1517
|
+
'Replace the org business hours. days: 0 Sunday to 6; an empty slots list closes that day.',
|
|
1518
|
+
{ enabled: z.boolean(), hours: z.array(z.object({ day: z.number().int().min(0).max(6), slots: z.array(z.object({ start: z.string().describe('HH:MM'), end: z.string().describe('HH:MM') })) })).optional(), out_of_hours_message: z.string().optional() },
|
|
1519
|
+
async (args) => {
|
|
1520
|
+
try {
|
|
1521
|
+
return success(await orgSettings.setBusinessHours({ businessHoursEnabled: args.enabled, businessHours: args.hours, ...(args.out_of_hours_message ? { businessHoursUtterance: args.out_of_hours_message } : {}) }));
|
|
1522
|
+
} catch (err) { return error(err); }
|
|
1523
|
+
},
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
reg.tool('add_holiday', 'Close the org on a date.', { date: z.string().describe('YYYY-MM-DD'), name: z.string(), message: z.string().optional().describe('What agents say that day') }, async (args) => {
|
|
1527
|
+
try { return success(await orgSettings.addHolidays([{ name: args.name, date: args.date, ...(args.message ? { holidayFallbackUtterance: args.message } : {}) }])); } catch (err) { return error(err); }
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
reg.tool(
|
|
1531
|
+
'set_org_admin_settings',
|
|
1532
|
+
'Indemn staff only: the org Settings → Admin tab (feature switches, Intake Manager name, web AI disclosure, conversation timers). Give only what should change; get_org_settings shows the current values.',
|
|
1533
|
+
{
|
|
1534
|
+
conversation_summaries: z.boolean().optional().describe('AI Conversation Summaries: an AI summary when a conversation is closed'),
|
|
1535
|
+
canned_responses: z.boolean().optional().describe('Canned Responses: saved replies agents can insert'),
|
|
1536
|
+
indemn_branding: z.boolean().optional().describe('Show Indemn Branding: the Indemn logo in the chat widget'),
|
|
1537
|
+
underwriting_portal: z.boolean().optional().describe('Underwriting Portal: the Intake Manager item in the sidebar'),
|
|
1538
|
+
underwriting_portal_name: z.string().optional().describe('Name of the Intake Manager item in the sidebar (the default is "Intake Manager")'),
|
|
1539
|
+
smart_inbox: z.boolean().optional().describe('Smart Inbox: email triage and intake'),
|
|
1540
|
+
solutions: z.boolean().optional().describe('Solutions: renewal desks, campaigns and book management'),
|
|
1541
|
+
tickets: z.boolean().optional().describe('Tickets: the tickets page and the ticket scan when a conversation is resolved'),
|
|
1542
|
+
agent_testing: z.boolean().optional().describe('Agent Testing Mode: internal agent testing tools and debug details'),
|
|
1543
|
+
composer_dictation: z.boolean().optional().describe('Composer Dictation: a mic button in the agents\' chat box'),
|
|
1544
|
+
web_ai_disclosure: z.boolean().optional().describe('Show Web AI Disclosure below the first AI reply in the web widget (needs its text)'),
|
|
1545
|
+
web_ai_disclosure_text: z.string().optional().describe('The web AI disclosure text'),
|
|
1546
|
+
ongoing_to_ended_minutes: z.number().int().min(0).optional().describe('Ongoing → Ended: minutes a quiet AI conversation waits before it ends (at least 1; 0 = the default, 30 minutes)'),
|
|
1547
|
+
ended_to_resolved_minutes: z.number().int().min(0).optional().describe('Ended → Resolved: minutes before an ended conversation resolves (at least 10; 0 = the default, 24 hours)'),
|
|
1548
|
+
resolved_to_archived_minutes: z.number().int().min(0).optional().describe('Resolved → Archived: minutes before a resolved conversation is archived (at least 1; 0 = the default, 7 days)'),
|
|
1549
|
+
},
|
|
1550
|
+
async (args) => {
|
|
1551
|
+
try {
|
|
1552
|
+
const { body } = await orgSettings.planAdmin(args);
|
|
1553
|
+
const saved = (await orgSettings.setAdminSettings(body)) as { orgConfigs?: Record<string, unknown> };
|
|
1554
|
+
return success({ saved: Object.fromEntries(Object.keys(body).map((k) => [k, saved?.orgConfigs?.[k] ?? body[k]])) });
|
|
1555
|
+
} catch (err) { return error(err); }
|
|
1556
|
+
},
|
|
1557
|
+
{ preview: async (a) => `This will ${(await orgSettings.planAdmin(a)).changes.join('; ')}.` },
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
reg.tool(
|
|
1561
|
+
'list_audit_logs',
|
|
1562
|
+
'Who changed what in the org, newest first, filtered here (the server has no filters).',
|
|
1563
|
+
{ since: z.string().optional(), action: z.string().optional(), resource_type: z.string().optional(), user_id: z.string().optional(), limit: z.number().int().min(1).max(500).optional() },
|
|
1564
|
+
async (args) => {
|
|
1565
|
+
try { return success(await new AuditSDK(client).list({ since: args.since, action: args.action, resourceType: args.resource_type, user: args.user_id, limit: args.limit })); } catch (err) { return error(err); }
|
|
1566
|
+
},
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1569
|
+
const tickets = new TicketsSDK(client);
|
|
1570
|
+
const ticketStatus = z.enum(['not_started', 'in_progress', 'completed']);
|
|
1571
|
+
const ticketPriority = z.enum(['urgent', 'high', 'normal', 'low']);
|
|
1572
|
+
|
|
1573
|
+
reg.tool(
|
|
1574
|
+
'list_tickets',
|
|
1575
|
+
'Support tickets of the org, newest first (the org must have tickets turned on).',
|
|
1576
|
+
{ status: z.array(ticketStatus).optional(), assignee_user_id: z.string().optional(), search: z.string().optional(), limit: z.number().int().min(1).max(500).optional() },
|
|
1577
|
+
async (args) => {
|
|
1578
|
+
try { return success(await tickets.list({ status: args.status, assignee: args.assignee_user_id, search: args.search, limit: args.limit })); } catch (err) { return error(err); }
|
|
1579
|
+
},
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
reg.tool('get_ticket', 'One ticket with its conversations and related tickets.', { ticket_id: z.string() }, async (args) => {
|
|
1583
|
+
try { return success(await tickets.get(args.ticket_id)); } catch (err) { return error(err); }
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
reg.tool(
|
|
1587
|
+
'create_ticket',
|
|
1588
|
+
'Create a ticket. Without force, similar open tickets are looked for first and nothing is created when some exist (the result lists them).',
|
|
1589
|
+
{
|
|
1590
|
+
title: z.string(), description: z.string().optional(), priority: ticketPriority.optional(),
|
|
1591
|
+
assignee_user_id: z.string().optional(), conversation_id: z.string().optional().describe('Conversation (request) id to attach'),
|
|
1592
|
+
force: z.boolean().optional(),
|
|
1593
|
+
},
|
|
1594
|
+
async (args) => {
|
|
1595
|
+
try {
|
|
1596
|
+
return success(await tickets.create({
|
|
1597
|
+
title: args.title, description: args.description, priority: args.priority,
|
|
1598
|
+
...(args.assignee_user_id ? { assignee: { type: 'user', target: args.assignee_user_id } } : {}),
|
|
1599
|
+
...(args.conversation_id ? { conversations: [{ request_id: args.conversation_id }] } : {}),
|
|
1600
|
+
}, { force: args.force }));
|
|
1601
|
+
} catch (err) { return error(err); }
|
|
1602
|
+
},
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
reg.tool(
|
|
1606
|
+
'update_ticket',
|
|
1607
|
+
'Change a ticket: fields, assignee (user id, or unassign: true), or close it with resolve: true (and an optional note).',
|
|
1608
|
+
{
|
|
1609
|
+
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(), notes: z.string().optional(),
|
|
1610
|
+
status: z.enum(['not_started', 'in_progress']).optional(), priority: ticketPriority.optional(),
|
|
1611
|
+
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(), note: z.string().optional(),
|
|
1612
|
+
link_ticket_id: z.string().optional().describe('Link to another ticket'),
|
|
1613
|
+
},
|
|
1614
|
+
async (args) => {
|
|
1615
|
+
try {
|
|
1616
|
+
const out: Record<string, unknown> = {};
|
|
1617
|
+
const changes: Record<string, unknown> = {};
|
|
1618
|
+
for (const k of ['title', 'description', 'notes', 'status', 'priority'] as const) if (args[k] !== undefined) changes[k] = args[k];
|
|
1619
|
+
if (Object.keys(changes).length) out.updated = await tickets.update(args.ticket_id, changes);
|
|
1620
|
+
if (args.assignee_user_id || args.unassign) out.assigned = await tickets.assign(args.ticket_id, args.unassign ? null : { type: 'user', target: args.assignee_user_id! });
|
|
1621
|
+
if (args.link_ticket_id) out.linked = await tickets.link(args.ticket_id, args.link_ticket_id);
|
|
1622
|
+
if (args.resolve) out.resolved = await tickets.resolve(args.ticket_id, args.note);
|
|
1623
|
+
if (!Object.keys(out).length) throw new Error('Nothing to change.');
|
|
1624
|
+
return success(out);
|
|
1625
|
+
} catch (err) { return error(err); }
|
|
1626
|
+
},
|
|
1627
|
+
);
|
|
1628
|
+
|
|
1629
|
+
reg.tool('archive_ticket', 'Archive a ticket.', { ticket_id: z.string() }, async (args) => {
|
|
1630
|
+
try { return success(await tickets.archive(args.ticket_id)); } catch (err) { return error(err); }
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
reg.tool('list_ticket_rules', 'Rules that let agents open tickets from conversations.', { agent_id: z.string().optional().describe('Only rules enabled for this agent') }, async (args) => {
|
|
1634
|
+
try { return success(await tickets.listRules({ agentId: args.agent_id })); } catch (err) { return error(err); }
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
reg.tool(
|
|
1638
|
+
'save_ticket_rule',
|
|
1639
|
+
'Create a ticket rule (prompt required; checked for overlaps unless force) or change one (rule_id).',
|
|
1640
|
+
{
|
|
1641
|
+
rule_id: z.string().optional(), name: z.string().max(75).optional(), prompt: z.string().optional(),
|
|
1642
|
+
agent_ids: z.array(z.string()).optional().describe('Only these agents; default all'),
|
|
1643
|
+
assignee_user_id: z.string().optional(), enabled: z.boolean().optional(), force: z.boolean().optional(),
|
|
1644
|
+
},
|
|
1645
|
+
async (args) => {
|
|
1646
|
+
try {
|
|
1647
|
+
return success(await tickets.saveRule({
|
|
1648
|
+
ruleId: args.rule_id, name: args.name, prompt: args.prompt, enabled: args.enabled,
|
|
1649
|
+
...(args.agent_ids ? { scope: { kind: 'specific', agents: args.agent_ids } } : args.rule_id ? {} : { scope: { kind: 'all_agents', agents: [] } }),
|
|
1650
|
+
...(args.assignee_user_id ? { assignment: { type: 'user', target: args.assignee_user_id } } : {}),
|
|
1651
|
+
}, { force: args.force }));
|
|
1652
|
+
} catch (err) { return error(err); }
|
|
1653
|
+
},
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
reg.tool('delete_ticket_rule', 'Delete a ticket rule.', { rule_id: z.string() }, async (args) => {
|
|
1657
|
+
try { return success(await tickets.deleteRule(args.rule_id)); } catch (err) { return error(err); }
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
reg.tool(
|
|
1661
|
+
'get_billing',
|
|
1662
|
+
'Read-only billing: status (subscription), invoices, unpaid (open invoices), usage_forecast, or plans. Nothing here changes a plan or moves money.',
|
|
1663
|
+
{ what: z.enum(['status', 'invoices', 'unpaid', 'usage_forecast', 'plans']) },
|
|
1664
|
+
async (args) => {
|
|
1665
|
+
try {
|
|
1666
|
+
const billing = new BillingSDK(client);
|
|
1667
|
+
const result = args.what === 'status' ? await billing.status()
|
|
1668
|
+
: args.what === 'invoices' ? await billing.invoices()
|
|
1669
|
+
: args.what === 'unpaid' ? await billing.unpaid()
|
|
1670
|
+
: args.what === 'usage_forecast' ? await billing.usageForecast()
|
|
1671
|
+
: await billing.plans();
|
|
1672
|
+
return success(result);
|
|
1673
|
+
} catch (err) { return error(err); }
|
|
1674
|
+
},
|
|
1675
|
+
);
|
|
1676
|
+
|
|
1677
|
+
reg.tool(
|
|
1678
|
+
'get_campaigns',
|
|
1679
|
+
'Read-only campaigns: list, one campaign (campaign_id), its counts / forecast / next-touchpoints / versions, templates, one template (template_id, versions: true for subject and body), or template skills.',
|
|
1680
|
+
{
|
|
1681
|
+
what: z.enum(['list', 'campaign', 'counts', 'forecast', 'next-touchpoints', 'versions', 'templates', 'template', 'template-skills']),
|
|
1682
|
+
campaign_id: z.string().optional(), template_id: z.string().optional(), versions: z.boolean().optional(),
|
|
1683
|
+
status: z.string().optional(), solution_id: z.string().optional(), channel: z.string().optional(),
|
|
1684
|
+
days: z.number().int().min(1).max(365).optional(), limit: z.number().int().min(1).max(500).optional(),
|
|
1685
|
+
},
|
|
1686
|
+
async (args) => {
|
|
1687
|
+
try {
|
|
1688
|
+
const c = new CampaignsSDK(client);
|
|
1689
|
+
const need = (v: string | undefined, name: string) => { if (!v) throw new Error(`${name} is required for ${args.what}.`); return v; };
|
|
1690
|
+
switch (args.what) {
|
|
1691
|
+
case 'list': return success(await c.list({ status: args.status, solutionId: args.solution_id, limit: args.limit }));
|
|
1692
|
+
case 'campaign': return success(await c.get(need(args.campaign_id, 'campaign_id')));
|
|
1693
|
+
case 'templates': return success(await c.templates({ channel: args.channel, solutionId: args.solution_id, limit: args.limit }));
|
|
1694
|
+
case 'template': return success(await c.template(need(args.template_id, 'template_id'), Boolean(args.versions)));
|
|
1695
|
+
case 'template-skills': return success(await c.templateSkills(args.solution_id));
|
|
1696
|
+
default: return success(await c.detail(need(args.campaign_id, 'campaign_id'), args.what, args.days));
|
|
1697
|
+
}
|
|
1698
|
+
} catch (err) { return error(err); }
|
|
1699
|
+
},
|
|
1700
|
+
);
|
|
1701
|
+
|
|
1702
|
+
const platform = new PlatformSDK(client);
|
|
1703
|
+
const platformResource = z.enum(['agents', 'templates', 'components', 'connectors', 'connector-bindings', 'scenarios']);
|
|
1704
|
+
|
|
1705
|
+
reg.tool(
|
|
1706
|
+
'platform_list',
|
|
1707
|
+
'List V2 platform items: agents, templates, components, connectors (shared catalogs), connector-bindings, scenarios (agent_id filters scenarios).',
|
|
1708
|
+
{ resource: platformResource, agent_id: z.string().optional() },
|
|
1709
|
+
async (args) => {
|
|
1710
|
+
try { return success(await platform.list(args.resource, args.agent_id ? { agent_id: args.agent_id } : undefined)); } catch (err) { return error(err); }
|
|
1711
|
+
},
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
reg.tool('platform_get', 'One V2 platform item by id.', { resource: platformResource, id: z.string() }, async (args) => {
|
|
1715
|
+
try { return success(await platform.get(args.resource, args.id)); } catch (err) { return error(err); }
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
reg.tool(
|
|
1719
|
+
'platform_save',
|
|
1720
|
+
'Create (no id) or change (id) a V2 agent, template, connector binding or scenario. Components and connectors are read-only shared catalogs.',
|
|
1721
|
+
{ resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body: z.record(z.unknown()) },
|
|
1722
|
+
async (args) => {
|
|
1723
|
+
try { return success(args.id ? await platform.update(args.resource, args.id, args.body) : await platform.create(args.resource, args.body)); } catch (err) { return error(err); }
|
|
1724
|
+
},
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
reg.tool('platform_delete', 'Delete a V2 agent, template, connector binding or scenario.', { resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string() }, async (args) => {
|
|
1728
|
+
try { return success(await platform.remove(args.resource, args.id)); } catch (err) { return error(err); }
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
reg.tool('validate_platform_template', 'Check a V2 template graph: components exist, edges point at real nodes.', { template_id: z.string() }, async (args) => {
|
|
1732
|
+
try { return success(await platform.validateTemplate(args.template_id)); } catch (err) { return error(err); }
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
reg.tool('run_simulation', 'Run scenarios against a V2 agent; poll simulation_status with the job id.', { agent_id: z.string(), scenario_ids: z.array(z.string()).optional() }, async (args) => {
|
|
1736
|
+
try { return success(await platform.runSimulation(args.agent_id, args.scenario_ids)); } catch (err) { return error(err); }
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
reg.tool(
|
|
1740
|
+
'simulation_status',
|
|
1741
|
+
'Simulation jobs (job_id for one) or results (result_id for one, or the latest of an agent).',
|
|
1742
|
+
{ job_id: z.string().optional(), result_id: z.string().optional(), agent_id: z.string().optional() },
|
|
1743
|
+
async (args) => {
|
|
1744
|
+
try {
|
|
1745
|
+
if (args.job_id) return success(await platform.simulationJob(args.job_id));
|
|
1746
|
+
if (args.result_id) return success(await platform.simulationResults({ resultId: args.result_id }));
|
|
1747
|
+
return success({ jobs: await platform.simulationJobs({ agentId: args.agent_id }), results: await platform.simulationResults({ agentId: args.agent_id }) });
|
|
1748
|
+
} catch (err) { return error(err); }
|
|
1749
|
+
},
|
|
1750
|
+
);
|
|
1751
|
+
|
|
1752
|
+
reg.tool('org_evaluations', 'Which agents of the org have a baseline evaluation (rubric and test set), and which are stale.', {}, async () => {
|
|
1753
|
+
try { return success(await platform.orgEvaluations()); } catch (err) { return error(err); }
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
reg.tool('jarvis_create_job', 'Ask Jarvis to generate baseline evaluations for 1 to 50 agents of the org. Poll jarvis_jobs with the job id; per-agent failures are in progress.results.', { bot_ids: z.array(z.string()).min(1).max(50) }, async (args) => {
|
|
1757
|
+
try { return success(await platform.createJarvisJob(args.bot_ids)); } catch (err) { return error(err); }
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
reg.tool('jarvis_jobs', 'Jarvis jobs of the org, or one job (job_id).', { job_id: z.string().optional(), status: z.enum(['pending', 'running', 'completed', 'failed']).optional() }, async (args) => {
|
|
1761
|
+
try { return success(args.job_id ? await platform.jarvisJob(args.job_id) : await platform.jarvisJobs(args.status)); } catch (err) { return error(err); }
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
reg.tool(
|
|
1765
|
+
'jarvis_chat',
|
|
1766
|
+
'One turn with Jarvis, the V2 platform assistant. Returns its reply, the tools it used, and a conversation_id to continue. agent_id tells Jarvis which V1 agent this is about.',
|
|
1767
|
+
{ message: z.string(), conversation_id: z.string().optional(), agent_id: z.string().optional() },
|
|
1768
|
+
async (args) => {
|
|
1769
|
+
try {
|
|
1770
|
+
return success(await jarvisChat(client, args.message, { conversationId: args.conversation_id, context: args.agent_id ? { federation_bot_id: args.agent_id } : undefined }));
|
|
1771
|
+
} catch (err) { return error(err); }
|
|
1772
|
+
},
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
reg.tool(
|
|
1776
|
+
'chat_with_agent',
|
|
1777
|
+
'Send one message to an agent and return its reply. Always a test conversation: never billed, never in analytics, shown with your name as tester. Pass the returned session_id to continue the same conversation.',
|
|
1778
|
+
{
|
|
1779
|
+
agent_id: z.string().describe('The agent ID to chat with'),
|
|
1780
|
+
message: z.string().describe('The message to send'),
|
|
1781
|
+
session_id: z.string().optional().describe('session_id from an earlier call, to continue that conversation'),
|
|
1782
|
+
end_conversation: z.boolean().optional().describe('End the conversation after this reply (default: keep it open so session_id can continue it)'),
|
|
1783
|
+
},
|
|
1784
|
+
async (args) => {
|
|
1785
|
+
try {
|
|
1786
|
+
const chat = await openTestChat(client, args.agent_id, { sessionId: args.session_id });
|
|
1787
|
+
try {
|
|
1788
|
+
const { greeting, response } = await exchange(chat, args.message);
|
|
1789
|
+
return success({
|
|
1790
|
+
session_id: chat.sessionId,
|
|
1791
|
+
test_conversation: true,
|
|
1792
|
+
tester: chat.tester,
|
|
1793
|
+
url: chat.url,
|
|
1794
|
+
greeting: chat.resumed ? undefined : greeting,
|
|
1795
|
+
response,
|
|
1796
|
+
});
|
|
1797
|
+
} finally {
|
|
1798
|
+
await chat.session.disconnect({ end: args.end_conversation === true });
|
|
1799
|
+
}
|
|
706
1800
|
} catch (err) { return error(err); }
|
|
707
1801
|
},
|
|
708
1802
|
);
|
|
@@ -711,73 +1805,231 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
711
1805
|
// Eval tools
|
|
712
1806
|
// ---------------------------------------------------------------------------
|
|
713
1807
|
|
|
714
|
-
|
|
1808
|
+
const testSetsForRun = new TestSetsSDK(client);
|
|
1809
|
+
|
|
1810
|
+
reg.tool(
|
|
715
1811
|
'run_evaluation',
|
|
716
|
-
'
|
|
1812
|
+
'Start an evaluation of an agent against its test set (paid model calls, cannot be cancelled; asks first). The agent must be in the org and the test set must belong to it. date_from/date_to grade real conversations (transcript mode). Poll get_eval_status; eval_gate_verdict judges a finished run.',
|
|
717
1813
|
{
|
|
718
|
-
|
|
719
|
-
|
|
1814
|
+
agent_id: z.string().optional().describe('The agent ID to evaluate'),
|
|
1815
|
+
bot_id: z.string().optional().describe('Same as agent_id (older name)'),
|
|
1816
|
+
test_set_id: z.string().describe('The test set ID (must belong to this agent)'),
|
|
720
1817
|
rubric_id: z.string().optional().describe('Optional rubric ID'),
|
|
721
|
-
concurrency: z.number().optional().describe('
|
|
722
|
-
eval_model: z.string().optional().describe('
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1818
|
+
concurrency: z.number().int().min(1).max(10).optional().describe('Parallel cases (default 1)'),
|
|
1819
|
+
eval_model: z.string().optional().describe('Judge model'),
|
|
1820
|
+
limit: z.number().int().min(1).optional().describe('Only the first N items'),
|
|
1821
|
+
component_scope: z.string().optional(),
|
|
1822
|
+
component_ids: z.array(z.string()).optional(),
|
|
1823
|
+
conversation_ids: z.array(z.string()).optional().describe('Transcript mode: these conversations'),
|
|
1824
|
+
date_from: z.string().optional().describe('Transcript mode: conversations from this ISO date'),
|
|
1825
|
+
date_to: z.string().optional().describe('Transcript mode: up to this ISO date, exclusive'),
|
|
1826
|
+
mode: z.enum(['transcript']).optional(),
|
|
1827
|
+
allow_other_agent: z.boolean().optional().describe('Run a test set written for another agent'),
|
|
728
1828
|
},
|
|
729
1829
|
async (args) => {
|
|
730
1830
|
try {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
};
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (args.
|
|
739
|
-
|
|
740
|
-
if (args.
|
|
741
|
-
if (args.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1831
|
+
const agentId = args.agent_id ?? args.bot_id;
|
|
1832
|
+
if (!agentId) throw new Error('Give agent_id: the agent to evaluate.');
|
|
1833
|
+
await evals.assertAgentInOrg(agentId);
|
|
1834
|
+
const ts = await testSetsForRun.getTestSet(args.test_set_id) as { agent_id?: string; items?: Array<{ type?: string }> };
|
|
1835
|
+
if (ts.agent_id && ts.agent_id !== agentId && !args.allow_other_agent) {
|
|
1836
|
+
throw new Error(`Test set ${args.test_set_id} was written for agent ${ts.agent_id}, not ${agentId}. Pass allow_other_agent: true to run it anyway.`);
|
|
1837
|
+
}
|
|
1838
|
+
if (Boolean(args.date_from) !== Boolean(args.date_to)) throw new Error('date_from and date_to go together.');
|
|
1839
|
+
const input: EvaluationTriggerInput = { bot_id: agentId, test_set_id: args.test_set_id };
|
|
1840
|
+
if (args.rubric_id) input.rubric_id = args.rubric_id;
|
|
1841
|
+
if (args.concurrency) input.concurrency = args.concurrency;
|
|
1842
|
+
// Voice simulations share call rooms; the dashboard runs them one at a time.
|
|
1843
|
+
if ((ts.items ?? []).some((i) => i.type === 'voice_simulation')) input.concurrency = 1;
|
|
1844
|
+
if (args.eval_model) input.eval_model = args.eval_model;
|
|
1845
|
+
if (args.limit) input.limit = args.limit;
|
|
1846
|
+
if (args.component_scope) input.component_scope = args.component_scope;
|
|
1847
|
+
if (args.component_ids) input.component_ids = args.component_ids;
|
|
1848
|
+
if (args.conversation_ids) input.conversation_ids = args.conversation_ids;
|
|
1849
|
+
if (args.mode) input.mode = args.mode;
|
|
1850
|
+
if (args.date_from) {
|
|
1851
|
+
input.date_from = args.date_from;
|
|
1852
|
+
input.date_to = args.date_to;
|
|
1853
|
+
input.mode = 'transcript';
|
|
1854
|
+
}
|
|
1855
|
+
return success(await evals.triggerEvaluation(input));
|
|
745
1856
|
} catch (err) { return error(err); }
|
|
746
1857
|
},
|
|
747
1858
|
);
|
|
748
1859
|
|
|
749
|
-
|
|
1860
|
+
reg.tool(
|
|
750
1861
|
'get_eval_status',
|
|
751
|
-
'
|
|
752
|
-
{
|
|
1862
|
+
'Status and counts of an evaluation run (passed, failed, infrastructure errors, latency). While the run is still going it waits up to wait_seconds for it to finish, so call it again only if it is still running.',
|
|
1863
|
+
{
|
|
1864
|
+
run_id: z.string().describe('The evaluation run ID'),
|
|
1865
|
+
wait_seconds: z.number().int().min(0).max(60).optional().describe('How long to wait for a running run to finish (default 20; 0 = answer at once)'),
|
|
1866
|
+
},
|
|
753
1867
|
async (args) => {
|
|
754
1868
|
try {
|
|
755
|
-
|
|
756
|
-
|
|
1869
|
+
// Waiting here instead of being called every few seconds: each call is a model turn for an agent.
|
|
1870
|
+
const deadline = Date.now() + 1000 * (args.wait_seconds ?? 20);
|
|
1871
|
+
let run = await evals.getRunInOrg(args.run_id);
|
|
1872
|
+
while (run.status !== 'completed' && run.status !== 'failed' && Date.now() < deadline) {
|
|
1873
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(5000, deadline - Date.now())));
|
|
1874
|
+
run = await evals.getRunStatus(args.run_id);
|
|
1875
|
+
}
|
|
1876
|
+
return success(run);
|
|
1877
|
+
} catch (err) { return error(err); }
|
|
1878
|
+
},
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
reg.tool(
|
|
1882
|
+
'eval_gate_verdict',
|
|
1883
|
+
'Judge a finished run like the CI gate: pass rate = passed/total, optional p95 limit, failed runs fail by default.',
|
|
1884
|
+
{
|
|
1885
|
+
run_id: z.string(),
|
|
1886
|
+
min_pass_rate: z.number().min(0).max(1).optional(),
|
|
1887
|
+
max_p95_ms: z.number().optional(),
|
|
1888
|
+
error_policy: z.enum(['block', 'warn']).optional(),
|
|
1889
|
+
fail_on_error: z.boolean().optional(),
|
|
1890
|
+
},
|
|
1891
|
+
async (args) => {
|
|
1892
|
+
try {
|
|
1893
|
+
const run = await evals.getRunInOrg(args.run_id);
|
|
1894
|
+
if (run.status !== 'completed' && run.status !== 'failed') return success({ finished: false, status: run.status });
|
|
1895
|
+
return success({ finished: true, ...gateVerdict(run, { minPassRate: args.min_pass_rate, maxP95Ms: args.max_p95_ms, errorPolicy: args.error_policy, failOnError: args.fail_on_error }) });
|
|
757
1896
|
} catch (err) { return error(err); }
|
|
758
1897
|
},
|
|
759
1898
|
);
|
|
760
1899
|
|
|
761
|
-
|
|
1900
|
+
reg.tool(
|
|
762
1901
|
'get_eval_results',
|
|
763
|
-
'
|
|
764
|
-
{
|
|
1902
|
+
'Per-case results of an evaluation run. response_format "concise" (default) keeps pass/fail, scores and failed criteria; "detailed" adds transcripts, tool calls and evidence.',
|
|
1903
|
+
{
|
|
1904
|
+
run_id: z.string().describe('The evaluation run ID'),
|
|
1905
|
+
response_format: z.enum(['concise', 'detailed']).optional(),
|
|
1906
|
+
failed_only: z.boolean().optional().describe('Only cases that did not pass'),
|
|
1907
|
+
},
|
|
765
1908
|
async (args) => {
|
|
766
1909
|
try {
|
|
767
|
-
|
|
768
|
-
|
|
1910
|
+
await evals.getRunInOrg(args.run_id);
|
|
1911
|
+
let results = await evals.getRunResults(args.run_id) as unknown as Array<Record<string, unknown>>;
|
|
1912
|
+
if (args.failed_only) results = results.filter((r) => r.passed !== true);
|
|
1913
|
+
if (args.response_format !== 'detailed') {
|
|
1914
|
+
results = results.map((r) => ({
|
|
1915
|
+
result_id: r.result_id, item_name: r.item_name, item_type: r.item_type, status: r.status, passed: r.passed,
|
|
1916
|
+
scores: r.scores, failed_criteria: Array.isArray(r.criteria_scores) ? (r.criteria_scores as Array<{ passed?: boolean }>).filter((c) => !c.passed) : undefined,
|
|
1917
|
+
failed_rules: Array.isArray(r.rubric_scores) ? (r.rubric_scores as Array<{ passed?: boolean }>).filter((c) => !c.passed) : undefined,
|
|
1918
|
+
error: r.error,
|
|
1919
|
+
}));
|
|
1920
|
+
}
|
|
1921
|
+
return success(results);
|
|
769
1922
|
} catch (err) { return error(err); }
|
|
770
1923
|
},
|
|
771
1924
|
);
|
|
772
1925
|
|
|
773
|
-
|
|
1926
|
+
reg.tool(
|
|
774
1927
|
'get_bot_context',
|
|
775
|
-
'
|
|
776
|
-
{ bot_id: z.string().describe('The
|
|
1928
|
+
'Everything the evaluator knows about an agent: prompt, tools, knowledge bases, LLM config.',
|
|
1929
|
+
{ bot_id: z.string().describe('The agent ID') },
|
|
777
1930
|
async (args) => {
|
|
778
1931
|
try {
|
|
779
|
-
|
|
780
|
-
return success(
|
|
1932
|
+
await evals.assertAgentInOrg(args.bot_id);
|
|
1933
|
+
return success(await evals.getBotContext(args.bot_id));
|
|
1934
|
+
} catch (err) { return error(err); }
|
|
1935
|
+
},
|
|
1936
|
+
);
|
|
1937
|
+
|
|
1938
|
+
reg.tool(
|
|
1939
|
+
'eval_trends',
|
|
1940
|
+
'Pass rate and latency over an agent\'s past runs, with drift when the pass rate dropped by 0.1 or more.',
|
|
1941
|
+
{ agent_id: z.string(), test_set_id: z.string().optional(), limit: z.number().int().min(1).max(200).optional() },
|
|
1942
|
+
async (args) => {
|
|
1943
|
+
try {
|
|
1944
|
+
await evals.assertAgentInOrg(args.agent_id);
|
|
1945
|
+
return success(await evals.trends(args.agent_id, args.test_set_id, args.limit));
|
|
1946
|
+
} catch (err) { return error(err); }
|
|
1947
|
+
},
|
|
1948
|
+
);
|
|
1949
|
+
|
|
1950
|
+
reg.tool(
|
|
1951
|
+
'compare_eval_runs',
|
|
1952
|
+
'Pass rate and p95 latency difference between two runs.',
|
|
1953
|
+
{ run_a: z.string(), run_b: z.string() },
|
|
1954
|
+
async (args) => {
|
|
1955
|
+
try {
|
|
1956
|
+
await evals.getRunInOrg(args.run_a);
|
|
1957
|
+
await evals.getRunInOrg(args.run_b);
|
|
1958
|
+
return success(await evals.compareRuns(args.run_a, args.run_b));
|
|
1959
|
+
} catch (err) { return error(err); }
|
|
1960
|
+
},
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
reg.tool(
|
|
1964
|
+
'eval_report_link',
|
|
1965
|
+
'Have the evaluations service render the PDF report of a completed run; returns a link valid for 15 minutes.',
|
|
1966
|
+
{ run_id: z.string(), force: z.boolean().optional().describe('Render again instead of reusing the last report') },
|
|
1967
|
+
async (args) => {
|
|
1968
|
+
try {
|
|
1969
|
+
await evals.getRunInOrg(args.run_id);
|
|
1970
|
+
return success(await evals.exportReport(args.run_id, args.force));
|
|
1971
|
+
} catch (err) { return error(err); }
|
|
1972
|
+
},
|
|
1973
|
+
);
|
|
1974
|
+
|
|
1975
|
+
reg.tool(
|
|
1976
|
+
'eval_quality_metrics',
|
|
1977
|
+
'Voice quality metrics a test item may require (names for expected.voice.required_metrics).',
|
|
1978
|
+
{},
|
|
1979
|
+
async () => {
|
|
1980
|
+
try { return success(await evals.qualityMetrics()); } catch (err) { return error(err); }
|
|
1981
|
+
},
|
|
1982
|
+
);
|
|
1983
|
+
|
|
1984
|
+
reg.tool(
|
|
1985
|
+
'list_prod_calls',
|
|
1986
|
+
'Ingested production voice calls of an agent, newest first; search matches the transcript.',
|
|
1987
|
+
{ agent_id: z.string(), search: z.string().optional(), limit: z.number().int().min(1).max(200).optional() },
|
|
1988
|
+
async (args) => {
|
|
1989
|
+
try {
|
|
1990
|
+
await evals.assertAgentInOrg(args.agent_id);
|
|
1991
|
+
return success(await evals.prodCalls(args.agent_id, { search: args.search, limit: args.limit }));
|
|
1992
|
+
} catch (err) { return error(err); }
|
|
1993
|
+
},
|
|
1994
|
+
);
|
|
1995
|
+
|
|
1996
|
+
reg.tool(
|
|
1997
|
+
'promote_prod_call',
|
|
1998
|
+
'Add an ingested production voice call as a test item to the agent\'s "Regressions (review)" test set.',
|
|
1999
|
+
{ session_id: z.string() },
|
|
2000
|
+
async (args) => {
|
|
2001
|
+
try {
|
|
2002
|
+
const call = await evals.prodCall(args.session_id);
|
|
2003
|
+
if (call.bot_id) await evals.assertAgentInOrg(String(call.bot_id));
|
|
2004
|
+
return success(await evals.promoteProdCall(args.session_id));
|
|
2005
|
+
} catch (err) { return error(err); }
|
|
2006
|
+
},
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
reg.tool(
|
|
2010
|
+
'evaluate_transcript',
|
|
2011
|
+
'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls).',
|
|
2012
|
+
{
|
|
2013
|
+
agent_id: z.string(),
|
|
2014
|
+
baseline_transcript: z.array(z.object({ sender: z.enum(['user', 'bot']), text: z.string() })),
|
|
2015
|
+
replay_transcript: z.array(z.object({ sender: z.enum(['user', 'bot']), text: z.string() })),
|
|
2016
|
+
},
|
|
2017
|
+
async (args) => {
|
|
2018
|
+
try {
|
|
2019
|
+
await evals.assertAgentInOrg(args.agent_id);
|
|
2020
|
+
return success(await evals.evaluateTranscript(args));
|
|
2021
|
+
} catch (err) { return error(err); }
|
|
2022
|
+
},
|
|
2023
|
+
);
|
|
2024
|
+
|
|
2025
|
+
reg.tool(
|
|
2026
|
+
'list_evaluator_configs',
|
|
2027
|
+
'Stored evaluator configs of an agent (which judges and rubric a run uses).',
|
|
2028
|
+
{ agent_id: z.string() },
|
|
2029
|
+
async (args) => {
|
|
2030
|
+
try {
|
|
2031
|
+
await evals.assertAgentInOrg(args.agent_id);
|
|
2032
|
+
return success(await evals.listEvaluatorConfigs(args.agent_id));
|
|
781
2033
|
} catch (err) { return error(err); }
|
|
782
2034
|
},
|
|
783
2035
|
);
|
|
@@ -786,91 +2038,96 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
786
2038
|
// Rubric tools
|
|
787
2039
|
// ---------------------------------------------------------------------------
|
|
788
2040
|
|
|
789
|
-
|
|
2041
|
+
reg.tool(
|
|
790
2042
|
'list_rubrics',
|
|
791
|
-
'
|
|
792
|
-
{ agent_id: z.string().
|
|
2043
|
+
'Evaluation rubrics (behaviour rules an agent is graded on) of one agent.',
|
|
2044
|
+
{ agent_id: z.string().describe('Agent ID (rubrics of other orgs are never listed)') },
|
|
793
2045
|
async (args) => {
|
|
794
2046
|
try {
|
|
2047
|
+
await evals.assertAgentInOrg(args.agent_id);
|
|
795
2048
|
const result = await rubrics.listRubrics(args.agent_id);
|
|
796
2049
|
return success(result);
|
|
797
2050
|
} catch (err) { return error(err); }
|
|
798
2051
|
},
|
|
799
2052
|
);
|
|
800
2053
|
|
|
801
|
-
|
|
2054
|
+
reg.tool(
|
|
802
2055
|
'get_rubric',
|
|
803
|
-
'
|
|
2056
|
+
'One rubric with its rules.',
|
|
804
2057
|
{ rubric_id: z.string().describe('The rubric ID') },
|
|
805
2058
|
async (args) => {
|
|
806
2059
|
try {
|
|
807
|
-
const result = await
|
|
2060
|
+
const result = await rubricInOrg(args.rubric_id);
|
|
808
2061
|
return success(result);
|
|
809
2062
|
} catch (err) { return error(err); }
|
|
810
2063
|
},
|
|
811
2064
|
);
|
|
812
2065
|
|
|
813
|
-
|
|
2066
|
+
reg.tool(
|
|
814
2067
|
'create_rubric',
|
|
815
|
-
'Create a
|
|
2068
|
+
'Create a rubric: 3 to 6 behaviour rules an agent must follow, each with a severity.',
|
|
816
2069
|
{
|
|
817
2070
|
name: z.string().describe('Rubric name'),
|
|
818
|
-
rules: z.array(
|
|
2071
|
+
rules: z.array(rubricRuleSchema).describe('The rubric rules'),
|
|
819
2072
|
agent_id: z.string().optional().describe('Agent ID to associate the rubric with'),
|
|
820
2073
|
description: z.string().optional().describe('Rubric description'),
|
|
821
2074
|
severity_definitions: z.record(z.string()).optional().describe('Severity level definitions'),
|
|
822
2075
|
},
|
|
823
2076
|
async (args) => {
|
|
824
2077
|
try {
|
|
825
|
-
const input: Record<string, unknown> = { name: args.name, rules: args.rules };
|
|
2078
|
+
const input: Record<string, unknown> = { name: args.name, rules: (args.rules as Array<Record<string, unknown>>).map(toRubricRule) };
|
|
826
2079
|
if (args.agent_id !== undefined) input.agent_id = args.agent_id;
|
|
827
2080
|
if (args.description !== undefined) input.description = args.description;
|
|
828
2081
|
if (args.severity_definitions !== undefined) input.severity_definitions = args.severity_definitions;
|
|
2082
|
+
if (args.agent_id) await evals.assertAgentInOrg(args.agent_id);
|
|
829
2083
|
const result = await rubrics.createRubric(input as any);
|
|
830
2084
|
return success(result);
|
|
831
2085
|
} catch (err) { return error(err); }
|
|
832
2086
|
},
|
|
833
2087
|
);
|
|
834
2088
|
|
|
835
|
-
|
|
2089
|
+
reg.tool(
|
|
836
2090
|
'update_rubric',
|
|
837
|
-
'
|
|
2091
|
+
'Change a rubric; this saves a new version.',
|
|
838
2092
|
{
|
|
839
2093
|
rubric_id: z.string().describe('The rubric ID'),
|
|
840
2094
|
name: z.string().optional().describe('Updated name'),
|
|
841
2095
|
description: z.string().optional().describe('Updated description'),
|
|
842
|
-
rules: z.array(
|
|
2096
|
+
rules: z.array(rubricRuleSchema).optional().describe('Updated rules'),
|
|
843
2097
|
},
|
|
844
2098
|
async (args) => {
|
|
845
2099
|
try {
|
|
846
2100
|
const input: Record<string, unknown> = {};
|
|
847
2101
|
if (args.name !== undefined) input.name = args.name;
|
|
848
2102
|
if (args.description !== undefined) input.description = args.description;
|
|
849
|
-
if (args.rules !== undefined) input.rules = args.rules;
|
|
2103
|
+
if (args.rules !== undefined) input.rules = (args.rules as Array<Record<string, unknown>>).map(toRubricRule);
|
|
2104
|
+
await rubricInOrg(args.rubric_id);
|
|
850
2105
|
const result = await rubrics.updateRubric(args.rubric_id, input as any);
|
|
851
2106
|
return success(result);
|
|
852
2107
|
} catch (err) { return error(err); }
|
|
853
2108
|
},
|
|
854
2109
|
);
|
|
855
2110
|
|
|
856
|
-
|
|
2111
|
+
reg.tool(
|
|
857
2112
|
'delete_rubric',
|
|
858
|
-
'Delete a rubric',
|
|
2113
|
+
'Delete a rubric.',
|
|
859
2114
|
{ rubric_id: z.string().describe('The rubric ID to delete') },
|
|
860
2115
|
async (args) => {
|
|
861
2116
|
try {
|
|
2117
|
+
await rubricInOrg(args.rubric_id);
|
|
862
2118
|
await rubrics.deleteRubric(args.rubric_id);
|
|
863
2119
|
return success({ deleted: true, rubric_id: args.rubric_id });
|
|
864
2120
|
} catch (err) { return error(err); }
|
|
865
2121
|
},
|
|
866
2122
|
);
|
|
867
2123
|
|
|
868
|
-
|
|
2124
|
+
reg.tool(
|
|
869
2125
|
'list_rubric_versions',
|
|
870
|
-
'
|
|
2126
|
+
'Saved versions of a rubric.',
|
|
871
2127
|
{ rubric_id: z.string().describe('The rubric ID') },
|
|
872
2128
|
async (args) => {
|
|
873
2129
|
try {
|
|
2130
|
+
await rubricInOrg(args.rubric_id);
|
|
874
2131
|
const result = await rubrics.listVersions(args.rubric_id);
|
|
875
2132
|
return success(result);
|
|
876
2133
|
} catch (err) { return error(err); }
|
|
@@ -881,89 +2138,94 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
881
2138
|
// Test set tools
|
|
882
2139
|
// ---------------------------------------------------------------------------
|
|
883
2140
|
|
|
884
|
-
|
|
2141
|
+
reg.tool(
|
|
885
2142
|
'list_test_sets',
|
|
886
|
-
'
|
|
887
|
-
{ agent_id: z.string().
|
|
2143
|
+
'Evaluation test sets (scenarios an agent is tested on) of one agent.',
|
|
2144
|
+
{ agent_id: z.string().describe('Agent ID (test sets of other orgs are never listed)') },
|
|
888
2145
|
async (args) => {
|
|
889
2146
|
try {
|
|
2147
|
+
await evals.assertAgentInOrg(args.agent_id);
|
|
890
2148
|
const result = await testSets.listTestSets(args.agent_id);
|
|
891
2149
|
return success(result);
|
|
892
2150
|
} catch (err) { return error(err); }
|
|
893
2151
|
},
|
|
894
2152
|
);
|
|
895
2153
|
|
|
896
|
-
|
|
2154
|
+
reg.tool(
|
|
897
2155
|
'get_test_set',
|
|
898
|
-
'
|
|
2156
|
+
'One test set with its items (single turns, scenarios, voice simulations).',
|
|
899
2157
|
{ test_set_id: z.string().describe('The test set ID') },
|
|
900
2158
|
async (args) => {
|
|
901
2159
|
try {
|
|
902
|
-
const result = await
|
|
2160
|
+
const result = await testSetInOrg(args.test_set_id);
|
|
903
2161
|
return success(result);
|
|
904
2162
|
} catch (err) { return error(err); }
|
|
905
2163
|
},
|
|
906
2164
|
);
|
|
907
2165
|
|
|
908
|
-
|
|
2166
|
+
reg.tool(
|
|
909
2167
|
'create_test_set',
|
|
910
|
-
'Create a
|
|
2168
|
+
'Create a test set for an agent: items with inputs and success criteria.',
|
|
911
2169
|
{
|
|
912
2170
|
name: z.string().describe('Test set name'),
|
|
913
|
-
items: z.array(
|
|
2171
|
+
items: z.array(testItemSchema).describe('The test items'),
|
|
914
2172
|
agent_id: z.string().optional().describe('Agent ID to associate the test set with'),
|
|
915
2173
|
description: z.string().optional().describe('Test set description'),
|
|
916
2174
|
},
|
|
917
2175
|
async (args) => {
|
|
918
2176
|
try {
|
|
919
|
-
const input: Record<string, unknown> = { name: args.name, items: args.items };
|
|
2177
|
+
const input: Record<string, unknown> = { name: args.name, items: (args.items as Array<Record<string, unknown>>).map(toTestItem) };
|
|
920
2178
|
if (args.agent_id !== undefined) input.agent_id = args.agent_id;
|
|
921
2179
|
if (args.description !== undefined) input.description = args.description;
|
|
2180
|
+
if (args.agent_id) await evals.assertAgentInOrg(args.agent_id);
|
|
922
2181
|
const result = await testSets.createTestSet(input as any);
|
|
923
2182
|
return success(result);
|
|
924
2183
|
} catch (err) { return error(err); }
|
|
925
2184
|
},
|
|
926
2185
|
);
|
|
927
2186
|
|
|
928
|
-
|
|
2187
|
+
reg.tool(
|
|
929
2188
|
'update_test_set',
|
|
930
|
-
'
|
|
2189
|
+
'Change a test set; this saves a new version.',
|
|
931
2190
|
{
|
|
932
2191
|
test_set_id: z.string().describe('The test set ID'),
|
|
933
2192
|
name: z.string().optional().describe('Updated name'),
|
|
934
2193
|
description: z.string().optional().describe('Updated description'),
|
|
935
|
-
items: z.array(
|
|
2194
|
+
items: z.array(testItemSchema).optional().describe('Updated test items'),
|
|
936
2195
|
},
|
|
937
2196
|
async (args) => {
|
|
938
2197
|
try {
|
|
939
2198
|
const input: Record<string, unknown> = {};
|
|
940
2199
|
if (args.name !== undefined) input.name = args.name;
|
|
941
2200
|
if (args.description !== undefined) input.description = args.description;
|
|
942
|
-
if (args.items !== undefined) input.items = args.items;
|
|
2201
|
+
if (args.items !== undefined) input.items = (args.items as Array<Record<string, unknown>>).map(toTestItem);
|
|
2202
|
+
await testSetInOrg(args.test_set_id);
|
|
943
2203
|
const result = await testSets.updateTestSet(args.test_set_id, input as any);
|
|
944
2204
|
return success(result);
|
|
945
2205
|
} catch (err) { return error(err); }
|
|
946
2206
|
},
|
|
947
2207
|
);
|
|
948
2208
|
|
|
949
|
-
|
|
2209
|
+
reg.tool(
|
|
950
2210
|
'delete_test_set',
|
|
951
|
-
'Delete a test set',
|
|
2211
|
+
'Delete a test set.',
|
|
952
2212
|
{ test_set_id: z.string().describe('The test set ID to delete') },
|
|
953
2213
|
async (args) => {
|
|
954
2214
|
try {
|
|
2215
|
+
await testSetInOrg(args.test_set_id);
|
|
955
2216
|
await testSets.deleteTestSet(args.test_set_id);
|
|
956
2217
|
return success({ deleted: true, test_set_id: args.test_set_id });
|
|
957
2218
|
} catch (err) { return error(err); }
|
|
958
2219
|
},
|
|
959
2220
|
);
|
|
960
2221
|
|
|
961
|
-
|
|
2222
|
+
reg.tool(
|
|
962
2223
|
'list_test_set_versions',
|
|
963
|
-
'
|
|
2224
|
+
'Saved versions of a test set.',
|
|
964
2225
|
{ test_set_id: z.string().describe('The test set ID') },
|
|
965
2226
|
async (args) => {
|
|
966
2227
|
try {
|
|
2228
|
+
await testSetInOrg(args.test_set_id);
|
|
967
2229
|
const result = await testSets.listVersions(args.test_set_id);
|
|
968
2230
|
return success(result);
|
|
969
2231
|
} catch (err) { return error(err); }
|
|
@@ -974,7 +2236,7 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
974
2236
|
// Export tools
|
|
975
2237
|
// ---------------------------------------------------------------------------
|
|
976
2238
|
|
|
977
|
-
|
|
2239
|
+
reg.tool(
|
|
978
2240
|
'export_eval_markdown',
|
|
979
2241
|
'Export evaluation results as a structured markdown file with conversations, scores, and criteria',
|
|
980
2242
|
{
|
|
@@ -989,7 +2251,7 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
989
2251
|
},
|
|
990
2252
|
);
|
|
991
2253
|
|
|
992
|
-
|
|
2254
|
+
reg.tool(
|
|
993
2255
|
'export_eval_report',
|
|
994
2256
|
'Generate a branded PDF evaluation report with cover page, scores, and per-item details',
|
|
995
2257
|
{
|
|
@@ -1004,7 +2266,7 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
1004
2266
|
},
|
|
1005
2267
|
);
|
|
1006
2268
|
|
|
1007
|
-
|
|
2269
|
+
reg.tool(
|
|
1008
2270
|
'export_agent_card',
|
|
1009
2271
|
'Generate a branded PDF agent card with system prompt, tools, KBs, LLM config, and evaluation history',
|
|
1010
2272
|
{
|
|
@@ -1018,4 +2280,6 @@ export function registerTools(server: McpServer, client: IndemnClient): void {
|
|
|
1018
2280
|
} catch (err) { return error(err); }
|
|
1019
2281
|
},
|
|
1020
2282
|
);
|
|
2283
|
+
|
|
2284
|
+
return reg;
|
|
1021
2285
|
}
|