askell-mcp 0.3.1 → 0.4.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 +25 -7
- package/mcp.json.example +14 -4
- package/package.json +3 -1
- package/src/config.ts +106 -8
- package/src/index.ts +3 -1
- package/src/server.ts +26 -4
- 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": {
|
|
30
|
+
"command": "bunx",
|
|
31
|
+
"args": ["-y", "askell-mcp@latest"],
|
|
32
|
+
"env": {
|
|
33
|
+
"ASKELL_ENV": "production",
|
|
34
|
+
"ASKELL_PRIVATE_API_KEY": "your_production_secret_api_key"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"askell-sandbox": {
|
|
28
38
|
"command": "bunx",
|
|
29
|
-
"args": ["-y", "askell-mcp"],
|
|
39
|
+
"args": ["-y", "askell-mcp@latest"],
|
|
30
40
|
"env": {
|
|
31
|
-
"
|
|
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
|
-
"args": ["-y", "askell-mcp"],
|
|
5
|
+
"args": ["-y", "askell-mcp@latest"],
|
|
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@latest"],
|
|
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.1",
|
|
4
4
|
"description": "MCP server for the Askell payment and subscription API (Bun + stdio)",
|
|
5
5
|
"author": "Neschadin Oleksandr",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"dev": "bun run src/index.ts",
|
|
46
|
+
"dev:prod": "bun --no-env-file --env-file=.env run src/index.ts",
|
|
47
|
+
"dev:sandbox": "bun --no-env-file --env-file=.env.sandbox run src/index.ts",
|
|
46
48
|
"start": "bun run bin/askell-mcp",
|
|
47
49
|
"test": "bun test",
|
|
48
50
|
"smoke": "bun run scripts/smoke-test.ts",
|
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@latest"],
|
|
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
|
-
"args": ["-y", "askell-mcp"],
|
|
156
|
+
"args": ["-y", "askell-mcp@latest"],
|
|
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
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,9 @@ try {
|
|
|
8
8
|
|
|
9
9
|
void serveStdio(() => createServer(config));
|
|
10
10
|
|
|
11
|
-
console.error(
|
|
11
|
+
console.error(
|
|
12
|
+
`askell-mcp running on stdio (${config.askellEnv} ${config.apiBaseUrl})`,
|
|
13
|
+
);
|
|
12
14
|
} catch (error) {
|
|
13
15
|
const message = error instanceof Error ? error.message : String(error);
|
|
14
16
|
console.error(`askell-mcp failed to start: ${message}`);
|
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.
|
|
@@ -47,15 +68,16 @@ Safety:
|
|
|
47
68
|
Resources:
|
|
48
69
|
- askell://spec/v1 and askell://spec/v2 — bundled OpenAPI
|
|
49
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