Context Propagation Flow for workers-oauth-provider in MCP Tool Calls: How OrgScope Affects Authorization
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 (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 (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 (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
orgScopeis"pinned"(the default), the token is organization-scoped. The function infers theroleas"owner"without performing a membership database lookup, as the organization ID is immutable for this credential. - If
orgScopeis"user", the credential is user-scoped (such as an API key generated viasrc/server/mcp/api-key-auth.ts). The tool must resolve the target organization from the request parameters (e.g., a project ID), and the effectiveroleis 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 (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 (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:
// 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:
// 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:
// 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
createWorkersOAuthMcpPropsinsrc/server/mcp/oauth-provider.ts. - During request processing, the provider populates
request.http.authInfo, which the application merges with stored props usingcreateMcpToolContextinsrc/server/mcp/context.ts. orgScopedefaults 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.orgScopeandauth.rolebefore accessing resources, as demonstrated in tools likesrc/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 (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 (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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →