@index9/mcp 4.0.2 → 5.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/dist/cli.js CHANGED
@@ -31,6 +31,16 @@ var COMMON_PROVIDERS = [
31
31
  // ../core/dist/schemas/common.js
32
32
  import { z } from "zod";
33
33
  var ErrorResponseSchema = z.object({ error: z.string() }).strict();
34
+ var RateLimitMetaSchema = z.object({
35
+ limit: z.string().optional(),
36
+ remaining: z.string().optional(),
37
+ reset: z.string().optional()
38
+ });
39
+ var Index9MetaSchema = z.object({
40
+ apiBaseUrl: z.string(),
41
+ retryAfterSeconds: z.number().optional(),
42
+ rateLimit: RateLimitMetaSchema.optional()
43
+ });
34
44
  var UserContentTextPartSchema = z.strictObject({
35
45
  type: z.literal("text"),
36
46
  text: z.string().trim().min(1)
@@ -68,64 +78,74 @@ var LIMITS = {
68
78
  var MODEL_COUNT = "300+";
69
79
  var WORKFLOW_INSTRUCTIONS = `Index9 provides 3 tools for AI model discovery, inspection, and benchmarking.
70
80
 
81
+ IMPORTANT \u2014 your model knowledge is stale by default.
82
+ New AI models ship weekly; pricing, aliases, and capabilities change. Treat any specific model ID, "flagship" name, or "good default" you recall from training as potentially out of date. Before naming, recommending, or benchmarking models, call find_models to anchor on what is actually live right now. Good first calls:
83
+ - sortBy=created (newest first) to see what has recently landed
84
+ - a semantic query describing the task to see current task-fit candidates
85
+ Skip find_models only when the user has typed a specific provider/model-id.
86
+
71
87
  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.
88
+ 1. find_models \u2014 Discover models by semantic query or filters. Start here unless the user named a specific model.
89
+ 2. get_models \u2014 Full metadata for specific IDs or aliases. Use after search, or directly when the user names a model.
90
+ 3. test_model \u2014 Run live inference, or set dryRun=true to estimate token usage/cost without running inference.
75
91
 
76
92
  Key rules:
77
93
  - 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.
94
+ - get_models accepts aliases (display names, short names) \u2014 not just full IDs. If a model you "know from memory" returns in missingIds, that is a signal to call find_models.
79
95
  - Use test_model with \`dryRun=true\` to estimate cost before live testing.
80
96
  - test_model with \`dryRun=false\` (default) requires OPENROUTER_API_KEY and incurs real usage costs.
97
+ - Reasoning-capable models (capabilities includes "reasoning") burn hidden reasoning tokens against \`maxTokens\` before emitting visible text. Leave \`maxTokens\` unset, or set it to at least 2000, when testing reasoning models \u2014 otherwise results may fail with finish_reason=length.
81
98
  - Cursors are opaque and tied to query/sort/filters. Reuse the same query/sort/filters when paginating. \`limit\` may change between pages.`;
82
99
  var TOOLS = {
83
100
  find_models: {
84
101
  name: "find_models",
102
+ title: "Search AI models",
85
103
  summary: "Search and paginate AI models by semantic query or filters",
86
104
  description: `Search and filter ${MODEL_COUNT} AI models. Returns ranked results with pricing, context windows, and capabilities.
87
105
 
88
- Call this tool first to discover model IDs, unless the user provides one (format: 'provider/model-name').
106
+ Call this first unless the user named a specific model \u2014 your training-data list of "good" models is likely stale.
89
107
 
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.
108
+ Extract filters from user queries. \`q\` drives semantic ranking; numeric and categorical constraints MUST go in structured filters. Shorthand: 1K=1000, 1M=1000000. Prices are USD per million input tokens.
92
109
 
93
110
  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)
111
+ - "1M context under $1" \u2192 q="model", minContext=1000000, maxPrice=1
112
+ - "cheap vision model from openai" \u2192 q="cheap vision model", capabilitiesAll=["vision"], provider="openai"
113
+ - "function calling under $0.50 with 128K" \u2192 q="function calling", capabilitiesAll=["function_calling"], maxPrice=0.5, minContext=128000
114
+ - "best coding model" \u2192 q="best coding model"
115
+ - "what's new" \u2192 sortBy="created" (no q needed)
98
116
 
99
- Convert shorthand: 1K=1000, 1M=1000000, 128K=128000. Prices are in USD per million input tokens.
117
+ Valid capabilities: ${CAPABILITIES.join(", ")}.
100
118
 
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
119
+ Each result: id, name, description, created (unix seconds), createdAt (ISO 8601), contextLength, maxOutputTokens, pricing.{promptPerMillion, completionPerMillion} (numbers, USD per million tokens), capabilities[], score.
111
120
 
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.
121
+ \`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.
113
122
 
114
- Use model IDs from results with get_models for full specs or test_model for live testing.`,
123
+ Pass result.id to get_models for full specs or to test_model for live testing.`,
115
124
  requiresKey: false
116
125
  },
117
126
  get_models: {
118
127
  name: "get_models",
128
+ title: "Inspect AI models",
119
129
  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.
130
+ description: `Get full specs for one or more models by id or alias. Accepts up to ${LIMITS.getModelsMax} ids per call \u2014 use this for batch comparison.
131
+
132
+ Call after find_models to inspect candidates, or directly when the user names a model (format: 'provider/model-name').
121
133
 
122
- Call after find_models to get full details, or when the user provides a model ID (format: 'provider/model-name').
134
+ Response: { results: (Model | null)[], missingIds: string[], resolvedAliases?: Record<alias, canonicalId>, ambiguousAliases?: Record<alias, candidateIds[]> }. Each non-null result has:
135
+ - id, canonicalSlug, name, description
136
+ - created (unix seconds), createdAt (ISO 8601), knowledgeCutoff (ISO date or null)
137
+ - contextLength (tokens), maxOutputTokens, isModerated
138
+ - pricing: { promptPerMillion, completionPerMillion, requestUsd, imageUsd } \u2014 all USD, all numbers. Token prices are per million tokens; request/image are per unit.
139
+ - architecture: { inputModalities[], outputModalities[], tokenizer, instructType }
140
+ - capabilities[]: normalized capability flags (same values as find_models and capabilitiesAll/Any)
141
+ - supportedParameters[]: OpenRouter parameters the model accepts (e.g., "temperature", "tools", "response_format")
123
142
 
124
- Returns 404 if model not found. Use find_models to discover valid IDs.`,
143
+ 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.`,
125
144
  requiresKey: false
126
145
  },
127
146
  test_model: {
128
147
  name: "test_model",
148
+ title: "Test AI models live",
129
149
  summary: "Run live inference or dry-run cost estimation across up to 10 models",
130
150
  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
151
 
@@ -140,9 +160,9 @@ Parameters:
140
160
  - prompt: Prompt text (required for dryRun; required for live unless userContent provided)
141
161
  - dryRun: If true, return cost estimates only
142
162
  - 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)
163
+ - maxTokens, systemPrompt, temperature, topP, seed, responseFormat, enforceJson, retries: Live-testing controls (ignored when dryRun=true)
144
164
 
145
- Use find_models or get_models first to identify model IDs.`,
165
+ 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.`,
146
166
  requiresKey: true
147
167
  }
148
168
  };
@@ -150,15 +170,17 @@ var PARAM_DESCRIPTIONS = {
150
170
  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
171
  sortBy: `Sort order for results. Options: 'relevance' (best semantic match, default), 'created' (newest models), 'price' (cheapest/most expensive, with sortOrder). Defaults to 'relevance'.`,
152
172
  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(", ")}.`,
173
+ capabilitiesAll: `Array of capabilities that must ALL be present on the model (AND logic). Valid values: ${CAPABILITIES.join(", ")}. Example: ["function_calling","vision"].`,
174
+ capabilitiesAny: `Array of capabilities where at least ONE must be present (OR logic). Valid values: ${CAPABILITIES.join(", ")}. Example: ["vision","audio_input"].`,
175
+ 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
176
  provider: `Provider prefix filter. Matches model IDs starting with this prefix (e.g., 'openai' matches 'openai/gpt-4o'). Common providers: ${COMMON_PROVIDERS.join(", ")}.`,
157
177
  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
178
  };
159
179
  var SITE = {
160
180
  nav: {
161
181
  brand: "index9",
182
+ tools: "Tools",
183
+ howItWorks: "How it works",
162
184
  install: "Install",
163
185
  faq: "FAQ",
164
186
  github: "GitHub",
@@ -168,19 +190,47 @@ var SITE = {
168
190
  titleLine1: "Test AI models on your actual prompts, ",
169
191
  titleLine2: "not generic benchmarks",
170
192
  subtitle: "Compare quality, speed, and cost across 300+ models \u2014 in Cursor, VS Code, or Claude Code.",
171
- getStarted: "Install index9",
193
+ identity: "index9 is an MCP server that lets your AI coding assistant search, compare, and live-test models from inside your editor.",
194
+ audiencePrefix: "Built for",
195
+ audience: ["AI engineers", "Indie developers", "Teams standardizing on models"],
196
+ pricingNote: "Free. You only pay OpenRouter for live model calls.",
197
+ getStarted: "Add to your editor",
172
198
  seeHowItWorks: "See an example",
173
- updatedBadge: "Pricing & specs refreshed "
199
+ updatedBadge: "Model pricing & specs from OpenRouter \u2014 refreshed "
200
+ },
201
+ howItWorks: {
202
+ label: "How it works",
203
+ heading: "Your assistant picks the right model, automatically",
204
+ subtitle: "index9 runs as an MCP (Model Context Protocol) server. Your editor already speaks MCP \u2014 index9 just plugs in as three extra tools your assistant can call.",
205
+ steps: [
206
+ {
207
+ number: "1",
208
+ title: "You ask your assistant",
209
+ body: '"Pick the cheapest model that can summarize this document well." You chat normally \u2014 no new UI to learn.'
210
+ },
211
+ {
212
+ number: "2",
213
+ title: "Your assistant calls index9",
214
+ body: "It runs find_models, get_models, and test_model against live OpenRouter data. Results come back with latency, tokens, and cost for your prompt."
215
+ },
216
+ {
217
+ number: "3",
218
+ title: "You get a measured recommendation",
219
+ body: "The assistant compares real outputs on your real prompt, then recommends the model that fits your constraints. Evidence, not guesswork."
220
+ }
221
+ ]
174
222
  },
175
223
  toolsSection: {
176
224
  label: "Tools",
177
- heading: "Search, test, and compare",
225
+ heading: "Search, inspect, and run live tests",
226
+ subheading: "Three MCP tools your assistant can call. Each maps to one clear job your assistant can do without leaving the chat.",
178
227
  openRouterKey: "OpenRouter API key (live tests only)",
179
228
  noKeyRequired: "No API key required",
180
229
  requiresLabel: "Requires ",
181
230
  cards: [
182
231
  {
183
232
  name: "find_models",
233
+ action: "Search",
184
234
  displayName: "find_models",
185
235
  fullName: null,
186
236
  description: `Search ${MODEL_COUNT} models by what you need \u2014 price, speed, context window, or capabilities like vision and function calling.`,
@@ -189,14 +239,16 @@ var SITE = {
189
239
  },
190
240
  {
191
241
  name: "get_models",
242
+ action: "Inspect",
192
243
  displayName: "get_models",
193
244
  fullName: null,
194
- description: "Get current pricing, limits, and capabilities for any model. Updated from OpenRouter every 30 minutes.",
245
+ description: "Get current pricing, limits, and capabilities for any model. Synced from OpenRouter every 30 minutes.",
195
246
  badge: null,
196
247
  requiresKey: false
197
248
  },
198
249
  {
199
250
  name: "test_model",
251
+ action: "Run live tests",
200
252
  displayName: "test_model",
201
253
  fullName: null,
202
254
  description: "Send your prompt to multiple models. Compare outputs, latency, and cost \u2014 measured, not estimated.",
@@ -234,7 +286,7 @@ var SITE = {
234
286
  },
235
287
  {
236
288
  question: "What's the project status?",
237
- 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.",
289
+ answer: "index9 is live and used daily. Core tools are stable. Improvements ship through changesets on GitHub; issues and feedback are welcome there.",
238
290
  link: null
239
291
  },
240
292
  {
@@ -246,7 +298,8 @@ var SITE = {
246
298
  },
247
299
  install: {
248
300
  label: "Setup",
249
- heading: "Add to your MCP config",
301
+ heading: "Add index9 to your editor",
302
+ subheading: "One line of config for Cursor, VS Code, or Claude Code. Your assistant can start using it immediately.",
250
303
  configs: [
251
304
  {
252
305
  id: "cursor-vscode",
@@ -288,6 +341,7 @@ var SITE = {
288
341
  comparison: {
289
342
  label: "Comparison",
290
343
  heading: "Evidence over intuition",
344
+ subheading: "Benchmark on your real prompts \u2014 not someone else's.",
291
345
  withoutLabel: "Without index9",
292
346
  withLabel: "With index9",
293
347
  withoutItems: [
@@ -439,16 +493,16 @@ var SearchQuerySchema = z2.object({
439
493
  minPrice: z2.number().min(0).optional(),
440
494
  maxPrice: z2.number().min(0).optional(),
441
495
  minContext: z2.number().int().min(1).optional(),
442
- capabilitiesAll: z2.array(z2.string().min(1)).optional(),
443
- capabilitiesAny: z2.array(z2.string().min(1)).optional(),
444
- modality: z2.string().min(1).optional(),
496
+ capabilitiesAll: z2.array(z2.enum(CAPABILITIES)).optional(),
497
+ capabilitiesAny: z2.array(z2.enum(CAPABILITIES)).optional(),
498
+ modality: z2.enum(OUTPUT_MODALITIES).optional(),
445
499
  provider: z2.string().min(1).optional()
446
500
  }).strict();
447
501
  var SearchResultSchema = z2.object({
448
- modelId: z2.string(),
502
+ id: z2.string(),
449
503
  name: z2.string(),
450
504
  description: z2.string(),
451
- createdUnix: z2.number().nullable(),
505
+ created: z2.number().nullable(),
452
506
  createdAt: z2.string().nullable(),
453
507
  contextLength: z2.number().nullable(),
454
508
  maxOutputTokens: z2.number().nullable(),
@@ -473,6 +527,9 @@ var SearchResponseSchema = z2.object({
473
527
  ranking: z2.literal("hybrid_rrf")
474
528
  })
475
529
  });
530
+ var FindModelsToolResultSchema = SearchResponseSchema.extend({
531
+ _index9: Index9MetaSchema
532
+ });
476
533
 
477
534
  // ../core/dist/schemas/model.js
478
535
  import { z as z3 } from "zod";
@@ -480,13 +537,47 @@ var BatchModelLookupRequestSchema = z3.object({
480
537
  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`),
481
538
  maxDescriptionChars: z3.number().int().min(0).max(2e3).optional()
482
539
  }).strict();
