@cello-protocol/client 0.0.5 → 0.0.6
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/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +84 -15
- package/dist/client.js.map +1 -1
- package/dist/mcp-server.d.ts +17 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +275 -18
- package/dist/mcp-server.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/mcp-server.d.ts
CHANGED
|
@@ -261,10 +261,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
261
261
|
import type { CelloClient } from "./types.js";
|
|
262
262
|
import type { CelloNode } from "@cello-protocol/transport";
|
|
263
263
|
import type { KeyProvider } from "@cello-protocol/crypto";
|
|
264
|
-
import type { CheckpointStatusProvider } from "@cello-protocol/interfaces";
|
|
264
|
+
import type { CheckpointStatusProvider, Logger } from "@cello-protocol/interfaces";
|
|
265
265
|
import type { ClientBackup } from "./client-backup.js";
|
|
266
|
+
/**
|
|
267
|
+
* AC-005 (DX-001): Default demo agent ID — the CELLO demo agent that strangers can connect to.
|
|
268
|
+
* Overridable via CELLO_DEMO_AGENT_ID env var without a code change.
|
|
269
|
+
* Must NOT be hardcoded as a string literal in tool implementations.
|
|
270
|
+
*/
|
|
271
|
+
export declare const DEFAULT_DEMO_AGENT_ID = "a2c55e2721f45cfa86cb3417a76e3f7b";
|
|
266
272
|
export declare function createMcpSessionServer(node: CelloNode, client: CelloClient, keyProvider: KeyProvider, opts?: {
|
|
267
273
|
checkpointStatusProvider?: CheckpointStatusProvider;
|
|
268
274
|
clientBackup?: ClientBackup;
|
|
275
|
+
/** AC-007 (DX-001): Directory HTTP URL for /agent-lookup endpoint. */
|
|
276
|
+
directoryUrl?: string;
|
|
277
|
+
/**
|
|
278
|
+
* AC-009 (DX-001): Promise that resolves when background init (DB open, directory dial,
|
|
279
|
+
* loadPersistedState) has completed. Tools that require the directory/FROST await this
|
|
280
|
+
* with a 10s timeout before proceeding.
|
|
281
|
+
*/
|
|
282
|
+
readyPromise?: Promise<void>;
|
|
283
|
+
/** Structured logger for observability events (e.g. client.directory.agent_lookup.failed). */
|
|
284
|
+
logger?: Logger;
|
|
269
285
|
}): McpServer;
|
|
270
286
|
//# sourceMappingURL=mcp-server.d.ts.map
|
package/dist/mcp-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,WAAW,EAA0B,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,WAAW,EAA0B,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA0BvD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,qCAAqC,CAAC;AAexE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IACL,wBAAwB,CAAC,EAAE,wBAAwB,CAAC;IACpD,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACA,SAAS,CAgyCX"}
|
package/dist/mcp-server.js
CHANGED
|
@@ -267,6 +267,26 @@ function jsonText(value) {
|
|
|
267
267
|
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
268
268
|
}
|
|
269
269
|
const TRANSPORT_NOT_STARTED = jsonText({ error: { reason: "transport_not_started" } });
|
|
270
|
+
/**
|
|
271
|
+
* AC-004 (DX-001): Structured not_registered error returned when any tool that requires
|
|
272
|
+
* registration is called before the agent has completed registration.
|
|
273
|
+
* SI-001: must not include the pre-auth token in any error message.
|
|
274
|
+
*/
|
|
275
|
+
const NOT_REGISTERED_ERROR = jsonText({
|
|
276
|
+
error: {
|
|
277
|
+
reason: "not_registered",
|
|
278
|
+
message: "This agent is not yet registered with the CELLO directory. " +
|
|
279
|
+
"To register: (1) Get a token from @CelloConnectStagingBot on Telegram, " +
|
|
280
|
+
"(2) call cello_register({ token: 'CELLO-...' }). " +
|
|
281
|
+
"Call cello_setup_guidance() for the full 6-step setup guide.",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
/**
|
|
285
|
+
* AC-005 (DX-001): Default demo agent ID — the CELLO demo agent that strangers can connect to.
|
|
286
|
+
* Overridable via CELLO_DEMO_AGENT_ID env var without a code change.
|
|
287
|
+
* Must NOT be hardcoded as a string literal in tool implementations.
|
|
288
|
+
*/
|
|
289
|
+
export const DEFAULT_DEMO_AGENT_ID = "a2c55e2721f45cfa86cb3417a76e3f7b";
|
|
270
290
|
function toHex(bytes) {
|
|
271
291
|
return Buffer.from(bytes).toString("hex");
|
|
272
292
|
}
|
|
@@ -280,6 +300,22 @@ function sleep(ms) {
|
|
|
280
300
|
export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
281
301
|
const checkpointStatusProvider = opts?.checkpointStatusProvider;
|
|
282
302
|
const clientBackup = opts?.clientBackup;
|
|
303
|
+
const directoryHttpUrl = opts?.directoryUrl;
|
|
304
|
+
const logger = opts?.logger;
|
|
305
|
+
const readyPromise = opts?.readyPromise;
|
|
306
|
+
/**
|
|
307
|
+
* AC-009 (DX-001): Await background init with a 10s timeout.
|
|
308
|
+
* Tools that require the directory/FROST (cello_initiate_session, cello_request_connection)
|
|
309
|
+
* call this before proceeding. cello_status and cello_setup_guidance do NOT await this.
|
|
310
|
+
*/
|
|
311
|
+
async function awaitReady() {
|
|
312
|
+
if (!readyPromise)
|
|
313
|
+
return;
|
|
314
|
+
await Promise.race([
|
|
315
|
+
readyPromise,
|
|
316
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("background_init_timeout")), 10_000)),
|
|
317
|
+
]);
|
|
318
|
+
}
|
|
283
319
|
const startedAt = Date.now();
|
|
284
320
|
// FIFO queue of inbound session assignment events.
|
|
285
321
|
// Populated by client.onSessionAssignment callback.
|
|
@@ -287,6 +323,16 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
287
323
|
function transportStarted() {
|
|
288
324
|
return node.listenAddresses().length > 0;
|
|
289
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* AC-004 (DX-001): Returns true if the agent is registered (registration state exists).
|
|
328
|
+
* Used by all tools that require registration to return NOT_REGISTERED_ERROR if not registered.
|
|
329
|
+
*/
|
|
330
|
+
function isRegistered() {
|
|
331
|
+
const regState = typeof client.getRegistrationState === "function"
|
|
332
|
+
? client.getRegistrationState()
|
|
333
|
+
: null;
|
|
334
|
+
return regState !== null;
|
|
335
|
+
}
|
|
290
336
|
function directoryReachable() {
|
|
291
337
|
// Check whether the libp2p node currently has an open connection to the directory peer.
|
|
292
338
|
// This is accurate: true means the signaling stream is (or was recently) live;
|
|
@@ -320,18 +366,58 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
320
366
|
// signaling stream, sends session_request, and awaits session_assignment from
|
|
321
367
|
// the directory. The M1 polling stub has been replaced.
|
|
322
368
|
server.registerTool("cello_initiate_session", {
|
|
323
|
-
description: "Initiate a session with a target agent
|
|
369
|
+
description: "Initiate a session with a target agent. Accepts either target_pubkey (64 hex chars) or target_agent_id (32 hex chars). Requires an existing connection (M3+).",
|
|
324
370
|
inputSchema: {
|
|
325
|
-
target_pubkey: z.string().describe("Target agent K_local pubkey as lowercase hex (64 chars)"),
|
|
371
|
+
target_pubkey: z.string().optional().describe("Target agent K_local pubkey as lowercase hex (64 chars)"),
|
|
372
|
+
target_agent_id: z.string().optional().describe("Target agent's agent_id as lowercase hex (32 chars). Resolved to pubkey via directory /agent-lookup."),
|
|
326
373
|
timeout_ms: z.number().int().min(0).optional().describe("Optional timeout in milliseconds"),
|
|
327
374
|
},
|
|
328
|
-
}, async ({ target_pubkey, timeout_ms }) => {
|
|
375
|
+
}, async ({ target_pubkey, target_agent_id, timeout_ms }) => {
|
|
376
|
+
if (!isRegistered())
|
|
377
|
+
return NOT_REGISTERED_ERROR;
|
|
378
|
+
// AC-009 (DX-001): await background init (directory connection, loadPersistedState)
|
|
379
|
+
try {
|
|
380
|
+
await awaitReady();
|
|
381
|
+
}
|
|
382
|
+
catch { /* proceed — may return directory_unreachable if needed */ }
|
|
329
383
|
if (!transportStarted())
|
|
330
384
|
return TRANSPORT_NOT_STARTED;
|
|
331
|
-
|
|
385
|
+
// AC-007b (DX-001): resolve target_agent_id to target_pubkey via /agent-lookup
|
|
386
|
+
let resolvedTargetPubkey = target_pubkey;
|
|
387
|
+
if (!resolvedTargetPubkey && target_agent_id) {
|
|
388
|
+
if (!/^[0-9a-f]{32}$/i.test(target_agent_id)) {
|
|
389
|
+
return jsonText({ ok: false, reason: "invalid_target_agent_id", agent_id: target_agent_id });
|
|
390
|
+
}
|
|
391
|
+
if (!directoryHttpUrl) {
|
|
392
|
+
return jsonText({ ok: false, reason: "directory_not_configured", agent_id: target_agent_id });
|
|
393
|
+
}
|
|
394
|
+
const t0Lookup = Date.now();
|
|
395
|
+
try {
|
|
396
|
+
const resp = await fetch(`${directoryHttpUrl}/agent-lookup?agent_id=${target_agent_id}`, { signal: AbortSignal.timeout(10_000) });
|
|
397
|
+
if (!resp.ok) {
|
|
398
|
+
logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "http_error", durationMs: Date.now() - t0Lookup });
|
|
399
|
+
return jsonText({ ok: false, reason: "agent_not_found", agent_id: target_agent_id });
|
|
400
|
+
}
|
|
401
|
+
const body = await resp.json();
|
|
402
|
+
if (!body.k_local_pubkey) {
|
|
403
|
+
logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "missing_pubkey_in_response", durationMs: Date.now() - t0Lookup });
|
|
404
|
+
return jsonText({ ok: false, reason: "agent_not_found", agent_id: target_agent_id });
|
|
405
|
+
}
|
|
406
|
+
resolvedTargetPubkey = body.k_local_pubkey;
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
410
|
+
logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: 0, reason, durationMs: Date.now() - t0Lookup });
|
|
411
|
+
return jsonText({ ok: false, reason: "agent_not_found", agent_id: target_agent_id });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!resolvedTargetPubkey) {
|
|
415
|
+
return jsonText({ ok: false, reason: "missing_target", message: "Provide either target_pubkey (64 hex) or target_agent_id (32 hex)." });
|
|
416
|
+
}
|
|
417
|
+
if (!client.hasConnection(resolvedTargetPubkey)) {
|
|
332
418
|
return jsonText({ ok: false, reason: "no_connection" });
|
|
333
419
|
}
|
|
334
|
-
const result = await client.initiateSession(
|
|
420
|
+
const result = await client.initiateSession(resolvedTargetPubkey, { timeoutMs: timeout_ms });
|
|
335
421
|
if (result.ok) {
|
|
336
422
|
return jsonText({
|
|
337
423
|
ok: true,
|
|
@@ -351,6 +437,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
351
437
|
timeout_ms: z.number().int().min(0).describe("Maximum wait time in milliseconds"),
|
|
352
438
|
},
|
|
353
439
|
}, async ({ timeout_ms }) => {
|
|
440
|
+
if (!isRegistered())
|
|
441
|
+
return NOT_REGISTERED_ERROR;
|
|
354
442
|
if (!transportStarted())
|
|
355
443
|
return TRANSPORT_NOT_STARTED;
|
|
356
444
|
const deadline = Date.now() + timeout_ms;
|
|
@@ -382,6 +470,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
382
470
|
content: z.string().describe("UTF-8 message content"),
|
|
383
471
|
},
|
|
384
472
|
}, async ({ session_id, content }) => {
|
|
473
|
+
if (!isRegistered())
|
|
474
|
+
return NOT_REGISTERED_ERROR;
|
|
385
475
|
if (!transportStarted())
|
|
386
476
|
return TRANSPORT_NOT_STARTED;
|
|
387
477
|
const contentBytes = new TextEncoder().encode(content);
|
|
@@ -419,6 +509,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
419
509
|
timeout_ms: z.number().int().min(0).describe("Maximum wait time in milliseconds"),
|
|
420
510
|
},
|
|
421
511
|
}, async ({ session_id, timeout_ms }) => {
|
|
512
|
+
if (!isRegistered())
|
|
513
|
+
return NOT_REGISTERED_ERROR;
|
|
422
514
|
if (!transportStarted())
|
|
423
515
|
return TRANSPORT_NOT_STARTED;
|
|
424
516
|
const msg = await client.receiveSessionMessageAsync(session_id, timeout_ms);
|
|
@@ -468,6 +560,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
468
560
|
timeout_ms: z.number().int().min(0).describe("Maximum wait time in milliseconds"),
|
|
469
561
|
},
|
|
470
562
|
}, async ({ timeout_ms }) => {
|
|
563
|
+
if (!isRegistered())
|
|
564
|
+
return NOT_REGISTERED_ERROR;
|
|
471
565
|
if (!transportStarted())
|
|
472
566
|
return TRANSPORT_NOT_STARTED;
|
|
473
567
|
const result = await client.receiveMessageAsync(timeout_ms);
|
|
@@ -518,6 +612,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
518
612
|
session_id: z.string().describe("Session ID as lowercase hex"),
|
|
519
613
|
},
|
|
520
614
|
}, async ({ session_id }) => {
|
|
615
|
+
if (!isRegistered())
|
|
616
|
+
return NOT_REGISTERED_ERROR;
|
|
521
617
|
if (!transportStarted())
|
|
522
618
|
return TRANSPORT_NOT_STARTED;
|
|
523
619
|
const result = await client.initiateSessionSeal(session_id);
|
|
@@ -603,6 +699,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
603
699
|
description: "List all known sessions and their current status.",
|
|
604
700
|
inputSchema: {},
|
|
605
701
|
}, async () => {
|
|
702
|
+
if (!isRegistered())
|
|
703
|
+
return NOT_REGISTERED_ERROR;
|
|
606
704
|
const records = client.listSessions();
|
|
607
705
|
const sessions = records.map((s) => ({
|
|
608
706
|
session_id: toHex(s.session_id),
|
|
@@ -674,6 +772,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
674
772
|
session_id: z.string().describe("Session ID as lowercase hex"),
|
|
675
773
|
},
|
|
676
774
|
}, async ({ session_id }) => {
|
|
775
|
+
if (!isRegistered())
|
|
776
|
+
return NOT_REGISTERED_ERROR;
|
|
677
777
|
const record = client.listSessions().find((s) => toHex(s.session_id) === session_id);
|
|
678
778
|
if (!record) {
|
|
679
779
|
return jsonText({ error: { reason: "session_not_found", session_id } });
|
|
@@ -748,6 +848,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
748
848
|
leaf_index: z.number().int().min(0).describe("Zero-based index of the target leaf"),
|
|
749
849
|
},
|
|
750
850
|
}, async ({ session_id, leaf_index }) => {
|
|
851
|
+
if (!isRegistered())
|
|
852
|
+
return NOT_REGISTERED_ERROR;
|
|
751
853
|
// PERSIST-017: if CheckpointStatusProvider is wired in, check MMR checkpoint status first.
|
|
752
854
|
// Returns the pending/confirmed/error response based on staging table state.
|
|
753
855
|
if (checkpointStatusProvider) {
|
|
@@ -822,18 +924,97 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
822
924
|
sealed_root: localRootHex,
|
|
823
925
|
});
|
|
824
926
|
});
|
|
927
|
+
// ── cello_setup_guidance ──────────────────────────────────────────────────
|
|
928
|
+
//
|
|
929
|
+
// AC-005 (DX-001): Returns the full 6-step CELLO setup guide (always shown,
|
|
930
|
+
// never collapsed) followed by a 'Your current status' section.
|
|
931
|
+
// Available to unregistered agents — does NOT require registration.
|
|
932
|
+
server.registerTool("cello_setup_guidance", {
|
|
933
|
+
description: "Get the full CELLO setup guide and your current status. Always shows all 6 steps. Call this first to understand how to use CELLO.",
|
|
934
|
+
inputSchema: {},
|
|
935
|
+
}, async () => {
|
|
936
|
+
// AC-005 (DX-001): Demo agent ID from env var or constant — NOT hardcoded inline.
|
|
937
|
+
const demoAgentId = process.env["CELLO_DEMO_AGENT_ID"] ?? DEFAULT_DEMO_AGENT_ID;
|
|
938
|
+
// Gather current status
|
|
939
|
+
const regStateRaw = typeof client.getRegistrationState === "function"
|
|
940
|
+
? client.getRegistrationState()
|
|
941
|
+
: null;
|
|
942
|
+
const registered = regStateRaw !== null;
|
|
943
|
+
const agentIdStr = registered ? regStateRaw.agent_id : null;
|
|
944
|
+
const connections = typeof client.listConnections === "function" ? client.listConnections() : [];
|
|
945
|
+
const sessions = client.listSessions().filter((s) => s.status === "active");
|
|
946
|
+
const dirReachable = directoryReachable();
|
|
947
|
+
// Determine current step for the pointer
|
|
948
|
+
let currentStep = 1;
|
|
949
|
+
let nextAction = "Call cello_register({ token: '<your token>' }) to register.";
|
|
950
|
+
if (registered && connections.length === 0) {
|
|
951
|
+
currentStep = 2;
|
|
952
|
+
nextAction = `Call cello_request_connection({ target_agent_id: '${demoAgentId}' }) to connect to the demo agent.`;
|
|
953
|
+
}
|
|
954
|
+
else if (registered && connections.length > 0 && sessions.length === 0) {
|
|
955
|
+
currentStep = 3;
|
|
956
|
+
const firstConn = connections[0];
|
|
957
|
+
nextAction = `Call cello_initiate_session({ target_agent_id: '${demoAgentId}' }) (or target_pubkey: '${firstConn.counterparty_pubkey}') to start a session.`;
|
|
958
|
+
}
|
|
959
|
+
else if (registered && sessions.length > 0) {
|
|
960
|
+
currentStep = 4;
|
|
961
|
+
const firstSession = sessions[0];
|
|
962
|
+
nextAction = `Call cello_send({ session_id: '${toHex(firstSession.session_id)}', content: 'Hello!' }) to send a message.`;
|
|
963
|
+
}
|
|
964
|
+
const guide = [
|
|
965
|
+
"# CELLO Setup Guide",
|
|
966
|
+
"",
|
|
967
|
+
"## Step 1: Get your registration token",
|
|
968
|
+
"Message @CelloConnectStagingBot on Telegram. Say 'register me' and it will send you a one-time token.",
|
|
969
|
+
"",
|
|
970
|
+
"## Step 2: Register with the directory",
|
|
971
|
+
"Call: cello_register({ token: 'CELLO-<your token>' })",
|
|
972
|
+
"This runs a cryptographic key ceremony (FROST DKG) with the directory. Takes 5-10 seconds.",
|
|
973
|
+
"",
|
|
974
|
+
"## Step 3: Connect to another agent",
|
|
975
|
+
`Call: cello_request_connection({ target_agent_id: '${demoAgentId}' })`,
|
|
976
|
+
`Or connect to any other agent by their agent_id (32 hex chars) or pubkey (64 hex chars).`,
|
|
977
|
+
"The demo agent is always online and will accept your connection automatically.",
|
|
978
|
+
"",
|
|
979
|
+
"## Step 4: Start a session",
|
|
980
|
+
`Call: cello_initiate_session({ target_agent_id: '${demoAgentId}' })`,
|
|
981
|
+
"A session is an encrypted, tamper-evident message channel.",
|
|
982
|
+
"",
|
|
983
|
+
"## Step 5: Send and receive messages",
|
|
984
|
+
"Call: cello_send({ session_id: '<session_id>', content: 'Hello!' })",
|
|
985
|
+
"Call: cello_receive({ timeout_ms: 10000 }) to receive replies",
|
|
986
|
+
"",
|
|
987
|
+
"## Step 6: Close the session and get your permanent record",
|
|
988
|
+
"Call: cello_close_session({ session_id: '<session_id>' })",
|
|
989
|
+
"Call: cello_get_sealed_receipt({ session_id: '<session_id>' }) for a cryptographic proof",
|
|
990
|
+
"",
|
|
991
|
+
"---",
|
|
992
|
+
"## Your current status",
|
|
993
|
+
`Registered: ${registered ? `YES (agent_id: ${agentIdStr})` : "NO"}`,
|
|
994
|
+
`Connections: ${connections.length}`,
|
|
995
|
+
`Active sessions: ${sessions.length}`,
|
|
996
|
+
`Directory reachable: ${dirReachable ? "YES" : "NO"}`,
|
|
997
|
+
"",
|
|
998
|
+
`→ You are at Step ${currentStep}. ${nextAction}`,
|
|
999
|
+
].join("\n");
|
|
1000
|
+
return { content: [{ type: "text", text: guide }] };
|
|
1001
|
+
});
|
|
825
1002
|
// ── cello_register ────────────────────────────────────────────────────────
|
|
826
1003
|
//
|
|
827
1004
|
// REG-001: DKG registration. Idempotent — second call returns already_registered.
|
|
828
1005
|
// SI-001: never emits ML-DSA secret key material.
|
|
829
1006
|
server.registerTool("cello_register", {
|
|
830
|
-
description: "Register this agent with the CELLO directory. Runs the REG-001 DKG ceremony. Idempotent — returns already_registered if already done.",
|
|
1007
|
+
description: "Register this agent with the CELLO directory. Runs the REG-001 DKG ceremony. Idempotent — returns already_registered if already done. Obtain your token from @CelloConnectStagingBot on Telegram.",
|
|
831
1008
|
inputSchema: {
|
|
832
|
-
|
|
833
|
-
|
|
1009
|
+
// AC-006 (DX-001): phone_stub removed from user-facing schema; token renamed from pre_auth_token.
|
|
1010
|
+
// The client sends phone_stub='' internally (directory accepts empty when token is present).
|
|
1011
|
+
token: z.string().describe("Pre-authorization token from @CelloConnectStagingBot on Telegram. Format: CELLO-<33 base58 chars> (production) or DEV-<...> (local). SI-001: this token must not appear in any log or response."),
|
|
834
1012
|
},
|
|
835
|
-
}, async ({
|
|
836
|
-
|
|
1013
|
+
}, async ({ token }) => {
|
|
1014
|
+
// AC-006 (DX-001): phone_stub is not a user-facing parameter.
|
|
1015
|
+
// Send '' as the wire value — the directory accepts empty phone_stub when pre_auth_token is valid.
|
|
1016
|
+
// SI-001: the token must NOT appear in any log event, error message, or MCP tool response.
|
|
1017
|
+
const result = await client.register("", token);
|
|
837
1018
|
if ("error" in result) {
|
|
838
1019
|
return jsonText({ error: { reason: result.error } });
|
|
839
1020
|
}
|
|
@@ -848,21 +1029,64 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
848
1029
|
// ── cello_request_connection ───────────────────────────────────────────────
|
|
849
1030
|
//
|
|
850
1031
|
// CONNREQ-002: Send Round 1 connection_request to target B.
|
|
851
|
-
//
|
|
1032
|
+
// AC-004 (DX-001): requires registration.
|
|
1033
|
+
// AC-007 (DX-001): accepts target_agent_id (32 hex) in addition to target_pubkey (64 hex).
|
|
1034
|
+
// AC-008 (DX-001): per-stage timeouts (10s dial, 10s send, 90s wait).
|
|
852
1035
|
server.registerTool("cello_request_connection", {
|
|
853
|
-
description: "Send a connection request to a target agent. Blocks until the target responds
|
|
1036
|
+
description: "Send a connection request to a target agent. Accepts either target_pubkey (64 hex chars) or target_agent_id (32 hex chars — looks up pubkey from directory). Blocks until the target responds or timeouts per stage.",
|
|
854
1037
|
inputSchema: {
|
|
855
|
-
target_pubkey: z.string().describe("Target agent's
|
|
1038
|
+
target_pubkey: z.string().optional().describe("Target agent's K_local pubkey as lowercase hex (64 chars). Use this if you know the raw pubkey."),
|
|
1039
|
+
target_agent_id: z.string().optional().describe("Target agent's agent_id as lowercase hex (32 chars). The directory resolves this to a pubkey automatically."),
|
|
856
1040
|
include_endorsements: z.boolean().optional().describe("Include endorsements in the connection package"),
|
|
857
1041
|
include_attestations: z.boolean().optional().describe("Include attestations in the connection package"),
|
|
858
1042
|
},
|
|
859
|
-
}, async ({ target_pubkey }) => {
|
|
1043
|
+
}, async ({ target_pubkey, target_agent_id }) => {
|
|
1044
|
+
if (!isRegistered())
|
|
1045
|
+
return NOT_REGISTERED_ERROR;
|
|
1046
|
+
// AC-009 (DX-001): await background init before attempting connection
|
|
1047
|
+
try {
|
|
1048
|
+
await awaitReady();
|
|
1049
|
+
}
|
|
1050
|
+
catch { /* proceed — per-stage timeouts handle directory_unreachable */ }
|
|
860
1051
|
const extendedClient = client;
|
|
1052
|
+
// AC-007 (DX-001): resolve target_agent_id to target_pubkey via /agent-lookup
|
|
1053
|
+
// This runs BEFORE the cello_request_connection function check so that agent_id
|
|
1054
|
+
// and missing_target errors are returned with clear messages in all cases.
|
|
1055
|
+
let resolvedTargetPubkey = target_pubkey;
|
|
1056
|
+
if (!resolvedTargetPubkey && target_agent_id) {
|
|
1057
|
+
if (!/^[0-9a-f]{32}$/i.test(target_agent_id)) {
|
|
1058
|
+
return jsonText({ error: { reason: "invalid_target_agent_id", message: "target_agent_id must be exactly 32 lowercase hex chars" } });
|
|
1059
|
+
}
|
|
1060
|
+
if (!directoryHttpUrl) {
|
|
1061
|
+
return jsonText({ error: { reason: "directory_not_configured", message: "Cannot resolve agent_id: directory URL is not configured." } });
|
|
1062
|
+
}
|
|
1063
|
+
const t0Lookup = Date.now();
|
|
1064
|
+
try {
|
|
1065
|
+
const resp = await fetch(`${directoryHttpUrl}/agent-lookup?agent_id=${target_agent_id}`, { signal: AbortSignal.timeout(10_000) });
|
|
1066
|
+
if (!resp.ok) {
|
|
1067
|
+
logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "http_error", durationMs: Date.now() - t0Lookup });
|
|
1068
|
+
return jsonText({ error: { reason: "agent_not_found", agent_id: target_agent_id } });
|
|
1069
|
+
}
|
|
1070
|
+
const body = await resp.json();
|
|
1071
|
+
if (!body.k_local_pubkey) {
|
|
1072
|
+
logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "missing_pubkey_in_response", durationMs: Date.now() - t0Lookup });
|
|
1073
|
+
return jsonText({ error: { reason: "agent_not_found", agent_id: target_agent_id } });
|
|
1074
|
+
}
|
|
1075
|
+
resolvedTargetPubkey = body.k_local_pubkey;
|
|
1076
|
+
}
|
|
1077
|
+
catch (e) {
|
|
1078
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1079
|
+
logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: 0, reason, durationMs: Date.now() - t0Lookup });
|
|
1080
|
+
return jsonText({ error: { reason: "agent_not_found", agent_id: target_agent_id } });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (!resolvedTargetPubkey) {
|
|
1084
|
+
return jsonText({ error: { reason: "missing_target", message: "Provide either target_pubkey (64 hex) or target_agent_id (32 hex)." } });
|
|
1085
|
+
}
|
|
861
1086
|
if (typeof extendedClient.cello_request_connection !== "function") {
|
|
862
|
-
return jsonText({ error: { reason: "
|
|
1087
|
+
return jsonText({ error: { reason: "not_implemented" } });
|
|
863
1088
|
}
|
|
864
1089
|
// Build a real ConnectionPackage (pseudonym binding) from registration state.
|
|
865
|
-
// Falls back to an empty package if the agent is not yet registered.
|
|
866
1090
|
let packageCbor = new Uint8Array(0);
|
|
867
1091
|
const regState = typeof extendedClient.getRegistrationState === "function"
|
|
868
1092
|
? extendedClient.getRegistrationState()
|
|
@@ -890,8 +1114,12 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
890
1114
|
}
|
|
891
1115
|
}
|
|
892
1116
|
const result = await extendedClient.cello_request_connection({
|
|
893
|
-
target_pubkey,
|
|
1117
|
+
target_pubkey: resolvedTargetPubkey,
|
|
894
1118
|
package_cbor: packageCbor,
|
|
1119
|
+
// AC-008 (DX-001): per-stage timeouts
|
|
1120
|
+
dialTimeoutMs: 10_000,
|
|
1121
|
+
sendTimeoutMs: 10_000,
|
|
1122
|
+
waitTimeoutMs: 90_000,
|
|
895
1123
|
});
|
|
896
1124
|
if (result.result === "established") {
|
|
897
1125
|
return jsonText({ result: "accepted", connection_id: result.connection_id });
|
|
@@ -910,7 +1138,16 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
910
1138
|
});
|
|
911
1139
|
}
|
|
912
1140
|
if (result.result === "timeout") {
|
|
913
|
-
|
|
1141
|
+
// AC-008: stage-specific error messages
|
|
1142
|
+
const stage = result.stage;
|
|
1143
|
+
if (stage === "dial") {
|
|
1144
|
+
return jsonText({ error: { reason: "directory_unreachable_timeout", message: "Could not reach directory. Check your network connection." } });
|
|
1145
|
+
}
|
|
1146
|
+
if (stage === "send") {
|
|
1147
|
+
return jsonText({ error: { reason: "request_delivery_timeout", message: "Connection request not delivered to directory." } });
|
|
1148
|
+
}
|
|
1149
|
+
// Default: target response timeout
|
|
1150
|
+
return jsonText({ error: { reason: "target_response_timeout", message: "Target agent did not respond within 90s. The agent may be offline. Try again with cello_request_connection." } });
|
|
914
1151
|
}
|
|
915
1152
|
return jsonText({ error: { reason: result.reason } });
|
|
916
1153
|
});
|
|
@@ -925,6 +1162,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
925
1162
|
include_attestations: z.boolean().optional().describe("Include attestations in the updated package"),
|
|
926
1163
|
},
|
|
927
1164
|
}, async ({ connection_request_id }) => {
|
|
1165
|
+
if (!isRegistered())
|
|
1166
|
+
return NOT_REGISTERED_ERROR;
|
|
928
1167
|
const extendedClient = client;
|
|
929
1168
|
if (typeof extendedClient.cello_respond_to_disclosure_request !== "function") {
|
|
930
1169
|
return jsonText({ error: { reason: "no_pending_disclosure_request" } });
|
|
@@ -982,6 +1221,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
982
1221
|
timeout_ms: z.number().int().min(0).optional().describe("Maximum wait time in ms. Default: 30000"),
|
|
983
1222
|
},
|
|
984
1223
|
}, async ({ timeout_ms }) => {
|
|
1224
|
+
if (!isRegistered())
|
|
1225
|
+
return NOT_REGISTERED_ERROR;
|
|
985
1226
|
const result = await client.awaitConnectionRequest(timeout_ms ?? 30_000);
|
|
986
1227
|
if (result.type === "timeout") {
|
|
987
1228
|
return jsonText({ type: "timeout" });
|
|
@@ -1003,6 +1244,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1003
1244
|
connection_request_id: z.string().describe("Connection request ID from cello_await_connection_request"),
|
|
1004
1245
|
},
|
|
1005
1246
|
}, async ({ connection_request_id }) => {
|
|
1247
|
+
if (!isRegistered())
|
|
1248
|
+
return NOT_REGISTERED_ERROR;
|
|
1006
1249
|
const result = await client.acceptConnection(connection_request_id);
|
|
1007
1250
|
if ("error" in result) {
|
|
1008
1251
|
return jsonText({ error: result.error });
|
|
@@ -1019,6 +1262,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1019
1262
|
reason: z.string().optional().describe("Optional rejection reason"),
|
|
1020
1263
|
},
|
|
1021
1264
|
}, async ({ connection_request_id, reason }) => {
|
|
1265
|
+
if (!isRegistered())
|
|
1266
|
+
return NOT_REGISTERED_ERROR;
|
|
1022
1267
|
const result = await client.rejectConnection(connection_request_id, reason);
|
|
1023
1268
|
if ("error" in result) {
|
|
1024
1269
|
return jsonText({ error: result.error });
|
|
@@ -1036,6 +1281,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1036
1281
|
requested_items: z.array(z.record(z.string(), z.unknown())).describe("List of disclosure items to request"),
|
|
1037
1282
|
},
|
|
1038
1283
|
}, async ({ connection_request_id, requested_items }) => {
|
|
1284
|
+
if (!isRegistered())
|
|
1285
|
+
return NOT_REGISTERED_ERROR;
|
|
1039
1286
|
const result = await client.requestMoreDisclosure(connection_request_id, requested_items);
|
|
1040
1287
|
if ("error" in result) {
|
|
1041
1288
|
return jsonText({ error: result.error });
|
|
@@ -1049,6 +1296,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1049
1296
|
description: "List all active connections for this agent.",
|
|
1050
1297
|
inputSchema: {},
|
|
1051
1298
|
}, async () => {
|
|
1299
|
+
if (!isRegistered())
|
|
1300
|
+
return NOT_REGISTERED_ERROR;
|
|
1052
1301
|
const records = client.listConnections();
|
|
1053
1302
|
const connections = records.map((c) => ({
|
|
1054
1303
|
connection_id: c.connection_id,
|
|
@@ -1073,6 +1322,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1073
1322
|
})).optional().describe("Signal requirements (optional)"),
|
|
1074
1323
|
},
|
|
1075
1324
|
}, async ({ mode, review_mode, requirements }) => {
|
|
1325
|
+
if (!isRegistered())
|
|
1326
|
+
return NOT_REGISTERED_ERROR;
|
|
1076
1327
|
const policy = {
|
|
1077
1328
|
mode,
|
|
1078
1329
|
review_mode,
|
|
@@ -1096,6 +1347,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1096
1347
|
description: "Return the current connection policy configuration.",
|
|
1097
1348
|
inputSchema: {},
|
|
1098
1349
|
}, async () => {
|
|
1350
|
+
if (!isRegistered())
|
|
1351
|
+
return NOT_REGISTERED_ERROR;
|
|
1099
1352
|
const getFn = client.getPolicy;
|
|
1100
1353
|
const policy = typeof getFn === "function"
|
|
1101
1354
|
? getFn.call(client)
|
|
@@ -1116,6 +1369,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1116
1369
|
"Returns ok:true on success. Returns ok:false with reason if backup fails or is not configured.",
|
|
1117
1370
|
inputSchema: {},
|
|
1118
1371
|
}, async () => {
|
|
1372
|
+
if (!isRegistered())
|
|
1373
|
+
return NOT_REGISTERED_ERROR;
|
|
1119
1374
|
if (!clientBackup) {
|
|
1120
1375
|
return jsonText({ ok: false, reason: "not_configured" });
|
|
1121
1376
|
}
|
|
@@ -1138,6 +1393,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
|
|
|
1138
1393
|
"Returns ok:true on success. Returns ok:false with reason if restore fails or is not configured.",
|
|
1139
1394
|
inputSchema: {},
|
|
1140
1395
|
}, async () => {
|
|
1396
|
+
if (!isRegistered())
|
|
1397
|
+
return NOT_REGISTERED_ERROR;
|
|
1141
1398
|
if (!clientBackup) {
|
|
1142
1399
|
return jsonText({ ok: false, reason: "not_configured" });
|
|
1143
1400
|
}
|