diff --git a/.agents/skills/qa/SKILL.md b/.agents/skills/qa/SKILL.md index 21ab7a207..d30fe59f1 100644 --- a/.agents/skills/qa/SKILL.md +++ b/.agents/skills/qa/SKILL.md @@ -66,6 +66,13 @@ behavior. For catalog tools, expect `search_sentry_tools` followed by `execute_sentry_tool(name: )`. For direct tools, expect the tool name in the transcript. `--list-tools` alone is not QA. +For output-format changes, also inspect the raw MCP tool result when possible, +not only the LLM's final answer. The final answer can add model-specific text +that is not part of the tool response. Review raw tool output against +`docs/contributing/tool-responses.md`: it should be user-facing, structured, +and free of raw API JSON, internal implementation IDs, empty placeholders, and +unrelated instructions. + If your changes involve agent mode or experimental tools: ```bash diff --git a/AGENTS.md b/AGENTS.md index fa7b61284..a9636d1da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Sentry MCP is a Model Context Protocol server that exposes Sentry's error tracki ## Principles - **Type Safety**: Prefer strict types over `any` - they catch bugs and improve tooling. Use `unknown` for truly unknown types. -- **Security**: Never log secrets. Validate external input. See docs/security.md. +- **Security**: Never log secrets. Validate external input. See docs/operations/security.md. - **Simplicity**: Follow existing patterns. Check neighboring files before inventing new approaches. ## Constraints @@ -31,26 +31,60 @@ sentry-mcp/ └── docs/ # All documentation ``` -## Essential Documentation - -**Read before making changes:** -- docs/adding-tools.md — Tool implementation guide -- docs/testing.md — Testing requirements -- docs/common-patterns.md — Error handling, Zod schemas, response formatting -- docs/error-handling.md — Error types and propagation - -**Reference:** -- docs/architecture.md — System design -- docs/api-patterns.md — Sentry API client usage -- docs/quality-checks.md — Pre-commit checklist -- docs/pr-management.md — Commit/PR guidelines -- docs/security.md — Authentication patterns -- docs/stdio-auth.md — Device code flow, token caching, client ID architecture -- docs/oauth-signout-playbook.md — Remote OAuth failure modes, telemetry, diagnostic runbook -- docs/embedded-agents.md — LLM provider configuration for AI-powered tools +## Documentation Map + +- docs/README.md — Full documentation index + +**Read before tool changes:** +- docs/contributing/adding-tools.md — Tool implementation guide +- docs/contributing/tool-responses.md — Tool output policy and QA review checklist +- docs/testing/overview.md — Testing requirements and snapshot policy +- docs/contributing/common-patterns.md — Error handling, Zod schemas, shared formatting patterns +- docs/contributing/error-handling.md — Error types and propagation + +**Contributing:** +- docs/contributing/api-patterns.md — Sentry API client usage +- docs/contributing/coding-guidelines.md — TypeScript and code style guidance +- docs/contributing/documentation-style-guide.md — Documentation style guide +- docs/contributing/pr-management.md — Commit and PR guidelines +- docs/contributing/quality-checks.md — Pre-commit checklist +- docs/contributing/search-events-api-patterns.md — Search Events API patterns + +**Testing:** +- docs/testing/overview.md — Unit, snapshot, eval, and agent CLI testing +- docs/testing/stdio.md — Stdio transport testing +- docs/testing/remote.md — Remote server and OAuth testing + +**Architecture and Operations:** +- docs/architecture/overview.md — System design +- docs/operations/security.md — Authentication and security patterns +- docs/operations/stdio-auth.md — Device code flow, token caching, client ID architecture +- docs/operations/oauth-signout-playbook.md — Remote OAuth diagnostic runbook +- docs/operations/embedded-agents.md — LLM provider configuration for AI-powered tools +- docs/operations/github-actions.md — GitHub Actions guidance +- docs/operations/logging.md — Logging guidance +- docs/operations/monitoring.md — Monitoring guidance +- docs/operations/token-cost-tracking.md — Tool definition token cost tracking + +**Cloudflare:** +- docs/cloudflare/overview.md — Cloudflare package overview +- docs/cloudflare/architecture.md — Cloudflare architecture +- docs/cloudflare/oauth-architecture.md — Cloudflare OAuth architecture + +**Integrations:** +- docs/integrations/claude-code-plugin.md — Plugin structure and agent prompts +- docs/integrations/flue-hooks.md — Flue hook notes +- docs/integrations/ide-instructions-refactor.md — IDE instruction refactor notes + +**Specs:** +- docs/specs/README.md — Specs index +- docs/specs/embedded-agent-openai-routing.md — Embedded agent OpenAI routing spec +- docs/specs/search-events.md — Search Events spec +- docs/specs/subpath-constraints.md — Subpath constraints spec + +**Releases:** - docs/releases/stdio.md — npm package release - docs/releases/cloudflare.md — Cloudflare deployment -- docs/claude-code-plugin.md — Plugin structure and agent prompts ## Commands @@ -89,8 +123,8 @@ Use `/dex` skill to coordinate complex work. Create tasks with full context, bre 1. Check neighboring files for existing patterns before writing new code. 2. When adding or modifying Sentry API endpoint usage, ALWAYS validate the endpoint behavior against the Sentry source code in `~/src/sentry` instead of assuming docs or client parameters are authoritative. 3. Update relevant docs when changing functionality. -4. Follow docs/error-handling.md for error types. -5. Follow docs/pr-management.md for commits and PRs. +4. Follow docs/contributing/error-handling.md for error types. +5. Follow docs/contributing/pr-management.md for commits and PRs. ## Commit Attribution diff --git a/README.md b/README.md index 80ae79d60..e3fd89310 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ SENTRY_HOST= # For self-hosted deployments MCP_DISABLE_SKILLS= # Disable specific skills (comma-separated, e.g. 'seer') ``` -**Important:** Always set `EMBEDDED_AGENT_PROVIDER` to explicitly specify your LLM provider. Auto-detection based on API keys alone is deprecated and will be removed in a future release. See [docs/embedded-agents.md](docs/embedded-agents.md) for detailed configuration options. +**Important:** Always set `EMBEDDED_AGENT_PROVIDER` to explicitly specify your LLM provider. Auto-detection based on API keys alone is deprecated and will be removed in a future release. See [docs/operations/embedded-agents.md](docs/operations/embedded-agents.md) for detailed configuration options. #### Example MCP Configuration @@ -228,8 +228,8 @@ pnpm -w run cli --access-token=TOKEN "query" Note: The CLI defaults to `http://localhost:5173`. Override with `--mcp-host` or set `MCP_URL` environment variable. **Comprehensive testing playbooks:** -- **Stdio testing:** See `docs/testing-stdio.md` for complete guide on building, running, and testing the stdio implementation (IDEs, MCP Inspector) -- **Remote testing:** See `docs/testing-remote.md` for complete guide on testing the remote server (OAuth, web UI, CLI client) +- **Stdio testing:** See `docs/testing/stdio.md` for complete guide on building, running, and testing the stdio implementation (IDEs, MCP Inspector) +- **Remote testing:** See `docs/testing/remote.md` for complete guide on testing the remote server (OAuth, web UI, CLI client) ## Development Notes diff --git a/TELEMETRY.md b/TELEMETRY.md index 86a2457fe..bbe480395 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -267,16 +267,16 @@ Attributes: `gen_ai.provider.name`, `gen_ai.request.model`, full request bodies, or other high-cardinality or sensitive values. - Do not log secrets. Authorization headers and access tokens must remain scrubbed. -- Update this document, `docs/monitoring.md`, and the relevant semantic lookup +- Update this document, `docs/operations/monitoring.md`, and the relevant semantic lookup data file under `packages/mcp-core/src/internal/agents/tools/data/` when adding or renaming telemetry fields. Do not add unit tests solely to assert telemetry attribute spelling. ## References -- `docs/monitoring.md` -- `docs/oauth-signout-playbook.md` -- `docs/error-handling.md` +- `docs/operations/monitoring.md` +- `docs/operations/oauth-signout-playbook.md` +- `docs/contributing/error-handling.md` - `packages/mcp-cloudflare/src/server/metrics.ts` - `packages/mcp-cloudflare/src/server/oauth/helpers.ts` - `packages/mcp-core/src/internal/agents/tools/data/mcp.json` diff --git a/docs/README.md b/docs/README.md index 59db72376..ecd35262e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,26 +1,75 @@ # Contributor Docs -This directory contains contributor documentation used by humans and LLMs. To avoid duplication, the canonical documentation map and contributor workflow live in `CLAUDE.md` (also available as `AGENTS.md`). +This directory contains contributor documentation used by humans and LLMs. The +canonical workflow and required docs live in [../AGENTS.md](../AGENTS.md) +(`CLAUDE.md` is a symlink to the same file). -## Purpose +## Start Here -- Central home for all contributor-focused docs (.md files) -- Consumed by tools (e.g., Cursor) via direct file references +- Tool implementation: [contributing/adding-tools.md](contributing/adding-tools.md) +- Tool output policy: [contributing/tool-responses.md](contributing/tool-responses.md) +- Testing: [testing/overview.md](testing/overview.md) +- Shared implementation patterns: [contributing/common-patterns.md](contributing/common-patterns.md) -## Start Here +## Topic Map + +### Contributing + +- [contributing/adding-tools.md](contributing/adding-tools.md) - Tool structure, visibility, implementation, and registration +- [contributing/api-patterns.md](contributing/api-patterns.md) - Sentry API client and MSW patterns +- [contributing/coding-guidelines.md](contributing/coding-guidelines.md) - TypeScript and code style guidance +- [contributing/common-patterns.md](contributing/common-patterns.md) - Shared Zod, validation, and formatting patterns +- [contributing/documentation-style-guide.md](contributing/documentation-style-guide.md) - Documentation style guide +- [contributing/error-handling.md](contributing/error-handling.md) - Error hierarchy and propagation +- [contributing/pr-management.md](contributing/pr-management.md) - Commit and PR guidelines +- [contributing/quality-checks.md](contributing/quality-checks.md) - Quality gates and pre-commit checks +- [contributing/search-events-api-patterns.md](contributing/search-events-api-patterns.md) - Search Events API guidance +- [contributing/tool-responses.md](contributing/tool-responses.md) - User-facing tool output policy, snapshot review, and QA expectations + +### Testing + +- [testing/overview.md](testing/overview.md) - Unit, snapshot, eval, and agent CLI testing +- [testing/stdio.md](testing/stdio.md) - Stdio transport testing +- [testing/remote.md](testing/remote.md) - Remote server and OAuth testing + +### Architecture And Operations + +- [architecture/overview.md](architecture/overview.md) - System design +- [operations/embedded-agents.md](operations/embedded-agents.md) - Embedded LLM provider configuration +- [operations/github-actions.md](operations/github-actions.md) - GitHub Actions guidance +- [operations/logging.md](operations/logging.md) - Logging guidance +- [operations/monitoring.md](operations/monitoring.md) - Monitoring guidance +- [operations/oauth-signout-playbook.md](operations/oauth-signout-playbook.md) - Remote OAuth diagnostic runbook +- [operations/security.md](operations/security.md) - Authentication and security patterns +- [operations/stdio-auth.md](operations/stdio-auth.md) - Device code auth and token caching +- [operations/token-cost-tracking.md](operations/token-cost-tracking.md) - Tool definition token cost tracking + +### Cloudflare + +- [cloudflare/overview.md](cloudflare/overview.md) - Cloudflare package overview +- [cloudflare/architecture.md](cloudflare/architecture.md) - Cloudflare architecture +- [cloudflare/oauth-architecture.md](cloudflare/oauth-architecture.md) - Cloudflare OAuth architecture + +### Integrations -- Doc map and workflow: see `CLAUDE.md` / `AGENTS.md` -- Per-topic guides live in this folder (e.g., `adding-tools.md`) +- [integrations/claude-code-plugin.md](integrations/claude-code-plugin.md) - Plugin structure and agent prompts +- [integrations/flue-hooks.md](integrations/flue-hooks.md) - Flue hook notes +- [integrations/ide-instructions-refactor.md](integrations/ide-instructions-refactor.md) - IDE instruction refactor notes -## Integration with Tools +### Specs -- Cursor IDE: this folder is referenced directly as contextual rules -- Other AI tools: reference specific `.md` files as needed +- [specs/README.md](specs/README.md) - Specs index +- [specs/embedded-agent-openai-routing.md](specs/embedded-agent-openai-routing.md) - Embedded agent OpenAI routing spec +- [specs/search-events.md](specs/search-events.md) - Search Events spec +- [specs/subpath-constraints.md](specs/subpath-constraints.md) - Subpath constraints spec -## LLM-Specific +### Releases -- Meta-docs live under `llms/` (e.g., `llms/document-scopes.md`) +- [releases/stdio.md](releases/stdio.md) - npm package release +- [releases/cloudflare.md](releases/cloudflare.md) - Cloudflare deployment ## Maintenance -Update docs when patterns change, new tools are added, or common issues arise. Keep the index in `CLAUDE.md` authoritative; avoid mirroring it here. +Update docs when patterns change, new tools are added, or common issues arise. +Prefer cross-links over duplicated guidance: topic docs should link to the +canonical policy or pattern that owns the detail. diff --git a/docs/architecture.md b/docs/architecture/overview.md similarity index 99% rename from docs/architecture.md rename to docs/architecture/overview.md index 1cd58fab0..cd0347b96 100644 --- a/docs/architecture.md +++ b/docs/architecture/overview.md @@ -74,7 +74,7 @@ A separate web chat application that uses the MCP server. **Note**: This is NOT part of the MCP server itself - it's a demonstration of how to build a chat interface that consumes MCP. -See "Overview" in @docs/cloudflare/overview.md for details. +See "Overview" in [Cloudflare Overview](../cloudflare/overview.md) for details. ### packages/mcp-server-evals diff --git a/docs/cloudflare/architecture.md b/docs/cloudflare/architecture.md index eeaa72cce..122e9ed12 100644 --- a/docs/cloudflare/architecture.md +++ b/docs/cloudflare/architecture.md @@ -183,7 +183,7 @@ SENTRY_CLIENT_SECRET = "..." # OAuth app secret ## Related Documentation -- See "OAuth Architecture" in @docs/cloudflare/oauth-architecture.md -- See "Chat Interface" in @docs/cloudflare/architecture.md -- See "Deployment" in @docs/cloudflare/deployment.md -- See "Architecture" in @docs/architecture.md +- See "OAuth Architecture" in [OAuth Architecture](oauth-architecture.md) +- See "Chat Interface" in this document +- See "Deployment" in [Cloudflare Release](../releases/cloudflare.md) +- See "Architecture" in [Architecture Overview](../architecture/overview.md) diff --git a/docs/cloudflare/overview.md b/docs/cloudflare/overview.md index 62b9f3ff5..d5256b4c3 100644 --- a/docs/cloudflare/overview.md +++ b/docs/cloudflare/overview.md @@ -35,13 +35,13 @@ Think of it as: ## Documentation Structure -- Architecture: @docs/cloudflare/architecture.md — Technical architecture of the web application -- OAuth Architecture: @docs/cloudflare/oauth-architecture.md — OAuth flow and token management -- Chat Interface: @docs/cloudflare/architecture.md — See "Chat Interface" section -- Deployment: @docs/cloudflare/deployment.md — Deploying to Cloudflare Workers +- Architecture: [Cloudflare Architecture](architecture.md) - Technical architecture of the web application +- OAuth Architecture: [OAuth Architecture](oauth-architecture.md) - OAuth flow and token management +- Chat Interface: [Cloudflare Architecture](architecture.md) - See "Chat Interface" section +- Deployment: [Cloudflare Release](../releases/cloudflare.md) - Deploying to Cloudflare Workers ## Quick Links - Live deployment: https://mcp.sentry.dev - Package location: `packages/mcp-cloudflare` -- **For MCP Server docs**: See "Architecture" in @docs/architecture.md +- **For MCP Server docs**: See "Architecture" in [Architecture Overview](../architecture/overview.md) diff --git a/docs/adding-tools.md b/docs/contributing/adding-tools.md similarity index 93% rename from docs/adding-tools.md rename to docs/contributing/adding-tools.md index aa2e8c3fa..8ec27ab74 100644 --- a/docs/adding-tools.md +++ b/docs/contributing/adding-tools.md @@ -178,14 +178,22 @@ async handler(params, context: ServerContext) { ### Response Formatting -See [common-patterns.md](common-patterns.md#response-formatting) for: -- Markdown structure -- ID/URL formatting -- Response notes guidance +Tool responses must follow [tool-responses.md](tool-responses.md). In +particular: + +- Format output as user-facing markdown, not raw upstream API payloads. +- Include actionable IDs and URLs when they support navigation or follow-up tool + calls. +- Omit internal implementation details, empty placeholder values, and raw JSON + unless the tool explicitly exists to return raw data. +- Keep response notes scoped to this result. + +See [common-patterns.md](common-patterns.md#response-formatting) for shared +formatting patterns and examples. ## Step 3: Add Tests -Follow comprehensive testing patterns from `testing.md` for unit, integration, and evaluation tests. +Follow comprehensive testing patterns from [../testing/overview.md](../testing/overview.md) for unit, integration, and evaluation tests. Create `packages/mcp-core/src/tools/catalog/your-tool-name.test.ts`: @@ -214,12 +222,14 @@ describe("your_tool_name", () => { ``` **Testing Requirements:** -- Input validation (see [testing.md](testing.md#testing-error-cases)) +- Input validation (see [../testing/overview.md](../testing/overview.md#testing-error-cases)) - Error handling (use patterns from [error-handling.md](error-handling.md)) - Output formatting with snapshots - At least one happy-path test must snapshot the full formatted handler response with `toMatchInlineSnapshot()`; partial `toContain()` assertions are supplemental only +- Review snapshots against [tool-responses.md](tool-responses.md), including + checks for internal IDs, raw JSON, empty placeholders, and user-facing labels - API integration with MSW mocks **After changing output, update snapshots:** @@ -470,8 +480,9 @@ This pattern works with both Cloudflare-hosted and stdio transports. - Error handling: [error-handling.md](error-handling.md) - API usage: `api-patterns.md` -- Testing: `testing.md` -- Response formatting: [common-patterns.md](common-patterns.md#response-formatting) +- Testing: [../testing/overview.md](../testing/overview.md) +- Response policy: [tool-responses.md](tool-responses.md) +- Formatting patterns: [common-patterns.md](common-patterns.md#response-formatting) ## References diff --git a/docs/api-patterns.md b/docs/contributing/api-patterns.md similarity index 90% rename from docs/api-patterns.md rename to docs/contributing/api-patterns.md index fc2e5dbc2..b256801eb 100644 --- a/docs/api-patterns.md +++ b/docs/contributing/api-patterns.md @@ -19,7 +19,9 @@ const api = new SentryApiService({ }); ``` -See: `packages/mcp-server/src/api-utils.ts` and `adding-tools.md#step-2-implement-the-handler` for usage in tools. +See `packages/mcp-server/src/api-utils.ts` and +[adding-tools.md](adding-tools.md#step-2-implement-the-handler) for usage in +tools. ### Common Operations @@ -79,11 +81,11 @@ const FlexibleSchema = BaseSchema z.union([DateSchema, z.null()]) ``` -See Zod patterns: `common-patterns.md#zod-schema-patterns` +See Zod patterns in [common-patterns.md](common-patterns.md#zod-schema-patterns). ### Type Safety -For testing API patterns, see `testing.md#mock-server-setup` +For testing API patterns, see [../testing/overview.md](../testing/overview.md#mock-server-setup). ```typescript // Derive types from schemas @@ -202,7 +204,7 @@ try { } ``` -See error patterns: `common-patterns.md#error-handling` +See error patterns in [common-patterns.md](common-patterns.md#error-handling). ## Best Practices @@ -217,4 +219,4 @@ See error patterns: `common-patterns.md#error-handling` - API Client: `packages/mcp-server/src/api-client/` - Mock handlers: `packages/mcp-server-mocks/src/handlers/` - Fixtures: `packages/mcp-server-mocks/src/fixtures/` -- API Utils: `packages/mcp-server/src/api-utils.ts` \ No newline at end of file +- API Utils: `packages/mcp-server/src/api-utils.ts` diff --git a/docs/coding-guidelines.md b/docs/contributing/coding-guidelines.md similarity index 82% rename from docs/coding-guidelines.md rename to docs/contributing/coding-guidelines.md index ac53688eb..3c4ea012c 100644 --- a/docs/coding-guidelines.md +++ b/docs/contributing/coding-guidelines.md @@ -121,10 +121,10 @@ pnpm -w run build # Build all ## Common Patterns For shared patterns see: -- Error handling: `common-patterns.md#error-handling` -- Zod schemas: `common-patterns.md#zod-schema-patterns` -- API usage: `api-patterns.md` -- Testing: `testing.md` +- Error handling: [common-patterns.md](common-patterns.md#error-handling) +- Zod schemas: [common-patterns.md](common-patterns.md#zod-schema-patterns) +- API usage: [api-patterns.md](api-patterns.md) +- Testing: [../testing/overview.md](../testing/overview.md) ## Monorepo Commands @@ -138,7 +138,7 @@ pnpm test ## References -- Architecture: `architecture.md` -- Testing guide: `testing.md` -- API patterns: `api-patterns.md` -- Common patterns: `common-patterns.md` \ No newline at end of file +- Architecture: [../architecture/overview.md](../architecture/overview.md) +- Testing guide: [../testing/overview.md](../testing/overview.md) +- API patterns: [api-patterns.md](api-patterns.md) +- Common patterns: [common-patterns.md](common-patterns.md) diff --git a/docs/common-patterns.md b/docs/contributing/common-patterns.md similarity index 92% rename from docs/common-patterns.md rename to docs/contributing/common-patterns.md index ed0c80f68..bb22f7a0c 100644 --- a/docs/common-patterns.md +++ b/docs/contributing/common-patterns.md @@ -59,6 +59,10 @@ Tool result text can include light, scoped steering when it helps the assistant - Avoid: `IMPORTANT`, `MUST`, `CRITICAL`, `Display these...`, or `# Using this information` in handler output. - Avoid: instructions that override assistant behavior beyond this result. +For the complete response contract, including what to include, what to omit, +snapshot review expectations, and QA expectations, see +[tool-responses.md](tool-responses.md). + ### Markdown Structure ```typescript @@ -139,5 +143,6 @@ export type ToolName = typeof TOOL_NAMES[number]; - Error handling: [error-handling.md](error-handling.md) - API patterns: [api-patterns.md](api-patterns.md) -- Testing: [testing.md](testing.md) +- Tool responses: [tool-responses.md](tool-responses.md) +- Testing: [../testing/overview.md](../testing/overview.md) - Quality checks: [quality-checks.md](quality-checks.md) diff --git a/docs/llms/documentation-style-guide.md b/docs/contributing/documentation-style-guide.md similarity index 77% rename from docs/llms/documentation-style-guide.md rename to docs/contributing/documentation-style-guide.md index e4a103afd..ca04259d0 100644 --- a/docs/llms/documentation-style-guide.md +++ b/docs/contributing/documentation-style-guide.md @@ -1,11 +1,12 @@ # Documentation Style Guide -This guide defines how to write effective documentation for LLMs working with the Sentry MCP codebase. +This guide defines how to write effective documentation for humans and AI +agents working with the Sentry MCP codebase. ## Core Principles ### 1. Assume Intelligence -- LLMs understand programming concepts - don't explain basics +- Readers understand programming concepts - don't explain basics - Focus on project-specific patterns and conventions - Skip obvious steps like "create a file" or "save your changes" @@ -17,7 +18,7 @@ This guide defines how to write effective documentation for LLMs working with th ### 3. Show, Don't Tell - Include minimal, focused code examples -- Reference actual implementations: `See @packages/mcp-server/src/server.ts:45` +- Reference actual implementations: `packages/mcp-server/src/server.ts` - Use real patterns from the codebase ## Document Structure @@ -48,12 +49,12 @@ Project-specific rules that must be followed. ## Common Patterns -Link to reusable patterns: See "Error Handling" in @docs/common-patterns.md +Link to reusable patterns: See "Error Handling" in [Common Patterns](common-patterns.md). ## References -- Implementation: `@packages/mcp-server/src/[file].ts` -- Tests: `@packages/mcp-server/src/[file].test.ts` +- Implementation: `packages/mcp-server/src/[file].ts` +- Tests: `packages/mcp-server/src/[file].test.ts` - Examples in codebase: [specific function/tool names] ``` @@ -71,7 +72,7 @@ Link to reusable patterns: See "Error Handling" in @docs/common-patterns.md - **Tool documentation** - How to use pnpm or Vitest - **Verbose examples** - Keep code samples minimal - **Redundant content** - Link to other docs instead -- **Step-by-step tutorials** - LLMs don't need hand-holding +- **Step-by-step tutorials** - Prefer concise project-specific procedures ## Code Examples @@ -103,17 +104,17 @@ export const ParamOrganizationSlug = z ## Cross-References ### File References (MANDATORY): -- Use @path syntax for local files: `@docs/common-patterns.md` -- Always reference from repo root: `@packages/mcp-server/src/server.ts` -- Do NOT use Markdown links for local files (avoid markdown `[text](./...)` patterns) -- Prefer path-only mentions to help agents parse +- Use Markdown links for documentation files: `[Common Patterns](common-patterns.md)` +- Use repo-root relative paths in backticks for code files: `packages/mcp-server/src/server.ts` +- Do not use at-prefixed local references; some agents inline the entire target document. +- Prefer clear link text for docs and concrete path mentions for code. ### Section References: -- Refer to sections by name, not anchors: `See "Error Handling" in @docs/common-patterns.md` -- If multiple sections share a name, include a short hint: `("Zod Patterns" in @docs/common-patterns.md)` +- Refer to sections by name, not anchors: `See "Error Handling" in [Common Patterns](common-patterns.md).` +- If multiple sections share a name, include a short hint: `("Zod Patterns" in [Common Patterns](common-patterns.md))` ### Code References: -- Use concrete paths and identifiers: `@packages/mcp-core/src/tools/catalog/search-events.ts:buildQuery` +- Use concrete paths and identifiers: `packages/mcp-core/src/tools/catalog/search-events.ts:buildQuery` - Optional line hints for humans: `server.ts:45-52` (agents may ignore) - Prefer real implementations over fabricated examples @@ -159,7 +160,7 @@ export const ParamOrganizationSlug = z ### Red Flags: - Verbose prose explaining what code could show -- Repeated content → extract to common-patterns.md +- Repeated content → extract to [Common Patterns](common-patterns.md) - No code references → add implementation examples - Generic programming advice → remove it - Multiple concepts in one doc → split by topic @@ -187,15 +188,16 @@ pnpm install cp .env.example .env # Add your API keys ``` -See "Development Setup" in @AGENTS.md for environment variables. +See "Development Setup" in [AGENTS.md](../../AGENTS.md) for environment variables. ``` -## Agent Readability Checklist +## Readability Checklist -- Uses @path for all local file references +- Uses Markdown links for docs and repo-root relative paths for code - Short, focused sections with concrete examples - Minimal prose; prefers code and commands - Clear preconditions and environment notes - Error handling and validation rules are explicit -This style guide ensures documentation remains focused, valuable, and maintainable for LLM consumption. +This style guide keeps documentation focused, valuable, and maintainable for +both human contributors and AI agents. diff --git a/docs/error-handling.md b/docs/contributing/error-handling.md similarity index 100% rename from docs/error-handling.md rename to docs/contributing/error-handling.md diff --git a/docs/pr-management.md b/docs/contributing/pr-management.md similarity index 100% rename from docs/pr-management.md rename to docs/contributing/pr-management.md diff --git a/docs/quality-checks.md b/docs/contributing/quality-checks.md similarity index 73% rename from docs/quality-checks.md rename to docs/contributing/quality-checks.md index 06f99f097..a5b3b2601 100644 --- a/docs/quality-checks.md +++ b/docs/contributing/quality-checks.md @@ -16,7 +16,10 @@ See [adding-tools.md](adding-tools.md#tool-count-limits) for current limits and ## Testing Requirements -See [testing.md](testing.md) for testing philosophy, patterns, and snapshot guidelines. Every tool must have at least one happy-path inline snapshot test. +See [../testing/overview.md](../testing/overview.md) for testing philosophy, +patterns, and snapshot guidelines. Every tool must have at least one +happy-path inline snapshot test. Review tool output snapshots against +[tool-responses.md](tool-responses.md). ## Pre-Commit Checklist diff --git a/docs/search-events-api-patterns.md b/docs/contributing/search-events-api-patterns.md similarity index 100% rename from docs/search-events-api-patterns.md rename to docs/contributing/search-events-api-patterns.md diff --git a/docs/contributing/tool-responses.md b/docs/contributing/tool-responses.md new file mode 100644 index 000000000..146319f57 --- /dev/null +++ b/docs/contributing/tool-responses.md @@ -0,0 +1,168 @@ +# Tool Responses + +Tool responses are product UX for agents and users. A response should help an +assistant answer a real Sentry question without forcing it to understand +upstream API shapes, internal implementation details, or placeholder values. + +Use this policy when adding a tool, changing formatted handler output, updating +snapshots, or QAing MCP behavior. For the full tool implementation flow, see +[adding-tools.md](adding-tools.md). For snapshot requirements, see +[../testing/overview.md](../testing/overview.md). For end-to-end MCP +validation, see [.agents/skills/qa/SKILL.md](../../.agents/skills/qa/SKILL.md). + +## Response Contract + +Every formatted tool response should optimize for: + +- **Usefulness**: Include the fields someone needs to inspect, compare, explain, + or act on the resource. +- **Legibility**: Use Sentry product terms and stable markdown sections. +- **Actionability**: Include IDs, URLs, pagination cursors, and follow-up tool + hints only when they help navigation or the next tool call. +- **Safety**: Do not expose secrets, credentials, raw tokens, or unrelated + internal details. Follow [../operations/security.md](../operations/security.md) + for security boundaries. +- **Agent fit**: The output should be easy for an LLM to quote, summarize, and + reason over without post-processing raw API payloads. + +## Markdown Shape + +Use a predictable markdown structure: + +```markdown +# Resource Type in **scope** + +## Resource Name + +**Kind**: Issue Alert +**ID**: 123 +**Project**: backend +**Status**: enabled +**URL**: https://example.sentry.io/... + +### Conditions + +- Event frequency count (comparison: 10, result: true) + +## Response Notes + +- Use `get_alert_rule` with `kind` and the numeric rule ID for full details. +``` + +Guidelines: + +- Start with a clear `#` title that names the resource set or detail scope. +- Use `##` and `###` sections for repeated resource groups and details. +- Prefer bold labels for scalar facts. +- Prefer bullets or compact tables for repeated items. +- Omit sections that have no meaningful data unless the absence itself matters. +- Use `## Response Notes` only for scoped, result-specific guidance. + +See [common-patterns.md](common-patterns.md) for shared schema and validation +patterns used alongside response formatting. + +## Include + +Include data that is useful for real user questions: + +- Resource identity: name, kind, ID, org, project, slug, status. +- Navigation: web URLs, dashboard URLs, issue URLs, or monitor URLs. +- Operational state: owner, assignee, environment, release, last seen, + last triggered, frequency, priority, or status. +- User-facing configuration: alert conditions, filters, triggers, actions, + routing, notification targets, dashboard widgets, monitor schedules. +- Follow-up handles: IDs or cursors needed for a documented next tool call. +- Timestamps when they answer real freshness, history, or audit questions. + +## Avoid + +Do not leak implementation details into ordinary tool output: + +- Raw API JSON unless the tool explicitly returns raw data. +- Internal IDs that are not useful follow-up handles, such as workflow component + IDs, detector IDs, synthetic filter IDs, or trace plumbing identifiers. +- Placeholder noise such as `null`, `undefined`, empty arrays, empty objects, + empty strings, `data:`, or `target display: unknown`. +- Upstream field names when a user-facing label is obvious, such as + `conditionResult` instead of `result`. +- Empty sections created only because the API has that property. +- Long opaque payloads that require the model to reverse-engineer meaning. +- Broad assistant instructions such as `IMPORTANT`, `MUST`, `CRITICAL`, or + output that tries to override behavior beyond the current result. + +Tool descriptions and parameter `.describe()` strings remain the right place +for durable tool-selection guidance. Result text can include light scoped +guidance, but it should not act like a system prompt. See "Response Formatting" +in [common-patterns.md](common-patterns.md) for examples of acceptable response +notes. + +## Formatting Upstream Data + +Translate upstream API shapes into user-facing terms: + +- Humanize machine names: `event_frequency_count` -> `Event frequency count`. +- Humanize keys: `conditionResult` -> `result`, `targetIdentifier` -> `target`. +- Flatten nested config only when it improves readability. +- Preserve values exactly when they are user-entered text, slugs, queries, or + identifiers needed for follow-up. +- Drop empty, nullish, or unknown placeholder values before rendering details. +- Cap long repeated lists and say how many additional items were omitted. +- Prefer shared helpers for dates, actors, IDs, and unknown values. If a helper + emits noisy placeholders for a domain-specific field, filter before calling it. + +When changing Sentry API endpoint usage, validate the upstream behavior in +`~/src/sentry` as required by [../../AGENTS.md](../../AGENTS.md). API schemas +should model what Sentry returns, but tool responses should model what users +need. + +## Response Notes + +Use response notes for narrow, operational guidance: + +- Good: `Use get_alert_rule with kind and the numeric rule ID for full details.` +- Good: `More results are available. Pass cursor: "..." with the same scope.` +- Good: `Use these details to inspect alert conditions, filters, routing, and notification actions before changing the rule in Sentry.` + +Avoid implementation jargon: + +- Avoid: `Treat the returned payload as the canonical source for mutation workflows.` +- Avoid: `Inspect detector IDs before constructing workflow payloads.` +- Avoid: `Display these results exactly as written.` + +## Snapshot Policy + +Every MCP tool test suite must include at least one representative successful +call that snapshots the full formatted handler response. This requirement is +defined in [../testing/overview.md](../testing/overview.md) and applies to both +new tools and meaningful output changes. + +When reviewing snapshots: + +- Review them as user-facing product output, not only as changed strings. +- Confirm the output includes the fields needed for common real-world questions. +- Confirm internal IDs, raw JSON, and placeholder values are absent unless they + are intentionally part of the contract. +- Add targeted negative assertions for known regression risks, such as raw + workflow JSON or stale internal fields. +- Include representative upstream internals in fixtures when migrations need to + prove that formatting cleans them up. + +Partial `toContain()` assertions are useful for branch-specific behavior, but +they do not replace a full-response snapshot. + +## QA Policy + +For output-format changes: + +- Run the normal quality gate from [quality-checks.md](quality-checks.md). +- Use the stdio MCP QA path in + [.agents/skills/qa/SKILL.md](../../.agents/skills/qa/SKILL.md). +- Inspect the raw MCP tool result when possible, not only the LLM's final + answer. The test client's final answer can add model-specific phrasing that is + not part of the tool response. +- Use a realistic prod prompt that asks for the fields the changed tool should + support. + +If the raw tool result is clean but the agent final answer adds unrelated +content, treat that as a client/agent prompt issue rather than a tool response +formatting issue. diff --git a/docs/claude-code-plugin.md b/docs/integrations/claude-code-plugin.md similarity index 100% rename from docs/claude-code-plugin.md rename to docs/integrations/claude-code-plugin.md diff --git a/docs/flue-hooks.md b/docs/integrations/flue-hooks.md similarity index 100% rename from docs/flue-hooks.md rename to docs/integrations/flue-hooks.md diff --git a/docs/ide-instructions-refactor.md b/docs/integrations/ide-instructions-refactor.md similarity index 100% rename from docs/ide-instructions-refactor.md rename to docs/integrations/ide-instructions-refactor.md diff --git a/docs/llms/README.md b/docs/llms/README.md deleted file mode 100644 index c6e196d3b..000000000 --- a/docs/llms/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# LLM-Specific Documentation - -This directory contains meta-documentation specifically for LLMs working with the Sentry MCP codebase. - -## Contents - -### documentation-style-guide.md -Guidelines for writing effective documentation that LLMs can consume efficiently. Defines principles like assuming intelligence, being concise, and showing rather than telling. - -### document-scopes.md -Defines the specific purpose, content requirements, and line count targets for each documentation file. Helps maintain focus and prevent scope creep. - -### documentation-todos.md -Specific tasks for improving each document based on the style guide and scope definitions. Tracks the documentation refactoring effort. - -## Purpose - -These documents help ensure that: -- Documentation remains concise and focused -- LLMs get project-specific information, not general programming knowledge -- Redundancy is minimized through proper cross-referencing -- Each document has a clear, defined purpose - -## For Human Contributors - -While these documents are designed for LLM consumption, they also serve as excellent guidelines for human contributors who want to understand: -- How to write documentation for this project -- What belongs in each document -- How to maintain consistency across docs \ No newline at end of file diff --git a/docs/llms/document-scopes.md b/docs/llms/document-scopes.md deleted file mode 100644 index b72a6097a..000000000 --- a/docs/llms/document-scopes.md +++ /dev/null @@ -1,224 +0,0 @@ -# Document Scopes - -Defines the specific purpose and content for each documentation file. - -## Reference Style (MANDATORY) - -- Use @path for all local file references, repo-root relative (e.g., `@packages/mcp-server/src/server.ts`). -- Refer to sections by name: `See "Error Handling" in @docs/common-patterns.md`. -- Keep Markdown links only for external sites. - -## Core Documents - -### architecture.md -**Purpose**: Explain system design and package interactions - -**Must Include**: -- Package responsibilities and boundaries -- Data flow between components -- Key architectural decisions and trade-offs -- How MCP concepts map to implementation - -**Must Exclude**: -- Installation instructions -- Implementation details (link to code instead) -- General MCP protocol explanation - -### common-patterns.md -**Purpose**: Reusable patterns used throughout the codebase - -**Must Include**: -- Error handling patterns (UserInputError, ApiError) -- Zod schema patterns and conventions -- TypeScript type helpers -- Response formatting patterns -- Parameter validation patterns - -**Must Exclude**: -- Tool/prompt/resource-specific patterns -- External library documentation -- One-off patterns used in single places - -### quality-checks.md -**Purpose**: Required checks and commands for code quality - -**Must Include**: -- Essential commands that must pass -- When to run each check -- What each check validates -- Common failure fixes - -**Must Exclude**: -- Tool installation instructions -- Detailed explanations of what linting is -- CI/CD configuration - -## Feature Implementation Guides - -### adding-tools.md -**Purpose**: How to add new MCP tools - -**Must Include**: -- Tool definition structure -- Handler implementation pattern -- Required tests (unit + eval) -- LLM-friendly descriptions -- References to existing tools - -**Must Exclude**: -- What MCP tools are conceptually -- Duplicate testing patterns (link to testing.md) -- Full code examples (reference real implementations) - -## Technical Guides - -### testing.md -**Purpose**: Testing strategies and patterns - -**Must Include**: -- Unit test patterns with snapshots -- Evaluation test setup -- Mock patterns with MSW -- When to update snapshots -- Test file organization - -**Must Exclude**: -- Vitest documentation -- General testing philosophy -- Duplicate mock examples - -### api-patterns.md -**Purpose**: Sentry API client usage and mocking - -**Must Include**: -- apiServiceFromContext pattern -- Schema definitions with Zod -- Mock handler patterns -- Multi-region support -- Error handling - -**Must Exclude**: -- HTTP basics -- Zod library documentation -- Duplicate error patterns (link to common-patterns.md) - -## Operations Guides - -### releases/cloudflare.md -**Purpose**: Cloudflare Workers release process - -**Must Include**: -- Wrangler configuration -- Environment variables -- MCP handler setup -- OAuth provider configuration -- Deployment commands (manual and automated) -- Version uploads and gradual rollouts -- Monitoring and troubleshooting - -**Must Exclude**: -- Cloudflare Workers concepts -- General deployment best practices -- npm package release process (see stdio.md) - -### releases/stdio.md -**Purpose**: npm package release process - -**Must Include**: -- Version management -- npm publishing workflow -- User installation instructions (Claude Desktop, Cursor) -- Environment variable configuration -- Testing releases locally -- Beta releases - -**Must Exclude**: -- Cloudflare deployment (see cloudflare.md) -- General npm documentation -- IDE-specific setup details - -### monitoring.md -**Purpose**: Observability and instrumentation - -**Must Include**: -- Sentry integration patterns -- Telemetry setup -- Error tracking -- Performance monitoring -- Tag conventions - -**Must Exclude**: -- What observability is -- Sentry product documentation -- General monitoring concepts - -### security.md -**Purpose**: Authentication and security patterns - -**Must Include**: -- OAuth implementation -- Token management -- Multi-tenant security -- CORS configuration -- Security headers - -**Must Exclude**: -- OAuth protocol explanation -- General security best practices -- Duplicate deployment content - -## Meta Documents - -### README.md -**Purpose**: Documentation index and navigation - -**Must Include**: -- Document listing with one-line descriptions -- Quick reference for common tasks -- Links to style guide and scopes - -**Must Exclude**: -- Detailed explanations -- Duplicate content from other docs -- Installation instructions - -### AGENTS.md -**Purpose**: Agent entry point (Claude Code, Cursor, etc.) - -**Must Include**: -- Brief project description -- Documentation directory reference -- Critical quality checks -- Agent-specific notes (tools, transports, auth defaults) - -**Must Exclude**: -- Detailed architecture (link to architecture.md) -- Development setup (link to relevant docs) -- Integration instructions (keep minimal) - -### claude-code-plugin.md -**Purpose**: Claude Code plugin structure and maintenance - -**Must Include**: -- Plugin directory layout and file roles -- Agent frontmatter fields (`description`, `mcpServers`, `allowedTools`) -- How `generate-definitions.ts` syncs `allowedTools` -- How to modify agent prompts -- Difference between stable and experimental variants - -**Must Exclude**: -- User installation instructions (covered in README.md) -- Claude Code plugin system documentation -- Tool implementation details (link to adding-tools.md) - -## Optimization Strategy - -Focus on clarity and usefulness, not arbitrary line counts. - -Key improvements needed: -- **security.md**: Too much OAuth theory → focus on implementation -- **api-patterns.md**: Redundant examples → consolidate patterns -- **releases/cloudflare.md**: Focus on MCP-specific config, not generic Cloudflare docs -- **monitoring.md**: Verbose explanations → code examples - -The goal: Each document should be focused enough to be useful in a single context window while remaining comprehensive for its topic. diff --git a/docs/embedded-agents.md b/docs/operations/embedded-agents.md similarity index 95% rename from docs/embedded-agents.md rename to docs/operations/embedded-agents.md index 53b0821b1..a7a6247d7 100644 --- a/docs/embedded-agents.md +++ b/docs/operations/embedded-agents.md @@ -270,6 +270,7 @@ Using openai responses API for AI-powered search tools (from EMBEDDED_AGENT_PROV ## Related Documentation -- [Security](./security.md) - Authentication and token management -- [Testing](./testing.md) - Testing MCP server functionality -- [Common Patterns](./common-patterns.md) - Error handling and response formatting +- [Security](security.md) - Authentication and token management +- [Testing](../testing/overview.md) - Testing MCP server functionality +- [Common Patterns](../contributing/common-patterns.md) - Error handling and shared formatting patterns +- [Tool Responses](../contributing/tool-responses.md) - Tool output policy and QA review checklist diff --git a/docs/github-actions.md b/docs/operations/github-actions.md similarity index 100% rename from docs/github-actions.md rename to docs/operations/github-actions.md diff --git a/docs/logging.md b/docs/operations/logging.md similarity index 84% rename from docs/logging.md rename to docs/operations/logging.md index 24ec86074..e1694a2ee 100644 --- a/docs/logging.md +++ b/docs/operations/logging.md @@ -1,6 +1,6 @@ # Logging Reference -How logging works in the Sentry MCP server using LogTape and Sentry. For tracing, spans, or metrics see @docs/monitoring.md. +How logging works in the Sentry MCP server using LogTape and Sentry. For tracing, spans, or metrics see [Monitoring](monitoring.md). ## Overview @@ -14,13 +14,13 @@ Log levels are controlled by: **Important**: We use a custom LogTape sink that calls `Sentry.logger.*` methods to send logs to Sentry's Logs product. The `@logtape/sentry` package uses `captureException/captureMessage` which creates Issues instead of Logs. -Implementation: @packages/mcp-server/src/telem/logging.ts +Implementation: `packages/mcp-server/src/telem/logging.ts` ## Stdio Transport Invariant: Logs MUST Go to Stderr The MCP stdio transport reserves **stdout** for JSON-RPC frames. Any non-JSON-RPC line written to stdout makes the client fail framing and close the transport. **All log output — every level, every sink that writes to a console — must go to stderr.** -This is enforced in @packages/mcp-core/src/telem/logging.ts via `STDERR_CONSOLE_LEVEL_MAP`, which routes every LogTape level through `console.error` (Node sends `console.error`/`console.warn` to stderr; `console.log`/`console.info`/`console.debug` go to stdout). Severity is preserved inside the JSON record (e.g. `"level":"INFO"`); the map only controls which `console.*` method is invoked. +This is enforced in `packages/mcp-core/src/telem/logging.ts` via `STDERR_CONSOLE_LEVEL_MAP`, which routes every LogTape level through `console.error` (Node sends `console.error`/`console.warn` to stderr; `console.log`/`console.info`/`console.debug` go to stdout). Severity is preserved inside the JSON record (e.g. `"level":"INFO"`); the map only controls which `console.*` method is invoked. Regression history: #922 — LogTape's default level map sent `info`/`debug` records through `console.info`/`console.debug`, landing structured JSON on stdout and breaking stdio clients. @@ -98,7 +98,7 @@ const eventId = logIssue(error, { **Skip logging:** - `UserInputError` - Expected validation failures - 4xx API responses - Client errors (except 429 rate limits) -- See @docs/error-handling.md for complete rules +- See [Error Handling](../contributing/error-handling.md) for complete rules ## Log Options @@ -137,7 +137,7 @@ app.use(createRequestLogger(["cloudflare", "http"])); ## References -- Implementation: @packages/mcp-server/src/telem/logging.ts -- Error handling patterns: @docs/error-handling.md -- Monitoring and tracing: @docs/monitoring.md +- Implementation: `packages/mcp-server/src/telem/logging.ts` +- Error handling patterns: [Error Handling](../contributing/error-handling.md) +- Monitoring and tracing: [Monitoring](monitoring.md) - LogTape docs: https://logtape.org/ diff --git a/docs/monitoring.md b/docs/operations/monitoring.md similarity index 98% rename from docs/monitoring.md rename to docs/operations/monitoring.md index 427f8e327..08eabc700 100644 --- a/docs/monitoring.md +++ b/docs/operations/monitoring.md @@ -14,7 +14,7 @@ Different Sentry SDKs for different environments: ### Error Logging -See `logIssue` in @packages/mcp-server/src/telem/logging.ts (documented in @docs/logging.md) for the canonical way to create an Issue and structured log entry. +See `logIssue` in `packages/mcp-server/src/telem/logging.ts` (documented in [Logging Reference](logging.md)) for the canonical way to create an Issue and structured log entry. ### Tracing Pattern diff --git a/docs/oauth-signout-playbook.md b/docs/operations/oauth-signout-playbook.md similarity index 100% rename from docs/oauth-signout-playbook.md rename to docs/operations/oauth-signout-playbook.md diff --git a/docs/security.md b/docs/operations/security.md similarity index 100% rename from docs/security.md rename to docs/operations/security.md diff --git a/docs/stdio-auth.md b/docs/operations/stdio-auth.md similarity index 100% rename from docs/stdio-auth.md rename to docs/operations/stdio-auth.md diff --git a/docs/token-cost-tracking.md b/docs/operations/token-cost-tracking.md similarity index 97% rename from docs/token-cost-tracking.md rename to docs/operations/token-cost-tracking.md index dc757d546..7e45188da 100644 --- a/docs/token-cost-tracking.md +++ b/docs/operations/token-cost-tracking.md @@ -126,4 +126,5 @@ tsx measure-token-cost.ts --help # Show help - Script: `packages/mcp-core/scripts/measure-token-cost.ts` - Workflow: `.github/workflows/token-cost.yml` -- Tool limits: See "Tool Count Limits" in `docs/adding-tools.md` +- Tool limits: See "Tool Count Limits" in + [../contributing/adding-tools.md](../contributing/adding-tools.md) diff --git a/docs/specs/subpath-constraints.md b/docs/specs/subpath-constraints.md index c21604e80..370cc8c7f 100644 --- a/docs/specs/subpath-constraints.md +++ b/docs/specs/subpath-constraints.md @@ -76,5 +76,5 @@ Supported URL patterns: ``` For implementation details and security notes, see: -- `docs/cloudflare/constraint-flow-verification.md` -- `docs/architecture.md` +- `docs/specs/subpath-constraints.md` +- `docs/architecture/overview.md` diff --git a/docs/testing.md b/docs/testing/overview.md similarity index 87% rename from docs/testing.md rename to docs/testing/overview.md index 48d7d5457..9ce045cbb 100644 --- a/docs/testing.md +++ b/docs/testing/overview.md @@ -79,6 +79,9 @@ Fast, focused tests of actual functionality: formatted handler response with `toMatchInlineSnapshot()`. Supplemental `toContain()` assertions are fine, but they do not replace a full-response snapshot. +- Review tool output snapshots against + [../contributing/tool-responses.md](../contributing/tool-responses.md) so + formatted output stays user-facing and avoids raw internals. ### 2. Evaluation Tests Real-world scenarios with LLM: @@ -159,7 +162,7 @@ For Codex, inspect the debug output for `UnexpectedContentType`, `AuthRequired`, ## Functional Testing Patterns -See `adding-tools.md#step-3-add-tests` for the complete tool testing workflow. +See [../contributing/adding-tools.md](../contributing/adding-tools.md#step-3-add-tests) for the complete tool testing workflow. ### Basic Test Structure @@ -180,7 +183,7 @@ describe("tool_name", () => { }); ``` -**NOTE**: Follow error handling patterns from [error-handling.md](error-handling.md) when testing error cases. +**NOTE**: Follow error handling patterns from [../contributing/error-handling.md](../contributing/error-handling.md) when testing error cases. ### Testing Error Cases @@ -205,7 +208,7 @@ it("handles API errors gracefully", async () => { ## Mock Server Setup -See [api-patterns.md](api-patterns.md) for MSW mock setup, handler patterns, and request validation examples. +See [../contributing/api-patterns.md](../contributing/api-patterns.md) for MSW mock setup, handler patterns, and request validation examples. ## Snapshot Testing @@ -222,6 +225,10 @@ For MCP tools specifically: that snapshots the full handler response. - Use targeted substring assertions only for additional branch-specific checks, not as the only output coverage. +- Snapshot fixtures should include representative upstream internals when the + formatter is expected to hide or humanize them. +- Add negative assertions for known junk-output risks, such as raw JSON, + internal component IDs, or empty placeholder values. ### Updating Snapshots @@ -232,7 +239,10 @@ cd packages/mcp-server pnpm vitest --run -u ``` -**Always review snapshot changes before committing!** +**Always review snapshot changes before committing!** Review them as +user-facing tool output using +[../contributing/tool-responses.md](../contributing/tool-responses.md), not +just as mechanically updated strings. ### Snapshot Best Practices @@ -336,7 +346,12 @@ it("streams large responses efficiently", async () => { ## Common Testing Patterns -See [common-patterns.md](common-patterns.md) for parameter validation and response formatting patterns, [error-handling.md](error-handling.md) for error testing, and [api-patterns.md](api-patterns.md) for mock setup. +See [../contributing/common-patterns.md](../contributing/common-patterns.md) +for parameter validation and formatting patterns, +[../contributing/tool-responses.md](../contributing/tool-responses.md) for +tool response policy, [../contributing/error-handling.md](../contributing/error-handling.md) +for error testing, and [../contributing/api-patterns.md](../contributing/api-patterns.md) +for mock setup. ## CI/CD Integration diff --git a/docs/testing-remote.md b/docs/testing/remote.md similarity index 99% rename from docs/testing-remote.md rename to docs/testing/remote.md index 76a936f84..d4250096c 100644 --- a/docs/testing-remote.md +++ b/docs/testing/remote.md @@ -738,7 +738,7 @@ pnpm -w run cli --mcp-host=https://your-worker.workers.dev "who am I?" ## References -- Remote setup: `docs/cloudflare/deployment.md` +- Remote setup: `docs/releases/cloudflare.md` - OAuth architecture: `docs/cloudflare/oauth-architecture.md` - CLI client: `packages/mcp-test-client/README.md` - Cloudflare package: `packages/mcp-cloudflare/README.md` diff --git a/docs/testing-stdio.md b/docs/testing/stdio.md similarity index 100% rename from docs/testing-stdio.md rename to docs/testing/stdio.md diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index f5435492c..9c4ecb238 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -66,7 +66,6 @@ import { UserSchema, UserRegionsSchema, IssueAlertRuleListSchema, - IssueAlertRuleSchema, MetricAlertRuleListSchema, MetricAlertRuleSchema, FlamegraphSchema, @@ -149,6 +148,18 @@ function normalizeStatsPeriod(statsPeriod?: string): string | undefined { return normalized || undefined; } +function formatWorkflowNameQuery(query: string): string { + const escapedQuery = query.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + return `name:"*${escapedQuery}*"`; +} + +// Workflow projectSlug responses can include unattached org-level workflows. +function filterAttachedIssueAlertRules( + rules: IssueAlertRuleList, +): IssueAlertRuleList { + return rules.filter((rule) => (rule.detectorIds ?? []).length > 0); +} + function parseStatsPeriod(statsPeriod: string): { amount: number; unit: string; @@ -912,15 +923,13 @@ export class SentryApiService { getIssueAlertRuleUrl( organizationSlug: string, - projectSlug: string, ruleId: string | number, ): string { - const encodedProject = encodeURIComponent(projectSlug); const encodedRuleId = encodeURIComponent(String(ruleId)); if (this.isSaas()) { - return `${this.protocol}://${organizationSlug}.sentry.io/issues/alerts/rules/${encodedProject}/${encodedRuleId}/details/`; + return `${this.protocol}://${organizationSlug}.sentry.io/monitors/alerts/${encodedRuleId}/`; } - return `${this.protocol}://${this.host}/organizations/${organizationSlug}/issues/alerts/rules/${encodedProject}/${encodedRuleId}/details/`; + return `${this.protocol}://${this.host}/organizations/${organizationSlug}/monitors/alerts/${encodedRuleId}/`; } getMetricAlertRuleUrl( @@ -1748,50 +1757,47 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise<{ rules: IssueAlertRuleList; nextCursor: string | null }> { - if (query) { - const project = await this.getProject( - { - organizationSlug, - projectSlugOrId: projectSlug, - }, + const rules: IssueAlertRuleList = []; + const targetLimit = limit ?? 100; + let currentCursor: string | null | undefined = cursor; + let nextCursor: string | null = null; + + do { + const searchQuery = new URLSearchParams(); + searchQuery.append("projectSlug", projectSlug); + searchQuery.set("sortBy", "-id"); + if (query) { + searchQuery.set("query", formatWorkflowNameQuery(query)); + } + if (currentCursor) { + searchQuery.set("cursor", currentCursor); + } + if (limit !== undefined) { + searchQuery.set("per_page", String(targetLimit - rules.length)); + } + + const response = await this.request( + `/organizations/${organizationSlug}/workflows/?${searchQuery.toString()}`, + undefined, opts, ); - const body = await this.listCombinedAlertRules( - { - organizationSlug, - name: query, - alertType: "rule", - projectId: project.id, - cursor, - limit, - }, - opts, + const body = await this.parseJsonResponse(response); + const attachedRules = filterAttachedIssueAlertRules( + IssueAlertRuleListSchema.parse(body), ); - return { - rules: IssueAlertRuleListSchema.parse(body.data), - nextCursor: body.nextCursor, - }; - } + const remaining = targetLimit - rules.length; + if (attachedRules.length > remaining) { + rules.push(...attachedRules.slice(0, remaining)); + nextCursor = null; + break; + } - const searchQuery = new URLSearchParams(); - if (cursor) { - searchQuery.set("cursor", cursor); - } - if (limit !== undefined) { - searchQuery.set("per_page", String(limit)); - } + rules.push(...attachedRules); + nextCursor = getNextCursor(response.headers.get("link")); + currentCursor = nextCursor; + } while (rules.length < targetLimit && nextCursor); - const queryString = searchQuery.toString(); - const response = await this.request( - `/projects/${organizationSlug}/${projectSlug}/rules/${queryString ? `?${queryString}` : ""}`, - undefined, - opts, - ); - const body = await this.parseJsonResponse(response); - return { - rules: IssueAlertRuleListSchema.parse(body), - nextCursor: getNextCursor(response.headers.get("link")), - }; + return { rules, nextCursor }; } async getIssueAlertRule( @@ -1806,12 +1812,30 @@ export class SentryApiService { }, opts?: RequestOptions, ): Promise { - const body = await this.requestJSON( - `/projects/${organizationSlug}/${projectSlug}/rules/${encodeURIComponent(String(ruleId))}/`, + const searchQuery = new URLSearchParams(); + searchQuery.append("projectSlug", projectSlug); + searchQuery.append("id", String(ruleId)); + searchQuery.set("per_page", "1"); + + const response = await this.request( + `/organizations/${organizationSlug}/workflows/?${searchQuery.toString()}`, undefined, opts, ); - return IssueAlertRuleSchema.parse(body); + const rules = filterAttachedIssueAlertRules( + IssueAlertRuleListSchema.parse(await this.parseJsonResponse(response)), + ); + const rule = rules[0]; + if (!rule) { + throw new ApiNotFoundError( + "Workflow not found", + undefined, + undefined, + "workflow", + String(ruleId), + ); + } + return rule; } async listMetricAlertRules( @@ -1915,8 +1939,7 @@ export class SentryApiService { } /** - * Uses Sentry's combined-rules endpoint for supported name search. - * `rule` selects issue alerts and `alert_rule` selects metric alerts. + * Uses Sentry's combined-rules endpoint for metric alert name search. */ private async listCombinedAlertRules( { @@ -1929,7 +1952,7 @@ export class SentryApiService { }: { organizationSlug: string; name: string; - alertType: "rule" | "alert_rule"; + alertType: "alert_rule"; projectId?: string | number; cursor?: string; limit?: number; diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index 6d9887ce3..ca2392a0c 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -265,15 +265,30 @@ export const IssueAlertRuleSchema = z id: z.union([z.string(), z.number()]), name: z.string(), status: z.string().optional(), + enabled: z.boolean().optional(), actionMatch: z.string().nullable().optional(), filterMatch: z.string().nullable().optional(), conditions: z.array(AlertRuleComponentSchema).optional().default([]), filters: z.array(AlertRuleComponentSchema).optional().default([]), actions: z.array(AlertRuleComponentSchema).optional().default([]), + triggers: AlertRuleComponentSchema.nullable().optional(), + actionFilters: z + .array(AlertRuleComponentSchema) + .nullable() + .optional() + .default([]), + detectorIds: z + .array(z.union([z.string(), z.number()])) + .nullable() + .optional() + .default([]), + config: z.record(z.string(), z.unknown()).optional().default({}), frequency: z.number().nullable().optional(), environment: z.string().nullable().optional(), owner: z.unknown().optional(), dateCreated: z.string().optional(), + dateUpdated: z.string().optional(), + lastTriggered: z.string().nullable().optional(), }) .passthrough(); diff --git a/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts b/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts index 6c8197e91..3e23cbdbe 100644 --- a/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts @@ -14,28 +14,42 @@ const context = { const issueAlertRule = { id: "123", name: "Notify backend team", - status: "active", - actionMatch: "any", - filterMatch: "all", - frequency: 30, + enabled: true, + config: { + frequency: 30, + }, environment: "production", + detectorIds: ["789"], owner: "team:backend", dateCreated: "2026-01-02T03:04:05.000Z", - conditions: [ - { - id: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", - }, - ], - filters: [ - { - id: "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", - }, - ], - actions: [ + dateUpdated: "2026-01-02T04:04:05.000Z", + triggers: { + id: "trigger-1", + logicType: "any", + conditions: [ + { + id: "condition-1", + type: "event_frequency_count", + comparison: 10, + conditionResult: true, + }, + ], + }, + actionFilters: [ { - id: "sentry.mail.actions.NotifyEmailAction", - targetType: "Team", - targetIdentifier: "1", + id: "filter-1", + logicType: "all", + conditions: [], + actions: [ + { + id: "action-1", + type: "email", + config: { + targetType: "Team", + targetIdentifier: "1", + }, + }, + ], }, ], }; @@ -79,7 +93,7 @@ function useAlertRuleHandlers() { () => HttpResponse.json(project), ), http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", () => HttpResponse.json([issueAlertRule]), ), http.get( @@ -96,8 +110,16 @@ function useAlertRuleHandlers() { describe("find_alert_rules", () => { it("serializes project-scoped issue and metric alert rules", async () => { useAlertRuleHandlers(); + let issueRequestUrl: string | null = null; let metricRequestUrl: string | null = null; mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + ({ request }) => { + issueRequestUrl = request.url; + return HttpResponse.json([issueAlertRule]); + }, + ), http.get( "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/alert-rules/", ({ request }) => { @@ -120,6 +142,10 @@ describe("find_alert_rules", () => { context, ); + expect(issueRequestUrl).not.toBeNull(); + expect(new URL(issueRequestUrl ?? "").searchParams.get("projectSlug")).toBe( + "cloudflare-mcp", + ); expect(metricRequestUrl).not.toBeNull(); expect(result).toMatchInlineSnapshot(` "# Alert Rules in **sentry-mcp-evals/cloudflare-mcp** @@ -131,14 +157,13 @@ describe("find_alert_rules", () => { **Kind**: Issue Alert **ID**: 123 **Project**: cloudflare-mcp - **Status**: active - **Action Match**: any - **Filter Match**: all + **Status**: enabled **Frequency**: 30 minutes **Environment**: production **Owner**: team:backend **Created**: 2026-01-02T03:04:05.000Z - **URL**: https://sentry-mcp-evals.sentry.io/issues/alerts/rules/cloudflare-mcp/123/details/ + **Updated**: 2026-01-02T04:04:05.000Z + **URL**: https://sentry-mcp-evals.sentry.io/monitors/alerts/123/ ## Metric Alert Rules @@ -160,7 +185,7 @@ describe("find_alert_rules", () => { ## Response Notes - Use \`get_alert_rule\` with \`kind\` and the numeric rule ID for full details. - - Alert rule actions can include integration-specific payloads; inspect existing rules before planning mutations. + - Use full details to inspect alert conditions, filters, and notification actions before changing a rule in Sentry. " `); }); @@ -192,11 +217,11 @@ describe("find_alert_rules", () => { useAlertRuleHandlers(); mswServer.use( http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", () => HttpResponse.json([issueAlertRule], { headers: { - Link: '; rel="next"; results="true"; cursor="issue-page-2"', + Link: '; rel="next"; results="true"; cursor="issue-page-2"', }, }), ), @@ -220,20 +245,23 @@ describe("find_alert_rules", () => { ); }); - it("uses the supported combined-rules name filter for query searches", async () => { - const requestUrls: string[] = []; + it("uses workflows for issue query searches and combined-rules for metric query searches", async () => { + let issueRequestUrl: string | null = null; + const combinedRequestUrls: string[] = []; useAlertRuleHandlers(); mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + ({ request }) => { + issueRequestUrl = request.url; + return HttpResponse.json([issueAlertRule]); + }, + ), http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/combined-rules/", ({ request }) => { - requestUrls.push(request.url); - const params = new URL(request.url).searchParams; - return HttpResponse.json( - params.get("alertType") === "rule" - ? [issueAlertRule] - : [metricAlertRule], - ); + combinedRequestUrls.push(request.url); + return HttpResponse.json([metricAlertRule]); }, ), ); @@ -253,23 +281,164 @@ describe("find_alert_rules", () => { expect(result).toContain("Notify backend team"); expect(result).toContain("P95 latency"); + expect(issueRequestUrl).not.toBeNull(); + const issueParams = new URL(issueRequestUrl ?? "").searchParams; + expect(issueParams.get("query")).toBe('name:"*backend*"'); + expect(issueParams.get("projectSlug")).toBe("cloudflare-mcp"); + expect(combinedRequestUrls).toHaveLength(1); + const metricParams = new URL(combinedRequestUrls[0]).searchParams; + expect(metricParams.get("name")).toBe("backend"); + expect(metricParams.get("query")).toBeNull(); + expect(metricParams.get("project")).toBe(project.id); + expect(metricParams.get("alertType")).toBe("alert_rule"); + }); + + it("filters unattached organization workflows from project-scoped issue results", async () => { + useAlertRuleHandlers(); + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + () => + HttpResponse.json([ + { + ...issueAlertRule, + id: "999", + name: "Organization workflow", + detectorIds: [], + }, + issueAlertRule, + ]), + ), + ); + + const result = await findAlertRules.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + kind: "issue", + projectSlug: "cloudflare-mcp", + query: null, + cursor: null, + limit: 10, + }, + context, + ); + + expect(result).toContain("Notify backend team"); + expect(result).not.toContain("Organization workflow"); + }); + + it("follows workflow pages until enough attached issue rules are found", async () => { + const requestUrls: string[] = []; + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + ({ request }) => { + requestUrls.push(request.url); + const params = new URL(request.url).searchParams; + if (params.get("cursor") === "workflow-page-2") { + return HttpResponse.json([issueAlertRule]); + } + return HttpResponse.json( + [ + { + ...issueAlertRule, + id: "999", + name: "Organization workflow", + detectorIds: [], + }, + ], + { + headers: { + Link: '; rel="next"; results="true"; cursor="workflow-page-2"', + }, + }, + ); + }, + ), + ); + + const result = await findAlertRules.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + kind: "issue", + projectSlug: "cloudflare-mcp", + query: null, + cursor: null, + limit: 1, + }, + context, + ); + expect(requestUrls).toHaveLength(2); - for (const requestUrl of requestUrls) { - const params = new URL(requestUrl).searchParams; - expect(params.get("name")).toBe("backend"); - expect(params.get("query")).toBeNull(); - expect(params.get("project")).toBe(project.id); - } - expect( - requestUrls - .map((url) => new URL(url).searchParams.get("alertType")) - .sort(), - ).toMatchInlineSnapshot(` - [ - "alert_rule", - "rule", - ] - `); + expect(new URL(requestUrls[1]).searchParams.get("cursor")).toBe( + "workflow-page-2", + ); + expect(result).toContain("Notify backend team"); + expect(result).not.toContain("Organization workflow"); + }); + + it("does not expose a workflow cursor when an overfull page is capped", async () => { + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + () => + HttpResponse.json( + [ + issueAlertRule, + { + ...issueAlertRule, + id: "124", + name: "Notify frontend team", + }, + ], + { + headers: { + Link: '; rel="next"; results="true"; cursor="workflow-page-2"', + }, + }, + ), + ), + ); + + const result = await findAlertRules.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + kind: "issue", + projectSlug: "cloudflare-mcp", + query: null, + cursor: null, + limit: 1, + }, + context, + ); + + expect(result).toMatchInlineSnapshot(` + "# Alert Rules in **sentry-mcp-evals/cloudflare-mcp** + + ## Issue Alert Rules + + ### Notify backend team + + **Kind**: Issue Alert + **ID**: 123 + **Project**: cloudflare-mcp + **Status**: enabled + **Frequency**: 30 minutes + **Environment**: production + **Owner**: team:backend + **Created**: 2026-01-02T03:04:05.000Z + **Updated**: 2026-01-02T04:04:05.000Z + **URL**: https://sentry-mcp-evals.sentry.io/monitors/alerts/123/ + + ## Response Notes + + - Use \`get_alert_rule\` with \`kind\` and the numeric rule ID for full details. + - Use full details to inspect alert conditions, filters, and notification actions before changing a rule in Sentry. + " + `); }); it("returns next cursors for combined-rules query searches", async () => { diff --git a/packages/mcp-core/src/tools/catalog/find-alert-rules.ts b/packages/mcp-core/src/tools/catalog/find-alert-rules.ts index 3222354f6..cdc708d7c 100644 --- a/packages/mcp-core/src/tools/catalog/find-alert-rules.ts +++ b/packages/mcp-core/src/tools/catalog/find-alert-rules.ts @@ -152,11 +152,7 @@ export default defineTool({ formatIssueAlertRule(rule, projectSlug ?? "", { headingLevel: 3, includeComponents: false, - url: apiService.getIssueAlertRuleUrl( - organizationSlug, - projectSlug ?? "", - rule.id, - ), + url: apiService.getIssueAlertRuleUrl(organizationSlug, rule.id), }), ) .join("\n\n"); @@ -182,7 +178,7 @@ export default defineTool({ output += "- Use `get_alert_rule` with `kind` and the numeric rule ID for full details.\n"; output += - "- Alert rule actions can include integration-specific payloads; inspect existing rules before planning mutations.\n"; + "- Use full details to inspect alert conditions, filters, and notification actions before changing a rule in Sentry.\n"; if (issuePage.nextCursor) { output += `- More issue alert rules are available. Pass \`kind: "issue"\` and \`cursor: "${issuePage.nextCursor}"\` with the same search scope to fetch the next page.\n`; } diff --git a/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts b/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts index 3b056df64..cd9061ed6 100644 --- a/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts @@ -22,28 +22,42 @@ const projectConstrainedContext = { const issueAlertRule = { id: "123", name: "Notify backend team", - status: "active", - actionMatch: "any", - filterMatch: "all", - frequency: 30, + enabled: true, + config: { + frequency: 30, + }, environment: "production", + detectorIds: ["789"], owner: "team:backend", dateCreated: "2026-01-02T03:04:05.000Z", - conditions: [ - { - id: "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", - }, - ], - filters: [ - { - id: "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter", - }, - ], - actions: [ + dateUpdated: "2026-01-02T04:04:05.000Z", + triggers: { + id: "trigger-1", + logicType: "any", + conditions: [ + { + id: "condition-1", + type: "event_frequency_count", + comparison: 10, + conditionResult: true, + }, + ], + }, + actionFilters: [ { - id: "sentry.mail.actions.NotifyEmailAction", - targetType: "Team", - targetIdentifier: "1", + id: "filter-1", + logicType: "all", + conditions: [], + actions: [ + { + id: "action-1", + type: "email", + config: { + targetType: "Team", + targetIdentifier: "1", + }, + }, + ], }, ], }; @@ -87,13 +101,9 @@ function useAlertRuleHandlers() { () => HttpResponse.json(project), ), http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", () => HttpResponse.json([issueAlertRule]), ), - http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/123/", - () => HttpResponse.json(issueAlertRule), - ), http.get( "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/alert-rules/", () => HttpResponse.json([metricAlertRule]), @@ -128,27 +138,27 @@ describe("get_alert_rule", () => { **Kind**: Issue Alert **ID**: 123 **Project**: cloudflare-mcp - **Status**: active - **Action Match**: any - **Filter Match**: all + **Status**: enabled **Frequency**: 30 minutes **Environment**: production **Owner**: team:backend **Created**: 2026-01-02T03:04:05.000Z - **URL**: https://sentry-mcp-evals.sentry.io/issues/alerts/rules/cloudflare-mcp/123/details/ - ### Conditions + **Updated**: 2026-01-02T04:04:05.000Z + **URL**: https://sentry-mcp-evals.sentry.io/monitors/alerts/123/ - - sentry.rules.conditions.first_seen_event.FirstSeenEventCondition - ### Filters + ### Triggers - - sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter - ### Actions + - Logic: any + - Conditions: Event frequency count (comparison: 10, result: true) - - sentry.mail.actions.NotifyEmailAction {"targetType":"Team","targetIdentifier":"1"} + ### Action Filters + + - Logic: all + - Actions: Email (target type: Team, target: 1) ## Response Notes - - This tool is read-only. Treat the returned payload as the canonical source for any future clone or mutation workflow. + - Use these details to inspect alert conditions, filters, routing, and notification actions before changing the rule in Sentry. " `); }); @@ -167,10 +177,34 @@ describe("get_alert_rule", () => { context, ); - expect(result).toContain("# Alert Rule in **sentry-mcp-evals**"); - expect(result).toContain("**Kind**: Metric Alert"); - expect(result).toContain("**ID**: 456"); - expect(result).toContain("### Triggers"); + expect(result).toMatchInlineSnapshot(` + "# Alert Rule in **sentry-mcp-evals** + + ## P95 latency + + **Kind**: Metric Alert + **ID**: 456 + **Status**: 0 + **Dataset**: transactions + **Aggregate**: p95(transaction.duration) + **Query**: environment:production + **Time Window**: 5 minutes + **Projects**: cloudflare-mcp + **Environment**: production + **Owner**: team:backend + **Created**: 2026-01-03T03:04:05.000Z + **URL**: https://sentry-mcp-evals.sentry.io/issues/alerts/rules/details/456/ + + ### Triggers + + - Trigger: Critical threshold: 500 + - Actions: Slack (target: alerts) + + ## Response Notes + + - Use these details to inspect alert conditions, filters, routing, and notification actions before changing the rule in Sentry. + " + `); }); it("falls back to organization metric alert details after a project ID miss", async () => { @@ -270,19 +304,8 @@ describe("get_alert_rule", () => { expect(result).toContain("### Triggers"); }); - it("handles workflow-engine issue alerts with null match fields", async () => { + it("handles native workflow issue alert payloads", async () => { useAlertRuleHandlers(); - mswServer.use( - http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/123/", - () => - HttpResponse.json({ - ...issueAlertRule, - actionMatch: null, - filterMatch: null, - }), - ), - ); const result = await getAlertRule.handler( { @@ -296,6 +319,8 @@ describe("get_alert_rule", () => { ); expect(result).toContain("**Kind**: Issue Alert"); + expect(result).toContain("### Triggers"); + expect(result).toContain("### Action Filters"); expect(result).not.toContain("**Action Match**"); expect(result).not.toContain("**Filter Match**"); }); @@ -303,28 +328,24 @@ describe("get_alert_rule", () => { it("fetches issue alert details after resolving an exact name", async () => { useAlertRuleHandlers(); let detailRequestCount = 0; + let listRequestUrl: string | null = null; mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/combined-rules/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", ({ request }) => { const params = new URL(request.url).searchParams; - return HttpResponse.json( - params.get("alertType") === "rule" - ? [ - { - id: issueAlertRule.id, - name: issueAlertRule.name, - }, - ] - : [], - ); - }, - ), - http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/123/", - () => { - detailRequestCount += 1; - return HttpResponse.json(issueAlertRule); + if (params.get("id") === "123") { + detailRequestCount += 1; + return HttpResponse.json([issueAlertRule]); + } + listRequestUrl = request.url; + return HttpResponse.json([ + { + id: issueAlertRule.id, + name: issueAlertRule.name, + detectorIds: issueAlertRule.detectorIds, + }, + ]); }, ), ); @@ -340,38 +361,104 @@ describe("get_alert_rule", () => { context, ); + expect(listRequestUrl).not.toBeNull(); + const listParams = new URL(listRequestUrl ?? "").searchParams; + expect(listParams.get("query")).toBe('name:"*Notify backend team*"'); + expect(listParams.get("projectSlug")).toBe("cloudflare-mcp"); expect(detailRequestCount).toBe(1); - expect(result).toContain("### Conditions"); + expect(result).toContain("### Triggers"); expect(result).toContain( - "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + "Event frequency count (comparison: 10, result: true)", ); }); - it("resolves digit-only issue alert names after a numeric ID miss", async () => { + it("quotes issue alert name lookups for workflow query syntax", async () => { + useAlertRuleHandlers(); + let listRequestUrl: string | null = null; mswServer.use( http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/", - () => HttpResponse.json(project), + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + ({ request }) => { + const params = new URL(request.url).searchParams; + if (params.get("id") === "123") { + return HttpResponse.json([ + { ...issueAlertRule, name: "Critical: backend" }, + ]); + } + listRequestUrl = request.url; + return HttpResponse.json([ + { ...issueAlertRule, name: "Critical: backend" }, + ]); + }, ), + ); + + const result = await getAlertRule.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + kind: "issue", + projectSlug: "cloudflare-mcp", + ruleIdOrName: "Critical: backend", + }, + context, + ); + + expect(listRequestUrl).not.toBeNull(); + expect(new URL(listRequestUrl ?? "").searchParams.get("query")).toBe( + 'name:"*Critical: backend*"', + ); + expect(result).toContain("## Critical: backend"); + }); + + it("ignores unattached organization workflows during issue detail lookup", async () => { + mswServer.use( http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/123/", - () => HttpResponse.json({}, { status: 404 }), + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + () => + HttpResponse.json([ + { + ...issueAlertRule, + detectorIds: [], + }, + ]), + ), + ); + + await expect( + getAlertRule.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + kind: "issue", + projectSlug: "cloudflare-mcp", + ruleIdOrName: "123", + }, + context, ), + ).rejects.toThrow('Issue alert rule "123" was not found'); + }); + + it("resolves digit-only issue alert names after a numeric ID miss", async () => { + mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/combined-rules/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", ({ request }) => { const params = new URL(request.url).searchParams; - return HttpResponse.json( - params.get("alertType") === "rule" - ? [{ ...issueAlertRule, id: "789", name: "123" }] - : [], - ); + if (params.get("id") === "123") { + return HttpResponse.json([]); + } + if ( + params.get("id") === "789" || + params.get("query") === 'name:"*123*"' + ) { + return HttpResponse.json([ + { ...issueAlertRule, id: "789", name: "123" }, + ]); + } + return HttpResponse.json([]); }, ), - http.get( - "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/rules/789/", - () => HttpResponse.json({ ...issueAlertRule, id: "789", name: "123" }), - ), ); const result = await getAlertRule.handler( @@ -395,6 +482,10 @@ describe("get_alert_rule", () => { "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/", () => HttpResponse.json(project), ), + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + () => HttpResponse.json([]), + ), http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/combined-rules/", ({ request }) => { @@ -438,16 +529,13 @@ describe("get_alert_rule", () => { "https://sentry.io/api/0/projects/sentry-mcp-evals/cloudflare-mcp/", () => HttpResponse.json(project), ), + http.get( + "https://sentry.io/api/0/organizations/sentry-mcp-evals/workflows/", + () => HttpResponse.json([{ ...issueAlertRule, name: "Same name" }]), + ), http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/combined-rules/", - ({ request }) => { - const params = new URL(request.url).searchParams; - return HttpResponse.json( - params.get("alertType") === "rule" - ? [{ ...issueAlertRule, name: "Same name" }] - : [{ ...metricAlertRule, name: "Same name" }], - ); - }, + () => HttpResponse.json([{ ...metricAlertRule, name: "Same name" }]), ), ); diff --git a/packages/mcp-core/src/tools/catalog/get-alert-rule.ts b/packages/mcp-core/src/tools/catalog/get-alert-rule.ts index 937e54e1c..2bc344c48 100644 --- a/packages/mcp-core/src/tools/catalog/get-alert-rule.ts +++ b/packages/mcp-core/src/tools/catalog/get-alert-rule.ts @@ -227,7 +227,6 @@ export default defineTool({ ? formatIssueAlertRule(match.rule, match.projectSlug, { url: apiService.getIssueAlertRuleUrl( organizationSlug, - match.projectSlug, match.rule.id, ), }) @@ -239,7 +238,7 @@ export default defineTool({ }); output += "\n\n## Response Notes\n\n"; output += - "- This tool is read-only. Treat the returned payload as the canonical source for any future clone or mutation workflow.\n"; + "- Use these details to inspect alert conditions, filters, routing, and notification actions before changing the rule in Sentry.\n"; return output; }, }); diff --git a/packages/mcp-core/src/tools/catalog/support/alerts.ts b/packages/mcp-core/src/tools/catalog/support/alerts.ts index a577ce477..a260f16b9 100644 --- a/packages/mcp-core/src/tools/catalog/support/alerts.ts +++ b/packages/mcp-core/src/tools/catalog/support/alerts.ts @@ -11,26 +11,227 @@ import { formatDate, formatId, formatUnknown, + isRecord, } from "./api-formatting"; const NUMERIC_ID_PATTERN = /^\d+$/; type AlertComponent = Record; +type AlertComponentDetail = [string, unknown]; + +const COMPONENT_DETAIL_SKIP_KEYS = new Set([ + "id", + "type", + "conditions", + "actions", +]); +const WORKFLOW_GROUP_DETAIL_SKIP_KEYS = new Set([ + "id", + "type", + "logicType", + "conditions", + "actions", +]); export function isNumericAlertRuleId(value: string): boolean { return NUMERIC_ID_PATTERN.test(value); } +function humanizeKey(value: string): string { + if (value === "conditionResult") { + return "result"; + } + if (value === "targetIdentifier") { + return "target"; + } + if (value === "alertThreshold") { + return "threshold"; + } + return value + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/[_-]+/g, " ") + .trim() + .toLowerCase(); +} + +function humanizeValue(value: string): string { + const normalized = value + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/[_-]+/g, " ") + .trim(); + if (!normalized) { + return value; + } + return normalized.charAt(0).toUpperCase() + normalized.slice(1); +} + +function formatScalar(value: unknown): string | null { + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed ? trimmed : null; + } + if (typeof value === "number" || typeof value === "boolean") { + return formatUnknown(value); + } + return null; +} + +function formatDetailValue(value: unknown): string | null { + if (Array.isArray(value)) { + const items = value + .map(formatDetailValue) + .filter((item): item is string => item !== null); + return items.length > 0 ? items.join(", ") : null; + } + if (isRecord(value)) { + const details = Object.entries(value) + .filter(([key]) => key !== "id") + .map(([key, item]) => { + const formatted = formatDetailValue(item); + return formatted ? `${humanizeKey(key)}: ${formatted}` : null; + }) + .filter((item): item is string => item !== null); + return details.length > 0 ? details.join(", ") : null; + } + return formatScalar(value); +} + +function shouldSkipDetail(key: string, value: unknown): boolean { + return ( + key === "targetDisplay" && + typeof value === "string" && + value.toLowerCase() === "unknown" + ); +} + +function getComponentDetailEntries( + component: AlertComponent, + skippedKeys: ReadonlySet, +): AlertComponentDetail[] { + const detailEntries: AlertComponentDetail[] = []; + for (const [key, value] of Object.entries(component)) { + if ( + !skippedKeys.has(key) && + value !== undefined && + value !== null && + !shouldSkipDetail(key, value) + ) { + if (key === "config" && isRecord(value)) { + detailEntries.push( + ...Object.entries(value).filter( + ([configKey, configValue]) => + !shouldSkipDetail(configKey, configValue), + ), + ); + continue; + } + detailEntries.push([key, value]); + } + } + return detailEntries; +} + +function formatComponentDetails( + component: AlertComponent, + skippedKeys: ReadonlySet, +): string | null { + const details = getComponentDetailEntries(component, skippedKeys) + .map(([key, value]) => { + const formatted = formatDetailValue(value); + return formatted ? `${humanizeKey(key)}: ${formatted}` : null; + }) + .filter((item): item is string => item !== null) + .join(", "); + return details || null; +} + function formatComponent(component: AlertComponent): string { - const id = component.id; - if (typeof id === "string" && id.trim()) { - const { id: _id, ...params } = component; - if (Object.keys(params).length === 0) { - return id; + const type = typeof component.type === "string" ? component.type : null; + const label = type ? humanizeValue(type) : "Component"; + const details = formatComponentDetails(component, COMPONENT_DETAIL_SKIP_KEYS); + if (!details) { + return label; + } + return `${label} (${details})`; +} + +function singularizeComponentLabel(label: string): string { + if (label.endsWith("ies")) { + return `${label.slice(0, -3)}y`; + } + if (label.endsWith("s")) { + return label.slice(0, -1); + } + return label; +} + +function formatThresholdTrigger(component: AlertComponent): string | null { + const label = formatScalar(component.label); + const threshold = formatScalar(component.alertThreshold); + const resolveThreshold = formatScalar(component.resolveThreshold); + if (!threshold) { + return null; + } + + const thresholdLabel = label + ? `${humanizeValue(label)} threshold` + : "Threshold"; + return resolveThreshold + ? `${thresholdLabel}: ${threshold}; resolves below: ${resolveThreshold}` + : `${thresholdLabel}: ${threshold}`; +} + +function formatWorkflowGroup( + component: AlertComponent, + componentLabel: string, +): string[] { + const lines: string[] = []; + const logicType = + typeof component.logicType === "string" ? component.logicType : null; + if (logicType) { + lines.push(`- Logic: ${logicType}`); + } + + const componentDetails = + componentLabel === "Trigger" + ? (formatThresholdTrigger(component) ?? + formatComponentDetails(component, WORKFLOW_GROUP_DETAIL_SKIP_KEYS)) + : formatComponentDetails(component, WORKFLOW_GROUP_DETAIL_SKIP_KEYS); + if (componentDetails) { + lines.push(`- ${componentLabel}: ${componentDetails}`); + } + + const conditions = readComponentList(component.conditions); + if (conditions?.length) { + lines.push( + `- Conditions: ${conditions.slice(0, 5).map(formatComponent).join("; ")}`, + ); + if (conditions.length > 5) { + lines.push(`- ...and ${conditions.length - 5} more conditions`); + } + } + + const actions = readComponentList(component.actions); + if (actions?.length) { + lines.push( + `- Actions: ${actions.slice(0, 5).map(formatComponent).join("; ")}`, + ); + if (actions.length > 5) { + lines.push(`- ...and ${actions.length - 5} more actions`); } - return `${id} ${formatUnknown(params)}`; } - return formatUnknown(component); + + if (lines.length === 0) { + lines.push(`- ${formatComponent(component)}`); + } + return lines; +} + +function readComponentList(value: unknown): AlertComponent[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + return value.filter(isRecord); } function formatComponentSummary( @@ -42,9 +243,10 @@ function formatComponentSummary( return []; } const heading = "#".repeat(Math.min(headingLevel, 6)); - const lines = [`${heading} ${label}`, ""]; + const lines = ["", `${heading} ${label}`, ""]; + const componentLabel = singularizeComponentLabel(label); for (const component of components.slice(0, 5)) { - lines.push(`- ${formatComponent(component)}`); + lines.push(...formatWorkflowGroup(component, componentLabel)); } if (components.length > 5) { lines.push(`- ...and ${components.length - 5} more`); @@ -52,6 +254,14 @@ function formatComponentSummary( return lines; } +function getIssueAlertRuleFrequency(rule: IssueAlertRule): number | null { + if (rule.frequency !== undefined && rule.frequency !== null) { + return rule.frequency; + } + const frequency = rule.config.frequency; + return typeof frequency === "number" ? frequency : null; +} + export function formatIssueAlertRule( rule: IssueAlertRule, projectSlug: string, @@ -65,27 +275,37 @@ export function formatIssueAlertRule( const includeComponents = options.includeComponents ?? true; const heading = "#".repeat(Math.min(headingLevel, 6)); const owner = rule.owner ? formatActor(rule.owner) : null; + const frequency = getIssueAlertRuleFrequency(rule); const lines = compactLines([ `${heading} ${rule.name}`, "", `**Kind**: Issue Alert`, `**ID**: ${formatId(rule.id)}`, `**Project**: ${projectSlug}`, - rule.status ? `**Status**: ${rule.status}` : null, + rule.status + ? `**Status**: ${rule.status}` + : rule.enabled !== undefined + ? `**Status**: ${rule.enabled ? "enabled" : "disabled"}` + : null, rule.actionMatch ? `**Action Match**: ${rule.actionMatch}` : null, rule.filterMatch ? `**Filter Match**: ${rule.filterMatch}` : null, - rule.frequency !== undefined && rule.frequency !== null - ? `**Frequency**: ${rule.frequency} minutes` - : null, + frequency !== null ? `**Frequency**: ${frequency} minutes` : null, rule.environment ? `**Environment**: ${rule.environment}` : null, owner ? `**Owner**: ${owner}` : null, formatDate(rule.dateCreated) ? `**Created**: ${formatDate(rule.dateCreated)}` : null, + formatDate(rule.dateUpdated) + ? `**Updated**: ${formatDate(rule.dateUpdated)}` + : null, + formatDate(rule.lastTriggered) + ? `**Last Triggered**: ${formatDate(rule.lastTriggered)}` + : null, options.url ? `**URL**: ${options.url}` : null, ]); if (includeComponents) { + const workflowTriggers = rule.triggers ? [rule.triggers] : []; lines.push( ...formatComponentSummary( "Conditions", @@ -94,6 +314,12 @@ export function formatIssueAlertRule( ), ...formatComponentSummary("Filters", rule.filters, headingLevel + 1), ...formatComponentSummary("Actions", rule.actions, headingLevel + 1), + ...formatComponentSummary("Triggers", workflowTriggers, headingLevel + 1), + ...formatComponentSummary( + "Action Filters", + rule.actionFilters ?? [], + headingLevel + 1, + ), ); } diff --git a/packages/mcp-server-evals/README.md b/packages/mcp-server-evals/README.md index 526af9ee0..89f73105a 100644 --- a/packages/mcp-server-evals/README.md +++ b/packages/mcp-server-evals/README.md @@ -21,4 +21,4 @@ This keeps permissions minimal and readable while still enabling every tool in e - No API keys are logged; MSW handles Sentry API mocking. - For code changes, ensure `pnpm run tsc && pnpm run lint && pnpm run test` all pass. -- See `docs/adding-tools.md` and `docs/testing.md` for contribution guidance. +- See `docs/contributing/adding-tools.md` and `docs/testing/overview.md` for contribution guidance. diff --git a/packages/mcp-server/src/auth/constants.ts b/packages/mcp-server/src/auth/constants.ts index e52dc0601..434231cac 100644 --- a/packages/mcp-server/src/auth/constants.ts +++ b/packages/mcp-server/src/auth/constants.ts @@ -6,7 +6,7 @@ import { isSentryHost } from "@sentry/mcp-core/utils/url-utils"; * No client secret — device code grant is a public client flow. * The Cloudflare transport uses a separate OAuth app with a secret. * Override via SENTRY_CLIENT_ID environment variable. - * See docs/stdio-auth.md for the full credential architecture. + * See docs/operations/stdio-auth.md for the full credential architecture. */ export const DEFAULT_SENTRY_CLIENT_ID = "0acbeba7d07d58076dd7dbde8cea2fed8ab525ce3713bda604988009ab35d765"; diff --git a/packages/mcp-server/src/auth/token-cache.ts b/packages/mcp-server/src/auth/token-cache.ts index d08ff0fe5..beaca5bbf 100644 --- a/packages/mcp-server/src/auth/token-cache.ts +++ b/packages/mcp-server/src/auth/token-cache.ts @@ -1,7 +1,7 @@ /** * Persistent token cache for device code auth. * Tokens are stored at ~/.sentry/mcp.json, keyed by {host}:{clientId}. - * See docs/stdio-auth.md for cache format and security details. + * See docs/operations/stdio-auth.md for cache format and security details. */ import * as fs from "node:fs/promises"; import * as path from "node:path"; diff --git a/scripts/check-doc-links.mjs b/scripts/check-doc-links.mjs index 34533ade3..201175bed 100644 --- a/scripts/check-doc-links.mjs +++ b/scripts/check-doc-links.mjs @@ -29,51 +29,56 @@ for (const file of files) { // Strip fenced code blocks to avoid false positives in examples const contentNoFences = content.replace(/```[\s\S]*?```/g, ""); - // 1) Flag local Markdown links like [text](./file.md) or [text](../file.mdc) - const localLinkRe = /\[[^\]]+\]\((\.\.?\/[^)]+)\)/g; + // 1) Validate local Markdown links like [text](./file.md) or [text](../file.mdc). + // Local docs should use Markdown links so humans can navigate them and agents do + // not inline entire files through at-prefixed references. + const localLinkRe = /!?\[[^\]]+\]\((?!https?:|mailto:|#)([^)]+)\)/g; for (const m of contentNoFences.matchAll(localLinkRe)) { // Skip illustrative placeholders if (m[1].includes("...")) continue; - problems.push({ - file: rel, - type: "local-markdown-link", - message: `Use @path for local docs instead of Markdown links: ${m[0]}`, - }); - } - - // 1b) Flag Markdown links that point to @path - const atMarkdownLinkRe = /\[[^\]]+\]\(@[^)]+\)/g; - for (const m of contentNoFences.matchAll(atMarkdownLinkRe)) { - problems.push({ - file: rel, - type: "atpath-markdown-link", - message: `Do not wrap @paths in Markdown links: ${m[0]}`, - }); - } - - // 2) Validate @path references point to real files (only for clear file tokens) - // Matches @path segments with known extensions or obvious repo files - const atPathRe = /@([A-Za-z0-9_.\-\/]+\.(?:mdc|md|ts|tsx|js|json))/g; - for (const m of contentNoFences.matchAll(atPathRe)) { - const relPath = m[1]; - const abs = join(root, relPath); + const target = m[1].split("#")[0].replace(/^<|>$/g, ""); + if (!target) continue; + const abs = resolve(file, "..", target); try { const st = statSync(abs); if (!st.isFile()) { problems.push({ file: rel, - type: "missing-file", - message: `@${relPath} is not a file`, + type: "missing-link-target", + message: `${m[0]} does not point to a file`, }); } } catch { problems.push({ file: rel, - type: "missing-file", - message: `@${relPath} does not exist`, + type: "missing-link-target", + message: `${m[0]} points to missing file ${target}`, }); } } + + // 1b) Flag Markdown links that point to at-prefixed repo paths. + const atMarkdownLinkRe = + /\[[^\]]+\]\(@(?:docs|packages|scripts|AGENTS\.md|README\.md|TELEMETRY\.md)[^)]+\)/g; + for (const m of contentNoFences.matchAll(atMarkdownLinkRe)) { + problems.push({ + file: rel, + type: "atpath-markdown-link", + message: `Use Markdown links or plain relative paths instead of at-prefixed repo paths: ${m[0]}`, + }); + } + + // 2) Flag at-prefixed repo-local references. These can force some agents to + // inline the entire target file. + const atPathRe = + /@(?:docs|packages|scripts)\/[A-Za-z0-9_.\-\/]+\.(?:mdc|md|ts|tsx|js|json)|@(?:AGENTS|README|TELEMETRY)\.md/g; + for (const m of contentNoFences.matchAll(atPathRe)) { + problems.push({ + file: rel, + type: "atpath-reference", + message: `Use Markdown links for docs or plain relative paths for code: ${m[0]}`, + }); + } } if (problems.length) { @@ -83,5 +88,7 @@ if (problems.length) { } process.exit(1); } else { - console.log("[docs:check] OK: no local Markdown links and all @paths exist."); + console.log( + "[docs:check] OK: local Markdown links resolve and no at-prefixed repo paths found.", + ); } diff --git a/warden.toml b/warden.toml index ba49dc50b..c9c262779 100644 --- a/warden.toml +++ b/warden.toml @@ -7,8 +7,9 @@ reportOn = "low" [[skills]] name = "mcp-audit" paths = [ - "docs/adding-tools.md", - "docs/stdio-auth.md", + "docs/contributing/adding-tools.md", + "docs/contributing/tool-responses.md", + "docs/operations/stdio-auth.md", "packages/mcp-core/src/server.ts", "packages/mcp-core/src/server.test.ts", "packages/mcp-core/src/skillDefinitions.json",