herdr-link 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/PROTOCOL.md +39 -19
- package/README.md +116 -12
- package/README.zh-CN.md +116 -12
- package/dist/herdr-link.mcp.js +224 -16
- package/dist/herdr-link.opencode.js +206 -10
- package/docs/mcp-wiring.md +18 -15
- package/examples/agent_config.example.json +41 -0
- package/package.json +3 -2
- package/src/herdr.ts +231 -0
- package/src/mcp.ts +55 -26
- package/src/opencode.ts +43 -11
- package/src/pi.ts +46 -11
- package/src/protocol.ts +49 -2
package/dist/herdr-link.mcp.js
CHANGED
|
@@ -6,16 +6,20 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
// src/herdr.ts
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
9
10
|
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { resolve } from "node:path";
|
|
10
12
|
|
|
11
13
|
// src/protocol.ts
|
|
12
14
|
var PROTOCOL_ID = "herdr-link/1";
|
|
13
15
|
var AGENT_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
14
16
|
var HERDR_LINK_GATEWAY = "herdr_link";
|
|
17
|
+
var TOOL_START = "herdr_link_start";
|
|
15
18
|
var TOOL_PEERS = "herdr_link_peers";
|
|
16
19
|
var TOOL_SEND = "herdr_link_send";
|
|
17
20
|
var TOOL_CLOSE = "herdr_link_close";
|
|
18
|
-
var HERDR_LINK_TOOLS = [TOOL_PEERS, TOOL_SEND, TOOL_CLOSE];
|
|
21
|
+
var HERDR_LINK_TOOLS = [TOOL_START, TOOL_PEERS, TOOL_SEND, TOOL_CLOSE];
|
|
22
|
+
var HERDR_LINK_COMMUNICATION_TOOLS = [TOOL_PEERS, TOOL_SEND, TOOL_CLOSE];
|
|
19
23
|
var AGENT_STATES = ["idle", "working", "blocked", "done", "unknown"];
|
|
20
24
|
function toAgentState(value) {
|
|
21
25
|
if (typeof value === "string") {
|
|
@@ -26,6 +30,7 @@ function toAgentState(value) {
|
|
|
26
30
|
}
|
|
27
31
|
return "unknown";
|
|
28
32
|
}
|
|
33
|
+
var START_TOOL_DESCRIPTION = "Start a new Herdr Agent in an existing pane. Provide name and pane, then choose exactly one complete parameter source: config_agent for .agents/agent_config.json, or kind plus args for explicit Herdr start parameters. These modes are mutually exclusive; partial overrides are not supported. This operation does not create panes or retry/fallback after failure.";
|
|
29
34
|
var HerdrLinkError = class extends Error {
|
|
30
35
|
code;
|
|
31
36
|
constructor(code, detail) {
|
|
@@ -39,7 +44,12 @@ var AGENT_ERROR_DETAILS = {
|
|
|
39
44
|
SELF_UNNAMED: "Herdr Link could not establish a stable Agent Name",
|
|
40
45
|
PEER_NOT_FOUND: "target agent is not a live peer",
|
|
41
46
|
SEND_FAILED: "Herdr did not accept message delivery",
|
|
42
|
-
CLOSE_FAILED: "Herdr pane close failed"
|
|
47
|
+
CLOSE_FAILED: "Herdr pane close failed",
|
|
48
|
+
START_CONFIG_NOT_FOUND: "configured Agent start configuration was not found",
|
|
49
|
+
START_AGENT_NOT_FOUND: "configured Agent start entry was not found",
|
|
50
|
+
START_CONFIG_INVALID: "configured Agent start configuration is invalid",
|
|
51
|
+
START_INPUT_INVALID: "Agent start input is invalid",
|
|
52
|
+
START_FAILED: "Herdr did not accept Agent start"
|
|
43
53
|
};
|
|
44
54
|
function formatAgentFacingError(error, fallbackCode) {
|
|
45
55
|
const code = error instanceof HerdrLinkError ? error.code : fallbackCode;
|
|
@@ -111,14 +121,14 @@ var COMMUNICATION_CONTRACT = `Herdr Link is the standard interoperability channe
|
|
|
111
121
|
function attachCliOutput(error, stdout, stderr) {
|
|
112
122
|
Object.assign(error, { stdout, stderr });
|
|
113
123
|
}
|
|
114
|
-
var defaultHerdrRunner = (file, args) => new Promise((
|
|
124
|
+
var defaultHerdrRunner = (file, args) => new Promise((resolve2, reject) => {
|
|
115
125
|
execFile(file, args, { encoding: "utf8", shell: false }, (error, stdout, stderr) => {
|
|
116
126
|
if (error) {
|
|
117
127
|
attachCliOutput(error, String(stdout), String(stderr));
|
|
118
128
|
reject(error);
|
|
119
129
|
return;
|
|
120
130
|
}
|
|
121
|
-
|
|
131
|
+
resolve2({ stdout: String(stdout), stderr: String(stderr) });
|
|
122
132
|
});
|
|
123
133
|
});
|
|
124
134
|
var herdrRunner = defaultHerdrRunner;
|
|
@@ -167,6 +177,180 @@ async function runFor(args, failureCode) {
|
|
|
167
177
|
throw operationError(error, failureCode);
|
|
168
178
|
}
|
|
169
179
|
}
|
|
180
|
+
var startCursors = /* @__PURE__ */ new Map();
|
|
181
|
+
var startLocks = /* @__PURE__ */ new Map();
|
|
182
|
+
var START_CONFIG_PATH_PARTS = [".agents", "agent_config.json"];
|
|
183
|
+
var START_INPUT_KEYS = /* @__PURE__ */ new Set(["name", "pane", "config_agent", "kind", "args"]);
|
|
184
|
+
function hasOwn(value, key) {
|
|
185
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
186
|
+
}
|
|
187
|
+
function startInputError(detail) {
|
|
188
|
+
return new HerdrLinkError("START_INPUT_INVALID", detail);
|
|
189
|
+
}
|
|
190
|
+
function startConfigError(code, detail) {
|
|
191
|
+
return new HerdrLinkError(code, detail);
|
|
192
|
+
}
|
|
193
|
+
function validateStartInput(input) {
|
|
194
|
+
const value = asRecord(input);
|
|
195
|
+
if (!value) throw startInputError("start input must be an object");
|
|
196
|
+
for (const key of Object.keys(value)) {
|
|
197
|
+
if (!START_INPUT_KEYS.has(key)) throw startInputError(`unknown start field "${key}"`);
|
|
198
|
+
}
|
|
199
|
+
const name = value.name;
|
|
200
|
+
if (typeof name !== "string" || !isValidAgentName(name)) {
|
|
201
|
+
throw startInputError('"name" must be a valid Herdr Agent Name');
|
|
202
|
+
}
|
|
203
|
+
const pane = value.pane;
|
|
204
|
+
if (typeof pane !== "string" || pane.trim() === "") {
|
|
205
|
+
throw startInputError('"pane" must be a non-empty pane id');
|
|
206
|
+
}
|
|
207
|
+
const hasConfigAgent = hasOwn(value, "config_agent");
|
|
208
|
+
const hasKind = hasOwn(value, "kind");
|
|
209
|
+
const hasArgs = hasOwn(value, "args");
|
|
210
|
+
if (hasConfigAgent && (hasKind || hasArgs)) {
|
|
211
|
+
throw startInputError("config_agent cannot be combined with kind or args");
|
|
212
|
+
}
|
|
213
|
+
if (hasConfigAgent) {
|
|
214
|
+
const configAgent = value.config_agent;
|
|
215
|
+
if (typeof configAgent !== "string" || configAgent.trim() === "") {
|
|
216
|
+
throw startInputError('"config_agent" must be a non-empty string');
|
|
217
|
+
}
|
|
218
|
+
return { mode: "configured", name, pane, configAgent };
|
|
219
|
+
}
|
|
220
|
+
if (!hasKind || !hasArgs) {
|
|
221
|
+
throw startInputError("explicit start requires both kind and args");
|
|
222
|
+
}
|
|
223
|
+
const kind = value.kind;
|
|
224
|
+
if (typeof kind !== "string" || kind.trim() === "") {
|
|
225
|
+
throw startInputError('"kind" must be a non-empty string');
|
|
226
|
+
}
|
|
227
|
+
const args = value.args;
|
|
228
|
+
if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
|
|
229
|
+
throw startInputError('"args" must be an array of strings');
|
|
230
|
+
}
|
|
231
|
+
return { mode: "explicit", name, pane, variant: { kind, args: [...args] } };
|
|
232
|
+
}
|
|
233
|
+
function assertAllowedKeys(value, allowed, label) {
|
|
234
|
+
const allowedSet = new Set(allowed);
|
|
235
|
+
for (const key of Object.keys(value)) {
|
|
236
|
+
if (!allowedSet.has(key)) throw startConfigError("START_CONFIG_INVALID", `${label} contains unknown field "${key}"`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function validateConfiguredDocument(document) {
|
|
240
|
+
const root = asRecord(document);
|
|
241
|
+
if (!root) throw startConfigError("START_CONFIG_INVALID", "configuration root must be an object");
|
|
242
|
+
assertAllowedKeys(root, ["version", "agents"], "configuration root");
|
|
243
|
+
if (root.version !== 1) throw startConfigError("START_CONFIG_INVALID", "configuration version must be 1");
|
|
244
|
+
const agents = asRecord(root.agents);
|
|
245
|
+
if (!agents) throw startConfigError("START_CONFIG_INVALID", "agents must be an object");
|
|
246
|
+
const result = /* @__PURE__ */ new Map();
|
|
247
|
+
for (const [configAgent, rawEntry] of Object.entries(agents)) {
|
|
248
|
+
if (configAgent.trim() === "") {
|
|
249
|
+
throw startConfigError("START_CONFIG_INVALID", "agents contains an empty configuration key");
|
|
250
|
+
}
|
|
251
|
+
const entry = asRecord(rawEntry);
|
|
252
|
+
if (!entry) throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent} must be an object`);
|
|
253
|
+
assertAllowedKeys(entry, ["strategy", "variants"], `agents.${configAgent}`);
|
|
254
|
+
const rawVariants = entry.variants;
|
|
255
|
+
if (!Array.isArray(rawVariants) || rawVariants.length === 0) {
|
|
256
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants must be non-empty`);
|
|
257
|
+
}
|
|
258
|
+
const hasStrategy = hasOwn(entry, "strategy");
|
|
259
|
+
if (hasStrategy && entry.strategy !== "round-robin") {
|
|
260
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.strategy is unsupported`);
|
|
261
|
+
}
|
|
262
|
+
if (rawVariants.length > 1 && entry.strategy !== "round-robin") {
|
|
263
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent} requires strategy round-robin for multiple variants`);
|
|
264
|
+
}
|
|
265
|
+
const variants = rawVariants.map((rawVariant, index) => {
|
|
266
|
+
const variant = asRecord(rawVariant);
|
|
267
|
+
if (!variant) throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants[${index}] must be an object`);
|
|
268
|
+
assertAllowedKeys(variant, ["kind", "args"], `agents.${configAgent}.variants[${index}]`);
|
|
269
|
+
const kind = variant.kind;
|
|
270
|
+
if (typeof kind !== "string" || kind.trim() === "") {
|
|
271
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants[${index}].kind must be non-empty`);
|
|
272
|
+
}
|
|
273
|
+
const args = variant.args;
|
|
274
|
+
if (hasOwn(variant, "args") && (!Array.isArray(args) || !args.every((arg) => typeof arg === "string"))) {
|
|
275
|
+
throw startConfigError("START_CONFIG_INVALID", `agents.${configAgent}.variants[${index}].args must be an array of strings`);
|
|
276
|
+
}
|
|
277
|
+
return { kind, args: Array.isArray(args) ? [...args] : [] };
|
|
278
|
+
});
|
|
279
|
+
result.set(configAgent, {
|
|
280
|
+
...hasStrategy ? { strategy: "round-robin" } : {},
|
|
281
|
+
variants
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
async function loadConfiguredStartAgents(configPath) {
|
|
287
|
+
let text;
|
|
288
|
+
try {
|
|
289
|
+
text = await readFile(configPath, "utf8");
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const code = asRecord(error)?.code;
|
|
292
|
+
if (code === "ENOENT") {
|
|
293
|
+
throw startConfigError("START_CONFIG_NOT_FOUND", "agent_config.json was not found");
|
|
294
|
+
}
|
|
295
|
+
throw startConfigError("START_CONFIG_INVALID", "agent_config.json could not be read");
|
|
296
|
+
}
|
|
297
|
+
let document;
|
|
298
|
+
try {
|
|
299
|
+
document = JSON.parse(text);
|
|
300
|
+
} catch {
|
|
301
|
+
throw startConfigError("START_CONFIG_INVALID", "agent_config.json is not valid JSON");
|
|
302
|
+
}
|
|
303
|
+
return validateConfiguredDocument(document);
|
|
304
|
+
}
|
|
305
|
+
async function withStartCursorLock(key, operation) {
|
|
306
|
+
const previous = startLocks.get(key) ?? Promise.resolve();
|
|
307
|
+
let release;
|
|
308
|
+
const current = new Promise((resolve2) => {
|
|
309
|
+
release = resolve2;
|
|
310
|
+
});
|
|
311
|
+
startLocks.set(key, current);
|
|
312
|
+
await previous;
|
|
313
|
+
try {
|
|
314
|
+
return await operation();
|
|
315
|
+
} finally {
|
|
316
|
+
release();
|
|
317
|
+
if (startLocks.get(key) === current) startLocks.delete(key);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function runStart(name, pane, variant) {
|
|
321
|
+
try {
|
|
322
|
+
await runHerdr(["agent", "start", name, "--kind", variant.kind, "--pane", pane, "--", ...variant.args]);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error instanceof HerdrLinkError && error.code === "NOT_IN_HERDR") throw error;
|
|
325
|
+
throw operationError(error, "START_FAILED");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function startAgent(input, options = {}) {
|
|
329
|
+
assertHerdrEnvironment();
|
|
330
|
+
const validated = validateStartInput(input);
|
|
331
|
+
if (validated.mode === "explicit") {
|
|
332
|
+
await runStart(validated.name, validated.pane, validated.variant);
|
|
333
|
+
return { status: "started", agent: validated.name, kind: validated.variant.kind };
|
|
334
|
+
}
|
|
335
|
+
const projectRoot = typeof options.cwd === "string" && options.cwd.trim() !== "" ? options.cwd : process.cwd();
|
|
336
|
+
const configPath = resolve(projectRoot, ...START_CONFIG_PATH_PARTS);
|
|
337
|
+
const cursorKey = `${configPath}\0${validated.configAgent}`;
|
|
338
|
+
return withStartCursorLock(cursorKey, async () => {
|
|
339
|
+
const configuredAgents = await loadConfiguredStartAgents(configPath);
|
|
340
|
+
const configured = configuredAgents.get(validated.configAgent);
|
|
341
|
+
if (!configured) {
|
|
342
|
+
throw startConfigError("START_AGENT_NOT_FOUND", `configured Agent "${validated.configAgent}" was not found`);
|
|
343
|
+
}
|
|
344
|
+
const current = startCursors.get(cursorKey) ?? 0;
|
|
345
|
+
const variantIndex = current % configured.variants.length;
|
|
346
|
+
const variant = configured.variants[variantIndex];
|
|
347
|
+
await runStart(validated.name, validated.pane, variant);
|
|
348
|
+
if (configured.variants.length > 1) {
|
|
349
|
+
startCursors.set(cursorKey, (variantIndex + 1) % configured.variants.length);
|
|
350
|
+
}
|
|
351
|
+
return { status: "started", agent: validated.name, kind: variant.kind };
|
|
352
|
+
});
|
|
353
|
+
}
|
|
170
354
|
var CLI_ERROR_CODE_MAP = {
|
|
171
355
|
agent_not_found: "PEER_NOT_FOUND",
|
|
172
356
|
not_in_herdr: "NOT_IN_HERDR"
|
|
@@ -266,7 +450,7 @@ function stableName(record) {
|
|
|
266
450
|
}
|
|
267
451
|
var SELF_PROBE_ATTEMPTS = 3;
|
|
268
452
|
var SELF_PROBE_DELAY_MS = 100;
|
|
269
|
-
var sleepMs = (ms) => new Promise((
|
|
453
|
+
var sleepMs = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
270
454
|
async function fetchSelfRecord(pane) {
|
|
271
455
|
for (let attempt = 1; ; attempt += 1) {
|
|
272
456
|
try {
|
|
@@ -438,11 +622,27 @@ var METHOD_NOT_FOUND = -32601;
|
|
|
438
622
|
var INVALID_PARAMS = -32602;
|
|
439
623
|
var NORMAL_MESSAGING_RULE = "Use Herdr Link, not raw Herdr CLI, pane ids, or terminal input, for normal inter-agent messaging.";
|
|
440
624
|
var TOOL_DESCRIPTIONS = {
|
|
625
|
+
[TOOL_START]: `${START_TOOL_DESCRIPTION} ${NORMAL_MESSAGING_RULE}`,
|
|
441
626
|
[TOOL_PEERS]: `Discover live named peers in the same Herdr workspace; each state is advisory and Agent Names are the only addresses. ${NORMAL_MESSAGING_RULE}`,
|
|
442
627
|
[TOOL_SEND]: `Send a herdr-link/1 message to a live named peer in your own workspace; status "sent" means Herdr accepted delivery. ${NORMAL_MESSAGING_RULE}`,
|
|
443
628
|
[TOOL_CLOSE]: `Close the pane currently hosting a named same-workspace agent. If you need to send a final message before closing, complete the send first and call close in a later tool step. ${NORMAL_MESSAGING_RULE}`
|
|
444
629
|
};
|
|
445
630
|
var TOOL_INPUT_SCHEMAS = {
|
|
631
|
+
[TOOL_START]: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
name: { type: "string", description: "New Herdr Agent Name" },
|
|
635
|
+
pane: { type: "string", description: "Existing pane id" },
|
|
636
|
+
config_agent: { type: "string", description: "Configured Agent key; do not combine with kind or args" },
|
|
637
|
+
kind: { type: "string", description: "Herdr Agent kind for explicit start" },
|
|
638
|
+
args: { type: "array", items: { type: "string" }, description: "Complete Herdr Agent arguments for explicit start" }
|
|
639
|
+
},
|
|
640
|
+
required: ["name", "pane"],
|
|
641
|
+
oneOf: [
|
|
642
|
+
{ required: ["config_agent"], not: { anyOf: [{ required: ["kind"] }, { required: ["args"] }] } },
|
|
643
|
+
{ required: ["kind", "args"], not: { required: ["config_agent"] } }
|
|
644
|
+
]
|
|
645
|
+
},
|
|
446
646
|
[TOOL_PEERS]: { type: "object", properties: {} },
|
|
447
647
|
[TOOL_SEND]: {
|
|
448
648
|
type: "object",
|
|
@@ -461,20 +661,21 @@ var TOOL_INPUT_SCHEMAS = {
|
|
|
461
661
|
}
|
|
462
662
|
};
|
|
463
663
|
var FALLBACK_ERROR_CODE = {
|
|
664
|
+
[TOOL_START]: "START_FAILED",
|
|
464
665
|
[TOOL_PEERS]: "NOT_IN_HERDR",
|
|
465
666
|
[TOOL_SEND]: "SEND_FAILED",
|
|
466
667
|
[TOOL_CLOSE]: "CLOSE_FAILED"
|
|
467
668
|
};
|
|
468
669
|
var GATEWAY_TOOL = {
|
|
469
670
|
name: HERDR_LINK_GATEWAY,
|
|
470
|
-
description: 'Herdr Link gateway. Activate only when the user explicitly asks to use Herdr or when handling an inbound Herdr Link message. Cross-agent
|
|
671
|
+
description: 'Herdr Link gateway. Activate only when the user explicitly asks to use Herdr or when handling an inbound Herdr Link message. Cross-agent control starts dormant: call this tool once with no arguments ({}) to activate it for this session \u2014 the host is notified via notifications/tools/list_changed and herdr_link_start / herdr_link_peers / herdr_link_send / herdr_link_close become available as regular tools. If your host did not refresh its tool list, keep dispatching through the gateway: {"action":"start","arguments":{...}}, {"action":"peers"}, {"action":"send","arguments":{"to":...,"message":...}}, or {"action":"close","arguments":{"agent":...}}.',
|
|
471
672
|
inputSchema: {
|
|
472
673
|
type: "object",
|
|
473
674
|
properties: {
|
|
474
675
|
action: {
|
|
475
676
|
type: "string",
|
|
476
|
-
enum: ["activate", "peers", "send", "close"],
|
|
477
|
-
description: 'Omit or use "activate" to turn the session on; other values dispatch the corresponding peers, send, or close capability.'
|
|
677
|
+
enum: ["activate", "start", "peers", "send", "close"],
|
|
678
|
+
description: 'Omit or use "activate" to turn the session on; other values dispatch the corresponding start, peers, send, or close capability.'
|
|
478
679
|
},
|
|
479
680
|
arguments: {
|
|
480
681
|
type: "object",
|
|
@@ -547,6 +748,7 @@ function createRequestHandler(deps = {}) {
|
|
|
547
748
|
const runPeers = deps.listPeers ?? listPeers;
|
|
548
749
|
const runSend = deps.sendMessage ?? sendMessage;
|
|
549
750
|
const runClose = deps.closeAgentPane ?? closeAgentPane;
|
|
751
|
+
const runStart2 = deps.startAgent ?? startAgent;
|
|
550
752
|
const notify = deps.notify ?? stdoutNotificationSink;
|
|
551
753
|
let activated = false;
|
|
552
754
|
function activateSession() {
|
|
@@ -572,6 +774,8 @@ function createRequestHandler(deps = {}) {
|
|
|
572
774
|
}
|
|
573
775
|
async function executeCanonical(canonicalName, args) {
|
|
574
776
|
switch (canonicalName) {
|
|
777
|
+
case TOOL_START:
|
|
778
|
+
return await runStart2(args);
|
|
575
779
|
case TOOL_PEERS:
|
|
576
780
|
return await runPeers();
|
|
577
781
|
case TOOL_SEND: {
|
|
@@ -602,15 +806,15 @@ function createRequestHandler(deps = {}) {
|
|
|
602
806
|
activateSession();
|
|
603
807
|
return callSuccess(id, {
|
|
604
808
|
status: "active",
|
|
605
|
-
capabilities: ["peers", "send", "close"]
|
|
809
|
+
capabilities: ["start", "peers", "send", "close"]
|
|
606
810
|
});
|
|
607
811
|
}
|
|
608
|
-
if (typeof action !== "string" || !["peers", "send", "close"].includes(action)) {
|
|
812
|
+
if (typeof action !== "string" || !["start", "peers", "send", "close"].includes(action)) {
|
|
609
813
|
return fail(id, INVALID_PARAMS, `Unknown gateway action: ${String(action)}`);
|
|
610
814
|
}
|
|
611
|
-
const canonicalName = action === "peers" ? TOOL_PEERS : action === "send" ? TOOL_SEND : TOOL_CLOSE;
|
|
815
|
+
const canonicalName = action === "start" ? TOOL_START : action === "peers" ? TOOL_PEERS : action === "send" ? TOOL_SEND : TOOL_CLOSE;
|
|
612
816
|
activateSession();
|
|
613
|
-
const dispatchArgs = isRecord(args.arguments) ? args.arguments : args;
|
|
817
|
+
const dispatchArgs = isRecord(args.arguments) ? args.arguments : Object.fromEntries(Object.entries(args).filter(([key]) => key !== "action"));
|
|
614
818
|
return await callCanonicalTool(id, canonicalName, dispatchArgs);
|
|
615
819
|
}
|
|
616
820
|
async function callTool(id, params) {
|
|
@@ -710,15 +914,18 @@ function contractWithAppendix(appendix) {
|
|
|
710
914
|
${appendix}`;
|
|
711
915
|
}
|
|
712
916
|
function buildMcpPrefixedCommunicationContract(namespace) {
|
|
713
|
-
const [peers, send, close] =
|
|
917
|
+
const [peers, send, close] = HERDR_LINK_COMMUNICATION_TOOLS.map(
|
|
714
918
|
(name) => mcpPresentedToolName(name, namespace)
|
|
715
919
|
);
|
|
920
|
+
const start = mcpPresentedToolName(TOOL_START, namespace);
|
|
716
921
|
const gateway = mcpPresentedToolName(HERDR_LINK_GATEWAY, namespace);
|
|
717
922
|
return contractWithAppendix(
|
|
718
923
|
`In this runtime Herdr Link starts dormant: only the ${gateway} gateway tool is listed until it is activated.
|
|
719
924
|
- Call ${gateway} once with no arguments ({}); the host then receives notifications/tools/list_changed and the cross-agent tools become available.
|
|
720
|
-
- If the host did not refresh its tool list, keep dispatching through the gateway: {"action":"peers"}, {"action":"send","arguments":{...}}, {"action":"close","arguments":{...}}.
|
|
925
|
+
- If the host did not refresh its tool list, keep dispatching through the gateway: {"action":"start","arguments":{...}}, {"action":"peers"}, {"action":"send","arguments":{...}}, {"action":"close","arguments":{...}}.
|
|
926
|
+
- ${START_TOOL_DESCRIPTION}
|
|
721
927
|
The tools are presented under MCP-prefixed names (the canonical name is always the suffix):
|
|
928
|
+
- herdr_link_start -> ${start}
|
|
722
929
|
- herdr_link_peers -> ${peers}
|
|
723
930
|
- herdr_link_send -> ${send}
|
|
724
931
|
- herdr_link_close -> ${close}`
|
|
@@ -728,13 +935,14 @@ function buildMcpWrapperCommunicationContract(wrapperName, serverName) {
|
|
|
728
935
|
return contractWithAppendix(
|
|
729
936
|
`In this runtime Herdr Link starts dormant: only the Tier 0 gateway (${HERDR_LINK_GATEWAY}) is listed until it is activated.
|
|
730
937
|
- Invoke the gateway once with empty Arguments {} (ToolName "${HERDR_LINK_GATEWAY}"); the host then receives notifications/tools/list_changed and the cross-agent tools become available.
|
|
731
|
-
- If the host did not refresh its tool list, keep dispatching through the gateway with ToolName "${HERDR_LINK_GATEWAY}" and an Arguments object carrying {"action":"peers"|"send"|"close", ...}.
|
|
938
|
+
- If the host did not refresh its tool list, keep dispatching through the gateway with ToolName "${HERDR_LINK_GATEWAY}" and an Arguments object carrying {"action":"start"|"peers"|"send"|"close", ...}.
|
|
939
|
+
- ${START_TOOL_DESCRIPTION}
|
|
732
940
|
|
|
733
941
|
After activation, Herdr Link MCP tools are invoked through ${wrapperName}.
|
|
734
942
|
|
|
735
943
|
Use:
|
|
736
944
|
- ServerName: "${serverName}"
|
|
737
|
-
- ToolName: "herdr_link_peers", "herdr_link_send", or "herdr_link_close"
|
|
945
|
+
- ToolName: "herdr_link_start", "herdr_link_peers", "herdr_link_send", or "herdr_link_close"
|
|
738
946
|
- Arguments: the canonical input object for that Herdr Link tool`
|
|
739
947
|
);
|
|
740
948
|
}
|