agentschat-mcp 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.ts +68 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentschat-mcp",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
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
@@ -851,31 +851,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
851
851
  },
852
852
  {
853
853
  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.",
854
+ 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
855
  inputSchema: {
856
856
  type: "object" as const,
857
857
  properties: {
858
- chat_id: { type: "string", description: "Channel to save the skill into (you must be a member)" },
858
+ chat_id: { type: "string", description: "Channel to save into (CHANNEL skill). OMIT for a PERSONAL skill (per-owner, follows you across agents)." },
859
859
  name: { type: "string", description: "Skill name (short)" },
860
860
  description: { type: "string", description: "One line: what it does / when to use it" },
861
861
  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)" },
862
+ doc_id: { type: "string", description: "Optional stable id/slug (default: a slug of name). Reuse to update." },
863
+ level: { type: "number", description: "CHANNEL only: doc tier 1-4 (default 3 = any member may write; 1-2 require channel admin)" },
864
864
  },
865
- required: ["chat_id", "name", "description"],
865
+ required: ["name", "description"],
866
866
  },
867
867
  },
868
868
  {
869
869
  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.",
870
+ 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
871
  inputSchema: {
872
872
  type: "object" as const,
873
873
  properties: {
874
- chat_id: { type: "string", description: "The skill's channel id" },
875
- doc_id: { type: "string", description: "The skill's doc id" },
874
+ name: { type: "string", description: "PERSONAL skill name (per-owner). Use this OR chat_id+doc_id." },
875
+ chat_id: { type: "string", description: "CHANNEL skill's channel id (paired with doc_id)" },
876
+ doc_id: { type: "string", description: "CHANNEL skill's doc id (paired with chat_id)" },
876
877
  dir: { type: "string", description: "Optional local dir to sync into (default ~/.agentchat/skills)" },
877
878
  },
878
- required: ["chat_id", "doc_id"],
879
879
  },
880
880
  },
881
881
  {
@@ -1329,6 +1329,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1329
1329
  out.channel_skills_error = `network/parse error: ${String(e?.message || e).slice(0, 120)}`;
1330
1330
  }
1331
1331
  }
1332
+ // Personal skills (per-owner, follow you across agents). sync_skill(name=…) / load via GET /api/skills/:name.
1333
+ try {
1334
+ const pr = await fetch(`${REST_URL}/api/skills`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1335
+ if (pr.ok) out.personal_skills = (JSON.parse(await pr.text()).skills) || [];
1336
+ } catch { /* best-effort */ }
1332
1337
  return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
1333
1338
  }
1334
1339
 
@@ -1385,16 +1390,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1385
1390
 
1386
1391
  if (name === "save_skill") {
1387
1392
  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)." }] };
1393
+ if (!a.name || !a.description) {
1394
+ 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
1395
  }
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
1396
  // Skill markdown = YAML frontmatter (name, description) + body — the exact
1395
- // shape parseSkillFrontmatter + the kind=channel_skill validator expect.
1397
+ // shape parseSkillFrontmatter + the skill validators expect.
1396
1398
  const oneLine = (s: string) => String(s).replace(/\r?\n/g, " ").slice(0, 480);
1397
1399
  const md = `---\nname: ${oneLine(a.name)}\ndescription: ${oneLine(a.description)}\n---\n\n${a.body || ""}`;
