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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ packages. For the CLI, see the [CLI guide](docs/cli/README.md) and the published
documentation at <https://cli.sentry.dev/>. The [documentation index](docs/README.md)
covers both products and shared contributor guidance.

The independent [Docs MCP Worker](apps/docs-mcp/README.md) provides documentation
search and AI-generated answers backed by `docs.sentry.io`.

## Sentry MCP

Sentry's MCP service is primarily designed for human-in-the-loop coding agents. Our tool selection and priorities are focused on developer workflows and debugging use cases, rather than providing a general-purpose MCP server for all Sentry functionality.
Expand Down
3 changes: 3 additions & 0 deletions apps/docs-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Required — routes both the primary and fallback docs agent models.
# Get one at https://openrouter.ai/keys.
OPENROUTER_API_KEY=
106 changes: 106 additions & 0 deletions apps/docs-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Docs MCP

An independent Cloudflare Worker for Sentry documentation search, cited answers,
and tailored SDK integration guides. It reads the Markdown exports and doctree
from `docs.sentry.io`; AI-backed operations use OpenRouter through Mastra.

Imported from [getsentry/cli-init-api at f3b32b49](https://github.com/getsentry/cli-init-api/tree/f3b32b49ebaa5eb13378e1e5ab9f05be2f39ba5d/apps/docs-mcp).
The Worker has no build or runtime dependency on Init Server. Its Cloudflare name
remains `docs-mcp`.

## Interfaces

Production access is through Cloudflare service bindings. `workers.dev` and
preview URLs are disabled. Both docs endpoints accept the `docs-mcp.internal`
origin used by the binding, or localhost during development.

| Endpoint | Contract |
| --------------------- | ----------------------------------------------------------- |
| `GET /health` | Readiness response `{ "status": "ok" }`. |
| `/mcp` | Streamable HTTP MCP with the four tools below. |
| `POST /internal/docs` | JSON API with `operation: "search-docs"` or `"query-docs"`. |

| MCP tool | Input | Output inside text content |
| ------------------------ | ------------------------------------------- | ---------------------------------------------------------------- |
| `search-docs` | `query`, optional `limit` | JSON `{ results: [{ title, url, description? }] }`; no LLM call. |
| `query-docs` | `query`, optional safe project `context` | JSON `{ answer, sources }` with validated citations. |
| `get-available-features` | `libs` | JSON `{ features }` with canonical feature slugs. |
| `get-docs` | `libs`, `features`, optional `stackSummary` | Tailored Markdown integration guide. |

The internal API uses the same search and query implementations. Query context is
limited to `frameworks`, `languages`, and `sentryConfigured`. Model failures and
unverifiable answers retain the safe `DOCS_MODEL_UNAVAILABLE` and
`DOCS_UNGROUNDED` error codes.

The existing `sentry docs` CLI calls Init Server's authenticated `/api/docs/*`
facade, which forwards requests through its `DOCS_MCP` binding. Moving this app
does not change that contract. Retiring Init Server would require a separate
decision about the CLI's entry point; it would not stop this Worker. Toolkit's
main MCP server has its own `search_docs` and `get_doc` tools and does not consume
this Worker.

## Development

From the Toolkit root:

```sh
pnpm install
cp apps/docs-mcp/.env.example apps/docs-mcp/.dev.vars
# Set OPENROUTER_API_KEY in the ignored .dev.vars file for AI-backed calls.
pnpm --filter docs-mcp dev
```

The server listens on `http://localhost:8788`. Search and readiness checks do not
require an OpenRouter key:

```sh
curl http://localhost:8788/health
curl http://localhost:8788/internal/docs \
-H 'Content-Type: application/json' \
-d '{"operation":"search-docs","query":"tracing","limit":3}'
```

Validation runs without model credentials:

```sh
pnpm --filter docs-mcp test
pnpm --filter docs-mcp typecheck
pnpm --filter docs-mcp build
```

`build` only bundles with `wrangler deploy --dry-run`; it does not deploy.
Toolkit's workspace CI discovers the app's build, typecheck, and `test:ci`
scripts automatically. Keep the app's AI SDK 5 and Sentry 10 dependencies
separate from Toolkit's newer catalogs until a dedicated upgrade is validated.
Wrangler follows the shared workspace catalog used by MCP and CLI docs.
The `execa` alias excludes Mastra's unsupported local-process execution path from
the Worker bundle.

## Deployment ownership

Importing this app does not change Cloudflare or enable a new deployment workflow.
The existing Worker is connected to the source repository through Workers Builds.
Toolkit's MCP deployment workflow targets `sentry-mcp`, not this app.

When a production cutover is scheduled:

1. Record the existing Worker's account, build settings, runtime secrets, and
active version. Keep the name `docs-mcp`, compatibility settings, private
access configuration, and `OPENROUTER_API_KEY` runtime secret.
2. Disconnect the old repository under the existing Worker's **Settings →
Builds**. Ensure no old build remains in flight before enabling a new deploy.
3. Connect that same Worker to `getsentry/toolkit`, with the repository root as
its root directory. Set build variables `NODE_VERSION=22.23.4`,
`PNPM_VERSION=11.8.0`, and `SKIP_DEPENDENCY_INSTALL=1`. Use
`pnpm install --frozen-lockfile && pnpm --filter docs-mcp run build` as the
build command and `pnpm --filter docs-mcp run deploy` as the deploy command.
Watch paths must cover this app, the root `package.json`, lockfile/workspace
configuration, and patches. Review preview settings so the private service
is not exposed publicly.
4. Deploy the tested revision and verify its active version and service-binding
calls before removing the source repository's copy.

The caller's `service: "docs-mcp"` binding still identifies the same Worker in
the same account. No consumer URL change is required. Runtime secrets belong to
the Worker and are separate from build secrets; never copy secret values into
this repository. See [Cloudflare's repository-switch procedure](https://developers.cloudflare.com/workers/ci-cd/builds/#disconnecting-builds).
30 changes: 30 additions & 0 deletions apps/docs-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "docs-mcp",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev --port 8788",
"build": "wrangler deploy --dry-run",
"deploy": "wrangler deploy --minify",
"generate:cf-types": "wrangler types --env-interface CloudflareBindings",
"test": "vitest run",
"test:ci": "vitest run --reporter=default --reporter=junit --outputFile=tests.junit.xml",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hono/mcp": "^0.2.3",
"@mastra/core": "^1.63.2",
"@modelcontextprotocol/sdk": "^1.31.0",
"@openrouter/ai-sdk-provider": "^1.5.4",
"@sentry/cloudflare": "^10.62.0",
"ai": "^5.0.0",
"hono": "^4.13.11",
"zod": "^4.5.4"
},
"devDependencies": {
"@types/node": "^22",
"typescript": "^5.9.3",
"wrangler": "catalog:",
"vitest": "catalog:"
}
}
19 changes: 19 additions & 0 deletions apps/docs-mcp/src/__tests__/access.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import { isDocsMcpServiceRequest } from "../access";

