Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
mantrakp04 marked this conversation as resolved.
# Email monitor configuration for tests
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:8101/handler/email-verification
STACK_EMAIL_MONITOR_PROJECT_ID=internal
Expand Down
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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)
: null;
import withPostHog from "@/analytics";

function getBackendApiBaseUrl(): string {
return (
getEnvVariable("NEXT_PUBLIC_SERVER_STACK_API_URL", "") ||
getEnvVariable("NEXT_PUBLIC_STACK_API_URL")
).replace(/\/$/, "");
}
Comment thread
mantrakp04 marked this conversation as resolved.

const handler = createMcpHandler(
async (server) => {
Expand All @@ -29,26 +34,19 @@ 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 }) => {
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",
Comment thread
mantrakp04 marked this conversation as resolved.
});
});

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({
Expand Down Expand Up @@ -86,44 +84,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]` }],
Comment thread
mantrakp04 marked this conversation as resolved.
};
},
);
},
{
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",
},
},
{
Expand Down
151 changes: 34 additions & 117 deletions apps/backend/src/lib/ai/tools/docs.ts
Original file line number Diff line number Diff line change
@@ -1,134 +1,51 @@
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
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";

type DocsToolHttpResult = {
content?: Array<{ type: string, text?: string }>,
isError?: boolean,
};
let mintlifyMcpClientPromise: Promise<MCPClient> | 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<string, unknown>): Promise<string> {
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<MCPClient> {
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;
Comment thread
mantrakp04 marked this conversation as resolved.
}

/**
* 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(),
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.",
}),
}),
execute: async ({ id }) => {
return await postDocsToolAction({ action: "fetch", id });
},
}),
};
};
}
Comment thread
mantrakp04 marked this conversation as resolved.
}
Comment thread
mantrakp04 marked this conversation as resolved.
Loading
Loading