agent-relay 13.4.1 → 15.0.0-next.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/README.md +45 -29
- 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 +204 -11
- package/dist/cli/agent-relay-mcp.js.map +1 -1
- package/dist/cli/bootstrap.d.ts.map +1 -1
- package/dist/cli/bootstrap.js +19 -0
- package/dist/cli/bootstrap.js.map +1 -1
- package/dist/cli/commands/agent.d.ts +5 -1
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +50 -18
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/cloud.js +4 -4
- package/dist/cli/commands/cloud.js.map +1 -1
- package/dist/cli/commands/fleet.d.ts +2 -1
- package/dist/cli/commands/fleet.d.ts.map +1 -1
- package/dist/cli/commands/fleet.js +48 -6
- package/dist/cli/commands/fleet.js.map +1 -1
- package/dist/cli/commands/init.d.ts +47 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +626 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/integration-recipient.d.ts.map +1 -1
- package/dist/cli/commands/integration-recipient.js +9 -5
- package/dist/cli/commands/integration-recipient.js.map +1 -1
- package/dist/cli/commands/local-workflow.d.ts.map +1 -1
- package/dist/cli/commands/local-workflow.js +6 -8
- package/dist/cli/commands/local-workflow.js.map +1 -1
- package/dist/cli/commands/workspace.d.ts.map +1 -1
- package/dist/cli/commands/workspace.js +24 -1
- package/dist/cli/commands/workspace.js.map +1 -1
- package/dist/cli/lib/agent-takeover.d.ts +40 -0
- package/dist/cli/lib/agent-takeover.d.ts.map +1 -0
- package/dist/cli/lib/agent-takeover.js +91 -0
- package/dist/cli/lib/agent-takeover.js.map +1 -0
- package/dist/cli/lib/agent-token-store.d.ts +38 -0
- package/dist/cli/lib/agent-token-store.d.ts.map +1 -0
- package/dist/cli/lib/agent-token-store.js +91 -0
- package/dist/cli/lib/agent-token-store.js.map +1 -0
- package/dist/cli/lib/agent-tools-mcp.d.ts +167 -0
- package/dist/cli/lib/agent-tools-mcp.d.ts.map +1 -0
- package/dist/cli/lib/agent-tools-mcp.js +748 -0
- package/dist/cli/lib/agent-tools-mcp.js.map +1 -0
- package/dist/cli/lib/broker-lifecycle.d.ts +1 -2
- package/dist/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/cli/lib/broker-lifecycle.js +6 -52
- package/dist/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/cli/lib/init-prompts.d.ts +56 -0
- package/dist/cli/lib/init-prompts.d.ts.map +1 -0
- package/dist/cli/lib/init-prompts.js +54 -0
- package/dist/cli/lib/init-prompts.js.map +1 -0
- package/dist/cli/lib/locked-json-file.d.ts +16 -0
- package/dist/cli/lib/locked-json-file.d.ts.map +1 -0
- package/dist/cli/lib/locked-json-file.js +111 -0
- package/dist/cli/lib/locked-json-file.js.map +1 -0
- package/dist/cli/lib/mcp-session-name.d.ts +42 -0
- package/dist/cli/lib/mcp-session-name.d.ts.map +1 -0
- package/dist/cli/lib/mcp-session-name.js +91 -0
- package/dist/cli/lib/mcp-session-name.js.map +1 -0
- package/dist/cli/lib/node-definition-loader.d.ts +0 -7
- package/dist/cli/lib/node-definition-loader.d.ts.map +1 -1
- package/dist/cli/lib/node-definition-loader.js +0 -10
- package/dist/cli/lib/node-definition-loader.js.map +1 -1
- package/dist/cli/lib/node-provider-child.d.ts +3 -4
- package/dist/cli/lib/node-provider-child.d.ts.map +1 -1
- package/dist/cli/lib/node-provider-child.js +2 -2
- package/dist/cli/lib/node-provider-child.js.map +1 -1
- package/dist/cli/lib/relaycast-origin-binding.d.ts +103 -0
- package/dist/cli/lib/relaycast-origin-binding.d.ts.map +1 -0
- package/dist/cli/lib/relaycast-origin-binding.js +277 -0
- package/dist/cli/lib/relaycast-origin-binding.js.map +1 -0
- package/dist/cli/lib/sdk-client.d.ts +8 -0
- package/dist/cli/lib/sdk-client.d.ts.map +1 -1
- package/dist/cli/lib/sdk-client.js +83 -8
- package/dist/cli/lib/sdk-client.js.map +1 -1
- package/dist/cli/lib/sdk-command.d.ts.map +1 -1
- package/dist/cli/lib/sdk-command.js +7 -1
- package/dist/cli/lib/sdk-command.js.map +1 -1
- package/dist/cli/lib/spawn-lifecycle.d.ts.map +1 -1
- package/dist/cli/lib/spawn-lifecycle.js +3 -2
- package/dist/cli/lib/spawn-lifecycle.js.map +1 -1
- package/dist/cli/mcp/types.d.ts +14 -0
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/telemetry/events.d.ts +2 -2
- package/dist/cli/telemetry/events.d.ts.map +1 -1
- package/dist/index.cjs +4 -2
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -346,19 +346,22 @@ sandbox is selected. Ambient credentials and persisted Cloud routing do not
|
|
|
346
346
|
change the local default. Workforce reporting metadata requires remote placement.
|
|
347
347
|
Automatic placement and release need only the workspace key.
|
|
348
348
|
|
|
349
|
-
The sandbox path provisions a fresh hosted instance
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
349
|
+
The sandbox path provisions a fresh hosted instance. Inside a GitHub checkout,
|
|
350
|
+
Relay infers the Git root, repository identity, exact pushed `HEAD`, and caller-relative
|
|
351
|
+
directory. Cloud makes a depth-1 clone, fetches that exact SHA, verifies `HEAD`,
|
|
352
|
+
and starts the worker under `/srv/agent-workforce/<repo>`. Repository tasks start
|
|
353
|
+
without waiting for Relayfile context. `--checkout` explicitly selects the same
|
|
354
|
+
static mode. Outside Git, the default still requires the workspace Relayfile mount.
|
|
355
|
+
|
|
356
|
+
Use `--sandbox-relayfile` to require live context before dispatch. Without
|
|
357
|
+
`--checkout`, this retains the live repository mirror under
|
|
358
|
+
`/workspace/github/repos/<owner>/<repo>/contents`; with `--checkout`, it mounts
|
|
359
|
+
context separately from the Git clone. An explicit `--sandbox-relayfile-path`
|
|
360
|
+
also requires its selected context before startup. `--no-sandbox-relayfile`
|
|
361
|
+
explicitly disables mounting (combine with `--checkout` to clone repository files).
|
|
362
|
+
Use `--sandbox-provider daytona` or `--sandbox-provider e2b` to require an
|
|
363
|
+
operator-enabled provider; omit the flag to let Cloud's sandbox router choose.
|
|
364
|
+
If provisioning times out or the spawn
|
|
362
365
|
fails, Relay asks Cloud to delete the newly created sandbox. Runs without a
|
|
363
366
|
custom name use one `sbx_<UUID>` identity and the matching
|
|
364
367
|
`fleet-sandbox-<UUID>` node name. Legacy custom `--sandbox-name` values remain supported when no
|
|
@@ -380,7 +383,7 @@ reachable from a configured GitHub remote. This prevents a remote worker from
|
|
|
380
383
|
silently starting at a different revision. Commit and push local work before
|
|
381
384
|
retrying when Relay reports dirty files or an unreachable commit.
|
|
382
385
|
|
|
383
|
-
|
|
386
|
+
In default repository mode or with `--checkout`, sandbox provisioning clones the attested `HEAD` under
|
|
384
387
|
`/srv/agent-workforce/<repo>`. The checkout must have no tracked changes or
|
|
385
388
|
untracked source files, and the exact commit must be reachable from an origin
|
|
386
389
|
remote. Relay's generated `.agentworkforce/relay/workspace-key.json`,
|
|
@@ -403,7 +406,7 @@ release commands reuse the persisted project route; if that remote session is
|
|
|
403
406
|
unavailable, the command reports the routing failure instead of selecting a
|
|
404
407
|
same-named local worker.
|
|
405
408
|
|
|
406
|
-
From a clean repository already pinned to a Relay workspace, the
|
|
409
|
+
From a clean repository already pinned to a Relay workspace, the checkout-first
|
|
407
410
|
path is:
|
|
408
411
|
|
|
409
412
|
```bash
|
|
@@ -416,31 +419,44 @@ agent-relay fleet agent list
|
|
|
416
419
|
agent-relay fleet release cloud-zero-config
|
|
417
420
|
```
|
|
418
421
|
|
|
419
|
-
Invoking the
|
|
420
|
-
`/
|
|
421
|
-
|
|
422
|
-
is mounted from the same workspace, and no `.git` directory is written into the
|
|
423
|
-
Relayfile mirror.
|
|
422
|
+
Invoking the command from `packages/web` starts the worker at
|
|
423
|
+
`/srv/agent-workforce/<repo>/packages/web`, with `.git` available and `HEAD`
|
|
424
|
+
pinned to the requested revision.
|
|
424
425
|
|
|
425
|
-
|
|
426
|
+
Attach optional context later using `cloudWorkspaceId` and `sandboxId` from the
|
|
427
|
+
spawn response (this uses Cloud sign-in):
|
|
426
428
|
|
|
427
429
|
```bash
|
|
428
|
-
agent-relay fleet
|
|
429
|
-
--
|
|
430
|
-
--
|
|
431
|
-
--
|
|
432
|
-
--checkout
|
|
430
|
+
agent-relay fleet relayfile attach \
|
|
431
|
+
--cloud-workspace-id <Cloud-workspace-UUID> \
|
|
432
|
+
--sandbox-id <sbx_UUID> \
|
|
433
|
+
--path '/linear' '/.skills'
|
|
433
434
|
```
|
|
434
435
|
|
|
436
|
+
Omit `--path` for the full workspace. For a dev sandbox, add
|
|
437
|
+
`--cloud-api-url https://dev.agentrelay.com/cloud`. The command waits at most
|
|
438
|
+
five minutes by default, reports the separate mount path, and does not restart
|
|
439
|
+
the worker or retry a failed attachment. Pending detached preparation is polled
|
|
440
|
+
against the same sandbox within that shared deadline. Tell an already running worker the
|
|
441
|
+
returned path when its task needs that context. The mount uses its own directory;
|
|
442
|
+
it never overlays the Git checkout. Attachment failure leaves the repository task
|
|
443
|
+
running.
|
|
444
|
+
|
|
445
|
+
For a live repository mirror that must be ready before the task starts, add
|
|
446
|
+
`--sandbox-relayfile` to spawn. Its decoded tree contains no `.git`; its source
|
|
447
|
+
metadata sits beside `contents` under `.relayfile`. Use
|
|
448
|
+
`--checkout --sandbox-relayfile` for a Git checkout plus required live context.
|
|
449
|
+
|
|
435
450
|
In checkout mode, invoking spawn from `packages/web` places the worker in that
|
|
436
451
|
same relative directory in the remote clone. In both modes, private repositories
|
|
437
452
|
use the pinned workspace's connected GitHub access; a repository-access error
|
|
438
453
|
means that connection must be granted access to the repository. No GitHub token
|
|
439
454
|
or workspace key needs to be copied into the task, mount, or checkout.
|
|
440
455
|
|
|
441
|
-
With `--checkout`, the Git checkout and Relayfile mirror are
|
|
456
|
+
With `--checkout --sandbox-relayfile`, the Git checkout and Relayfile mirror are
|
|
457
|
+
separate trees. Checkout-only mode has no Relayfile mirror or workspace skills. In
|
|
442
458
|
the ordinary live mode, source files are decoded from Relayfile records without
|
|
443
|
-
placing `.git` in the mirror.
|
|
459
|
+
placing `.git` in the mirror. When a live mount is requested at startup, workspace `.skills` are exposed through the agent
|
|
444
460
|
CLIs' usual skill directories, and the worker's task context identifies the
|
|
445
461
|
mirror and exact source revision.
|
|
446
462
|
|
|
@@ -618,7 +634,7 @@ of polling `gh`. `--to self` cannot be combined with `--spawn`.
|
|
|
618
634
|
|
|
619
635
|
`local` remains as a deprecated hidden alias of `node` (it prints a one-time warning).
|
|
620
636
|
|
|
621
|
-
Node workflow runs use Relayflows for YAML
|
|
637
|
+
Node workflow runs use Relayflows for YAML and TypeScript workflow files.
|
|
622
638
|
|
|
623
639
|
Hosted equivalents live under `agent-relay cloud …`.
|
|
624
640
|
|
|
@@ -3,7 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import type { AgentRelayMcpServerOptions, AgentType, RegistrationSession, RelayCastLike, SessionSetter } from './mcp/types.js';
|
|
4
4
|
export type { AgentRelayMcpServerOptions } from './mcp/types.js';
|
|
5
5
|
export declare const AGENT_RELAY_MCP_VERSION: string;
|
|
6
|
-
export declare const AGENT_RELAY_MCP_INSTRUCTIONS = "You are an AI agent in a collaborative workspace powered by Agent Relay. You can communicate with other agents using these MCP tools:\n\n## Coordination rule\n- When the user asks you to work with, contact, coordinate with, or wait for named participants that already exist in this Agent Relay workspace, use Agent Relay tools such as \"list_agents\", \"send_dm\", \"post_message\", and \"check_inbox\".\n- Existing Relay participants are not local or built-in subagents. Do not replace them with your CLI's native subagent, team, task, or collaboration feature.\n- Do not claim to have contacted or waited for a Relay participant unless the corresponding Relay tool call succeeded.\n\n## Getting Started\n1. The current project workspace is resumed automatically when one was selected before\n2. Call \"create_workspace\" only when you explicitly want to start a new workspace session\n3. If someone shared an existing workspace key with you, call \"set_workspace_key\"\n4. When a workspace key is available at startup, this MCP server
|
|
6
|
+
export declare const AGENT_RELAY_MCP_INSTRUCTIONS = "You are an AI agent in a collaborative workspace powered by Agent Relay. You can communicate with other agents using these MCP tools:\n\n## Coordination rule\n- When the user asks you to work with, contact, coordinate with, or wait for named participants that already exist in this Agent Relay workspace, use Agent Relay tools such as \"list_agents\", \"send_dm\", \"post_message\", and \"check_inbox\".\n- Existing Relay participants are not local or built-in subagents. Do not replace them with your CLI's native subagent, team, task, or collaboration feature.\n- Do not claim to have contacted or waited for a Relay participant unless the corresponding Relay tool call succeeded.\n\n## Getting Started\n1. The current project workspace is resumed automatically when one was selected before\n2. Call \"create_workspace\" only when you explicitly want to start a new workspace session\n3. If someone shared an existing workspace key with you, call \"set_workspace_key\"\n4. When a workspace key is available at startup, this MCP server registers the session automatically: as RELAY_AGENT_NAME when it is set, otherwise as your tool plus the project folder (for example \"claude-myproject\", with a short suffix when another live session holds that name). Call \"register_agent\" to choose a different name, or to join when no workspace key was available\n5. Use \"list_channels\" to see available channels\n6. Use \"join_channel\" to join channels of interest\n7. Use \"check_inbox\" to see unread messages and mentions\n\n## Communication\n- Post messages to channels with \"post_message\"\n- Send direct messages with \"send_dm\"\n- Reply to threads with \"reply_to_thread\"\n- React to messages with \"add_reaction\"\n\n## Fleet\n- Use \"query_nodes\" to find fleet nodes by capability or name\n- Use \"spawn\" with either a CLI or an AgentWorkforce persona to invoke the fleet spawn action on an eligible node\n- Persona spawns require a node exposing \"spawn:persona\" through @agentworkforce/local-surface's defineWorkforcePersonaSpawnNode\n\n## Best Practices\n- Check your inbox regularly for new messages and mentions\n- Use channels for topic-based discussions\n- Use threads for detailed discussions to keep channels organized\n- React with emoji to acknowledge messages\n- Keep messages concise and actionable";
|
|
7
7
|
type RegisterAgentWithRebindArgs = {
|
|
8
8
|
session: RegistrationSession;
|
|
9
9
|
setSession: SessionSetter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-relay-mcp.d.ts","sourceRoot":"","sources":["../../src/cli/agent-relay-mcp.ts"],"names":[],"mappings":";AAgBA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-relay-mcp.d.ts","sourceRoot":"","sources":["../../src/cli/agent-relay-mcp.ts"],"names":[],"mappings":";AAgBA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA+DpE,OAAO,KAAK,EAEV,0BAA0B,EAC1B,SAAS,EAGT,mBAAmB,EACnB,aAAa,EACb,aAAa,EAEd,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAEjE,eAAO,MAAM,uBAAuB,QACuC,CAAC;AAoU5E,eAAO,MAAM,4BAA4B,uxEAgCF,CAAC;AAiBxC,KAAK,2BAA2B,GAAG;IACjC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,aAAa,CAAC;IAC1B,QAAQ,EAAE,MAAM,aAAa,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B,gFAAgF;IAChF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAcF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASrE;AAqCD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAOjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAMnF;AAsID,wBAAsB,uBAAuB,CAAC,EAC5C,OAAO,EACP,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,qBAAqB,GACtB,EAAE,2BAA2B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA+FhE;AA22BD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CA0OxF;AAqMD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CA0BrC;AAED,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyChG;AAED,wBAAgB,cAAc,IAAI,0BAA0B,CAkD3D"}
|
|
@@ -7,11 +7,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { isDeepStrictEqual } from 'node:util';
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
import { ListToolsRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { CallToolRequestSchema, InitializeRequestSchema, ListToolsRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
11
11
|
import { AgentRelay, RELAYCAST_SDK_VERSION, createAgentClient, createObserverToken, createRealtimeClient, createWorkspaceClient, isInvalidAgentTokenError, safeRelayErrorMessage, } from '@agent-relay/sdk';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { declaredWorkforceMetadata } from './lib/registration-metadata.js';
|
|
14
14
|
import { isBundledBunEntrypointPath } from './lib/agent-relay-mcp-command.js';
|
|
15
|
+
import { guardCredentialOrigin } from './lib/relaycast-origin-binding.js';
|
|
16
|
+
import { isAgentAlreadyExists, takeOverAgent } from './lib/agent-takeover.js';
|
|
17
|
+
import { pickAvailableName, sessionBaseName } from './lib/mcp-session-name.js';
|
|
15
18
|
import { DEFAULT_AGENT_REGISTRATION_TIMEOUT_MS, withAgentRegistrationDeadline, withDeadline, } from './lib/agent-registration.js';
|
|
16
19
|
import { attributableReleaseReason } from './lib/release-reason.js';
|
|
17
20
|
import { sanitizedSpawnReceipt, spawnPlacementReceipt, } from './lib/spawn-lifecycle.js';
|
|
@@ -283,7 +286,7 @@ export const AGENT_RELAY_MCP_INSTRUCTIONS = `You are an AI agent in a collaborat
|
|
|
283
286
|
1. The current project workspace is resumed automatically when one was selected before
|
|
284
287
|
2. Call "create_workspace" only when you explicitly want to start a new workspace session
|
|
285
288
|
3. If someone shared an existing workspace key with you, call "set_workspace_key"
|
|
286
|
-
4. When a workspace key is available at startup, this MCP server
|
|
289
|
+
4. When a workspace key is available at startup, this MCP server registers the session automatically: as RELAY_AGENT_NAME when it is set, otherwise as your tool plus the project folder (for example "claude-myproject", with a short suffix when another live session holds that name). Call "register_agent" to choose a different name, or to join when no workspace key was available
|
|
287
290
|
5. Use "list_channels" to see available channels
|
|
288
291
|
6. Use "join_channel" to join channels of interest
|
|
289
292
|
7. Use "check_inbox" to see unread messages and mentions
|
|
@@ -1425,8 +1428,185 @@ export function createAgentRelayMcpServer(options) {
|
|
|
1425
1428
|
return result;
|
|
1426
1429
|
});
|
|
1427
1430
|
}
|
|
1431
|
+
if (options.autoRegister) {
|
|
1432
|
+
installSessionAutoRegistration(mcpServer, handlers, options.autoRegister, {
|
|
1433
|
+
getSession,
|
|
1434
|
+
setSession,
|
|
1435
|
+
getRelay,
|
|
1436
|
+
agentType: options.agentType,
|
|
1437
|
+
baseUrl: options.baseUrl,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1428
1440
|
return mcpServer;
|
|
1429
1441
|
}
|
|
1442
|
+
const RELAYCAST_HOSTED_ORIGIN = 'https://cast.agentrelay.com';
|
|
1443
|
+
/** Under Relaycast's 60s presence TTL, with room for one missed beat. */
|
|
1444
|
+
const SESSION_HEARTBEAT_MS = 25_000;
|
|
1445
|
+
/** How an auto-registered session marks itself offline when it ends. */
|
|
1446
|
+
const sessionDisconnectors = new WeakMap();
|
|
1447
|
+
/**
|
|
1448
|
+
* Names of the sessions connected right now. The presence endpoint tracks
|
|
1449
|
+
* live connections (a session that exits drops out within its heartbeat TTL,
|
|
1450
|
+
* at once when it disconnects cleanly), where the roster's `online` filter
|
|
1451
|
+
* keeps a registration for minutes after its process is gone.
|
|
1452
|
+
*/
|
|
1453
|
+
async function liveAgentNames(relay, workspaceKey, baseUrl, fetchImpl) {
|
|
1454
|
+
const live = new Set();
|
|
1455
|
+
try {
|
|
1456
|
+
const response = await fetchImpl(new URL('/v1/agents/presence', baseUrl ?? RELAYCAST_HOSTED_ORIGIN), {
|
|
1457
|
+
headers: { authorization: `Bearer ${workspaceKey}` },
|
|
1458
|
+
signal: AbortSignal.timeout(5_000),
|
|
1459
|
+
});
|
|
1460
|
+
if (response.ok) {
|
|
1461
|
+
const body = (await response.json());
|
|
1462
|
+
if (Array.isArray(body.data)) {
|
|
1463
|
+
for (const entry of body.data) {
|
|
1464
|
+
if (entry?.status === 'online' && typeof entry.agent_name === 'string')
|
|
1465
|
+
live.add(entry.agent_name);
|
|
1466
|
+
}
|
|
1467
|
+
return live;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
catch {
|
|
1472
|
+
// Fall back to the roster below.
|
|
1473
|
+
}
|
|
1474
|
+
try {
|
|
1475
|
+
for (const agent of await relay.agents.list({ status: 'online' })) {
|
|
1476
|
+
const name = agent?.name;
|
|
1477
|
+
if (typeof name === 'string')
|
|
1478
|
+
live.add(name);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
catch {
|
|
1482
|
+
// Without either, the base name is still the best guess.
|
|
1483
|
+
}
|
|
1484
|
+
return live;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Register a session that has no configured name as soon as the MCP client
|
|
1488
|
+
* says who it is. The name comes from `clientInfo.name` and the folder the
|
|
1489
|
+
* tool started in (`claude-myproject`); a live session already holding it
|
|
1490
|
+
* pushes this one to `claude-myproject-2`. Tool calls wait for the
|
|
1491
|
+
* registration, so the first call never races it.
|
|
1492
|
+
*/
|
|
1493
|
+
function installSessionAutoRegistration(mcpServer, handlers, autoRegister, ctx) {
|
|
1494
|
+
const fetchImpl = autoRegister.fetch ?? globalThis.fetch;
|
|
1495
|
+
let pending = null;
|
|
1496
|
+
const register = async (clientName) => {
|
|
1497
|
+
const base = sessionBaseName({ clientName, hint: autoRegister.clientHint, cwd: autoRegister.cwd });
|
|
1498
|
+
const workspaceKey = ctx.getSession().workspaceKey;
|
|
1499
|
+
const live = await liveAgentNames(ctx.getRelay(), workspaceKey, ctx.baseUrl, fetchImpl);
|
|
1500
|
+
const name = pickAvailableName(base, live);
|
|
1501
|
+
let token;
|
|
1502
|
+
let registeredName;
|
|
1503
|
+
try {
|
|
1504
|
+
const payload = await registerAgentWithRebind({
|
|
1505
|
+
session: ctx.getSession(),
|
|
1506
|
+
setSession: ctx.setSession,
|
|
1507
|
+
getRelay: ctx.getRelay,
|
|
1508
|
+
name,
|
|
1509
|
+
type: ctx.agentType ?? 'agent',
|
|
1510
|
+
});
|
|
1511
|
+
token = typeof payload.token === 'string' ? payload.token : null;
|
|
1512
|
+
registeredName = typeof payload.registered_name === 'string' ? payload.registered_name : null;
|
|
1513
|
+
}
|
|
1514
|
+
catch (error) {
|
|
1515
|
+
// The name belongs to an earlier session of this tool in this folder
|
|
1516
|
+
// that is no longer connected (a restart): resume that identity.
|
|
1517
|
+
if (!isAgentAlreadyExists(error))
|
|
1518
|
+
throw error;
|
|
1519
|
+
const grant = await takeOverAgent({ workspaceKey, baseUrl: ctx.baseUrl, fetch: fetchImpl }, name, 'agent-relay mcp: resume the identity of an earlier session of this tool in this folder');
|
|
1520
|
+
token = grant.token;
|
|
1521
|
+
registeredName = grant.name;
|
|
1522
|
+
}
|
|
1523
|
+
if (token && registeredName) {
|
|
1524
|
+
const agents = new Map(ctx.getSession().agents);
|
|
1525
|
+
agents.set(registeredName, createRegisteredAgent(registeredName, token));
|
|
1526
|
+
ctx.setSession({ agentToken: token, agentName: registeredName, agents });
|
|
1527
|
+
startHeartbeat();
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
// Keep this session present while it runs, so the next session in the same
|
|
1531
|
+
// folder sees the name is taken. Presence lapses about a minute after the
|
|
1532
|
+
// last beat, which is how a crashed session frees its name.
|
|
1533
|
+
let heartbeat;
|
|
1534
|
+
const beat = () => {
|
|
1535
|
+
const token = ctx.getSession().agentToken;
|
|
1536
|
+
if (!token)
|
|
1537
|
+
return;
|
|
1538
|
+
void fetchImpl(new URL('/v1/agents/heartbeat', ctx.baseUrl ?? RELAYCAST_HOSTED_ORIGIN), {
|
|
1539
|
+
method: 'POST',
|
|
1540
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1541
|
+
signal: AbortSignal.timeout(5_000),
|
|
1542
|
+
}).catch(() => undefined);
|
|
1543
|
+
};
|
|
1544
|
+
const startHeartbeat = () => {
|
|
1545
|
+
if (heartbeat)
|
|
1546
|
+
return;
|
|
1547
|
+
beat();
|
|
1548
|
+
heartbeat = setInterval(beat, SESSION_HEARTBEAT_MS);
|
|
1549
|
+
heartbeat.unref?.();
|
|
1550
|
+
};
|
|
1551
|
+
// Read clientInfo from the initialize request itself, and start right after
|
|
1552
|
+
// answering it, so nothing depends on the order a client's messages land in.
|
|
1553
|
+
const initialize = handlers.get('initialize');
|
|
1554
|
+
if (initialize) {
|
|
1555
|
+
mcpServer.server.setRequestHandler(InitializeRequestSchema, async (req, extra) => {
|
|
1556
|
+
const result = await initialize(req, extra);
|
|
1557
|
+
const session = ctx.getSession();
|
|
1558
|
+
if (!pending && session.workspaceKey && !session.agentToken) {
|
|
1559
|
+
pending = register(req.params.clientInfo?.name).catch((error) => {
|
|
1560
|
+
process.stderr.write(`[agent-relay] Could not register this session automatically (${safeRelayErrorMessage(error)}). ` +
|
|
1561
|
+
'Call "register_agent" to join the workspace.\n');
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
return result;
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
const callTool = handlers.get('tools/call');
|
|
1568
|
+
if (callTool) {
|
|
1569
|
+
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
1570
|
+
if (pending)
|
|
1571
|
+
await pending;
|
|
1572
|
+
return (await callTool(req, extra));
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
// Leave cleanly so the next session in this folder can take the name at once.
|
|
1576
|
+
sessionDisconnectors.set(mcpServer, async () => {
|
|
1577
|
+
if (heartbeat)
|
|
1578
|
+
clearInterval(heartbeat);
|
|
1579
|
+
const token = ctx.getSession().agentToken;
|
|
1580
|
+
if (!token)
|
|
1581
|
+
return;
|
|
1582
|
+
ctx.getSession().wsBridge?.stop();
|
|
1583
|
+
await fetchImpl(new URL('/v1/agents/disconnect', ctx.baseUrl ?? RELAYCAST_HOSTED_ORIGIN), {
|
|
1584
|
+
method: 'POST',
|
|
1585
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
1586
|
+
body: '{}',
|
|
1587
|
+
signal: AbortSignal.timeout(2_000),
|
|
1588
|
+
}).catch(() => undefined);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Mark an auto-registered session offline when its tool stops it: stdin
|
|
1593
|
+
* closes (the MCP stdio shutdown) or the process is signalled.
|
|
1594
|
+
*/
|
|
1595
|
+
function disconnectSessionOnExit(mcpServer) {
|
|
1596
|
+
const disconnect = sessionDisconnectors.get(mcpServer);
|
|
1597
|
+
if (!disconnect)
|
|
1598
|
+
return;
|
|
1599
|
+
let closing = false;
|
|
1600
|
+
const shutdown = () => {
|
|
1601
|
+
if (closing)
|
|
1602
|
+
return;
|
|
1603
|
+
closing = true;
|
|
1604
|
+
void Promise.race([disconnect(), new Promise((resolve) => setTimeout(resolve, 2_500))]).finally(() => process.exit(0));
|
|
1605
|
+
};
|
|
1606
|
+
process.stdin.once('end', shutdown);
|
|
1607
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
|
|
1608
|
+
process.once(signal, shutdown);
|
|
1609
|
+
}
|
|
1430
1610
|
/** Relaycast agent tokens are opaque `at_live_<hex>` literals. Anything else
|
|
1431
1611
|
* (for example a RelayAuth JWT carried in RELAY_AGENT_TOKEN by `relay on start`)
|
|
1432
1612
|
* is not a valid Relaycast credential and must be replaced. */
|
|
@@ -1493,6 +1673,7 @@ export async function startAgentRelayMcpStdio(options) {
|
|
|
1493
1673
|
});
|
|
1494
1674
|
const transport = new StdioServerTransport();
|
|
1495
1675
|
await mcpServer.connect(transport);
|
|
1676
|
+
disconnectSessionOnExit(mcpServer);
|
|
1496
1677
|
}
|
|
1497
1678
|
export function optionsFromEnv() {
|
|
1498
1679
|
let workspaceKey = resolveEnv('RELAY_WORKSPACE_KEY') ??
|
|
@@ -1514,21 +1695,33 @@ export function optionsFromEnv() {
|
|
|
1514
1695
|
// session can still be selected explicitly with set_workspace_key.
|
|
1515
1696
|
}
|
|
1516
1697
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1698
|
+
// With no configured name the session is named after the MCP client and the
|
|
1699
|
+
// project folder once the client connects (see installSessionAutoRegistration),
|
|
1700
|
+
// so two sessions never share one identity.
|
|
1701
|
+
const agentName = resolveEnv('RELAY_AGENT_NAME') ?? resolveEnv('RELAY_CLAW_NAME');
|
|
1702
|
+
// An agent token has no workspace identity encoded locally. Only reuse it
|
|
1703
|
+
// when the workspace was selected alongside it through the environment;
|
|
1704
|
+
// a persisted project/store fallback must register into that workspace
|
|
1705
|
+
// instead of silently pairing it with a possibly stale ambient token.
|
|
1706
|
+
const agentToken = resumedPersistedWorkspace ? undefined : resolveEnv('RELAY_AGENT_TOKEN');
|
|
1707
|
+
// The same rule as the CLI: a credential bound to a self-hosted or local
|
|
1708
|
+
// server (by `relay init`, `workspace join`, …) only ever goes back to it,
|
|
1709
|
+
// so the MCP server never sends it to the hosted default.
|
|
1710
|
+
const credential = workspaceKey ?? agentToken;
|
|
1711
|
+
const baseUrl = credential
|
|
1712
|
+
? guardCredentialOrigin(credential, resolveEnv('RELAY_BASE_URL'))
|
|
1713
|
+
: resolveEnv('RELAY_BASE_URL');
|
|
1520
1714
|
return {
|
|
1521
1715
|
workspaceKey,
|
|
1522
|
-
baseUrl
|
|
1523
|
-
|
|
1524
|
-
// when the workspace was selected alongside it through the environment;
|
|
1525
|
-
// a persisted project/store fallback must register into that workspace
|
|
1526
|
-
// instead of silently pairing it with a possibly stale ambient token.
|
|
1527
|
-
agentToken: resumedPersistedWorkspace ? undefined : resolveEnv('RELAY_AGENT_TOKEN'),
|
|
1716
|
+
baseUrl,
|
|
1717
|
+
agentToken,
|
|
1528
1718
|
agentName,
|
|
1529
1719
|
agentType: normalizeAgentType(resolveEnv('RELAY_AGENT_TYPE')),
|
|
1530
1720
|
strictAgentName: envFlagEnabled(resolveEnv('RELAY_STRICT_AGENT_NAME')),
|
|
1531
1721
|
skipBootstrap: envFlagEnabled(resolveEnv('RELAY_SKIP_BOOTSTRAP')),
|
|
1722
|
+
autoRegister: workspaceKey && !agentName && !agentToken
|
|
1723
|
+
? { cwd: process.cwd(), clientHint: resolveEnv('AGENT_RELAY_CLIENT') }
|
|
1724
|
+
: undefined,
|
|
1532
1725
|
};
|
|
1533
1726
|
}
|
|
1534
1727
|
if (isEntrypoint()) {
|