@rymi/mcp 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rymi AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @rymi/mcp
2
+
3
+ MCP server for [Rymi](https://rymi.ai) — lets any MCP-capable AI client (Claude Desktop, Claude Code, Cursor, or a Rymi voice agent) create and manage AI voice agents without writing HTTP code.
4
+
5
+ ## Quickstart
6
+
7
+ No install needed. Point your client at `https://mcp.rymi.live` and pass your API key as a Bearer token.
8
+
9
+ ## Claude Desktop config
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "rymi": {
15
+ "type": "http",
16
+ "url": "https://mcp.rymi.live/",
17
+ "headers": {
18
+ "Authorization": "Bearer rymi_your_secret_key"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ## Tools
26
+
27
+ **Agents:** `list_agents` · `get_agent` · `create_agent` · `update_agent` · `delete_agent` · `clone_agent` · `apply_agent_changes` · `generate_agent_draft` · `enrich_company` · `validate_agent_publish` · `preview_stack`
28
+
29
+ **Discovery:** `list_llm_options` · `list_voices`
30
+
31
+ **Knowledge & history:** `list_knowledge_sources` · `add_knowledge_source` · `delete_knowledge_source` · `list_agent_changes` · `undo_agent_change`
32
+
33
+ **Insight:** `get_usage_summary` (minutes-based) · `list_agent_templates` · `run_evals` · `list_eval_runs` · `get_eval_run`
34
+
35
+ **Calls (read-only):** `list_calls` · `list_active_calls` · `get_call` · `get_call_summary` · `get_call_transcript` · `get_call_recording` · `get_call_queue_stats` · `reprocess_call` · `list_calls_for_agent`
36
+
37
+ **Numbers:** `list_numbers` · `register_number` · `attach_number` · `remove_number`
38
+
39
+ **Telephony (read-only):** `telephony_status` · `list_telephony_numbers`
40
+
41
+ **Keys (read-only):** `list_publishable_keys`
42
+
43
+ **Gated tools:**
44
+
45
+ - `create_call`, `batch_call` — place real, billable outbound calls.
46
+ - `publish_agent` — flips an agent live to end users.
47
+
48
+ > Carrier connect/disconnect and publishable-key creation/revocation are intentionally **not** exposed over MCP (they enter credentials and change standing configuration) — do those from the dashboard.
49
+
50
+ ## Configuring an agent
51
+
52
+ `create_agent` and `update_agent` accept the full agent configuration surface. Always call `list_llm_options` first to get valid model and voice IDs.
53
+
54
+ - **Multi-language / bilingual** — set `supported_languages` to every BCP-47 tag the agent should handle, e.g. `["hi-IN","en-US"]`. The primary `language` is merged in automatically. The server resolves the per-language model stack for you; some languages may require a higher `agent_role`.
55
+ - **Model stack** — pin `llm_provider` + `llm_model`, and optionally `stt_provider`/`stt_model` and `tts_provider`/`tts_model`. Realtime LLMs (and Deepgram TTS) carry their own voice, so leave `voice` empty for those.
56
+ - **Fallbacks** — `llm_fallback_*`, `stt_fallback_*`, `tts_fallback_*` set a secondary provider/model per channel. Pass `null` to clear.
57
+ - **Self-hosted endpoints** (Enterprise) — `custom_llm_url`, `custom_voice_url` (+ `custom_voice_mode`), `custom_transcriber_url`. `https://` or `wss://` only; `null` clears.
58
+ - **`agent_role`** — the cost/capability tier (`operator` → `concierge`). `concierge` unlocks realtime models. The chosen LLM model must fit the role's cost ceiling.
59
+ - **`persona`** — note that `callerPersonas` entries are objects of shape `{ type, approach, detectedWhen }`, not plain strings.
60
+
61
+ `provider_config` is server-derived (recomputed from role + supported languages on every write) and is not accepted as an input.
62
+
63
+ ## Documentation
64
+
65
+ [rymi.ai/docs/api/mcp](https://docs.rymi.live/api/mcp)
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,954 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/server.ts
27
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
28
+ var import_node = __toESM(require("@rymi/node"));
29
+
30
+ // src/tools/agents.ts
31
+ var import_zod = require("zod");
32
+ var agentConfigFields = {
33
+ system_prompt: import_zod.z.string().optional().describe("Full system prompt for the agent. If given without persona/playbook, the server auto-structures it."),
34
+ voice: import_zod.z.string().optional().describe('Voice ID (e.g. "Aoede", "Charon"). Call list_llm_options for valid values. Leave empty for realtime LLMs and Deepgram TTS, which carry their own voice.'),
35
+ language: import_zod.z.string().optional().describe('Primary BCP-47 language tag (e.g. "hi-IN", "en-US").'),
36
+ supported_languages: import_zod.z.array(import_zod.z.string()).optional().describe('All BCP-47 languages the agent should handle, e.g. ["hi-IN","en-US"] for a bilingual agent. The primary `language` is added automatically if omitted here.'),
37
+ llm_provider: import_zod.z.enum(["gemini", "openai", "anthropic", "sarvam"]).optional(),
38
+ llm_model: import_zod.z.string().optional().describe('LLM model id from list_llm_options (e.g. "gemini-2.5-flash", "sarvam-m"). Must fit the agent_role cost tier.'),
39
+ llm_fallback_provider: import_zod.z.string().nullable().optional().describe("Fallback LLM provider; null clears it."),
40
+ llm_fallback_model: import_zod.z.string().nullable().optional().describe("Fallback LLM model; null clears it."),
41
+ stt_provider: import_zod.z.string().optional().describe('Speech-to-text provider (e.g. "deepgram", "sarvam").'),
42
+ stt_model: import_zod.z.string().optional(),
43
+ stt_fallback_provider: import_zod.z.string().nullable().optional(),
44
+ stt_fallback_model: import_zod.z.string().nullable().optional(),
45
+ tts_provider: import_zod.z.string().optional().describe('Text-to-speech provider (e.g. "elevenlabs", "sarvam", "cartesia").'),
46
+ tts_model: import_zod.z.string().optional(),
47
+ tts_fallback_provider: import_zod.z.string().nullable().optional(),
48
+ tts_fallback_model: import_zod.z.string().nullable().optional(),
49
+ custom_llm_url: import_zod.z.string().nullable().optional().describe("Self-hosted LLM endpoint (https:// or wss://). Enterprise only; null clears."),
50
+ custom_voice_url: import_zod.z.string().nullable().optional().describe("Self-hosted TTS endpoint (https:// or wss://). Enterprise only; null clears."),
51
+ custom_voice_mode: import_zod.z.enum(["rymi", "openai-compat"]).optional().describe("Wire format for custom_voice_url."),
52
+ custom_transcriber_url: import_zod.z.string().nullable().optional().describe("Self-hosted STT endpoint (https:// or wss://). Enterprise only; null clears."),
53
+ agent_role: import_zod.z.enum(["operator", "specialist", "executive", "concierge"]).optional().describe('Cost/capability tier. "concierge" unlocks realtime models; higher tiers cost more.'),
54
+ prompt_mode: import_zod.z.enum(["builder", "raw"]).optional().describe('How the system prompt is interpreted: "builder" (server structures persona/playbook from system_prompt) or "raw" (use system_prompt verbatim \u2014 requires system_prompt). Defaults to "builder".'),
55
+ persona: import_zod.z.record(import_zod.z.any()).optional().describe("Persona object. Note: callerPersonas must be objects of shape {type, approach, detectedWhen}, NOT plain strings."),
56
+ playbook: import_zod.z.record(import_zod.z.any()).optional().describe("Playbook configuration object"),
57
+ advanced: import_zod.z.record(import_zod.z.any()).optional(),
58
+ features: import_zod.z.record(import_zod.z.any()).optional(),
59
+ post_call: import_zod.z.record(import_zod.z.any()).optional()
60
+ };
61
+ function ok(result) {
62
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
63
+ }
64
+ function fail(err) {
65
+ const e = err;
66
+ const body = { error: e?.message || String(err) };
67
+ if (e?.code) body.code = e.code;
68
+ if (e?.status) body.status = e.status;
69
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
70
+ }
71
+ function foldPromptMode(params) {
72
+ const { prompt_mode, ...rest } = params;
73
+ if (!prompt_mode) return rest;
74
+ return { ...rest, advanced: { ...rest.advanced || {}, prompt_mode } };
75
+ }
76
+ function registerAgentTools(server, rymi, isReadOnly = false) {
77
+ server.tool(
78
+ "list_agents",
79
+ "List all your Rymi AI voice agents.",
80
+ {
81
+ limit: import_zod.z.number().int().min(1).max(500).optional().describe("Max agents to return (default 50)"),
82
+ offset: import_zod.z.number().int().min(0).optional().describe("Pagination offset")
83
+ },
84
+ async ({ limit, offset }) => {
85
+ const result = await rymi.agents.list({ limit, offset });
86
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
87
+ }
88
+ );
89
+ server.tool(
90
+ "get_agent",
91
+ "Retrieve a single Rymi voice agent by ID.",
92
+ { agent_id: import_zod.z.string().uuid().describe("The agent UUID") },
93
+ async ({ agent_id }) => {
94
+ const result = await rymi.agents.retrieve(agent_id);
95
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
96
+ }
97
+ );
98
+ server.tool(
99
+ "list_llm_options",
100
+ "Fetch all available LLM models and voices you can use when creating or updating an agent. Always call this before create_agent to pick valid values.",
101
+ {},
102
+ async () => {
103
+ const result = await rymi.agents.llmOptions();
104
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
105
+ }
106
+ );
107
+ server.tool(
108
+ "list_voices",
109
+ "List available agent/TTS voices, optionally filtered by provider or model. Much smaller payload than list_llm_options \u2014 use this to pick a `voice` value for create_agent/update_agent.",
110
+ {
111
+ provider: import_zod.z.string().optional().describe('Filter to one voice provider, e.g. "elevenlabs", "sarvam", "gemini", "cartesia", "deepgram".'),
112
+ model_id: import_zod.z.string().optional().describe("Filter to voices compatible with a given model id (matched against the voice's supported_model_ids).")
113
+ },
114
+ async ({ provider, model_id }) => {
115
+ const { voices } = await rymi.agents.llmOptions();
116
+ let filtered = Array.isArray(voices) ? voices : [];
117
+ if (provider) {
118
+ const p = provider.toLowerCase();
119
+ filtered = filtered.filter((v) => String(v?.provider || "").toLowerCase() === p);
120
+ }
121
+ if (model_id) {
122
+ filtered = filtered.filter((v) => Array.isArray(v?.supported_model_ids) && v.supported_model_ids.includes(model_id));
123
+ }
124
+ const trimmed = filtered.map((v) => ({
125
+ id: v?.id,
126
+ provider: v?.provider,
127
+ name: v?.name,
128
+ label: v?.label,
129
+ gender: v?.gender,
130
+ supported_model_ids: v?.supported_model_ids,
131
+ byok_required: v?.byok_required,
132
+ preview_url: v?.preview_url
133
+ }));
134
+ return { content: [{ type: "text", text: JSON.stringify({ voices: trimmed, total: trimmed.length }, null, 2) }] };
135
+ }
136
+ );
137
+ server.tool(
138
+ "preview_stack",
139
+ "Preview the resolved per-language model stack (STT/LLM/TTS), blockers, warnings, model diffs, and any required role upgrades for a set of supported languages \u2014 without saving. Use before create/update to confirm a multi-language setup is valid. Note: the concierge (realtime) role is not supported here.",
140
+ {
141
+ supported_languages: import_zod.z.array(import_zod.z.string()).min(1).describe('BCP-47 languages to resolve a stack for, e.g. ["hi-IN","en-US"].'),
142
+ agent_role: import_zod.z.enum(["operator", "specialist", "executive"]).optional().describe("Role to resolve the stack for (default operator)."),
143
+ language: import_zod.z.string().optional().describe("Primary BCP-47 language (defaults to the first supported language)."),
144
+ current_provider_config: import_zod.z.record(import_zod.z.any()).optional().describe("Existing provider_config to diff against, if any.")
145
+ },
146
+ async (params) => {
147
+ const result = await rymi.agents.previewStack(params);
148
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
149
+ }
150
+ );
151
+ if (!isReadOnly) {
152
+ server.tool(
153
+ "create_agent",
154
+ 'Create a new Rymi AI voice agent. Call list_llm_options first to discover valid voice and model values. For a multi-language agent, set supported_languages (e.g. ["hi-IN","en-US"]).',
155
+ {
156
+ name: import_zod.z.string().min(1).describe("Agent display name"),
157
+ ...agentConfigFields
158
+ },
159
+ async (params) => {
160
+ try {
161
+ const result = await rymi.agents.create(foldPromptMode(params));
162
+ return ok(result);
163
+ } catch (err) {
164
+ return fail(err);
165
+ }
166
+ }
167
+ );
168
+ server.tool(
169
+ "update_agent",
170
+ "Update an existing Rymi voice agent's configuration. Only the fields you pass are changed; nullable fields accept null to clear them.",
171
+ {
172
+ agent_id: import_zod.z.string().uuid().describe("The agent UUID to update"),
173
+ name: import_zod.z.string().optional(),
174
+ ...agentConfigFields,
175
+ chat_summary: import_zod.z.string().nullable().optional()
176
+ },
177
+ async ({ agent_id, ...params }) => {
178
+ try {
179
+ const result = await rymi.agents.update(agent_id, foldPromptMode(params));
180
+ return ok(result);
181
+ } catch (err) {
182
+ return fail(err);
183
+ }
184
+ }
185
+ );
186
+ server.tool(
187
+ "delete_agent",
188
+ "Permanently delete a Rymi voice agent.",
189
+ { agent_id: import_zod.z.string().uuid().describe("The agent UUID to delete") },
190
+ async ({ agent_id }) => {
191
+ const result = await rymi.agents.delete(agent_id);
192
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
193
+ }
194
+ );
195
+ server.tool(
196
+ "clone_agent",
197
+ 'Duplicate an existing agent. The copy gets " (Copy)" appended to its name.',
198
+ { agent_id: import_zod.z.string().uuid().describe("The agent UUID to clone") },
199
+ async ({ agent_id }) => {
200
+ const result = await rymi.agents.clone(agent_id);
201
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
202
+ }
203
+ );
204
+ }
205
+ server.tool(
206
+ "list_calls_for_agent",
207
+ "List calls made with a specific agent.",
208
+ {
209
+ agent_id: import_zod.z.string().uuid(),
210
+ limit: import_zod.z.number().int().min(1).max(200).optional(),
211
+ offset: import_zod.z.number().int().min(0).optional(),
212
+ status: import_zod.z.string().optional().describe("Filter by call status")
213
+ },
214
+ async ({ agent_id, ...params }) => {
215
+ const result = await rymi.agents.listCalls(agent_id, params);
216
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
217
+ }
218
+ );
219
+ server.tool(
220
+ "validate_agent_publish",
221
+ "Check whether an agent is ready to go live. Returns a validation report without persisting any changes.",
222
+ {
223
+ agent_id: import_zod.z.string().uuid().optional().describe("Merge validation with a persisted agent"),
224
+ name: import_zod.z.string().optional(),
225
+ voice: import_zod.z.string().optional(),
226
+ persona: import_zod.z.record(import_zod.z.any()).optional(),
227
+ playbook: import_zod.z.record(import_zod.z.any()).optional()
228
+ },
229
+ async (params) => {
230
+ const result = await rymi.agents.validatePublish(params);
231
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
232
+ }
233
+ );
234
+ server.tool(
235
+ "apply_agent_changes",
236
+ "Validate and resolve a flat key/value change-set against the AgentConfig field registry. Does NOT persist \u2014 follow up with update_agent.",
237
+ {
238
+ agent_id: import_zod.z.string().uuid().describe("Agent to load currentConfig from (recommended)"),
239
+ changes: import_zod.z.array(import_zod.z.object({ key: import_zod.z.string(), value: import_zod.z.any() })).describe("Array of {key, value} field changes"),
240
+ mode: import_zod.z.enum(["create", "edit"]).describe("create = new agent, edit = updating existing"),
241
+ lenient: import_zod.z.boolean().optional().describe("Skip unknown-field hard-fail (not recommended)")
242
+ },
243
+ async ({ agent_id, changes, mode, lenient }) => {
244
+ const current = await rymi.agents.retrieve(agent_id);
245
+ const normalizedChanges = changes.map(({ key, value }) => ({ key, value }));
246
+ const result = await rymi.agents.applyChanges({
247
+ currentConfig: current,
248
+ changes: normalizedChanges,
249
+ mode,
250
+ lenient
251
+ });
252
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
253
+ }
254
+ );
255
+ server.tool(
256
+ "enrich_company",
257
+ "Use AI + Google Search grounding to auto-generate a company description from a website URL, suitable for an agent's persona.",
258
+ {
259
+ company_name: import_zod.z.string().min(1),
260
+ website_url: import_zod.z.string().url()
261
+ },
262
+ async ({ company_name, website_url }) => {
263
+ const result = await rymi.agents.enrichCompany({ companyName: company_name, websiteUrl: website_url });
264
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
265
+ }
266
+ );
267
+ server.tool(
268
+ "generate_agent_draft",
269
+ "Use AI to generate an agent configuration draft from a plain-text description.",
270
+ {
271
+ prompt: import_zod.z.string().min(1).describe("Description of the agent you want to create"),
272
+ mode: import_zod.z.enum(["create", "edit"]).optional().default("create"),
273
+ current_config: import_zod.z.record(import_zod.z.any()).optional().describe("Existing config when editing"),
274
+ options: import_zod.z.record(import_zod.z.any()).optional().describe("Override hints e.g. {voice, llm_provider}")
275
+ },
276
+ async (params) => {
277
+ const result = await rymi.agents.generate({ prompt: params.prompt, options: params.options });
278
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
279
+ }
280
+ );
281
+ }
282
+
283
+ // src/tools/calls.ts
284
+ var import_zod2 = require("zod");
285
+
286
+ // src/utils/errors.ts
287
+ function handleMcpError(err) {
288
+ const e = err;
289
+ const body = { error: e?.message || String(err) };
290
+ if (e?.code) body.code = e.code;
291
+ if (e?.status) body.status = e.status;
292
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
293
+ }
294
+ function withToolErrors(handler) {
295
+ return async (args) => {
296
+ try {
297
+ return await handler(args);
298
+ } catch (err) {
299
+ return handleMcpError(err);
300
+ }
301
+ };
302
+ }
303
+
304
+ // src/tools/calls.ts
305
+ function registerCallTools(server, rymi, isReadOnly = false) {
306
+ server.tool(
307
+ "list_calls",
308
+ "List previous and active calls across your account.",
309
+ {
310
+ limit: import_zod2.z.number().int().min(1).max(200).optional().describe("Max calls to return"),
311
+ offset: import_zod2.z.number().int().min(0).optional().describe("Pagination offset"),
312
+ cursor: import_zod2.z.string().optional().describe("Opaque cursor for keyset pagination"),
313
+ status: import_zod2.z.string().optional().describe("Filter by call status (e.g. completed, in_progress, failed)")
314
+ },
315
+ async (params) => {
316
+ const result = await rymi.calls.list(params);
317
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
318
+ }
319
+ );
320
+ server.tool(
321
+ "list_active_calls",
322
+ "List calls currently in progress.",
323
+ {
324
+ limit: import_zod2.z.number().int().min(1).max(200).optional(),
325
+ offset: import_zod2.z.number().int().min(0).optional()
326
+ },
327
+ async (params) => {
328
+ const result = await rymi.calls.active(params);
329
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
330
+ }
331
+ );
332
+ server.tool(
333
+ "get_call",
334
+ "Retrieve details, participants, status, duration, and cost for a single call.",
335
+ { call_id: import_zod2.z.string().describe("The call ID") },
336
+ async ({ call_id }) => {
337
+ const result = await rymi.calls.retrieve(call_id);
338
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
339
+ }
340
+ );
341
+ server.tool(
342
+ "get_call_summary",
343
+ "Retrieve the post-call summary for a call.",
344
+ { call_id: import_zod2.z.string().describe("The call ID") },
345
+ async ({ call_id }) => {
346
+ const result = await rymi.calls.summary(call_id);
347
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
348
+ }
349
+ );
350
+ server.tool(
351
+ "get_call_transcript",
352
+ "Retrieve the full transcript for a call. May contain personal data \u2014 handle accordingly.",
353
+ { call_id: import_zod2.z.string().describe("The call ID") },
354
+ async ({ call_id }) => {
355
+ const result = await rymi.calls.transcript(call_id);
356
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
357
+ }
358
+ );
359
+ server.tool(
360
+ "get_call_recording",
361
+ "Retrieve recording metadata (e.g. playback URL) for a call, when recording is enabled.",
362
+ { call_id: import_zod2.z.string().describe("The call ID") },
363
+ async ({ call_id }) => {
364
+ const result = await rymi.calls.recording(call_id);
365
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
366
+ }
367
+ );
368
+ server.tool(
369
+ "get_call_queue_stats",
370
+ "Retrieve current outbound call queue statistics.",
371
+ {},
372
+ async () => {
373
+ const result = await rymi.calls.queueStats();
374
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
375
+ }
376
+ );
377
+ if (!isReadOnly) {
378
+ server.tool(
379
+ "reprocess_call",
380
+ "Re-run post-call intelligence (summary, extraction, evaluation) for a call.",
381
+ { call_id: import_zod2.z.string().describe("The call ID to reprocess") },
382
+ async ({ call_id }) => {
383
+ const result = await rymi.calls.reprocess(call_id);
384
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
385
+ }
386
+ );
387
+ }
388
+ }
389
+ function registerOutboundCallTools(server, rymi) {
390
+ server.tool(
391
+ "create_call",
392
+ "WARNING: places a real, billable PSTN call. `from_number` is the caller ID shown to the recipient and must be a phone number registered to your account \u2014 omit it to use the agent's attached number. `identity` is the destination in strict E.164 (e.g. +15555550123).",
393
+ {
394
+ agent_id: import_zod2.z.string().uuid().describe("The agent that will handle the call"),
395
+ participants: import_zod2.z.array(import_zod2.z.object({
396
+ transport: import_zod2.z.enum(["webrtc", "pstn"]),
397
+ identity: import_zod2.z.string().describe("Destination phone number (pstn) or participant identity (webrtc)"),
398
+ from_number: import_zod2.z.string().optional().describe("Caller ID / from number for pstn"),
399
+ metadata: import_zod2.z.record(import_zod2.z.any()).optional()
400
+ })).min(1),
401
+ metadata: import_zod2.z.record(import_zod2.z.any()).optional(),
402
+ variables: import_zod2.z.record(import_zod2.z.any()).optional().describe("Playbook variables to seed the call"),
403
+ post_call: import_zod2.z.record(import_zod2.z.any()).optional()
404
+ },
405
+ withToolErrors(async (params) => {
406
+ const result = await rymi.calls.create(params);
407
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
408
+ })
409
+ );
410
+ server.tool(
411
+ "batch_call",
412
+ "Queue up to 500 outbound PSTN recipients in one request. WARNING: places real phone calls and incurs per-minute charges. `from_number` is the caller ID shown to the recipient and must be a phone number registered to your account.",
413
+ {
414
+ agent_id: import_zod2.z.string().uuid().describe("The agent that will handle the calls"),
415
+ to: import_zod2.z.array(import_zod2.z.string()).optional().describe("Simple list of destination phone numbers"),
416
+ recipients: import_zod2.z.array(import_zod2.z.object({
417
+ to: import_zod2.z.string().optional(),
418
+ from_number: import_zod2.z.string().optional(),
419
+ metadata: import_zod2.z.record(import_zod2.z.any()).optional()
420
+ })).optional().describe("Per-recipient targets with optional from_number/metadata"),
421
+ from_number: import_zod2.z.string().optional().describe("Default caller ID / from number"),
422
+ batch_id: import_zod2.z.string().optional(),
423
+ metadata: import_zod2.z.record(import_zod2.z.any()).optional(),
424
+ variables: import_zod2.z.record(import_zod2.z.any()).optional(),
425
+ post_call: import_zod2.z.record(import_zod2.z.any()).optional()
426
+ },
427
+ withToolErrors(async (params) => {
428
+ const result = await rymi.calls.batch(params);
429
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
430
+ })
431
+ );
432
+ }
433
+ function registerCallControlTools(server, rymi) {
434
+ server.tool(
435
+ "end_call",
436
+ "WARNING: immediately terminates an in-progress call. The call transitions to completed and post-call processing runs.",
437
+ { call_id: import_zod2.z.string().describe("The call ID to end") },
438
+ withToolErrors(async ({ call_id }) => {
439
+ const result = await rymi.calls.end(call_id);
440
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
441
+ })
442
+ );
443
+ server.tool(
444
+ "add_call_participant",
445
+ "WARNING: dials/adds participants to a live call (warm transfer / conference). PSTN additions are billable.",
446
+ {
447
+ call_id: import_zod2.z.string().describe("The in-progress call ID"),
448
+ participants: import_zod2.z.array(import_zod2.z.object({
449
+ transport: import_zod2.z.enum(["webrtc", "pstn"]),
450
+ identity: import_zod2.z.string().describe("Destination phone number (pstn, E.164) or participant identity (webrtc)"),
451
+ from_number: import_zod2.z.string().optional().describe("Caller ID for pstn"),
452
+ metadata: import_zod2.z.record(import_zod2.z.any()).optional()
453
+ })).min(1)
454
+ },
455
+ withToolErrors(async ({ call_id, participants }) => {
456
+ const result = await rymi.calls.addParticipants(call_id, { participants });
457
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
458
+ })
459
+ );
460
+ }
461
+
462
+ // src/tools/numbers.ts
463
+ var import_zod3 = require("zod");
464
+ function registerNumberTools(server, rymi, isReadOnly = false) {
465
+ server.tool(
466
+ "list_numbers",
467
+ "List all phone numbers on your Rymi account and which agent each is attached to.",
468
+ {
469
+ limit: import_zod3.z.number().int().min(1).max(500).optional(),
470
+ offset: import_zod3.z.number().int().min(0).optional()
471
+ },
472
+ async (params) => {
473
+ const result = await rymi.numbers.list(params);
474
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
475
+ }
476
+ );
477
+ if (!isReadOnly) {
478
+ server.tool(
479
+ "register_number",
480
+ "Register a phone number on your account, optionally attaching it to an agent.",
481
+ {
482
+ number: import_zod3.z.string().describe("Phone number in E.164 format (e.g. +14155550123)"),
483
+ agent_id: import_zod3.z.string().uuid().optional().describe("Agent to attach the number to on registration")
484
+ },
485
+ async ({ number, agent_id }) => {
486
+ const result = await rymi.numbers.register(number, agent_id ? { agent_id } : {});
487
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
488
+ }
489
+ );
490
+ server.tool(
491
+ "attach_number",
492
+ "Attach an existing number to an agent so inbound calls route to it.",
493
+ {
494
+ number: import_zod3.z.string().describe("Phone number in E.164 format"),
495
+ agent_id: import_zod3.z.string().uuid().describe("Agent to route this number to")
496
+ },
497
+ async ({ number, agent_id }) => {
498
+ const result = await rymi.numbers.attach(number, agent_id);
499
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
500
+ }
501
+ );
502
+ server.tool(
503
+ "remove_number",
504
+ "Remove a phone number from your account. The number stops routing to any agent.",
505
+ { number: import_zod3.z.string().describe("Phone number in E.164 format") },
506
+ async ({ number }) => {
507
+ const result = await rymi.numbers.remove(number);
508
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
509
+ }
510
+ );
511
+ }
512
+ }
513
+
514
+ // src/tools/telephony.ts
515
+ function registerTelephonyTools(server, rymi, isReadOnly = false) {
516
+ server.tool(
517
+ "telephony_status",
518
+ "Report whether a telephony carrier is connected, and which provider/account.",
519
+ {},
520
+ async () => {
521
+ const result = await rymi.telephony.status();
522
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
523
+ }
524
+ );
525
+ server.tool(
526
+ "list_telephony_numbers",
527
+ "List numbers available on the connected telephony carrier account.",
528
+ {},
529
+ async () => {
530
+ const result = await rymi.telephony.numbers();
531
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
532
+ }
533
+ );
534
+ }
535
+
536
+ // src/tools/keys.ts
537
+ function registerKeyTools(server, rymi, isReadOnly = false) {
538
+ server.tool(
539
+ "list_publishable_keys",
540
+ "List publishable (browser-safe) keys and which agent/channels each is scoped to. Returns key prefixes only, never full secrets.",
541
+ {},
542
+ async () => {
543
+ const result = await rymi.keys.listPublishable();
544
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
545
+ }
546
+ );
547
+ }
548
+
549
+ // src/tools/knowledge.ts
550
+ var import_zod4 = require("zod");
551
+ function registerKnowledgeTools(server, rymi, isReadOnly = false) {
552
+ server.tool(
553
+ "list_knowledge_sources",
554
+ "List the knowledge sources (RAG context) attached to an agent.",
555
+ { agent_id: import_zod4.z.string().uuid().describe("The agent UUID") },
556
+ async ({ agent_id }) => {
557
+ const result = await rymi.agents.listKnowledgeSources(agent_id);
558
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
559
+ }
560
+ );
561
+ server.tool(
562
+ "list_agent_changes",
563
+ "List recorded configuration changes for an agent (for auditing or before an undo).",
564
+ {
565
+ agent_id: import_zod4.z.string().uuid().describe("The agent UUID"),
566
+ since: import_zod4.z.string().optional().describe("ISO timestamp \u2014 only return changes after this time")
567
+ },
568
+ async ({ agent_id, since }) => {
569
+ const result = await rymi.agents.listChanges(agent_id, since ? { since } : {});
570
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
571
+ }
572
+ );
573
+ if (!isReadOnly) {
574
+ server.tool(
575
+ "add_knowledge_source",
576
+ "Add a knowledge source to an agent from raw text or a URL (the URL is fetched and ingested).",
577
+ {
578
+ agent_id: import_zod4.z.string().uuid().describe("The agent UUID"),
579
+ kind: import_zod4.z.enum(["text", "url"]).describe("Source type"),
580
+ title: import_zod4.z.string().min(1).describe("Human-readable title for the source"),
581
+ text: import_zod4.z.string().optional().describe('Required when kind="text": the raw content to ingest'),
582
+ url: import_zod4.z.string().url().optional().describe('Required when kind="url": the page to fetch and ingest')
583
+ },
584
+ async ({ agent_id, kind, title, text, url }) => {
585
+ const data = kind === "text" ? { kind: "text", title, text: text ?? "" } : { kind: "url", title, url: url ?? "" };
586
+ const result = await rymi.agents.addKnowledgeSource(agent_id, data);
587
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
588
+ }
589
+ );
590
+ server.tool(
591
+ "delete_knowledge_source",
592
+ "Delete a knowledge source from an agent.",
593
+ {
594
+ agent_id: import_zod4.z.string().uuid().describe("The agent UUID"),
595
+ source_id: import_zod4.z.string().describe("The knowledge source ID to delete")
596
+ },
597
+ async ({ agent_id, source_id }) => {
598
+ const result = await rymi.agents.deleteKnowledgeSource(agent_id, source_id);
599
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
600
+ }
601
+ );
602
+ server.tool(
603
+ "undo_agent_change",
604
+ "Undo a single recorded configuration change, reverting that field to its previous value.",
605
+ {
606
+ agent_id: import_zod4.z.string().uuid().describe("The agent UUID"),
607
+ change_id: import_zod4.z.string().describe("The change_id to undo (from list_agent_changes)")
608
+ },
609
+ async ({ agent_id, change_id }) => {
610
+ const result = await rymi.agents.undoChange(agent_id, change_id);
611
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
612
+ }
613
+ );
614
+ }
615
+ }
616
+
617
+ // src/tools/insights.ts
618
+ var import_zod5 = require("zod");
619
+ function registerInsightTools(server, rymi, isReadOnly = false) {
620
+ server.tool(
621
+ "get_usage_summary",
622
+ "Get this account's usage summary: remaining voice-runtime MINUTES, Studio AI unit usage, and post-call intelligence usage. Voice balance is reported in minutes, not dollars.",
623
+ {},
624
+ async () => {
625
+ const result = await rymi.billing.usageSummary();
626
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
627
+ }
628
+ );
629
+ server.tool(
630
+ "list_agent_templates",
631
+ "List published agent templates. Use a template's `defaults` as the starting config for create_agent.",
632
+ {},
633
+ async () => {
634
+ const result = await rymi.templates.list();
635
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
636
+ }
637
+ );
638
+ server.tool(
639
+ "list_eval_runs",
640
+ "List previous evaluation runs for an agent.",
641
+ { agent_id: import_zod5.z.string().uuid().describe("The agent UUID") },
642
+ async ({ agent_id }) => {
643
+ const result = await rymi.agents.listEvalRuns(agent_id);
644
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
645
+ }
646
+ );
647
+ server.tool(
648
+ "get_eval_run",
649
+ "Retrieve a single evaluation run, including per-scenario scores.",
650
+ {
651
+ agent_id: import_zod5.z.string().uuid().describe("The agent UUID"),
652
+ run_id: import_zod5.z.string().describe("The evaluation run ID")
653
+ },
654
+ async ({ agent_id, run_id }) => {
655
+ const result = await rymi.agents.getEvalRun(agent_id, run_id);
656
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
657
+ }
658
+ );
659
+ if (!isReadOnly) {
660
+ server.tool(
661
+ "run_evals",
662
+ 'Run the evaluation suite for an agent. mode="synthetic" (default) uses the offline scorer; mode="live" runs the model-driven runner (consumes Studio AI units).',
663
+ {
664
+ agent_id: import_zod5.z.string().uuid().describe("The agent UUID to evaluate"),
665
+ mode: import_zod5.z.enum(["synthetic", "live"]).optional().describe("Evaluation mode (default synthetic)")
666
+ },
667
+ async ({ agent_id, mode }) => {
668
+ const result = await rymi.agents.runEvals(agent_id, mode ? { mode } : {});
669
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
670
+ }
671
+ );
672
+ }
673
+ }
674
+
675
+ // src/tools/publish.ts
676
+ var import_zod6 = require("zod");
677
+ function registerPublishTool(server, rymi) {
678
+ server.tool(
679
+ "publish_agent",
680
+ "Publish a Rymi voice agent to make it live. IMPORTANT: this immediately makes the agent callable by end users.",
681
+ {
682
+ agent_id: import_zod6.z.string().uuid().describe("The agent UUID to publish")
683
+ },
684
+ async ({ agent_id }) => {
685
+ const validation = await rymi.agents.validatePublish({ agent_id });
686
+ if (!validation.valid) {
687
+ return {
688
+ content: [{
689
+ type: "text",
690
+ text: JSON.stringify({ published: false, reason: "Validation failed", details: validation }, null, 2)
691
+ }]
692
+ };
693
+ }
694
+ return {
695
+ content: [{
696
+ type: "text",
697
+ text: JSON.stringify({
698
+ published: false,
699
+ reason: "publish_agent requires PUT /agents/:id status field (coming soon). Validation passed \u2014 agent is ready to publish from the Rymi dashboard.",
700
+ agent_id,
701
+ validation
702
+ }, null, 2)
703
+ }]
704
+ };
705
+ }
706
+ );
707
+ }
708
+
709
+ // src/tools/dnc.ts
710
+ var import_zod7 = require("zod");
711
+ function registerDncTools(server, rymi, isReadOnly) {
712
+ server.tool(
713
+ "list_dnc",
714
+ "List all numbers on your Do-Not-Call registry.",
715
+ {
716
+ limit: import_zod7.z.number().int().min(1).max(500).optional(),
717
+ offset: import_zod7.z.number().int().min(0).optional()
718
+ },
719
+ withToolErrors(async (params) => {
720
+ const result = await rymi.dnc.list(params);
721
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
722
+ })
723
+ );
724
+ server.tool(
725
+ "check_dnc",
726
+ "Check whether phone numbers are on the Do-Not-Call registry. Read-only \u2014 adds nothing.",
727
+ { phone_numbers: import_zod7.z.array(import_zod7.z.string()).min(1).max(500).describe("E.164 numbers to check") },
728
+ withToolErrors(async ({ phone_numbers }) => {
729
+ const result = await rymi.dnc.check({ phone_numbers });
730
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
731
+ })
732
+ );
733
+ if (isReadOnly) return;
734
+ server.tool(
735
+ "add_dnc",
736
+ "Add a phone number to the Do-Not-Call registry so outbound calls to it are blocked.",
737
+ {
738
+ phone_number: import_zod7.z.string().describe("Phone number (any format; normalized to E.164)"),
739
+ reason: import_zod7.z.string().optional()
740
+ },
741
+ withToolErrors(async ({ phone_number, reason }) => {
742
+ const result = await rymi.dnc.add({ phone_number, reason });
743
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
744
+ })
745
+ );
746
+ server.tool(
747
+ "add_dnc_batch",
748
+ "Add up to 1000 numbers to the Do-Not-Call registry. Invalid numbers are skipped and returned in `invalid`.",
749
+ {
750
+ phone_numbers: import_zod7.z.array(import_zod7.z.string()).min(1).max(1e3),
751
+ reason: import_zod7.z.string().optional()
752
+ },
753
+ withToolErrors(async ({ phone_numbers, reason }) => {
754
+ const result = await rymi.dnc.addBatch({ phone_numbers, reason });
755
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
756
+ })
757
+ );
758
+ server.tool(
759
+ "remove_dnc",
760
+ "WARNING: removes a number from the Do-Not-Call registry, re-enabling outbound calls to it.",
761
+ { phone: import_zod7.z.string().describe("Phone number to remove") },
762
+ withToolErrors(async ({ phone }) => {
763
+ const result = await rymi.dnc.remove(phone);
764
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
765
+ })
766
+ );
767
+ }
768
+
769
+ // src/tools/webhooks.ts
770
+ var import_zod8 = require("zod");
771
+ var import_crypto = require("crypto");
772
+ function registerWebhookTools(server, rymi, isReadOnly) {
773
+ server.tool(
774
+ "list_webhooks",
775
+ "List your registered webhook endpoints. Signing secrets are never returned.",
776
+ {
777
+ limit: import_zod8.z.number().int().min(1).max(500).optional(),
778
+ offset: import_zod8.z.number().int().min(0).optional()
779
+ },
780
+ withToolErrors(async (params) => {
781
+ const result = await rymi.webhooks.list(params);
782
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
783
+ })
784
+ );
785
+ if (isReadOnly) return;
786
+ server.tool(
787
+ "create_webhook",
788
+ "Register a webhook to receive call lifecycle events (e.g. call.completed, transcript.ready). URL must be public https. If you omit `secret`, one is generated and shown ONCE in the response \u2014 store it to verify signatures.",
789
+ {
790
+ url: import_zod8.z.string().url().describe("Public https endpoint"),
791
+ events: import_zod8.z.array(import_zod8.z.string()).min(1).describe("Event names to subscribe to"),
792
+ secret: import_zod8.z.string().min(16).max(256).optional().describe("Signing secret; auto-generated if omitted"),
793
+ alert_email: import_zod8.z.string().email().optional()
794
+ },
795
+ withToolErrors(async ({ url, events, secret, alert_email }) => {
796
+ const finalSecret = secret ?? (0, import_crypto.randomBytes)(24).toString("hex");
797
+ const result = await rymi.webhooks.create({ url, events, secret: finalSecret, alert_email });
798
+ const echoed = secret ? result : { ...result, secret: finalSecret, secret_notice: "Store this secret now \u2014 it cannot be retrieved later." };
799
+ return { content: [{ type: "text", text: JSON.stringify(echoed, null, 2) }] };
800
+ })
801
+ );
802
+ server.tool(
803
+ "update_webhook",
804
+ "Update a webhook endpoint. Only provided fields change. Pass `secret` to rotate it.",
805
+ {
806
+ id: import_zod8.z.string(),
807
+ url: import_zod8.z.string().url().optional(),
808
+ events: import_zod8.z.array(import_zod8.z.string()).min(1).optional(),
809
+ secret: import_zod8.z.string().min(16).max(256).optional(),
810
+ alert_email: import_zod8.z.string().email().nullable().optional()
811
+ },
812
+ withToolErrors(async ({ id, ...rest }) => {
813
+ const result = await rymi.webhooks.update(id, rest);
814
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
815
+ })
816
+ );
817
+ server.tool(
818
+ "delete_webhook",
819
+ "WARNING: stops event delivery to this endpoint.",
820
+ { id: import_zod8.z.string() },
821
+ withToolErrors(async ({ id }) => {
822
+ const result = await rymi.webhooks.delete(id);
823
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
824
+ })
825
+ );
826
+ }
827
+
828
+ // src/tools/billing.ts
829
+ var import_zod9 = require("zod");
830
+ function registerBillingControlTools(server, rymi, isReadOnly) {
831
+ server.tool(
832
+ "estimate_call_cost",
833
+ "Estimate how much balance a call will consume for a given tier and duration. Results are usage estimates; surface remaining balance to customers in minutes.",
834
+ {
835
+ tier: import_zod9.z.enum(["operator", "specialist", "executive", "concierge"]).optional(),
836
+ duration_seconds: import_zod9.z.number().min(0).optional()
837
+ },
838
+ withToolErrors(async (params) => {
839
+ const result = await rymi.billing.estimate(params);
840
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
841
+ })
842
+ );
843
+ if (isReadOnly) return;
844
+ server.tool(
845
+ "set_auto_recharge",
846
+ "Configure auto-recharge. `pack_usd` must exceed `threshold_usd` or the API rejects it (recharge-loop guard).",
847
+ {
848
+ enabled: import_zod9.z.boolean().optional(),
849
+ pack_usd: import_zod9.z.number().min(1).max(1e3).optional(),
850
+ threshold_usd: import_zod9.z.number().min(0).max(100).optional()
851
+ },
852
+ withToolErrors(async (params) => {
853
+ const result = await rymi.billing.setAutoRecharge(params);
854
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
855
+ })
856
+ );
857
+ server.tool(
858
+ "set_spend_alerts",
859
+ "Configure spend-alert thresholds and low-balance / email preferences.",
860
+ {
861
+ thresholds_usd: import_zod9.z.array(import_zod9.z.number()).max(10).optional(),
862
+ low_balance_pct: import_zod9.z.number().min(0).max(100).optional(),
863
+ email_enabled: import_zod9.z.boolean().optional()
864
+ },
865
+ withToolErrors(async (params) => {
866
+ const result = await rymi.billing.setAlerts(params);
867
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
868
+ })
869
+ );
870
+ }
871
+
872
+ // src/server.ts
873
+ function createServer(apiKey) {
874
+ const rymi = new import_node.default({ apiKey });
875
+ const server = new import_mcp.McpServer({
876
+ name: "rymi",
877
+ version: "0.5.0"
878
+ });
879
+ const isReadOnly = process.env.RYMI_MCP_READONLY === "1";
880
+ registerAgentTools(server, rymi, isReadOnly);
881
+ registerCallTools(server, rymi, isReadOnly);
882
+ registerNumberTools(server, rymi, isReadOnly);
883
+ registerTelephonyTools(server, rymi, isReadOnly);
884
+ registerKeyTools(server, rymi, isReadOnly);
885
+ registerKnowledgeTools(server, rymi, isReadOnly);
886
+ registerInsightTools(server, rymi, isReadOnly);
887
+ registerDncTools(server, rymi, isReadOnly);
888
+ registerWebhookTools(server, rymi, isReadOnly);
889
+ registerBillingControlTools(server, rymi, isReadOnly);
890
+ if (!isReadOnly) {
891
+ registerCallControlTools(server, rymi);
892
+ registerOutboundCallTools(server, rymi);
893
+ registerPublishTool(server, rymi);
894
+ }
895
+ return server;
896
+ }
897
+
898
+ // src/transport/stdio.ts
899
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
900
+ async function runStdio(server) {
901
+ const transport2 = new import_stdio.StdioServerTransport();
902
+ await server.connect(transport2);
903
+ }
904
+
905
+ // src/transport/http.ts
906
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
907
+ var import_http = require("http");
908
+ var PORT = parseInt(process.env.RYMI_MCP_PORT || "8787", 10);
909
+ async function runHttp() {
910
+ const httpServer = (0, import_http.createServer)(async (req, res) => {
911
+ if (req.url === "/health") {
912
+ res.writeHead(200, { "Content-Type": "application/json" });
913
+ res.end(JSON.stringify({ ok: true }));
914
+ return;
915
+ }
916
+ const authHeader = req.headers["authorization"] || "";
917
+ const apiKey = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
918
+ if (!apiKey) {
919
+ res.writeHead(401, { "Content-Type": "application/json" });
920
+ res.end(JSON.stringify({ error: "Missing Authorization: Bearer <RYMI_API_KEY>" }));
921
+ return;
922
+ }
923
+ const server = createServer(apiKey);
924
+ const transport2 = new import_streamableHttp.StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
925
+ await server.connect(transport2);
926
+ await transport2.handleRequest(req, res);
927
+ });
928
+ httpServer.listen(PORT, () => {
929
+ process.stderr.write(`Rymi MCP HTTP server listening on port ${PORT}
930
+ `);
931
+ });
932
+ }
933
+
934
+ // src/index.ts
935
+ var transport = process.argv.includes("--transport") ? process.argv[process.argv.indexOf("--transport") + 1] : "stdio";
936
+ if (transport === "http") {
937
+ runHttp().catch((err) => {
938
+ process.stderr.write(`Fatal: ${err.message}
939
+ `);
940
+ process.exit(1);
941
+ });
942
+ } else {
943
+ const apiKey = process.env.RYMI_API_KEY || "";
944
+ if (!apiKey) {
945
+ process.stderr.write("Error: RYMI_API_KEY environment variable is required.\n");
946
+ process.exit(1);
947
+ }
948
+ const server = createServer(apiKey);
949
+ runStdio(server).catch((err) => {
950
+ process.stderr.write(`Fatal: ${err.message}
951
+ `);
952
+ process.exit(1);
953
+ });
954
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@rymi/mcp",
3
+ "version": "0.6.0",
4
+ "description": "Rymi MCP server — manage AI voice agents via Model Context Protocol",
5
+ "license": "MIT",
6
+ "author": "Rymi AI <engineering@rymi.live>",
7
+ "homepage": "https://rymi.live",
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "voice-ai",
12
+ "voice-agent",
13
+ "claude",
14
+ "cursor",
15
+ "rymi"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/rymi-live/rymi-mcp.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/rymi-live/rymi-mcp/issues"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "provenance": true
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "bin": {
31
+ "rymi-mcp": "./dist/index.js"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "require": "./dist/index.js"
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format cjs --dts --clean",
41
+ "dev": "tsup src/index.ts --format cjs --watch",
42
+ "lint": "tsc --noEmit",
43
+ "test": "vitest run"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.11.1",
47
+ "@rymi/node": "workspace:*",
48
+ "zod": "^3.23.0"
49
+ },
50
+ "devDependencies": {
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.0.0",
53
+ "vitest": "^1.6.0"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "LICENSE"
59
+ ]
60
+ }