@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 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 Amirlan"_
6
- > → the agent dials, opens with _"Hi, this is an AI assistant calling on behalf of Amirlan…"_,
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:eleven_turbo_v2_5",
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
- const lineReason = lineTypeBlockedReason(typeof payload.line_type === "string" ? payload.line_type : null);
983
- if (lineReason)
984
- throw new RejectionError(lineReason, MAKE_CALL_NEXT_STEP);
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: [deps.cfg.llmPin] } : {}
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, "--env", `SPEKO_API_KEY=${key}`, "--", "npx", "-y", PKG],
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/CheckCallReadinessTool.ts
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
- var schema2 = z2.object({});
1785
- var CheckCallReadinessTool = class extends MCPTool2 {
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 = schema2;
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 MCPTool3 } from "mcp-framework";
1806
- import { z as z3 } from "zod";
1807
- var schema3 = z3.object({
1808
- name: z3.string().min(1).describe(`Business name, e.g. "Joe's Pizza".`),
1809
- location: z3.string().optional().describe("Optional city or area to disambiguate, e.g. 'New York'.")
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 MCPTool3 {
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 = schema3;
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 MCPTool4 } from "mcp-framework";
1838
- import { z as z4 } from "zod";
1839
- var schema4 = z4.object({
1840
- dial_token: z4.string().describe("Signed dial token minted by lookup_business. Raw phone numbers are rejected."),
1841
- objective: z4.string().describe("Single transactional question, e.g. 'Do you have a table for 4 at 8pm tonight?'."),
1842
- caller_name: z4.string().describe("Name of the human the call is on behalf of (1-80 chars); spoken in the AI-disclosure opening line."),
1843
- context: z4.string().optional().describe("Optional extra task context (party size, dates, order numbers)."),
1844
- max_duration_seconds: z4.number().int().optional().describe("Max seconds to wait for the call to finish; clamped to 30-300.")
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 MIN_WAIT = 30;
1847
- var MAX_WAIT = 300;
1848
- var HEARTBEAT_MS = 5e3;
1849
- var clamp2 = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
1850
- function summarize(s) {
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 MCPTool4 {
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 = schema4;
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 = clamp2(input.max_duration_seconds ?? MAX_WAIT, MIN_WAIT, MAX_WAIT);
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 += HEARTBEAT_MS / 1e3;
2072
+ elapsed += HEARTBEAT_MS2 / 1e3;
1889
2073
  void this.reportProgress(elapsed, maxWait, `Call in progress \u2014 ${elapsed}s elapsed`).catch(() => {
1890
2074
  });
1891
- }, HEARTBEAT_MS);
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: summarize(summary), ...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.1.1",
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