@indemn/cli 1.6.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.
Files changed (311) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +9 -4
  3. package/.github/workflows/ci.yml +10 -0
  4. package/.github/workflows/publish.yml +2 -2
  5. package/.mcp.json +1 -4
  6. package/CONTRIBUTING.md +3 -1
  7. package/README.md +69 -176
  8. package/dist/cli/agents.d.ts.map +1 -1
  9. package/dist/cli/agents.js +226 -18
  10. package/dist/cli/agents.js.map +1 -1
  11. package/dist/cli/analytics.d.ts +3 -0
  12. package/dist/cli/analytics.d.ts.map +1 -0
  13. package/dist/cli/analytics.js +133 -0
  14. package/dist/cli/analytics.js.map +1 -0
  15. package/dist/cli/auth.d.ts.map +1 -1
  16. package/dist/cli/auth.js +62 -17
  17. package/dist/cli/auth.js.map +1 -1
  18. package/dist/cli/billing.d.ts +3 -0
  19. package/dist/cli/billing.d.ts.map +1 -0
  20. package/dist/cli/billing.js +23 -0
  21. package/dist/cli/billing.js.map +1 -0
  22. package/dist/cli/campaigns.d.ts +3 -0
  23. package/dist/cli/campaigns.d.ts.map +1 -0
  24. package/dist/cli/campaigns.js +34 -0
  25. package/dist/cli/campaigns.js.map +1 -0
  26. package/dist/cli/chat.d.ts.map +1 -1
  27. package/dist/cli/chat.js +69 -102
  28. package/dist/cli/chat.js.map +1 -1
  29. package/dist/cli/config.d.ts.map +1 -1
  30. package/dist/cli/config.js +71 -27
  31. package/dist/cli/config.js.map +1 -1
  32. package/dist/cli/conversations.d.ts +3 -0
  33. package/dist/cli/conversations.d.ts.map +1 -0
  34. package/dist/cli/conversations.js +129 -0
  35. package/dist/cli/conversations.js.map +1 -0
  36. package/dist/cli/distribution.d.ts +3 -0
  37. package/dist/cli/distribution.d.ts.map +1 -0
  38. package/dist/cli/distribution.js +122 -0
  39. package/dist/cli/distribution.js.map +1 -0
  40. package/dist/cli/eval.d.ts.map +1 -1
  41. package/dist/cli/eval.js +315 -71
  42. package/dist/cli/eval.js.map +1 -1
  43. package/dist/cli/functions.d.ts.map +1 -1
  44. package/dist/cli/functions.js +109 -38
  45. package/dist/cli/functions.js.map +1 -1
  46. package/dist/cli/index.js +63 -54
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/init.d.ts +27 -0
  49. package/dist/cli/init.d.ts.map +1 -0
  50. package/dist/cli/init.js +152 -0
  51. package/dist/cli/init.js.map +1 -0
  52. package/dist/cli/kb.d.ts.map +1 -1
  53. package/dist/cli/kb.js +141 -15
  54. package/dist/cli/kb.js.map +1 -1
  55. package/dist/cli/link.d.ts +21 -0
  56. package/dist/cli/link.d.ts.map +1 -0
  57. package/dist/cli/link.js +112 -0
  58. package/dist/cli/link.js.map +1 -0
  59. package/dist/cli/org.d.ts.map +1 -1
  60. package/dist/cli/org.js +79 -67
  61. package/dist/cli/org.js.map +1 -1
  62. package/dist/cli/platform.d.ts +3 -0
  63. package/dist/cli/platform.d.ts.map +1 -0
  64. package/dist/cli/platform.js +133 -0
  65. package/dist/cli/platform.js.map +1 -0
  66. package/dist/cli/tickets.d.ts +3 -0
  67. package/dist/cli/tickets.d.ts.map +1 -0
  68. package/dist/cli/tickets.js +125 -0
  69. package/dist/cli/tickets.js.map +1 -0
  70. package/dist/cli/utils/output.d.ts +15 -9
  71. package/dist/cli/utils/output.d.ts.map +1 -1
  72. package/dist/cli/utils/output.js +53 -46
  73. package/dist/cli/utils/output.js.map +1 -1
  74. package/dist/cli/voice.d.ts +3 -0
  75. package/dist/cli/voice.d.ts.map +1 -0
  76. package/dist/cli/voice.js +206 -0
  77. package/dist/cli/voice.js.map +1 -0
  78. package/dist/core/diff.d.ts +16 -0
  79. package/dist/core/diff.d.ts.map +1 -0
  80. package/dist/core/diff.js +57 -0
  81. package/dist/core/diff.js.map +1 -0
  82. package/dist/core/org-resolver.d.ts +37 -0
  83. package/dist/core/org-resolver.d.ts.map +1 -0
  84. package/dist/core/org-resolver.js +73 -0
  85. package/dist/core/org-resolver.js.map +1 -0
  86. package/dist/core/runtime.d.ts +36 -0
  87. package/dist/core/runtime.d.ts.map +1 -0
  88. package/dist/core/runtime.js +62 -0
  89. package/dist/core/runtime.js.map +1 -0
  90. package/dist/core/scope.d.ts +33 -0
  91. package/dist/core/scope.d.ts.map +1 -0
  92. package/dist/core/scope.js +37 -0
  93. package/dist/core/scope.js.map +1 -0
  94. package/dist/mcp/registry.d.ts +67 -0
  95. package/dist/mcp/registry.d.ts.map +1 -0
  96. package/dist/mcp/registry.js +195 -0
  97. package/dist/mcp/registry.js.map +1 -0
  98. package/dist/mcp/server.d.ts +9 -0
  99. package/dist/mcp/server.d.ts.map +1 -1
  100. package/dist/mcp/server.js +87 -2
  101. package/dist/mcp/server.js.map +1 -1
  102. package/dist/mcp/tool-meta.d.ts +21 -0
  103. package/dist/mcp/tool-meta.d.ts.map +1 -0
  104. package/dist/mcp/tool-meta.js +203 -0
  105. package/dist/mcp/tool-meta.js.map +1 -0
  106. package/dist/mcp/tools.d.ts +2 -1
  107. package/dist/mcp/tools.d.ts.map +1 -1
  108. package/dist/mcp/tools.js +1462 -215
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/sdk/agent-bundle.d.ts +99 -0
  111. package/dist/sdk/agent-bundle.d.ts.map +1 -0
  112. package/dist/sdk/agent-bundle.js +211 -0
  113. package/dist/sdk/agent-bundle.js.map +1 -0
  114. package/dist/sdk/agents.d.ts +54 -1
  115. package/dist/sdk/agents.d.ts.map +1 -1
  116. package/dist/sdk/agents.js +59 -5
  117. package/dist/sdk/agents.js.map +1 -1
  118. package/dist/sdk/analytics.d.ts +158 -0
  119. package/dist/sdk/analytics.d.ts.map +1 -0
  120. package/dist/sdk/analytics.js +185 -0
  121. package/dist/sdk/analytics.js.map +1 -0
  122. package/dist/sdk/auth.d.ts +34 -12
  123. package/dist/sdk/auth.d.ts.map +1 -1
  124. package/dist/sdk/auth.js +60 -77
  125. package/dist/sdk/auth.js.map +1 -1
  126. package/dist/sdk/billing.d.ts +25 -0
  127. package/dist/sdk/billing.d.ts.map +1 -0
  128. package/dist/sdk/billing.js +61 -0
  129. package/dist/sdk/billing.js.map +1 -0
  130. package/dist/sdk/campaigns.d.ts +35 -0
  131. package/dist/sdk/campaigns.d.ts.map +1 -0
  132. package/dist/sdk/campaigns.js +59 -0
  133. package/dist/sdk/campaigns.js.map +1 -0
  134. package/dist/sdk/chat.d.ts +25 -2
  135. package/dist/sdk/chat.d.ts.map +1 -1
  136. package/dist/sdk/chat.js +37 -10
  137. package/dist/sdk/chat.js.map +1 -1
  138. package/dist/sdk/client.d.ts +93 -5
  139. package/dist/sdk/client.d.ts.map +1 -1
  140. package/dist/sdk/client.js +362 -100
  141. package/dist/sdk/client.js.map +1 -1
  142. package/dist/sdk/config-change.d.ts +74 -0
  143. package/dist/sdk/config-change.d.ts.map +1 -0
  144. package/dist/sdk/config-change.js +117 -0
  145. package/dist/sdk/config-change.js.map +1 -0
  146. package/dist/sdk/config.d.ts.map +1 -1
  147. package/dist/sdk/config.js +2 -1
  148. package/dist/sdk/config.js.map +1 -1
  149. package/dist/sdk/conversations.d.ts +101 -0
  150. package/dist/sdk/conversations.d.ts.map +1 -0
  151. package/dist/sdk/conversations.js +155 -0
  152. package/dist/sdk/conversations.js.map +1 -0
  153. package/dist/sdk/credentials.d.ts +25 -0
  154. package/dist/sdk/credentials.d.ts.map +1 -0
  155. package/dist/sdk/credentials.js +44 -0
  156. package/dist/sdk/credentials.js.map +1 -0
  157. package/dist/sdk/distribution.d.ts +94 -0
  158. package/dist/sdk/distribution.d.ts.map +1 -0
  159. package/dist/sdk/distribution.js +100 -0
  160. package/dist/sdk/distribution.js.map +1 -0
  161. package/dist/sdk/eval.d.ts +81 -0
  162. package/dist/sdk/eval.d.ts.map +1 -1
  163. package/dist/sdk/eval.js +117 -0
  164. package/dist/sdk/eval.js.map +1 -1
  165. package/dist/sdk/functions.d.ts +14 -1
  166. package/dist/sdk/functions.d.ts.map +1 -1
  167. package/dist/sdk/functions.js +48 -5
  168. package/dist/sdk/functions.js.map +1 -1
  169. package/dist/sdk/index.d.ts +2 -0
  170. package/dist/sdk/index.d.ts.map +1 -1
  171. package/dist/sdk/index.js +2 -0
  172. package/dist/sdk/index.js.map +1 -1
  173. package/dist/sdk/jarvis-chat.d.ts +28 -0
  174. package/dist/sdk/jarvis-chat.d.ts.map +1 -0
  175. package/dist/sdk/jarvis-chat.js +117 -0
  176. package/dist/sdk/jarvis-chat.js.map +1 -0
  177. package/dist/sdk/kb.d.ts +69 -1
  178. package/dist/sdk/kb.d.ts.map +1 -1
  179. package/dist/sdk/kb.js +111 -24
  180. package/dist/sdk/kb.js.map +1 -1
  181. package/dist/sdk/orgs.d.ts +13 -0
  182. package/dist/sdk/orgs.d.ts.map +1 -0
  183. package/dist/sdk/orgs.js +59 -0
  184. package/dist/sdk/orgs.js.map +1 -0
  185. package/dist/sdk/platform.d.ts +62 -0
  186. package/dist/sdk/platform.d.ts.map +1 -0
  187. package/dist/sdk/platform.js +95 -0
  188. package/dist/sdk/platform.js.map +1 -0
  189. package/dist/sdk/session.d.ts +25 -0
  190. package/dist/sdk/session.d.ts.map +1 -0
  191. package/dist/sdk/session.js +69 -0
  192. package/dist/sdk/session.js.map +1 -0
  193. package/dist/sdk/skills.d.ts +46 -0
  194. package/dist/sdk/skills.d.ts.map +1 -0
  195. package/dist/sdk/skills.js +203 -0
  196. package/dist/sdk/skills.js.map +1 -0
  197. package/dist/sdk/test-chat.d.ts +52 -0
  198. package/dist/sdk/test-chat.d.ts.map +1 -0
  199. package/dist/sdk/test-chat.js +134 -0
  200. package/dist/sdk/test-chat.js.map +1 -0
  201. package/dist/sdk/tickets.d.ts +98 -0
  202. package/dist/sdk/tickets.d.ts.map +1 -0
  203. package/dist/sdk/tickets.js +91 -0
  204. package/dist/sdk/tickets.js.map +1 -0
  205. package/dist/sdk/types.d.ts +91 -6
  206. package/dist/sdk/types.d.ts.map +1 -1
  207. package/dist/sdk/voice.d.ts +147 -0
  208. package/dist/sdk/voice.d.ts.map +1 -0
  209. package/dist/sdk/voice.js +236 -0
  210. package/dist/sdk/voice.js.map +1 -0
  211. package/package.json +22 -3
  212. package/scripts/check-skills.mjs +25 -0
  213. package/skills/agent-builder/SKILL.md +89 -252
  214. package/skills/evaluations/SKILL.md +23 -234
  215. package/skills/evaluations/references/analysis.md +95 -0
  216. package/skills/evaluations/references/cli-reference.md +28 -0
  217. package/skills/evaluations/references/json-formats.md +100 -0
  218. package/src/cli/agents.ts +202 -22
  219. package/src/cli/analytics.ts +138 -0
  220. package/src/cli/auth.ts +56 -18
  221. package/src/cli/billing.ts +25 -0
  222. package/src/cli/campaigns.ts +33 -0
  223. package/src/cli/chat.ts +66 -115
  224. package/src/cli/config.ts +65 -31
  225. package/src/cli/conversations.ts +133 -0
  226. package/src/cli/distribution.ts +116 -0
  227. package/src/cli/eval.ts +254 -70
  228. package/src/cli/functions.ts +105 -39
  229. package/src/cli/index.ts +58 -55
  230. package/src/cli/init.ts +154 -0
  231. package/src/cli/kb.ts +120 -17
  232. package/src/cli/link.ts +117 -0
  233. package/src/cli/org.ts +81 -75
  234. package/src/cli/platform.ts +128 -0
  235. package/src/cli/tickets.ts +130 -0
  236. package/src/cli/utils/output.ts +51 -42
  237. package/src/cli/voice.ts +204 -0
  238. package/src/core/diff.ts +52 -0
  239. package/src/core/org-resolver.ts +113 -0
  240. package/src/core/runtime.ts +73 -0
  241. package/src/core/scope.ts +64 -0
  242. package/src/mcp/registry.ts +236 -0
  243. package/src/mcp/server.ts +84 -2
  244. package/src/mcp/tool-meta.ts +241 -0
  245. package/src/mcp/tools.ts +1529 -265
  246. package/src/sdk/agent-bundle.ts +245 -0
  247. package/src/sdk/agents.ts +74 -5
  248. package/src/sdk/analytics.ts +220 -0
  249. package/src/sdk/auth.ts +87 -107
  250. package/src/sdk/billing.ts +59 -0
  251. package/src/sdk/campaigns.ts +60 -0
  252. package/src/sdk/chat.ts +44 -10
  253. package/src/sdk/client.ts +373 -102
  254. package/src/sdk/config-change.ts +148 -0
  255. package/src/sdk/config.ts +2 -1
  256. package/src/sdk/conversations.ts +199 -0
  257. package/src/sdk/credentials.ts +53 -0
  258. package/src/sdk/distribution.ts +132 -0
  259. package/src/sdk/eval.ts +140 -0
  260. package/src/sdk/functions.ts +52 -6
  261. package/src/sdk/index.ts +2 -0
  262. package/src/sdk/jarvis-chat.ts +112 -0
  263. package/src/sdk/kb.ts +116 -29
  264. package/src/sdk/orgs.ts +62 -0
  265. package/src/sdk/platform.ts +115 -0
  266. package/src/sdk/session.ts +90 -0
  267. package/src/sdk/skills.ts +188 -0
  268. package/src/sdk/test-chat.ts +151 -0
  269. package/src/sdk/tickets.ts +120 -0
  270. package/src/sdk/types.ts +88 -6
  271. package/src/sdk/voice.ts +262 -0
  272. package/test/cli/entry.test.ts +34 -0
  273. package/test/cli/init.test.ts +66 -0
  274. package/test/cli/link.test.ts +57 -0
  275. package/test/core/org-resolver.test.ts +92 -0
  276. package/test/core/runtime.test.ts +30 -0
  277. package/test/core/scope.test.ts +93 -0
  278. package/test/harness.test.ts +24 -0
  279. package/test/helpers/config-home.ts +22 -0
  280. package/test/helpers/fake-middleware.ts +56 -0
  281. package/test/helpers/fetch-recorder.ts +75 -0
  282. package/test/helpers/jwt.ts +9 -0
  283. package/test/helpers/mcp-pair.ts +38 -0
  284. package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
  285. package/test/mcp/askers.test.ts +60 -0
  286. package/test/mcp/elicit-timeout.test.ts +29 -0
  287. package/test/mcp/registry.test.ts +169 -0
  288. package/test/mcp/shaping.test.ts +41 -0
  289. package/test/mcp/tools-snapshot.test.ts +42 -0
  290. package/test/sdk/agents.test.ts +232 -0
  291. package/test/sdk/analytics.test.ts +89 -0
  292. package/test/sdk/auth.test.ts +70 -0
  293. package/test/sdk/billing.test.ts +47 -0
  294. package/test/sdk/campaigns.test.ts +43 -0
  295. package/test/sdk/client.test.ts +80 -0
  296. package/test/sdk/config-change.test.ts +142 -0
  297. package/test/sdk/conversations.test.ts +80 -0
  298. package/test/sdk/credentials.test.ts +33 -0
  299. package/test/sdk/distribution.test.ts +55 -0
  300. package/test/sdk/eval.test.ts +155 -0
  301. package/test/sdk/kb-upload.test.ts +50 -0
  302. package/test/sdk/kb.test.ts +95 -0
  303. package/test/sdk/orgs.test.ts +48 -0
  304. package/test/sdk/platform.test.ts +89 -0
  305. package/test/sdk/renewal.test.ts +115 -0
  306. package/test/sdk/session.test.ts +60 -0
  307. package/test/sdk/skills.test.ts +235 -0
  308. package/test/sdk/test-chat.test.ts +112 -0
  309. package/test/sdk/tickets.test.ts +42 -0
  310. package/test/sdk/voice.test.ts +147 -0
  311. 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 { ChatSession } from '../sdk/chat.js';
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
- export function registerTools(server: McpServer, client: IndemnClient): void {
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
- server.tool(
259
+ reg.tool(
39
260
  'list_agents',
40
- 'List all agents in the organization',
41
- { limit: z.number().optional().describe('Maximum number of agents to return') },
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
- let result = await agents.listAgents();
45
- if (args.limit) result = result.slice(0, args.limit);
46
- return success(result);
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
- server.tool(
283
+ reg.tool(
52
284
  'get_agent',
53
- 'Get details of a specific agent',
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
- server.tool(
295
+ reg.tool(
64
296
  'create_agent',
65
- 'Create a new agent',
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 (e.g. ["WEB"])'),
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 result = await agents.createAgent({ name: args.name, channels: args.channels });
73
- return success(result);
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
- server.tool(
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
- 'Update an existing agent',
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
- server.tool(
373
+ reg.tool(
101
374
  'delete_agent',
102
- 'Delete an agent',
103
- { agent_id: z.string().describe('The agent ID to delete') },
104
- async (args) => {
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
- server.tool(
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
- 'Clone an existing agent',
115
- { agent_id: z.string().describe('The agent ID to clone') },
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
- const result = await agents.cloneAgent(args.agent_id);
119
- return success(result);
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
- server.tool(
489
+ reg.tool(
129
490
  'get_config',
130
- 'Get the full configuration for an agent',
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
- server.tool(
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
- 'Update configuration for an agent',
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('System prompt text'),
146
- first_message: z.string().optional().describe('First message text shown to users'),
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
- server.tool(
536
+ reg.tool(
160
537
  'get_prompt',
161
- 'Get the system prompt for an agent',
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
- server.tool(
548
+ reg.tool(
172
549
  'set_prompt',
173
- 'Set the system prompt for an agent',
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 text'),
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
- server.tool(
566
+ reg.tool(
191
567
  'list_functions',
192
- 'List all functions (tools) for an agent',
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
- server.tool(
578
+ reg.tool(
203
579
  'create_function',
204
- 'Create a new function (tool) for an agent',
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('Endpoint URL'),
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, ...input } = args;
216
- const result = await functions.createFunction(agent_id, input);
217
- return success(result);
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
- server.tool(
633
+ reg.tool(
223
634
  'update_function',
224
- 'Update an existing function (tool) for an agent',
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('Updated endpoint URL'),
231
- method: z.string().optional().describe('Updated HTTP method'),
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, function_id, ...input } = args;
236
- const result = await functions.updateFunction(agent_id, function_id, input);
237
- return success(result);
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
- server.tool(
669
+ reg.tool(
243
670
  'delete_function',
244
- 'Delete a function (tool) from an agent',
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 functions.deleteFunction(args.agent_id, args.function_id);
252
- return success({ deleted: true, agent_id: args.agent_id, function_id: args.function_id });
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
- server.tool(
685
+ reg.tool(
258
686
  'test_function',
259
- 'Test a function (tool) with optional parameters',
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: z.record(z.unknown()).optional().describe('Test parameters to send'),
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
- const result = await functions.testFunction(
268
- args.agent_id,
269
- args.function_id,
270
- args.params as Record<string, unknown> | undefined,
271
- );
272
- return success(result);
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
- server.tool(
708
+ reg.tool(
278
709
  'export_functions',
279
- 'Export all functions from an agent',
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
- server.tool(
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
- server.tool(
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
- 'List parameters for a function',
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
- server.tool(
791
+ reg.tool(
320
792
  'add_function_parameter',
321
- 'Add a parameter to a function',
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
- server.tool(
812
+ reg.tool(
340
813
  'update_function_parameter',
341
- 'Update a function parameter',
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
- server.tool(
834
+ reg.tool(
361
835
  'delete_function_parameter',
362
- 'Delete a function parameter',
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
- server.tool(
850
+ reg.tool(
377
851
  'list_master_functions',
378
- 'List available master function templates',
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 result = await functions.listMasterFunctions();
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
- server.tool(
863
+ reg.tool(
389
864
  'list_llm_models',
390
- 'List available LLM models',
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
- server.tool(
879
+ reg.tool(
405
880
  'list_knowledge_bases',
406
- 'List all knowledge bases in the organization',
407
- { limit: z.number().optional().describe('Maximum number of KBs to return') },
408
- async (args) => {
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
- let result = await kb.listKBs();
411
- if (args.limit) result = result.slice(0, args.limit);
412
- return success(result);
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
- server.tool(
893
+ reg.tool(
418
894
  'get_knowledge_base',
419
- 'Get details of a specific knowledge base',
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
- server.tool(
905
+ reg.tool(
430
906
  'create_knowledge_base',
431
- 'Create a new knowledge base',
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
- server.tool(
920
+ reg.tool(
445
921
  'update_knowledge_base',
446
- 'Update an existing knowledge base',
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
- server.tool(
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
- server.tool(
951
+ reg.tool(
476
952
  'list_kb_data',
477
- 'List data sources in a knowledge base',
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
- server.tool(
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
- server.tool(
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
- server.tool(
1009
+ reg.tool(
534
1010
  'upload_kb_file',
535
- 'Upload a file to a knowledge base',
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
- server.tool(
1024
+ reg.tool(
549
1025
  'update_kb_data',
550
- 'Update a data source entry in a knowledge base',
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
- server.tool(
1044
+ reg.tool(
569
1045
  'delete_kb_data',
570
- 'Delete a data source entry from a knowledge base',
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
- server.tool(
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
- 'Link knowledge bases to an agent',
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
- server.tool(
1130
+ reg.tool(
599
1131
  'unlink_kb',
600
- 'Unlink a knowledge base from an agent',
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
- server.tool(
1145
+ reg.tool(
614
1146
  'export_kb',
615
- 'Export knowledge base data',
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
- server.tool(
1160
+ reg.tool(
629
1161
  'import_kb',
630
- 'Import data into a knowledge base',
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
- server.tool(
648
- 'chat_with_agent',
649
- 'Send a message to an agent and get a response. Not interactive — sends one message and returns the reply.',
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('The agent ID (bot type) to chat with'),
652
- message: z.string().describe('The message to send'),
653
- session_id: z.string().optional().describe('Optional session ID to continue a conversation'),
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 middlewareUrl = client.getHosts().middleware;
658
- const chat = new ChatSession(args.agent_id, middlewareUrl, args.session_id);
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
- const greetingMessages: string[] = [];
670
- chat.on('message', (text: string) => {
671
- greetingMessages.push(text);
672
- messageCount++;
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
- const sessionId = await chat.connect();
1249
+ const voice = new VoiceSDK(client);
676
1250
 
677
- // Wait for greeting to finish streaming (up to 8s)
678
- await new Promise<void>((resolve) => {
679
- const check = setInterval(() => {
680
- if (!receiving) { clearInterval(check); resolve(); }
681
- }, 100);
682
- setTimeout(() => { clearInterval(check); resolve(); }, 8000);
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
- // Now swap the listener — next message is the actual response
686
- const greetingCount = messageCount;
687
- chat.removeAllListeners('message');
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
- const response = await new Promise<string>((resolve, reject) => {
690
- const timeout = setTimeout(() => {
691
- chat.disconnect();
692
- reject(new Error('Agent did not respond within 60 seconds'));
693
- }, 60000);
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
- chat.on('message', (text: string) => {
696
- clearTimeout(timeout);
697
- resolve(text);
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
- chat.send(args.message);
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
- chat.disconnect();
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
- return success({ session_id: sessionId, greeting: greetingMessages.slice(0, greetingCount).join('\n'), response });
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
- server.tool(
1808
+ const testSetsForRun = new TestSetsSDK(client);
1809
+
1810
+ reg.tool(
715
1811
  'run_evaluation',
716
- 'Trigger an evaluation run for a bot against a test set',
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
- bot_id: z.string().describe('The bot ID to evaluate'),
719
- test_set_id: z.string().describe('The test set ID'),
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('Number of concurrent evaluations'),
722
- eval_model: z.string().optional().describe('Model to use for evaluation judging'),
723
- keep: z.boolean().optional().describe('Keep evaluation data after completion'),
724
- limit: z.number().optional().describe('Limit items to evaluate'),
725
- component_scope: z.string().optional().describe('Component scope filter'),
726
- component_ids: z.array(z.string()).optional().describe('Component ID filter'),
727
- mode: z.string().optional().describe('Evaluation mode'),
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 input: Record<string, unknown> = {
732
- bot_id: args.bot_id,
733
- test_set_id: args.test_set_id,
734
- };
735
- if (args.rubric_id !== undefined) input.rubric_id = args.rubric_id;
736
- if (args.concurrency !== undefined) input.concurrency = args.concurrency;
737
- if (args.eval_model !== undefined) input.eval_model = args.eval_model;
738
- if (args.keep !== undefined) input.keep = args.keep;
739
- if (args.limit !== undefined) input.limit = args.limit;
740
- if (args.component_scope !== undefined) input.component_scope = args.component_scope;
741
- if (args.component_ids !== undefined) input.component_ids = args.component_ids;
742
- if (args.mode !== undefined) input.mode = args.mode;
743
- const result = await evals.triggerEvaluation(input as any);
744
- return success(result);
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
- server.tool(
1860
+ reg.tool(
750
1861
  'get_eval_status',
751
- 'Get the status of an evaluation run',
752
- { run_id: z.string().describe('The evaluation run ID') },
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
- const result = await evals.getRunStatus(args.run_id);
756
- return success(result);
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
- server.tool(
1900
+ reg.tool(
762
1901
  'get_eval_results',
763
- 'Get the detailed results of a completed evaluation run',
764
- { run_id: z.string().describe('The evaluation run ID') },
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
- const result = await evals.getRunResults(args.run_id);
768
- return success(result);
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
- server.tool(
1926
+ reg.tool(
774
1927
  'get_bot_context',
775
- 'Get the full context of a bot (prompt, tools, KBs, LLM config)',
776
- { bot_id: z.string().describe('The bot ID') },
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
- const result = await evals.getBotContext(args.bot_id);
780
- return success(result);
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
- server.tool(
2041
+ reg.tool(
790
2042
  'list_rubrics',
791
- 'List evaluation rubrics, optionally filtered by agent',
792
- { agent_id: z.string().optional().describe('Filter rubrics by agent ID') },
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
- server.tool(
2054
+ reg.tool(
802
2055
  'get_rubric',
803
- 'Get details of a specific rubric',
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 rubrics.getRubric(args.rubric_id);
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
- server.tool(
2066
+ reg.tool(
814
2067
  'create_rubric',
815
- 'Create a new evaluation rubric',
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(z.record(z.unknown())).describe('Array of rubric rules'),
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
- server.tool(
2089
+ reg.tool(
836
2090
  'update_rubric',
837
- 'Update an existing rubric',
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(z.record(z.unknown())).optional().describe('Updated rules'),
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
- server.tool(
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
- server.tool(
2124
+ reg.tool(
869
2125
  'list_rubric_versions',
870
- 'List all versions of a rubric',
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
- server.tool(
2141
+ reg.tool(
885
2142
  'list_test_sets',
886
- 'List test sets, optionally filtered by agent',
887
- { agent_id: z.string().optional().describe('Filter test sets by agent ID') },
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
- server.tool(
2154
+ reg.tool(
897
2155
  'get_test_set',
898
- 'Get details of a specific test set',
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 testSets.getTestSet(args.test_set_id);
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
- server.tool(
2166
+ reg.tool(
909
2167
  'create_test_set',
910
- 'Create a new test set',
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(z.record(z.unknown())).describe('Array of test items'),
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
- server.tool(
2187
+ reg.tool(
929
2188
  'update_test_set',
930
- 'Update an existing test set',
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(z.record(z.unknown())).optional().describe('Updated test items'),
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
- server.tool(
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
- server.tool(
2222
+ reg.tool(
962
2223
  'list_test_set_versions',
963
- 'List all versions of a test set',
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
- server.tool(
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
- server.tool(
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
- server.tool(
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
  }