chainlesschain 0.47.7 → 0.47.9
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/package.json +10 -8
- package/src/commands/activitypub.js +533 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +344 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/social.js +265 -0
- package/src/index.js +2 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/ueba.js +403 -0
- package/src/repl/agent-repl.js +23 -0
package/src/commands/matrix.js
CHANGED
|
@@ -14,6 +14,14 @@ import {
|
|
|
14
14
|
getMessages,
|
|
15
15
|
joinRoom,
|
|
16
16
|
getLoginState,
|
|
17
|
+
sendThreadReply,
|
|
18
|
+
getThreadMessages,
|
|
19
|
+
getThreadRoots,
|
|
20
|
+
createSpace,
|
|
21
|
+
addSpaceChild,
|
|
22
|
+
removeSpaceChild,
|
|
23
|
+
listSpaceChildren,
|
|
24
|
+
listSpaces,
|
|
17
25
|
} from "../lib/matrix-bridge.js";
|
|
18
26
|
|
|
19
27
|
export function registerMatrixCommand(program) {
|
|
@@ -165,4 +173,279 @@ export function registerMatrixCommand(program) {
|
|
|
165
173
|
process.exit(1);
|
|
166
174
|
}
|
|
167
175
|
});
|
|
176
|
+
|
|
177
|
+
// ── Threads (MSC3440 / spec §11.38) ──────────────────────────────
|
|
178
|
+
|
|
179
|
+
const thread = matrix
|
|
180
|
+
.command("thread")
|
|
181
|
+
.description("Matrix threaded replies (m.thread relation)");
|
|
182
|
+
|
|
183
|
+
thread
|
|
184
|
+
.command("send <room-id> <root-event-id> <body>")
|
|
185
|
+
.description("Send a threaded reply referencing a root event")
|
|
186
|
+
.option("-t, --type <msgtype>", "Message type", "m.text")
|
|
187
|
+
.option(
|
|
188
|
+
"--reply-to <event-id>",
|
|
189
|
+
"Event the reply directly targets (defaults to root)",
|
|
190
|
+
)
|
|
191
|
+
.option(
|
|
192
|
+
"--no-fallback",
|
|
193
|
+
"Disable is_falling_back (non-thread clients won't see it as a reply)",
|
|
194
|
+
)
|
|
195
|
+
.option("--json", "Output as JSON")
|
|
196
|
+
.action(async (roomId, rootEventId, body, options) => {
|
|
197
|
+
try {
|
|
198
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
199
|
+
if (!ctx.db) {
|
|
200
|
+
logger.error("Database not available");
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const db = ctx.db.getDatabase();
|
|
204
|
+
ensureMatrixTables(db);
|
|
205
|
+
|
|
206
|
+
const result = sendThreadReply(db, {
|
|
207
|
+
roomId,
|
|
208
|
+
rootEventId,
|
|
209
|
+
body,
|
|
210
|
+
msgtype: options.type,
|
|
211
|
+
inReplyTo: options.replyTo,
|
|
212
|
+
isFallingBack: options.fallback !== false,
|
|
213
|
+
});
|
|
214
|
+
if (options.json) {
|
|
215
|
+
console.log(JSON.stringify(result, null, 2));
|
|
216
|
+
} else {
|
|
217
|
+
logger.success(`Thread reply sent to ${chalk.cyan(roomId)}`);
|
|
218
|
+
logger.log(` ${chalk.bold("Root:")} ${rootEventId}`);
|
|
219
|
+
logger.log(` ${chalk.bold("Event:")} ${result.event.eventId}`);
|
|
220
|
+
}
|
|
221
|
+
await shutdown();
|
|
222
|
+
} catch (err) {
|
|
223
|
+
logger.error(`Failed: ${err.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
thread
|
|
229
|
+
.command("list <room-id> <root-event-id>")
|
|
230
|
+
.description("List all replies in a thread")
|
|
231
|
+
.option("--json", "Output as JSON")
|
|
232
|
+
.action(async (roomId, rootEventId, options) => {
|
|
233
|
+
try {
|
|
234
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
235
|
+
if (!ctx.db) {
|
|
236
|
+
logger.error("Database not available");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const db = ctx.db.getDatabase();
|
|
240
|
+
ensureMatrixTables(db);
|
|
241
|
+
|
|
242
|
+
const messages = getThreadMessages(roomId, rootEventId);
|
|
243
|
+
if (options.json) {
|
|
244
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
245
|
+
} else if (messages.length === 0) {
|
|
246
|
+
logger.info("No replies in this thread.");
|
|
247
|
+
} else {
|
|
248
|
+
for (const m of messages) {
|
|
249
|
+
logger.log(` ${chalk.gray(m.sender)} ${m.content.body}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
await shutdown();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
logger.error(`Failed: ${err.message}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
thread
|
|
260
|
+
.command("roots <room-id>")
|
|
261
|
+
.description("List distinct thread roots within a room")
|
|
262
|
+
.option("--json", "Output as JSON")
|
|
263
|
+
.action(async (roomId, options) => {
|
|
264
|
+
try {
|
|
265
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
266
|
+
if (!ctx.db) {
|
|
267
|
+
logger.error("Database not available");
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const db = ctx.db.getDatabase();
|
|
271
|
+
ensureMatrixTables(db);
|
|
272
|
+
|
|
273
|
+
const roots = getThreadRoots(roomId);
|
|
274
|
+
if (options.json) {
|
|
275
|
+
console.log(JSON.stringify(roots, null, 2));
|
|
276
|
+
} else if (roots.length === 0) {
|
|
277
|
+
logger.info("No threads found.");
|
|
278
|
+
} else {
|
|
279
|
+
for (const r of roots) {
|
|
280
|
+
logger.log(
|
|
281
|
+
` ${chalk.cyan(r.rootEventId.slice(0, 16))}... replies=${r.replyCount} last=${r.lastReplyAt}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await shutdown();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
logger.error(`Failed: ${err.message}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ── Spaces (spec §11.34) ─────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
const space = matrix
|
|
295
|
+
.command("space")
|
|
296
|
+
.description("Matrix Spaces — hierarchical room grouping (m.space)");
|
|
297
|
+
|
|
298
|
+
space
|
|
299
|
+
.command("create <name>")
|
|
300
|
+
.description("Create a new Matrix Space")
|
|
301
|
+
.option("-t, --topic <text>", "Space topic / description")
|
|
302
|
+
.option("--json", "Output as JSON")
|
|
303
|
+
.action(async (name, options) => {
|
|
304
|
+
try {
|
|
305
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
306
|
+
if (!ctx.db) {
|
|
307
|
+
logger.error("Database not available");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const db = ctx.db.getDatabase();
|
|
311
|
+
ensureMatrixTables(db);
|
|
312
|
+
|
|
313
|
+
const result = createSpace(db, { name, topic: options.topic });
|
|
314
|
+
if (options.json) {
|
|
315
|
+
console.log(JSON.stringify(result, null, 2));
|
|
316
|
+
} else {
|
|
317
|
+
logger.success(`Space created: ${chalk.cyan(result.space.name)}`);
|
|
318
|
+
logger.log(` ${chalk.bold("Room ID:")} ${result.space.roomId}`);
|
|
319
|
+
}
|
|
320
|
+
await shutdown();
|
|
321
|
+
} catch (err) {
|
|
322
|
+
logger.error(`Failed: ${err.message}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
space
|
|
328
|
+
.command("add-child <space-id> <child-room-id>")
|
|
329
|
+
.description("Add a child room to a Space")
|
|
330
|
+
.option("--via <server...>", "Homeserver(s) via which child is reachable")
|
|
331
|
+
.option("--json", "Output as JSON")
|
|
332
|
+
.action(async (spaceId, childRoomId, options) => {
|
|
333
|
+
try {
|
|
334
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
335
|
+
if (!ctx.db) {
|
|
336
|
+
logger.error("Database not available");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
const db = ctx.db.getDatabase();
|
|
340
|
+
ensureMatrixTables(db);
|
|
341
|
+
|
|
342
|
+
const result = addSpaceChild(db, {
|
|
343
|
+
spaceId,
|
|
344
|
+
childRoomId,
|
|
345
|
+
via: options.via,
|
|
346
|
+
});
|
|
347
|
+
if (options.json) {
|
|
348
|
+
console.log(JSON.stringify(result, null, 2));
|
|
349
|
+
} else {
|
|
350
|
+
logger.success(
|
|
351
|
+
`Added ${chalk.cyan(childRoomId)} to space ${chalk.cyan(spaceId)}`,
|
|
352
|
+
);
|
|
353
|
+
logger.log(` ${chalk.bold("Via:")} ${result.via.join(", ")}`);
|
|
354
|
+
}
|
|
355
|
+
await shutdown();
|
|
356
|
+
} catch (err) {
|
|
357
|
+
logger.error(`Failed: ${err.message}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
space
|
|
363
|
+
.command("remove-child <space-id> <child-room-id>")
|
|
364
|
+
.description("Remove a child room from a Space")
|
|
365
|
+
.action(async (spaceId, childRoomId) => {
|
|
366
|
+
try {
|
|
367
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
368
|
+
if (!ctx.db) {
|
|
369
|
+
logger.error("Database not available");
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
const db = ctx.db.getDatabase();
|
|
373
|
+
ensureMatrixTables(db);
|
|
374
|
+
|
|
375
|
+
const result = removeSpaceChild(db, { spaceId, childRoomId });
|
|
376
|
+
if (result.removed) {
|
|
377
|
+
logger.success(
|
|
378
|
+
`Removed ${chalk.cyan(childRoomId)} from ${chalk.cyan(spaceId)}`,
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
logger.info(`${childRoomId} was not a child of ${spaceId}`);
|
|
382
|
+
}
|
|
383
|
+
await shutdown();
|
|
384
|
+
} catch (err) {
|
|
385
|
+
logger.error(`Failed: ${err.message}`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
space
|
|
391
|
+
.command("children <space-id>")
|
|
392
|
+
.description("List all child rooms of a Space")
|
|
393
|
+
.option("--json", "Output as JSON")
|
|
394
|
+
.action(async (spaceId, options) => {
|
|
395
|
+
try {
|
|
396
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
397
|
+
if (!ctx.db) {
|
|
398
|
+
logger.error("Database not available");
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
const db = ctx.db.getDatabase();
|
|
402
|
+
ensureMatrixTables(db);
|
|
403
|
+
|
|
404
|
+
const children = listSpaceChildren(spaceId);
|
|
405
|
+
if (options.json) {
|
|
406
|
+
console.log(JSON.stringify(children, null, 2));
|
|
407
|
+
} else if (children.length === 0) {
|
|
408
|
+
logger.info("Space has no children.");
|
|
409
|
+
} else {
|
|
410
|
+
for (const c of children) {
|
|
411
|
+
logger.log(` ${chalk.cyan(c.childRoomId)} via=${c.via.join(",")}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
await shutdown();
|
|
415
|
+
} catch (err) {
|
|
416
|
+
logger.error(`Failed: ${err.message}`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
space
|
|
422
|
+
.command("list")
|
|
423
|
+
.description("List all Spaces")
|
|
424
|
+
.option("--json", "Output as JSON")
|
|
425
|
+
.action(async (options) => {
|
|
426
|
+
try {
|
|
427
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
428
|
+
if (!ctx.db) {
|
|
429
|
+
logger.error("Database not available");
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
const db = ctx.db.getDatabase();
|
|
433
|
+
ensureMatrixTables(db);
|
|
434
|
+
|
|
435
|
+
const spaces = listSpaces();
|
|
436
|
+
if (options.json) {
|
|
437
|
+
console.log(JSON.stringify(spaces, null, 2));
|
|
438
|
+
} else if (spaces.length === 0) {
|
|
439
|
+
logger.info("No spaces found.");
|
|
440
|
+
} else {
|
|
441
|
+
for (const s of spaces) {
|
|
442
|
+
logger.log(` ${chalk.cyan(s.roomId)} ${s.name}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
await shutdown();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
logger.error(`Failed: ${err.message}`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
168
451
|
}
|
package/src/commands/mcp.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* chainlesschain mcp servers|connect|disconnect|tools|call
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
6
8
|
import chalk from "chalk";
|
|
7
9
|
import ora from "ora";
|
|
8
10
|
import { logger } from "../lib/logger.js";
|
|
@@ -12,6 +14,17 @@ import {
|
|
|
12
14
|
validateMcpServer,
|
|
13
15
|
annotateMcpCompatibility,
|
|
14
16
|
} from "@chainlesschain/session-core";
|
|
17
|
+
import {
|
|
18
|
+
generateMcpServerScaffold,
|
|
19
|
+
SUPPORTED_TRANSPORTS,
|
|
20
|
+
} from "../lib/mcp-scaffold.js";
|
|
21
|
+
import {
|
|
22
|
+
CATALOG as REGISTRY_CATALOG,
|
|
23
|
+
CATEGORIES as REGISTRY_CATEGORIES,
|
|
24
|
+
listServers as registryListServers,
|
|
25
|
+
searchServers as registrySearchServers,
|
|
26
|
+
getServer as registryGetServer,
|
|
27
|
+
} from "../lib/mcp-registry.js";
|
|
15
28
|
|
|
16
29
|
// Singleton MCP client for session reuse
|
|
17
30
|
let mcpClient = null;
|
|
@@ -381,4 +394,335 @@ export function registerMcpCommand(program) {
|
|
|
381
394
|
process.exit(1);
|
|
382
395
|
}
|
|
383
396
|
});
|
|
397
|
+
|
|
398
|
+
// mcp scaffold — generate a boilerplate MCP server project
|
|
399
|
+
mcp
|
|
400
|
+
.command("scaffold <name>")
|
|
401
|
+
.description("Scaffold a new MCP server project (stdio or http+sse)")
|
|
402
|
+
.option("-d, --description <text>", "Short description of the server")
|
|
403
|
+
.option(
|
|
404
|
+
"-t, --transport <kind>",
|
|
405
|
+
`Transport: ${SUPPORTED_TRANSPORTS.join("|")}`,
|
|
406
|
+
"stdio",
|
|
407
|
+
)
|
|
408
|
+
.option("-o, --output <dir>", "Target directory (defaults to ./<name>)")
|
|
409
|
+
.option("-a, --author <name>", "package.json author field")
|
|
410
|
+
.option("-p, --port <n>", "HTTP port (http transport only)", (v) =>
|
|
411
|
+
parseInt(v, 10),
|
|
412
|
+
)
|
|
413
|
+
.option("--force", "Overwrite existing files")
|
|
414
|
+
.option("--dry-run", "Print files that would be written, don't touch disk")
|
|
415
|
+
.option("--json", "Output as JSON")
|
|
416
|
+
.action(async (name, options) => {
|
|
417
|
+
try {
|
|
418
|
+
const { files, summary } = generateMcpServerScaffold({
|
|
419
|
+
name,
|
|
420
|
+
description: options.description,
|
|
421
|
+
transport: options.transport,
|
|
422
|
+
author: options.author,
|
|
423
|
+
port: options.port,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const targetDir = path.resolve(options.output || `./${summary.name}`);
|
|
427
|
+
|
|
428
|
+
if (options.dryRun) {
|
|
429
|
+
if (options.json) {
|
|
430
|
+
console.log(JSON.stringify({ targetDir, summary, files }, null, 2));
|
|
431
|
+
} else {
|
|
432
|
+
logger.log(
|
|
433
|
+
`${chalk.bold("Would write")} ${files.length} files to ${chalk.cyan(targetDir)}:`,
|
|
434
|
+
);
|
|
435
|
+
for (const f of files) {
|
|
436
|
+
logger.log(
|
|
437
|
+
` ${chalk.cyan(f.path)} ` +
|
|
438
|
+
chalk.dim(`(${f.content.length} bytes)`),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Collision check — refuse to clobber unless --force.
|
|
446
|
+
if (!options.force && fs.existsSync(targetDir)) {
|
|
447
|
+
const clashing = files.filter((f) =>
|
|
448
|
+
fs.existsSync(path.join(targetDir, f.path)),
|
|
449
|
+
);
|
|
450
|
+
if (clashing.length > 0) {
|
|
451
|
+
logger.error(
|
|
452
|
+
`Refusing to overwrite existing files in ${targetDir}: ` +
|
|
453
|
+
clashing.map((f) => f.path).join(", ") +
|
|
454
|
+
`. Re-run with --force to overwrite.`,
|
|
455
|
+
);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
461
|
+
for (const f of files) {
|
|
462
|
+
const full = path.join(targetDir, f.path);
|
|
463
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
464
|
+
fs.writeFileSync(full, f.content, "utf-8");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (options.json) {
|
|
468
|
+
console.log(
|
|
469
|
+
JSON.stringify(
|
|
470
|
+
{
|
|
471
|
+
targetDir,
|
|
472
|
+
summary,
|
|
473
|
+
files: files.map((f) => f.path),
|
|
474
|
+
},
|
|
475
|
+
null,
|
|
476
|
+
2,
|
|
477
|
+
),
|
|
478
|
+
);
|
|
479
|
+
} else {
|
|
480
|
+
logger.success(
|
|
481
|
+
`Scaffolded ${chalk.cyan(summary.name)} ` +
|
|
482
|
+
chalk.dim(
|
|
483
|
+
`(${summary.transport}${summary.port ? `, port ${summary.port}` : ""})`,
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
logger.log(` ${chalk.bold("Path:")} ${targetDir}`);
|
|
487
|
+
for (const f of files) {
|
|
488
|
+
logger.log(` ${chalk.dim("+")} ${f.path}`);
|
|
489
|
+
}
|
|
490
|
+
logger.log("");
|
|
491
|
+
logger.log(chalk.bold("Next steps:"));
|
|
492
|
+
logger.log(
|
|
493
|
+
` ${chalk.dim("$")} cd ${path.relative(process.cwd(), targetDir) || "."}`,
|
|
494
|
+
);
|
|
495
|
+
logger.log(` ${chalk.dim("$")} npm install`);
|
|
496
|
+
if (summary.transport === "stdio") {
|
|
497
|
+
logger.log(
|
|
498
|
+
` ${chalk.dim("$")} cc mcp add ${summary.name} -c node -a "./index.js"`,
|
|
499
|
+
);
|
|
500
|
+
} else {
|
|
501
|
+
logger.log(` ${chalk.dim("$")} npm start`);
|
|
502
|
+
logger.log(
|
|
503
|
+
` ${chalk.dim("$")} cc mcp add ${summary.name} -u http://localhost:${summary.port}/mcp`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} catch (err) {
|
|
508
|
+
logger.error(`Scaffold failed: ${err.message}`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// mcp registry — browse + search + one-shot install from the bundled catalog
|
|
514
|
+
const registry = mcp
|
|
515
|
+
.command("registry")
|
|
516
|
+
.description("Browse the curated catalog of community MCP servers");
|
|
517
|
+
|
|
518
|
+
registry
|
|
519
|
+
.command("list")
|
|
520
|
+
.description("List catalog entries (filter by category/tag/author)")
|
|
521
|
+
.option("-c, --category <name>", "Filter by category")
|
|
522
|
+
.option("-t, --tags <list>", "Filter by comma-separated tags (any match)")
|
|
523
|
+
.option("--author <name>", "Filter by author (substring, case-insensitive)")
|
|
524
|
+
.option("--sort <field>", "Sort by 'name' | 'rating' | 'category'", "name")
|
|
525
|
+
.option("--order <dir>", "'asc' | 'desc'", "asc")
|
|
526
|
+
.option("--limit <n>", "Max results", (v) => parseInt(v, 10))
|
|
527
|
+
.option("--offset <n>", "Pagination offset", (v) => parseInt(v, 10))
|
|
528
|
+
.option("--json", "Output as JSON")
|
|
529
|
+
.action((options) => {
|
|
530
|
+
try {
|
|
531
|
+
const tags = options.tags
|
|
532
|
+
? options.tags
|
|
533
|
+
.split(",")
|
|
534
|
+
.map((t) => t.trim())
|
|
535
|
+
.filter(Boolean)
|
|
536
|
+
: undefined;
|
|
537
|
+
const { servers, total } = registryListServers({
|
|
538
|
+
category: options.category,
|
|
539
|
+
tags,
|
|
540
|
+
author: options.author,
|
|
541
|
+
sortBy: options.sort,
|
|
542
|
+
sortOrder: options.order,
|
|
543
|
+
limit: options.limit,
|
|
544
|
+
offset: options.offset,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (options.json) {
|
|
548
|
+
console.log(JSON.stringify({ servers, total }, null, 2));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (servers.length === 0) {
|
|
553
|
+
logger.info("No catalog entries match your filters.");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
logger.log(
|
|
558
|
+
chalk.bold(`MCP Registry — ${servers.length}/${total} servers\n`),
|
|
559
|
+
);
|
|
560
|
+
for (const s of servers) {
|
|
561
|
+
logger.log(
|
|
562
|
+
` ${chalk.cyan(s.name)} ${chalk.gray(`(${s.id})`)} ` +
|
|
563
|
+
chalk.dim(`★${s.rating ?? "-"} ${s.category}`),
|
|
564
|
+
);
|
|
565
|
+
logger.log(` ${chalk.gray(s.description)}`);
|
|
566
|
+
if (s.tags?.length) {
|
|
567
|
+
logger.log(
|
|
568
|
+
` ${chalk.gray("tags:")} ${s.tags.slice(0, 5).join(", ")}`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} catch (err) {
|
|
573
|
+
logger.error(`Registry list failed: ${err.message}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
registry
|
|
579
|
+
.command("search <keyword>")
|
|
580
|
+
.description("Keyword search across name/description/tags")
|
|
581
|
+
.option("--json", "Output as JSON")
|
|
582
|
+
.action((keyword, options) => {
|
|
583
|
+
try {
|
|
584
|
+
const hits = registrySearchServers(keyword);
|
|
585
|
+
if (options.json) {
|
|
586
|
+
console.log(JSON.stringify({ hits, total: hits.length }, null, 2));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (hits.length === 0) {
|
|
590
|
+
logger.info(`No matches for "${keyword}".`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
logger.log(chalk.bold(`${hits.length} matches for "${keyword}":\n`));
|
|
594
|
+
for (const s of hits) {
|
|
595
|
+
logger.log(
|
|
596
|
+
` ${chalk.cyan(s.name)} ${chalk.gray(`(${s.id})`)} ` +
|
|
597
|
+
chalk.dim(s.category),
|
|
598
|
+
);
|
|
599
|
+
logger.log(` ${chalk.gray(s.description)}`);
|
|
600
|
+
}
|
|
601
|
+
} catch (err) {
|
|
602
|
+
logger.error(`Registry search failed: ${err.message}`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
registry
|
|
608
|
+
.command("show <idOrName>")
|
|
609
|
+
.description("Show full catalog entry (id or short name)")
|
|
610
|
+
.option("--json", "Output as JSON")
|
|
611
|
+
.action((idOrName, options) => {
|
|
612
|
+
try {
|
|
613
|
+
const entry = registryGetServer(idOrName);
|
|
614
|
+
if (!entry) {
|
|
615
|
+
logger.error(`Not found: "${idOrName}".`);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
if (options.json) {
|
|
619
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
logger.log(
|
|
623
|
+
`${chalk.bold(entry.displayName)} ${chalk.gray(`(${entry.id})`)}`,
|
|
624
|
+
);
|
|
625
|
+
logger.log(` ${chalk.gray("Author:")} ${entry.author}`);
|
|
626
|
+
logger.log(` ${chalk.gray("Category:")} ${entry.category}`);
|
|
627
|
+
logger.log(` ${chalk.gray("Version:")} ${entry.version}`);
|
|
628
|
+
logger.log(` ${chalk.gray("Rating:")} ★${entry.rating ?? "-"}`);
|
|
629
|
+
logger.log(` ${chalk.gray("Package:")} ${entry.npmPackage}`);
|
|
630
|
+
logger.log(
|
|
631
|
+
` ${chalk.gray("Command:")} ${entry.command} ${entry.args.join(" ")}`,
|
|
632
|
+
);
|
|
633
|
+
logger.log(` ${chalk.gray("Transport:")}${entry.transport}`);
|
|
634
|
+
if (entry.homepage) {
|
|
635
|
+
logger.log(` ${chalk.gray("Homepage:")} ${entry.homepage}`);
|
|
636
|
+
}
|
|
637
|
+
logger.log(`\n ${entry.description}`);
|
|
638
|
+
if (entry.tools?.length) {
|
|
639
|
+
logger.log(`\n ${chalk.bold("Tools:")} ${entry.tools.join(", ")}`);
|
|
640
|
+
}
|
|
641
|
+
logger.log(
|
|
642
|
+
`\n ${chalk.dim("Install:")} cc mcp registry install ${entry.name}`,
|
|
643
|
+
);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
logger.error(`Registry show failed: ${err.message}`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
registry
|
|
651
|
+
.command("install <idOrName>")
|
|
652
|
+
.description("Install a catalog entry by registering it as an MCP server")
|
|
653
|
+
.option("--as <name>", "Override the stored server name")
|
|
654
|
+
.option("--auto-connect", "Mark the server to auto-connect on startup")
|
|
655
|
+
.option("--json", "Output as JSON")
|
|
656
|
+
.action(async (idOrName, options) => {
|
|
657
|
+
try {
|
|
658
|
+
const entry = registryGetServer(idOrName);
|
|
659
|
+
if (!entry) {
|
|
660
|
+
logger.error(
|
|
661
|
+
`Not found: "${idOrName}". Try 'cc mcp registry list' or 'search'.`,
|
|
662
|
+
);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
667
|
+
if (!ctx.db) {
|
|
668
|
+
logger.error("Database not available");
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const db = ctx.db.getDatabase();
|
|
673
|
+
const config = new MCPServerConfig(db);
|
|
674
|
+
const storedName = (options.as || entry.name).trim();
|
|
675
|
+
|
|
676
|
+
config.add(storedName, {
|
|
677
|
+
command: entry.command,
|
|
678
|
+
args: entry.args,
|
|
679
|
+
autoConnect: !!options.autoConnect,
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
if (options.json) {
|
|
683
|
+
console.log(
|
|
684
|
+
JSON.stringify(
|
|
685
|
+
{
|
|
686
|
+
name: storedName,
|
|
687
|
+
id: entry.id,
|
|
688
|
+
command: entry.command,
|
|
689
|
+
args: entry.args,
|
|
690
|
+
autoConnect: !!options.autoConnect,
|
|
691
|
+
},
|
|
692
|
+
null,
|
|
693
|
+
2,
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
} else {
|
|
697
|
+
logger.success(
|
|
698
|
+
`Installed ${chalk.cyan(storedName)} ` + chalk.dim(`(${entry.id})`),
|
|
699
|
+
);
|
|
700
|
+
logger.log(
|
|
701
|
+
` ${chalk.gray("Command:")} ${entry.command} ${entry.args.join(" ")}`,
|
|
702
|
+
);
|
|
703
|
+
logger.log(` ${chalk.gray("Next:")} cc mcp connect ${storedName}`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
await shutdown();
|
|
707
|
+
} catch (err) {
|
|
708
|
+
logger.error(`Registry install failed: ${err.message}`);
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
registry
|
|
714
|
+
.command("categories")
|
|
715
|
+
.description("List available registry categories")
|
|
716
|
+
.option("--json", "Output as JSON")
|
|
717
|
+
.action((options) => {
|
|
718
|
+
if (options.json) {
|
|
719
|
+
console.log(JSON.stringify(REGISTRY_CATEGORIES, null, 2));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
logger.log(chalk.bold("Categories:"));
|
|
723
|
+
for (const c of REGISTRY_CATEGORIES) {
|
|
724
|
+
const count = REGISTRY_CATALOG.filter((s) => s.category === c).length;
|
|
725
|
+
logger.log(` ${chalk.cyan(c)} ${chalk.dim(`(${count})`)}`);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
384
728
|
}
|