@aggc/or-info 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,12 +181,17 @@ or-info refresh # Force-refresh OpenRouter catalog + LMArena ELO
181
181
 
182
182
  | Tool | Description |
183
183
  |------|-------------|
184
- | `get_model_info` | Pricing, context, architecture, features and LMArena ELO for a model |
185
- | `list_models` | List models with optional filter, sort and limit |
186
- | `get_benchmarks` | LMArena ELO score, global rank, vote count and confidence interval for a model |
187
- | `compare_models` | Side-by-side comparison of two models |
188
- | `best_for_task` | Ranked top models for coding/reasoning/general/vision/cheap |
189
- | `refresh_cache` | Force-refresh OpenRouter catalog + LMArena ELO |
184
+ | `models.get` | Pricing, context, architecture, features and LMArena ELO for a model |
185
+ | `models.list` | List models with optional filter, sort and limit |
186
+ | `models.compare` | Side-by-side comparison of two models |
187
+ | `models.top` | Ranked top models for coding/reasoning/general/vision/cheap |
188
+ | `benchmarks.get` | LMArena ELO score, global rank, vote count and confidence interval for a model |
189
+ | `cache.refresh` | Force-refresh OpenRouter catalog + LMArena ELO |
190
+
191
+ Legacy flat names (`get_model_info`, `list_models`, `get_benchmarks`,
192
+ `compare_models`, `best_for_task`, `refresh_cache`) are still advertised in
193
+ `tools/list` as deprecated aliases (same schemas, prefixed `[Deprecated]`)
194
+ and remain callable. The dot-notation names are the canonical ones.
190
195
 
191
196
  ### Register in Claude Code
192
197
 
package/bin/or-info.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
2
3
  import { InvalidArgumentError, program } from 'commander';
3
4
  import chalk from 'chalk';
5
+
6
+ const { version } = createRequire(import.meta.url)('../package.json');
4
7
  import { fetchModels, findModel, pricePerMillion, contextLength } from '../lib/openrouter.mjs';
5
8
  import { getElo, getAllElo, loadLeaderboard } from '../lib/lmarena.mjs';
6
9
  import { rankModels } from '../lib/scorer.mjs';