483
- var ModelResponseSchema = z3.record(z3.string(), z3.unknown());
540
+ var ModelPricingSchema = z3.object({
541
+ promptPerMillion: z3.number().nullable(),
542
+ completionPerMillion: z3.number().nullable(),
543
+ requestUsd: z3.number().nullable(),
544
+ imageUsd: z3.number().nullable()
545
+ });
546
+ var ModelArchitectureSchema = z3.object({
547
+ inputModalities: z3.array(z3.string()),
548
+ outputModalities: z3.array(z3.string()),
549
+ tokenizer: z3.string().nullable(),
550
+ instructType: z3.string().nullable()
551
+ });
552
+ var ModelResponseSchema = z3.object({
553
+ id: z3.string(),
554
+ canonicalSlug: z3.string().nullable(),
555
+ name: z3.string(),
556
+ description: z3.string(),
557
+ created: z3.number().nullable(),
558
+ createdAt: z3.string().nullable(),
559
+ knowledgeCutoff: z3.string().nullable(),
560
+ contextLength: z3.number().nullable(),
561
+ maxOutputTokens: z3.number().nullable(),
562
+ isModerated: z3.boolean().nullable(),
563
+ pricing: ModelPricingSchema,
564
+ architecture: ModelArchitectureSchema,
565
+ capabilities: z3.array(z3.string()),
566
+ supportedParameters: z3.array(z3.string())
567
+ });
484
568
  var BatchModelLookupResponseSchema = z3.object({
485
569
  results: z3.array(ModelResponseSchema.nullable()),
486
570
  missingIds: z3.array(z3.string()),
487
571
  resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
488
572
  ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional()
489
573
  }).strict();
