@indemn/cli 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +6 -4
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts.map +1 -1
- package/dist/mcp/tool-meta.js +35 -9
- package/dist/mcp/tool-meta.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +235 -49
- package/dist/mcp/tools.js.map +1 -1
- package/dist/report/styles.d.ts +1 -0
- package/dist/report/styles.d.ts.map +1 -1
- package/dist/report/styles.js +5 -1
- package/dist/report/styles.js.map +1 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +11 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.d.ts +4 -0
- package/dist/sdk/config-change.d.ts.map +1 -1
- package/dist/sdk/config-change.js +27 -5
- package/dist/sdk/config-change.js.map +1 -1
- package/dist/sdk/kb.d.ts +13 -0
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +48 -6
- package/dist/sdk/kb.js.map +1 -1
- package/dist/sdk/voice.js +5 -5
- package/dist/sdk/voice.js.map +1 -1
- package/package.json +2 -2
- package/src/mcp/registry.ts +9 -4
- package/src/mcp/tool-meta.ts +35 -9
- package/src/mcp/tools.ts +238 -49
- package/src/report/styles.ts +5 -1
- package/src/sdk/client.ts +5 -1
- package/src/sdk/config-change.ts +27 -5
- package/src/sdk/kb.ts +53 -10
- package/src/sdk/voice.ts +5 -5
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +1 -0
- package/test/mcp/darwin-fixes.test.ts +249 -0
- package/test/mcp/tools-snapshot.test.ts +1 -1
package/src/mcp/tools.ts
CHANGED
|
@@ -54,9 +54,12 @@ function withAuthorization<T extends AuthFields & { configuration?: Record<strin
|
|
|
54
54
|
return { ...args, configuration: { ...(args.configuration ?? {}), authorization } };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
/** The CI gate's threshold when the caller gives none (evaluations repo scripts/eval_gate.py uses the same). */
|
|
58
|
+
const DEFAULT_MIN_PASS_RATE = 0.8;
|
|
59
|
+
|
|
60
|
+
// Helper: format a successful MCP tool result (the registry adds the context and formats it)
|
|
58
61
|
function success(data: unknown) {
|
|
59
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(data
|
|
62
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(data) }] };
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
// Helper: format an error MCP tool result
|
|
@@ -130,6 +133,20 @@ const parseJsonText = (v: unknown) => {
|
|
|
130
133
|
try { return JSON.parse(v); } catch { return v; }
|
|
131
134
|
};
|
|
132
135
|
const jsonObject = () => z.preprocess(parseJsonText, z.record(z.any()));
|
|
136
|
+
// A list a model may also write as JSON text.
|
|
137
|
+
const jsonArray = <T extends z.ZodTypeAny>(item: T) => z.preprocess(parseJsonText, z.array(item));
|
|
138
|
+
|
|
139
|
+
// What the knowledge base import accepts: typed, so a wrong shape is refused here instead of being
|
|
140
|
+
// "imported" as nothing (copilot answers success for any body).
|
|
141
|
+
const kbImportSchema = z.preprocess(parseJsonText, z.object({
|
|
142
|
+
type: z.enum(['file']).describe('Kind of import; files from the organization\'s file store'),
|
|
143
|
+
sources: z.array(z.object({
|
|
144
|
+
name: z.string().optional().describe('File name shown in the knowledge base'),
|
|
145
|
+
file_type: z.string().optional().describe('Extension, e.g. pdf, txt, docx (taken from the name when missing)'),
|
|
146
|
+
url: z.string().describe('The file\'s link in the file store'),
|
|
147
|
+
}).passthrough()).min(1),
|
|
148
|
+
is_qna_import: z.boolean().optional().describe('The file holds question/answer pairs'),
|
|
149
|
+
}).passthrough());
|
|
133
150
|
|
|
134
151
|
// A skill for bulk_create_skills with flat fields (the configuration is built as create_function builds it);
|
|
135
152
|
// a proposal from generate_skills_from_doc keeps its own configurationSchema.
|
|
@@ -170,12 +187,26 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
170
187
|
},
|
|
171
188
|
(err) => { if (what && notFound.test(err instanceof Error ? err.message : String(err))) missing.push(`${what} ${id}`); })
|
|
172
189
|
: undefined;
|
|
190
|
+
const agentId = typeof args.agent_id === 'string' ? args.agent_id : undefined;
|
|
191
|
+
const label = (name: unknown) => ({ name: typeof name === 'string' && name ? name : undefined });
|
|
173
192
|
await Promise.all([
|
|
174
193
|
add(args.agent_id ?? args.bot_id, 'agent', (id) => agents.getAgent(id)),
|
|
175
|
-
|
|
194
|
+
agentId ? add(args.function_id, 'skill', (id) => skillOf(agentId, id)) : undefined,
|
|
176
195
|
add(args.kb_id, 'knowledge base', (id) => kb.getKB(id)),
|
|
196
|
+
...((Array.isArray(args.kb_ids) ? args.kb_ids : []) as unknown[]).map((id) => add(id, 'knowledge base', (x) => kb.getKB(x))),
|
|
177
197
|
add(args.test_set_id, 'test set', (id) => testSets.getTestSet(id)),
|
|
178
198
|
add(args.rubric_id, 'rubric', (id) => rubrics.getRubric(id)),
|
|
199
|
+
// The rest name things the cards used to show as raw ids.
|
|
200
|
+
// Q&A entries come from the entry list; files and pages only from the knowledge base's sources.
|
|
201
|
+
typeof args.kb_id === 'string' ? add(args.source_id, null, (id) => kb.findDataSource(String(args.kb_id), id).then((d) => d
|
|
202
|
+
? label(d.question ?? d.name ?? d.source_url)
|
|
203
|
+
: kb.scrapeStatus(String(args.kb_id)).then((all) => { const s = all.find((x) => x.id === id); return label(s?.name ?? s?.source); }))) : undefined,
|
|
204
|
+
add(args.ticket_id, null, (id) => tickets.get(id).then((t) => { const x = t as { ticket_ref?: string; title?: string }; return label([x.ticket_ref, x.title].filter(Boolean).join(' ')); })),
|
|
205
|
+
add(args.rule_id, null, (id) => tickets.listRules({ includeTrashed: true }).then((rules) => label((rules as Array<{ _id?: string; id?: string; name?: string }>).find((r) => (r._id ?? r.id) === id)?.name))),
|
|
206
|
+
add(args.distribution_id, null, (id) => distribution.list().then((d) => { const p = (d.paths ?? []).find((x) => x.id === id); return label(p ? `${p.path_name ?? ''} (${p.host_name ?? '*'}${p.path_expression ?? ''})`.trim() : undefined); })),
|
|
207
|
+
add(args.template_id, null, (id) => distribution.listTemplates().then((ts) => label((ts as Array<{ _id?: string; name?: string }>).find((t) => t._id === id)?.name))),
|
|
208
|
+
add(args.owner_id, null, (id) => orgs.listMembers().then((ms) => { const m = ms.find((x) => x.user_id === id); return label(m?.name ?? m?.email); })),
|
|
209
|
+
agentId && typeof args.function_id === 'string' ? add(args.parameter_id, null, (id) => skillOf(agentId, String(args.function_id)).then((s) => functions.listParameters(agentId, s._id)).then((ps) => label(ps.find((p) => p._id === id)?.name))) : undefined,
|
|
179
210
|
add(orgId, null, (id) => client.copilotGet<{ organization?: { name?: string } }>(`/organization/${id}`).then((r) => r.organization)),
|
|
180
211
|
]);
|
|
181
212
|
return { names, missing };
|
|
@@ -231,6 +262,18 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
231
262
|
const config = new ConfigSDK(client);
|
|
232
263
|
const functions = new FunctionsSDK(client);
|
|
233
264
|
// A skill of an agent by its id.
|
|
265
|
+
/** An export result with a signed download_url: the services hand out unsigned file-store links (403 in a browser). */
|
|
266
|
+
const withDownloadLink = async <T extends object>(result: T, stored?: string) => {
|
|
267
|
+
if (!stored || typeof stored !== 'string') return result;
|
|
268
|
+
const signed = /X-Amz-Signature=/.test(stored) ? stored : await kb.signedLink(stored);
|
|
269
|
+
return { ...result, download_url: signed ?? stored, ...(signed ? {} : { note: 'The link could not be signed; it may need signing in to the dashboard.' }) };
|
|
270
|
+
};
|
|
271
|
+
/** A report file written by this process, put in the organization's file store so the person can download it. */
|
|
272
|
+
const deliver = async (result: { file_path: string; file_name: string; file_type: string }, upload?: boolean) => {
|
|
273
|
+
if (!upload) return result;
|
|
274
|
+
const { signedUrl, url } = await kb.storeFile(result.file_path);
|
|
275
|
+
return { ...result, download_url: signedUrl ?? url };
|
|
276
|
+
};
|
|
234
277
|
const skillOf = async (agentId: string, id: string) => {
|
|
235
278
|
const skill = (await functions.listFunctions(agentId)).find((f) => f._id === id);
|
|
236
279
|
if (!skill) throw new Error(`Skill ${id} is not on agent ${agentId}.`);
|
|
@@ -259,11 +302,15 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
259
302
|
reg.tool(
|
|
260
303
|
'list_agents',
|
|
261
304
|
'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.',
|
|
262
|
-
{
|
|
305
|
+
{
|
|
306
|
+
channel: z.string().optional().describe('Only agents on this channel, e.g. web, voice, email'),
|
|
307
|
+
search: z.string().optional().describe('Only agents whose name contains this text'),
|
|
308
|
+
},
|
|
263
309
|
async (args) => {
|
|
264
310
|
try {
|
|
265
311
|
const pages = await orgPages(client);
|
|
266
312
|
const wanted = args.channel?.trim().toUpperCase();
|
|
313
|
+
const needle = args.search?.trim().toLowerCase();
|
|
267
314
|
// What a person needs, not the stored record (a page of full records ran to 73k characters).
|
|
268
315
|
const rows = (await agents.listAgents()).map((a) => ({
|
|
269
316
|
_id: a._id,
|
|
@@ -275,7 +322,8 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
275
322
|
updated_at: a.updatedAt,
|
|
276
323
|
url: `${pages}/ai-studio/bot/${a._id}/configurations`,
|
|
277
324
|
}));
|
|
278
|
-
|
|
325
|
+
const byChannel = wanted ? rows.filter((a) => a.channels.some((c) => c.toUpperCase() === wanted)) : rows;
|
|
326
|
+
return success(needle ? byChannel.filter((a) => a.name.toLowerCase().includes(needle)) : byChannel);
|
|
279
327
|
} catch (err) { return error(err); }
|
|
280
328
|
},
|
|
281
329
|
);
|
|
@@ -392,7 +440,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
392
440
|
preview: async (a) => {
|
|
393
441
|
const agent = await agents.getAgent(String(a.agent_id));
|
|
394
442
|
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
|
|
443
|
+
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored.`;
|
|
396
444
|
},
|
|
397
445
|
},
|
|
398
446
|
);
|
|
@@ -450,7 +498,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
450
498
|
},
|
|
451
499
|
async (args) => {
|
|
452
500
|
try {
|
|
453
|
-
|
|
501
|
+
const copy = await agents.cloneAgent(args.agent_id, { name: args.name, channels: args.channels });
|
|
502
|
+
// copilot's clone answers { new_agent_id, new_agent_name, ... } and leaves the description empty:
|
|
503
|
+
// carry the original's over.
|
|
504
|
+
const copyId = copy._id ?? (copy as unknown as { new_agent_id?: string }).new_agent_id;
|
|
505
|
+
const source = await agents.getAgent(args.agent_id);
|
|
506
|
+
if (source.description && !copy.description && copyId) {
|
|
507
|
+
const described = await agents.updateAgent(copyId, { description: source.description }).then(() => true).catch(() => false);
|
|
508
|
+
return success({ ...copy, ...(described ? { description: source.description } : {}) });
|
|
509
|
+
}
|
|
510
|
+
return success(copy);
|
|
454
511
|
} catch (err) { return error(err); }
|
|
455
512
|
},
|
|
456
513
|
);
|
|
@@ -468,7 +525,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
468
525
|
'import_agent',
|
|
469
526
|
'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
527
|
{
|
|
471
|
-
bundle:
|
|
528
|
+
bundle: jsonObject().describe('The bundle returned by export_agent (an object, or JSON text)'),
|
|
472
529
|
name: z.string().optional().describe('Name for the new agent (default: the bundle name)'),
|
|
473
530
|
link_knowledge_bases: z.boolean().optional().describe('Link the same knowledge base ids if they exist in this org'),
|
|
474
531
|
allow_duplicate: z.boolean().optional(),
|
|
@@ -515,16 +572,18 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
515
572
|
system_prompt: a.system_prompt as string | undefined,
|
|
516
573
|
greeting: a.first_message as string | undefined,
|
|
517
574
|
model: a.model ? modelArg(String(a.model)) : undefined,
|
|
575
|
+
parameters: a.temperature !== undefined ? { temperature: a.temperature } : undefined,
|
|
518
576
|
});
|
|
519
577
|
|
|
520
578
|
reg.tool(
|
|
521
579
|
'set_config',
|
|
522
|
-
'Change an agent\'s prompt, greeting and/or
|
|
580
|
+
'Change an agent\'s prompt, greeting, model and/or temperature. 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).',
|
|
523
581
|
{
|
|
524
582
|
agent_id: z.string().describe('The agent ID'),
|
|
525
583
|
system_prompt: z.string().optional().describe('New system prompt (whole text)'),
|
|
526
584
|
first_message: z.string().optional().describe('New greeting shown to users'),
|
|
527
585
|
model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
|
|
586
|
+
temperature: z.number().min(0).max(2).optional().describe('Model temperature (0 = most predictable); only for models that expose it'),
|
|
528
587
|
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
529
588
|
},
|
|
530
589
|
async (args, meta) => {
|
|
@@ -592,10 +651,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
592
651
|
...skillAuthFields,
|
|
593
652
|
configuration: jsonObject().optional().describe('Other configurationSchema fields (an object, or JSON text); the fields above win'),
|
|
594
653
|
transfer_to: z.string().optional().describe('transfer_call: phone number (E.164, e.g. +15551234567) or SIP address (sip:...) calls are transferred to'),
|
|
654
|
+
parameters: jsonArray(z.object({
|
|
655
|
+
name: z.string(), type: z.string().describe('string, number or boolean'),
|
|
656
|
+
description: z.string().optional(), required: z.boolean().optional(),
|
|
657
|
+
allowed_values: z.array(z.string()).optional().describe('Fixed choices the customer picks from'),
|
|
658
|
+
})).optional().describe('What the skill collects from the customer before it runs ({{name}} placeholders in url/payload)'),
|
|
595
659
|
},
|
|
596
660
|
async (args) => {
|
|
597
661
|
try {
|
|
598
662
|
const { agent_id, type, name, description } = args;
|
|
663
|
+
const parameters = (args.parameters ?? []) as Array<{ name: string; type: string; description?: string; required?: boolean; allowed_values?: string[] }>;
|
|
599
664
|
const configurationSchema = skillConfiguration(type, withAuthorization(args));
|
|
600
665
|
// copilot sends a REST answer straight to the customer unless craftResponse is on; with nothing to say
|
|
601
666
|
// how to present it, let the agent phrase it.
|
|
@@ -613,9 +678,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
613
678
|
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
679
|
// copilot turns configurationSchema into the executable form only on update: create, then configure.
|
|
615
680
|
const created = await functions.createFunction(agent_id, { type, name, description });
|
|
616
|
-
|
|
681
|
+
// Parameters are their own records: a skill created with parameters used to lose them silently.
|
|
682
|
+
const addParameters = async () => {
|
|
683
|
+
const saved = [];
|
|
684
|
+
for (const { allowed_values, ...p } of parameters) saved.push(await functions.addParameter(agent_id, created._id, { ...p, allowedValues: allowed_values }));
|
|
685
|
+
return saved;
|
|
686
|
+
};
|
|
687
|
+
if (!configurationSchema) return success(parameters.length ? { ...created, parameters: await addParameters() } : created);
|
|
617
688
|
try {
|
|
618
|
-
|
|
689
|
+
const configured = await functions.updateFunction(agent_id, created._id, { type, description: description ?? '', configurationSchema });
|
|
690
|
+
return success(parameters.length ? { ...configured, parameters: await addParameters() } : configured);
|
|
619
691
|
} catch (err) {
|
|
620
692
|
// Leave no half-made skill behind: remove the empty one, then say why nothing was saved.
|
|
621
693
|
const why = err instanceof Error ? err.message : String(err);
|
|
@@ -707,12 +779,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
707
779
|
|
|
708
780
|
reg.tool(
|
|
709
781
|
'export_functions',
|
|
710
|
-
'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions).',
|
|
782
|
+
'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions); download_url is the link to give the person.',
|
|
711
783
|
{ agent_id: z.string().describe('The agent ID') },
|
|
712
784
|
async (args) => {
|
|
713
785
|
try {
|
|
714
|
-
const result = await functions.exportFunctions(args.agent_id);
|
|
715
|
-
return success(result);
|
|
786
|
+
const result = await functions.exportFunctions(args.agent_id) as { fileUrl?: string; signedUrl?: string; url?: string };
|
|
787
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl ?? result.url));
|
|
716
788
|
} catch (err) { return error(err); }
|
|
717
789
|
},
|
|
718
790
|
);
|
|
@@ -807,6 +879,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
807
879
|
return success(result);
|
|
808
880
|
} catch (err) { return error(err); }
|
|
809
881
|
},
|
|
882
|
+
liveChange('a skill parameter', ['function_id']),
|
|
810
883
|
);
|
|
811
884
|
|
|
812
885
|
reg.tool(
|
|
@@ -829,6 +902,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
829
902
|
return success(result);
|
|
830
903
|
} catch (err) { return error(err); }
|
|
831
904
|
},
|
|
905
|
+
liveChange('a skill parameter', ['function_id', 'parameter_id']),
|
|
832
906
|
);
|
|
833
907
|
|
|
834
908
|
reg.tool(
|
|
@@ -950,11 +1024,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
950
1024
|
|
|
951
1025
|
reg.tool(
|
|
952
1026
|
'list_kb_data',
|
|
953
|
-
'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base).',
|
|
1027
|
+
'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base). Each entry: id, question, answer, whether it is trained. full: true adds the stored chunks.',
|
|
954
1028
|
{
|
|
955
1029
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
956
1030
|
page: z.number().optional().describe('Page number'),
|
|
957
1031
|
limit: z.number().optional().describe('Items per page'),
|
|
1032
|
+
full: z.boolean().optional().describe('Include the chunks and statistics of every entry'),
|
|
958
1033
|
},
|
|
959
1034
|
async (args) => {
|
|
960
1035
|
try {
|
|
@@ -962,7 +1037,15 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
962
1037
|
if (args.page !== undefined) query.page = String(args.page);
|
|
963
1038
|
if (args.limit !== undefined) query.limit = String(args.limit);
|
|
964
1039
|
const result = await kb.listDataSources(args.kb_id, Object.keys(query).length ? query : undefined);
|
|
965
|
-
return success(result);
|
|
1040
|
+
if (args.full) return success(result);
|
|
1041
|
+
// The stored record repeats every chunk twice (chunks, chunks_text): a page of it is tens of kilobytes.
|
|
1042
|
+
const entries = (result.dataSources ?? []).map((d) => ({
|
|
1043
|
+
_id: d._id, type: d.type, question: d.question ?? d.name, answer: d.answer,
|
|
1044
|
+
...(d.source_url ? { source_url: d.source_url } : {}),
|
|
1045
|
+
trained: (d as { trained?: boolean }).trained, ...(d.status ? { status: d.status } : {}),
|
|
1046
|
+
...((d as { error_message?: string }).error_message ? { error: (d as { error_message?: string }).error_message } : {}),
|
|
1047
|
+
}));
|
|
1048
|
+
return success({ ...result, dataSources: entries });
|
|
966
1049
|
} catch (err) { return error(err); }
|
|
967
1050
|
},
|
|
968
1051
|
);
|
|
@@ -1144,30 +1227,37 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1144
1227
|
|
|
1145
1228
|
reg.tool(
|
|
1146
1229
|
'export_kb',
|
|
1147
|
-
'Export the question/answer pairs of a knowledge base as a CSV or XLSX
|
|
1230
|
+
'Export the question/answer pairs of a knowledge base as a CSV or XLSX file; download_url is the link to give the person (valid for a while).',
|
|
1148
1231
|
{
|
|
1149
1232
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1150
|
-
format: z.string().optional().describe('Export format (
|
|
1233
|
+
format: z.string().optional().describe('Export format: csv (default) or xlsx'),
|
|
1151
1234
|
},
|
|
1152
1235
|
async (args) => {
|
|
1153
1236
|
try {
|
|
1154
|
-
const result = await kb.exportKB(args.kb_id, args.format || '
|
|
1155
|
-
return success(result);
|
|
1237
|
+
const result = await kb.exportKB(args.kb_id, args.format || 'csv') as { fileUrl?: string; signedUrl?: string };
|
|
1238
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl));
|
|
1156
1239
|
} catch (err) { return error(err); }
|
|
1157
1240
|
},
|
|
1158
1241
|
);
|
|
1159
1242
|
|
|
1160
1243
|
reg.tool(
|
|
1161
1244
|
'import_kb',
|
|
1162
|
-
'Import
|
|
1245
|
+
'Import files (already in the organization\'s file store) into a knowledge base; processed in the background (see kb_scrape_status). For a website use scrape_url.',
|
|
1163
1246
|
{
|
|
1164
1247
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1165
|
-
data:
|
|
1248
|
+
data: kbImportSchema.describe('{ type: "file", sources: [{ name, file_type, url }] } (an object, or JSON text)'),
|
|
1166
1249
|
},
|
|
1167
1250
|
async (args) => {
|
|
1168
1251
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1252
|
+
const data = args.data as { type: string; sources: Array<{ name?: string; url: string; file_type?: string }> };
|
|
1253
|
+
// The import service tells the file's kind from file_type: fill it from the name or the link.
|
|
1254
|
+
const sources = data.sources.map((s) => ({
|
|
1255
|
+
...s,
|
|
1256
|
+
name: s.name ?? s.url.split('?')[0].split('/').pop() ?? 'file',
|
|
1257
|
+
file_type: (s.file_type ?? (s.name ?? s.url.split('?')[0]).split('.').pop() ?? '').toLowerCase(),
|
|
1258
|
+
}));
|
|
1259
|
+
const result = await kb.importKB(args.kb_id, { ...data, sources });
|
|
1260
|
+
return success({ ...(result as object), imported: sources.map((s) => s.name) });
|
|
1171
1261
|
} catch (err) { return error(err); }
|
|
1172
1262
|
},
|
|
1173
1263
|
);
|
|
@@ -1220,7 +1310,8 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1220
1310
|
return success({
|
|
1221
1311
|
conversation_id: conversation.request_id, test: conversation.isTestMode === true,
|
|
1222
1312
|
url: conversationUrl(client, projectId, conversation.request_id),
|
|
1223
|
-
|
|
1313
|
+
// System lines carry the dashboard's template ("{{userLabel}} has ended the call"): say it in words.
|
|
1314
|
+
messages: messages.map((m) => ({ role: m.role, text: (m.text ?? '').replace(/\{\{\s*userLabel\s*\}\}/g, 'The customer'), at: m.createdAt })),
|
|
1224
1315
|
});
|
|
1225
1316
|
} catch (err) { return error(err); }
|
|
1226
1317
|
},
|
|
@@ -1322,10 +1413,18 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1322
1413
|
|
|
1323
1414
|
reg.tool(
|
|
1324
1415
|
'list_agent_numbers',
|
|
1325
|
-
'Phone numbers attached to an agent, or (without agent_id) every number the org owns.',
|
|
1416
|
+
'Phone numbers attached to an agent, or (without agent_id) every number the org owns: the ones attached to an agent (with the agent) and the free ones.',
|
|
1326
1417
|
{ agent_id: z.string().optional() },
|
|
1327
1418
|
async (args) => {
|
|
1328
|
-
try {
|
|
1419
|
+
try {
|
|
1420
|
+
if (args.agent_id) return success(await voice.agentNumbers(args.agent_id));
|
|
1421
|
+
// copilot's org list holds only unattached ("reserve") numbers; the attached ones sit on the agents.
|
|
1422
|
+
const [free, all] = await Promise.all([voice.orgNumbers(), agents.listAgents()]);
|
|
1423
|
+
const attached = all
|
|
1424
|
+
.filter((a) => (a as { connected_phone?: string }).connected_phone)
|
|
1425
|
+
.map((a) => ({ phone_number: (a as { connected_phone?: string }).connected_phone, attached: true, agent_id: a._id, agent_name: a.name }));
|
|
1426
|
+
return success([...attached, ...free.map((n) => ({ phone_number: n.phoneNumber, attached: false, name: n.friendlyName, type: n.type }))]);
|
|
1427
|
+
} catch (err) { return error(err); }
|
|
1329
1428
|
},
|
|
1330
1429
|
);
|
|
1331
1430
|
|
|
@@ -1374,7 +1473,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1374
1473
|
async (args, meta) => {
|
|
1375
1474
|
try {
|
|
1376
1475
|
// 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
|
|
1476
|
+
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 instead.');
|
|
1378
1477
|
return success(await voice.buyNumber(args.agent_id, args.phone_number, { country: args.country, tollFree: args.toll_free }));
|
|
1379
1478
|
} catch (err) { return error(err); }
|
|
1380
1479
|
},
|
|
@@ -1512,19 +1611,64 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1512
1611
|
try { return success(await orgSettings.setTimezone(args.timezone)); } catch (err) { return error(err); }
|
|
1513
1612
|
});
|
|
1514
1613
|
|
|
1614
|
+
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
1615
|
+
const clock = (hhmm: string) => {
|
|
1616
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
1617
|
+
const hour = ((h + 11) % 12) + 1;
|
|
1618
|
+
return `${hour}${m ? `:${String(m).padStart(2, '0')}` : ''}${h < 12 ? 'am' : 'pm'}`;
|
|
1619
|
+
};
|
|
1620
|
+
/** What agents say about the hours, built from the hours themselves so it never contradicts them. */
|
|
1621
|
+
const hoursMessage = (days: Array<{ day: number; slots: Array<{ start: string; end: string }> }>) => {
|
|
1622
|
+
const open = days.filter((d) => d.slots.length).sort((a, b) => a.day - b.day);
|
|
1623
|
+
if (!open.length) return 'We are currently closed.';
|
|
1624
|
+
const parts = open.map((d) => `${DAY_NAMES[d.day]} ${d.slots.map((s) => `${clock(s.start)} to ${clock(s.end)}`).join(' and ')}`);
|
|
1625
|
+
return `We're open ${parts.join('; ')}.`;
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1515
1628
|
reg.tool(
|
|
1516
1629
|
'set_business_hours',
|
|
1517
|
-
'Replace the org business hours. days: 0 Sunday to 6
|
|
1518
|
-
{
|
|
1630
|
+
'Replace the org business hours: every day not listed is closed. days: 0 Sunday to 6. enabled turns enforcement on or off (unchanged when omitted). The out-of-hours message is rebuilt from the hours unless given.',
|
|
1631
|
+
{
|
|
1632
|
+
enabled: z.boolean().optional().describe('Enforce the hours (agents say the out-of-hours message outside them); unchanged when omitted'),
|
|
1633
|
+
hours: jsonArray(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(),
|
|
1634
|
+
out_of_hours_message: z.string().optional(),
|
|
1635
|
+
},
|
|
1519
1636
|
async (args) => {
|
|
1520
1637
|
try {
|
|
1521
|
-
|
|
1638
|
+
const current = (await orgSettings.get()) as { businessHoursConfig?: { businessHoursEnabled?: boolean } };
|
|
1639
|
+
const enabled = args.enabled ?? Boolean(current.businessHoursConfig?.businessHoursEnabled);
|
|
1640
|
+
const given = (args.hours ?? []) as Array<{ day: number; slots: Array<{ start: string; end: string }> }>;
|
|
1641
|
+
// Replace means replace: a day the person did not mention is closed.
|
|
1642
|
+
const days = args.hours ? DAY_NAMES.map((_, day) => ({ day, slots: given.filter((d) => d.day === day).flatMap((d) => d.slots) })) : undefined;
|
|
1643
|
+
return success(await orgSettings.setBusinessHours({
|
|
1644
|
+
businessHoursEnabled: enabled,
|
|
1645
|
+
...(days ? { businessHours: days, businessHoursUtterance: args.out_of_hours_message ?? hoursMessage(days) } : {}),
|
|
1646
|
+
...(!days && args.out_of_hours_message ? { businessHoursUtterance: args.out_of_hours_message } : {}),
|
|
1647
|
+
}));
|
|
1522
1648
|
} catch (err) { return error(err); }
|
|
1523
1649
|
},
|
|
1524
|
-
|
|
1650
|
+
{ preview: async (a) => {
|
|
1651
|
+
const hours = (a.hours ?? []) as Array<{ day: number; slots: Array<{ start: string; end: string }> }>;
|
|
1652
|
+
const what = a.hours ? hoursMessage(DAY_NAMES.map((_, day) => ({ day, slots: hours.filter((d) => d.day === day).flatMap((d) => d.slots) }))) : 'the hours unchanged';
|
|
1653
|
+
return `This will replace the org business hours: ${what}${a.enabled === undefined ? '' : a.enabled ? ' Enforced: agents say the out-of-hours message outside them.' : ' Not enforced.'}`;
|
|
1654
|
+
} },
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1657
|
+
reg.tool('add_holiday', 'Close the org on a date.', {
|
|
1658
|
+
date: z.string().describe('YYYY-MM-DD'), name: z.string(),
|
|
1659
|
+
message: z.string().optional().describe('What agents say that day (default: closed on account of the holiday)'),
|
|
1660
|
+
type: z.string().optional().describe('public or custom (default custom)'),
|
|
1661
|
+
}, async (args) => {
|
|
1662
|
+
try {
|
|
1663
|
+
return success(await orgSettings.addHolidays([{
|
|
1664
|
+
name: args.name, date: args.date, type: args.type ?? 'custom',
|
|
1665
|
+
holidayFallbackUtterance: args.message ?? `Our offices are currently closed on account of ${args.name}.`,
|
|
1666
|
+
}]));
|
|
1667
|
+
} catch (err) { return error(err); }
|
|
1668
|
+
});
|
|
1525
1669
|
|
|
1526
|
-
reg.tool('
|
|
1527
|
-
try { return success(await orgSettings.
|
|
1670
|
+
reg.tool('remove_holiday', 'Open the org again on a date that was a holiday.', { date: z.string().describe('YYYY-MM-DD, as get_org_settings lists it') }, async (args) => {
|
|
1671
|
+
try { return success(await orgSettings.removeHolidays([args.date])); } catch (err) { return error(err); }
|
|
1528
1672
|
});
|
|
1529
1673
|
|
|
1530
1674
|
reg.tool(
|
|
@@ -1562,7 +1706,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1562
1706
|
'Who changed what in the org, newest first, filtered here (the server has no filters).',
|
|
1563
1707
|
{ 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
1708
|
async (args) => {
|
|
1565
|
-
try {
|
|
1709
|
+
try {
|
|
1710
|
+
const entries = await new AuditSDK(client).list({ since: args.since, action: args.action, resourceType: args.resource_type, user: args.user_id, limit: args.limit });
|
|
1711
|
+
// copilot's log carries the person's id only: name them from the member list (one read).
|
|
1712
|
+
const members = entries.some((e) => e.id_user) ? await orgs.listMembers().catch(() => []) : [];
|
|
1713
|
+
const byId = new Map(members.map((m) => [m.user_id, m]));
|
|
1714
|
+
return success(entries.map((e) => {
|
|
1715
|
+
const m = byId.get(String(e.id_user ?? ''));
|
|
1716
|
+
return m ? { ...e, user_name: m.name, user_email: m.email } : e;
|
|
1717
|
+
}));
|
|
1718
|
+
} catch (err) { return error(err); }
|
|
1566
1719
|
},
|
|
1567
1720
|
);
|
|
1568
1721
|
|
|
@@ -1604,11 +1757,13 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1604
1757
|
|
|
1605
1758
|
reg.tool(
|
|
1606
1759
|
'update_ticket',
|
|
1607
|
-
'Change a ticket: fields, assignee (user id, or unassign: true), or close it with resolve: true
|
|
1760
|
+
'Change a ticket: fields, a note (added to its notes, or the closing note with resolve), assignee (user id, or unassign: true), or close it with resolve: true.',
|
|
1608
1761
|
{
|
|
1609
|
-
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1762
|
+
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1763
|
+
notes: z.string().optional().describe('Replace the whole notes text'),
|
|
1610
1764
|
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(),
|
|
1765
|
+
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(),
|
|
1766
|
+
note: z.string().optional().describe('A note to add: appended to the ticket\'s notes, or the closing note when resolving'),
|
|
1612
1767
|
link_ticket_id: z.string().optional().describe('Link to another ticket'),
|
|
1613
1768
|
},
|
|
1614
1769
|
async (args) => {
|
|
@@ -1616,6 +1771,11 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1616
1771
|
const out: Record<string, unknown> = {};
|
|
1617
1772
|
const changes: Record<string, unknown> = {};
|
|
1618
1773
|
for (const k of ['title', 'description', 'notes', 'status', 'priority'] as const) if (args[k] !== undefined) changes[k] = args[k];
|
|
1774
|
+
// A note outside resolve used to be dropped silently: it goes onto the ticket's notes.
|
|
1775
|
+
if (args.note && !args.resolve) {
|
|
1776
|
+
const existing = String(((await tickets.get(args.ticket_id)) as { notes?: unknown }).notes ?? '').trim();
|
|
1777
|
+
changes.notes = args.notes !== undefined ? `${args.notes}\n${args.note}`.trim() : existing ? `${existing}\n${args.note}` : args.note;
|
|
1778
|
+
}
|
|
1619
1779
|
if (Object.keys(changes).length) out.updated = await tickets.update(args.ticket_id, changes);
|
|
1620
1780
|
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
1781
|
if (args.link_ticket_id) out.linked = await tickets.link(args.ticket_id, args.link_ticket_id);
|
|
@@ -1718,7 +1878,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1718
1878
|
reg.tool(
|
|
1719
1879
|
'platform_save',
|
|
1720
1880
|
'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:
|
|
1881
|
+
{ resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body: jsonObject().describe('The item to save (an object, or JSON text)') },
|
|
1722
1882
|
async (args) => {
|
|
1723
1883
|
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
1884
|
},
|
|
@@ -1883,7 +2043,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1883
2043
|
'Judge a finished run like the CI gate: pass rate = passed/total, optional p95 limit, failed runs fail by default.',
|
|
1884
2044
|
{
|
|
1885
2045
|
run_id: z.string(),
|
|
1886
|
-
min_pass_rate: z.number().min(0).max(1).optional(),
|
|
2046
|
+
min_pass_rate: z.number().min(0).max(1).optional().describe('Pass rate the run must reach (default 0.8)'),
|
|
1887
2047
|
max_p95_ms: z.number().optional(),
|
|
1888
2048
|
error_policy: z.enum(['block', 'warn']).optional(),
|
|
1889
2049
|
fail_on_error: z.boolean().optional(),
|
|
@@ -1892,7 +2052,9 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1892
2052
|
try {
|
|
1893
2053
|
const run = await evals.getRunInOrg(args.run_id);
|
|
1894
2054
|
if (run.status !== 'completed' && run.status !== 'failed') return success({ finished: false, status: run.status });
|
|
1895
|
-
|
|
2055
|
+
// Without a threshold any finished run "passed" (a 1-of-3 run was reported as a pass): judge against 80%.
|
|
2056
|
+
const minPassRate = args.min_pass_rate ?? DEFAULT_MIN_PASS_RATE;
|
|
2057
|
+
return success({ finished: true, min_pass_rate: minPassRate, ...gateVerdict(run, { minPassRate, maxP95Ms: args.max_p95_ms, errorPolicy: args.error_policy, failOnError: args.fail_on_error }) });
|
|
1896
2058
|
} catch (err) { return error(err); }
|
|
1897
2059
|
},
|
|
1898
2060
|
);
|
|
@@ -2006,20 +2168,42 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2006
2168
|
},
|
|
2007
2169
|
);
|
|
2008
2170
|
|
|
2171
|
+
// get_conversation says "agent"; the evaluations service says "bot". Both are accepted.
|
|
2172
|
+
const transcriptTurn = z.object({ sender: z.enum(['user', 'bot', 'agent', 'system']), text: z.string() });
|
|
2173
|
+
const transcriptForService = (turns: Array<{ sender: string; text: string }>) => turns
|
|
2174
|
+
.filter((t) => t.sender !== 'system')
|
|
2175
|
+
.map((t) => ({ sender: (t.sender === 'user' ? 'user' : 'bot') as 'user' | 'bot', text: t.text }));
|
|
2176
|
+
const transcriptPreconditions = async (a: Record<string, unknown>) => {
|
|
2177
|
+
const agentId = String(a.agent_id);
|
|
2178
|
+
await evals.assertAgentInOrg(agentId);
|
|
2179
|
+
const configs = await evals.listEvaluatorConfigs(agentId) as { items?: unknown[] } | unknown[];
|
|
2180
|
+
const count = Array.isArray(configs) ? configs.length : (configs.items ?? []).length;
|
|
2181
|
+
if (!count) throw new Error('This agent has no evaluator yet, so a transcript cannot be graded. Create a rubric and run an evaluation first (run_evaluation stores the evaluator).');
|
|
2182
|
+
if (JSON.stringify(a.baseline_transcript) === JSON.stringify(a.replay_transcript)) {
|
|
2183
|
+
throw new Error('The baseline and the replay are the same conversation; grading compares a replay with its baseline. Give the replayed conversation as replay_transcript.');
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2009
2187
|
reg.tool(
|
|
2010
2188
|
'evaluate_transcript',
|
|
2011
|
-
'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls).',
|
|
2189
|
+
'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls). The agent needs an evaluator (a finished run), and the replay must differ from the baseline.',
|
|
2012
2190
|
{
|
|
2013
2191
|
agent_id: z.string(),
|
|
2014
|
-
baseline_transcript:
|
|
2015
|
-
replay_transcript:
|
|
2192
|
+
baseline_transcript: jsonArray(transcriptTurn),
|
|
2193
|
+
replay_transcript: jsonArray(transcriptTurn),
|
|
2016
2194
|
},
|
|
2017
2195
|
async (args) => {
|
|
2018
2196
|
try {
|
|
2019
|
-
await
|
|
2020
|
-
return success(await evals.evaluateTranscript(
|
|
2197
|
+
await transcriptPreconditions(args);
|
|
2198
|
+
return success(await evals.evaluateTranscript({
|
|
2199
|
+
agent_id: args.agent_id,
|
|
2200
|
+
baseline_transcript: transcriptForService(args.baseline_transcript),
|
|
2201
|
+
replay_transcript: transcriptForService(args.replay_transcript),
|
|
2202
|
+
}));
|
|
2021
2203
|
} catch (err) { return error(err); }
|
|
2022
2204
|
},
|
|
2205
|
+
// Checked before the approval card: a card for something that cannot run wastes the person's answer.
|
|
2206
|
+
{ preview: async (a) => { await transcriptPreconditions(a); return `This will grade a replayed transcript for agent ${a.agent_id} with paid model calls.`; } },
|
|
2023
2207
|
);
|
|
2024
2208
|
|
|
2025
2209
|
reg.tool(
|
|
@@ -2236,17 +2420,20 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2236
2420
|
// Export tools
|
|
2237
2421
|
// ---------------------------------------------------------------------------
|
|
2238
2422
|
|
|
2423
|
+
const uploadArg = z.boolean().optional().describe('Put the file in the organization\'s file store and return download_url (use it when the person cannot reach this machine\'s files)');
|
|
2424
|
+
|
|
2239
2425
|
reg.tool(
|
|
2240
2426
|
'export_eval_markdown',
|
|
2241
2427
|
'Export evaluation results as a structured markdown file with conversations, scores, and criteria',
|
|
2242
2428
|
{
|
|
2243
2429
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2244
2430
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2431
|
+
upload: uploadArg,
|
|
2245
2432
|
},
|
|
2246
2433
|
async (args) => {
|
|
2247
2434
|
try {
|
|
2248
2435
|
const result = await exportEvalMarkdown(args.run_id, args.output_path, client);
|
|
2249
|
-
return success(result);
|
|
2436
|
+
return success(await deliver(result, args.upload));
|
|
2250
2437
|
} catch (err) { return error(err); }
|
|
2251
2438
|
},
|
|
2252
2439
|
);
|
|
@@ -2257,11 +2444,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2257
2444
|
{
|
|
2258
2445
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2259
2446
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2447
|
+
upload: uploadArg,
|
|
2260
2448
|
},
|
|
2261
2449
|
async (args) => {
|
|
2262
2450
|
try {
|
|
2263
2451
|
const result = await exportEvalReport(args.run_id, args.output_path, client);
|
|
2264
|
-
return success(result);
|
|
2452
|
+
return success(await deliver(result, args.upload));
|
|
2265
2453
|
} catch (err) { return error(err); }
|
|
2266
2454
|
},
|
|
2267
2455
|
);
|
|
@@ -2272,11 +2460,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2272
2460
|
{
|
|
2273
2461
|
agent_id: z.string().describe('The agent ID'),
|
|
2274
2462
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2463
|
+
upload: uploadArg,
|
|
2275
2464
|
},
|
|
2276
2465
|
async (args) => {
|
|
2277
2466
|
try {
|
|
2278
2467
|
const result = await exportAgentCard(args.agent_id, args.output_path, client);
|
|
2279
|
-
return success(result);
|
|
2468
|
+
return success(await deliver(result, args.upload));
|
|
2280
2469
|
} catch (err) { return error(err); }
|
|
2281
2470
|
},
|
|
2282
2471
|
);
|
package/src/report/styles.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import { Font, StyleSheet } from '@react-pdf/renderer';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
+
// Fonts and logo: copied next to the built code by `npm run build`; the published package also carries
|
|
9
|
+
// src/report/assets, so an install whose build step skipped the copy still finds them.
|
|
10
|
+
const candidates = [join(__dirname, 'assets'), join(__dirname, '..', '..', 'src', 'report', 'assets')];
|
|
11
|
+
export const assetsDir = candidates.find((dir) => existsSync(join(dir, 'fonts', 'Barlow-Regular.ttf'))) ?? candidates[0];
|
|
8
12
|
|
|
9
13
|
// Register Barlow font family from bundled files
|
|
10
14
|
Font.register({
|
package/src/sdk/client.ts
CHANGED
|
@@ -483,7 +483,11 @@ export class IndemnClient {
|
|
|
483
483
|
throw new SessionExpiredError('Authentication failed. Run `indemn login` to re-authenticate.');
|
|
484
484
|
}
|
|
485
485
|
if (res.status === 403) {
|
|
486
|
-
|
|
486
|
+
// Keep the server's reason: "Tickets are not enabled for this organization." is not a permission problem.
|
|
487
|
+
const text = await res.text().catch(() => '');
|
|
488
|
+
let reason = '';
|
|
489
|
+
try { const b = JSON.parse(text) as { message?: unknown; error?: unknown; msg?: unknown }; reason = String(b.message ?? b.error ?? b.msg ?? ''); } catch { reason = ''; }
|
|
490
|
+
throw new Error(reason ? `Not allowed: ${reason}` : 'Access denied. You may not have permission for this operation.');
|
|
487
491
|
}
|
|
488
492
|
if (res.status === 404) {
|
|
489
493
|
// Keep the server's reason: voice-service answers 404 with specific messages (no phone number, ...).
|