# How OpenSEO Implements Per-Project Authorization for MCP: A Deep Dive

> Discover how OpenSEO implements per-project authorization for MCP using withMcpProjectAuth to validate tokens and keys, ensuring secure project-specific operations.

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

---

**OpenSEO enforces per-project authorization for MCP by wrapping every tool call with `withMcpProjectAuth`, which validates OAuth tokens or API keys against the specific project's organization before executing any operation.**

The every-app/open-seo repository implements a strict authorization layer for its Model Context Protocol (MCP) server, ensuring that every tool invocation is scoped to a specific project and verified against the caller's organizational membership. This design prevents cross-project data leakage while supporting both OAuth tokens and API key authentication methods. Understanding this flow is essential for developers integrating with OpenSEO's MCP tools or building similar authorization systems.

## MCP Authentication Flow Overview

OpenSEO's MCP layer operates as a **fail-closed security system** where every request must pass through a five-stage validation pipeline before touching project data. The architecture separates transport-level authentication from project-level authorization, creating clear boundaries between verifying *who* is calling and *what* they can access.

The system supports **dual credential models**: OAuth tokens carry an `orgScope` of `"pinned"` (tied to a specific organization), while API keys use `"user"` scope requiring dynamic organization resolution. Both paths converge in the same project authorization wrapper, ensuring consistent enforcement regardless of authentication method.

## The Five-Step Authorization Process

### Step 1: Transport Layer Validation

Every MCP request enters through `handleAuthenticatedOpenSeoMcpRequest` in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts). This function acts as the gatekeeper, verifying that the request carries both a valid OAuth token and the specific `MCP` scope before allowing further processing.

```typescript
// transport.ts – entry point for an MCP request
export async function handleAuthenticatedOpenSeoMcpRequest(
  request: Request,
  props: unknown,
) {
  const result = hostedWorkersOAuthMcpPropsSchema.safeParse(props);
  if (!result.success) return new Response("MCP auth context required", {status: 403});
  if (!result.data[MCP_AUTH_CONTEXT_PROP].scopes.includes(MCP_SCOPE))
    return new Response("MCP scope required", {status: 403});
  // ... proceeds to tool handling
}

```

If the scope check fails, the server returns a **403 Forbidden** response immediately, preventing unauthorized requests from consuming resources or reaching project data.

### Step 2: Building the MCP Auth Context

Once past transport security, `createMcpToolContext` in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) assembles a `ToolAuthContext` object. This context captures the **orgScope** (`"pinned"` for OAuth tokens, `"user"` for API keys) and prepares the foundation for project-level checks.

The context creation distinguishes between credential types:
- **OAuth tokens**: Pre-validated against a specific organization
- **API keys**: Require lookup to determine organizational membership

### Step 3: Project-Level Authorization Checks

The critical authorization logic resides in [`src/server/mcp/project-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/project-auth.ts). Every MCP tool that accesses project data must be wrapped with `withMcpProjectAuth`, which calls `requireProjectAccess` to perform the actual security validation.

```typescript
// project-auth.ts – core per‑project auth wrapper
export function withMcpProjectAuth<TArgs extends {projectId: string}, TResult>(
  handler: (args: TArgs, ctx: McpProjectAuthContext) => Promise<TResult> | TResult,
) {
  return async (args: TArgs, toolContext: ToolContext) => {
    const context = await requireProjectAccess(toolContext, args.projectId);
    return handler(args, context);
  };
}

```

The `requireProjectAccess` function implements **branching validation logic**:

- **If `orgScope` = `"user"` (API key)**: Fetches the target project, derives its organization, and confirms the caller's membership in that organization
- **If `orgScope` = `"pinned"` (OAuth token)**: Validates that the requested `projectId` belongs to the token's pre-authorized organization

Both failure paths return a generic `FORBIDDEN` error (lines 26-28) to prevent organization enumeration attacks.

### Step 4: Tool Execution with Resolved Context

Upon successful authorization, the system constructs a `McpProjectAuthContext` containing resolved `auth`, `billing`, and `project` objects. This context is injected into the tool handler, allowing safe data operations.

```typescript
// Example tool using the wrapper
export const getProjectContextTool = {
  name: "get_project_context",
  config: { /* ... */ },
  handler: withMcpProjectAuth(async (args, context) => {
    const projectContext = await ProjectContextService.getProjectContext(args.projectId);
    return mcpResponse({
      text: ProjectContextService.renderProjectContextMarkdown(projectContext),
      meta: buildProjectMeta(context, args.projectId, `/p/${args.projectId}/settings/context`),
      structuredContent: projectContext,
    });
  }),
};

```

Tools like `get_project_context` in [`src/server/mcp/tools/project-context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/project-context.ts) demonstrate how the authorization wrapper enables secure data access without duplicating security logic in each handler.

