diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 5222b10b..dadacc8c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -49,14 +49,38 @@ jobs:
- name: Install dependencies
run: pnpm install
- # === BUILD AND DEPLOY CANARY WORKER ===
- - name: Build
+ # === DEPLOY DOCS (CANARY) FIRST ===
+ - name: Deploy Docs Canary Worker
+ id: deploy_docs_canary
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ workingDirectory: packages/docs
+ command: deploy --config wrangler.canary.jsonc
+ packageManager: pnpm
+
+ - name: Smoke Test Docs Canary
+ id: smoke_docs_canary
+ if: success()
+ run: |
+ set -euo pipefail
+ URL="https://sentry-mcp-docs-canary.getsentry.workers.dev/docs"
+ echo "Testing $URL"
+ STATUS=$(curl -s -o /tmp/docs_canary.html -w "%{http_code}" "$URL")
+ if [ "$STATUS" -ne 200 ]; then
+ echo "Unexpected status: $STATUS"; cat /tmp/docs_canary.html; exit 1; fi
+ if ! grep -q "
Documentation" /tmp/docs_canary.html; then
+ echo "Title check failed"; cat /tmp/docs_canary.html; exit 1; fi
+
+ # === BUILD AND DEPLOY APP (CANARY) ===
+ - name: Build App (Canary)
working-directory: packages/mcp-cloudflare
run: pnpm build
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- - name: Deploy to Canary Worker
+ - name: Deploy App Canary Worker
id: deploy_canary
uses: cloudflare/wrangler-action@v3
with:
@@ -91,11 +115,23 @@ jobs:
check_name: "Canary Smoke Test Results"
fail_on_failure: false
- # === DEPLOY PRODUCTION WORKER (only if canary tests pass) ===
- - name: Deploy to Production Worker
- id: deploy_production
+ # === DEPLOY DOCS PRODUCTION (only if canary tests pass) ===
+ - name: Deploy Docs Production Worker
+ id: deploy_docs_production
if: steps.canary_smoke_tests.outcome == 'success'
uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ workingDirectory: packages/docs
+ command: deploy
+ packageManager: pnpm
+
+ # === DEPLOY APP PRODUCTION (only if canary tests pass) ===
+ - name: Deploy App Production Worker
+ id: deploy_production
+ if: steps.canary_smoke_tests.outcome == 'success' && steps.smoke_docs_canary.outcome == 'success'
+ uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -128,9 +164,22 @@ jobs:
check_name: "Production Smoke Test Results"
fail_on_failure: false
+ - name: Smoke Test Docs Production
+ id: smoke_docs_production
+ if: steps.deploy_docs_production.outcome == 'success'
+ run: |
+ set -euo pipefail
+ URL="https://sentry-mcp-docs.getsentry.workers.dev/docs"
+ echo "Testing $URL"
+ STATUS=$(curl -s -o /tmp/docs_prod.html -w "%{http_code}" "$URL")
+ if [ "$STATUS" -ne 200 ]; then
+ echo "Unexpected status: $STATUS"; cat /tmp/docs_prod.html; exit 1; fi
+ if ! grep -q "Documentation" /tmp/docs_prod.html; then
+ echo "Title check failed"; cat /tmp/docs_prod.html; exit 1; fi
+
# === ROLLBACK IF PRODUCTION SMOKE TESTS FAIL ===
- - name: Rollback Production on Smoke Test Failure
- if: steps.production_smoke_tests.outcome == 'failure'
+ - name: Rollback App Production on Smoke Test Failure
+ if: steps.production_smoke_tests.outcome == 'failure' || steps.smoke_docs_production.outcome == 'failure'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -140,8 +189,19 @@ jobs:
packageManager: pnpm
continue-on-error: true
+ - name: Rollback Docs Production on Smoke Test Failure
+ if: steps.production_smoke_tests.outcome == 'failure' || steps.smoke_docs_production.outcome == 'failure'
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ workingDirectory: packages/docs
+ command: rollback
+ packageManager: pnpm
+ continue-on-error: true
+
- name: Fail Job if Production Smoke Tests Failed
- if: steps.production_smoke_tests.outcome == 'failure'
+ if: steps.production_smoke_tests.outcome == 'failure' || steps.smoke_docs_production.outcome == 'failure'
run: |
echo "Production smoke tests failed - job failed after rollback"
exit 1
diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml
index fce6062c..441090b8 100644
--- a/.github/workflows/smoke-tests.yml
+++ b/.github/workflows/smoke-tests.yml
@@ -47,14 +47,34 @@ jobs:
- name: Build
run: pnpm build
- - name: Start local dev server
- working-directory: packages/mcp-cloudflare
+ - name: Start local dev servers (app + docs)
+ working-directory: .
run: |
- # Start wrangler in background and capture output
+ # Start docs worker first (port 8790)
+ cd packages/docs
+ pnpm exec wrangler dev --port 8790 --local > wrangler-docs.log 2>&1 &
+ DOCS_PID=$!
+ echo "DOCS_PID=$DOCS_PID" >> $GITHUB_ENV
+ echo "Waiting for docs server to start (PID: $DOCS_PID)..."
+
+ # Wait for docs to be ready (up to 2 minutes)
+ MAX_ATTEMPTS=24
+ ATTEMPT=0
+ while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
+ if ! kill -0 $DOCS_PID 2>/dev/null; then
+ echo "❌ Docs wrangler died unexpectedly!"; tail -50 wrangler-docs.log; exit 1; fi
+ if curl -s -f -o /dev/null http://localhost:8790/docs; then
+ echo "✅ Docs server is ready!"; cat wrangler-docs.log; break; fi
+ ATTEMPT=$((ATTEMPT+1)); sleep 5
+ done
+ if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then echo "❌ Docs failed to start"; cat wrangler-docs.log; exit 1; fi
+
+ # Start app worker (port 8788) in its package
+ cd ../mcp-cloudflare
pnpm exec wrangler dev --port 8788 --local > wrangler.log 2>&1 &
WRANGLER_PID=$!
echo "WRANGLER_PID=$WRANGLER_PID" >> $GITHUB_ENV
- echo "Waiting for server to start (PID: $WRANGLER_PID)..."
+ echo "Waiting for app server to start (PID: $WRANGLER_PID)..."
# Wait for server to be ready (up to 2 minutes)
MAX_ATTEMPTS=24
@@ -131,9 +151,12 @@ jobs:
check_name: "Local Smoke Test Results"
fail_on_failure: true
- - name: Stop local server
+ - name: Stop local servers
if: always()
run: |
if [ ! -z "$WRANGLER_PID" ]; then
kill $WRANGLER_PID || true
- fi
\ No newline at end of file
+ fi
+ if [ ! -z "$DOCS_PID" ]; then
+ kill $DOCS_PID || true
+ fi
diff --git a/docs/cloudflare/deployment.md b/docs/cloudflare/deployment.md
index b0c2f7d9..59790ea3 100644
--- a/docs/cloudflare/deployment.md
+++ b/docs/cloudflare/deployment.md
@@ -299,4 +299,32 @@ Monitor via Cloudflare dashboard:
- Worker code: `packages/mcp-cloudflare/src/server/`
- Client UI: `packages/mcp-cloudflare/src/client/`
- Wrangler config: `packages/mcp-cloudflare/wrangler.jsonc`
-- Cloudflare docs: https://developers.cloudflare.com/workers/
\ No newline at end of file
+- Cloudflare docs: https://developers.cloudflare.com/workers/
+
+---
+
+## Multi-Worker Deployment (App + Docs)
+
+We deploy two Workers: the App (mcp-cloudflare) and Docs (docs). The App proxies `/docs` to the Docs service binding.
+
+Key configs
+- App (wrangler.jsonc):
+ - `services: [{ binding: "DOCS", service: "sentry-mcp-docs" }]`
+- App Canary (wrangler.canary.jsonc):
+ - `services: [{ binding: "DOCS", service: "sentry-mcp-docs-canary" }]`
+- Docs: `packages/docs/wrangler.jsonc` and `wrangler.canary.jsonc` (names: `sentry-mcp-docs`, `sentry-mcp-docs-canary`).
+
+Deploy order (GitHub Actions)
+- Deploy Docs Canary → curl `https://sentry-mcp-docs-canary.getsentry.workers.dev/docs` and assert `Documentation`.
+- Build + Deploy App Canary (bound to docs-canary) → smoke tests via app.
+- If canary smoke passes: deploy Docs Production, then App Production.
+- Smoke-test app (existing suite) and docs directly at `https://sentry-mcp-docs.getsentry.workers.dev/docs`.
+- On failure: rollback both app and docs.
+
+Local CI smoke (smoke-tests.yml)
+- Starts Docs worker (8790) first, then App (8788), so `/docs` binding is live.
+
+Notes
+- Keep two Vite servers in dev (5173 app, 5174 docs) to mirror prod.
+- Root `pnpm run dev` uses Turbo to start app dev and co-run server/docs dev.
+- Build uses Turbo for ordering/caching; unchanged packages aren’t rebuilt.
diff --git a/docs/cloudflare/overview.md b/docs/cloudflare/overview.md
index f10ffacd..3fbc7e53 100644
--- a/docs/cloudflare/overview.md
+++ b/docs/cloudflare/overview.md
@@ -46,3 +46,45 @@ Think of it as:
- Live deployment: https://mcp.sentry.dev
- Package location: `packages/mcp-cloudflare`
- **For MCP Server docs**: See "Architecture" in @docs/architecture.mdc
+
+## Local Dev Topology
+
+Two Vite servers run in development to mirror production:
+
+- mcp-cloudflare (primary)
+ - Worker: http://localhost:8788
+ - Vite HMR: http://localhost:5173
+- docs worker (service: `sentry-mcp-docs`)
+ - Worker: http://localhost:8790
+ - Vite HMR: http://localhost:5174
+
+Service bindings are defined in `packages/mcp-cloudflare/wrangler.jsonc`:
+
+- `services: [{ binding: "DOCS", service: "sentry-mcp-docs" }]`
+- `dev.services: [{ binding: "DOCS", local_port: 8790 }]`
+
+The default export in `packages/mcp-cloudflare/src/server/index.ts` forwards `/docs` to the `DOCS` binding before falling through to the app/OAuth handler.
+
+## Dev Commands (Root)
+
+- `pnpm run dev`
+ - Uses Turbo to start `@sentry/mcp-cloudflare#dev` and co-run `@sentry/mcp-server#dev` and `@sentry/mcp-docs#dev` via a package-level Turbo config.
+ - Do not start auxiliary workers manually; the two Vite servers are intentional.
+
+## Build Commands (Root)
+
+- `pnpm run build`
+ - Turbo builds upstream packages first (`^build`), then `mcp-cloudflare`. Unchanged packages are skipped via caching.
+
+## Troubleshooting
+
+- If Vite briefly fails to resolve `@sentry/mcp-server/*` during dev, it’s usually because the server package is mid-build. Restart dev after the initial build completes.
+- Ensure `@sentry/mcp-server` is a runtime dependency of `@sentry/mcp-cloudflare` (declared under `dependencies`).
+- For per-package workflows, prefer root `dev`/`build`; Turbo handles ordering and caching.
+
+## Smoke Test
+
+Run smoke tests against a local or preview URL to verify `/docs` routing:
+
+- `PREVIEW_URL=http://localhost:8788 pnpm --filter @sentry/mcp-smoke-tests test`
+- Expects status `200` and `Documentation` in the `/docs` response.
diff --git a/package.json b/package.json
index b922707f..edfd00d8 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
},
"scripts": {
"docs:check": "node scripts/check-doc-links.mjs",
- "dev": "dotenv -e .env -e .env.local -- turbo dev",
+ "dev": "dotenv -e .env -e .env.local -- turbo run dev --filter @sentry/mcp-cloudflare",
"build": "turbo build after-build",
"deploy": "turbo deploy",
"eval": "dotenv -e .env -e .env.local -- turbo eval",
diff --git a/packages/docs/client/index.html b/packages/docs/client/index.html
new file mode 100644
index 00000000..4470ebf8
--- /dev/null
+++ b/packages/docs/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Sentry MCP Docs
+
+
+
+
+
+
+
diff --git a/packages/docs/client/main.tsx b/packages/docs/client/main.tsx
new file mode 100644
index 00000000..01b8f161
--- /dev/null
+++ b/packages/docs/client/main.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import App from "./src/App";
+import "./src/index.css";
+
+const router = createBrowserRouter([
+ {
+ path: "/docs/*",
+ element: ,
+ },
+]);
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+ ,
+);
diff --git a/packages/docs/client/src/components/ui/button.tsx b/packages/docs/client/src/components/ui/button.tsx
new file mode 100644
index 00000000..372e3435
--- /dev/null
+++ b/packages/docs/client/src/components/ui/button.tsx
@@ -0,0 +1,51 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "../../lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none cursor-pointer",
+ {
+ variants: {
+ variant: {
+ default: "bg-violet-300 text-black shadow hover:bg-violet-300/90",
+ outline:
+ "bg-slate-800/50 border border-slate-600/50 hover:bg-slate-700/50 hover:text-white",
+ secondary:
+ "bg-transparent border border-slate-600/60 hover:bg-slate-800/50",
+ link: "text-violet-300 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
+ xs: "h-7 gap-1.5 px-2 has-[>svg]:px-1.5",
+ lg: "h-10 px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ active: { true: "text-violet-300 underline" },
+ },
+ defaultVariants: { variant: "default", size: "default" },
+ },
+);
+
+export function Button({
+ className,
+ variant,
+ size,
+ active = false,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ active?: boolean;
+ }) {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+}
+
+export { buttonVariants };
diff --git a/packages/docs/client/src/components/ui/header.tsx b/packages/docs/client/src/components/ui/header.tsx
new file mode 100644
index 00000000..c1af2b9f
--- /dev/null
+++ b/packages/docs/client/src/components/ui/header.tsx
@@ -0,0 +1,26 @@
+import { SentryIcon } from "./icons/sentry";
+import { Button } from "./button";
+
+export function Header() {
+ return (
+
+ );
+}
diff --git a/packages/docs/client/src/components/ui/icon.tsx b/packages/docs/client/src/components/ui/icon.tsx
new file mode 100644
index 00000000..664bd381
--- /dev/null
+++ b/packages/docs/client/src/components/ui/icon.tsx
@@ -0,0 +1,26 @@
+interface IconProps {
+ className?: string;
+ path: string;
+ viewBox?: string;
+ title?: string;
+}
+
+export function Icon({
+ className,
+ path,
+ viewBox = "0 0 32 32",
+ title = "Icon",
+}: IconProps) {
+ return (
+
+ {title}
+
+
+ );
+}
diff --git a/packages/docs/client/src/components/ui/icons/sentry.tsx b/packages/docs/client/src/components/ui/icons/sentry.tsx
new file mode 100644
index 00000000..c4f37b41
--- /dev/null
+++ b/packages/docs/client/src/components/ui/icons/sentry.tsx
@@ -0,0 +1,11 @@
+import { Icon } from "../icon";
+
+export function SentryIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/packages/docs/client/src/components/ui/prose.tsx b/packages/docs/client/src/components/ui/prose.tsx
new file mode 100644
index 00000000..df34fc0d
--- /dev/null
+++ b/packages/docs/client/src/components/ui/prose.tsx
@@ -0,0 +1,19 @@
+import { cn } from "../../src/lib/utils";
+
+export function Prose({
+ children,
+ className,
+ ...props
+}: { children: React.ReactNode } & React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/docs/client/src/index.css b/packages/docs/client/src/index.css
new file mode 100644
index 00000000..8d5b5d0c
--- /dev/null
+++ b/packages/docs/client/src/index.css
@@ -0,0 +1,7 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ color-scheme: dark;
+}
diff --git a/packages/docs/client/src/lib/utils.ts b/packages/docs/client/src/lib/utils.ts
new file mode 100644
index 00000000..a5ef1935
--- /dev/null
+++ b/packages/docs/client/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/packages/docs/client/src/pages/Overview.tsx b/packages/docs/client/src/pages/Overview.tsx
new file mode 100644
index 00000000..6b46355b
--- /dev/null
+++ b/packages/docs/client/src/pages/Overview.tsx
@@ -0,0 +1,93 @@
+import { Prose } from "../components/ui/prose";
+
+export default function Overview() {
+ return (
+
+
+
+
+
+ Sentry MCP
+
+ This service implements the Model Context Protocol (MCP) for
+ interacting with
+ Sentry , focused on
+ human-in-the-loop coding agents and developer workflows.
+
+
+ What is a Model Context Protocol?
+
+ In short, it plugs Sentry's API into an LLM so you can ask
+ questions about your data within the model's context — helping
+ with debugging, production issues, and understanding app behavior.
+
+
+
+
+
+ );
+}
diff --git a/packages/docs/package.json b/packages/docs/package.json
new file mode 100644
index 00000000..bd76e65c
--- /dev/null
+++ b/packages/docs/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@sentry/mcp-docs",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "FSL-1.1-ALv2",
+ "files": ["./dist/*"],
+ "exports": {
+ ".": {
+ "types": "./dist/index.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build && vite build -c vite.client.config.ts",
+ "deploy": "wrangler deploy"
+ },
+ "devDependencies": {
+ "@cloudflare/vite-plugin": "catalog:",
+ "@cloudflare/workers-types": "catalog:",
+ "@sentry/mcp-server-tsconfig": "workspace:*",
+ "@tailwindcss/typography": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "autoprefixer": "catalog:",
+ "postcss": "catalog:",
+ "tailwindcss": "catalog:",
+ "vite": "catalog:",
+ "wrangler": "catalog:"
+ },
+ "dependencies": {
+ "@sentry/mcp-server": "workspace:*",
+ "clsx": "catalog:",
+ "hono": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ "react-router-dom": "catalog:",
+ "tailwind-merge": "catalog:"
+ }
+}
diff --git a/packages/docs/postcss.config.cjs b/packages/docs/postcss.config.cjs
new file mode 100644
index 00000000..12a703d9
--- /dev/null
+++ b/packages/docs/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/packages/docs/src/index.ts b/packages/docs/src/index.ts
new file mode 100644
index 00000000..e1ebf559
--- /dev/null
+++ b/packages/docs/src/index.ts
@@ -0,0 +1,13 @@
+import { Hono } from "hono";
+
+export type Env = {
+ ASSETS: Fetcher;
+};
+
+const app = new Hono<{ Bindings: Env }>();
+
+// Route SPA paths to static assets
+app.get("/docs/*", (c) => c.env.ASSETS.fetch(c.req.raw));
+app.get("/docs", (c) => c.env.ASSETS.fetch(c.req.raw));
+
+export default { fetch: app.fetch };
diff --git a/packages/docs/tailwind.config.ts b/packages/docs/tailwind.config.ts
new file mode 100644
index 00000000..f27603cc
--- /dev/null
+++ b/packages/docs/tailwind.config.ts
@@ -0,0 +1,10 @@
+import type { Config } from "tailwindcss";
+
+export default {
+ content: ["./client/**/*.{html,ts,tsx}", "./index.html"],
+ darkMode: ["class"],
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography")],
+} satisfies Config;
diff --git a/packages/docs/tsconfig.json b/packages/docs/tsconfig.json
new file mode 100644
index 00000000..18b5531c
--- /dev/null
+++ b/packages/docs/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../mcp-server-tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.docs.tsbuildinfo",
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": [
+ "@cloudflare/workers-types"
+ ]
+ },
+ "include": ["src"]
+}
diff --git a/packages/docs/tsconfig.node.json b/packages/docs/tsconfig.node.json
new file mode 100644
index 00000000..2e7e7bab
--- /dev/null
+++ b/packages/docs/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../mcp-server-tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "outDir": "dist",
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts"]
+}
+
diff --git a/packages/docs/vite.client.config.ts b/packages/docs/vite.client.config.ts
new file mode 100644
index 00000000..a3377e49
--- /dev/null
+++ b/packages/docs/vite.client.config.ts
@@ -0,0 +1,12 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+import path from "node:path";
+
+export default defineConfig({
+ root: path.resolve(__dirname, "client"),
+ plugins: [react()],
+ build: {
+ outDir: path.resolve(__dirname, "dist/client"),
+ emptyOutDir: true,
+ },
+});
diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts
new file mode 100644
index 00000000..2673d023
--- /dev/null
+++ b/packages/docs/vite.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vite";
+import { cloudflare } from "@cloudflare/vite-plugin";
+
+export default defineConfig({
+ plugins: [cloudflare({ configPath: "./wrangler.jsonc" })],
+ server: {
+ port: 5174,
+ },
+});
diff --git a/packages/docs/wrangler.canary.jsonc b/packages/docs/wrangler.canary.jsonc
new file mode 100644
index 00000000..2b7c323a
--- /dev/null
+++ b/packages/docs/wrangler.canary.jsonc
@@ -0,0 +1,19 @@
+/**
+ * Canary configuration for sentry-mcp-docs worker
+ */
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "sentry-mcp-docs-canary",
+ "main": "./src/index.ts",
+ "compatibility_date": "2025-03-21",
+ "compatibility_flags": [
+ "nodejs_compat",
+ "nodejs_compat_populate_process_env",
+ "global_fetch_strictly_public"
+ ],
+ "keep_vars": true,
+ "vars": {},
+ "dev": {
+ "port": 8791
+ }
+}
diff --git a/packages/docs/wrangler.jsonc b/packages/docs/wrangler.jsonc
new file mode 100644
index 00000000..9bdd4e49
--- /dev/null
+++ b/packages/docs/wrangler.jsonc
@@ -0,0 +1,25 @@
+/**
+ * Wrangler config for the docs worker.
+ * See https://developers.cloudflare.com/workers/wrangler/configuration/
+ */
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "sentry-mcp-docs",
+ "main": "./src/index.ts",
+ "compatibility_date": "2025-03-21",
+ "compatibility_flags": [
+ "nodejs_compat",
+ "nodejs_compat_populate_process_env",
+ "global_fetch_strictly_public"
+ ],
+ "keep_vars": true,
+ "assets": {
+ "directory": "./dist/client",
+ "binding": "ASSETS",
+ "not_found_handling": "single-page-application"
+ },
+ "vars": {},
+ "dev": {
+ "port": 8790
+ }
+}
diff --git a/packages/mcp-cloudflare/package.json b/packages/mcp-cloudflare/package.json
index 80c70390..f493f465 100644
--- a/packages/mcp-cloudflare/package.json
+++ b/packages/mcp-cloudflare/package.json
@@ -4,9 +4,7 @@
"private": true,
"type": "module",
"license": "FSL-1.1-ALv2",
- "files": [
- "./dist/*"
- ],
+ "files": ["./dist/*"],
"exports": {
".": {
"types": "./dist/index.ts",
@@ -29,7 +27,6 @@
"@cloudflare/vite-plugin": "catalog:",
"@cloudflare/vitest-pool-workers": "catalog:",
"@cloudflare/workers-types": "catalog:",
- "@sentry/mcp-server": "workspace:*",
"@sentry/mcp-server-mocks": "workspace:*",
"@sentry/mcp-server-tsconfig": "workspace:*",
"@sentry/vite-plugin": "catalog:",
@@ -46,6 +43,8 @@
"wrangler": "catalog:"
},
"dependencies": {
+ "@sentry/core": "catalog:",
+ "@sentry/mcp-server": "workspace:*",
"@ai-sdk/openai": "catalog:",
"@ai-sdk/react": "catalog:",
"@cloudflare/workers-oauth-provider": "catalog:",
diff --git a/packages/mcp-cloudflare/src/server/index.ts b/packages/mcp-cloudflare/src/server/index.ts
index 1fa6ea54..e568a6b0 100644
--- a/packages/mcp-cloudflare/src/server/index.ts
+++ b/packages/mcp-cloudflare/src/server/index.ts
@@ -73,7 +73,27 @@ const corsWrappedOAuthProvider = {
},
};
-export default Sentry.withSentry(
+const baseHandler = Sentry.withSentry(
getSentryConfig,
corsWrappedOAuthProvider,
-) satisfies ExportedHandler;
+) as ExportedHandler;
+
+const handler: ExportedHandler = {
+ async fetch(request, env, ctx) {
+ try {
+ const url = new URL(request.url);
+ if (url.pathname.startsWith("/docs")) {
+ return env.DOCS.fetch(request);
+ }
+ } catch (error: unknown) {
+ // Maintain minimal logging and avoid leaking secrets
+ const err = error as Error;
+ // eslint-disable-next-line no-console
+ console.error("[ERROR]", err.message, err.stack);
+ }
+
+ return baseHandler.fetch!(request, env, ctx);
+ },
+};
+
+export default handler;
diff --git a/packages/mcp-cloudflare/src/server/types.ts b/packages/mcp-cloudflare/src/server/types.ts
index cb4075c0..e5d4c4c2 100644
--- a/packages/mcp-cloudflare/src/server/types.ts
+++ b/packages/mcp-cloudflare/src/server/types.ts
@@ -17,6 +17,7 @@ export type WorkerProps = ServerContext & {
export interface Env {
NODE_ENV: string;
ASSETS: Fetcher;
+ DOCS: Fetcher;
OAUTH_KV: KVNamespace;
COOKIE_SECRET: string;
SENTRY_CLIENT_ID: string;
diff --git a/packages/mcp-cloudflare/turbo.json b/packages/mcp-cloudflare/turbo.json
new file mode 100644
index 00000000..44b134fd
--- /dev/null
+++ b/packages/mcp-cloudflare/turbo.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "dev": {
+ "persistent": true,
+ "cache": false,
+ "with": ["@sentry/mcp-server#dev", "@sentry/mcp-docs#dev"]
+ }
+ }
+}
diff --git a/packages/mcp-cloudflare/vite.config.ts b/packages/mcp-cloudflare/vite.config.ts
index 3ca11f8c..a12006c4 100644
--- a/packages/mcp-cloudflare/vite.config.ts
+++ b/packages/mcp-cloudflare/vite.config.ts
@@ -8,7 +8,7 @@ import path from "node:path";
export default defineConfig({
plugins: [
react(),
- cloudflare(),
+ cloudflare({ configPath: "./wrangler.jsonc" }),
tailwindcss(),
sentryVitePlugin({
org: "sentry",
@@ -23,4 +23,7 @@ export default defineConfig({
build: {
sourcemap: true,
},
+ server: {
+ port: 5173,
+ },
});
diff --git a/packages/mcp-cloudflare/wrangler.canary.jsonc b/packages/mcp-cloudflare/wrangler.canary.jsonc
index cdac863f..81987383 100644
--- a/packages/mcp-cloudflare/wrangler.canary.jsonc
+++ b/packages/mcp-cloudflare/wrangler.canary.jsonc
@@ -27,6 +27,12 @@
"version_metadata": {
"binding": "CF_VERSION_METADATA"
},
+ "services": [
+ {
+ "binding": "DOCS",
+ "service": "sentry-mcp-docs-canary"
+ }
+ ],
"vars": {},
"durable_objects": {
"bindings": [
diff --git a/packages/mcp-cloudflare/wrangler.jsonc b/packages/mcp-cloudflare/wrangler.jsonc
index 93159f4b..a2314652 100644
--- a/packages/mcp-cloudflare/wrangler.jsonc
+++ b/packages/mcp-cloudflare/wrangler.jsonc
@@ -30,6 +30,12 @@
"version_metadata": {
"binding": "CF_VERSION_METADATA"
},
+ "services": [
+ {
+ "binding": "DOCS",
+ "service": "sentry-mcp-docs"
+ }
+ ],
"vars": {},
"durable_objects": {
"bindings": [
@@ -79,6 +85,12 @@
// { "service": "sentry-mcp-tail" }
],
"dev": {
- "port": 8788
+ "port": 8788,
+ "services": [
+ {
+ "binding": "DOCS",
+ "local_port": 8790
+ }
+ ]
}
}
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index 45807997..0124acab 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -132,5 +132,12 @@
"ai": "catalog:",
"dotenv": "catalog:",
"zod": "catalog:"
+ },
+ "turbo": {
+ "pipeline": {
+ "dev": {
+ "dependsOn": []
+ }
+ }
}
}
diff --git a/packages/smoke-tests/src/smoke.test.ts b/packages/smoke-tests/src/smoke.test.ts
index 8a1ac580..9e2dc6e0 100644
--- a/packages/smoke-tests/src/smoke.test.ts
+++ b/packages/smoke-tests/src/smoke.test.ts
@@ -103,6 +103,13 @@ describeIfPreviewUrl(
expect(response.status).toBe(200);
});
+ it("should serve documentation at /docs", async () => {
+ const { response, data } = await safeFetch(`${PREVIEW_URL}/docs`);
+ expect(response.status).toBe(200);
+ const body = typeof data === "string" ? data : String(data);
+ expect(body).toContain("Documentation");
+ });
+
it("should have MCP endpoint that returns server info (with auth error)", async () => {
const { response, data } = await safeFetch(`${PREVIEW_URL}/mcp`, {
method: "POST",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e048136e..0406b32e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -220,6 +220,31 @@ importers:
specifier: ^7.0.15
version: 7.0.15
+ packages/docs:
+ dependencies:
+ '@sentry/mcp-server':
+ specifier: workspace:*
+ version: link:../mcp-server
+ hono:
+ specifier: 'catalog:'
+ version: 4.9.6
+ devDependencies:
+ '@cloudflare/vite-plugin':
+ specifier: 'catalog:'
+ version: 1.11.4(rollup@4.44.1)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))(workerd@1.20250813.0)(wrangler@4.29.1(@cloudflare/workers-types@4.20250704.0))
+ '@cloudflare/workers-types':
+ specifier: 'catalog:'
+ version: 4.20250704.0
+ '@sentry/mcp-server-tsconfig':
+ specifier: workspace:*
+ version: link:../mcp-server-tsconfig
+ vite:
+ specifier: 'catalog:'
+ version: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0)
+ wrangler:
+ specifier: 'catalog:'
+ version: 4.29.1(@cloudflare/workers-types@4.20250704.0)
+
packages/mcp-cloudflare:
dependencies:
'@ai-sdk/openai':
@@ -243,6 +268,12 @@ importers:
'@sentry/cloudflare':
specifier: 'catalog:'
version: 9.34.0(@cloudflare/workers-types@4.20250704.0)
+ '@sentry/core':
+ specifier: 'catalog:'
+ version: 9.34.0
+ '@sentry/mcp-server':
+ specifier: workspace:*
+ version: link:../mcp-server
'@sentry/react':
specifier: 'catalog:'
version: 9.34.0(react@19.1.0)
@@ -297,16 +328,13 @@ importers:
devDependencies:
'@cloudflare/vite-plugin':
specifier: 'catalog:'
- version: 1.11.4(rollup@4.44.1)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))(workerd@1.20250813.0)(wrangler@4.29.1(@cloudflare/workers-types@4.20250704.0))
+ version: 1.11.4(rollup@4.44.1)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))(workerd@1.20250617.0)(wrangler@4.29.1(@cloudflare/workers-types@4.20250704.0))
'@cloudflare/vitest-pool-workers':
specifier: 'catalog:'
version: 0.8.49(@cloudflare/workers-types@4.20250704.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.0.10)(typescript@5.8.3))(tsx@4.20.3)(yaml@2.8.0))
'@cloudflare/workers-types':
specifier: 'catalog:'
version: 4.20250704.0
- '@sentry/mcp-server':
- specifier: workspace:*
- version: link:../mcp-server
'@sentry/mcp-server-mocks':
specifier: workspace:*
version: link:../mcp-server-mocks
@@ -5042,12 +5070,37 @@ snapshots:
optionalDependencies:
workerd: 1.20250617.0
+ '@cloudflare/unenv-preset@2.6.1(unenv@2.0.0-rc.19)(workerd@1.20250617.0)':
+ dependencies:
+ unenv: 2.0.0-rc.19
+ optionalDependencies:
+ workerd: 1.20250617.0
+
'@cloudflare/unenv-preset@2.6.1(unenv@2.0.0-rc.19)(workerd@1.20250813.0)':
dependencies:
unenv: 2.0.0-rc.19
optionalDependencies:
workerd: 1.20250813.0
+ '@cloudflare/vite-plugin@1.11.4(rollup@4.44.1)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))(workerd@1.20250617.0)(wrangler@4.29.1(@cloudflare/workers-types@4.20250704.0))':
+ dependencies:
+ '@cloudflare/unenv-preset': 2.6.1(unenv@2.0.0-rc.19)(workerd@1.20250617.0)
+ '@mjackson/node-fetch-server': 0.6.1
+ '@rollup/plugin-replace': 6.0.2(rollup@4.44.1)
+ get-port: 7.1.0
+ miniflare: 4.20250813.0
+ picocolors: 1.1.1
+ tinyglobby: 0.2.14
+ unenv: 2.0.0-rc.19
+ vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0)
+ wrangler: 4.29.1(@cloudflare/workers-types@4.20250704.0)
+ ws: 8.18.0
+ transitivePeerDependencies:
+ - bufferutil
+ - rollup
+ - utf-8-validate
+ - workerd
+
'@cloudflare/vite-plugin@1.11.4(rollup@4.44.1)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))(workerd@1.20250813.0)(wrangler@4.29.1(@cloudflare/workers-types@4.20250704.0))':
dependencies:
'@cloudflare/unenv-preset': 2.6.1(unenv@2.0.0-rc.19)(workerd@1.20250813.0)