OmniRoute MCP Scopes: How Multi-Channel Protocol Permissions Work

MCP scopes in OmniRoute are permission strings declared per tool, validated via environment variables and JWT claims, and enforced through wildcard-aware matching in scopeEnforcement.ts.

OmniRoute's MCP (Multi-Channel Protocol) server implements fine-grained access control by requiring callers to present specific permission strings—called scopes—before executing protected tools. This article explains how MCP scopes are defined, configured, and enforced according to the OmniRoute source code.


How MCP Scopes Are Defined in OmniRoute

Each MCP tool declares its required scopes directly in its tool definition. The scopes field is an array of strings that specifies exactly which permissions a caller needs.

In open-sse/mcp-server/schemas/tools.ts, tools declare their scope requirements like this:

// open-sse/mcp-server/tools/healthTool.ts
export const getHealthTool = {
  name: "omniroute_get_health",
  description: "Fetch server health status",
  phase: 1,
  scopes: ["read:health"],  // Required MCP scope
  handler: async (args, extra) => { /* … */ },
};

Different tools can require different scope combinations. For example, a write operation might need ["write:combos"] while a read operation needs ["read:health"]. This per-tool granularity enables precise access control across the OmniRoute MCP server.


Configuring MCP Scope Enforcement

Scope enforcement operates through two environment variables that control behavior without code changes.

Global Allowed Scopes

The server reads OMNIROUTE_MCP_SCOPES as a comma-separated list of globally permitted scopes. In open-sse/mcp-server/server.ts, this becomes a Set called MCP_ALLOWED_SCOPES:

export OMNIROUTE_MCP_SCOPES="read:health,write:combos"

Toggle Enforcement On or Off

Scope validation is controlled by OMNIROUTE_MCP_ENFORCE_SCOPES. When set to "true", the server validates every tool call against scope requirements. When "false" or unset, all calls are permitted regardless of scopes.

export OMNIROUTE_MCP_ENFORCE_SCOPES=true

This runtime configurability allows operators to adjust security posture without redeploying code.


How Callers Supply MCP Scopes

The function resolveCallerScopeContext in open-sse/mcp-server/scopeEnforcement.ts extracts a caller's scopes from three sources, in priority order:

  1. extra.authInfo.scopes — JWT payload containing a scopes array
  2. extra._meta — Custom metadata object with nested scopes, auth, or omniroute fields
  3. Environment fallback — Global MCP_ALLOWED_SCOPES when no caller-specific scopes are found
import { resolveCallerScopeContext } from "./scopeEnforcement.ts";

const extra = {
  authInfo: { clientId: "abc123", scopes: ["read:health"] },
  sessionId: "sess-42",
};

const ctx = resolveCallerScopeContext(extra, Array.from(MCP_ALLOWED_SCOPES));
// Result: { callerId: "abc123", scopes: ["read:health"], source: "authInfo" }

The function returns a context object identifying the caller, their resolved scopes, and which source provided them.


MCP Scope Matching and Wildcard Support

Core enforcement happens in evaluateToolScopes, also in scopeEnforcement.ts. This function:

  • Normalizes both caller scopes and tool-required scopes
  • Supports wildcards (* or trailing *, e.g., read:*)
  • Returns detailed results including missing scopes and reason codes
import { evaluateToolScopes } from "./scopeEnforcement.ts";

// Exact match succeeds
const check = evaluateToolScopes(
  "omniroute_get_health",
  ["read:health"],   // caller's scopes
  true,              // enforcement enabled
);
console.log(check.allowed); // true

// Wildcard grants entire scope families
const wildcard = evaluateToolScopes(
  "omniroute_get_health",
  ["read:*"],        // grants any read:* scope
  true,
);
console.log(wildcard.allowed); // true

// Missing scope fails with details
const failed = evaluateToolScopes(
  "omniroute_get_health",
  ["read:quota"],    // wrong scope
  true,
);
console.log(failed.allowed); // false
console.log(failed.missing); // ["read:health"]
console.log(failed.reason);  // "missing_scopes"

The wildcard support simplifies permission management—grant read:* once instead of enumerating every read operation.


Scope Enforcement Wrapper

Every tool handler is wrapped by withScopeEnforcement in open-sse/mcp-server/server.ts. This wrapper:

  • Resolves caller scopes via resolveCallerScopeContext
  • Validates against tool requirements via evaluateToolScopes
  • Logs detailed audit information on failures
  • Returns clear "Insufficient MCP scopes" errors
// open-sse/mcp-server/server.ts excerpt
const wrappedHandler = withScopeEnforcement(
  "omniroute_get_health",
  getHealthTool.handler,
  getHealthTool.scopes,
);

The complete flow: client calls tool → wrapper resolves scopes → evaluateToolScopes validates → tool executes or error returns.


Key Source Files for MCP Scopes

File Purpose
open-sse/mcp-server/scopeEnforcement.ts Core utilities: resolveCallerScopeContext, evaluateToolScopes, wildcard matching
open-sse/mcp-server/server.ts Server bootstrap, env variable reading, withScopeEnforcement wrapping
open-sse/mcp-server/schemas/tools.ts Tool definitions with scopes arrays
tests/unit/t08-mcp-scope-enforcement.test.ts Unit tests for resolution, enforcement, and wildcards
tests/unit/obsidian-tools.test.ts Obsidian integration scope examples
tests/unit/notion-tools.test.ts Notion integration scope examples

Summary

  • Declaration: MCP scopes are string arrays in each tool's definition, declared in schemas/tools.ts
  • Configuration: OMNIROUTE_MCP_SCOPES sets global permissions; OMNIROUTE_MCP_ENFORCE_SCOPES toggles validation
  • Resolution: resolveCallerScopeContext extracts scopes from JWT claims, metadata, or environment fallbacks
  • Validation: evaluateToolScopes performs wildcard-aware matching with detailed failure reporting
  • Enforcement: withScopeEnforcement wraps all handlers, providing audit logging and clear error messages

Frequently Asked Questions

What format do OmniRoute MCP scopes use?

OmniRoute MCP scopes use colon-separated strings like read:health, write:combos, or admin:*. The structure is category:action, with wildcard support for the action portion. This follows common OAuth2 scope conventions and appears throughout open-sse/mcp-server/schemas/tools.ts.

Can I disable MCP scope checking during development?

Yes. Set OMNIROUTE_MCP_ENFORCE_SCOPES=false (or leave it unset) to disable all scope validation. When disabled, evaluateToolScopes bypasses checks and allows every call regardless of provided or required scopes. Re-enable in production by setting the variable to "true".

How do wildcards work in MCP scope matching?

Wildcards match any characters in the action portion. A scope of read:* grants access to read:health, read:quota, read:logs, and any other read: prefixed scope. The matching logic in scopeEnforcement.ts handles both standalone * and trailing * patterns.

Where should I add scopes when creating a new MCP tool?

Add the scopes array directly to your tool definition object in open-sse/mcp-server/schemas/tools.ts or a corresponding tool file. Then register the tool in the server with withScopeEnforcement, passing the same scopes array as the third argument to ensure validation occurs.

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 →