How OpenSEO MCP Server Authorization and Project Authentication Work
OpenSEO implements a two-stage security architecture that first authenticates users via a type-safe context object stored in AsyncLocalStorage, then authorizes project access through explicit organization-project validation gates.
OpenSEO (every-app/open-seo) secures its Model-Context-Protocol (MCP) layer through a strict separation between authentication and project authorization. This design ensures that every request reaching a server-side function carries validated user credentials while enforcing rigid access controls that verify organization ownership before allowing project-specific operations.
Authentication Flow: Establishing the McpToolAuthContext
The authentication stage materializes caller credentials into a validated McpToolAuthContext object. According to the OpenSEO source code, this context contains the user’s ID, email, organization ID, scopes, and the base URL of the MCP endpoint.
In src/server/mcp/context.ts (lines [25‑34]), a Zod schema defines the strict structure of this auth payload. The system stores the validated context in an AsyncLocalStorage instance (lines [46‑55]), creating an asynchronous context store that eliminates the need to pass authentication data through every function signature manually.
The helper function requireMcpToolAuthContext (lines [92‑107]) retrieves this context from the async store or extracts it from extra.authInfo for callers that supply it directly. This pattern guarantees that downstream tools always access type-safe, validated authentication data without repeating validation logic.
Project Authorization: Enforcing Organization-Project Boundaries
After authentication, requests targeting specific projects must prove the authenticated user belongs to the organization that owns the target project. This authorization layer prevents cross-organization data access through explicit database verification.
In src/server/mcp/project-auth.ts (lines [13‑25]), the requireProjectAccess function 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 creates a hard security gate that persists even if underlying service error handling changes.
Upon successful validation, the function assembles a project-scoped context (lines [27‑34]) containing the original auth information, a billing helper, and the full project row. The withMcpProjectAuth wrapper (lines [39‑49]) encapsulates this logic, allowing any tool handler to enforce project authorization declaratively.
Middleware Integration with TanStack Server Functions
OpenSEO integrates these security layers into its TanStack Server Function pipeline through middleware composition. The src/serverFunctions/middleware.ts file orchestrates the authentication and authorization checks for every server function.
Lines [16‑24] implement requireAuthenticatedContext, which validates the base context and ensures every request carries a valid EnsuredUserContext. For operations requiring project access, lines [42‑60] add the project guard through requireProjectContext, injecting the validated projectId and project objects into the function context.
This middleware approach ensures that business logic remains free of security boilerplate while maintaining guarantee that authentication and authorization checks execute before any data operations.
Practical Implementation Examples
Retrieving Authentication Context Directly
Tools that only need user identity can access the authentication context directly using the requireMcpToolAuthContext helper:
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])
Enforcing Project Access with the Authorization Wrapper
Project-scoped tools use the withMcpProjectAuth wrapper to automatically enforce organization-project matching:
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 Pipeline Integration
Server functions integrate these checks through middleware composition:
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 handled via
McpToolAuthContextinsrc/server/mcp/context.ts, validated with Zod and stored in AsyncLocalStorage for downstream access without parameter drilling. - Authorization enforces organization-project boundaries through
requireProjectAccessinsrc/server/mcp/project-auth.ts, which performs explicit database lookups and returns forbidden errors for unauthorized access attempts. - Integration occurs through TanStack Server Function middleware in
src/serverFunctions/middleware.ts, ensuring every endpoint receives validated contexts before executing business logic. - Developer experience is streamlined through helpers like
withMcpProjectAuth, which wrap tool handlers to automatically inject authorized contexts and billing helpers.
Frequently Asked Questions
What data does McpToolAuthContext contain?
McpToolAuthContext contains the user’s ID, email address, organization ID, authorized scopes, and the MCP base URL. This schema is strictly defined using Zod in src/server/mcp/context.ts (lines [25‑34]) to ensure type safety across the entire request lifecycle.
How does OpenSEO prevent users from accessing projects outside their organization?
The system enforces a hard gate through requireProjectAccess in src/server/mcp/project-auth.ts (lines [13‑25]). This function queries the database to verify that the requested project belongs to the organization encoded in the authentication token. If the lookup fails, it immediately throws a FORBIDDEN error before any project data returns to the caller.
Why does OpenSEO use AsyncLocalStorage for authentication context?
AsyncLocalStorage eliminates the need to pass authentication parameters through every function signature in the call stack. By storing the validated McpToolAuthContext in async local storage (implemented in src/server/mcp/context.ts, lines [46‑55]), downstream tools can access user credentials implicitly while maintaining isolation between concurrent requests.
Can individual MCP tools override or skip project authorization?
No. Project authorization is enforced through the withMcpProjectAuth wrapper (lines [39‑49] in src/server/mcp/project-auth.ts), which executes the database verification before invoking the tool handler. This design ensures that authorization logic cannot be bypassed accidentally, as the wrapper controls the execution context and only proceeds if the organization-project match validates successfully.
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 →