# Context Propagation Flow for workers-oauth-provider in MCP Tool Calls: How OrgScope Affects Authorization

> Understand context propagation for workers-oauth-provider in MCP tool calls. Learn how orgScope impacts authorization and cross-organizational access in Open-SEO.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-09-12

---

**The workers-oauth-provider injects OAuth grant properties into HTTP requests, which the Open-SEO server merges into a ToolContext via `createMcpToolContext`; the `orgScope` flag then determines whether the credential is locked to a specific organization (`"pinned"`) or is a portable user-scoped key (`"user"`), directly controlling role inference and cross-organizational access boundaries.**

The every-app/open-seo repository implements the Model-Code-Protocol (MCP) using Cloudflare's **workers-oauth-provider** to secure tool invocations with OAuth 2.0 grants. Understanding the exact **context propagation flow** from initial consent through to tool execution—and specifically how the **orgScope** parameter governs authorization decisions—is essential for developers building secure, multi-tenant MCP integrations.

## Step-by-Step Context Propagation Through workers-oauth-provider

The MCP integration follows a strict seven-phase pipeline to move identity data from the OAuth provider into the executing tool's authorization context.

### Phase 1: OAuth Consent and Property Injection

When a user authorizes an MCP tool, the consent handler constructs MCP-specific identity data by invoking **`createWorkersOAuthMcpProps`** in **[`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts)** (lines 36-55). This function packages the `userId`, `userEmail`, `organizationId`, and requested scopes into a metadata object that will accompany the grant.

### Phase 2: Token Creation and Metadata Embedding

The **`workers-oauth-provider`** stores the MCP properties directly inside the access-token grant. In [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts) (lines 45-55), the code calls `oauth.completeAuthorization`, passing the `props` object alongside the `clientId` and mandatory `MCP_SCOPE`. The provider embeds these values into the token payload, ensuring they are cryptographically bound to the credential.

### Phase 3: Request Validation and authInfo Population

Upon receiving a request at the `/mcp/*` endpoint, the `workers-oauth-provider` validates the Bearer token from the **Authorization** header. Internally, the provider extracts the embedded `clientId` and scopes, injecting them into `request.http.authInfo`. This step occurs entirely within the provider's internal validation logic before the request reaches the application layer.

### Phase 4: ToolContext Assembly via createMcpToolContext

The MCP transport handler, **`handleAuthenticatedOpenSeoMcpRequest`**, retrieves the raw token properties from `ctx.props` and merges them with the `authInfo` provided by the provider. It then calls **`createMcpToolContext`** in **[`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)** (lines 84-119) to instantiate a stable `ToolContext`. This function reconciles the original grant metadata with the current request's authentication state, preparing the final values for `orgScope`, `role`, and `clientId`.

### Phase 5: OrgScope and Role Resolution

Inside `createMcpToolContext` (lines 99-105), the logic branches based on the `orgScope` value:

- If `orgScope` is **`"pinned"`** (the default), the token is organization-scoped. The function infers the `role` as **`"owner"`** without performing a membership database lookup, as the organization ID is immutable for this credential.
- If `orgScope` is **`"user"`**, the credential is user-scoped (such as an API key generated via **[`src/server/mcp/api-key-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/api-key-auth.ts)**). The tool must resolve the target organization from the request parameters (e.g., a project ID), and the effective `role` is determined by the user's membership record in that specific organization.

### Phase 6: Tool-Level Authorization Enforcement

Each MCP tool receives the assembled `ToolContext` through `ctx.auth` and enforces fine-grained permissions. For example, **[`src/server/mcp/tools/list-projects.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/list-projects.ts)** (lines 14-16) validates that `auth.orgScope === "user"` before proceeding, rejecting pinned tokens for operations that require cross-organizational flexibility. Tools acting on organization-wide resources (like billing) accept the pinned scope and rely on the pre-computed `role`.

### Phase 7: Audit Logging

After successful authorization, the server records the resolved `orgScope`, `clientId`, and granted scopes for telemetry. The function **`recordMcpAuthorized`** is invoked in **[`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts)** (lines 56-58) to persist this audit trail.

## How OrgScope Affects Authorization Boundaries

The **`orgScope`** property acts as a capability boundary flag within the `ToolAuthContext`, determining how the system resolves the organization context and user permissions.

**Pinned Scope (`"pinned"`)**

When `orgScope` is pinned, the OAuth token is cryptographically bound to a specific `organizationId` encoded at issuance. The authorization system treats this as a single-tenant credential:

- **Role Inference**: Defaults to **`"owner"`** unless explicitly overridden in the token, avoiding additional database queries.
- **Usage Constraint**: The token cannot be used to act upon other organizations, effectively sandboxing the credential to the tenant specified during the OAuth consent flow.

**User Scope (`"user"`)**

When `orgScope` is set to `"user"`, the credential represents a portable identity (such as an API key) not locked to a specific organization:

- **Role Resolution**: Requires a live lookup of the user's membership in the target organization, returning `"owner"`, `"admin"`, or `"member"` based on the database record.
- **Cross-Org Capability**: A single user-scoped credential can operate across any organization the user belongs to, enabling tools to dynamically infer the organization from request parameters like `projectId`.

## Implementing Context Propagation in Open-SEO

The following patterns from the every-app/open-seo codebase demonstrate how to work with the propagation flow and orgScope validation.

### Building MCP Properties During OAuth Consent

When handling OAuth callbacks, construct the MCP property bundle using `createWorkersOAuthMcpProps`:

```typescript
// src/server/mcp/oauth-provider.ts (lines 36-55)
import { createWorkersOAuthMcpProps } from "@/server/mcp/context";

const mcpProps = createWorkersOAuthMcpProps({
  userId: ctx.userId,
  userEmail: ctx.userEmail,
  organizationId: ctx.organizationId,
  baseUrl: getHostedBaseUrl(),
  clientId: authRequest.clientId,
  scopes, // ["mcp", "read:projects", …]
});

// Pass to workers-oauth-provider for token embedding
await oauth.completeAuthorization({
  request: authRequest,
  userId: ctx.userId,
  metadata: { clientId: authRequest.clientId, organizationId: ctx.organizationId },
  scope: scopes,
  props: mcpProps,
});

```

### Re-assembling Context on Tool Invocation

When a request reaches the MCP endpoint, reconstruct the authorization context:

```typescript
// src/server/mcp/context.ts (lines 84-119)
import { createMcpToolContext } from "@/server/mcp/context";

export async function handleMcpRequest(
  serverCtx: ServerContext,
  mcpProps: McpProps,
) {
  // serverCtx.http.authInfo is populated by workers-oauth-provider
  const toolCtx = createMcpToolContext(serverCtx, mcpProps);

  // toolCtx.auth contains: userId, userEmail, organizationId, 
  // role, orgScope ("pinned" | "user"), clientId, scopes, baseUrl
  return someMcpTool(toolCtx);
}

```

### Enforcing OrgScope in Tool Implementations

Validate the authorization scope before executing business logic:

```typescript
// src/server/mcp/tools/list-projects.ts (lines 14-16)
export async function listProjects(toolCtx: ToolContext) {
  const { auth } = toolCtx;

  // Require user-scoped credentials for organization-spanning operations
  if (auth.orgScope !== "user") {
    throw new Error("list-projects requires a user-scoped credential");
  }

  // Check resolved role for fine-grained permissions
  if (!["owner", "admin"].includes(auth.role)) {
    throw new Error("Insufficient permissions");
  }

  // Proceed with project listing...
}

```

## Summary

- The **workers-oauth-provider** manages the OAuth flow, embedding MCP metadata into tokens via `createWorkersOAuthMcpProps` in **[`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts)**.
- During request processing, the provider populates `request.http.authInfo`, which the application merges with stored props using **`createMcpToolContext`** in **[`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)**.
- **`orgScope`** defaults to `"pinned"`, locking the credential to a single organization and defaulting the role to `"owner"`.
- **`orgScope: "user"`** creates portable credentials where the organization and role are resolved dynamically from request parameters and membership lookups.
- Tools enforce boundaries by checking `auth.orgScope` and `auth.role` before accessing resources, as demonstrated in tools like **[`src/server/mcp/tools/list-projects.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/list-projects.ts)**.

## Frequently Asked Questions

### What is the default orgScope value in workers-oauth-provider MCP tokens?

The default value is **`"pinned"`**. According to the implementation in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) (lines 99-105), when a token does not explicitly specify an organization scope, the system assumes a pinned organization context, which binds the credential to the `organizationId` encoded during the OAuth consent flow.

### How does the application determine the user's role when using a user-scoped credential?

When `orgScope` is set to `"user"`, the application cannot infer the role from the token alone. Instead, the specific tool implementation must identify the target organization from request parameters (such as a project ID) and then query the user's membership record in that organization to determine whether they hold the `"owner"`, `"admin"`, or `"member"` role.

### Can a pinned-scope token be used to access resources across multiple organizations?

No. A pinned-scope token is cryptographically bound to a specific `organizationId` established during the initial OAuth authorization. The `createMcpToolContext` function treats this organization as immutable, and tools that require cross-organizational access (such as `list-projects`) explicitly reject pinned tokens by validating `auth.orgScope !== "user"`.

### Where does workers-oauth-provider store the MCP metadata during the token lifecycle?

The provider stores the metadata inside the access token grant itself. During the consent phase in [`src/server/mcp/oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/oauth-provider.ts) (lines 45-55), the `mcpProps` object is passed to `oauth.completeAuthorization` and embedded into the token payload. When the token is later presented, the provider extracts these properties into `request.http.authInfo` for the application to consume.