@aggc/or-info 0.2.8 → 0.2.10

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/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 ─────────────────────────────────────────────────────────────────
@@ -105,7 +108,7 @@ program
105
108
 
106
109
  if (opts.json) {
107
110
  console.log(JSON.stringify({
108
- model: model.id,
111
+ id: model.id,
109
112
  pricing: model.pricing,
110
113
  context_length: contextLength(model),
111
114
  }, null, 2));
@@ -100,7 +100,17 @@ export function supportsFeature(model, feature) {
100
100
  const featureMap = {
101
101
  reasoning: ['include_reasoning', 'reasoning'],
102
102
  tools: ['tools', 'tool_choice'],
103
- vision: () => (model?.architecture?.input_modalities ?? []).includes('image'),
103
+ vision: () => {
104
+ // Prefer the canonical modality string (e.g. "text+image->text") because
105
+ // input_modalities is inconsistently populated by OpenRouter providers.
106
+ const modality = model?.architecture?.modality ?? '';
107
+ if (modality) {
108
+ return modality.split('->')[0].split('+').map((s) => s.trim()).includes('image');
109
+ }
110
+ const inputMods = model?.architecture?.input_modalities;
111
+ if (Array.isArray(inputMods)) return inputMods.includes('image');
112
+ return false;
113
+ },
104
114
  structured: ['structured_outputs'],
105
115
  };
106
116
  const check = featureMap[feature];
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: {
@@ -322,33 +326,99 @@ async function handleTool(name, args) {
322
326
  return errorContent(`Unknown tool: ${name}`);
323
327
  }
324
328
 
325
- export async function startMcp() {
326
- const server = new Server(
327
- { name: 'or-info', version: '0.1.5' },
329
+ function makeServer() {
330
+ return new Server(
331
+ { name: 'or-info', version },
328
332
  { capabilities: { tools: {} } }
329
333
  );
334
+ }
330
335
 
336
+ function wireHandlers(server) {
331
337
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
338
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
339
+ const { name, arguments: args } = req.params;
340
+ try {
341
+ return await handleTool(name, args ?? {});
342
+ } catch (err) {
343
+ const safe = err.message?.replace(/sk-[a-zA-Z0-9-]+/g, '[REDACTED]') ?? 'Unexpected error';
344
+ return errorContent(safe);
345
+ }
346
+ });
347
+ }
348
+
349
+ export async function startMcp() {
350
+ // Track in-flight tool calls so we don't exit while a response is still being written.
351
+ // Race condition: stdin EOF fires before the async handleTool completes, causing
352
+ // process.exit(0) to kill the process before the MCP SDK writes the response to stdout.
353
+ let pending = 0;
354
+ let stdinEnded = false;
355
+ let resolveWhenDone;
356
+ const donePromise = new Promise((res) => { resolveWhenDone = res; });
332
357
 
358
+ function checkDone() {
359
+ if (stdinEnded && pending === 0) resolveWhenDone();
360
+ }
361
+
362
+ const server = makeServer();
363
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
333
364
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
334
365
  const { name, arguments: args } = req.params;
366
+ pending++;
335
367
  try {
336
368
  return await handleTool(name, args ?? {});
337
369
  } catch (err) {
338
370
  const safe = err.message?.replace(/sk-[a-zA-Z0-9-]+/g, '[REDACTED]') ?? 'Unexpected error';
339
371
  return errorContent(safe);
372
+ } finally {
373
+ pending--;
374
+ // Defer checkDone by one tick so the SDK's response-write microtask runs first.
375
+ setImmediate(checkDone);
340
376
  }
341
377
  });
342
378
 
343
379
  const transport = new StdioServerTransport();
344
380
  await server.connect(transport);
345
381
 
346
- // server.connect() returns immediately after wiring up the transport.
347
- // Block here until stdin closes so the process stays alive while serving.
348
382
  if (!process.stdin.destroyed) {
349
- await new Promise((resolve) => {
350
- process.stdin.once('close', resolve);
351
- process.stdin.once('end', resolve);
352
- });
383
+ process.stdin.once('close', () => { stdinEnded = true; checkDone(); });
384
+ process.stdin.once('end', () => { stdinEnded = true; checkDone(); });
385
+ await donePromise;
386
+ }
387
+ // One extra tick for any buffered stdout writes before the caller calls process.exit().
388
+ await new Promise((resolve) => setImmediate(resolve));
389
+ }
390
+
391
+ export async function startHttpMcp() {
392
+ const { createServer } = await import('node:http');
393
+ const port = Number(process.env.PORT) || 8000;
394
+
395
+ // Bridge config values Smithery may inject from smithery.yaml configSchema.
396
+ // Smithery passes schema properties as-is or uppercased depending on version.
397
+ if (!process.env.OPENROUTER_API_KEY) {
398
+ process.env.OPENROUTER_API_KEY = process.env.api_key ?? process.env.API_KEY ?? '';
353
399
  }
400
+
401
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
402
+ const server = makeServer();
403
+ wireHandlers(server);
404
+ await server.connect(transport);
405
+
406
+ const serverCard = JSON.stringify({
407
+ serverInfo: { name: 'or-info', version },
408
+ tools: CANONICAL_TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
409
+ });
410
+
411
+ createServer(async (req, res) => {
412
+ if (req.url?.startsWith('/mcp')) {
413
+ await transport.handleRequest(req, res);
414
+ } else if (req.method === 'GET' && req.url === '/.well-known/mcp/server-card.json') {
415
+ res.writeHead(200, { 'Content-Type': 'application/json' });
416
+ res.end(serverCard);
417
+ } else {
418
+ res.writeHead(404);
419
+ res.end();
420
+ }
421
+ }).listen(port, () => {
422
+ process.stderr.write(`or-info HTTP MCP listening on port ${port}\n`);
423
+ });
354
424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aggc/or-info",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "CLI + MCP server for OpenRouter models: prices, benchmarks, context and comparisons",
5
5
  "type": "module",
6
6
  "engines": {