Files
OmniRoute/open-sse/mcp-server/server.ts
Diego Rodrigues de Sa e Souza a25d5f1ef6 Release v3.8.13 (#3327)
* chore(release): open v3.8.13 development cycle

Bump 3.8.12 → 3.8.13 across package.json, lockfile, electron/, open-sse/, and
docs/reference/openapi.yaml; add the [3.8.13] cycle placeholder to the root
CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.13 cycle —
fixes/features land here via per-issue PRs and it merges to main at release time.

* fix(ci): skip auto-deploy when VPS host is unreachable from the runner (#3299)

Integrated into release/v3.8.13

* fix(dev): auto-rebuild better-sqlite3 on Node ABI mismatch at dev startup (#3301)

Integrated into release/v3.8.13

* feat(api): accept path-scoped API keys on client API routes (#3300)

Integrated into release/v3.8.13

* fix(sse): harden against empty responses causing Copilot Chat failures (#3297)

Integrated into release/v3.8.13

* fix(api): remove Completions.me rickroll provider (discussion #3293) (#3302)

Integrated into release/v3.8.13

* fix(opencode-provider): extract contextLength from live model catalog (#3298)

Integrated into release/v3.8.13

* feat(web-cookie): self-service login infrastructure + auto-refresh daemon (#3292)

Integrated into release/v3.8.13

* docs(changelog): record the v3.8.13 PRs merged this round (#3292/#3300/#3297/#3298/#3301/#3302/#3299)

* fix(auth): harden URL token extraction — drop query-string fallback, gate to client routes (security follow-up to #3300) (#3309)

Security follow-up to #3300 — integrated into release/v3.8.13

* docs: rename resolve-issues → review-issues skill references

* fix(dashboard): keep no-auth providers visible under 'Show configured only' (#3290) (#3312)

no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) never
create a DB connection row so stats.total stays 0, which the configured-only
filter treated as 'unconfigured' and hid them — even though they are always
usable and appear unconditionally in /v1/models. filterConfiguredProviderEntries
now treats displayAuthType === 'no-auth' as configured.

Co-authored-by: uniQta <uniQta@users.noreply.github.com>

* fix(cli): resolve update paths relative to script + recursive backup (#3295) (#3313)

omniroute update always failed on a global install:
- getCurrentVersion() read package.json from process.cwd(), which on a global
  npm/brew install is the user's working dir, not the package root → null →
  'Could not determine current version'.
- createBackup() resolved bin/ from cwd too, and passed the 'cli' directory to
  copyFileSync → EISDIR, swallowed by the catch → 'Failed to create backup'.

Both now resolve package.json/bin relative to the script via import.meta.url,
and the backup uses cpSync({recursive:true}) so the cli/ directory is copied.

Co-authored-by: uniQta <uniQta@users.noreply.github.com>

* fix(theoldllm): read upstream body once to avoid [502] body-already-read (#3296) (#3314)

On the cached-token path the executor never enters the refresh branch, so the
same upstream Response was read with .text() twice (token-rejection check +
final body). A Response body is single-use, so the second read threw
'Body is unusable: Body has already been read', caught and surfaced as [502].

Read the body once into finalBody and only re-read after a token-rejection
refetch.

Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>

* fix(sse): strip leaked internal tool envelopes from streaming output (#3311)

Integrated into release/v3.8.13

* fix(sse): expose Claude + Gemini budget tiers in the antigravity catalog (#3184) (#3303)

Integrated into release/v3.8.13 (#3184)

* fix(catalog): compute combo context_length from known targets only (#3304)

Integrated into release/v3.8.13 — live contextLength + known-targets combo context (#3298 follow-up)

* chore(i18n): add message keys for proxy UI + vscode/ollama endpoint (#3307)

Integrated into release/v3.8.13 — i18n message keys for proxy UI + vscode/ollama

* feat(dashboard): i18n the proxy settings UI (#3310)

Integrated into release/v3.8.13 — i18n the proxy settings UI

* feat(api): model catalog enrichment + MCP model-catalog tools (#3306)

Integrated into release/v3.8.13 — model catalog enrichment + MCP model-catalog tools, reconciled with #3309 URL-token hardening

* test(catalog): align Antigravity preview-alias test with #3303 budget tiers

#3303 added the Gemini `-high`/`-low` budget tiers to ANTIGRAVITY_PUBLIC_MODELS
(user-callable on the Antigravity OAuth backend, verified via #3184), but did
not update the catalog-route test that asserted `antigravity/gemini-3.1-pro-high`
must NOT be exposed. The assertion now reflects the intended behavior — the
client-visible budget alias IS surfaced — while keeping the legacy
`gemini-claude-*` alias keys unexposed. Caught running the full catalog suite
on the merged release HEAD (the #3303 round only ran the antigravity-aliases
and usage-hardening files).

* docs(changelog): record the 6 PRs merged this review round into v3.8.13

#3306/#3307/#3310 (New Features — VS Code split: catalog+MCP, i18n keys, proxy
UI i18n), #3311/#3303/#3304 (Bug Fixes — SSE envelope sanitizer, antigravity
budget tiers, combo known-targets context_length).

* chore(release): finalize v3.8.13 changelog and cleanup

Finalize the v3.8.13 changelog with release date, maintenance notes,
and contributor credits. Update MCP docs to reference the correct tool
inventory diagram, exclude nested .claude worktrees from ESLint scans,
and tighten a response sanitizer type guard.

* fix(dashboard): refresh connections after provider auth import (#3320)

Integrated into release/v3.8.13 — refresh connections after provider auth import

* fix(codex): strip client-only params on native /responses passthrough (#3317) (#3325)

A /v1/responses request against the built-in codex/ provider does an
openai-responses -> openai-responses passthrough (CodexExecutor.transformRequest
returns the body early for _nativeCodexPassthrough). It forwarded client-only
fields verbatim and the Codex upstream rejected them with 400 Unsupported
parameter: prompt_cache_retention / safety_identifier / user — breaking Factory
Droid (which injects all three). The chat-completions path already strips these
(base.ts #1884, openai-responses translator #2770) but the passthrough skips
translation. Strip the three fields in the shared block before the passthrough
return; user is removed unconditionally since Codex /responses always rejects it.

Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>

* fix(dashboard): normalize agent-bridge /state response to stop page crash (#3318) (#3326)

The Agent Bridge page seeded a well-shaped initialData default then replaced it
wholesale with the raw /api/tools/agent-bridge/state response. The route returns
{ server, agents } but the UI reads { serverState, agentStates, bypassPatterns,
mappings }, so serverState became undefined and AgentBridgeServerCard crashed on
serverState.running — surfaced as the full-page 'Internal Server Error' boundary
(client render error, not a real 5xx).

Add a shared normalizeAgentBridgeState() that maps the route shape into the page
contract (server.running/certExists -> serverState) and always returns safe
defaults (never undefined serverState). Wired into both the SSR loader (page.tsx)
and the polling hook. The legacy 'agents' entry shape differs from AgentStateEntry
so it is not coerced; full route<->page contract reconciliation (port, upstreamCa,
bypassPatterns, mappings, agentStates) is a follow-up.

Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>

* docs: VS Code/Ollama endpoints + env & i18n tooling (#3319)

Integrated into release/v3.8.13 — VS Code/Ollama docs + env & i18n tooling

* feat(provider): test-all endpoint, rate-limit overrides, visibility f… (#3267)

Integrated into release/v3.8.13 — provider test-all endpoint, rate-limit overrides, model visibility

* feat: auto-combo optimization, playground model dropdown, only-configured toggle (#3322)

Integrated into release/v3.8.13 — auto-combo candidate expansion + playground dropdown + only-configured toggle

* feat(api): VS Code Copilot Ollama-compatible BYOK endpoint (#3316)

Integrated into release/v3.8.13 — VS Code Copilot Ollama-compatible BYOK endpoint (reconciled with #3306/#3309 auth hardening)

* chore(release): document #3320 in the v3.8.13 changelog + contributor credits

---------

Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
Co-authored-by: Vinayrnani <vinayrnani@gmail.com>
2026-06-06 19:13:11 -03:00

1457 lines
50 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 { z } from "zod";
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 { agentSkillTools } from "./tools/agentSkillTools.ts";
import { skillRegistry } from "../../src/lib/skills/registry.ts";
import { skillExecutor } from "../../src/lib/skills/executor.ts";
import { pluginTools } from "./tools/pluginTools.ts";
import { compressionTools } from "./tools/compressionTools.ts";
import { gamificationTools } from "./tools/gamificationTools.ts";
import { notionTools } from "./tools/notionTools.ts";
import { obsidianTools } from "./tools/obsidianTools.ts";
import { compressMcpRegistryMetadata } from "./descriptionCompressor.ts";
import { smartFilterText } from "../services/compression/engines/mcpAccessibility/index.ts";
import {
DEFAULT_MCP_ACCESSIBILITY_CONFIG,
type McpAccessibilityConfig,
} from "../services/compression/engines/mcpAccessibility/constants.ts";
import { getDbInstance } from "../../src/lib/db/core.ts";
import { getProviderConnections } from "../../src/lib/db/providers.ts";
import { getCodexRequestDefaults } from "../../src/lib/providers/requestDefaults.ts";
import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts";
import { AI_PROVIDERS, NOAUTH_PROVIDERS } from "../../src/shared/constants/providers.ts";
import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts";
// ============ Configuration ============
const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl();
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 +
Object.keys(agentSkillTools).length +
gamificationTools.length +
pluginTools.length +
notionTools.length +
obsidianTools.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;
}
}
function readMcpAccessibilityConfig(): McpAccessibilityConfig {
try {
const row = getDbInstance()
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
.get("compression", "mcpAccessibility") as { value?: string } | undefined;
if (!row?.value) return { ...DEFAULT_MCP_ACCESSIBILITY_CONFIG };
const parsed = JSON.parse(row.value);
if (!parsed || typeof parsed !== "object") return { ...DEFAULT_MCP_ACCESSIBILITY_CONFIG };
return { ...DEFAULT_MCP_ACCESSIBILITY_CONFIG, ...parsed };
} catch {
return { ...DEFAULT_MCP_ACCESSIBILITY_CONFIG };
}
}
type TextToolResult = {
content: Array<{ type: "text"; text: string }>;
isError?: boolean;
};
type McpCatalogStatus = "available" | "degraded" | "unavailable";
type McpCatalogResponse = {
models: Array<{
id: string;
provider: string;
capabilities: string[];
status: McpCatalogStatus;
thinkingEffort?: string;
pricing?: unknown;
}>;
source: string;
warning?: string;
};
type ProviderConnectionLike = {
id?: string;
provider?: string;
isActive?: boolean;
providerSpecificData?: unknown;
};
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.
*/
function getOmniRouteApiKey(): string {
return process.env.OMNIROUTE_API_KEY || "";
}
async function omniRouteFetch(path: string, options: RequestInit = {}): Promise<unknown> {
const url = `${OMNIROUTE_BASE_URL}${path}`;
const apiKey = getOmniRouteApiKey();
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
...((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 buildProviderAliasMap(): Record<string, string> {
const aliasMap: Record<string, string> = {};
for (const provider of Object.values(AI_PROVIDERS)) {
if (!provider?.id) continue;
aliasMap[provider.id] = provider.id;
if (typeof provider.alias === "string" && provider.alias.length > 0) {
aliasMap[provider.alias] = provider.id;
}
}
for (const provider of Object.values(NOAUTH_PROVIDERS)) {
if (!provider?.id) continue;
aliasMap[provider.id] = provider.id;
if ("alias" in provider && typeof provider.alias === "string" && provider.alias.length > 0) {
aliasMap[provider.alias] = provider.id;
}
}
return aliasMap;
}
function normalizeCapability(value: string): string {
switch (value) {
case "embeddings":
return "embedding";
case "images":
return "image";
case "videos":
return "video";
case "moderations":
return "moderation";
case "chat-completions":
return "chat";
default:
return value;
}
}
function getCatalogModelCapabilities(model: JsonRecord): string[] {
if (Array.isArray(model.capabilities) && model.capabilities.length > 0) {
return toStringArray(model.capabilities, ["chat"]).map(normalizeCapability);
}
if (Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length > 0) {
return toStringArray(model.supportedEndpoints, ["chat"]).map(normalizeCapability);
}
const type = toString(model.type);
if (type) return [normalizeCapability(type)];
return ["chat"];
}
function normalizeCatalogStatus(model: JsonRecord, source: string, warning?: string): McpCatalogStatus {
const explicitStatus = toString(model.status);
if (explicitStatus === "available" || explicitStatus === "degraded" || explicitStatus === "unavailable") {
return explicitStatus;
}
if (warning || source === "local_catalog") return "degraded";
return "available";
}
function getConnectionThinkingEffort(connection: ProviderConnectionLike): string | undefined {
const provider = typeof connection.provider === "string" ? connection.provider : null;
const providerSpecificData = toRecord(connection.providerSpecificData);
if (provider === "codex") {
return getCodexRequestDefaults(providerSpecificData).reasoningEffort || "medium";
}
const rawThinkingEffort = toString(providerSpecificData.thinkingEffort);
return rawThinkingEffort || undefined;
}
function normalizeProviderModelRecord(
rawModel: unknown,
fallbackProvider: string,
source: string,
warning?: string,
thinkingEffort?: string
) {
const model = toRecord(rawModel);
const id = toString(model.id, "");
return {
id,
provider: toString(model.owned_by, toString(model.provider, fallbackProvider)),
capabilities: getCatalogModelCapabilities(model),
status: normalizeCatalogStatus(model, source, warning),
...(thinkingEffort ? { thinkingEffort } : {}),
pricing: model.pricing,
};
}
export async function getMcpModelsCatalog(
args: { provider?: string; capability?: string },
deps: {
fetchJson?: (path: string) => Promise<unknown>;
listProviderConnections?: () => Promise<ProviderConnectionLike[]>;
} = {}
): Promise<McpCatalogResponse> {
const fetchJson = deps.fetchJson ?? ((path: string) => omniRouteFetch(path));
const listProviderConnections = deps.listProviderConnections ?? getProviderConnections;
const aliasMap = buildProviderAliasMap();
const normalizeProviderId = (value: string) => aliasMap[value] || value;
const requestedProvider = args.provider ? normalizeProviderId(args.provider) : null;
const requestedCapability = args.capability ? normalizeCapability(args.capability) : null;
let connections = await listProviderConnections();
connections = Array.isArray(connections) ? connections : [];
const activeConnections = connections.filter((connection) => {
const provider = typeof connection?.provider === "string" ? normalizeProviderId(connection.provider) : null;
if (!provider || !connection?.id || connection.isActive === false) return false;
if (requestedProvider && provider !== requestedProvider) return false;
return true;
});
const requestSpecs = activeConnections.map((connection) => ({
provider: normalizeProviderId(String(connection.provider)),
path: `/api/providers/${encodeURIComponent(String(connection.id))}/models?excludeHidden=true`,
thinkingEffort: getConnectionThinkingEffort(connection),
}));
if (requestedProvider && requestSpecs.length === 0) {
const isNoAuthProvider = Object.values(NOAUTH_PROVIDERS).some((provider) => provider.id === requestedProvider);
if (isNoAuthProvider) {
requestSpecs.push({
provider: requestedProvider,
path: `/api/v1/providers/${encodeURIComponent(requestedProvider)}/models`,
thinkingEffort: undefined,
});
} else {
return {
models: [],
source: "provider_connections",
warning: `No active connections found for provider '${requestedProvider}'.`,
};
}
}
const collectedModels = new Map<string, McpCatalogResponse["models"][number]>();
const warnings = new Set<string>();
const sources = new Set<string>();
for (const spec of requestSpecs) {
const raw = toRecord(await fetchJson(spec.path));
const source = toString(raw.source, spec.path.startsWith("/api/providers/") ? "api" : "v1_catalog");
const warning = raw.warning ? String(raw.warning) : undefined;
if (warning) warnings.add(warning);
sources.add(source);
const rawModels = Array.isArray(raw.models)
? raw.models
: Array.isArray(raw.data)
? raw.data
: [];
for (const rawModel of rawModels) {
const normalized = normalizeProviderModelRecord(rawModel, spec.provider, source, warning);
if (spec.thinkingEffort && !normalized.thinkingEffort) {
normalized.thinkingEffort = spec.thinkingEffort;
}
if (!normalized.id) continue;
if (requestedCapability && !normalized.capabilities.includes(requestedCapability)) continue;
const key = `${normalized.provider}:${normalized.id}`;
if (!collectedModels.has(key)) {
collectedModels.set(key, normalized);
}
}
}
return {
models: [...collectedModels.values()],
source: sources.size === 1 ? [...sources][0] : "aggregated_provider_models",
...(warnings.size > 0 ? { warning: [...warnings].join(" | ") } : {}),
};
}
function withScopeEnforcement(
toolName: string,
handler: (args: unknown, extra?: McpToolExtraLike) => Promise<TextToolResult>,
toolScopes?: readonly string[]
) {
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, toolScopes);
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 {
const result = await getMcpModelsCatalog(args);
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 mcpAccessibilityConfig = readMcpAccessibilityConfig();
const registerTool = server.registerTool.bind(server);
server.registerTool = ((name: string, config: Record<string, unknown>, handler: unknown) => {
const metadata = compressMcpRegistryMetadata(config, {
enabled: mcpDescriptionCompressionEnabled,
});
const filteredHandler = mcpAccessibilityConfig.enabled
? async (args: unknown, extra?: unknown) => {
const result = await (handler as (a: unknown, e?: unknown) => Promise<TextToolResult>)(
args,
extra
);
if (Array.isArray(result?.content)) {
for (const block of result.content) {
if (block && block.type === "text" && typeof block.text === "string") {
block.text = smartFilterText(block.text, mcpAccessibilityConfig);
}
}
}
return result;
}
: handler;
return registerTool(name, metadata, filteredHandler 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;
const RESERVED_MCP_NAMES = new Set([
...MCP_TOOLS.map((t) => t.name),
...Object.keys(memoryTools),
...Object.keys(skillTools),
...Object.keys(compressionTools),
...pluginTools.map((t) => t.name),
...gamificationTools.map((t) => t.name),
...obsidianTools.map((t) => t.name),
...notionTools.map((t) => t.name),
]);
// 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: any) => {
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 type lost through dynamic Object.values() access
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 };
}
},
toolDef.scopes
)
);
});
// ── Skill Tools ──────────────────────────────
Object.values(skillTools).forEach((toolDef: any) => {
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 type lost through dynamic Object.values() access
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 };
}
},
toolDef.scopes
)
);
});
// ── Agent Skill Tools ─────────────────────────
Object.values(agentSkillTools).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-expect-error - handler type lost through dynamic Object.values() access
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 };
}
})
);
});
// ── Plugin Tools ──────────────────────────────
pluginTools.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 };
}
},
toolDef.scopes
)
);
});
// ── Compression Tools ─────────────────────────
Object.values(compressionTools).forEach((toolDef: any) => {
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 type lost through dynamic Object.values() access
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 };
}
},
toolDef.scopes
)
);
});
// ── Gamification Tools ────────────────────────
gamificationTools.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 };
}
},
toolDef.scopes
)
);
});
// ── Notion Context Source Tools ───────────────
notionTools.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 };
}
},
toolDef.scopes
)
);
});
// ── Obsidian Context Source Tools ─────────────
obsidianTools.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 };
}
},
toolDef.scopes
)
);
});
// ── Dynamic Skill Tools (from skills table) ──
const skillToMcpToolName = (skill: { name: string }) => `skill_${skill.name.replace(/[^a-z0-9_-]/gi, "_")}`;
try {
const enabledSkills = skillRegistry.list().filter((s) => s.enabled);
for (const skill of enabledSkills) {
const toolName = skillToMcpToolName(skill);
if (RESERVED_MCP_NAMES.has(toolName)) continue;
server.registerTool(
toolName,
{
description: skill.description,
inputSchema: z.object({}).passthrough(),
},
withScopeEnforcement(
toolName,
async (args, extra) => {
const scopeContext = resolveCallerScopeContext(extra, Array.from(MCP_ALLOWED_SCOPES));
const apiKeyId = scopeContext.callerId || "mcp";
try {
const execution = await skillExecutor.execute(
skill.name,
(args ?? {}) as Record<string, unknown>,
{ apiKeyId }
);
return {
content: [
{ type: "text" as const, text: JSON.stringify(execution.output, null, 2) },
],
};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
content: [{ type: "text" as const, text: `Error: ${msg}` }],
isError: true,
};
}
},
["execute:skills"]
)
);
}
} catch {
// Skills not loaded yet — skip dynamic registration until next reconnect
}
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);
});
}