describe("isDocsMcpServiceRequest", () => {
test.each([
"https://docs-mcp.internal/mcp",
"http://localhost:8788/mcp",
"http://127.0.0.1:8788/mcp",
])("allows the service-binding or local development origin: %s", (url) => {
expect(isDocsMcpServiceRequest(new Request(url))).toBe(true);
});

test.each([
"https://docs-mcp.getsentry.workers.dev/mcp",
"https://preview-docs-mcp.getsentry.workers.dev/mcp",
])("rejects public worker origins: %s", (url) => {
expect(isDocsMcpServiceRequest(new Request(url))).toBe(false);
});
});
99 changes: 99 additions & 0 deletions apps/docs-mcp/src/__tests__/doctree.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, test } from "vitest";
import { MAX_SEARCH_TERMS, searchDocTree } from "../sentry-docs/doctree";

describe("searchDocTree", () => {
test("bounds token scoring for long natural-language questions", () => {
expect(MAX_SEARCH_TERMS).toBeLessThanOrEqual(24);
});

test("ranks matching visible pages and returns canonical docs URLs", () => {
const results = searchDocTree(
{
children: [
{
frontmatter: {
description: "Set up Sentry for Next.js applications.",
title: "Next.js",
},
path: "/platforms/javascript/guides/nextjs/",
},
{
frontmatter: {
description: "Configure Next.js tracing.",
title: "Tracing",
},
path: "/platforms/javascript/guides/nextjs/tracing/",
},
{
frontmatter: { sidebar_hidden: true, title: "Hidden" },
path: "/internal/nextjs/",
},
],
path: "/",
},
"nextjs",
1,
);

expect(results).toEqual([
{
description: "Set up Sentry for Next.js applications.",
title: "Next.js",
url: "https://docs.sentry.io/platforms/javascript/guides/nextjs/",
},
]);
});

test("breaks equal-score ties by path", () => {
const results = searchDocTree(
{
children: [
{ frontmatter: { title: "Tracing" }, path: "/z/tracing/" },
{ frontmatter: { title: "Tracing" }, path: "/a/tracing/" },
],
path: "/",
},
"tracing",
2,
);

expect(results.map((result) => result.url)).toEqual([
"https://docs.sentry.io/a/tracing/",
"https://docs.sentry.io/z/tracing/",
]);
});

test("finds relevant pages for a natural-language docs question", () => {
const results = searchDocTree(
{
children: [
{
frontmatter: {
description: "Set up Sentry for Next.js applications.",
title: "Next.js",
},
path: "/platforms/javascript/guides/nextjs/",
},
{
frontmatter: {
description: "Configure tracing in your Next.js app.",
title: "Tracing",
},
path: "/platforms/javascript/guides/nextjs/tracing/",
},
],
path: "/",
},
"How do I configure Next.js tracing?",
1,
);

expect(results).toEqual([
{
description: "Configure tracing in your Next.js app.",
title: "Tracing",
url: "https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/",
},
]);
});
});
23 changes: 23 additions & 0 deletions apps/docs-mcp/src/__tests__/fetcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, test } from "vitest";
import { normalizeDocPath } from "../sentry-docs/fetcher";

describe("normalizeDocPath", () => {
test.each([
["/platforms/javascript/", "/platforms/javascript"],
["/platforms/javascript", "/platforms/javascript"],
["platforms/javascript", "/platforms/javascript"],
["https://docs.sentry.io/platforms/javascript/", "/platforms/javascript"],
["/platforms/javascript/.md", "/platforms/javascript"],
["https://docs.sentry.io", "/"],
["https://docs.sentry.io/", "/"],
])("normalizes supported docs input %s", (input, expected) => {
expect(normalizeDocPath(input)).toBe(expected);
});

test.each([
"https://docs.sentry.io.example.com/platforms/javascript",
"https://[email protected]/platforms/javascript",
])("does not strip the docs prefix from a different origin: %s", (input) => {
expect(normalizeDocPath(input)).toBe(`/${input}`);
});
});
Loading
Loading