Files
OmniRoute/open-sse/mcp-server/server.ts
Diego Rodrigues de Sa e Souza 99c6dc7fd6 Release v3.7.9 (#1917)
* chore(release): v3.7.9 — gemini-cli cloud code separation

* chore(provider): Update Jina AI model catalog (#1874)

Integrated into release/v3.7.9

* docs: update CHANGELOG for PR 1874 and retroactive credits

* fix: resolve stream defaults and codex prompt mapping (#1873, #1872)

* chore(compression): start caveman compression update

* feat(compression): expand caveman compression and analytics pipeline

Add caveman intensity levels, output mode instructions, validation, and
preview diffs across the compression pipeline.

Extend MCP and dashboard settings to support auto-trigger mode, system
prompt preservation, MCP description compression, and caveman rule
metadata. Record richer compression analytics with receipt fields,
validation fallbacks, output mode data, and add the related database
migration.

Improve preservation handling for code, URLs, markdown, math, and other
protected content while adding broad unit, integration, and golden-set
coverage for caveman parity and compression behavior.

* feat(compression): expose rule intensities and track usd savings

Add estimated USD savings to compression analytics so saved tokens can
be reported in cost terms alongside existing token metrics.

Expose caveman rule intensity metadata for settings consumers and add a
settings API route alias for rule lookup. Also preserve system prompts
when aggressive compression falls back to lite mode.

* feat(compression): RTK compression roadmap (#1889)

* chore(rtk): initialize compression roadmap branch

* feat(compression): add RTK engine and compression combos

Introduce RTK command-aware tool-output compression alongside
stacked RTK -> Caveman pipelines for mixed prompt contexts.

Add engine registration, declarative RTK filter packs, language-aware
Caveman rule loading, compression combo persistence and assignments,
analytics grouped by engine/combo, and new MCP/API endpoints for
configuration, previews, filters, and combo management.

Expose the new capabilities in the dashboard with dedicated Context &
Cache pages for Caveman, RTK, and compression combos, and update docs,
i18n strings, migrations, and tests to cover the expanded compression
surface.

* feat(compression): expand RTK DSL, filter catalog, and recovery APIs

Add RTK parity features across the compression pipeline, dashboard,
and management APIs. This expands the built-in filter catalog, adds
trust-gated custom filter loading, inline filter verification, code
stripping, smarter detection, and optional redacted raw-output
retention for authenticated recovery.

Also extend Caveman with file-based multilingual rule packs, localized
output-mode instructions, stricter preview/config schemas, engine
registry metadata, analytics fields, and broad unit test coverage for
RTK, rule loading, and stacked compression behavior.

* fix(auth): protect oauth routes and health reset operations

Require authenticated dashboard access for OAuth endpoints that can
create or import provider connections when login enforcement is
enabled.

Move `/api/monitoring/health` to the readonly public route list so
safe methods remain public while DELETE now returns 401 for anonymous
requests.

Also update Next.js native `.node` handling to avoid webpack parse
failures from external packages such as ngrok and keytar, and add
coverage for the new auth behavior.

* build(compression): ship RTK rule and filter assets with app bundles

Include compression JSON assets in Next output tracing, prepublish copies,
and pack artifact policy checks so standalone and packaged builds can
load RTK filters and caveman rule packs at runtime.

Also harden compression runtime behavior by resolving alternate asset
directories, scoping rule cache entries by source path, carrying RTK raw
output pointers through stacked runs, degrading oversized preview diffs,
and applying combo language/output mode defaults during chat routing.

Add coverage for packaging rules, provider-scoped model parsing, smart
truncate edge cases, raw output retention, and combo-driven compression
behavior.

* docs(workflows): update local repo paths to OmniRoute

Replace outdated `/home/diegosouzapw/dev/proxys/9router` references
with the current `OmniRoute` directory across deploy, release, and
version bump workflow guides so local command examples match the
renamed repository layout

* feat(compression): complete RTK parity coverage

* test(build): align next config assertions

---------

Co-authored-by: diegosouzapw <diego.souza.pw@gmail.com>

* feat(compression): expand caveman parity and MCP metadata compression

Compress MCP registry and list metadata descriptions for tools, prompts,
resources, and resource templates while keeping tool-response bodies
unchanged. Expose those savings in compression status as
`mcp_metadata_estimate` metadata rather than provider usage.

Add Caveman rule-pack support for custom regex flags and match-specific
replacement maps, update English rules for upstream parity, and tighten
article and pleasantry handling. Also process RTK multipart text blocks
independently so mixed media content compresses safely without
duplicating output.

* feat(compression): unify config validation and persist MCP savings

Centralize compression config schemas across settings, preview, RTK,
and combo APIs to enforce consistent validation for stacked pipelines
and engine-specific options.

Expand caveman and stacked compression behavior by applying default
combos at runtime, surfacing validation and fallback metadata, and
exposing aggressive and ultra adapter schemas for configuration UI and
tests.

Persist MCP description compression snapshots into analytics without
counting them as provider usage, and extend the dashboard with the new
RTK controls and localized labels.

* fix(auth): require dashboard management auth for compression preview

Block preview requests unless they come from a valid management session
token so protected settings cannot be probed through the preview API.

Add unit coverage for unauthenticated requests, invalid bearer tokens,
and successful authenticated preview execution.

* fix(compression): preserve stacked defaults and secure metadata routes

Only apply saved default compression combos when they contain a stacked
pipeline so seeded Caveman-only defaults do not replace the builtin
stacked behavior.

Also require management auth for compression language pack and rules
metadata endpoints, and defer usage receipt attachment until compression
analytics writes have completed to keep analytics records consistent.

* fix(compression): align seeded standard savings combo with stacked default

Update the seeded default compression combo to use the RTK then
Caveman pipeline in both fresh installs and upgraded databases.

Add a targeted migration and runtime guard that only rewrites the
legacy seeded record when its original metadata and single-step
pipeline still match, preserving user-customized default combos.
Refresh docs and tests to reflect the stacked default and expanded
RTK filter catalog.

* docs(compression): document RTK+Caveman stacked savings ranges

Refresh the compression docs and README to describe the default
stacked pipeline in terms of eligible-context savings instead of the
older generic token-saving range.

Add upstream RTK and Caveman benchmark references, explain the
multiplicative savings math behind the stacked default, and update
feature summaries plus package metadata to match the revised
positioning.

* feat(image-gen): add NanoGPT image generation provider (#1899)

Integrated into release/v3.7.9

* fix(codex): sanitize raw responses input (#1895)

Integrated into release/v3.7.9

* Fix combo provider breaker profile handling (#1891)

Integrated into release/v3.7.9

* fix(combos): align strategy contracts (#1892)

Integrated into release/v3.7.9

* feat(proxy): move proxy configuration to dedicated System → Proxy page (#1907)

Integrated into release/v3.7.9

* feat: add K/M/B/T cost shortener to prevent UI overflow (#1902)

Integrated into release/v3.7.9

* feat(providers): implement bulk paste for extra API keys (#1916)

Integrated into release/v3.7.9

* fix(migrations): treat duplicate-column ALTER as no-op (#1886)

Integrated into release/v3.7.9

* fix(analytics): robust model pricing resolution, dark mode charts and SQL aggregation fixes (#1896)

Integrated into release/v3.7.9 (migration renumbered to 044)

* fix(oauth): per-connection mutex for rotating refresh tokens (#1885)

Integrated into release/v3.7.9

* fix: resolve 3 bugs — Codex tool normalization (#1914), image gen proxy (#1904), zero-arg MCP tools (#1898)

- fix(codex): flatten Chat Completions tool format to Responses format in
  normalizeCodexTools. Prevents 'Missing required parameter: tools[0].name'
  upstream errors when clients send {type:'function', function:{name,...}}
  instead of {type:'function', name,...}.

- fix(proxy): add proxy-aware execution context to image generation route.
  Image requests now correctly use proxy settings from the connection's
  ProxyRegistry assignment, matching the pattern used by chat pipeline.

- fix(translator): inject properties:{} into zero-argument MCP tool schemas
  during Anthropic→OpenAI translation. OpenAI strict mode requires explicit
  properties even for empty object schemas.

Closes #1914, Closes #1904, Closes #1898

* chore(release): v3.7.9 — all changes in ONE commit

* fix: allow local ollama provider connections (#1893)

* fix(copilot): emit compatible reasoning text deltas (#1919)

Integrated into release/v3.7.9

* fix(api-manager): show validation errors inline in modals, not behind backdrop (#1920)

Integrated into release/v3.7.9

* docs: update changelog and pr body with merged prs

* fix(providers): route agentrouter through anthropic endpoint headers

Update the AgentRouter provider registry to use the Claude-compatible
messages API and required Anthropic-style authentication headers. This
bypasses unauthorized_client_error responses and exposes the supported
model list through passthrough configuration.

Also update the changelog and release PR notes to document the fix for
#1921

* feat(logs): show compression tokens in request log UI (#1923)

* docs: add PR #1923 to changelog

---------

Co-authored-by: diegosouzapw <diego.souza.pw@gmail.com>
Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: Aculeasis <42580940+Aculeasis@users.noreply.github.com>
Co-authored-by: Raxxoor <manker_lol@hotmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Tubagus <54710482+0xtbug@users.noreply.github.com>
Co-authored-by: smartenok-ops <smartenok@gmail.com>
Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com>
Co-authored-by: ivan-mezentsev <ivan@mezentsev.me>
Co-authored-by: Andrew Munsell <andrew@wizardapps.net>
2026-05-04 01:36:53 -03:00

1037 lines
35 KiB
TypeScript

/**
* OmniRoute MCP Server — Model Context Protocol server exposing
* OmniRoute gateway intelligence as tools for AI agents.
*
* Supports two transports:
* 1. stdio — for IDE integration (VS Code, Cursor, Claude Desktop)
* 2. HTTP — for remote/programmatic access
*
* Tools wrap existing OmniRoute API endpoints and add intelligence
* such as routing simulation, budget guards, and session snapshots.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
getComboModelProvider,
getComboModelString,
getComboStepTarget,
} from "../../src/lib/combos/steps.ts";
import {
MCP_TOOLS,
getHealthInput,
listCombosInput,
getComboMetricsInput,
switchComboInput,
checkQuotaInput,
routeRequestInput,
costReportInput,
listModelsCatalogInput,
webSearchInput,
simulateRouteInput,
setBudgetGuardInput,
setRoutingStrategyInput,
setResilienceProfileInput,
testComboInput,
getProviderMetricsInput,
bestComboForTaskInput,
explainRouteInput,
getSessionSnapshotInput,
dbHealthCheckInput,
syncPricingInput,
cacheStatsInput,
cacheFlushInput,
oneproxyFetchInput,
oneproxyRotateInput,
oneproxyStatsInput,
} from "./schemas/tools.ts";
import { startMcpHeartbeat } from "./runtimeHeartbeat.ts";
import { closeAuditDb, logToolCall } from "./audit.ts";
import {
evaluateToolScopes,
resolveCallerScopeContext,
type McpToolExtraLike,
} from "./scopeEnforcement.ts";
import {
handleSimulateRoute,
handleSetBudgetGuard,
handleSetRoutingStrategy,
handleSetResilienceProfile,
handleTestCombo,
handleGetProviderMetrics,
handleBestComboForTask,
handleExplainRoute,
handleGetSessionSnapshot,
handleDbHealthCheck,
handleSyncPricing,
handleCacheStats,
handleCacheFlush,
handleOneproxyFetch,
handleOneproxyRotate,
handleOneproxyStats,
} from "./tools/advancedTools.ts";
import { memoryTools } from "./tools/memoryTools.ts";
import { skillTools } from "./tools/skillTools.ts";
import { compressionTools } from "./tools/compressionTools.ts";
import { compressMcpRegistryMetadata } from "./descriptionCompressor.ts";
import { getDbInstance } from "../../src/lib/db/core.ts";
import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts";
import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts";
// ============ Configuration ============
const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl();
const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || "";
const MCP_ENFORCE_SCOPES = process.env.OMNIROUTE_MCP_ENFORCE_SCOPES === "true";
const MCP_ALLOWED_SCOPES = new Set(
(process.env.OMNIROUTE_MCP_SCOPES || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
);
const TOTAL_MCP_TOOL_COUNT =
MCP_TOOLS.length + Object.keys(memoryTools).length + Object.keys(skillTools).length;
type JsonRecord = Record<string, unknown>;
function readMcpDescriptionCompressionEnabled(): boolean {
try {
const row = getDbInstance()
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
.get("compression", "mcpDescriptionCompressionEnabled") as { value?: string } | undefined;
if (!row?.value) return true;
return JSON.parse(row.value) !== false;
} catch {
return true;
}
}
type TextToolResult = {
content: Array<{ type: "text"; text: string }>;
isError?: boolean;
};
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function toString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function toNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function toStringArray(value: unknown, fallback: string[] = []): string[] {
const values = toArray(value).filter((entry): entry is string => typeof entry === "string");
return values.length > 0 ? values : fallback;
}
function normalizeComboModels(
rawModels: unknown
): Array<{ provider: string; model: string; priority: number }> {
return toArray(rawModels).map((rawModel, index) => {
const modelRecord = toRecord(rawModel);
const modelString = getComboModelString(rawModel);
const target = getComboStepTarget(rawModel);
const provider =
getComboModelProvider(rawModel) ||
(modelString ? "unknown" : target ? "combo" : toString(modelRecord.provider, "unknown"));
return {
provider,
model: modelString || target || toString(modelRecord.model, "unknown"),
priority: toNumber(modelRecord.priority, index + 1),
};
});
}
/**
* Internal fetch helper that calls OmniRoute API endpoints.
*/
async function omniRouteFetch(path: string, options: RequestInit = {}): Promise<unknown> {
const url = `${OMNIROUTE_BASE_URL}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}),
...((options.headers as Record<string, string>) || {}),
};
const signal = options.signal || AbortSignal.timeout(10000);
const response = await fetch(url, { ...options, headers, signal });
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error");
throw new Error(`OmniRoute API error [${response.status}]: ${errorText}`);
}
return response.json();
}
function withScopeEnforcement(
toolName: string,
handler: (args: unknown, extra?: McpToolExtraLike) => Promise<TextToolResult>
) {
return async (args: unknown, extra?: McpToolExtraLike): Promise<TextToolResult> => {
const scopeContext = resolveCallerScopeContext(extra, Array.from(MCP_ALLOWED_SCOPES));
const scopeCheck = evaluateToolScopes(toolName, scopeContext.scopes, MCP_ENFORCE_SCOPES);
if (!scopeCheck.allowed) {
const missingScopes =
scopeCheck.missing.length > 0 ? scopeCheck.missing.join(", ") : "unavailable";
const reason = scopeCheck.reason || "scope_check_failed";
const msg =
`Insufficient MCP scopes for ${toolName}. ` +
`Missing: ${missingScopes}. ` +
`Caller=${scopeContext.callerId}, source=${scopeContext.source}.`;
const safeArgs = args && typeof args === "object" ? toRecord(args) : { rawArgs: args };
await logToolCall(
toolName,
{
...safeArgs,
_scopeCheck: {
callerId: scopeContext.callerId,
source: scopeContext.source,
required: scopeCheck.required,
provided: scopeCheck.provided,
missing: scopeCheck.missing,
},
},
null,
0,
false,
`scope_denied:${reason}`
);
return {
content: [{ type: "text" as const, text: `Error: ${msg}` }],
isError: true,
};
}
return handler(args, extra);
};
}
// ============ Tool Handlers ============
async function handleGetHealth() {
const start = Date.now();
try {
const [healthRaw, resilienceRaw, rateLimitsRaw] = await Promise.allSettled([
omniRouteFetch("/api/monitoring/health"),
omniRouteFetch("/api/resilience"),
omniRouteFetch("/api/rate-limits"),
]);
const health = healthRaw.status === "fulfilled" ? toRecord(healthRaw.value) : {};
const resilience = resilienceRaw.status === "fulfilled" ? toRecord(resilienceRaw.value) : {};
const rateLimits = rateLimitsRaw.status === "fulfilled" ? toRecord(rateLimitsRaw.value) : {};
const memoryUsageRaw = toRecord(health.memoryUsage);
const cacheStatsRaw = toRecord(health.cacheStats);
const resilienceCircuitBreakers = toArray(resilience.circuitBreakers);
const rateLimitEntries = toArray(rateLimits.limits);
const result = {
uptime: toString(health.uptime, "unknown"),
version: toString(health.version, "unknown"),
memoryUsage: {
heapUsed: toNumber(memoryUsageRaw.heapUsed, 0),
heapTotal: toNumber(memoryUsageRaw.heapTotal, 0),
},
circuitBreakers: resilienceCircuitBreakers,
rateLimits: rateLimitEntries,
cacheStats:
Object.keys(cacheStatsRaw).length > 0
? {
hits: toNumber(cacheStatsRaw.hits, 0),
misses: toNumber(cacheStatsRaw.misses, 0),
hitRate: toNumber(cacheStatsRaw.hitRate, 0),
}
: undefined,
cryptography: health.cryptography
? {
status: toString(toRecord(health.cryptography).status, "missing_or_invalid"),
provider: toString(toRecord(health.cryptography).provider, "unknown"),
}
: undefined,
};
await logToolCall("omniroute_get_health", {}, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_get_health", {}, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleListCombos(args: { includeMetrics?: boolean }) {
const start = Date.now();
try {
const combosRaw = await omniRouteFetch("/api/combos");
const combosRecord = toRecord(combosRaw);
const combos = Array.isArray(combosRecord.combos)
? combosRecord.combos
: Array.isArray(combosRaw)
? combosRaw
: [];
let metrics: JsonRecord = {};
if (args.includeMetrics) {
metrics = toRecord(await omniRouteFetch("/api/combos/metrics").catch(() => ({})));
}
const result = {
combos: toArray(combos).map((rawCombo) => {
const combo = toRecord(rawCombo);
const comboData = toRecord(combo.data);
const comboId = toString(combo.id, "");
const modelsSource =
Array.isArray(combo.models) && combo.models.length > 0 ? combo.models : comboData.models;
return {
id: comboId,
name: toString(combo.name, comboId || "unnamed"),
models: normalizeComboModels(modelsSource),
strategy: toString(combo.strategy, toString(comboData.strategy, "priority")),
enabled: combo.enabled !== false,
...(args.includeMetrics ? { metrics: metrics[comboId] ?? null } : {}),
};
}),
};
await logToolCall("omniroute_list_combos", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_list_combos", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleGetComboMetrics(args: { comboId: string }) {
const start = Date.now();
try {
const result = await omniRouteFetch(
`/api/combos/metrics?comboId=${encodeURIComponent(args.comboId)}`
);
await logToolCall("omniroute_get_combo_metrics", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_get_combo_metrics", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleSwitchCombo(args: { comboId: string; active: boolean }) {
const start = Date.now();
try {
const result = await omniRouteFetch(`/api/combos/${encodeURIComponent(args.comboId)}`, {
method: "PUT",
body: JSON.stringify({ isActive: args.active }),
});
await logToolCall("omniroute_switch_combo", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_switch_combo", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleCheckQuota(args: { provider?: string; connectionId?: string }) {
const start = Date.now();
try {
let path = "/api/usage/quota";
if (args.connectionId) path += `?connectionId=${encodeURIComponent(args.connectionId)}`;
else if (args.provider) path += `?provider=${encodeURIComponent(args.provider)}`;
const result = normalizeQuotaResponse(await omniRouteFetch(path), {
provider: args.provider || null,
connectionId: args.connectionId || null,
});
await logToolCall("omniroute_check_quota", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_check_quota", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleRouteRequest(args: {
model: string;
messages: Array<{ role: string; content: string }>;
combo?: string;
budget?: number;
role?: string;
stream?: boolean;
}) {
const start = Date.now();
try {
const body: Record<string, unknown> = {
model: args.model,
messages: args.messages,
stream: false, // MCP tool always returns non-streaming
};
if (args.combo) {
body["x-combo"] = args.combo;
}
const raw = (await omniRouteFetch("/v1/chat/completions", {
method: "POST",
body: JSON.stringify(body),
})) as JsonRecord;
const choices = toArray(raw.choices);
const firstChoice = toRecord(choices[0]);
const firstMessage = toRecord(firstChoice.message);
const usage = toRecord(raw.usage);
const result = {
response: {
content: toString(firstMessage.content, ""),
model: toString(raw.model, args.model),
tokens: {
prompt: toNumber(usage.prompt_tokens, 0),
completion: toNumber(usage.completion_tokens, 0),
},
},
routing: {
provider: toString(raw.provider, "unknown"),
combo: raw.combo ?? null,
fallbacksTriggered: toNumber(raw.fallbacksTriggered, 0),
cost: toNumber(raw.cost, 0),
latencyMs: Date.now() - start,
routingExplanation: toString(
raw.routingExplanation,
"Request routed through primary provider"
),
},
};
await logToolCall(
"omniroute_route_request",
{ model: args.model, messageCount: args.messages.length },
result.routing,
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall(
"omniroute_route_request",
{ model: args.model },
null,
Date.now() - start,
false,
msg
);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleCostReport(args: { period?: string }) {
const start = Date.now();
try {
const period = args.period || "session";
const rangeMap: Record<string, string> = {
session: "1d",
day: "1d",
week: "7d",
month: "30d",
};
const range = rangeMap[period] || "30d";
const raw = toRecord(
await omniRouteFetch(`/api/usage/analytics?range=${encodeURIComponent(range)}`)
);
const tokenCount = toRecord(raw.tokenCount);
const budget = toRecord(raw.budget);
const result = {
period,
totalCost: toNumber(raw.totalCost, 0),
requestCount: toNumber(raw.requestCount, 0),
tokenCount: {
prompt: toNumber(tokenCount.prompt, 0),
completion: toNumber(tokenCount.completion, 0),
},
byProvider: toArray(raw.byProvider),
byModel: toArray(raw.byModel),
budget: {
limit: budget.limit ?? null,
remaining: budget.remaining ?? null,
},
};
await logToolCall("omniroute_cost_report", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_cost_report", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleListModelsCatalog(args: { provider?: string; capability?: string }) {
const start = Date.now();
try {
let path = "/v1/models";
let isProviderSpecific = false;
let source = "local_catalog";
let warning: string | undefined;
if (args.provider && !args.capability) {
// Use direct provider fetch to get real-time API status
path = `/api/providers/${encodeURIComponent(args.provider)}/models?excludeHidden=true`;
isProviderSpecific = true;
} else {
const params = new URLSearchParams();
if (args.provider) params.set("provider", args.provider);
if (args.capability) params.set("capability", args.capability);
if (params.toString()) path += `?${params.toString()}`;
}
const raw = toRecord(await omniRouteFetch(path));
// If we used the direct provider endpoint
let rawModels: unknown[] = [];
if (isProviderSpecific) {
rawModels = Array.isArray(raw.models) ? raw.models : [];
source = typeof raw.source === "string" ? raw.source : "api";
if (raw.warning) warning = String(raw.warning);
} else {
rawModels = Array.isArray(raw.data) ? raw.data : [];
source = "local_catalog";
// OmniRoute's global /v1/models is always a cached/local catalog
}
const result = {
models: rawModels.map((rawModel) => {
const model = toRecord(rawModel);
return {
id: toString(model.id, ""),
provider: toString(model.owned_by, toString(model.provider, args.provider || "unknown")),
capabilities: toStringArray(model.capabilities, ["chat"]),
status: toString(model.status, "available"),
pricing: model.pricing,
};
}),
source,
...(warning ? { warning } : {}),
};
await logToolCall(
"omniroute_list_models_catalog",
args,
{ modelCount: result.models.length },
Date.now() - start,
true
);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_list_models_catalog", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
async function handleWebSearch(args: {
query: string;
max_results?: number;
search_type?: "web" | "news";
provider?:
| "serper-search"
| "brave-search"
| "perplexity-search"
| "exa-search"
| "tavily-search"
| "google-pse-search"
| "linkup-search"
| "searchapi-search"
| "searxng-search";
}) {
const start = Date.now();
try {
const body: Record<string, unknown> = {
query: args.query,
max_results: args.max_results ?? 5,
search_type: args.search_type ?? "web",
};
if (args.provider) body.provider = args.provider;
const result = await omniRouteFetch("/v1/search", {
method: "POST",
body: JSON.stringify(body),
signal: AbortSignal.timeout(60000),
});
await logToolCall("omniroute_web_search", args, result, Date.now() - start, true);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await logToolCall("omniroute_web_search", args, null, Date.now() - start, false, msg);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
}
// ============ MCP Server Setup ============
/**
* Create and configure the OmniRoute MCP Server with all essential tools.
*/
export function createMcpServer(): McpServer {
const server = new McpServer({
name: "omniroute",
version: process.env.npm_package_version || "1.8.1",
});
const mcpDescriptionCompressionEnabled = readMcpDescriptionCompressionEnabled();
const registerTool = server.registerTool.bind(server);
server.registerTool = ((name: string, config: Record<string, unknown>, handler: unknown) => {
const metadata = compressMcpRegistryMetadata(config, {
enabled: mcpDescriptionCompressionEnabled,
});
return registerTool(name, metadata, handler as never);
}) as typeof server.registerTool;
const registerPrompt = server.registerPrompt.bind(server);
server.registerPrompt = ((name: string, config: Record<string, unknown>, handler: unknown) => {
const metadata = compressMcpRegistryMetadata(config, {
enabled: mcpDescriptionCompressionEnabled,
});
return registerPrompt(name, metadata as never, handler as never);
}) as typeof server.registerPrompt;
const registerResource = server.registerResource.bind(server);
server.registerResource = ((
name: string,
uriOrTemplate: unknown,
config: Record<string, unknown>,
readCallback: unknown
) => {
const metadata = compressMcpRegistryMetadata(config, {
enabled: mcpDescriptionCompressionEnabled,
});
return registerResource(name, uriOrTemplate as never, metadata as never, readCallback as never);
}) as typeof server.registerResource;
// Register essential tools
server.registerTool(
"omniroute_get_health",
{
description:
"Returns OmniRoute health status including uptime, memory, circuit breakers, rate limits, and cache stats",
inputSchema: getHealthInput,
},
withScopeEnforcement("omniroute_get_health", async (args) => {
getHealthInput.parse(args ?? {});
return handleGetHealth();
})
);
server.registerTool(
"omniroute_list_combos",
{
description:
"Lists all configured combos (model chains) with strategies and optional metrics",
inputSchema: listCombosInput,
},
withScopeEnforcement("omniroute_list_combos", (args) =>
handleListCombos(listCombosInput.parse(args))
)
);
server.registerTool(
"omniroute_get_combo_metrics",
{
description: "Returns detailed performance metrics for a specific combo",
inputSchema: getComboMetricsInput,
},
withScopeEnforcement("omniroute_get_combo_metrics", (args) =>
handleGetComboMetrics(getComboMetricsInput.parse(args))
)
);
server.registerTool(
"omniroute_switch_combo",
{
description: "Activates or deactivates a combo for routing",
inputSchema: switchComboInput,
},
withScopeEnforcement("omniroute_switch_combo", (args) =>
handleSwitchCombo(switchComboInput.parse(args))
)
);
server.registerTool(
"omniroute_check_quota",
{
description: "Checks remaining API quota for one or all providers",
inputSchema: checkQuotaInput,
},
withScopeEnforcement("omniroute_check_quota", (args) =>
handleCheckQuota(checkQuotaInput.parse(args))
)
);
server.registerTool(
"omniroute_route_request",
{
description: "Sends a chat completion request through OmniRoute intelligent routing",
inputSchema: routeRequestInput,
},
withScopeEnforcement("omniroute_route_request", (args) =>
handleRouteRequest(routeRequestInput.parse(args))
)
);
server.registerTool(
"omniroute_cost_report",
{
description: "Generates a cost report for the specified period",
inputSchema: costReportInput,
},
withScopeEnforcement("omniroute_cost_report", (args) =>
handleCostReport(costReportInput.parse(args))
)
);
server.registerTool(
"omniroute_list_models_catalog",
{
description: "Lists all available AI models across providers with capabilities and pricing",
inputSchema: listModelsCatalogInput,
},
withScopeEnforcement("omniroute_list_models_catalog", (args) =>
handleListModelsCatalog(listModelsCatalogInput.parse(args))
)
);
// ── Advanced Tools (Phase 3) ──────────────────────────────
server.registerTool(
"omniroute_simulate_route",
{
description: "Simulates the routing path a request would take without executing it (dry-run)",
inputSchema: simulateRouteInput,
},
withScopeEnforcement("omniroute_simulate_route", (args) =>
handleSimulateRoute(simulateRouteInput.parse(args))
)
);
server.registerTool(
"omniroute_set_budget_guard",
{
description:
"Sets a session budget limit with configurable action when exceeded (degrade/block/alert)",
inputSchema: setBudgetGuardInput,
},
withScopeEnforcement("omniroute_set_budget_guard", (args) =>
handleSetBudgetGuard(setBudgetGuardInput.parse(args))
)
);
server.registerTool(
"omniroute_set_routing_strategy",
{
description:
"Updates combo routing strategy at runtime (priority/weighted/round-robin/auto/etc.)",
inputSchema: setRoutingStrategyInput,
},
withScopeEnforcement("omniroute_set_routing_strategy", (args) =>
handleSetRoutingStrategy(setRoutingStrategyInput.parse(args))
)
);
server.registerTool(
"omniroute_set_resilience_profile",
{
description:
"Applies a resilience profile controlling circuit breakers, retries, timeouts, and fallback depth",
inputSchema: setResilienceProfileInput,
},
withScopeEnforcement("omniroute_set_resilience_profile", (args) =>
handleSetResilienceProfile(setResilienceProfileInput.parse(args))
)
);
server.registerTool(
"omniroute_test_combo",
{
description:
"Tests each provider in a combo with a real prompt, reporting latency, cost, and success per provider",
inputSchema: testComboInput,
},
withScopeEnforcement("omniroute_test_combo", (args) =>
handleTestCombo(testComboInput.parse(args))
)
);
server.registerTool(
"omniroute_get_provider_metrics",
{
description:
"Returns detailed metrics for a specific provider including latency percentiles and circuit breaker state",
inputSchema: getProviderMetricsInput,
},
withScopeEnforcement("omniroute_get_provider_metrics", (args) =>
handleGetProviderMetrics(getProviderMetricsInput.parse(args))
)
);
server.registerTool(
"omniroute_best_combo_for_task",
{
description:
"Recommends the best combo for a task type based on provider fitness and constraints",
inputSchema: bestComboForTaskInput,
},
withScopeEnforcement("omniroute_best_combo_for_task", (args) =>
handleBestComboForTask(bestComboForTaskInput.parse(args))
)
);
server.registerTool(
"omniroute_explain_route",
{
description:
"Explains why a request was routed to a specific provider, showing scoring factors and fallbacks",
inputSchema: explainRouteInput,
},
withScopeEnforcement("omniroute_explain_route", (args) =>
handleExplainRoute(explainRouteInput.parse(args))
)
);
server.registerTool(
"omniroute_get_session_snapshot",
{
description:
"Returns a full snapshot of the current working session: cost, tokens, top models, errors, budget status",
inputSchema: getSessionSnapshotInput,
},
withScopeEnforcement("omniroute_get_session_snapshot", async (args) => {
getSessionSnapshotInput.parse(args ?? {});
return handleGetSessionSnapshot();
})
);
server.registerTool(
"omniroute_db_health_check",
{
description:
"Diagnoses or repairs OmniRoute database drift, including broken combo references and orphan quota/domain rows",
inputSchema: dbHealthCheckInput,
},
withScopeEnforcement("omniroute_db_health_check", (args) =>
handleDbHealthCheck(dbHealthCheckInput.parse(args ?? {}))
)
);
server.registerTool(
"omniroute_sync_pricing",
{
description:
"Syncs pricing data from external sources (LiteLLM) into OmniRoute without overwriting user-set prices",
inputSchema: syncPricingInput,
},
withScopeEnforcement("omniroute_sync_pricing", (args) =>
handleSyncPricing(syncPricingInput.parse(args))
)
);
server.registerTool(
"omniroute_web_search",
{
description:
"Performs a web search using OmniRoute's search gateway. Supports multiple providers (Serper, Brave, Perplexity, Exa, Tavily) with automatic failover. Returns search results with titles, URLs, snippets, and position data.",
inputSchema: webSearchInput,
},
withScopeEnforcement("omniroute_web_search", (args) =>
handleWebSearch(webSearchInput.parse(args))
)
);
server.registerTool(
"omniroute_cache_stats",
{
description:
"Returns cache statistics including semantic cache hit rate, prompt cache metrics by provider, and idempotency layer stats.",
inputSchema: cacheStatsInput,
},
withScopeEnforcement("omniroute_cache_stats", () => handleCacheStats())
);
server.registerTool(
"omniroute_cache_flush",
{
description:
"Flush cache entries. Provide signature to invalidate a single entry, model to invalidate all entries for a model, or omit both to clear all.",
inputSchema: cacheFlushInput,
},
withScopeEnforcement("omniroute_cache_flush", (args) =>
handleCacheFlush(cacheFlushInput.parse(args))
)
);
// ── 1proxy Tools ──────────────────────────────
server.registerTool(
"omniroute_oneproxy_fetch",
{
description:
"Fetch free proxies from the 1proxy marketplace with optional filters for protocol, country, and quality. Returns validated proxies with quality scores.",
inputSchema: oneproxyFetchInput,
},
withScopeEnforcement("omniroute_oneproxy_fetch", (args) =>
handleOneproxyFetch(oneproxyFetchInput.parse(args))
)
);
server.registerTool(
"omniroute_oneproxy_rotate",
{
description:
"Get the next available free proxy from the 1proxy pool using the specified rotation strategy.",
inputSchema: oneproxyRotateInput,
},
withScopeEnforcement("omniroute_oneproxy_rotate", (args) =>
handleOneproxyRotate(oneproxyRotateInput.parse(args))
)
);
server.registerTool(
"omniroute_oneproxy_stats",
{
description:
"Returns 1proxy sync status and statistics: total proxies, average quality, sync history, and distribution by protocol and country.",
inputSchema: oneproxyStatsInput,
},
withScopeEnforcement("omniroute_oneproxy_stats", (args) =>
handleOneproxyStats(oneproxyStatsInput.parse(args))
)
);
// ── Memory Tools ──────────────────────────────
Object.values(memoryTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
// @ts-ignore: dynamic zod access
inputSchema: toolDef.inputSchema,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
});
// ── Skill Tools ──────────────────────────────
Object.values(skillTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
// @ts-ignore: dynamic zod access
inputSchema: toolDef.inputSchema,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
});
// ── Compression Tools ─────────────────────────
Object.values(compressionTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
// @ts-ignore: dynamic zod access
inputSchema: toolDef.inputSchema,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
// @ts-ignore: handler expected specific object
const result = await toolDef.handler(parsedArgs);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
});
return server;
}
// ============ Main Entry Point (stdio) ============
/**
* Start the MCP server with stdio transport.
* Called when `omniroute --mcp` is used.
*/
export async function startMcpStdio(): Promise<void> {
const server = createMcpServer();
const transport = new StdioServerTransport();
const version = process.env.npm_package_version || "1.8.1";
const stopHeartbeat = startMcpHeartbeat({
version,
scopesEnforced: MCP_ENFORCE_SCOPES,
allowedScopes: Array.from(MCP_ALLOWED_SCOPES),
toolCount: TOTAL_MCP_TOOL_COUNT,
});
const stopHeartbeatOnce = () => {
stopHeartbeat();
};
process.once("exit", stopHeartbeatOnce);
process.once("SIGINT", stopHeartbeatOnce);
process.once("SIGTERM", stopHeartbeatOnce);
console.error("[MCP] OmniRoute MCP Server starting (stdio transport)...");
try {
await server.connect(transport);
console.error("[MCP] OmniRoute MCP Server connected and ready.");
} finally {
if (closeAuditDb()) {
console.error("[MCP] Audit database checkpointed and closed.");
}
stopHeartbeatOnce();
process.off("exit", stopHeartbeatOnce);
process.off("SIGINT", stopHeartbeatOnce);
process.off("SIGTERM", stopHeartbeatOnce);
}
}
// If this file is run directly, start stdio server
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) {
startMcpStdio().catch((err) => {
console.error("[MCP] Fatal error:", err);
process.exit(1);
});
}