@scp-dev/mcp 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _scp_dev_core from '@scp-dev/core';
2
- import { Claim } from '@scp-dev/core';
2
+ import { Claim, Wake } from '@scp-dev/core';
3
3
  import { RelayClient } from '@scp-dev/broker';
4
4
 
5
5
  declare const MCP_PROTOCOLS: readonly ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
@@ -25,6 +25,8 @@ interface RelayMcpOptions {
25
25
  channels?: boolean;
26
26
  /** the client was started with channels loaded (Claude Code does not declare the capability itself) */
27
27
  launchedWithChannels?: boolean;
28
+ /** the wake the endpoint advertises while this shim pushes by channel, over `pi` for a pi client and `channels` for any other */
29
+ wake?: Wake;
28
30
  version?: string;
29
31
  /** starts a worker for relay_spawn, resolving with its name and its runner's pid once its harness started; without it there is no relay_spawn */
30
32
  spawn?: (request: SpawnRequest) => Promise<{
@@ -289,7 +291,7 @@ declare const TOOLS: readonly [{
289
291
  };
290
292
  }, {
291
293
  readonly name: "relay_join";
292
- readonly description: "Put this session on the relay, so peers can message it and new messages reach you after your tool calls; name sets your handle once (renaming you in place when you already joined).";
294
+ readonly description: "Put this session on the relay, so peers can message it and new messages reach you after your tool calls; name sets your handle once (renaming you in place when you already joined); under links you below that agent, as if it had spawned you: you show as its child and reach only what a worker of it reaches.";
293
295
  readonly inputSchema: {
294
296
  readonly type: "object";
295
297
  readonly properties: {
@@ -297,6 +299,10 @@ declare const TOOLS: readonly [{
297
299
  readonly type: "string";
298
300
  readonly maxLength: 63;
299
301
  };
302
+ readonly under: {
303
+ readonly type: "string";
304
+ readonly maxLength: 128;
305
+ };
300
306
  };
301
307
  readonly additionalProperties: false;
302
308
  };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  deliveryText,
8
8
  LIMITS,
9
9
  PREAMBLE,
10
+ presenceText,
10
11
  reachLines,
11
12
  receiptLines,
12
13
  RelayError,
@@ -160,8 +161,12 @@ var TOOLS = [
160
161
  },
161
162
  {
162
163
  name: "relay_join",
163
- description: "Put this session on the relay, so peers can message it and new messages reach you after your tool calls; name sets your handle once (renaming you in place when you already joined).",
164
- inputSchema: { type: "object", properties: { name: { type: "string", maxLength: 63 } }, additionalProperties: false }
164
+ description: "Put this session on the relay, so peers can message it and new messages reach you after your tool calls; name sets your handle once (renaming you in place when you already joined); under links you below that agent, as if it had spawned you: you show as its child and reach only what a worker of it reaches.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: { name: { type: "string", maxLength: 63 }, under: { type: "string", maxLength: 128 } },
168
+ additionalProperties: false
169
+ }
165
170
  },
166
171
  {
167
172
  name: "relay_leave",
@@ -274,7 +279,11 @@ function heldText(held) {
274
279
  return `held for review by your human: hold ${held.hold_id}; nothing was delivered`;
275
280
  }
276
281
  function sentContent(result) {
277
- return { id: result.id, thread: result.thread, recipients: result.deliveries.map((d) => ({ address: d.address, ...d.state ? { state: d.state } : {} })) };
282
+ return {
283
+ id: result.id,
284
+ thread: result.thread,
285
+ recipients: result.deliveries.map((d) => ({ address: d.address, ...d.state ? { state: d.state } : {}, ...d.wake ? { wake: d.wake } : {} }))
286
+ };
278
287
  }
279
288
  function redactedText(redacted) {
280
289
  return redacted ? `; redacted ${redacted.n} span${redacted.n === 1 ? "" : "s"} (${redacted.kinds.join(", ")}) before delivery` : "";
@@ -400,6 +409,8 @@ async function runRelayMcp(options) {
400
409
  let stopped = false;
401
410
  const channelPushed = /* @__PURE__ */ new Set();
402
411
  let channelLoop;
412
+ let wake;
413
+ let advertised;
403
414
  const write = (message) => {
404
415
  if (!stopped) options.output(`${JSON.stringify(message)}
405
416
  `);
@@ -442,8 +453,17 @@ async function runRelayMcp(options) {
442
453
  throw new Error("the relay MCP shim is stopping");
443
454
  }
444
455
  client = next;
456
+ advertise();
445
457
  return next;
446
458
  }
459
+ function advertise() {
460
+ if (!client || wake === void 0) return;
461
+ const via = wake ?? "none";
462
+ if (advertised?.client === client && advertised.via === via || via === "none" && !advertised) return;
463
+ advertised = { client, via };
464
+ client.request("wake", { via }).catch(() => {
465
+ });
466
+ }
447
467
  function ensure() {
448
468
  if (client) return Promise.resolve(client);
449
469
  connecting ??= open().finally(() => connecting = void 0);
@@ -469,7 +489,7 @@ async function runRelayMcp(options) {
469
489
  return [
470
490
  `${e.address}${me && e.id === me.id ? " (you)" : ""}`,
471
491
  e.harness ?? e.kind,
472
- e.presence,
492
+ presenceText(e),
473
493
  e.title ? `title ${quote(e.title)}` : "",
474
494
  status,
475
495
  cwd ? quote(cwd) : "",
@@ -634,17 +654,20 @@ async function runRelayMcp(options) {
634
654
  }
635
655
  case "relay_join": {
636
656
  const name2 = str(args, "name");
657
+ const under = str(args, "under");
637
658
  const joined = options.membership?.() ?? {};
638
659
  const claim = "handle" in joined && joined.handle ? { ...options.claim, handle: joined.handle } : options.claim;
639
660
  const next = await options.connect();
640
661
  try {
641
- const hello = await next.hello({ transient: true, claim, join: name2 ? { handle: name2 } : {} });
662
+ const hello = await next.hello({ transient: true, claim, join: { ...name2 ? { handle: name2 } : {}, ...under ? { under } : {} } });
642
663
  me = hello.endpoint;
643
664
  } finally {
644
665
  next.close();
645
666
  }
646
667
  await ensure();
647
- return text(`joined the relay as ${me?.address}: peers can message you, and new messages reach you after your tool calls`);
668
+ return text(
669
+ `joined the relay as ${me?.address}${me?.parent ? ` under ${me.parent}` : ""}: peers can message you, and new messages reach you after your tool calls`
670
+ );
648
671
  }
649
672
  case "relay_leave": {
650
673
  const view = await call("leave");
@@ -808,6 +831,9 @@ ${wrapMessage(message)}`,
808
831
  const capabilities = params.capabilities;
809
832
  const channel = capabilities?.experimental?.["claude/channel"];
810
833
  channelClient = typeof channel === "object" && channel !== null || options.launchedWithChannels === true;
834
+ const clientInfo = params.clientInfo;
835
+ wake = options.channels && channelClient ? options.wake ?? (clientInfo?.name === "pi" ? "pi" : "channels") : null;
836
+ advertise();
811
837
  return respond({
812
838
  protocolVersion,
813
839
  capabilities: { tools: {}, ...options.channels ? { experimental: { "claude/channel": {} } } : {} },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scp-dev/mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "The SCP stdio MCP server: gives an agent session the relay's tools.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scp.dev",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc -p ."
30
30
  },
31
31
  "dependencies": {
32
- "@scp-dev/broker": "0.1.3",
33
- "@scp-dev/core": "0.1.2"
32
+ "@scp-dev/broker": "0.1.5",
33
+ "@scp-dev/core": "0.1.4"
34
34
  },
35
35
  "devDependencies": {
36
36
  "tsup": "^8.5.1"
package/src/server.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  type SendResult,
19
19
  type PeerView,
20
20
  PREAMBLE,
21
+ presenceText,
21
22
  reachLines,
22
23
  receiptLines,
23
24
  RelayError,
@@ -26,6 +27,7 @@ import {
26
27
  shortId,
27
28
  STATUS_STATES,
28
29
  type ThreadMessage,
30
+ type Wake,
29
31
  type WhyResult,
30
32
  widenText,
31
33
  wrapMessage,
@@ -65,6 +67,8 @@ export interface RelayMcpOptions {
65
67
  channels?: boolean
66
68
  /** the client was started with channels loaded (Claude Code does not declare the capability itself) */
67
69
  launchedWithChannels?: boolean
70
+ /** the wake the endpoint advertises while this shim pushes by channel, over `pi` for a pi client and `channels` for any other */
71
+ wake?: Wake
68
72
  version?: string
69
73
  /** starts a worker for relay_spawn, resolving with its name and its runner's pid once its harness started; without it there is no relay_spawn */
70
74
  spawn?: (request: SpawnRequest) => Promise<{ name: string; pid: number }>
@@ -232,8 +236,12 @@ export const TOOLS = [
232
236
  {
233
237
  name: "relay_join",
234
238
  description:
235
- "Put this session on the relay, so peers can message it and new messages reach you after your tool calls; name sets your handle once (renaming you in place when you already joined).",
236
- inputSchema: { type: "object", properties: { name: { type: "string", maxLength: 63 } }, additionalProperties: false },
239
+ "Put this session on the relay, so peers can message it and new messages reach you after your tool calls; name sets your handle once (renaming you in place when you already joined); under links you below that agent, as if it had spawned you: you show as its child and reach only what a worker of it reaches.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: { name: { type: "string", maxLength: 63 }, under: { type: "string", maxLength: 128 } },
243
+ additionalProperties: false,
244
+ },
237
245
  },
238
246
  {
239
247
  name: "relay_leave",
@@ -362,7 +370,11 @@ function heldText(held: JudgeHeld): string {
362
370
 
363
371
  /** a send's id, thread and recipients with their states, for a result's structuredContent */
364
372
  function sentContent(result: SendResult): Record<string, unknown> {
365
- return { id: result.id, thread: result.thread, recipients: result.deliveries.map((d) => ({ address: d.address, ...(d.state ? { state: d.state } : {}) })) }
373
+ return {
374
+ id: result.id,
375
+ thread: result.thread,
376
+ recipients: result.deliveries.map((d) => ({ address: d.address, ...(d.state ? { state: d.state } : {}), ...(d.wake ? { wake: d.wake } : {}) })),
377
+ }
366
378
  }
367
379
 
368
380
  function redactedText(redacted: JudgeRedacted | undefined): string {
@@ -526,6 +538,9 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
526
538
  /** seqs this shim pushed, so a push whose confirm was lost is not pushed again */
527
539
  const channelPushed = new Set<number>()
528
540
  let channelLoop: Promise<void> | undefined
541
+ /** what the endpoint advertises once initialize said whether the client takes channel pushes; null advertises nothing */
542
+ let wake: Wake | null | undefined
543
+ let advertised: { client: RelayClient; via: Wake } | undefined
529
544
 
530
545
  const write = (message: Json) => {
531
546
  if (!stopped) options.output(`${JSON.stringify(message)}\n`)
@@ -572,9 +587,19 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
572
587
  throw new Error("the relay MCP shim is stopping")
573
588
  }
574
589
  client = next
590
+ advertise()
575
591
  return next
576
592
  }
577
593
 
594
+ /** tells the broker how a message wakes this session, once per connection and value, when both it and the client's capabilities are known */
595
+ function advertise() {
596
+ if (!client || wake === undefined) return
597
+ const via = wake ?? "none"
598
+ if ((advertised?.client === client && advertised.via === via) || (via === "none" && !advertised)) return
599
+ advertised = { client, via }
600
+ client.request("wake", { via }).catch(() => {})
601
+ }
602
+
578
603
  function ensure(): Promise<RelayClient> {
579
604
  if (client) return Promise.resolve(client)
580
605
  connecting ??= open().finally(() => (connecting = undefined))
@@ -604,7 +629,7 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
604
629
  return [
605
630
  `${e.address}${me && e.id === me.id ? " (you)" : ""}`,
606
631
  e.harness ?? e.kind,
607
- e.presence,
632
+ presenceText(e),
608
633
  e.title ? `title ${quote(e.title)}` : "",
609
634
  status,
610
635
  cwd ? quote(cwd) : "",
@@ -777,17 +802,20 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
777
802
  }
778
803
  case "relay_join": {
779
804
  const name = str(args, "name")
805
+ const under = str(args, "under")
780
806
  const joined = options.membership?.() ?? {}
781
807
  const claim = "handle" in joined && joined.handle ? { ...options.claim, handle: joined.handle } : options.claim
782
808
  const next = await options.connect()
783
809
  try {
784
- const hello = await next.hello({ transient: true, claim, join: name ? { handle: name } : {} })
810
+ const hello = await next.hello({ transient: true, claim, join: { ...(name ? { handle: name } : {}), ...(under ? { under } : {}) } })
785
811
  me = hello.endpoint
786
812
  } finally {
787
813
  next.close()
788
814
  }
789
815
  await ensure()
790
- return text(`joined the relay as ${me?.address}: peers can message you, and new messages reach you after your tool calls`)
816
+ return text(
817
+ `joined the relay as ${me?.address}${me?.parent ? ` under ${me.parent}` : ""}: peers can message you, and new messages reach you after your tool calls`,
818
+ )
791
819
  }
792
820
  case "relay_leave": {
793
821
  const view = await call<EndpointView>("leave")
@@ -953,6 +981,9 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
953
981
  const capabilities = params.capabilities as { experimental?: Record<string, unknown> } | undefined
954
982
  const channel = capabilities?.experimental?.["claude/channel"]
955
983
  channelClient = (typeof channel === "object" && channel !== null) || options.launchedWithChannels === true
984
+ const clientInfo = params.clientInfo as { name?: unknown } | undefined
985
+ wake = options.channels && channelClient ? (options.wake ?? (clientInfo?.name === "pi" ? "pi" : "channels")) : null
986
+ advertise()
956
987
  return respond({
957
988
  protocolVersion,
958
989
  capabilities: { tools: {}, ...(options.channels ? { experimental: { "claude/channel": {} } } : {}) },