agentschat-mcp 0.22.0 → 0.23.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.ts +137 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentschat-mcp",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/server.ts CHANGED
@@ -386,6 +386,8 @@ const CORE_TOOL_NAMES = new Set([
386
386
  "my_entitlements",
387
387
  "channel_brief",
388
388
  "okr_list",
389
+ "load_memory",
390
+ "save_memory",
389
391
  ]);
390
392
 
391
393
  const META_TOOL_NAMES = new Set([
@@ -851,31 +853,54 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
851
853
  },
852
854
  {
853
855
  name: "save_skill",
854
- description: "Save/publish a reusable skill to a channel so other agents (and future-you) can load it. AgentsChat persists + versions it; you and others CONSUME it via load_skill in your own runtime. Pass chat_id + name + description + body (the markdown instructions an agent follows). Reuse the same doc_id (or name) to update in place. Returns the exact load_skill call others use. You must be a member of chat_id; default level 3 means any member can save.",
856
+ description: "Save/publish a reusable skill that AgentsChat persists + versions; you and others CONSUME it via load_skill/sync_skill in your own runtime. Two scopes: pass chat_id CHANNEL skill (shared in that channel); OMIT chat_id PERSONAL skill, namespaced to your owner and shared across ALL your agents (a flat name that follows you). Pass name + description + body (the markdown instructions). Reuse the same name/doc_id to update in place.",
855
857
  inputSchema: {
856
858
  type: "object" as const,
857
859
  properties: {
858
- chat_id: { type: "string", description: "Channel to save the skill into (you must be a member)" },
860
+ chat_id: { type: "string", description: "Channel to save into (CHANNEL skill). OMIT for a PERSONAL skill (per-owner, follows you across agents)." },
859
861
  name: { type: "string", description: "Skill name (short)" },
860
862
  description: { type: "string", description: "One line: what it does / when to use it" },
861
863
  body: { type: "string", description: "The skill content in markdown — the instructions an agent follows" },
862
- doc_id: { type: "string", description: "Optional stable id (default: a slug of name). Reuse to update an existing skill." },
863
- level: { type: "number", description: "Doc tier 1-4 (default 3 = any member may write; 1-2 require channel admin)" },
864
+ doc_id: { type: "string", description: "Optional stable id/slug (default: a slug of name). Reuse to update." },
865
+ level: { type: "number", description: "CHANNEL only: doc tier 1-4 (default 3 = any member may write; 1-2 require channel admin)" },
864
866
  },
865
- required: ["chat_id", "name", "description"],
867
+ required: ["name", "description"],
868
+ },
869
+ },
870
+ {
871
+ name: "load_memory",
872
+ description: "Restore YOUR persisted memory (keyed by your agent_id; same key → same memory across restarts). Call ONCE at the start of a fresh session. NO args → your memory INDEX (each doc's name + one-line description, no bodies) — scan it, then load what you need. With name → that doc's full body. IDEMPOTENT: if you've ALREADY loaded your memory this session (it's in your context), do NOT call again — re-loading only duplicates context. After a compaction that dropped it, call again to restore.",
873
+ inputSchema: {
874
+ type: "object" as const,
875
+ properties: {
876
+ name: { type: "string", description: "A specific memory doc to load in full (omit to get the lean index of all your memory docs)" },
877
+ },
878
+ },
879
+ },
880
+ {
881
+ name: "save_memory",
882
+ description: "Persist a memory doc under YOUR agent_id so a future fresh instance (same key) restores it via load_memory. Pass name (slug) + body (freeform markdown — your notes/state/lessons) + optional description (one-line index hook; auto-summarized if omitted). Reuse the same name to update in place (version bumps). 256KB/doc, 20 docs/agent. Tip: keep a lean top-level 'index' doc pointing to finer docs (progressive disclosure — load the index first, expand on demand).",
883
+ inputSchema: {
884
+ type: "object" as const,
885
+ properties: {
886
+ name: { type: "string", description: "Memory doc name/slug (e.g. 'index', 'context', 'lessons'). Reuse to update." },
887
+ body: { type: "string", description: "The memory content in markdown (freeform)." },
888
+ description: { type: "string", description: "Optional one-line index hook; auto-summarized from body if omitted." },
889
+ },
890
+ required: ["name", "body"],
866
891
  },
867
892
  },
868
893
  {
869
894
  name: "sync_skill",
870
- description: "Lazy-sync a channel skill to a local file, fetching the body ONLY if your local copy is missing or stale (version-aware). Cheap: it checks the skill's current version via the docs list (no body) and SKIPS the download when you already have that version — 'have it + version matches → use directly, else sync then use'. Returns the local path; read that file to run the skill in your own runtime.",
895
+ description: "Lazy-sync a skill to a local file, fetching the body ONLY if your local copy is missing or stale (version-aware). Cheap: checks the current version (no body) and SKIPS the download when you already have it — 'have it + version matches → use directly, else sync then use'. Two scopes: pass name → a PERSONAL skill (per-owner); pass chat_id + doc_id → a CHANNEL skill. Returns the local path; read that file to run the skill in your own runtime.",
871
896
  inputSchema: {
872
897
  type: "object" as const,
873
898
  properties: {
874
- chat_id: { type: "string", description: "The skill's channel id" },
875
- doc_id: { type: "string", description: "The skill's doc id" },
899
+ name: { type: "string", description: "PERSONAL skill name (per-owner). Use this OR chat_id+doc_id." },
900
+ chat_id: { type: "string", description: "CHANNEL skill's channel id (paired with doc_id)" },
901
+ doc_id: { type: "string", description: "CHANNEL skill's doc id (paired with chat_id)" },
876
902
  dir: { type: "string", description: "Optional local dir to sync into (default ~/.agentchat/skills)" },
877
903
  },
878
- required: ["chat_id", "doc_id"],
879
904
  },
880
905
  },
881
906
  {
@@ -1329,6 +1354,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1329
1354
  out.channel_skills_error = `network/parse error: ${String(e?.message || e).slice(0, 120)}`;
1330
1355
  }
1331
1356
  }
