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 CHANGED
@@ -1,21 +1,29 @@
1
1
  # voxplo-mcp
2
2
 
3
- MCP server that gives any MCP client (Claude Desktop, Cursor, etc.) a phone -- place outbound AI calls, poll for results, and list call history via the Voxplo Agent API.
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
- ## Prerequisites
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
- - Node 18 or newer
8
- - A Voxplo agent API key (starts with `bys_ak_`)
10
+ ## Option A: hosted server (recommended)
9
11
 
10
- ## Quick start
12
+ No install. Add `https://mcp.voxplo.ai/mcp` as a remote MCP server in your client:
11
13
 
12
- ```bash
13
- cd mcp
14
- npm install
15
- VOXPLO_API_KEY=bys_ak_your_key_here npm start
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "voxplo": { "type": "streamable-http", "url": "https://mcp.voxplo.ai/mcp" }
18
+ }
19
+ }
16
20
  ```
17
21
 
18
- ## MCP client configuration
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": "node",
29
- "args": ["/absolute/path/to/voip-agent/mcp/src/server.js"],
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
- ## Manual end-to-end test
132
+ ## Safety
119
133
 
120
- 1. Set `VOXPLO_API_KEY` and optionally `VOXPLO_API_BASE` in your MCP client config.
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
- cd mcp && node --test
141
+ node --test
133
142
  ```
134
143
 
135
- Check server parses:
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 mcp/src/server.js
147
+ VOXPLO_API_KEY=bys_ak_test node src/server.js
145
148
  ```
146
149
 
147
- Smoke test (with dummy key, should print "ready on stdio" to stderr):
150
+ ## Links
148
151
 
149
- ```bash
150
- VOXPLO_API_KEY=bys_ak_test node mcp/src/server.js
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.0",
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/voip-agent.git",
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
- // SDK high-level registration: (name, description, zodRawShape, handler).
21
- // SDK version 1.29.0 uses this variadic overload.
22
- server.tool(t.name, t.description, t.inputShape, t.handler);
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 three MCP tools, as plain definitions ({name, description, inputShape, handler})
2
- // so they are unit-testable without the MCP SDK. server.js registers them on an
3
- // McpServer. Handlers call the injected REST client and shape the MCP result; API
4
- // errors are surfaced as { isError: true } with the mapped message (never thrown).
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