# How MCP Server Authentication and Project Authorization Work in OpenSEO

> Explore OpenSEO's MCP server authentication and project authorization. Learn how Zod schemas and AsyncLocalStorage secure your tools and enforce ownership boundaries.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-07-21

---

**OpenSEO's Model-Context-Protocol (MCP) layer implements a two-stage security model that first validates caller identity via Zod-schemas stored in AsyncLocalStorage, then enforces organization-project ownership boundaries before executing any tool handler.**

OpenSEO's MCP server secures every request through a strict authentication and authorization pipeline. The `every-app/open-seo` repository implements a type-safe boundary that validates user credentials before allowing access to project-scoped resources. Understanding how MCP server authentication and project authorization work is essential for developers extending the platform's server-side functions.

## Authentication via McpToolAuthContext

The authentication stage materializes incoming credentials into a type-safe `McpToolAuthContext` object. In [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts), lines **[25‑34]** define the Zod schema that validates the auth payload containing the user's ID, email, organization ID, scopes, and the MCP base URL.

To avoid passing context through every function signature, the system stores the validated context in an `AsyncLocalStorage` instance (lines **[46‑55]**). This allows downstream functions to access authentication state implicitly while maintaining thread safety across asynchronous operations.

### Validating and Retrieving Context

The `requireMcpToolAuthContext` function (lines **[92‑107]** in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)) retrieves the context from the async store or extracts it from `extra.authInfo` passed by the transport layer. If the context is missing, the function throws an error, creating a hard authentication gate.

```typescript
import { requireMcpToolAuthContext } from "@/server/mcp/context";

export async function whoAmI(extra: ToolExtra) {
  const auth = requireMcpToolAuthContext(extra);
  return { userId: auth.userId, email: auth.userEmail, org: auth.organizationId };
}

```

*Source:* [`src/server/mcp/tools/whoami.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/whoami.ts) (lines **[1‑9]**)

## Project Authorization Flow

After authentication, requests targeting specific projects must pass an authorization check that verifies the authenticated user belongs to the organization owning the target project. This enforcement occurs in [`src/server/mcp/project-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/project-auth.ts).

The `requireProjectAccess` function (lines **[13‑25]**) loads the project using `ProjectService.getProjectForOrganization(orgId, projectId)`. If the lookup fails—indicating the project does not belong to the user's organization—the function throws a `FORBIDDEN` error. This guarantees a hard gate even if underlying service error handling changes.

### The withMcpProjectAuth Wrapper

To simplify enforcement, lines **[39‑49]** expose `withMcpProjectAuth`, a higher-order function that wraps tool handlers. On success, the wrapper returns a project-scoped context containing:
- The original `McpToolAuthContext`
- A billing helper for downstream API calls
- The full project row

```typescript
import { withMcpProjectAuth } from "@/server/mcp/project-auth";

type Args = { projectId: string; keyword: string };

export const getKeywordSuggestions = withMcpProjectAuth<Args, Suggestion[]>(
  async ({ projectId, keyword }, ctx) => {
    // `ctx.project` is guaranteed to belong to `ctx.auth.organizationId`
    // `ctx.billing` can be passed to downstream billing APIs
    return await KeywordService.suggest(keyword, ctx.project.id);
  },
);

```

*Source:* [`src/server/mcp/tools/search-console-tools.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/search-console-tools.ts) (lines **[45‑53]**)

## Middleware Integration for Server Functions

The TanStack Server Function pipeline integrates these checks through middleware defined in [`src/serverFunctions/middleware.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/middleware.ts). Lines **[16‑24]** implement `requireAuthenticatedContext`, which validates the base context using Zod and injects an `EnsuredUserContext` into the function pipeline.

For project-scoped operations, lines **[42‑60]** add `requireProjectContext`, which invokes the authorization layer to validate the `projectId` parameter and attach the full project object to the context. This ensures every server function receives a validated context without duplicating authentication logic.

```typescript
export const requireAuthenticatedContext = [
  createMiddleware({ type: "function" }).server(async ({ next, context }) => {
    const auth = getAuthenticatedContext(context); // validates via Zod
    return next({ context: auth });
  }),
];

```

*Source:* [`src/serverFunctions/middleware.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/middleware.ts) (lines **[32‑40]**)

## Summary

- **Authentication** is enforced via `McpToolAuthContext`, validated with Zod schemas in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) and stored in `AsyncLocalStorage` to eliminate parameter drilling.
- **Authorization** guarantees organization-project alignment through `requireProjectAccess` in [`src/server/mcp/project-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/project-auth.ts), throwing `FORBIDDEN` errors for unauthorized access attempts.
- **Tool handlers** use the `withMcpProjectAuth` wrapper to receive ready-to-use contexts containing billing helpers and project data without repeating database queries.
- **Middleware integration** in [`src/serverFunctions/middleware.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/middleware.ts) ensures all TanStack Server Functions receive type-safe, validated contexts before executing business logic.

## Frequently Asked Questions

### What data structure stores the authenticated user context in OpenSEO's MCP server?

The system uses a `McpToolAuthContext` object defined in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts). This structure contains the user ID, email, organization ID, scopes, and MCP base URL. The context is stored in an `AsyncLocalStorage` instance (lines **[46‑55]**) to provide implicit access across asynchronous call stacks without passing the object through every function parameter.

### How does OpenSEO verify that a user can access a specific project?

The `requireProjectAccess` function in [`src/server/mcp/project-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/project-auth.ts) (lines **[13‑25]**) performs the verification by querying `ProjectService.getProjectForOrganization(orgId, projectId)`. If the project does not belong to the user's organization encoded in the authentication token, the function immediately throws a `FORBIDDEN` error, creating a hard authorization boundary that operates independently of the underlying service layer.

### Can MCP tool handlers access billing information after successful authorization?

Yes. When using the `withMcpProjectAuth` wrapper (lines **[39‑49]** in [`src/server/mcp/project-auth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/project-auth.ts)), the context object passed to the handler includes a billing helper and dashboard URL generators alongside the authenticated user info and full project row. This allows tools like keyword suggestion engines to pass billing contexts directly to downstream APIs without additional database lookups.

### Where does the authentication context enter the TanStack Server Function pipeline?

The context enters through `requireAuthenticatedContext` middleware in [`src/serverFunctions/middleware.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/middleware.ts) (lines **[16‑24]**). This middleware validates the incoming context using Zod schemas and injects an `EnsuredUserContext` into the function pipeline. For project-specific endpoints, `requireProjectContext` (lines **[42‑60]**) adds the additional authorization layer before the handler executes.