@scp-dev/mcp 0.1.2 → 0.1.4

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
@@ -13,6 +13,12 @@ interface RelayMcpOptions {
13
13
  input: AsyncIterable<Uint8Array | string> | ReadableStream<Uint8Array>;
14
14
  /** one JSON-RPC line, newline included */
15
15
  output: (line: string) => void;
16
+ /** whether the session is on the relay now: `dark` (why not) keeps the shim from claiming, `handle` is the name it joined under */
17
+ membership?: () => {
18
+ dark: string;
19
+ } | {
20
+ handle?: string;
21
+ };
16
22
  /** HOME shortens cwds in relay_list */
17
23
  env?: Record<string, string | undefined>;
18
24
  /** push deliveries as `notifications/claude/channel` to a client that declares `claude/channel` or was launched with channels */
@@ -98,6 +104,10 @@ declare const TOOLS: readonly [{
98
104
  readonly type: "boolean";
99
105
  readonly description: "only for recipients allowed private data";
100
106
  };
107
+ readonly live: {
108
+ readonly type: "boolean";
109
+ readonly description: "refuse (not_live) instead of queueing when a recipient has no live session";
110
+ };
101
111
  };
102
112
  readonly required: readonly ["to", "body"];
103
113
  readonly additionalProperties: false;
@@ -277,6 +287,31 @@ declare const TOOLS: readonly [{
277
287
  readonly required: readonly ["now"];
278
288
  readonly additionalProperties: false;
279
289
  };
290
+ }, {
291
+ 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); 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
+ readonly inputSchema: {
294
+ readonly type: "object";
295
+ readonly properties: {
296
+ readonly name: {
297
+ readonly type: "string";
298
+ readonly maxLength: 63;
299
+ };
300
+ readonly under: {
301
+ readonly type: "string";
302
+ readonly maxLength: 128;
303
+ };
304
+ };
305
+ readonly additionalProperties: false;
306
+ };
307
+ }, {
308
+ readonly name: "relay_leave";
309
+ readonly description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.";
310
+ readonly inputSchema: {
311
+ readonly type: "object";
312
+ readonly properties: {};
313
+ readonly additionalProperties: false;
314
+ };
280
315
  }, {
281
316
  readonly name: "relay_set_title";
282
317
  readonly description: "Set your title, a short line on what you work on that every agent and your user see in relay_list and swarm list; an empty title clears it.";
package/dist/index.js CHANGED
@@ -70,7 +70,8 @@ var TOOLS = [
70
70
  to: { oneOf: [{ type: "string" }, { type: "array", items: { type: "string" }, minItems: 1, maxItems: LIMITS.recipients }] },
71
71
  body: { type: "string" },
72
72
  thread: { type: "string", description: "continue this thread: its id, or the id or short id of a message in it" },
73
- private: { type: "boolean", description: "only for recipients allowed private data" }
73
+ private: { type: "boolean", description: "only for recipients allowed private data" },
74
+ live: { type: "boolean", description: "refuse (not_live) instead of queueing when a recipient has no live session" }
74
75
  },
75
76
  required: ["to", "body"],
76
77
  additionalProperties: false
@@ -157,6 +158,20 @@ var TOOLS = [
157
158
  description: "Set your now, a one-line note on what you are doing right now, shown with your title in relay_list and relay_peers; your status state stays (working when unset).",
158
159
  inputSchema: { type: "object", properties: { now: { type: "string", maxLength: LIMITS.noteChars } }, required: ["now"], additionalProperties: false }
159
160
  },
161
+ {
162
+ 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); 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.",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: { name: { type: "string", maxLength: 63 }, under: { type: "string", maxLength: 128 } },
167
+ additionalProperties: false
168
+ }
169
+ },
170
+ {
171
+ name: "relay_leave",
172
+ description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.",
173
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
174
+ },
160
175
  {
161
176
  name: "relay_set_title",
162
177
  description: `Set your title, a short line on what you work on that every agent and your user see in relay_list and ${CLI_NAME} list; an empty title clears it.`,
@@ -398,7 +413,13 @@ async function runRelayMcp(options) {
398
413
  sent.set(message.id, { from: message.from.address, thread: message.thread });
399
414
  while (sent.size > DELIVERED_KEEP) sent.delete(sent.keys().next().value);
400
415
  };
416
+ function sessionClaim() {
417
+ const joined = options.membership?.() ?? {};
418
+ if ("dark" in joined) throw new RelayError("unavailable", joined.dark);
419
+ return joined.handle ? { ...options.claim, handle: joined.handle } : options.claim;
420
+ }
401
421
  async function open() {
422
+ const claim = sessionClaim();
402
423
  const next = await options.connect();
403
424
  try {
404
425
  let hello;
@@ -409,7 +430,7 @@ async function runRelayMcp(options) {
409
430
  if (!(error instanceof RelayError)) throw error;
410
431
  }
411
432
  }
412
- hello ??= await next.hello({ claim: options.claim, ...options.claim.cwd === void 0 ? {} : { cwd: options.claim.cwd } });
433
+ hello ??= await next.hello({ claim, ...claim.cwd === void 0 ? {} : { cwd: claim.cwd } });
413
434
  if (hello.token) token = hello.token;
414
435
  me = hello.endpoint;
415
436
  if (!announced) {
@@ -516,10 +537,11 @@ async function runRelayMcp(options) {
516
537
  body: str(args, "body", true),
517
538
  ...args.thread !== void 0 ? { thread: str(args, "thread") } : {},
518
539
  ...bool(args, "private") ? { data_class: "private" } : {},
540
+ ...bool(args, "live") ? { live: true } : {},
519
541
  client_id: clientId()
520
542
  });
521
543
  if (result.held) return text(heldText(result.held));
522
- const lines = reachLines(result.deliveries);
544
+ const lines = [...reachLines(result.deliveries), ...result.warnings ?? []];
523
545
  const to = result.deliveries.map((d) => lines.length ? d.address : deliveryText(d)).join(", ");
524
546
  const receipt = result.deliveries.some((d) => d.state === "running" || d.state === "starting") ? [`relay_status with id ${shortId(result.id)} shows when it is read`] : [];
525
547
  return {
@@ -614,6 +636,30 @@ async function runRelayMcp(options) {
614
636
  const view = await call("status", { note: args.now });
615
637
  return text(`${view.address}: ${view.status?.state}${view.status?.note ? `: ${quote(view.status.note)}` : ""}`);
616
638
  }
639
+ case "relay_join": {
640
+ const name2 = str(args, "name");
641
+ const under = str(args, "under");
642
+ const joined = options.membership?.() ?? {};
643
+ const claim = "handle" in joined && joined.handle ? { ...options.claim, handle: joined.handle } : options.claim;
644
+ const next = await options.connect();
645
+ try {
646
+ const hello = await next.hello({ transient: true, claim, join: { ...name2 ? { handle: name2 } : {}, ...under ? { under } : {} } });
647
+ me = hello.endpoint;
648
+ } finally {
649
+ next.close();
650
+ }
651
+ await ensure();
652
+ return text(
653
+ `joined the relay as ${me?.address}${me?.parent ? ` under ${me.parent}` : ""}: peers can message you, and new messages reach you after your tool calls`
654
+ );
655
+ }
656
+ case "relay_leave": {
657
+ const view = await call("leave");
658
+ client?.close();
659
+ client = void 0;
660
+ token = void 0;
661
+ return text(`${view.address} left the relay: it shows offline and gets no new messages pushed; its mail is kept until relay_join`);
662
+ }
617
663
  case "relay_set_title": {
618
664
  if (typeof args.title !== "string") throw new ToolInputError("title must be a string");
619
665
  const view = await call("title", { text: args.title });
@@ -807,7 +853,16 @@ ${wrapMessage(message)}`,
807
853
  inFlight.add(running);
808
854
  void running.finally(() => inFlight.delete(running));
809
855
  }
810
- ensure().catch((error) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`));
856
+ const dark = (() => {
857
+ try {
858
+ sessionClaim();
859
+ return false;
860
+ } catch {
861
+ return true;
862
+ }
863
+ })();
864
+ if (!dark)
865
+ ensure().catch((error) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`));
811
866
  const heartbeat = setInterval(() => {
812
867
  call("whoami").catch(() => {
813
868
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scp-dev/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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.2",
33
- "@scp-dev/core": "0.1.1"
32
+ "@scp-dev/broker": "0.1.4",
33
+ "@scp-dev/core": "0.1.3"
34
34
  },
35
35
  "devDependencies": {
36
36
  "tsup": "^8.5.1"
package/src/server.ts CHANGED
@@ -57,6 +57,8 @@ export interface RelayMcpOptions {
57
57
  input: AsyncIterable<Uint8Array | string> | ReadableStream<Uint8Array>
58
58
  /** one JSON-RPC line, newline included */
59
59
  output: (line: string) => void
60
+ /** whether the session is on the relay now: `dark` (why not) keeps the shim from claiming, `handle` is the name it joined under */
61
+ membership?: () => { dark: string } | { handle?: string }
60
62
  /** HOME shortens cwds in relay_list */
61
63
  env?: Record<string, string | undefined>
62
64
  /** push deliveries as `notifications/claude/channel` to a client that declares `claude/channel` or was launched with channels */
@@ -137,6 +139,7 @@ export const TOOLS = [
137
139
  body: { type: "string" },
138
140
  thread: { type: "string", description: "continue this thread: its id, or the id or short id of a message in it" },
139
141
  private: { type: "boolean", description: "only for recipients allowed private data" },
142
+ live: { type: "boolean", description: "refuse (not_live) instead of queueing when a recipient has no live session" },
140
143
  },
141
144
  required: ["to", "body"],
142
145
  additionalProperties: false,
@@ -226,6 +229,21 @@ export const TOOLS = [
226
229
  "Set your now, a one-line note on what you are doing right now, shown with your title in relay_list and relay_peers; your status state stays (working when unset).",
227
230
  inputSchema: { type: "object", properties: { now: { type: "string", maxLength: LIMITS.noteChars } }, required: ["now"], additionalProperties: false },
228
231
  },
232
+ {
233
+ name: "relay_join",
234
+ 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); 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.",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: { name: { type: "string", maxLength: 63 }, under: { type: "string", maxLength: 128 } },
239
+ additionalProperties: false,
240
+ },
241
+ },
242
+ {
243
+ name: "relay_leave",
244
+ description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.",
245
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
246
+ },
229
247
  {
230
248
  name: "relay_set_title",
231
249
  description: `Set your title, a short line on what you work on that every agent and your user see in relay_list and ${CLI_NAME} list; an empty title clears it.`,
@@ -523,7 +541,15 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
523
541
  while (sent.size > DELIVERED_KEEP) sent.delete(sent.keys().next().value!)
524
542
  }
525
543
 
544
+ /** the claim for the session as it stands; a session off the relay is refused `unavailable` */
545
+ function sessionClaim(): Claim {
546
+ const joined = options.membership?.() ?? {}
547
+ if ("dark" in joined) throw new RelayError("unavailable", joined.dark)
548
+ return joined.handle ? { ...options.claim, handle: joined.handle } : options.claim
549
+ }
550
+
526
551
  async function open(): Promise<RelayClient> {
552
+ const claim = sessionClaim()
527
553
  const next = await options.connect()
528
554
  try {
529
555
  let hello: HelloResult | undefined
@@ -534,7 +560,7 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
534
560
  if (!(error instanceof RelayError)) throw error
535
561
  }
536
562
  }
537
- hello ??= await next.hello({ claim: options.claim, ...(options.claim.cwd === undefined ? {} : { cwd: options.claim.cwd }) })
563
+ hello ??= await next.hello({ claim, ...(claim.cwd === undefined ? {} : { cwd: claim.cwd }) })
538
564
  if (hello.token) token = hello.token
539
565
  me = hello.endpoint
540
566
  if (!announced) {
@@ -653,10 +679,11 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
653
679
  body: str(args, "body", true),
654
680
  ...(args.thread !== undefined ? { thread: str(args, "thread") } : {}),
655
681
  ...(bool(args, "private") ? { data_class: "private" } : {}),
682
+ ...(bool(args, "live") ? { live: true } : {}),
656
683
  client_id: clientId(),
657
684
  })
658
685
  if (result.held) return text(heldText(result.held))
659
- const lines = reachLines(result.deliveries)
686
+ const lines = [...reachLines(result.deliveries), ...(result.warnings ?? [])]
660
687
  const to = result.deliveries.map((d) => (lines.length ? d.address : deliveryText(d))).join(", ")
661
688
  const receipt = result.deliveries.some((d) => d.state === "running" || d.state === "starting")
662
689
  ? [`relay_status with id ${shortId(result.id)} shows when it is read`]
@@ -752,6 +779,30 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
752
779
  const view = await call<EndpointView>("status", { note: args.now })
753
780
  return text(`${view.address}: ${view.status?.state}${view.status?.note ? `: ${quote(view.status.note)}` : ""}`)
754
781
  }
782
+ case "relay_join": {
783
+ const name = str(args, "name")
784
+ const under = str(args, "under")
785
+ const joined = options.membership?.() ?? {}
786
+ const claim = "handle" in joined && joined.handle ? { ...options.claim, handle: joined.handle } : options.claim
787
+ const next = await options.connect()
788
+ try {
789
+ const hello = await next.hello({ transient: true, claim, join: { ...(name ? { handle: name } : {}), ...(under ? { under } : {}) } })
790
+ me = hello.endpoint
791
+ } finally {
792
+ next.close()
793
+ }
794
+ await ensure()
795
+ return text(
796
+ `joined the relay as ${me?.address}${me?.parent ? ` under ${me.parent}` : ""}: peers can message you, and new messages reach you after your tool calls`,
797
+ )
798
+ }
799
+ case "relay_leave": {
800
+ const view = await call<EndpointView>("leave")
801
+ client?.close()
802
+ client = undefined
803
+ token = undefined
804
+ return text(`${view.address} left the relay: it shows offline and gets no new messages pushed; its mail is kept until relay_join`)
805
+ }
755
806
  case "relay_set_title": {
756
807
  if (typeof args.title !== "string") throw new ToolInputError("title must be a string")
757
808
  const view = await call<EndpointView>("title", { text: args.title })
@@ -949,7 +1000,16 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
949
1000
  void running.finally(() => inFlight.delete(running))
950
1001
  }
951
1002
 
952
- ensure().catch((error: unknown) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`))
1003
+ const dark = (() => {
1004
+ try {
1005
+ sessionClaim()
1006
+ return false
1007
+ } catch {
1008
+ return true
1009
+ }
1010
+ })()
1011
+ if (!dark)
1012
+ ensure().catch((error: unknown) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`))
953
1013
  const heartbeat = setInterval(() => {
954
1014
  call("whoami").catch(() => {})
955
1015
  }, options.heartbeatMs ?? 30_000)