@spekoai/mcp-calls 0.2.0 → 0.3.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/README.md +10 -5
- package/dist/index.js +288 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/speko-calls/SKILL.md +2 -2
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
|
|
@@ -15,9 +15,12 @@ Desktop, and any MCP client. Powered by [Speko](https://speko.ai).
|
|
|
15
15
|
npx @spekoai/mcp-calls@latest init
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The wizard
|
|
19
|
-
client config (Claude Code or Claude Desktop), and
|
|
20
|
-
your agent to call a business.
|
|
18
|
+
The wizard signs you in **with your browser** (OAuth — no key to copy or paste), fetches your
|
|
19
|
+
key automatically, writes the MCP into your client config (Claude Code or Claude Desktop), and
|
|
20
|
+
installs a companion skill. Then just ask your agent to call a business.
|
|
21
|
+
|
|
22
|
+
Already have a key, or on a headless box? `--token sk_...` or `--paste` skips the browser.
|
|
23
|
+
Re-authenticate anytime with `npx @spekoai/mcp-calls login`.
|
|
21
24
|
|
|
22
25
|
It runs **single-process**: give it your `SPEKO_API_KEY` and it calls `api.speko.dev`
|
|
23
26
|
directly — no separate server to run.
|
|
@@ -44,6 +47,8 @@ backing server instead of running in-process, set `SPEKO_MCP_SERVER_URL`.
|
|
|
44
47
|
| --- | --- |
|
|
45
48
|
| `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
49
|
| `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`. |
|
|
50
|
+
| `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`. |
|
|
51
|
+
| `get_call(call_id)` | Read-only: re-check a call's status, `OUTCOME`, and transcript. Never dials. |
|
|
47
52
|
| `check_call_readiness()` | Read-only preflight: auth, credit balance, outbound caller-ID. Never dials. |
|
|
48
53
|
|
|
49
54
|
## Safety
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// ../server/dist/config.js
|
|
13
|
-
import { createHash } from "crypto";
|
|
13
|
+
import { createHash as createHash2 } from "crypto";
|
|
14
14
|
import { existsSync as existsSync3 } from "fs";
|
|
15
15
|
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
16
16
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -70,9 +70,9 @@ 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";
|
|
@@ -93,7 +93,7 @@ function loadConfig() {
|
|
|
93
93
|
return cached;
|
|
94
94
|
}
|
|
95
95
|
function serverBearerHash(cfg) {
|
|
96
|
-
return
|
|
96
|
+
return createHash2("sha256").update(cfg.speko.apiKey, "utf-8").digest("hex").slice(0, 16);
|
|
97
97
|
}
|
|
98
98
|
var ConfigError, cached;
|
|
99
99
|
var init_config = __esm({
|
|
@@ -842,9 +842,9 @@ var init_objective = __esm({
|
|
|
842
842
|
});
|
|
843
843
|
|
|
844
844
|
// ../server/dist/safety/prompt.js
|
|
845
|
-
import { randomBytes } from "crypto";
|
|
845
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
846
846
|
function delimitedBlock(label, content) {
|
|
847
|
-
const nonce =
|
|
847
|
+
const nonce = randomBytes2(8).toString("hex");
|
|
848
848
|
return `${BLOCK_RULE} ${label} ${nonce} ${BLOCK_RULE}
|
|
849
849
|
${content}
|
|
850
850
|
${BLOCK_RULE} END ${label} ${nonce} ${BLOCK_RULE}`;
|
|
@@ -1017,7 +1017,7 @@ async function makeCall(input, deps) {
|
|
|
1017
1017
|
allowedProviders: {
|
|
1018
1018
|
tts: [deps.cfg.ttsPin],
|
|
1019
1019
|
stt: [deps.cfg.sttPin],
|
|
1020
|
-
...deps.cfg.llmPin ? { llm:
|
|
1020
|
+
...deps.cfg.llmPin ? { llm: deps.cfg.llmPin.split(",").map((m) => m.trim()).filter(Boolean) } : {}
|
|
1021
1021
|
}
|
|
1022
1022
|
},
|
|
1023
1023
|
sttOptions: { keywords: [caller, businessName, ...DIAL_STT_KEYWORDS] },
|
|
@@ -1374,13 +1374,191 @@ var init_core = __esm({
|
|
|
1374
1374
|
import { MCPServer } from "mcp-framework";
|
|
1375
1375
|
|
|
1376
1376
|
// src/cli/init.ts
|
|
1377
|
-
import { spawn, spawnSync } from "child_process";
|
|
1377
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
1378
1378
|
import { createInterface } from "readline";
|
|
1379
1379
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1380
|
-
import { homedir, platform } from "os";
|
|
1380
|
+
import { homedir, platform as platform2 } from "os";
|
|
1381
1381
|
import { dirname, join, resolve } from "path";
|
|
1382
1382
|
import { fileURLToPath } from "url";
|
|
1383
|
+
|
|
1384
|
+
// src/cli/login.ts
|
|
1385
|
+
import { createServer } from "http";
|
|
1386
|
+
import { randomBytes, createHash } from "crypto";
|
|
1387
|
+
import { spawn } from "child_process";
|
|
1388
|
+
import { platform } from "os";
|
|
1383
1389
|
var API_BASE = (process.env.SPEKOAI_API_URL || "https://api.speko.dev").replace(/\/+$/, "");
|
|
1390
|
+
var AUTH_DISCOVERY = process.env.SPEKO_OAUTH_DISCOVERY || "https://platform.speko.dev/.well-known/oauth-authorization-server/api/auth";
|
|
1391
|
+
var LOGIN_TIMEOUT_MS = 5 * 6e4;
|
|
1392
|
+
function b64url(buf) {
|
|
1393
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1394
|
+
}
|
|
1395
|
+
function escapeHtml(s) {
|
|
1396
|
+
return s.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
1397
|
+
}
|
|
1398
|
+
function resultPage(title, body) {
|
|
1399
|
+
return `<!doctype html><meta charset="utf-8"><title>${escapeHtml(title)}</title>
|
|
1400
|
+
<body style="font-family:system-ui,-apple-system,sans-serif;max-width:30rem;margin:18vh auto;text-align:center;color:#111">
|
|
1401
|
+
<div style="font-size:2.5rem">\u{1F4DE}</div>
|
|
1402
|
+
<h1 style="font-size:1.35rem;margin:.5rem 0">${escapeHtml(title)}</h1>
|
|
1403
|
+
<p style="color:#555;line-height:1.5">${escapeHtml(body)}</p></body>`;
|
|
1404
|
+
}
|
|
1405
|
+
function openBrowser(url) {
|
|
1406
|
+
if (["1", "true", "yes"].includes((process.env.SPEKO_NO_BROWSER ?? "").toLowerCase())) return;
|
|
1407
|
+
try {
|
|
1408
|
+
const p = platform();
|
|
1409
|
+
const cmd2 = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
1410
|
+
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
1411
|
+
const child = spawn(cmd2, args, { stdio: "ignore", detached: true });
|
|
1412
|
+
child.on("error", () => {
|
|
1413
|
+
});
|
|
1414
|
+
child.unref();
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
async function discover() {
|
|
1419
|
+
const r = await fetch(AUTH_DISCOVERY, { signal: AbortSignal.timeout(15e3) });
|
|
1420
|
+
if (!r.ok) throw new Error(`OAuth discovery failed (HTTP ${r.status}) at ${AUTH_DISCOVERY}`);
|
|
1421
|
+
const d = await r.json();
|
|
1422
|
+
if (!d.authorization_endpoint || !d.token_endpoint || !d.registration_endpoint || !d.issuer) {
|
|
1423
|
+
throw new Error("OAuth discovery doc is missing required endpoints");
|
|
1424
|
+
}
|
|
1425
|
+
return d;
|
|
1426
|
+
}
|
|
1427
|
+
async function registerClient(registrationEndpoint, redirectUri) {
|
|
1428
|
+
const r = await fetch(registrationEndpoint, {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
headers: { "content-type": "application/json" },
|
|
1431
|
+
body: JSON.stringify({
|
|
1432
|
+
client_name: "Speko Calls CLI",
|
|
1433
|
+
redirect_uris: [redirectUri],
|
|
1434
|
+
grant_types: ["authorization_code"],
|
|
1435
|
+
response_types: ["code"],
|
|
1436
|
+
token_endpoint_auth_method: "none",
|
|
1437
|
+
type: "native",
|
|
1438
|
+
scope: "openid profile email"
|
|
1439
|
+
}),
|
|
1440
|
+
signal: AbortSignal.timeout(15e3)
|
|
1441
|
+
});
|
|
1442
|
+
if (!r.ok) throw new Error(`client registration failed (HTTP ${r.status})`);
|
|
1443
|
+
const j = await r.json();
|
|
1444
|
+
if (!j.client_id) throw new Error("client registration returned no client_id");
|
|
1445
|
+
return j.client_id;
|
|
1446
|
+
}
|
|
1447
|
+
async function fetchOrgKey(bearer2) {
|
|
1448
|
+
const r = await fetch(`${API_BASE}/v1/api-keys/organization-credentials`, {
|
|
1449
|
+
headers: { authorization: `Bearer ${bearer2}` },
|
|
1450
|
+
signal: AbortSignal.timeout(15e3)
|
|
1451
|
+
});
|
|
1452
|
+
if (r.status === 403) {
|
|
1453
|
+
throw new Error("your account has no organization yet \u2014 finish signup at platform.speko.dev, then retry");
|
|
1454
|
+
}
|
|
1455
|
+
if (!r.ok) {
|
|
1456
|
+
const body = await r.text().catch(() => "");
|
|
1457
|
+
throw new Error(`couldn't fetch your API key (HTTP ${r.status})${body ? `: ${body.slice(0, 160)}` : ""}`);
|
|
1458
|
+
}
|
|
1459
|
+
const j = await r.json();
|
|
1460
|
+
const key = j.mcpApiKey?.key;
|
|
1461
|
+
if (!key) throw new Error("API-key response was missing mcpApiKey.key");
|
|
1462
|
+
return key;
|
|
1463
|
+
}
|
|
1464
|
+
function startLoopback(expectedState) {
|
|
1465
|
+
return new Promise((resolve4, reject) => {
|
|
1466
|
+
let resolveCode;
|
|
1467
|
+
let rejectCode;
|
|
1468
|
+
const waitForCode = new Promise((res, rej) => {
|
|
1469
|
+
resolveCode = res;
|
|
1470
|
+
rejectCode = rej;
|
|
1471
|
+
});
|
|
1472
|
+
const timeout = setTimeout(
|
|
1473
|
+
() => rejectCode(new Error("login timed out (5 min) \u2014 no redirect received")),
|
|
1474
|
+
LOGIN_TIMEOUT_MS
|
|
1475
|
+
);
|
|
1476
|
+
if (typeof timeout.unref === "function") timeout.unref();
|
|
1477
|
+
const server2 = createServer((req, res) => {
|
|
1478
|
+
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1479
|
+
if (u.pathname !== "/callback") {
|
|
1480
|
+
res.writeHead(404);
|
|
1481
|
+
res.end();
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const send = (status, title, body) => {
|
|
1485
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
1486
|
+
res.end(resultPage(title, body));
|
|
1487
|
+
};
|
|
1488
|
+
const err = u.searchParams.get("error");
|
|
1489
|
+
const code = u.searchParams.get("code");
|
|
1490
|
+
const state = u.searchParams.get("state");
|
|
1491
|
+
clearTimeout(timeout);
|
|
1492
|
+
if (err) {
|
|
1493
|
+
send(400, "Sign-in failed", `Authorization was denied (${err}). You can close this tab and try again.`);
|
|
1494
|
+
rejectCode(new Error(`authorization denied: ${err}`));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (!code || state !== expectedState) {
|
|
1498
|
+
send(400, "Sign-in failed", "The response was invalid or didn't match. Close this tab and re-run the login.");
|
|
1499
|
+
rejectCode(new Error("state mismatch or missing authorization code"));
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
send(200, "You're connected \u2713", "Speko Calls is signed in. You can close this tab and return to your terminal.");
|
|
1503
|
+
resolveCode(code);
|
|
1504
|
+
});
|
|
1505
|
+
server2.on("error", reject);
|
|
1506
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
1507
|
+
const port = server2.address().port;
|
|
1508
|
+
resolve4({ server: server2, redirectUri: `http://127.0.0.1:${port}/callback`, waitForCode });
|
|
1509
|
+
});
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
async function browserLogin(log = () => {
|
|
1513
|
+
}) {
|
|
1514
|
+
const disc = await discover();
|
|
1515
|
+
const verifier = b64url(randomBytes(32));
|
|
1516
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
1517
|
+
const state = b64url(randomBytes(16));
|
|
1518
|
+
const { server: server2, redirectUri, waitForCode } = await startLoopback(state);
|
|
1519
|
+
try {
|
|
1520
|
+
const clientId = await registerClient(disc.registration_endpoint, redirectUri);
|
|
1521
|
+
const authUrl = new URL(disc.authorization_endpoint);
|
|
1522
|
+
authUrl.searchParams.set("response_type", "code");
|
|
1523
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
1524
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
1525
|
+
authUrl.searchParams.set("scope", "openid profile email");
|
|
1526
|
+
authUrl.searchParams.set("state", state);
|
|
1527
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
1528
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1529
|
+
log("Opening your browser to sign in to Speko\u2026");
|
|
1530
|
+
log(`If it doesn't open, paste this URL into your browser:
|
|
1531
|
+
${authUrl.toString()}`);
|
|
1532
|
+
openBrowser(authUrl.toString());
|
|
1533
|
+
log("Waiting for you to finish signing in\u2026");
|
|
1534
|
+
const code = await waitForCode;
|
|
1535
|
+
const tok = await fetch(disc.token_endpoint, {
|
|
1536
|
+
method: "POST",
|
|
1537
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1538
|
+
body: new URLSearchParams({
|
|
1539
|
+
grant_type: "authorization_code",
|
|
1540
|
+
code,
|
|
1541
|
+
redirect_uri: redirectUri,
|
|
1542
|
+
client_id: clientId,
|
|
1543
|
+
code_verifier: verifier
|
|
1544
|
+
}),
|
|
1545
|
+
signal: AbortSignal.timeout(2e4)
|
|
1546
|
+
});
|
|
1547
|
+
if (!tok.ok) {
|
|
1548
|
+
const body = await tok.text().catch(() => "");
|
|
1549
|
+
throw new Error(`token exchange failed (HTTP ${tok.status})${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
1550
|
+
}
|
|
1551
|
+
const tj = await tok.json();
|
|
1552
|
+
const bearer2 = tj.id_token ?? tj.access_token;
|
|
1553
|
+
if (!bearer2) throw new Error("token endpoint returned neither an id_token nor an access_token");
|
|
1554
|
+
return await fetchOrgKey(bearer2);
|
|
1555
|
+
} finally {
|
|
1556
|
+
server2.close();
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/cli/init.ts
|
|
1561
|
+
var API_BASE2 = (process.env.SPEKOAI_API_URL || "https://api.speko.dev").replace(/\/+$/, "");
|
|
1384
1562
|
var DASHBOARD = "https://platform.speko.dev";
|
|
1385
1563
|
var PKG = "@spekoai/mcp-calls";
|
|
1386
1564
|
var SERVER_NAME = "speko-calls";
|
|
@@ -1393,7 +1571,7 @@ var c = {
|
|
|
1393
1571
|
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
1394
1572
|
};
|
|
1395
1573
|
function parseFlags(argv) {
|
|
1396
|
-
const f = { scope: "user", yes: false, printConfig: false };
|
|
1574
|
+
const f = { scope: "user", yes: false, printConfig: false, paste: false };
|
|
1397
1575
|
for (let i = 0; i < argv.length; i++) {
|
|
1398
1576
|
const a = argv[i];
|
|
1399
1577
|
if (a === "--token") f.token = argv[++i];
|
|
@@ -1401,6 +1579,7 @@ function parseFlags(argv) {
|
|
|
1401
1579
|
else if (a === "--scope") f.scope = argv[++i] ?? "user";
|
|
1402
1580
|
else if (a === "--yes" || a === "-y") f.yes = true;
|
|
1403
1581
|
else if (a === "--print-config") f.printConfig = true;
|
|
1582
|
+
else if (a === "--paste" || a === "--manual") f.paste = true;
|
|
1404
1583
|
}
|
|
1405
1584
|
return f;
|
|
1406
1585
|
}
|
|
@@ -1451,12 +1630,12 @@ function askSecret(query) {
|
|
|
1451
1630
|
stdin.on("data", onData);
|
|
1452
1631
|
});
|
|
1453
1632
|
}
|
|
1454
|
-
function
|
|
1633
|
+
function openBrowser2(url) {
|
|
1455
1634
|
try {
|
|
1456
|
-
const p =
|
|
1635
|
+
const p = platform2();
|
|
1457
1636
|
const cmd2 = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
1458
1637
|
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
1459
|
-
const child =
|
|
1638
|
+
const child = spawn2(cmd2, args, { stdio: "ignore", detached: true });
|
|
1460
1639
|
child.on("error", () => {
|
|
1461
1640
|
});
|
|
1462
1641
|
child.unref();
|
|
@@ -1465,7 +1644,7 @@ function openBrowser(url) {
|
|
|
1465
1644
|
}
|
|
1466
1645
|
async function verifyKey(key) {
|
|
1467
1646
|
try {
|
|
1468
|
-
const r = await fetch(`${
|
|
1647
|
+
const r = await fetch(`${API_BASE2}/v1/organization`, {
|
|
1469
1648
|
headers: { authorization: `Bearer ${key}` },
|
|
1470
1649
|
signal: AbortSignal.timeout(15e3)
|
|
1471
1650
|
});
|
|
@@ -1485,11 +1664,13 @@ function claudeCliPresent() {
|
|
|
1485
1664
|
}
|
|
1486
1665
|
function desktopConfigPath() {
|
|
1487
1666
|
const home = homedir();
|
|
1488
|
-
if (
|
|
1489
|
-
if (
|
|
1667
|
+
if (platform2() === "darwin") return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1668
|
+
if (platform2() === "win32") return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1490
1669
|
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1491
1670
|
}
|
|
1492
|
-
function configureClaudeCode(key, scope) {
|
|
1671
|
+
function configureClaudeCode(key, scope, extraEnv = {}) {
|
|
1672
|
+
const envArgs = ["--env", `SPEKO_API_KEY=${key}`];
|
|
1673
|
+
for (const [k, v] of Object.entries(extraEnv)) envArgs.push("--env", `${k}=${v}`);
|
|
1493
1674
|
const manual = `claude mcp add ${SERVER_NAME} --scope ${scope} --env SPEKO_API_KEY=<your-key> -- npx -y ${PKG}`;
|
|
1494
1675
|
if (!claudeCliPresent()) {
|
|
1495
1676
|
console.log(c.yellow(" \u2022 Claude Code CLI not found on PATH. Run this yourself once installed:"));
|
|
@@ -1499,7 +1680,7 @@ function configureClaudeCode(key, scope) {
|
|
|
1499
1680
|
spawnSync("claude", ["mcp", "remove", SERVER_NAME, "--scope", scope], { stdio: "ignore" });
|
|
1500
1681
|
const r = spawnSync(
|
|
1501
1682
|
"claude",
|
|
1502
|
-
["mcp", "add", SERVER_NAME, "--scope", scope,
|
|
1683
|
+
["mcp", "add", SERVER_NAME, "--scope", scope, ...envArgs, "--", "npx", "-y", PKG],
|
|
1503
1684
|
{ stdio: "inherit" }
|
|
1504
1685
|
);
|
|
1505
1686
|
if (r.status === 0) {
|
|
@@ -1510,7 +1691,7 @@ function configureClaudeCode(key, scope) {
|
|
|
1510
1691
|
console.log(" " + c.cyan(manual));
|
|
1511
1692
|
return false;
|
|
1512
1693
|
}
|
|
1513
|
-
function configureClaudeDesktop(key) {
|
|
1694
|
+
function configureClaudeDesktop(key, extraEnv = {}) {
|
|
1514
1695
|
const path = desktopConfigPath();
|
|
1515
1696
|
try {
|
|
1516
1697
|
let cfg = {};
|
|
@@ -1527,7 +1708,7 @@ function configureClaudeDesktop(key) {
|
|
|
1527
1708
|
mkdirSync(dirname(path), { recursive: true });
|
|
1528
1709
|
}
|
|
1529
1710
|
const servers = cfg.mcpServers && typeof cfg.mcpServers === "object" ? cfg.mcpServers : {};
|
|
1530
|
-
servers[SERVER_NAME] = { command: "npx", args: ["-y", PKG], env: { SPEKO_API_KEY: key } };
|
|
1711
|
+
servers[SERVER_NAME] = { command: "npx", args: ["-y", PKG], env: { SPEKO_API_KEY: key, ...extraEnv } };
|
|
1531
1712
|
cfg.mcpServers = servers;
|
|
1532
1713
|
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}
|
|
1533
1714
|
`);
|
|
@@ -1542,8 +1723,11 @@ function configureClaudeDesktop(key) {
|
|
|
1542
1723
|
function installSkill() {
|
|
1543
1724
|
try {
|
|
1544
1725
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1545
|
-
const src =
|
|
1546
|
-
|
|
1726
|
+
const src = [
|
|
1727
|
+
resolve(here, "..", "skills", SERVER_NAME, "SKILL.md"),
|
|
1728
|
+
resolve(here, "..", "..", "skills", SERVER_NAME, "SKILL.md")
|
|
1729
|
+
].find((p) => existsSync(p));
|
|
1730
|
+
if (!src) {
|
|
1547
1731
|
console.log(c.yellow(" \u2022 Bundled skill not found in package; skipping skill install."));
|
|
1548
1732
|
return false;
|
|
1549
1733
|
}
|
|
@@ -1561,27 +1745,33 @@ function installSkill() {
|
|
|
1561
1745
|
return false;
|
|
1562
1746
|
}
|
|
1563
1747
|
}
|
|
1564
|
-
async function runInit(argv) {
|
|
1748
|
+
async function runInit(argv, mode = "init") {
|
|
1565
1749
|
const f = parseFlags(argv);
|
|
1566
|
-
|
|
1567
|
-
console.log(
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (ok === "n" || ok === "no") {
|
|
1573
|
-
console.log(" Aborted.");
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1750
|
+
const quick = mode === "login";
|
|
1751
|
+
console.log(c.bold(quick ? "\n Speko Calls \u2014 sign in\n" : "\n Speko Calls \u2014 setup\n"));
|
|
1752
|
+
if (!quick) {
|
|
1753
|
+
console.log(" This MCP places " + c.bold("real, disclosed") + " outbound phone calls to " + c.bold("businesses") + ",");
|
|
1754
|
+
console.log(" straight from your coding agent. Every call opens with an AI disclosure;");
|
|
1755
|
+
console.log(" business lines only; quiet hours 08:00\u201321:00 in the destination's local time.\n");
|
|
1576
1756
|
}
|
|
1577
1757
|
let key = (f.token ?? process.env.SPEKO_API_KEY ?? "").trim();
|
|
1758
|
+
if (!key && !f.paste) {
|
|
1759
|
+
console.log("\n Sign in to connect \u2014 this opens your browser. " + c.dim("No key to copy or paste."));
|
|
1760
|
+
try {
|
|
1761
|
+
key = await browserLogin((m) => console.log(c.dim(" " + m)));
|
|
1762
|
+
console.log(c.green(" \u2713 Signed in \u2014 fetched your API key automatically."));
|
|
1763
|
+
} catch (e) {
|
|
1764
|
+
console.log(c.yellow(` \u2022 Browser sign-in didn't complete (${e.message}).`));
|
|
1765
|
+
console.log(" Falling back to manual key entry. " + c.dim("(Use --paste to skip the browser next time.)"));
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1578
1768
|
if (!key) {
|
|
1579
1769
|
console.log(`
|
|
1580
1770
|
Opening ${c.cyan(DASHBOARD)} \u2014 sign in and create an API key (starts with "sk_").`);
|
|
1581
1771
|
console.log(c.dim(` (If it doesn't open: visit ${DASHBOARD} and copy your key.)
|
|
1582
1772
|
`));
|
|
1583
1773
|
if (!f.yes) await ask(" Press Enter to open your browser\u2026 ");
|
|
1584
|
-
|
|
1774
|
+
openBrowser2(DASHBOARD);
|
|
1585
1775
|
key = await askSecret(" Paste your Speko API key: ");
|
|
1586
1776
|
}
|
|
1587
1777
|
if (!key) {
|
|
@@ -1607,17 +1797,26 @@ async function runInit(argv) {
|
|
|
1607
1797
|
console.log(" " + c.cyan(JSON.stringify({ [SERVER_NAME]: { command: "npx", args: ["-y", PKG], env: { SPEKO_API_KEY: key } } })));
|
|
1608
1798
|
return;
|
|
1609
1799
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1800
|
+
const target = (f.client || "both").toLowerCase();
|
|
1801
|
+
const extraEnv = {};
|
|
1802
|
+
if (!quick && !f.yes) {
|
|
1803
|
+
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();
|
|
1804
|
+
if (demo === "y" || demo === "yes") {
|
|
1805
|
+
const num = (await ask(" Number to ring, E.164 (e.g. +15551234567): ")).replace(/\s/g, "");
|
|
1806
|
+
if (/^\+?[1-9]\d{6,14}$/.test(num)) {
|
|
1807
|
+
const biz = (await ask(" Business name to say on the call (default: Sakura Sushi): ")).trim() || "Sakura Sushi";
|
|
1808
|
+
extraEnv.SPEKO_DEMO = "1";
|
|
1809
|
+
extraEnv.SPEKO_DEMO_E164 = num.startsWith("+") ? num : `+${num}`;
|
|
1810
|
+
extraEnv.SPEKO_DEMO_BUSINESS = biz;
|
|
1811
|
+
console.log(c.dim(` Demo on: "call ${biz}" will ring ${extraEnv.SPEKO_DEMO_E164}.`));
|
|
1812
|
+
} else {
|
|
1813
|
+
console.log(c.yellow(" \u2022 Skipping demo \u2014 that didn't look like an E.164 number."));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1617
1816
|
}
|
|
1618
1817
|
console.log("");
|
|
1619
|
-
if (target === "code" || target === "both") configureClaudeCode(key, f.scope);
|
|
1620
|
-
if (target === "desktop" || target === "both") configureClaudeDesktop(key);
|
|
1818
|
+
if (target === "code" || target === "both") configureClaudeCode(key, f.scope, extraEnv);
|
|
1819
|
+
if (target === "desktop" || target === "both") configureClaudeDesktop(key, extraEnv);
|
|
1621
1820
|
installSkill();
|
|
1622
1821
|
console.log(c.bold("\n \u2705 Done.\n"));
|
|
1623
1822
|
console.log(" Try it: open your agent and say");
|
|
@@ -1687,7 +1886,7 @@ import { MCPTool as MCPTool2 } from "mcp-framework";
|
|
|
1687
1886
|
import { z as z2 } from "zod";
|
|
1688
1887
|
|
|
1689
1888
|
// src/http/serverClient.ts
|
|
1690
|
-
import { randomBytes as
|
|
1889
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1691
1890
|
var DemoServerError = class extends Error {
|
|
1692
1891
|
name = "DemoServerError";
|
|
1693
1892
|
};
|
|
@@ -1710,7 +1909,7 @@ var InProcessBackend = class {
|
|
|
1710
1909
|
if (!this.ready) {
|
|
1711
1910
|
this.ready = (async () => {
|
|
1712
1911
|
if (!(process.env.SPEKO_DIAL_TOKEN_SECRET ?? "").trim()) {
|
|
1713
|
-
process.env.SPEKO_DIAL_TOKEN_SECRET =
|
|
1912
|
+
process.env.SPEKO_DIAL_TOKEN_SECRET = randomBytes3(32).toString("hex");
|
|
1714
1913
|
}
|
|
1715
1914
|
const core = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
1716
1915
|
const cfg = core.loadConfig();
|
|
@@ -1847,9 +2046,9 @@ function getServerClient() {
|
|
|
1847
2046
|
// src/tools/CallNumberTool.ts
|
|
1848
2047
|
var schema2 = z2.object({
|
|
1849
2048
|
phone_number: z2.string().describe("Number to call, E.164 (e.g. +77011234567). A real number the user has consent to call."),
|
|
1850
|
-
objective: z2.string().describe("What to say / accomplish, e.g. 'Tell
|
|
2049
|
+
objective: z2.string().describe("What to say / accomplish, e.g. 'Tell Sam that John says happy birthday and misses him.'"),
|
|
1851
2050
|
caller_name: z2.string().describe("Name of the human the call is on behalf of (1-80 chars); spoken in the AI-disclosure opening."),
|
|
1852
|
-
recipient_name: z2.string().optional().describe("Who you're calling, used in the greeting (e.g. '
|
|
2051
|
+
recipient_name: z2.string().optional().describe("Who you're calling, used in the greeting (e.g. 'Sam')."),
|
|
1853
2052
|
context: z2.string().optional().describe("Optional extra context for the message."),
|
|
1854
2053
|
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."),
|
|
1855
2054
|
max_duration_seconds: z2.number().int().optional().describe("Max seconds to wait for the call to finish; clamped 30-300.")
|
|
@@ -1944,17 +2143,40 @@ var CheckCallReadinessTool = class extends MCPTool3 {
|
|
|
1944
2143
|
}
|
|
1945
2144
|
};
|
|
1946
2145
|
|
|
1947
|
-
// src/tools/
|
|
2146
|
+
// src/tools/GetCallTool.ts
|
|
1948
2147
|
import { MCPTool as MCPTool4 } from "mcp-framework";
|
|
1949
2148
|
import { z as z4 } from "zod";
|
|
1950
2149
|
var schema4 = z4.object({
|
|
1951
|
-
|
|
1952
|
-
location: z4.string().optional().describe("Optional city or area to disambiguate, e.g. 'New York'.")
|
|
2150
|
+
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.")
|
|
1953
2151
|
});
|
|
1954
|
-
var
|
|
2152
|
+
var GetCallTool = class extends MCPTool4 {
|
|
2153
|
+
name = "get_call";
|
|
2154
|
+
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.";
|
|
2155
|
+
schema = schema4;
|
|
2156
|
+
annotations = {
|
|
2157
|
+
title: "Get Call",
|
|
2158
|
+
readOnlyHint: true,
|
|
2159
|
+
destructiveHint: false,
|
|
2160
|
+
idempotentHint: true,
|
|
2161
|
+
openWorldHint: true
|
|
2162
|
+
};
|
|
2163
|
+
async execute(input) {
|
|
2164
|
+
const id = encodeURIComponent(String(input.call_id ?? "").trim());
|
|
2165
|
+
return await getServerClient().get(`/call/${id}`);
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
|
|
2169
|
+
// src/tools/LookupBusinessTool.ts
|
|
2170
|
+
import { MCPTool as MCPTool5 } from "mcp-framework";
|
|
2171
|
+
import { z as z5 } from "zod";
|
|
2172
|
+
var schema5 = z5.object({
|
|
2173
|
+
name: z5.string().min(1).describe(`Business name, e.g. "Joe's Pizza".`),
|
|
2174
|
+
location: z5.string().optional().describe("Optional city or area to disambiguate, e.g. 'New York'.")
|
|
2175
|
+
});
|
|
2176
|
+
var LookupBusinessTool = class extends MCPTool5 {
|
|
1955
2177
|
name = "lookup_business";
|
|
1956
2178
|
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.";
|
|
1957
|
-
schema =
|
|
2179
|
+
schema = schema5;
|
|
1958
2180
|
annotations = {
|
|
1959
2181
|
title: "Lookup Business",
|
|
1960
2182
|
readOnlyHint: true,
|
|
@@ -1977,14 +2199,14 @@ var LookupBusinessTool = class extends MCPTool4 {
|
|
|
1977
2199
|
};
|
|
1978
2200
|
|
|
1979
2201
|
// src/tools/MakeCallTool.ts
|
|
1980
|
-
import { MCPTool as
|
|
1981
|
-
import { z as
|
|
1982
|
-
var
|
|
1983
|
-
dial_token:
|
|
1984
|
-
objective:
|
|
1985
|
-
caller_name:
|
|
1986
|
-
context:
|
|
1987
|
-
max_duration_seconds:
|
|
2202
|
+
import { MCPTool as MCPTool6 } from "mcp-framework";
|
|
2203
|
+
import { z as z6 } from "zod";
|
|
2204
|
+
var schema6 = z6.object({
|
|
2205
|
+
dial_token: z6.string().describe("Signed dial token minted by lookup_business. Raw phone numbers are rejected."),
|
|
2206
|
+
objective: z6.string().describe("Single transactional question, e.g. 'Do you have a table for 4 at 8pm tonight?'."),
|
|
2207
|
+
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."),
|
|
2208
|
+
context: z6.string().optional().describe("Optional extra task context (party size, dates, order numbers)."),
|
|
2209
|
+
max_duration_seconds: z6.number().int().optional().describe("Max seconds to wait for the call to finish; clamped to 30-300.")
|
|
1988
2210
|
});
|
|
1989
2211
|
var MIN_WAIT2 = 30;
|
|
1990
2212
|
var MAX_WAIT2 = 300;
|
|
@@ -2012,10 +2234,10 @@ function summarize2(s) {
|
|
|
2012
2234
|
if (outcome) return outcome;
|
|
2013
2235
|
return `Call ${callId ?? ""} finished with status '${status}' and no OUTCOME line.`.trim();
|
|
2014
2236
|
}
|
|
2015
|
-
var MakeCallTool = class extends
|
|
2237
|
+
var MakeCallTool = class extends MCPTool6 {
|
|
2016
2238
|
name = "make_call";
|
|
2017
2239
|
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.";
|
|
2018
|
-
schema =
|
|
2240
|
+
schema = schema6;
|
|
2019
2241
|
annotations = {
|
|
2020
2242
|
title: "Make Call",
|
|
2021
2243
|
readOnlyHint: false,
|
|
@@ -2054,19 +2276,20 @@ var MakeCallTool = class extends MCPTool5 {
|
|
|
2054
2276
|
// src/index.ts
|
|
2055
2277
|
var cmd = process.argv[2];
|
|
2056
2278
|
if (cmd === "init" || cmd === "setup" || cmd === "login") {
|
|
2057
|
-
await runInit(process.argv.slice(3));
|
|
2279
|
+
await runInit(process.argv.slice(3), cmd);
|
|
2058
2280
|
process.exit(0);
|
|
2059
2281
|
}
|
|
2060
2282
|
loadEnv();
|
|
2061
2283
|
var server = new MCPServer({
|
|
2062
2284
|
name: "speko-calls",
|
|
2063
|
-
version: "0.
|
|
2285
|
+
version: "0.3.0",
|
|
2064
2286
|
transport: { type: "stdio" }
|
|
2065
2287
|
});
|
|
2066
2288
|
server.addTool(LookupBusinessTool);
|
|
2067
2289
|
server.addTool(MakeCallTool);
|
|
2068
2290
|
server.addTool(CallNumberTool);
|
|
2069
2291
|
server.addTool(CheckCallReadinessTool);
|
|
2292
|
+
server.addTool(GetCallTool);
|
|
2070
2293
|
server.addTool(CallMeTool);
|
|
2071
2294
|
await server.start();
|
|
2072
2295
|
//# sourceMappingURL=index.js.map
|