How MCP Server Authentication and Project Authorization Work in OpenSEO
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, 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) 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.
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 (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.
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
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 (lines [45‑53])
Middleware Integration for Server Functions
The TanStack Server Function pipeline integrates these checks through middleware defined in 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.
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 (lines [32‑40])
Summary
- Authentication is enforced via
McpToolAuthContext, validated with Zod schemas insrc/server/mcp/context.tsand stored inAsyncLocalStorageto eliminate parameter drilling. - Authorization guarantees organization-project alignment through
requireProjectAccessinsrc/server/mcp/project-auth.ts, throwingFORBIDDENerrors for unauthorized access attempts. - Tool handlers use the
withMcpProjectAuthwrapper to receive ready-to-use contexts containing billing helpers and project data without repeating database queries. - Middleware integration in
src/serverFunctions/middleware.tsensures 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. 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 (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), 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 (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.
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 →