@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 +31 -0
- package/dist/index.js +52 -4
- package/package.json +3 -3
- package/src/server.ts +56 -3
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
33
|
-
"@scp-dev/core": "0.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
|
|
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
|
-
|
|
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)
|