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/lib/matrix-bridge.js
CHANGED
|
@@ -8,6 +8,7 @@ import crypto from "crypto";
|
|
|
8
8
|
/* ── In-memory stores ──────────────────────────────────────── */
|
|
9
9
|
const _rooms = new Map();
|
|
10
10
|
const _messages = new Map();
|
|
11
|
+
const _spaceChildren = new Map(); // spaceRoomId → Map<childRoomId, { via: string[] }>
|
|
11
12
|
let _loginState = {
|
|
12
13
|
state: "logged_out",
|
|
13
14
|
userId: null,
|
|
@@ -181,11 +182,262 @@ export function getMessages(roomId, filter = {}) {
|
|
|
181
182
|
return messages.slice(0, limit);
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
/* ── Threads (MSC3440 / spec §11.38) ───────────────────────── */
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Send a threaded reply. Stores an m.room.message event whose content has
|
|
189
|
+
* m.relates_to: { rel_type: "m.thread", event_id: <root>, is_falling_back, m.in_reply_to }
|
|
190
|
+
* Clients that don't understand threads fall back to rendering as a reply
|
|
191
|
+
* when is_falling_back=true (the default per spec).
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} db
|
|
194
|
+
* @param {Object} params
|
|
195
|
+
* @param {string} params.roomId
|
|
196
|
+
* @param {string} params.rootEventId - Event id of the thread's root message
|
|
197
|
+
* @param {string} params.body
|
|
198
|
+
* @param {string} [params.msgtype="m.text"]
|
|
199
|
+
* @param {string} [params.inReplyTo] - Defaults to rootEventId
|
|
200
|
+
* @param {boolean} [params.isFallingBack=true]
|
|
201
|
+
*/
|
|
202
|
+
export function sendThreadReply(
|
|
203
|
+
db,
|
|
204
|
+
{ roomId, rootEventId, body, msgtype, inReplyTo, isFallingBack = true },
|
|
205
|
+
) {
|
|
206
|
+
if (!roomId) throw new Error("Room ID is required");
|
|
207
|
+
if (!rootEventId) throw new Error("rootEventId is required");
|
|
208
|
+
if (!body) throw new Error("Message body is required");
|
|
209
|
+
|
|
210
|
+
const id = crypto.randomUUID();
|
|
211
|
+
const eventId = `$${crypto.randomBytes(16).toString("hex")}`;
|
|
212
|
+
const now = new Date().toISOString();
|
|
213
|
+
|
|
214
|
+
const relatesTo = {
|
|
215
|
+
rel_type: "m.thread",
|
|
216
|
+
event_id: rootEventId,
|
|
217
|
+
is_falling_back: isFallingBack,
|
|
218
|
+
"m.in_reply_to": { event_id: inReplyTo || rootEventId },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const message = {
|
|
222
|
+
id,
|
|
223
|
+
eventId,
|
|
224
|
+
roomId,
|
|
225
|
+
sender: _loginState.userId || "cli-user",
|
|
226
|
+
eventType: "m.room.message",
|
|
227
|
+
content: {
|
|
228
|
+
body,
|
|
229
|
+
msgtype: msgtype || "m.text",
|
|
230
|
+
"m.relates_to": relatesTo,
|
|
231
|
+
},
|
|
232
|
+
originServerTs: now,
|
|
233
|
+
isEncrypted: _loginState.e2eeEnabled,
|
|
234
|
+
createdAt: now,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
_messages.set(id, message);
|
|
238
|
+
|
|
239
|
+
db.prepare(
|
|
240
|
+
`INSERT INTO matrix_events (id, event_id, room_id, sender, event_type, content, origin_server_ts, is_encrypted, decrypted_content, created_at)
|
|
241
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
242
|
+
).run(
|
|
243
|
+
id,
|
|
244
|
+
eventId,
|
|
245
|
+
roomId,
|
|
246
|
+
message.sender,
|
|
247
|
+
"m.room.message",
|
|
248
|
+
JSON.stringify(message.content),
|
|
249
|
+
now,
|
|
250
|
+
message.isEncrypted ? 1 : 0,
|
|
251
|
+
body,
|
|
252
|
+
now,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return { success: true, event: message };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* List all replies to a given thread root (in chronological order).
|
|
260
|
+
*/
|
|
261
|
+
export function getThreadMessages(roomId, rootEventId) {
|
|
262
|
+
if (!roomId) throw new Error("Room ID is required");
|
|
263
|
+
if (!rootEventId) throw new Error("rootEventId is required");
|
|
264
|
+
|
|
265
|
+
return [..._messages.values()]
|
|
266
|
+
.filter((m) => {
|
|
267
|
+
if (m.roomId !== roomId) return false;
|
|
268
|
+
const rel = m.content && m.content["m.relates_to"];
|
|
269
|
+
return rel && rel.rel_type === "m.thread" && rel.event_id === rootEventId;
|
|
270
|
+
})
|
|
271
|
+
.sort((a, b) =>
|
|
272
|
+
a.originServerTs < b.originServerTs
|
|
273
|
+
? -1
|
|
274
|
+
: a.originServerTs > b.originServerTs
|
|
275
|
+
? 1
|
|
276
|
+
: 0,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* List distinct thread roots within a room (events that have at least one
|
|
282
|
+
* threaded reply). Returns { rootEventId, replyCount, lastReplyAt }.
|
|
283
|
+
*/
|
|
284
|
+
export function getThreadRoots(roomId) {
|
|
285
|
+
if (!roomId) throw new Error("Room ID is required");
|
|
286
|
+
|
|
287
|
+
const rollup = new Map();
|
|
288
|
+
for (const m of _messages.values()) {
|
|
289
|
+
if (m.roomId !== roomId) continue;
|
|
290
|
+
const rel = m.content && m.content["m.relates_to"];
|
|
291
|
+
if (!rel || rel.rel_type !== "m.thread") continue;
|
|
292
|
+
const rootId = rel.event_id;
|
|
293
|
+
const existing = rollup.get(rootId) || {
|
|
294
|
+
rootEventId: rootId,
|
|
295
|
+
replyCount: 0,
|
|
296
|
+
lastReplyAt: null,
|
|
297
|
+
};
|
|
298
|
+
existing.replyCount += 1;
|
|
299
|
+
if (!existing.lastReplyAt || m.originServerTs > existing.lastReplyAt) {
|
|
300
|
+
existing.lastReplyAt = m.originServerTs;
|
|
301
|
+
}
|
|
302
|
+
rollup.set(rootId, existing);
|
|
303
|
+
}
|
|
304
|
+
return [...rollup.values()];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ── Spaces (spec §11.34) ──────────────────────────────────── */
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a Matrix Space. A Space is a room whose creation content declares
|
|
311
|
+
* type: "m.space"
|
|
312
|
+
* — we model this by setting room.type on the in-memory room record and
|
|
313
|
+
* persisting it into matrix_rooms (reusing the existing topic column would
|
|
314
|
+
* be wrong, so we encode type by a "#space:" prefix on the room name).
|
|
315
|
+
*
|
|
316
|
+
* @param {Object} db
|
|
317
|
+
* @param {Object} params
|
|
318
|
+
* @param {string} params.name
|
|
319
|
+
* @param {string} [params.topic]
|
|
320
|
+
*/
|
|
321
|
+
export function createSpace(db, { name, topic }) {
|
|
322
|
+
if (!name) throw new Error("Space name is required");
|
|
323
|
+
|
|
324
|
+
const id = crypto.randomUUID();
|
|
325
|
+
const roomId = `!space_${crypto.randomBytes(8).toString("hex")}`;
|
|
326
|
+
const now = new Date().toISOString();
|
|
327
|
+
|
|
328
|
+
const space = {
|
|
329
|
+
id,
|
|
330
|
+
roomId,
|
|
331
|
+
name,
|
|
332
|
+
topic: topic || null,
|
|
333
|
+
type: "m.space",
|
|
334
|
+
isEncrypted: false, // Spaces are not encrypted per spec
|
|
335
|
+
memberCount: 1,
|
|
336
|
+
lastEventAt: now,
|
|
337
|
+
joinedAt: now,
|
|
338
|
+
status: "joined",
|
|
339
|
+
createdAt: now,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
_rooms.set(id, space);
|
|
343
|
+
_spaceChildren.set(roomId, new Map());
|
|
344
|
+
|
|
345
|
+
db.prepare(
|
|
346
|
+
`INSERT INTO matrix_rooms (id, room_id, name, topic, is_encrypted, member_count, last_event_at, joined_at, status, created_at)
|
|
347
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
348
|
+
).run(id, roomId, name, topic || null, 0, 1, now, now, "joined", now);
|
|
349
|
+
|
|
350
|
+
return { success: true, space };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Add a child room to a Space. In real Matrix this writes an m.space.child
|
|
355
|
+
* state event to the parent space with state_key = childRoomId.
|
|
356
|
+
*
|
|
357
|
+
* @param {Object} db
|
|
358
|
+
* @param {Object} params
|
|
359
|
+
* @param {string} params.spaceId - Parent space's room id (!space_*)
|
|
360
|
+
* @param {string} params.childRoomId - Child room id
|
|
361
|
+
* @param {string[]} [params.via] - Homeservers via which the child is reachable
|
|
362
|
+
*/
|
|
363
|
+
export function addSpaceChild(db, { spaceId, childRoomId, via }) {
|
|
364
|
+
if (!spaceId) throw new Error("spaceId is required");
|
|
365
|
+
if (!childRoomId) throw new Error("childRoomId is required");
|
|
366
|
+
|
|
367
|
+
const children = _spaceChildren.get(spaceId);
|
|
368
|
+
if (!children) {
|
|
369
|
+
throw new Error(`Space not found: ${spaceId}`);
|
|
370
|
+
}
|
|
371
|
+
const viaList = Array.isArray(via) && via.length > 0 ? via : ["matrix.org"];
|
|
372
|
+
children.set(childRoomId, { via: viaList });
|
|
373
|
+
|
|
374
|
+
// Persist as an m.space.child state event in matrix_events
|
|
375
|
+
const id = crypto.randomUUID();
|
|
376
|
+
const eventId = `$${crypto.randomBytes(16).toString("hex")}`;
|
|
377
|
+
const now = new Date().toISOString();
|
|
378
|
+
const content = { via: viaList };
|
|
379
|
+
|
|
380
|
+
db.prepare(
|
|
381
|
+
`INSERT INTO matrix_events (id, event_id, room_id, sender, event_type, content, origin_server_ts, is_encrypted, decrypted_content, created_at)
|
|
382
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
383
|
+
).run(
|
|
384
|
+
id,
|
|
385
|
+
eventId,
|
|
386
|
+
spaceId,
|
|
387
|
+
_loginState.userId || "cli-user",
|
|
388
|
+
"m.space.child",
|
|
389
|
+
JSON.stringify({ state_key: childRoomId, content }),
|
|
390
|
+
now,
|
|
391
|
+
0,
|
|
392
|
+
null,
|
|
393
|
+
now,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return { success: true, spaceId, childRoomId, via: viaList };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Remove a child from a Space (equivalent to sending an empty m.space.child
|
|
401
|
+
* state event in real Matrix).
|
|
402
|
+
*/
|
|
403
|
+
export function removeSpaceChild(db, { spaceId, childRoomId }) {
|
|
404
|
+
if (!spaceId) throw new Error("spaceId is required");
|
|
405
|
+
if (!childRoomId) throw new Error("childRoomId is required");
|
|
406
|
+
|
|
407
|
+
const children = _spaceChildren.get(spaceId);
|
|
408
|
+
if (!children) {
|
|
409
|
+
throw new Error(`Space not found: ${spaceId}`);
|
|
410
|
+
}
|
|
411
|
+
const removed = children.delete(childRoomId);
|
|
412
|
+
return { success: true, removed };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* List all children of a Space.
|
|
417
|
+
*/
|
|
418
|
+
export function listSpaceChildren(spaceId) {
|
|
419
|
+
if (!spaceId) throw new Error("spaceId is required");
|
|
420
|
+
const children = _spaceChildren.get(spaceId);
|
|
421
|
+
if (!children) return [];
|
|
422
|
+
return [...children.entries()].map(([childRoomId, meta]) => ({
|
|
423
|
+
childRoomId,
|
|
424
|
+
via: meta.via,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* List all Spaces (rooms with type === "m.space").
|
|
430
|
+
*/
|
|
431
|
+
export function listSpaces() {
|
|
432
|
+
return [..._rooms.values()].filter((r) => r.type === "m.space");
|
|
433
|
+
}
|
|
434
|
+
|
|
184
435
|
/* ── Reset (for testing) ───────────────────────────────────── */
|
|
185
436
|
|
|
186
437
|
export function _resetState() {
|
|
187
438
|
_rooms.clear();
|
|
188
439
|
_messages.clear();
|
|
440
|
+
_spaceChildren.clear();
|
|
189
441
|
_loginState = {
|
|
190
442
|
state: "logged_out",
|
|
191
443
|
userId: null,
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP community server registry — pure read-only catalog.
|
|
3
|
+
*
|
|
4
|
+
* Ships a bundled list of well-known community MCP servers so users can
|
|
5
|
+
* discover, search, and one-shot install them without maintaining a
|
|
6
|
+
* remote index. Mirrors the curated catalog from
|
|
7
|
+
* `desktop-app-vue/src/main/mcp/community-registry.js` but pared down
|
|
8
|
+
* to what the CLI actually needs: browse + search + hand the chosen
|
|
9
|
+
* entry to `mcp add`.
|
|
10
|
+
*
|
|
11
|
+
* The catalog is intentionally small (the 8 first-party
|
|
12
|
+
* `@modelcontextprotocol/server-*` packages). Users who want more can
|
|
13
|
+
* still `cc mcp add` any server by hand — the registry is a
|
|
14
|
+
* convenience, not the source of truth for which servers exist.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/* ── constants ──────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
export const CATEGORIES = Object.freeze([
|
|
20
|
+
"database",
|
|
21
|
+
"filesystem",
|
|
22
|
+
"version-control",
|
|
23
|
+
"search",
|
|
24
|
+
"automation",
|
|
25
|
+
"communication",
|
|
26
|
+
"cloud",
|
|
27
|
+
"productivity",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export const TRANSPORTS = Object.freeze(["stdio", "http"]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bundled catalog. Frozen at module load — treat as read-only. Each
|
|
34
|
+
* entry carries enough for `cc mcp add` to wire it up verbatim
|
|
35
|
+
* (command + args) plus prose/examples that help the agent REPL
|
|
36
|
+
* decide when to use it.
|
|
37
|
+
*/
|
|
38
|
+
export const CATALOG = Object.freeze([
|
|
39
|
+
{
|
|
40
|
+
id: "mcp-server-filesystem",
|
|
41
|
+
name: "filesystem",
|
|
42
|
+
displayName: "File System",
|
|
43
|
+
description:
|
|
44
|
+
"File system access — read, write, list, and manage files and directories.",
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
author: "Anthropic",
|
|
47
|
+
category: "filesystem",
|
|
48
|
+
tags: ["file", "directory", "read", "write", "filesystem"],
|
|
49
|
+
npmPackage: "@modelcontextprotocol/server-filesystem",
|
|
50
|
+
command: "npx",
|
|
51
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
52
|
+
transport: "stdio",
|
|
53
|
+
tools: [
|
|
54
|
+
"read_file",
|
|
55
|
+
"write_file",
|
|
56
|
+
"list_directory",
|
|
57
|
+
"create_directory",
|
|
58
|
+
"move_file",
|
|
59
|
+
"search_files",
|
|
60
|
+
"get_file_info",
|
|
61
|
+
],
|
|
62
|
+
homepage:
|
|
63
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
|
|
64
|
+
rating: 5.0,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "mcp-server-postgresql",
|
|
68
|
+
name: "postgresql",
|
|
69
|
+
displayName: "PostgreSQL",
|
|
70
|
+
description:
|
|
71
|
+
"PostgreSQL database — execute queries, inspect schema, manage data.",
|
|
72
|
+
version: "1.0.0",
|
|
73
|
+
author: "Anthropic",
|
|
74
|
+
category: "database",
|
|
75
|
+
tags: ["database", "sql", "postgresql", "postgres", "query"],
|
|
76
|
+
npmPackage: "@modelcontextprotocol/server-postgres",
|
|
77
|
+
command: "npx",
|
|
78
|
+
args: ["-y", "@modelcontextprotocol/server-postgres"],
|
|
79
|
+
transport: "stdio",
|
|
80
|
+
tools: ["query", "list_tables", "describe_table"],
|
|
81
|
+
homepage:
|
|
82
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
|
|
83
|
+
rating: 4.8,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "mcp-server-sqlite",
|
|
87
|
+
name: "sqlite",
|
|
88
|
+
displayName: "SQLite",
|
|
89
|
+
description:
|
|
90
|
+
"SQLite database — local, embedded query execution and schema management.",
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
author: "Anthropic",
|
|
93
|
+
category: "database",
|
|
94
|
+
tags: ["database", "sql", "sqlite", "local", "embedded"],
|
|
95
|
+
npmPackage: "@modelcontextprotocol/server-sqlite",
|
|
96
|
+
command: "npx",
|
|
97
|
+
args: ["-y", "@modelcontextprotocol/server-sqlite"],
|
|
98
|
+
transport: "stdio",
|
|
99
|
+
tools: [
|
|
100
|
+
"read_query",
|
|
101
|
+
"write_query",
|
|
102
|
+
"create_table",
|
|
103
|
+
"list_tables",
|
|
104
|
+
"describe_table",
|
|
105
|
+
"append_insight",
|
|
106
|
+
],
|
|
107
|
+
homepage:
|
|
108
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite",
|
|
109
|
+
rating: 4.7,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "mcp-server-git",
|
|
113
|
+
name: "git",
|
|
114
|
+
displayName: "Git",
|
|
115
|
+
description:
|
|
116
|
+
"Git version control — status, diff, log, commit, branch management.",
|
|
117
|
+
version: "1.0.0",
|
|
118
|
+
author: "Anthropic",
|
|
119
|
+
category: "version-control",
|
|
120
|
+
tags: ["git", "version-control", "diff", "commit", "branch"],
|
|
121
|
+
npmPackage: "@modelcontextprotocol/server-git",
|
|
122
|
+
command: "npx",
|
|
123
|
+
args: ["-y", "@modelcontextprotocol/server-git"],
|
|
124
|
+
transport: "stdio",
|
|
125
|
+
tools: [
|
|
126
|
+
"git_status",
|
|
127
|
+
"git_diff",
|
|
128
|
+
"git_log",
|
|
129
|
+
"git_commit",
|
|
130
|
+
"git_branch_list",
|
|
131
|
+
"git_checkout",
|
|
132
|
+
],
|
|
133
|
+
homepage:
|
|
134
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/git",
|
|
135
|
+
rating: 4.9,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "mcp-server-brave-search",
|
|
139
|
+
name: "brave-search",
|
|
140
|
+
displayName: "Brave Search",
|
|
141
|
+
description:
|
|
142
|
+
"Web search via Brave Search API — general web + local business queries.",
|
|
143
|
+
version: "1.0.0",
|
|
144
|
+
author: "Anthropic",
|
|
145
|
+
category: "search",
|
|
146
|
+
tags: ["search", "web", "brave", "internet", "query"],
|
|
147
|
+
npmPackage: "@modelcontextprotocol/server-brave-search",
|
|
148
|
+
command: "npx",
|
|
149
|
+
args: ["-y", "@modelcontextprotocol/server-brave-search"],
|
|
150
|
+
transport: "stdio",
|
|
151
|
+
tools: ["brave_web_search", "brave_local_search"],
|
|
152
|
+
homepage:
|
|
153
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
|
|
154
|
+
rating: 4.6,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "mcp-server-puppeteer",
|
|
158
|
+
name: "puppeteer",
|
|
159
|
+
displayName: "Puppeteer",
|
|
160
|
+
description:
|
|
161
|
+
"Browser automation — navigate, screenshot, click, fill forms, evaluate JS.",
|
|
162
|
+
version: "1.0.0",
|
|
163
|
+
author: "Anthropic",
|
|
164
|
+
category: "automation",
|
|
165
|
+
tags: ["browser", "automation", "puppeteer", "scraping", "screenshot"],
|
|
166
|
+
npmPackage: "@modelcontextprotocol/server-puppeteer",
|
|
167
|
+
command: "npx",
|
|
168
|
+
args: ["-y", "@modelcontextprotocol/server-puppeteer"],
|
|
169
|
+
transport: "stdio",
|
|
170
|
+
tools: [
|
|
171
|
+
"puppeteer_navigate",
|
|
172
|
+
"puppeteer_screenshot",
|
|
173
|
+
"puppeteer_click",
|
|
174
|
+
"puppeteer_fill",
|
|
175
|
+
"puppeteer_evaluate",
|
|
176
|
+
],
|
|
177
|
+
homepage:
|
|
178
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer",
|
|
179
|
+
rating: 4.5,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "mcp-server-slack",
|
|
183
|
+
name: "slack",
|
|
184
|
+
displayName: "Slack",
|
|
185
|
+
description:
|
|
186
|
+
"Slack workspace — read messages, post updates, manage channels and threads.",
|
|
187
|
+
version: "1.0.0",
|
|
188
|
+
author: "Anthropic",
|
|
189
|
+
category: "communication",
|
|
190
|
+
tags: ["slack", "messaging", "chat", "communication", "team"],
|
|
191
|
+
npmPackage: "@modelcontextprotocol/server-slack",
|
|
192
|
+
command: "npx",
|
|
193
|
+
args: ["-y", "@modelcontextprotocol/server-slack"],
|
|
194
|
+
transport: "stdio",
|
|
195
|
+
tools: [
|
|
196
|
+
"slack_list_channels",
|
|
197
|
+
"slack_post_message",
|
|
198
|
+
"slack_reply_to_thread",
|
|
199
|
+
"slack_get_channel_history",
|
|
200
|
+
"slack_search_messages",
|
|
201
|
+
],
|
|
202
|
+
homepage:
|
|
203
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/slack",
|
|
204
|
+
rating: 4.4,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "mcp-server-github",
|
|
208
|
+
name: "github",
|
|
209
|
+
displayName: "GitHub",
|
|
210
|
+
description:
|
|
211
|
+
"GitHub API — manage repositories, issues, pull requests, and actions.",
|
|
212
|
+
version: "1.0.0",
|
|
213
|
+
author: "Anthropic",
|
|
214
|
+
category: "version-control",
|
|
215
|
+
tags: ["github", "repository", "issues", "pull-request", "api"],
|
|
216
|
+
npmPackage: "@modelcontextprotocol/server-github",
|
|
217
|
+
command: "npx",
|
|
218
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
219
|
+
transport: "stdio",
|
|
220
|
+
tools: [
|
|
221
|
+
"create_or_update_file",
|
|
222
|
+
"search_repositories",
|
|
223
|
+
"create_repository",
|
|
224
|
+
"get_file_contents",
|
|
225
|
+
"push_files",
|
|
226
|
+
"create_issue",
|
|
227
|
+
"create_pull_request",
|
|
228
|
+
"list_issues",
|
|
229
|
+
"list_commits",
|
|
230
|
+
],
|
|
231
|
+
homepage:
|
|
232
|
+
"https://github.com/modelcontextprotocol/servers/tree/main/src/github",
|
|
233
|
+
rating: 4.8,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
/* ── public API ─────────────────────────────────────────────── */
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Return the catalog filtered, sorted, and paginated.
|
|
241
|
+
*
|
|
242
|
+
* @param {object} [filters]
|
|
243
|
+
* @param {string} [filters.category]
|
|
244
|
+
* @param {string[]} [filters.tags] — any-match
|
|
245
|
+
* @param {string} [filters.author] — substring, case-insensitive
|
|
246
|
+
* @param {"name"|"rating"|"category"} [filters.sortBy="name"]
|
|
247
|
+
* @param {"asc"|"desc"} [filters.sortOrder="asc"]
|
|
248
|
+
* @param {number} [filters.limit]
|
|
249
|
+
* @param {number} [filters.offset]
|
|
250
|
+
* @returns {{ servers: object[], total: number }}
|
|
251
|
+
*/
|
|
252
|
+
export function listServers(filters = {}) {
|
|
253
|
+
let servers = CATALOG.slice();
|
|
254
|
+
|
|
255
|
+
if (filters.category) {
|
|
256
|
+
const cat = String(filters.category).toLowerCase();
|
|
257
|
+
servers = servers.filter((s) => s.category === cat);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (Array.isArray(filters.tags) && filters.tags.length > 0) {
|
|
261
|
+
const wanted = new Set(filters.tags.map((t) => String(t).toLowerCase()));
|
|
262
|
+
servers = servers.filter(
|
|
263
|
+
(s) => s.tags && s.tags.some((tag) => wanted.has(tag.toLowerCase())),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (filters.author) {
|
|
268
|
+
const a = String(filters.author).toLowerCase();
|
|
269
|
+
servers = servers.filter(
|
|
270
|
+
(s) => s.author && s.author.toLowerCase().includes(a),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const total = servers.length;
|
|
275
|
+
|
|
276
|
+
const sortBy = filters.sortBy || "name";
|
|
277
|
+
const sortDir = filters.sortOrder === "desc" ? -1 : 1;
|
|
278
|
+
servers = servers.slice().sort((a, b) => {
|
|
279
|
+
const av = a[sortBy];
|
|
280
|
+
const bv = b[sortBy];
|
|
281
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
282
|
+
return (av - bv) * sortDir;
|
|
283
|
+
}
|
|
284
|
+
return String(av ?? "").localeCompare(String(bv ?? "")) * sortDir;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const offset =
|
|
288
|
+
Number.isInteger(filters.offset) && filters.offset > 0 ? filters.offset : 0;
|
|
289
|
+
const limit =
|
|
290
|
+
Number.isInteger(filters.limit) && filters.limit > 0
|
|
291
|
+
? filters.limit
|
|
292
|
+
: servers.length;
|
|
293
|
+
servers = servers.slice(offset, offset + limit);
|
|
294
|
+
|
|
295
|
+
return { servers, total };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Keyword search across `name`, `displayName`, `description`, and `tags`.
|
|
300
|
+
* Returns matches ranked by a simple weighted score — name/id are
|
|
301
|
+
* weighted highest, then tags, then description.
|
|
302
|
+
*/
|
|
303
|
+
export function searchServers(keyword) {
|
|
304
|
+
if (typeof keyword !== "string" || !keyword.trim()) return [];
|
|
305
|
+
const q = keyword.trim().toLowerCase();
|
|
306
|
+
|
|
307
|
+
const scored = [];
|
|
308
|
+
for (const entry of CATALOG) {
|
|
309
|
+
let score = 0;
|
|
310
|
+
if (entry.name && entry.name.toLowerCase() === q) score += 100;
|
|
311
|
+
else if (entry.name && entry.name.toLowerCase().includes(q)) score += 40;
|
|
312
|
+
if (entry.id.toLowerCase().includes(q)) score += 20;
|
|
313
|
+
if (entry.displayName && entry.displayName.toLowerCase().includes(q)) {
|
|
314
|
+
score += 30;
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(entry.tags)) {
|
|
317
|
+
for (const tag of entry.tags) {
|
|
318
|
+
if (tag.toLowerCase() === q) score += 25;
|
|
319
|
+
else if (tag.toLowerCase().includes(q)) score += 10;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (entry.description && entry.description.toLowerCase().includes(q)) {
|
|
323
|
+
score += 15;
|
|
324
|
+
}
|
|
325
|
+
if (entry.category && entry.category.toLowerCase().includes(q)) {
|
|
326
|
+
score += 10;
|
|
327
|
+
}
|
|
328
|
+
if (score > 0) scored.push({ entry, score });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
scored.sort((a, b) => b.score - a.score);
|
|
332
|
+
return scored.map((s) => s.entry);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Look up a catalog entry by `id` or by `name`. Returns `null` if
|
|
337
|
+
* neither matches. Matching is case-insensitive.
|
|
338
|
+
*/
|
|
339
|
+
export function getServer(idOrName) {
|
|
340
|
+
if (typeof idOrName !== "string" || !idOrName.trim()) return null;
|
|
341
|
+
const needle = idOrName.trim().toLowerCase();
|
|
342
|
+
return (
|
|
343
|
+
CATALOG.find(
|
|
344
|
+
(s) => s.id.toLowerCase() === needle || s.name.toLowerCase() === needle,
|
|
345
|
+
) || null
|
|
346
|
+
);
|
|
347
|
+
}
|