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