voxplo-mcp 0.1.0 → 0.1.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 +51 -46
- package/package.json +3 -3
- package/src/server.js +6 -3
- package/src/tools.js +14 -4
package/README.md
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
# voxplo-mcp
|
|
2
2
|
|
|
3
|
-
MCP server
|
|
3
|
+
Give your AI agent a phone. This MCP server lets any MCP client (Claude, Cursor, ChatGPT developer mode, and others) place real outbound AI phone calls through [Voxplo](https://voxplo.ai): the AI calls a person, conducts the conversation toward an objective, and returns a summary, transcript, and structured extracted fields.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **Hosted (remote) server:** `https://mcp.voxplo.ai/mcp` (Streamable HTTP + OAuth, no install)
|
|
6
|
+
- **Local (stdio) server:** `npx voxplo-mcp` (this package)
|
|
7
|
+
- **Docs:** https://voxplo.ai/docs/agent-api/mcp
|
|
8
|
+
- **Tools:** `place_call`, `set_appointment`, `get_call`, `list_calls`
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
- A Voxplo agent API key (starts with `bys_ak_`)
|
|
10
|
+
## Option A: hosted server (recommended)
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
No install. Add `https://mcp.voxplo.ai/mcp` as a remote MCP server in your client:
|
|
11
13
|
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"voxplo": { "type": "streamable-http", "url": "https://mcp.voxplo.ai/mcp" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
In Claude: Settings, then Connectors, then "Add custom connector", paste `https://mcp.voxplo.ai/mcp`. Your browser opens Voxplo's consent screen; approve by pasting an Agent API key from your [Developers page](https://voxplo.ai/account/developers). The client never sees the key itself, only an OAuth token.
|
|
23
|
+
|
|
24
|
+
## Option B: local server (this package)
|
|
25
|
+
|
|
26
|
+
Prerequisites: Node 18+, a Voxplo Agent API key (starts with `bys_ak_`, from your [Developers page](https://voxplo.ai/account/developers)).
|
|
19
27
|
|
|
20
28
|
### Claude Desktop
|
|
21
29
|
|
|
@@ -25,8 +33,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
25
33
|
{
|
|
26
34
|
"mcpServers": {
|
|
27
35
|
"voxplo": {
|
|
28
|
-
"command": "
|
|
29
|
-
"args": ["
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "voxplo-mcp"],
|
|
30
38
|
"env": { "VOXPLO_API_KEY": "bys_ak_your_key_here" }
|
|
31
39
|
}
|
|
32
40
|
}
|
|
@@ -35,19 +43,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
35
43
|
|
|
36
44
|
### Cursor
|
|
37
45
|
|
|
38
|
-
Add to `.cursor/mcp.json` in your project (or the global `~/.cursor/mcp.json`)
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"mcpServers": {
|
|
43
|
-
"voxplo": {
|
|
44
|
-
"command": "node",
|
|
45
|
-
"args": ["/absolute/path/to/voip-agent/mcp/src/server.js"],
|
|
46
|
-
"env": { "VOXPLO_API_KEY": "bys_ak_your_key_here" }
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
```
|
|
46
|
+
Add the same block to `.cursor/mcp.json` in your project (or the global `~/.cursor/mcp.json`).
|
|
51
47
|
|
|
52
48
|
To point at a staging or self-hosted instance, add `"VOXPLO_API_BASE": "https://staging.example.com"` to the `env` block.
|
|
53
49
|
|
|
@@ -70,6 +66,24 @@ Places an outbound AI phone call toward an objective. Returns a `callId` immedia
|
|
|
70
66
|
|
|
71
67
|
**Returns:** `{ callId, status }` where `status` is `queued` on success.
|
|
72
68
|
|
|
69
|
+
### `set_appointment`
|
|
70
|
+
|
|
71
|
+
Places an outbound AI phone call to set up a meeting. The assistant calls the person, agrees a time, and the result (poll `get_call`) carries an `outcome` (`appointment_set | callback_requested | declined | no_answer | failed`) and, when set, the appointment details (`startISO`, `timezone`, `durationMinutes`, `attendee`, `subject`). Returns a `callId` immediately.
|
|
72
|
+
|
|
73
|
+
**Inputs:**
|
|
74
|
+
|
|
75
|
+
| Field | Type | Required | Description |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `to` | string | yes | Who to call, in E.164 |
|
|
78
|
+
| `subject` | string | yes | What the meeting is about |
|
|
79
|
+
| `durationMinutes` | number | no | Meeting length in minutes (default 30) |
|
|
80
|
+
| `timePreference` | string | no | Natural-language window, e.g. "weekday mornings next week" |
|
|
81
|
+
| `slots` | array | no | Specific start times to offer: `[{ startISO }]`, ISO-8601 with offset |
|
|
82
|
+
| `timezone` | string | no | IANA timezone for the appointment, e.g. `Asia/Jerusalem` |
|
|
83
|
+
| `autoBook` | boolean | no | Also book the agreed time on your connected calendar |
|
|
84
|
+
| `webhookUrl` | string | no | HTTPS URL to receive the signed result |
|
|
85
|
+
| `callerId` | string | no | Caller ID override; must be a number on your account |
|
|
86
|
+
|
|
73
87
|
### `get_call`
|
|
74
88
|
|
|
75
89
|
Fetches the current status and result of a call.
|
|
@@ -78,7 +92,7 @@ Fetches the current status and result of a call.
|
|
|
78
92
|
|
|
79
93
|
| Field | Type | Required | Description |
|
|
80
94
|
|---|---|---|---|
|
|
81
|
-
| `callId` | string | yes | The `callId` from `place_call` |
|
|
95
|
+
| `callId` | string | yes | The `callId` from `place_call` or `set_appointment` |
|
|
82
96
|
|
|
83
97
|
**Returns:** `{ id, status, summary, transcript, extracted, recordingUrl, to, objective, startedAt, endedAt, durationSec, error? }`
|
|
84
98
|
|
|
@@ -115,37 +129,28 @@ Lists recent calls placed by your account (most recent first).
|
|
|
115
129
|
4. Ask: "Check on call `<callId>` -- did it complete?"
|
|
116
130
|
5. The assistant calls `get_call` and returns the status, summary, and any extracted fields.
|
|
117
131
|
|
|
118
|
-
##
|
|
132
|
+
## Safety
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
2. Configure the client using the JSON above.
|
|
122
|
-
3. Ask your agent to call a number you control with a specific objective and one extraction field (e.g. `{ name: "confirmed", type: "boolean" }`).
|
|
123
|
-
4. Confirm `place_call` returns a `callId`.
|
|
124
|
-
5. Call `get_call` repeatedly until `status` is terminal; verify `extracted.confirmed` is present.
|
|
125
|
-
6. Try a blocked destination (e.g. a premium-rate number) and confirm the mapped error message is returned.
|
|
134
|
+
Outbound destinations are screened server-side (premium-rate, satellite, and other high-risk routes are blocked), calls carry an AI disclosure when asked, and the call-placing tools are annotated `openWorldHint: true` so clients ask for confirmation before dialing. Details: https://voxplo.ai/security
|
|
126
135
|
|
|
127
136
|
## Development
|
|
128
137
|
|
|
129
138
|
Run tests:
|
|
130
139
|
|
|
131
140
|
```bash
|
|
132
|
-
|
|
141
|
+
node --test
|
|
133
142
|
```
|
|
134
143
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
node --check mcp/src/server.js
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Smoke test (missing key, should exit 1):
|
|
144
|
+
Smoke test (with dummy key, should print "ready on stdio" to stderr):
|
|
142
145
|
|
|
143
146
|
```bash
|
|
144
|
-
node
|
|
147
|
+
VOXPLO_API_KEY=bys_ak_test node src/server.js
|
|
145
148
|
```
|
|
146
149
|
|
|
147
|
-
|
|
150
|
+
## Links
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
- Voxplo: https://voxplo.ai
|
|
153
|
+
- Agent API reference: https://voxplo.ai/docs/agent-api
|
|
154
|
+
- OpenAPI spec: https://www.voxplo.ai/openapi.json
|
|
155
|
+
- Hosted MCP server card: https://mcp.voxplo.ai/.well-known/mcp/server-card.json
|
|
156
|
+
- License: MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voxplo-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"mcpName": "ai.voxplo/voxplo",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"description": "MCP server giving AI agents a phone via Voxplo outbound AI calls.",
|
|
6
7
|
"license": "MIT",
|
|
@@ -8,8 +9,7 @@
|
|
|
8
9
|
"homepage": "https://voxplo.ai/docs/agent-api",
|
|
9
10
|
"repository": {
|
|
10
11
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/allexp1/
|
|
12
|
-
"directory": "mcp"
|
|
12
|
+
"url": "https://github.com/allexp1/voxplo-mcp.git"
|
|
13
13
|
},
|
|
14
14
|
"keywords": ["voice", "ai", "phone", "outbound", "agent", "mcp", "voip", "calls"],
|
|
15
15
|
"files": ["src", "README.md", "LICENSE"],
|
package/src/server.js
CHANGED
|
@@ -17,9 +17,12 @@ async function main() {
|
|
|
17
17
|
const server = new McpServer({ name: 'voxplo-mcp', version: '0.1.0' });
|
|
18
18
|
|
|
19
19
|
for (const t of buildTools(client)) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
server.registerTool(t.name, {
|
|
21
|
+
title: t.title,
|
|
22
|
+
description: t.description,
|
|
23
|
+
inputSchema: t.inputShape,
|
|
24
|
+
annotations: t.annotations,
|
|
25
|
+
}, t.handler);
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
await server.connect(new StdioServerTransport());
|
package/src/tools.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// so they are unit-testable without the MCP SDK. server.js
|
|
3
|
-
// McpServer. Handlers call the injected REST client and shape the
|
|
4
|
-
// errors are surfaced as { isError: true } with the mapped message
|
|
1
|
+
// The four MCP tools, as plain definitions ({name, title, description, inputShape,
|
|
2
|
+
// annotations, handler}) so they are unit-testable without the MCP SDK. server.js
|
|
3
|
+
// registers them on an McpServer. Handlers call the injected REST client and shape the
|
|
4
|
+
// MCP result; API errors are surfaced as { isError: true } with the mapped message
|
|
5
|
+
// (never thrown). Annotations follow the MCP tool-annotation hints: the two call-placing
|
|
6
|
+
// tools reach a real person by phone (openWorldHint), the two read tools are readOnly.
|
|
5
7
|
import { z } from 'zod';
|
|
6
8
|
|
|
7
9
|
const okText = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -12,10 +14,12 @@ export function buildTools(client) {
|
|
|
12
14
|
return [
|
|
13
15
|
{
|
|
14
16
|
name: 'place_call',
|
|
17
|
+
title: 'Place an AI phone call',
|
|
15
18
|
description:
|
|
16
19
|
'Place an outbound AI phone call toward an objective. Returns a callId immediately; ' +
|
|
17
20
|
'the call is conducted asynchronously. Poll get_call with that callId until status is ' +
|
|
18
21
|
'terminal (completed | no_answer | voicemail | declined | failed).',
|
|
22
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
19
23
|
inputShape: {
|
|
20
24
|
to: z.string().describe('Destination phone number in E.164, e.g. +14155550123.'),
|
|
21
25
|
objective: z.string().describe('What the call should accomplish.'),
|
|
@@ -35,11 +39,13 @@ export function buildTools(client) {
|
|
|
35
39
|
},
|
|
36
40
|
{
|
|
37
41
|
name: 'set_appointment',
|
|
42
|
+
title: 'Set an appointment by phone',
|
|
38
43
|
description:
|
|
39
44
|
'Place an outbound AI phone call to set up a meeting. The assistant calls the person, ' +
|
|
40
45
|
'agrees a time, and the result (poll get_call) carries outcome (appointment_set | ' +
|
|
41
46
|
'callback_requested | declined | no_answer | failed) and, when set, the appointment ' +
|
|
42
47
|
'(startISO, timezone, durationMinutes, attendee, subject). Returns a callId immediately.',
|
|
48
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
43
49
|
inputShape: {
|
|
44
50
|
to: z.string().describe('Who to call, in E.164, e.g. +14155550123.'),
|
|
45
51
|
subject: z.string().describe('What the meeting is about.'),
|
|
@@ -59,16 +65,20 @@ export function buildTools(client) {
|
|
|
59
65
|
},
|
|
60
66
|
{
|
|
61
67
|
name: 'get_call',
|
|
68
|
+
title: 'Get call status and result',
|
|
62
69
|
description:
|
|
63
70
|
'Get the status and result of a call by id. status is one of queued | ringing | ' +
|
|
64
71
|
'in_progress (still running, poll again) or completed | no_answer | voicemail | ' +
|
|
65
72
|
'declined | failed (terminal). Returns summary, transcript, and extracted fields.',
|
|
73
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
66
74
|
inputShape: { callId: z.string().describe('The callId returned by place_call.') },
|
|
67
75
|
handler: async (args) => result(await client.request('GET', `/v1/agent/calls/${encodeURIComponent(args.callId)}`)),
|
|
68
76
|
},
|
|
69
77
|
{
|
|
70
78
|
name: 'list_calls',
|
|
79
|
+
title: 'List recent calls',
|
|
71
80
|
description: 'List recent calls placed by your account (most recent first).',
|
|
81
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
72
82
|
inputShape: { limit: z.number().int().positive().max(100).optional().describe('Max calls to return (default 20, max 100).') },
|
|
73
83
|
handler: async (args) => {
|
|
74
84
|
const limit = Math.min(Math.max(1, Number(args.limit) || 20), 100); // backstop for direct callers bypassing zod
|