@spekoai/mcp-calls 0.1.1 → 0.2.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 +4 -2
- package/dist/index.js +230 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/speko-calls/SKILL.md +11 -4
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Place real, _disclosed_ phone calls to businesses — straight from your coding agent.**
|
|
4
4
|
|
|
5
|
-
> _"call Sakura Sushi and ask if they have a table for 4 at 8pm — my name is
|
|
6
|
-
> → the agent dials, opens with _"Hi, this is an AI assistant calling on behalf of
|
|
5
|
+
> _"call Sakura Sushi and ask if they have a table for 4 at 8pm — my name is John"_
|
|
6
|
+
> → the agent dials, opens with _"Hi, this is an AI assistant calling on behalf of John…"_,
|
|
7
7
|
> and the `OUTCOME:` (booked / not available) lands back in your terminal.
|
|
8
8
|
|
|
9
9
|
A [Model Context Protocol](https://modelcontextprotocol.io) server for Claude Code, Claude
|
|
@@ -44,6 +44,8 @@ backing server instead of running in-process, set `SPEKO_MCP_SERVER_URL`.
|
|
|
44
44
|
| --- | --- |
|
|
45
45
|
| `lookup_business(name, location?)` | Resolve a business → dialable candidates + a signed `dial_token` per callable one. The only path that can authorize a call. |
|
|
46
46
|
| `make_call(dial_token, objective, caller_name, context?)` | Place the disclosed, objective-scoped call; wait for it to finish; return the `OUTCOME` + transcript. Honest `connected`/`answered`/`not_connected`. |
|
|
47
|
+
| `call_number(phone_number, objective, caller_name)` | Disclosed PERSONAL call to a specific number (e.g. a friend) — mobiles allowed. Opt-in via `SPEKO_ALLOW_DIRECT_DIAL=1`. |
|
|
48
|
+
| `get_call(call_id)` | Read-only: re-check a call's status, `OUTCOME`, and transcript. Never dials. |
|
|
47
49
|
| `check_call_readiness()` | Read-only preflight: auth, credit balance, outbound caller-ID. Never dials. |
|
|
48
50
|
|
|
49
51
|
## Safety
|
package/dist/index.js
CHANGED
|
@@ -70,13 +70,14 @@ function loadConfig() {
|
|
|
70
70
|
const n = Number(process.env.SPEKO_DEMO_TTS_SPEED);
|
|
71
71
|
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
72
72
|
})(),
|
|
73
|
-
ttsPin: (process.env.SPEKO_TTS_PIN ?? "").trim() || "elevenlabs:
|
|
74
|
-
sttPin: (process.env.SPEKO_STT_PIN ?? "").trim() || "deepgram",
|
|
75
|
-
llmPin: (process.env.SPEKO_LLM_PIN ?? "").trim() || "groq:llama-3.3-70b-versatile",
|
|
73
|
+
ttsPin: (process.env.SPEKO_TTS_PIN ?? "").trim() || "elevenlabs:eleven_flash_v2_5",
|
|
74
|
+
sttPin: (process.env.SPEKO_STT_PIN ?? "").trim() || "deepgram:nova-3",
|
|
75
|
+
llmPin: (process.env.SPEKO_LLM_PIN ?? "").trim() || "groq:llama-3.3-70b-versatile,openai:gpt-4.1-mini",
|
|
76
76
|
optimizeFor: (() => {
|
|
77
77
|
const v = (process.env.SPEKO_OPTIMIZE_FOR ?? "").trim();
|
|
78
78
|
return ["balanced", "accuracy", "latency", "cost"].includes(v) ? v : "latency";
|
|
79
79
|
})(),
|
|
80
|
+
allowDirectDial: ["1", "true", "yes"].includes((process.env.SPEKO_ALLOW_DIRECT_DIAL ?? "").trim().toLowerCase()),
|
|
80
81
|
dialTokenSecret,
|
|
81
82
|
googlePlacesApiKey: (process.env.GOOGLE_PLACES_API_KEY ?? "").trim() || void 0,
|
|
82
83
|
twilio: twilioSid && twilioToken ? { sid: twilioSid, token: twilioToken } : void 0,
|
|
@@ -979,9 +980,11 @@ async function makeCall(input, deps) {
|
|
|
979
980
|
const dialReason = dialBlockedReason(e164);
|
|
980
981
|
if (dialReason)
|
|
981
982
|
throw new RejectionError(dialReason, MAKE_CALL_NEXT_STEP);
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
983
|
+
if (!deps.allowAnyLineType) {
|
|
984
|
+
const lineReason = lineTypeBlockedReason(typeof payload.line_type === "string" ? payload.line_type : null);
|
|
985
|
+
if (lineReason)
|
|
986
|
+
throw new RejectionError(lineReason, MAKE_CALL_NEXT_STEP);
|
|
987
|
+
}
|
|
985
988
|
const offset = typeof payload.utc_offset_minutes === "number" ? payload.utc_offset_minutes : null;
|
|
986
989
|
const quietReason = quietHoursReason(offset);
|
|
987
990
|
if (quietReason) {
|
|
@@ -1014,7 +1017,7 @@ async function makeCall(input, deps) {
|
|
|
1014
1017
|
allowedProviders: {
|
|
1015
1018
|
tts: [deps.cfg.ttsPin],
|
|
1016
1019
|
stt: [deps.cfg.sttPin],
|
|
1017
|
-
...deps.cfg.llmPin ? { llm:
|
|
1020
|
+
...deps.cfg.llmPin ? { llm: deps.cfg.llmPin.split(",").map((m) => m.trim()).filter(Boolean) } : {}
|
|
1018
1021
|
}
|
|
1019
1022
|
},
|
|
1020
1023
|
sttOptions: { keywords: [caller, businessName, ...DIAL_STT_KEYWORDS] },
|
|
@@ -1148,6 +1151,51 @@ var init_makeCall = __esm({
|
|
|
1148
1151
|
}
|
|
1149
1152
|
});
|
|
1150
1153
|
|
|
1154
|
+
// ../server/dist/calls/callNumber.js
|
|
1155
|
+
async function callNumber(input, deps) {
|
|
1156
|
+
if (!deps.cfg.allowDirectDial) {
|
|
1157
|
+
throw new RejectionError("Direct dialing is OFF. call_number can ring any number (including mobiles) for personal calls, but it is disabled by default. Turn it on by setting SPEKO_ALLOW_DIRECT_DIAL=1 \u2014 doing so confirms you have consent to call this number and take responsibility for compliance. The call still opens with the AI disclosure and respects quiet hours either way.", "Set SPEKO_ALLOW_DIRECT_DIAL=1 in the MCP's env and restart, then retry \u2014 or use lookup_business for a business.");
|
|
1158
|
+
}
|
|
1159
|
+
const e164 = typeof input.phoneNumber === "string" ? input.phoneNumber.trim() : "";
|
|
1160
|
+
const blocked = dialBlockedReason(e164);
|
|
1161
|
+
if (blocked) {
|
|
1162
|
+
throw new RejectionError(blocked, "Pass a valid E.164 number (e.g. +77011234567) that you have consent to call.");
|
|
1163
|
+
}
|
|
1164
|
+
const offset = typeof input.utcOffsetMinutes === "number" ? input.utcOffsetMinutes : offsetFromE164(e164);
|
|
1165
|
+
const token = mintDialToken({
|
|
1166
|
+
e164,
|
|
1167
|
+
lineType: "personal",
|
|
1168
|
+
// cosmetic; the business-line check is skipped for the direct path
|
|
1169
|
+
businessName: input.recipientName && input.recipientName.trim() || "your contact",
|
|
1170
|
+
utcOffsetMinutes: offset,
|
|
1171
|
+
bearerHash: deps.bearerHash,
|
|
1172
|
+
secret: deps.cfg.dialTokenSecret
|
|
1173
|
+
});
|
|
1174
|
+
return makeCall({
|
|
1175
|
+
dialToken: token,
|
|
1176
|
+
objective: input.objective,
|
|
1177
|
+
callerName: input.callerName,
|
|
1178
|
+
context: input.context ?? null,
|
|
1179
|
+
maxDurationSeconds: input.maxDurationSeconds
|
|
1180
|
+
}, {
|
|
1181
|
+
client: deps.client,
|
|
1182
|
+
cfg: deps.cfg,
|
|
1183
|
+
bearerHash: deps.bearerHash,
|
|
1184
|
+
sleep: deps.sleep,
|
|
1185
|
+
allowAnyLineType: true
|
|
1186
|
+
// set server-side only, behind cfg.allowDirectDial
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
var init_callNumber = __esm({
|
|
1190
|
+
"../server/dist/calls/callNumber.js"() {
|
|
1191
|
+
"use strict";
|
|
1192
|
+
init_errors();
|
|
1193
|
+
init_dialToken();
|
|
1194
|
+
init_timezone();
|
|
1195
|
+
init_makeCall();
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1151
1199
|
// ../server/dist/calls/readiness.js
|
|
1152
1200
|
async function checkReadiness(client) {
|
|
1153
1201
|
let authFailed = false;
|
|
@@ -1300,6 +1348,7 @@ __export(core_exports, {
|
|
|
1300
1348
|
ConfigError: () => ConfigError,
|
|
1301
1349
|
RejectionError: () => RejectionError,
|
|
1302
1350
|
buildContext: () => buildContext,
|
|
1351
|
+
callNumber: () => callNumber,
|
|
1303
1352
|
checkReadiness: () => checkReadiness,
|
|
1304
1353
|
describeCall: () => describeCall,
|
|
1305
1354
|
loadConfig: () => loadConfig,
|
|
@@ -1314,6 +1363,7 @@ var init_core = __esm({
|
|
|
1314
1363
|
init_context();
|
|
1315
1364
|
init_lookup();
|
|
1316
1365
|
init_makeCall();
|
|
1366
|
+
init_callNumber();
|
|
1317
1367
|
init_readiness();
|
|
1318
1368
|
init_getCall();
|
|
1319
1369
|
init_errors();
|
|
@@ -1439,7 +1489,9 @@ function desktopConfigPath() {
|
|
|
1439
1489
|
if (platform() === "win32") return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1440
1490
|
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1441
1491
|
}
|
|
1442
|
-
function configureClaudeCode(key, scope) {
|
|
1492
|
+
function configureClaudeCode(key, scope, extraEnv = {}) {
|
|
1493
|
+
const envArgs = ["--env", `SPEKO_API_KEY=${key}`];
|
|
1494
|
+
for (const [k, v] of Object.entries(extraEnv)) envArgs.push("--env", `${k}=${v}`);
|
|
1443
1495
|
const manual = `claude mcp add ${SERVER_NAME} --scope ${scope} --env SPEKO_API_KEY=<your-key> -- npx -y ${PKG}`;
|
|
1444
1496
|
if (!claudeCliPresent()) {
|
|
1445
1497
|
console.log(c.yellow(" \u2022 Claude Code CLI not found on PATH. Run this yourself once installed:"));
|
|
@@ -1449,7 +1501,7 @@ function configureClaudeCode(key, scope) {
|
|
|
1449
1501
|
spawnSync("claude", ["mcp", "remove", SERVER_NAME, "--scope", scope], { stdio: "ignore" });
|
|
1450
1502
|
const r = spawnSync(
|
|
1451
1503
|
"claude",
|
|
1452
|
-
["mcp", "add", SERVER_NAME, "--scope", scope,
|
|
1504
|
+
["mcp", "add", SERVER_NAME, "--scope", scope, ...envArgs, "--", "npx", "-y", PKG],
|
|
1453
1505
|
{ stdio: "inherit" }
|
|
1454
1506
|
);
|
|
1455
1507
|
if (r.status === 0) {
|
|
@@ -1460,7 +1512,7 @@ function configureClaudeCode(key, scope) {
|
|
|
1460
1512
|
console.log(" " + c.cyan(manual));
|
|
1461
1513
|
return false;
|
|
1462
1514
|
}
|
|
1463
|
-
function configureClaudeDesktop(key) {
|
|
1515
|
+
function configureClaudeDesktop(key, extraEnv = {}) {
|
|
1464
1516
|
const path = desktopConfigPath();
|
|
1465
1517
|
try {
|
|
1466
1518
|
let cfg = {};
|
|
@@ -1477,7 +1529,7 @@ function configureClaudeDesktop(key) {
|
|
|
1477
1529
|
mkdirSync(dirname(path), { recursive: true });
|
|
1478
1530
|
}
|
|
1479
1531
|
const servers = cfg.mcpServers && typeof cfg.mcpServers === "object" ? cfg.mcpServers : {};
|
|
1480
|
-
servers[SERVER_NAME] = { command: "npx", args: ["-y", PKG], env: { SPEKO_API_KEY: key } };
|
|
1532
|
+
servers[SERVER_NAME] = { command: "npx", args: ["-y", PKG], env: { SPEKO_API_KEY: key, ...extraEnv } };
|
|
1481
1533
|
cfg.mcpServers = servers;
|
|
1482
1534
|
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}
|
|
1483
1535
|
`);
|
|
@@ -1565,9 +1617,25 @@ async function runInit(argv) {
|
|
|
1565
1617
|
Configure which client? [code/desktop/both] (${def}) `)).toLowerCase();
|
|
1566
1618
|
target = ans || def;
|
|
1567
1619
|
}
|
|
1620
|
+
const extraEnv = {};
|
|
1621
|
+
if (!f.yes) {
|
|
1622
|
+
const demo = (await ask('\n Set up a quick DEMO so "call <a business>" works right away \u2014 rings a number you control? [y/N] ')).toLowerCase();
|
|
1623
|
+
if (demo === "y" || demo === "yes") {
|
|
1624
|
+
const num = (await ask(" Number to ring, E.164 (e.g. +15551234567): ")).replace(/\s/g, "");
|
|
1625
|
+
if (/^\+?[1-9]\d{6,14}$/.test(num)) {
|
|
1626
|
+
const biz = (await ask(" Business name to say on the call (default: Sakura Sushi): ")).trim() || "Sakura Sushi";
|
|
1627
|
+
extraEnv.SPEKO_DEMO = "1";
|
|
1628
|
+
extraEnv.SPEKO_DEMO_E164 = num.startsWith("+") ? num : `+${num}`;
|
|
1629
|
+
extraEnv.SPEKO_DEMO_BUSINESS = biz;
|
|
1630
|
+
console.log(c.dim(` Demo on: "call ${biz}" will ring ${extraEnv.SPEKO_DEMO_E164}.`));
|
|
1631
|
+
} else {
|
|
1632
|
+
console.log(c.yellow(" \u2022 Skipping demo \u2014 that didn't look like an E.164 number."));
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1568
1636
|
console.log("");
|
|
1569
|
-
if (target === "code" || target === "both") configureClaudeCode(key, f.scope);
|
|
1570
|
-
if (target === "desktop" || target === "both") configureClaudeDesktop(key);
|
|
1637
|
+
if (target === "code" || target === "both") configureClaudeCode(key, f.scope, extraEnv);
|
|
1638
|
+
if (target === "desktop" || target === "both") configureClaudeDesktop(key, extraEnv);
|
|
1571
1639
|
installSkill();
|
|
1572
1640
|
console.log(c.bold("\n \u2705 Done.\n"));
|
|
1573
1641
|
console.log(" Try it: open your agent and say");
|
|
@@ -1632,7 +1700,7 @@ var CallMeTool = class extends MCPTool {
|
|
|
1632
1700
|
}
|
|
1633
1701
|
};
|
|
1634
1702
|
|
|
1635
|
-
// src/tools/
|
|
1703
|
+
// src/tools/CallNumberTool.ts
|
|
1636
1704
|
import { MCPTool as MCPTool2 } from "mcp-framework";
|
|
1637
1705
|
import { z as z2 } from "zod";
|
|
1638
1706
|
|
|
@@ -1691,6 +1759,20 @@ var InProcessBackend = class {
|
|
|
1691
1759
|
{ client: ctx.client, cfg: ctx.cfg, bearerHash: ctx.bearerHash }
|
|
1692
1760
|
);
|
|
1693
1761
|
}
|
|
1762
|
+
if (path === "/call-number") {
|
|
1763
|
+
return await core.callNumber(
|
|
1764
|
+
{
|
|
1765
|
+
phoneNumber: String(b.phone_number ?? ""),
|
|
1766
|
+
objective: String(b.objective ?? ""),
|
|
1767
|
+
callerName: String(b.caller_name ?? ""),
|
|
1768
|
+
context: b.context ?? null,
|
|
1769
|
+
recipientName: b.recipient_name ?? null,
|
|
1770
|
+
utcOffsetMinutes: typeof b.utc_offset_minutes === "number" ? b.utc_offset_minutes : void 0,
|
|
1771
|
+
maxDurationSeconds: typeof b.max_duration_seconds === "number" ? b.max_duration_seconds : void 0
|
|
1772
|
+
},
|
|
1773
|
+
{ client: ctx.client, cfg: ctx.cfg, bearerHash: ctx.bearerHash }
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1694
1776
|
throw new DemoServerError(`Unknown backend path: POST ${path}`);
|
|
1695
1777
|
} catch (e) {
|
|
1696
1778
|
throw normalizeError(e);
|
|
@@ -1780,12 +1862,91 @@ function getServerClient() {
|
|
|
1780
1862
|
return cached2;
|
|
1781
1863
|
}
|
|
1782
1864
|
|
|
1865
|
+
// src/tools/CallNumberTool.ts
|
|
1866
|
+
var schema2 = z2.object({
|
|
1867
|
+
phone_number: z2.string().describe("Number to call, E.164 (e.g. +77011234567). A real number the user has consent to call."),
|
|
1868
|
+
objective: z2.string().describe("What to say / accomplish, e.g. 'Tell Karim that Amirlan says happy birthday and misses him.'"),
|
|
1869
|
+
caller_name: z2.string().describe("Name of the human the call is on behalf of (1-80 chars); spoken in the AI-disclosure opening."),
|
|
1870
|
+
recipient_name: z2.string().optional().describe("Who you're calling, used in the greeting (e.g. 'Karim')."),
|
|
1871
|
+
context: z2.string().optional().describe("Optional extra context for the message."),
|
|
1872
|
+
utc_offset_minutes: z2.number().int().optional().describe("Callee UTC offset in minutes for quiet hours (e.g. 300 = UTC+5). Auto-derived from the number; pass it only if a call is blocked for unknown timezone."),
|
|
1873
|
+
max_duration_seconds: z2.number().int().optional().describe("Max seconds to wait for the call to finish; clamped 30-300.")
|
|
1874
|
+
});
|
|
1875
|
+
var MIN_WAIT = 30;
|
|
1876
|
+
var MAX_WAIT = 300;
|
|
1877
|
+
var HEARTBEAT_MS = 5e3;
|
|
1878
|
+
var clamp2 = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
1879
|
+
function summarize(s) {
|
|
1880
|
+
const status = typeof s.status === "string" ? s.status : "unknown";
|
|
1881
|
+
const callId = typeof s.call_id === "string" ? s.call_id : null;
|
|
1882
|
+
const outcome = typeof s.outcome === "string" ? s.outcome : null;
|
|
1883
|
+
const reason = typeof s.reason === "string" ? s.reason : null;
|
|
1884
|
+
const connected = s.connected === true;
|
|
1885
|
+
const answered = s.answered === true;
|
|
1886
|
+
if (status === "not_placed") {
|
|
1887
|
+
return reason ?? "The call was NOT placed: no outbound caller-ID/SIP is configured for this deployment.";
|
|
1888
|
+
}
|
|
1889
|
+
if (status === "not_connected") {
|
|
1890
|
+
return (reason ?? "The call did not connect \u2014 no telephony leg reached the carrier, so the phone never rang.") + " Re-dialing will not help until the deployment's outbound trunk is fixed.";
|
|
1891
|
+
}
|
|
1892
|
+
if (status === "timeout") {
|
|
1893
|
+
return `Reached the wait limit; the call may still be in progress${callId ? ` (call_id '${callId}')` : ""}.`;
|
|
1894
|
+
}
|
|
1895
|
+
if (connected && !answered) {
|
|
1896
|
+
return reason ?? `The call connected but no one responded${callId ? ` (call_id '${callId}')` : ""}.`;
|
|
1897
|
+
}
|
|
1898
|
+
if (outcome) return outcome;
|
|
1899
|
+
return `Call ${callId ?? ""} finished with status '${status}'.`.trim();
|
|
1900
|
+
}
|
|
1901
|
+
var CallNumberTool = class extends MCPTool2 {
|
|
1902
|
+
name = "call_number";
|
|
1903
|
+
description = "Place a disclosed PERSONAL call to a specific phone number (e.g. a friend) \u2014 NOT a business lookup. Requires the operator to have opted in (SPEKO_ALLOW_DIRECT_DIAL=1); otherwise it returns how to enable it. Every call opens with the non-removable AI disclosure, and quiet hours + the no-sell/no-spam screen still apply (mobiles are allowed here, unlike make_call). Use lookup_business + make_call for businesses; use this only for a number the user explicitly provides and has consent to call.";
|
|
1904
|
+
schema = schema2;
|
|
1905
|
+
annotations = {
|
|
1906
|
+
title: "Call a Number",
|
|
1907
|
+
readOnlyHint: false,
|
|
1908
|
+
destructiveHint: false,
|
|
1909
|
+
idempotentHint: false,
|
|
1910
|
+
openWorldHint: true
|
|
1911
|
+
};
|
|
1912
|
+
async execute(input) {
|
|
1913
|
+
const maxWait = clamp2(input.max_duration_seconds ?? MAX_WAIT, MIN_WAIT, MAX_WAIT);
|
|
1914
|
+
const client = getServerClient();
|
|
1915
|
+
let elapsed = 0;
|
|
1916
|
+
const timer = setInterval(() => {
|
|
1917
|
+
elapsed += HEARTBEAT_MS / 1e3;
|
|
1918
|
+
void this.reportProgress(elapsed, maxWait, `Call in progress \u2014 ${elapsed}s elapsed`).catch(() => {
|
|
1919
|
+
});
|
|
1920
|
+
}, HEARTBEAT_MS);
|
|
1921
|
+
try {
|
|
1922
|
+
const summary = await client.post(
|
|
1923
|
+
"/call-number",
|
|
1924
|
+
{
|
|
1925
|
+
phone_number: input.phone_number,
|
|
1926
|
+
objective: input.objective,
|
|
1927
|
+
caller_name: input.caller_name,
|
|
1928
|
+
recipient_name: input.recipient_name,
|
|
1929
|
+
context: input.context,
|
|
1930
|
+
utc_offset_minutes: input.utc_offset_minutes,
|
|
1931
|
+
max_duration_seconds: input.max_duration_seconds
|
|
1932
|
+
},
|
|
1933
|
+
{ timeoutMs: (maxWait + 30) * 1e3, signal: this.abortSignal }
|
|
1934
|
+
);
|
|
1935
|
+
return { summary: summarize(summary), ...summary };
|
|
1936
|
+
} finally {
|
|
1937
|
+
clearInterval(timer);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1783
1942
|
// src/tools/CheckCallReadinessTool.ts
|
|
1784
|
-
|
|
1785
|
-
|
|
1943
|
+
import { MCPTool as MCPTool3 } from "mcp-framework";
|
|
1944
|
+
import { z as z3 } from "zod";
|
|
1945
|
+
var schema3 = z3.object({});
|
|
1946
|
+
var CheckCallReadinessTool = class extends MCPTool3 {
|
|
1786
1947
|
name = "check_call_readiness";
|
|
1787
1948
|
description = 'Read-only preflight: can this account place calls? Reports auth, prepaid credit balance, and outbound caller-ID readiness \u2014 each with a concrete next step. Never dials. Run it first if calling does not work, or as the simple "am I set up?" check before the first make_call.';
|
|
1788
|
-
schema =
|
|
1949
|
+
schema = schema3;
|
|
1789
1950
|
annotations = {
|
|
1790
1951
|
title: "Check Call Readiness",
|
|
1791
1952
|
readOnlyHint: true,
|
|
@@ -1801,17 +1962,40 @@ var CheckCallReadinessTool = class extends MCPTool2 {
|
|
|
1801
1962
|
}
|
|
1802
1963
|
};
|
|
1803
1964
|
|
|
1965
|
+
// src/tools/GetCallTool.ts
|
|
1966
|
+
import { MCPTool as MCPTool4 } from "mcp-framework";
|
|
1967
|
+
import { z as z4 } from "zod";
|
|
1968
|
+
var schema4 = z4.object({
|
|
1969
|
+
call_id: z4.string().describe("The call_id returned by make_call or call_number \u2014 to re-check a call's status, outcome, and transcript.")
|
|
1970
|
+
});
|
|
1971
|
+
var GetCallTool = class extends MCPTool4 {
|
|
1972
|
+
name = "get_call";
|
|
1973
|
+
description = "Read-only: re-check an existing call by its call_id \u2014 status, connected/answered, the OUTCOME line, and the transcript. Never dials. Use it after make_call or call_number reports a timeout, or to inspect a finished call.";
|
|
1974
|
+
schema = schema4;
|
|
1975
|
+
annotations = {
|
|
1976
|
+
title: "Get Call",
|
|
1977
|
+
readOnlyHint: true,
|
|
1978
|
+
destructiveHint: false,
|
|
1979
|
+
idempotentHint: true,
|
|
1980
|
+
openWorldHint: true
|
|
1981
|
+
};
|
|
1982
|
+
async execute(input) {
|
|
1983
|
+
const id = encodeURIComponent(String(input.call_id ?? "").trim());
|
|
1984
|
+
return await getServerClient().get(`/call/${id}`);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1804
1988
|
// src/tools/LookupBusinessTool.ts
|
|
1805
|
-
import { MCPTool as
|
|
1806
|
-
import { z as
|
|
1807
|
-
var
|
|
1808
|
-
name:
|
|
1809
|
-
location:
|
|
1989
|
+
import { MCPTool as MCPTool5 } from "mcp-framework";
|
|
1990
|
+
import { z as z5 } from "zod";
|
|
1991
|
+
var schema5 = z5.object({
|
|
1992
|
+
name: z5.string().min(1).describe(`Business name, e.g. "Joe's Pizza".`),
|
|
1993
|
+
location: z5.string().optional().describe("Optional city or area to disambiguate, e.g. 'New York'.")
|
|
1810
1994
|
});
|
|
1811
|
-
var LookupBusinessTool = class extends
|
|
1995
|
+
var LookupBusinessTool = class extends MCPTool5 {
|
|
1812
1996
|
name = "lookup_business";
|
|
1813
1997
|
description = "Resolve a business name (plus optional location) to dialable candidates and mint a signed dial_token for each callable one. This is the only path that can authorize make_call \u2014 raw phone numbers are rejected.";
|
|
1814
|
-
schema =
|
|
1998
|
+
schema = schema5;
|
|
1815
1999
|
annotations = {
|
|
1816
2000
|
title: "Lookup Business",
|
|
1817
2001
|
readOnlyHint: true,
|
|
@@ -1834,20 +2018,20 @@ var LookupBusinessTool = class extends MCPTool3 {
|
|
|
1834
2018
|
};
|
|
1835
2019
|
|
|
1836
2020
|
// src/tools/MakeCallTool.ts
|
|
1837
|
-
import { MCPTool as
|
|
1838
|
-
import { z as
|
|
1839
|
-
var
|
|
1840
|
-
dial_token:
|
|
1841
|
-
objective:
|
|
1842
|
-
caller_name:
|
|
1843
|
-
context:
|
|
1844
|
-
max_duration_seconds:
|
|
2021
|
+
import { MCPTool as MCPTool6 } from "mcp-framework";
|
|
2022
|
+
import { z as z6 } from "zod";
|
|
2023
|
+
var schema6 = z6.object({
|
|
2024
|
+
dial_token: z6.string().describe("Signed dial token minted by lookup_business. Raw phone numbers are rejected."),
|
|
2025
|
+
objective: z6.string().describe("Single transactional question, e.g. 'Do you have a table for 4 at 8pm tonight?'."),
|
|
2026
|
+
caller_name: z6.string().describe("Name of the human the call is on behalf of (1-80 chars); spoken in the AI-disclosure opening line."),
|
|
2027
|
+
context: z6.string().optional().describe("Optional extra task context (party size, dates, order numbers)."),
|
|
2028
|
+
max_duration_seconds: z6.number().int().optional().describe("Max seconds to wait for the call to finish; clamped to 30-300.")
|
|
1845
2029
|
});
|
|
1846
|
-
var
|
|
1847
|
-
var
|
|
1848
|
-
var
|
|
1849
|
-
var
|
|
1850
|
-
function
|
|
2030
|
+
var MIN_WAIT2 = 30;
|
|
2031
|
+
var MAX_WAIT2 = 300;
|
|
2032
|
+
var HEARTBEAT_MS2 = 5e3;
|
|
2033
|
+
var clamp3 = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
2034
|
+
function summarize2(s) {
|
|
1851
2035
|
const status = typeof s.status === "string" ? s.status : "unknown";
|
|
1852
2036
|
const callId = typeof s.call_id === "string" ? s.call_id : null;
|
|
1853
2037
|
const outcome = typeof s.outcome === "string" ? s.outcome : null;
|
|
@@ -1869,10 +2053,10 @@ function summarize(s) {
|
|
|
1869
2053
|
if (outcome) return outcome;
|
|
1870
2054
|
return `Call ${callId ?? ""} finished with status '${status}' and no OUTCOME line.`.trim();
|
|
1871
2055
|
}
|
|
1872
|
-
var MakeCallTool = class extends
|
|
2056
|
+
var MakeCallTool = class extends MCPTool6 {
|
|
1873
2057
|
name = "make_call";
|
|
1874
2058
|
description = "Place a disclosed, objective-scoped phone call authorized by a dial_token from lookup_business. Stays open until the call finishes and returns the OUTCOME line plus the transcript. Every call opens with a non-removable AI disclosure; selling, promotion, surveys, fundraising, and campaigning are blocked. All safety rails are enforced server-side.";
|
|
1875
|
-
schema =
|
|
2059
|
+
schema = schema6;
|
|
1876
2060
|
annotations = {
|
|
1877
2061
|
title: "Make Call",
|
|
1878
2062
|
readOnlyHint: false,
|
|
@@ -1881,14 +2065,14 @@ var MakeCallTool = class extends MCPTool4 {
|
|
|
1881
2065
|
openWorldHint: true
|
|
1882
2066
|
};
|
|
1883
2067
|
async execute(input) {
|
|
1884
|
-
const maxWait =
|
|
2068
|
+
const maxWait = clamp3(input.max_duration_seconds ?? MAX_WAIT2, MIN_WAIT2, MAX_WAIT2);
|
|
1885
2069
|
const client = getServerClient();
|
|
1886
2070
|
let elapsed = 0;
|
|
1887
2071
|
const timer = setInterval(() => {
|
|
1888
|
-
elapsed +=
|
|
2072
|
+
elapsed += HEARTBEAT_MS2 / 1e3;
|
|
1889
2073
|
void this.reportProgress(elapsed, maxWait, `Call in progress \u2014 ${elapsed}s elapsed`).catch(() => {
|
|
1890
2074
|
});
|
|
1891
|
-
},
|
|
2075
|
+
}, HEARTBEAT_MS2);
|
|
1892
2076
|
try {
|
|
1893
2077
|
const summary = await client.post(
|
|
1894
2078
|
"/call",
|
|
@@ -1901,7 +2085,7 @@ var MakeCallTool = class extends MCPTool4 {
|
|
|
1901
2085
|
},
|
|
1902
2086
|
{ timeoutMs: (maxWait + 30) * 1e3, signal: this.abortSignal }
|
|
1903
2087
|
);
|
|
1904
|
-
return { summary:
|
|
2088
|
+
return { summary: summarize2(summary), ...summary };
|
|
1905
2089
|
} finally {
|
|
1906
2090
|
clearInterval(timer);
|
|
1907
2091
|
}
|
|
@@ -1917,12 +2101,14 @@ if (cmd === "init" || cmd === "setup" || cmd === "login") {
|
|
|
1917
2101
|
loadEnv();
|
|
1918
2102
|
var server = new MCPServer({
|
|
1919
2103
|
name: "speko-calls",
|
|
1920
|
-
version: "0.
|
|
2104
|
+
version: "0.2.1",
|
|
1921
2105
|
transport: { type: "stdio" }
|
|
1922
2106
|
});
|
|
1923
2107
|
server.addTool(LookupBusinessTool);
|
|
1924
2108
|
server.addTool(MakeCallTool);
|
|
2109
|
+
server.addTool(CallNumberTool);
|
|
1925
2110
|
server.addTool(CheckCallReadinessTool);
|
|
2111
|
+
server.addTool(GetCallTool);
|
|
1926
2112
|
server.addTool(CallMeTool);
|
|
1927
2113
|
await server.start();
|
|
1928
2114
|
//# sourceMappingURL=index.js.map
|