574
+ var GetModelsToolResultSchema = z3.object({
575
+ results: z3.array(ModelResponseSchema.nullable()),
576
+ missingIds: z3.array(z3.string()),
577
+ resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
578
+ ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional(),
579
+ _index9: Index9MetaSchema
580
+ });
490
581
 
491
582
  // ../core/dist/schemas/test.js
492
583
  import { z as z4 } from "zod";
@@ -540,7 +631,7 @@ var TestPricingUsedSchema = z4.object({
540
631
  var TestModelMetadataSchema = z4.object({
541
632
  id: z4.string(),
542
633
  name: z4.string(),
543
- createdUnix: z4.number().nullable().optional(),
634
+ created: z4.number().nullable().optional(),
544
635
  createdAt: z4.string().nullable().optional(),
545
636
  pricingUsed: TestPricingUsedSchema.optional()
546
637
  });
@@ -552,7 +643,8 @@ var TestResultSuccessSchema = z4.object({
552
643
  response: z4.string(),
553
644
  latencyMs: z4.number().min(0),
554
645
  tokens: UsageTokensSchema,
555
- cost: z4.number().nullable().optional()
646
+ cost: z4.number().nullable().optional(),
647
+ truncated: z4.boolean().optional()
556
648
  });
557
649
  var TestResultFailureSchema = z4.object({
558
650
  modelId: z4.string(),
@@ -683,10 +775,14 @@ function baseHeaders(ctx) {
683
775
  return h;
684
776
  }
685
777
  function toResponse(payload, isError = false) {
686
- return {
778
+ const response = {
687
779
  content: [{ type: "text", text: JSON.stringify(payload) }],
688
780
  isError: isError || void 0
689
781
  };
782
+ if (!isError && typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
783
+ response.structuredContent = payload;
784
+ }
785
+ return response;
690
786
  }
691
787
  function parseRetryAfterSeconds(value) {
692
788
  if (!value) return void 0;
@@ -818,6 +914,7 @@ async function createServer() {
818
914
  server.registerTool(
819
915
  "find_models",
820
916
  {
917
+ title: TOOLS.find_models.title,
821
918
  description: TOOLS.find_models.description,
822
919
  inputSchema: {
823
920
  q: z5.string().min(1).optional().describe(PARAM_DESCRIPTIONS.q),
@@ -830,11 +927,12 @@ async function createServer() {
830
927
  minPrice: z5.number().min(0).optional().describe("Minimum prompt price in USD per million tokens."),
831
928
  maxPrice: z5.number().min(0).optional().describe("Maximum prompt price in USD per million tokens."),
832
929
  minContext: z5.number().int().min(1).optional().describe("Minimum context window in tokens."),
833
- capabilitiesAll: z5.array(z5.string()).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAll),
834
- capabilitiesAny: z5.array(z5.string()).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAny),
835
- modality: z5.string().optional().describe(PARAM_DESCRIPTIONS.modality),
930
+ capabilitiesAll: z5.array(z5.enum(CAPABILITIES)).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAll),
931
+ capabilitiesAny: z5.array(z5.enum(CAPABILITIES)).optional().describe(PARAM_DESCRIPTIONS.capabilitiesAny),
932
+ modality: z5.enum(OUTPUT_MODALITIES).optional().describe(PARAM_DESCRIPTIONS.modality),
836
933
  provider: z5.string().min(1).optional().describe(PARAM_DESCRIPTIONS.provider)
837
934
  },
935
+ outputSchema: FindModelsToolResultSchema.shape,
838
936
  annotations: { readOnlyHint: true }
839
937
  },
840
938
  async (args) => handleSearchModels(ctx, args)
@@ -842,11 +940,13 @@ async function createServer() {
842
940
  server.registerTool(
843
941
  "get_models",
844
942
  {
943
+ title: TOOLS.get_models.title,
845
944
  description: TOOLS.get_models.description,
846
945
  inputSchema: {
847
946
  ids: z5.array(z5.string().min(1)).min(1).max(100).describe("Model identifiers or aliases. Up to 100."),
848
947
  maxDescriptionChars: z5.number().int().min(0).max(2e3).optional().describe("Truncate descriptions to this many characters.")
849
948
  },
949
+ outputSchema: GetModelsToolResultSchema.shape,
850
950
  annotations: { readOnlyHint: true }
851
951
  },
852
952
  async (args) => handleGetModels(ctx, args)
@@ -854,6 +954,7 @@ async function createServer() {
854
954
  server.registerTool(
855
955
  "test_model",
856
956
  {
957
+ title: TOOLS.test_model.title,
857
958
  description: TOOLS.test_model.description,
858
959
  inputSchema: {
859
960
  prompt: z5.string().min(1).optional().describe("Prompt sent to each model."),
@@ -864,7 +965,9 @@ async function createServer() {
864
965
  expectedCompletionTokens: z5.number().int().min(1).optional().describe(PARAM_DESCRIPTIONS.expectedCompletionTokens),
865
966
  models: z5.array(z5.string().min(1)).min(1).max(LIMITS.testModelsMax).describe(`Model IDs to evaluate (1-${LIMITS.testModelsMax}).`),
866
967
  timeoutMs: z5.number().int().min(1).optional().describe("Per-model timeout in ms (default 15000, max 60000)."),
867
- maxTokens: z5.number().int().min(1).optional().describe("Completion token cap."),
968
+ maxTokens: z5.number().int().min(1).optional().describe(
969
+ "Completion token cap. For reasoning-capable models, set \u2265 2000 (or omit) \u2014 reasoning tokens count against this before visible output, and too-low caps cause finish_reason=length."
970
+ ),
868
971
  systemPrompt: z5.string().min(1).optional().describe("System instruction prepended to prompt."),
869
972
  temperature: z5.number().min(0).max(2).optional().describe("Sampling temperature (0-2)."),
870
973
  topP: z5.number().gt(0).max(1).optional().describe("Nucleus sampling (0-1]."),
@@ -875,6 +978,9 @@ async function createServer() {
875
978
  enforceJson: z5.boolean().optional().describe("When true, output must parse as JSON."),
876
979
  retries: z5.number().int().min(0).max(3).optional().describe("Retries for transient failures.")
877
980
  },
981
+ // No outputSchema: test_model returns a z.union of dry-run and live shapes.
982
+ // The SDK supports only ZodRawShape | AnySchema for outputSchema; a discriminated-union
983
+ // output is represented accurately in the tool description instead.
878
984
  annotations: { readOnlyHint: false }
879
985
  },
880
986
  async (args) => handleTestModels(ctx, args)
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "index9",
4
- "version": "4.0.1",
4
+ "version": "5.0.0",
5
5
  "description": "Search, inspect, and benchmark 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": "4.0.2",
3
+ "version": "5.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",