askell-mcp 0.4.2 → 0.4.3
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 +1 -1
- package/package.json +2 -2
- package/spec/openapi-v2.json +1 -1
- package/src/client/askell-client.ts +4 -2
- package/src/client/redact.ts +54 -0
- package/src/client/response-formatter.ts +10 -3
- package/src/resources/register.ts +3 -2
- package/src/server.ts +1 -0
- package/src/tools/analysis.ts +1 -1
- package/src/tools/call.ts +2 -2
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ Typical agent workflow:
|
|
|
113
113
|
| `askell_customer_overview` | v1 customer + subscriptions |
|
|
114
114
|
| `askell_contract_overview` | v2 subscription contract + billing runs |
|
|
115
115
|
| `askell_billing_run_triage` | v2 billing run (+ optional contract) |
|
|
116
|
-
| `askell_list_webhooks` | List configured webhooks
|
|
116
|
+
| `askell_list_webhooks` | List configured webhooks (`hmac_secret` redacted) |
|
|
117
117
|
|
|
118
118
|
### Resources
|
|
119
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "askell-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "MCP server for the Askell payment and subscription API (Bun + stdio)",
|
|
5
5
|
"author": "Neschadin Oleksandr",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"build:all": "bun run build && bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:darwin-arm64 && bun run build:darwin-x64"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@types/bun": "1.4.
|
|
65
|
+
"@types/bun": "1.4.1",
|
|
66
66
|
"typescript": "7.0.2"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
package/spec/openapi-v2.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeBaseUrl, type AppConfig } from '../config.ts';
|
|
2
2
|
import { normalizeApiPath } from './paths.ts';
|
|
3
|
+
import { redactSecretsInText } from './redact.ts';
|
|
3
4
|
import {
|
|
4
5
|
buildBoundedListPayload,
|
|
5
6
|
formatApiResponse,
|
|
@@ -182,10 +183,11 @@ export class AskellClient {
|
|
|
182
183
|
try {
|
|
183
184
|
parsed = JSON.parse(bodyText);
|
|
184
185
|
} catch {
|
|
186
|
+
const text = redactSecretsInText(bodyText);
|
|
185
187
|
return {
|
|
186
|
-
text
|
|
188
|
+
text,
|
|
187
189
|
truncated: false,
|
|
188
|
-
byteLength: Buffer.byteLength(
|
|
190
|
+
byteLength: Buffer.byteLength(text, 'utf8'),
|
|
189
191
|
ok: false,
|
|
190
192
|
status: lastStatus,
|
|
191
193
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const SECRET_KEYS = new Set(['hmac_secret']);
|
|
2
|
+
|
|
3
|
+
const JSON_SECRET_STRING =
|
|
4
|
+
/"(hmac_secret)"\s*:\s*("(?:\\.|[^"\\])*")/g;
|
|
5
|
+
|
|
6
|
+
export function formatRedactedSecret(value: unknown): string {
|
|
7
|
+
if (typeof value === 'string' && value.startsWith('<redacted')) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
return `<redacted len=${value.length}>`;
|
|
12
|
+
}
|
|
13
|
+
return '<redacted>';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function redactSensitiveFields(value: unknown): unknown {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return value.map(redactSensitiveFields);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (value !== null && typeof value === 'object') {
|
|
22
|
+
const out: Record<string, unknown> = {};
|
|
23
|
+
for (const [key, nested] of Object.entries(
|
|
24
|
+
value as Record<string, unknown>,
|
|
25
|
+
)) {
|
|
26
|
+
out[key] = SECRET_KEYS.has(key)
|
|
27
|
+
? formatRedactedSecret(nested)
|
|
28
|
+
: redactSensitiveFields(nested);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Fallback for non-JSON / truncated bodies. Leaves already-redacted values alone. */
|
|
37
|
+
export function redactSecretsInText(text: string): string {
|
|
38
|
+
if (!text.includes('hmac_secret')) {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(redactSensitiveFields(JSON.parse(text)));
|
|
44
|
+
} catch {
|
|
45
|
+
JSON_SECRET_STRING.lastIndex = 0;
|
|
46
|
+
return text.replace(JSON_SECRET_STRING, (_match, key: string, raw: string) => {
|
|
47
|
+
try {
|
|
48
|
+
return `"${key}": ${JSON.stringify(formatRedactedSecret(JSON.parse(raw)))}`;
|
|
49
|
+
} catch {
|
|
50
|
+
return `"${key}": ${JSON.stringify(formatRedactedSecret(undefined))}`;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redactSensitiveFields,
|
|
3
|
+
redactSecretsInText,
|
|
4
|
+
} from './redact.ts';
|
|
5
|
+
|
|
1
6
|
export interface FormattedResponse {
|
|
2
7
|
text: string;
|
|
3
8
|
truncated: boolean;
|
|
@@ -203,7 +208,8 @@ export function buildBoundedListPayload(input: {
|
|
|
203
208
|
items: unknown[];
|
|
204
209
|
maxBytes: number;
|
|
205
210
|
}): FormattedResponse {
|
|
206
|
-
const { status, meta,
|
|
211
|
+
const { status, meta, maxBytes } = input;
|
|
212
|
+
const items = input.items.map((item) => redactSensitiveFields(item));
|
|
207
213
|
|
|
208
214
|
const attempts: Array<{
|
|
209
215
|
items: unknown[];
|
|
@@ -334,13 +340,14 @@ export function formatApiResponse(
|
|
|
334
340
|
): FormattedResponse {
|
|
335
341
|
const byteLength = Buffer.byteLength(bodyText, 'utf8');
|
|
336
342
|
const truncated = byteLength > maxBytes;
|
|
337
|
-
const
|
|
343
|
+
const safeText = redactSecretsInText(bodyText);
|
|
344
|
+
const visibleBody = truncated ? truncateUtf8(safeText, maxBytes) : safeText;
|
|
338
345
|
|
|
339
346
|
let parsedBody: unknown = visibleBody;
|
|
340
347
|
try {
|
|
341
348
|
parsedBody = JSON.parse(visibleBody);
|
|
342
349
|
} catch {
|
|
343
|
-
// keep raw text
|
|
350
|
+
// keep redacted raw text
|
|
344
351
|
}
|
|
345
352
|
|
|
346
353
|
const payload = {
|
|
@@ -21,9 +21,10 @@ Rare historical payloads used \`{ event, data, ref?, sender? }\`. If both \`even
|
|
|
21
21
|
|
|
22
22
|
## Registering endpoints (v1 management API)
|
|
23
23
|
|
|
24
|
-
- GET/POST \`/webhooks/\` · GET/PUT/PATCH/DELETE \`/webhooks/{id}/\` (secret key)
|
|
24
|
+
- GET/POST \`/webhooks/\` · GET/PUT/PATCH/DELETE \`/webhooks/{id}/\` (secret key). Live GET \`/webhooks/{id}/\` exists even if OpenAPI omits it.
|
|
25
25
|
- Create body: \`{ url, event }\` (\`event\` may be a specific type or a family wildcard like \`payment.*\`)
|
|
26
|
-
-
|
|
26
|
+
- Askell returns plaintext \`hmac_secret\` on list, get, and create (it is re-readable, not create-only), plus \`hmac_digest\` (typically \`SHA512\`)
|
|
27
|
+
- MCP tool output redacts \`hmac_secret\` to \`<redacted len=N>\`. Do not treat that placeholder as the real secret. Copy the secret from the Askell dashboard or a direct API call outside MCP.
|
|
27
28
|
|
|
28
29
|
Tools: \`askell_list_webhooks\`, \`askell_call\` (GET), \`askell_mutate\` (POST/PUT/PATCH/DELETE).
|
|
29
30
|
|
package/src/server.ts
CHANGED
|
@@ -64,6 +64,7 @@ Safety:
|
|
|
64
64
|
- Writes go through askell_mutate (destructiveHint). Reads go through askell_call (readOnlyHint).
|
|
65
65
|
- mutationGate=auto (default): confirmation form only if this request's envelope declared form elicitation; otherwise the client's own tool-allow UI is the gate. elicit always returns a form (SDK refuses if the client cannot fulfil it). off never asks.
|
|
66
66
|
- Large list responses are compacted (index of id/dates/plan/customer) to fit responseMaxBytes before dropping rows; check meta.truncatedByMaxBytes, meta.compacted, and meta.compactedMode.
|
|
67
|
+
- Tool output redacts webhook hmac_secret to \`<redacted len=N>\` (Askell list/get/create return the plaintext secret).
|
|
67
68
|
|
|
68
69
|
Resources:
|
|
69
70
|
- askell://spec/v1 and askell://spec/v2 — bundled OpenAPI
|
package/src/tools/analysis.ts
CHANGED
|
@@ -278,7 +278,7 @@ export function registerAnalysisTools(
|
|
|
278
278
|
{
|
|
279
279
|
title: 'List configured webhooks (v1)',
|
|
280
280
|
description:
|
|
281
|
-
'List Askell webhook endpoints configured for the account (management API only).',
|
|
281
|
+
'List Askell webhook endpoints configured for the account (management API only). hmac_secret is redacted in the tool output (`<redacted len=N>`); copy the real secret from the dashboard or a non-MCP API call.',
|
|
282
282
|
inputSchema: z.object({
|
|
283
283
|
page_size: z
|
|
284
284
|
.int()
|
package/src/tools/call.ts
CHANGED
|
@@ -171,7 +171,7 @@ export function registerCallTools(
|
|
|
171
171
|
{
|
|
172
172
|
title: 'Call Askell API (read)',
|
|
173
173
|
description:
|
|
174
|
-
'Read-only Askell API call (GET, HEAD) for any v1/v2 path. For POST/PUT/PATCH/DELETE use askell_mutate. Discover paths with askell_list_operations and askell_describe_operation first.',
|
|
174
|
+
'Read-only Askell API call (GET, HEAD) for any v1/v2 path. For POST/PUT/PATCH/DELETE use askell_mutate. Discover paths with askell_list_operations and askell_describe_operation first. Webhook hmac_secret is redacted in the response.',
|
|
175
175
|
inputSchema: callInputSchema,
|
|
176
176
|
annotations: {
|
|
177
177
|
readOnlyHint: true,
|
|
@@ -190,7 +190,7 @@ export function registerCallTools(
|
|
|
190
190
|
{
|
|
191
191
|
title: 'Mutate Askell API',
|
|
192
192
|
description:
|
|
193
|
-
'Mutating Askell API call (POST, PUT, PATCH, DELETE). Clients that declared form elicitation get a confirmation form; others rely on the client tool-approval UI. Use askell_call for GET. Discover paths with askell_list_operations and askell_describe_operation first.',
|
|
193
|
+
'Mutating Askell API call (POST, PUT, PATCH, DELETE). Clients that declared form elicitation get a confirmation form; others rely on the client tool-approval UI. Use askell_call for GET. Discover paths with askell_list_operations and askell_describe_operation first. Webhook hmac_secret is redacted in the response (including POST /webhooks/ create).',
|
|
194
194
|
inputSchema: mutateInputSchema,
|
|
195
195
|
annotations: {
|
|
196
196
|
readOnlyHint: false,
|