Ben Senescu 5cef024c68
Add skills for agents (#211)
* Add OpenSEO skills for keyword and link workflows

* Remove credit scare copy from MCP and skills

* Remove local skills test sandbox

* Format OpenSEO skill docs

* Disclose OpenSEO credit threshold for MCP research

* Tighten OpenSEO skill workflows
2026-05-22 08:46:06 -04:00

107 lines
3.3 KiB
TypeScript

import { createMcpHandler } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getMcpResource, MCP_SCOPE } from "@/lib/oauth-resource";
import { resolveCloudflareAccessContext } from "@/middleware/ensure-user/cloudflareAccess";
import { resolveLocalNoAuthContext } from "@/middleware/ensure-user/delegated";
import {
createWorkersOAuthMcpProps,
MCP_AUTH_CONTEXT_PROP,
MCP_ROUTE,
runWithMcpToolAuthContext,
workersOAuthMcpPropsSchema,
} from "@/server/mcp/context";
import { getPublicOrigin } from "@/server/mcp/public-origin";
import { registerOpenSeoMcpTools } from "@/server/mcp/server";
function createOpenSeoMcpServer() {
const server = new McpServer(
{
name: "OpenSEO MCP",
version: "0.0.10",
},
{
instructions:
"OpenSEO research tools use credits. Proceed with normal focused research, but ask the user for confirmation before planned batches over 2,000 credits.",
},
);
registerOpenSeoMcpTools(server);
return server;
}
export async function handleAuthenticatedOpenSeoMcpRequest(
request: Request,
props: unknown,
env: unknown,
ctx: ExecutionContext,
): Promise<Response> {
const result = workersOAuthMcpPropsSchema.safeParse(props);
const scopes = result.success
? result.data[MCP_AUTH_CONTEXT_PROP].scopes
: [];
if (!result.success || !scopes.includes(MCP_SCOPE)) {
return new Response("MCP auth context required", { status: 403 });
}
return handleOpenSeoMcpRequest(request, result.data, env, ctx);
}
export async function handleSelfHostedOpenSeoMcpRequest(
request: Request,
authMode: "cloudflare_access" | "local_noauth",
env: unknown,
ctx: ExecutionContext,
): Promise<Response> {
// Self-hosted auth mirrors the app: local_noauth uses the local admin
// workspace, while cloudflare_access trusts Cloudflare's Access JWT.
// CORS/preflight still needs to reach the MCP transport before auth context
// exists, so OPTIONS intentionally bypasses context creation.
if (request.method === "OPTIONS") {
return handleOpenSeoMcpRequest(request, undefined, env, ctx);
}
const baseUrl = getPublicOrigin(request);
const context =
authMode === "local_noauth"
? await resolveLocalNoAuthContext()
: await resolveCloudflareAccessContext(request.headers);
const props = createWorkersOAuthMcpProps({
userId: context.userId,
userEmail: context.userEmail,
organizationId: context.organizationId,
clientId: null,
scopes: [],
audience: getMcpResource(baseUrl),
subject: context.userId,
baseUrl,
});
return handleOpenSeoMcpRequest(request, props, env, ctx);
}
function handleOpenSeoMcpRequest(
request: Request,
props: ReturnType<typeof createWorkersOAuthMcpProps> | undefined,
env: unknown,
ctx: ExecutionContext,
): Promise<Response> {
const server = createOpenSeoMcpServer();
const handler = createMcpHandler(server, {
route: MCP_ROUTE,
enableJsonResponse: true,
authContext: props ? { props } : undefined,
corsOptions: {
headers:
"Authorization, Content-Type, Last-Event-ID, mcp-protocol-version, mcp-session-id",
exposeHeaders: "mcp-protocol-version, mcp-session-id",
},
});
if (!props) return handler(request, env, ctx);
return runWithMcpToolAuthContext(props[MCP_AUTH_CONTEXT_PROP], () =>
handler(request, env, ctx),
);
}