agent-relay 8.7.2 → 8.8.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/dist/auto/classifier.d.ts +29 -0
- package/dist/auto/classifier.d.ts.map +1 -0
- package/dist/auto/classifier.js +126 -0
- package/dist/auto/classifier.js.map +1 -0
- package/dist/auto/composer.d.ts +105 -0
- package/dist/auto/composer.d.ts.map +1 -0
- package/dist/auto/composer.js +439 -0
- package/dist/auto/composer.js.map +1 -0
- package/dist/auto/director-prompt.d.ts +25 -0
- package/dist/auto/director-prompt.d.ts.map +1 -0
- package/dist/auto/director-prompt.js +53 -0
- package/dist/auto/director-prompt.js.map +1 -0
- package/dist/auto/index.d.ts +15 -0
- package/dist/auto/index.d.ts.map +1 -0
- package/dist/auto/index.js +13 -0
- package/dist/auto/index.js.map +1 -0
- package/dist/cli/agent-relay-mcp.d.ts +1 -1
- package/dist/cli/agent-relay-mcp.d.ts.map +1 -1
- package/dist/cli/agent-relay-mcp.js +428 -518
- package/dist/cli/agent-relay-mcp.js.map +1 -1
- package/dist/cli/bootstrap.d.ts.map +1 -1
- package/dist/cli/bootstrap.js +2 -0
- package/dist/cli/bootstrap.js.map +1 -1
- package/dist/cli/commands/core.d.ts +1 -0
- package/dist/cli/commands/core.d.ts.map +1 -1
- package/dist/cli/commands/core.js +1 -1
- package/dist/cli/commands/core.js.map +1 -1
- package/dist/cli/commands/fleet.d.ts +16 -0
- package/dist/cli/commands/fleet.d.ts.map +1 -0
- package/dist/cli/commands/fleet.js +188 -0
- package/dist/cli/commands/fleet.js.map +1 -0
- package/dist/cli/commands/local-agent.d.ts.map +1 -1
- package/dist/cli/commands/local-agent.js +43 -11
- package/dist/cli/commands/local-agent.js.map +1 -1
- package/dist/cli/lib/broker-lifecycle.d.ts +5 -1
- package/dist/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/cli/lib/broker-lifecycle.js +32 -2
- package/dist/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/cli/lib/fleet-sidecar.d.ts +53 -0
- package/dist/cli/lib/fleet-sidecar.d.ts.map +1 -0
- package/dist/cli/lib/fleet-sidecar.js +400 -0
- package/dist/cli/lib/fleet-sidecar.js.map +1 -0
- package/dist/index.cjs +2545 -16773
- package/package.json +10 -8
|
@@ -7,10 +7,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { ListToolsRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { RelayCast, SDK_VERSION, WsClient } from '@relaycast/sdk';
|
|
9
9
|
import { INVALID_AGENT_TOKEN_CODE, agentTokenRecoveryMessage, isInvalidAgentTokenError, isInvalidAgentTokenToolResult, } from '@agent-relay/sdk';
|
|
10
|
+
import { AgentRelay } from '@agent-relay/sdk';
|
|
10
11
|
import { z } from 'zod';
|
|
11
|
-
import { initTelemetry, shutdown as shutdownTelemetry
|
|
12
|
+
import { initTelemetry, shutdown as shutdownTelemetry } from './telemetry/index.js';
|
|
12
13
|
import { relaycastWorkspaceTelemetryOptions, withRelaycastTelemetry } from './lib/relaycast-telemetry.js';
|
|
13
|
-
import { errorClassName } from './lib/telemetry-helpers.js';
|
|
14
14
|
const DEFAULT_BASE_URL = 'https://gateway.relaycast.dev';
|
|
15
15
|
export const AGENT_RELAY_MCP_VERSION = process.env.AGENT_RELAY_CLI_VERSION ?? SDK_VERSION ?? 'unknown';
|
|
16
16
|
let mcpTelemetryExitHookInstalled = false;
|
|
@@ -30,6 +30,10 @@ const DEFAULT_SYSTEM_PROMPT = `You are an AI agent in a collaborative workspace
|
|
|
30
30
|
- Reply to threads with "reply_to_thread"
|
|
31
31
|
- React to messages with "add_reaction"
|
|
32
32
|
|
|
33
|
+
## Fleet
|
|
34
|
+
- Use "query_nodes" to find fleet nodes by capability or name
|
|
35
|
+
- Use "spawn" to invoke the fleet spawn action on an eligible node
|
|
36
|
+
|
|
33
37
|
## Best Practices
|
|
34
38
|
- Check your inbox regularly for new messages and mentions
|
|
35
39
|
- Use channels for topic-based discussions
|
|
@@ -207,319 +211,6 @@ function createInitialSession(options) {
|
|
|
207
211
|
wsInitAttempted: false,
|
|
208
212
|
};
|
|
209
213
|
}
|
|
210
|
-
async function createWorkspace(name, baseUrl) {
|
|
211
|
-
return (await RelayCast.createWorkspace(name, {
|
|
212
|
-
baseUrl,
|
|
213
|
-
...relaycastWorkspaceTelemetryOptions(),
|
|
214
|
-
}));
|
|
215
|
-
}
|
|
216
|
-
function extractWorkspaceKey(payload) {
|
|
217
|
-
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
218
|
-
const value = payload.workspaceKey ??
|
|
219
|
-
payload.workspace_key ??
|
|
220
|
-
payload.apiKey ??
|
|
221
|
-
payload.api_key ??
|
|
222
|
-
data.workspaceKey ??
|
|
223
|
-
data.workspace_key ??
|
|
224
|
-
data.apiKey ??
|
|
225
|
-
data.api_key;
|
|
226
|
-
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
227
|
-
}
|
|
228
|
-
function extractWorkspaceName(payload, fallback) {
|
|
229
|
-
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
230
|
-
const value = payload.workspaceName ?? payload.workspace_name ?? payload.name ?? data.workspaceName;
|
|
231
|
-
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
232
|
-
}
|
|
233
|
-
function requireWorkspaceKey(session) {
|
|
234
|
-
if (session.workspaceKey) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
throw new Error('Workspace key not configured. Call "create_workspace" first, or "set_workspace_key" if someone shared a workspace key.');
|
|
238
|
-
}
|
|
239
|
-
function jsonContent(value) {
|
|
240
|
-
const structuredContent = typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
241
|
-
? value
|
|
242
|
-
: { value };
|
|
243
|
-
return {
|
|
244
|
-
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
245
|
-
structuredContent,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
function textContent(message, structuredContent = { message }) {
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: 'text', text: message }],
|
|
251
|
-
structuredContent,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
function isSchemaObject(schema) {
|
|
255
|
-
return Boolean(schema &&
|
|
256
|
-
typeof schema === 'object' &&
|
|
257
|
-
!Array.isArray(schema) &&
|
|
258
|
-
typeof schema.safeParse !== 'function');
|
|
259
|
-
}
|
|
260
|
-
function getSchemaDescription(schema) {
|
|
261
|
-
return isSchemaObject(schema) && typeof schema.description === 'string' ? schema.description : undefined;
|
|
262
|
-
}
|
|
263
|
-
function zodFromJsonSchema(schema) {
|
|
264
|
-
if (schema === false) {
|
|
265
|
-
return z.never();
|
|
266
|
-
}
|
|
267
|
-
if (!isSchemaObject(schema)) {
|
|
268
|
-
return z.unknown();
|
|
269
|
-
}
|
|
270
|
-
let zodType;
|
|
271
|
-
const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
272
|
-
switch (schemaType) {
|
|
273
|
-
case 'array':
|
|
274
|
-
zodType = z.array(zodFromJsonSchema(schema.items));
|
|
275
|
-
break;
|
|
276
|
-
case 'boolean':
|
|
277
|
-
zodType = z.boolean();
|
|
278
|
-
break;
|
|
279
|
-
case 'integer':
|
|
280
|
-
zodType = z.number().int();
|
|
281
|
-
break;
|
|
282
|
-
case 'number':
|
|
283
|
-
zodType = z.number();
|
|
284
|
-
break;
|
|
285
|
-
case 'object':
|
|
286
|
-
if (schema.properties) {
|
|
287
|
-
const required = new Set(schema.required ?? []);
|
|
288
|
-
const shape = {};
|
|
289
|
-
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
|
290
|
-
const child = zodFromJsonSchema(childSchema);
|
|
291
|
-
shape[key] = required.has(key) ? child : child.optional();
|
|
292
|
-
}
|
|
293
|
-
zodType = z.object(shape).passthrough();
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
zodType = z.record(z.string(), z.unknown());
|
|
297
|
-
}
|
|
298
|
-
break;
|
|
299
|
-
case 'string':
|
|
300
|
-
zodType = z.string();
|
|
301
|
-
break;
|
|
302
|
-
default:
|
|
303
|
-
zodType = z.unknown();
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
const description = getSchemaDescription(schema);
|
|
307
|
-
return description ? zodType.describe(description) : zodType;
|
|
308
|
-
}
|
|
309
|
-
function actionToolInputSchema(schema) {
|
|
310
|
-
const zodShape = zodObjectShape(schema);
|
|
311
|
-
if (zodShape) {
|
|
312
|
-
return zodShape;
|
|
313
|
-
}
|
|
314
|
-
if (!isSchemaObject(schema) || schema.type !== 'object') {
|
|
315
|
-
return {
|
|
316
|
-
input: z.unknown().describe('Action input payload. The action registry performs final validation.'),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
const required = new Set(schema.required ?? []);
|
|
320
|
-
const shape = {};
|
|
321
|
-
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
322
|
-
const child = zodFromJsonSchema(childSchema);
|
|
323
|
-
shape[key] = required.has(key) ? child : child.optional();
|
|
324
|
-
}
|
|
325
|
-
return shape;
|
|
326
|
-
}
|
|
327
|
-
function actionInvocationInput(descriptor, args) {
|
|
328
|
-
const schema = descriptor.inputSchema;
|
|
329
|
-
if (zodObjectShape(schema)) {
|
|
330
|
-
return args;
|
|
331
|
-
}
|
|
332
|
-
if (!isSchemaObject(schema) || schema.type !== 'object') {
|
|
333
|
-
return typeof args === 'object' && args !== null && 'input' in args
|
|
334
|
-
? args.input
|
|
335
|
-
: args;
|
|
336
|
-
}
|
|
337
|
-
return args;
|
|
338
|
-
}
|
|
339
|
-
function zodObjectShape(schema) {
|
|
340
|
-
if (schema instanceof z.ZodObject) {
|
|
341
|
-
return schema.shape;
|
|
342
|
-
}
|
|
343
|
-
return undefined;
|
|
344
|
-
}
|
|
345
|
-
function serializableActionDescriptor(descriptor) {
|
|
346
|
-
return {
|
|
347
|
-
name: descriptor.name,
|
|
348
|
-
description: descriptor.description,
|
|
349
|
-
visibility: descriptor.visibility,
|
|
350
|
-
...(descriptor.inputSchema ? { inputSchema: serializableActionSchema(descriptor.inputSchema) } : {}),
|
|
351
|
-
...(descriptor.outputSchema ? { outputSchema: serializableActionSchema(descriptor.outputSchema) } : {}),
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
function serializableActionSchema(schema) {
|
|
355
|
-
if (isSchemaObject(schema)) {
|
|
356
|
-
return schema;
|
|
357
|
-
}
|
|
358
|
-
if (isZodLikeSchema(schema)) {
|
|
359
|
-
return {
|
|
360
|
-
type: 'zod',
|
|
361
|
-
...(schema.description ? { description: schema.description } : {}),
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
return schema;
|
|
365
|
-
}
|
|
366
|
-
function isZodLikeSchema(schema) {
|
|
367
|
-
return Boolean(schema &&
|
|
368
|
-
typeof schema === 'object' &&
|
|
369
|
-
!Array.isArray(schema) &&
|
|
370
|
-
typeof schema.safeParse === 'function');
|
|
371
|
-
}
|
|
372
|
-
function registerAgentRelayActionTools(server, actions, getSession, onAuditEvent, getAgentClient, actionToolNames) {
|
|
373
|
-
if (!actions) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Fire-and-forget invocation through the relay: returns an immediate ack
|
|
378
|
-
* (with an `invocation_id`) and does NOT run the handler inline. Falls back to
|
|
379
|
-
* the local in-process registry when the relay action surface is unavailable.
|
|
380
|
-
*/
|
|
381
|
-
const invokeAction = async (name, input) => {
|
|
382
|
-
const relayActions = getRelayAgentActions(getAgentClient);
|
|
383
|
-
if (relayActions) {
|
|
384
|
-
try {
|
|
385
|
-
const ack = await relayActions.invoke(name, asInputRecord(input));
|
|
386
|
-
return jsonContent({ ok: true, status: 'invoked', invocation: ack });
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const session = getSession();
|
|
393
|
-
const result = await actions.invoke({
|
|
394
|
-
name,
|
|
395
|
-
input,
|
|
396
|
-
context: {
|
|
397
|
-
caller: { name: session.agentName ?? 'mcp', type: 'agent' },
|
|
398
|
-
emit: onAuditEvent,
|
|
399
|
-
},
|
|
400
|
-
});
|
|
401
|
-
return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
|
|
402
|
-
};
|
|
403
|
-
server.registerTool('list_actions', {
|
|
404
|
-
title: 'List Actions',
|
|
405
|
-
description: 'List Agent Relay actions available to this agent.',
|
|
406
|
-
inputSchema: {},
|
|
407
|
-
outputSchema: jsonResult,
|
|
408
|
-
annotations: {
|
|
409
|
-
readOnlyHint: true,
|
|
410
|
-
destructiveHint: false,
|
|
411
|
-
idempotentHint: true,
|
|
412
|
-
openWorldHint: false,
|
|
413
|
-
},
|
|
414
|
-
}, async () => jsonContent({
|
|
415
|
-
actions: (await actions.list({ visibility: 'agent' })).map(serializableActionDescriptor),
|
|
416
|
-
}));
|
|
417
|
-
server.registerTool('invoke_action', {
|
|
418
|
-
title: 'Invoke Action',
|
|
419
|
-
description: 'Invoke a registered Agent Relay action by name. Fire-and-forget: returns an ack with an invocation id; the result arrives asynchronously to the action handler.',
|
|
420
|
-
inputSchema: {
|
|
421
|
-
name: z.string().describe('Registered action name'),
|
|
422
|
-
input: z.unknown().describe('Action input payload'),
|
|
423
|
-
},
|
|
424
|
-
outputSchema: jsonResult,
|
|
425
|
-
annotations: {
|
|
426
|
-
readOnlyHint: false,
|
|
427
|
-
destructiveHint: false,
|
|
428
|
-
idempotentHint: false,
|
|
429
|
-
openWorldHint: false,
|
|
430
|
-
},
|
|
431
|
-
}, async ({ name, input }) => invokeAction(name, input));
|
|
432
|
-
void actions
|
|
433
|
-
.list({ visibility: 'agent' })
|
|
434
|
-
.then((descriptors) => {
|
|
435
|
-
for (const descriptor of descriptors) {
|
|
436
|
-
actionToolNames?.add(descriptor.name);
|
|
437
|
-
server.registerTool(descriptor.name, {
|
|
438
|
-
title: descriptor.name,
|
|
439
|
-
description: descriptor.description,
|
|
440
|
-
inputSchema: actionToolInputSchema(descriptor.inputSchema),
|
|
441
|
-
outputSchema: jsonResult,
|
|
442
|
-
annotations: {
|
|
443
|
-
readOnlyHint: false,
|
|
444
|
-
destructiveHint: false,
|
|
445
|
-
idempotentHint: false,
|
|
446
|
-
openWorldHint: false,
|
|
447
|
-
},
|
|
448
|
-
}, async (args) => invokeAction(descriptor.name, actionInvocationInput(descriptor, args)));
|
|
449
|
-
}
|
|
450
|
-
})
|
|
451
|
-
.catch(() => undefined);
|
|
452
|
-
}
|
|
453
|
-
/** The relay-backed action surface on the live agent client, when available. */
|
|
454
|
-
function getRelayAgentActions(getAgentClient) {
|
|
455
|
-
if (!getAgentClient) {
|
|
456
|
-
return undefined;
|
|
457
|
-
}
|
|
458
|
-
try {
|
|
459
|
-
return getAgentClient().actions;
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
return undefined;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
function asInputRecord(input) {
|
|
466
|
-
if (input === undefined || input === null) {
|
|
467
|
-
return undefined;
|
|
468
|
-
}
|
|
469
|
-
if (typeof input === 'object' && !Array.isArray(input)) {
|
|
470
|
-
return input;
|
|
471
|
-
}
|
|
472
|
-
return { input };
|
|
473
|
-
}
|
|
474
|
-
function errorMessage(error) {
|
|
475
|
-
return error instanceof Error ? error.message : String(error);
|
|
476
|
-
}
|
|
477
|
-
function createRegisteredAgent(agentName, agentToken) {
|
|
478
|
-
return { agentName, agentToken };
|
|
479
|
-
}
|
|
480
|
-
export async function registerAgentWithRebind({ session, setSession, getRelay, name, type, persona, metadata, strictAgentName, preferredAgentName, forcedAgentType, }) {
|
|
481
|
-
requireWorkspaceKey(session);
|
|
482
|
-
const configuredName = session.agentName ?? preferredAgentName?.trim() ?? null;
|
|
483
|
-
const warnings = [];
|
|
484
|
-
const effectiveName = strictAgentName && configuredName ? configuredName : name;
|
|
485
|
-
if (strictAgentName && configuredName && name.trim() !== configuredName) {
|
|
486
|
-
warnings.push(`Strict worker identity is enabled; ignoring requested name "${name}" and using "${configuredName}".`);
|
|
487
|
-
}
|
|
488
|
-
const effectiveType = forcedAgentType ?? type;
|
|
489
|
-
if (forcedAgentType && type && type !== forcedAgentType) {
|
|
490
|
-
warnings.push(`Forced worker type is enabled; ignoring requested type "${type}" and using "${forcedAgentType}".`);
|
|
491
|
-
}
|
|
492
|
-
if (session.agentToken && effectiveName && strictAgentName) {
|
|
493
|
-
// If the session tracks per-identity agents, only short-circuit when the
|
|
494
|
-
// strict-named identity is still registered. After an `agent_token_invalid`
|
|
495
|
-
// recovery the entry is dropped from the map, which lets this fall through
|
|
496
|
-
// to a fresh registerOrRotate instead of handing back the dead token.
|
|
497
|
-
const cachedAgent = session.agents?.get(effectiveName);
|
|
498
|
-
const knowsIdentities = session.agents !== undefined;
|
|
499
|
-
if (!knowsIdentities || cachedAgent) {
|
|
500
|
-
return {
|
|
501
|
-
name: effectiveName,
|
|
502
|
-
token: cachedAgent?.agentToken ?? session.agentToken,
|
|
503
|
-
registered_name: effectiveName,
|
|
504
|
-
warnings,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
const relay = getRelay();
|
|
509
|
-
const result = await relay.agents.registerOrRotate({
|
|
510
|
-
name: effectiveName,
|
|
511
|
-
type: effectiveType,
|
|
512
|
-
persona,
|
|
513
|
-
metadata,
|
|
514
|
-
});
|
|
515
|
-
const reboundName = result.name?.trim() ? result.name : effectiveName;
|
|
516
|
-
setSession({ agentToken: result.token, agentName: reboundName });
|
|
517
|
-
return {
|
|
518
|
-
...result,
|
|
519
|
-
registered_name: reboundName,
|
|
520
|
-
warnings,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
214
|
class SubscriptionManager {
|
|
524
215
|
subscriptions = new Set();
|
|
525
216
|
subscribe(uri) {
|
|
@@ -574,15 +265,11 @@ function eventToResourceUris(event) {
|
|
|
574
265
|
case 'member.joined':
|
|
575
266
|
case 'member.left':
|
|
576
267
|
return ['relay://channels'];
|
|
577
|
-
case 'webhook.received':
|
|
268
|
+
case 'webhook.received':
|
|
269
|
+
case 'command.invoked': {
|
|
578
270
|
const channel = getStringEventField(event, 'channel');
|
|
579
271
|
return channel ? [`relay://channels/${channel}/messages`] : [];
|
|
580
272
|
}
|
|
581
|
-
case 'action.invoked':
|
|
582
|
-
case 'action.completed':
|
|
583
|
-
case 'action.failed':
|
|
584
|
-
// Actions are not channel-scoped; surface invocations via the inbox.
|
|
585
|
-
return ['relay://inbox'];
|
|
586
273
|
case 'reaction.added':
|
|
587
274
|
case 'reaction.removed':
|
|
588
275
|
return ['relay://inbox'];
|
|
@@ -701,208 +388,383 @@ function formatInbox(inbox, selfName) {
|
|
|
701
388
|
lines.push(` :${reaction.emoji}: on your message in #${reaction.channelName} by @${reaction.agentName}`);
|
|
702
389
|
}
|
|
703
390
|
}
|
|
704
|
-
return lines.length === 1 ? '' : lines.join('\n');
|
|
391
|
+
return lines.length === 1 ? '' : lines.join('\n');
|
|
392
|
+
}
|
|
393
|
+
function readAsIdentity(args) {
|
|
394
|
+
const [input] = args;
|
|
395
|
+
if (typeof input !== 'object' || input === null)
|
|
396
|
+
return undefined;
|
|
397
|
+
const as = input.as;
|
|
398
|
+
return typeof as === 'string' ? as : undefined;
|
|
399
|
+
}
|
|
400
|
+
function invalidAgentTokenToolResult() {
|
|
401
|
+
const text = agentTokenRecoveryMessage();
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: 'text', text }],
|
|
404
|
+
structuredContent: {
|
|
405
|
+
error: { code: INVALID_AGENT_TOKEN_CODE, message: text },
|
|
406
|
+
},
|
|
407
|
+
isError: true,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken) {
|
|
411
|
+
const original = mcpServer.registerTool.bind(mcpServer);
|
|
412
|
+
const mutableServer = mcpServer;
|
|
413
|
+
mutableServer.registerTool = (name, config, handler) => {
|
|
414
|
+
if (!handler) {
|
|
415
|
+
return original(name, config, handler);
|
|
416
|
+
}
|
|
417
|
+
const wrapped = async (...args) => {
|
|
418
|
+
const asIdentity = readAsIdentity(args);
|
|
419
|
+
let result;
|
|
420
|
+
try {
|
|
421
|
+
result = await handler(...args);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
if (name !== 'register_agent' && isInvalidAgentTokenError(err)) {
|
|
425
|
+
invalidateAgentToken(asIdentity);
|
|
426
|
+
return invalidAgentTokenToolResult();
|
|
427
|
+
}
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
if (name !== 'register_agent' && isInvalidAgentTokenToolResult(result)) {
|
|
431
|
+
invalidateAgentToken(asIdentity);
|
|
432
|
+
if (hasContentArray(result)) {
|
|
433
|
+
result.content.push({ type: 'text', text: agentTokenRecoveryMessage() });
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
if (SKIP_PIGGYBACK.has(name) || !getSession().agentToken || !hasContentArray(result)) {
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
const inbox = await getAgentClient(asIdentity).inbox();
|
|
442
|
+
const inboxText = formatInbox(inbox, asIdentity ?? getSession().agentName);
|
|
443
|
+
if (inboxText) {
|
|
444
|
+
result.content.push({ type: 'text', text: inboxText });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
if (isInvalidAgentTokenError(err)) {
|
|
449
|
+
invalidateAgentToken(asIdentity);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
};
|
|
454
|
+
return original(name, config, wrapped);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function createWorkspace(name, baseUrl) {
|
|
458
|
+
return (await RelayCast.createWorkspace(name, {
|
|
459
|
+
baseUrl,
|
|
460
|
+
...relaycastWorkspaceTelemetryOptions(),
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
function extractWorkspaceKey(payload) {
|
|
464
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
465
|
+
const value = payload.workspaceKey ??
|
|
466
|
+
payload.workspace_key ??
|
|
467
|
+
payload.apiKey ??
|
|
468
|
+
payload.api_key ??
|
|
469
|
+
data.workspaceKey ??
|
|
470
|
+
data.workspace_key ??
|
|
471
|
+
data.apiKey ??
|
|
472
|
+
data.api_key;
|
|
473
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
474
|
+
}
|
|
475
|
+
function extractWorkspaceName(payload, fallback) {
|
|
476
|
+
const data = payload.data && typeof payload.data === 'object' ? payload.data : {};
|
|
477
|
+
const value = payload.workspaceName ?? payload.workspace_name ?? payload.name ?? data.workspaceName;
|
|
478
|
+
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
479
|
+
}
|
|
480
|
+
function requireWorkspaceKey(session) {
|
|
481
|
+
if (session.workspaceKey) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
throw new Error('Workspace key not configured. Call "create_workspace" first, or "set_workspace_key" if someone shared a workspace key.');
|
|
485
|
+
}
|
|
486
|
+
function jsonContent(value) {
|
|
487
|
+
const structuredContent = typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
488
|
+
? value
|
|
489
|
+
: { value };
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
492
|
+
structuredContent,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function textContent(message, structuredContent = { message }) {
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: 'text', text: message }],
|
|
498
|
+
structuredContent,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function isSchemaObject(schema) {
|
|
502
|
+
return Boolean(schema &&
|
|
503
|
+
typeof schema === 'object' &&
|
|
504
|
+
!Array.isArray(schema) &&
|
|
505
|
+
typeof schema.safeParse !== 'function');
|
|
506
|
+
}
|
|
507
|
+
function getSchemaDescription(schema) {
|
|
508
|
+
return isSchemaObject(schema) && typeof schema.description === 'string' ? schema.description : undefined;
|
|
509
|
+
}
|
|
510
|
+
function zodFromJsonSchema(schema) {
|
|
511
|
+
if (schema === false) {
|
|
512
|
+
return z.never();
|
|
513
|
+
}
|
|
514
|
+
if (!isSchemaObject(schema)) {
|
|
515
|
+
return z.unknown();
|
|
516
|
+
}
|
|
517
|
+
let zodType;
|
|
518
|
+
const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
519
|
+
switch (schemaType) {
|
|
520
|
+
case 'array':
|
|
521
|
+
zodType = z.array(zodFromJsonSchema(schema.items));
|
|
522
|
+
break;
|
|
523
|
+
case 'boolean':
|
|
524
|
+
zodType = z.boolean();
|
|
525
|
+
break;
|
|
526
|
+
case 'integer':
|
|
527
|
+
zodType = z.number().int();
|
|
528
|
+
break;
|
|
529
|
+
case 'number':
|
|
530
|
+
zodType = z.number();
|
|
531
|
+
break;
|
|
532
|
+
case 'object':
|
|
533
|
+
if (schema.properties) {
|
|
534
|
+
const required = new Set(schema.required ?? []);
|
|
535
|
+
const shape = {};
|
|
536
|
+
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
|
537
|
+
const child = zodFromJsonSchema(childSchema);
|
|
538
|
+
shape[key] = required.has(key) ? child : child.optional();
|
|
539
|
+
}
|
|
540
|
+
zodType = z.object(shape).passthrough();
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
zodType = z.record(z.string(), z.unknown());
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
case 'string':
|
|
547
|
+
zodType = z.string();
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
zodType = z.unknown();
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
const description = getSchemaDescription(schema);
|
|
554
|
+
return description ? zodType.describe(description) : zodType;
|
|
555
|
+
}
|
|
556
|
+
function actionToolInputSchema(schema) {
|
|
557
|
+
const zodShape = zodObjectShape(schema);
|
|
558
|
+
if (zodShape) {
|
|
559
|
+
return zodShape;
|
|
560
|
+
}
|
|
561
|
+
if (!isSchemaObject(schema) || schema.type !== 'object') {
|
|
562
|
+
return {
|
|
563
|
+
input: z.unknown().describe('Action input payload. The action registry performs final validation.'),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const required = new Set(schema.required ?? []);
|
|
567
|
+
const shape = {};
|
|
568
|
+
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
569
|
+
const child = zodFromJsonSchema(childSchema);
|
|
570
|
+
shape[key] = required.has(key) ? child : child.optional();
|
|
571
|
+
}
|
|
572
|
+
return shape;
|
|
573
|
+
}
|
|
574
|
+
function actionInvocationInput(descriptor, args) {
|
|
575
|
+
const schema = descriptor.inputSchema;
|
|
576
|
+
if (zodObjectShape(schema)) {
|
|
577
|
+
return args;
|
|
578
|
+
}
|
|
579
|
+
if (!isSchemaObject(schema) || schema.type !== 'object') {
|
|
580
|
+
return typeof args === 'object' && args !== null && 'input' in args
|
|
581
|
+
? args.input
|
|
582
|
+
: args;
|
|
583
|
+
}
|
|
584
|
+
return args;
|
|
705
585
|
}
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return typeof as === 'string' ? as : undefined;
|
|
586
|
+
function zodObjectShape(schema) {
|
|
587
|
+
if (schema instanceof z.ZodObject) {
|
|
588
|
+
return schema.shape;
|
|
589
|
+
}
|
|
590
|
+
return undefined;
|
|
712
591
|
}
|
|
713
|
-
function
|
|
714
|
-
const text = agentTokenRecoveryMessage();
|
|
592
|
+
function serializableActionDescriptor(descriptor) {
|
|
715
593
|
return {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
},
|
|
720
|
-
|
|
594
|
+
name: descriptor.name,
|
|
595
|
+
description: descriptor.description,
|
|
596
|
+
visibility: descriptor.visibility,
|
|
597
|
+
...(descriptor.inputSchema ? { inputSchema: serializableActionSchema(descriptor.inputSchema) } : {}),
|
|
598
|
+
...(descriptor.outputSchema ? { outputSchema: serializableActionSchema(descriptor.outputSchema) } : {}),
|
|
721
599
|
};
|
|
722
600
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
invoke_action: { toolType: 'action.invoke', toolCategory: 'action' },
|
|
727
|
-
list_actions: { toolType: 'action.list', toolCategory: 'action' },
|
|
728
|
-
submit_result: { toolType: 'result.submit', toolCategory: 'result' },
|
|
729
|
-
create_workspace: { toolType: 'workspace.create', toolCategory: 'workspace' },
|
|
730
|
-
set_workspace_key: { toolType: 'workspace.set_key', toolCategory: 'workspace' },
|
|
731
|
-
register_agent: { toolType: 'agent.register', toolCategory: 'agent' },
|
|
732
|
-
list_agents: { toolType: 'agent.list', toolCategory: 'agent' },
|
|
733
|
-
post_message: { toolType: 'message.post', toolCategory: 'message' },
|
|
734
|
-
send_dm: { toolType: 'message.dm', toolCategory: 'message' },
|
|
735
|
-
send_group_dm: { toolType: 'message.group_dm', toolCategory: 'message' },
|
|
736
|
-
list_dms: { toolType: 'message.dm_list', toolCategory: 'message' },
|
|
737
|
-
list_messages: { toolType: 'message.list', toolCategory: 'message' },
|
|
738
|
-
get_message: { toolType: 'message.get', toolCategory: 'message' },
|
|
739
|
-
reply_to_thread: { toolType: 'message.reply', toolCategory: 'message' },
|
|
740
|
-
get_message_thread: { toolType: 'message.thread', toolCategory: 'message' },
|
|
741
|
-
get_thread: { toolType: 'message.thread', toolCategory: 'message' },
|
|
742
|
-
search_messages: { toolType: 'message.search', toolCategory: 'message' },
|
|
743
|
-
create_channel: { toolType: 'channel.create', toolCategory: 'channel' },
|
|
744
|
-
list_channels: { toolType: 'channel.list', toolCategory: 'channel' },
|
|
745
|
-
join_channel: { toolType: 'channel.join', toolCategory: 'channel' },
|
|
746
|
-
leave_channel: { toolType: 'channel.leave', toolCategory: 'channel' },
|
|
747
|
-
set_channel_topic: { toolType: 'channel.set_topic', toolCategory: 'channel' },
|
|
748
|
-
archive_channel: { toolType: 'channel.archive', toolCategory: 'channel' },
|
|
749
|
-
invite_to_channel: { toolType: 'channel.invite', toolCategory: 'channel' },
|
|
750
|
-
list_channel_members: { toolType: 'channel.member_list', toolCategory: 'channel' },
|
|
751
|
-
add_reaction: { toolType: 'reaction.add', toolCategory: 'reaction' },
|
|
752
|
-
remove_reaction: { toolType: 'reaction.remove', toolCategory: 'reaction' },
|
|
753
|
-
check_inbox: { toolType: 'inbox.check', toolCategory: 'inbox' },
|
|
754
|
-
mark_message_read: { toolType: 'inbox.mark_read', toolCategory: 'inbox' },
|
|
755
|
-
get_message_readers: { toolType: 'inbox.reader_list', toolCategory: 'inbox' },
|
|
756
|
-
};
|
|
757
|
-
function readInvokedActionName(name, args) {
|
|
758
|
-
if (name !== 'invoke_action') {
|
|
759
|
-
return undefined;
|
|
601
|
+
function serializableActionSchema(schema) {
|
|
602
|
+
if (isSchemaObject(schema)) {
|
|
603
|
+
return schema;
|
|
760
604
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
605
|
+
if (isZodLikeSchema(schema)) {
|
|
606
|
+
return {
|
|
607
|
+
type: 'zod',
|
|
608
|
+
...(schema.description ? { description: schema.description } : {}),
|
|
609
|
+
};
|
|
764
610
|
}
|
|
765
|
-
|
|
766
|
-
return typeof actionName === 'string' && actionName.trim() ? actionName : undefined;
|
|
611
|
+
return schema;
|
|
767
612
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
case 'status':
|
|
778
|
-
return 'agent';
|
|
779
|
-
default:
|
|
780
|
-
return 'action';
|
|
613
|
+
function isZodLikeSchema(schema) {
|
|
614
|
+
return Boolean(schema &&
|
|
615
|
+
typeof schema === 'object' &&
|
|
616
|
+
!Array.isArray(schema) &&
|
|
617
|
+
typeof schema.safeParse === 'function');
|
|
618
|
+
}
|
|
619
|
+
function registerAgentRelayActionTools(server, actions, getSession, onAuditEvent, getAgentClient, actionToolNames) {
|
|
620
|
+
if (!actions) {
|
|
621
|
+
return;
|
|
781
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Fire-and-forget invocation through the relay: returns an immediate ack
|
|
625
|
+
* (with an `invocation_id`) and does NOT run the handler inline. Falls back to
|
|
626
|
+
* the local in-process registry when the relay action surface is unavailable.
|
|
627
|
+
*/
|
|
628
|
+
const invokeAction = async (name, input) => {
|
|
629
|
+
const relayActions = getRelayAgentActions(getAgentClient);
|
|
630
|
+
if (relayActions) {
|
|
631
|
+
try {
|
|
632
|
+
const ack = await relayActions.invoke(name, asInputRecord(input));
|
|
633
|
+
return jsonContent({ ok: true, status: 'invoked', invocation: ack });
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
return { ...jsonContent({ ok: false, error: errorMessage(error) }), isError: true };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const session = getSession();
|
|
640
|
+
const result = await actions.invoke({
|
|
641
|
+
name,
|
|
642
|
+
input,
|
|
643
|
+
context: {
|
|
644
|
+
caller: { name: session.agentName ?? 'mcp', type: 'agent' },
|
|
645
|
+
emit: onAuditEvent,
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
return result.ok ? jsonContent(result) : { ...jsonContent(result), isError: true };
|
|
649
|
+
};
|
|
650
|
+
server.registerTool('list_actions', {
|
|
651
|
+
title: 'List Actions',
|
|
652
|
+
description: 'List Agent Relay actions available to this agent.',
|
|
653
|
+
inputSchema: {},
|
|
654
|
+
outputSchema: jsonResult,
|
|
655
|
+
annotations: {
|
|
656
|
+
readOnlyHint: true,
|
|
657
|
+
destructiveHint: false,
|
|
658
|
+
idempotentHint: true,
|
|
659
|
+
openWorldHint: false,
|
|
660
|
+
},
|
|
661
|
+
}, async () => jsonContent({
|
|
662
|
+
actions: (await actions.list({ visibility: 'agent' })).map(serializableActionDescriptor),
|
|
663
|
+
}));
|
|
664
|
+
server.registerTool('invoke_action', {
|
|
665
|
+
title: 'Invoke Action',
|
|
666
|
+
description: 'Invoke a registered Agent Relay action by name. Fire-and-forget: returns an ack with an invocation id; the result arrives asynchronously to the action handler.',
|
|
667
|
+
inputSchema: {
|
|
668
|
+
name: z.string().describe('Registered action name'),
|
|
669
|
+
input: z.unknown().describe('Action input payload'),
|
|
670
|
+
},
|
|
671
|
+
outputSchema: jsonResult,
|
|
672
|
+
annotations: {
|
|
673
|
+
readOnlyHint: false,
|
|
674
|
+
destructiveHint: false,
|
|
675
|
+
idempotentHint: false,
|
|
676
|
+
openWorldHint: false,
|
|
677
|
+
},
|
|
678
|
+
}, async ({ name, input }) => invokeAction(name, input));
|
|
679
|
+
void actions
|
|
680
|
+
.list({ visibility: 'agent' })
|
|
681
|
+
.then((descriptors) => {
|
|
682
|
+
for (const descriptor of descriptors) {
|
|
683
|
+
actionToolNames?.add(descriptor.name);
|
|
684
|
+
server.registerTool(descriptor.name, {
|
|
685
|
+
title: descriptor.name,
|
|
686
|
+
description: descriptor.description,
|
|
687
|
+
inputSchema: actionToolInputSchema(descriptor.inputSchema),
|
|
688
|
+
outputSchema: jsonResult,
|
|
689
|
+
annotations: {
|
|
690
|
+
readOnlyHint: false,
|
|
691
|
+
destructiveHint: false,
|
|
692
|
+
idempotentHint: false,
|
|
693
|
+
openWorldHint: false,
|
|
694
|
+
},
|
|
695
|
+
}, async (args) => invokeAction(descriptor.name, actionInvocationInput(descriptor, args)));
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
.catch(() => undefined);
|
|
782
699
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (
|
|
786
|
-
return
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
700
|
+
/** The relay-backed action surface on the live agent client, when available. */
|
|
701
|
+
function getRelayAgentActions(getAgentClient) {
|
|
702
|
+
if (!getAgentClient) {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
return getAgentClient().actions;
|
|
790
707
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
return known;
|
|
708
|
+
catch {
|
|
709
|
+
return undefined;
|
|
794
710
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
711
|
+
}
|
|
712
|
+
function asInputRecord(input) {
|
|
713
|
+
if (input === undefined || input === null) {
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
if (typeof input === 'object' && !Array.isArray(input)) {
|
|
717
|
+
return input;
|
|
800
718
|
}
|
|
801
|
-
return {
|
|
719
|
+
return { input };
|
|
802
720
|
}
|
|
803
|
-
function
|
|
804
|
-
return
|
|
721
|
+
function errorMessage(error) {
|
|
722
|
+
return error instanceof Error ? error.message : String(error);
|
|
805
723
|
}
|
|
806
|
-
function
|
|
807
|
-
|
|
808
|
-
tool_name: input.toolName,
|
|
809
|
-
tool_type: input.toolType,
|
|
810
|
-
tool_category: input.toolCategory,
|
|
811
|
-
transport: input.transport ?? 'unknown',
|
|
812
|
-
success: input.success,
|
|
813
|
-
duration_ms: Date.now() - input.startedAt,
|
|
814
|
-
...(input.errorClass ? { error_class: input.errorClass } : {}),
|
|
815
|
-
});
|
|
724
|
+
function createRegisteredAgent(agentName, agentToken) {
|
|
725
|
+
return { agentName, agentToken };
|
|
816
726
|
}
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
727
|
+
export async function registerAgentWithRebind({ session, setSession, getRelay, name, type, persona, metadata, strictAgentName, preferredAgentName, forcedAgentType, }) {
|
|
728
|
+
requireWorkspaceKey(session);
|
|
729
|
+
const configuredName = session.agentName ?? preferredAgentName?.trim() ?? null;
|
|
730
|
+
const warnings = [];
|
|
731
|
+
const effectiveName = strictAgentName && configuredName ? configuredName : name;
|
|
732
|
+
if (strictAgentName && configuredName && name.trim() !== configuredName) {
|
|
733
|
+
warnings.push(`Strict worker identity is enabled; ignoring requested name "${name}" and using "${configuredName}".`);
|
|
734
|
+
}
|
|
735
|
+
const effectiveType = forcedAgentType ?? type;
|
|
736
|
+
if (forcedAgentType && type && type !== forcedAgentType) {
|
|
737
|
+
warnings.push(`Forced worker type is enabled; ignoring requested type "${type}" and using "${forcedAgentType}".`);
|
|
738
|
+
}
|
|
739
|
+
if (session.agentToken && effectiveName && strictAgentName) {
|
|
740
|
+
// If the session tracks per-identity agents, only short-circuit when the
|
|
741
|
+
// strict-named identity is still registered. After an `agent_token_invalid`
|
|
742
|
+
// recovery the entry is dropped from the map, which lets this fall through
|
|
743
|
+
// to a fresh registerOrRotate instead of handing back the dead token.
|
|
744
|
+
const cachedAgent = session.agents?.get(effectiveName);
|
|
745
|
+
const knowsIdentities = session.agents !== undefined;
|
|
746
|
+
if (!knowsIdentities || cachedAgent) {
|
|
747
|
+
return {
|
|
748
|
+
name: effectiveName,
|
|
749
|
+
token: cachedAgent?.agentToken ?? session.agentToken,
|
|
750
|
+
registered_name: effectiveName,
|
|
751
|
+
warnings,
|
|
752
|
+
};
|
|
823
753
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
toolName: name,
|
|
839
|
-
toolType: toolMetadata.toolType,
|
|
840
|
-
toolCategory: toolMetadata.toolCategory,
|
|
841
|
-
transport: telemetryTransport,
|
|
842
|
-
startedAt,
|
|
843
|
-
success: false,
|
|
844
|
-
errorClass: errorClassName(err) ?? 'InvalidAgentToken',
|
|
845
|
-
});
|
|
846
|
-
return invalidAgentTokenToolResult();
|
|
847
|
-
}
|
|
848
|
-
trackAgentRelayToolCall({
|
|
849
|
-
toolName: name,
|
|
850
|
-
toolType: toolMetadata.toolType,
|
|
851
|
-
toolCategory: toolMetadata.toolCategory,
|
|
852
|
-
transport: telemetryTransport,
|
|
853
|
-
startedAt,
|
|
854
|
-
success: false,
|
|
855
|
-
errorClass: errorClassName(err),
|
|
856
|
-
});
|
|
857
|
-
throw err;
|
|
858
|
-
}
|
|
859
|
-
// Successful response that still carries an "Invalid agent token" body.
|
|
860
|
-
if (name !== 'register_agent' && isInvalidAgentTokenToolResult(result)) {
|
|
861
|
-
invalidateAgentToken(asIdentity);
|
|
862
|
-
trackAgentRelayToolCall({
|
|
863
|
-
toolName: name,
|
|
864
|
-
toolType: toolMetadata.toolType,
|
|
865
|
-
toolCategory: toolMetadata.toolCategory,
|
|
866
|
-
transport: telemetryTransport,
|
|
867
|
-
startedAt,
|
|
868
|
-
success: false,
|
|
869
|
-
errorClass: 'InvalidAgentToken',
|
|
870
|
-
});
|
|
871
|
-
if (hasContentArray(result)) {
|
|
872
|
-
result.content.push({ type: 'text', text: agentTokenRecoveryMessage() });
|
|
873
|
-
}
|
|
874
|
-
return result;
|
|
875
|
-
}
|
|
876
|
-
if (!SKIP_PIGGYBACK.has(name) && getSession().agentToken && hasContentArray(result)) {
|
|
877
|
-
try {
|
|
878
|
-
const inbox = await getAgentClient(asIdentity).inbox();
|
|
879
|
-
const inboxText = formatInbox(inbox, asIdentity ?? getSession().agentName);
|
|
880
|
-
if (inboxText) {
|
|
881
|
-
result.content.push({ type: 'text', text: inboxText });
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
catch (err) {
|
|
885
|
-
// Inbox piggyback is opportunistic; the original tool result should
|
|
886
|
-
// still win. But if the inbox fetch itself reveals an invalid token,
|
|
887
|
-
// clear the stale identity so the next call doesn't reuse it.
|
|
888
|
-
if (isInvalidAgentTokenError(err)) {
|
|
889
|
-
invalidateAgentToken(asIdentity);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
const resultIsError = isErrorToolResult(result);
|
|
894
|
-
trackAgentRelayToolCall({
|
|
895
|
-
toolName: name,
|
|
896
|
-
toolType: toolMetadata.toolType,
|
|
897
|
-
toolCategory: toolMetadata.toolCategory,
|
|
898
|
-
transport: telemetryTransport,
|
|
899
|
-
startedAt,
|
|
900
|
-
success: !resultIsError,
|
|
901
|
-
...(resultIsError ? { errorClass: 'ToolResultError' } : {}),
|
|
902
|
-
});
|
|
903
|
-
return result;
|
|
904
|
-
};
|
|
905
|
-
return original(name, config, wrapped);
|
|
754
|
+
}
|
|
755
|
+
const relay = getRelay();
|
|
756
|
+
const result = await relay.agents.registerOrRotate({
|
|
757
|
+
name: effectiveName,
|
|
758
|
+
type: effectiveType,
|
|
759
|
+
persona,
|
|
760
|
+
metadata,
|
|
761
|
+
});
|
|
762
|
+
const reboundName = result.name?.trim() ? result.name : effectiveName;
|
|
763
|
+
setSession({ agentToken: result.token, agentName: reboundName });
|
|
764
|
+
return {
|
|
765
|
+
...result,
|
|
766
|
+
registered_name: reboundName,
|
|
767
|
+
warnings,
|
|
906
768
|
};
|
|
907
769
|
}
|
|
908
770
|
function resolveEmoji(input) {
|
|
@@ -1047,6 +909,23 @@ function registerAgentRelayTools(server, getRelay, getAgentClient, getSession, s
|
|
|
1047
909
|
const agents = await getRelay().agents.list(status ? { status } : undefined);
|
|
1048
910
|
return jsonContent({ agents });
|
|
1049
911
|
});
|
|
912
|
+
server.registerTool('query_nodes', {
|
|
913
|
+
title: 'Query Fleet Nodes',
|
|
914
|
+
description: 'Query registered fleet nodes by capability or name.',
|
|
915
|
+
inputSchema: {
|
|
916
|
+
capability: z.string().optional().describe('Optional capability name filter'),
|
|
917
|
+
name: z.string().optional().describe('Optional node name filter'),
|
|
918
|
+
},
|
|
919
|
+
outputSchema: {
|
|
920
|
+
nodes: z.array(z.object({}).passthrough()).describe('Fleet nodes'),
|
|
921
|
+
},
|
|
922
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
923
|
+
}, async ({ capability, name }) => {
|
|
924
|
+
const session = getSession();
|
|
925
|
+
requireWorkspaceKey(session);
|
|
926
|
+
const relay = new AgentRelay({ workspaceKey: session.workspaceKey ?? undefined, baseUrl });
|
|
927
|
+
return jsonContent({ nodes: await relay.nodes.list({ capability, name }) });
|
|
928
|
+
});
|
|
1050
929
|
server.registerTool('create_channel', {
|
|
1051
930
|
title: 'Create Channel',
|
|
1052
931
|
description: 'Create a new workspace channel.',
|
|
@@ -1355,10 +1234,47 @@ function registerAgentRelayTools(server, getRelay, getAgentClient, getSession, s
|
|
|
1355
1234
|
task,
|
|
1356
1235
|
channel,
|
|
1357
1236
|
persona,
|
|
1358
|
-
//
|
|
1359
|
-
// the
|
|
1360
|
-
|
|
1237
|
+
// SpawnAgentRequest has no top-level model field; pass via metadata
|
|
1238
|
+
// so the broker can extract it and forward --model to the launched CLI.
|
|
1239
|
+
metadata: model ? { model } : undefined,
|
|
1361
1240
|
})));
|
|
1241
|
+
server.registerTool('spawn', {
|
|
1242
|
+
title: 'Spawn Agent',
|
|
1243
|
+
description: 'Invoke the fleet spawn action. Optionally target a specific node.',
|
|
1244
|
+
inputSchema: {
|
|
1245
|
+
name: z.string().describe('Agent name'),
|
|
1246
|
+
cli: z.enum(['claude', 'codex', 'gemini', 'aider', 'goose']).describe('AI CLI to launch'),
|
|
1247
|
+
task: z.string().optional().describe('Initial task instructions'),
|
|
1248
|
+
channel: z.string().optional().describe('Channel to join'),
|
|
1249
|
+
channels: z.array(z.string()).optional().describe('Channels to join'),
|
|
1250
|
+
model: z.string().optional().describe('Model powering the worker'),
|
|
1251
|
+
session_ref: z.string().optional().describe('Session reference for resumable spawns'),
|
|
1252
|
+
target_node: z.string().optional().describe('Optional target fleet node name'),
|
|
1253
|
+
...identityOverrideInputShape,
|
|
1254
|
+
},
|
|
1255
|
+
outputSchema: jsonResult,
|
|
1256
|
+
annotations: {
|
|
1257
|
+
readOnlyHint: false,
|
|
1258
|
+
destructiveHint: false,
|
|
1259
|
+
idempotentHint: false,
|
|
1260
|
+
openWorldHint: true,
|
|
1261
|
+
},
|
|
1262
|
+
}, async ({ name, cli, task, channel, channels, model, session_ref, target_node, as }) => {
|
|
1263
|
+
const actions = getAgentClient(as).actions;
|
|
1264
|
+
if (!actions) {
|
|
1265
|
+
throw new Error('spawn requires an agent-scoped Relaycast actions client.');
|
|
1266
|
+
}
|
|
1267
|
+
const actionInput = {
|
|
1268
|
+
name,
|
|
1269
|
+
cli,
|
|
1270
|
+
...(task ? { task } : {}),
|
|
1271
|
+
...(model ? { model } : {}),
|
|
1272
|
+
...(session_ref ? { session_ref } : {}),
|
|
1273
|
+
...(target_node ? { target_node } : {}),
|
|
1274
|
+
...((channels ?? (channel ? [channel] : undefined)) ? { channels: channels ?? [channel] } : {}),
|
|
1275
|
+
};
|
|
1276
|
+
return jsonContent({ invocation: await actions.invoke('spawn', actionInput) });
|
|
1277
|
+
});
|
|
1362
1278
|
server.registerTool('remove_agent', {
|
|
1363
1279
|
title: 'Remove Agent',
|
|
1364
1280
|
description: 'Release a worker agent from active duty.',
|
|
@@ -1430,10 +1346,10 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1430
1346
|
if (session.agentToken && !session.wsBridge && !session.wsInitAttempted) {
|
|
1431
1347
|
try {
|
|
1432
1348
|
const subscriptions = new SubscriptionManager();
|
|
1433
|
-
const wsClient = new WsClient(
|
|
1349
|
+
const wsClient = new WsClient({
|
|
1434
1350
|
token: session.agentToken,
|
|
1435
1351
|
baseUrl: options.baseUrl,
|
|
1436
|
-
})
|
|
1352
|
+
});
|
|
1437
1353
|
const wsBridge = new RealtimeResourceBridge(wsClient, subscriptions, (uri) => {
|
|
1438
1354
|
mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined);
|
|
1439
1355
|
});
|
|
@@ -1457,9 +1373,6 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1457
1373
|
nextAgents.delete(targetName);
|
|
1458
1374
|
partial.agents = nextAgents;
|
|
1459
1375
|
}
|
|
1460
|
-
// Clear the active-session token when the invalidated identity is the
|
|
1461
|
-
// active one (or the caller didn't pin to a particular identity). The
|
|
1462
|
-
// active workspaceKey stays intact so `register_agent` can recover.
|
|
1463
1376
|
if (!asIdentity || asIdentity === session.agentName) {
|
|
1464
1377
|
if (session.agentToken !== null) {
|
|
1465
1378
|
partial.agentToken = null;
|
|
@@ -1492,7 +1405,7 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1492
1405
|
baseUrl: options.baseUrl,
|
|
1493
1406
|
})).as(agentToken, { autoHeartbeatMs: false });
|
|
1494
1407
|
};
|
|
1495
|
-
enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken
|
|
1408
|
+
enableInboxPiggyback(mcpServer, getSession, getAgentClient, invalidateAgentToken);
|
|
1496
1409
|
registerResourceDefinitions(mcpServer, getAgentClient, getRelay);
|
|
1497
1410
|
mcpServer.server.setRequestHandler(SubscribeRequestSchema, async (req) => {
|
|
1498
1411
|
session.subscriptions?.subscribe(req.params.uri);
|
|
@@ -1536,9 +1449,6 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1536
1449
|
return result;
|
|
1537
1450
|
});
|
|
1538
1451
|
}
|
|
1539
|
-
if (session.agentToken && !session.wsBridge) {
|
|
1540
|
-
setSession({ agentToken: session.agentToken, agentName: session.agentName });
|
|
1541
|
-
}
|
|
1542
1452
|
return mcpServer;
|
|
1543
1453
|
}
|
|
1544
1454
|
/** Relaycast agent tokens are opaque `at_live_<hex>` literals. Anything else
|