@scp-dev/mcp 0.1.2 → 0.1.3

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,27 @@ 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).";
293
+ readonly inputSchema: {
294
+ readonly type: "object";
295
+ readonly properties: {
296
+ readonly name: {
297
+ readonly type: "string";
298
+ readonly maxLength: 63;
299
+ };
300
+ };
301
+ readonly additionalProperties: false;
302
+ };
303
+ }, {
304
+ readonly name: "relay_leave";
305
+ readonly description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.";
306
+ readonly inputSchema: {
307
+ readonly type: "object";
308
+ readonly properties: {};
309
+ readonly additionalProperties: false;
310
+ };
280
311
  }, {
281
312
  readonly name: "relay_set_title";
282
313
  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,16 @@ 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).",
164
+ inputSchema: { type: "object", properties: { name: { type: "string", maxLength: 63 } }, additionalProperties: false }
165
+ },
166
+ {
167
+ name: "relay_leave",
168
+ description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.",
169
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
170
+ },
160
171
  {
161
172
  name: "relay_set_title",
162
173
  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 +409,13 @@ async function runRelayMcp(options) {
398
409
  sent.set(message.id, { from: message.from.address, thread: message.thread });
399
410
  while (sent.size > DELIVERED_KEEP) sent.delete(sent.keys().next().value);
400
411
  };
412
+ function sessionClaim() {
413
+ const joined = options.membership?.() ?? {};
414
+ if ("dark" in joined) throw new RelayError("unavailable", joined.dark);
415
+ return joined.handle ? { ...options.claim, handle: joined.handle } : options.claim;
416
+ }
401
417
  async function open() {
418
+ const claim = sessionClaim();
402
419
  const next = await options.connect();
403
420
  try {
404
421
  let hello;
@@ -409,7 +426,7 @@ async function runRelayMcp(options) {
409
426
  if (!(error instanceof RelayError)) throw error;
410
427
  }
411
428
  }
412
- hello ??= await next.hello({ claim: options.claim, ...options.claim.cwd === void 0 ? {} : { cwd: options.claim.cwd } });
429
+ hello ??= await next.hello({ claim, ...claim.cwd === void 0 ? {} : { cwd: claim.cwd } });
413
430
  if (hello.token) token = hello.token;
414
431
  me = hello.endpoint;
415
432
  if (!announced) {
@@ -516,10 +533,11 @@ async function runRelayMcp(options) {
516
533
  body: str(args, "body", true),
517
534
  ...args.thread !== void 0 ? { thread: str(args, "thread") } : {},
518
535
  ...bool(args, "private") ? { data_class: "private" } : {},
536
+ ...bool(args, "live") ? { live: true } : {},
519
537
  client_id: clientId()
520
538
  });
521
539
  if (result.held) return text(heldText(result.held));
522
- const lines = reachLines(result.deliveries);
540
+ const lines = [...reachLines(result.deliveries), ...result.warnings ?? []];
523
541
  const to = result.deliveries.map((d) => lines.length ? d.address : deliveryText(d)).join(", ");
524
542
  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
543
  return {
@@ -614,6 +632,27 @@ async function runRelayMcp(options) {
614
632
  const view = await call("status", { note: args.now });
615
633
  return text(`${view.address}: ${view.status?.state}${view.status?.note ? `: ${quote(view.status.note)}` : ""}`);
616
634
  }
635
+ case "relay_join": {
636
+ const name2 = str(args, "name");
637
+ const joined = options.membership?.() ?? {};
638
+ const claim = "handle" in joined && joined.handle ? { ...options.claim, handle: joined.handle } : options.claim;
639
+ const next = await options.connect();
640
+ try {
641
+ const hello = await next.hello({ transient: true, claim, join: name2 ? { handle: name2 } : {} });
642
+ me = hello.endpoint;
643
+ } finally {
644
+ next.close();
645
+ }
646
+ await ensure();
647
+ return text(`joined the relay as ${me?.address}: peers can message you, and new messages reach you after your tool calls`);
648
+ }
649
+ case "relay_leave": {
650
+ const view = await call("leave");
651
+ client?.close();
652
+ client = void 0;
653
+ token = void 0;
654
+ return text(`${view.address} left the relay: it shows offline and gets no new messages pushed; its mail is kept until relay_join`);
655
+ }
617
656
  case "relay_set_title": {
618
657
  if (typeof args.title !== "string") throw new ToolInputError("title must be a string");
619
658
  const view = await call("title", { text: args.title });
@@ -807,7 +846,16 @@ ${wrapMessage(message)}`,
807
846
  inFlight.add(running);
808
847
  void running.finally(() => inFlight.delete(running));
809
848
  }
810
- ensure().catch((error) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`));
849
+ const dark = (() => {
850
+ try {
851
+ sessionClaim();
852
+ return false;
853
+ } catch {
854
+ return true;
855
+ }
856
+ })();
857
+ if (!dark)
858
+ ensure().catch((error) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`));
811
859
  const heartbeat = setInterval(() => {
812
860
  call("whoami").catch(() => {
813
861
  });
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.3",
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.3",
33
+ "@scp-dev/core": "0.1.2"
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,17 @@ 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).",
236
+ inputSchema: { type: "object", properties: { name: { type: "string", maxLength: 63 } }, additionalProperties: false },
237
+ },
238
+ {
239
+ name: "relay_leave",
240
+ description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.",
241
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
242
+ },
229
243
  {
230
244
  name: "relay_set_title",
231
245
  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 +537,15 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
523
537
  while (sent.size > DELIVERED_KEEP) sent.delete(sent.keys().next().value!)
524
538
  }
525
539
 
540
+ /** the claim for the session as it stands; a session off the relay is refused `unavailable` */
541
+ function sessionClaim(): Claim {
542
+ const joined = options.membership?.() ?? {}
543
+ if ("dark" in joined) throw new RelayError("unavailable", joined.dark)
544
+ return joined.handle ? { ...options.claim, handle: joined.handle } : options.claim
545
+ }
546
+
526
547
  async function open(): Promise<RelayClient> {
548
+ const claim = sessionClaim()
527
549
  const next = await options.connect()
528
550
  try {
529
551
  let hello: HelloResult | undefined
@@ -534,7 +556,7 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
534
556
  if (!(error instanceof RelayError)) throw error
535
557
  }
536
558
  }
537
- hello ??= await next.hello({ claim: options.claim, ...(options.claim.cwd === undefined ? {} : { cwd: options.claim.cwd }) })
559
+ hello ??= await next.hello({ claim, ...(claim.cwd === undefined ? {} : { cwd: claim.cwd }) })
538
560
  if (hello.token) token = hello.token
539
561
  me = hello.endpoint
540
562
  if (!announced) {
@@ -653,10 +675,11 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
653
675
  body: str(args, "body", true),
654
676
  ...(args.thread !== undefined ? { thread: str(args, "thread") } : {}),
655
677
  ...(bool(args, "private") ? { data_class: "private" } : {}),
678
+ ...(bool(args, "live") ? { live: true } : {}),
656
679
  client_id: clientId(),
657
680
  })
658
681
  if (result.held) return text(heldText(result.held))
659
- const lines = reachLines(result.deliveries)
682
+ const lines = [...reachLines(result.deliveries), ...(result.warnings ?? [])]
660
683
  const to = result.deliveries.map((d) => (lines.length ? d.address : deliveryText(d))).join(", ")
661
684
  const receipt = result.deliveries.some((d) => d.state === "running" || d.state === "starting")
662
685
  ? [`relay_status with id ${shortId(result.id)} shows when it is read`]
@@ -752,6 +775,27 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
752
775
  const view = await call<EndpointView>("status", { note: args.now })
753
776
  return text(`${view.address}: ${view.status?.state}${view.status?.note ? `: ${quote(view.status.note)}` : ""}`)
754
777
  }
778
+ case "relay_join": {
779
+ const name = str(args, "name")
780
+ const joined = options.membership?.() ?? {}
781
+ const claim = "handle" in joined && joined.handle ? { ...options.claim, handle: joined.handle } : options.claim
782
+ const next = await options.connect()
783
+ try {
784
+ const hello = await next.hello({ transient: true, claim, join: name ? { handle: name } : {} })
785
+ me = hello.endpoint
786
+ } finally {
787
+ next.close()
788
+ }
789
+ await ensure()
790
+ return text(`joined the relay as ${me?.address}: peers can message you, and new messages reach you after your tool calls`)
791
+ }
792
+ case "relay_leave": {
793
+ const view = await call<EndpointView>("leave")
794
+ client?.close()
795
+ client = undefined
796
+ token = undefined
797
+ return text(`${view.address} left the relay: it shows offline and gets no new messages pushed; its mail is kept until relay_join`)
798
+ }
755
799
  case "relay_set_title": {
756
800
  if (typeof args.title !== "string") throw new ToolInputError("title must be a string")
757
801
  const view = await call<EndpointView>("title", { text: args.title })
@@ -949,7 +993,16 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
949
993
  void running.finally(() => inFlight.delete(running))
950
994
  }
951
995
 
952
- ensure().catch((error: unknown) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`))
996
+ const dark = (() => {
997
+ try {
998
+ sessionClaim()
999
+ return false
1000
+ } catch {
1001
+ return true
1002
+ }
1003
+ })()
1004
+ if (!dark)
1005
+ ensure().catch((error: unknown) => log(`${CLI_NAME} mcp: could not reach the relay broker yet: ${error instanceof Error ? error.message : String(error)}`))
953
1006
  const heartbeat = setInterval(() => {
954
1007
  call("whoami").catch(() => {})
955
1008
  }, options.heartbeatMs ?? 30_000)