# How to Implement Scope-Based Authentication for OmniRoute's MCP Server

> Implement scope-based authentication for OmniRoute MCP server. Control tool access with API keys and scopes for secure execution. Learn how to set up this essential security feature.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-14

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/schemas/tools.ts) include a `scopes` array. This array lists all permissions required to invoke the tool.

```typescript
// 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.

```typescript
// src/lib/db/apiKeys.ts
// Returns { key: string, scopes: string[], ... } when validating a key

```

When a request arrives, [`open-sse/mcp-server/httpAuthContext.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/httpAuthContext.ts) extracts and validates the API key, then constructs a `McpCallerAuthInfo` object containing the key's scopes:

```typescript
// 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) |

```typescript
// 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:

```typescript
// 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`:

```bash
curl http://localhost:20128/api/mcp/status

# Returns: { "scopesEnforced": true, ... }

```

## Complete Implementation Example

### 1. Define a Tool with Required Scopes

```typescript
// 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

```typescript
// 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

```typescript
// 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

```bash

# .env

OMNIROUTE_MCP_ENFORCE_SCOPES=true

```

### 5. Make an Authenticated Client Call

```typescript
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/schemas/tools.ts) | Declares tool definitions and `scopes` arrays |
| Scope resolution | [`open-sse/mcp-server/scopeEnforcement.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/scopeEnforcement.ts) | Resolves caller scopes and evaluates matches |
| HTTP auth | [`open-sse/mcp-server/httpAuthContext.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/httpAuthContext.ts) | Extracts and validates API keys from headers |
| Handler wrapping | [`open-sse/mcp-server/server.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/server.ts) | Applies `withScopeEnforcement` to tool handlers |
| Feature flags | [`src/shared/constants/featureFlagDefinitions.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/featureFlagDefinitions.ts) | Defines `OMNIROUTE_MCP_ENFORCE_SCOPES` toggle |
| Status API | [`src/app/api/mcp/status/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/mcp-server/scopeEnforcement.ts) that your API key has the exact scope or a matching wildcard.