How to Implement Scope-Based Authentication for OmniRoute's MCP Server
Scope-based authentication in OmniRoute's MCP server controls access to tools by requiring API keys to carry specific scopes, which are validated against each tool's declared requirements before execution.
OmniRoute's Model Context Protocol (MCP) server uses a granular, scope-based authentication system to protect every tool. Each tool declares required permissions, and the server validates that the caller's API key (or alternative scope source) matches those requirements before allowing execution. This guide walks through the complete implementation based on the diegosouzapw/OmniRoute source code.
How Scope-Based Authentication Works in OmniRoute
The authentication flow spans eight distinct stages, from tool declaration to enforcement:
- Tool declaration — Each tool specifies required scopes in its schema definition.
- API key storage — Scopes are stored as JSON arrays in the database.
- HTTP header extraction — Caller identity is extracted from request headers.
- Context propagation — Auth info flows through
AsyncLocalStorageto handlers. - Scope resolution — The effective scope set is determined from multiple sources.
- Scope evaluation — Caller scopes are matched against tool requirements.
- Pre-execution enforcement — Checks run before the handler is invoked.
- Global toggle — A feature flag enables or disables enforcement entirely.
Defining Required Scopes for Each Tool
Tool declarations in open-sse/mcp-server/schemas/tools.ts include a scopes array. This array lists all permissions required to invoke the tool.
// open-sse/mcp-server/schemas/tools.ts
export const myNewToolInput = z.object({ /* … */ });
export const myNewToolOutput = z.object({ /* … */ });
export const myNewTool: McpToolDefinition<typeof myNewToolInput, typeof myNewToolOutput> = {
name: "omniroute_my_new_tool",
description: "Does something useful.",
inputSchema: myNewToolInput,
outputSchema: myNewToolOutput,
scopes: ["write:mytool"], // <-- required scope
auditLevel: "full",
phase: 1,
sourceEndpoints: ["/api/mynewtool"],
};
The scopes field accepts an array of strings. Each scope can be:
- Exact match —
"read:health","write:users" - Wildcard suffix —
"read:*"grants access to anyread:scope - Universal grant —
"*"grants access to all scopes
Storing and Retrieving API Key Scopes
Scopes are tied to API keys in the database. The api_keys table stores a JSON array of scopes per key.
// src/lib/db/apiKeys.ts
// Returns { key: string, scopes: string[], ... } when validating a key
When a request arrives, open-sse/mcp-server/httpAuthContext.ts extracts and validates the API key, then constructs a McpCallerAuthInfo object containing the key's scopes:
// open-sse/mcp-server/httpAuthContext.ts#L44-L65
// Reads authorization, cookie, x-api-key, and anthropic-version headers
// Validates the key against the database
// Returns: { clientId: string, scopes: string[] }
Resolving the Effective Scope Set
The resolveCallerScopeContext function in open-sse/mcp-server/scopeEnforcement.ts#L72-L96 determines which scopes apply to the current call by checking sources in priority order:
| Priority | Source | Description |
|---|---|---|
| 1 | authInfo |
Scopes from the validated API key |
| 2 | meta |
Scopes passed in the tool call's _meta payload |
| 3 | env |
Default scopes from OMNIROUTE_MCP_ALLOWED_SCOPES |
| 4 | none |
Empty scope list (no access) |
// open-sse/mcp-server/scopeEnforcement.ts
const result = resolveCallerScopeContext(
extra, // Contains authInfo from HTTP context
fallbackScopes // From OMNIROUTE_MCP_ALLOWED_SCOPES env var
);
Evaluating and Enforcing Scope Requirements
Once resolved, evaluateToolScopes (open-sse/mcp-server/scopeEnforcement.ts#L99-L135) compares the caller's scopes against the tool's required scopes. It returns a ScopeCheckResult indicating success or which scopes are missing.
Enforcement happens in open-sse/mcp-server/server.ts#L24-L70 via the withScopeEnforcement wrapper:
// open-sse/mcp-server/server.ts
export const myNewToolHandler = withScopeEnforcement(
"omniroute_my_new_tool", // Tool name for scope lookup
handleMyNewTool // Original handler
);
If the check fails, the server returns an error without executing the handler:
Error: Insufficient MCP scopes for omniroute_my_new_tool. Missing: write:mytool. Caller=sk-xxxx, source=authInfo.
Successful checks proceed to the original handler with full context available.
Toggling Enforcement with Feature Flags
Global control over scope enforcement is provided by OMNIROUTE_MCP_ENFORCE_SCOPES, defined in src/shared/constants/featureFlagDefinitions.ts#L287:
| Flag Value | Behavior |
|---|---|
true |
All scope checks are enforced; unauthorized calls are rejected |
false |
Scope checks are skipped; all calls succeed regardless of scopes |
Check the current enforcement status via the status endpoint at src/app/api/mcp/status/route.ts#L41:
curl http://localhost:20128/api/mcp/status
# Returns: { "scopesEnforced": true, ... }
Complete Implementation Example
1. Define a Tool with Required Scopes
// open-sse/mcp-server/schemas/tools.ts
export const deleteUserTool: McpToolDefinition<typeof deleteUserInput, typeof deleteUserOutput> = {
name: "omniroute_delete_user",
description: "Permanently deletes a user account.",
inputSchema: deleteUserInput,
outputSchema: deleteUserOutput,
scopes: ["admin:users", "write:users"], // Multiple scopes required
auditLevel: "full",
phase: 1,
sourceEndpoints: ["/api/users/:id"],
};
2. Create an API Key with Appropriate Scopes
// Admin script or UI
import { createApiKey } from "@/src/lib/db/apiKeys.ts";
await createApiKey({
key: "sk-live-xyz789",
scopes: ["admin:users", "read:health", "write:users"],
environment: "production",
});
3. Register the Handler with Enforcement
// open-sse/mcp-server/server.ts
import { deleteUserTool } from "./schemas/tools.ts";
async function handleDeleteUser(args: z.infer<typeof deleteUserInput>, extra?: McpToolExtraLike) {
// Execute deletion logic
return { content: [{ type: "text", text: `User ${args.userId} deleted.` }] };
}
export const deleteUserHandler = withScopeEnforcement(
"omniroute_delete_user",
handleDeleteUser
);
4. Enable Enforcement in Production
# .env
OMNIROUTE_MCP_ENFORCE_SCOPES=true
5. Make an Authenticated Client Call
import { createMcpClient } from "@omniroute/open-sse/mcp-client";
const client = createMcpClient({
baseUrl: "https://api.omniroute.io/api/mcp",
apiKey: "sk-live-xyz789", // Has admin:users and write:users
});
// Succeeds: key has required scopes
await client.callTool("omniroute_delete_user", { userId: "usr-123" });
// Fails with different key:
const limitedClient = createMcpClient({
baseUrl: "https://api.omniroute.io/api/mcp",
apiKey: "sk-limited-abc", // Only has read:health
});
await limitedClient.callTool("omniroute_delete_user", { userId: "usr-456" });
// Error: Insufficient MCP scopes for omniroute_delete_user. Missing: admin:users, write:users.
Key Files for Scope-Based Authentication
| Component | Path | Purpose |
|---|---|---|
| Tool schemas | open-sse/mcp-server/schemas/tools.ts |
Declares tool definitions and scopes arrays |
| Scope resolution | open-sse/mcp-server/scopeEnforcement.ts |
Resolves caller scopes and evaluates matches |
| HTTP auth | open-sse/mcp-server/httpAuthContext.ts |
Extracts and validates API keys from headers |
| Handler wrapping | open-sse/mcp-server/server.ts |
Applies withScopeEnforcement to tool handlers |
| Feature flags | src/shared/constants/featureFlagDefinitions.ts |
Defines OMNIROUTE_MCP_ENFORCE_SCOPES toggle |
| Status API | src/app/api/mcp/status/route.ts |
Exposes enforcement status |
Summary
- Scope-based authentication in OmniRoute MCP requires tools to declare
scopesand callers to present matching permissions via API keys - Four scope sources are checked in priority: API key metadata,
_metapayload, environment variable fallback, then empty set - Wildcard matching supports flexible permission patterns like
read:*and universal* - Feature flag control allows disabling enforcement globally without code changes
- Auth context flows through
AsyncLocalStoragefrom HTTP extraction to handler execution
Frequently Asked Questions
How do I add a new scope to an existing tool?
Modify the scopes array in open-sse/mcp-server/schemas/tools.ts for your tool definition, then update any API keys that need access. Scope changes take effect immediately without server restart.
Can I override scopes for a single call without creating a new API key?
Yes. Pass scopes in the _meta payload when calling the tool. These scopes take precedence after API key scopes but before environment fallbacks. Note that this requires the caller to already have some valid authentication context.
What happens if OMNIROUTE_MCP_ENFORCE_SCOPES is false?
All scope checks are bypassed. Every authenticated request succeeds regardless of scopes. The status endpoint at /api/mcp/status reports scopesEnforced: false, and audit logs still record the call but without scope verification.
How do I debug why a scope check is failing?
Check the error message returned by the server—it specifies the missing scopes, caller identifier, and which source provided the caller's scopes. Verify at open-sse/mcp-server/scopeEnforcement.ts that your API key has the exact scope or a matching wildcard.
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 →