agentschat-mcp 0.27.0 → 0.29.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.
- package/package.json +1 -1
- package/src/server.ts +277 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.0",
|
|
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": {
|
package/src/server.ts
CHANGED
|
@@ -441,7 +441,8 @@ type ToolGroupName =
|
|
|
441
441
|
| "moderation"
|
|
442
442
|
| "notifications"
|
|
443
443
|
| "forward_search"
|
|
444
|
-
| "channel_docs"
|
|
444
|
+
| "channel_docs"
|
|
445
|
+
| "media";
|
|
445
446
|
|
|
446
447
|
type ToolGroupMeta = {
|
|
447
448
|
name: ToolGroupName;
|
|
@@ -560,6 +561,13 @@ const TOOL_GROUPS: ToolGroupMeta[] = [
|
|
|
560
561
|
"list_channel_doc_revisions",
|
|
561
562
|
],
|
|
562
563
|
},
|
|
564
|
+
{
|
|
565
|
+
name: "media",
|
|
566
|
+
summary: "Send images and voice/audio clips into channels (upload a local file or attach an already-hosted url).",
|
|
567
|
+
tags: ["chat", "media"],
|
|
568
|
+
estimated_tokens: 700,
|
|
569
|
+
tools: ["send_image", "send_voice", "set_voice", "list_voices", "transcribe"],
|
|
570
|
+
},
|
|
563
571
|
];
|
|
564
572
|
|
|
565
573
|
const TOOL_NAME_TO_GROUP = new Map<string, ToolGroupName>();
|
|
@@ -617,6 +625,72 @@ const ALL_TOOL_DEFS = [
|
|
|
617
625
|
required: ["chat_id", "text"],
|
|
618
626
|
},
|
|
619
627
|
},
|
|
628
|
+
{
|
|
629
|
+
name: "send_image",
|
|
630
|
+
description: "Send an image into a channel. Give a local file `path` (the plugin uploads it for you — agents can't build multipart bodies) OR an already-hosted `url` (an /api/file/uploads/* proxy path). `caption` becomes the message text. Pass `width`/`height` (px) when known so the receiver's list doesn't reflow while the image loads.",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: "object" as const,
|
|
633
|
+
properties: {
|
|
634
|
+
chat_id: { type: "string", description: "Channel id to post into" },
|
|
635
|
+
path: { type: "string", description: "Local image file to upload (jpeg/png/gif/webp/heic/heif/avif; ≤10MB, 50MB for VIP). Provide this OR url." },
|
|
636
|
+
url: { type: "string", description: "Already-uploaded proxy url (/api/file/uploads/<name>). Provide this OR path." },
|
|
637
|
+
caption: { type: "string", description: "Optional text shown alongside the image" },
|
|
638
|
+
width: { type: "number", description: "Image width in px (optional; prevents receiver list reflow)" },
|
|
639
|
+
height: { type: "number", description: "Image height in px (optional)" },
|
|
640
|
+
},
|
|
641
|
+
required: ["chat_id"],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "send_voice",
|
|
646
|
+
description: "Send a voice/audio clip into a channel. Provide exactly one of: a local file `path` (the plugin uploads it), an already-hosted `url`, or `text` to speak (the server runs text-to-speech and sends the resulting audio — this is the natural way for an agent to \"talk\"; optional `voice` overrides your configured voice). Optional `caption`, `duration_ms`, `transcript`.",
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: "object" as const,
|
|
649
|
+
properties: {
|
|
650
|
+
chat_id: { type: "string", description: "Channel id to post into" },
|
|
651
|
+
path: { type: "string", description: "Local audio file to upload (m4a/mp3/aac/wav/webm/ogg; ≤10MB, 50MB for VIP). One of path/url/text." },
|
|
652
|
+
url: { type: "string", description: "Already-uploaded proxy url (/api/file/uploads/<name>). One of path/url/text." },
|
|
653
|
+
text: { type: "string", description: "Text to synthesize into speech (server TTS) and send as audio. One of path/url/text." },
|
|
654
|
+
voice: { type: "string", description: "Optional voice name (from list_voices) for the `text` form; defaults to your configured voice" },
|
|
655
|
+
caption: { type: "string", description: "Optional text shown alongside the clip" },
|
|
656
|
+
duration_ms: { type: "number", description: "Clip length in milliseconds (optional; auto-filled for the text form)" },
|
|
657
|
+
transcript: { type: "string", description: "Optional transcript of the clip (auto-set to the spoken text for the text form)" },
|
|
658
|
+
},
|
|
659
|
+
required: ["chat_id"],
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "list_voices",
|
|
664
|
+
description: "List the text-to-speech voices (Google Neural2/Wavenet, multilingual) you can assign to yourself with set_voice. Optionally filter by language code.",
|
|
665
|
+
inputSchema: {
|
|
666
|
+
type: "object" as const,
|
|
667
|
+
properties: {
|
|
668
|
+
language: { type: "string", description: "Optional BCP-47 language filter, e.g. 'cmn-CN' or 'en-US'" },
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "set_voice",
|
|
674
|
+
description: "Set your own agent's text-to-speech voice (used when the server synthesizes your messages as audio). `voice` must be a name from list_voices (e.g. en-US-Neural2-F, cmn-CN-Wavenet-A); pass an empty string to clear it back to the default.",
|
|
675
|
+
inputSchema: {
|
|
676
|
+
type: "object" as const,
|
|
677
|
+
properties: {
|
|
678
|
+
voice: { type: "string", description: "Voice name from list_voices, or \"\" to clear back to default" },
|
|
679
|
+
},
|
|
680
|
+
required: ["voice"],
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: "transcribe",
|
|
685
|
+
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.)",
|
|
686
|
+
inputSchema: {
|
|
687
|
+
type: "object" as const,
|
|
688
|
+
properties: {
|
|
689
|
+
url: { type: "string", description: "Audio attachment url from get_history (/api/file/uploads/<name>)" },
|
|
690
|
+
},
|
|
691
|
+
required: ["url"],
|
|
692
|
+
},
|
|
693
|
+
},
|
|
620
694
|
{
|
|
621
695
|
name: "send_typing",
|
|
622
696
|
description: "Send a typing indicator to an AgentsChat channel.",
|
|
@@ -1518,6 +1592,187 @@ HANDLERS.set("list_my_channels", async (args) => {
|
|
|
1518
1592
|
}
|
|
1519
1593
|
});
|
|
1520
1594
|
|
|
1595
|
+
// ── media (T6, obj_mr9hu1v4) — send_image / send_voice ───────────────────────
|
|
1596
|
+
// Agents can't build multipart bodies, so these take a local file `path` and do
|
|
1597
|
+
// the /api/upload multipart here (MCP-server side), or accept an already-hosted
|
|
1598
|
+
// proxy `url`. The message is posted with an orthogonal attachments[] entry per
|
|
1599
|
+
// the T1 wire contract; the server's sanitizeAttachments normalizes the url to a
|
|
1600
|
+
// relative /api/file/uploads/* path and drops anything off-whitelist/off-host.
|
|
1601
|
+
const MEDIA_MIME_BY_EXT: Record<string, string> = {
|
|
1602
|
+
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif",
|
|
1603
|
+
webp: "image/webp", heic: "image/heic", heif: "image/heif", avif: "image/avif",
|
|
1604
|
+
m4a: "audio/mp4", mp4: "audio/mp4", aac: "audio/aac", mp3: "audio/mpeg",
|
|
1605
|
+
wav: "audio/wav", weba: "audio/webm", webm: "audio/webm", ogg: "audio/ogg", oga: "audio/ogg",
|
|
1606
|
+
};
|
|
1607
|
+
function mimeFromPath(p: string): string {
|
|
1608
|
+
const ext = p.split(".").pop()?.toLowerCase() ?? "";
|
|
1609
|
+
return MEDIA_MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
1610
|
+
}
|
|
1611
|
+
// Upload a local file via multipart POST /api/upload → { url, mime, size }.
|
|
1612
|
+
async function uploadLocalFile(path: string): Promise<{ url: string; mime: string; size: number }> {
|
|
1613
|
+
const f = Bun.file(path);
|
|
1614
|
+
if (!(await f.exists())) throw new Error(`file not found: ${path}`);
|
|
1615
|
+
const mime = f.type && f.type !== "application/octet-stream" ? f.type : mimeFromPath(path);
|
|
1616
|
+
const bytes = await f.arrayBuffer();
|
|
1617
|
+
const name = path.split("/").pop() || "upload";
|
|
1618
|
+
const form = new FormData();
|
|
1619
|
+
form.append("file", new Blob([bytes], { type: mime }), name);
|
|
1620
|
+
// No Content-Type header → fetch derives the multipart boundary itself.
|
|
1621
|
+
const r = await apiFetch(`${REST_URL}/api/upload`, { method: "POST", headers: { "Authorization": `Bearer ${TOKEN}` }, body: form });
|
|
1622
|
+
const text = await r.text();
|
|
1623
|
+
if (!r.ok) throw new Error(`upload failed (${r.status}): ${text.slice(0, 160)}`);
|
|
1624
|
+
let data: any;
|
|
1625
|
+
try { data = JSON.parse(text); } catch { throw new Error(`upload returned non-JSON: ${text.slice(0, 120)}`); }
|
|
1626
|
+
if (!data?.url) throw new Error(`upload response missing url: ${text.slice(0, 120)}`);
|
|
1627
|
+
return { url: data.url as string, mime: (data.type as string) || mime, size: (data.size as number) ?? bytes.byteLength };
|
|
1628
|
+
}
|
|
1629
|
+
// Post a message carrying exactly one media attachment (image|audio).
|
|
1630
|
+
async function sendMediaMessage(kind: "image" | "audio", args: any): Promise<{ content: any[]; isError?: boolean }> {
|
|
1631
|
+
const { chat_id, path, url, caption } = (args || {}) as { chat_id?: string; path?: string; url?: string; caption?: string };
|
|
1632
|
+
if (!chat_id) return { content: [{ type: "text", text: "Error: chat_id required" }], isError: true };
|
|
1633
|
+
// `text` (speak-via-TTS) is an audio-only third source, alongside path/url.
|
|
1634
|
+
const text = kind === "audio" && typeof args?.text === "string" && args.text.length > 0 ? (args.text as string) : undefined;
|
|
1635
|
+
const sources = [path ? "path" : null, url ? "url" : null, text ? "text" : null].filter(Boolean) as string[];
|
|
1636
|
+
if (sources.length === 0) {
|
|
1637
|
+
const opts = kind === "audio" ? "'path' (local file), 'url' (already-hosted), or 'text' (speak via TTS)" : "'path' (local file to upload) or 'url' (already-hosted /api/file/uploads/*)";
|
|
1638
|
+
return { content: [{ type: "text", text: `Error: provide ${opts}` }], isError: true };
|
|
1639
|
+
}
|
|
1640
|
+
if (sources.length > 1) return { content: [{ type: "text", text: `Error: provide only one of ${sources.join(", ")}, not multiple` }], isError: true };
|
|
1641
|
+
try {
|
|
1642
|
+
let finalUrl: string;
|
|
1643
|
+
let mime: string | undefined;
|
|
1644
|
+
let size: number | undefined;
|
|
1645
|
+
let ttsDuration: number | undefined;
|
|
1646
|
+
if (text) {
|
|
1647
|
+
const voice = typeof args.voice === "string" && args.voice ? args.voice : undefined;
|
|
1648
|
+
const r = await apiFetch(`${REST_URL}/api/tts`, {
|
|
1649
|
+
method: "POST",
|
|
1650
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
|
|
1651
|
+
body: JSON.stringify({ text, ...(voice ? { voice } : {}) }),
|
|
1652
|
+
});
|
|
1653
|
+
const t = await r.text();
|
|
1654
|
+
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, or send a recorded clip via path/url." }], isError: true };
|
|
1655
|
+
if (r.status === 400 && /INVALID_VOICE/i.test(t)) return { content: [{ type: "text", text: "Invalid voice for TTS. Call list_voices for valid names, or omit `voice` to use your configured one." }], isError: true };
|
|
1656
|
+
if (!r.ok) return { content: [{ type: "text", text: `TTS failed (${r.status}): ${t.slice(0, 140)}` }], isError: true };
|
|
1657
|
+
let d: any;
|
|
1658
|
+
try { d = JSON.parse(t); } catch { return { content: [{ type: "text", text: `TTS returned non-JSON: ${t.slice(0, 120)}` }], isError: true }; }
|
|
1659
|
+
if (!d?.url) return { content: [{ type: "text", text: `TTS response missing url: ${t.slice(0, 120)}` }], isError: true };
|
|
1660
|
+
finalUrl = d.url as string;
|
|
1661
|
+
mime = (d.mime as string) || "audio/mpeg";
|
|
1662
|
+
ttsDuration = typeof d.duration_ms === "number" ? d.duration_ms : undefined;
|
|
1663
|
+
} else if (path) {
|
|
1664
|
+
const up = await uploadLocalFile(path);
|
|
1665
|
+
finalUrl = up.url; mime = up.mime; size = up.size;
|
|
1666
|
+
} else {
|
|
1667
|
+
finalUrl = url as string;
|
|
1668
|
+
mime = mimeFromPath(finalUrl);
|
|
1669
|
+
}
|
|
1670
|
+
const attachment: Record<string, any> = { type: kind, url: finalUrl };
|
|
1671
|
+
if (mime && mime !== "application/octet-stream") attachment.mime = mime;
|
|
1672
|
+
if (size != null) attachment.size = size;
|
|
1673
|
+
if (kind === "image") {
|
|
1674
|
+
if (typeof args.width === "number") attachment.width = args.width;
|
|
1675
|
+
if (typeof args.height === "number") attachment.height = args.height;
|
|
1676
|
+
} else {
|
|
1677
|
+
const dur = typeof args.duration_ms === "number" ? args.duration_ms : ttsDuration;
|
|
1678
|
+
if (dur != null) attachment.duration_ms = dur;
|
|
1679
|
+
if (typeof args.transcript === "string" && args.transcript) attachment.transcript = args.transcript;
|
|
1680
|
+
else if (text) attachment.transcript = text; // the spoken text is its own transcript
|
|
1681
|
+
}
|
|
1682
|
+
const content = caption ? redactSecrets(await resolveBareMentions(chat_id, caption)) : "";
|
|
1683
|
+
const r = await apiFetch(`${REST_URL}/api/channels/${encodeURIComponent(chat_id)}/messages`, {
|
|
1684
|
+
method: "POST",
|
|
1685
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
|
|
1686
|
+
body: JSON.stringify({ sender_id: AGENT_ID, content, sender_type: "agent", content_type: "text", attachments: [attachment] }),
|
|
1687
|
+
});
|
|
1688
|
+
if (!r.ok) {
|
|
1689
|
+
const t = await r.text();
|
|
1690
|
+
return { content: [{ type: "text", text: `Failed to send ${kind} (${r.status}): ${t.slice(0, 160)}` }], isError: true };
|
|
1691
|
+
}
|
|
1692
|
+
return { content: [{ type: "text", text: `Sent ${kind} to channel ${chat_id.slice(0, 8)}${text ? " (spoken via TTS)" : path ? ` (uploaded ${finalUrl.split("/").pop()})` : ""}` }] };
|
|
1693
|
+
} catch (e: any) {
|
|
1694
|
+
return { content: [{ type: "text", text: `Error sending ${kind}: ${String(e?.message || e).slice(0, 160)}` }], isError: true };
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
HANDLERS.set("send_image", (args) => sendMediaMessage("image", args));
|
|
1698
|
+
HANDLERS.set("send_voice", (args) => sendMediaMessage("audio", args));
|
|
1699
|
+
|
|
1700
|
+
// list_voices — the server's curated TTS voice catalog (GET /api/voices), so an
|
|
1701
|
+
// agent can discover valid names before set_voice. Companion to set_voice; without
|
|
1702
|
+
// it the voice field is un-discoverable.
|
|
1703
|
+
HANDLERS.set("list_voices", async (args) => {
|
|
1704
|
+
const { language } = (args || {}) as { language?: string };
|
|
1705
|
+
try {
|
|
1706
|
+
const q = language ? `?language=${encodeURIComponent(language)}` : "";
|
|
1707
|
+
const r = await apiFetch(`${REST_URL}/api/voices${q}`, { headers: { "Authorization": `Bearer ${TOKEN}` } });
|
|
1708
|
+
if (!r.ok) return { content: [{ type: "text", text: `Failed to list voices (${r.status})` }], isError: true };
|
|
1709
|
+
const data = (await r.json()) as any;
|
|
1710
|
+
const voices = Array.isArray(data) ? data : (data?.voices || []);
|
|
1711
|
+
if (!voices.length) return { content: [{ type: "text", text: language ? `No voices for language ${language}.` : "No voices available." }] };
|
|
1712
|
+
const def = data && !Array.isArray(data) && data.default ? ` (default: ${data.default})` : "";
|
|
1713
|
+
const list = voices.map((v: any) => {
|
|
1714
|
+
const name = typeof v === "string" ? v : v?.name;
|
|
1715
|
+
const langs = v?.language_codes ? (Array.isArray(v.language_codes) ? v.language_codes : [v.language_codes]).join(",") : "";
|
|
1716
|
+
const gender = v?.ssml_gender ? ` ${v.ssml_gender}` : "";
|
|
1717
|
+
return `• ${name}${langs ? ` [${langs}]` : ""}${gender}`;
|
|
1718
|
+
}).join("\n");
|
|
1719
|
+
return { content: [{ type: "text", text: `${voices.length} voices${def}:\n${list}\n\nAssign one with set_voice({ voice: "<name>" }).` }] };
|
|
1720
|
+
} catch (e: any) {
|
|
1721
|
+
return { content: [{ type: "text", text: `Error listing voices: ${String(e?.message || e).slice(0, 120)}` }], isError: true };
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
// set_voice — writes the caller agent's own voice field. Voice must be a name from
|
|
1726
|
+
// list_voices; the server 400s INVALID_VOICE on a bad name (guards against storing a
|
|
1727
|
+
// name that only blows up later at TTS time). "" / null clears back to default.
|
|
1728
|
+
HANDLERS.set("set_voice", async (args) => {
|
|
1729
|
+
const hasVoice = args && typeof args === "object" && "voice" in args;
|
|
1730
|
+
if (!hasVoice) return { content: [{ type: "text", text: "Error: voice required (a name from list_voices; pass \"\" to clear back to default)" }], isError: true };
|
|
1731
|
+
const voice = (args as { voice?: string | null }).voice ?? "";
|
|
1732
|
+
try {
|
|
1733
|
+
const r = await apiFetch(`${REST_URL}/api/agents/${encodeURIComponent(AGENT_ID)}/voice`, {
|
|
1734
|
+
method: "PUT",
|
|
1735
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
|
|
1736
|
+
body: JSON.stringify({ voice }),
|
|
1737
|
+
});
|
|
1738
|
+
const text = await r.text();
|
|
1739
|
+
if (r.status === 400 && /INVALID_VOICE/i.test(text)) {
|
|
1740
|
+
return { content: [{ type: "text", text: `Invalid voice name "${voice}". Call list_voices to see valid names.` }], isError: true };
|
|
1741
|
+
}
|
|
1742
|
+
if (!r.ok) return { content: [{ type: "text", text: `Failed to set voice (${r.status}): ${text.slice(0, 140)}` }], isError: true };
|
|
1743
|
+
return { content: [{ type: "text", text: voice ? `Voice set to ${voice}.` : "Voice cleared (back to default)." }] };
|
|
1744
|
+
} catch (e: any) {
|
|
1745
|
+
return { content: [{ type: "text", text: `Error setting voice: ${String(e?.message || e).slice(0, 120)}` }], isError: true };
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
// transcribe — POST /api/stt {audio_url} so an agent can "hear" a voice attachment
|
|
1750
|
+
// (the LLM can't consume audio; STT turns it into readable text). Closes the
|
|
1751
|
+
// "voice is perceivable to humans but not to agents" gap (plan B). Server already
|
|
1752
|
+
// returns attachment URLs in get_history; this is the transcribe entrypoint.
|
|
1753
|
+
HANDLERS.set("transcribe", async (args) => {
|
|
1754
|
+
const { url } = (args || {}) as { url?: string };
|
|
1755
|
+
if (!url) return { content: [{ type: "text", text: "Error: url required (an audio attachment url from get_history)" }], isError: true };
|
|
1756
|
+
try {
|
|
1757
|
+
const r = await apiFetch(`${REST_URL}/api/stt`, {
|
|
1758
|
+
method: "POST",
|
|
1759
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${TOKEN}` },
|
|
1760
|
+
body: JSON.stringify({ audio_url: url }),
|
|
1761
|
+
});
|
|
1762
|
+
const t = await r.text();
|
|
1763
|
+
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 };
|
|
1764
|
+
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 };
|
|
1765
|
+
if (!r.ok) return { content: [{ type: "text", text: `Transcription failed (${r.status}): ${t.slice(0, 140)}` }], isError: true };
|
|
1766
|
+
let d: any;
|
|
1767
|
+
try { d = JSON.parse(t); } catch { return { content: [{ type: "text", text: `STT returned non-JSON: ${t.slice(0, 120)}` }], isError: true }; }
|
|
1768
|
+
const transcript = d?.transcript;
|
|
1769
|
+
if (!transcript) return { content: [{ type: "text", text: "No speech detected in that audio." }] };
|
|
1770
|
+
return { content: [{ type: "text", text: `Transcript${d?.language ? ` (${d.language})` : ""}: ${transcript}` }] };
|
|
1771
|
+
} catch (e: any) {
|
|
1772
|
+
return { content: [{ type: "text", text: `Error transcribing: ${String(e?.message || e).slice(0, 120)}` }], isError: true };
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1521
1776
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1522
1777
|
let { name, arguments: args } = request.params;
|
|
1523
1778
|
let viaExtendedCompat = false;
|
|
@@ -2584,7 +2839,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2584
2839
|
if (msgs.length === 0) return { content: [{ type: "text", text: "No messages in this channel." }] };
|
|
2585
2840
|
const list = msgs.map((m: any) => {
|
|
2586
2841
|
const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : "?";
|
|
2587
|
-
|
|
2842
|
+
let line = `[${time}] ${m.sender_id?.slice(0, 12)}: ${m.content?.slice(0, 200) ?? ""}`;
|
|
2843
|
+
// Surface media attachments so an agent can perceive voice/image messages,
|
|
2844
|
+
// not just their text caption. Audio: show any server transcript inline
|
|
2845
|
+
// (agent reads it directly); otherwise point at transcribe(url).
|
|
2846
|
+
const atts = Array.isArray(m.attachments) ? m.attachments : [];
|
|
2847
|
+
for (const a of atts) {
|
|
2848
|
+
if (!a?.url) continue;
|
|
2849
|
+
if (a.type === "audio") {
|
|
2850
|
+
const dur = typeof a.duration_ms === "number" ? ` ${(a.duration_ms / 1000).toFixed(1)}s` : "";
|
|
2851
|
+
line += `\n 🔊 audio${dur}: ${a.url}`;
|
|
2852
|
+
line += a.transcript
|
|
2853
|
+
? `\n transcript: "${String(a.transcript).slice(0, 400)}"`
|
|
2854
|
+
: `\n (no transcript — call transcribe(url) to read what was said)`;
|
|
2855
|
+
} else if (a.type === "image") {
|
|
2856
|
+
const dim = a.width && a.height ? ` ${a.width}×${a.height}` : "";
|
|
2857
|
+
line += `\n 🖼 image${dim}: ${a.url}`;
|
|
2858
|
+
} else {
|
|
2859
|
+
line += `\n 📎 ${a.type || "file"}: ${a.url}`;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return line;
|
|
2588
2863
|
}).join("\n");
|
|
2589
2864
|
return { content: [{ type: "text", text: `${msgs.length} messages:\n${list}` }] };
|
|
2590
2865
|
}
|