@dudamel/mcp-agents 2026.917.2

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/dist/index.js ADDED
@@ -0,0 +1,753 @@
1
+ // ../common/dist/http-client.js
2
+ function parseApiErrorBody(body) {
3
+ try {
4
+ const parsed = JSON.parse(body);
5
+ if (typeof parsed !== "object" || parsed === null || !("error" in parsed))
6
+ return void 0;
7
+ const error = parsed.error;
8
+ if (typeof error === "object" && error !== null && typeof error.code === "string" && typeof error.message === "string") {
9
+ return error;
10
+ }
11
+ return void 0;
12
+ } catch {
13
+ return void 0;
14
+ }
15
+ }
16
+ var DudamelApiError = class extends Error {
17
+ status;
18
+ statusText;
19
+ body;
20
+ /** Stable machine-readable code from the API error body, or 'UNKNOWN' for non-conforming bodies. */
21
+ code;
22
+ /** Per-field validation errors, when the API provided them. */
23
+ details;
24
+ /** Extra machine-readable data (e.g. `inputsSchema` on execute validation failures). */
25
+ context;
26
+ constructor(status, statusText, body) {
27
+ const apiError = parseApiErrorBody(body);
28
+ const lines = [
29
+ apiError ? `Dudamel API error ${status} ${statusText}: ${apiError.code}: ${apiError.message}` : `Dudamel API error ${status} ${statusText}: ${body}`
30
+ ];
31
+ if (apiError?.details?.length) {
32
+ lines.push(...apiError.details.map((d) => ` - ${d.path}: ${d.message}`));
33
+ }
34
+ if (apiError?.context?.["inputsSchema"] !== void 0) {
35
+ lines.push(`Expected inputs schema: ${JSON.stringify(apiError.context["inputsSchema"])}`);
36
+ }
37
+ super(lines.join("\n"));
38
+ this.status = status;
39
+ this.statusText = statusText;
40
+ this.body = body;
41
+ this.name = "DudamelApiError";
42
+ this.code = apiError?.code ?? "UNKNOWN";
43
+ this.details = apiError?.details;
44
+ this.context = apiError?.context;
45
+ }
46
+ };
47
+ var DudamelApiClient = class {
48
+ baseUrl;
49
+ headers;
50
+ constructor(baseUrl, apiKey, authScheme) {
51
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
52
+ this.headers = {
53
+ Authorization: `${authScheme ?? "ApiKey"} ${apiKey}`,
54
+ "Content-Type": "application/json"
55
+ };
56
+ }
57
+ async get(path, params) {
58
+ const url = this.buildUrl(path, params);
59
+ return this.request(url, { method: "GET" });
60
+ }
61
+ async post(path, body) {
62
+ const url = this.buildUrl(path);
63
+ return this.request(url, {
64
+ method: "POST",
65
+ body: body !== void 0 ? JSON.stringify(body) : void 0
66
+ });
67
+ }
68
+ async put(path, body) {
69
+ const url = this.buildUrl(path);
70
+ return this.request(url, {
71
+ method: "PUT",
72
+ body: body !== void 0 ? JSON.stringify(body) : void 0
73
+ });
74
+ }
75
+ async delete(path) {
76
+ const url = this.buildUrl(path);
77
+ return this.request(url, { method: "DELETE" });
78
+ }
79
+ buildUrl(path, params) {
80
+ const url = new URL(path, this.baseUrl);
81
+ if (params) {
82
+ for (const [key, value] of Object.entries(params)) {
83
+ if (value !== void 0 && value !== "") {
84
+ url.searchParams.set(key, value);
85
+ }
86
+ }
87
+ }
88
+ return url.toString();
89
+ }
90
+ async request(url, init) {
91
+ const response = await fetch(url, {
92
+ ...init,
93
+ headers: this.headers
94
+ });
95
+ if (!response.ok) {
96
+ const body = await response.text();
97
+ throw new DudamelApiError(response.status, response.statusText, body);
98
+ }
99
+ const text = await response.text();
100
+ if (!text) {
101
+ return void 0;
102
+ }
103
+ return JSON.parse(text);
104
+ }
105
+ };
106
+
107
+ // ../common/dist/read-only-filter.js
108
+ function withReadOnlyFilter(server, readOnly) {
109
+ if (!readOnly)
110
+ return server;
111
+ const original = server.registerTool.bind(server);
112
+ const filtered = (...args) => {
113
+ const config = args[1];
114
+ if (config?.annotations?.readOnlyHint === false) {
115
+ return void 0;
116
+ }
117
+ return original(...args);
118
+ };
119
+ return new Proxy(server, {
120
+ get(target, prop, receiver) {
121
+ if (prop === "registerTool")
122
+ return filtered;
123
+ return Reflect.get(target, prop, receiver);
124
+ }
125
+ });
126
+ }
127
+
128
+ // ../common/dist/create-server.js
129
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
130
+ function createSurfaceServer(opts) {
131
+ const { config } = opts;
132
+ const server = new McpServer({ name: opts.name, version: "0.1.0" }, {
133
+ capabilities: { tools: {} },
134
+ instructions: opts.instructions
135
+ });
136
+ const client = new DudamelApiClient(config.apiUrl, config.apiKey, config.authScheme);
137
+ const ctx = {
138
+ role: config.role,
139
+ readOnly: config.readOnly ?? false,
140
+ agentName: config.agentName,
141
+ callChain: config.callChain,
142
+ turnId: config.turnId,
143
+ rootTurnId: config.rootTurnId,
144
+ conversationId: config.conversationId,
145
+ requesterUserId: config.requesterUserId,
146
+ responseTarget: config.responseTarget
147
+ };
148
+ const host = withReadOnlyFilter(server, ctx.readOnly);
149
+ for (const register of opts.registrars)
150
+ register(host, client, ctx);
151
+ return server;
152
+ }
153
+
154
+ // ../common/dist/util.js
155
+ function jsonResult(data) {
156
+ return {
157
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
158
+ };
159
+ }
160
+
161
+ // src/tools/agents.ts
162
+ import { z } from "zod";
163
+ function registerAgentTools(server, client, ctx) {
164
+ server.registerTool(
165
+ "list-agents",
166
+ {
167
+ description: "List all agent definitions. Returns name, description, status, and config for each agent.",
168
+ annotations: { readOnlyHint: true }
169
+ },
170
+ async () => {
171
+ const page = await client.get("/api/agents");
172
+ return jsonResult({ items: page.items, total: page.total });
173
+ }
174
+ );
175
+ server.registerTool(
176
+ "get-agent",
177
+ {
178
+ description: "Get detailed information about a specific agent, including prompt sections, system prompt, and config.",
179
+ inputSchema: {
180
+ id: z.string().describe("Agent ID (UUID)")
181
+ },
182
+ annotations: { readOnlyHint: true }
183
+ },
184
+ async ({ id }) => {
185
+ const agent = await client.get(`/api/agents/${id}`);
186
+ return jsonResult(agent);
187
+ }
188
+ );
189
+ server.registerTool(
190
+ "get-agent-logs",
191
+ {
192
+ description: "Get the agent's recent conversational turns, newest first \u2014 status, duration, model, tokens, the MCP servers the turn brought up, and the failure reason when it failed. This is the way to debug an agent that is not answering: a `failed` turn carries the error code and the CLI stderr tail. Returns metadata only, never the conversation text.",
193
+ inputSchema: {
194
+ id: z.string().describe("Agent ID (UUID)"),
195
+ status: z.enum(["completed", "failed"]).optional().describe("Only return turns with this outcome"),
196
+ limit: z.number().int().min(1).max(200).optional().describe("Number of turns to return (default: 50)"),
197
+ offset: z.number().int().min(0).optional().describe("Pagination offset")
198
+ },
199
+ annotations: { readOnlyHint: true }
200
+ },
201
+ async ({ id, status, limit, offset }) => {
202
+ const params = {};
203
+ if (status !== void 0) params.status = status;
204
+ if (limit !== void 0) params.limit = String(limit);
205
+ if (offset !== void 0) params.offset = String(offset);
206
+ const page = await client.get(
207
+ `/api/agents/${id}/turns`,
208
+ params
209
+ );
210
+ return jsonResult({
211
+ items: page.items,
212
+ total: page.total,
213
+ offset: page.offset,
214
+ limit: page.limit,
215
+ consecutiveFailures: page.consecutiveFailures
216
+ });
217
+ }
218
+ );
219
+ server.registerTool(
220
+ "get-agent-stats",
221
+ {
222
+ description: "Get token usage statistics per agent over a time window. Useful for monitoring costs.",
223
+ inputSchema: {
224
+ windowHours: z.number().int().min(1).optional().describe("Time window in hours (default: 24)")
225
+ },
226
+ annotations: { readOnlyHint: true }
227
+ },
228
+ async ({ windowHours }) => {
229
+ const params = {};
230
+ if (windowHours !== void 0) params.window = String(windowHours);
231
+ const page = await client.get("/api/agents/stats/tokens", params);
232
+ return jsonResult({ items: page.items, total: page.total });
233
+ }
234
+ );
235
+ server.registerTool(
236
+ "get-agent-sub-agents",
237
+ {
238
+ description: "List the sub-agents assigned to a specific (parent) agent.",
239
+ inputSchema: {
240
+ id: z.string().describe("Parent agent ID (UUID)")
241
+ },
242
+ annotations: { readOnlyHint: true }
243
+ },
244
+ async ({ id }) => {
245
+ const page = await client.get(`/api/agents/${id}/sub-agents`);
246
+ return jsonResult({ items: page.items, total: page.total });
247
+ }
248
+ );
249
+ server.registerTool(
250
+ "download-agent",
251
+ {
252
+ description: "Download an agent definition (persona + skills) as files in the target tool's on-disk layout (e.g. .claude/agents/<name>.md + .claude/skills/ for Claude Code). Returns {agentName, files: [{path, content, skipIfExists?}]} \u2014 write each file at its path relative to the current working directory; do NOT overwrite existing files marked skipIfExists. Iterate locally, then push changes back with update-agent/update-skill.",
253
+ inputSchema: {
254
+ id: z.string().describe("Agent ID (UUID)"),
255
+ format: z.enum(["claude-code", "codex", "kimi-code", "opencode", "raw"]).optional().describe(
256
+ "Target tool layout \u2014 defaults to 'claude-code'; 'raw' is a dudamel-native <name>/AGENT.md layout"
257
+ )
258
+ },
259
+ annotations: { readOnlyHint: true }
260
+ },
261
+ async ({ id, format }) => {
262
+ const result = await client.get(`/api/agents/${id}/export?format=${format ?? "claude-code"}`);
263
+ return jsonResult(result);
264
+ }
265
+ );
266
+ server.registerTool(
267
+ "send-message-to-agent",
268
+ {
269
+ description: "Send a message to a PEER Dudamel agent and get their response. Peers are the agents listed in the Peer Agents section of your system prompt: separate agents with their own tools, memory and history, running in their own process. This is the ONLY way to reach them \u2014 your coding tool's own agent-messaging tool (e.g. SendMessage) sees only subagents spawned inside your session and cannot reach a peer. Use this to delegate tasks to or ask questions of another Dudamel agent.",
270
+ inputSchema: {
271
+ agentName: z.string().describe("Target agent name (as shown in the peer agents list)"),
272
+ message: z.string().describe("Message to send to the target agent")
273
+ },
274
+ annotations: { readOnlyHint: false, destructiveHint: false }
275
+ },
276
+ async ({ agentName, message }) => {
277
+ const result = await client.post(
278
+ `/api/agents/by-name/${encodeURIComponent(agentName)}/message`,
279
+ // The chain this turn arrived with, plus ourselves (Plan 43 C1). This used to send a
280
+ // hardcoded `depth: 0`, which made the server's depth guard decorative: nothing ever
281
+ // reported being deeper than zero, so A→B→C→D was unbounded and A→B→A unnoticed.
282
+ {
283
+ message,
284
+ callChain: [...ctx.callChain ?? [], ...ctx.agentName ? [ctx.agentName] : []],
285
+ fromAgent: ctx.agentName,
286
+ // Plan 43 C3: the callee's turn records WHICH turn asked, so a multi-agent
287
+ // interaction can be reconstructed as a tree instead of guessed from timestamps.
288
+ parentTurnId: ctx.turnId,
289
+ rootTurnId: ctx.rootTurnId ?? ctx.turnId
290
+ }
291
+ );
292
+ return jsonResult(result);
293
+ }
294
+ );
295
+ server.registerTool(
296
+ "notify-agent",
297
+ {
298
+ description: "Hand control back to the conversation that launched THIS workflow run. Use it to report an outcome or to ask the user something through the agent, from a workflow step. Only works from inside a workflow execution, and only for the conversation that launched it \u2014 pass the ids from `{{ trigger.callback }}`. Nobody is blocked waiting: the message is delivered and the agent answers in its own channel.",
299
+ inputSchema: {
300
+ conversationId: z.string().describe("From `{{ trigger.callback.conversationId }}` \u2014 the conversation to resume"),
301
+ message: z.string().describe("What to tell the agent (it will act on it, not echo it)")
302
+ },
303
+ annotations: { readOnlyHint: false, destructiveHint: false }
304
+ },
305
+ async ({ conversationId, message }) => {
306
+ const executionWorkflowId = process.env["DUDAMEL_WORKFLOW_ID"];
307
+ if (!executionWorkflowId) {
308
+ throw new Error(
309
+ "notify-agent requires a workflow execution context (DUDAMEL_WORKFLOW_ID) \u2014 it only works from a workflow step, not from an agent conversation."
310
+ );
311
+ }
312
+ const result = await client.post("/api/agents/notify", {
313
+ conversationId,
314
+ executionWorkflowId,
315
+ message,
316
+ callChain: ctx.callChain
317
+ });
318
+ return jsonResult(result);
319
+ }
320
+ );
321
+ server.registerTool(
322
+ "ask-agent",
323
+ {
324
+ description: 'Ask the human a question THROUGH the agent, from a workflow step, and park this run until they answer. Pair it with a `waitForSignal` step on the same `signalName`. Unlike a fixed approval prompt, the agent turns a free-form answer ("s\xED dale, pero solo staging") into the structured payload your signal expects. Only works inside a workflow execution, and only for the conversation that launched it \u2014 pass the id from `{{ trigger.callback.conversationId }}`.',
325
+ inputSchema: {
326
+ conversationId: z.string().describe("From `{{ trigger.callback.conversationId }}` \u2014 where to ask"),
327
+ question: z.string().describe("What to ask the human, in their language"),
328
+ signalName: z.string().optional().describe("Signal your waitForSignal step listens on (default: humanApproval)")
329
+ },
330
+ annotations: { readOnlyHint: false, destructiveHint: false }
331
+ },
332
+ async ({ conversationId, question, signalName }) => {
333
+ const executionWorkflowId = process.env["DUDAMEL_WORKFLOW_ID"];
334
+ if (!executionWorkflowId) {
335
+ throw new Error(
336
+ "ask-agent requires a workflow execution context (DUDAMEL_WORKFLOW_ID) \u2014 it only works from a workflow step, not from an agent conversation."
337
+ );
338
+ }
339
+ const result = await client.post("/api/conversation-waits", {
340
+ conversationId,
341
+ executionWorkflowId,
342
+ question,
343
+ signalName: signalName ?? "humanApproval",
344
+ callChain: ctx.callChain
345
+ });
346
+ return jsonResult(result);
347
+ }
348
+ );
349
+ server.registerTool(
350
+ "answer-workflow",
351
+ {
352
+ description: "Unblock a workflow that is waiting on the human, with the answer turned into the payload it expects. Call it once you actually have the answer \u2014 translating what the person said into structured fields is your job here, and the reason a workflow asks through you instead of posting a fixed form.",
353
+ inputSchema: {
354
+ waitId: z.string().describe("The waitId the workflow gave you when it asked"),
355
+ payload: z.record(z.unknown()).optional().describe('Structured answer for the signal, e.g. {"approved": true, "env": "staging"}')
356
+ },
357
+ annotations: { readOnlyHint: false, destructiveHint: false }
358
+ },
359
+ async ({ waitId, payload }) => {
360
+ const result = await client.post("/api/conversation-waits/answer", { waitId, payload });
361
+ return jsonResult(result);
362
+ }
363
+ );
364
+ server.registerTool(
365
+ "update-agent",
366
+ {
367
+ description: "Update an agent definition. Requires edit permission on the agent (business-role permission or admin).",
368
+ inputSchema: {
369
+ id: z.string().describe("Agent ID (UUID)"),
370
+ name: z.string().optional().describe("New name"),
371
+ displayName: z.string().optional().describe("New display name"),
372
+ description: z.string().optional().describe("New description"),
373
+ identity: z.string().optional().describe("New identity"),
374
+ instructions: z.string().optional().describe("New instructions"),
375
+ systemPrompt: z.string().optional().describe("New system prompt"),
376
+ model: z.string().optional().describe("New model identifier"),
377
+ skipPermissions: z.boolean().optional().describe("Skip permission prompts"),
378
+ driverProvider: z.string().optional().describe("New AI provider instance name"),
379
+ driverMode: z.enum(["api", "cli"]).optional().describe("New driver mode"),
380
+ kind: z.enum(["system", "user"]).optional().describe(
381
+ "Agent kind \u2014 set to 'user' to convert a disk-seeded 'system' agent into a normal editable user agent"
382
+ )
383
+ },
384
+ annotations: { readOnlyHint: false, destructiveHint: false }
385
+ },
386
+ async ({ id, ...updates }) => {
387
+ const result = await client.put(`/api/agents/${id}`, updates);
388
+ return jsonResult(result);
389
+ }
390
+ );
391
+ server.registerTool(
392
+ "set-agent-sub-agents",
393
+ {
394
+ description: "Set the sub-agent assignments for a (parent) agent. Replaces existing assignments. Each sub-agent id must be an agent created with isSubAgent=true. Requires edit permission on the parent agent.",
395
+ inputSchema: {
396
+ id: z.string().describe("Parent agent ID (UUID)"),
397
+ subAgentIds: z.array(z.string()).describe("Sub-agent IDs (UUIDs) to assign")
398
+ },
399
+ annotations: { readOnlyHint: false, destructiveHint: false }
400
+ },
401
+ async ({ id, subAgentIds }) => {
402
+ const page = await client.put(`/api/agents/${id}/sub-agents`, {
403
+ subAgentIds
404
+ });
405
+ return jsonResult({ subAgentIds: page.items });
406
+ }
407
+ );
408
+ if (ctx.role !== "admin") return;
409
+ server.registerTool(
410
+ "create-agent",
411
+ {
412
+ description: "Create a new agent definition.",
413
+ inputSchema: {
414
+ name: z.string().describe("Agent name (unique, kebab-case)"),
415
+ displayName: z.string().optional().describe("Display name shown in the UI"),
416
+ description: z.string().optional().describe("Agent description"),
417
+ identity: z.string().optional().describe('Agent identity (e.g. "senior backend developer")'),
418
+ instructions: z.string().optional().describe("Agent instructions (appended to system prompt)"),
419
+ systemPrompt: z.string().optional().describe("System prompt for the agent"),
420
+ model: z.string().optional().describe('Model identifier (e.g. "claude-sonnet-4-6")'),
421
+ skipPermissions: z.boolean().optional().describe("Skip permission prompts in coding agents"),
422
+ driverProvider: z.string().optional().describe("AI provider instance name for the driver"),
423
+ driverMode: z.enum(["api", "cli"]).optional().describe("Driver mode: api (conversational) or cli (coding)"),
424
+ isSubAgent: z.boolean().optional().describe("Mark as a sub-agent (not shown in top-level lists)")
425
+ },
426
+ annotations: { readOnlyHint: false, destructiveHint: false }
427
+ },
428
+ async ({ name, ...rest }) => {
429
+ const result = await client.post("/api/agents", { name, ...rest });
430
+ return jsonResult(result);
431
+ }
432
+ );
433
+ server.registerTool(
434
+ "activate-agent",
435
+ {
436
+ description: "Activate an agent so it starts receiving and answering messages.",
437
+ inputSchema: {
438
+ id: z.string().describe("Agent ID (UUID)")
439
+ },
440
+ annotations: { readOnlyHint: false, destructiveHint: false }
441
+ },
442
+ async ({ id }) => {
443
+ const result = await client.post(`/api/agents/${id}/activate`);
444
+ return jsonResult(result);
445
+ }
446
+ );
447
+ server.registerTool(
448
+ "deactivate-agent",
449
+ {
450
+ description: "Deactivate an agent \u2014 it stops receiving messages (Slack, dashboard chat) until reactivated. The definition is kept; this is the way to silence an agent without deleting it.",
451
+ inputSchema: {
452
+ id: z.string().describe("Agent ID (UUID)")
453
+ },
454
+ annotations: { readOnlyHint: false, destructiveHint: false }
455
+ },
456
+ async ({ id }) => {
457
+ const result = await client.post(`/api/agents/${id}/deactivate`);
458
+ return jsonResult(result);
459
+ }
460
+ );
461
+ }
462
+
463
+ // src/tools/skills.ts
464
+ import { z as z2 } from "zod";
465
+ function registerSkillTools(server, client, ctx) {
466
+ server.registerTool(
467
+ "list-skills",
468
+ {
469
+ description: "List skill definitions (active by default). Returns id, name, description, status, origin, version, and timestamps.",
470
+ inputSchema: {
471
+ status: z2.enum(["active", "archived"]).optional().describe("Lifecycle filter \u2014 defaults to 'active'; pass 'archived' for archived skills")
472
+ },
473
+ annotations: { readOnlyHint: true }
474
+ },
475
+ async ({ status }) => {
476
+ const query = status ? `?status=${status}` : "";
477
+ const page = await client.get(`/api/skills${query}`);
478
+ return jsonResult({ items: page.items, total: page.total });
479
+ }
480
+ );
481
+ server.registerTool(
482
+ "get-skill",
483
+ {
484
+ description: "Get detailed information about a specific skill, including content, allowed tools, files, and version info.",
485
+ inputSchema: {
486
+ id: z2.string().describe("Skill ID (UUID)")
487
+ },
488
+ annotations: { readOnlyHint: true }
489
+ },
490
+ async ({ id }) => {
491
+ const skill = await client.get(`/api/skills/${id}`);
492
+ return jsonResult(skill);
493
+ }
494
+ );
495
+ server.registerTool(
496
+ "get-skill-versions",
497
+ {
498
+ description: "List version history for a skill. Returns version numbers, change summaries, and creation dates.",
499
+ inputSchema: {
500
+ id: z2.string().describe("Skill ID (UUID)")
501
+ },
502
+ annotations: { readOnlyHint: true }
503
+ },
504
+ async ({ id }) => {
505
+ const page = await client.get(`/api/skills/${id}/versions`);
506
+ return jsonResult({ items: page.items, total: page.total });
507
+ }
508
+ );
509
+ server.registerTool(
510
+ "download-skill",
511
+ {
512
+ description: "Download a skill as files in the target tool's on-disk layout (e.g. .claude/skills/<name>/SKILL.md for Claude Code). Returns {files: [{path, content}]} \u2014 write each file at its path relative to the current working directory. Iterate locally, then push changes back with update-skill.",
513
+ inputSchema: {
514
+ id: z2.string().describe("Skill ID (UUID)"),
515
+ format: z2.enum(["claude-code", "codex", "kimi-code", "opencode", "raw"]).optional().describe(
516
+ "Target tool layout \u2014 defaults to 'claude-code'; 'raw' is the dudamel-native <name>/SKILL.md layout"
517
+ )
518
+ },
519
+ annotations: { readOnlyHint: true }
520
+ },
521
+ async ({ id, format }) => {
522
+ const result = await client.get(`/api/skills/${id}/export?format=${format ?? "claude-code"}`);
523
+ return jsonResult(result);
524
+ }
525
+ );
526
+ server.registerTool(
527
+ "get-agent-skills",
528
+ {
529
+ description: "Get the skill definitions assigned to a specific agent.",
530
+ inputSchema: {
531
+ agentId: z2.string().describe("Agent ID (UUID)")
532
+ },
533
+ annotations: { readOnlyHint: true }
534
+ },
535
+ async ({ agentId }) => {
536
+ const page = await client.get(`/api/agents/${agentId}/skills`);
537
+ return jsonResult({ items: page.items, total: page.total });
538
+ }
539
+ );
540
+ if (ctx.readOnly) return;
541
+ server.registerTool(
542
+ "create-skill",
543
+ {
544
+ description: "Create a new skill definition. The creator becomes the owner and can edit it and share edition with business roles via set-skill-roles.",
545
+ inputSchema: {
546
+ name: z2.string().describe("Skill name (unique, kebab-case)"),
547
+ content: z2.string().describe("Skill content (markdown body)"),
548
+ description: z2.string().optional().describe("Skill description"),
549
+ disableModelInvocation: z2.boolean().optional().describe("Disable model-triggered invocation"),
550
+ userInvocable: z2.boolean().optional().describe("Allow user to invoke via slash command"),
551
+ model: z2.string().optional().describe("Model identifier or tier"),
552
+ allowedTools: z2.array(z2.string()).optional().describe("List of allowed tool names"),
553
+ currentVersion: z2.string().optional().describe("Initial version string"),
554
+ files: z2.array(
555
+ z2.object({
556
+ path: z2.string().describe("Relative file path within the skill"),
557
+ content: z2.string().describe("File content")
558
+ })
559
+ ).optional().describe("Embedded skill files")
560
+ },
561
+ annotations: { readOnlyHint: false, destructiveHint: false }
562
+ },
563
+ async ({
564
+ name,
565
+ content,
566
+ description,
567
+ disableModelInvocation,
568
+ userInvocable,
569
+ model,
570
+ allowedTools,
571
+ currentVersion,
572
+ files
573
+ }) => {
574
+ const result = await client.post("/api/skills", {
575
+ name,
576
+ content,
577
+ description,
578
+ disableModelInvocation,
579
+ userInvocable,
580
+ model,
581
+ allowedTools,
582
+ currentVersion,
583
+ files
584
+ });
585
+ return jsonResult(result);
586
+ }
587
+ );
588
+ server.registerTool(
589
+ "update-skill",
590
+ {
591
+ description: "Update a skill definition. Creates a new version entry.",
592
+ inputSchema: {
593
+ id: z2.string().describe("Skill ID (UUID)"),
594
+ name: z2.string().optional().describe(
595
+ "New skill name (unique, kebab-case) \u2014 rename the skill; agent assignments are by id and survive the rename"
596
+ ),
597
+ content: z2.string().optional().describe("New skill content"),
598
+ description: z2.string().optional().describe("New description"),
599
+ disableModelInvocation: z2.boolean().optional().describe("Disable model-triggered invocation"),
600
+ userInvocable: z2.boolean().optional().describe("Allow user to invoke via slash command"),
601
+ model: z2.string().optional().describe("Model identifier or tier"),
602
+ allowedTools: z2.array(z2.string()).optional().describe("List of allowed tool names"),
603
+ currentVersion: z2.string().optional().describe("Version string for this update"),
604
+ changeSummary: z2.string().optional().describe("Summary of what changed in this version"),
605
+ files: z2.array(
606
+ z2.object({
607
+ path: z2.string().describe("Relative file path within the skill"),
608
+ content: z2.string().describe("File content")
609
+ })
610
+ ).optional().describe("Embedded skill files")
611
+ },
612
+ annotations: { readOnlyHint: false, destructiveHint: false }
613
+ },
614
+ async ({ id, ...updates }) => {
615
+ const result = await client.put(`/api/skills/${id}`, updates);
616
+ return jsonResult(result);
617
+ }
618
+ );
619
+ server.registerTool(
620
+ "archive-skill",
621
+ {
622
+ description: "Archive a skill (logical delete \u2014 versions and files are kept; it disappears from default listings and cannot be assigned). Fails with SKILL_IN_USE while assigned to agents: unassign it first.",
623
+ inputSchema: {
624
+ id: z2.string().describe("Skill ID (UUID)")
625
+ },
626
+ annotations: { readOnlyHint: false, destructiveHint: false }
627
+ },
628
+ async ({ id }) => {
629
+ const result = await client.post(`/api/skills/${id}/archive`);
630
+ return jsonResult(result);
631
+ }
632
+ );
633
+ server.registerTool(
634
+ "unarchive-skill",
635
+ {
636
+ description: "Restore an archived skill to active, making it assignable again.",
637
+ inputSchema: {
638
+ id: z2.string().describe("Skill ID (UUID)")
639
+ },
640
+ annotations: { readOnlyHint: false, destructiveHint: false }
641
+ },
642
+ async ({ id }) => {
643
+ const result = await client.post(`/api/skills/${id}/unarchive`);
644
+ return jsonResult(result);
645
+ }
646
+ );
647
+ server.registerTool(
648
+ "get-skill-roles",
649
+ {
650
+ description: "List the business roles that can edit a skill. Requires edit permission on the skill (owner, permitted role, or admin).",
651
+ inputSchema: {
652
+ id: z2.string().describe("Skill ID (UUID)")
653
+ },
654
+ annotations: { readOnlyHint: true }
655
+ },
656
+ async ({ id }) => {
657
+ const page = await client.get(`/api/skills/${id}/permissions`);
658
+ return jsonResult({ items: page.items, total: page.total });
659
+ }
660
+ );
661
+ server.registerTool(
662
+ "set-skill-roles",
663
+ {
664
+ description: "Replace the set of business roles that can edit a skill. The owner uses this to share (or revoke) edition with team roles. Requires edit permission on the skill.",
665
+ inputSchema: {
666
+ id: z2.string().describe("Skill ID (UUID)"),
667
+ roleIds: z2.array(z2.string()).describe("Business role IDs that may edit the skill")
668
+ },
669
+ annotations: { readOnlyHint: false, destructiveHint: false }
670
+ },
671
+ async ({ id, roleIds }) => {
672
+ const page = await client.put(`/api/skills/${id}/permissions`, {
673
+ roleIds
674
+ });
675
+ return jsonResult({ items: page.items });
676
+ }
677
+ );
678
+ server.registerTool(
679
+ "set-agent-skills",
680
+ {
681
+ description: "Set the skill assignments for an agent. Replaces existing assignments.",
682
+ inputSchema: {
683
+ agentId: z2.string().describe("Agent ID (UUID)"),
684
+ skills: z2.array(
685
+ z2.object({
686
+ skillName: z2.string().describe("Skill name to assign")
687
+ })
688
+ ).describe("List of skills to assign")
689
+ },
690
+ annotations: { readOnlyHint: false, destructiveHint: false }
691
+ },
692
+ async ({ agentId, skills }) => {
693
+ const page = await client.put(`/api/agents/${agentId}/skills`, {
694
+ skills
695
+ });
696
+ return jsonResult({ skills: page.items });
697
+ }
698
+ );
699
+ }
700
+
701
+ // src/tools/agent-mcp.ts
702
+ import { z as z3 } from "zod";
703
+ function registerAgentMcpTools(server, client, ctx) {
704
+ server.registerTool(
705
+ "get-agent-mcp-servers",
706
+ {
707
+ description: "List MCP servers assigned to a specific agent.",
708
+ inputSchema: {
709
+ agentId: z3.string().describe("Agent ID (UUID)")
710
+ },
711
+ annotations: { readOnlyHint: true }
712
+ },
713
+ async ({ agentId }) => {
714
+ const page = await client.get(`/api/agents/${agentId}/mcp-servers`);
715
+ return jsonResult({ items: page.items, total: page.total });
716
+ }
717
+ );
718
+ if (ctx.readOnly || ctx.role !== "admin") return;
719
+ server.registerTool(
720
+ "set-agent-mcp-servers",
721
+ {
722
+ description: "Set the MCP server assignments for an agent. Replaces existing assignments. Optionally set per-server access mode via `modes` (keyed by MCP server id); ids omitted from `modes` default to 'write' (read-write).",
723
+ inputSchema: {
724
+ agentId: z3.string().describe("Agent ID (UUID)"),
725
+ mcpServerIds: z3.array(z3.string()).describe("List of MCP Server IDs (UUIDs) to assign"),
726
+ modes: z3.record(z3.enum(["read", "write"])).optional().describe(
727
+ "Per-server access mode keyed by MCP server id: 'read' (read-only tools) or 'write' (all tools). Omitted ids default to 'write'."
728
+ )
729
+ },
730
+ annotations: { readOnlyHint: false, destructiveHint: false }
731
+ },
732
+ async ({ agentId, mcpServerIds, modes }) => {
733
+ const page = await client.put(`/api/agents/${agentId}/mcp-servers`, {
734
+ mcpServerIds,
735
+ modes
736
+ });
737
+ return jsonResult({ mcpServerIds: page.items });
738
+ }
739
+ );
740
+ }
741
+
742
+ // src/server.ts
743
+ function createServer(config) {
744
+ return createSurfaceServer({
745
+ name: "dudamel-agents",
746
+ instructions: `Dudamel Agents MCP \u2014 manage agents, skills, and their skill/MCP assignments (plus agent-to-agent messaging). Running as role=${config.role}, readOnly=${config.readOnly ?? false}.`,
747
+ registrars: [registerAgentTools, registerSkillTools, registerAgentMcpTools],
748
+ config
749
+ });
750
+ }
751
+ export {
752
+ createServer
753
+ };