mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(mcp): reorder enforceScopes guard before MCP_TOOL_MAP lookup, add scopes to all dynamic tool definitions (#2958)
Integrated into release/v3.8.8
This commit is contained in:
@@ -99,26 +99,28 @@ export function resolveCallerScopeContext(
|
||||
export function evaluateToolScopes(
|
||||
toolName: string,
|
||||
callerScopes: readonly string[],
|
||||
enforceScopes: boolean
|
||||
enforceScopes: boolean,
|
||||
inlineScopes?: readonly string[]
|
||||
): ScopeCheckResult {
|
||||
const toolDef = MCP_TOOL_MAP[toolName];
|
||||
if (!toolDef) {
|
||||
const provided = normalizeScopeList(callerScopes);
|
||||
|
||||
if (!enforceScopes) {
|
||||
return { allowed: true, required: [], provided, missing: [] };
|
||||
}
|
||||
|
||||
const toolScopes = inlineScopes ?? MCP_TOOL_MAP[toolName]?.scopes;
|
||||
const required = Array.isArray(toolScopes) ? Array.from(toolScopes) : [];
|
||||
|
||||
if (required.length === 0) {
|
||||
return {
|
||||
allowed: false,
|
||||
required: [],
|
||||
provided: Array.from(callerScopes),
|
||||
provided,
|
||||
missing: [],
|
||||
reason: "tool_definition_missing",
|
||||
};
|
||||
}
|
||||
|
||||
const required = Array.isArray(toolDef.scopes) ? Array.from(toolDef.scopes) : [];
|
||||
const provided = normalizeScopeList(callerScopes);
|
||||
|
||||
if (!enforceScopes || required.length === 0) {
|
||||
return { allowed: true, required, provided, missing: [] };
|
||||
}
|
||||
|
||||
const missing = required.filter(
|
||||
(requiredScope) => !provided.some((grantedScope) => scopeMatches(grantedScope, requiredScope))
|
||||
);
|
||||
|
||||
@@ -205,11 +205,12 @@ async function omniRouteFetch(path: string, options: RequestInit = {}): Promise<
|
||||
|
||||
function withScopeEnforcement(
|
||||
toolName: string,
|
||||
handler: (args: unknown, extra?: McpToolExtraLike) => Promise<TextToolResult>
|
||||
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);
|
||||
const scopeCheck = evaluateToolScopes(toolName, scopeContext.scopes, MCP_ENFORCE_SCOPES, toolScopes);
|
||||
if (!scopeCheck.allowed) {
|
||||
const missingScopes =
|
||||
scopeCheck.missing.length > 0 ? scopeCheck.missing.join(", ") : "unavailable";
|
||||
@@ -962,7 +963,7 @@ export function createMcpServer(): McpServer {
|
||||
);
|
||||
|
||||
// ── Memory Tools ──────────────────────────────
|
||||
Object.values(memoryTools).forEach((toolDef) => {
|
||||
Object.values(memoryTools).forEach((toolDef: any) => {
|
||||
server.registerTool(
|
||||
toolDef.name,
|
||||
{
|
||||
@@ -970,22 +971,26 @@ export function createMcpServer(): McpServer {
|
||||
// @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 };
|
||||
}
|
||||
})
|
||||
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 };
|
||||
}
|
||||
},
|
||||
toolDef.scopes
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// ── Skill Tools ──────────────────────────────
|
||||
Object.values(skillTools).forEach((toolDef) => {
|
||||
Object.values(skillTools).forEach((toolDef: any) => {
|
||||
server.registerTool(
|
||||
toolDef.name,
|
||||
{
|
||||
@@ -993,17 +998,21 @@ export function createMcpServer(): McpServer {
|
||||
// @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 };
|
||||
}
|
||||
})
|
||||
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 };
|
||||
}
|
||||
},
|
||||
toolDef.scopes
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1016,22 +1025,26 @@ export function createMcpServer(): McpServer {
|
||||
// @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 };
|
||||
}
|
||||
})
|
||||
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) => {
|
||||
Object.values(compressionTools).forEach((toolDef: any) => {
|
||||
server.registerTool(
|
||||
toolDef.name,
|
||||
{
|
||||
@@ -1039,17 +1052,21 @@ export function createMcpServer(): McpServer {
|
||||
// @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 };
|
||||
}
|
||||
})
|
||||
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 };
|
||||
}
|
||||
},
|
||||
toolDef.scopes
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1062,17 +1079,21 @@ export function createMcpServer(): McpServer {
|
||||
// @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 };
|
||||
}
|
||||
})
|
||||
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
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -299,6 +299,7 @@ export const compressionTools = {
|
||||
name: "omniroute_compression_status",
|
||||
description:
|
||||
"Returns current compression configuration, strategy, analytics summary (requests compressed, tokens saved, avg ratio), and provider-aware cache statistics.",
|
||||
scopes: ["read:compression"],
|
||||
inputSchema: compressionStatusInput,
|
||||
handler: (args: z.infer<typeof compressionStatusInput>) => handleCompressionStatus(args),
|
||||
},
|
||||
@@ -306,24 +307,28 @@ export const compressionTools = {
|
||||
name: "omniroute_compression_configure",
|
||||
description:
|
||||
"Configure compression settings at runtime. Supports enabling/disabling compression, changing strategy (off/lite/standard/aggressive/ultra/rtk/stacked), adjusting maxTokens threshold, targetRatio, auto-trigger mode, system prompt preservation, and MCP description compression.",
|
||||
scopes: ["write:compression"],
|
||||
inputSchema: compressionConfigureInput,
|
||||
handler: (args: z.infer<typeof compressionConfigureInput>) => handleCompressionConfigure(args),
|
||||
},
|
||||
omniroute_set_compression_engine: {
|
||||
name: "omniroute_set_compression_engine",
|
||||
description: "Set the active compression engine and Caveman/RTK runtime options.",
|
||||
scopes: ["write:compression"],
|
||||
inputSchema: setCompressionEngineInput,
|
||||
handler: (args: z.infer<typeof setCompressionEngineInput>) => handleSetCompressionEngine(args),
|
||||
},
|
||||
omniroute_list_compression_combos: {
|
||||
name: "omniroute_list_compression_combos",
|
||||
description: "List compression combos and their engine pipelines.",
|
||||
scopes: ["read:compression"],
|
||||
inputSchema: listCompressionCombosInput,
|
||||
handler: (_args: z.infer<typeof listCompressionCombosInput>) => handleListCompressionCombos(),
|
||||
},
|
||||
omniroute_compression_combo_stats: {
|
||||
name: "omniroute_compression_combo_stats",
|
||||
description: "Get compression analytics grouped by engine and compression combo.",
|
||||
scopes: ["read:compression"],
|
||||
inputSchema: compressionComboStatsInput,
|
||||
handler: (args: z.infer<typeof compressionComboStatsInput>) =>
|
||||
handleCompressionComboStats(args),
|
||||
|
||||
@@ -10,6 +10,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_leaderboard",
|
||||
description: "Get leaderboard rankings for a scope (global, weekly, monthly, tokens_shared).",
|
||||
scopes: ["read:gamification"],
|
||||
inputSchema: z.object({
|
||||
scope: z.enum(["global", "weekly", "monthly", "tokens_shared"]).default("global"),
|
||||
limit: z.number().min(1).max(100).default(50),
|
||||
@@ -23,6 +24,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_rank",
|
||||
description: "Get rank for an API key in a leaderboard scope.",
|
||||
scopes: ["read:gamification"],
|
||||
inputSchema: z.object({
|
||||
apiKeyId: z.string(),
|
||||
scope: z.enum(["global", "weekly", "monthly", "tokens_shared"]).default("global"),
|
||||
@@ -36,6 +38,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_profile",
|
||||
description: "Get XP, level, and badges for an API key.",
|
||||
scopes: ["read:gamification"],
|
||||
inputSchema: z.object({
|
||||
apiKeyId: z.string(),
|
||||
}),
|
||||
@@ -64,6 +67,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_badges",
|
||||
description: "List all badge definitions or earned badges for an API key.",
|
||||
scopes: ["read:gamification"],
|
||||
inputSchema: z.object({
|
||||
apiKeyId: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
@@ -83,6 +87,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_transfer",
|
||||
description: "Transfer tokens between API keys.",
|
||||
scopes: ["write:gamification"],
|
||||
inputSchema: z.object({
|
||||
fromApiKeyId: z.string(),
|
||||
toApiKeyId: z.string(),
|
||||
@@ -108,6 +113,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_invite",
|
||||
description: "Create an invite token for server connection.",
|
||||
scopes: ["write:gamification"],
|
||||
inputSchema: z.object({
|
||||
apiKeyId: z.string(),
|
||||
serverUrl: z.string().optional(),
|
||||
@@ -122,6 +128,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_servers",
|
||||
description: "List connected community servers.",
|
||||
scopes: ["read:gamification"],
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const { listServers } = await import("../../../src/lib/gamification/servers");
|
||||
@@ -131,6 +138,7 @@ export const gamificationTools = [
|
||||
{
|
||||
name: "gamification_anomalies",
|
||||
description: "Get flagged anomalous XP activity (admin only).",
|
||||
scopes: ["read:gamification"],
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const { getAnomalies } = await import("../../../src/lib/gamification/antiCheat");
|
||||
|
||||
@@ -30,6 +30,7 @@ export const memoryTools = {
|
||||
omniroute_memory_search: {
|
||||
name: "omniroute_memory_search",
|
||||
description: "Search memories by query, type, or API key with token budget enforcement",
|
||||
scopes: ["read:memory"],
|
||||
inputSchema: MemorySearchSchema,
|
||||
handler: async (args: z.infer<typeof MemorySearchSchema>) => {
|
||||
const config = {
|
||||
@@ -63,6 +64,7 @@ export const memoryTools = {
|
||||
omniroute_memory_add: {
|
||||
name: "omniroute_memory_add",
|
||||
description: "Add a new memory entry",
|
||||
scopes: ["write:memory"],
|
||||
inputSchema: MemoryAddSchema,
|
||||
handler: async (args: z.infer<typeof MemoryAddSchema>) => {
|
||||
const memory = await createMemory({
|
||||
@@ -88,6 +90,7 @@ export const memoryTools = {
|
||||
omniroute_memory_clear: {
|
||||
name: "omniroute_memory_clear",
|
||||
description: "Clear memories for an API key, optionally filtered by type or age",
|
||||
scopes: ["write:memory"],
|
||||
inputSchema: MemoryClearSchema,
|
||||
handler: async (args: z.infer<typeof MemoryClearSchema>) => {
|
||||
const result = await listMemories({
|
||||
|
||||
@@ -12,6 +12,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_list",
|
||||
description: "List all installed plugins with their status, hooks, and metadata.",
|
||||
scopes: ["read:plugins"],
|
||||
inputSchema: z.object({
|
||||
status: z
|
||||
.enum(["installed", "active", "inactive", "error"])
|
||||
@@ -39,6 +40,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_install",
|
||||
description: "Install a plugin from a local directory path.",
|
||||
scopes: ["write:plugins"],
|
||||
inputSchema: z.object({
|
||||
path: z.string().describe("Absolute path to the plugin directory containing plugin.json"),
|
||||
}),
|
||||
@@ -58,6 +60,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_activate",
|
||||
description: "Activate an installed plugin (loads hooks into the request pipeline).",
|
||||
scopes: ["write:plugins"],
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe("Plugin name (kebab-case)"),
|
||||
}),
|
||||
@@ -70,6 +73,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_deactivate",
|
||||
description: "Deactivate an active plugin (unloads hooks from the request pipeline).",
|
||||
scopes: ["write:plugins"],
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe("Plugin name (kebab-case)"),
|
||||
}),
|
||||
@@ -82,6 +86,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_uninstall",
|
||||
description: "Uninstall a plugin (deactivates, removes files, removes from DB).",
|
||||
scopes: ["write:plugins"],
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe("Plugin name (kebab-case)"),
|
||||
}),
|
||||
@@ -94,6 +99,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_configure",
|
||||
description: "Get or update a plugin's configuration.",
|
||||
scopes: ["write:plugins"],
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe("Plugin name"),
|
||||
config: z
|
||||
@@ -122,6 +128,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_executions",
|
||||
description: "View plugin execution history (from skill_executions table).",
|
||||
scopes: ["read:plugins"],
|
||||
inputSchema: z.object({
|
||||
name: z.string().optional().describe("Filter by plugin name"),
|
||||
limit: z.number().min(1).max(100).default(20).describe("Max results to return"),
|
||||
@@ -137,6 +144,7 @@ export const pluginTools = [
|
||||
{
|
||||
name: "plugin_scan",
|
||||
description: "Scan the plugin directory for new plugins and sync with DB.",
|
||||
scopes: ["write:plugins"],
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await pluginManager.scan();
|
||||
|
||||
@@ -25,6 +25,7 @@ export const skillTools = {
|
||||
omniroute_skills_list: {
|
||||
name: "omniroute_skills_list",
|
||||
description: "List all registered skills with optional filtering by API key or name",
|
||||
scopes: ["read:skills"],
|
||||
inputSchema: SkillListSchema,
|
||||
handler: async (args: z.infer<typeof SkillListSchema>) => {
|
||||
await skillRegistry.loadFromDatabase(args.apiKeyId);
|
||||
@@ -55,6 +56,7 @@ export const skillTools = {
|
||||
omniroute_skills_enable: {
|
||||
name: "omniroute_skills_enable",
|
||||
description: "Enable or disable a specific skill by ID",
|
||||
scopes: ["write:skills"],
|
||||
inputSchema: SkillEnableSchema,
|
||||
handler: async (args: z.infer<typeof SkillEnableSchema>) => {
|
||||
await skillRegistry.loadFromDatabase(args.apiKeyId);
|
||||
@@ -70,6 +72,7 @@ export const skillTools = {
|
||||
omniroute_skills_execute: {
|
||||
name: "omniroute_skills_execute",
|
||||
description: "Execute a skill with provided input and return the result",
|
||||
scopes: ["execute:skills"],
|
||||
inputSchema: SkillExecuteSchema,
|
||||
handler: async (args: z.infer<typeof SkillExecuteSchema>) => {
|
||||
const execution = await skillExecutor.execute(args.skillName, args.input, {
|
||||
@@ -92,6 +95,7 @@ export const skillTools = {
|
||||
omniroute_skills_executions: {
|
||||
name: "omniroute_skills_executions",
|
||||
description: "List recent skill execution history",
|
||||
scopes: ["read:skills"],
|
||||
inputSchema: z.object({
|
||||
apiKeyId: z.string().optional(),
|
||||
limit: z.number().int().positive().max(100).optional(),
|
||||
|
||||
Reference in New Issue
Block a user