askell-mcp 0.3.0 → 0.4.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 CHANGED
@@ -19,16 +19,27 @@ In the Askell dashboard, copy your **private (secret)** API key. Optionally also
19
19
 
20
20
  ### 2. Add to your MCP client
21
21
 
22
+ Prefer **two server entries** if you have both production and sandbox keys. Tool names are the same on both; the client distinguishes them by the `mcp.json` key (`askell-prod` vs `askell-sandbox`). Set `ASKELL_ENV` — the server picks the host. Each instance's instructions include the environment it is talking to.
23
+
22
24
  **With Bun** (`bunx`):
23
25
 
24
26
  ```json
25
27
  {
26
28
  "mcpServers": {
27
- "askell": {
29
+ "askell-prod": {
28
30
  "command": "bunx",
29
31
  "args": ["-y", "askell-mcp"],
30
32
  "env": {
31
- "ASKELL_PRIVATE_API_KEY": "your_secret_api_key"
33
+ "ASKELL_ENV": "production",
34
+ "ASKELL_PRIVATE_API_KEY": "your_production_secret_api_key"
35
+ }
36
+ },
37
+ "askell-sandbox": {
38
+ "command": "bunx",
39
+ "args": ["-y", "askell-mcp"],
40
+ "env": {
41
+ "ASKELL_ENV": "sandbox",
42
+ "ASKELL_PRIVATE_API_KEY": "your_sandbox_secret_api_key"
32
43
  }
33
44
  }
34
45
  }
@@ -40,10 +51,11 @@ In the Askell dashboard, copy your **private (secret)** API key. Optionally also
40
51
  ```json
