@index9/mcp 1.0.32 → 4.0.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 +16 -64
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +868 -0
- package/manifest.json +55 -0
- package/package.json +25 -42
- package/LICENSE +0 -21
- package/dist/client.d.ts +0 -5
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -39
- package/dist/config.d.ts +0 -8
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -9
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -32
- package/dist/logger.d.ts +0 -4
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -8
- package/dist/mcp.d.ts +0 -4
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js +0 -147
- package/dist/schemas.d.ts +0 -162
- package/dist/schemas.d.ts.map +0 -1
- package/dist/schemas.js +0 -208
- package/dist/tools/find_models.d.ts +0 -3
- package/dist/tools/find_models.d.ts.map +0 -1
- package/dist/tools/find_models.js +0 -4
- package/dist/tools/get_model.d.ts +0 -3
- package/dist/tools/get_model.d.ts.map +0 -1
- package/dist/tools/get_model.js +0 -4
- package/dist/tools/test_model.d.ts +0 -3
- package/dist/tools/test_model.d.ts.map +0 -1
- package/dist/tools/test_model.js +0 -5
- package/dist/types/api.d.ts +0 -107
- package/dist/types/api.d.ts.map +0 -1
- package/dist/types/api.js +0 -1
- package/dist/types/index.d.ts +0 -3
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -2
- package/dist/types/models.d.ts +0 -63
- package/dist/types/models.d.ts.map +0 -1
- package/dist/types/models.js +0 -1
- package/dist/utils/rateLimiter.d.ts +0 -7
- package/dist/utils/rateLimiter.d.ts.map +0 -1
- package/dist/utils/rateLimiter.js +0 -20
package/dist/cli.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../core/dist/constants.js
|
|
4
|
+
var API_PATHS = {
|
|
5
|
+
search: "/api/search",
|
|
6
|
+
model: "/api/model",
|
|
7
|
+
test: "/api/test"
|
|
8
|
+
};
|
|
9
|
+
var CAPABILITIES = [
|
|
10
|
+
"function_calling",
|
|
11
|
+
"structured_output",
|
|
12
|
+
"reasoning",
|
|
13
|
+
"web_search",
|
|
14
|
+
"vision",
|
|
15
|
+
"audio_input",
|
|
16
|
+
"file_input",
|
|
17
|
+
"image_generation",
|
|
18
|
+
"audio_output"
|
|
19
|
+
];
|
|
20
|
+
var OUTPUT_MODALITIES = ["text", "image", "audio"];
|
|
21
|
+
var COMMON_PROVIDERS = [
|
|
22
|
+
"openai",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"google",
|
|
25
|
+
"meta-llama",
|
|
26
|
+
"mistralai",
|
|
27
|
+
"deepseek",
|
|
28
|
+
"cohere"
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// ../core/dist/schemas/common.js
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
var ErrorResponseSchema = z.object({ error: z.string() }).strict();
|
|
34
|
+
var UserContentTextPartSchema = z.strictObject({
|
|
35
|
+
type: z.literal("text"),
|
|
36
|
+
text: z.string().trim().min(1)
|
|
37
|
+
});
|
|
38
|
+
var UserContentImagePartSchema = z.strictObject({
|
|
39
|
+
type: z.literal("image_url"),
|
|
40
|
+
imageUrl: z.strictObject({
|
|
41
|
+
url: z.string().trim().min(1),
|
|
42
|
+
detail: z.enum(["auto", "low", "high"]).optional()
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
var UserContentAudioPartSchema = z.strictObject({
|
|
46
|
+
type: z.literal("input_audio"),
|
|
47
|
+
inputAudio: z.strictObject({
|
|
48
|
+
data: z.string().trim().min(1),
|
|
49
|
+
format: z.string().trim().min(1)
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
var UserContentPartSchema = z.discriminatedUnion("type", [
|
|
53
|
+
UserContentTextPartSchema,
|
|
54
|
+
UserContentImagePartSchema,
|
|
55
|
+
UserContentAudioPartSchema
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// ../core/dist/schemas/search.js
|
|
59
|
+
import { z as z2 } from "zod";
|
|
60
|
+
|
|
61
|
+
// ../core/dist/content.js
|
|
62
|
+
var LIMITS = {
|
|
63
|
+
searchMax: 100,
|
|
64
|
+
searchDefault: 20,
|
|
65
|
+
getModelsMax: 100,
|
|
66
|
+
testModelsMax: 10
|
|
67
|
+
};
|
|
68
|
+
var MODEL_COUNT = "300+";
|
|
69
|
+
var WORKFLOW_INSTRUCTIONS = `Index9 provides 3 tools for AI model discovery, inspection, and benchmarking.
|
|
70
|
+
|
|
71
|
+
Typical workflow:
|
|
72
|
+
1. **find_models** \u2014 Discover models by semantic query or filters. Start here when the user needs to find models matching criteria.
|
|
73
|
+
2. **get_models** \u2014 Get full metadata for specific model IDs or aliases. Use after search to inspect details, or directly when the user names a specific model.
|
|
74
|
+
3. **test_model** \u2014 Run live inference, or set dryRun=true to estimate token usage/cost without running inference.
|
|
75
|
+
|
|
76
|
+
Key rules:
|
|
77
|
+
- find_models requires \`q\` when \`sortBy=relevance\` (the default). Omit \`q\` only with \`sortBy=created\` or \`sortBy=price\`.
|
|
78
|
+
- get_models accepts aliases (display names, short names) \u2014 not just full IDs.
|
|
79
|
+
- Use test_model with \`dryRun=true\` to estimate cost before live testing.
|
|
80
|
+
- test_model with \`dryRun=false\` (default) requires OPENROUTER_API_KEY and incurs real usage costs.
|
|
81
|
+
- Cursors are opaque and tied to query/sort/filters. Reuse the same query/sort/filters when paginating. \`limit\` may change between pages.`;
|
|
82
|
+
var TOOLS = {
|
|
83
|
+
find_models: {
|
|
84
|
+
name: "find_models",
|
|
85
|
+
summary: "Search and paginate AI models by semantic query or filters",
|
|
86
|
+
description: `Search and filter ${MODEL_COUNT} AI models. Returns ranked results with pricing, context windows, and capabilities.
|
|
87
|
+
|
|
88
|
+
Call this tool first to discover model IDs, unless the user provides one (format: 'provider/model-name').
|
|
89
|
+
|
|
90
|
+
IMPORTANT \u2014 Extract filters from user queries:
|
|
91
|
+
When the user mentions numeric or categorical constraints, you MUST map them to the structured filter parameters below instead of relying solely on \`q\`. The \`q\` parameter drives semantic ranking but does NOT enforce hard constraints.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
- "model with 1M context under $1" \u2192 q="model", minContext=1000000, maxPrice=1
|
|
95
|
+
- "cheap vision model from openai" \u2192 q="cheap vision model", capabilitiesAll="vision", provider="openai"
|
|
96
|
+
- "function calling under $0.50 with 128K context" \u2192 q="function calling", capabilitiesAll="function_calling", maxPrice=0.5, minContext=128000
|
|
97
|
+
- "best coding model" \u2192 q="best coding model" (no filters needed)
|
|
98
|
+
|
|
99
|
+
Convert shorthand: 1K=1000, 1M=1000000, 128K=128000. Prices are in USD per million input tokens.
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
- q: Semantic search query \u2014 use for intent/quality ranking, not for numeric constraints
|
|
103
|
+
- provider: Filter by provider(s). Comma-separated for multiple (e.g., 'openai,anthropic')
|
|
104
|
+
- minPrice, maxPrice: Price bounds in USD per million input tokens
|
|
105
|
+
- minContext: Minimum context window in tokens
|
|
106
|
+
- capabilitiesAll: Capabilities the model MUST have (AND logic). Valid: ${CAPABILITIES.join(", ")}
|
|
107
|
+
- capabilitiesAny: Capabilities where at least one must be present (OR logic)
|
|
108
|
+
- sortBy: 'relevance' (default), 'created', 'price'
|
|
109
|
+
- limit: Page size
|
|
110
|
+
- cursor: Opaque pagination cursor
|
|
111
|
+
|
|
112
|
+
Scores: Results include a 'score' field (0-100). Higher = more relevant. The best match in each page scores 100; others are scaled proportionally. Combines semantic similarity and keyword matching. Null when sorting by price or date.
|
|
113
|
+
|
|
114
|
+
Use model IDs from results with get_models for full specs or test_model for live testing.`,
|
|
115
|
+
requiresKey: false
|
|
116
|
+
},
|
|
117
|
+
get_models: {
|
|
118
|
+
name: "get_models",
|
|
119
|
+
summary: "Get full model metadata by IDs or aliases (batch, up to 100)",
|
|
120
|
+
description: `Get complete specs for a model by ID. Returns pricing, context window, capabilities, architecture, and per-request limits.
|
|
121
|
+
|
|
122
|
+
Call after find_models to get full details, or when the user provides a model ID (format: 'provider/model-name').
|
|
123
|
+
|
|
124
|
+
Returns 404 if model not found. Use find_models to discover valid IDs.`,
|
|
125
|
+
requiresKey: false
|
|
126
|
+
},
|
|
127
|
+
test_model: {
|
|
128
|
+
name: "test_model",
|
|
129
|
+
summary: "Run live inference or dry-run cost estimation across up to 10 models",
|
|
130
|
+
description: `Run model tests on 1-${LIMITS.testModelsMax} models. Use dryRun=true to estimate token usage/cost, or dryRun=false (default) to run live OpenRouter inference.
|
|
131
|
+
|
|
132
|
+
When dryRun=true:
|
|
133
|
+
- No OpenRouter API key required
|
|
134
|
+
- No inference call is made
|
|
135
|
+
- prompt is required
|
|
136
|
+
- expectedCompletionTokens defaults to 256 when omitted
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
- models: 1-${LIMITS.testModelsMax} model IDs to test (all receive identical prompts)
|
|
140
|
+
- prompt: Prompt text (required for dryRun; required for live unless userContent provided)
|
|
141
|
+
- dryRun: If true, return cost estimates only
|
|
142
|
+
- expectedCompletionTokens: Optional completion token estimate used by dryRun
|
|
143
|
+
- max_tokens, systemPrompt, temperature, topP, seed, responseFormat, enforceJson, retries: Live-testing controls (ignored when dryRun=true)
|
|
144
|
+
|
|
145
|
+
Use find_models or get_models first to identify model IDs.`,
|
|
146
|
+
requiresKey: true
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var PARAM_DESCRIPTIONS = {
|
|
150
|
+
q: "Natural language search query describing desired model characteristics (e.g., 'fast cheap coding model'). Uses semantic search with fuzzy matching. Optional - omit to use filters only.",
|
|
151
|
+
sortBy: `Sort order for results. Options: 'relevance' (best semantic match, default), 'created' (newest models), 'price' (cheapest/most expensive, with sortOrder). Defaults to 'relevance'.`,
|
|
152
|
+
cursor: `Opaque pagination cursor from a previous response's \`nextCursor\` field. IMPORTANT: cursors are bound to the exact query text, filters, and sort order that produced them. Reuse the same query+filters+sort when paginating. \`limit\` may change between pages. To start a new search, omit the cursor.`,
|
|
153
|
+
capabilitiesAll: `Comma-separated capabilities that must ALL be present on the model (AND logic). Valid values: ${CAPABILITIES.join(", ")}. Example: 'function_calling,vision'. Invalid values silently filter to zero results.`,
|
|
154
|
+
capabilitiesAny: `Comma-separated capabilities where at least ONE must be present (OR logic). Valid values: ${CAPABILITIES.join(", ")}. Example: 'vision,audio_input'. Invalid values silently filter to zero results.`,
|
|
155
|
+
modality: `Required output modality. Filters on the model's output modalities, not input capabilities. For example, 'image' finds image-generation models, while capabilitiesAll=vision finds models that accept image input. Valid values: ${OUTPUT_MODALITIES.join(", ")}.`,
|
|
156
|
+
provider: `Provider prefix filter. Matches model IDs starting with this prefix (e.g., 'openai' matches 'openai/gpt-4o'). Common providers: ${COMMON_PROVIDERS.join(", ")}.`,
|
|
157
|
+
expectedCompletionTokens: `Expected number of completion tokens for cost estimation (default: 256). Typical ranges: 100-500 for quick tests, 1000-2000 for code generation, 4000+ for long-form content. This is a heuristic \u2014 actual billed tokens may differ.`
|
|
158
|
+
};
|
|
159
|
+
var SITE = {
|
|
160
|
+
nav: {
|
|
161
|
+
brand: "index9",
|
|
162
|
+
howItWorks: "How it works",
|
|
163
|
+
install: "Install",
|
|
164
|
+
faq: "FAQ",
|
|
165
|
+
github: "GitHub",
|
|
166
|
+
githubLabel: "GitHub repository"
|
|
167
|
+
},
|
|
168
|
+
hero: {
|
|
169
|
+
titleLine1: "Test AI models on your actual prompts, ",
|
|
170
|
+
titleLine2: "not generic benchmarks",
|
|
171
|
+
subtitle: "Compare quality, speed, and cost across 300+ models \u2014 in Cursor, VS Code, or Claude Code.",
|
|
172
|
+
getStarted: "Install index9",
|
|
173
|
+
seeHowItWorks: "See an example",
|
|
174
|
+
updatedBadge: "Pricing & specs refreshed "
|
|
175
|
+
},
|
|
176
|
+
toolsSection: {
|
|
177
|
+
label: "Tools",
|
|
178
|
+
heading: "Search, test, and compare",
|
|
179
|
+
openRouterKey: "OpenRouter API key (live tests only)",
|
|
180
|
+
noKeyRequired: "No API key required",
|
|
181
|
+
requiresLabel: "Requires ",
|
|
182
|
+
cards: [
|
|
183
|
+
{
|
|
184
|
+
name: "find_models",
|
|
185
|
+
displayName: "find_models",
|
|
186
|
+
fullName: null,
|
|
187
|
+
description: `Search ${MODEL_COUNT} models by what you need \u2014 price, speed, context window, or capabilities like vision and function calling.`,
|
|
188
|
+
badge: null,
|
|
189
|
+
requiresKey: false
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "get_models",
|
|
193
|
+
displayName: "get_models",
|
|
194
|
+
fullName: null,
|
|
195
|
+
description: "Get current pricing, limits, and capabilities for any model. Updated from OpenRouter every 30 minutes.",
|
|
196
|
+
badge: null,
|
|
197
|
+
requiresKey: false
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "test_model",
|
|
201
|
+
displayName: "test_model",
|
|
202
|
+
fullName: null,
|
|
203
|
+
description: "Send your prompt to multiple models. Compare outputs, latency, and cost \u2014 measured, not estimated.",
|
|
204
|
+
badge: "Live Testing",
|
|
205
|
+
requiresKey: true
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
howItWorks: {
|
|
210
|
+
label: "Workflow",
|
|
211
|
+
heading: "How it works",
|
|
212
|
+
steps: [
|
|
213
|
+
{
|
|
214
|
+
number: "01",
|
|
215
|
+
title: "Analyze",
|
|
216
|
+
description: "Describe what you need. Your assistant finds models matching your cost, speed, and capability requirements."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
number: "02",
|
|
220
|
+
title: "Test",
|
|
221
|
+
description: "Run your prompt across the top candidates. See each output with latency and cost."
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
number: "03",
|
|
225
|
+
title: "Compare",
|
|
226
|
+
description: "Pick the best model for your task \u2014 based on real results, not generic rankings."
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
faq: {
|
|
231
|
+
label: "FAQ",
|
|
232
|
+
heading: "Common questions",
|
|
233
|
+
items: [
|
|
234
|
+
{
|
|
235
|
+
question: "What is MCP?",
|
|
236
|
+
answer: "MCP (Model Context Protocol) is an open standard that lets AI assistants call external tools. Adding index9 gives your assistant model discovery and testing tools inside any MCP-compatible client.",
|
|
237
|
+
link: {
|
|
238
|
+
label: "Learn more about MCP",
|
|
239
|
+
url: "https://modelcontextprotocol.io"
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
question: "How does live testing work?",
|
|
244
|
+
answer: `When you use test_model with dryRun=false (default), your prompt is sent to 1\u2013${LIMITS.testModelsMax} models via OpenRouter. Results include the full output plus latency, token usage, and cost. This requires an OpenRouter API key, which index9 forwards per-request and does not store or log. With dryRun=true, no inference call is made and no key is required \u2014 you get estimated token usage and projected cost only.`,
|
|
245
|
+
link: null
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
question: "Does index9 recommend which model to use?",
|
|
249
|
+
answer: "index9 provides the raw results \u2014 outputs, latency, cost, and specs. Your assistant can then recommend a model based on those results and your specific constraints.",
|
|
250
|
+
link: null
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
question: "What models are available?",
|
|
254
|
+
answer: `index9 provides access to ${MODEL_COUNT} models via OpenRouter, including OpenAI, Anthropic, Google, Meta, Mistral, and many others. Model metadata is continuously synced from OpenRouter so you have current pricing, context windows, and capabilities.`,
|
|
255
|
+
link: null
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
question: "What's the project status?",
|
|
259
|
+
answer: "index9 is in active development. The core tools are stable and ready for daily use, and improvements ship regularly. Issues and feedback are tracked on GitHub.",
|
|
260
|
+
link: null
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
question: "Is my data stored?",
|
|
264
|
+
answer: "No. index9 does not store or log your prompts, outputs, or API keys. For live tests, requests are proxied to OpenRouter to run inference.",
|
|
265
|
+
link: null
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
install: {
|
|
270
|
+
label: "Setup",
|
|
271
|
+
heading: "Add to your MCP config",
|
|
272
|
+
configs: [
|
|
273
|
+
{
|
|
274
|
+
id: "cursor-vscode",
|
|
275
|
+
label: "Cursor / VS Code",
|
|
276
|
+
paths: [".cursor/mcp.json", ".vscode/mcp.json"],
|
|
277
|
+
config: `{
|
|
278
|
+
"mcpServers": {
|
|
279
|
+
"index9": {
|
|
280
|
+
"command": "npx",
|
|
281
|
+
"args": ["-y", "@index9/mcp"]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}`
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "claude-code",
|
|
288
|
+
label: "Claude Code",
|
|
289
|
+
paths: ["Run in terminal (adds to ~/.claude.json)"],
|
|
290
|
+
config: `claude mcp add --transport stdio index9 -- npx -y @index9/mcp`,
|
|
291
|
+
copyHint: "# Run in terminal (adds to ~/.claude.json)"
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
workflowHeading: "Recommended workflow",
|
|
295
|
+
workflowIntro: "Add these to your assistant rules to guide model selection:",
|
|
296
|
+
workflowRulesLabel: ".cursor/rules or AGENTS.md",
|
|
297
|
+
workflowRules: [
|
|
298
|
+
"Use find_models to shortlist candidates based on task requirements (cost, speed, context window, capabilities).",
|
|
299
|
+
"Use get_models to confirm pricing, limits, and capabilities for the shortlist.",
|
|
300
|
+
"Use test_model with dryRun=true to estimate cost, then run live tests with a task-representative prompt. Compare outputs first, then optimize for speed/cost."
|
|
301
|
+
],
|
|
302
|
+
copyButton: "Copy",
|
|
303
|
+
copiedButton: "Copied",
|
|
304
|
+
copyAriaLabel: "Copy configuration",
|
|
305
|
+
openRouterNote: " (for live tests) requires an ",
|
|
306
|
+
openRouterLink: "OpenRouter API key",
|
|
307
|
+
openRouterUrl: "https://openrouter.ai/keys",
|
|
308
|
+
openRouterNoteSuffix: ". Add OPENROUTER_API_KEY to your config env for live tests. dryRun=true does not require a key; keys are passed per-request and never stored or logged."
|
|
309
|
+
},
|
|
310
|
+
comparison: {
|
|
311
|
+
label: "Comparison",
|
|
312
|
+
heading: "Evidence over intuition",
|
|
313
|
+
withoutLabel: "Without index9",
|
|
314
|
+
withLabel: "With index9",
|
|
315
|
+
withoutItems: [
|
|
316
|
+
"Pricing and specs may be weeks old",
|
|
317
|
+
"Quality assessed on generic benchmarks, not your task",
|
|
318
|
+
"Testing a model means a throwaway script or manual API switching"
|
|
319
|
+
],
|
|
320
|
+
withItems: [
|
|
321
|
+
"Pricing and specs synced from OpenRouter every 30 minutes",
|
|
322
|
+
"Quality measured on your actual prompts, side by side",
|
|
323
|
+
"Test and compare in your editor \u2014 no scripts, no switching"
|
|
324
|
+
],
|
|
325
|
+
sampleTableLabel: 'Sample comparison \u2014 "Extract the action items from this meeting transcript"',
|
|
326
|
+
tableHeaders: {
|
|
327
|
+
model: "Model",
|
|
328
|
+
latency: "Latency",
|
|
329
|
+
cost: "Cost",
|
|
330
|
+
notes: "Notes"
|
|
331
|
+
},
|
|
332
|
+
sampleRows: [
|
|
333
|
+
{
|
|
334
|
+
model: "gpt-4.1-nano",
|
|
335
|
+
latency: "310ms",
|
|
336
|
+
tokens: "96",
|
|
337
|
+
cost: "$0.0001",
|
|
338
|
+
note: "Fastest; missed one implicit action item"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
model: "gemini-2.5-flash",
|
|
342
|
+
latency: "560ms",
|
|
343
|
+
tokens: "142",
|
|
344
|
+
cost: "$0.0004",
|
|
345
|
+
note: "Caught all items; good balance"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
model: "claude-sonnet-4.5",
|
|
349
|
+
latency: "1,120ms",
|
|
350
|
+
tokens: "189",
|
|
351
|
+
cost: "$0.0018",
|
|
352
|
+
note: "Most thorough; grouped items by owner"
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
tableNote: "The right model depends on the task."
|
|
356
|
+
},
|
|
357
|
+
footer: {
|
|
358
|
+
brand: "index9",
|
|
359
|
+
tagline: "Model data synced from OpenRouter. Search, testing, and MCP tools by index9.",
|
|
360
|
+
copyright: "\xA9 2026 index9",
|
|
361
|
+
github: "GitHub",
|
|
362
|
+
privacy: "Privacy",
|
|
363
|
+
terms: "Terms"
|
|
364
|
+
},
|
|
365
|
+
terminalDemo: {
|
|
366
|
+
sectionLabel: "Example",
|
|
367
|
+
titleBar: "Cursor \u2014 assistant chat",
|
|
368
|
+
userLabel: "You",
|
|
369
|
+
assistantLabel: "Assistant",
|
|
370
|
+
userPrompt: "Find the cheapest, most capable model for my use case.",
|
|
371
|
+
assistantReplyPrefix: "I'll check your ",
|
|
372
|
+
assistantReplyFile: "summarize-ticket.ts",
|
|
373
|
+
assistantReplySuffix: " handler, search for suitable models, confirm specs for the shortlist, then test the top 3.",
|
|
374
|
+
findModelsCall: {
|
|
375
|
+
label: "find_models",
|
|
376
|
+
suffix: " \u2014 search by use case",
|
|
377
|
+
queryLabel: "q:",
|
|
378
|
+
queryContent: '"cheap fast summarization model"',
|
|
379
|
+
sortByLabel: "sortBy:",
|
|
380
|
+
sortByValue: "price"
|
|
381
|
+
},
|
|
382
|
+
testModelsCall: {
|
|
383
|
+
label: "test_model",
|
|
384
|
+
suffix: " \u2014 top 3 from find_models (via OpenRouter)",
|
|
385
|
+
modelsLabel: "models:",
|
|
386
|
+
modelsContent: "gemini-2.5-flash-lite, gpt-4.1-mini, claude-haiku-4.5",
|
|
387
|
+
promptLabel: "prompt:",
|
|
388
|
+
promptContent: '"Checkout fails on iPhone Safari. Payment subdomain SSL cert expired."'
|
|
389
|
+
},
|
|
390
|
+
resultsLabel: "Results",
|
|
391
|
+
conclusionPrefix: "Best fit: ",
|
|
392
|
+
conclusionModel: "gemini-2.5-flash-lite",
|
|
393
|
+
conclusionSuffix: " \u2014 cheapest and fastest, with comparable quality.",
|
|
394
|
+
resultCards: [
|
|
395
|
+
{
|
|
396
|
+
model: "gemini-2.5-flash-lite",
|
|
397
|
+
latency: "480ms",
|
|
398
|
+
cost: "$0.00005",
|
|
399
|
+
tokens: 124,
|
|
400
|
+
output: "Mobile Safari checkout failed \u2014 expired SSL cert on payment subdomain. Renewed; customer confirmed."
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
model: "gpt-4.1-mini",
|
|
404
|
+
latency: "720ms",
|
|
405
|
+
cost: "$0.0002",
|
|
406
|
+
tokens: 138,
|
|
407
|
+
output: "Checkout failures on mobile Safari. Root cause: expired SSL cert on payment subdomain. Resolved; fix confirmed."
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
model: "claude-haiku-4.5",
|
|
411
|
+
latency: "980ms",
|
|
412
|
+
cost: "$0.0008",
|
|
413
|
+
tokens: 155,
|
|
414
|
+
output: "Repeated checkout failures on iPhone Safari traced to expired payment subdomain SSL cert. Renewed; customer verified."
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var README = {
|
|
420
|
+
tagline: `Landing page, API, and MCP server for searching, inspecting, and benchmarking ${MODEL_COUNT} AI models.`,
|
|
421
|
+
mcpDescription: `Search, inspect, and benchmark ${MODEL_COUNT} AI models from your editor`,
|
|
422
|
+
monorepoLayout: {
|
|
423
|
+
appsWeb: "apps/web \u2014 Next.js 16 app (UI + API routes)",
|
|
424
|
+
packagesCore: "packages/core \u2014 Shared Zod schemas, types, constants (@index9/core)",
|
|
425
|
+
packagesMcp: "packages/mcp \u2014 Thin MCP stdio server calling the hosted API (@index9/mcp)"
|
|
426
|
+
},
|
|
427
|
+
quickStart: {
|
|
428
|
+
install: "pnpm install",
|
|
429
|
+
build: "pnpm build",
|
|
430
|
+
test: "pnpm test",
|
|
431
|
+
dev: "pnpm dev # run web app"
|
|
432
|
+
},
|
|
433
|
+
envNote: "Copy apps/web/.env.example to apps/web/.env.local and fill in values for local development.",
|
|
434
|
+
mcpInstall: {
|
|
435
|
+
cli: "npx -y @index9/mcp@latest",
|
|
436
|
+
envNote: "Optional: OPENROUTER_API_KEY for live test_model calls.",
|
|
437
|
+
claudeCode: "Claude Code: Run `claude mcp add --transport stdio index9 -- npx -y @index9/mcp` or add the same config to .mcp.json / ~/.claude.json."
|
|
438
|
+
},
|
|
439
|
+
release: {
|
|
440
|
+
step1: "Make changes in packages/mcp (core is internal, bundled into mcp)",
|
|
441
|
+
step2: "Run pnpm changeset \u2014 add a changeset, select packages, choose bump type",
|
|
442
|
+
step3: "Commit and push; open PR to main",
|
|
443
|
+
step4: "Merge the PR; CI creates a Version Packages PR when changesets exist",
|
|
444
|
+
step5: "Merge the version PR; CI publishes to npm and creates a GitHub Release with the .mcpb artifact attached",
|
|
445
|
+
step6: "Users can install via npx @index9/mcp@latest or download .mcpb from Releases"
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// ../core/dist/schemas/search.js
|
|
450
|
+
var SearchSortBySchema = z2.enum(["relevance", "created", "price"]);
|
|
451
|
+
var SearchSortOrderSchema = z2.enum(["asc", "desc"]);
|
|
452
|
+
var SearchQuerySchema = z2.object({
|
|
453
|
+
q: z2.string().min(1).optional(),
|
|
454
|
+
limit: z2.number().int().min(1).max(LIMITS.searchMax).default(LIMITS.searchDefault),
|
|
455
|
+
cursor: z2.string().min(1).optional(),
|
|
456
|
+
sortBy: SearchSortBySchema.default("relevance"),
|
|
457
|
+
sortOrder: SearchSortOrderSchema.optional(),
|
|
458
|
+
createdAfter: z2.string().min(1).optional(),
|
|
459
|
+
createdBefore: z2.string().min(1).optional(),
|
|
460
|
+
minPrice: z2.number().min(0).optional(),
|
|
461
|
+
maxPrice: z2.number().min(0).optional(),
|
|
462
|
+
minContext: z2.number().int().min(1).optional(),
|
|
463
|
+
capabilitiesAll: z2.array(z2.string().min(1)).optional(),
|
|
464
|
+
capabilitiesAny: z2.array(z2.string().min(1)).optional(),
|
|
465
|
+
modality: z2.string().min(1).optional(),
|
|
466
|
+
provider: z2.string().min(1).optional()
|
|
467
|
+
}).strict();
|
|
468
|
+
var SearchResultSchema = z2.object({
|
|
469
|
+
modelId: z2.string(),
|
|
470
|
+
name: z2.string(),
|
|
471
|
+
description: z2.string(),
|
|
472
|
+
createdUnix: z2.number().nullable(),
|
|
473
|
+
createdAt: z2.string().nullable(),
|
|
474
|
+
contextLength: z2.number().nullable(),
|
|
475
|
+
maxOutputTokens: z2.number().nullable(),
|
|
476
|
+
pricing: z2.object({
|
|
477
|
+
promptPerMillion: z2.number().nullable(),
|
|
478
|
+
completionPerMillion: z2.number().nullable()
|
|
479
|
+
}),
|
|
480
|
+
capabilities: z2.array(z2.string()),
|
|
481
|
+
score: z2.number().nullable()
|
|
482
|
+
});
|
|
483
|
+
var SearchResponseSchema = z2.object({
|
|
484
|
+
results: z2.array(SearchResultSchema),
|
|
485
|
+
nextCursor: z2.string().nullable(),
|
|
486
|
+
pageInfo: z2.object({
|
|
487
|
+
limit: z2.number(),
|
|
488
|
+
hasMore: z2.boolean(),
|
|
489
|
+
sortBy: SearchSortBySchema,
|
|
490
|
+
sortOrder: SearchSortOrderSchema
|
|
491
|
+
}),
|
|
492
|
+
meta: z2.object({
|
|
493
|
+
queryMode: z2.enum(["semantic", "filter_only"]),
|
|
494
|
+
ranking: z2.literal("hybrid_rrf")
|
|
495
|
+
})
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// ../core/dist/schemas/model.js
|
|
499
|
+
import { z as z3 } from "zod";
|
|
500
|
+
var BatchModelLookupRequestSchema = z3.object({
|
|
501
|
+
ids: z3.array(z3.string().min(1)).min(1, "ids are required").max(LIMITS.getModelsMax, `ids must contain between 1 and ${LIMITS.getModelsMax} model IDs`),
|
|
502
|
+
maxDescriptionChars: z3.number().int().min(0).max(2e3).optional()
|
|
503
|
+
}).strict();
|
|
504
|
+
var ModelResponseSchema = z3.record(z3.string(), z3.unknown());
|
|
505
|
+
var BatchModelLookupResponseSchema = z3.object({
|
|
506
|
+
results: z3.array(ModelResponseSchema.nullable()),
|
|
507
|
+
missingIds: z3.array(z3.string()),
|
|
508
|
+
resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
|
|
509
|
+
ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional()
|
|
510
|
+
}).strict();
|
|
511
|
+
|
|
512
|
+
// ../core/dist/schemas/test.js
|
|
513
|
+
import { z as z4 } from "zod";
|
|
514
|
+
var ResponseFormatSchema = z4.object({
|
|
515
|
+
type: z4.string().min(1)
|
|
516
|
+
}).catchall(z4.unknown()).optional();
|
|
517
|
+
var TestRequestSchema = z4.object({
|
|
518
|
+
prompt: z4.string().min(1).optional(),
|
|
519
|
+
userContent: z4.array(UserContentPartSchema).min(1).optional(),
|
|
520
|
+
dryRun: z4.boolean().optional(),
|
|
521
|
+
expectedCompletionTokens: z4.number().int().positive().optional(),
|
|
522
|
+
models: z4.array(z4.string().min(1)).min(1, "Models are required").max(LIMITS.testModelsMax, `Models must contain between 1 and ${LIMITS.testModelsMax} model IDs`),
|
|
523
|
+
timeoutMs: z4.number().int().positive().optional(),
|
|
524
|
+
maxTokens: z4.number().int().positive().optional(),
|
|
525
|
+
systemPrompt: z4.string().min(1).optional(),
|
|
526
|
+
temperature: z4.number().min(0).max(2).optional(),
|
|
527
|
+
topP: z4.number().gt(0).lte(1).optional(),
|
|
528
|
+
seed: z4.number().int().optional(),
|
|
529
|
+
responseFormat: ResponseFormatSchema,
|
|
530
|
+
enforceJson: z4.boolean().optional(),
|
|
531
|
+
retries: z4.number().int().min(0).max(3).optional()
|
|
532
|
+
}).strict().superRefine((data, ctx) => {
|
|
533
|
+
if (data.dryRun === true) {
|
|
534
|
+
if (!data.prompt) {
|
|
535
|
+
ctx.addIssue({
|
|
536
|
+
code: z4.ZodIssueCode.custom,
|
|
537
|
+
message: "Prompt is required when dryRun is true",
|
|
538
|
+
path: ["prompt"]
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (!data.prompt && !data.userContent?.length) {
|
|
544
|
+
ctx.addIssue({
|
|
545
|
+
code: z4.ZodIssueCode.custom,
|
|
546
|
+
message: "Prompt or userContent is required",
|
|
547
|
+
path: ["prompt"]
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
var UsageTokensSchema = z4.object({
|
|
552
|
+
prompt: z4.number().min(0),
|
|
553
|
+
completion: z4.number().min(0)
|
|
554
|
+
});
|
|
555
|
+
var TestPricingUsedSchema = z4.object({
|
|
556
|
+
promptPerToken: z4.number().nullable().optional(),
|
|
557
|
+
completionPerToken: z4.number().nullable().optional(),
|
|
558
|
+
promptPerMillion: z4.number().nullable().optional(),
|
|
559
|
+
completionPerMillion: z4.number().nullable().optional()
|
|
560
|
+
});
|
|
561
|
+
var TestModelMetadataSchema = z4.object({
|
|
562
|
+
id: z4.string(),
|
|
563
|
+
name: z4.string(),
|
|
564
|
+
createdUnix: z4.number().nullable().optional(),
|
|
565
|
+
createdAt: z4.string().nullable().optional(),
|
|
566
|
+
pricingUsed: TestPricingUsedSchema.optional()
|
|
567
|
+
});
|
|
568
|
+
var TestResultSuccessSchema = z4.object({
|
|
569
|
+
modelId: z4.string(),
|
|
570
|
+
resolvedModelId: z4.string().optional(),
|
|
571
|
+
ok: z4.literal(true),
|
|
572
|
+
model: TestModelMetadataSchema,
|
|
573
|
+
response: z4.string(),
|
|
574
|
+
latencyMs: z4.number().min(0),
|
|
575
|
+
tokens: UsageTokensSchema,
|
|
576
|
+
cost: z4.number().nullable().optional()
|
|
577
|
+
});
|
|
578
|
+
var TestResultFailureSchema = z4.object({
|
|
579
|
+
modelId: z4.string(),
|
|
580
|
+
resolvedModelId: z4.string().optional(),
|
|
581
|
+
ok: z4.literal(false),
|
|
582
|
+
model: TestModelMetadataSchema,
|
|
583
|
+
error: z4.string(),
|
|
584
|
+
latencyMs: z4.number().min(0)
|
|
585
|
+
});
|
|
586
|
+
var TestResultSchema = z4.discriminatedUnion("ok", [
|
|
587
|
+
TestResultSuccessSchema,
|
|
588
|
+
TestResultFailureSchema
|
|
589
|
+
]);
|
|
590
|
+
var TestEstimateResultSchema = z4.object({
|
|
591
|
+
modelId: z4.string(),
|
|
592
|
+
resolvedModelId: z4.string().optional(),
|
|
593
|
+
model: TestModelMetadataSchema,
|
|
594
|
+
tokens: UsageTokensSchema,
|
|
595
|
+
estimatedCost: z4.number().nullable().optional()
|
|
596
|
+
});
|
|
597
|
+
var TestDryRunResponseSchema = z4.object({
|
|
598
|
+
dryRun: z4.literal(true),
|
|
599
|
+
results: z4.array(TestEstimateResultSchema),
|
|
600
|
+
disclaimer: z4.string()
|
|
601
|
+
});
|
|
602
|
+
var TestLiveResponseSchema = z4.object({
|
|
603
|
+
results: z4.array(TestResultSchema)
|
|
604
|
+
});
|
|
605
|
+
var TestResponseSchema = z4.union([TestDryRunResponseSchema, TestLiveResponseSchema]);
|
|
606
|
+
|
|
607
|
+
// src/server.ts
|
|
608
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
609
|
+
import { z as z5 } from "zod";
|
|
610
|
+
|
|
611
|
+
// src/config.ts
|
|
612
|
+
var DEFAULT_BASE_URL = "https://index9.dev";
|
|
613
|
+
function loadConfig() {
|
|
614
|
+
const env = process.env.INDEX9_API_BASE_URL?.trim();
|
|
615
|
+
const baseUrl = env || DEFAULT_BASE_URL;
|
|
616
|
+
const normalized = baseUrl.replace(/\/$/, "");
|
|
617
|
+
return {
|
|
618
|
+
baseUrl: normalized,
|
|
619
|
+
apiToken: process.env.INDEX9_API_TOKEN?.trim() || void 0,
|
|
620
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY?.trim() || process.env.API_KEY_OPENROUTERAPIKEY?.trim() || void 0
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/client.ts
|
|
625
|
+
var RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
626
|
+
function isRetryable(status) {
|
|
627
|
+
return status === 429 || status >= 500;
|
|
628
|
+
}
|
|
629
|
+
async function sleep(ms) {
|
|
630
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
631
|
+
}
|
|
632
|
+
async function fetchWithRetry(url, options) {
|
|
633
|
+
let lastResponse = null;
|
|
634
|
+
for (let i = 0; i <= RETRY_DELAYS_MS.length; i++) {
|
|
635
|
+
const res = await fetch(url, options);
|
|
636
|
+
lastResponse = res;
|
|
637
|
+
if (res.ok || !isRetryable(res.status)) return res;
|
|
638
|
+
if (i < RETRY_DELAYS_MS.length) {
|
|
639
|
+
await sleep(RETRY_DELAYS_MS[i]);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return lastResponse;
|
|
643
|
+
}
|
|
644
|
+
function buildUrl(baseUrl, path, params) {
|
|
645
|
+
const url = new URL(path, baseUrl);
|
|
646
|
+
if (params) {
|
|
647
|
+
for (const [k, v] of Object.entries(params)) {
|
|
648
|
+
if (v !== void 0 && v !== "") url.searchParams.set(k, v);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return url.toString();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/tools.ts
|
|
655
|
+
function baseHeaders(ctx) {
|
|
656
|
+
const h = { "Content-Type": "application/json" };
|
|
657
|
+
if (ctx.apiToken) h["Authorization"] = `Bearer ${ctx.apiToken}`;
|
|
658
|
+
return h;
|
|
659
|
+
}
|
|
660
|
+
function toResponse(payload, isError = false) {
|
|
661
|
+
return {
|
|
662
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
663
|
+
isError: isError || void 0
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function parseRetryAfterSeconds(value) {
|
|
667
|
+
if (!value) return void 0;
|
|
668
|
+
const trimmed = value.trim();
|
|
669
|
+
if (!trimmed) return void 0;
|
|
670
|
+
if (/^\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);
|
|
671
|
+
const retryAt = Date.parse(trimmed);
|
|
672
|
+
if (!Number.isFinite(retryAt)) return void 0;
|
|
673
|
+
return Math.max(0, Math.ceil((retryAt - Date.now()) / 1e3));
|
|
674
|
+
}
|
|
675
|
+
function buildMeta(ctx, headers) {
|
|
676
|
+
const meta = { apiBaseUrl: ctx.baseUrl };
|
|
677
|
+
if (!headers) return meta;
|
|
678
|
+
const retryAfterSeconds = parseRetryAfterSeconds(headers.get("retry-after"));
|
|
679
|
+
if (retryAfterSeconds !== void 0) meta.retryAfterSeconds = retryAfterSeconds;
|
|
680
|
+
const rateLimit = {
|
|
681
|
+
limit: headers.get("x-ratelimit-limit")?.trim() || void 0,
|
|
682
|
+
remaining: headers.get("x-ratelimit-remaining")?.trim() || void 0,
|
|
683
|
+
reset: headers.get("x-ratelimit-reset")?.trim() || void 0
|
|
684
|
+
};
|
|
685
|
+
if (rateLimit.limit || rateLimit.remaining || rateLimit.reset) {
|
|
686
|
+
meta.rateLimit = rateLimit;
|
|
687
|
+
}
|
|
688
|
+
return meta;
|
|
689
|
+
}
|
|
690
|
+
function withMeta(ctx, payload, headers) {
|
|
691
|
+
const base = typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : { data: payload };
|
|
692
|
+
return { ...base, _index9: buildMeta(ctx, headers) };
|
|
693
|
+
}
|
|
694
|
+
function extractError(body) {
|
|
695
|
+
if (typeof body === "object" && body !== null) {
|
|
696
|
+
const err = body.error;
|
|
697
|
+
if (typeof err === "string" && err.length > 0) return err;
|
|
698
|
+
}
|
|
699
|
+
return "Request failed";
|
|
700
|
+
}
|
|
701
|
+
async function callApi(ctx, url, options, responseSchema) {
|
|
702
|
+
const res = await fetchWithRetry(url, options);
|
|
703
|
+
let body;
|
|
704
|
+
try {
|
|
705
|
+
body = await res.json();
|
|
706
|
+
} catch {
|
|
707
|
+
body = { error: "Invalid API response body" };
|
|
708
|
+
}
|
|
709
|
+
if (!res.ok) {
|
|
710
|
+
return toResponse(
|
|
711
|
+
{ error: extractError(body), status: res.status, _index9: buildMeta(ctx, res.headers) },
|
|
712
|
+
true
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
const validated = responseSchema.safeParse(body);
|
|
716
|
+
if (!validated.success) {
|
|
717
|
+
return toResponse(withMeta(ctx, { error: "Invalid API response" }, res.headers), true);
|
|
718
|
+
}
|
|
719
|
+
return toResponse(withMeta(ctx, validated.data, res.headers));
|
|
720
|
+
}
|
|
721
|
+
async function handleSearchModels(ctx, args) {
|
|
722
|
+
const parsed = SearchQuerySchema.safeParse(args);
|
|
723
|
+
if (!parsed.success) {
|
|
724
|
+
return toResponse({ error: parsed.error.message }, true);
|
|
725
|
+
}
|
|
726
|
+
const q = parsed.data;
|
|
727
|
+
const params = {};
|
|
728
|
+
if (q.q) params.q = q.q;
|
|
729
|
+
params.limit = String(q.limit);
|
|
730
|
+
if (q.cursor) params.cursor = q.cursor;
|
|
731
|
+
params.sortBy = q.sortBy;
|
|
732
|
+
if (q.sortOrder) params.sortOrder = q.sortOrder;
|
|
733
|
+
if (q.createdAfter) params.createdAfter = q.createdAfter;
|
|
734
|
+
if (q.createdBefore) params.createdBefore = q.createdBefore;
|
|
735
|
+
if (q.minPrice !== void 0) params.minPrice = String(q.minPrice);
|
|
736
|
+
if (q.maxPrice !== void 0) params.maxPrice = String(q.maxPrice);
|
|
737
|
+
if (q.minContext !== void 0) params.minContext = String(q.minContext);
|
|
738
|
+
if (q.capabilitiesAll?.length) params.capabilitiesAll = q.capabilitiesAll.join(",");
|
|
739
|
+
if (q.capabilitiesAny?.length) params.capabilitiesAny = q.capabilitiesAny.join(",");
|
|
740
|
+
if (q.modality) params.modality = q.modality;
|
|
741
|
+
if (q.provider) params.provider = q.provider;
|
|
742
|
+
return callApi(
|
|
743
|
+
ctx,
|
|
744
|
+
buildUrl(ctx.baseUrl, API_PATHS.search, params),
|
|
745
|
+
{ method: "GET", headers: baseHeaders(ctx) },
|
|
746
|
+
SearchResponseSchema
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
async function handleGetModels(ctx, args) {
|
|
750
|
+
const parsed = BatchModelLookupRequestSchema.safeParse(args);
|
|
751
|
+
if (!parsed.success) {
|
|
752
|
+
return toResponse({ error: parsed.error.message }, true);
|
|
753
|
+
}
|
|
754
|
+
return callApi(
|
|
755
|
+
ctx,
|
|
756
|
+
`${ctx.baseUrl}${API_PATHS.model}`,
|
|
757
|
+
{ method: "POST", headers: baseHeaders(ctx), body: JSON.stringify(parsed.data) },
|
|
758
|
+
BatchModelLookupResponseSchema
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
async function handleTestModels(ctx, args) {
|
|
762
|
+
const parsed = TestRequestSchema.safeParse(args);
|
|
763
|
+
if (!parsed.success) {
|
|
764
|
+
return toResponse({ error: parsed.error.message }, true);
|
|
765
|
+
}
|
|
766
|
+
if (parsed.data.dryRun !== true && !ctx.openRouterApiKey) {
|
|
767
|
+
return toResponse(
|
|
768
|
+
{
|
|
769
|
+
error: "OPENROUTER_API_KEY is required for live test_model calls. Set dryRun=true to estimate without a key."
|
|
770
|
+
},
|
|
771
|
+
true
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
const reqHeaders = baseHeaders(ctx);
|
|
775
|
+
if (parsed.data.dryRun !== true && ctx.openRouterApiKey) {
|
|
776
|
+
reqHeaders["x-openrouter-api-key"] = ctx.openRouterApiKey;
|
|
777
|
+
}
|
|
778
|
+
return callApi(
|
|
779
|
+
ctx,
|
|
780
|
+
`${ctx.baseUrl}${API_PATHS.test}`,
|
|
781
|
+
{ method: "POST", headers: reqHeaders, body: JSON.stringify(parsed.data) },
|
|
782
|
+
TestResponseSchema
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/server.ts
|
|
787
|
+
async function createServer() {
|
|
788
|
+
const ctx = loadConfig();
|
|
789
|
+
const server = new McpServer(
|
|
790
|
+
{ name: "index9", version: "4.0.0" },
|
|
791
|
+
{ instructions: WORKFLOW_INSTRUCTIONS }
|
|
792
|
+
);
|
|
793
|
+
server.registerTool(
|
|
794
|
+
"find_models",
|
|
795
|
+
{
|
|
796
|
+
description: TOOLS.find_models.description,
|
|
797
|
+
inputSchema: {
|
|
798
|
+
q: z5.string().min(1).optional().describe(PARAM_DESCRIPTIONS.q),
|
|
799
|
+
limit: z5.number().int().min(1).max(100).default(20).describe("Page size (1-100, default 20)."),
|
|
800
|
+
cursor: z5.string().min(1).optional().describe(PARAM_DESCRIPTIONS.cursor),
|
|
801
|
+
sortBy: z5.enum(["relevance", "created", "price"]).default("relevance").describe(PARAM_DESCRIPTIONS.sortBy),
|
|
802
|
+
sortOrder: z5.enum(["asc", "desc"]).optional().describe("Sort order. Defaults by sortBy."),
|
|
803
|
+
createdAfter: z5.string().optional().describe("Lower bound for model created timestamp."),
|
|
804
|
+
createdBefore: z5.string().optional().describe("Upper bound for model created timestamp."),
|
|
805
|
+
minPrice: z5.number().min(0).optional().describe("Minimum prompt price in USD per million tokens."),
|
|
806
|
+
maxPrice: z5.number().min(0).optional().describe("Maximum prompt price in USD per million tokens."),
|
|
807
|
+
minContext: z5.number().int().min(1).optional().describe("Minimum context window in tokens."),
|
|
808
|
+
capabilitiesAll: z5.array(z5.string()).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAll),
|
|
809
|
+
capabilitiesAny: z5.array(z5.string()).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAny),
|
|
810
|
+
modality: z5.string().optional().describe(PARAM_DESCRIPTIONS.modality),
|
|
811
|
+
provider: z5.string().min(1).optional().describe(PARAM_DESCRIPTIONS.provider)
|
|
812
|
+
},
|
|
813
|
+
annotations: { readOnlyHint: true }
|
|
814
|
+
},
|
|
815
|
+
async (args) => handleSearchModels(ctx, args)
|
|
816
|
+
);
|
|
817
|
+
server.registerTool(
|
|
818
|
+
"get_models",
|
|
819
|
+
{
|
|
820
|
+
description: TOOLS.get_models.description,
|
|
821
|
+
inputSchema: {
|
|
822
|
+
ids: z5.array(z5.string().min(1)).min(1).max(100).describe("Model identifiers or aliases. Up to 100."),
|
|
823
|
+
maxDescriptionChars: z5.number().int().min(0).max(2e3).optional().describe("Truncate descriptions to this many characters.")
|
|
824
|
+
},
|
|
825
|
+
annotations: { readOnlyHint: true }
|
|
826
|
+
},
|
|
827
|
+
async (args) => handleGetModels(ctx, args)
|
|
828
|
+
);
|
|
829
|
+
server.registerTool(
|
|
830
|
+
"test_model",
|
|
831
|
+
{
|
|
832
|
+
description: TOOLS.test_model.description,
|
|
833
|
+
inputSchema: {
|
|
834
|
+
prompt: z5.string().min(1).optional().describe("Prompt sent to each model."),
|
|
835
|
+
userContent: z5.array(UserContentPartSchema).min(1).optional().describe("Multimodal user content. At least one of prompt or userContent required."),
|
|
836
|
+
dryRun: z5.boolean().optional().describe(
|
|
837
|
+
"When true, returns estimated token usage and cost without calling OpenRouter (no API key required)."
|
|
838
|
+
),
|
|
839
|
+
expectedCompletionTokens: z5.number().int().min(1).optional().describe(PARAM_DESCRIPTIONS.expectedCompletionTokens),
|
|
840
|
+
models: z5.array(z5.string().min(1)).min(1).max(LIMITS.testModelsMax).describe(`Model IDs to evaluate (1-${LIMITS.testModelsMax}).`),
|
|
841
|
+
timeoutMs: z5.number().int().min(1).optional().describe("Per-model timeout in ms (default 15000, max 60000)."),
|
|
842
|
+
maxTokens: z5.number().int().min(1).optional().describe("Completion token cap."),
|
|
843
|
+
systemPrompt: z5.string().min(1).optional().describe("System instruction prepended to prompt."),
|
|
844
|
+
temperature: z5.number().min(0).max(2).optional().describe("Sampling temperature (0-2)."),
|
|
845
|
+
topP: z5.number().gt(0).max(1).optional().describe("Nucleus sampling (0-1]."),
|
|
846
|
+
seed: z5.number().int().optional().describe("Seed for repeatable outputs."),
|
|
847
|
+
responseFormat: ResponseFormatSchema.describe(
|
|
848
|
+
"Structured output shape request forwarded to OpenRouter (e.g., { type: 'json_object' })."
|
|
849
|
+
),
|
|
850
|
+
enforceJson: z5.boolean().optional().describe("When true, output must parse as JSON."),
|
|
851
|
+
retries: z5.number().int().min(0).max(3).optional().describe("Retries for transient failures.")
|
|
852
|
+
},
|
|
853
|
+
annotations: { readOnlyHint: false }
|
|
854
|
+
},
|
|
855
|
+
async (args) => handleTestModels(ctx, args)
|
|
856
|
+
);
|
|
857
|
+
return server;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/cli.ts
|
|
861
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
862
|
+
createServer().then(async (server) => {
|
|
863
|
+
const transport = new StdioServerTransport();
|
|
864
|
+
await server.connect(transport);
|
|
865
|
+
}).catch((error) => {
|
|
866
|
+
console.error("Failed to start MCP server:", error);
|
|
867
|
+
process.exit(1);
|
|
868
|
+
});
|