@saperly/mcp
Advanced tools
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import type { Saperly } from "@saperly/sdk"; | ||
| export declare function registerMessagesTools(server: McpServer, client: Saperly): void; |
| import { z } from "zod"; | ||
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerMessagesTools(server, client) { | ||
| server.tool("saperly_send_sms", "reply to an SMS conversation. only works within 24 hours of receiving an inbound SMS from the recipient.", { | ||
| lineId: z.string().describe("line id to send from"), | ||
| to: z.string().describe("recipient phone number (E.164, e.g. +14155551234)"), | ||
| text: z.string().describe("message text"), | ||
| }, async (args) => { | ||
| try { | ||
| const msg = await client.messages.send(args); | ||
| return toolResult(`SMS sent!\nid: ${msg.id}\nto: ${msg.to}\nstatus: ${msg.status}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| server.tool("saperly_list_conversations", "list SMS conversations grouped by contact. shows most recent message and count.", { | ||
| lineId: z.string().optional().describe("filter by line (optional)"), | ||
| limit: z.number().optional().describe("max results (default 20)"), | ||
| }, async (args) => { | ||
| try { | ||
| const result = await client.conversations.list(args); | ||
| if (result.conversations.length === 0) { | ||
| return toolResult("no conversations found."); | ||
| } | ||
| const list = result.conversations | ||
| .map((c) => ` ${c.phoneNumber} ${c.messageCount} msgs last: "${c.lastMessageText ?? "\u2014"}" (${c.lastMessageDirection})`) | ||
| .join("\n"); | ||
| return toolResult(`${result.conversations.length} conversation(s):\n\n${list}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| server.tool("saperly_get_conversation", "get full SMS message history for a conversation with a specific contact.", { | ||
| lineId: z.string().describe("line id"), | ||
| phoneNumber: z.string().describe("contact phone number (E.164)"), | ||
| limit: z.number().optional().describe("max messages (default 50)"), | ||
| }, async (args) => { | ||
| try { | ||
| const result = await client.conversations.messages(args.lineId, args.phoneNumber, { | ||
| limit: args.limit, | ||
| }); | ||
| if (result.messages.length === 0) { | ||
| return toolResult("no messages in this conversation."); | ||
| } | ||
| const msgs = result.messages | ||
| .map((m) => ` [${m.direction}] ${m.text} (${m.timestamp})`) | ||
| .join("\n"); | ||
| return toolResult(`${result.messages.length} message(s):\n\n${msgs}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import type { Saperly } from "@saperly/sdk"; | ||
| export declare function registerSettingsTools(server: McpServer, client: Saperly): void; |
| import { z } from "zod"; | ||
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerSettingsTools(server, client) { | ||
| server.tool("saperly_get_settings", "get your account settings. currently shows default webhook URL.", {}, async () => { | ||
| try { | ||
| const settings = await client.settings.get(); | ||
| const parts = ["account settings:"]; | ||
| parts.push(` default webhook: ${settings.defaultWebhookUrl ?? "(not set)"}`); | ||
| return toolResult(parts.join("\n")); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| server.tool("saperly_update_settings", "update account settings. set or clear the default webhook URL used for new lines.", { | ||
| defaultWebhookUrl: z | ||
| .string() | ||
| .optional() | ||
| .describe("default webhook URL for new text-mode lines. omit to clear."), | ||
| }, async (args) => { | ||
| try { | ||
| const settings = await client.settings.update({ | ||
| defaultWebhookUrl: args.defaultWebhookUrl ?? null, | ||
| }); | ||
| return toolResult(`settings updated!\n default webhook: ${settings.defaultWebhookUrl ?? "(not set)"}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import type { Saperly } from "@saperly/sdk"; | ||
| export declare function registerUsageTools(server: McpServer, client: Saperly): void; |
| import { z } from "zod"; | ||
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerUsageTools(server, client) { | ||
| server.tool("saperly_get_usage", "get usage statistics. shows calls, minutes, SMS counts, and costs by day or month.", { | ||
| period: z | ||
| .enum(["daily", "monthly"]) | ||
| .optional() | ||
| .describe("aggregation period (default: daily)"), | ||
| count: z | ||
| .number() | ||
| .optional() | ||
| .describe("number of periods (default 7 for daily, 3 for monthly)"), | ||
| }, async (args) => { | ||
| try { | ||
| const period = args.period ?? "daily"; | ||
| if (period === "daily") { | ||
| const result = await client.usage.daily({ days: args.count ?? 7 }); | ||
| if (result.daily.length === 0) | ||
| return toolResult("no usage data yet."); | ||
| const lines = result.daily | ||
| .map((d) => ` ${d.date} ${d.calls} calls ${d.minutes} min $${(d.costCents / 100).toFixed(2)}`) | ||
| .join("\n"); | ||
| return toolResult(`daily usage:\n\n${lines}`); | ||
| } | ||
| else { | ||
| const result = await client.usage.monthly({ months: args.count ?? 3 }); | ||
| if (result.monthly.length === 0) | ||
| return toolResult("no usage data yet."); | ||
| const lines = result.monthly | ||
| .map((m) => ` ${m.month} ${m.calls} calls ${m.minutes} min $${(m.costCents / 100).toFixed(2)}`) | ||
| .join("\n"); | ||
| return toolResult(`monthly usage:\n\n${lines}`); | ||
| } | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import type { Saperly } from "@saperly/sdk"; | ||
| export declare function registerVoicesTools(server: McpServer, client: Saperly): void; |
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerVoicesTools(server, client) { | ||
| server.tool("saperly_list_voices", "list available TTS voices for hosted-mode calls. use the voice id when creating or updating a line.", {}, async () => { | ||
| try { | ||
| const result = await client.voices.list(); | ||
| if (result.voices.length === 0) { | ||
| return toolResult("no voices available."); | ||
| } | ||
| const list = result.voices | ||
| .map((v) => ` ${v.id} ${v.name} ${v.gender} ${v.accent} ${v.style}`) | ||
| .join("\n"); | ||
| return toolResult(`${result.voices.length} voice(s):\n\n${list}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
| import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import type { Saperly } from "@saperly/sdk"; | ||
| export declare function registerWebhookTools(server: McpServer, client: Saperly): void; |
| import { z } from "zod"; | ||
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerWebhookTools(server, client) { | ||
| server.tool("saperly_webhook_deliveries", "list recent webhook delivery attempts. shows status, duration, errors for each delivery.", { | ||
| lineId: z.string().optional().describe("filter by line id"), | ||
| eventType: z.string().optional().describe("filter: call_started, message, call_ended, sms_received, test"), | ||
| status: z.string().optional().describe("filter: success, failed"), | ||
| limit: z.number().optional().describe("max results (default 50, max 100)"), | ||
| offset: z.number().optional().describe("pagination offset"), | ||
| }, async (args) => { | ||
| try { | ||
| const result = await client.webhooks.deliveries(args); | ||
| if (result.deliveries.length === 0) | ||
| return toolResult("no webhook deliveries found."); | ||
| const list = result.deliveries | ||
| .map((d) => ` ${d.createdAt.slice(0, 16)} ${d.eventType.padEnd(14)} ${d.status.padEnd(7)} ${d.durationMs ?? "?"}ms HTTP ${d.httpStatus ?? "N/A"}`) | ||
| .join("\n"); | ||
| return toolResult(`${result.total} delivery(ies):\n\n${list}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| server.tool("saperly_webhook_stats", "get aggregate webhook delivery statistics.", { lineId: z.string().optional().describe("filter by line id") }, async (args) => { | ||
| try { | ||
| const stats = await client.webhooks.stats(args); | ||
| let text = `total: ${stats.total}, success: ${stats.success}, failed: ${stats.failed}, rate: ${stats.successRate}%`; | ||
| if (stats.byEventType.length > 0) { | ||
| text += "\n\nby event type:"; | ||
| for (const et of stats.byEventType) | ||
| text += `\n ${et.eventType.padEnd(14)} total: ${et.total} success: ${et.success} failed: ${et.failed}`; | ||
| } | ||
| if (stats.byHour.length > 0) { | ||
| text += "\n\nlast 24h by hour:"; | ||
| for (const h of stats.byHour) | ||
| text += `\n ${h.hour.slice(11, 16)} total: ${h.total} success: ${h.success} failed: ${h.failed}`; | ||
| } | ||
| return toolResult(text); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| server.tool("saperly_webhook_test", "send a test webhook to a line's configured URL.", { lineId: z.string().describe("line id to test") }, async (args) => { | ||
| try { | ||
| const result = await client.webhooks.test(args); | ||
| const d = result.delivery; | ||
| let text = `test result: ${d.status}\n HTTP: ${d.httpStatus ?? "N/A"}\n duration: ${d.durationMs}ms`; | ||
| if (d.responseBody) | ||
| text += `\n response: ${d.responseBody.slice(0, 200)}`; | ||
| return toolResult(text); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
+21
| MIT License | ||
| Copyright (c) 2026 Saperly | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
+10
-0
@@ -12,2 +12,7 @@ #!/usr/bin/env node | ||
| import { registerAccountTools } from "./tools/account.js"; | ||
| import { registerWebhookTools } from "./tools/webhooks.js"; | ||
| import { registerMessagesTools } from "./tools/messages.js"; | ||
| import { registerUsageTools } from "./tools/usage.js"; | ||
| import { registerSettingsTools } from "./tools/settings.js"; | ||
| import { registerVoicesTools } from "./tools/voices.js"; | ||
| const apiKey = process.env.SAPERLY_API_KEY; | ||
@@ -35,3 +40,8 @@ if (!apiKey) { | ||
| registerAccountTools(server, client); | ||
| registerWebhookTools(server, client); | ||
| registerMessagesTools(server, client); | ||
| registerUsageTools(server, client); | ||
| registerSettingsTools(server, client); | ||
| registerVoicesTools(server, client); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); |
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerAccountTools(server, client) { | ||
| server.tool("saperly_account_overview", "get a full snapshot of your saperly account: all lines, balance, and 5 most recent calls. use this first to understand current state.", {}, async () => { | ||
| server.tool("saperly_account_overview", "get a full snapshot of your saperly account: all lines, balance, usage, and 5 most recent calls. use this first to understand current state.", {}, async () => { | ||
| try { | ||
@@ -21,5 +21,29 @@ const [lines, callsResult, balanceResult] = await Promise.all([ | ||
| : callsResult.calls | ||
| .map((c) => ` ${c.createdAt.slice(0, 16)} ${c.direction} ${c.fromNumber} → ${c.toNumber} ${c.status}`) | ||
| .map((c) => ` ${c.createdAt.slice(0, 16)} ${c.direction} ${c.fromNumber} \u2192 ${c.toNumber} ${c.status}`) | ||
| .join("\n"); | ||
| return toolResult(`saperly account overview\n\n${balanceText}\n\nlines (${lines.length}):\n${linesList}\n\nrecent calls (${callsResult.total} total):\n${callsList}`); | ||
| const parts = [ | ||
| `saperly account overview\n\n${balanceText}\n\nlines (${lines.length}):\n${linesList}\n\nrecent calls (${callsResult.total} total):\n${callsList}`, | ||
| ]; | ||
| try { | ||
| const usage = await client.usage.daily({ days: 7 }); | ||
| if (usage.daily.length > 0) { | ||
| const totalCalls = usage.daily.reduce((sum, d) => sum + d.calls, 0); | ||
| const totalMinutes = usage.daily.reduce((sum, d) => sum + d.minutes, 0); | ||
| const totalCost = usage.daily.reduce((sum, d) => sum + d.costCents, 0); | ||
| parts.push(`\n\u2014 last 7 days \u2014\n${totalCalls} calls, ${totalMinutes} minutes, $${(totalCost / 100).toFixed(2)}`); | ||
| } | ||
| } | ||
| catch { | ||
| /* usage not critical for overview */ | ||
| } | ||
| try { | ||
| const convos = await client.conversations.list({ limit: 1 }); | ||
| if (convos.conversations.length > 0 || convos.hasMore) { | ||
| parts.push(`\nSMS conversations: active`); | ||
| } | ||
| } | ||
| catch { | ||
| /* not critical */ | ||
| } | ||
| return toolResult(parts.join("")); | ||
| } | ||
@@ -26,0 +50,0 @@ catch (err) { |
@@ -0,5 +1,6 @@ | ||
| import { z } from "zod"; | ||
| import { NotFoundError } from "@saperly/sdk"; | ||
| import { toolResult, toolError } from "./utils.js"; | ||
| export function registerBillingTools(server, client) { | ||
| server.tool("saperly_get_balance", "check your account credit balance.", {}, async () => { | ||
| server.tool("saperly_get_balance", "check your credit balance. calls cost $0.11/min (webhook mode) or $0.20/min (hosted mode). numbers are $2/mo.", {}, async () => { | ||
| try { | ||
@@ -17,2 +18,48 @@ const balance = await client.billing.balance(); | ||
| }); | ||
| server.tool("saperly_add_funds", "add credits to your saperly account. returns a checkout url. amounts: $10 (1000), $25 (2500), $50 (5000), $100 (10000).", { | ||
| amount_cents: z | ||
| .number() | ||
| .describe("amount in cents: 1000, 2500, 5000, or 10000"), | ||
| }, async ({ amount_cents }) => { | ||
| try { | ||
| const result = await client.billing.addFunds({ | ||
| amountCents: amount_cents, | ||
| }); | ||
| return toolResult(`checkout ready!\n\nopen this url to complete your purchase:\n${result.checkoutUrl}\n\namount: $${(amount_cents / 100).toFixed(2)}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| server.tool("saperly_list_transactions", "list recent billing transactions: credits, charges, refunds. shows amount, type, and running balance.", { | ||
| limit: z | ||
| .number() | ||
| .optional() | ||
| .describe("number of transactions to return (1-100, default 20)"), | ||
| cursor: z | ||
| .string() | ||
| .optional() | ||
| .describe("iso date cursor for pagination from a previous response"), | ||
| }, async ({ limit, cursor }) => { | ||
| try { | ||
| const result = await client.billing.transactions({ limit, cursor }); | ||
| if (result.transactions.length === 0) { | ||
| return toolResult("no transactions found."); | ||
| } | ||
| const lines = result.transactions.map((t) => { | ||
| const isDebit = t.type === "call_charge" || t.type === "number_fee"; | ||
| const sign = isDebit ? "-" : "+"; | ||
| const dollars = (t.amountCents / 100).toFixed(2); | ||
| const balDollars = (t.balanceAfterCents / 100).toFixed(2); | ||
| return ` ${t.createdAt.slice(0, 16)} ${sign}$${dollars} ${t.type.replace(/_/g, " ")} bal: $${balDollars}`; | ||
| }); | ||
| const footer = result.hasMore | ||
| ? `\n\n(more available — use cursor: "${result.nextCursor}")` | ||
| : ""; | ||
| return toolResult(`${result.transactions.length} transaction(s):\n\n${lines.join("\n")}${footer}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
+62
-0
@@ -11,5 +11,9 @@ import { z } from "zod"; | ||
| c.durationSec != null ? `duration: ${c.durationSec}s` : null, | ||
| c.recordingUrl ? `recording: ${c.recordingUrl}` : null, | ||
| c.startedAt ? `started: ${c.startedAt}` : null, | ||
| c.endedAt ? `ended: ${c.endedAt}` : null, | ||
| `created: ${c.createdAt}`, | ||
| c.transcript && Array.isArray(c.transcript) && c.transcript.length > 0 | ||
| ? `transcript: ${c.transcript.length} turns` | ||
| : null, | ||
| ] | ||
@@ -76,2 +80,60 @@ .filter(Boolean) | ||
| }); | ||
| server.tool("saperly_conversation_call", "make an AI phone call. saperly runs the LLM with your instructions. returns the full transcript when the call ends. no webhook or backend needed.", { | ||
| lineId: z.string().describe("line to call from"), | ||
| toNumber: z | ||
| .string() | ||
| .describe("phone number to call (E.164 format, e.g. +15551234567)"), | ||
| topic: z | ||
| .string() | ||
| .describe("instructions for the AI agent. what should it accomplish on this call?"), | ||
| beginMessage: z | ||
| .string() | ||
| .optional() | ||
| .describe("first thing the agent says when the call connects"), | ||
| maxDurationSeconds: z | ||
| .number() | ||
| .optional() | ||
| .describe("maximum call duration. default 300 (5 minutes)"), | ||
| }, async (args) => { | ||
| try { | ||
| const call = await client.calls.conversation({ | ||
| lineId: args.lineId, | ||
| toNumber: args.toNumber, | ||
| topic: args.topic, | ||
| beginMessage: args.beginMessage, | ||
| maxDurationSeconds: args.maxDurationSeconds, | ||
| }); | ||
| const maxDuration = (args.maxDurationSeconds ?? 300) * 1000 + 30_000; | ||
| const startTime = Date.now(); | ||
| let result = call; | ||
| while (Date.now() - startTime < maxDuration) { | ||
| result = await client.calls.get(call.id); | ||
| if (["completed", "failed", "no_answer"].includes(result.status)) | ||
| break; | ||
| await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
| } | ||
| const parts = [ | ||
| `call_id: ${result.id}`, | ||
| `status: ${result.status}`, | ||
| result.durationSec != null ? `duration: ${result.durationSec}s` : null, | ||
| result.recordingUrl ? `recording: ${result.recordingUrl}` : null, | ||
| ].filter(Boolean); | ||
| if (result.transcript && | ||
| Array.isArray(result.transcript) && | ||
| result.transcript.length > 0) { | ||
| parts.push("\ntranscript:"); | ||
| for (const turn of result.transcript) { | ||
| const t = turn; | ||
| parts.push(` [${String(t.role ?? "unknown")}]: ${String(t.text ?? "")}`); | ||
| } | ||
| } | ||
| else if (result.status === "completed") { | ||
| parts.push("\n(no transcript available)"); | ||
| } | ||
| return toolResult(parts.join("\n")); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
+62
-0
@@ -14,2 +14,9 @@ import { z } from "zod"; | ||
| l.statusCallbackUrl ? `status callback: ${l.statusCallbackUrl}` : null, | ||
| l.systemPrompt | ||
| ? `system prompt: ${l.systemPrompt.slice(0, 100)}${l.systemPrompt.length > 100 ? "..." : ""}` | ||
| : null, | ||
| l.beginMessage ? `begin message: ${l.beginMessage}` : null, | ||
| l.voice ? `voice: ${l.voice}` : null, | ||
| l.contextLimit != null ? `context limit: ${l.contextLimit} turns` : null, | ||
| l.recordingEnabled ? `recording: enabled` : null, | ||
| `created: ${l.createdAt}`, | ||
@@ -39,2 +46,19 @@ ] | ||
| .describe("optional. receives call lifecycle events."), | ||
| systemPrompt: z | ||
| .string() | ||
| .optional() | ||
| .describe("system prompt for hosted mode. tells the AI agent how to behave."), | ||
| beginMessage: z | ||
| .string() | ||
| .optional() | ||
| .describe("first thing the agent says when a call connects."), | ||
| voice: z.string().optional().describe("TTS voice id. use saperly_list_voices to see options."), | ||
| contextLimit: z | ||
| .number() | ||
| .optional() | ||
| .describe("conversation memory in turns (1-50). default 20."), | ||
| recordingEnabled: z | ||
| .boolean() | ||
| .optional() | ||
| .describe("enable call recording for this line."), | ||
| }, async (args) => { | ||
@@ -86,2 +110,40 @@ try { | ||
| }); | ||
| server.tool("saperly_update_line", "update a phone line's configuration. can change webhook urls, system prompt, voice, recording, and other settings.", { | ||
| lineId: z.string().describe("the line id to update"), | ||
| name: z.string().optional().describe("display name"), | ||
| webhookUrl: z.string().optional().describe("webhook URL for text mode"), | ||
| audioHandlerUrl: z | ||
| .string() | ||
| .optional() | ||
| .describe("websocket URL for audio mode"), | ||
| statusCallbackUrl: z | ||
| .string() | ||
| .optional() | ||
| .describe("receives call lifecycle events"), | ||
| systemPrompt: z | ||
| .string() | ||
| .optional() | ||
| .describe("system prompt for hosted mode"), | ||
| beginMessage: z | ||
| .string() | ||
| .optional() | ||
| .describe("first thing the agent says"), | ||
| voice: z.string().optional().describe("TTS voice id"), | ||
| contextLimit: z | ||
| .number() | ||
| .optional() | ||
| .describe("conversation memory (1-50 turns)"), | ||
| recordingEnabled: z | ||
| .boolean() | ||
| .optional() | ||
| .describe("enable call recording"), | ||
| }, async ({ lineId, ...params }) => { | ||
| try { | ||
| const line = await client.lines.update(lineId, params); | ||
| return toolResult(`line updated!\n\n${formatLine(line)}`); | ||
| } | ||
| catch (err) { | ||
| return toolError(err); | ||
| } | ||
| }); | ||
| } |
+10
-4
| { | ||
| "name": "@saperly/mcp", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "type": "module", | ||
@@ -14,5 +14,10 @@ "bin": { | ||
| }, | ||
| "files": ["dist", "SKILL.md"], | ||
| "files": [ | ||
| "dist", | ||
| "SKILL.md" | ||
| ], | ||
| "license": "MIT", | ||
| "engines": { "node": ">=18" }, | ||
| "engines": { | ||
| "node": ">=18" | ||
| }, | ||
| "scripts": { | ||
@@ -25,6 +30,7 @@ "build": "tsc --project tsconfig.build.json", | ||
| "@modelcontextprotocol/sdk": "^1.28.0", | ||
| "@saperly/sdk": "^0.1.0", | ||
| "@saperly/sdk": "^0.2.0", | ||
| "zod": "^4.3.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.6.0", | ||
| "typescript": "^5", | ||
@@ -31,0 +37,0 @@ "vitest": "^4.1.1" |
+2
-12
@@ -73,15 +73,5 @@ # @saperly/mcp | ||
| ```bash | ||
| # Install dependencies | ||
| npm install | ||
| # Build SDK first (workspace dependency) | ||
| cd ../sdk && npm run build && cd ../mcp | ||
| # Type check | ||
| npx tsc --noEmit | ||
| # Run tests | ||
| npx vitest run | ||
| # Build | ||
| npm run typecheck | ||
| npm test | ||
| npm run build | ||
@@ -88,0 +78,0 @@ ``` |
+92
-12
@@ -61,8 +61,21 @@ --- | ||
| ### /conversation-call | ||
| make an AI phone call where saperly handles the LLM. no webhook needed. | ||
| returns the full transcript when the call ends. | ||
| usage: `/conversation-call +14155551234 schedule a demo for next tuesday` | ||
| ### /lines | ||
| list all your phone lines with numbers and mode. | ||
| list all your phone lines with numbers and mode. lines support voice calls and inbound SMS. | ||
| usage: `/lines` | ||
| ### /update-line | ||
| update a phone line's settings: webhook, system prompt, voice, recording, etc. | ||
| usage: `/update-line <line-id> systemPrompt="You are a helpful assistant"` | ||
| ### /calls | ||
@@ -74,2 +87,20 @@ | ||
| ### /sms | ||
| send an SMS reply within a conversation. | ||
| usage: `/sms +14155551234 Thanks for reaching out!` | ||
| ### /conversations | ||
| list SMS conversations grouped by contact. | ||
| usage: `/conversations` | ||
| ### /conversation | ||
| view full message history for a conversation. | ||
| usage: `/conversation <line-id> +14155551234` | ||
| ### /balance | ||
@@ -81,2 +112,20 @@ | ||
| ### /usage | ||
| view usage statistics by day or month. | ||
| usage: `/usage` or `/usage monthly` | ||
| ### /settings | ||
| view or update account settings like default webhook URL. | ||
| usage: `/settings` | ||
| ### /voices | ||
| list available TTS voices for hosted-mode calls. | ||
| usage: `/voices` | ||
| ## example | ||
@@ -102,2 +151,36 @@ | ||
| > /conversation-call +14155551234 confirm the appointment for friday | ||
| call_id: call-xyz789 | ||
| status: completed | ||
| duration: 42s | ||
| transcript: | ||
| [assistant]: Hi, I'm calling to confirm your appointment on Friday. | ||
| [user]: Yes, Friday at 2pm works great. | ||
| [assistant]: Perfect, you're confirmed for Friday at 2pm. Have a great day! | ||
| > /sms +14155551234 Your appointment is confirmed for Friday 2pm. | ||
| SMS sent! | ||
| id: msg-abc123 | ||
| to: +14155551234 | ||
| status: queued | ||
| > /conversations | ||
| 2 conversation(s): | ||
| +14155551234 5 msgs last: "Thanks!" (inbound) | ||
| +14155559876 1 msgs last: "Hi there" (inbound) | ||
| > /usage | ||
| daily usage: | ||
| 2026-04-08 3 calls 12 min $1.32 | ||
| 2026-04-07 1 calls 5 min $0.55 | ||
| > /voices | ||
| 4 voice(s): | ||
| nova Nova female american conversational | ||
| echo Echo male american warm | ||
| > /lines | ||
@@ -109,8 +192,3 @@ 1 line(s): | ||
| > /balance | ||
| balance: $4.85 USD | ||
| rates: | ||
| outbound: $0.05/min | ||
| inbound: $0.03/min | ||
| phone number: $2.00/mo | ||
| balance: $3.13 USD | ||
| ``` | ||
@@ -120,11 +198,13 @@ | ||
| saperly is a phone carrier, not an ai platform. you bring your own agent. | ||
| saperly gives it a phone number, handles compliance (tcpa disclosure, | ||
| consent tracking, audit trail), and manages the telephony infrastructure. | ||
| saperly is a phone carrier for ai agents. three modes: | ||
| **text mode:** caller speaks -> saperly transcribes -> posts to your webhook -> | ||
| you respond with text -> saperly speaks it -> caller hears. | ||
| you respond with text -> saperly speaks it -> caller hears. $0.11/min. | ||
| **audio mode:** raw audio streams to your websocket. you handle s2t/t2s. | ||
| **audio mode:** raw audio streams to your websocket. you handle s2t/t2s. $0.11/min. | ||
| **hosted mode:** saperly runs the LLM for you. just provide a system prompt | ||
| and saperly handles the entire conversation. $0.20/min. use conversation-call | ||
| or configure a line with a system prompt. | ||
| ## resources | ||
@@ -131,0 +211,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
47342
90.03%32
52.38%868
94.18%3
50%82
-10.87%+ Added
- Removed
Updated