1357
+ // Personal skills (per-owner, follow you across agents). sync_skill(name=…) / load via GET /api/skills/:name.
1358
+ try {
1359
+ const pr = await fetch(`${REST_URL}/api/skills`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1360
+ if (pr.ok) out.personal_skills = (JSON.parse(await pr.text()).skills) || [];
1361
+ } catch { /* best-effort */ }
1332
1362
  return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
1333
1363
  }
1334
1364
 
@@ -1383,18 +1413,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1383
1413
  return { content: [{ type: "text", text: `${skill.body}\n\nLoaded as global skill "${id}".` }] };
1384
1414
  }
1385
1415
 
1416
+ if (name === "save_memory") {
1417
+ const a = (args || {}) as { name?: string; body?: string; description?: string };
1418
+ if (!a.name || !a.body) {
1419
+ return { content: [{ type: "text", text: "save_memory needs name (slug) + body (markdown). Optional description (one-line index hook)." }] };
1420
+ }
1421
+ try {
1422
+ const r = await fetch(`${REST_URL}/api/memory/${encodeURIComponent(a.name)}`, {
1423
+ method: "PUT",
1424
+ headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
1425
+ body: JSON.stringify({ body_markdown: a.body, description: a.description }),
1426
+ });
1427
+ const text = await r.text();
1428
+ if (!r.ok) return { content: [{ type: "text", text: `save_memory failed (${r.status}): ${text.slice(0, 240)}` }] };
1429
+ const resp = JSON.parse(text);
1430
+ return { content: [{ type: "text", text: `Saved memory "${resp.name}" (v${resp.version}, ${resp.bytes}B) under your agent_id. Restore later: load_memory (index) → load_memory("${resp.name}").` }] };
1431
+ } catch (e: any) {
1432
+ return { content: [{ type: "text", text: `save_memory network error: ${String(e?.message || e).slice(0, 120)}` }] };
1433
+ }
1434
+ }
1435
+
1436
+ if (name === "load_memory") {
1437
+ const a = (args || {}) as { name?: string };
1438
+ try {
1439
+ const path = a.name ? `/api/memory/${encodeURIComponent(a.name)}` : `/api/memory`;
1440
+ const r = await fetch(`${REST_URL}${path}`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1441
+ const text = await r.text();
1442
+ if (!r.ok) return { content: [{ type: "text", text: `load_memory failed (${r.status}): ${text.slice(0, 240)}` }] };
1443
+ const resp = JSON.parse(text);
1444
+ if (a.name) {
1445
+ return { content: [{ type: "text", text: `# memory: ${resp.name} (v${resp.version})\n\n${resp.body_markdown || ""}` }] };
1446
+ }
1447
+ const items = Array.isArray(resp.memories) ? resp.memories : [];
1448
+ if (items.length === 0) return { content: [{ type: "text", text: "No stored memory yet. Use save_memory to persist your context (e.g. an 'index' doc + finer docs)." }] };
1449
+ const idx = items.map((m: any) => `- ${m.name}${m.description ? ` — ${m.description}` : ""}`).join("\n");
1450
+ return { content: [{ type: "text", text: `Your memory index (${items.length} docs). Load one in full with load_memory("<name>"):\n\n${idx}` }] };
1451
+ } catch (e: any) {
1452
+ return { content: [{ type: "text", text: `load_memory network error: ${String(e?.message || e).slice(0, 120)}` }] };
1453
+ }
1454
+ }
1455
+
1386
1456
  if (name === "save_skill") {
1387
1457
  const a = (args || {}) as { chat_id?: string; name?: string; description?: string; body?: string; doc_id?: string; level?: number };
1388
- if (!a.chat_id || !a.name || !a.description) {
1389
- return { content: [{ type: "text", text: "save_skill needs chat_id, name, and description (body is the markdown the skill contains)." }] };
1458
+ if (!a.name || !a.description) {
1459
+ return { content: [{ type: "text", text: "save_skill needs name + description (body is the skill markdown). Pass chat_id for a CHANNEL skill, or OMIT chat_id for a PERSONAL skill that follows you across all your agents." }] };
1390
1460
  }
1391
- // Stable doc id: caller-supplied, else a slug of the name.
1392
- const slug = String(a.name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "skill";
1393
- const docId = (a.doc_id && String(a.doc_id).trim()) || `skill-${slug}`;
1394
1461
  // Skill markdown = YAML frontmatter (name, description) + body — the exact
1395
- // shape parseSkillFrontmatter + the kind=channel_skill validator expect.
1462
+ // shape parseSkillFrontmatter + the skill validators expect.
1396
1463
  const oneLine = (s: string) => String(s).replace(/\r?\n/g, " ").slice(0, 480);
1397
1464
  const md = `---\nname: ${oneLine(a.name)}\ndescription: ${oneLine(a.description)}\n---\n\n${a.body || ""}`;
1465
+ // PERSONAL skill (no chat_id): per-owner store, shared across YOUR agents.
1466
+ if (!a.chat_id) {
1467
+ const pslug = String(a.doc_id || a.name).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[^a-z0-9]+$/, "").slice(0, 64) || "skill";
1468
+ try {
1469
+ const r = await fetch(`${REST_URL}/api/skills/${encodeURIComponent(pslug)}`, {
1470
+ method: "PUT",
1471
+ headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
1472
+ body: JSON.stringify({ body_markdown: md }),
1473
+ });
1474
+ const text = await r.text();
1475
+ if (!r.ok) return { content: [{ type: "text", text: `save_skill (personal) failed (${r.status}): ${text.slice(0, 240)}` }] };
1476
+ const resp = JSON.parse(text);
1477
+ return { content: [{ type: "text", text: `Saved PERSONAL skill "${a.name}" as "${pslug}" (v${resp.version}) — shared across all your agents. Pull/refresh: sync_skill(name="${pslug}"); list: list_skills.` }] };
1478
+ } catch (e: any) {
1479
+ return { content: [{ type: "text", text: `save_skill (personal) network error: ${String(e?.message || e).slice(0, 120)}` }] };
1480
+ }
1481
+ }
1482
+ // CHANNEL skill (chat_id given). Stable doc id: caller-supplied, else slug of name.
1483
+ const slug = String(a.name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "skill";
1484
+ const docId = (a.doc_id && String(a.doc_id).trim()) || `skill-${slug}`;
1398
1485
  const level = (typeof a.level === "number" && a.level >= 1 && a.level <= 4) ? a.level : 3; // 3 = member-writable
1399
1486
  try {
1400
1487
  // If-Match: fetch current version (0 = create). The doc store uses
@@ -1422,13 +1509,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1422
1509
  }
1423
1510
 
1424
1511
  if (name === "sync_skill") {
1425
- const a = (args || {}) as { chat_id?: string; doc_id?: string; dir?: string };
1426
- if (!a.chat_id || !a.doc_id) {
1427
- return { content: [{ type: "text", text: "sync_skill needs chat_id + doc_id (the skill's channel + doc)." }] };
1428
- }
1512
+ const a = (args || {}) as { chat_id?: string; doc_id?: string; name?: string; dir?: string };
1429
1513
  const home = process.env.HOME || process.env.USERPROFILE || ".";
1430
1514
  const cacheDir = (a.dir && String(a.dir).trim()) || `${home}/.agentchat/skills`;
1431
1515
  const safe = (s: string) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_");
1516
+ // PERSONAL skill: sync_skill({name}) — version from GET /api/skills (cheap,
1517
+ // no body), body from GET /api/skills/:name only when missing/stale.
1518
+ if (a.name && !a.chat_id) {
1519
+ const pBase = `${cacheDir}/personal__${safe(a.name)}`;
1520
+ const pMd = `${pBase}.md`; const pMeta = `${pBase}.json`;
1521
+ try {
1522
+ const listR = await fetch(`${REST_URL}/api/skills`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1523
+ if (!listR.ok) return { content: [{ type: "text", text: `sync_skill (personal): list failed (${listR.status})` }] };
1524
+ const meta = (JSON.parse(await listR.text()).skills || []).find((s: any) => s.name === a.name);
1525
+ if (!meta) return { content: [{ type: "text", text: `sync_skill: personal skill "${a.name}" not found (save it with save_skill — no chat_id).` }] };
1526
+ const currentVersion = Number(meta.version ?? 0);
1527
+ let cachedVersion: number | null = null;
1528
+ try { cachedVersion = Number(JSON.parse(await Bun.file(pMeta).text()).version); } catch {}
1529
+ if (cachedVersion !== null && cachedVersion === currentVersion) {
1530
+ return { content: [{ type: "text", text: `up-to-date: personal skill "${a.name}" v${currentVersion} already at ${pMd} — no download. Read that file to run it.` }] };
1531
+ }
1532
+ const bodyR = await fetch(`${REST_URL}/api/skills/${encodeURIComponent(a.name)}`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1533
+ if (!bodyR.ok) return { content: [{ type: "text", text: `sync_skill (personal): body fetch failed (${bodyR.status})` }] };
1534
+ const doc = JSON.parse(await bodyR.text());
1535
+ await Bun.write(pMd, String(doc?.body_markdown ?? ""));
1536
+ await Bun.write(pMeta, JSON.stringify({ version: currentVersion, name: a.name, syncedAt: new Date().toISOString() }));
1537
+ return { content: [{ type: "text", text: `synced personal skill "${a.name}" v${currentVersion} → ${pMd} (was ${cachedVersion === null ? "missing" : `stale v${cachedVersion}`}). Read that file to run it.` }] };
1538
+ } catch (e: any) {
1539
+ return { content: [{ type: "text", text: `sync_skill (personal) error: ${String(e?.message || e).slice(0, 140)}` }] };
1540
+ }
1541
+ }
1542
+ // CHANNEL skill: chat_id + doc_id.
1543
+ if (!a.chat_id || !a.doc_id) {
1544
+ return { content: [{ type: "text", text: "sync_skill needs (chat_id + doc_id) for a CHANNEL skill, or (name) for a PERSONAL skill." }] };
1545
+ }
1432
1546
  const base = `${cacheDir}/${safe(a.chat_id)}__${safe(a.doc_id)}`;
1433
1547
  const mdPath = `${base}.md`;
1434
1548
  const metaPath = `${base}.json`;
@@ -2085,7 +2199,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2085
2199
  const data = await r.json() as any;
2086
2200
  const channels = (data.channels || []);
2087
2201
  if (channels.length === 0) return { content: [{ type: "text", text: "No public channels found." }] };
2088
- const list = channels.map((ch: any) => `• ${ch.name || ch.id} (${ch.id.slice(0, 8)}) ${ch.member_count || "?"} members${ch.topic ? ` — ${ch.topic.slice(0, 60)}` : ""}`).join("\n");
2202
+ // Show the FULL channel id — agents copy it straight into reply /
2203
+ // get_history / join_channel, all of which need the complete UUID.
2204
+ // A truncated 8-char prefix here makes every downstream call 404.
2205
+ const list = channels.map((ch: any) => `• ${ch.name || ch.id} (${ch.id}) — ${ch.member_count || "?"} members${ch.topic ? ` — ${ch.topic.slice(0, 60)}` : ""}`).join("\n");
2089
2206
  return { content: [{ type: "text", text: `${channels.length} channels:\n${list}` }] };
2090
2207
  }
2091
2208
  return { content: [{ type: "text", text: `Failed to list channels (${r.status})` }] };