How the MCP Server in OmniRoute Manages 94 Tools and 30 Permission Scopes
The MCP server in OmniRoute operates through a dynamic tool registry that lazily loads 94 specialized tools across 12 modules, while enforcing fine-grained access control via 30 permission scopes using a layered middleware architecture.
The OmniRoute repository (diegosouzapw/OmniRoute) provides a production-grade Model Context Protocol (MCP) server implementation built on TypeScript. This system exposes nearly one hundred discrete capabilities to AI agents and external clients through a unified JSON-RPC interface, protected by granular scope-based permissions.
Dynamic Tool Registration and the 94-Tool Ecosystem
The server consolidates 94 distinct tools into a runtime registry without static imports, keeping the startup footprint minimal while ensuring all capabilities remain available on demand.
Modular Tool Architecture
Tool definitions are organized by functional domain across twelve separate modules under open-sse/mcp-server/tools/. The tools/index.ts file aggregates these capabilities through named exports:
// open-sse/mcp-server/tools/index.ts
export * from "./advancedTools"
export * from "./agentSkillTools"
export * from "./compressionTools"
export * from "./memoryTools"
export * from "./gamificationTools"
export * from "./pluginTools"
export * from "./notionTools"
export * from "./obsidianTools"
export * from "./skillTools"
export * from "./poolTools"
export * from "./pickFastestModel"
export * from "./githubSkillTools"
Each module exports a register function that returns a typed tool definition via defineTool. This pattern ensures consistent schema validation using Zod while allowing domain-specific implementations to remain isolated.
Lazy Loading Implementation
The buildMcpServer function in open-sse/mcp-server/server.ts dynamically imports the entire tool catalog at runtime:
export const buildMcpServer = async () => {
const tools: MCPTool[] = []
const toolModules = await import("@omniroute/open-sse/mcp-server/tools")
for (const name of Object.keys(toolModules)) {
const mod = (toolModules as any)[name]
if (mod && typeof mod.register === "function") {
const tool = await mod.register()
tools.push(tool)
}
}
const baseServer = createMcpServer({ tools })
// ... middleware attachment
return baseServer
}
This architecture ensures that heavy dependencies (such as Notion or GitHub API clients) are only loaded when the specific tool module is first accessed, reducing memory consumption for partially utilized deployments.
Scope-Based Access Control with 30 Permission Scopes
Access to the 94 tools is governed by approximately 30 distinct scopes (e.g., tools:health, tools:compression, tools:memory, tools:plugin), which are linked to API keys in the database and enforced through middleware.
The Scope Enforcement Middleware
The createScopeEnforcementMiddleware function in open-sse/mcp-server/scopeEnforcement.ts intercepts every incoming request to validate permissions:
export const createScopeEnforcementMiddleware = () => {
return async (ctx: any, next: () => Promise<void>) => {
const apiKey = getApiKeyFromRequest(ctx.req)
if (!apiKey) {
await next() // Public mode fallback
return
}
const scopes = await ctx.db.getScopesForKey(apiKey)
ctx.allowedScopes = new Set(scopes)
await next()
}
}
This middleware attaches an allowedScopes Set to the request context, which downstream handlers check before executing tool-specific logic. If a tool requires a scope not present in the caller's permissions, the server returns a 403 Forbidden response before reaching the handler.
API Key Authentication Flow
The authentication chain operates in three distinct phases as defined in open-sse/mcp-server/catalog.ts:
- Scope Enforcement (order 10): Validates tool permissions against the API key's allowed scopes
- Logging (order 20): Records request metadata for audit trails
- Auth Validation (order 30): Confirms the presence of the
x-api-keyheader
The MIDDLEWARE_ORDER constants ensure deterministic execution regardless of registration sequence:
export const MIDDLEWARE_ORDER = {
scope: 10,
logging: 20,
auth: 30,
}
Middleware Architecture and Request Processing
The server processes JSON-RPC requests through a layered pipeline that transforms HTTP calls into tool invocations.
Ordered Middleware Pipeline
The buildMcpServer function attaches middleware using explicit ordering to prevent race conditions:
const baseServer = createMcpServer({ tools })
const scopeMiddleware = createScopeEnforcementMiddleware()
baseServer.addMiddleware(scopeMiddleware, MIDDLEWARE_ORDER.scope)
for (const mw of MIDDLEWARE) {
baseServer.addMiddleware(mw.handler, mw.order)
}
This guarantees that scope checks execute before business logic, while logging and authentication occur at predictable intervals in the request lifecycle.
HTTP Transport Layer
The createMcpHttpServer function wraps the core server with an HTTP transport that resolves authentication context from incoming requests:
export const createMcpHttpServer = async () => {
const server = await buildMcpServer()
const httpTransport = createOpenSseHttpTransport({
server,
getAuthContext: getOpenSseHttpAuthContext,
})
return httpTransport
}
The transport extracts the API key from request headers, queries the database for associated scopes, and injects this context into the middleware chain before dispatching to the appropriate tool handler.
Building Custom Tools and Extending the Server
Adding new capabilities requires implementing the standard registration pattern. Each tool defines its contract using Zod schemas and implements execution logic in an async handler.
Example Tool Implementation
The following pattern from advancedTools.ts demonstrates the structure for implementing health checks:
import { defineTool } from "@omniroute/open-sse/mcp-server"
import { z } from "zod"
export const register = async () => {
return defineTool({
name: "get_health",
description: "Retrieve health status and version of the OmniRoute server.",
inputSchema: z.object({}),
outputSchema: z.object({
ok: z.boolean(),
version: z.string(),
uptime_seconds: z.number(),
}),
handler: async () => {
return {
ok: true,
version: "3.8.47",
uptime_seconds: Math.floor(process.uptime()),
}
},
})
}
Session Memory Manipulation
Tools can interact with persistent storage through the context object. The memory_add tool from memoryTools.ts illustrates session state management:
export const register = async () => {
return defineTool({
name: "memory_add",
description: "Add a memory entry for the current session.",
inputSchema: z.object({
key: z.string(),
value: z.string(),
}),
outputSchema: z.object({ success: z.boolean() }),
handler: async ({ input, ctx }) => {
ctx.memoryStore.set(input.key, input.value)
return { success: true }
},
})
}
Client Invocation Example
Clients interact with the server via HTTP POST requests containing JSON-RPC payloads:
const payload = {
jsonrpc: "2.0",
id: 1,
method: "memory_add",
params: { key: "user_preference", value: "dark_mode" }
}
const response = await fetch("https://api.omniroute.example/api/mcp", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "sk_live_xxxxxxxx"
},
body: JSON.stringify(payload)
})
Summary
- Dynamic Tool Loading: The
buildMcpServerfunction inopen-sse/mcp-server/server.tslazily imports 94 tools from 12 domain modules, keeping startup overhead minimal while maintaining runtime availability. - Scope Enforcement: Approximately 30 permission scopes regulate access to tool groups, enforced by
createScopeEnforcementMiddlewarebefore handler execution. - Ordered Middleware: The
MIDDLEWARE_ORDERconstants incatalog.tsensure deterministic execution of scope checks (10), logging (20), and authentication (30). - Zod Schema Validation: Every tool uses
defineToolwith strict input/output schemas, providing type-safe contracts between clients and the server. - Transport Agnostic: The same server core supports both HTTP (
createOpenSseHttpTransport) and stdio transports, ensuring consistent behavior across deployment environments.
Frequently Asked Questions
How does the OmniRoute MCP server handle the loading of 94 tools without impacting startup performance?
The server uses dynamic imports in buildMcpServer to load tool modules only when the server initializes, rather than at build time. Each module exports a register function that returns a tool definition; these are collected into an array and passed to createMcpServer. This lazy-loading pattern ensures that heavy dependencies (like Notion or GitHub API clients) are only instantiated when the specific tool is first invoked.
What determines the order of middleware execution in the OmniRoute MCP server?
Middleware execution follows the numeric priority system defined in MIDDLEWARE_ORDER within open-sse/mcp-server/catalog.ts. Scope enforcement runs at priority 10, logging at 20, and authentication validation at 30. When baseServer.addMiddleware() is called, it inserts handlers into a sorted chain based on these constants, ensuring that scope permissions are checked before logging or business logic executes.
How are the 30 permission scopes assigned to API keys in the OmniRoute system?
Scopes are stored in the database and linked to API keys through the getScopesForKey method (referenced in scopeEnforcement.ts). When a request arrives, createScopeEnforcementMiddleware extracts the API key using getApiKeyFromRequest, queries the database for associated scopes, and stores them as a Set in ctx.allowedScopes. The middleware then continues to the next layer, where individual tool handlers verify that their required scope exists within this set.
Can developers add custom tools to the OmniRoute MCP server without modifying core files?
Yes, developers can create new tool modules under open-sse/mcp-server/tools/ following the standard export pattern. The module must export a register function that returns the result of defineTool, including Zod schemas for validation and an async handler. Once the module is added to tools/index.ts using an export statement (e.g., export * from "./customTools"), the buildMcpServer function will automatically include it in the registry during the next server startup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →