@aistupidlevel/mcp 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +223 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +61 -0
- package/dist/data-api.d.ts +76 -0
- package/dist/data-api.js +216 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/format.d.ts +20 -0
- package/dist/format.js +47 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/router-api.d.ts +33 -0
- package/dist/router-api.js +131 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +514 -0
- package/package.json +33 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Studio Platforms (AI Stupid Level)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,224 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aistupidlevel/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for [AI Stupid Level](https://aistupidlevel.info) (ASL).
|
|
4
|
+
It lets Claude, Cursor, VS Code or any other MCP client read ASL's LLM leaderboard, score history,
|
|
5
|
+
drift status, incidents and provider status. With a router key it can also ask the
|
|
6
|
+
[ASL Smart Router](https://aistupidlevel.info/router) which model it would pick for a prompt, or
|
|
7
|
+
send the prompt through the router.
|
|
8
|
+
|
|
9
|
+
Ask your assistant things like:
|
|
10
|
+
|
|
11
|
+
- "Which model is best for coding right now, according to AI Stupid Level?"
|
|
12
|
+
- "Has Claude Opus 5.5 got worse this week?"
|
|
13
|
+
- "Were there any provider incidents in the last few days?"
|
|
14
|
+
- "Which model would the Smart Router pick for this prompt, and why?"
|
|
15
|
+
|
|
16
|
+
It runs locally over stdio and needs Node.js 18 or later.
|
|
17
|
+
|
|
18
|
+
## Keys
|
|
19
|
+
|
|
20
|
+
The tools use two different keys, and you only need the one for the tools you want.
|
|
21
|
+
|
|
22
|
+
| Key | Used by | Where to get it |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `ASL_DATA_KEY` (`asl_live_…`) | leaderboard, model, history, drift, incidents, providers, recommendations, global index | <https://aistupidlevel.info/account/data-keys> (free tier available) |
|
|
25
|
+
| `ASL_ROUTER_KEY` (`aism_…`) | `preview_route`, `ask_router`, router usage | <https://aistupidlevel.info/router/keys>, plus at least one provider key in your account |
|
|
26
|
+
|
|
27
|
+
A tool whose key is missing does not fail the server. It answers with a message that says which
|
|
28
|
+
key it needs and where to create it.
|
|
29
|
+
|
|
30
|
+
`ASL_API_KEY` is also accepted: it is used as the data key if it starts with `asl_`, and as the router
|
|
31
|
+
key if it starts with `aism_`.
|
|
32
|
+
|
|
33
|
+
### Free-tier limits
|
|
34
|
+
|
|
35
|
+
A free Data API key allows **10 requests a day and 1 a minute**. Paid plans allow 10,000 a day or more
|
|
36
|
+
([pricing](https://aistupidlevel.info/pricing)). The free tier is meant for trying the API, so the
|
|
37
|
+
server is careful with it:
|
|
38
|
+
|
|
39
|
+
- Every answer is cached in memory for 15 minutes (`ASL_CACHE_TTL`). Asking the same question again,
|
|
40
|
+
or for another vendor or a shorter list from the same leaderboard, makes no request, and model
|
|
41
|
+
names are looked up in the cached leaderboard.
|
|
42
|
+
- One tool call makes at most one Data API request. The exception is a model given by name before
|
|
43
|
+
any leaderboard has been fetched, which takes one extra request to look up the model ids.
|
|
44
|
+
- Once the daily quota is used up, the server stops sending requests until 00:00 UTC. A request
|
|
45
|
+
refused for quota still counts against it.
|
|
46
|
+
- If a fresh answer cannot be fetched, the server returns the last cached answer (up to 24 hours old)
|
|
47
|
+
and says how old it is.
|
|
48
|
+
- Every answer ends with the number of requests left today.
|
|
49
|
+
|
|
50
|
+
On a free key, calling several different data tools within a minute hits the per-minute limit. The
|
|
51
|
+
tool then says when to try again. If the wait is 15 seconds or less (`ASL_MAX_WAIT`), the server waits
|
|
52
|
+
and retries once by itself.
|
|
53
|
+
|
|
54
|
+
The newest-run view (`period: "latest"`) needs a paid key. Free keys read the 24-hour, 7-day and
|
|
55
|
+
30-day views.
|
|
56
|
+
|
|
57
|
+
`preview_route` and `get_usage` are not counted against your Smart Router plan. `ask_router` is a
|
|
58
|
+
real routed request: the router calls the model with your own provider key, so the provider bills
|
|
59
|
+
you and the request counts against your plan. Pass `sandbox: true`, or set `ASL_SANDBOX=true`, to get
|
|
60
|
+
mock answers from the real routing at no cost.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
The package runs with `npx`, so there is nothing to install first. Put your keys in place of the
|
|
65
|
+
placeholders, and leave out any key you do not have.
|
|
66
|
+
|
|
67
|
+
### Claude Code
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
claude mcp add aistupidlevel -e ASL_DATA_KEY=asl_live_... -e ASL_ROUTER_KEY=aism_... -- npx -y @aistupidlevel/mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Add `-s user` to make it available in every project.
|
|
74
|
+
|
|
75
|
+
### Claude Desktop
|
|
76
|
+
|
|
77
|
+
Settings → Developer → Edit Config, then add the server to `claude_desktop_config.json`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"aistupidlevel": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["-y", "@aistupidlevel/mcp"],
|
|
85
|
+
"env": {
|
|
86
|
+
"ASL_DATA_KEY": "asl_live_...",
|
|
87
|
+
"ASL_ROUTER_KEY": "aism_..."
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Restart Claude Desktop afterwards.
|
|
95
|
+
|
|
96
|
+
### Cursor
|
|
97
|
+
|
|
98
|
+
Add the following to `~/.cursor/mcp.json` (for all projects) or `.cursor/mcp.json` (for one project):
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"aistupidlevel": {
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["-y", "@aistupidlevel/mcp"],
|
|
106
|
+
"env": {
|
|
107
|
+
"ASL_DATA_KEY": "asl_live_...",
|
|
108
|
+
"ASL_ROUTER_KEY": "aism_..."
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### VS Code
|
|
116
|
+
|
|
117
|
+
Add the following to `.vscode/mcp.json` (or to your user configuration with **MCP: Open User
|
|
118
|
+
Configuration**). VS Code prompts for the keys and stores them securely:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"inputs": [
|
|
123
|
+
{ "type": "promptString", "id": "asl-data-key", "description": "AI Stupid Level Data API key (asl_live_...)", "password": true },
|
|
124
|
+
{ "type": "promptString", "id": "asl-router-key", "description": "AI Stupid Level router key (aism_...)", "password": true }
|
|
125
|
+
],
|
|
126
|
+
"servers": {
|
|
127
|
+
"aistupidlevel": {
|
|
128
|
+
"type": "stdio",
|
|
129
|
+
"command": "npx",
|
|
130
|
+
"args": ["-y", "@aistupidlevel/mcp"],
|
|
131
|
+
"env": {
|
|
132
|
+
"ASL_DATA_KEY": "${input:asl-data-key}",
|
|
133
|
+
"ASL_ROUTER_KEY": "${input:asl-router-key}"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Other clients
|
|
141
|
+
|
|
142
|
+
Any client that can start a stdio server can run it: the command is `npx -y @aistupidlevel/mcp`,
|
|
143
|
+
and the keys go in the environment variables listed under [Configuration](#configuration).
|
|
144
|
+
|
|
145
|
+
## Tools
|
|
146
|
+
|
|
147
|
+
All tools except `ask_router` are read-only.
|
|
148
|
+
|
|
149
|
+
| Tool | Key | What it returns |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `get_leaderboard` | data | Tracked models ranked by combined, coding, reasoning or tool-use score, or by score per list-price dollar. Each row has the score, trend, status and stability, and the model ids the other tools take. Filter by vendor; choose the view (24h, 7d, 1m, or latest on a paid key). |
|
|
152
|
+
| `get_model` | data | One model's details: vendor, version, notes, tool-calling support and days tracked. |
|
|
153
|
+
| `get_model_history` | data | One model's score for each run in the window, with a summary and the per-axis results of the newest run. |
|
|
154
|
+
| `get_drift_status` | data | One model's drift signature: regime, current score against its baseline, the change-point detector for each suite, the last significant change, and which axes moved. |
|
|
155
|
+
| `get_incidents` | data | Recent incidents (provider-wide and per model) with severity, the models affected, when each was detected and when it was resolved. |
|
|
156
|
+
| `get_provider_status` | data | Each provider's API status from ASL's health probes, with the probe's response time and the last error. |
|
|
157
|
+
| `get_recommendations` | data | ASL's current picks (best for coding, most reliable, fastest, best value, poor value, avoid now), each with its reason and the evidence behind it. |
|
|
158
|
+
| `get_global_index` | data | ASL's global index across the tracked models, now and over the last 24 hours. |
|
|
159
|
+
| `preview_route` | router | The model the Smart Router would pick for a prompt, its score and estimated cost, the reason, and the fallback chain. Nothing is called or counted. |
|
|
160
|
+
| `ask_router` | router | Sends a prompt through the Smart Router and returns the answer, the model and provider that answered, why it was chosen, the latency and the tokens used. Set `sandbox: true` for a mock answer at no cost. |
|
|
161
|
+
| `get_usage` | either | This month's router usage and budget, and the Data API requests left today. |
|
|
162
|
+
|
|
163
|
+
Models can be given by numeric id, by name (`claude-opus-5-5`) or by display name (`Claude Opus 5.5`).
|
|
164
|
+
|
|
165
|
+
`preview_route` and `ask_router` take a routing `mode` (`auto`, `auto-best`, `auto-coding`,
|
|
166
|
+
`auto-reasoning`, `auto-tooling`, `auto-task`, `auto-value`, `auto-value-coding`,
|
|
167
|
+
`auto-value-reasoning`, `auto-value-tooling`, `auto-fastest-quality`, `auto-consistent`,
|
|
168
|
+
`auto-fastest`, `auto-cheapest`), or `pin_model` to name a model directly. The
|
|
169
|
+
[router documentation](https://aistupidlevel.info/router/docs#strategies) explains each mode.
|
|
170
|
+
|
|
171
|
+
The server also provides one prompt, `choose_model`, which walks the assistant through choosing a
|
|
172
|
+
model for a task you describe.
|
|
173
|
+
|
|
174
|
+
Answers from the Data API end with a source line. The data is licensed with attribution required:
|
|
175
|
+
cite aistupidlevel.info when you republish it.
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
| Variable | Default | Meaning |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `ASL_DATA_KEY` | none | Data API key (`asl_live_…`) |
|
|
182
|
+
| `ASL_ROUTER_KEY` | none | Smart Router key (`aism_…`) |
|
|
183
|
+
| `ASL_SANDBOX` | `false` | `true` puts every Smart Router call in sandbox mode: real routing, mock answer, nothing spent or counted |
|
|
184
|
+
| `ASL_CACHE_TTL` | `900` | Seconds a Data API answer is reused |
|
|
185
|
+
| `ASL_MAX_WAIT` | `15` | When the per-minute limit asks for a wait this short (seconds) or shorter, wait and retry once. `0` turns this off |
|
|
186
|
+
| `ASL_TIMEOUT_MS` | `300000` | Timeout for a Smart Router request |
|
|
187
|
+
|
|
188
|
+
Keys are never logged. The server writes its diagnostics to stderr, because stdout carries the MCP
|
|
189
|
+
protocol.
|
|
190
|
+
|
|
191
|
+
## Use as a library
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { createServer } from '@aistupidlevel/mcp';
|
|
195
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
196
|
+
|
|
197
|
+
const server = createServer({ dataKey: process.env.ASL_DATA_KEY, cacheTtlSeconds: 600 });
|
|
198
|
+
await server.connect(new StdioServerTransport());
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`createServer` accepts the same settings as the environment variables, plus a custom `fetch`.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
npm ci && npm run build && npm test
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The unit tests start the server in-process, connect an MCP client to it over an in-memory transport,
|
|
210
|
+
and replay real API responses from `test/fixtures`. The live tests are skipped unless keys are set:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
ASL_ROUTER_KEY=aism_... npm test # Smart Router, sandbox only: nothing is spent or counted
|
|
214
|
+
ASL_DATA_KEY=asl_live_... npm test # Data API: one request
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Links
|
|
218
|
+
|
|
219
|
+
- [Data API documentation](https://aistupidlevel.info/api-docs)
|
|
220
|
+
- [Smart Router documentation](https://aistupidlevel.info/router/docs)
|
|
221
|
+
- [TypeScript SDK for the router](https://www.npmjs.com/package/@aistupidlevel/router)
|
|
222
|
+
- [Leaderboard](https://aistupidlevel.info)
|
|
223
|
+
|
|
224
|
+
MIT licence.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aistupidlevel-mcp: the AI Stupid Level MCP server over stdio.
|
|
4
|
+
* stdout carries the protocol, so every diagnostic goes to stderr, and keys are never printed.
|
|
5
|
+
*/
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { createServer } from './server.js';
|
|
8
|
+
import { VERSION, resolveConfig } from './config.js';
|
|
9
|
+
const HELP = `aistupidlevel-mcp ${VERSION}: MCP server for AI Stupid Level (https://aistupidlevel.info)
|
|
10
|
+
|
|
11
|
+
Usage: npx -y @aistupidlevel/mcp (speaks MCP over stdio; add it to your MCP client)
|
|
12
|
+
|
|
13
|
+
Environment:
|
|
14
|
+
ASL_DATA_KEY Data API key (asl_live_...) for rankings, history, drift, incidents.
|
|
15
|
+
Free key: https://aistupidlevel.info/account/data-keys (10 requests/day, 1/minute)
|
|
16
|
+
ASL_ROUTER_KEY Smart Router key (aism_...) for preview_route, ask_router and usage.
|
|
17
|
+
https://aistupidlevel.info/router/keys
|
|
18
|
+
ASL_SANDBOX true: every Smart Router call returns a mock answer (nothing spent)
|
|
19
|
+
ASL_CACHE_TTL seconds a Data API answer is reused (default 900)
|
|
20
|
+
ASL_MAX_WAIT wait and retry once when the per-minute limit asks for at most this many seconds (default 15)
|
|
21
|
+
ASL_TIMEOUT_MS Smart Router request timeout in ms (default 300000)
|
|
22
|
+
`;
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
25
|
+
process.stderr.write(HELP);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
29
|
+
process.stderr.write(`${VERSION}\n`);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
const log = (m) => process.stderr.write(`[aistupidlevel-mcp] ${m}\n`);
|
|
33
|
+
const cfg = resolveConfig();
|
|
34
|
+
const server = createServer({ log });
|
|
35
|
+
const transport = new StdioServerTransport();
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
log(`v${VERSION} running on stdio; Data API key ${cfg.dataKey ? 'set' : 'not set (ASL_DATA_KEY)'}, router key ${cfg.routerKey ? 'set' : 'not set (ASL_ROUTER_KEY)'}${cfg.forceSandbox ? ', sandbox forced' : ''}.`);
|
|
38
|
+
const shutdown = () => { server.close().finally(() => process.exit(0)); };
|
|
39
|
+
process.on('SIGINT', shutdown);
|
|
40
|
+
process.on('SIGTERM', shutdown);
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Settings, read from the environment by default. Keys are never logged or echoed back. */
|
|
2
|
+
export declare const VERSION = "0.1.0";
|
|
3
|
+
export declare const DEFAULT_DATA_BASE_URL = "https://aistupidlevel.info/api/v1";
|
|
4
|
+
export declare const DEFAULT_ROUTER_BASE_URL = "https://aistupidlevel.info/v1";
|
|
5
|
+
export declare const LINKS: {
|
|
6
|
+
readonly dataKeys: "https://aistupidlevel.info/account/data-keys";
|
|
7
|
+
readonly dataDocs: "https://aistupidlevel.info/api-docs";
|
|
8
|
+
readonly routerKeys: "https://aistupidlevel.info/router/keys";
|
|
9
|
+
readonly routerDocs: "https://aistupidlevel.info/router/docs";
|
|
10
|
+
readonly providerKeys: "https://aistupidlevel.info/router/providers";
|
|
11
|
+
readonly pricing: "https://aistupidlevel.info/pricing";
|
|
12
|
+
readonly billing: "https://aistupidlevel.info/account/billing";
|
|
13
|
+
readonly site: "https://aistupidlevel.info";
|
|
14
|
+
};
|
|
15
|
+
export interface ServerOptions {
|
|
16
|
+
/** Data API key (asl_live_…). Default: ASL_DATA_KEY, or ASL_API_KEY when it is an asl_ key. */
|
|
17
|
+
dataKey?: string;
|
|
18
|
+
/** Smart Router key (aism_…). Default: ASL_ROUTER_KEY, or ASL_API_KEY when it is an aism_ key. */
|
|
19
|
+
routerKey?: string;
|
|
20
|
+
/** Default: ASL_DATA_BASE_URL or https://aistupidlevel.info/api/v1 */
|
|
21
|
+
dataBaseURL?: string;
|
|
22
|
+
/** Default: ASL_BASE_URL or https://aistupidlevel.info/v1 */
|
|
23
|
+
routerBaseURL?: string;
|
|
24
|
+
/** How long a Data API response is reused, in seconds. Default: ASL_CACHE_TTL or 900 (15 minutes). */
|
|
25
|
+
cacheTtlSeconds?: number;
|
|
26
|
+
/**
|
|
27
|
+
* When the Data API's per-minute limit answers 429 with a Retry-After of at most this many
|
|
28
|
+
* seconds, wait and retry once. Default: ASL_MAX_WAIT or 15. 0 never waits.
|
|
29
|
+
*/
|
|
30
|
+
maxWaitSeconds?: number;
|
|
31
|
+
/** Force sandbox mode for every Smart Router call (mock answers, nothing spent). Default: ASL_SANDBOX. */
|
|
32
|
+
forceSandbox?: boolean;
|
|
33
|
+
/** Per-request timeout for Smart Router calls, ms. Default: ASL_TIMEOUT_MS or 300000. */
|
|
34
|
+
routerTimeoutMs?: number;
|
|
35
|
+
/** A fetch implementation (tests, proxies). Default: the global fetch. */
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
/** Read settings from this object instead of process.env. */
|
|
38
|
+
env?: Record<string, string | undefined>;
|
|
39
|
+
/** Where diagnostics go. Default: stderr (stdout belongs to the stdio transport). */
|
|
40
|
+
log?: (message: string) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface ResolvedConfig {
|
|
43
|
+
dataKey: string | null;
|
|
44
|
+
routerKey: string | null;
|
|
45
|
+
dataBaseURL: string;
|
|
46
|
+
routerBaseURL: string;
|
|
47
|
+
cacheTtlMs: number;
|
|
48
|
+
maxWaitMs: number;
|
|
49
|
+
forceSandbox: boolean;
|
|
50
|
+
routerTimeoutMs: number;
|
|
51
|
+
fetch: typeof fetch;
|
|
52
|
+
log: (message: string) => void;
|
|
53
|
+
/** Problems with the configuration, reported by the tools that are affected. */
|
|
54
|
+
warnings: {
|
|
55
|
+
data?: string;
|
|
56
|
+
router?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export declare function resolveConfig(o?: ServerOptions): ResolvedConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Settings, read from the environment by default. Keys are never logged or echoed back. */
|
|
2
|
+
export const VERSION = '0.1.0';
|
|
3
|
+
export const DEFAULT_DATA_BASE_URL = 'https://aistupidlevel.info/api/v1';
|
|
4
|
+
export const DEFAULT_ROUTER_BASE_URL = 'https://aistupidlevel.info/v1';
|
|
5
|
+
export const LINKS = {
|
|
6
|
+
dataKeys: 'https://aistupidlevel.info/account/data-keys',
|
|
7
|
+
dataDocs: 'https://aistupidlevel.info/api-docs',
|
|
8
|
+
routerKeys: 'https://aistupidlevel.info/router/keys',
|
|
9
|
+
routerDocs: 'https://aistupidlevel.info/router/docs',
|
|
10
|
+
providerKeys: 'https://aistupidlevel.info/router/providers',
|
|
11
|
+
pricing: 'https://aistupidlevel.info/pricing',
|
|
12
|
+
billing: 'https://aistupidlevel.info/account/billing',
|
|
13
|
+
site: 'https://aistupidlevel.info',
|
|
14
|
+
};
|
|
15
|
+
const truthy = (v) => !!v && /^(1|true|yes|on)$/i.test(v.trim());
|
|
16
|
+
const num = (v, fallback) => {
|
|
17
|
+
if (v === undefined || v.trim() === '')
|
|
18
|
+
return fallback;
|
|
19
|
+
const n = Number(v);
|
|
20
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
21
|
+
};
|
|
22
|
+
const clean = (v) => (v && v.trim() ? v.trim() : null);
|
|
23
|
+
export function resolveConfig(o = {}) {
|
|
24
|
+
const env = o.env ?? globalThis.process?.env ?? {};
|
|
25
|
+
const generic = clean(env.ASL_API_KEY);
|
|
26
|
+
let dataKey = clean(o.dataKey) ?? clean(env.ASL_DATA_KEY) ?? (generic?.startsWith('asl_') ? generic : null);
|
|
27
|
+
let routerKey = clean(o.routerKey) ?? clean(env.ASL_ROUTER_KEY) ?? (generic?.startsWith('aism_') ? generic : null);
|
|
28
|
+
const warnings = {};
|
|
29
|
+
// The two products use different keys; a swapped pair is the most likely set-up mistake.
|
|
30
|
+
if (dataKey?.startsWith('aism_')) {
|
|
31
|
+
warnings.data = 'ASL_DATA_KEY holds a Smart Router key (aism_…). The Data API needs a Data API key (asl_live_…).';
|
|
32
|
+
if (!routerKey)
|
|
33
|
+
routerKey = dataKey;
|
|
34
|
+
dataKey = null;
|
|
35
|
+
}
|
|
36
|
+
if (routerKey?.startsWith('asl_')) {
|
|
37
|
+
warnings.router = 'ASL_ROUTER_KEY holds a Data API key (asl_…). The Smart Router needs a router key (aism_…).';
|
|
38
|
+
if (!dataKey)
|
|
39
|
+
dataKey = routerKey;
|
|
40
|
+
routerKey = null;
|
|
41
|
+
}
|
|
42
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
43
|
+
if (typeof f !== 'function')
|
|
44
|
+
throw new Error('No fetch implementation is available: use Node 18 or later.');
|
|
45
|
+
return {
|
|
46
|
+
dataKey,
|
|
47
|
+
routerKey,
|
|
48
|
+
dataBaseURL: (o.dataBaseURL ?? clean(env.ASL_DATA_BASE_URL) ?? DEFAULT_DATA_BASE_URL).replace(/\/+$/, ''),
|
|
49
|
+
routerBaseURL: (o.routerBaseURL ?? clean(env.ASL_BASE_URL) ?? DEFAULT_ROUTER_BASE_URL).replace(/\/+$/, ''),
|
|
50
|
+
cacheTtlMs: (o.cacheTtlSeconds ?? num(env.ASL_CACHE_TTL, 900)) * 1000,
|
|
51
|
+
maxWaitMs: (o.maxWaitSeconds ?? num(env.ASL_MAX_WAIT, 15)) * 1000,
|
|
52
|
+
forceSandbox: o.forceSandbox ?? truthy(env.ASL_SANDBOX),
|
|
53
|
+
routerTimeoutMs: o.routerTimeoutMs ?? num(env.ASL_TIMEOUT_MS, 300_000),
|
|
54
|
+
fetch: o.fetch ? f : f.bind(globalThis),
|
|
55
|
+
log: o.log ?? ((m) => { try {
|
|
56
|
+
globalThis.process?.stderr?.write(`[aistupidlevel-mcp] ${m}\n`);
|
|
57
|
+
}
|
|
58
|
+
catch { /* ignore */ } }),
|
|
59
|
+
warnings,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the ASL Data API (https://aistupidlevel.info/api/v1).
|
|
3
|
+
*
|
|
4
|
+
* A free key allows 10 requests a day and 1 a minute, so every response is cached in memory
|
|
5
|
+
* (default 15 minutes, keyed by URL), identical requests in flight are shared, refusals that will
|
|
6
|
+
* not change (403, 404) are cached too, and once the daily quota is spent no further request is
|
|
7
|
+
* sent until it resets. When a request cannot be made, an older cached answer is served with a
|
|
8
|
+
* note saying how old it is.
|
|
9
|
+
*/
|
|
10
|
+
import { type ResolvedConfig } from './config.js';
|
|
11
|
+
import { ToolError } from './errors.js';
|
|
12
|
+
/** The envelope every Data API response is wrapped in. */
|
|
13
|
+
export interface Envelope<T = unknown> {
|
|
14
|
+
success: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
generated_at?: string;
|
|
17
|
+
license?: string;
|
|
18
|
+
period?: string;
|
|
19
|
+
sort_by?: string;
|
|
20
|
+
count?: number;
|
|
21
|
+
limit?: number;
|
|
22
|
+
data: T;
|
|
23
|
+
[extra: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface DataResult<T = unknown> {
|
|
26
|
+
body: Envelope<T>;
|
|
27
|
+
/** When this answer was fetched from the API. */
|
|
28
|
+
fetchedAt: number;
|
|
29
|
+
cached: boolean;
|
|
30
|
+
/** Set when an expired answer is served because a fresh one could not be fetched. */
|
|
31
|
+
staleReason?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Quota {
|
|
34
|
+
limit: number;
|
|
35
|
+
remaining: number;
|
|
36
|
+
/** Epoch ms of the next reset (00:00 UTC). */
|
|
37
|
+
resetAt: number | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ModelRef {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
displayName: string | null;
|
|
43
|
+
}
|
|
44
|
+
export declare class DataApi {
|
|
45
|
+
private readonly cfg;
|
|
46
|
+
private readonly cache;
|
|
47
|
+
private readonly inflight;
|
|
48
|
+
private readonly models;
|
|
49
|
+
private modelsSeenAt;
|
|
50
|
+
/** The newest leaderboard (/models) answer, for showing a model's row without a request. */
|
|
51
|
+
lastBoard: Envelope | null;
|
|
52
|
+
private dailyBlockedUntil;
|
|
53
|
+
private dailyBlockedMessage;
|
|
54
|
+
private lastQuota;
|
|
55
|
+
/** Network requests actually sent (for tests and diagnostics). */
|
|
56
|
+
requestsSent: number;
|
|
57
|
+
constructor(cfg: ResolvedConfig);
|
|
58
|
+
get hasKey(): boolean;
|
|
59
|
+
/** The quota from the newest response, or null once its day is over. */
|
|
60
|
+
get quota(): Quota | null;
|
|
61
|
+
/** The error a data tool returns when no Data API key is configured. */
|
|
62
|
+
missingKeyError(): ToolError;
|
|
63
|
+
/**
|
|
64
|
+
* GET a Data API path. `query` values that are undefined are left out, so "let the API choose"
|
|
65
|
+
* and an explicit value are cached separately, exactly as the API would answer them.
|
|
66
|
+
*/
|
|
67
|
+
get<T = unknown>(path: string, query?: Record<string, string | number | undefined>): Promise<DataResult<T>>;
|
|
68
|
+
/** Resolve a model given as a numeric id, a model name (claude-opus-5-5) or a display name. */
|
|
69
|
+
resolveModel(input: string): Promise<ModelRef>;
|
|
70
|
+
/** Every model the last /models answer listed (no request is made). */
|
|
71
|
+
knownModel(id: number): ModelRef | undefined;
|
|
72
|
+
private learnModels;
|
|
73
|
+
private staleOr;
|
|
74
|
+
private readQuota;
|
|
75
|
+
private fetchUrl;
|
|
76
|
+
}
|