|
1 | | -import { createMCPClient } from "@ai-sdk/mcp"; |
2 | | -import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; |
| 1 | +import { tool } from "ai"; |
| 2 | +import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; |
| 3 | +import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; |
| 4 | +import { z } from "zod"; |
| 5 | + |
| 6 | +type DocsToolHttpResult = { |
| 7 | + content?: Array<{ type: string, text?: string }>, |
| 8 | + isError?: boolean, |
| 9 | +}; |
| 10 | + |
| 11 | +function getDocsToolsBaseUrl(): string { |
| 12 | + const fromEnv = getEnvVariable("STACK_DOCS_INTERNAL_BASE_URL", ""); |
| 13 | + if (fromEnv !== "") { |
| 14 | + return fromEnv.replace(/\/$/, ""); |
| 15 | + } |
| 16 | + if (getNodeEnvironment() === "development") { |
| 17 | + const portPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); |
| 18 | + return `http://localhost:${portPrefix}04`; |
| 19 | + } |
| 20 | + return "https://mcp.stack-auth.com"; |
| 21 | +} |
| 22 | + |
| 23 | +async function postDocsToolAction(action: Record<string, unknown>): Promise<string> { |
| 24 | + const base = getDocsToolsBaseUrl(); |
| 25 | + |
| 26 | + const res = await fetch(`${base}/api/internal/docs-tools`, { |
| 27 | + method: "POST", |
| 28 | + headers: { "Content-Type": "application/json" }, |
| 29 | + body: JSON.stringify(action), |
| 30 | + }); |
| 31 | + |
| 32 | + if (!res.ok) { |
| 33 | + const errBody = await res.text(); |
| 34 | + captureError("docs-tools-http-error", new Error(`Stack Auth docs tools error (${res.status}): ${errBody}`)); |
| 35 | + return `Stack Auth docs tools error (${res.status}): ${errBody}`; |
| 36 | + } |
| 37 | + |
| 38 | + const data = (await res.json()) as DocsToolHttpResult; |
| 39 | + const text = data.content |
| 40 | + ?.filter((c): c is { type: "text", text: string } => c.type === "text" && typeof c.text === "string") |
| 41 | + .map((c) => c.text) |
| 42 | + .join("\n") ?? ""; |
| 43 | + |
| 44 | + if (data.isError === true) { |
| 45 | + return text || "Unknown docs tool error"; |
| 46 | + } |
| 47 | + |
| 48 | + return text; |
| 49 | +} |
3 | 50 |
|
4 | 51 | /** |
5 | | - * Creates an MCP client connected to the Stack Auth documentation server. |
| 52 | + * Documentation tools backed by the docs app's `/api/internal/docs-tools` endpoint. |
6 | 53 | * |
7 | | - * In development: connects to local docs server at http://localhost:8126 |
8 | | - * In production: connects to production docs server at https://mcp.stack-auth.com |
| 54 | + * The public MCP server at the same docs origin exposes only `ask_stack_auth`, which proxies to |
| 55 | + * `/api/latest/ai/query/generate`; these tools avoid MCP recursion by calling the HTTP API directly. |
9 | 56 | */ |
10 | 57 | export async function createDocsTools() { |
11 | | - const mcpUrl = |
12 | | - getNodeEnvironment() === "development" |
13 | | - ? new URL("/api/internal/mcp", "http://localhost:8126") |
14 | | - : new URL("/api/internal/mcp", "https://mcp.stack-auth.com"); |
| 58 | + return { |
| 59 | + list_available_docs: tool({ |
| 60 | + description: |
| 61 | + "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.", |
| 62 | + inputSchema: z.object({}), |
| 63 | + execute: async () => { |
| 64 | + return await postDocsToolAction({ action: "list_available_docs" }); |
| 65 | + }, |
| 66 | + }), |
15 | 67 |
|
16 | | - const stackAuthMcp = await createMCPClient({ |
17 | | - transport: { type: "http", url: mcpUrl.toString() }, |
18 | | - }); |
| 68 | + search_docs: tool({ |
| 69 | + description: |
| 70 | + "Search through all Stack Auth documentation including API docs, guides, and examples. Returns ranked results with snippets and relevance scores.", |
| 71 | + inputSchema: z.object({ |
| 72 | + search_query: z.string().describe("The search query to find relevant documentation"), |
| 73 | + result_limit: z.number().optional().describe("Maximum number of results to return (default: 50)"), |
| 74 | + }), |
| 75 | + execute: async ({ search_query, result_limit = 50 }) => { |
| 76 | + return await postDocsToolAction({ |
| 77 | + action: "search_docs", |
| 78 | + search_query, |
| 79 | + result_limit, |
| 80 | + }); |
| 81 | + }, |
| 82 | + }), |
| 83 | + |
| 84 | + get_docs_by_id: tool({ |
| 85 | + description: |
| 86 | + "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.", |
| 87 | + inputSchema: z.object({ |
| 88 | + id: z.string(), |
| 89 | + }), |
| 90 | + execute: async ({ id }) => { |
| 91 | + return await postDocsToolAction({ action: "get_docs_by_id", id }); |
| 92 | + }, |
| 93 | + }), |
| 94 | + |
| 95 | + get_stack_auth_setup_instructions: tool({ |
| 96 | + description: |
| 97 | + "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.", |
| 98 | + inputSchema: z.object({}), |
| 99 | + execute: async () => { |
| 100 | + return await postDocsToolAction({ action: "get_stack_auth_setup_instructions" }); |
| 101 | + }, |
| 102 | + }), |
| 103 | + |
| 104 | + search: tool({ |
| 105 | + description: |
| 106 | + "Search for Stack Auth documentation pages.\n\nUse this tool to find documentation pages that contain a specific keyword or phrase.", |
| 107 | + inputSchema: z.object({ |
| 108 | + query: z.string(), |
| 109 | + }), |
| 110 | + execute: async ({ query }) => { |
| 111 | + return await postDocsToolAction({ action: "search", query }); |
| 112 | + }, |
| 113 | + }), |
19 | 114 |
|
20 | | - return await stackAuthMcp.tools(); |
| 115 | + fetch: tool({ |
| 116 | + description: |
| 117 | + "Fetch a particular Stack Auth Documentation page by its ID.\n\nThis tool is identical to `get_docs_by_id`.", |
| 118 | + inputSchema: z.object({ |
| 119 | + id: z.string(), |
| 120 | + }), |
| 121 | + execute: async ({ id }) => { |
| 122 | + return await postDocsToolAction({ action: "fetch", id }); |
| 123 | + }, |
| 124 | + }), |
| 125 | + }; |
21 | 126 | } |
0 commit comments