@honkio/mcp 1.4.0 → 1.6.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 +193 -65
- package/dist/auth.d.ts +40 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +98 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +43 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +97 -22
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +51 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +248 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -46
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +10 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +10 -0
- package/dist/log.js.map +1 -0
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +23 -23
- package/dist/prompts.js.map +1 -1
- package/dist/rateLimit.d.ts +50 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +85 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +4 -4
- package/dist/resources.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +46 -0
- package/dist/server.js.map +1 -0
- package/dist/toolMeta.d.ts +20 -0
- package/dist/toolMeta.d.ts.map +1 -0
- package/dist/toolMeta.js +81 -0
- package/dist/toolMeta.js.map +1 -0
- package/dist/tools.d.ts +10 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +344 -209
- package/dist/tools.js.map +1 -1
- package/package.json +16 -7
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Model Context Protocol (MCP) server for [HonkIO](https://honkio.ca), the Canadian SMS API. It lets an AI agent send SMS, run phone verification, manage Canadian numbers, and handle CASL compliance through natural language.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
47 tools, 4 resources, and 5 guided prompts, all backed by the live HonkIO REST API. Use it two ways: point your client at the hosted endpoint at `https://mcp.honkio.ca/mcp`, or run it locally with `npx`. Both talk to the same API with the same key.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
|
-
- **Node.js 20 or newer**
|
|
12
11
|
- A HonkIO account and API key
|
|
12
|
+
- For the local `npx` route only: **Node.js 20 or newer**
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ Sign in at **<https://honkio.ca/dashboard/keys>** and copy a key:
|
|
|
22
22
|
| `mk_test_…` | Test | Messages are simulated as delivered. **No SMS is sent and nothing is charged.** |
|
|
23
23
|
| `mk_live_…` | Live | Real SMS, real Canadian numbers, **real charges against your balance**. |
|
|
24
24
|
|
|
25
|
-
**Start with a test key.**
|
|
25
|
+
**Start with a test key.** It works before your account's first top-up and can't spend anything: sends and verifications are simulated for free, and a test send still runs the same compliance checks a live one does (consent, opt-out, allow/deny lists, reserved and undeliverable destinations), just without requiring you to own the `from` number. A handful of tools that touch live money or data (provisioning or releasing a number, writing or reading a webhook's dead letters, erasure, and volume/allowance requests) need a live key and answer `LIVE_KEY_REQUIRED` from a test one. Switch to a live key when you want real delivery.
|
|
26
26
|
|
|
27
27
|
Live sending also requires that the account owner has completed phone verification, and that you have provisioned at least one number to send from.
|
|
28
28
|
|
|
@@ -30,55 +30,97 @@ Live sending also requires that the account owner has completed phone verificati
|
|
|
30
30
|
|
|
31
31
|
## 2. Connect your AI tool
|
|
32
32
|
|
|
33
|
-
There
|
|
33
|
+
There are two ways in. The hosted endpoint needs nothing installed and is the right choice for claude.ai connectors, the Claude Messages API, and any client that speaks Streamable HTTP. The local route runs the same server on your machine through `npx`.
|
|
34
34
|
|
|
35
|
-
In every example below, put your own key
|
|
35
|
+
In every example below, put your own key where it says `mk_test_YOUR_KEY_HERE`.
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### Hosted endpoint
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
The key travels in a header on every request, so never paste it into a file you commit. The JSON examples read it from an environment variable.
|
|
40
|
+
|
|
41
|
+
**Claude Code**
|
|
40
42
|
|
|
41
43
|
```sh
|
|
42
|
-
claude mcp add honkio \
|
|
43
|
-
--
|
|
44
|
-
|
|
44
|
+
claude mcp add --transport http honkio https://mcp.honkio.ca/mcp \
|
|
45
|
+
--header "Authorization: Bearer mk_test_YOUR_KEY_HERE"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Prefer to sign in rather than paste a key? Add the endpoint without a header and run `/mcp` inside Claude Code: a browser window opens on honkio.ca where you approve the connection and choose live or test mode. A live connection also asks for your account password. The connection shows up on your API keys page as `OAuth: Claude Code`, and revoking it there disconnects the agent.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
claude mcp add --transport http honkio https://mcp.honkio.ca/mcp
|
|
45
52
|
```
|
|
46
53
|
|
|
47
|
-
Or
|
|
54
|
+
Or in a shared `.mcp.json`, with the key coming from `HONKIO_API_KEY` in your shell:
|
|
48
55
|
|
|
49
56
|
```json
|
|
50
57
|
{
|
|
51
58
|
"mcpServers": {
|
|
52
59
|
"honkio": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
60
|
+
"type": "http",
|
|
61
|
+
"url": "https://mcp.honkio.ca/mcp",
|
|
62
|
+
"headers": { "Authorization": "Bearer ${HONKIO_API_KEY}" }
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
```
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
68
|
+
**Cursor** (`.cursor/mcp.json` or `~/.cursor/mcp.json`)
|
|
64
69
|
|
|
65
70
|
```json
|
|
66
71
|
{
|
|
67
72
|
"mcpServers": {
|
|
68
73
|
"honkio": {
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"env": { "HONKIO_API_KEY": "mk_test_YOUR_KEY_HERE" }
|
|
74
|
+
"url": "https://mcp.honkio.ca/mcp",
|
|
75
|
+
"headers": { "Authorization": "Bearer mk_test_YOUR_KEY_HERE" }
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
```
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
**VS Code** (`.vscode/mcp.json`)
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"servers": {
|
|
86
|
+
"honkio": {
|
|
87
|
+
"type": "http",
|
|
88
|
+
"url": "https://mcp.honkio.ca/mcp",
|
|
89
|
+
"headers": { "Authorization": "Bearer mk_test_YOUR_KEY_HERE" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
80
94
|
|
|
81
|
-
|
|
95
|
+
**Claude Messages API** (no MCP client needed)
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"model": "claude-opus-5",
|
|
100
|
+
"max_tokens": 1024,
|
|
101
|
+
"messages": [{ "role": "user", "content": "List my HonkIO phone numbers." }],
|
|
102
|
+
"mcp_servers": [
|
|
103
|
+
{ "type": "url", "url": "https://mcp.honkio.ca/mcp", "name": "honkio", "authorization_token": "mk_test_YOUR_KEY_HERE" }
|
|
104
|
+
],
|
|
105
|
+
"tools": [{ "type": "mcp_toolset", "mcp_server_name": "honkio" }]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Send that with the `anthropic-beta: mcp-client-2025-11-20` header. The endpoint also accepts the key as `X-API-Key` if your client cannot set `Authorization`.
|
|
110
|
+
|
|
111
|
+
### Local with npx
|
|
112
|
+
|
|
113
|
+
`npx` fetches the server on first use and caches it. Needs Node.js 20 or newer.
|
|
114
|
+
|
|
115
|
+
**Claude Code**
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
claude mcp add honkio \
|
|
119
|
+
--env HONKIO_API_KEY=mk_test_YOUR_KEY_HERE \
|
|
120
|
+
-- npx -y @honkio/mcp
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or in `.mcp.json` (keep the real key out of git, see [Keeping your key out of git](#keeping-your-key-out-of-git)):
|
|
82
124
|
|
|
83
125
|
```json
|
|
84
126
|
{
|
|
@@ -92,9 +134,11 @@ Edit `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` globally:
|
|
|
92
134
|
}
|
|
93
135
|
```
|
|
94
136
|
|
|
95
|
-
|
|
137
|
+
**Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows), same `mcpServers` block as above, then restart Claude Desktop.
|
|
138
|
+
|
|
139
|
+
**Cursor**: `.cursor/mcp.json` or `~/.cursor/mcp.json`, same `mcpServers` block.
|
|
96
140
|
|
|
97
|
-
|
|
141
|
+
**VS Code** (`.vscode/mcp.json`)
|
|
98
142
|
|
|
99
143
|
```json
|
|
100
144
|
{
|
|
@@ -128,22 +172,38 @@ To check the server outside any AI tool:
|
|
|
128
172
|
HONKIO_API_KEY=mk_test_YOUR_KEY npx -y @honkio/mcp
|
|
129
173
|
```
|
|
130
174
|
|
|
131
|
-
It should start and wait silently on stdin
|
|
175
|
+
It should start and wait silently on stdin. An MCP server speaks JSON-RPC over stdio, so no output is the healthy state. Press Ctrl-C to exit. If it exits immediately with an error, the message tells you what is wrong.
|
|
176
|
+
|
|
177
|
+
To check the hosted endpoint outside any AI tool:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
curl -s https://mcp.honkio.ca/healthz
|
|
181
|
+
curl -s -i -X POST https://mcp.honkio.ca/mcp | head -3
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The first returns `{"ok":true,"version":"..."}`. The second, with no key, returns `401` and a `WWW-Authenticate: Bearer` header, which is the endpoint telling you where the key goes.
|
|
132
185
|
|
|
133
186
|
---
|
|
134
187
|
|
|
135
188
|
## Environment variables
|
|
136
189
|
|
|
137
|
-
| Variable | Required | Description |
|
|
138
|
-
|
|
139
|
-
| `HONKIO_API_KEY` | Yes | Your API key (`mk_live_…` or `mk_test_…`). The server refuses to start without it. |
|
|
140
|
-
| `
|
|
190
|
+
| Variable | Used by | Required | Description |
|
|
191
|
+
|---|---|---|---|
|
|
192
|
+
| `HONKIO_API_KEY` | local `npx` | Yes | Your API key (`mk_live_…` or `mk_test_…`). The stdio server refuses to start without it. The hosted endpoint takes the key from the request header instead. |
|
|
193
|
+
| `API_URL` | both | No | Override the API base URL. Defaults to `https://api.honkio.ca`. Only needed for self-hosting or local development. The pre-1.5 name `HONKIO_API_URL` still works. |
|
|
194
|
+
| `PORT` | hosted | No | Port for `honkio-mcp-http`. Defaults to `8080`. |
|
|
195
|
+
| `MCP_ALLOWED_ORIGINS` | hosted | No | Comma-separated browser origins allowed to call the endpoint. Empty by default, which refuses any request carrying an `Origin` header. Command-line and server-side clients send none and are unaffected. |
|
|
196
|
+
| `MCP_RATE_LIMIT_PER_MINUTE` | hosted | No | Requests per minute allowed from one client address. Default `300`. Beyond it the endpoint answers `429` with `Retry-After`. |
|
|
197
|
+
| `MCP_AUTH_FAILURES_PER_MINUTE` | hosted | No | Refused requests (bad or missing key, bad Origin, malformed body) per minute allowed from one client address before the address is locked out for the rest of the minute. Default `10`. |
|
|
198
|
+
| `MCP_TRUSTED_PROXIES` | hosted | No | How many proxy hops in front of the process append to `X-Forwarded-For`. The client address is read that many entries from the right. Railway's edge is one hop, the default. Set `0` when nothing sits in front. |
|
|
199
|
+
| `MCP_SERVICE_KEY` | hosted | No | This service's key at the HonkIO API (the API's `MCP_SERVICE_KEY`). Lets the endpoint accept OAuth access tokens, so clients can sign in instead of pasting a key. Unset means API keys only. |
|
|
200
|
+
| `MCP_PUBLIC_URL` | hosted | No | The public URL access tokens are bound to. Default `https://mcp.honkio.ca/mcp`. |
|
|
141
201
|
|
|
142
202
|
### Keeping your key out of git
|
|
143
203
|
|
|
144
204
|
`.mcp.json` and `.vscode/mcp.json` are usually committed. An API key pasted into one is a live credential in your repository history. Either keep those files untracked, or reference an environment variable your shell already exports and add the file to `.gitignore`.
|
|
145
205
|
|
|
146
|
-
If a key does leak, revoke it immediately at <https://honkio.ca/dashboard/keys
|
|
206
|
+
If a key does leak, revoke it immediately at <https://honkio.ca/dashboard/keys>. A revoked key stops working at once.
|
|
147
207
|
|
|
148
208
|
---
|
|
149
209
|
|
|
@@ -152,11 +212,11 @@ If a key does leak, revoke it immediately at <https://honkio.ca/dashboard/keys>
|
|
|
152
212
|
### Messages
|
|
153
213
|
| Tool | Description |
|
|
154
214
|
|---|---|
|
|
155
|
-
| `send_sms` | Send an SMS from one of your numbers. Enforces CASL consent unless overridden. Pass `idempotency_key` when retrying. A reserved exchange (555-XXXX, N11, test codes)
|
|
215
|
+
| `send_sms` | Send an SMS from one of your numbers. Enforces CASL consent unless overridden. Pass `idempotency_key` when retrying. A reserved exchange (555-XXXX, N11, test codes) is refused for free with `422 RESERVED_DESTINATION`. |
|
|
156
216
|
| `list_messages` | List messages, filterable by number, status, direction and date. |
|
|
157
217
|
| `get_message` | Full detail for one message, including delivery status and cost. |
|
|
158
218
|
|
|
159
|
-
Both directions are billed: sending an SMS charges per
|
|
219
|
+
Both directions are billed per part, the way the carrier splits the body: sending an SMS charges per part, and receiving one on a provisioned number charges the inbound per-part cost. `get_pricing` returns both figures. Inbound charges happen automatically whenever someone texts your number, independent of any tool call.
|
|
160
220
|
|
|
161
221
|
### Phone verification (OTP)
|
|
162
222
|
| Tool | Description |
|
|
@@ -166,7 +226,7 @@ Both directions are billed: sending an SMS charges per segment, and receiving on
|
|
|
166
226
|
| `get_verification` | Status of a single verification attempt. |
|
|
167
227
|
| `list_verifications` | List verification attempts. |
|
|
168
228
|
|
|
169
|
-
On a live key, verification is billed per message segment plus a per-verification surcharge
|
|
229
|
+
On a live key, verification is billed per message segment plus a per-verification surcharge. `get_pricing` returns the current figure. On a test key nothing is sent and nothing is charged, and the code is always `000000`, padded to the requested length. Every verification carries a `mode` of `LIVE` or `TEST`, so you never have to infer which happened.
|
|
170
230
|
|
|
171
231
|
### Pricing
|
|
172
232
|
| Tool | Description |
|
|
@@ -178,11 +238,14 @@ Prices are set at runtime and change without a release. Read them rather than ha
|
|
|
178
238
|
### Phone numbers
|
|
179
239
|
| Tool | Description |
|
|
180
240
|
|---|---|
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
241
|
+
| `list_area_codes` | Provinces HonkIO has numbers in, and the active area codes within each. |
|
|
242
|
+
| `search_phone_numbers` | Search available Canadian numbers, filtered by up to 5 area codes. |
|
|
243
|
+
| `provision_phone_number` | Purchase a number. **Charges the first month plus a one-time activation fee, then monthly rent.** Live keys only. |
|
|
183
244
|
| `list_phone_numbers` | List the numbers on your account. |
|
|
184
245
|
| `get_phone_number` | Detail for one number. |
|
|
185
|
-
| `release_phone_number` | Release a number and stop its recurring rent. |
|
|
246
|
+
| `release_phone_number` | Release a number and stop its recurring rent. Live keys only. |
|
|
247
|
+
| `request_number_allowance` | Ask to hold more numbers than the current per-account limit. Live keys only. |
|
|
248
|
+
| `list_number_allowance_requests` | List past and pending allowance requests. |
|
|
186
249
|
|
|
187
250
|
### CASL compliance
|
|
188
251
|
| Tool | Description |
|
|
@@ -194,7 +257,7 @@ Prices are set at runtime and change without a release. Read them rather than ha
|
|
|
194
257
|
| `record_opt_out` | Record an opt-out manually. |
|
|
195
258
|
| `list_opt_outs` | List opt-out records. |
|
|
196
259
|
|
|
197
|
-
### DNCL
|
|
260
|
+
### DNCL: not available yet
|
|
198
261
|
| Tool | Description |
|
|
199
262
|
|---|---|
|
|
200
263
|
| `check_dncl` | Returns `501 DNCL_COMING_SOON`. |
|
|
@@ -205,34 +268,37 @@ CRTC Do Not Call List checking is not live. `dncl_exemptions` on `send_sms` is a
|
|
|
205
268
|
### Webhooks
|
|
206
269
|
| Tool | Description |
|
|
207
270
|
|---|---|
|
|
208
|
-
| `create_webhook` | Register an endpoint for delivery receipts and inbound messages. |
|
|
271
|
+
| `create_webhook` | Register an endpoint for delivery receipts and inbound messages. Live keys only. |
|
|
209
272
|
| `list_webhooks` | List registered webhooks. |
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
273
|
+
| `get_webhook` | Detail for one webhook. |
|
|
274
|
+
| `update_webhook` | Change a webhook's URL or subscribed events. Live keys only. |
|
|
275
|
+
| `delete_webhook` | Remove a webhook. Live keys only. |
|
|
212
276
|
| `list_webhook_deliveries` | Delivery attempts for a webhook, for debugging. |
|
|
213
|
-
| `list_webhook_dead_letters` | Deliveries that exhausted their retries. |
|
|
214
|
-
| `replay_webhook_dead_letter` | Retry a dead-lettered delivery. |
|
|
215
|
-
| `discard_webhook_dead_letter` | Drop a dead-lettered delivery. |
|
|
216
|
-
| `reactivate_webhook` | Re-enable a webhook disabled by repeated failures. |
|
|
277
|
+
| `list_webhook_dead_letters` | Deliveries that exhausted their retries. Live keys only, since a dead letter carries the failed payload. |
|
|
278
|
+
| `replay_webhook_dead_letter` | Retry a dead-lettered delivery. Live keys only. |
|
|
279
|
+
| `discard_webhook_dead_letter` | Drop a dead-lettered delivery. Live keys only. |
|
|
280
|
+
| `reactivate_webhook` | Re-enable a webhook disabled by repeated failures. Live keys only. |
|
|
217
281
|
|
|
218
282
|
### Sending limits
|
|
219
283
|
|
|
220
284
|
| Tool | What it does |
|
|
221
285
|
|---|---|
|
|
222
286
|
| `get_send_limit` | Daily cap (rolling 24 h), usage, the per-recipient cap (`recipient_rate_per_hour` / `recipient_rate_per_day`), probation status, any active pause, and past volume requests. |
|
|
223
|
-
| `request_send_limit` | File a "request a higher volume" for staff review once probation has ended. |
|
|
287
|
+
| `request_send_limit` | File a "request a higher volume" for staff review once probation has ended. Live keys only. |
|
|
288
|
+
| `list_send_limit_requests` | List past and pending volume requests. |
|
|
224
289
|
| `get_topup_allowance` | How much credit can be added right now under the balance and 30-day top-up caps. |
|
|
225
290
|
|
|
226
|
-
New accounts can send 250 live messages per rolling 24 hours; 30 days after the first live send they can request more. Identical messages to many recipients, per-number rate, link shorteners, and unusually high opt-out or failure rates are also limited
|
|
291
|
+
New accounts can send 250 live messages per rolling 24 hours; 30 days after the first live send they can request more. Identical messages to many recipients, per-number rate, link shorteners, and unusually high opt-out or failure rates are also limited. See [honkio.ca/docs#limits](https://honkio.ca/docs#limits). Refused sends return `DAILY_LIMIT_REACHED`, `FANOUT_LIMIT_REACHED`, `NUMBER_RATE_LIMITED`, `RECIPIENT_RATE_LIMITED` (30 an hour / 100 a day to one number), `SENDING_PAUSED`, `UNDELIVERABLE_NUMBER` (three consecutive carrier failures list a number for 90 days), `NOT_A_MOBILE_NUMBER` (a landline or VoIP destination, refused before sending), `RESERVED_DESTINATION` (a reserved exchange such as 555-XXXX, N11 or a carrier test code, refused in both modes) or `LINK_SHORTENER_BLOCKED` with details.
|
|
227
292
|
|
|
228
293
|
### Account and keys
|
|
229
294
|
| Tool | Description |
|
|
230
295
|
|---|---|
|
|
231
|
-
| `whoami` |
|
|
296
|
+
| `whoami` | The account the API key belongs to (id, name, balance, status) and its list of API keys. Does not say which key you are calling with. |
|
|
232
297
|
| `get_account` | Account detail and credit balance. |
|
|
233
298
|
| `update_account` | Update the account name. |
|
|
234
299
|
| `get_usage` | Usage for a billing period, including live delivery health (`delivery`: delivered, failed, undelivered, pending, `failureRatePct`; `byNumber`: the same per sending number). |
|
|
235
|
-
| `
|
|
300
|
+
| `list_transactions` | Balance transaction ledger (top-ups, refunds, rent, provisioning and verification fees), newest first. |
|
|
301
|
+
| `create_api_key` | Issue a new live or test key, with permissions no broader than the calling key's own. |
|
|
236
302
|
| `rotate_api_key` | Replace a key, invalidating the old value. |
|
|
237
303
|
| `revoke_api_key` | Revoke a key immediately. |
|
|
238
304
|
|
|
@@ -241,7 +307,18 @@ New accounts can send 250 live messages per rolling 24 hours; 30 days after the
|
|
|
241
307
|
### PIPEDA
|
|
242
308
|
| Tool | Description |
|
|
243
309
|
|---|---|
|
|
244
|
-
| `request_erasure` | Execute a right-to-erasure request for a phone number. |
|
|
310
|
+
| `request_erasure` | Execute a right-to-erasure request for a phone number. Live keys only. |
|
|
311
|
+
|
|
312
|
+
### Not covered here
|
|
313
|
+
|
|
314
|
+
The REST API has more surface than this package exposes as tools. Reachable directly through the API (not through MCP):
|
|
315
|
+
|
|
316
|
+
- **Contacts, contact groups, and their broadcast sends** (`/v1/contacts`, `/v1/contact-groups`)
|
|
317
|
+
- **Allow/deny lists**, both the standalone contact lists (`/v1/lists`) and the ones attached to an API key (`/v1/accounts/:id/api-keys/:keyId/lists`)
|
|
318
|
+
- **Owner phone verification** (`/v1/accounts/:id/phone-verification` and its `/confirm`), the one-time step a live account needs before it can send
|
|
319
|
+
- **An API key's default-deny flag** (`PATCH /v1/accounts/:id/api-keys/:keyId/default-deny`)
|
|
320
|
+
|
|
321
|
+
See [honkio.ca/docs](https://honkio.ca/docs) for the full REST reference.
|
|
245
322
|
|
|
246
323
|
---
|
|
247
324
|
|
|
@@ -293,21 +370,21 @@ Show me any webhook deliveries that failed and ended up in the dead-letter queue
|
|
|
293
370
|
Two tools move real money on a live key:
|
|
294
371
|
|
|
295
372
|
- **`provision_phone_number`** charges the first month's rent plus a one-time activation fee, together, and starts monthly rent. Rent keeps accruing until you call `release_phone_number`; the activation fee is not refunded on release.
|
|
296
|
-
- **`send_sms`** and **`start_verification`** charge per SMS part against your balance, counted the way the carrier splits the body (typographic quotes and dashes are smart-encoded to GSM-7; emoji force Unicode parts). The charge is settled to the carrier's part count after the send. A send the carrier rejects outright costs nothing
|
|
373
|
+
- **`send_sms`** and **`start_verification`** charge per SMS part against your balance, counted the way the carrier splits the body (typographic quotes and dashes are smart-encoded to GSM-7; emoji force Unicode parts). The charge is settled to the carrier's part count after the send. A send the carrier rejects outright costs nothing, and so does a send to a reserved exchange (555-XXXX and similar), which is refused here; a message the carrier accepts but cannot deliver keeps its charge. A body over 10 parts is refused with `422 MESSAGE_TOO_LONG` before any charge. `start_verification` adds a per-verification surcharge on top. `get_pricing` tells you what each costs right now.
|
|
297
374
|
|
|
298
|
-
A third charge is not tool-triggered at all: **inbound SMS is billed per segment** the moment a carrier delivers it to one of your provisioned numbers, whether or not you ever call a tool. Traffic to a number you provisioned draws down your balance on its own (STOP/START/HELP replies are not charged), and a received message is debited even when the balance cannot cover it
|
|
375
|
+
A third charge is not tool-triggered at all: **inbound SMS is billed per segment** the moment a carrier delivers it to one of your provisioned numbers, whether or not you ever call a tool. Traffic to a number you provisioned draws down your balance on its own (STOP/START/HELP replies are not charged), and a received message is debited even when the balance cannot cover it: the balance goes negative and sends are frozen until the next top-up.
|
|
299
376
|
|
|
300
|
-
Agents act on instructions that can be vaguer than you intended. If you are exploring, use a test key
|
|
377
|
+
Agents act on instructions that can be vaguer than you intended. If you are exploring, use a test key: sends and verifications are simulated and charge nothing, but buying or releasing a number, changing webhooks and running erasure need a live key.
|
|
301
378
|
|
|
302
|
-
A test-mode send still comes back with status `DELIVERED`, because it simulates a successful delivery. That is not a claim that a phone received anything. The `mode` field on the response
|
|
379
|
+
A test-mode send still comes back with status `DELIVERED`, because it simulates a successful delivery. That is not a claim that a phone received anything. The `mode` field on the response, `LIVE` or `TEST`, is the one that tells you whether an SMS actually left the building.
|
|
303
380
|
|
|
304
381
|
---
|
|
305
382
|
|
|
306
383
|
## Canadian compliance
|
|
307
384
|
|
|
308
|
-
- **CASL
|
|
309
|
-
- **DNCL
|
|
310
|
-
- **PIPEDA
|
|
385
|
+
- **CASL**: commercial messages need consent on record. Use `record_consent` before sending; `send_sms` enforces it unless you pass `skip_consent_check`, which only a test-mode key can do (a live key gets 403 FORBIDDEN). Express consent does not expire; implied consent expires two years after the last transaction.
|
|
386
|
+
- **DNCL**: CRTC Do Not Call List checking is not available yet and is not enforced.
|
|
387
|
+
- **PIPEDA**: customer data is **stored** in Canada (`ca-central-1`). Request processing currently runs on infrastructure outside Canada, so data crosses the border in transit; see the [privacy policy](https://honkio.ca/legal/privacy) for the full disclosure. Use `request_erasure` for right-to-erasure requests.
|
|
311
388
|
|
|
312
389
|
---
|
|
313
390
|
|
|
@@ -315,13 +392,14 @@ A test-mode send still comes back with status `DELIVERED`, because it simulates
|
|
|
315
392
|
|
|
316
393
|
| Symptom | Cause and fix |
|
|
317
394
|
|---|---|
|
|
318
|
-
| `
|
|
319
|
-
| Tools do not appear in the agent | The client did not start the server. Restart the client, and check that `npx` is on its `PATH
|
|
395
|
+
| `UNAUTHORIZED` | Key is wrong, revoked, or never reached the server. Check the `env` block, or the `Authorization` header on the hosted endpoint. |
|
|
396
|
+
| Tools do not appear in the agent | The client did not start the server. Restart the client, and check that `npx` is on its `PATH`, since GUI apps do not always inherit your shell's `PATH`. |
|
|
320
397
|
| First start is slow, or times out once | `npx` downloads the package on first use. Run `npx -y @honkio/mcp` once in a terminal to warm the cache, then restart your client. |
|
|
321
|
-
| `npm ERR! 404
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
398
|
+
| `npm ERR! 404` | Usually an npm registry override or a private proxy. Check `npm config get registry`. |
|
|
399
|
+
| `PAYMENT_REQUIRED` | A **live** key needs the account's first top-up before use; a test key works without one. |
|
|
400
|
+
| `LIVE_KEY_REQUIRED` | The tool you called touches live money or data. Test keys are a sandbox and can't reach it; use a live key. |
|
|
401
|
+
| `ACCOUNT_NOT_VERIFIED` | Live sending needs the account owner's phone verified. Do it in the dashboard. |
|
|
402
|
+
| `DNCL_COMING_SOON` | Expected, answered as 501. DNCL checking is not live yet. |
|
|
325
403
|
| Server starts then exits silently | That is normal when nothing is attached to stdin. It only means something is wrong if it prints an error. |
|
|
326
404
|
|
|
327
405
|
---
|
|
@@ -340,9 +418,59 @@ npm run typecheck # types only
|
|
|
340
418
|
npm run build # compile to dist/
|
|
341
419
|
```
|
|
342
420
|
|
|
343
|
-
Point a local checkout at your own API with `
|
|
421
|
+
Point a local checkout at your own API with `API_URL=http://localhost:3000`, and at a local build by using `node /path/to/honkio/mcp/dist/index.js` as the `command` in your client config instead of `npx`.
|
|
422
|
+
|
|
423
|
+
`dist/` is gitignored and the published tarball is built from it, so `prepublishOnly` rebuilds on every `npm publish`, so never publish without letting it run.
|
|
424
|
+
|
|
425
|
+
### Running the hosted entry yourself
|
|
426
|
+
|
|
427
|
+
The same package ships `honkio-mcp-http`, the process behind `mcp.honkio.ca`:
|
|
428
|
+
|
|
429
|
+
```sh
|
|
430
|
+
PORT=8080 API_URL=https://api.honkio.ca npx -y -p @honkio/mcp honkio-mcp-http
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
It answers `GET /healthz` and `POST /mcp`, verifies each caller's key against the API once a minute, rate-limits each client address (see the environment table), and serves both the 2025 handshake protocol and the 2026-07-28 revision. There is a `Dockerfile` and a `railway.toml` in this directory for hosting it.
|
|
434
|
+
|
|
435
|
+
### Publishing to the MCP Registry
|
|
436
|
+
|
|
437
|
+
`server.json` describes both the npm package and the hosted endpoint for the [official MCP Registry](https://registry.modelcontextprotocol.io) under the `ca.honkio` namespace, which is verified by a DNS record on `honkio.ca`. The registry entry is live; these are the steps to repeat for a new release.
|
|
438
|
+
|
|
439
|
+
The publisher is a Go binary, not an npm package:
|
|
440
|
+
|
|
441
|
+
```sh
|
|
442
|
+
brew install mcp-publisher
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Generate the signing key once. macOS ships LibreSSL, whose `genpkey` has no Ed25519, so use Node:
|
|
446
|
+
|
|
447
|
+
```sh
|
|
448
|
+
node -e "
|
|
449
|
+
const {generateKeyPairSync}=require('crypto'),fs=require('fs');
|
|
450
|
+
const {publicKey,privateKey}=generateKeyPairSync('ed25519');
|
|
451
|
+
const pub=publicKey.export({format:'der',type:'spki'});
|
|
452
|
+
fs.writeFileSync('mcp-registry.pem',privateKey.export({format:'pem',type:'pkcs8'}),{mode:0o600});
|
|
453
|
+
console.log('TXT value: v=MCPv1; k=ed25519; p='+pub.subarray(pub.length-32).toString('base64'));
|
|
454
|
+
"
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Publish that value as a TXT record on the apex of `honkio.ca` (host `@`). The key file is gitignored; keep it, since every future release signs with it.
|
|
458
|
+
|
|
459
|
+
Then, for each release, after `npm publish` and with a matching `version` in `server.json`:
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
PRIV=$(node -e "
|
|
463
|
+
const {createPrivateKey}=require('crypto'),fs=require('fs');
|
|
464
|
+
const der=createPrivateKey(fs.readFileSync('mcp-registry.pem')).export({format:'der',type:'pkcs8'});
|
|
465
|
+
console.log(der.subarray(der.length-32).toString('hex'));
|
|
466
|
+
")
|
|
467
|
+
mcp-publisher login dns --domain honkio.ca --private-key "$PRIV"
|
|
468
|
+
mcp-publisher publish
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The registry checks that the published npm package carries a matching `mcpName` field, so `npm publish` has to happen first.
|
|
344
472
|
|
|
345
|
-
`
|
|
473
|
+
The `serverJson` test pins `server.json` to `package.json`, so a version bump that forgets one of them fails `npm test`.
|
|
346
474
|
|
|
347
475
|
## License
|
|
348
476
|
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import type { AuthInfo } from '@modelcontextprotocol/server';
|
|
3
|
+
/** OAuth access tokens minted by the API's authorization server. */
|
|
4
|
+
export declare const isAccessToken: (credential: string) => boolean;
|
|
5
|
+
/** The key from `Authorization: Bearer` or, as the API also accepts, `X-API-Key`. Null when absent or not a HonkIO key. */
|
|
6
|
+
export declare function extractApiKey(headers: IncomingHttpHeaders): string | null;
|
|
7
|
+
export type VerifyResult = {
|
|
8
|
+
status: 'ok';
|
|
9
|
+
authInfo: AuthInfo;
|
|
10
|
+
accountId: string;
|
|
11
|
+
isTestMode: boolean;
|
|
12
|
+
} | {
|
|
13
|
+
status: 'invalid';
|
|
14
|
+
} | {
|
|
15
|
+
status: 'unavailable';
|
|
16
|
+
};
|
|
17
|
+
export interface KeyVerifierOptions {
|
|
18
|
+
apiBaseUrl: string;
|
|
19
|
+
/** Cache lifetime for both good and rejected keys. Default 60 s. */
|
|
20
|
+
ttlMs?: number;
|
|
21
|
+
/** Cap on cached keys; the oldest entry is dropped past it. Default 10,000. */
|
|
22
|
+
maxEntries?: number;
|
|
23
|
+
/** Clock, injectable for tests. */
|
|
24
|
+
now?: () => number;
|
|
25
|
+
/** This service's key at the API (MCP_SERVICE_KEY). Without it access tokens are refused outright, since the API would refuse them too. */
|
|
26
|
+
serviceKey?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class KeyVerifier {
|
|
29
|
+
private readonly cache;
|
|
30
|
+
private readonly apiBaseUrl;
|
|
31
|
+
private readonly ttlMs;
|
|
32
|
+
private readonly maxEntries;
|
|
33
|
+
private readonly now;
|
|
34
|
+
private readonly serviceKey;
|
|
35
|
+
constructor(opts: KeyVerifierOptions);
|
|
36
|
+
get size(): number;
|
|
37
|
+
verify(key: string): Promise<VerifyResult>;
|
|
38
|
+
private toResult;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAK7D,oEAAoE;AACpE,eAAO,MAAM,aAAa,GAAI,YAAY,MAAM,KAAG,OAA0C,CAAC;AAE9F,2HAA2H;AAC3H,wBAAgB,aAAa,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAYzE;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAC5E;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,CAAC;AAI9B,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,2IAA2I;IAC3I,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;gBAEpC,IAAI,EAAE,kBAAkB;IAQpC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAEK,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA8BhD,OAAO,CAAC,QAAQ;CAmBjB"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-key auth for the hosted MCP endpoint.
|
|
3
|
+
*
|
|
4
|
+
* The endpoint is an OAuth-shaped resource server in name only: the bearer
|
|
5
|
+
* token is a HonkIO API key, and the authority that decides whether it is
|
|
6
|
+
* valid is the REST API. verify() asks GET /v1/accounts/me and remembers the
|
|
7
|
+
* answer for a minute, so a chatty agent costs one lookup per key per minute
|
|
8
|
+
* rather than one per tool call.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { HonkioClient, HonkioError } from './client.js';
|
|
12
|
+
const KEY_PATTERN = /^(mk_(live|test)_[A-Za-z0-9]+|hk_at_[A-Za-z0-9_-]+)$/;
|
|
13
|
+
/** OAuth access tokens minted by the API's authorization server. */
|
|
14
|
+
export const isAccessToken = (credential) => credential.startsWith('hk_at_');
|
|
15
|
+
/** The key from `Authorization: Bearer` or, as the API also accepts, `X-API-Key`. Null when absent or not a HonkIO key. */
|
|
16
|
+
export function extractApiKey(headers) {
|
|
17
|
+
const auth = headers.authorization;
|
|
18
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
19
|
+
const key = auth.slice('Bearer '.length).trim();
|
|
20
|
+
return KEY_PATTERN.test(key) ? key : null;
|
|
21
|
+
}
|
|
22
|
+
const xKey = headers['x-api-key'];
|
|
23
|
+
if (typeof xKey === 'string') {
|
|
24
|
+
const key = xKey.trim();
|
|
25
|
+
return KEY_PATTERN.test(key) ? key : null;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export class KeyVerifier {
|
|
30
|
+
cache = new Map();
|
|
31
|
+
apiBaseUrl;
|
|
32
|
+
ttlMs;
|
|
33
|
+
maxEntries;
|
|
34
|
+
now;
|
|
35
|
+
serviceKey;
|
|
36
|
+
constructor(opts) {
|
|
37
|
+
this.apiBaseUrl = opts.apiBaseUrl;
|
|
38
|
+
this.ttlMs = opts.ttlMs ?? 60_000;
|
|
39
|
+
this.maxEntries = opts.maxEntries ?? 10_000;
|
|
40
|
+
this.now = opts.now ?? Date.now;
|
|
41
|
+
this.serviceKey = opts.serviceKey;
|
|
42
|
+
}
|
|
43
|
+
get size() {
|
|
44
|
+
return this.cache.size;
|
|
45
|
+
}
|
|
46
|
+
async verify(key) {
|
|
47
|
+
const hash = createHash('sha256').update(key).digest('hex');
|
|
48
|
+
const now = this.now();
|
|
49
|
+
const hit = this.cache.get(hash);
|
|
50
|
+
if (hit && hit.expiresAt > now)
|
|
51
|
+
return this.toResult(key, hit);
|
|
52
|
+
const token = isAccessToken(key);
|
|
53
|
+
if (token && !this.serviceKey)
|
|
54
|
+
return { status: 'invalid' };
|
|
55
|
+
let verdict;
|
|
56
|
+
try {
|
|
57
|
+
const me = await new HonkioClient({ apiKey: key, baseUrl: this.apiBaseUrl, serviceKey: token ? this.serviceKey : undefined }).getCurrentAccount();
|
|
58
|
+
verdict = { accountId: me.account.id };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (err instanceof HonkioError && (err.status === 401 || err.status === 403)) {
|
|
62
|
+
verdict = 'invalid';
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return { status: 'unavailable' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const entry = { expiresAt: now + this.ttlMs, verdict };
|
|
69
|
+
if (this.cache.size >= this.maxEntries) {
|
|
70
|
+
const oldest = this.cache.keys().next().value;
|
|
71
|
+
if (oldest !== undefined)
|
|
72
|
+
this.cache.delete(oldest);
|
|
73
|
+
}
|
|
74
|
+
this.cache.set(hash, entry);
|
|
75
|
+
return this.toResult(key, entry);
|
|
76
|
+
}
|
|
77
|
+
toResult(key, entry) {
|
|
78
|
+
if (entry.verdict === 'invalid')
|
|
79
|
+
return { status: 'invalid' };
|
|
80
|
+
const credential = isAccessToken(key) ? 'token' : 'key';
|
|
81
|
+
// Test mode is a property of the key behind a token; the API decides. For logs only.
|
|
82
|
+
const isTestMode = credential === 'key' && key.startsWith('mk_test_');
|
|
83
|
+
const accountId = entry.verdict.accountId;
|
|
84
|
+
return {
|
|
85
|
+
status: 'ok',
|
|
86
|
+
accountId,
|
|
87
|
+
isTestMode,
|
|
88
|
+
authInfo: {
|
|
89
|
+
token: key,
|
|
90
|
+
clientId: accountId,
|
|
91
|
+
scopes: [],
|
|
92
|
+
expiresAt: Math.floor(entry.expiresAt / 1000),
|
|
93
|
+
extra: { accountId, isTestMode, credential },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|