# OmniRoute MCP Scopes: How Multi-Channel Protocol Permissions Work

> Understand OmniRoute MCP scopes, permission strings validated by environment variables and JWT claims, and enforced with wildcard-aware matching for robust multi-channel protocol control.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: deep-dive
- Published: 2026-08-07

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/schemas/tools.ts), tools declare their scope requirements like this:

```ts
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/server.ts), this becomes a `Set` called `MCP_ALLOWED_SCOPES`:

```bash
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.

```bash
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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

```ts
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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

```ts
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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

```ts
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/scopeEnforcement.ts) | Core utilities: `resolveCallerScopeContext`, `evaluateToolScopes`, wildcard matching |
| [`open-sse/mcp-server/server.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/server.ts) | Server bootstrap, env variable reading, `withScopeEnforcement` wrapping |
| [`open-sse/mcp-server/schemas/tools.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/schemas/tools.ts) | Tool definitions with `scopes` arrays |
| [`tests/unit/t08-mcp-scope-enforcement.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/t08-mcp-scope-enforcement.test.ts) | Unit tests for resolution, enforcement, and wildcards |
| [`tests/unit/obsidian-tools.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/obsidian-tools.test.ts) | Obsidian integration scope examples |
| [`tests/unit/notion-tools.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.