41
52
  {
42
53
  "mcpServers": {
43
- "askell": {
54
+ "askell-prod": {
44
55
  "command": "/absolute/path/to/askell-mcp-linux-x64",
45
56
  "env": {
46
- "ASKELL_PRIVATE_API_KEY": "your_secret_api_key"
57
+ "ASKELL_ENV": "production",
58
+ "ASKELL_PRIVATE_API_KEY": "your_production_secret_api_key"
47
59
  }
48
60
  }
49
61
  }
@@ -60,12 +72,18 @@ Restart the client after saving.
60
72
  | ---------------------------------- | -------- | ----------------------- | ----------------------------------------------- |
61
73
  | `ASKELL_PRIVATE_API_KEY` | yes\* | — | Secret API key (_or_ `ASKELL_SECRET_API_KEY`) |
62
74
  | `ASKELL_PUBLIC_API_KEY` | no | — | Public key for a few checkout/payment endpoints |
63
- | `ASKELL_API_URL` | no | `https://askell.is/api` | API base URL (_or_ `ASKELL_API_BASE_URL`) |
75
+ | `ASKELL_ENV` | no | `production` | `production` \| `sandbox` selects the official API host |
76
+ | `ASKELL_API_URL` | no | — | Custom/local API base only (_or_ `ASKELL_API_BASE_URL`). Do not set together with `ASKELL_ENV` unless it matches |
64
77
  | `ASKELL_RESPONSE_MAX_BYTES` | no | `64000` | Max response size returned to the model |
65
78
  | `ASKELL_MUTATION_GATE` | no | `auto` | `auto` / `elicit` / `off` — see below |
66
79
  | `ASKELL_REQUIRE_MUTATION_APPROVAL` | no | — | Deprecated alias: `true`→`elicit`, `false`→`off` |
67
80
 
68
- Askell has **no separate sandbox host** — production and test traffic use the same URL. Use the **Áskell Test Gateway** acquirer in your dashboard for safe payment testing. See [Askell getting started](https://docs.askell.is/en/getting_started/index.html).
81
+ `ASKELL_ENV` picks a stable host (same v1/v2 surface):
82
+
83
+ - **production** — `https://askell.is/api`
84
+ - **sandbox** — `https://sandbox.askell.is/api` (isolated tenant; keys from that dashboard)
85
+
86
+ Point a second MCP server entry at sandbox (`ASKELL_ENV=sandbox`) rather than switching env on one process. Keys do not work across hosts. **Áskell Test Gateway** is a payment acquirer (fake cards) on either host — not the same as the sandbox API. Official prose at [docs.askell.is](https://docs.askell.is/en/getting_started/index.html) still documents Test Gateway and may omit the sandbox host.
69
87
 
70
88
  `ASKELL_MUTATION_GATE`:
71
89
 
package/mcp.json.example CHANGED
@@ -1,11 +1,21 @@
1
1
  {
2
2
  "mcpServers": {
3
- "askell": {
3
+ "askell-prod": {
4
4
  "command": "bunx",
5
5
  "args": ["-y", "askell-mcp"],
6
6
  "env": {
7
- "ASKELL_PRIVATE_API_KEY": "your_secret_api_key",
8
- "ASKELL_PUBLIC_API_KEY": "your_public_api_key_optional"
7
+ "ASKELL_ENV": "production",
8
+ "ASKELL_PRIVATE_API_KEY": "your_production_secret_api_key",
9
+ "ASKELL_PUBLIC_API_KEY": "your_production_public_api_key_optional"
10
+ }
11
+ },
12
+ "askell-sandbox": {
13
+ "command": "bunx",
14
+ "args": ["-y", "askell-mcp"],
15
+ "env": {
16
+ "ASKELL_ENV": "sandbox",
17
+ "ASKELL_PRIVATE_API_KEY": "your_sandbox_secret_api_key",
18
+ "ASKELL_PUBLIC_API_KEY": "your_sandbox_public_api_key_optional"
9
19
  }
10
20
  }
11
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askell-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for the Askell payment and subscription API (Bun + stdio)",
5
5
  "author": "Neschadin Oleksandr",
6
6
  "license": "MIT",
@@ -65,6 +65,6 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@modelcontextprotocol/server": "2.0.0",
68
- "zod": "4.4.3"
68
+ "zod": "4.5.4"
69
69
  }
70
70
  }
@@ -8,7 +8,8 @@
8
8
  "name": "Documentation",
9
9
  "url": "https://docs.askell.is"
10
10
  },
11
- "version": "1.0.0"
11
+ "version": "1.0.0",
12
+ "x-askell-mcp-patched": "v1-overlay-1"
12
13
  },
13
14
  "servers": [
14
15
  {
@@ -52,10 +53,6 @@
52
53
  "name": "Webhooks",
53
54
  "description": "Management of webhooks. Requires secret api key."
54
55
  },
55
- {
56
- "name": "Webhook calls",
57
- "description": "Webhook calls from Áskell to your system."
58
- },
59
56
  {
60
57
  "name": "Transaction",
61
58
  "description": "Transaction operations, requires secret api key."
@@ -339,7 +336,14 @@
339
336
  },
340
337
  "responses": {
341
338
  "201": {
342
- "description": "Successful operation"
339
+ "description": "Successful operation",
340
+ "content": {
341
+ "application/json": {
342
+ "schema": {
343
+ "$ref": "#/components/schemas/Customer"
344
+ }
345
+ }
346
+ }
343
347
  },
344
348
  "400": {
345
349
  "description": "Invalid status value",
@@ -506,7 +510,14 @@
506
510
  ],
507
511
  "responses": {
508
512
  "200": {
509
- "description": "Successful operation"
513
+ "description": "Successful operation",
514
+ "content": {
515
+ "application/json": {
516
+ "schema": {
517
+ "$ref": "#/components/schemas/Customer"
518
+ }
519
+ }
520
+ }
510
521
  },
511
522
  "404": {
512
523
  "description": "Not found"
@@ -572,7 +583,14 @@
572
583
  ],
573
584
  "responses": {
574
585
  "200": {
575
- "description": "Successful operation"
586
+ "description": "Successful operation",
587
+ "content": {
588
+ "application/json": {
589
+ "schema": {
590
+ "$ref": "#/components/schemas/Customer"
591
+ }
592
+ }
593
+ }
576
594
  },
577
595
  "404": {
578
596
  "description": "Not found"
@@ -1367,23 +1385,6 @@
1367
1385
  ]
1368
1386
  }
1369
1387
  },
1370
- "/your-webhook-url/": {
1371
- "post": {
1372
- "tags": [
1373
- "Webhook calls"
1374
- ],
1375
- "summary": "Called when subscription changed or renewed",
1376
- "description": "We recommend that you verify all calls from us to your webhook endpoints. We sign each request with the `Hook-HMAC` header. The value is a HMAC digest derived from the body of the request and a secret key only known by the API and you. We also supply the type of event in the `Hook-Event` header.\n\nHere is a sample implementation in Python. This process is very similar to the one Shopify uses so more information can possibly be gleaned from their [documentation](https://shopify.dev/tutorials/manage-webhooks#verify-webhook).\n```\nimport base64\nimport hmac\nimport hashlib\n\nWEBHOOK_SECRET = \"Your webhook secret\".encode()\nWEBHOOK_DIGEST_TYPE = 'sha512'\n\ndef verify(hmac_header, digest_method, secret, message):\n digestmod = getattr(hashlib, digest_method)\n signed = base64.b64encode(\n hmac.new(secret, message, digestmod).digest(),\n ).strip()\n return hmac.compare_digest(signed, hmac_header)\n\n# your view function\ndef handle_webhook(request):\n # The signature\n digest = request.META.get('HTTP_HOOK_HMAC').encode()\n # The name of the webhook event\n event = request.META.get('HTTP_HOOK_EVENT').encode()\n\n body = request.body\n if verify(digest, WEBHOOK_DIGEST_TYPE, WEBHOOK_SECRET, body):\n payload = json.loads(body)\n # ... the rest of your code here```\n",
1377
- "requestBody": {
1378
- "$ref": "#/components/requestBodies/SubscriptionMultiLite"
1379
- },
1380
- "responses": {
1381
- "200": {
1382
- "description": "Successful operation"
1383
- }
1384
- }
1385
- }
1386
- },
1387
1388
  "/webhooks/": {
1388
1389
  "get": {
1389
1390
  "tags": [
@@ -1423,7 +1424,14 @@
1423
1424
  },
1424
1425
  "responses": {
1425
1426
  "201": {
1426
- "description": "Successful operation"
1427
+ "description": "Successful operation",
1428
+ "content": {
1429
+ "application/json": {
1430
+ "schema": {
1431
+ "$ref": "#/components/schemas/Webhook"
1432
+ }
1433
+ }
1434
+ }
1427
1435
  },
1428
1436
  "400": {
1429
1437
  "description": "Invalid status value",
@@ -1521,7 +1529,14 @@
1521
1529
  },
1522
1530
  "responses": {
1523
1531
  "200": {
1524
- "description": "Successful operation"
1532
+ "description": "Successful operation",
1533
+ "content": {
1534
+ "application/json": {
1535
+ "schema": {
1536
+ "$ref": "#/components/schemas/Webhook"
1537
+ }
1538
+ }
1539
+ }
1525
1540
  },
1526
1541
  "400": {
1527
1542
  "description": "Invalid status value",
@@ -1964,27 +1979,94 @@
1964
1979
  }
1965
1980
  },
1966
1981
  "Customer": {
1967
- "allOf": [
1968
- {
1969
- "$ref": "#/components/schemas/CustomerCreate"
1982
+ "type": "object",
1983
+ "required": [
1984
+ "id",
1985
+ "first_name",
1986
+ "last_name",
1987
+ "email",
1988
+ "customer_reference",
1989
+ "phone",
1990
+ "delivery_name",
1991
+ "address_1",
1992
+ "address_2",
1993
+ "address_3",
1994
+ "zip_code",
1995
+ "city",
1996
+ "country",
1997
+ "payment_method",
1998
+ "address"
1999
+ ],
2000
+ "properties": {
2001
+ "id": {
2002
+ "type": "integer",
2003
+ "format": "int64"
1970
2004
  },
1971
- {
1972
- "type": "object",
1973
- "properties": {
1974
- "id": {
1975
- "type": "integer",
1976
- "format": "int64",
1977
- "example": 1
1978
- },
1979
- "payment_method": {
1980
- "type": "array",
1981
- "items": {
1982
- "$ref": "#/components/schemas/PaymentMethod"
1983
- }
1984
- }
2005
+ "first_name": {
2006
+ "type": "string",
2007
+ "maxLength": 128
2008
+ },
2009
+ "last_name": {
2010
+ "type": "string",
2011
+ "maxLength": 128
2012
+ },
2013
+ "email": {
2014
+ "type": "string",
2015
+ "nullable": true,
2016
+ "maxLength": 254
2017
+ },
2018
+ "phone": {
2019
+ "type": "string",
2020
+ "nullable": true,
2021
+ "maxLength": 32
2022
+ },
2023
+ "customer_reference": {
2024
+ "type": "string",
2025
+ "maxLength": 256
2026
+ },
2027
+ "delivery_name": {
2028
+ "type": "string",
2029
+ "nullable": true
2030
+ },
2031
+ "address_1": {
2032
+ "type": "string",
2033
+ "nullable": true
2034
+ },
2035
+ "address_2": {
2036
+ "type": "string",
2037
+ "nullable": true
2038
+ },
2039
+ "address_3": {
2040
+ "type": "string",
2041
+ "nullable": true
2042
+ },
2043
+ "zip_code": {
2044
+ "type": "string",
2045
+ "nullable": true
2046
+ },
2047
+ "city": {
2048
+ "type": "string",
2049
+ "nullable": true
2050
+ },
2051
+ "country": {
2052
+ "type": "string",
2053
+ "nullable": true
2054
+ },
2055
+ "payment_method": {
2056
+ "type": "array",
2057
+ "items": {
2058
+ "$ref": "#/components/schemas/PaymentMethod"
1985
2059
  }
2060
+ },
2061
+ "address": {
2062
+ "allOf": [
2063
+ {
2064
+ "$ref": "#/components/schemas/CustomerAddress"
2065
+ }
2066
+ ],
2067
+ "nullable": true
1986
2068
  }
1987
- ]
2069
+ }
1988
2070
  },
1989
2071
  "CustomerPaginated": {
1990
2072
  "type": "object",
@@ -2593,6 +2675,48 @@
2593
2675
  "example": "tokencreated"
2594
2676
  }
2595
2677
  }
2678
+ },
2679
+ "CustomerAddress": {
2680
+ "type": "object",
2681
+ "required": [
2682
+ "delivery_name",
2683
+ "address_1",
2684
+ "address_2",
2685
+ "address_3",
2686
+ "zip_code",
2687
+ "city",
2688
+ "country"
2689
+ ],
2690
+ "properties": {
2691
+ "delivery_name": {
2692
+ "type": "string",
2693
+ "nullable": true
2694
+ },
2695
+ "address_1": {
2696
+ "type": "string",
2697
+ "nullable": true
2698
+ },
2699
+ "address_2": {
2700
+ "type": "string",
2701
+ "nullable": true
2702
+ },
2703
+ "address_3": {
2704
+ "type": "string",
2705
+ "nullable": true
2706
+ },
2707
+ "zip_code": {
2708
+ "type": "string",
2709
+ "nullable": true
2710
+ },
2711
+ "city": {
2712
+ "type": "string",
2713
+ "nullable": true
2714
+ },
2715
+ "country": {
2716
+ "type": "string",
2717
+ "nullable": true
2718
+ }
2719
+ }
2596
2720
  }
2597
2721
  },
2598
2722
  "requestBodies": {
@@ -2780,4 +2904,4 @@
2780
2904
  }
2781
2905
  }
2782
2906
  }
2783
- }
2907
+ }
package/src/config.ts CHANGED
@@ -1,11 +1,23 @@
1
1
  import * as z from 'zod';
2
2
 
3
+ export const PRODUCTION_API_BASE_URL = 'https://askell.is/api';
4
+ export const SANDBOX_API_BASE_URL = 'https://sandbox.askell.is/api';
5
+
6
+ export const ASKELL_ENVS = ['production', 'sandbox'] as const;
7
+ export type AskellOfficialEnv = (typeof ASKELL_ENVS)[number];
8
+ export type AskellEnv = AskellOfficialEnv | 'custom';
9
+
10
+ export const ASKELL_API_BASE_URLS = {
11
+ production: PRODUCTION_API_BASE_URL,
12
+ sandbox: SANDBOX_API_BASE_URL,
13
+ } as const satisfies Record<AskellOfficialEnv, string>;
14
+
3
15
  export const MUTATION_GATES = ['auto', 'elicit', 'off'] as const;
4
16
  export type MutationGate = (typeof MUTATION_GATES)[number];
5
17
 
6
18
  const httpUrl = z
7
19
  .url({ protocol: /^https?$/ })
8
- .describe('Askell API base URL (default production host)');
20
+ .describe('Custom Askell API base URL (local/fork override)');
9
21
 
10
22
  const mutationGateAliases = z
11
23
  .enum(['true', 'false', 'on', 'yes', 'no', '1', '0'])
@@ -26,8 +38,55 @@ export const MutationGateSchema = z
26
38
  'Mutation confirmation: auto (elicit if client declared it), elicit (require form), off (never)',
27
39
  );
28
40
 
29
- export const ConfigSchema = z.object({
30
- apiBaseUrl: httpUrl.default('https://askell.is/api'),
41
+ export const AskellEnvSchema = z
42
+ .string()
43
+ .trim()
44
+ .toLowerCase()
45
+ .transform((value) => (value === 'prod' ? 'production' : value))
46
+ .pipe(z.enum(ASKELL_ENVS));
47
+
48
+ export function classifyAskellHost(apiBaseUrl: string): AskellEnv {
49
+ const normalized = normalizeBaseUrl(apiBaseUrl);
50
+ if (normalized === PRODUCTION_API_BASE_URL) {
51
+ return 'production';
52
+ }
53
+ if (normalized === SANDBOX_API_BASE_URL) {
54
+ return 'sandbox';
55
+ }
56
+ return 'custom';
57
+ }
58
+
59
+ export function resolveAskellTarget(input: {
60
+ askellEnv?: AskellOfficialEnv;
61
+ apiBaseUrl?: string;
62
+ }): { askellEnv: AskellEnv; apiBaseUrl: string } {
63
+ if (input.apiBaseUrl) {
64
+ const apiBaseUrl = normalizeBaseUrl(input.apiBaseUrl);
65
+ const classified = classifyAskellHost(apiBaseUrl);
66
+
67
+ if (input.askellEnv !== undefined) {
68
+ if (classified === 'custom') {
69
+ throw new Error(
70
+ `ASKELL_ENV=${input.askellEnv} selects an official Askell host; omit ASKELL_ENV when ASKELL_API_URL is custom (${apiBaseUrl})`,
71
+ );
72
+ }
73
+ if (classified !== input.askellEnv) {
74
+ throw new Error(
75
+ `ASKELL_ENV=${input.askellEnv} does not match ASKELL_API_URL (${apiBaseUrl}). Omit ASKELL_API_URL and let ASKELL_ENV pick the host, or omit ASKELL_ENV.`,
76
+ );
77
+ }
78
+ }
79
+
80
+ return { askellEnv: classified, apiBaseUrl };
81
+ }
82
+
83
+ const askellEnv = input.askellEnv ?? 'production';
84
+ return { askellEnv, apiBaseUrl: ASKELL_API_BASE_URLS[askellEnv] };
85
+ }
86
+
87
+ const ConfigInputSchema = z.object({
88
+ askellEnv: AskellEnvSchema.optional(),
89
+ apiBaseUrl: httpUrl.optional(),
31
90
  secretApiKey: z.string().min(1).describe('Secret (private) API key'),
32
91
  publicApiKey: z
33
92
  .string()
@@ -43,23 +102,60 @@ export const ConfigSchema = z.object({
43
102
  mutationGate: MutationGateSchema,
44
103
  });
45
104
 
46
- export type AppConfig = z.infer<typeof ConfigSchema>;
105
+ export const ConfigSchema = ConfigInputSchema.superRefine((value, ctx) => {
106
+ try {
107
+ resolveAskellTarget({
108
+ askellEnv: value.askellEnv,
109
+ apiBaseUrl: value.apiBaseUrl,
110
+ });
111
+ } catch (error) {
112
+ ctx.addIssue({
113
+ code: 'custom',
114
+ message: error instanceof Error ? error.message : String(error),
115
+ });
116
+ }
117
+ }).transform((value) => {
118
+ const target = resolveAskellTarget({
119
+ askellEnv: value.askellEnv,
120
+ apiBaseUrl: value.apiBaseUrl,
121
+ });
122
+ return {
123
+ ...value,
124
+ askellEnv: target.askellEnv,
125
+ apiBaseUrl: target.apiBaseUrl,
126
+ };
127
+ });
128
+
129
+ export type AppConfig = z.output<typeof ConfigSchema>;
47
130
 
48
131
  const CONFIG_HELP = `Askell MCP credentials missing.
49
132
 
50
- Set ASKELL_PRIVATE_API_KEY (or ASKELL_SECRET_API_KEY), optionally ASKELL_PUBLIC_API_KEY and ASKELL_API_URL:
133
+ Set ASKELL_PRIVATE_API_KEY (or ASKELL_SECRET_API_KEY), optionally ASKELL_PUBLIC_API_KEY.
134
+ Set ASKELL_ENV=production|sandbox (default production) — the server picks the host.
135
+ Keys are per host. ASKELL_API_URL is only for a custom/local API.
51
136
 
52
137
  Local dev — create .env in the project root (Bun loads it automatically):
138
+ ASKELL_ENV=sandbox
53
139
  ASKELL_PRIVATE_API_KEY=...
54
140
  ASKELL_PUBLIC_API_KEY=...
55
141
 
56
- Published package (requires Bun) — Cursor / Claude mcp.json:
142
+ Published package (requires Bun) — Cursor / Claude mcp.json (two entries if you use sandbox):
57
143
  {
58
144
  "mcpServers": {
59
- "askell": {
145
+ "askell-prod": {
146
+ "command": "bunx",
147
+ "args": ["-y", "askell-mcp"],
148
+ "env": {
149
+ "ASKELL_ENV": "production",
150
+ "ASKELL_PRIVATE_API_KEY": "...",
151
+ "ASKELL_PUBLIC_API_KEY": "..."
152
+ }
153
+ },
154
+ "askell-sandbox": {
60
155
  "command": "bunx",
61
156
  "args": ["-y", "askell-mcp"],
62
157
  "env": {
158
+ "ASKELL_ENV": "sandbox",
63
159
  "ASKELL_PRIVATE_API_KEY": "...",
64
160
  "ASKELL_PUBLIC_API_KEY": "..."
65
161
  }
@@ -75,6 +171,7 @@ function loadConfigFromEnv(): unknown {
75
171
  return undefined;
76
172
  }
77
173
 
174
+ const askellEnv = env.ASKELL_ENV?.trim() || undefined;
78
175
  const apiBaseUrl = env.ASKELL_API_URL ?? env.ASKELL_API_BASE_URL;
79
176
  const responseMaxBytes = env.ASKELL_RESPONSE_MAX_BYTES;
80
177
  const mutationGateRaw =
@@ -82,6 +179,7 @@ function loadConfigFromEnv(): unknown {
82
179
  const mutationGate = mutationGateRaw?.trim().toLowerCase() || undefined;
83
180
 
84
181
  return {
182
+ ...(askellEnv ? { askellEnv } : {}),
85
183
  ...(apiBaseUrl ? { apiBaseUrl } : {}),
86
184
  secretApiKey,
87
185
  ...(env.ASKELL_PUBLIC_API_KEY
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Overlay for Askell OpenAPI v1. Upstream swagger is wrong in a few places;
3
+ * this is the same transform as askell_client_v1 `patchAskellSpec` (without Orval).
4
+ *
5
+ * Idempotent: safe to run on an already-patched document.
6
+ */
7
+
8
+ type JsonSchema = Record<string, unknown>;
9
+ type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
10
+
11
+ type OpenApiResponse = {
12
+ description?: string;
13
+ content?: Record<string, { schema?: JsonSchema }>;
14
+ [key: string]: unknown;
15
+ };
16
+
17
+ type OpenApiOperation = {
18
+ tags?: string[];
19
+ responses?: Record<string, OpenApiResponse>;
20
+ [key: string]: unknown;
21
+ };
22
+
23
+ type OpenApiPathItem = Partial<Record<HttpMethod, OpenApiOperation>> & {
24
+ [key: string]: unknown;
25
+ };
26
+
27
+ type OpenApiDocument = {
28
+ info?: Record<string, unknown>;
29
+ tags?: Array<{ name: string; description?: string }>;
30
+ paths?: Record<string, OpenApiPathItem>;
31
+ components?: {
32
+ schemas?: Record<string, JsonSchema>;
33
+ [key: string]: unknown;
34
+ };
35
+ [key: string]: unknown;
36
+ };
37
+
38
+ export const ASKELL_V1_PATCH = 'v1-overlay-1';
39
+ export const WEBHOOK_CALLS_TAG = 'Webhook calls';
40
+
41
+ /** Success bodies missing from swagger `content` (DELETE 204 stays empty). */
42
+ const RESPONSE_BODIES = [
43
+ ['/customers/', 'post', '201', 'Customer'],
44
+ ['/customers/{customerReference}/', 'put', '200', 'Customer'],
45
+ ['/customers/{customerReference}/', 'patch', '200', 'Customer'],
46
+ ['/webhooks/', 'post', '201', 'Webhook'],
47
+ ['/webhooks/{id}/', 'patch', '200', 'Webhook'],
48
+ ] as const satisfies ReadonlyArray<
49
+ readonly [string, HttpMethod, string, string]
50
+ >;
51
+
52
+ const HTTP_METHODS = [
53
+ 'get',
54
+ 'post',
55
+ 'put',
56
+ 'patch',
57
+ 'delete',
58
+ 'head',
59
+ ] as const;
60
+
61
+ const nullableString = (maxLength?: number): JsonSchema => ({
62
+ type: 'string',
63
+ nullable: true,
64
+ ...(maxLength === undefined ? {} : { maxLength }),
65
+ });
66
+
67
+ /** Flattened + nested address as returned by GET /customers/ and customer.* webhooks. */
68
+ const CUSTOMER_ADDRESS_SCHEMA: JsonSchema = {
69
+ type: 'object',
70
+ required: [
71
+ 'delivery_name',
72
+ 'address_1',
73
+ 'address_2',
74
+ 'address_3',
75
+ 'zip_code',
76
+ 'city',
77
+ 'country',
78
+ ],
79
+ properties: {
80
+ delivery_name: nullableString(),
81
+ address_1: nullableString(),
82
+ address_2: nullableString(),
83
+ address_3: nullableString(),
84
+ zip_code: nullableString(),
85
+ city: nullableString(),
86
+ country: nullableString(),
87
+ },
88
+ };
89
+
90
+ /**
91
+ * Askell swagger reuses CustomerCreate (write) as the GET model via allOf.
92
+ * Live GET / webhooks return extra address fields, null emails/phones, and a
93
+ * required numeric id. Keep CustomerCreate as the POST body.
94
+ */
95
+ const CUSTOMER_READ_SCHEMA: JsonSchema = {
96
+ type: 'object',
97
+ required: [
98
+ 'id',
99
+ 'first_name',
100
+ 'last_name',
101
+ 'email',
102
+ 'customer_reference',
103
+ 'phone',
104
+ 'delivery_name',
105
+ 'address_1',
106
+ 'address_2',
107
+ 'address_3',
108
+ 'zip_code',
109
+ 'city',
110
+ 'country',
111
+ 'payment_method',
112
+ 'address',
113
+ ],
114
+ properties: {
115
+ id: { type: 'integer', format: 'int64' },
116
+ first_name: { type: 'string', maxLength: 128 },
117
+ last_name: { type: 'string', maxLength: 128 },
118
+ email: nullableString(254),
119
+ phone: nullableString(32),
120
+ customer_reference: { type: 'string', maxLength: 256 },
121
+ delivery_name: nullableString(),
122
+ address_1: nullableString(),
123
+ address_2: nullableString(),
124
+ address_3: nullableString(),
125
+ zip_code: nullableString(),
126
+ city: nullableString(),
127
+ country: nullableString(),
128
+ payment_method: {
129
+ type: 'array',
130
+ items: { $ref: '#/components/schemas/PaymentMethod' },
131
+ },
132
+ address: {
133
+ allOf: [{ $ref: '#/components/schemas/CustomerAddress' }],
134
+ nullable: true,
135
+ },
136
+ },
137
+ };
138
+
139
+ function isRecord(value: unknown): value is Record<string, unknown> {
140
+ return value != null && typeof value === 'object' && !Array.isArray(value);
141
+ }
142
+
143
+ function dropWebhookCallOperations(doc: OpenApiDocument): void {
144
+ const paths = doc.paths;
145
+ if (!paths) {
146
+ return;
147
+ }
148
+
149
+ for (const path of Object.keys(paths)) {
150
+ const pathItem = paths[path];
151
+ if (!pathItem) {
152
+ continue;
153
+ }
154
+
155
+ for (const method of HTTP_METHODS) {
156
+ const operation = pathItem[method as HttpMethod];
157
+ if (operation?.tags?.includes(WEBHOOK_CALLS_TAG)) {
158
+ delete pathItem[method as HttpMethod];
159
+ }
160
+ }
161
+
162
+ const remaining = HTTP_METHODS.some(
163
+ (method) => pathItem[method as HttpMethod] != null,
164
+ );
165
+ if (!remaining) {
166
+ delete paths[path];
167
+ }
168
+ }
169
+
170
+ if (doc.tags) {
171
+ doc.tags = doc.tags.filter((tag) => tag.name !== WEBHOOK_CALLS_TAG);
172
+ }
173
+ }
174
+
175
+ function ensureResponseBody(
176
+ doc: OpenApiDocument,
177
+ path: string,
178
+ method: HttpMethod,
179
+ status: string,
180
+ schemaName: string,
181
+ ): void {
182
+ const pathItem = doc.paths?.[path];
183
+ const operation = pathItem?.[method];
184
+ const response = operation?.responses?.[status];
185
+ const label = `${method.toUpperCase()} ${path} ${status}`;
186
+
187
+ if (!pathItem || !operation || !response) {
188
+ throw new Error(`Askell spec missing ${label}`);
189
+ }
190
+
191
+ if (response.content?.['application/json']?.schema) {
192
+ return;
193
+ }
194
+
195
+ response.content = {
196
+ ...response.content,
197
+ 'application/json': {
198
+ schema: { $ref: `#/components/schemas/${schemaName}` },
199
+ },
200
+ };
201
+ }
202
+
203
+ export function patchAskellV1Spec(spec: unknown): OpenApiDocument {
204
+ if (!isRecord(spec)) {
205
+ throw new Error('Askell v1 spec is not an object');
206
+ }
207
+
208
+ const doc = structuredClone(spec) as OpenApiDocument;
209
+ const schemas = doc.components?.schemas;
210
+ if (!schemas?.Customer) {
211
+ throw new Error('Askell spec missing components.schemas.Customer');
212
+ }
213
+ if (!schemas.Webhook) {
214
+ throw new Error('Askell spec missing components.schemas.Webhook');
215
+ }
216
+
217
+ dropWebhookCallOperations(doc);
218
+
219
+ doc.components = {
220
+ ...doc.components,
221
+ schemas: {
222
+ ...schemas,
223
+ CustomerAddress: CUSTOMER_ADDRESS_SCHEMA,
224
+ Customer: CUSTOMER_READ_SCHEMA,
225
+ },
226
+ };
227
+
228
+ for (const [path, method, status, schemaName] of RESPONSE_BODIES) {
229
+ ensureResponseBody(doc, path, method, status, schemaName);
230
+ }
231
+
232
+ doc.info = {
233
+ ...doc.info,
234
+ 'x-askell-mcp-patched': ASKELL_V1_PATCH,
235
+ };
236
+
237
+ return doc;
238
+ }
@@ -11,11 +11,11 @@ Headers:
11
11
  - Hook-Event: event type (\`subscription.renewed\`, \`payment.changed\`, or a family wildcard \`subscription.*\`)
12
12
  - Hook-API-Version: \`v1\` for plan/subscription/customer/payment/checkout, \`v2\` for subscription_contract / billing_run
13
13
 
14
- ## Body shape (OpenAPI is wrong here)
14
+ ## Body shape (not in OpenAPI)
15
15
 
16
16
  JSON body **is the event object**. It is **not** \`{ event, data }\`.
17
17
 
18
- Ignore \`POST /your-webhook-url/\` in the bundled v1 spec — its requestBody (\`SubscriptionMultiLite\`: \`{ customer, subscriptions[] }\`) does not match live webhooks.
18
+ Upstream swagger used to document a dummy \`POST /your-webhook-url/\` with \`SubscriptionMultiLite\` (\`{ customer, subscriptions[] }\`). \`sync-specs\` strips that path. Inbound payloads are still undocumented in OpenAPI — this resource is the overlay.
19
19
 
20
20
  Rare historical payloads used \`{ event, data, ref?, sender? }\`. If both \`event\` and \`data\` are objects, use \`data\`.
21
21
 
package/src/server.ts CHANGED
@@ -1,13 +1,34 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
 
3
3
  import { AskellClient } from './client/askell-client.ts';
4
- import type { AppConfig } from './config.ts';
4
+ import {
5
+ PRODUCTION_API_BASE_URL,
6
+ SANDBOX_API_BASE_URL,
7
+ normalizeBaseUrl,
8
+ type AppConfig,
9
+ } from './config.ts';
5
10
  import { registerResources } from './resources/register.ts';
6
11
  import { registerAnalysisTools } from './tools/analysis.ts';
7
12
  import { registerCallTools } from './tools/call.ts';
8
13
  import { registerDiscoveryTools } from './tools/discovery.ts';
14
+ import { PACKAGE_VERSION } from './version.ts';
9
15
 
10
- const SERVER_INSTRUCTIONS = `Askell MCP server for payment and subscription operations.
16
+ export function buildServerInstructions(config: AppConfig): string {
17
+ const apiBase = normalizeBaseUrl(config.apiBaseUrl);
18
+ const envLine =
19
+ config.askellEnv === 'custom'
20
+ ? `This instance: custom API base ${apiBase} (ASKELL_API_URL override)`
21
+ : `This instance: ${config.askellEnv} (${apiBase})`;
22
+
23
+ return `Askell MCP server for payment and subscription operations.
24
+
25
+ ${envLine}
26
+ Official hosts (picked by ASKELL_ENV=production|sandbox; do not pass the URL):
27
+ - production: ${PRODUCTION_API_BASE_URL}
28
+ - sandbox (isolated tenant, separate API keys): ${SANDBOX_API_BASE_URL}
29
+ v1 and v2 share that base (v2 paths start with /v2/). Keys belong to one host — do not reuse production keys on sandbox or the reverse.
30
+ Áskell Test Gateway is a payment acquirer on either host, not a separate API host.
31
+ If both askell-prod and askell-sandbox MCP servers are connected, pick the instance whose environment matches the intended tenant.
11
32
 
12
33
  Workflow:
13
34
  1. Use askell_list_operations and askell_describe_operation to discover endpoints, parameters, and auth requirements.
@@ -46,16 +67,17 @@ Safety:
46
67
 
47
68
  Resources:
48
69
  - askell://spec/v1 and askell://spec/v2 — bundled OpenAPI
49
- - askell://docs/webhook-events — inbound webhook payloads (ignore OpenAPI /your-webhook-url/), HMAC-SHA512, /webhooks/ hmac_secret`;
70
+ - askell://docs/webhook-events — inbound webhook payloads (not in OpenAPI; dummy /your-webhook-url/ is stripped on sync), HMAC-SHA512, /webhooks/ hmac_secret`;
71
+ }
50
72
 
51
73
  export function createServer(config: AppConfig): McpServer {
52
74
  const server = new McpServer(
53
75
  {
54
76
  name: 'askell-mcp',
55
- version: '0.3.0',
77
+ version: PACKAGE_VERSION,
56
78
  },
57
79
  {
58
- instructions: SERVER_INSTRUCTIONS,
80
+ instructions: buildServerInstructions(config),
59
81
  },
60
82
  );
61
83
 
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ import packageJson from '../package.json' with { type: 'json' };
2
+
3
+ export const PACKAGE_VERSION: string = packageJson.version;