@dudamel/mcp-peers 0.0.0-stage → 2026.1104.1
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.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +265 -0
- package/dist/poll.d.ts +21 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/stdio-server.d.ts +3 -0
- package/dist/stdio-server.d.ts.map +1 -0
- package/dist/stdio-server.js +309 -0
- package/dist/tools/peers.d.ts +5 -0
- package/dist/tools/peers.d.ts.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.d.ts.map +1 -0
- package/package.json +42 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
// ../common/dist/http-client.js
|
|
5
|
+
function parseApiErrorBody(body) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(body);
|
|
8
|
+
if (typeof parsed !== "object" || parsed === null || !("error" in parsed))
|
|
9
|
+
return void 0;
|
|
10
|
+
const error = parsed.error;
|
|
11
|
+
if (typeof error === "object" && error !== null && typeof error.code === "string" && typeof error.message === "string") {
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
var DudamelApiError = class extends Error {
|
|
20
|
+
status;
|
|
21
|
+
statusText;
|
|
22
|
+
body;
|
|
23
|
+
/** Stable machine-readable code from the API error body, or 'UNKNOWN' for non-conforming bodies. */
|
|
24
|
+
code;
|
|
25
|
+
/** Per-field validation errors, when the API provided them. */
|
|
26
|
+
details;
|
|
27
|
+
/** Extra machine-readable data (e.g. `inputsSchema` on execute validation failures). */
|
|
28
|
+
context;
|
|
29
|
+
constructor(status, statusText, body) {
|
|
30
|
+
const apiError = parseApiErrorBody(body);
|
|
31
|
+
const lines = [
|
|
32
|
+
apiError ? `Dudamel API error ${status} ${statusText}: ${apiError.code}: ${apiError.message}` : `Dudamel API error ${status} ${statusText}: ${body}`
|
|
33
|
+
];
|
|
34
|
+
if (apiError?.details?.length) {
|
|
35
|
+
lines.push(...apiError.details.map((d) => ` - ${d.path}: ${d.message}`));
|
|
36
|
+
}
|
|
37
|
+
if (apiError?.context?.["inputsSchema"] !== void 0) {
|
|
38
|
+
lines.push(`Expected inputs schema: ${JSON.stringify(apiError.context["inputsSchema"])}`);
|
|
39
|
+
}
|
|
40
|
+
super(lines.join("\n"));
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.statusText = statusText;
|
|
43
|
+
this.body = body;
|
|
44
|
+
this.name = "DudamelApiError";
|
|
45
|
+
this.code = apiError?.code ?? "UNKNOWN";
|
|
46
|
+
this.details = apiError?.details;
|
|
47
|
+
this.context = apiError?.context;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var DudamelApiClient = class {
|
|
51
|
+
baseUrl;
|
|
52
|
+
headers;
|
|
53
|
+
constructor(baseUrl, apiKey, authScheme) {
|
|
54
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
55
|
+
this.headers = {
|
|
56
|
+
Authorization: `${authScheme ?? "ApiKey"} ${apiKey}`,
|
|
57
|
+
"Content-Type": "application/json"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async get(path, params) {
|
|
61
|
+
const url = this.buildUrl(path, params);
|
|
62
|
+
return this.request(url, { method: "GET" });
|
|
63
|
+
}
|
|
64
|
+
async post(path, body, params) {
|
|
65
|
+
const url = this.buildUrl(path, params);
|
|
66
|
+
return this.request(url, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async put(path, body, params) {
|
|
72
|
+
const url = this.buildUrl(path, params);
|
|
73
|
+
return this.request(url, {
|
|
74
|
+
method: "PUT",
|
|
75
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async delete(path, params) {
|
|
79
|
+
const url = this.buildUrl(path, params);
|
|
80
|
+
return this.request(url, { method: "DELETE" });
|
|
81
|
+
}
|
|
82
|
+
buildUrl(path, params) {
|
|
83
|
+
const url = new URL(path, this.baseUrl);
|
|
84
|
+
if (params) {
|
|
85
|
+
for (const [key, value] of Object.entries(params)) {
|
|
86
|
+
if (value !== void 0 && value !== "") {
|
|
87
|
+
url.searchParams.set(key, value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return url.toString();
|
|
92
|
+
}
|
|
93
|
+
async request(url, init) {
|
|
94
|
+
const response = await fetch(url, {
|
|
95
|
+
...init,
|
|
96
|
+
headers: this.headers
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const body = await response.text();
|
|
100
|
+
throw new DudamelApiError(response.status, response.statusText, body);
|
|
101
|
+
}
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
if (!text) {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
return JSON.parse(text);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ../common/dist/read-only-filter.js
|
|
111
|
+
function withReadOnlyFilter(server, readOnly) {
|
|
112
|
+
if (!readOnly)
|
|
113
|
+
return server;
|
|
114
|
+
const original = server.registerTool.bind(server);
|
|
115
|
+
const filtered = (...args) => {
|
|
116
|
+
const config = args[1];
|
|
117
|
+
if (config?.annotations?.readOnlyHint === false) {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
return original(...args);
|
|
121
|
+
};
|
|
122
|
+
return new Proxy(server, {
|
|
123
|
+
get(target, prop, receiver) {
|
|
124
|
+
if (prop === "registerTool")
|
|
125
|
+
return filtered;
|
|
126
|
+
return Reflect.get(target, prop, receiver);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ../common/dist/create-server.js
|
|
132
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
133
|
+
|
|
134
|
+
// ../common/dist/util.js
|
|
135
|
+
function jsonResult(data) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function errorResult(error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: message }],
|
|
144
|
+
isError: true
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/tools/peers.ts
|
|
149
|
+
import { z } from "zod";
|
|
150
|
+
|
|
151
|
+
// src/poll.ts
|
|
152
|
+
var GET_TASK_MAX_WAIT_SECONDS = 120;
|
|
153
|
+
var INITIAL_POLL_INTERVAL_MS = 2e3;
|
|
154
|
+
var MAX_POLL_INTERVAL_MS = 1e4;
|
|
155
|
+
function isTerminal(task) {
|
|
156
|
+
const status = task?.status;
|
|
157
|
+
return status === "completed" || status === "failed";
|
|
158
|
+
}
|
|
159
|
+
async function pollTask(fetchTask, options) {
|
|
160
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
161
|
+
const now = options.now ?? Date.now;
|
|
162
|
+
const deadline = now() + options.waitMs;
|
|
163
|
+
let interval = INITIAL_POLL_INTERVAL_MS;
|
|
164
|
+
for (; ; ) {
|
|
165
|
+
const task = await fetchTask();
|
|
166
|
+
if (isTerminal(task)) return task;
|
|
167
|
+
const remaining = deadline - now();
|
|
168
|
+
if (remaining <= 0) return task;
|
|
169
|
+
await sleep(Math.min(interval, remaining));
|
|
170
|
+
interval = Math.min(interval * 1.5, MAX_POLL_INTERVAL_MS);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/tools/peers.ts
|
|
175
|
+
function registerPeerTools(server, client, config) {
|
|
176
|
+
server.registerTool(
|
|
177
|
+
"list-agents",
|
|
178
|
+
{
|
|
179
|
+
description: "List the dudamel agents you can message, as agent cards: name, description and skills. The `name` is what send-message takes. Read the cards to decide WHO to delegate to \u2014 an empty list means no other agent is active, not an error.",
|
|
180
|
+
inputSchema: {},
|
|
181
|
+
annotations: { readOnlyHint: true }
|
|
182
|
+
},
|
|
183
|
+
async () => {
|
|
184
|
+
try {
|
|
185
|
+
return jsonResult(await client.get("/api/conversations/peers"));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return errorResult(err);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
server.registerTool(
|
|
192
|
+
"send-message",
|
|
193
|
+
{
|
|
194
|
+
description: "Send a message to another dudamel agent. Returns a task: in async mode (the default) it comes back `submitted` immediately \u2014 tell the user you delegated and end your turn; the answer arrives later as an `a2a-task-completed` system message (or poll it with get-task if you have no conversation). `sync` blocks and returns the task `completed` with the answer \u2014 only for short questions. Every response carries a `contextId`: pass it back on the next send-message to CONTINUE that conversation with the peer (it keeps its full history); omit it to start fresh.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
agentName: z.string().min(1).describe("Peer agent to message \u2014 a `name` from list-agents"),
|
|
197
|
+
message: z.string().min(1).describe(
|
|
198
|
+
"The message. Be specific and self-contained: the peer does not see your conversation"
|
|
199
|
+
),
|
|
200
|
+
contextId: z.string().optional().describe("contextId from a previous exchange \u2014 continues that same thread"),
|
|
201
|
+
mode: z.enum(["async", "sync"]).optional().describe("async (default): fire and get notified. sync: wait for the answer inline")
|
|
202
|
+
},
|
|
203
|
+
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
204
|
+
},
|
|
205
|
+
async ({ agentName, message, contextId, mode }) => {
|
|
206
|
+
try {
|
|
207
|
+
return jsonResult(
|
|
208
|
+
await client.post("/api/conversations/peers/messages", {
|
|
209
|
+
agentName,
|
|
210
|
+
message,
|
|
211
|
+
...contextId ? { contextId } : {},
|
|
212
|
+
mode: mode ?? "async",
|
|
213
|
+
// Infrastructure correlation — from the env the bridge injects, never
|
|
214
|
+
// model-visible parameters (Plan 44, decisión 6).
|
|
215
|
+
...config.conversationId ? { callerConversationId: config.conversationId } : {},
|
|
216
|
+
...config.responseTarget ? { callerResponseTarget: config.responseTarget } : {},
|
|
217
|
+
chain: config.agentChain ?? []
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return errorResult(err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
server.registerTool(
|
|
226
|
+
"get-task",
|
|
227
|
+
{
|
|
228
|
+
description: `Status and result of a send-message task by its \`taskId\`. Statuses: submitted (the peer has not answered yet), completed (\`result\` holds the answer), failed (\`errorSummary\` says why). Use it when you sent async and no completion message can reach you (e.g. you run outside a dudamel conversation). Pass \`waitSeconds\` (max ${GET_TASK_MAX_WAIT_SECONDS}) to have the tool poll internally and return as soon as the task goes terminal \u2014 if it is still submitted at the deadline, call again later instead of looping yourself.`,
|
|
229
|
+
inputSchema: {
|
|
230
|
+
taskId: z.string().min(1).describe("The `taskId` a send-message response returned"),
|
|
231
|
+
waitSeconds: z.number().int().min(0).max(GET_TASK_MAX_WAIT_SECONDS).optional().describe("Keep polling internally up to this many seconds (default 0: one look)")
|
|
232
|
+
},
|
|
233
|
+
annotations: { readOnlyHint: true }
|
|
234
|
+
},
|
|
235
|
+
async ({ taskId, waitSeconds }) => {
|
|
236
|
+
try {
|
|
237
|
+
const task = await pollTask(
|
|
238
|
+
() => client.get(`/api/conversations/tasks/${encodeURIComponent(taskId)}`),
|
|
239
|
+
{ waitMs: Math.min(waitSeconds ?? 0, GET_TASK_MAX_WAIT_SECONDS) * 1e3 }
|
|
240
|
+
);
|
|
241
|
+
return jsonResult(task);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return errorResult(err);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/server.ts
|
|
250
|
+
function createPeersServer(config) {
|
|
251
|
+
const server = new McpServer2(
|
|
252
|
+
{ name: "dudamel-peers", version: "0.1.0" },
|
|
253
|
+
{
|
|
254
|
+
capabilities: { tools: {} },
|
|
255
|
+
instructions: "Dudamel Peers MCP \u2014 message other dudamel agents. Flow: list-agents to see who exists and what they are good at \u2192 send-message (async by default: tell the user you delegated, end your turn, and react when the a2a-task-completed system message arrives; sync only for short questions) \u2192 re-send with the returned contextId to continue the same thread, where the peer remembers everything. These are OTHER agents of this dudamel instance, reached over the platform \u2014 NOT your own sub-agents."
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
const client = new DudamelApiClient(config.apiUrl, config.apiKey, config.authScheme);
|
|
259
|
+
const host = withReadOnlyFilter(server, config.readOnly ?? false);
|
|
260
|
+
registerPeerTools(host, client, config);
|
|
261
|
+
return server;
|
|
262
|
+
}
|
|
263
|
+
export {
|
|
264
|
+
createPeersServer
|
|
265
|
+
};
|
package/dist/poll.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard ceiling on the wait. A conversational turn has a 5-minute inactivity watchdog
|
|
3
|
+
* (`TURN_STALLED`): a tool call that stays silent longer kills the CALLER's turn, so
|
|
4
|
+
* the cap must sit well under it. Waits longer than this mean the answer should come
|
|
5
|
+
* through the completion event (conversations) or a later get-task (terminal).
|
|
6
|
+
*/
|
|
7
|
+
export declare const GET_TASK_MAX_WAIT_SECONDS = 120;
|
|
8
|
+
export interface PollOptions {
|
|
9
|
+
waitMs: number;
|
|
10
|
+
/** Injectable for tests — defaults to a real timer. */
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
/** Injectable clock for tests. */
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Fetch the task, and — within `waitMs` — keep re-fetching with backoff until it goes
|
|
17
|
+
* terminal. Always returns the LAST snapshot: a task still `submitted` at the deadline
|
|
18
|
+
* is handed back as-is for the caller to decide (poll later, or give up).
|
|
19
|
+
*/
|
|
20
|
+
export declare function pollTask(fetchTask: () => Promise<unknown>, options: PollOptions): Promise<unknown>;
|
|
21
|
+
//# sourceMappingURL=poll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAK7C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAOD;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EACjC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAelB"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAuBnE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-server.d.ts","sourceRoot":"","sources":["../src/stdio-server.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/stdio-server.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// ../common/dist/http-client.js
|
|
7
|
+
function parseApiErrorBody(body) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(body);
|
|
10
|
+
if (typeof parsed !== "object" || parsed === null || !("error" in parsed))
|
|
11
|
+
return void 0;
|
|
12
|
+
const error = parsed.error;
|
|
13
|
+
if (typeof error === "object" && error !== null && typeof error.code === "string" && typeof error.message === "string") {
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
return void 0;
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var DudamelApiError = class extends Error {
|
|
22
|
+
status;
|
|
23
|
+
statusText;
|
|
24
|
+
body;
|
|
25
|
+
/** Stable machine-readable code from the API error body, or 'UNKNOWN' for non-conforming bodies. */
|
|
26
|
+
code;
|
|
27
|
+
/** Per-field validation errors, when the API provided them. */
|
|
28
|
+
details;
|
|
29
|
+
/** Extra machine-readable data (e.g. `inputsSchema` on execute validation failures). */
|
|
30
|
+
context;
|
|
31
|
+
constructor(status, statusText, body) {
|
|
32
|
+
const apiError = parseApiErrorBody(body);
|
|
33
|
+
const lines = [
|
|
34
|
+
apiError ? `Dudamel API error ${status} ${statusText}: ${apiError.code}: ${apiError.message}` : `Dudamel API error ${status} ${statusText}: ${body}`
|
|
35
|
+
];
|
|
36
|
+
if (apiError?.details?.length) {
|
|
37
|
+
lines.push(...apiError.details.map((d) => ` - ${d.path}: ${d.message}`));
|
|
38
|
+
}
|
|
39
|
+
if (apiError?.context?.["inputsSchema"] !== void 0) {
|
|
40
|
+
lines.push(`Expected inputs schema: ${JSON.stringify(apiError.context["inputsSchema"])}`);
|
|
41
|
+
}
|
|
42
|
+
super(lines.join("\n"));
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.statusText = statusText;
|
|
45
|
+
this.body = body;
|
|
46
|
+
this.name = "DudamelApiError";
|
|
47
|
+
this.code = apiError?.code ?? "UNKNOWN";
|
|
48
|
+
this.details = apiError?.details;
|
|
49
|
+
this.context = apiError?.context;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var DudamelApiClient = class {
|
|
53
|
+
baseUrl;
|
|
54
|
+
headers;
|
|
55
|
+
constructor(baseUrl, apiKey2, authScheme) {
|
|
56
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
57
|
+
this.headers = {
|
|
58
|
+
Authorization: `${authScheme ?? "ApiKey"} ${apiKey2}`,
|
|
59
|
+
"Content-Type": "application/json"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async get(path, params) {
|
|
63
|
+
const url = this.buildUrl(path, params);
|
|
64
|
+
return this.request(url, { method: "GET" });
|
|
65
|
+
}
|
|
66
|
+
async post(path, body, params) {
|
|
67
|
+
const url = this.buildUrl(path, params);
|
|
68
|
+
return this.request(url, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async put(path, body, params) {
|
|
74
|
+
const url = this.buildUrl(path, params);
|
|
75
|
+
return this.request(url, {
|
|
76
|
+
method: "PUT",
|
|
77
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async delete(path, params) {
|
|
81
|
+
const url = this.buildUrl(path, params);
|
|
82
|
+
return this.request(url, { method: "DELETE" });
|
|
83
|
+
}
|
|
84
|
+
buildUrl(path, params) {
|
|
85
|
+
const url = new URL(path, this.baseUrl);
|
|
86
|
+
if (params) {
|
|
87
|
+
for (const [key, value] of Object.entries(params)) {
|
|
88
|
+
if (value !== void 0 && value !== "") {
|
|
89
|
+
url.searchParams.set(key, value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return url.toString();
|
|
94
|
+
}
|
|
95
|
+
async request(url, init) {
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
...init,
|
|
98
|
+
headers: this.headers
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const body = await response.text();
|
|
102
|
+
throw new DudamelApiError(response.status, response.statusText, body);
|
|
103
|
+
}
|
|
104
|
+
const text = await response.text();
|
|
105
|
+
if (!text) {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
return JSON.parse(text);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ../common/dist/read-only-filter.js
|
|
113
|
+
function withReadOnlyFilter(server2, readOnly2) {
|
|
114
|
+
if (!readOnly2)
|
|
115
|
+
return server2;
|
|
116
|
+
const original = server2.registerTool.bind(server2);
|
|
117
|
+
const filtered = (...args) => {
|
|
118
|
+
const config = args[1];
|
|
119
|
+
if (config?.annotations?.readOnlyHint === false) {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
return original(...args);
|
|
123
|
+
};
|
|
124
|
+
return new Proxy(server2, {
|
|
125
|
+
get(target, prop, receiver) {
|
|
126
|
+
if (prop === "registerTool")
|
|
127
|
+
return filtered;
|
|
128
|
+
return Reflect.get(target, prop, receiver);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ../common/dist/create-server.js
|
|
134
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
135
|
+
|
|
136
|
+
// ../common/dist/stdio.js
|
|
137
|
+
function resolveStdioContext() {
|
|
138
|
+
return {
|
|
139
|
+
apiUrl: process.env["DUDAMEL_API_URL"] ?? "http://localhost:3000",
|
|
140
|
+
apiKey: process.env["DUDAMEL_API_KEY"],
|
|
141
|
+
envRole: process.env["DUDAMEL_USER_ROLE"],
|
|
142
|
+
// read/write access is per agent-assignment; the spawner injects the mode.
|
|
143
|
+
readOnly: process.env["DUDAMEL_MCP_MODE"] === "readonly",
|
|
144
|
+
agentName: process.env["DUDAMEL_AGENT_NAME"]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ../common/dist/util.js
|
|
149
|
+
function jsonResult(data) {
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function errorResult(error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: message }],
|
|
158
|
+
isError: true
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/server.ts
|
|
163
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
164
|
+
|
|
165
|
+
// src/tools/peers.ts
|
|
166
|
+
import { z } from "zod";
|
|
167
|
+
|
|
168
|
+
// src/poll.ts
|
|
169
|
+
var GET_TASK_MAX_WAIT_SECONDS = 120;
|
|
170
|
+
var INITIAL_POLL_INTERVAL_MS = 2e3;
|
|
171
|
+
var MAX_POLL_INTERVAL_MS = 1e4;
|
|
172
|
+
function isTerminal(task) {
|
|
173
|
+
const status = task?.status;
|
|
174
|
+
return status === "completed" || status === "failed";
|
|
175
|
+
}
|
|
176
|
+
async function pollTask(fetchTask, options) {
|
|
177
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
178
|
+
const now = options.now ?? Date.now;
|
|
179
|
+
const deadline = now() + options.waitMs;
|
|
180
|
+
let interval = INITIAL_POLL_INTERVAL_MS;
|
|
181
|
+
for (; ; ) {
|
|
182
|
+
const task = await fetchTask();
|
|
183
|
+
if (isTerminal(task)) return task;
|
|
184
|
+
const remaining = deadline - now();
|
|
185
|
+
if (remaining <= 0) return task;
|
|
186
|
+
await sleep(Math.min(interval, remaining));
|
|
187
|
+
interval = Math.min(interval * 1.5, MAX_POLL_INTERVAL_MS);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/tools/peers.ts
|
|
192
|
+
function registerPeerTools(server2, client, config) {
|
|
193
|
+
server2.registerTool(
|
|
194
|
+
"list-agents",
|
|
195
|
+
{
|
|
196
|
+
description: "List the dudamel agents you can message, as agent cards: name, description and skills. The `name` is what send-message takes. Read the cards to decide WHO to delegate to \u2014 an empty list means no other agent is active, not an error.",
|
|
197
|
+
inputSchema: {},
|
|
198
|
+
annotations: { readOnlyHint: true }
|
|
199
|
+
},
|
|
200
|
+
async () => {
|
|
201
|
+
try {
|
|
202
|
+
return jsonResult(await client.get("/api/conversations/peers"));
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return errorResult(err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
server2.registerTool(
|
|
209
|
+
"send-message",
|
|
210
|
+
{
|
|
211
|
+
description: "Send a message to another dudamel agent. Returns a task: in async mode (the default) it comes back `submitted` immediately \u2014 tell the user you delegated and end your turn; the answer arrives later as an `a2a-task-completed` system message (or poll it with get-task if you have no conversation). `sync` blocks and returns the task `completed` with the answer \u2014 only for short questions. Every response carries a `contextId`: pass it back on the next send-message to CONTINUE that conversation with the peer (it keeps its full history); omit it to start fresh.",
|
|
212
|
+
inputSchema: {
|
|
213
|
+
agentName: z.string().min(1).describe("Peer agent to message \u2014 a `name` from list-agents"),
|
|
214
|
+
message: z.string().min(1).describe(
|
|
215
|
+
"The message. Be specific and self-contained: the peer does not see your conversation"
|
|
216
|
+
),
|
|
217
|
+
contextId: z.string().optional().describe("contextId from a previous exchange \u2014 continues that same thread"),
|
|
218
|
+
mode: z.enum(["async", "sync"]).optional().describe("async (default): fire and get notified. sync: wait for the answer inline")
|
|
219
|
+
},
|
|
220
|
+
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
221
|
+
},
|
|
222
|
+
async ({ agentName, message, contextId, mode }) => {
|
|
223
|
+
try {
|
|
224
|
+
return jsonResult(
|
|
225
|
+
await client.post("/api/conversations/peers/messages", {
|
|
226
|
+
agentName,
|
|
227
|
+
message,
|
|
228
|
+
...contextId ? { contextId } : {},
|
|
229
|
+
mode: mode ?? "async",
|
|
230
|
+
// Infrastructure correlation — from the env the bridge injects, never
|
|
231
|
+
// model-visible parameters (Plan 44, decisión 6).
|
|
232
|
+
...config.conversationId ? { callerConversationId: config.conversationId } : {},
|
|
233
|
+
...config.responseTarget ? { callerResponseTarget: config.responseTarget } : {},
|
|
234
|
+
chain: config.agentChain ?? []
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return errorResult(err);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
server2.registerTool(
|
|
243
|
+
"get-task",
|
|
244
|
+
{
|
|
245
|
+
description: `Status and result of a send-message task by its \`taskId\`. Statuses: submitted (the peer has not answered yet), completed (\`result\` holds the answer), failed (\`errorSummary\` says why). Use it when you sent async and no completion message can reach you (e.g. you run outside a dudamel conversation). Pass \`waitSeconds\` (max ${GET_TASK_MAX_WAIT_SECONDS}) to have the tool poll internally and return as soon as the task goes terminal \u2014 if it is still submitted at the deadline, call again later instead of looping yourself.`,
|
|
246
|
+
inputSchema: {
|
|
247
|
+
taskId: z.string().min(1).describe("The `taskId` a send-message response returned"),
|
|
248
|
+
waitSeconds: z.number().int().min(0).max(GET_TASK_MAX_WAIT_SECONDS).optional().describe("Keep polling internally up to this many seconds (default 0: one look)")
|
|
249
|
+
},
|
|
250
|
+
annotations: { readOnlyHint: true }
|
|
251
|
+
},
|
|
252
|
+
async ({ taskId, waitSeconds }) => {
|
|
253
|
+
try {
|
|
254
|
+
const task = await pollTask(
|
|
255
|
+
() => client.get(`/api/conversations/tasks/${encodeURIComponent(taskId)}`),
|
|
256
|
+
{ waitMs: Math.min(waitSeconds ?? 0, GET_TASK_MAX_WAIT_SECONDS) * 1e3 }
|
|
257
|
+
);
|
|
258
|
+
return jsonResult(task);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
return errorResult(err);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/server.ts
|
|
267
|
+
function createPeersServer(config) {
|
|
268
|
+
const server2 = new McpServer2(
|
|
269
|
+
{ name: "dudamel-peers", version: "0.1.0" },
|
|
270
|
+
{
|
|
271
|
+
capabilities: { tools: {} },
|
|
272
|
+
instructions: "Dudamel Peers MCP \u2014 message other dudamel agents. Flow: list-agents to see who exists and what they are good at \u2192 send-message (async by default: tell the user you delegated, end your turn, and react when the a2a-task-completed system message arrives; sync only for short questions) \u2192 re-send with the returned contextId to continue the same thread, where the peer remembers everything. These are OTHER agents of this dudamel instance, reached over the platform \u2014 NOT your own sub-agents."
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
const client = new DudamelApiClient(config.apiUrl, config.apiKey, config.authScheme);
|
|
276
|
+
const host = withReadOnlyFilter(server2, config.readOnly ?? false);
|
|
277
|
+
registerPeerTools(host, client, config);
|
|
278
|
+
return server2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/stdio-server.ts
|
|
282
|
+
function parseJsonEnv(name) {
|
|
283
|
+
const raw = process.env[name];
|
|
284
|
+
if (!raw) return void 0;
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(raw);
|
|
287
|
+
} catch {
|
|
288
|
+
process.stderr.write(`Warning: ${name} is not valid JSON \u2014 ignored.
|
|
289
|
+
`);
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var { apiUrl, apiKey, readOnly } = resolveStdioContext();
|
|
294
|
+
var conversationId = process.env["DUDAMEL_CONVERSATION_ID"];
|
|
295
|
+
var responseTarget = parseJsonEnv("DUDAMEL_RESPONSE_TARGET");
|
|
296
|
+
var agentChain = parseJsonEnv("DUDAMEL_AGENT_CHAIN");
|
|
297
|
+
if (!apiKey) {
|
|
298
|
+
process.stderr.write("Error: DUDAMEL_API_KEY environment variable is required.\n");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
var server = createPeersServer({
|
|
302
|
+
apiUrl,
|
|
303
|
+
apiKey,
|
|
304
|
+
readOnly,
|
|
305
|
+
conversationId,
|
|
306
|
+
responseTarget,
|
|
307
|
+
agentChain
|
|
308
|
+
});
|
|
309
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { DudamelApiClient } from '@dudamel/mcp-common';
|
|
3
|
+
import type { McpPeersConfig } from '../types.js';
|
|
4
|
+
export declare function registerPeerTools(server: Pick<McpServer, 'registerTool'>, client: DudamelApiClient, config: McpPeersConfig): void;
|
|
5
|
+
//# sourceMappingURL=peers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peers.d.ts","sourceRoot":"","sources":["../../src/tools/peers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EACvC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,cAAc,GACrB,IAAI,CA0GN"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface McpPeersConfig {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
authScheme?: 'ApiKey' | 'Bearer';
|
|
5
|
+
/** From `DUDAMEL_MCP_MODE=readonly` — hides `send-message`. */
|
|
6
|
+
readOnly?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The conversation this turn serves (`DUDAMEL_CONVERSATION_ID`, injected by the
|
|
9
|
+
* bridge). Sent as the async reply target — absent (a terminal, a workflow step) the
|
|
10
|
+
* result is recovered with `get-task` instead.
|
|
11
|
+
*/
|
|
12
|
+
conversationId?: string;
|
|
13
|
+
/** `DUDAMEL_RESPONSE_TARGET` JSON — where the caller's turn is answering, captured so
|
|
14
|
+
* the completion event can reach that channel later. */
|
|
15
|
+
responseTarget?: Record<string, unknown>;
|
|
16
|
+
/** `DUDAMEL_AGENT_CHAIN` JSON — the causal agent chain, forwarded for the loop guard. */
|
|
17
|
+
agentChain?: string[];
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;6DACyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB"}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudamel/mcp-peers",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2026.1104.1",
|
|
4
|
+
"description": "Dudamel Peers MCP — stdio client for messaging the dudamel agents of an org (A2A-shaped tasks) over its REST API.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"dudamel-mcp-peers": "./dist/stdio-server.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/agents-squad/dudamel.git",
|
|
24
|
+
"directory": "packages/mcp/peers"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
31
|
+
"zod": "^3.24.4"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"esbuild": "^0.25.0",
|
|
35
|
+
"@dudamel/mcp-common": "0.1.0",
|
|
36
|
+
"@dudamel/mcp-tool-contract": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "esbuild src/stdio-server.ts src/index.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:@modelcontextprotocol/sdk --external:zod && tsc --emitDeclarationOnly",
|
|
40
|
+
"lint": "eslint src/",
|
|
41
|
+
"lint:fix": "eslint src/ --fix",
|
|
42
|
+
"test": "vitest run --passWithNoTests",
|
|
43
|
+
"clean": "rm -rf dist"
|
|
44
|
+
}
|
|
6
45
|
}
|
package/README.md
DELETED