@orcarouter/mcp 1.1.2

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/index.js ADDED
@@ -0,0 +1,736 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import {
9
+ CallToolRequestSchema,
10
+ ListToolsRequestSchema
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import Ajv from "ajv";
13
+ import addFormats from "ajv-formats";
14
+
15
+ // src/errors.ts
16
+ var ApiError = class extends Error {
17
+ status;
18
+ body;
19
+ constructor(message, status, body, options) {
20
+ super(message, options);
21
+ this.name = "ApiError";
22
+ this.status = status;
23
+ this.body = body;
24
+ }
25
+ };
26
+ var AuthenticationError = class extends ApiError {
27
+ constructor(message, body) {
28
+ super(message, 401, body);
29
+ this.name = "AuthenticationError";
30
+ }
31
+ };
32
+ var PermissionDeniedError = class extends ApiError {
33
+ constructor(message, body) {
34
+ super(message, 403, body);
35
+ this.name = "PermissionDeniedError";
36
+ }
37
+ };
38
+ var InsufficientQuotaError = class extends ApiError {
39
+ constructor(message, status, body) {
40
+ super(message, status, body);
41
+ this.name = "InsufficientQuotaError";
42
+ }
43
+ };
44
+ var RequestCancelledError = class extends Error {
45
+ constructor(message = "OrcaRouter request cancelled by caller.", options) {
46
+ super(message, options);
47
+ this.name = "AbortError";
48
+ }
49
+ };
50
+ var RateLimitError = class extends ApiError {
51
+ retryAfter;
52
+ constructor(message, retryAfter, body) {
53
+ super(message, 429, body);
54
+ this.name = "RateLimitError";
55
+ this.retryAfter = retryAfter;
56
+ }
57
+ };
58
+ var InternalServerError = class extends ApiError {
59
+ constructor(message, status, body) {
60
+ super(message, status, body);
61
+ this.name = "InternalServerError";
62
+ }
63
+ };
64
+ var MissingApiKeyError = class extends Error {
65
+ constructor(toolName) {
66
+ super(
67
+ `ORCAROUTER_API_KEY is required for tool "${toolName}". Set the ORCAROUTER_API_KEY environment variable when launching the MCP server.`
68
+ );
69
+ this.name = "MissingApiKeyError";
70
+ }
71
+ };
72
+
73
+ // src/version.ts
74
+ var PACKAGE_VERSION = "1.1.2";
75
+
76
+ // src/api_client.ts
77
+ var DEFAULT_BASE_URL = "https://api.orcarouter.ai";
78
+ var DEFAULT_TIMEOUT_MS = 3e5;
79
+ var USER_AGENT = `@orcarouter/mcp/${PACKAGE_VERSION} (Node.js)`;
80
+ var ApiClient = class {
81
+ apiKey;
82
+ baseUrl;
83
+ timeoutMs;
84
+ fetchImpl;
85
+ constructor(opts = {}) {
86
+ this.apiKey = opts.apiKey;
87
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
88
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
89
+ this.timeoutMs = typeof opts.timeoutMs === "number" && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
90
+ }
91
+ get(path, opts = {}) {
92
+ return this.request("GET", path, void 0, opts);
93
+ }
94
+ post(path, body, opts = {}) {
95
+ return this.request("POST", path, body, opts);
96
+ }
97
+ async raw(method, path, body, opts = {}) {
98
+ const url = this.buildUrl(path, opts.query);
99
+ const headers = this.buildHeaders(body !== void 0, opts.headers);
100
+ const externalSignal = opts.signal;
101
+ const { signal, cancel, didTimeout } = this.makeTimeoutSignal(externalSignal);
102
+ const init = {
103
+ method,
104
+ headers,
105
+ signal
106
+ };
107
+ if (body !== void 0) {
108
+ init.body = typeof body === "string" ? body : JSON.stringify(body);
109
+ }
110
+ try {
111
+ return await this.fetchImpl(url, init);
112
+ } catch (e) {
113
+ if (didTimeout()) {
114
+ throw new Error(
115
+ `OrcaRouter request timed out after ${this.timeoutMs}ms (${method} ${path}).`
116
+ );
117
+ }
118
+ if (externalSignal?.aborted || signal.aborted) {
119
+ throw new RequestCancelledError(
120
+ `OrcaRouter request cancelled by caller (${method} ${path}).`,
121
+ { cause: e }
122
+ );
123
+ }
124
+ if (this.isAbortError(e)) {
125
+ throw new RequestCancelledError(
126
+ `OrcaRouter request cancelled by caller (${method} ${path}).`,
127
+ { cause: e }
128
+ );
129
+ }
130
+ throw e;
131
+ } finally {
132
+ cancel();
133
+ }
134
+ }
135
+ isAbortError(e) {
136
+ if (!e || typeof e !== "object") return false;
137
+ const name = e.name;
138
+ return name === "AbortError" || name === "TimeoutError";
139
+ }
140
+ makeTimeoutSignal(external) {
141
+ const controller = new AbortController();
142
+ let didTimeoutFlag = false;
143
+ const timer = setTimeout(() => {
144
+ didTimeoutFlag = true;
145
+ controller.abort(new Error("OrcaRouter request timeout"));
146
+ }, this.timeoutMs);
147
+ const onExternalAbort = () => controller.abort(external?.reason);
148
+ if (external) {
149
+ if (external.aborted) {
150
+ controller.abort(external.reason);
151
+ } else {
152
+ external.addEventListener("abort", onExternalAbort, { once: true });
153
+ }
154
+ }
155
+ const cancel = () => {
156
+ clearTimeout(timer);
157
+ if (external) external.removeEventListener("abort", onExternalAbort);
158
+ };
159
+ const didTimeout = () => didTimeoutFlag;
160
+ return { signal: controller.signal, cancel, didTimeout };
161
+ }
162
+ async request(method, path, body, opts) {
163
+ const response = await this.raw(method, path, body, opts);
164
+ if (!response.ok) {
165
+ await this.throwForStatus(response);
166
+ }
167
+ if (response.status === 204) {
168
+ return void 0;
169
+ }
170
+ return await response.json();
171
+ }
172
+ buildUrl(path, query) {
173
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
174
+ const url = new URL(this.baseUrl + normalizedPath);
175
+ if (query) {
176
+ for (const [key, value] of Object.entries(query)) {
177
+ if (value === void 0 || value === null) continue;
178
+ url.searchParams.append(key, String(value));
179
+ }
180
+ }
181
+ return url.toString();
182
+ }
183
+ buildHeaders(hasBody, extra) {
184
+ const headers = new Headers();
185
+ if (this.apiKey) {
186
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
187
+ }
188
+ headers.set("Accept", "application/json");
189
+ headers.set("User-Agent", USER_AGENT);
190
+ if (hasBody) {
191
+ headers.set("Content-Type", "application/json");
192
+ }
193
+ if (extra) {
194
+ for (const [k, v] of Object.entries(extra)) {
195
+ headers.set(k, v);
196
+ }
197
+ }
198
+ return headers;
199
+ }
200
+ async throwForStatus(response) {
201
+ const status = response.status;
202
+ let bodyText = "";
203
+ let parsed = void 0;
204
+ try {
205
+ bodyText = await response.text();
206
+ if (bodyText) {
207
+ try {
208
+ parsed = JSON.parse(bodyText);
209
+ } catch {
210
+ parsed = void 0;
211
+ }
212
+ }
213
+ } catch {
214
+ }
215
+ const message = extractErrorMessage(parsed) ?? (bodyText || response.statusText);
216
+ if ((status === 402 || status === 403) && hasInsufficientQuotaCode(parsed)) {
217
+ throw new InsufficientQuotaError(message, status, parsed);
218
+ }
219
+ if (status === 401) {
220
+ throw new AuthenticationError(message, parsed);
221
+ }
222
+ if (status === 403) {
223
+ throw new PermissionDeniedError(message, parsed);
224
+ }
225
+ if (status === 429) {
226
+ throw new RateLimitError(
227
+ message,
228
+ parseRetryAfter(response.headers.get("retry-after")),
229
+ parsed
230
+ );
231
+ }
232
+ if (status >= 500) {
233
+ throw new InternalServerError(message, status, parsed);
234
+ }
235
+ throw new ApiError(message, status, parsed);
236
+ }
237
+ };
238
+ function parseRetryAfter(raw) {
239
+ if (!raw) return void 0;
240
+ const trimmed = raw.trim();
241
+ if (trimmed.length === 0) return void 0;
242
+ const asNumber = Number(trimmed);
243
+ if (Number.isFinite(asNumber)) {
244
+ return asNumber;
245
+ }
246
+ const asDate = Date.parse(trimmed);
247
+ if (Number.isFinite(asDate)) {
248
+ return Math.max(0, Math.round((asDate - Date.now()) / 1e3));
249
+ }
250
+ return void 0;
251
+ }
252
+ function hasInsufficientQuotaCode(body) {
253
+ if (!body || typeof body !== "object") return false;
254
+ const codes = /* @__PURE__ */ new Set(["insufficient_quota", "insufficient_user_quota"]);
255
+ const b = body;
256
+ if (typeof b.code === "string" && codes.has(b.code)) return true;
257
+ if (b.error && typeof b.error === "object") {
258
+ const inner = b.error;
259
+ if (typeof inner.code === "string" && codes.has(inner.code)) return true;
260
+ }
261
+ return false;
262
+ }
263
+ function extractErrorMessage(body) {
264
+ if (!body || typeof body !== "object") return void 0;
265
+ const b = body;
266
+ if (typeof b.message === "string") return b.message;
267
+ if (b.error && typeof b.error === "object") {
268
+ const inner = b.error;
269
+ if (typeof inner.message === "string") return inner.message;
270
+ }
271
+ if (typeof b.error === "string") return b.error;
272
+ return void 0;
273
+ }
274
+
275
+ // src/tools/types.ts
276
+ var ValidationError = class extends Error {
277
+ constructor(message) {
278
+ super(message);
279
+ this.name = "ValidationError";
280
+ }
281
+ };
282
+ function textResult(text) {
283
+ return { content: [{ type: "text", text }] };
284
+ }
285
+
286
+ // src/tools/chat.ts
287
+ var DEFAULT_MODEL = "orcarouter/auto";
288
+ var DEFAULT_TEMPERATURE = 0.7;
289
+ var DEFAULT_MAX_TOKENS = 2e3;
290
+ function isReasoningModel(modelId) {
291
+ const bare = modelId.split("/").pop() ?? modelId;
292
+ return /^(gpt-5|o[1-9])([-.]|$)/i.test(bare);
293
+ }
294
+ function stringifyContent(content) {
295
+ if (typeof content === "string") return content;
296
+ if (Array.isArray(content)) {
297
+ const parts = content.map((p) => {
298
+ if (typeof p === "string") return p;
299
+ if (p && typeof p === "object") {
300
+ const pp = p;
301
+ if (pp.type === "text" && typeof pp.text === "string") return pp.text;
302
+ }
303
+ return "";
304
+ }).filter(Boolean);
305
+ if (parts.length > 0) return parts.join("\n");
306
+ }
307
+ try {
308
+ return JSON.stringify(content);
309
+ } catch {
310
+ return String(content);
311
+ }
312
+ }
313
+ var chatTool = {
314
+ name: "orcarouter_chat",
315
+ description: "Send a single-turn chat request to OrcaRouter. Default model is the workspace's auto-router. Use `orcarouter/<name>` for other routers or `<provider>/<model>` for direct calls. For OpenAI reasoning models (gpt-5/o1/o3/...), max_tokens is automatically routed to max_completion_tokens at the wire level.",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ model: {
320
+ type: "string",
321
+ default: DEFAULT_MODEL,
322
+ description: "Model to call. Defaults to `orcarouter/auto` \u2014 your workspace's seeded auto-router. Use `orcarouter/<name>` for other workspace routers, or `<provider>/<model>` for direct upstream selection (e.g. `openai/gpt-4o-mini`, `anthropic/claude-haiku-4.5`)."
323
+ },
324
+ prompt: {
325
+ type: "string",
326
+ description: "User message to send (single-turn)."
327
+ },
328
+ system_prompt: {
329
+ type: "string",
330
+ description: "Optional system prompt prepended to the conversation."
331
+ },
332
+ max_tokens: {
333
+ type: "integer",
334
+ default: DEFAULT_MAX_TOKENS,
335
+ description: "Maximum tokens to generate (default 2000). Automatically translated to max_completion_tokens for OpenAI reasoning models."
336
+ },
337
+ temperature: {
338
+ type: "number",
339
+ default: DEFAULT_TEMPERATURE,
340
+ description: "Sampling temperature (default 0.7)."
341
+ },
342
+ models: {
343
+ type: "array",
344
+ items: { type: "string" },
345
+ minItems: 1,
346
+ maxItems: 5,
347
+ description: "Optional fallback chain. Models are tried in order if the primary fails. Max 5 entries including the primary."
348
+ }
349
+ },
350
+ required: ["prompt"],
351
+ additionalProperties: false
352
+ },
353
+ async handler(input, { client }) {
354
+ if (!client.apiKey) {
355
+ throw new MissingApiKeyError("orcarouter_chat");
356
+ }
357
+ const rawModel = typeof input.model === "string" ? input.model.trim() : "";
358
+ const effectiveModel = rawModel.length > 0 ? rawModel : DEFAULT_MODEL;
359
+ if (typeof input.prompt !== "string" || input.prompt.length === 0) {
360
+ throw new ValidationError("prompt must be a non-empty string");
361
+ }
362
+ if (input.system_prompt !== void 0 && (typeof input.system_prompt !== "string" || input.system_prompt.length === 0)) {
363
+ throw new ValidationError(
364
+ "system_prompt must be a non-empty string when provided"
365
+ );
366
+ }
367
+ const effectiveMessages = typeof input.system_prompt === "string" ? [
368
+ { role: "system", content: input.system_prompt },
369
+ { role: "user", content: input.prompt }
370
+ ] : [{ role: "user", content: input.prompt }];
371
+ let effectiveModels;
372
+ if (input.models !== void 0) {
373
+ if (!Array.isArray(input.models) || input.models.length === 0) {
374
+ throw new ValidationError(
375
+ "models must be a non-empty array of model slug strings"
376
+ );
377
+ }
378
+ if (input.models.some((m) => typeof m !== "string" || m.length === 0)) {
379
+ throw new ValidationError("each entry in models must be a non-empty string");
380
+ }
381
+ const chain = [
382
+ effectiveModel,
383
+ ...input.models.filter((m) => m !== effectiveModel)
384
+ ];
385
+ if (chain.length > 5) {
386
+ throw new ValidationError(
387
+ "the effective fallback chain (primary model + models) must not exceed 5 entries; reduce the models array or omit the primary from it"
388
+ );
389
+ }
390
+ if (chain.length >= 2) {
391
+ effectiveModels = chain;
392
+ }
393
+ }
394
+ if (input.stream === true) {
395
+ throw new ValidationError(
396
+ "stream:true is not supported via MCP tools \u2014 MCP returns a single result. Set stream:false or omit."
397
+ );
398
+ }
399
+ const effectiveTemperature = typeof input.temperature === "number" ? input.temperature : DEFAULT_TEMPERATURE;
400
+ const effectiveMaxTokens = typeof input.max_tokens === "number" ? input.max_tokens : DEFAULT_MAX_TOKENS;
401
+ const body = {
402
+ model: effectiveModel,
403
+ messages: effectiveMessages,
404
+ stream: false,
405
+ temperature: effectiveTemperature
406
+ };
407
+ if (isReasoningModel(effectiveModel)) {
408
+ body.max_completion_tokens = effectiveMaxTokens;
409
+ } else {
410
+ body.max_tokens = effectiveMaxTokens;
411
+ }
412
+ if (effectiveModels && effectiveModels.length > 0) {
413
+ body.extra_body = { models: effectiveModels, route: "fallback" };
414
+ }
415
+ const data = await client.post("/v1/chat/completions", body);
416
+ const choice = data.choices?.[0];
417
+ const content = choice?.message?.content;
418
+ const text = stringifyContent(content ?? "");
419
+ return textResult(text);
420
+ }
421
+ };
422
+
423
+ // src/tools/model_card.ts
424
+ function errorResult(text) {
425
+ return { isError: true, content: [{ type: "text", text }] };
426
+ }
427
+ var modelCardTool = {
428
+ name: "orcarouter_model_card",
429
+ description: "Get detailed information about a single model: pricing, context window, supported endpoints, latency.",
430
+ inputSchema: {
431
+ type: "object",
432
+ properties: {
433
+ model: {
434
+ type: "string",
435
+ description: "Model ref in `provider/slug` form (e.g. `openai/gpt-4o-mini`, `anthropic/claude-haiku-4.5`). Use the exact `id` value returned by orcarouter_models_list."
436
+ }
437
+ },
438
+ required: ["model"],
439
+ additionalProperties: false
440
+ },
441
+ async handler(input, { client }) {
442
+ if (typeof input.model !== "string" || input.model.trim().length === 0) {
443
+ throw new ValidationError("model must be a non-empty string");
444
+ }
445
+ const ref = input.model.trim();
446
+ const slashIdx = ref.indexOf("/");
447
+ let urlPath;
448
+ if (slashIdx === -1) {
449
+ urlPath = `/api/public/models/${encodeURIComponent(ref)}`;
450
+ } else if (slashIdx === 0 || slashIdx === ref.length - 1) {
451
+ throw new ValidationError(
452
+ `model must not start or end with '/'; got: ${input.model}`
453
+ );
454
+ } else {
455
+ const provider = encodeURIComponent(ref.slice(0, slashIdx));
456
+ const slug = encodeURIComponent(ref.slice(slashIdx + 1));
457
+ urlPath = `/api/public/models/${provider}/${slug}`;
458
+ }
459
+ let body;
460
+ try {
461
+ body = await client.get(urlPath);
462
+ } catch (e) {
463
+ if (e instanceof ApiError) {
464
+ const msg = e.message || "";
465
+ if (e.status === 404 || /not found/i.test(msg)) {
466
+ return errorResult(
467
+ `Model not found: ${ref}. Check the model id, or use orcarouter_models_list to discover available models.`
468
+ );
469
+ }
470
+ return errorResult(
471
+ `Failed to fetch model card for ${ref}: ${msg || `HTTP ${e.status}`}`
472
+ );
473
+ }
474
+ throw e instanceof Error ? new Error(`Failed to fetch model card for ${ref}: ${e.message}`, {
475
+ cause: e
476
+ }) : e;
477
+ }
478
+ if (body == null || body.success === false || body.data == null) {
479
+ const msg = body && body.message || "";
480
+ if (/not found/i.test(msg) || msg === "") {
481
+ return errorResult(
482
+ `Model not found: ${ref}. Check the model id, or use orcarouter_models_list to discover available models.`
483
+ );
484
+ }
485
+ return errorResult(`Failed to fetch model card for ${ref}: ${msg}`);
486
+ }
487
+ const summary = `Model card for ${ref}:`;
488
+ const json = JSON.stringify(body.data, null, 2);
489
+ return textResult(`${summary}
490
+
491
+ ${json}`);
492
+ }
493
+ };
494
+
495
+ // src/tools/models_list.ts
496
+ var modelsListTool = {
497
+ name: "orcarouter_models_list",
498
+ description: "List available LLM models. Filter by provider, capability, or minimum context length \u2014 filters are applied server-side by the OrcaRouter backend.",
499
+ inputSchema: {
500
+ type: "object",
501
+ properties: {
502
+ provider: {
503
+ type: "string",
504
+ description: "Provider id (lowercase, e.g. 'openai', 'anthropic'). Get the full list via orcarouter_providers_list."
505
+ },
506
+ capability: {
507
+ type: "string",
508
+ enum: ["chat", "embedding", "image", "audio"],
509
+ description: "Filter to models supporting this capability."
510
+ },
511
+ min_context: {
512
+ type: "integer",
513
+ minimum: 1,
514
+ description: "Filter to models with context window at least this large (tokens)."
515
+ }
516
+ },
517
+ additionalProperties: false
518
+ },
519
+ async handler(input, { client }) {
520
+ const query = {};
521
+ if (typeof input.provider === "string" && input.provider.trim()) {
522
+ query.provider = input.provider.trim().toLowerCase();
523
+ }
524
+ if (typeof input.capability === "string" && input.capability.trim()) {
525
+ query.capability = input.capability.trim().toLowerCase();
526
+ }
527
+ if (typeof input.min_context === "number" && input.min_context > 0) {
528
+ query.min_context = String(input.min_context);
529
+ }
530
+ const data = await client.get("/v1/models", { query });
531
+ const all = Array.isArray(data.data) ? data.data : [];
532
+ if (all.length === 0) {
533
+ const activeFilters = [];
534
+ if (query.provider) activeFilters.push(`provider=${query.provider}`);
535
+ if (query.capability) activeFilters.push(`capability=${query.capability}`);
536
+ if (query.min_context) activeFilters.push(`min_context=${query.min_context}`);
537
+ const hint = activeFilters.length > 0 ? `No models match: ${activeFilters.join(", ")}` : `No models currently available.`;
538
+ return textResult(hint);
539
+ }
540
+ const summary = `Found ${all.length} model${all.length === 1 ? "" : "s"}:`;
541
+ const json = JSON.stringify(all, null, 2);
542
+ return textResult(`${summary}
543
+
544
+ ${json}`);
545
+ }
546
+ };
547
+
548
+ // src/tools/providers_list.ts
549
+ function errorResult2(text) {
550
+ return { isError: true, content: [{ type: "text", text }] };
551
+ }
552
+ var providersListTool = {
553
+ name: "orcarouter_providers_list",
554
+ description: "List all model providers on OrcaRouter with their model counts and display metadata. Works without an API key. Useful for discovering provider ids (e.g. 'openai', 'anthropic') to pass to orcarouter_models_list as the `provider` filter.",
555
+ inputSchema: {
556
+ type: "object",
557
+ properties: {},
558
+ additionalProperties: false
559
+ },
560
+ async handler(_input, { client }) {
561
+ let body;
562
+ try {
563
+ body = await client.get("/api/public/providers");
564
+ } catch (e) {
565
+ if (e instanceof ApiError) {
566
+ if (e.status === 404) {
567
+ return errorResult2(
568
+ "This OrcaRouter deployment does not expose provider discovery. Use orcarouter_models_list to discover available models."
569
+ );
570
+ }
571
+ return errorResult2(
572
+ `Failed to fetch providers: ${e.message || `HTTP ${e.status}`}`
573
+ );
574
+ }
575
+ throw e instanceof Error ? new Error(`Failed to fetch providers: ${e.message}`, { cause: e }) : e;
576
+ }
577
+ if (body == null || body.success === false || body.data == null) {
578
+ const msg = body && body.message || "unknown";
579
+ return errorResult2(`Failed to fetch providers: ${msg}`);
580
+ }
581
+ if (!Array.isArray(body.data.providers)) {
582
+ return errorResult2(
583
+ `Invalid response from OrcaRouter: 'providers' field is not an array.`
584
+ );
585
+ }
586
+ const providers = body.data.providers;
587
+ const summary = `Found ${providers.length} providers:`;
588
+ const json = JSON.stringify(body.data, null, 2);
589
+ return textResult(`${summary}
590
+
591
+ ${json}`);
592
+ }
593
+ };
594
+
595
+ // src/server.ts
596
+ var TOOLS = [
597
+ chatTool,
598
+ modelsListTool,
599
+ modelCardTool,
600
+ providersListTool
601
+ ];
602
+ function buildValidators() {
603
+ const AjvCtor = Ajv.default ?? Ajv;
604
+ const addFmt = addFormats.default ?? addFormats;
605
+ const ajv = new AjvCtor({ allErrors: true, strict: false });
606
+ addFmt(ajv);
607
+ const map = /* @__PURE__ */ new Map();
608
+ for (const t of TOOLS) {
609
+ map.set(t.name, ajv.compile(t.inputSchema));
610
+ }
611
+ return map;
612
+ }
613
+ function createOrcaRouterMcpServer(opts = {}) {
614
+ const client = new ApiClient({
615
+ apiKey: opts.apiKey,
616
+ baseUrl: opts.baseUrl,
617
+ timeoutMs: opts.timeoutMs
618
+ });
619
+ const ctx = {
620
+ client
621
+ };
622
+ const validators = buildValidators();
623
+ const server = new Server(
624
+ {
625
+ name: opts.serverName ?? "@orcarouter/mcp",
626
+ version: opts.serverVersion ?? "1.1.2"
627
+ },
628
+ {
629
+ capabilities: {
630
+ tools: {}
631
+ }
632
+ }
633
+ );
634
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
635
+ return {
636
+ tools: TOOLS.map((t) => ({
637
+ name: t.name,
638
+ description: t.description,
639
+ inputSchema: t.inputSchema
640
+ }))
641
+ };
642
+ });
643
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
644
+ const name = req.params.name;
645
+ const args = req.params.arguments ?? {};
646
+ const tool = TOOLS.find((t) => t.name === name);
647
+ if (!tool) {
648
+ return {
649
+ isError: true,
650
+ content: [
651
+ { type: "text", text: `Unknown tool: ${name}` }
652
+ ]
653
+ };
654
+ }
655
+ const validate = validators.get(name);
656
+ if (validate && !validate(args)) {
657
+ return {
658
+ isError: true,
659
+ content: [
660
+ { type: "text", text: `Invalid input: ${formatAjvErrors(validate)}` }
661
+ ]
662
+ };
663
+ }
664
+ try {
665
+ const result = await tool.handler(args, ctx);
666
+ return result;
667
+ } catch (e) {
668
+ const msg = formatToolError(e);
669
+ return {
670
+ isError: true,
671
+ content: [{ type: "text", text: msg }]
672
+ };
673
+ }
674
+ });
675
+ return { server, client };
676
+ }
677
+ function formatAjvErrors(validate) {
678
+ const errs = validate.errors ?? [];
679
+ if (errs.length === 0) return "schema validation failed";
680
+ return errs.map((e) => {
681
+ const path = e.instancePath || "(root)";
682
+ const detail = e.keyword === "additionalProperties" && e.params && "additionalProperty" in e.params ? `unexpected property '${e.params.additionalProperty}'` : e.message ?? "invalid";
683
+ return `${path} ${detail}`;
684
+ }).join("; ");
685
+ }
686
+ function formatToolError(err) {
687
+ if (err instanceof MissingApiKeyError) return err.message;
688
+ if (err instanceof ValidationError) return `Invalid input: ${err.message}`;
689
+ if (err instanceof RateLimitError) {
690
+ const seconds = err.retryAfter;
691
+ if (typeof seconds === "number" && Number.isFinite(seconds)) {
692
+ return `OrcaRouter rate limited (429). Retry after ${seconds} seconds.`;
693
+ }
694
+ return `OrcaRouter rate limited (429). Retry shortly.`;
695
+ }
696
+ if (err instanceof InsufficientQuotaError) {
697
+ return "OrcaRouter quota exhausted. Top up at https://orcarouter.ai/console/billing.";
698
+ }
699
+ if (err instanceof PermissionDeniedError) {
700
+ return `OrcaRouter permission denied (403): ${err.message}`;
701
+ }
702
+ if (err instanceof ApiError) {
703
+ return `OrcaRouter API error (${err.status}): ${err.message}`;
704
+ }
705
+ if (err instanceof Error) return err.message;
706
+ return String(err);
707
+ }
708
+
709
+ // src/index.ts
710
+ async function main() {
711
+ const apiKey = process.env.ORCAROUTER_API_KEY?.trim() || void 0;
712
+ const baseUrl = process.env.ORCAROUTER_BASE_URL?.trim() || void 0;
713
+ const timeoutRaw = process.env.ORCAROUTER_REQUEST_TIMEOUT?.trim();
714
+ let timeoutMs;
715
+ if (timeoutRaw) {
716
+ const parsedSeconds = Number(timeoutRaw);
717
+ if (Number.isFinite(parsedSeconds) && parsedSeconds > 0) {
718
+ timeoutMs = parsedSeconds * 1e3;
719
+ }
720
+ }
721
+ const { server } = createOrcaRouterMcpServer({
722
+ apiKey,
723
+ baseUrl,
724
+ timeoutMs
725
+ });
726
+ const transport = new StdioServerTransport();
727
+ await server.connect(transport);
728
+ }
729
+ main().catch((err) => {
730
+ process.stderr.write(
731
+ `[orcarouter-mcp] fatal: ${err instanceof Error ? err.message : String(err)}
732
+ `
733
+ );
734
+ process.exit(1);
735
+ });
736
+ //# sourceMappingURL=index.js.map