@chamade/mcp-server 1.1.8 → 2.0.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/README.md +12 -10
- package/dist/index.d.ts +8 -1
- package/dist/index.js +239 -214
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,16 +90,18 @@ With channel mode, transcripts, messages, and incoming calls are pushed to the a
|
|
|
90
90
|
|
|
91
91
|
| Tool | Description |
|
|
92
92
|
|------|-------------|
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
93
|
+
| `chamade_call_join` | Join a voice meeting (platform + meeting_url). |
|
|
94
|
+
| `chamade_call_say` | Speak text aloud via TTS in the meeting. |
|
|
95
|
+
| `chamade_call_chat` | Send a text chat message in the meeting. |
|
|
96
|
+
| `chamade_call_status` | Get call state and new transcript lines (delta pattern). |
|
|
97
|
+
| `chamade_call_accept` | Answer a ringing inbound call (SIP, etc.). |
|
|
98
|
+
| `chamade_call_refuse` | Refuse/reject a ringing inbound call. |
|
|
99
|
+
| `chamade_call_typing` | Send a typing indicator in meeting chat. |
|
|
100
|
+
| `chamade_call_leave` | Hang up and leave the meeting. |
|
|
101
|
+
| `chamade_call_list` | List all active calls. |
|
|
102
|
+
| `chamade_inbox` | Check DM conversations (Discord, Telegram, Teams, WhatsApp, Slack). |
|
|
103
|
+
| `chamade_dm_chat` | Send a DM message by platform. |
|
|
104
|
+
| `chamade_dm_typing` | Send a typing indicator in DM by platform. |
|
|
103
105
|
| `chamade_account` | Check account status, plan, credit/quota, and platform readiness. |
|
|
104
106
|
|
|
105
107
|
## Resources
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* and receive live transcripts via resource subscriptions.
|
|
8
8
|
*
|
|
9
9
|
* Two modes:
|
|
10
|
-
* - Standard MCP (default): agent polls
|
|
10
|
+
* - Standard MCP (default): agent polls chamade_call_status for transcripts
|
|
11
11
|
* - Channel mode (--channel): transcripts and events pushed via notifications
|
|
12
|
+
*
|
|
13
|
+
* Tools: 13 total
|
|
14
|
+
* DM: chamade_dm_chat, chamade_dm_typing
|
|
15
|
+
* Call: chamade_call_join, chamade_call_accept, chamade_call_refuse,
|
|
16
|
+
* chamade_call_say, chamade_call_chat, chamade_call_typing,
|
|
17
|
+
* chamade_call_status, chamade_call_leave, chamade_call_list
|
|
18
|
+
* Other: chamade_inbox, chamade_account
|
|
12
19
|
*/
|
|
13
20
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
* and receive live transcripts via resource subscriptions.
|
|
8
8
|
*
|
|
9
9
|
* Two modes:
|
|
10
|
-
* - Standard MCP (default): agent polls
|
|
10
|
+
* - Standard MCP (default): agent polls chamade_call_status for transcripts
|
|
11
11
|
* - Channel mode (--channel): transcripts and events pushed via notifications
|
|
12
|
+
*
|
|
13
|
+
* Tools: 13 total
|
|
14
|
+
* DM: chamade_dm_chat, chamade_dm_typing
|
|
15
|
+
* Call: chamade_call_join, chamade_call_accept, chamade_call_refuse,
|
|
16
|
+
* chamade_call_say, chamade_call_chat, chamade_call_typing,
|
|
17
|
+
* chamade_call_status, chamade_call_leave, chamade_call_list
|
|
18
|
+
* Other: chamade_inbox, chamade_account
|
|
12
19
|
*/
|
|
13
20
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -83,59 +90,61 @@ const CHANNEL_PREAMBLE = [
|
|
|
83
90
|
"## Channel mode — events arrive automatically",
|
|
84
91
|
"",
|
|
85
92
|
"Transcripts, messages, and calls are pushed to you in real-time.",
|
|
86
|
-
"Do NOT poll
|
|
87
|
-
"When you receive transcript lines, respond using
|
|
88
|
-
"When you receive a message, reply using
|
|
89
|
-
"When you receive an incoming call, use
|
|
90
|
-
"You can still call
|
|
93
|
+
"Do NOT poll chamade_call_status in a loop — you will be notified automatically.",
|
|
94
|
+
"When you receive transcript lines, respond using chamade_call_say (TTS) or chamade_call_chat (text).",
|
|
95
|
+
"When you receive a message, ALWAYS reply using chamade_dm_chat — even if the message is a command you act on (e.g. 'join meeting X'). A short acknowledgment is enough.",
|
|
96
|
+
"When you receive an incoming call, use chamade_call_accept to pick up.",
|
|
97
|
+
"You can still call chamade_call_status manually to catch up on missed transcript.",
|
|
91
98
|
"",
|
|
92
99
|
].join("\n");
|
|
93
100
|
const BASE_INSTRUCTIONS = [
|
|
94
101
|
"Chamade is a voice gateway for AI agents. It lets you join voice meetings (Discord, Teams, Meet, Telegram, SIP) and interact via speech and text.",
|
|
95
102
|
"",
|
|
96
103
|
"## Voice call workflow",
|
|
97
|
-
"1. Use
|
|
98
|
-
"2. Poll
|
|
99
|
-
"3. When you see new transcript, respond to the CONTENT using
|
|
104
|
+
"1. Use chamade_call_join to enter a meeting — returns a call_id and capabilities.",
|
|
105
|
+
"2. Poll chamade_call_status in a loop to read new transcript lines (delta pattern — only new lines since last call).",
|
|
106
|
+
"3. When you see new transcript, respond to the CONTENT using chamade_call_say (TTS) or chamade_call_chat (text).",
|
|
100
107
|
"4. Repeat step 2-3. Keep the call active — do NOT leave between exchanges.",
|
|
101
|
-
"5. Use
|
|
108
|
+
"5. Use chamade_call_leave only when explicitly asked or when the meeting ends.",
|
|
102
109
|
"",
|
|
103
110
|
"## SIP phone calls",
|
|
104
111
|
"SIP supports both outbound and inbound calls. Setup depends on the tier:",
|
|
105
|
-
"- Outbound (Pro plan only): call
|
|
106
|
-
"- Inbound (répondeur): user activates a phone number (DID) in the dashboard. Incoming calls appear as 'ringing' in
|
|
112
|
+
"- Outbound (Pro plan only): call chamade_call_join with platform='sip' and meeting_url='sip:+33...@provider'. Uses the user's own SIP trunk (configured in the dashboard).",
|
|
113
|
+
"- Inbound (répondeur): user activates a phone number (DID) in the dashboard. Incoming calls appear as 'ringing' in chamade_call_list.",
|
|
107
114
|
"- Inbound (BYOT): user connects their own SIP trunk + DIDs in the dashboard. Same inbound flow.",
|
|
108
115
|
"",
|
|
109
116
|
"Inbound call workflow:",
|
|
110
|
-
"1. Use
|
|
111
|
-
"2. Use
|
|
112
|
-
"3. Use
|
|
117
|
+
"1. Use chamade_call_list to see ringing calls (poll regularly).",
|
|
118
|
+
"2. Use chamade_call_accept to pick up the call.",
|
|
119
|
+
"3. Use chamade_call_say to speak and chamade_call_status to read transcript (same as outbound).",
|
|
113
120
|
"4. If auto_answer is enabled, calls go straight to 'active' — no need to answer.",
|
|
121
|
+
"5. Use chamade_call_refuse to reject a ringing call.",
|
|
114
122
|
"",
|
|
115
123
|
"## Recovery from disconnection",
|
|
116
124
|
"If the bridge restarts or connection is lost, the call state becomes 'disconnected'.",
|
|
117
|
-
"
|
|
118
|
-
"To recover: call
|
|
125
|
+
"chamade_call_status will show instructions to reconnect.",
|
|
126
|
+
"To recover: call chamade_call_join again with the same platform and meeting_url.",
|
|
119
127
|
"A new call_id is issued. Previous transcript is lost.",
|
|
120
128
|
"",
|
|
121
129
|
"## DM / Messaging workflow",
|
|
122
130
|
"1. Use chamade_inbox to check for incoming DM conversations.",
|
|
123
|
-
"2. Before composing a reply, use
|
|
124
|
-
"3. Use
|
|
125
|
-
"4. When a
|
|
131
|
+
"2. Before composing a reply, use chamade_dm_typing to show the user you are working on it.",
|
|
132
|
+
"3. Use chamade_dm_chat to reply — specify the platform (e.g. 'teams', 'discord').",
|
|
133
|
+
"4. When a call_invite event has a call_id, use chamade_call_say/chamade_call_status on that call.",
|
|
134
|
+
"IMPORTANT: ALWAYS reply to every DM with chamade_dm_chat, even if the message is a command you act on (e.g. 'join meeting X'). A short acknowledgment ('Joining the meeting now') is enough. If you don't reply, the user sees a typing indicator for 60 seconds followed by a timeout error.",
|
|
126
135
|
"",
|
|
127
136
|
"## Capabilities",
|
|
128
137
|
"Each call and conversation returns a capabilities array. Meaning:",
|
|
129
138
|
"- listen: receive audio transcripts (STT)",
|
|
130
|
-
"- speak: send audio via TTS (
|
|
139
|
+
"- speak: send audio via TTS (chamade_call_say)",
|
|
131
140
|
"- read: receive text chat messages",
|
|
132
|
-
"- write: send text chat messages (
|
|
133
|
-
"- typing: typing indicator supported
|
|
141
|
+
"- write: send text chat messages (chamade_call_chat)",
|
|
142
|
+
"- typing: typing indicator supported",
|
|
134
143
|
"- files: file sharing supported",
|
|
135
144
|
"",
|
|
136
145
|
"Per-platform defaults:",
|
|
137
146
|
"- discord/teams: listen, speak, read, write, typing, files (full voice + chat)",
|
|
138
|
-
"- meet: listen, read, write (no TTS — use
|
|
147
|
+
"- meet: listen, read, write (no TTS — use chamade_call_chat instead). Note: chat (read/write) only works for calendar-scheduled meetings. Ad-hoc Meet links have no chat API access. IMPORTANT: when the bot joins a Meet call, a participant in the meeting must manually accept it (Google shows a 'Someone wants to join' popup). Tell the user to accept the bot in the Meet UI.",
|
|
139
148
|
"- zoom: listen, speak, read, write, files",
|
|
140
149
|
"- telegram: read, write, typing, files (text DM only, no voice join)",
|
|
141
150
|
"- whatsapp: read, write (text DM only, no voice join, no typing indicator)",
|
|
@@ -143,7 +152,7 @@ const BASE_INSTRUCTIONS = [
|
|
|
143
152
|
"- nctalk: listen, speak, read, write, typing",
|
|
144
153
|
"- slack: read, write, files (text only, no voice — Huddles have no API)",
|
|
145
154
|
"",
|
|
146
|
-
"If 'speak' is not in capabilities, use
|
|
155
|
+
"If 'speak' is not in capabilities, use chamade_call_chat to send text instead of chamade_call_say.",
|
|
147
156
|
"",
|
|
148
157
|
"## Platform requirements",
|
|
149
158
|
"Each platform requires specific setup. Use chamade_account to check what is already configured.",
|
|
@@ -173,8 +182,8 @@ const BASE_INSTRUCTIONS = [
|
|
|
173
182
|
"- \"Voice quota exceeded\" → monthly voice minutes used up (free plan: 10 min/month)",
|
|
174
183
|
"- \"SIP not available on free plan\" → user needs to upgrade to Pro for SIP calls",
|
|
175
184
|
"- \"No SIP trunk configured\" → user needs to connect a SIP trunk in the dashboard for outbound SIP",
|
|
176
|
-
"- \"Direct audio requires a Pro plan\" → direct audio streaming (raw PCM via WebSocket) is Pro only. Free users can still use
|
|
177
|
-
"- \"Slack does not support voice calls\" → use chamade_inbox/
|
|
185
|
+
"- \"Direct audio requires a Pro plan\" → direct audio streaming (raw PCM via WebSocket) is Pro only. Free users can still use chamade_call_say (TTS) within their voice quota",
|
|
186
|
+
"- \"Slack does not support voice calls\" → use chamade_inbox/chamade_dm_chat for Slack messaging instead",
|
|
178
187
|
"",
|
|
179
188
|
"## Account management",
|
|
180
189
|
`Chamade is fully bot-friendly. All account management (API keys, bot tokens, platform connections, SIP, billing) is available via the REST API at the same endpoints the dashboard uses. See ${CHAMADE_URL}/llms.txt for the full machine-readable documentation. This MCP server focuses on runtime operations (calls + messaging); use the REST API directly for account setup and configuration.`,
|
|
@@ -196,15 +205,18 @@ const INSTRUCTIONS = CHANNEL_MODE
|
|
|
196
205
|
// ---------------------------------------------------------------------------
|
|
197
206
|
const server = new McpServer({
|
|
198
207
|
name: "chamade",
|
|
199
|
-
version: "
|
|
208
|
+
version: "2.0.0",
|
|
200
209
|
}, {
|
|
201
210
|
capabilities: CHANNEL_MODE
|
|
202
211
|
? { experimental: { "claude/channel": {} } }
|
|
203
212
|
: undefined,
|
|
204
213
|
instructions: INSTRUCTIONS,
|
|
205
214
|
});
|
|
206
|
-
//
|
|
207
|
-
|
|
215
|
+
// ===========================================================================
|
|
216
|
+
// CALL TOOLS
|
|
217
|
+
// ===========================================================================
|
|
218
|
+
// -- Tool: call_join ----------------------------------------------------------
|
|
219
|
+
server.tool("chamade_call_join", "Join a voice meeting on any platform (Discord, Teams, Meet, Telegram, SIP, WhatsApp). Returns a call_id. After joining, poll chamade_call_status regularly to read new transcript lines. Use chamade_call_say to speak (TTS) and chamade_call_leave to hang up. Keep the call active — do NOT leave between exchanges.", {
|
|
208
220
|
platform: z
|
|
209
221
|
.enum(["discord", "teams", "meet", "telegram", "sip", "nctalk", "zoom", "whatsapp"])
|
|
210
222
|
.describe("Meeting platform"),
|
|
@@ -245,13 +257,13 @@ server.tool("chamade_join", "Join a voice meeting on any platform (Discord, Team
|
|
|
245
257
|
lines.push(`Audio: ${audio.sample_rate}Hz ${audio.format} (${audio.frame_duration_ms}ms frames, ${audio.frame_bytes} bytes/frame)`);
|
|
246
258
|
}
|
|
247
259
|
if (!capabilities.includes("speak")) {
|
|
248
|
-
lines.push(`Note: TTS (speak) is not supported on ${platform}. Use
|
|
260
|
+
lines.push(`Note: TTS (speak) is not supported on ${platform}. Use chamade_call_chat to send text instead.`);
|
|
249
261
|
}
|
|
250
262
|
if (CHANNEL_MODE) {
|
|
251
|
-
lines.push(``, `Transcript will be pushed automatically. Use
|
|
263
|
+
lines.push(``, `Transcript will be pushed automatically. Use chamade_call_say to speak, chamade_call_chat to send text, chamade_call_leave to hang up.`);
|
|
252
264
|
}
|
|
253
265
|
else {
|
|
254
|
-
lines.push(`Transcript: chamade://calls/${callId}/transcript`, ``, `Use
|
|
266
|
+
lines.push(`Transcript: chamade://calls/${callId}/transcript`, ``, `Use chamade_call_say to speak, chamade_call_chat to send text, chamade_call_status to check transcript, chamade_call_leave to hang up.`);
|
|
255
267
|
}
|
|
256
268
|
return {
|
|
257
269
|
content: [
|
|
@@ -262,9 +274,59 @@ server.tool("chamade_join", "Join a voice meeting on any platform (Discord, Team
|
|
|
262
274
|
],
|
|
263
275
|
};
|
|
264
276
|
});
|
|
265
|
-
// -- Tool:
|
|
266
|
-
server.tool("
|
|
267
|
-
call_id: z.string().describe("Call ID
|
|
277
|
+
// -- Tool: call_accept --------------------------------------------------------
|
|
278
|
+
server.tool("chamade_call_accept", "Answer a ringing inbound call. Use this when chamade_call_list or a call_invite event shows a call in 'ringing' state (incoming phone call). The call transitions to 'active' and you can start speaking.", {
|
|
279
|
+
call_id: z.string().describe("Call ID of the ringing call"),
|
|
280
|
+
}, async ({ call_id }) => {
|
|
281
|
+
await chamadePost(`/api/call/${call_id}/accept`, {});
|
|
282
|
+
// Fetch call details to get real capabilities and platform
|
|
283
|
+
const data = (await chamadeGet(`/api/call/${call_id}`));
|
|
284
|
+
const platform = data.platform || "sip";
|
|
285
|
+
const capabilities = data.capabilities ?? ["listen", "speak"];
|
|
286
|
+
// Track the call locally
|
|
287
|
+
calls.set(call_id, {
|
|
288
|
+
callId: call_id,
|
|
289
|
+
platform,
|
|
290
|
+
meetingUrl: data.meeting_url || "",
|
|
291
|
+
capabilities,
|
|
292
|
+
lastTranscriptLength: 0,
|
|
293
|
+
});
|
|
294
|
+
// Channel mode: auto-watch for transcript push
|
|
295
|
+
if (CHANNEL_MODE) {
|
|
296
|
+
channelWatchCall(call_id);
|
|
297
|
+
}
|
|
298
|
+
const lines = [
|
|
299
|
+
`Answered call ${call_id}.`,
|
|
300
|
+
`State: active`,
|
|
301
|
+
`Capabilities: ${capabilities.join(", ")}`,
|
|
302
|
+
``,
|
|
303
|
+
`Use chamade_call_say to speak, chamade_call_status to check transcript, chamade_call_leave to hang up.`,
|
|
304
|
+
];
|
|
305
|
+
if (!capabilities.includes("speak")) {
|
|
306
|
+
lines.push(`Note: TTS (speak) is not supported on ${platform}. Use chamade_call_chat to send text instead.`);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: lines.join("\n"),
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
// -- Tool: call_refuse --------------------------------------------------------
|
|
318
|
+
server.tool("chamade_call_refuse", "Refuse (reject) a ringing inbound call. The call is ended and the caller hears a disconnect.", {
|
|
319
|
+
call_id: z.string().describe("Call ID of the ringing call"),
|
|
320
|
+
}, async ({ call_id }) => {
|
|
321
|
+
await chamadePost(`/api/call/${call_id}/refuse`, {});
|
|
322
|
+
calls.delete(call_id);
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: "text", text: `Refused call ${call_id}.` }],
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
// -- Tool: call_say -----------------------------------------------------------
|
|
328
|
+
server.tool("chamade_call_say", "Speak text aloud in the meeting via TTS. Write naturally and concisely — avoid markdown, code blocks, or long lists as the text is converted to speech. Respond to the CONTENT of what people say, do not echo or repeat their words. Use punctuation to shape delivery: '...' for hesitation, '!' for energy, '—' for a pause. Use CAPS sparingly for emphasis.", {
|
|
329
|
+
call_id: z.string().describe("Call ID from chamade_call_join"),
|
|
268
330
|
text: z.string().describe("Text to speak aloud"),
|
|
269
331
|
}, async ({ call_id, text }) => {
|
|
270
332
|
await chamadePost(`/api/call/${call_id}/say`, { text });
|
|
@@ -272,9 +334,9 @@ server.tool("chamade_say", "Speak text aloud in the meeting via TTS. Write natur
|
|
|
272
334
|
content: [{ type: "text", text: `Speaking: "${text}"` }],
|
|
273
335
|
};
|
|
274
336
|
});
|
|
275
|
-
// -- Tool:
|
|
276
|
-
server.tool("
|
|
277
|
-
call_id: z.string().describe("Call ID from
|
|
337
|
+
// -- Tool: call_chat ----------------------------------------------------------
|
|
338
|
+
server.tool("chamade_call_chat", "Send a text chat message in the meeting. Set sender_name to identify yourself (e.g. your name or 'AI Assistant').", {
|
|
339
|
+
call_id: z.string().describe("Call ID from chamade_call_join"),
|
|
278
340
|
text: z.string().describe("Chat message to send"),
|
|
279
341
|
sender_name: z.string().optional().describe("Display name shown in the chat (e.g. 'Claude', 'AI Assistant')"),
|
|
280
342
|
}, async ({ call_id, text, sender_name }) => {
|
|
@@ -286,9 +348,18 @@ server.tool("chamade_chat", "Send a text chat message in the meeting. Set sender
|
|
|
286
348
|
content: [{ type: "text", text: `Chat sent: "${text}"` }],
|
|
287
349
|
};
|
|
288
350
|
});
|
|
289
|
-
// -- Tool:
|
|
290
|
-
server.tool("
|
|
291
|
-
call_id: z.string().describe("Call ID from
|
|
351
|
+
// -- Tool: call_typing --------------------------------------------------------
|
|
352
|
+
server.tool("chamade_call_typing", "Send a typing indicator in the call's chat. Only works on platforms with 'typing' in capabilities.", {
|
|
353
|
+
call_id: z.string().describe("Call ID from chamade_call_join"),
|
|
354
|
+
}, async ({ call_id }) => {
|
|
355
|
+
await chamadePost(`/api/call/${call_id}/typing`, {});
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: "Typing indicator sent." }],
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
// -- Tool: call_status --------------------------------------------------------
|
|
361
|
+
server.tool("chamade_call_status", "Get call status and new transcript lines since last check. Poll this regularly to listen to what people are saying. Returns only new lines since the previous call (delta pattern).", {
|
|
362
|
+
call_id: z.string().describe("Call ID from chamade_call_join"),
|
|
292
363
|
}, async ({ call_id }) => {
|
|
293
364
|
const state = calls.get(call_id);
|
|
294
365
|
const since = state?.lastTranscriptLength ?? 0;
|
|
@@ -321,15 +392,15 @@ server.tool("chamade_status", "Get call status and new transcript lines since la
|
|
|
321
392
|
data.ended_at ? `Ended: ${data.ended_at}` : "",
|
|
322
393
|
];
|
|
323
394
|
if (data.state === "ringing") {
|
|
324
|
-
statusLines.push(``, `Incoming call! Use
|
|
395
|
+
statusLines.push(``, `Incoming call! Use chamade_call_accept to pick up, or chamade_call_refuse to reject.`);
|
|
325
396
|
}
|
|
326
397
|
if (data.state === "disconnected") {
|
|
327
398
|
const meetingUrl = state?.meetingUrl || data.meeting_url || "";
|
|
328
399
|
const platform = state?.platform || data.platform || "";
|
|
329
|
-
statusLines.push(``,
|
|
400
|
+
statusLines.push(``, `Bridge connection lost. The voice session has been interrupted.`, `To reconnect, call chamade_call_join again with platform="${platform}" and meeting_url="${meetingUrl}".`, `A new call_id will be issued. Previous transcript is lost.`);
|
|
330
401
|
}
|
|
331
402
|
if (!capabilities.includes("speak")) {
|
|
332
|
-
statusLines.push(`Note: TTS (speak) is not supported on ${data.platform}. Use
|
|
403
|
+
statusLines.push(`Note: TTS (speak) is not supported on ${data.platform}. Use chamade_call_chat to send text instead.`);
|
|
333
404
|
}
|
|
334
405
|
return {
|
|
335
406
|
content: [
|
|
@@ -342,73 +413,9 @@ server.tool("chamade_status", "Get call status and new transcript lines since la
|
|
|
342
413
|
],
|
|
343
414
|
};
|
|
344
415
|
});
|
|
345
|
-
// -- Tool:
|
|
346
|
-
server.tool("
|
|
347
|
-
call_id: z.string().describe("Call ID
|
|
348
|
-
}, async ({ call_id }) => {
|
|
349
|
-
await chamadePost(`/api/call/${call_id}/answer`, {});
|
|
350
|
-
// Fetch call details to get real capabilities and platform
|
|
351
|
-
const data = (await chamadeGet(`/api/call/${call_id}`));
|
|
352
|
-
const platform = data.platform || "sip";
|
|
353
|
-
const capabilities = data.capabilities ?? ["listen", "speak"];
|
|
354
|
-
// Track the call locally
|
|
355
|
-
calls.set(call_id, {
|
|
356
|
-
callId: call_id,
|
|
357
|
-
platform,
|
|
358
|
-
meetingUrl: data.meeting_url || "",
|
|
359
|
-
capabilities,
|
|
360
|
-
lastTranscriptLength: 0,
|
|
361
|
-
});
|
|
362
|
-
// Channel mode: auto-watch for transcript push
|
|
363
|
-
if (CHANNEL_MODE) {
|
|
364
|
-
channelWatchCall(call_id);
|
|
365
|
-
}
|
|
366
|
-
const lines = [
|
|
367
|
-
`Answered call ${call_id}.`,
|
|
368
|
-
`State: active`,
|
|
369
|
-
`Capabilities: ${capabilities.join(", ")}`,
|
|
370
|
-
``,
|
|
371
|
-
`Use chamade_say to speak, chamade_status to check transcript, chamade_leave to hang up.`,
|
|
372
|
-
];
|
|
373
|
-
if (!capabilities.includes("speak")) {
|
|
374
|
-
lines.push(`Note: TTS (speak) is not supported on ${platform}. Use chamade_chat to send text instead.`);
|
|
375
|
-
}
|
|
376
|
-
return {
|
|
377
|
-
content: [
|
|
378
|
-
{
|
|
379
|
-
type: "text",
|
|
380
|
-
text: lines.join("\n"),
|
|
381
|
-
},
|
|
382
|
-
],
|
|
383
|
-
};
|
|
384
|
-
});
|
|
385
|
-
// -- Tool: typing -----------------------------------------------------------
|
|
386
|
-
server.tool("chamade_typing", "Send a typing indicator. Use this before composing a reply so the user sees you are working on it. Only works on platforms with 'typing' in capabilities (Discord, Teams, Telegram, NC Talk).", {
|
|
387
|
-
call_id: z
|
|
388
|
-
.string()
|
|
389
|
-
.optional()
|
|
390
|
-
.describe("Call ID (for typing in a call's chat)"),
|
|
391
|
-
conversation_id: z
|
|
392
|
-
.string()
|
|
393
|
-
.optional()
|
|
394
|
-
.describe("Conversation ID (for typing in a DM)"),
|
|
395
|
-
}, async ({ call_id, conversation_id }) => {
|
|
396
|
-
if (call_id) {
|
|
397
|
-
await chamadePost(`/api/call/${call_id}/typing`, {});
|
|
398
|
-
}
|
|
399
|
-
else if (conversation_id) {
|
|
400
|
-
await chamadePost("/api/typing", { conversation_id });
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
throw new Error("Either call_id or conversation_id is required");
|
|
404
|
-
}
|
|
405
|
-
return {
|
|
406
|
-
content: [{ type: "text", text: "Typing indicator sent." }],
|
|
407
|
-
};
|
|
408
|
-
});
|
|
409
|
-
// -- Tool: leave ------------------------------------------------------------
|
|
410
|
-
server.tool("chamade_leave", "Leave the meeting and hang up. Only use when explicitly asked to leave or when the meeting is over.", {
|
|
411
|
-
call_id: z.string().describe("Call ID from chamade_join"),
|
|
416
|
+
// -- Tool: call_leave ---------------------------------------------------------
|
|
417
|
+
server.tool("chamade_call_leave", "Leave the meeting and hang up. Only use when explicitly asked to leave or when the meeting is over.", {
|
|
418
|
+
call_id: z.string().describe("Call ID from chamade_call_join"),
|
|
412
419
|
}, async ({ call_id }) => {
|
|
413
420
|
calls.delete(call_id);
|
|
414
421
|
// Channel mode: stop watching
|
|
@@ -420,8 +427,8 @@ server.tool("chamade_leave", "Leave the meeting and hang up. Only use when expli
|
|
|
420
427
|
content: [{ type: "text", text: `Left call ${call_id}.` }],
|
|
421
428
|
};
|
|
422
429
|
});
|
|
423
|
-
// -- Tool:
|
|
424
|
-
server.tool("
|
|
430
|
+
// -- Tool: call_list ----------------------------------------------------------
|
|
431
|
+
server.tool("chamade_call_list", "List all active calls.", {}, async () => {
|
|
425
432
|
const data = (await chamadeGet("/api/calls"));
|
|
426
433
|
if (data.calls.length === 0) {
|
|
427
434
|
return {
|
|
@@ -438,57 +445,63 @@ server.tool("chamade_list_calls", "List all active calls.", {}, async () => {
|
|
|
438
445
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
439
446
|
};
|
|
440
447
|
});
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const limitStr = concurrent.limit === 0 ? "unlimited" : String(concurrent.limit);
|
|
457
|
-
lines.push(`Active calls: ${concurrent.active} / ${limitStr}`);
|
|
458
|
-
}
|
|
459
|
-
// Platform statuses
|
|
460
|
-
const platforms = data.platforms;
|
|
461
|
-
if (platforms) {
|
|
462
|
-
lines.push("", "Platforms:");
|
|
463
|
-
for (const [name, status] of Object.entries(platforms)) {
|
|
464
|
-
const icon = status.startsWith("ok") ? "+" : status.startsWith("error") ? "!" : "-";
|
|
465
|
-
lines.push(` ${icon} ${name}: ${status}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
// DM TOOLS
|
|
450
|
+
// ===========================================================================
|
|
451
|
+
// -- Tool: dm_chat ------------------------------------------------------------
|
|
452
|
+
server.tool("chamade_dm_chat", "Send a message to the active DM conversation on a platform. The message will be delivered to the user on their platform (Discord, Telegram, Teams, etc.).", {
|
|
453
|
+
platform: z
|
|
454
|
+
.enum(["discord", "teams", "telegram", "whatsapp", "slack", "nctalk"])
|
|
455
|
+
.describe("Platform where the DM conversation is active"),
|
|
456
|
+
text: z.string().describe("Message text to send"),
|
|
457
|
+
}, async ({ platform, text }) => {
|
|
458
|
+
channelStopTyping(platform);
|
|
459
|
+
const data = (await chamadePost("/api/dm/chat", {
|
|
460
|
+
platform,
|
|
461
|
+
text,
|
|
462
|
+
}));
|
|
468
463
|
return {
|
|
469
|
-
content: [
|
|
464
|
+
content: [
|
|
465
|
+
{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: `Message sent to ${platform} (${data.delivery || "async"}): "${text}"`,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
470
|
};
|
|
471
471
|
});
|
|
472
|
-
// -- Tool:
|
|
473
|
-
server.tool("
|
|
474
|
-
|
|
475
|
-
.
|
|
472
|
+
// -- Tool: dm_typing ----------------------------------------------------------
|
|
473
|
+
server.tool("chamade_dm_typing", "Send a typing indicator on a DM conversation. Use this before composing a reply so the user sees you are working on it. Only works on platforms with 'typing' in capabilities (Discord, Teams, Telegram, NC Talk).", {
|
|
474
|
+
platform: z
|
|
475
|
+
.enum(["discord", "teams", "telegram", "whatsapp", "slack", "nctalk"])
|
|
476
|
+
.describe("Platform where the DM conversation is active"),
|
|
477
|
+
}, async ({ platform }) => {
|
|
478
|
+
await chamadePost("/api/dm/typing", { platform });
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: "text", text: `Typing indicator sent on ${platform}.` }],
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
// ===========================================================================
|
|
484
|
+
// OTHER TOOLS
|
|
485
|
+
// ===========================================================================
|
|
486
|
+
// -- Tool: inbox --------------------------------------------------------------
|
|
487
|
+
server.tool("chamade_inbox", "Check inbox for DM conversations. Without a platform, lists all active conversations. With a platform, returns the active DM conversation details and message history for that platform.", {
|
|
488
|
+
platform: z
|
|
489
|
+
.enum(["discord", "teams", "telegram", "whatsapp", "slack", "nctalk"])
|
|
476
490
|
.optional()
|
|
477
|
-
.describe("Optional
|
|
491
|
+
.describe("Optional: get messages for a specific platform's DM"),
|
|
478
492
|
limit: z
|
|
479
493
|
.number()
|
|
480
494
|
.default(20)
|
|
481
495
|
.describe("Max messages to return (when viewing a conversation)"),
|
|
482
|
-
}, async ({
|
|
483
|
-
if (
|
|
484
|
-
const data = (await chamadeGet(`/api/inbox
|
|
496
|
+
}, async ({ platform, limit }) => {
|
|
497
|
+
if (platform) {
|
|
498
|
+
const data = (await chamadeGet(`/api/inbox?platform=${platform}&limit=${limit}`));
|
|
485
499
|
const conv = data.conversation;
|
|
486
500
|
const messages = data.messages;
|
|
487
501
|
const capabilities = conv.capabilities ?? [];
|
|
488
502
|
const lines = [
|
|
489
|
-
`Conversation: ${conv.id}`,
|
|
490
503
|
`Platform: ${conv.platform}`,
|
|
491
|
-
`Contact: ${conv.remote_name ||
|
|
504
|
+
`Contact: ${conv.remote_name || "unknown"}`,
|
|
492
505
|
`Type: ${conv.channel_type}`,
|
|
493
506
|
`Capabilities: ${capabilities.join(", ") || "text"}`,
|
|
494
507
|
`Messages: ${conv.message_count}`,
|
|
@@ -521,32 +534,44 @@ server.tool("chamade_inbox", "Check inbox for DM conversations. Without conversa
|
|
|
521
534
|
const capabilities = c.capabilities ?? [];
|
|
522
535
|
const cap = capabilities.length > 0 ? ` [${capabilities.join(",")}]` : "";
|
|
523
536
|
const callInfo = c.call_id ? ` (voice: ${c.call_id})` : "";
|
|
524
|
-
return `- ${c.
|
|
537
|
+
return `- ${c.platform} — ${c.remote_name || "unknown"} — ${c.message_count} msgs${cap}${callInfo}`;
|
|
525
538
|
});
|
|
526
539
|
return {
|
|
527
540
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
528
541
|
};
|
|
529
542
|
});
|
|
530
|
-
// -- Tool:
|
|
531
|
-
server.tool("
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
543
|
+
// -- Tool: account ------------------------------------------------------------
|
|
544
|
+
server.tool("chamade_account", "Check account status: plan, credit/quota remaining, and per-platform readiness (ok, not_configured, error). Use this to diagnose setup issues or check remaining credit before making calls.", {}, async () => {
|
|
545
|
+
const data = (await chamadeGet("/api/account"));
|
|
546
|
+
const lines = [`Plan: ${data.plan}`];
|
|
547
|
+
// Credit or voice quota
|
|
548
|
+
const credit = data.credit;
|
|
549
|
+
const voice = data.voice_quota;
|
|
550
|
+
if (credit) {
|
|
551
|
+
lines.push(`Credit: ${credit.remaining_eur.toFixed(2)} EUR remaining (${credit.used_eur.toFixed(2)} / ${credit.total_eur.toFixed(2)} EUR used)`);
|
|
552
|
+
}
|
|
553
|
+
else if (voice) {
|
|
554
|
+
lines.push(`Voice quota: ${voice.used_minutes.toFixed(1)} / ${voice.limit_minutes} min used`);
|
|
555
|
+
}
|
|
556
|
+
const concurrent = data.concurrent_calls;
|
|
557
|
+
if (concurrent) {
|
|
558
|
+
const limitStr = concurrent.limit === 0 ? "unlimited" : String(concurrent.limit);
|
|
559
|
+
lines.push(`Active calls: ${concurrent.active} / ${limitStr}`);
|
|
560
|
+
}
|
|
561
|
+
// Platform statuses
|
|
562
|
+
const platforms = data.platforms;
|
|
563
|
+
if (platforms) {
|
|
564
|
+
lines.push("", "Platforms:");
|
|
565
|
+
for (const [name, status] of Object.entries(platforms)) {
|
|
566
|
+
const icon = status.startsWith("ok") ? "+" : status.startsWith("error") ? "!" : "-";
|
|
567
|
+
lines.push(` ${icon} ${name}: ${status}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
540
570
|
return {
|
|
541
|
-
content: [
|
|
542
|
-
{
|
|
543
|
-
type: "text",
|
|
544
|
-
text: `Message sent (${data.delivery || "async"}): "${text}"`,
|
|
545
|
-
},
|
|
546
|
-
],
|
|
571
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
547
572
|
};
|
|
548
573
|
});
|
|
549
|
-
// -- Resource: transcript
|
|
574
|
+
// -- Resource: transcript -----------------------------------------------------
|
|
550
575
|
const transcriptTemplate = new ResourceTemplate("chamade://calls/{call_id}/transcript", {
|
|
551
576
|
list: async () => {
|
|
552
577
|
return {
|
|
@@ -603,18 +628,18 @@ const _callWatchers = new Map();
|
|
|
603
628
|
const _closedWatchers = new Set();
|
|
604
629
|
const _typingActive = new Set();
|
|
605
630
|
/**
|
|
606
|
-
* Start typing indicator for a conversation.
|
|
631
|
+
* Start typing indicator for a DM conversation (keyed by platform).
|
|
607
632
|
* Sends a single POST — Maquisard manages the repeat loop internally
|
|
608
633
|
* and stops it automatically when a message is sent.
|
|
609
634
|
*/
|
|
610
|
-
function channelStartTyping(
|
|
611
|
-
if (_typingActive.has(
|
|
635
|
+
function channelStartTyping(platform) {
|
|
636
|
+
if (_typingActive.has(platform))
|
|
612
637
|
return;
|
|
613
|
-
_typingActive.add(
|
|
614
|
-
chamadePost("/api/typing", {
|
|
638
|
+
_typingActive.add(platform);
|
|
639
|
+
chamadePost("/api/dm/typing", { platform }).catch(() => { });
|
|
615
640
|
}
|
|
616
|
-
function channelStopTyping(
|
|
617
|
-
_typingActive.delete(
|
|
641
|
+
function channelStopTyping(platform) {
|
|
642
|
+
_typingActive.delete(platform);
|
|
618
643
|
}
|
|
619
644
|
/**
|
|
620
645
|
* Push a channel notification to Claude Code.
|
|
@@ -639,6 +664,12 @@ async function channelPush(content, meta = {}) {
|
|
|
639
664
|
/**
|
|
640
665
|
* Watch a call's transcript via WebSocket push.
|
|
641
666
|
* Opens a WS to /api/call/{id}/stream and pushes each event.
|
|
667
|
+
*
|
|
668
|
+
* Events from server (already remapped by Chamade backend):
|
|
669
|
+
* - call_transcript: speaker + text
|
|
670
|
+
* - call_chat: sender + text
|
|
671
|
+
* - call_state: state changes
|
|
672
|
+
* - call_error: errors
|
|
642
673
|
*/
|
|
643
674
|
function channelWatchCall(callId) {
|
|
644
675
|
if (_callWatchers.has(callId) || _closedWatchers.has(callId))
|
|
@@ -660,28 +691,27 @@ function channelWatchCall(callId) {
|
|
|
660
691
|
try {
|
|
661
692
|
const data = JSON.parse(raw.toString());
|
|
662
693
|
switch (data.type) {
|
|
663
|
-
case "
|
|
694
|
+
case "call_transcript":
|
|
664
695
|
retryCount = 0; // Real data — reset retry counter
|
|
665
696
|
if (data.is_final !== false) {
|
|
666
|
-
await channelPush(`[${data.speaker || "?"}] ${data.text || ""}`, { type: "
|
|
697
|
+
await channelPush(`[${data.speaker || "?"}] ${data.text || ""}`, { type: "call_transcript", call_id: callId });
|
|
667
698
|
}
|
|
668
699
|
break;
|
|
669
|
-
case "
|
|
670
|
-
|
|
671
|
-
await channelPush(`[chat:${data.sender || "?"}] ${data.text || ""}`, { type: "chat", call_id: callId });
|
|
700
|
+
case "call_chat":
|
|
701
|
+
await channelPush(`[chat:${data.sender || "?"}] ${data.text || ""}`, { type: "call_chat", call_id: callId });
|
|
672
702
|
break;
|
|
673
|
-
case "
|
|
674
|
-
await channelPush(`Call state: ${data.state}`, { type: "
|
|
703
|
+
case "call_state":
|
|
704
|
+
await channelPush(`Call state: ${data.state}`, { type: "call_state", call_id: callId });
|
|
675
705
|
if (data.state === "ended" || data.state === "error") {
|
|
676
706
|
channelUnwatchCall(callId);
|
|
677
707
|
}
|
|
678
708
|
break;
|
|
679
|
-
case "
|
|
680
|
-
await channelPush(`Call error: ${data.message || data.code || "unknown"}`, { type: "
|
|
709
|
+
case "call_error":
|
|
710
|
+
await channelPush(`Call error: ${data.message || data.code || "unknown"}`, { type: "call_error", call_id: callId });
|
|
681
711
|
// Stop watching on any terminal error
|
|
682
712
|
channelUnwatchCall(callId);
|
|
683
713
|
if (data.code === "bridge_disconnected") {
|
|
684
|
-
await channelPush(`Bridge connection lost for call ${callId}. Use
|
|
714
|
+
await channelPush(`Bridge connection lost for call ${callId}. Use chamade_call_join with the same meeting_url to reconnect.`, { type: "call_state", call_id: callId });
|
|
685
715
|
}
|
|
686
716
|
break;
|
|
687
717
|
}
|
|
@@ -700,7 +730,7 @@ function channelWatchCall(callId) {
|
|
|
700
730
|
else if (!_closedWatchers.has(callId)) {
|
|
701
731
|
console.error(`[chamade-channel] Call ${callId}: max retries (${MAX_RETRIES}) reached, giving up`);
|
|
702
732
|
channelUnwatchCall(callId);
|
|
703
|
-
channelPush(`Call ${callId} ended (connection lost). Use
|
|
733
|
+
channelPush(`Call ${callId} ended (connection lost). Use chamade_call_leave to clean up, or chamade_call_join to reconnect.`, { type: "call_state", call_id: callId });
|
|
704
734
|
}
|
|
705
735
|
});
|
|
706
736
|
ws.on("error", () => {
|
|
@@ -726,6 +756,13 @@ function channelUnwatchCall(callId) {
|
|
|
726
756
|
/**
|
|
727
757
|
* Watch inbox for DMs, incoming calls, and conversation events.
|
|
728
758
|
* Reconnects automatically on disconnect.
|
|
759
|
+
*
|
|
760
|
+
* Events from server (already remapped by Chamade backend):
|
|
761
|
+
* - dm_chat: new DM message
|
|
762
|
+
* - dm_started: new DM conversation
|
|
763
|
+
* - dm_ended: DM conversation closed
|
|
764
|
+
* - call_invite: incoming call (SIP or DM voice)
|
|
765
|
+
* - call_state: call state change (disconnected, etc.)
|
|
729
766
|
*/
|
|
730
767
|
function channelWatchInbox() {
|
|
731
768
|
let retryDelay = 1000;
|
|
@@ -751,46 +788,34 @@ function channelWatchInbox() {
|
|
|
751
788
|
try {
|
|
752
789
|
const data = JSON.parse(raw.toString());
|
|
753
790
|
switch (data.type) {
|
|
754
|
-
case "
|
|
755
|
-
// Auto-send
|
|
756
|
-
if (data.
|
|
757
|
-
channelStartTyping(data.
|
|
791
|
+
case "dm_chat":
|
|
792
|
+
// Auto-send typing indicator by platform
|
|
793
|
+
if (data.platform) {
|
|
794
|
+
channelStartTyping(data.platform);
|
|
758
795
|
}
|
|
759
796
|
await channelPush(`New message from ${data.sender_name || "unknown"} on ${data.platform}: "${data.text}"`, {
|
|
760
|
-
type: "
|
|
761
|
-
conversation_id: data.conversation_id || "",
|
|
797
|
+
type: "dm_chat",
|
|
762
798
|
platform: data.platform || "",
|
|
763
799
|
});
|
|
764
800
|
break;
|
|
765
|
-
case "
|
|
766
|
-
await channelPush(`Incoming call from ${data.caller
|
|
767
|
-
// Auto-watch if already active (auto_answer)
|
|
801
|
+
case "call_invite":
|
|
802
|
+
await channelPush(`Incoming call${data.caller ? ` from ${data.caller}` : ""} on ${data.platform || "sip"}. Call ID: ${data.call_id}. State: ${data.state || "ringing"}. Use chamade_call_accept to pick up.`, { type: "call_invite", call_id: data.call_id || "" });
|
|
803
|
+
// Auto-watch if already active (auto_answer or DM voice)
|
|
768
804
|
if (data.state === "active" && data.call_id) {
|
|
769
805
|
channelWatchCall(data.call_id);
|
|
770
806
|
}
|
|
771
807
|
break;
|
|
772
|
-
case "
|
|
773
|
-
await channelPush(`Voice started. Call ID: ${data.call_id}. Platform: ${data.platform}.`, {
|
|
774
|
-
type: "voice_started",
|
|
775
|
-
call_id: data.call_id || "",
|
|
776
|
-
conversation_id: data.conversation_id || "",
|
|
777
|
-
});
|
|
778
|
-
if (data.call_id) {
|
|
779
|
-
channelWatchCall(data.call_id);
|
|
780
|
-
}
|
|
781
|
-
break;
|
|
782
|
-
case "conversation_started":
|
|
808
|
+
case "dm_started":
|
|
783
809
|
await channelPush(`New conversation from ${data.remote_name || "unknown"} on ${data.platform}.`, {
|
|
784
|
-
type: "
|
|
785
|
-
conversation_id: data.conversation_id || "",
|
|
810
|
+
type: "dm_started",
|
|
786
811
|
platform: data.platform || "",
|
|
787
812
|
});
|
|
788
813
|
break;
|
|
789
|
-
case "
|
|
790
|
-
await channelPush(`Conversation ended.`, { type: "
|
|
814
|
+
case "dm_ended":
|
|
815
|
+
await channelPush(`Conversation ended on ${data.platform || "unknown"}.`, { type: "dm_ended", platform: data.platform || "" });
|
|
791
816
|
break;
|
|
792
|
-
case "
|
|
793
|
-
await channelPush(`Call
|
|
817
|
+
case "call_state":
|
|
818
|
+
await channelPush(`Call ${data.call_id}: ${data.state}${data.message ? ` — ${data.message}` : ""}`, { type: "call_state", call_id: data.call_id || "" });
|
|
794
819
|
break;
|
|
795
820
|
}
|
|
796
821
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chamade/mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "MCP server for Chamade — voice gateway for AI agents. Join Discord, Teams, Meet, Telegram, SIP, Zoom meetings and interact via speech and text. Supports Claude Code channel mode for push events.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|