Move internal MCP server to backend, use Mintlify MCP for docs tools#1389
Move internal MCP server to backend, use Mintlify MCP for docs tools#1389mantrakp04 merged 3 commits intodevfrom
Conversation
- 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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughBackend replaces a local HTTP-based docs-tools bridge with a Mintlify MCP client (driven by STACK_MINTLIFY_MCP_URL and Changes
Sequence DiagramsequenceDiagram
autonumber
participant Agent as AI Agent / LLM
participant Backend as Backend API (/api/internal/mcp)
participant MCPClient as MCP Client (`@vercel/mcp-adapter`)
participant Mintlify as Mintlify MCP Server
Agent->>Backend: requests tools/list
Backend->>MCPClient: forward tools/list via STACK_MINTLIFY_MCP_URL
MCPClient->>Mintlify: HTTP POST JSON-RPC
Mintlify-->>MCPClient: tools list response
MCPClient-->>Backend: tool definitions
Backend-->>Agent: return tools list
Agent->>Backend: requests tools/call (ask_stack_auth ...)
Backend->>MCPClient: forward tools/call (execute tool)
MCPClient->>Mintlify: HTTP POST execute
Mintlify-->>MCPClient: execution result / error
MCPClient-->>Backend: result / error
Backend-->>Agent: return result / validation error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR moves the
Confidence Score: 4/5Safe to merge after addressing the broken-client resilience gap in the MCP singleton. One P1 finding: a successful initial connection followed by a client.tools() failure leaves the singleton unrecoverable, permanently degrading docs tooling until the process restarts. No P0 issues; the rest of the changes are clean. apps/backend/src/lib/ai/tools/docs.ts — singleton reset logic needs to cover tools() failures, not just initial connection failures. Important Files Changed
Sequence DiagramsequenceDiagram
participant LLMAgent as AI Agent (backend)
participant DocsTools as createDocsTools()
participant MintlifyMCP as Mintlify MCP Server
participant MCPRoute as /api/internal/[transport]
participant AIQuery as /api/latest/ai/query/generate
Note over LLMAgent,MintlifyMCP: Tool registration (module init)
LLMAgent->>DocsTools: createDocsTools()
DocsTools->>MintlifyMCP: createMCPClient() + client.tools()
MintlifyMCP-->>DocsTools: search/fetch tool definitions
DocsTools-->>LLMAgent: tools ready
Note over MCPRoute,AIQuery: External MCP caller
MCPRoute->>AIQuery: POST /api/latest/ai/query/generate
AIQuery->>LLMAgent: invoke AI agent with docs tools
LLMAgent->>MintlifyMCP: search / fetch calls
MintlifyMCP-->>LLMAgent: doc results
LLMAgent-->>MCPRoute: ask_stack_auth response
Prompt To Fix All With AIThis is a comment left during a code review.
Path: apps/backend/src/lib/ai/tools/docs.ts
Line: 13-27
Comment:
**Broken client not reset — no recovery after `tools()` failure**
The `.catch` reset only fires when `createMCPClient` itself rejects (initial connection failure). If the client connects successfully but `client.tools()` later throws (e.g. Mintlify drops the connection after the handshake), `mintlifyMcpClientPromise` remains pointing at the resolved-but-broken client. Every subsequent call to `getMintlifyMcpClient()` returns that dead client, `tools()` keeps failing, and the agent is permanently stuck with the `docsUnavailable` fallback until the process restarts — even if Mintlify comes back online. The old `postDocsToolAction` made a fresh stateless HTTP call each time and recovered automatically.
Consider resetting the singleton on any failure path, not just initial creation:
```typescript
export async function createDocsTools() {
try {
const client = await getMintlifyMcpClient();
return await client.tools();
} catch (error) {
mintlifyMcpClientPromise = null; // allow reconnection on next call
captureError("mintlify-mcp-docs-tools", error);
return {
docsUnavailable: tool({ ... }),
};
}
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "Enhance error handling in createDocsTool..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
Moves the internal MCP endpoint from the docs app into the backend and updates the backend AI docs tool bundle to use Mintlify’s generated MCP server directly.
Changes:
- Relocates the
/api/internal/[transport]MCP handler toapps/backend, proxyingask_stack_authto the backend AI query API. - Replaces the backend’s bespoke docs-tools HTTP client with an
@ai-sdk/mcpclient targeting Mintlify’s MCP server (STACK_MINTLIFY_MCP_URL). - Adds e2e coverage for
tools/listandtools/callvalidation on the new backend MCP endpoint.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Moves @vercel/mcp-adapter resolution from docs to backend and updates lock snapshots accordingly. |
| docs/package.json | Removes @vercel/mcp-adapter dependency from the docs app. |
| apps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.ts | Adds e2e tests for the backend MCP endpoint’s tool listing and argument validation. |
| apps/backend/src/lib/ai/tools/docs.ts | Switches docs tools to Mintlify MCP via @ai-sdk/mcp and new env var STACK_MINTLIFY_MCP_URL. |
| apps/backend/src/app/api/internal/[transport]/route.ts | Adds backend MCP handler for ask_stack_auth and proxies calls to /api/latest/ai/query/generate. |
| apps/backend/package.json | Adds @vercel/mcp-adapter dependency to the backend app. |
| apps/backend/.env.development | Updates dev env to use STACK_MINTLIFY_MCP_URL. |
| apps/backend/.env | Replaces STACK_DOCS_INTERNAL_BASE_URL with STACK_MINTLIFY_MCP_URL documentation. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
apps/backend/src/app/api/internal/[transport]/route.ts:9
- This route constructs a
PostHogclient without setting thehost(elsewhere the backend useshttps://eu.i.posthog.cominapps/backend/src/analytics.tsx) and without flushing/shutting down. In serverless/short-lived executions this can silently drop events or send them to the wrong region. Recommend reusing the existingwithPostHoghelper (or at least configuringhost,flushAt,flushInterval, and callingshutdown()/flush()appropriately).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/backend/src/app/api/internal/[transport]/route.ts (1)
64-70:⚠️ Potential issue | 🟡 MinorAvoid forwarding raw upstream error bodies to MCP clients.
errTexthere is the verbatim response body from/api/latest/ai/query/generate, which may include internal error details (stack/cause text) that you don't want exposed to arbitrary MCP callers. Consider returning a sanitized message and logging the full body server-side.🛡️ Suggested change
if (!res.ok) { - const errText = await res.text(); + const errText = await res.text(); + console.error("ask_stack_auth upstream error", { status: res.status, body: errText }); return { - content: [{ type: "text", text: `Stack Auth AI error (${res.status}): ${errText}` }], + content: [{ type: "text", text: `Stack Auth AI error (${res.status}). Please retry or contact support.` }], isError: true, }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/internal/`[transport]/route.ts around lines 64 - 70, The handler in route.ts currently returns the raw upstream error body (errText) to MCP clients when res.ok is false; change this to log the full errText server-side (using processLogger or similar) and return a sanitized error content to the client such as "Upstream service error" including the HTTP status, ensuring you do not forward stack traces or internal details; update the block around res and the returned object (the code that builds content with errText and isError) to use the sanitized message while keeping full errText written to server logs for debugging.
🧹 Nitpick comments (3)
apps/backend/src/app/api/internal/[transport]/route.ts (2)
95-98:serverInfo.versionis hardcoded.
0.1.0will drift from the real backend version over time. Consider sourcing it frompackage.json(or a build-injected constant) so MCP clients can reason about server identity. Optional nit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/internal/`[transport]/route.ts around lines 95 - 98, Replace the hardcoded serverInfo.version value in route.ts with a real source-of-truth (e.g. import the version from package.json or use a build-injected constant/env var) so the server reports the actual backend version; update the assignment to serverInfo.version (in the object where serverInfo.name is set) to read from that imported value or process/env variable and ensure the import/constant is exported or injected at build time for runtime use.
51-51: UseurlStringtemplate tag for URL construction.Per repo conventions, URLs should be built with the
urlStringtag (orencodeURIComponent) rather than plain template interpolation, even when the path segments are constant.♻️ Suggested change
- const res = await fetch(`${getBackendApiBaseUrl()}/api/latest/ai/query/generate`, { + const res = await fetch(urlString`${getBackendApiBaseUrl()}/api/latest/ai/query/generate`, {As per coding guidelines: "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs, for consistency even if it's not strictly necessary".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/internal/`[transport]/route.ts at line 51, Replace the plain template-interpolated URL passed to fetch with the repo-convention tagged template urlString to build the URL safely; locate the fetch call that currently uses `${getBackendApiBaseUrl()}/api/latest/ai/query/generate` inside route.ts and change it to use urlString`...` (or wrap path segments with encodeURIComponent if urlString is unavailable), keeping getBackendApiBaseUrl() as the base and preserving the fetch options.apps/backend/src/lib/ai/tools/docs.ts (1)
10-25: Consider adding runtime error handling and explicit client lifecycle management.The
.catchhere only invalidates the cache on initial connection failure fromcreateMCPClient(). Once the promise resolves successfully, any subsequent transport failure (e.g., MCP server restart or network disruption) will not clear the cached client, causing the agent to keep reusing a broken connection. Add anonerrorcallback to the transport to detect and handle runtime errors:createMCPClient({ transport: { type: "http", url: getMintlifyMcpUrl(), onerror: (error) => { console.error('MCP transport error:', error); mintlifyMcpClientPromise = null; // invalidate cache on transport failure }, }, name: "stack-auth-backend-docs-agent", })Per the AI SDK MCP docs, also ensure the client is closed when no longer needed. For a long-lived backend, wire this into your graceful shutdown handler (e.g., on SIGTERM):
await mintlifyMcpClientPromise?.then(client => client.close());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/lib/ai/tools/docs.ts` around lines 10 - 25, The getMintlifyMcpClient function currently only clears mintlifyMcpClientPromise on initial createMCPClient() rejection but does not handle runtime transport errors or client shutdown; update the createMCPClient call inside getMintlifyMcpClient to add a transport.onerror handler that logs the error and sets mintlifyMcpClientPromise = null to invalidate the cached client on runtime failures, and ensure the created client is closed during process shutdown by wiring await mintlifyMcpClientPromise?.then(client => client.close()) (or equivalent) into your graceful shutdown/SIGTERM handler so the MCPClient instance is properly terminated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/.env.development`:
- Line 81: Remove the hardcoded STACK_MINTLIFY_MCP_URL from .env.development
(it's redundant with the default in docs.ts/getMintlifyMcpUrl()) or set it to an
empty value for isolated dev; then update the MCP client initialization (the
code that reads STACK_MINTLIFY_MCP_URL and/or calls getMintlifyMcpUrl()) to
treat an empty/undefined URL as "disabled" and skip making outbound calls (or
return a mock/no-op client) so local tests/dev don't hit the live Mintlify MCP
endpoint.
---
Outside diff comments:
In `@apps/backend/src/app/api/internal/`[transport]/route.ts:
- Around line 64-70: The handler in route.ts currently returns the raw upstream
error body (errText) to MCP clients when res.ok is false; change this to log the
full errText server-side (using processLogger or similar) and return a sanitized
error content to the client such as "Upstream service error" including the HTTP
status, ensuring you do not forward stack traces or internal details; update the
block around res and the returned object (the code that builds content with
errText and isError) to use the sanitized message while keeping full errText
written to server logs for debugging.
---
Nitpick comments:
In `@apps/backend/src/app/api/internal/`[transport]/route.ts:
- Around line 95-98: Replace the hardcoded serverInfo.version value in route.ts
with a real source-of-truth (e.g. import the version from package.json or use a
build-injected constant/env var) so the server reports the actual backend
version; update the assignment to serverInfo.version (in the object where
serverInfo.name is set) to read from that imported value or process/env variable
and ensure the import/constant is exported or injected at build time for runtime
use.
- Line 51: Replace the plain template-interpolated URL passed to fetch with the
repo-convention tagged template urlString to build the URL safely; locate the
fetch call that currently uses
`${getBackendApiBaseUrl()}/api/latest/ai/query/generate` inside route.ts and
change it to use urlString`...` (or wrap path segments with encodeURIComponent
if urlString is unavailable), keeping getBackendApiBaseUrl() as the base and
preserving the fetch options.
In `@apps/backend/src/lib/ai/tools/docs.ts`:
- Around line 10-25: The getMintlifyMcpClient function currently only clears
mintlifyMcpClientPromise on initial createMCPClient() rejection but does not
handle runtime transport errors or client shutdown; update the createMCPClient
call inside getMintlifyMcpClient to add a transport.onerror handler that logs
the error and sets mintlifyMcpClientPromise = null to invalidate the cached
client on runtime failures, and ensure the created client is closed during
process shutdown by wiring await mintlifyMcpClientPromise?.then(client =>
client.close()) (or equivalent) into your graceful shutdown/SIGTERM handler so
the MCPClient instance is properly terminated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d249fd54-41df-412b-b5a0-e8c359287e24
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
apps/backend/.envapps/backend/.env.developmentapps/backend/package.jsonapps/backend/src/app/api/internal/[transport]/route.tsapps/backend/src/lib/ai/tools/docs.tsapps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.tsdocs/package.json
💤 Files with no reviewable changes (1)
- docs/package.json
- 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.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/src/lib/ai/tools/docs.ts`:
- Around line 13-50: The cached MCP client (mintlifyMcpClientPromise) is never
cleared if a later client.tools() call fails, causing permanent use of a broken
client; update createDocsTools to catch failures from client.tools(), set
mintlifyMcpClientPromise = null before rethrowing or returning the fallback so
the next call re-initializes, i.e., wrap the await client.tools() call in
try/catch, on error clear mintlifyMcpClientPromise, call
captureError("mintlify-mcp-docs-tools", error) and then return the
docsUnavailable tool fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ca967c0e-0632-44e7-8b6c-98146eb786e0
📒 Files selected for processing (1)
apps/backend/src/lib/ai/tools/docs.ts
- 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/backend/src/app/api/internal/[transport]/route.ts (1)
49-49: Use the repository URL helper style for endpoint construction.Line 49 uses normal string interpolation to build a URL. Please switch to `urlString`` (or encode components) for consistency with project conventions.
As per coding guidelines: "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs, for consistency even if it's not strictly necessary".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/internal/`[transport]/route.ts at line 49, Replace the string-interpolated fetch URL in the fetch call that assigns to res with the repository's urlString`` helper (or encode dynamic parts with encodeURIComponent) to follow project conventions; locate the fetch(...) invocation that uses getBackendApiBaseUrl() in route.ts (the res = await fetch(...) line) and reconstruct the request URL using urlString`` (or encode path/query components) so the base URL and "/api/latest/ai/query/generate" are combined via the template tag instead of normal string interpolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/src/app/api/internal/`[transport]/route.ts:
- Line 87: The response always appends a continuation hint even when
responseConversationId is empty; update the code that builds the content (the
array entry using responseConversationId in route.ts) to only append the
"[conversationId: ...]" instruction when responseConversationId is
present/truthy (e.g., conditionally build the text or push a second content
item). Locate the string construction that uses responseConversationId and wrap
the continuation hint so it is omitted when responseConversationId is falsy,
ensuring you still include the "(empty response)" fallback for the main text.
- Around line 41-45: The posthog.capture call inside withPostHog sending event
"ask_stack_auth_mcp" currently includes the raw question text; change it so you
do not send PII/secrets by removing or redacting the question field in
properties for the capture. Instead populate properties with intent-only
metadata such as reason, question_length (e.g., question?.length), a redacted
boolean flag, and any derived non-sensitive flags (e.g., contains_code,
contains_url) before calling posthog.capture in the withPostHog callback; update
the properties object construction where posthog.capture is invoked so it no
longer contains the raw question string.
---
Nitpick comments:
In `@apps/backend/src/app/api/internal/`[transport]/route.ts:
- Line 49: Replace the string-interpolated fetch URL in the fetch call that
assigns to res with the repository's urlString`` helper (or encode dynamic parts
with encodeURIComponent) to follow project conventions; locate the fetch(...)
invocation that uses getBackendApiBaseUrl() in route.ts (the res = await
fetch(...) line) and reconstruct the request URL using urlString`` (or encode
path/query components) so the base URL and "/api/latest/ai/query/generate" are
combined via the template tag instead of normal string interpolation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f9cbc92c-6b69-47d8-81a9-10452df21415
📒 Files selected for processing (1)
apps/backend/src/app/api/internal/[transport]/route.ts
Summary
/api/internal/[transport]MCP route from the docs app to the backend, so the publicask_stack_authMCP tool is served from the same origin as the AI query API it proxies to.apps/backend/src/lib/ai/tools/docs.tswith an@ai-sdk/mcpclient that talks to Mintlify's generated MCP server. The backend AI agent now consumes Mintlify's lower-level search/fetch tools directly instead of going through the docs app.STACK_DOCS_INTERNAL_BASE_URLforSTACK_MINTLIFY_MCP_URL(defaults to the Mintlify-hosted MCP URL).@vercel/mcp-adapterdependency fromdocstoapps/backend.Test plan
pnpm typecheckpnpm lintapps/e2e/tests/backend/endpoints/api/v1/internal/mcp.test.tscoverstools/listand validation ontools/callPOST /api/internal/mcpon the backend and confirmask_stack_authis listed and callableSummary by CodeRabbit
New Features
Chores
Tests