### Step 5: Billing Alignment

The final stage ensures financial accountability. The `buildBillingCustomer` function (called within `requireProjectAccess`) creates a `BillingCustomerContext` mirroring the resolved organization and project. This guarantees that all MCP tool usage is correctly attributed to the right billing entity, preventing cross-customer charging errors.

## Key Implementation Files

Understanding the file structure helps navigate the authorization stack:

- **[`src/server/mcp/project-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/project-auth.ts)**: Central per-project authorization logic containing `withMcpProjectAuth` and `requireProjectAccess`
- **[`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)**: Definition of `ToolAuthContext` and factory functions for building MCP authentication contexts
- **[`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts)**: Entry point validating MCP scope and OAuth tokens before tool dispatch
- **[`src/server/mcp/tools/project-context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/project-context.ts)**: Reference implementation showing authorized tool patterns

## Security Design Principles

OpenSEO's per-project authorization for MCP follows **defense-in-depth** principles:

1. **Single Source of Truth**: All tools delegate to `withMcpProjectAuth`, eliminating authorization bypass risks from inconsistent checks
2. **Fail-Closed by Default**: Any missing project or failed membership test results in immediate rejection
3. **Generic Error Messages**: The system returns indistinguishable `FORBIDDEN` responses for missing projects versus unauthorized access, preventing information leakage about project existence
4. **Context Immutability**: Once constructed, the `McpProjectAuthContext` is passed downstream as a read-only object, preventing tool logic from elevating privileges

## Summary

- **Transport validation** in [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts) ensures all MCP requests carry proper OAuth scopes before processing begins
- **Dual credential support** handles both API keys (`orgScope: "user"`) and OAuth tokens (`orgScope: "pinned"`) through a unified interface
- **Project-level enforcement** via `withMcpProjectAuth` in [`project-auth.ts`](https://github.com/every-app/open-seo/blob/main/project-auth.ts) guarantees every tool call is verified against the specific project's organization
- **Billing integration** ensures usage attribution aligns with the resolved project context
- **Fail-closed security** prevents data access when authorization states are ambiguous or invalid

## Frequently Asked Questions

### How does OpenSEO prevent users from accessing projects outside their organization?

OpenSEO prevents cross-organization access through the `requireProjectAccess` function in [`project-auth.ts`](https://github.com/every-app/open-seo/blob/main/project-auth.ts). For API key requests, it fetches the requested project and verifies the user belongs to that project's organization. For OAuth tokens, it validates that the project ID exists within the token's pinned organization. Both checks return identical `FORBIDDEN` errors to prevent project enumeration attacks.

### What is the difference between "pinned" and "user" org scopes in MCP authorization?

The `orgScope` property distinguishes authentication methods: `"pinned"` indicates an OAuth token pre-authorized for a specific organization, while `"user"` indicates an API key requiring dynamic organization lookup. The authorization wrapper handles both cases transparently, resolving the correct organization context before allowing project access regardless of the credential type used.

### Can MCP tools function without the `withMcpProjectAuth` wrapper?

While technically possible, bypassing `withMcpProjectAuth` would violate OpenSEO's security model by allowing unverified project access. The wrapper ensures consistent application of billing context, organizational membership verification, and audit logging. All production tools in the codebase use this wrapper to maintain security boundaries.

### Where does the initial MCP scope validation occur?

The initial validation occurs in `handleAuthenticatedOpenSeoMcpRequest` within [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts). This function checks that the incoming request includes the `MCP` scope in its OAuth credentials before any tool routing occurs, providing the first layer of defense against unauthorized MCP access attempts.