agentschat-mcp 0.28.0 → 0.29.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.
- package/dist/server.js +4030 -0
- package/package.json +8 -4
- package/src/cli.mjs +20 -0
- package/src/server.ts +87 -25
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.1",
|
|
5
5
|
"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.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"agentschat-mcp": "src/
|
|
9
|
-
"agentchat-mcp": "src/
|
|
8
|
+
"agentschat-mcp": "src/cli.mjs",
|
|
9
|
+
"agentchat-mcp": "src/cli.mjs"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
12
|
"bun": ">=1.0.0"
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"dev": "bun --watch src/server.ts",
|
|
17
17
|
"test": "bun test",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
|
+
"build": "bun scripts/build.mjs",
|
|
19
20
|
"check:version": "bun scripts/check-version-sync.mjs",
|
|
20
|
-
"verify": "bun scripts/check-version-sync.mjs && tsc --noEmit && bun test"
|
|
21
|
+
"verify": "bun run build && bun scripts/check-version-sync.mjs && tsc --noEmit && bun test",
|
|
22
|
+
"prepublishOnly": "bun run build"
|
|
21
23
|
},
|
|
22
24
|
"keywords": [
|
|
23
25
|
"agentchat",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"typescript": "^5.9.3"
|
|
53
55
|
},
|
|
54
56
|
"files": [
|
|
57
|
+
"src/cli.mjs",
|
|
55
58
|
"src/server.ts",
|
|
56
59
|
"src/heartbeat.ts",
|
|
57
60
|
"src/redact.ts",
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
"src/reconnect.ts",
|
|
61
64
|
"src/timestamps.ts",
|
|
62
65
|
"src/argcheck.ts",
|
|
66
|
+
"dist/server.js",
|
|
63
67
|
"README.md"
|
|
64
68
|
]
|
|
65
69
|
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Universal entry point for the AgentsChat MCP server.
|
|
3
|
+
//
|
|
4
|
+
// One bin, two runtimes — bunx and npx both land here:
|
|
5
|
+
// • Bun (bunx agentschat-mcp / claude mcp add ... bunx ...): run the TypeScript
|
|
6
|
+
// source directly. No build step, full fidelity — this is the path Claude
|
|
7
|
+
// Code users take, and the runtime the plugin is designed for.
|
|
8
|
+
// • Node (npx agentschat-mcp, or registry/directory introspection like Glama
|
|
9
|
+
// that installs + runs over stdio): run the prebuilt Node bundle in dist/.
|
|
10
|
+
// Bun.* file I/O was replaced with node:fs so the exact same logic runs on
|
|
11
|
+
// both runtimes; the bundle inlines local modules + the JSON version import.
|
|
12
|
+
//
|
|
13
|
+
// bunx ignores this shebang and runs the file under Bun (so `typeof Bun` is
|
|
14
|
+
// defined); npx honors the shebang and runs it under Node. CLI args in argv are
|
|
15
|
+
// inherited by the imported entrypoint, so --name/--profile/etc. work unchanged.
|
|
16
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
17
|
+
await import("./server.ts");
|
|
18
|
+
} else {
|
|
19
|
+
await import("../dist/server.js");
|
|
20
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -4,16 +4,14 @@
|
|
|
4
4
|
* 像 weixin 插件一样:WebSocket 消息 → MCP channel notification → Claude Code 对话
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Runtime
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
7
|
+
// Runtime: Bun is the primary/first-class runtime (`bunx agentschat-mcp` runs
|
|
8
|
+
// this .ts source directly, no build step). It ALSO runs on plain Node via the
|
|
9
|
+
// prebuilt bundle in dist/ (see src/cli.mjs launcher) so directory/registry
|
|
10
|
+
// tooling that introspects over stdio with Node/npx (e.g. Glama) can install
|
|
11
|
+
// and list tools. All Bun-specific I/O was replaced with node:fs; the WebSocket
|
|
12
|
+
// connect is wrapped in try/catch, so a missing global WebSocket (older Node)
|
|
13
|
+
// degrades to a reconnect log rather than crashing stdio/tools-list. Node 22+
|
|
14
|
+
// (global WebSocket/fetch/FormData) gets full functionality.
|
|
17
15
|
|
|
18
16
|
// ── Proxy bypass ──────────────────────────────────────────────────
|
|
19
17
|
// MCP subprocess inherits the parent's HTTP_PROXY/HTTPS_PROXY which
|
|
@@ -566,7 +564,7 @@ const TOOL_GROUPS: ToolGroupMeta[] = [
|
|
|
566
564
|
summary: "Send images and voice/audio clips into channels (upload a local file or attach an already-hosted url).",
|
|
567
565
|
tags: ["chat", "media"],
|
|
568
566
|
estimated_tokens: 700,
|
|
569
|
-
tools: ["send_image", "send_voice", "set_voice", "list_voices"],
|
|
567
|
+
tools: ["send_image", "send_voice", "set_voice", "list_voices", "transcribe"],
|
|
570
568
|
},
|
|
571
569
|
];
|
|
572
570
|
|
|
@@ -680,6 +678,17 @@ const ALL_TOOL_DEFS = [
|
|
|
680
678
|
required: ["voice"],
|
|
681
679
|
},
|
|
682
680
|
},
|
|
681
|
+
{
|
|
682
|
+
name: "transcribe",
|
|
683
|
+
description: "Transcribe a voice/audio attachment to text via the server's speech-to-text, so you can \"hear\" a voice message. Pass the audio `url` from get_history (an /api/file/uploads/* proxy path). Returns the spoken text. (If get_history already shows a transcript for that clip, just read it — no need to call this.)",
|
|
684
|
+
inputSchema: {
|
|
685
|
+
type: "object" as const,
|
|
686
|
+
properties: {
|
|
687
|
+
url: { type: "string", description: "Audio attachment url from get_history (/api/file/uploads/<name>)" },
|
|
688
|
+
},
|
|
689
|
+
required: ["url"],
|
|
690
|
+
},
|
|
691
|
+
},
|
|
683
692
|
{
|
|
684
693
|
name: "send_typing",
|
|
685
694
|
description: "Send a typing indicator to an AgentsChat channel.",
|
|
@@ -1599,13 +1608,14 @@ function mimeFromPath(p: string): string {
|
|
|
1599
1608
|
}
|
|
1600
1609
|
// Upload a local file via multipart POST /api/upload → { url, mime, size }.
|
|
1601
1610
|
async function uploadLocalFile(path: string): Promise<{ url: string; mime: string; size: number }> {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
const
|
|
1611
|
+
// node:fs (not Bun.file) so this runs on plain Node too — Bun supports node:fs
|
|
1612
|
+
// identically, and the Node path is what registry introspection (Glama) uses.
|
|
1613
|
+
if (!existsSync(path)) throw new Error(`file not found: ${path}`);
|
|
1614
|
+
const mime = mimeFromPath(path);
|
|
1615
|
+
const buf = readFileSync(path);
|
|
1606
1616
|
const name = path.split("/").pop() || "upload";
|
|
1607
1617
|
const form = new FormData();
|
|
1608
|
-
form.append("file", new Blob([
|
|
1618
|
+
form.append("file", new Blob([new Uint8Array(buf)], { type: mime }), name);
|
|
1609
1619
|
// No Content-Type header → fetch derives the multipart boundary itself.
|
|
1610
1620
|
const r = await apiFetch(`${REST_URL}/api/upload`, { method: "POST", headers: { "Authorization": `Bearer ${TOKEN}` }, body: form });
|
|
1611
1621
|
const text = await r.text();
|
|
@@ -1613,7 +1623,7 @@ async function uploadLocalFile(path: string): Promise<{ url: string; mime: strin
|
|
|
1613
1623
|
let data: any;
|
|
1614
1624
|
try { data = JSON.parse(text); } catch { throw new Error(`upload returned non-JSON: ${text.slice(0, 120)}`); }
|
|
1615
1625
|
if (!data?.url) throw new Error(`upload response missing url: ${text.slice(0, 120)}`);
|
|
1616
|
-
return { url: data.url as string, mime: (data.type as string) || mime, size: (data.size as number) ??
|
|
1626
|
+
return { url: data.url as string, mime: (data.type as string) || mime, size: (data.size as number) ?? buf.byteLength };
|
|
1617
1627
|
}
|
|
1618
1628
|
// Post a message carrying exactly one media attachment (image|audio).
|
|
1619
1629
|
async function sendMediaMessage(kind: "image" | "audio", args: any): Promise<{ content: any[]; isError?: boolean }> {
|
|
@@ -1735,6 +1745,33 @@ HANDLERS.set("set_voice", async (args) => {
|
|
|
1735
1745
|
}
|
|
1736
1746
|
});
|
|
1737
1747
|
|
|
1748
|
+
// transcribe — POST /api/stt {audio_url} so an agent can "hear" a voice attachment
|
|
1749
|
+
// (the LLM can't consume audio; STT turns it into readable text). Closes the
|
|
1750
|
+
// "voice is perceivable to humans but not to agents" gap (plan B). Server already
|
|
1751
|
+
// returns attachment URLs in get_history; this is the transcribe entrypoint.
|
|
1752
|
+
HANDLERS.set("transcribe", async (args) => {
|
|
1753
|
+
const { url } = (args || {}) as { url?: string };
|
|
1754
|
+
if (!url) return { content: [{ type: "text", text: "Error: url required (an audio attachment url from get_history)" }], isError: true };
|
|
1755
|
+
try {
|
|
1756
|
+
const r = await apiFetch(`${REST_URL}/api/stt`, {
|
|
1757
|
+
method: "POST",
|
|
1758
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
|
|
1759
|
+
body: JSON.stringify({ audio_url: url }),
|
|
1760
|
+
});
|
|
1761
|
+
const t = await r.text();
|
|
1762
|
+
if (r.status === 429 && /MEDIA_BUDGET_EXCEEDED/i.test(t)) return { content: [{ type: "text", text: "Voice budget exhausted for today (MEDIA_BUDGET_EXCEEDED) — try again tomorrow." }], isError: true };
|
|
1763
|
+
if (r.status === 415 && /UNSUPPORTED_AUDIO_ENCODING/i.test(t)) return { content: [{ type: "text", text: "That audio format can't be transcribed (m4a/AAC aren't supported by the STT engine; wav/mp3/ogg/opus/webm are)." }], isError: true };
|
|
1764
|
+
if (!r.ok) return { content: [{ type: "text", text: `Transcription failed (${r.status}): ${t.slice(0, 140)}` }], isError: true };
|
|
1765
|
+
let d: any;
|
|
1766
|
+
try { d = JSON.parse(t); } catch { return { content: [{ type: "text", text: `STT returned non-JSON: ${t.slice(0, 120)}` }], isError: true }; }
|
|
1767
|
+
const transcript = d?.transcript;
|
|
1768
|
+
if (!transcript) return { content: [{ type: "text", text: "No speech detected in that audio." }] };
|
|
1769
|
+
return { content: [{ type: "text", text: `Transcript${d?.language ? ` (${d.language})` : ""}: ${transcript}` }] };
|
|
1770
|
+
} catch (e: any) {
|
|
1771
|
+
return { content: [{ type: "text", text: `Error transcribing: ${String(e?.message || e).slice(0, 120)}` }], isError: true };
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1738
1775
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1739
1776
|
let { name, arguments: args } = request.params;
|
|
1740
1777
|
let viaExtendedCompat = false;
|
|
@@ -1943,15 +1980,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1943
1980
|
if (!meta) return { content: [{ type: "text", text: `sync_skill: personal skill "${a.name}" not found (save it with save_skill — no chat_id).` }], isError: true };
|
|
1944
1981
|
const currentVersion = Number(meta.version ?? 0);
|
|
1945
1982
|
let cachedVersion: number | null = null;
|
|
1946
|
-
try { cachedVersion = Number(JSON.parse(
|
|
1983
|
+
try { cachedVersion = Number(JSON.parse(readFileSync(pMeta, "utf8")).version); } catch {}
|
|
1947
1984
|
if (cachedVersion !== null && cachedVersion === currentVersion) {
|
|
1948
1985
|
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.` }] };
|
|
1949
1986
|
}
|
|
1950
1987
|
const bodyR = await apiFetch(`${REST_URL}/api/skills/${encodeURIComponent(a.name)}`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
|
|
1951
1988
|
if (!bodyR.ok) return { content: [{ type: "text", text: `sync_skill (personal): body fetch failed (${bodyR.status})` }], isError: true };
|
|
1952
1989
|
const doc = JSON.parse(await bodyR.text());
|
|
1953
|
-
|
|
1954
|
-
|
|
1990
|
+
// node:fs (not Bun.write) — runs on plain Node too. Bun.write auto-creates
|
|
1991
|
+
// parent dirs; writeFileSync does not, so mkdir the cache dir first.
|
|
1992
|
+
mkdirSync(dirname(pMd), { recursive: true });
|
|
1993
|
+
writeFileSync(pMd, String(doc?.body_markdown ?? ""));
|
|
1994
|
+
writeFileSync(pMeta, JSON.stringify({ version: currentVersion, name: a.name, syncedAt: new Date().toISOString() }));
|
|
1955
1995
|
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.` }] };
|
|
1956
1996
|
} catch (e: any) {
|
|
1957
1997
|
return { content: [{ type: "text", text: `sync_skill (personal) error: ${String(e?.message || e).slice(0, 140)}` }], isError: true };
|
|
@@ -1975,7 +2015,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1975
2015
|
const currentVersion = Number(meta.version ?? 0);
|
|
1976
2016
|
// 2. Local cache check — skip the body fetch if we already have this version.
|
|
1977
2017
|
let cachedVersion: number | null = null;
|
|
1978
|
-
try { cachedVersion = Number(JSON.parse(
|
|
2018
|
+
try { cachedVersion = Number(JSON.parse(readFileSync(metaPath, "utf8")).version); } catch {}
|
|
1979
2019
|
if (cachedVersion !== null && cachedVersion === currentVersion) {
|
|
1980
2020
|
return { content: [{ type: "text", text: `up-to-date: "${a.doc_id}" v${currentVersion} already at ${mdPath} — no download. Read that file to run it.` }] };
|
|
1981
2021
|
}
|
|
@@ -1984,9 +2024,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1984
2024
|
if (!docR.ok) return { content: [{ type: "text", text: `sync_skill: fetch body failed (${docR.status})` }], isError: true };
|
|
1985
2025
|
const doc = JSON.parse(await docR.text());
|
|
1986
2026
|
const body = String(doc?.body_markdown ?? doc?.bodyMarkdown ?? "");
|
|
1987
|
-
// 4. Write the local mirror + version sidecar.
|
|
1988
|
-
|
|
1989
|
-
|
|
2027
|
+
// 4. Write the local mirror + version sidecar (node:fs, not Bun.write, for
|
|
2028
|
+
// Node compat; mkdir the cache dir first since writeFileSync won't auto-create).
|
|
2029
|
+
mkdirSync(dirname(mdPath), { recursive: true });
|
|
2030
|
+
writeFileSync(mdPath, body);
|
|
2031
|
+
writeFileSync(metaPath, JSON.stringify({ version: currentVersion, title: doc?.title, doc_id: a.doc_id, chat_id: a.chat_id, syncedAt: new Date().toISOString() }));
|
|
1990
2032
|
const was = cachedVersion === null ? "missing" : `stale v${cachedVersion}`;
|
|
1991
2033
|
return { content: [{ type: "text", text: `synced "${doc?.title || a.doc_id}" v${currentVersion} → ${mdPath} (was ${was}). Read that file to run it in your runtime.` }] };
|
|
1992
2034
|
} catch (e: any) {
|
|
@@ -2801,7 +2843,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2801
2843
|
if (msgs.length === 0) return { content: [{ type: "text", text: "No messages in this channel." }] };
|
|
2802
2844
|
const list = msgs.map((m: any) => {
|
|
2803
2845
|
const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : "?";
|
|
2804
|
-
|
|
2846
|
+
let line = `[${time}] ${m.sender_id?.slice(0, 12)}: ${m.content?.slice(0, 200) ?? ""}`;
|
|
2847
|
+
// Surface media attachments so an agent can perceive voice/image messages,
|
|
2848
|
+
// not just their text caption. Audio: show any server transcript inline
|
|
2849
|
+
// (agent reads it directly); otherwise point at transcribe(url).
|
|
2850
|
+
const atts = Array.isArray(m.attachments) ? m.attachments : [];
|
|
2851
|
+
for (const a of atts) {
|
|
2852
|
+
if (!a?.url) continue;
|
|
2853
|
+
if (a.type === "audio") {
|
|
2854
|
+
const dur = typeof a.duration_ms === "number" ? ` ${(a.duration_ms / 1000).toFixed(1)}s` : "";
|
|
2855
|
+
line += `\n 🔊 audio${dur}: ${a.url}`;
|
|
2856
|
+
line += a.transcript
|
|
2857
|
+
? `\n transcript: "${String(a.transcript).slice(0, 400)}"`
|
|
2858
|
+
: `\n (no transcript — call transcribe(url) to read what was said)`;
|
|
2859
|
+
} else if (a.type === "image") {
|
|
2860
|
+
const dim = a.width && a.height ? ` ${a.width}×${a.height}` : "";
|
|
2861
|
+
line += `\n 🖼 image${dim}: ${a.url}`;
|
|
2862
|
+
} else {
|
|
2863
|
+
line += `\n 📎 ${a.type || "file"}: ${a.url}`;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return line;
|
|
2805
2867
|
}).join("\n");
|
|
2806
2868
|
return { content: [{ type: "text", text: `${msgs.length} messages:\n${list}` }] };
|
|
2807
2869
|
}
|