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 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.2",
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.0",
65
+ "@types/bun": "1.4.1",
66
66
  "typescript": "7.0.2"
67
67
  },
68
68
  "dependencies": {
@@ -5334,4 +5334,4 @@
5334
5334
  }
5335
5335
  }
5336
5336
  }
5337
- }
5337
+ }
@@ -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: bodyText,
188
+ text,
187
189
  truncated: false,
188
- byteLength: Buffer.byteLength(bodyText, 'utf8'),
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, items, maxBytes } = input;
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 visibleBody = truncated ? truncateUtf8(bodyText, maxBytes) : bodyText;
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
- - Create/get response includes \`hmac_secret\` (store it; Askell will not show it again in a useful way if you lose it) and \`hmac_digest\` (typically \`SHA512\`)
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
@@ -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,