@@ -40,7 +43,7 @@ async function apiKey() {
40
43
  program
41
44
  .name('or-info')
42
45
  .description('OpenRouter model info: prices, benchmarks, context and comparisons')
43
- .version('0.1.5')
46
+ .version(version)
44
47
  .option('--mcp', 'Start MCP server (stdio transport)');
45
48
 
46
49
  // ── models ─────────────────────────────────────────────────────────────────
package/mcp/server.mjs CHANGED
@@ -1,11 +1,15 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
6
  import { fetchModels, findModel, pricePerMillion, contextLength, modelTags } from '../lib/openrouter.mjs';
5
7
  import { getElo, getAllElo, loadLeaderboard } from '../lib/lmarena.mjs';
6
8
  import { rankModels } from '../lib/scorer.mjs';
7
9
  import { getApiKey } from '../lib/secrets.mjs';
8
10
 
11
+ const { version } = createRequire(import.meta.url)('../package.json');
12
+
9
13
  const MODEL_SUMMARY_SCHEMA = {
10
14
  type: 'object',
11
15
  properties: {
@@ -26,9 +30,18 @@ const MODEL_SUMMARY_SCHEMA = {
26
30
 
27
31
  const ELO_SCHEMA = { type: ['object', 'null'], description: 'LMArena ELO entry or null when not tracked' };
28
32
 
29
- const TOOLS = [
33
+ const TOOL_ALIASES = {
34
+ get_model_info: 'models.get',
35
+ list_models: 'models.list',
36
+ compare_models: 'models.compare',
37
+ best_for_task: 'models.top',
38
+ get_benchmarks: 'benchmarks.get',
39
+ refresh_cache: 'cache.refresh',
40
+ };
41
+
42
+ const CANONICAL_TOOLS = [
30
43
  {
31
- name: 'get_model_info',
44
+ name: 'models.get',
32
45
  description: 'Get pricing, context length, architecture and features for a specific OpenRouter model',
33
46
  inputSchema: {
34
47
  type: 'object',
@@ -42,14 +55,14 @@ const TOOLS = [
42
55
  properties: { ...MODEL_SUMMARY_SCHEMA.properties, lmarena_elo: ELO_SCHEMA },
43
56
  },
44
57
  annotations: {
45
- title: 'Get model info',
58
+ title: 'Get model',
46
59
  readOnlyHint: true,
47
60
  idempotentHint: true,
48
61
  openWorldHint: true,
49
62
  },
50
63
  },
51
64
  {
52
- name: 'list_models',
65
+ name: 'models.list',
53
66
  description: 'List OpenRouter models with pricing. Optionally filter by name/id, sort, and limit results.',
54
67
  inputSchema: {
55
68
  type: 'object',
@@ -76,7 +89,7 @@ const TOOLS = [
76
89
  },
77
90
  },
78
91
  {
79
- name: 'get_benchmarks',
92
+ name: 'benchmarks.get',
80
93
  description: 'Get LMArena ELO ranking for a model: score, global rank, vote count and confidence interval',
81
94
  inputSchema: {
82
95
  type: 'object',
@@ -94,14 +107,14 @@ const TOOLS = [
94
107
  required: ['model_id'],
95
108
  },
96
109
  annotations: {
97
- title: 'Get benchmarks',
110
+ title: 'Get benchmark',
98
111
  readOnlyHint: true,
99
112
  idempotentHint: true,
100
113
  openWorldHint: true,
101
114
  },
102
115
  },
103
116
  {
104
- name: 'compare_models',
117
+ name: 'models.compare',
105
118
  description: 'Side-by-side comparison of two models: pricing, context, benchmarks and features',
106
119
  inputSchema: {
107
120
  type: 'object',
@@ -127,7 +140,7 @@ const TOOLS = [
127
140
  },
128
141
  },
129
142
  {
130
- name: 'best_for_task',
143
+ name: 'models.top',
131
144
  description: 'Rank the best models for a specific task, optionally within a price budget',
132
145
  inputSchema: {
133
146
  type: 'object',
@@ -160,14 +173,14 @@ const TOOLS = [
160
173
  required: ['task', 'results'],
161
174
  },
162
175
  annotations: {
163
- title: 'Best models for task',
176
+ title: 'Top models for task',
164
177
  readOnlyHint: true,
165
178
  idempotentHint: true,
166
179
  openWorldHint: true,
167
180
  },
168
181
  },
169
182
  {
170
- name: 'refresh_cache',
183
+ name: 'cache.refresh',
171
184
  description: 'Force-refresh the local cache: OpenRouter model catalog + LMArena ELO data',
172
185
  inputSchema: { type: 'object', properties: {} },
173
186
  outputSchema: {
@@ -188,6 +201,25 @@ const TOOLS = [
188
201
  },
189
202
  ];
190
203
 
204
+ // Legacy flat names kept advertised in tools/list for discoverability,
205
+ // derived from the canonical tools so input/output schemas stay in sync.
206
+ const LEGACY_BY_CANONICAL = Object.fromEntries(
207
+ Object.entries(TOOL_ALIASES).map(([legacy, canonical]) => [canonical, legacy])
208
+ );
209
+
210
+ const LEGACY_TOOLS = CANONICAL_TOOLS.flatMap((tool) => {
211
+ const legacyName = LEGACY_BY_CANONICAL[tool.name];
212
+ if (!legacyName) return [];
213
+ return [{
214
+ ...tool,
215
+ name: legacyName,
216
+ description: `[Deprecated] Alias of \`${tool.name}\`. Use \`${tool.name}\` instead.`,
217
+ annotations: { ...tool.annotations, title: `[Deprecated] ${tool.annotations.title}` },
218
+ }];
219
+ });
220
+
221
+ const TOOLS = [...CANONICAL_TOOLS, ...LEGACY_TOOLS];
222
+
191
223
  function safeModelSummary(model) {
192
224
  const price = pricePerMillion(model);
193
225
  return {
@@ -218,9 +250,12 @@ function errorContent(msg) {
218
250
  }
219
251
 
220
252
  async function handleTool(name, args) {
253
+ // Accept legacy flat names (get_model_info, list_models, ...) by mapping
254
+ // them to the dot-notation canonical names exposed in tools/list.
255
+ name = TOOL_ALIASES[name] ?? name;
221
256
  const key = await getApiKey();
222
257
 
223
- if (name === 'get_model_info') {
258
+ if (name === 'models.get') {
224
259
  const { model_id } = args;
225
260
  if (!model_id || typeof model_id !== 'string') return errorContent('model_id is required');
226
261
  const models = await fetchModels({ apiKey: key });
@@ -230,7 +265,7 @@ async function handleTool(name, args) {
230
265
  return result({ ...safeModelSummary(model), lmarena_elo: elo ?? null });
231
266
  }
232
267
 
233
- if (name === 'list_models') {
268
+ if (name === 'models.list') {
234
269
  const filter = String(args.filter ?? '').toLowerCase();
235
270
  const sortBy = args.sort_by ?? 'name';
236
271
  const limit = Math.min(200, Math.max(1, args.limit ?? 50));
@@ -248,14 +283,14 @@ async function handleTool(name, args) {
248
283
  return result({ total: models.length, models: models.map(safeModelSummary) });
249
284
  }
250
285
 
251
- if (name === 'get_benchmarks') {
286
+ if (name === 'benchmarks.get') {
252
287
  const { model_id } = args;
253
288
  if (!model_id || typeof model_id !== 'string') return errorContent('model_id is required');
254
289
  const elo = await getElo(model_id);
255
290
  return result({ model_id, lmarena_elo: elo ?? null });
256
291
  }
257
292
 
258
- if (name === 'compare_models') {
293
+ if (name === 'models.compare') {
259
294
  const { model_a, model_b } = args;
260
295
  if (!model_a || !model_b) return errorContent('model_a and model_b are required');
261
296
  const [models, eloA, eloB] = await Promise.all([
@@ -270,7 +305,7 @@ async function handleTool(name, args) {
270
305
  return result({ a: { ...safeModelSummary(mA), lmarena_elo: eloA }, b: { ...safeModelSummary(mB), lmarena_elo: eloB } });
271
306
  }
272
307
 
273
- if (name === 'best_for_task') {
308
+ if (name === 'models.top') {
274
309
  const task = args.task ?? 'general';
275
310
  const limit = Math.min(20, Math.max(1, args.limit ?? 5));
276
311
  const maxPrice = args.max_price_per_m_output ?? undefined;
@@ -280,7 +315,7 @@ async function handleTool(name, args) {
280
315
  return result({ task, results: ranked.map((r) => ({ ...safeModelSummary(r.model), score: r.score, lmarena_elo: r.eloEntry })) });
281
316
  }
282
317
 
283
- if (name === 'refresh_cache') {
318
+ if (name === 'cache.refresh') {
284
319
  const [models, elo] = await Promise.all([
285
320
  fetchModels({ force: true, apiKey: key }),
286
321
  loadLeaderboard({ force: true }),
@@ -291,14 +326,15 @@ async function handleTool(name, args) {
291
326
  return errorContent(`Unknown tool: ${name}`);
292
327
  }
293
328
 
294
- export async function startMcp() {
295
- const server = new Server(
296
- { name: 'or-info', version: '0.1.5' },
329
+ function makeServer() {
330
+ return new Server(
331
+ { name: 'or-info', version },
297
332
  { capabilities: { tools: {} } }
298
333
  );
334
+ }
299
335
 
336
+ function wireHandlers(server) {
300
337
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
301
-
302
338
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
303
339
  const { name, arguments: args } = req.params;
304
340
  try {
@@ -308,6 +344,11 @@ export async function startMcp() {
308
344
  return errorContent(safe);
309
345
  }
310
346
  });
347
+ }
348
+
349
+ export async function startMcp() {
350
+ const server = makeServer();
351
+ wireHandlers(server);
311
352
 
312
353
  const transport = new StdioServerTransport();
313
354
  await server.connect(transport);
@@ -321,3 +362,38 @@ export async function startMcp() {
321
362
  });
322
363
  }
323
364
  }
365
+
366
+ export async function startHttpMcp() {
367
+ const { createServer } = await import('node:http');
368
+ const port = Number(process.env.PORT) || 8000;
369
+
370
+ // Bridge config values Smithery may inject from smithery.yaml configSchema.
371
+ // Smithery passes schema properties as-is or uppercased depending on version.
372
+ if (!process.env.OPENROUTER_API_KEY) {
373
+ process.env.OPENROUTER_API_KEY = process.env.api_key ?? process.env.API_KEY ?? '';
374
+ }
375
+
376
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
377
+ const server = makeServer();
378
+ wireHandlers(server);
379
+ await server.connect(transport);
380
+
381
+ const serverCard = JSON.stringify({
382
+ serverInfo: { name: 'or-info', version },
383
+ tools: CANONICAL_TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
384
+ });
385
+
386
+ createServer(async (req, res) => {
387
+ if (req.url?.startsWith('/mcp')) {
388
+ await transport.handleRequest(req, res);
389
+ } else if (req.method === 'GET' && req.url === '/.well-known/mcp/server-card.json') {
390
+ res.writeHead(200, { 'Content-Type': 'application/json' });
391
+ res.end(serverCard);
392
+ } else {
393
+ res.writeHead(404);
394
+ res.end();
395
+ }
396
+ }).listen(port, () => {
397
+ process.stderr.write(`or-info HTTP MCP listening on port ${port}\n`);
398
+ });
399
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aggc/or-info",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI + MCP server for OpenRouter models: prices, benchmarks, context and comparisons",
5
5
  "type": "module",
6
6
  "engines": {