@index9/mcp 5.3.0 → 6.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.
Files changed (3) hide show
  1. package/dist/cli.js +145 -123
  2. package/manifest.json +1 -1
  3. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -43,6 +43,11 @@ var Index9MetaSchema = z.object({
43
43
  retryAfterSeconds: z.number().optional(),
44
44
  rateLimit: RateLimitMetaSchema.optional()
45
45
  });
46
+ var MissingModelDiagnosticSchema = z.object({
47
+ reason: z.enum(["unknown_provider", "no_match", "suggestions_available", "ambiguous_alias"]),
48
+ provider: z.string().optional(),
49
+ message: z.string()
50
+ });
46
51
  var UserContentTextPartSchema = z.strictObject({
47
52
  type: z.literal("text"),
48
53
  text: z.string().trim().min(1)
@@ -101,8 +106,9 @@ Typical workflow:
101
106
  Key rules:
102
107
  - find_models requires \`q\` when \`sortBy=relevance\` (the default). Omit \`q\` only with \`sortBy=created\` or \`sortBy=price\`.
103
108
  - find_models price-asc tends to be dominated by free preview models \u2014 pass \`excludeFree=true\` when you want a paid SLA.
104
- - find_models flags \`meta.confidence: "low"\` when no candidate matched on keyword (BM25). When that fires, prefer \`meta.suggestion\` over the returned scores; weak hits are capped at score=30 so they don't masquerade as strong matches.
105
- - get_models accepts aliases (display names, short names) \u2014 not just full IDs. Unknown ids return in missingIds with \`suggestions\` (token-fuzzy or recency-anchored newest-from-provider). Retry with one of the suggested ids.
109
+ - find_models always emits \`meta.confidence\` ("high" | "low") on semantic queries. Low means no candidate matched on keyword (BM25); \`meta.lowConfidenceReason\` is "no_keyword_matches" or "no_results" and \`meta.suggestion\` carries an actionable hint. Weak hits are capped at score=30 so they don't masquerade as strong matches. Pass \`requireKeywordMatch: true\` to get an empty page instead of weak vector-only neighbors.
110
+ - find_models with sortBy=price exposes \`pricing.effectivePromptPerMillion\` and \`pageInfo.priceSortBasis\` \u2014 sort order may diverge from displayed promptPerMillion for models with per-request fees.
111
+ - get_models accepts aliases (display names, short names) \u2014 not just full IDs. Unknown ids return in missingIds with \`suggestions\` (token-fuzzy or recency-anchored newest-from-provider) and \`missingDiagnostics\` keyed by id with \`reason\` ("unknown_provider" | "no_match" | "suggestions_available" | "ambiguous_alias") so retry strategy is explicit. Retry with one of the suggested ids.
106
112
  - compare_models accepts the same alias formats as get_models. Use it instead of N parallel get_models calls when the user is comparing finalists.
107
113
  - Use test_model with \`dryRun=true\` to estimate cost before live testing. Pass \`expectedPromptTokens\` for capacity planning at sizes you don't want to paste in full.
108
114
  - test_model with \`dryRun=false\` (default) requires OPENROUTER_API_KEY and incurs real usage costs.
@@ -129,11 +135,11 @@ Examples:
129
135
 
130
136
  Valid capabilities: ${CAPABILITIES.join(", ")}.
131
137
 
132
- Each result: id, name, description, created (unix seconds), createdAt (ISO 8601), contextLength, maxOutputTokens, pricing.{promptPerMillion, completionPerMillion} (numbers, USD per million tokens), inputModalities[] / outputModalities[] (e.g. ["text","image"] \u2014 check at a glance to spot text-only vs multimodal models), capabilities[], score.
138
+ Each result: id, name, description, created (unix seconds), createdAt (ISO 8601), contextLength, maxOutputTokens, pricing.{promptPerMillion, completionPerMillion} (rounded display $/M), pricing.{promptPerToken, completionPerToken, requestUsd} (exact, use for cost math), inputModalities[] / outputModalities[], capabilities[], score. With sortBy=price, results also expose pricing.effectivePromptPerMillion and pageInfo.priceSortBasis \u2014 sort order may diverge from displayed promptPerMillion for models with per-request fees.
133
139
 
134
140
  \`score\` is 0-100: the best match per page scores 100; others scale proportionally. Combines semantic similarity and keyword matching. Null when sorting by price or date.
135
141
 
136
- \`q\` must be at least 2 characters when provided. \`meta.confidence\` is "low" when no candidate matched on keyword (BM25), meaning the ranker fell back to vector similarity alone \u2014 typo, gibberish, or a query the catalog can't answer. When low, \`meta.suggestion\` carries an actionable hint and \`score\` values are capped at 30 so weak hits don't masquerade as strong ones.
142
+ \`q\` must be at least 2 characters when provided. For semantic queries, \`meta.confidence\` is always emitted as "high" or "low". Low means no candidate matched on keyword (BM25); \`meta.lowConfidenceReason\` is "no_keyword_matches" or "no_results" and \`meta.suggestion\` carries an actionable hint. Pass \`requireKeywordMatch: true\` to suppress weak hits and get an empty page on low confidence.
137
143
 
138
144
  Pass result.id to get_models for full specs or to test_model for live testing.`,
139
145
  requiresKey: false
@@ -150,12 +156,14 @@ Response: { results: (Model | null)[], missingIds: string[], resolvedAliases?: R
150
156
  - id, canonicalSlug, name, description
151
157
  - created (unix seconds), createdAt (ISO 8601), knowledgeCutoff (ISO date or null)
152
158
  - contextLength (tokens), maxOutputTokens, isModerated
153
- - pricing: { promptPerMillion, completionPerMillion, requestUsd, imageUsd } \u2014 all USD, all numbers. Token prices are per million tokens; request/image are per unit.
159
+ - pricing: { promptPerMillion, completionPerMillion, promptPerToken, completionPerToken, requestUsd, imageUsd } \u2014 *PerMillion is rounded display, *PerToken is exact (use for cost math). request/image are flat per-unit fees.
154
160
  - architecture: { inputModalities[], outputModalities[], tokenizer, instructType }
155
161
  - capabilities[]: normalized capability flags (same values as find_models and capabilitiesAll/Any)
156
162
  - supportedParameters[]: OpenRouter parameters the model accepts (e.g., "temperature", "tools", "response_format")
157
163
 
158
- Entries in results are null when the id is unknown; those ids appear in missingIds. Ambiguous aliases appear in ambiguousAliases with candidate canonical ids \u2014 pass a canonical id to disambiguate. Unknown ids that partially match (e.g. "sonnet" \u2192 all Claude Sonnet variants) appear in suggestions with up to 5 candidate ids. When token-overlap finds nothing but the id is shaped like \`provider/<unknown>\` and the provider exists, suggestions falls back to the 5 newest models from that provider (real created timestamps, no hardcoded "popular" list). Retry with one of the suggested ids.`,
164
+ Entries in results are null when the id is unknown; those ids appear in missingIds. Ambiguous aliases appear in ambiguousAliases with candidate canonical ids \u2014 pass a canonical id to disambiguate. Unknown ids that partially match (e.g. "sonnet" \u2192 all Claude Sonnet variants) appear in suggestions with up to 5 candidate ids. When token-overlap finds nothing but the id is shaped like \`provider/<unknown>\` and the provider exists, suggestions falls back to the 5 newest models from that provider (real created timestamps, no hardcoded "popular" list). Retry with one of the suggested ids.
165
+
166
+ \`missingDiagnostics\` (when present) gives a machine-readable reason per missing id: \`unknown_provider\` (the prefix before / isn't in the catalog \u2014 fix the provider, not the model name), \`ambiguous_alias\`, \`suggestions_available\` (mirrors suggestions[id]), or \`no_match\`.`,
159
167
  requiresKey: false
160
168
  },
161
169
  compare_models: {
@@ -170,9 +178,9 @@ Response: { models: ModelResponse[], diff: { contextLength, maxOutputTokens, pro
170
178
 
171
179
  Each numeric/string diff field has { allEqual: boolean, values: Record<id, value|null> }. Capability/parameter diffs have { commonAll: string[], uniquePerModel: Record<id, string[]> }. cheapestForPromptPerMillion / largestContext are convenience picks across the supplied models \u2014 null when the field is missing on every model.
172
180
 
173
- Optional: pass \`expectedPromptTokens\` AND \`expectedCompletionTokens\` to also receive \`workloadCosts\` (per-model totalCostUsd) and \`cheapestForRealisticWorkload\` \u2014 the actual cheapest given the user's expected token mix. This matters when prompt:completion price ratios diverge across models (e.g., a model with cheap prompt but expensive completion can lose against a flatter-priced sibling under heavy completions).
181
+ Optional: pass \`expectedPromptTokens\` AND \`expectedCompletionTokens\` to also receive \`workloadCosts\` and \`cheapestForRealisticWorkload\` \u2014 the actual cheapest given the user's expected token mix. Each \`workloadCosts[i]\` carries \`tokenCostUsd\` (token-only), \`requestCostUsd\` (per-request fee), \`totalCostUsd\` (sum, includes request fees), and \`pricingBasis\` ("exact_per_token" | "rounded_per_million" | "unavailable"). This matters when prompt:completion price ratios diverge across models, or when a model has a per-request fee.
174
182
 
175
- Accepts the same alias formats as get_models. Unknown ids are returned in missingIds (with suggestions when partial matches exist).`,
183
+ Accepts the same alias formats as get_models. Unknown ids are returned in missingIds (with suggestions when partial matches exist, plus \`missingDiagnostics\` carrying a machine-readable reason per id).`,
176
184
  requiresKey: false
177
185
  },
178
186
  list_facets: {
@@ -206,7 +214,9 @@ Parameters:
206
214
  - expectedCompletionTokens: Optional completion token estimate used by dryRun
207
215
  - maxTokens, systemPrompt, temperature, topP, seed, responseFormat, enforceJson, retries: Live-testing controls (ignored when dryRun=true)
208
216
 
209
- Results (live): each result carries modelId (the id you passed), resolvedModelId (canonical id, present when the input was an alias), ok, response, latencyMs, tokens { prompt, completion }, cost (USD; live from OpenRouter when available, else estimated from cached pricing), and truncated=true when finish_reason is "length". Use find_models or get_models first to identify model ids.`,
217
+ Results (live): each result carries modelId (the id you passed), resolvedModelId (canonical id, present when the input was an alias), ok, response, latencyMs, tokens { prompt, completion }, cost (USD; live from OpenRouter when available, else estimated from cached pricing), and truncated=true when finish_reason is "length". On failure, results include \`error\` (free-form) plus \`failureReason\` ("insufficient_credits" | "model_unavailable" | "rate_limited" | "timeout" | "invalid_request" | "unknown") so callers can pick a retry strategy without parsing the error string.
218
+
219
+ Results (dryRun): each entry carries \`tokenCostUsd\`, \`requestCostUsd\`, \`totalCostUsd\` (matches \`estimatedCost\`, includes per-request fees), and \`estimatedCostBasis\` (same enum as compare_models.workloadCosts). Use find_models or get_models first to identify model ids.`,
210
220
  requiresKey: true
211
221
  }
212
222
  };
@@ -219,6 +229,7 @@ var PARAM_DESCRIPTIONS = {
219
229
  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(", ")}.`,
220
230
  provider: `Provider prefix filter. Array of provider slugs \u2014 a model matches if its ID starts with any of them (e.g., ['openai'] matches 'openai/gpt-4o'; ['openai','anthropic'] matches both). Pass a single-element array for one provider. Common providers: ${COMMON_PROVIDERS.join(", ")}.`,
221
231
  excludeFree: `When true, exclude models with id ending in ':free'. Useful for sortBy=price (which would otherwise be dominated by free-tier preview models) and when you want a paid SLA. Default false.`,
232
+ requireKeywordMatch: `When true, suppress weak vector-only results from semantic queries. If no candidate has a BM25 keyword hit, returns an empty page with meta.confidence='low' and meta.lowConfidenceReason \u2014 instead of returning misleading nearest-neighbor matches. Filter-only queries (sortBy=created or sortBy=price without q) ignore this flag. Default false.`,
222
233
  expectedPromptTokens: `Expected number of prompt tokens for dryRun cost estimation. When set, overrides the heuristic that counts characters from the literal \`prompt\` string \u2014 use this for capacity planning ("what would 6000-token reviews cost?") without pasting filler. If both are omitted, the prompt string is tokenized at ~4 chars/token.`,
223
234
  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.`
224
235
  };
@@ -235,184 +246,167 @@ var SITE = {
235
246
  },
236
247
  hero: {
237
248
  titleLine1: "Pick the right AI model",
238
- titleLine2: "without leaving your editor",
239
- subtitle: "Index9 is an MCP server. Your AI assistant searches, compares, and live-tests 300+ models on your prompt, so picks are measured, not guessed.",
240
- proof: ["Live OpenRouter data", "300+ models, refreshed every 30 min"],
249
+ titleLine2: "from chat",
250
+ subtitle: "Index9 is an MCP server. Your coding assistant uses it to search, compare, and live-test 300+ models on the task you're working on, so it recommends the best fit.",
251
+ proof: ["Live OpenRouter data \xB7 300+ models \xB7 refreshed every 30 min"],
241
252
  pricingNote: "Free. You only pay OpenRouter for live model calls.",
242
253
  getStarted: "Add index9 to your editor",
243
254
  seeHowItWorks: "See a real session",
244
255
  updatedBadge: "OpenRouter data \xB7 refreshed "
245
256
  },
257
+ problem: {
258
+ label: "Why this exists",
259
+ heading: "Your assistant's model knowledge is stale",
260
+ body: [
261
+ 'New models ship every week. Pricing changes. "Use GPT-4" or "use Claude 3.5" is usually months behind reality.',
262
+ "Without live data, your assistant defaults to whatever it learned in training \u2014 often a model that's been superseded by something cheaper or better-suited to your task.",
263
+ "Index9 gives it the data and the tools to actually compare."
264
+ ]
265
+ },
246
266
  howItWorks: {
247
267
  label: "How it works",
248
- heading: "Your assistant does the model-picking. You stay in the chat.",
249
- subtitle: "Index9 adds five MCP tools to your editor. When you ask about models, your assistant calls them, and gets live data back.",
268
+ heading: "How it works",
269
+ subtitle: "Index9 adds 5 tools to your editor. Your assistant calls them when you ask about models.",
250
270
  steps: [
251
271
  {
252
272
  number: "1",
253
- title: "You ask your assistant",
254
- body: '"Pick the cheapest model that can summarize this document well." Just chat. No new UI to learn.'
273
+ title: "You ask in chat",
274
+ body: '"Pick the cheapest model that can review TypeScript PRs well."'
255
275
  },
256
276
  {
257
277
  number: "2",
258
278
  title: "Your assistant calls index9",
259
- body: "It pulls live OpenRouter data: search results, full specs, cost diffs, and test outputs on your prompt."
279
+ body: "It searches live model data, compares finalists, and runs your prompt against the top candidates."
260
280
  },
261
281
  {
262
282
  number: "3",
263
- title: "You get a measured recommendation",
264
- body: "The assistant compares actual outputs and recommends the model that fits your constraints. Evidence, not guesswork."
283
+ title: "You get a measured pick",
284
+ body: "Backed by real cost numbers and real outputs \u2014 not training-data memory."
265
285
  }
266
286
  ]
267
287
  },
268
288
  caseStudy: {
269
289
  label: "Case study",
270
- heading: "A real session, not a mock",
271
- subheading: "A Claude Code session picking a TypeScript code-review model. Actual tool calls, decisions, and final pick. Captured 2026-04-24.",
290
+ heading: "A real session, not a mockup",
291
+ subheading: "A Claude Code session picking a TypeScript code-review model. Real tool calls, real verdict.",
272
292
  prompt: {
273
293
  title: "The prompt",
274
- body: "Pick 3 models for a TypeScript code-review bot. Test them on a sample PR diff and recommend the best one. Quality matters more than price."
294
+ body: "Pick a model for a TypeScript code-review bot that runs on every PR. I want real quality without paying frontier rates on routine reviews. Test against this sample diff."
275
295
  },
276
296
  toolCalls: {
277
- title: "Selection path",
297
+ title: "What the assistant did",
278
298
  subtitle: "in order",
279
299
  calls: [
300
+ { tool: "find_models", params: "newest first", note: "skip stale training picks" },
280
301
  {
281
302
  tool: "find_models",
282
- params: "sortBy=created, limit=10",
283
- note: "recent releases"
284
- },
285
- {
286
- tool: "find_models",
287
- params: 'q="code review reasoning", structured_output',
303
+ params: "code review + structured output",
288
304
  note: "task fit"
289
305
  },
290
- {
291
- tool: "find_models",
292
- params: 'q="not frontier price", maxPrice=6',
293
- note: "budget filter"
294
- },
295
- { tool: "get_models", params: "\xD7 12 candidates", note: "metadata lookup" },
306
+ { tool: "find_models", params: "max $2/M, every-PR budget", note: "rule out frontier" },
307
+ { tool: "get_models", params: "8 candidates", note: "metadata lookup" },
296
308
  {
297
309
  tool: "compare_models",
298
- params: "ids=[3 finalists], expectedPromptTokens=6000",
299
- note: "workload-cost flip"
310
+ params: "4 finalists, ~3000 token PR diff",
311
+ note: "per-PR cost projection"
300
312
  },
301
- { tool: "test_model", params: "dryRun \xD7 2", note: "cost estimate" },
313
+ { tool: "test_model", params: "dry-run \xD7 4", note: "cost estimate" },
302
314
  {
303
315
  tool: "test_model",
304
- params: "live \xD7 2, enforceJson=true",
305
- note: "real inference"
316
+ params: "live \xD7 4, JSON output",
317
+ note: "real bug-catch test"
306
318
  }
307
319
  ]
308
320
  },
309
- consideredTitle: "Every recent model, evaluated",
310
- consideredSubtitle: "Recent releases were checked with explicit accept, test, or skip decisions.",
321
+ consideredTitle: "Recent models, evaluated",
322
+ consideredSubtitle: "A trimmed view of the candidates the assistant ruled in and out. Each row pairs a decision with the reason behind it.",
311
323
  consideredRows: [
312
- {
313
- id: "openai/gpt-5.5-pro",
314
- age: "6h ago",
315
- decision: "skip",
316
- reason: "too expensive for every PR"
317
- },
318
324
  {
319
325
  id: "openai/gpt-5.5",
320
- age: "6h ago",
326
+ age: "1d ago",
321
327
  decision: "skip",
322
- reason: "frontier-priced vs Codex"
328
+ reason: "~$0.027 per PR, 5\xD7 the pick for the same outcome"
323
329
  },
324
330
  {
325
- id: "deepseek/deepseek-v4-pro",
326
- age: "14h ago",
331
+ id: "xiaomi/mimo-v2.5-pro",
332
+ age: "3d ago",
327
333
  decision: "tested",
328
- reason: "live test hit upstream 429 twice"
334
+ reason: "good structure, missed the precision edge case"
329
335
  },
330
336
  {
331
337
  id: "deepseek/deepseek-v4-flash",
332
- age: "14h ago",
333
- decision: "skip",
334
- reason: "cheaper sibling, lower quality expected"
335
- },
336
- {
337
- id: "xiaomi/mimo-v2.5-pro",
338
- age: "2d ago",
339
- decision: "shortlisted",
340
- reason: "recent + reasoning + structured output"
341
- },
342
- {
343
- id: "inclusionai/ling-2.6-1t:free",
344
338
  age: "1d ago",
345
- decision: "skip",
346
- reason: "no reasoning capability flag"
339
+ decision: "tested",
340
+ reason: "7\xD7 cheaper than the pick, but missed both bugs"
347
341
  },
348
342
  {
349
- id: "arcee-ai/trinity-large-thinking",
350
- age: "3w ago",
351
- decision: "skip",
352
- reason: "MiMo Pro had stronger positioning"
343
+ id: "z-ai/glm-5.1",
344
+ age: "2w ago",
345
+ decision: "shortlisted",
346
+ reason: "caught both bugs at ~$0.005 per PR"
353
347
  }
354
348
  ],
355
349
  verdict: {
356
- title: "The final pick",
357
- model: "openai/gpt-5.3-codex",
358
- body: "The only tested model that caught both sample bugs. About $0.015 per PR: higher than budget models, far below frontier rates."
350
+ title: "The pick",
351
+ model: "z-ai/glm-5.1",
352
+ body: "Open-weight, $1.05 per million input tokens. Caught both bugs in the sample diff at roughly $0.005 per PR, about 5\xD7 cheaper than running gpt-5.5 on every commit."
359
353
  },
360
354
  quote: {
361
- body: "The cheapest candidate ran 4\xD7 cheaper than Codex, and missed both bugs in the sample diff. The only way to know was a live test.",
362
- attribution: "index9 session trace, 2026-04-24"
355
+ body: "The frontier model would have caught both bugs, at 5\xD7 the cost. The cheapest candidate missed them entirely. Only the live test surfaced the model that did both.",
356
+ attribution: "index9 session trace"
363
357
  }
364
358
  },
365
359
  toolsSection: {
366
360
  label: "Tools",
367
- heading: "Five MCP tools, composable in any client",
368
- subheading: "Discover, shortlist, compare, cost-model, and live-test. Your assistant chains them together to make a measured pick.",
369
- openRouterKey: "OpenRouter API key (live tests only)",
370
- noKeyRequired: "No API key required",
361
+ heading: "The 5 tools",
362
+ subheading: "Your assistant chains these together. You don't call them directly.",
363
+ openRouterKey: "OpenRouter API key",
364
+ noKeyRequired: "No key required",
371
365
  requiresLabel: "Requires ",
372
366
  cards: [
373
367
  {
374
368
  name: "list_facets",
375
- action: "Discover",
369
+ action: "list_facets",
376
370
  displayName: "list_facets",
377
371
  fullName: null,
378
- description: "List the live filter vocabulary (providers, capabilities, modalities) before constructing a search.",
372
+ description: "Lists available providers, capabilities, and modalities to filter by.",
379
373
  badge: null,
380
374
  requiresKey: false
381
375
  },
382
376
  {
383
377
  name: "find_models",
384
- action: "Shortlist",
378
+ action: "find_models",
385
379
  displayName: "find_models",
386
380
  fullName: null,
387
- description: `Filter ${MODEL_COUNT} models by price, context, and capabilities. Natural-language search refines the ranking.`,
381
+ description: `Searches ${MODEL_COUNT} models by price, context size, capabilities, or natural language.`,
388
382
  badge: null,
389
383
  requiresKey: false
390
384
  },
391
385
  {
392
386
  name: "get_models",
393
- action: "Inspect",
387
+ action: "get_models",
394
388
  displayName: "get_models",
395
389
  fullName: null,
396
- description: "Inspect current pricing, limits, and capabilities for any model.",
390
+ description: "Pulls full specs and current pricing for any model.",
397
391
  badge: null,
398
392
  requiresKey: false
399
393
  },
400
394
  {
401
395
  name: "compare_models",
402
- action: "Compare",
396
+ action: "compare_models",
403
397
  displayName: "compare_models",
404
398
  fullName: null,
405
- description: "Side-by-side spec, capability, and workload-cost diff for 2\u201310 finalists.",
399
+ description: "Diffs 2\u201310 finalists side-by-side. Flags the cheapest pick for your expected token mix.",
406
400
  badge: null,
407
401
  requiresKey: false
408
402
  },
409
403
  {
410
404
  name: "test_model",
411
- action: "Run live tests",
405
+ action: "test_model",
412
406
  displayName: "test_model",
413
407
  fullName: null,
414
- description: "Run one prompt across models and compare output, latency, and cost.",
415
- badge: "Live Testing",
408
+ description: "Runs your prompt across models. Returns output, latency, and real cost. Or dry-run for cost only.",
409
+ badge: "Live",
416
410
  requiresKey: true
417
411
  }
418
412
  ]
@@ -423,7 +417,7 @@ var SITE = {
423
417
  items: [
424
418
  {
425
419
  question: "What is MCP?",
426
- answer: "MCP (Model Context Protocol) lets AI assistants call external tools. index9 adds five composable tools (list_facets, find_models, get_models, compare_models, test_model) to any MCP-compatible client.",
420
+ answer: "A protocol that lets AI assistants call external tools. Index9 is one of those tools.",
427
421
  link: {
428
422
  label: "Learn more about MCP",
429
423
  url: "https://modelcontextprotocol.io"
@@ -431,45 +425,40 @@ var SITE = {
431
425
  },
432
426
  {
433
427
  question: "Who is index9 for?",
434
- answer: "Developers using AI coding assistants (Claude Code, Cursor, Codex, VS Code) who want their assistant to pick models based on live cost and quality data, not training-data guesses.",
428
+ answer: "Developers using Claude Code, Cursor, VS Code, or Codex who want their assistant to pick models from current data instead of training-data memory.",
435
429
  link: null
436
430
  },
437
431
  {
438
- question: "How does live testing work?",
439
- answer: `test_model sends your prompt to 1\u2013${LIMITS.testModelsMax} models via OpenRouter and returns output, latency, token usage, and cost. Live tests require an OpenRouter key; dryRun=true only estimates cost (pass expectedPromptTokens to model larger workloads without pasting filler).`,
432
+ question: "Does it pick the model for me?",
433
+ answer: "No \u2014 it gives your assistant the data (search results, specs, cost diffs, live test outputs). Your assistant makes the call.",
440
434
  link: null
441
435
  },
442
436
  {
443
- question: "Does index9 recommend which model to use?",
444
- answer: "index9 returns outputs, latency, cost, and specs. Your assistant uses those results to make the recommendation.",
437
+ question: "How does live testing work?",
438
+ answer: `test_model sends your prompt to up to ${LIMITS.testModelsMax} models via OpenRouter and returns output, latency, tokens, and cost. Dry-run mode estimates cost without running inference.`,
445
439
  link: null
446
440
  },
447
441
  {
448
- question: "How is compare_models different from calling get_models on each candidate?",
449
- answer: "compare_models returns a diff matrix (which fields are equal, which differ), plus convenience picks: cheapestForPromptPerMillion, largestContext, and (when you pass expectedPromptTokens + expectedCompletionTokens) cheapestForRealisticWorkload accounting for prompt:completion ratio differences. One call instead of N parallel get_models calls plus manual diffing.",
442
+ question: "Which models?",
443
+ answer: `${MODEL_COUNT} from OpenRouter \u2014 OpenAI, Anthropic, Google, Meta, Mistral, DeepSeek, and more. Metadata refreshes every 30 minutes.`,
450
444
  link: null
451
445
  },
452
446
  {
453
- question: "What models are available?",
454
- answer: `index9 covers ${MODEL_COUNT} OpenRouter models, including OpenAI, Anthropic, Google, Meta, Mistral, and others. Metadata refreshes every 30 minutes.`,
447
+ question: "Do you store my prompts or keys?",
448
+ answer: "No. Index9 doesn't store prompts, outputs, or API keys. Live tests are proxied straight to OpenRouter.",
455
449
  link: null
456
450
  },
457
451
  {
458
452
  question: "What's the project status?",
459
- answer: "The hosted API and MCP server are stable and in active use. Issues and feature requests welcome on GitHub.",
460
- link: null
461
- },
462
- {
463
- question: "Is my data stored?",
464
- answer: "No. index9 does not store prompts, outputs, or API keys. Live tests are proxied to OpenRouter.",
453
+ answer: "Stable and in active use. Issues and feature requests welcome on GitHub.",
465
454
  link: null
466
455
  }
467
456
  ]
468
457
  },
469
458
  install: {
470
459
  label: "Setup",
471
- heading: "Add index9 to your editor",
472
- subheading: "Choose your client and copy the config.",
460
+ heading: "Install",
461
+ subheading: "Pick your editor and paste the config.",
473
462
  configs: [
474
463
  {
475
464
  id: "cursor",
@@ -501,14 +490,14 @@ var SITE = {
501
490
  {
502
491
  id: "claude-code",
503
492
  label: "Claude Code",
504
- paths: ["Terminal command"],
493
+ paths: [],
505
494
  config: `claude mcp add --transport stdio index9 -- npx -y @index9/mcp@latest`,
506
495
  copyHint: "# Run in terminal (adds to ~/.claude.json)"
507
496
  },
508
497
  {
509
498
  id: "codex",
510
499
  label: "Codex",
511
- paths: ["Terminal command"],
500
+ paths: [],
512
501
  config: `codex mcp add index9 -- npx -y @index9/mcp@latest`,
513
502
  copyHint: "# Run in terminal (adds to ~/.codex/config.toml)"
514
503
  }
@@ -516,8 +505,9 @@ var SITE = {
516
505
  copyButton: "Copy",
517
506
  copiedButton: "Copied",
518
507
  copyAriaLabel: "Copy configuration",
519
- setupNote: "Need live tests or custom setup?",
520
- setupLink: "Set up live testing",
508
+ copiedAnnouncement: "Configuration copied to clipboard",
509
+ setupNote: "Want live tests?",
510
+ setupLink: "Add an OpenRouter key",
521
511
  setupUrl: "https://github.com/index9-org/mcp#openrouter-api-key"
522
512
  },
523
513
  footer: {
@@ -577,7 +567,8 @@ var SearchQuerySchema = z2.object({
577
567
  capabilitiesAny: z2.array(z2.enum(CAPABILITIES)).optional(),
578
568
  modality: z2.enum(OUTPUT_MODALITIES).optional(),
579
569
  provider: z2.array(z2.string().min(1)).optional(),
580
- excludeFree: z2.boolean().optional()
570
+ excludeFree: z2.boolean().optional(),
571
+ requireKeywordMatch: z2.boolean().optional()
581
572
  }).strict();
582
573
  var SearchResultSchema = z2.object({
583
574
  id: z2.string(),
@@ -589,7 +580,11 @@ var SearchResultSchema = z2.object({
589
580
  maxOutputTokens: z2.number().nullable(),
590
581
  pricing: z2.object({
591
582
  promptPerMillion: z2.number().nullable(),
592
- completionPerMillion: z2.number().nullable()
583
+ completionPerMillion: z2.number().nullable(),
584
+ promptPerToken: z2.number().nullable().optional(),
585
+ completionPerToken: z2.number().nullable().optional(),
586
+ requestUsd: z2.number().nullable().optional(),
587
+ effectivePromptPerMillion: z2.number().nullable().optional()
593
588
  }),
594
589
  inputModalities: z2.array(z2.string()),
595
590
  outputModalities: z2.array(z2.string()),
@@ -603,13 +598,15 @@ var SearchResponseSchema = z2.object({
603
598
  limit: z2.number(),
604
599
  hasMore: z2.boolean(),
605
600
  sortBy: SearchSortBySchema,
606
- sortOrder: SearchSortOrderSchema
601
+ sortOrder: SearchSortOrderSchema,
602
+ priceSortBasis: z2.literal("effective_prompt_per_million").optional()
607
603
  }),
608
604
  meta: z2.object({
609
605
  queryMode: z2.enum(["semantic", "filter_only"]),
610
606
  ranking: z2.literal("hybrid_rrf"),
611
607
  confidence: z2.enum(["high", "low"]).optional(),
612
- suggestion: z2.string().optional()
608
+ suggestion: z2.string().optional(),
609
+ lowConfidenceReason: z2.enum(["no_keyword_matches", "no_results"]).optional()
613
610
  })
614
611
  });
615
612
  var FindModelsToolResultSchema = SearchResponseSchema.extend({
@@ -625,6 +622,8 @@ var BatchModelLookupRequestSchema = z3.object({
625
622
  var ModelPricingSchema = z3.object({
626
623
  promptPerMillion: z3.number().nullable(),
627
624
  completionPerMillion: z3.number().nullable(),
625
+ promptPerToken: z3.number().nullable().optional(),
626
+ completionPerToken: z3.number().nullable().optional(),
628
627
  requestUsd: z3.number().nullable(),
629
628
  imageUsd: z3.number().nullable()
630
629
  });
@@ -655,7 +654,8 @@ var BatchModelLookupResponseSchema = z3.object({
655
654
  missingIds: z3.array(z3.string()),
656
655
  resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
657
656
  ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional(),
658
- suggestions: z3.record(z3.string(), z3.array(z3.string())).optional()
657
+ suggestions: z3.record(z3.string(), z3.array(z3.string())).optional(),
658
+ missingDiagnostics: z3.record(z3.string(), MissingModelDiagnosticSchema).optional()
659
659
  }).strict();
660
660
  var GetModelsToolResultSchema = z3.object({
661
661
  results: z3.array(ModelResponseSchema.nullable()),
@@ -663,11 +663,13 @@ var GetModelsToolResultSchema = z3.object({
663
663
  resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
664
664
  ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional(),
665
665
  suggestions: z3.record(z3.string(), z3.array(z3.string())).optional(),
666
+ missingDiagnostics: z3.record(z3.string(), MissingModelDiagnosticSchema).optional(),
666
667
  _index9: Index9MetaSchema
667
668
  });
668
669
 
669
670
  // ../core/dist/schemas/compare.js
670
671
  import { z as z4 } from "zod";
672
+ var PricingBasisSchema = z4.enum(["exact_per_token", "rounded_per_million", "unavailable"]);
671
673
  var CompareRequestSchema = z4.object({
672
674
  ids: z4.array(z4.string().min(1)).min(2, "compare requires at least 2 ids").max(LIMITS.compareModelsMax, `ids must contain between 2 and ${LIMITS.compareModelsMax} model IDs`),
673
675
  expectedPromptTokens: z4.number().int().positive().optional(),
@@ -704,7 +706,10 @@ var CompareWorkloadCostSchema = z4.object({
704
706
  modelId: z4.string(),
705
707
  promptTokens: z4.number().int().nonnegative(),
706
708
  completionTokens: z4.number().int().nonnegative(),
707
- totalCostUsd: z4.number().nullable()
709
+ totalCostUsd: z4.number().nullable(),
710
+ tokenCostUsd: z4.number().nullable().optional(),
711
+ requestCostUsd: z4.number().nullable().optional(),
712
+ pricingBasis: PricingBasisSchema.optional()
708
713
  });
709
714
  var CompareResponseSchema = z4.object({
710
715
  models: z4.array(ModelResponseSchema),
@@ -716,7 +721,8 @@ var CompareResponseSchema = z4.object({
716
721
  resolvedAliases: z4.record(z4.string(), z4.string()).optional(),
717
722
  missingIds: z4.array(z4.string()),
718
723
  suggestions: z4.record(z4.string(), z4.array(z4.string())).optional(),
719
- ambiguousAliases: z4.record(z4.string(), z4.array(z4.string())).optional()
724
+ ambiguousAliases: z4.record(z4.string(), z4.array(z4.string())).optional(),
725
+ missingDiagnostics: z4.record(z4.string(), MissingModelDiagnosticSchema).optional()
720
726
  }).strict();
721
727
  var CompareModelsToolResultSchema = CompareResponseSchema.extend({
722
728
  _index9: Index9MetaSchema
@@ -791,8 +797,17 @@ var TestPricingUsedSchema = z6.object({
791
797
  promptPerToken: z6.number().nullable().optional(),
792
798
  completionPerToken: z6.number().nullable().optional(),
793
799
  promptPerMillion: z6.number().nullable().optional(),
794
- completionPerMillion: z6.number().nullable().optional()
800
+ completionPerMillion: z6.number().nullable().optional(),
801
+ requestUsd: z6.number().nullable().optional()
795
802
  });
803
+ var TestFailureReasonSchema = z6.enum([
804
+ "insufficient_credits",
805
+ "model_unavailable",
806
+ "rate_limited",
807
+ "timeout",
808
+ "invalid_request",
809
+ "unknown"
810
+ ]);
796
811
  var TestModelMetadataSchema = z6.object({
797
812
  id: z6.string(),
798
813
  name: z6.string(),
@@ -817,6 +832,7 @@ var TestResultFailureSchema = z6.object({
817
832
  ok: z6.literal(false),
818
833
  model: TestModelMetadataSchema,
819
834
  error: z6.string(),
835
+ failureReason: TestFailureReasonSchema.optional(),
820
836
  latencyMs: z6.number().min(0)
821
837
  });
822
838
  var TestResultSchema = z6.discriminatedUnion("ok", [
@@ -828,7 +844,11 @@ var TestEstimateResultSchema = z6.object({
828
844
  resolvedModelId: z6.string().optional(),
829
845
  model: TestModelMetadataSchema,
830
846
  tokens: UsageTokensSchema,
831
- estimatedCost: z6.number().nullable().optional()
847
+ estimatedCost: z6.number().nullable().optional(),
848
+ tokenCostUsd: z6.number().nullable().optional(),
849
+ requestCostUsd: z6.number().nullable().optional(),
850
+ totalCostUsd: z6.number().nullable().optional(),
851
+ estimatedCostBasis: PricingBasisSchema.optional()
832
852
  });
833
853
  var TestDryRunResponseSchema = z6.object({
834
854
  dryRun: z6.literal(true),
@@ -1026,6 +1046,7 @@ async function handleSearchModels(ctx, args) {
1026
1046
  if (q.modality) params.modality = q.modality;
1027
1047
  if (q.provider?.length) params.provider = q.provider.join(",");
1028
1048
  if (q.excludeFree === true) params.excludeFree = "true";
1049
+ if (q.requireKeywordMatch === true) params.requireKeywordMatch = "true";
1029
1050
  return callApi(
1030
1051
  ctx,
1031
1052
  buildUrl(ctx.baseUrl, API_PATHS.search, params),
@@ -1117,7 +1138,8 @@ async function createServer() {
1117
1138
  capabilitiesAny: z7.array(z7.enum(CAPABILITIES)).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAny),
1118
1139
  modality: z7.enum(OUTPUT_MODALITIES).optional().describe(PARAM_DESCRIPTIONS.modality),
1119
1140
  provider: z7.array(z7.string().min(1)).optional().describe(PARAM_DESCRIPTIONS.provider),
1120
- excludeFree: z7.boolean().optional().describe(PARAM_DESCRIPTIONS.excludeFree)
1141
+ excludeFree: z7.boolean().optional().describe(PARAM_DESCRIPTIONS.excludeFree),
1142
+ requireKeywordMatch: z7.boolean().optional().describe(PARAM_DESCRIPTIONS.requireKeywordMatch)
1121
1143
  },
1122
1144
  outputSchema: FindModelsToolResultSchema.shape,
1123
1145
  annotations: { readOnlyHint: true }
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "index9",
4
- "version": "5.2.0",
4
+ "version": "6.0.0",
5
5
  "description": "Discover, shortlist, compare, cost-model, and live-test 300+ AI models from your editor",
6
6
  "author": {
7
7
  "name": "Index9"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@index9/mcp",
3
- "version": "5.3.0",
3
+ "version": "6.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,14 +21,14 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.29.0",
24
- "zod": "^4.3.6"
24
+ "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {
27
- "@types/node": "^25.6.0",
27
+ "@types/node": "^25.6.1",
28
28
  "tsup": "^8.5.1",
29
29
  "typescript": "6.0.3",
30
30
  "vitest": "^4.1.5",
31
- "@index9/core": "2.3.2"
31
+ "@index9/core": "2.4.0"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=20"