From 5b2d27930aefe619694cfed6ed383df759dc0f93 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 8 Sep 2025 14:27:26 -0700 Subject: [PATCH 1/2] chore(cloudflare,docs): sync Cloudflare config, docs, and MCP definitions - Update Cloudflare docs: deployment and overview - Add turbo config for mcp-cloudflare - Update wrangler and vite configs - Adjust Cloudflare server types and index - Update MCP prompts and resources definitions - Refresh deploy and smoke-test workflows - Update package manifests and lockfile - Add docs package scaffold - Update smoke tests Co-Authored-By: Codex CLI Agent --- .github/workflows/deploy.yml | 78 ++++++++++++++++--- .github/workflows/smoke-tests.yml | 35 +++++++-- docs/cloudflare/deployment.md | 30 ++++++- docs/cloudflare/overview.md | 42 ++++++++++ package.json | 2 +- packages/docs/package.json | 28 +++++++ packages/docs/src/index.ts | 49 ++++++++++++ packages/docs/tsconfig.json | 12 +++ packages/docs/tsconfig.node.json | 10 +++ packages/docs/vite.config.ts | 9 +++ packages/docs/wrangler.canary.jsonc | 19 +++++ packages/docs/wrangler.jsonc | 20 +++++ packages/mcp-cloudflare/package.json | 7 +- packages/mcp-cloudflare/src/server/index.ts | 24 +++++- packages/mcp-cloudflare/src/server/types.ts | 1 + packages/mcp-cloudflare/turbo.json | 11 +++ packages/mcp-cloudflare/vite.config.ts | 5 +- packages/mcp-cloudflare/wrangler.canary.jsonc | 6 ++ packages/mcp-cloudflare/wrangler.jsonc | 14 +++- packages/mcp-server/package.json | 7 ++ packages/smoke-tests/src/smoke.test.ts | 7 ++ pnpm-lock.yaml | 58 +++++++++++++- 22 files changed, 445 insertions(+), 29 deletions(-) create mode 100644 packages/docs/package.json create mode 100644 packages/docs/src/index.ts create mode 100644 packages/docs/tsconfig.json create mode 100644 packages/docs/tsconfig.node.json create mode 100644 packages/docs/vite.config.ts create mode 100644 packages/docs/wrangler.canary.jsonc create mode 100644 packages/docs/wrangler.jsonc create mode 100644 packages/mcp-cloudflare/turbo.json 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 "<title>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 `<title>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 `<title>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/package.json b/packages/docs/package.json new file mode 100644 index 00000000..52b48677 --- /dev/null +++ b/packages/docs/package.json @@ -0,0 +1,28 @@ +{ + "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", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "catalog:", + "@cloudflare/workers-types": "catalog:", + "@sentry/mcp-server-tsconfig": "workspace:*", + "vite": "catalog:", + "wrangler": "catalog:" + }, + "dependencies": { + "hono": "catalog:" + } +} diff --git a/packages/docs/src/index.ts b/packages/docs/src/index.ts new file mode 100644 index 00000000..6921adf0 --- /dev/null +++ b/packages/docs/src/index.ts @@ -0,0 +1,49 @@ +import { Hono } from "hono"; + +// Define Cloudflare bindings if/when needed +export type Env = Record<string, never>; + +const app = new Hono<{ Bindings: Env }>(); + +app.get("/docs", (c) => { + const html = `<!doctype html> + <html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Documentation + + + +
+
+

Documentation (Placeholder)

+
+
+
+
+
+
+
+
+
+ + `; + + return c.html(html); +}); + +// Default export compatible with Cloudflare Modules +export default { fetch: app.fetch }; 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.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..44d9cb05 --- /dev/null +++ b/packages/docs/wrangler.jsonc @@ -0,0 +1,20 @@ +/** + * 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, + "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..f6244905 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,28 @@ importers: specifier: ^7.0.15 version: 7.0.15 + packages/docs: + dependencies: + 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 +265,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 +325,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 +5067,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) From bfeb814c4f53f235b401ed68296da0e2765cacfb Mon Sep 17 00:00:00 2001 From: David Cramer <dcramer@gmail.com> Date: Tue, 9 Sep 2025 10:50:23 -0700 Subject: [PATCH 2/2] bad idea --- packages/docs/client/index.html | 13 +++ packages/docs/client/main.tsx | 19 ++++ .../docs/client/src/components/ui/button.tsx | 51 ++++++++++ .../docs/client/src/components/ui/header.tsx | 26 ++++++ .../docs/client/src/components/ui/icon.tsx | 26 ++++++ .../client/src/components/ui/icons/sentry.tsx | 11 +++ .../docs/client/src/components/ui/prose.tsx | 19 ++++ packages/docs/client/src/index.css | 7 ++ packages/docs/client/src/lib/utils.ts | 6 ++ packages/docs/client/src/pages/Overview.tsx | 93 +++++++++++++++++++ packages/docs/package.json | 16 +++- packages/docs/postcss.config.cjs | 6 ++ packages/docs/src/index.ts | 48 ++-------- packages/docs/tailwind.config.ts | 10 ++ packages/docs/vite.client.config.ts | 12 +++ packages/docs/wrangler.jsonc | 5 + pnpm-lock.yaml | 3 + 17 files changed, 328 insertions(+), 43 deletions(-) create mode 100644 packages/docs/client/index.html create mode 100644 packages/docs/client/main.tsx create mode 100644 packages/docs/client/src/components/ui/button.tsx create mode 100644 packages/docs/client/src/components/ui/header.tsx create mode 100644 packages/docs/client/src/components/ui/icon.tsx create mode 100644 packages/docs/client/src/components/ui/icons/sentry.tsx create mode 100644 packages/docs/client/src/components/ui/prose.tsx create mode 100644 packages/docs/client/src/index.css create mode 100644 packages/docs/client/src/lib/utils.ts create mode 100644 packages/docs/client/src/pages/Overview.tsx create mode 100644 packages/docs/postcss.config.cjs create mode 100644 packages/docs/tailwind.config.ts create mode 100644 packages/docs/vite.client.config.ts 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 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>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 ( +
+
+
+ +
Sentry MCP
+
+
+ +
+
+
+ ); +} 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 index 52b48677..bd76e65c 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -13,16 +13,30 @@ }, "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": { - "hono": "catalog:" + "@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 index 6921adf0..e1ebf559 100644 --- a/packages/docs/src/index.ts +++ b/packages/docs/src/index.ts @@ -1,49 +1,13 @@ import { Hono } from "hono"; -// Define Cloudflare bindings if/when needed -export type Env = Record; +export type Env = { + ASSETS: Fetcher; +}; const app = new Hono<{ Bindings: Env }>(); -app.get("/docs", (c) => { - const html = ` - - - - - Documentation - - - -
-
-

Documentation (Placeholder)

-
-
-
-
-
-
-
-
-
- - `; +// 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)); - return c.html(html); -}); - -// Default export compatible with Cloudflare Modules 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/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/wrangler.jsonc b/packages/docs/wrangler.jsonc index 44d9cb05..9bdd4e49 100644 --- a/packages/docs/wrangler.jsonc +++ b/packages/docs/wrangler.jsonc @@ -13,6 +13,11 @@ "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/pnpm-lock.yaml b/pnpm-lock.yaml index f6244905..0406b32e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,6 +222,9 @@ importers: packages/docs: dependencies: + '@sentry/mcp-server': + specifier: workspace:* + version: link:../mcp-server hono: specifier: 'catalog:' version: 4.9.6