import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; import { Hono } from "hono"; import { auth } from "../auth"; import { createAuthCode, exchangeCode, getClient, registerClient, } from "./oauth"; import { registerMcpTools } from "./tools"; const clientUrl = process.env.KANEO_CLIENT_URL || "http://localhost:5173"; const apiUrl = (process.env.KANEO_API_URL || "http://localhost:1337").replace( /\/api\/?$/, "", ); const sessions = new Map(); function createMcpServerForUser(token: string): McpServer { const server = new McpServer({ name: "kaneo-mcp", version: "1.0.0", }); registerMcpTools(server, apiUrl, token); return server; } async function validateBearerToken( req: Request, ): Promise<{ userId: string; token: string } | null> { const authHeader = req.headers.get("authorization"); if (!authHeader) return null; const match = authHeader.match(/^Bearer\s+(\S+)$/i); if (!match?.[1]) return null; const token = match[1]; const headers = new Headers(); headers.set("authorization", `Bearer ${token}`); const session = await auth.api.getSession({ headers }); if (!session?.user?.id) return null; return { userId: session.user.id, token }; } const mcp = new Hono(); mcp.post("/mcp/register", async (c) => { const body = await c.req.json(); const client = registerClient({ redirectUris: body.redirect_uris ?? [], clientName: body.client_name, }); return c.json({ client_id: client.clientId, client_id_issued_at: client.issuedAt, redirect_uris: client.redirectUris, client_name: client.clientName, token_endpoint_auth_method: body.token_endpoint_auth_method ?? "none", grant_types: body.grant_types ?? ["authorization_code"], response_types: body.response_types ?? ["code"], }); }); mcp.get("/mcp/authorize", async (c) => { const clientId = c.req.query("client_id"); const redirectUri = c.req.query("redirect_uri"); const codeChallenge = c.req.query("code_challenge"); const state = c.req.query("state"); if (!clientId || !redirectUri || !codeChallenge) { return c.json({ error: "invalid_request" }, 400); } const client = getClient(clientId); if (!client) { return c.json({ error: "invalid_client" }, 400); } const session = await auth.api.getSession({ headers: c.req.raw.headers, }); if (session?.user?.id) { const code = createAuthCode({ clientId, userId: session.user.id, codeChallenge, redirectUri, }); const url = new URL(redirectUri); url.searchParams.set("code", code); if (state) url.searchParams.set("state", state); return c.redirect(url.toString()); } const deviceRes = await fetch(`${apiUrl}/api/auth/device/code`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: "kaneo-mcp" }), }); if (!deviceRes.ok) { return c.json({ error: "device_code_failed" }, 500); } const device = (await deviceRes.json()) as { device_code: string; user_code: string; verification_uri: string; interval: number; expires_in: number; }; const devicePageUrl = `${clientUrl}/device?user_code=${encodeURIComponent(device.user_code)}`; return c.html(` Kaneo MCP Redirecting to Kaneo… `); }); mcp.post("/mcp/authorize/callback", async (c) => { const body = await c.req.json(); const { access_token, client_id, code_challenge, redirect_uri, state } = body; if (!access_token || !client_id || !code_challenge || !redirect_uri) { return c.json({ error: "invalid_request" }, 400); } const headers = new Headers(); headers.set("authorization", `Bearer ${access_token}`); const session = await auth.api.getSession({ headers }); if (!session?.user?.id) { return c.json({ error: "invalid_token" }, 401); } const code = createAuthCode({ clientId: client_id, userId: session.user.id, codeChallenge: code_challenge, redirectUri: redirect_uri, }); const url = new URL(redirect_uri); url.searchParams.set("code", code); if (state) url.searchParams.set("state", state); return c.json({ redirect: url.toString() }); }); mcp.post("/mcp/token", async (c) => { const contentType = c.req.header("content-type") || ""; let params: Record; if (contentType.includes("application/x-www-form-urlencoded")) { const body = await c.req.text(); params = Object.fromEntries(new URLSearchParams(body)); } else { params = await c.req.json(); } const { grant_type, code, client_id, code_verifier, redirect_uri } = params; if (grant_type !== "authorization_code") { return c.json({ error: "unsupported_grant_type" }, 400); } if (!code || !client_id || !code_verifier || !redirect_uri) { return c.json({ error: "invalid_request" }, 400); } const result = await exchangeCode( code, client_id, code_verifier, redirect_uri, ); if (!result) { return c.json({ error: "invalid_grant" }, 400); } return c.json({ access_token: result.accessToken, token_type: "bearer", expires_in: result.expiresIn, }); }); mcp.get("/.well-known/oauth-protected-resource/api/mcp", (c) => c.json({ resource: `${apiUrl}/api/mcp`, authorization_servers: [`${apiUrl}/api`], }), ); mcp.get("/.well-known/oauth-authorization-server/api", (c) => c.json({ issuer: `${apiUrl}/api`, authorization_endpoint: `${apiUrl}/api/mcp/authorize`, token_endpoint: `${apiUrl}/api/mcp/token`, registration_endpoint: `${apiUrl}/api/mcp/register`, response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"], }), ); mcp.all("/mcp", async (c) => { const authResult = await validateBearerToken(c.req.raw); if (!authResult) { const prmUrl = `${apiUrl}/api/.well-known/oauth-protected-resource/api/mcp`; c.header("WWW-Authenticate", `Bearer resource_metadata="${prmUrl}"`); return c.json( { error: "invalid_token", error_description: "Missing or invalid token", }, 401, ); } const sessionId = c.req.header("mcp-session-id"); if (sessionId) { const existing = sessions.get(sessionId); if (existing) { return existing.handleRequest(c.req.raw); } return c.json({ error: "Session not found" }, 404); } if (c.req.method !== "POST") { return c.json({ error: "Method not allowed" }, 405); } const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), }); transport.onclose = () => { if (transport.sessionId) { sessions.delete(transport.sessionId); } }; const server = createMcpServerForUser(authResult.token); await server.connect(transport); const response = await transport.handleRequest(c.req.raw); if (transport.sessionId) { sessions.set(transport.sessionId, transport); } return response; }); export default mcp; export function mcpWellKnownRoutes(baseUrl: string) { const wellKnown = new Hono(); wellKnown.get("/.well-known/oauth-protected-resource/api/mcp", (c) => c.json({ resource: `${baseUrl}/api/mcp`, authorization_servers: [`${baseUrl}/api`], }), ); wellKnown.get("/.well-known/oauth-authorization-server/api", (c) => c.json({ issuer: `${baseUrl}/api`, authorization_endpoint: `${baseUrl}/api/mcp/authorize`, token_endpoint: `${baseUrl}/api/mcp/token`, registration_endpoint: `${baseUrl}/api/mcp/register`, response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"], }), ); return wellKnown; }