From 03a8a52b9499ab512f68aa4c58f44f45d4e78257 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Mon, 27 Apr 2026 14:32:06 -0700 Subject: [PATCH 1/3] Move internal MCP server to backend and use Mintlify MCP for docs tools - Relocate `/api/internal/[transport]` route from docs app to backend - Replace bespoke docs-tools HTTP client with `@ai-sdk/mcp` client pointing at Mintlify MCP - Swap `STACK_DOCS_INTERNAL_BASE_URL` for `STACK_MINTLIFY_MCP_URL` - Add e2e test covering tools/list and tools/call validation --- apps/backend/.env | 2 +- apps/backend/.env.development | 2 +- apps/backend/package.json | 1 + .../src/app/api/internal/[transport]/route.ts | 63 +++----- apps/backend/src/lib/ai/tools/docs.ts | 143 +++--------------- .../endpoints/api/v1/internal/mcp.test.ts | 120 +++++++++++++++ docs/package.json | 1 - pnpm-lock.yaml | 22 ++- 8 files changed, 181 insertions(+), 173 deletions(-) rename {docs => apps/backend}/src/app/api/internal/[transport]/route.ts (62%) create mode 100644 apps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.ts diff --git a/apps/backend/.env b/apps/backend/.env index 6f28c1c4ef..228c065825 100644 --- a/apps/backend/.env +++ b/apps/backend/.env @@ -117,7 +117,7 @@ STACK_TELEGRAM_BOT_TOKEN= # enter you telegram bot token STACK_TELEGRAM_CHAT_ID=# enter your telegram chat id # Docs AI tool bundle -STACK_DOCS_INTERNAL_BASE_URL=# override the docs origin used by the backend's AI tool bundle to call the docs app's `/api/internal/docs-tools` endpoint. Defaults to http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}04 in dev, https://mcp.stack-auth.com in prod +STACK_MINTLIFY_MCP_URL=# override the Mintlify MCP server used by the backend's AI docs tool bundle. Defaults to https://stackauth-e0affa27.mintlify.app/mcp # MCP review tool (SpacetimeDB) STACK_SPACETIMEDB_URI=# SpacetimeDB host URI; default empty (logging disabled) diff --git a/apps/backend/.env.development b/apps/backend/.env.development index 8ec5f41785..f20581e078 100644 --- a/apps/backend/.env.development +++ b/apps/backend/.env.development @@ -78,7 +78,7 @@ STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION STACK_FEEDBACK_MODE=FORWARD_TO_PRODUCTION -# STACK_DOCS_INTERNAL_BASE_URL=http://localhost:8104 +STACK_MINTLIFY_MCP_URL=https://stackauth-e0affa27.mintlify.app/mcp # Email monitor configuration for tests STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification STACK_EMAIL_MONITOR_PROJECT_ID=internal diff --git a/apps/backend/package.json b/apps/backend/package.json index 6569d968bc..b0500582df 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -87,6 +87,7 @@ "@stackframe/stack-shared": "workspace:*", "@upstash/qstash": "^2.8.2", "@vercel/functions": "^2.0.0", + "@vercel/mcp-adapter": "^1.0.0", "@vercel/otel": "^1.10.4", "@vercel/sandbox": "^1.2.0", "ai": "^6.0.0", diff --git a/docs/src/app/api/internal/[transport]/route.ts b/apps/backend/src/app/api/internal/[transport]/route.ts similarity index 62% rename from docs/src/app/api/internal/[transport]/route.ts rename to apps/backend/src/app/api/internal/[transport]/route.ts index 925a587b3e..966eaf9084 100644 --- a/docs/src/app/api/internal/[transport]/route.ts +++ b/apps/backend/src/app/api/internal/[transport]/route.ts @@ -1,11 +1,20 @@ +import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { createMcpHandler } from "@vercel/mcp-adapter"; import { PostHog } from "posthog-node"; import { z } from "zod"; -const nodeClient = process.env.NEXT_PUBLIC_POSTHOG_KEY - ? new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY) +const postHogKey = getEnvVariable("NEXT_PUBLIC_POSTHOG_KEY", ""); +const nodeClient = postHogKey !== "" + ? new PostHog(postHogKey) : null; +function getBackendApiBaseUrl(): string { + return ( + getEnvVariable("NEXT_PUBLIC_SERVER_STACK_API_URL", "") || + getEnvVariable("NEXT_PUBLIC_STACK_API_URL") + ).replace(/\/$/, ""); +} + const handler = createMcpHandler( async (server) => { server.tool( @@ -29,7 +38,7 @@ const handler = createMcpHandler( .string() .optional() .describe( - "Pass the conversationId from a previous response to group related calls into the same conversation. Omit on the first call — the server will generate one and return it.", + "Pass the conversationId from a previous response to group related calls into the same conversation. Omit on the first call - the server will generate one and return it.", ), }, async ({ question, reason, userPrompt, conversationId }) => { @@ -39,16 +48,7 @@ const handler = createMcpHandler( distinctId: "mcp-handler", }); - const apiBase = process.env.NEXT_PUBLIC_STACK_API_URL; - if (apiBase == null || apiBase === "") { - return { - content: [{ type: "text", text: "NEXT_PUBLIC_STACK_API_URL is not configured on the docs server." }], - isError: true, - }; - } - - const url = `${apiBase.replace(/\/$/, "")}/api/latest/ai/query/generate`; - const res = await fetch(url, { + const res = await fetch(`${getBackendApiBaseUrl()}/api/latest/ai/query/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -86,44 +86,15 @@ const handler = createMcpHandler( const responseConversationId = body.conversationId ?? conversationId ?? ""; return { - content: [{ type: "text", text: `${text.length > 0 ? text : "(empty response)"}\n\n[conversationId: ${responseConversationId} — pass this value as the conversationId parameter in your next ask_stack_auth call to continue this conversation]` }], + content: [{ type: "text", text: `${text.length > 0 ? text : "(empty response)"}\n\n[conversationId: ${responseConversationId} - pass this value as the conversationId parameter in your next ask_stack_auth call to continue this conversation]` }], }; }, ); }, { - capabilities: { - tools: { - ask_stack_auth: { - description: - "Ask the Stack Auth documentation assistant any question about Stack Auth (setup, APIs, SDKs, configuration, troubleshooting).", - parameters: { - type: "object", - properties: { - question: { - type: "string", - description: "The full question to ask about Stack Auth.", - }, - reason: { - type: "string", - description: - "Why the agent invoked this tool (for analytics and debugging). Not sent to the documentation model.", - }, - userPrompt: { - type: "string", - description: - "The original user message/prompt that triggered this tool call. Copy the user's exact words.", - }, - conversationId: { - type: "string", - description: - "Pass the conversationId from a previous response to group related calls. Omit on first call.", - }, - }, - required: ["question", "reason", "userPrompt"], - }, - }, - }, + serverInfo: { + name: "stack-auth-mcp", + version: "0.1.0", }, }, { diff --git a/apps/backend/src/lib/ai/tools/docs.ts b/apps/backend/src/lib/ai/tools/docs.ts index f03f90f7ed..9b0e2a7151 100644 --- a/apps/backend/src/lib/ai/tools/docs.ts +++ b/apps/backend/src/lib/ai/tools/docs.ts @@ -1,134 +1,35 @@ -import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; -import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; -import { tool } from "ai"; -import { z } from "zod"; +import { createMCPClient, type MCPClient } from "@ai-sdk/mcp"; +import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; -type DocsToolHttpResult = { - content?: Array<{ type: string, text?: string }>, - isError?: boolean, -}; +let mintlifyMcpClientPromise: Promise | null = null; -function getDocsToolsBaseUrl(): string { - const fromEnv = getEnvVariable("STACK_DOCS_INTERNAL_BASE_URL", ""); - if (fromEnv !== "") { - return fromEnv.replace(/\/$/, ""); - } - if (getNodeEnvironment() === "development") { - const portPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); - return `http://localhost:${portPrefix}26`; - } - return "https://mcp.stack-auth.com"; +function getMintlifyMcpUrl(): string { + return getEnvVariable("STACK_MINTLIFY_MCP_URL", "https://stackauth-e0affa27.mintlify.app/mcp"); } -async function postDocsToolAction(action: Record): Promise { - const base = getDocsToolsBaseUrl(); - try { - const res = await fetch(`${base}/api/internal/docs-tools`, { - method: "POST", - headers: { - "Content-Type": "application/json", - // MCP-style JSON-RPC endpoint requires clients to advertise both JSON and SSE. - Accept: "application/json, text/event-stream", +async function getMintlifyMcpClient(): Promise { + if (mintlifyMcpClientPromise == null) { + mintlifyMcpClientPromise = createMCPClient({ + transport: { + type: "http", + url: getMintlifyMcpUrl(), }, - body: JSON.stringify(action), + name: "stack-auth-backend-docs-agent", + }).catch((err: unknown) => { + mintlifyMcpClientPromise = null; + throw err; }); - - if (!res.ok) { - const errBody = await res.text(); - captureError("docs-tools-http-error", new Error(`Stack Auth docs tools error (${res.status}): ${errBody}`)); - return "Stack Auth docs tools returned an error. Please try again later."; - } - - const data = (await res.json()) as DocsToolHttpResult; - const text = data.content - ?.filter((c): c is { type: "text", text: string } => c.type === "text" && typeof c.text === "string") - .map((c) => c.text) - .join("\n") ?? ""; - - if (data.isError === true) { - return text || "Unknown docs tool error"; - } - - return text; - } catch (err) { - captureError("docs-tools-transport-error", err instanceof Error ? err : new Error(String(err))); - return "Stack Auth docs tools are temporarily unavailable. Please try again later."; } + + return await mintlifyMcpClientPromise; } /** - * Documentation tools backed by the docs app's `/api/internal/docs-tools` endpoint. - * - * The public MCP server at the same docs origin exposes only `ask_stack_auth`, which proxies to - * `/api/latest/ai/query/generate`; these tools avoid MCP recursion by calling the HTTP API directly. + * Documentation tools backed by Mintlify's generated MCP server. + * The public Stack Auth MCP server still exposes the higher-level `ask_stack_auth` tool; + * that agent uses these lower-level Mintlify tools for search and page reads. */ export async function createDocsTools() { - return { - list_available_docs: tool({ - description: - "Use this tool to learn about what Stack Auth is, available documentation, and see if you can use it for what you're working on. It returns a list of all available Stack Auth Documentation pages.", - inputSchema: z.object({}), - execute: async () => { - return await postDocsToolAction({ action: "list_available_docs" }); - }, - }), - - search_docs: tool({ - description: - "Search through all Stack Auth documentation including API docs, guides, and examples. Returns ranked results with snippets and relevance scores.", - inputSchema: z.object({ - search_query: z.string().describe("The search query to find relevant documentation"), - result_limit: z.number().optional().describe("Maximum number of results to return (default: 50)"), - }), - execute: async ({ search_query, result_limit = 50 }) => { - return await postDocsToolAction({ - action: "search_docs", - search_query, - result_limit, - }); - }, - }), - - get_docs_by_id: tool({ - description: - "Use this tool to retrieve a specific Stack Auth Documentation page by its ID. It gives you the full content of the page so you can know exactly how to use specific Stack Auth APIs. Whenever using Stack Auth, you should always check the documentation first to have the most up-to-date information. When you write code using Stack Auth documentation you should reference the content you used in your comments.", - inputSchema: z.object({ - id: z.string(), - }), - execute: async ({ id }) => { - return await postDocsToolAction({ action: "get_docs_by_id", id }); - }, - }), - - get_stack_auth_setup_instructions: tool({ - description: - "Use this tool when the user wants to set up authentication in a new project. It provides step-by-step instructions for installing and configuring Stack Auth authentication.", - inputSchema: z.object({}), - execute: async () => { - return await postDocsToolAction({ action: "get_stack_auth_setup_instructions" }); - }, - }), - - search: tool({ - description: - "Search for Stack Auth documentation pages.\n\nUse this tool to find documentation pages that contain a specific keyword or phrase.", - inputSchema: z.object({ - query: z.string(), - }), - execute: async ({ query }) => { - return await postDocsToolAction({ action: "search", query }); - }, - }), - - fetch: tool({ - description: - "Fetch a particular Stack Auth Documentation page by its ID.\n\nThis tool is identical to `get_docs_by_id`.", - inputSchema: z.object({ - id: z.string(), - }), - execute: async ({ id }) => { - return await postDocsToolAction({ action: "fetch", id }); - }, - }), - }; + const client = await getMintlifyMcpClient(); + return await client.tools(); } diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.ts new file mode 100644 index 0000000000..d10d2b08a5 --- /dev/null +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.ts @@ -0,0 +1,120 @@ +import { STACK_BACKEND_BASE_URL, it, niceFetch } from "../../../../../helpers"; + +async function mcpRequest(body: unknown) { + return await niceFetch(new URL("/api/internal/mcp", STACK_BACKEND_BASE_URL), { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json, text/event-stream", + }, + body: JSON.stringify(body), + }); +} + +function parseMcpBody(body: unknown): unknown { + if (typeof body !== "string") { + return body; + } + + const dataLine = body + .split("\n") + .find((line) => line.startsWith("data: ")); + if (dataLine == null) { + return JSON.parse(body); + } + + return JSON.parse(dataLine.slice("data: ".length)); +} + +it("internal MCP endpoint should expose the Stack Auth docs assistant tool", async ({ expect }) => { + const response = await mcpRequest({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + }); + + expect(response.status).toBe(200); + expect(parseMcpBody(response.body)).toMatchInlineSnapshot(` + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "tools": [ + { + "description": "Ask the Stack Auth documentation assistant. Use this for any question about Stack Auth: setup, APIs, SDK usage, configuration, or troubleshooting. The assistant searches official documentation and answers with citations. Always set \`reason\` to a short explanation of why you are calling this tool (for product analytics and debugging).", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "conversationId": { + "description": "Pass the conversationId from a previous response to group related calls into the same conversation. Omit on the first call - the server will generate one and return it.", + "type": "string", + }, + "question": { + "description": "The full question to ask about Stack Auth.", + "type": "string", + }, + "reason": { + "description": "Why the agent invoked this tool (e.g. user asked about OAuth setup, need Stack Auth API headers). Used for analytics, not sent to the model.", + "minLength": 1, + "type": "string", + }, + "userPrompt": { + "description": "The original user message/prompt that triggered this tool call. Copy the user's exact words. Don't include any sensitive information.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "question", + "reason", + "userPrompt", + ], + "type": "object", + }, + "name": "ask_stack_auth", + }, + ], + }, + } + `); +}); + +it("internal MCP endpoint should reject missing required docs assistant fields before invoking AI", async ({ expect }) => { + const response = await mcpRequest({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "ask_stack_auth", + arguments: { + question: "How do I set up OAuth?", + userPrompt: "How do I set up OAuth?", + }, + }, + }); + + expect(response.status).toBe(200); + expect(parseMcpBody(response.body)).toMatchInlineSnapshot(` + { + "error": { + "code": -32602, + "message": deindent\` + MCP error -32602: Invalid arguments for tool ask_stack_auth: [ + { + "code": "invalid_type", + "expected": "string", + "received": "undefined", + "path": [ + "reason" + ], + "message": "Required" + } + ] + \`, + }, + "id": 1, + "jsonrpc": "2.0", + } + `); +}); diff --git a/docs/package.json b/docs/package.json index 911336e540..a8cf5c5a53 100644 --- a/docs/package.json +++ b/docs/package.json @@ -29,7 +29,6 @@ "@radix-ui/react-tabs": "^1.1.12", "@stackframe/stack": "workspace:^", "@stackframe/stack-shared": "workspace:^", - "@vercel/mcp-adapter": "^1.0.0", "ai": "^6.0.0", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ec346963c..482352d2f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: '@vercel/functions': specifier: ^2.0.0 version: 2.0.0(@aws-sdk/credential-provider-web-identity@3.972.27) + '@vercel/mcp-adapter': + specifier: ^1.0.0 + version: 1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@vercel/otel': specifier: ^1.10.4 version: 1.10.4(@opentelemetry/api-logs@0.53.0)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0)) @@ -900,9 +903,6 @@ importers: '@stackframe/stack-shared': specifier: workspace:^ version: link:../packages/stack-shared - '@vercel/mcp-adapter': - specifier: ^1.0.0 - version: 1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) ai: specifier: ^6.0.0 version: 6.0.81(zod@3.25.76) @@ -30643,6 +30643,13 @@ snapshots: optionalDependencies: next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@vercel/mcp-adapter@1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + '@modelcontextprotocol/sdk': 1.17.2 + mcp-handler: 1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + optionalDependencies: + next: 16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@vercel/oidc@3.1.0': {} '@vercel/otel@1.10.4(@opentelemetry/api-logs@0.53.0)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.6.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))': @@ -36231,6 +36238,15 @@ snapshots: optionalDependencies: next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + mcp-handler@1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + dependencies: + '@modelcontextprotocol/sdk': 1.17.2 + chalk: 5.6.2 + commander: 11.1.0 + redis: 4.7.1 + optionalDependencies: + next: 16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 From 5941e97330609d822c28fa911ce4d3188aff6584 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Mon, 27 Apr 2026 14:42:45 -0700 Subject: [PATCH 2/3] Enhance error handling in createDocsTools function - Wrap the Mintlify MCP client call in a try-catch block to handle potential errors. - Capture errors using the captureError utility and return a structured response indicating that documentation tools are unavailable. - Introduce a tool response that informs users of temporary unavailability, improving user experience during failures. --- apps/backend/src/lib/ai/tools/docs.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/lib/ai/tools/docs.ts b/apps/backend/src/lib/ai/tools/docs.ts index 9b0e2a7151..da5dedf980 100644 --- a/apps/backend/src/lib/ai/tools/docs.ts +++ b/apps/backend/src/lib/ai/tools/docs.ts @@ -1,5 +1,8 @@ import { createMCPClient, type MCPClient } from "@ai-sdk/mcp"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { tool } from "ai"; +import { z } from "zod"; let mintlifyMcpClientPromise: Promise | null = null; @@ -30,6 +33,19 @@ async function getMintlifyMcpClient(): Promise { * that agent uses these lower-level Mintlify tools for search and page reads. */ export async function createDocsTools() { - const client = await getMintlifyMcpClient(); - return await client.tools(); + try { + const client = await getMintlifyMcpClient(); + return await client.tools(); + } catch (error) { + captureError("mintlify-mcp-docs-tools", error); + return { + docsUnavailable: tool({ + description: "Report that the Stack Auth documentation search tools are currently unavailable.", + inputSchema: z.object({}), + execute: async () => ({ + error: "Stack Auth documentation search is temporarily unavailable. Please try again later.", + }), + }), + }; + } } From 260c853c3870b71b721afc766f6e9f65736d49a3 Mon Sep 17 00:00:00 2001 From: mantrakp04 Date: Tue, 28 Apr 2026 15:37:20 -0700 Subject: [PATCH 3/3] Refactor analytics integration in MCP handler - Replace direct PostHog client instantiation with a higher-order function `withPostHog` for capturing events. - Update event capturing logic to ensure proper integration with the new analytics approach, enhancing maintainability and consistency across the codebase. --- .../src/app/api/internal/[transport]/route.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/app/api/internal/[transport]/route.ts b/apps/backend/src/app/api/internal/[transport]/route.ts index 966eaf9084..7b5880ba06 100644 --- a/apps/backend/src/app/api/internal/[transport]/route.ts +++ b/apps/backend/src/app/api/internal/[transport]/route.ts @@ -1,12 +1,8 @@ import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { createMcpHandler } from "@vercel/mcp-adapter"; -import { PostHog } from "posthog-node"; import { z } from "zod"; -const postHogKey = getEnvVariable("NEXT_PUBLIC_POSTHOG_KEY", ""); -const nodeClient = postHogKey !== "" - ? new PostHog(postHogKey) - : null; +import withPostHog from "@/analytics"; function getBackendApiBaseUrl(): string { return ( @@ -42,10 +38,12 @@ const handler = createMcpHandler( ), }, async ({ question, reason, userPrompt, conversationId }) => { - nodeClient?.capture({ - event: "ask_stack_auth_mcp", - properties: { question, reason }, - distinctId: "mcp-handler", + await withPostHog(async (posthog) => { + posthog.capture({ + event: "ask_stack_auth_mcp", + properties: { question, reason }, + distinctId: "mcp-handler", + }); }); const res = await fetch(`${getBackendApiBaseUrl()}/api/latest/ai/query/generate`, {