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:

  1. Tool declaration — Each tool specifies required scopes in its schema definition.
  2. API key storage — Scopes are stored as JSON arrays in the database.
  3. HTTP header extraction — Caller identity is extracted from request headers.
  4. Context propagation — Auth info flows through AsyncLocalStorage to handlers.
  5. Scope resolution — The effective scope set is determined from multiple sources.
  6. Scope evaluation — Caller scopes are matched against tool requirements.
  7. Pre-execution enforcement — Checks run before the handler is invoked.
  8. 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 any read: 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 scopes and callers to present matching permissions via API keys
  • Four scope sources are checked in priority: API key metadata, _meta payload, 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 AsyncLocalStorage from 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →