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 +24 -6
- package/mcp.json.example +13 -3
- package/package.json +2 -2
- package/spec/openapi-v1.json +170 -46
- package/src/config.ts +105 -7
- package/src/openapi/patch-v1.ts +238 -0
- package/src/resources/register.ts +2 -2
- package/src/server.ts +27 -5
- package/src/version.ts +3 -0
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
"
|
|
8
|
-
"
|
|
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
|
+
"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
|
|
68
|
+
"zod": "4.5.4"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/spec/openapi-v1.json
CHANGED
|
@@ -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
|
-
"
|
|
1968
|
-
|
|
1969
|
-
|
|
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": "
|
|
1973
|
-
"
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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 (
|
|
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
|
|
30
|
-
|
|
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
|
|
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
|
|
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 (
|
|
14
|
+
## Body shape (not in OpenAPI)
|
|
15
15
|
|
|
16
16
|
JSON body **is the event object**. It is **not** \`{ event, data }\`.
|
|
17
17
|
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
77
|
+
version: PACKAGE_VERSION,
|
|
56
78
|
},
|
|
57
79
|
{
|
|
58
|
-
instructions:
|
|
80
|
+
instructions: buildServerInstructions(config),
|
|
59
81
|
},
|
|
60
82
|
);
|
|
61
83
|
|
package/src/version.ts
ADDED