askell-mcp 0.3.1 → 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.1",
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",
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
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.
@@ -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: '0.3.1',
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;