@scp-dev/mcp 0.1.9 → 0.2.0
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 +25 -1
- package/dist/index.js +65 -12
- package/package.json +3 -3
- package/src/index.ts +11 -1
- package/src/server.ts +70 -11
package/dist/index.d.ts
CHANGED
|
@@ -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
|
+
/** Claude Code's per-session inbox socket (CLAUDE_CODE_MESSAGING_SOCKET): with channels enabled but not loaded, as in a background session, deliveries are queued there as the next turn */
|
|
29
|
+
sessionInbox?: SessionInbox;
|
|
28
30
|
/** the wake the endpoint advertises while this shim pushes by channel, over `pi` for a pi client and `channels` for any other */
|
|
29
31
|
wake?: Wake;
|
|
30
32
|
version?: string;
|
|
@@ -306,6 +308,19 @@ declare const TOOLS: readonly [{
|
|
|
306
308
|
};
|
|
307
309
|
readonly additionalProperties: false;
|
|
308
310
|
};
|
|
311
|
+
}, {
|
|
312
|
+
readonly name: "relay_link_accept";
|
|
313
|
+
readonly description: "Accept a link request; you then sit under the requesting agent as its worker.";
|
|
314
|
+
readonly inputSchema: {
|
|
315
|
+
readonly type: "object";
|
|
316
|
+
readonly properties: {
|
|
317
|
+
readonly id: {
|
|
318
|
+
readonly type: "string";
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
readonly required: readonly ["id"];
|
|
322
|
+
readonly additionalProperties: false;
|
|
323
|
+
};
|
|
309
324
|
}, {
|
|
310
325
|
readonly name: "relay_leave";
|
|
311
326
|
readonly description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.";
|
|
@@ -480,10 +495,19 @@ declare const TOOLS: readonly [{
|
|
|
480
495
|
readonly additionalProperties: false;
|
|
481
496
|
};
|
|
482
497
|
}];
|
|
498
|
+
type SessionInbox = {
|
|
499
|
+
socket: string;
|
|
500
|
+
token?: string;
|
|
501
|
+
sessionId?: string;
|
|
502
|
+
};
|
|
503
|
+
/** the inbox of the Claude Code session that started this process, from its environment */
|
|
504
|
+
declare function sessionInboxOf(env: Record<string, string | undefined>): SessionInbox | undefined;
|
|
505
|
+
/** queues one user turn on a Claude Code session's inbox socket; resolves once the lines are flushed and the socket closed */
|
|
506
|
+
declare function sendToSessionInbox(inbox: SessionInbox, content: string, timeoutMs?: number): Promise<void>;
|
|
483
507
|
/**
|
|
484
508
|
* The relay's MCP server over stdio: newline-delimited JSON-RPC 2.0, nothing else on the output.
|
|
485
509
|
* It claims one agent endpoint, keeps one broker connection (heartbeat, reconnect on the next call), and resolves when the input ends.
|
|
486
510
|
*/
|
|
487
511
|
declare function runRelayMcp(options: RelayMcpOptions): Promise<void>;
|
|
488
512
|
|
|
489
|
-
export { INSTRUCTIONS, MCP_PROTOCOLS, type RelayMcpOptions, type SpawnRequest, TOOLS, runRelayMcp };
|
|
513
|
+
export { INSTRUCTIONS, MCP_PROTOCOLS, type RelayMcpOptions, type SessionInbox, type SpawnRequest, TOOLS, runRelayMcp, sendToSessionInbox, sessionInboxOf };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
2
|
import { statSync } from "fs";
|
|
3
|
+
import { connect as connectSocket } from "net";
|
|
3
4
|
import { isAbsolute } from "path";
|
|
4
5
|
import {
|
|
5
6
|
CLI_NAME,
|
|
@@ -168,6 +169,11 @@ var TOOLS = [
|
|
|
168
169
|
additionalProperties: false
|
|
169
170
|
}
|
|
170
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: "relay_link_accept",
|
|
174
|
+
description: "Accept a link request; you then sit under the requesting agent as its worker.",
|
|
175
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false }
|
|
176
|
+
},
|
|
171
177
|
{
|
|
172
178
|
name: "relay_leave",
|
|
173
179
|
description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.",
|
|
@@ -393,6 +399,37 @@ function delivered(messages) {
|
|
|
393
399
|
` : "";
|
|
394
400
|
return [PREAMBLE, ...messages.map((message) => `${shown(message)}${wrapMessage(message)}`)].join("\n\n");
|
|
395
401
|
}
|
|
402
|
+
function sessionInboxOf(env) {
|
|
403
|
+
const socket = env.CLAUDE_CODE_MESSAGING_SOCKET;
|
|
404
|
+
if (!socket || !isAbsolute(socket)) return void 0;
|
|
405
|
+
return {
|
|
406
|
+
socket,
|
|
407
|
+
...env.CLAUDE_CODE_MESSAGING_TOKEN ? { token: env.CLAUDE_CODE_MESSAGING_TOKEN } : {},
|
|
408
|
+
...env.CLAUDE_CODE_SESSION_ID ? { sessionId: env.CLAUDE_CODE_SESSION_ID } : {}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function sendToSessionInbox(inbox, content, timeoutMs = 5e3) {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const socket = connectSocket(inbox.socket);
|
|
414
|
+
const timer = setTimeout(() => socket.destroy(new Error("session inbox timed out")), timeoutMs);
|
|
415
|
+
socket.once("error", (error) => {
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
reject(error);
|
|
418
|
+
});
|
|
419
|
+
socket.once("close", (failed) => {
|
|
420
|
+
clearTimeout(timer);
|
|
421
|
+
if (!failed) resolve();
|
|
422
|
+
});
|
|
423
|
+
socket.once("connect", () => {
|
|
424
|
+
const lines = [
|
|
425
|
+
...inbox.token ? [{ type: "auth", token: inbox.token }] : [],
|
|
426
|
+
{ type: "user", ...inbox.sessionId ? { session_id: inbox.sessionId } : {}, priority: "next", message: { role: "user", content } }
|
|
427
|
+
];
|
|
428
|
+
socket.end(lines.map((line) => `${JSON.stringify(line)}
|
|
429
|
+
`).join(""));
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
396
433
|
async function runRelayMcp(options) {
|
|
397
434
|
const log = options.log ?? (() => {
|
|
398
435
|
});
|
|
@@ -406,6 +443,7 @@ async function runRelayMcp(options) {
|
|
|
406
443
|
let announced = false;
|
|
407
444
|
let initialized = false;
|
|
408
445
|
let channelClient = false;
|
|
446
|
+
let inbox;
|
|
409
447
|
let stopped = false;
|
|
410
448
|
const channelPushed = /* @__PURE__ */ new Set();
|
|
411
449
|
let channelLoop;
|
|
@@ -669,6 +707,11 @@ async function runRelayMcp(options) {
|
|
|
669
707
|
`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
708
|
);
|
|
671
709
|
}
|
|
710
|
+
case "relay_link_accept": {
|
|
711
|
+
const result = await call("link_accept", { id: str(args, "id", true) });
|
|
712
|
+
me = result.endpoint;
|
|
713
|
+
return text(`${result.endpoint.address} now sits under ${result.parent} as its worker`);
|
|
714
|
+
}
|
|
672
715
|
case "relay_leave": {
|
|
673
716
|
const view = await call("leave");
|
|
674
717
|
client?.close();
|
|
@@ -778,7 +821,7 @@ ${wrapPeek(result)}`);
|
|
|
778
821
|
}
|
|
779
822
|
}
|
|
780
823
|
async function pushChannel() {
|
|
781
|
-
while (!stopped && channelClient) {
|
|
824
|
+
while (!stopped && (channelClient || inbox)) {
|
|
782
825
|
try {
|
|
783
826
|
const { messages, hold } = await call("inbox", { wait_s: LIMITS.injectHoldS, offer: true });
|
|
784
827
|
for (const message of messages) {
|
|
@@ -786,16 +829,22 @@ ${wrapPeek(result)}`);
|
|
|
786
829
|
channelPushed.add(message.seq);
|
|
787
830
|
if (channelPushed.size > DELIVERED_KEEP) channelPushed.delete(channelPushed.values().next().value);
|
|
788
831
|
remember(message);
|
|
789
|
-
|
|
790
|
-
jsonrpc: "2.0",
|
|
791
|
-
method: "notifications/claude/channel",
|
|
792
|
-
params: {
|
|
793
|
-
content: `${PREAMBLE}
|
|
832
|
+
const content = `${PREAMBLE}
|
|
794
833
|
|
|
795
|
-
${wrapMessage(message)}
|
|
796
|
-
|
|
834
|
+
${wrapMessage(message)}`;
|
|
835
|
+
if (inbox) {
|
|
836
|
+
try {
|
|
837
|
+
await sendToSessionInbox(inbox, content);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
channelPushed.delete(message.seq);
|
|
840
|
+
throw error;
|
|
797
841
|
}
|
|
798
|
-
}
|
|
842
|
+
} else
|
|
843
|
+
write({
|
|
844
|
+
jsonrpc: "2.0",
|
|
845
|
+
method: "notifications/claude/channel",
|
|
846
|
+
params: { content, meta: { from: message.from.address, id: message.id, seq: String(message.seq), thread: message.thread } }
|
|
847
|
+
});
|
|
799
848
|
}
|
|
800
849
|
const pushed = messages.flatMap((message) => message.seq !== void 0 && channelPushed.has(message.seq) ? [message.seq] : []);
|
|
801
850
|
if (pushed.length) await call("injected", { seqs: pushed, hold });
|
|
@@ -819,7 +868,8 @@ ${wrapMessage(message)}`,
|
|
|
819
868
|
}
|
|
820
869
|
const params = typeof message.params === "object" && message.params !== null ? message.params : {};
|
|
821
870
|
if (!isRequest) {
|
|
822
|
-
if (method === "notifications/initialized" && options.channels && channelClient
|
|
871
|
+
if (method === "notifications/initialized" && options.channels && (channelClient || inbox))
|
|
872
|
+
channelLoop ??= pushChannel().finally(() => channelLoop = void 0);
|
|
823
873
|
return;
|
|
824
874
|
}
|
|
825
875
|
const respond = (result) => write({ jsonrpc: "2.0", id, result });
|
|
@@ -832,7 +882,8 @@ ${wrapMessage(message)}`,
|
|
|
832
882
|
const channel = capabilities?.experimental?.["claude/channel"];
|
|
833
883
|
channelClient = typeof channel === "object" && channel !== null || options.launchedWithChannels === true;
|
|
834
884
|
const clientInfo = params.clientInfo;
|
|
835
|
-
|
|
885
|
+
inbox = options.channels && !channelClient && clientInfo?.name === "claude-code" ? options.sessionInbox : void 0;
|
|
886
|
+
wake = options.channels && channelClient ? options.wake ?? (clientInfo?.name === "pi" ? "pi" : "channels") : inbox ? "claude-inbox" : null;
|
|
836
887
|
advertise();
|
|
837
888
|
return respond({
|
|
838
889
|
protocolVersion,
|
|
@@ -922,5 +973,7 @@ export {
|
|
|
922
973
|
INSTRUCTIONS,
|
|
923
974
|
MCP_PROTOCOLS,
|
|
924
975
|
TOOLS,
|
|
925
|
-
runRelayMcp
|
|
976
|
+
runRelayMcp,
|
|
977
|
+
sendToSessionInbox,
|
|
978
|
+
sessionInboxOf
|
|
926
979
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scp-dev/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
33
|
-
"@scp-dev/core": "0.
|
|
32
|
+
"@scp-dev/broker": "0.4.0",
|
|
33
|
+
"@scp-dev/core": "0.3.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"tsup": "^8.5.1"
|
package/src/index.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
INSTRUCTIONS,
|
|
3
|
+
MCP_PROTOCOLS,
|
|
4
|
+
runRelayMcp,
|
|
5
|
+
sendToSessionInbox,
|
|
6
|
+
sessionInboxOf,
|
|
7
|
+
TOOLS,
|
|
8
|
+
type RelayMcpOptions,
|
|
9
|
+
type SessionInbox,
|
|
10
|
+
type SpawnRequest,
|
|
11
|
+
} from "./server"
|
package/src/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { statSync } from "node:fs"
|
|
2
|
+
import { connect as connectSocket } from "node:net"
|
|
2
3
|
import { isAbsolute } from "node:path"
|
|
3
4
|
import {
|
|
4
5
|
type AskResult,
|
|
@@ -67,6 +68,8 @@ export interface RelayMcpOptions {
|
|
|
67
68
|
channels?: boolean
|
|
68
69
|
/** the client was started with channels loaded (Claude Code does not declare the capability itself) */
|
|
69
70
|
launchedWithChannels?: boolean
|
|
71
|
+
/** Claude Code's per-session inbox socket (CLAUDE_CODE_MESSAGING_SOCKET): with channels enabled but not loaded, as in a background session, deliveries are queued there as the next turn */
|
|
72
|
+
sessionInbox?: SessionInbox
|
|
70
73
|
/** the wake the endpoint advertises while this shim pushes by channel, over `pi` for a pi client and `channels` for any other */
|
|
71
74
|
wake?: Wake
|
|
72
75
|
version?: string
|
|
@@ -243,6 +246,11 @@ export const TOOLS = [
|
|
|
243
246
|
additionalProperties: false,
|
|
244
247
|
},
|
|
245
248
|
},
|
|
249
|
+
{
|
|
250
|
+
name: "relay_link_accept",
|
|
251
|
+
description: "Accept a link request; you then sit under the requesting agent as its worker.",
|
|
252
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false },
|
|
253
|
+
},
|
|
246
254
|
{
|
|
247
255
|
name: "relay_leave",
|
|
248
256
|
description: "Take this session off the relay: it goes offline and gets no new messages pushed; its mail is kept until relay_join.",
|
|
@@ -517,6 +525,42 @@ function delivered(messages: Envelope[]): string {
|
|
|
517
525
|
return [PREAMBLE, ...messages.map((message) => `${shown(message)}${wrapMessage(message)}`)].join("\n\n")
|
|
518
526
|
}
|
|
519
527
|
|
|
528
|
+
export type SessionInbox = { socket: string; token?: string; sessionId?: string }
|
|
529
|
+
|
|
530
|
+
/** the inbox of the Claude Code session that started this process, from its environment */
|
|
531
|
+
export function sessionInboxOf(env: Record<string, string | undefined>): SessionInbox | undefined {
|
|
532
|
+
const socket = env.CLAUDE_CODE_MESSAGING_SOCKET
|
|
533
|
+
if (!socket || !isAbsolute(socket)) return undefined
|
|
534
|
+
return {
|
|
535
|
+
socket,
|
|
536
|
+
...(env.CLAUDE_CODE_MESSAGING_TOKEN ? { token: env.CLAUDE_CODE_MESSAGING_TOKEN } : {}),
|
|
537
|
+
...(env.CLAUDE_CODE_SESSION_ID ? { sessionId: env.CLAUDE_CODE_SESSION_ID } : {}),
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** queues one user turn on a Claude Code session's inbox socket; resolves once the lines are flushed and the socket closed */
|
|
542
|
+
export function sendToSessionInbox(inbox: SessionInbox, content: string, timeoutMs = 5000): Promise<void> {
|
|
543
|
+
return new Promise((resolve, reject) => {
|
|
544
|
+
const socket = connectSocket(inbox.socket)
|
|
545
|
+
const timer = setTimeout(() => socket.destroy(new Error("session inbox timed out")), timeoutMs)
|
|
546
|
+
socket.once("error", (error) => {
|
|
547
|
+
clearTimeout(timer)
|
|
548
|
+
reject(error)
|
|
549
|
+
})
|
|
550
|
+
socket.once("close", (failed) => {
|
|
551
|
+
clearTimeout(timer)
|
|
552
|
+
if (!failed) resolve()
|
|
553
|
+
})
|
|
554
|
+
socket.once("connect", () => {
|
|
555
|
+
const lines = [
|
|
556
|
+
...(inbox.token ? [{ type: "auth", token: inbox.token }] : []),
|
|
557
|
+
{ type: "user", ...(inbox.sessionId ? { session_id: inbox.sessionId } : {}), priority: "next", message: { role: "user", content } },
|
|
558
|
+
]
|
|
559
|
+
socket.end(lines.map((line) => `${JSON.stringify(line)}\n`).join(""))
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
520
564
|
/**
|
|
521
565
|
* The relay's MCP server over stdio: newline-delimited JSON-RPC 2.0, nothing else on the output.
|
|
522
566
|
* It claims one agent endpoint, keeps one broker connection (heartbeat, reconnect on the next call), and resolves when the input ends.
|
|
@@ -534,6 +578,8 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
|
|
|
534
578
|
let initialized = false
|
|
535
579
|
/** whether the client takes channel notifications: it says so, or it was launched with channels (Claude Code does not say so itself) */
|
|
536
580
|
let channelClient = false
|
|
581
|
+
/** the session inbox deliveries go to instead, for a Claude Code client without channels loaded */
|
|
582
|
+
let inbox: SessionInbox | undefined
|
|
537
583
|
let stopped = false
|
|
538
584
|
/** seqs this shim pushed, so a push whose confirm was lost is not pushed again */
|
|
539
585
|
const channelPushed = new Set<number>()
|
|
@@ -817,6 +863,11 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
|
|
|
817
863
|
`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
864
|
)
|
|
819
865
|
}
|
|
866
|
+
case "relay_link_accept": {
|
|
867
|
+
const result = await call<{ endpoint: EndpointView; parent: string }>("link_accept", { id: str(args, "id", true) })
|
|
868
|
+
me = result.endpoint
|
|
869
|
+
return text(`${result.endpoint.address} now sits under ${result.parent} as its worker`)
|
|
870
|
+
}
|
|
820
871
|
case "relay_leave": {
|
|
821
872
|
const view = await call<EndpointView>("leave")
|
|
822
873
|
client?.close()
|
|
@@ -929,7 +980,7 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
|
|
|
929
980
|
}
|
|
930
981
|
|
|
931
982
|
async function pushChannel(): Promise<void> {
|
|
932
|
-
while (!stopped && channelClient) {
|
|
983
|
+
while (!stopped && (channelClient || inbox)) {
|
|
933
984
|
try {
|
|
934
985
|
const { messages, hold } = await call<{ messages: Envelope[]; hold?: string | null }>("inbox", { wait_s: LIMITS.injectHoldS, offer: true })
|
|
935
986
|
for (const message of messages) {
|
|
@@ -937,14 +988,20 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
|
|
|
937
988
|
channelPushed.add(message.seq)
|
|
938
989
|
if (channelPushed.size > DELIVERED_KEEP) channelPushed.delete(channelPushed.values().next().value!)
|
|
939
990
|
remember(message)
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
991
|
+
const content = `${PREAMBLE}\n\n${wrapMessage(message)}`
|
|
992
|
+
if (inbox) {
|
|
993
|
+
try {
|
|
994
|
+
await sendToSessionInbox(inbox, content)
|
|
995
|
+
} catch (error) {
|
|
996
|
+
channelPushed.delete(message.seq)
|
|
997
|
+
throw error
|
|
998
|
+
}
|
|
999
|
+
} else
|
|
1000
|
+
write({
|
|
1001
|
+
jsonrpc: "2.0",
|
|
1002
|
+
method: "notifications/claude/channel",
|
|
1003
|
+
params: { content, meta: { from: message.from.address, id: message.id, seq: String(message.seq), thread: message.thread } },
|
|
1004
|
+
})
|
|
948
1005
|
}
|
|
949
1006
|
const pushed = messages.flatMap((message) => (message.seq !== undefined && channelPushed.has(message.seq) ? [message.seq] : []))
|
|
950
1007
|
if (pushed.length) await call("injected", { seqs: pushed, hold })
|
|
@@ -969,7 +1026,8 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
|
|
|
969
1026
|
}
|
|
970
1027
|
const params = (typeof message.params === "object" && message.params !== null ? message.params : {}) as Json
|
|
971
1028
|
if (!isRequest) {
|
|
972
|
-
if (method === "notifications/initialized" && options.channels && channelClient
|
|
1029
|
+
if (method === "notifications/initialized" && options.channels && (channelClient || inbox))
|
|
1030
|
+
channelLoop ??= pushChannel().finally(() => (channelLoop = undefined))
|
|
973
1031
|
return
|
|
974
1032
|
}
|
|
975
1033
|
const respond = (result: unknown) => write({ jsonrpc: "2.0", id, result })
|
|
@@ -982,7 +1040,8 @@ export async function runRelayMcp(options: RelayMcpOptions): Promise<void> {
|
|
|
982
1040
|
const channel = capabilities?.experimental?.["claude/channel"]
|
|
983
1041
|
channelClient = (typeof channel === "object" && channel !== null) || options.launchedWithChannels === true
|
|
984
1042
|
const clientInfo = params.clientInfo as { name?: unknown } | undefined
|
|
985
|
-
|
|
1043
|
+
inbox = options.channels && !channelClient && clientInfo?.name === "claude-code" ? options.sessionInbox : undefined
|
|
1044
|
+
wake = options.channels && channelClient ? (options.wake ?? (clientInfo?.name === "pi" ? "pi" : "channels")) : inbox ? "claude-inbox" : null
|
|
986
1045
|
advertise()
|
|
987
1046
|
return respond({
|
|
988
1047
|
protocolVersion,
|