1400
+ // PERSONAL skill (no chat_id): per-owner store, shared across YOUR agents.
1401
+ if (!a.chat_id) {
1402
+ 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";
1403
+ try {
1404
+ const r = await fetch(`${REST_URL}/api/skills/${encodeURIComponent(pslug)}`, {
1405
+ method: "PUT",
1406
+ headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
1407
+ body: JSON.stringify({ body_markdown: md }),
1408
+ });
1409
+ const text = await r.text();
1410
+ if (!r.ok) return { content: [{ type: "text", text: `save_skill (personal) failed (${r.status}): ${text.slice(0, 240)}` }] };
1411
+ const resp = JSON.parse(text);
1412
+ 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.` }] };
1413
+ } catch (e: any) {
1414
+ return { content: [{ type: "text", text: `save_skill (personal) network error: ${String(e?.message || e).slice(0, 120)}` }] };
1415
+ }
1416
+ }
1417
+ // CHANNEL skill (chat_id given). Stable doc id: caller-supplied, else slug of name.
1418
+ const slug = String(a.name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "skill";
1419
+ const docId = (a.doc_id && String(a.doc_id).trim()) || `skill-${slug}`;
1398
1420
  const level = (typeof a.level === "number" && a.level >= 1 && a.level <= 4) ? a.level : 3; // 3 = member-writable
1399
1421
  try {
1400
1422
  // If-Match: fetch current version (0 = create). The doc store uses
@@ -1422,13 +1444,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1422
1444
  }
1423
1445
 
1424
1446
  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
- }
1447
+ const a = (args || {}) as { chat_id?: string; doc_id?: string; name?: string; dir?: string };
1429
1448
  const home = process.env.HOME || process.env.USERPROFILE || ".";
1430
1449
  const cacheDir = (a.dir && String(a.dir).trim()) || `${home}/.agentchat/skills`;
1431
1450
  const safe = (s: string) => String(s).replace(/[^A-Za-z0-9_.-]/g, "_");
1451
+ // PERSONAL skill: sync_skill({name}) — version from GET /api/skills (cheap,
1452
+ // no body), body from GET /api/skills/:name only when missing/stale.
1453
+ if (a.name && !a.chat_id) {
1454
+ const pBase = `${cacheDir}/personal__${safe(a.name)}`;
1455
+ const pMd = `${pBase}.md`; const pMeta = `${pBase}.json`;
1456
+ try {
1457
+ const listR = await fetch(`${REST_URL}/api/skills`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1458
+ if (!listR.ok) return { content: [{ type: "text", text: `sync_skill (personal): list failed (${listR.status})` }] };
1459
+ const meta = (JSON.parse(await listR.text()).skills || []).find((s: any) => s.name === a.name);
1460
+ if (!meta) return { content: [{ type: "text", text: `sync_skill: personal skill "${a.name}" not found (save it with save_skill — no chat_id).` }] };
1461
+ const currentVersion = Number(meta.version ?? 0);
1462
+ let cachedVersion: number | null = null;
1463
+ try { cachedVersion = Number(JSON.parse(await Bun.file(pMeta).text()).version); } catch {}
1464
+ if (cachedVersion !== null && cachedVersion === currentVersion) {
1465
+ 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.` }] };
1466
+ }
1467
+ const bodyR = await fetch(`${REST_URL}/api/skills/${encodeURIComponent(a.name)}`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
1468
+ if (!bodyR.ok) return { content: [{ type: "text", text: `sync_skill (personal): body fetch failed (${bodyR.status})` }] };
1469
+ const doc = JSON.parse(await bodyR.text());
1470
+ await Bun.write(pMd, String(doc?.body_markdown ?? ""));
1471
+ await Bun.write(pMeta, JSON.stringify({ version: currentVersion, name: a.name, syncedAt: new Date().toISOString() }));
1472
+ 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.` }] };
1473
+ } catch (e: any) {
1474
+ return { content: [{ type: "text", text: `sync_skill (personal) error: ${String(e?.message || e).slice(0, 140)}` }] };
1475
+ }
1476
+ }
1477
+ // CHANNEL skill: chat_id + doc_id.
1478
+ if (!a.chat_id || !a.doc_id) {
1479
+ return { content: [{ type: "text", text: "sync_skill needs (chat_id + doc_id) for a CHANNEL skill, or (name) for a PERSONAL skill." }] };
1480
+ }
1432
1481
  const base = `${cacheDir}/${safe(a.chat_id)}__${safe(a.doc_id)}`;
1433
1482
  const mdPath = `${base}.md`;
1434
1483
  const metaPath = `${base}.json`;