@honkio/mcp 1.5.0 → 1.6.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 +75 -34
- package/dist/client.d.ts +30 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +57 -8
- package/dist/client.js.map +1 -1
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/toolMeta.d.ts.map +1 -1
- package/dist/toolMeta.js +6 -0
- package/dist/toolMeta.js.map +1 -1
- package/dist/tools.d.ts +9 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +197 -52
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAEzD,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAEzD,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,aAAa,CAAC;AAgB7D,eAAO,MAAM,cAAc,8TAejB,CAAC;AAMX,eAAO,MAAM,oBAAoB,2LAcvB,CAAC;AA6DX,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,CAetG;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAgnB3E"}
|
package/dist/tools.js
CHANGED
|
@@ -9,7 +9,11 @@ const DNCL_EXEMPTIONS = [
|
|
|
9
9
|
'newspaper_subscription',
|
|
10
10
|
'personal',
|
|
11
11
|
];
|
|
12
|
-
|
|
12
|
+
// Copied from api/src/services/webhookDelivery.ts (WebhookEventType) minus
|
|
13
|
+
// the email.*/email_domain.* events — the email product has no MCP surface
|
|
14
|
+
// yet. Keep the two lists in step (see webhookEvents.test.ts). Exported so
|
|
15
|
+
// that test can compare it against the API source without duplicating it.
|
|
16
|
+
export const WEBHOOK_EVENTS = [
|
|
13
17
|
'message.queued',
|
|
14
18
|
'message.sending',
|
|
15
19
|
'message.sent',
|
|
@@ -22,14 +26,84 @@ const WEBHOOK_EVENTS = [
|
|
|
22
26
|
'account.delivery_warning',
|
|
23
27
|
'account.sending_paused',
|
|
24
28
|
'account.spend_warning',
|
|
29
|
+
'phone_number.suspended',
|
|
30
|
+
'phone_number.released',
|
|
25
31
|
];
|
|
32
|
+
// Copied from api/src/permissions.ts (RESOURCES) — the resource names an API
|
|
33
|
+
// key's permissions object may name. Keep the two lists in step (see
|
|
34
|
+
// permissionResources.test.ts). Exported so that test can compare it against
|
|
35
|
+
// the API source without duplicating it.
|
|
36
|
+
export const PERMISSION_RESOURCES = [
|
|
37
|
+
'messages',
|
|
38
|
+
'phone_numbers',
|
|
39
|
+
'contacts',
|
|
40
|
+
'contact_groups',
|
|
41
|
+
'lists',
|
|
42
|
+
'compliance',
|
|
43
|
+
'webhooks',
|
|
44
|
+
'verify',
|
|
45
|
+
'account',
|
|
46
|
+
'api_keys',
|
|
47
|
+
'emails',
|
|
48
|
+
'email_domains',
|
|
49
|
+
'email_suppressions',
|
|
50
|
+
];
|
|
51
|
+
const PERMISSIONS_ARG = z
|
|
52
|
+
.record(z.string(), z.string())
|
|
53
|
+
.refine((perms) => Object.entries(perms).every(([resource, ops]) => PERMISSION_RESOURCES.includes(resource) && /^[rwmd]*$/.test(ops)), { message: `Resource names must be one of: ${PERMISSION_RESOURCES.join(', ')}. Each value is a string made only of r, w, m, d.` })
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Per-resource permissions: each key is a resource name (' + PERMISSION_RESOURCES.join(', ') +
|
|
56
|
+
'), each value a string of allowed operations (r=read, w=write, m=modify, d=delete). Omit a resource to deny it. ' +
|
|
57
|
+
'Omit this whole field to default to the calling key\'s own permissions. A key can only be created with permissions the calling key itself holds.');
|
|
26
58
|
function ok(data) {
|
|
27
59
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
28
60
|
}
|
|
29
|
-
|
|
61
|
+
// Since Task 8.1, a VALIDATION_ERROR's top-level `message` is the generic
|
|
62
|
+
// bilingual sentence ("Request validation failed."); the field-specific text
|
|
63
|
+
// an agent needs to self-correct a bad call moved into `details` — an AJV
|
|
64
|
+
// array of `{instancePath, message, ...}` entries. Other error codes (e.g.
|
|
65
|
+
// the suspended/closed 403) carry an ad hoc object there instead
|
|
66
|
+
// (`{reason: 'account_suspended'}`). Render whichever shape it is, so that
|
|
67
|
+
// text isn't lost.
|
|
68
|
+
const DETAILS_CHAR_CAP = 1000;
|
|
69
|
+
function isAjvDetails(details) {
|
|
70
|
+
return Array.isArray(details) && details.length > 0 &&
|
|
71
|
+
details.every((d) => d && typeof d === 'object' && 'instancePath' in d && 'message' in d);
|
|
72
|
+
}
|
|
73
|
+
function formatDetails(details) {
|
|
74
|
+
const rendered = isAjvDetails(details)
|
|
75
|
+
? details.map((d) => `${d.instancePath} ${d.message}`).join('\n')
|
|
76
|
+
: JSON.stringify(details);
|
|
77
|
+
return rendered.length > DETAILS_CHAR_CAP ? `${rendered.slice(0, DETAILS_CHAR_CAP)}…` : rendered;
|
|
78
|
+
}
|
|
79
|
+
// The response body's own standard fields (see api/src/i18n/errors.ts'
|
|
80
|
+
// buildError): everything else is a code-specific extra the API attached at
|
|
81
|
+
// the top level rather than under `details` — e.g. `attempts_remaining` on a
|
|
82
|
+
// verify-check 422 (api/src/routes/verify/index.ts). HonkioError.details
|
|
83
|
+
// only ever holds `details`; the raw body (HonkioError.body) is what carries
|
|
84
|
+
// these, so render whatever's left over from it too.
|
|
85
|
+
const KNOWN_ERROR_BODY_FIELDS = new Set(['code', 'message', 'messageEn', 'messageFr', 'statusCode', 'details', 'error']);
|
|
86
|
+
function formatExtraFields(body) {
|
|
87
|
+
if (!body)
|
|
88
|
+
return undefined;
|
|
89
|
+
const lines = Object.entries(body)
|
|
90
|
+
.filter(([key]) => !KNOWN_ERROR_BODY_FIELDS.has(key))
|
|
91
|
+
.map(([key, value]) => `${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
92
|
+
if (lines.length === 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
const rendered = lines.join('\n');
|
|
95
|
+
return rendered.length > DETAILS_CHAR_CAP ? `${rendered.slice(0, DETAILS_CHAR_CAP)}…` : rendered;
|
|
96
|
+
}
|
|
97
|
+
export function formatError(err) {
|
|
30
98
|
if (err instanceof HonkioError) {
|
|
99
|
+
const parts = [`Error [${err.code}]: ${err.message}`];
|
|
100
|
+
if (err.details !== undefined)
|
|
101
|
+
parts.push(formatDetails(err.details));
|
|
102
|
+
const extra = formatExtraFields(err.body);
|
|
103
|
+
if (extra !== undefined)
|
|
104
|
+
parts.push(extra);
|
|
31
105
|
return {
|
|
32
|
-
content: [{ type: 'text', text:
|
|
106
|
+
content: [{ type: 'text', text: parts.join('\n') }],
|
|
33
107
|
isError: true,
|
|
34
108
|
};
|
|
35
109
|
}
|
|
@@ -49,17 +123,19 @@ export function registerTools(server, client) {
|
|
|
49
123
|
.optional()
|
|
50
124
|
.describe('Your account ID. Omit it and it is resolved from your API key.');
|
|
51
125
|
// ── Messages ──────────────────────────────────────────────
|
|
52
|
-
server.registerTool('send_sms', { ...meta('send_sms'), description: 'Send an SMS message from one of your provisioned Canadian numbers. Automatically checks CASL consent (CRTC DNCL checking is coming soon and not yet enforced) unless overridden. Billed per SMS part as the carrier splits the body (get_pricing → message_cost_cents; typographic quotes and dashes count as 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; a message the carrier accepts but cannot deliver keeps its charge. A send to a reserved exchange (555-XXXX, N11 exchanges, carrier test codes) is refused for free as 422 RESERVED_DESTINATION, in test mode too
|
|
126
|
+
server.registerTool('send_sms', { ...meta('send_sms'), description: 'Send an SMS message from one of your provisioned Canadian numbers. Automatically checks CASL consent (CRTC DNCL checking is coming soon and not yet enforced) unless overridden. Billed per SMS part as the carrier splits the body (get_pricing → message_cost_cents; typographic quotes and dashes count as 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; a message the carrier accepts but cannot deliver keeps its charge. A send to a reserved exchange (555-XXXX, N11 exchanges, carrier test codes) is refused for free as 422 RESERVED_DESTINATION, in test mode too; use a real recipient. A number the carrier has failed three times running, for any customer, is refused for free as 422 UNDELIVERABLE_NUMBER for 90 days. Use test-mode keys (mk_test_...) to simulate a send: it runs the same compliance checks as a live send (consent, opt-out, allow/deny lists, reserved and undeliverable destinations) but does not require owning the from number, and nothing is sent or charged. A test-mode send still returns status "DELIVERED": check the "mode" field, not the status, to tell a simulation from a real send. ' +
|
|
53
127
|
'Live sends are subject to sending limits (get_send_limit): a daily cap, a cap on identical messages to distinct recipients, a per-number rate, a per-recipient cap (30 an hour and 100 a day to one number), ' +
|
|
54
|
-
'and an automatic pause on high opt-out or failure rates
|
|
128
|
+
'and an automatic pause on high opt-out or failure rates: refusals come back as DAILY_LIMIT_REACHED, FANOUT_LIMIT_REACHED, NUMBER_RATE_LIMITED, RECIPIENT_RATE_LIMITED or SENDING_PAUSED with details. ' +
|
|
55
129
|
'Link shorteners (bit.ly and similar) are refused in both modes (LINK_SHORTENER_BLOCKED); use the full URL. ' +
|
|
56
130
|
'When you retry a send, pass the same idempotency_key: the API returns the original message instead of sending and charging again. ' +
|
|
57
|
-
'A 503 CARRIER_UNAVAILABLE means the carrier could not be reached
|
|
58
|
-
'A
|
|
131
|
+
'A 503 CARRIER_UNAVAILABLE means the carrier could not be reached: nothing was sent or charged; retry shortly. ' +
|
|
132
|
+
'A 503 CARRIER_TIMEOUT means the carrier did not answer in time: the message may or may not have been sent and nothing is charged; check list_messages before sending again. A retry with the same idempotency_key returns the failed message instead of sending it again. ' +
|
|
133
|
+
'A 422 NOT_A_MOBILE_NUMBER means the destination is a landline or VoIP number: the carrier refuses it before sending, nothing is charged, and three such refusals list the number as undeliverable. ' +
|
|
134
|
+
'A 422 NON_CANADIAN_NUMBER means the recipient is not a Canadian number, and a 422 INVALID_PHONE_NUMBER means the carrier found the number invalid; neither costs anything.', inputSchema: z.object({
|
|
59
135
|
from: z.string().describe('Sending phone number in E.164 format (e.g. +14165551234). Must be a number provisioned on your account.'),
|
|
60
136
|
to: z.string().describe('Recipient phone number in E.164 format. Must be a Canadian number.'),
|
|
61
137
|
body: z.string().max(1600).describe('Message body text. Maximum 1600 characters and 10 SMS parts (about 1,530 GSM-7 or 670 Unicode characters); a longer body is rejected with 422 MESSAGE_TOO_LONG before anything is charged. Billing is per part, counted the way the carrier splits it.'),
|
|
62
|
-
skip_consent_check: z.boolean().optional().describe('Skip the CASL consent gate. Use only when you have consent recorded outside HonkIO.'),
|
|
138
|
+
skip_consent_check: z.boolean().optional().describe('Skip the CASL consent gate. Test-mode keys only: a live key gets 403 FORBIDDEN. Use only when you have consent recorded outside HonkIO.'),
|
|
63
139
|
dncl_exemptions: z.array(z.enum(DNCL_EXEMPTIONS)).optional().describe('DNCL exemption reasons. Required if the recipient is on the CRTC Do Not Call List.'),
|
|
64
140
|
idempotency_key: z.string().min(1).max(255).optional().describe('Sent as the Idempotency-Key header. Reuse the same key on a retry so it can never become a second send or a second charge.'),
|
|
65
141
|
}) }, async (input) => {
|
|
@@ -71,7 +147,7 @@ export function registerTools(server, client) {
|
|
|
71
147
|
return formatError(err);
|
|
72
148
|
}
|
|
73
149
|
});
|
|
74
|
-
server.registerTool('list_messages', { ...meta('list_messages'), description: 'List SMS messages for your account with optional filters. Returns paginated results. On each message, cost_cents is what it actually cost in CAD cents (settled to the carrier\'s part count; 0 on a TEST simulation or when the carrier refused the send outright
|
|
150
|
+
server.registerTool('list_messages', { ...meta('list_messages'), description: 'List SMS messages for your account with optional filters. Returns paginated results. On each message, cost_cents is what it actually cost in CAD cents (settled to the carrier\'s part count; 0 on a TEST simulation or when the carrier refused the send outright: a message the carrier accepted but could not deliver keeps its charge; inbound messages carry the inbound charge) and segment_count is the carrier\'s part count. On a FAILED or UNDELIVERED message, error_code and error_message carry the carrier\'s reason. The list includes the automatic confirmations sent when a recipient texts STOP, START or HELP: auto_reply_keyword names the keyword on those (null on every other message); the account holder did not send them and is never charged, so leave them out when counting what was sent.', inputSchema: z.object({
|
|
75
151
|
from: z.string().optional().describe('Filter by sending phone number (E.164).'),
|
|
76
152
|
to: z.string().optional().describe('Filter by recipient phone number (E.164).'),
|
|
77
153
|
status: z.enum(['QUEUED', 'SENDING', 'SENT', 'DELIVERED', 'FAILED', 'UNDELIVERED', 'RECEIVED']).optional().describe('Filter by message status.'),
|
|
@@ -89,7 +165,7 @@ export function registerTools(server, client) {
|
|
|
89
165
|
return formatError(err);
|
|
90
166
|
}
|
|
91
167
|
});
|
|
92
|
-
server.registerTool('get_message', { ...meta('get_message'), description: 'Get full details of a single SMS message by its ID
|
|
168
|
+
server.registerTool('get_message', { ...meta('get_message'), description: 'Get full details of a single SMS message by its ID. cost_cents is what it actually cost in CAD cents (settled to the carrier\'s part count; 0 on a TEST simulation or when the carrier refused the send outright; an undelivered message keeps its charge) and segment_count is the carrier\'s part count. On a FAILED or UNDELIVERED message, error_code and error_message carry the carrier\'s reason. auto_reply_keyword is set (STOP, START or HELP) on an automatic confirmation the account holder did not send and is never charged for.', inputSchema: z.object({
|
|
93
169
|
id: z.string().describe('Message ID.'),
|
|
94
170
|
}) }, async ({ id }) => {
|
|
95
171
|
try {
|
|
@@ -101,26 +177,41 @@ export function registerTools(server, client) {
|
|
|
101
177
|
}
|
|
102
178
|
});
|
|
103
179
|
// ── Phone Numbers ─────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
|
|
180
|
+
const AREA_CODE = z.string().regex(/^\d{3}$/);
|
|
181
|
+
server.registerTool('list_area_codes', { ...meta('list_area_codes'), description: 'List the provinces HonkIO has numbers in and the active Canadian area codes within each. Use this to see what is available before calling search_phone_numbers with an area_code.', inputSchema: z.object({}) }, async () => {
|
|
182
|
+
try {
|
|
183
|
+
const result = await client.listAreaCodes();
|
|
184
|
+
return ok(result);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return formatError(err);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
server.registerTool('search_phone_numbers', { ...meta('search_phone_numbers'), description: 'Search available Canadian phone numbers you can provision. Filter by one area code (area_code) or up to five (area_codes) to find numbers in specific provinces; omit both to search everywhere. See list_area_codes for what is active.', inputSchema: z.object({
|
|
191
|
+
area_code: AREA_CODE.optional().describe('A single Canadian area code to filter by (e.g. "416" for Toronto, "514" for Montreal, "604" for Vancouver). Combined with area_codes if both are given.'),
|
|
192
|
+
area_codes: z.array(AREA_CODE).max(5).optional().describe('Up to 5 Canadian area codes to filter by.'),
|
|
106
193
|
limit: z.number().int().min(1).max(50).optional().describe('Number of results to return (default: 10, max: 50).'),
|
|
107
|
-
}) }, async (
|
|
194
|
+
}) }, async ({ area_code, area_codes, limit }) => {
|
|
108
195
|
try {
|
|
109
|
-
const
|
|
196
|
+
const codes = [...(area_code ? [area_code] : []), ...(area_codes ?? [])];
|
|
197
|
+
const result = await client.searchPhoneNumbers({
|
|
198
|
+
area_codes: codes.length ? codes.join(',') : undefined,
|
|
199
|
+
limit,
|
|
200
|
+
});
|
|
110
201
|
return ok(result);
|
|
111
202
|
}
|
|
112
203
|
catch (err) {
|
|
113
204
|
return formatError(err);
|
|
114
205
|
}
|
|
115
206
|
});
|
|
116
|
-
server.registerTool('provision_phone_number', { ...meta('provision_phone_number'), description: 'Provision (purchase) a Canadian phone number to your account. Get the phone_number value from search_phone_numbers first. ' +
|
|
117
|
-
'
|
|
207
|
+
server.registerTool('provision_phone_number', { ...meta('provision_phone_number'), description: 'Provision (purchase) a Canadian phone number to your account. Get the phone_number value from search_phone_numbers first. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. ' +
|
|
208
|
+
'This spends real money: the first month\'s rent plus a one-time activation fee are debited together the ' +
|
|
118
209
|
'moment the number is bought (search results and get_pricing show both amounts), and rent recurs monthly until ' +
|
|
119
210
|
'release_phone_number. A balance that cannot cover the total returns 402 INSUFFICIENT_BALANCE with nothing charged. ' +
|
|
120
|
-
'Accounts are capped on how many numbers they may hold at once
|
|
121
|
-
'default staff can change at runtime
|
|
122
|
-
'
|
|
123
|
-
'account is already running; this is transient and safe to retry after a short delay
|
|
211
|
+
'Accounts are capped on how many numbers they may hold at once: a per-account limit that falls back to a platform-wide ' +
|
|
212
|
+
'default staff can change at runtime. Exceeding it returns NUMBER_LIMIT_REACHED, and a higher allowance is requested with ' +
|
|
213
|
+
'request_number_allowance. May also return a 409 PURCHASE_IN_PROGRESS if another purchase for this ' +
|
|
214
|
+
'account is already running; this is transient and safe to retry after a short delay, unlike the terminal 409 returned ' +
|
|
124
215
|
'when the number is already owned, it does not mean the purchase failed. Firing several provision_phone_number calls in ' +
|
|
125
216
|
'parallel will produce these; retry rather than dropping them.', inputSchema: z.object({
|
|
126
217
|
phone_number: z.string().describe('E.164 phone number to provision (e.g. +14165551234). Must be from the search_phone_numbers results.'),
|
|
@@ -153,7 +244,7 @@ export function registerTools(server, client) {
|
|
|
153
244
|
return formatError(err);
|
|
154
245
|
}
|
|
155
246
|
});
|
|
156
|
-
server.registerTool('release_phone_number', { ...meta('release_phone_number'), description: 'Release (cancel) a provisioned phone number. This will stop monthly billing for the number; the one-time activation fee and the current month are not refunded. This action cannot be undone.', inputSchema: z.object({
|
|
247
|
+
server.registerTool('release_phone_number', { ...meta('release_phone_number'), description: 'Release (cancel) a provisioned phone number. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. This will stop monthly billing for the number; the one-time activation fee and the current month are not refunded. This action cannot be undone.', inputSchema: z.object({
|
|
157
248
|
id: z.string().describe('Phone number record ID to release.'),
|
|
158
249
|
}) }, async ({ id }) => {
|
|
159
250
|
try {
|
|
@@ -164,6 +255,27 @@ export function registerTools(server, client) {
|
|
|
164
255
|
return formatError(err);
|
|
165
256
|
}
|
|
166
257
|
});
|
|
258
|
+
server.registerTool('request_number_allowance', { ...meta('request_number_allowance'), description: 'File a request to raise how many phone numbers this account may hold at once, for HonkIO staff to review. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. Only one request may be pending at a time (a second returns 409 ALLOWANCE_REQUEST_PENDING); the owner is emailed when it is decided.', inputSchema: z.object({
|
|
259
|
+
requested_limit: z.number().int().min(2).max(100).describe('Total numbers you want to be able to hold, not an increment. Must exceed the current limit (see get_account → phone_number_limit).'),
|
|
260
|
+
reason: z.string().min(10).max(1000).describe('What the numbers are for. Staff decide on this.'),
|
|
261
|
+
}) }, async (input) => {
|
|
262
|
+
try {
|
|
263
|
+
const result = await client.requestNumberAllowance(input);
|
|
264
|
+
return ok(result);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
return formatError(err);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
server.registerTool('list_number_allowance_requests', { ...meta('list_number_allowance_requests'), description: 'List past and pending phone-number allowance requests for this account.', inputSchema: z.object({}) }, async () => {
|
|
271
|
+
try {
|
|
272
|
+
const result = await client.listNumberAllowanceRequests();
|
|
273
|
+
return ok(result);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
return formatError(err);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
167
279
|
// ── Compliance: CASL Consents ─────────────────────────────
|
|
168
280
|
server.registerTool('record_consent', { ...meta('record_consent'), description: 'Record CASL consent for a phone number before sending commercial messages. Express consent requires source_description. Implied consent requires relationship_type and is valid for 2 years from last_transaction_date.', inputSchema: z.object({
|
|
169
281
|
phone_number: z.string().describe('E.164 phone number of the subscriber granting consent.'),
|
|
@@ -220,7 +332,7 @@ export function registerTools(server, client) {
|
|
|
220
332
|
}
|
|
221
333
|
});
|
|
222
334
|
// ── Compliance: Opt-Outs ──────────────────────────────────
|
|
223
|
-
server.registerTool('record_opt_out', { ...meta('record_opt_out'), description: 'Manually record an opt-out for a subscriber. Use this when a subscriber contacts you directly to opt out rather than replying STOP to a message.', inputSchema: z.object({
|
|
335
|
+
server.registerTool('record_opt_out', { ...meta('record_opt_out'), description: 'Manually record an opt-out for a subscriber. Use this when a subscriber contacts you directly to opt out rather than replying STOP to a message. A live key must own from_number: opting a subscriber out of a number you don\'t hold is refused with 403 PHONE_NUMBER_NOT_OWNED. A test key skips that check.', inputSchema: z.object({
|
|
224
336
|
phone_number: z.string().describe('E.164 phone number of the subscriber opting out.'),
|
|
225
337
|
from_number: z.string().describe('E.164 number the subscriber is opting out from (your sending number).'),
|
|
226
338
|
}) }, async (input) => {
|
|
@@ -247,7 +359,7 @@ export function registerTools(server, client) {
|
|
|
247
359
|
}
|
|
248
360
|
});
|
|
249
361
|
// ── Compliance: DNCL ─────────────────────────────────────
|
|
250
|
-
server.registerTool('check_dncl', { ...meta('check_dncl'), description: "Coming soon: check if a Canadian phone number is on the CRTC Do Not Call List. CRTC DNCL checking is not available yet
|
|
362
|
+
server.registerTool('check_dncl', { ...meta('check_dncl'), description: "Coming soon: check if a Canadian phone number is on the CRTC Do Not Call List. CRTC DNCL checking is not available yet: this endpoint currently returns 501 (DNCL_COMING_SOON).", inputSchema: z.object({
|
|
251
363
|
phone_number: z.string().describe('Canadian phone number to check in E.164 format.'),
|
|
252
364
|
}) }, async ({ phone_number }) => {
|
|
253
365
|
try {
|
|
@@ -258,7 +370,7 @@ export function registerTools(server, client) {
|
|
|
258
370
|
return formatError(err);
|
|
259
371
|
}
|
|
260
372
|
});
|
|
261
|
-
server.registerTool('batch_check_dncl', { ...meta('batch_check_dncl'), description: 'Coming soon: check up to 100 Canadian phone numbers against the CRTC Do Not Call List in a single request. CRTC DNCL checking is not available yet
|
|
373
|
+
server.registerTool('batch_check_dncl', { ...meta('batch_check_dncl'), description: 'Coming soon: check up to 100 Canadian phone numbers against the CRTC Do Not Call List in a single request. CRTC DNCL checking is not available yet: this endpoint currently returns 501 (DNCL_COMING_SOON).', inputSchema: z.object({
|
|
262
374
|
phone_numbers: z.array(z.string()).min(1).max(100).describe('Array of Canadian phone numbers in E.164 format.'),
|
|
263
375
|
}) }, async ({ phone_numbers }) => {
|
|
264
376
|
try {
|
|
@@ -270,7 +382,7 @@ export function registerTools(server, client) {
|
|
|
270
382
|
}
|
|
271
383
|
});
|
|
272
384
|
// ── Compliance: Erasure ───────────────────────────────────
|
|
273
|
-
server.registerTool('request_erasure', { ...meta('request_erasure'), description: 'Execute a PIPEDA/Quebec Law 25 right-to-erasure request for a phone number.
|
|
385
|
+
server.registerTool('request_erasure', { ...meta('request_erasure'), description: 'Execute a PIPEDA/Quebec Law 25 right-to-erasure request for a phone number. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. Erases message bodies (keeping message metadata), consent records, verifications, contacts, non-DENY contact-list entries, and webhook dead letters referencing the number. Opt-out records are NOT erased (CASL requires keeping them as compliance evidence), and any DENY-list block on the number is kept so the number stays blocked; the response reports opt_outs_preserved: true. Creates an immutable audit log entry.', inputSchema: z.object({
|
|
274
386
|
phone_number: z.string().describe('E.164 phone number to erase data for.'),
|
|
275
387
|
reason: z.string().optional().describe('Reason for the erasure request (recorded in the audit log).'),
|
|
276
388
|
}) }, async (input) => {
|
|
@@ -283,7 +395,7 @@ export function registerTools(server, client) {
|
|
|
283
395
|
}
|
|
284
396
|
});
|
|
285
397
|
// ── Webhooks ──────────────────────────────────────────────
|
|
286
|
-
server.registerTool('create_webhook', { ...meta('create_webhook'), description: 'Register a webhook endpoint to receive HonkIO event notifications. The signing_secret in the response is shown once
|
|
398
|
+
server.registerTool('create_webhook', { ...meta('create_webhook'), description: 'Register a webhook endpoint to receive HonkIO event notifications. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. The signing_secret in the response is shown once; store it to verify X-HonkIO-Signature on incoming requests.', inputSchema: z.object({
|
|
287
399
|
url: z.string().url().describe('HTTPS URL to deliver events to.'),
|
|
288
400
|
events: z.array(z.enum(WEBHOOK_EVENTS)).min(1).describe('Event types to subscribe to.'),
|
|
289
401
|
}) }, async (input) => {
|
|
@@ -304,10 +416,21 @@ export function registerTools(server, client) {
|
|
|
304
416
|
return formatError(err);
|
|
305
417
|
}
|
|
306
418
|
});
|
|
307
|
-
server.registerTool('
|
|
419
|
+
server.registerTool('get_webhook', { ...meta('get_webhook'), description: 'Get details for a single registered webhook endpoint by ID.', inputSchema: z.object({
|
|
420
|
+
id: z.string().describe('Webhook ID.'),
|
|
421
|
+
}) }, async ({ id }) => {
|
|
422
|
+
try {
|
|
423
|
+
const result = await client.getWebhook(id);
|
|
424
|
+
return ok(result);
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
return formatError(err);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
server.registerTool('update_webhook', { ...meta('update_webhook'), description: 'Update a webhook endpoint URL, event subscriptions, or active status. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED.', inputSchema: z.object({
|
|
308
431
|
id: z.string().describe('Webhook ID to update.'),
|
|
309
432
|
url: z.string().url().optional().describe('New HTTPS URL.'),
|
|
310
|
-
events: z.array(z.enum(WEBHOOK_EVENTS)).optional().describe('New event subscriptions (replaces existing).'),
|
|
433
|
+
events: z.array(z.enum(WEBHOOK_EVENTS)).min(1).optional().describe('New event subscriptions (replaces existing). Cannot be an empty array: that would silently mute the webhook, so it is refused with 422.'),
|
|
311
434
|
active: z.boolean().optional().describe('Enable or disable the webhook.'),
|
|
312
435
|
}) }, async ({ id, ...updates }) => {
|
|
313
436
|
try {
|
|
@@ -318,7 +441,7 @@ export function registerTools(server, client) {
|
|
|
318
441
|
return formatError(err);
|
|
319
442
|
}
|
|
320
443
|
});
|
|
321
|
-
server.registerTool('delete_webhook', { ...meta('delete_webhook'), description: 'Delete a registered webhook endpoint.', inputSchema: z.object({
|
|
444
|
+
server.registerTool('delete_webhook', { ...meta('delete_webhook'), description: 'Delete a registered webhook endpoint. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED.', inputSchema: z.object({
|
|
322
445
|
id: z.string().describe('Webhook ID to delete.'),
|
|
323
446
|
}) }, async ({ id }) => {
|
|
324
447
|
try {
|
|
@@ -341,7 +464,7 @@ export function registerTools(server, client) {
|
|
|
341
464
|
return formatError(err);
|
|
342
465
|
}
|
|
343
466
|
});
|
|
344
|
-
server.registerTool('reactivate_webhook', { ...meta('reactivate_webhook'), description: 'Re-enable a webhook that was auto-disabled by repeated delivery failures. The destination URL is re-validated (SSRF check) before reactivation.', inputSchema: z.object({
|
|
467
|
+
server.registerTool('reactivate_webhook', { ...meta('reactivate_webhook'), description: 'Re-enable a webhook that was auto-disabled by repeated delivery failures. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. The destination URL is re-validated (SSRF check) before reactivation.', inputSchema: z.object({
|
|
345
468
|
webhook_id: z.string().describe('Webhook ID to reactivate.'),
|
|
346
469
|
}) }, async ({ webhook_id }) => {
|
|
347
470
|
try {
|
|
@@ -352,7 +475,7 @@ export function registerTools(server, client) {
|
|
|
352
475
|
return formatError(err);
|
|
353
476
|
}
|
|
354
477
|
});
|
|
355
|
-
server.registerTool('list_webhook_dead_letters', { ...meta('list_webhook_dead_letters'), description: 'List events that exhausted all retry attempts (dead-letter queue). These are events that failed delivery and were never received by your endpoint. Use replay_webhook_dead_letter to retry after fixing the issue.', inputSchema: z.object({
|
|
478
|
+
server.registerTool('list_webhook_dead_letters', { ...meta('list_webhook_dead_letters'), description: 'List events that exhausted all retry attempts (dead-letter queue). Live keys only: a test key gets 403 LIVE_KEY_REQUIRED, since a dead letter carries the failed payload. These are events that failed delivery and were never received by your endpoint. Use replay_webhook_dead_letter to retry after fixing the issue.', inputSchema: z.object({
|
|
356
479
|
webhook_id: z.string().describe('Webhook ID.'),
|
|
357
480
|
limit: z.number().int().min(1).max(200).optional().describe('Max rows (default 50).'),
|
|
358
481
|
include_replayed: z.boolean().optional().describe('Include events that have been successfully replayed (default false).'),
|
|
@@ -365,7 +488,7 @@ export function registerTools(server, client) {
|
|
|
365
488
|
return formatError(err);
|
|
366
489
|
}
|
|
367
490
|
});
|
|
368
|
-
server.registerTool('replay_webhook_dead_letter', { ...meta('replay_webhook_dead_letter'), description: 'Resend a dead-lettered event against the original webhook URL. The event is re-signed with the current timestamp; on success the dead-letter row is marked as replayed.', inputSchema: z.object({
|
|
491
|
+
server.registerTool('replay_webhook_dead_letter', { ...meta('replay_webhook_dead_letter'), description: 'Resend a dead-lettered event against the original webhook URL. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. The event is re-signed with the current timestamp; on success the dead-letter row is marked as replayed.', inputSchema: z.object({
|
|
369
492
|
dead_letter_id: z.string().describe('Dead-letter row ID (from list_webhook_dead_letters).'),
|
|
370
493
|
}) }, async ({ dead_letter_id }) => {
|
|
371
494
|
try {
|
|
@@ -376,7 +499,7 @@ export function registerTools(server, client) {
|
|
|
376
499
|
return formatError(err);
|
|
377
500
|
}
|
|
378
501
|
});
|
|
379
|
-
server.registerTool('discard_webhook_dead_letter', { ...meta('discard_webhook_dead_letter'), description: 'Permanently discard a dead-lettered event without replaying it. Use when the event is no longer relevant (e.g. the underlying message has expired).', inputSchema: z.object({
|
|
502
|
+
server.registerTool('discard_webhook_dead_letter', { ...meta('discard_webhook_dead_letter'), description: 'Permanently discard a dead-lettered event without replaying it. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. Use when the event is no longer relevant (e.g. the underlying message has expired).', inputSchema: z.object({
|
|
380
503
|
dead_letter_id: z.string().describe('Dead-letter row ID.'),
|
|
381
504
|
}) }, async ({ dead_letter_id }) => {
|
|
382
505
|
try {
|
|
@@ -388,7 +511,7 @@ export function registerTools(server, client) {
|
|
|
388
511
|
}
|
|
389
512
|
});
|
|
390
513
|
// ── Pricing ───────────────────────────────────────────────
|
|
391
|
-
server.registerTool('get_pricing', { ...meta('get_pricing'), description: 'Current HonkIO prices in CAD cents: per-SMS-segment cost for outbound messages, the per-segment cost charged for inbound SMS received on a provisioned number (STOP/START/HELP keywords are free), the verification upcharge, what a typical single-segment verification costs, phone-number upfront (first month) and monthly rent, and the one-time activation fee charged with the first month on every number. Call this before quoting a cost to a user or deciding whether an operation is worth its price
|
|
514
|
+
server.registerTool('get_pricing', { ...meta('get_pricing'), description: 'Current HonkIO prices in CAD cents: per-SMS-segment cost for outbound messages, the per-segment cost charged for inbound SMS received on a provisioned number (STOP/START/HELP keywords are free), the verification upcharge, what a typical single-segment verification costs, phone-number upfront (first month) and monthly rent, and the one-time activation fee charged with the first month on every number. Call this before quoting a cost to a user or deciding whether an operation is worth its price: these are set at runtime and change without a release, so never assume a figure.', inputSchema: z.object({}) }, async () => {
|
|
392
515
|
try {
|
|
393
516
|
const result = await client.getPricing();
|
|
394
517
|
return ok(result);
|
|
@@ -398,7 +521,7 @@ export function registerTools(server, client) {
|
|
|
398
521
|
}
|
|
399
522
|
});
|
|
400
523
|
// ── Verify (OTP) ──────────────────────────────────────────
|
|
401
|
-
server.registerTool('start_verification', { ...meta('start_verification'), description: 'Send a phone-number verification code (OTP) via SMS. The recipient receives a 4/6/8-digit numeric code. With a live key (mk_live_...) this sends a real SMS and charges the per-part message cost (one part unless app_name is long or non-GSM; settled to the carrier\'s part count) plus the verification upcharge
|
|
524
|
+
server.registerTool('start_verification', { ...meta('start_verification'), description: 'Send a phone-number verification code (OTP) via SMS. The recipient receives a 4/6/8-digit numeric code. With a live key (mk_live_...) this sends a real SMS and charges the per-part message cost (one part unless app_name is long or non-GSM; settled to the carrier\'s part count) plus the verification upcharge: call get_pricing for the current amount; a rejected OTP is refunded in full. With a test-mode key (mk_test_...) nothing is sent and nothing is charged: the code is always zeros for the chosen length (000000 for 6 digits), and the returned verification has mode "TEST". Check the "mode" field to confirm which happened.', inputSchema: z.object({
|
|
402
525
|
from: z.string().describe('Your HonkIO number (E.164, must be active on your account).'),
|
|
403
526
|
to: z.string().describe('The phone number to verify (E.164, Canadian numbers only).'),
|
|
404
527
|
code_length: z.union([z.literal(4), z.literal(6), z.literal(8)]).optional().describe('OTP digit length (default 6).'),
|
|
@@ -413,7 +536,7 @@ export function registerTools(server, client) {
|
|
|
413
536
|
return formatError(err);
|
|
414
537
|
}
|
|
415
538
|
});
|
|
416
|
-
server.registerTool('check_verification', { ...meta('check_verification'), description: 'Submit the OTP a user entered to complete verification.
|
|
539
|
+
server.registerTool('check_verification', { ...meta('check_verification'), description: 'Submit the OTP a user entered to complete verification. On success, returns the verification with status "verified" and attempts_remaining: 0. A wrong code comes back as a 422 VERIFICATION_INVALID_CODE error carrying attempts_remaining (5 attempts total); an expired verification is a 410 VERIFICATION_EXPIRED error; five failed attempts is a 429 VERIFICATION_MAX_ATTEMPTS error; checking one already completed is a 409 VERIFICATION_ALREADY_VERIFIED error. These come back as tool errors to catch, not a status value to branch on.', inputSchema: z.object({
|
|
417
540
|
verification_id: z.string().describe('Verification ID returned from start_verification.'),
|
|
418
541
|
code: z.string().regex(/^\d{4,8}$/).describe('The 4/6/8-digit code the user submitted.'),
|
|
419
542
|
}) }, async ({ verification_id, code }) => {
|
|
@@ -436,11 +559,10 @@ export function registerTools(server, client) {
|
|
|
436
559
|
return formatError(err);
|
|
437
560
|
}
|
|
438
561
|
});
|
|
439
|
-
server.registerTool('list_verifications', { ...meta('list_verifications'), description: 'List
|
|
440
|
-
phone_number: z.string().optional().describe('Filter to verifications for this E.164 number.'),
|
|
562
|
+
server.registerTool('list_verifications', { ...meta('list_verifications'), description: 'List verifications for your account, newest first, optionally filtered by status.', inputSchema: z.object({
|
|
441
563
|
status: z.enum(['pending', 'verified', 'expired', 'max_attempts']).optional().describe('Filter by status.'),
|
|
442
|
-
|
|
443
|
-
|
|
564
|
+
limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 50, max: 100).'),
|
|
565
|
+
offset: z.number().int().min(0).optional().describe('Number of records to skip (default: 0).'),
|
|
444
566
|
}) }, async (input) => {
|
|
445
567
|
try {
|
|
446
568
|
const result = await client.listVerifications(input);
|
|
@@ -454,7 +576,7 @@ export function registerTools(server, client) {
|
|
|
454
576
|
// ─── Sending limits ──────────────────────────────────────────
|
|
455
577
|
server.registerTool('get_send_limit', { ...meta('get_send_limit'), description: 'Your account\'s sending limits and current usage: the daily cap (a rolling 24 hours, 250/day for new accounts until a volume request is approved), ' +
|
|
456
578
|
'how many live messages were sent in the last 24 hours and how many remain, the probation status that gates "request a higher volume" ' +
|
|
457
|
-
'(it opens 30 days after the first live send), the per-recipient cap (recipient_rate_per_hour / recipient_rate_per_day
|
|
579
|
+
'(it opens 30 days after the first live send), the per-recipient cap (recipient_rate_per_hour / recipient_rate_per_day: a breach comes back as RECIPIENT_RATE_LIMITED with Retry-After), ' +
|
|
458
580
|
'whether live sending is currently paused and until when, and past volume requests. ' +
|
|
459
581
|
'Check this when a send is refused with DAILY_LIMIT_REACHED, RECIPIENT_RATE_LIMITED or SENDING_PAUSED, or before a batch of sends.', inputSchema: z.object({}) }, async () => {
|
|
460
582
|
try {
|
|
@@ -465,10 +587,10 @@ export function registerTools(server, client) {
|
|
|
465
587
|
return formatError(err);
|
|
466
588
|
}
|
|
467
589
|
});
|
|
468
|
-
server.registerTool('request_send_limit', { ...meta('request_send_limit'), description: 'File a "request a higher volume" for HonkIO staff to review. Only available once the account has completed its probation period ' +
|
|
469
|
-
'(get_send_limit → probation.eligible_to_request); one request may be pending at a time. Omit requested_limit to ask for the standard ' +
|
|
590
|
+
server.registerTool('request_send_limit', { ...meta('request_send_limit'), description: 'File a "request a higher volume" for HonkIO staff to review. Live keys only: a test key gets 403 LIVE_KEY_REQUIRED. Only available once the account has completed its probation period ' +
|
|
591
|
+
'(get_send_limit → probation.eligible_to_request); one request may be pending at a time (a second returns 409 SEND_LIMIT_REQUEST_PENDING). Omit requested_limit to ask for the standard ' +
|
|
470
592
|
'approved volume (get_send_limit → approved_daily_limit), or name a target above the current cap. The owner is emailed when it is decided. ' +
|
|
471
|
-
'HonkIO is for transactional and relationship messaging: describe what is sent, to whom, and roughly how many a day
|
|
593
|
+
'HonkIO is for transactional and relationship messaging: describe what is sent, to whom, and roughly how many a day; bulk marketing is not approved.', inputSchema: z.object({
|
|
472
594
|
requested_limit: z.number().int().min(1).optional().describe('Messages per day wanted (a total, not an increment). Must exceed the current daily limit. Omit for the standard approved volume.'),
|
|
473
595
|
reason: z.string().min(10).max(1000).describe('What is sent and to whom, and the expected daily volume. Staff decide on this.'),
|
|
474
596
|
}) }, async (input) => {
|
|
@@ -480,6 +602,15 @@ export function registerTools(server, client) {
|
|
|
480
602
|
return formatError(err);
|
|
481
603
|
}
|
|
482
604
|
});
|
|
605
|
+
server.registerTool('list_send_limit_requests', { ...meta('list_send_limit_requests'), description: 'List past and pending "higher volume" sending requests for this account.', inputSchema: z.object({}) }, async () => {
|
|
606
|
+
try {
|
|
607
|
+
const result = await client.listSendLimitRequests();
|
|
608
|
+
return ok(result);
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
return formatError(err);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
483
614
|
server.registerTool('get_topup_allowance', { ...meta('get_topup_allowance'), description: 'How much credit can be added to the account right now. Top-ups are capped by a maximum balance and a rolling 30-day total ' +
|
|
484
615
|
'(platform defaults $500 and $1,000 CAD, raisable per account); a checkout above max_topup_now_cents is refused with TOPUP_LIMIT_REACHED. ' +
|
|
485
616
|
'Returns the caps, the current balance, the 30-day total so far, and the amount that fits now, all in CAD cents.', inputSchema: z.object({
|
|
@@ -493,7 +624,7 @@ export function registerTools(server, client) {
|
|
|
493
624
|
return formatError(err);
|
|
494
625
|
}
|
|
495
626
|
});
|
|
496
|
-
server.registerTool('whoami', { ...meta('whoami'), description: 'Identify the HonkIO account the configured API key belongs to
|
|
627
|
+
server.registerTool('whoami', { ...meta('whoami'), description: 'Identify the HonkIO account the configured API key belongs to: the account (ID, name, credit balance, status) and its list of API keys (each with its own prefix, mode and label). It does not say which of those keys is the one you are calling with (the API never identifies the calling key back to itself), so track your own key\'s mode (mk_live_... or mk_test_...) separately. Call this first when you need an account ID.', inputSchema: z.object({}) }, async () => {
|
|
497
628
|
try {
|
|
498
629
|
const result = await client.getCurrentAccount();
|
|
499
630
|
return ok(result);
|
|
@@ -503,7 +634,7 @@ export function registerTools(server, client) {
|
|
|
503
634
|
}
|
|
504
635
|
});
|
|
505
636
|
server.registerTool('get_account', { ...meta('get_account'), description: 'Get details for your HonkIO account including name, credit balance, status, and active API keys. ' +
|
|
506
|
-
'Also returns phone_number_limit and phone_numbers_used
|
|
637
|
+
'Also returns phone_number_limit and phone_numbers_used: check these before calling ' +
|
|
507
638
|
'provision_phone_number, since a purchase past the limit is rejected with NUMBER_LIMIT_REACHED.', inputSchema: z.object({
|
|
508
639
|
account_id: ACCOUNT_ID_ARG,
|
|
509
640
|
}) }, async ({ account_id }) => {
|
|
@@ -540,20 +671,34 @@ export function registerTools(server, client) {
|
|
|
540
671
|
return formatError(err);
|
|
541
672
|
}
|
|
542
673
|
});
|
|
543
|
-
server.registerTool('
|
|
674
|
+
server.registerTool('list_transactions', { ...meta('list_transactions'), description: 'List the account\'s balance transaction ledger, newest first: top-ups, refunds, disputes, phone rent, provisioning fees, activation fees, and verification upcharges. Per-message SMS costs are NOT included here: see list_messages/get_message for those.', inputSchema: z.object({
|
|
675
|
+
account_id: ACCOUNT_ID_ARG,
|
|
676
|
+
limit: z.number().int().min(1).max(100).optional().describe('Results per page (default: 50, max: 100).'),
|
|
677
|
+
before: z.string().optional().describe('Id of the oldest transaction already seen: returns the next (older) page.'),
|
|
678
|
+
}) }, async ({ account_id, limit, before }) => {
|
|
679
|
+
try {
|
|
680
|
+
const result = await client.listTransactions(await resolveAccountId(account_id), { limit, before });
|
|
681
|
+
return ok(result);
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
return formatError(err);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
server.registerTool('create_api_key', { ...meta('create_api_key'), description: 'Issue a new API key for your account. The raw key is shown once: store it securely. A key can only be created with permissions the calling key itself holds: asking for more is refused with 403 PERMISSION_ESCALATION. A test key can only create test keys (mode "live" from a test key is refused with 403 LIVE_KEY_REQUIRED).', inputSchema: z.object({
|
|
544
688
|
account_id: ACCOUNT_ID_ARG,
|
|
545
|
-
mode: z.enum(['live', 'test']).optional().describe('Key mode: "live"
|
|
689
|
+
mode: z.enum(['live', 'test']).optional().describe('Key mode: "live" or "test". Defaults to the calling key\'s own mode.'),
|
|
546
690
|
label: z.string().max(100).optional().describe('Human-readable label to identify this key (e.g. "Production server", "CI pipeline").'),
|
|
547
|
-
|
|
691
|
+
permissions: PERMISSIONS_ARG,
|
|
692
|
+
}) }, async ({ account_id, mode, label, permissions }) => {
|
|
548
693
|
try {
|
|
549
|
-
const result = await client.createApiKey(await resolveAccountId(account_id), { mode, label });
|
|
694
|
+
const result = await client.createApiKey(await resolveAccountId(account_id), { mode, label, permissions });
|
|
550
695
|
return ok(result);
|
|
551
696
|
}
|
|
552
697
|
catch (err) {
|
|
553
698
|
return formatError(err);
|
|
554
699
|
}
|
|
555
700
|
});
|
|
556
|
-
server.registerTool('revoke_api_key', { ...meta('revoke_api_key'), description: 'Revoke an API key, immediately blocking all requests using that key. This cannot be undone.', inputSchema: z.object({
|
|
701
|
+
server.registerTool('revoke_api_key', { ...meta('revoke_api_key'), description: 'Revoke an API key, immediately blocking all requests using that key. This cannot be undone. A test key can revoke any of the account\'s keys except a live one: revoking a live key needs a live key (403 LIVE_KEY_REQUIRED).', inputSchema: z.object({
|
|
557
702
|
account_id: ACCOUNT_ID_ARG,
|
|
558
703
|
key_id: z.string().describe('The API key record ID to revoke (from get_account api_keys list).'),
|
|
559
704
|
}) }, async ({ account_id, key_id }) => {
|
|
@@ -565,7 +710,7 @@ export function registerTools(server, client) {
|
|
|
565
710
|
return formatError(err);
|
|
566
711
|
}
|
|
567
712
|
});
|
|
568
|
-
server.registerTool('rotate_api_key', { ...meta('rotate_api_key'), description: 'Atomically issue a replacement API key inheriting the same permissions
|
|
713
|
+
server.registerTool('rotate_api_key', { ...meta('rotate_api_key'), description: 'Atomically issue a replacement API key inheriting the same permissions, label, mode, allow/deny lists and default-deny flag as the source key, and revoke the source key in the same transaction. Returns the new raw key ONCE. A test key can only rotate a test key (403 LIVE_KEY_REQUIRED otherwise). A dashboard session key can never be the source: it always expires on its own (409 CONFLICT). Rotating a key into more permissions than the calling key holds, or calling this tool with a session key, is refused with 403 PERMISSION_ESCALATION.', inputSchema: z.object({
|
|
569
714
|
account_id: ACCOUNT_ID_ARG,
|
|
570
715
|
key_id: z.string().describe('The API key record ID to rotate.'),
|
|
571
716
|
}) }, async ({ account_id, key_id }) => {
|