name: pr-review description: Reviews pull requests for the Confluent MCP server. Use when reviewing PRs, doing self-review before sharing with the team, or when the user mentions "review PR", "help with PR", "review changes", "self-review", "review local changes", or "check my PR". Focuses on MCP tool wiring, OpenAPI/type coupling, ESM stubbability, transport security, and project-specific patterns. allowed-tools: - Read - Bash - Grep - Glob - Task
PR Review Skill
Reviews pull requests for the Confluent MCP (Model Context Protocol) server, focusing on project-specific patterns and the failure modes most likely to slip past TypeScript and lint.
Two Review Modes
The mode is selected by the invocation context, not by the user. If the user supplies a PR number/URL or asks about someone else's PR, run Formal Review Mode. Otherwise (no PR number, working from a local branch, phrases like "self-review" or "check my PR") run Self-Review Mode. When ambiguous, ask which mode to use.
Self-Review Mode (for PR authors)
Use when: the author wants to check their own changes before sharing with the team. Typically used on a draft PR or against local changes before pushing.
Goals:
- Catch issues early, before formal review
- Verify MCP tool wiring is complete (enum + handler + registry + predicate)
- Ensure new external I/O is stubbable
- Confirm OpenAPI spec and generated types stay in sync
Formal Review Mode (for reviewers)
Use when: a reviewer needs to evaluate a PR from another team member.
Goals:
- Quickly understand the scope and purpose of changes
- Identify potential issues or concerns
- Provide constructive feedback grounded in MCP/Confluent conventions
- Verify the PR template checklist is honored
Review Process
Step 1: Gather Information
For local changes (self-review):
# files changed since divergence from main
git diff main --name-only
# overview and full diff
git diff main --stat
git diff main
For GitHub PRs:
# PR metadata
gh pr view <PR_NUMBER> --json number,title,body,author,baseRefName,headRefName,additions,deletions,changedFiles,state,reviewDecision
# if no PR number is given, try the current branch
gh pr view --json number,title,body,author,baseRefName,headRefName,additions,deletions,changedFiles,state,reviewDecision
# diff
gh pr diff <PR_NUMBER>
# existing reviews and inline comments
gh pr view <PR_NUMBER> --json reviews,comments
# referenced issues
gh issue view <ISSUE_NUMBER> --json body,comments
Step 2: Filter Files for Review
SKIP these paths entirely (auto-generated or vendored):
dist/**(compiled output)node_modules/**coverage/**src/confluent/openapi-schema.d.ts(generated bynpm run generate:openapi-types)package-lock.json(review only top-level dependency changes viapackage.json)TEST-result.xml
DO review carefully (small file, big blast radius):
openapi.json(paired with the regenerated.d.ts)package.json(new deps, scripts, engine bumps)config.example.yaml,.env.example,.env.integration.example(user-facing configuration)vitest.config.ts,tsconfig.json,tsconfig.build.json,eslint.config.mjs.semaphore/**(CI configuration)Dockerfile,docker-compose.yml
Step 3: Categorize the Changes
| Category | File patterns | What to check |
|---|---|---|
| Tool handlers | src/confluent/tools/handlers/<domain>/** |
Enum entry, registry entry, enabledConnectionIds, Zod schema |
| Tool registry | src/confluent/tools/tool-registry.ts |
New handler imported and added to handlers map |
| Tool name enum | src/confluent/tools/tool-name.ts |
New enum member with stable string value |
| Connection predicates | src/confluent/tools/connection-predicates.ts |
New predicate composes existing service-block checks |
| Domain base classes | src/confluent/tools/handlers/<domain>/*-tool-handler |
Domain-wide gating still correct after edits |
| Client managers | src/confluent/{base-,direct-,}client-manager.ts |
Client lifecycle, single source of truth per connection |
| Configuration | src/config/**, src/env-schema.ts |
Zod schema, YAML interpolation, both config paths covered |
| Transports | src/mcp/transports/** |
API-key auth, DNS rebinding protection, port handling |
| OpenAPI surface | openapi.json |
Regenerated .d.ts is committed in the same PR |
| Unit tests | **/*.test.ts (excluding *.integration.test.ts) |
Vitest patterns, node-deps.ts indirection, no vi.mock |
| Integration tests | **/*.integration.test.ts, tests/harness/** |
Credential gating via early-return, tags, startServer |
| CI / release | .semaphore/**, scripts/**, Dockerfile |
No skipped checks, no smuggled secrets |
| Docs | README.md, docs/**, CHANGELOG.md, telemetry.md |
Accuracy, MCP-tool inventory, user-facing wording |
| Project rules / skills | .claude/rules/**, .claude/skills/** |
Frontmatter, path globs, trigger phrases |
Step 4: Check Critical Requirements
IMPORTANT: only review lines that were actually changed in the PR diff. Context lines from the diff are for understanding, not for review. Do not flag pre-existing issues in unchanged code.
1. Tool Wiring Completeness (MANDATORY for new tools)
A new MCP tool needs all four of:
- Entry in
ToolNameenum (src/confluent/tools/tool-name.ts) - Handler class extending
BaseToolHandler(or a domain subclass likeFlinkToolHandler) -
enabledConnectionIds(runtime)using a predicate fromconnection-predicates.ts - Registration in
ToolHandlerRegistry.handlers(src/confluent/tools/tool-registry.ts)
Red flags:
- New handler class but no
tool-registry.tschange → tool will not be loaded - New enum entry but no handler → runtime error when iterating enabled tools
- Wrong predicate (e.g.,
hasKafkafor a Schema Registry tool) → tool enables on the wrong connections and silently no-ops or errors
The canonical wiring lives in .claude/rules/tool-handlers.md, which auto-loads when files under
src/confluent/tools/**/*.ts are touched.
2. OpenAPI / Generated Types Coupling
If openapi.json changed:
-
src/confluent/openapi-schema.d.tsis regenerated and committed in the same PR - The diff in the
.d.tsreflects the spec change (no stale paths or schemas)
If openapi-schema.d.ts changed without openapi.json changing, the file was hand-edited - flag
it. The generator is npm run generate:openapi-types; the file should never be edited manually.
3. ESM Stubbability via node-deps.ts
ESM named imports are read-only from outside the defining module, so vi.spyOn cannot intercept
direct named imports. This project's workaround is src/confluent/node-deps.ts: external I/O
(filesystem, env, network not via openapi-fetch/Kafka clients, third-party constructors) routes
through that namespace, and tests spy on the wrapper.
Red flags in non-test code:
// BAD: direct named import at use site → not stubbable
import { readFile } from "node:fs/promises";
const config = await readFile(path, "utf8");
// GOOD: route through node-deps for stubbability
import { nodeDeps } from "@src/confluent/node-deps.js";
const config = await nodeDeps.readFile(path, "utf8");
Red flag in tests: vi.mock(...) calls. The project does not use vi.mock; wrap the dependency
in node-deps.ts and use vi.spyOn(nodeDeps, "readFile") instead. The /vitest skill confirms
the patterns.
4. Type Safety
- No new explicit
any(the project disablesnoImplicitAnyfor OpenAPI types, but explicitanyat use sites is still a code smell) - Tool input schemas are Zod schemas, not loose object types
- REST calls use
openapi-fetchwith typed paths from the generated schema, not rawfetch - Imports use
.jsextensions (ESM requirement) and@src/*for internal modules - No
@ts-ignore/@ts-expect-errorwithout a comment explaining why
5. Single Responsibility
- Handlers do one tool's worth of work; cross-domain logic moves to a shared helper
- No "god clients" -
BaseClientManagerowns REST and Schema Registry;DirectClientManageradds Kafka admin/producer/consumer. New client kinds extend, not inflate, these.
Step 5: Check Project-Specific Patterns
Predicate-Based Tool Gating
-
enabledConnectionIds(runtime)uses an existing predicate where one fits - If a brand-new predicate is introduced, it composes existing service-block checks rather
than re-walking
runtime.connectionsad hoc - Domain base classes (e.g.,
FlinkToolHandler) implementenabledConnectionIdsonce for the whole domain; per-handler overrides should be rare and well-justified
Configuration Surface (two paths must stay in sync)
MCPServerConfiguration is produced by either loadConfigFromYaml() (-c <path>) or
buildConfigFromEnvAndCli() (legacy env+CLI). Both produce the same Zod-validated shape.
- New configuration fields are added to the Zod schema in
src/config/models.tsAND honored bybuildConfigFromEnvAndCli(or explicitly excluded from the legacy path with a note) -
config.example.yamland.env.exampleare updated when user-facing config changes -
${VAR}interpolation continues to work for any new YAML fields
Transport Security
- HTTP/SSE handlers preserve API-key auth and DNS rebinding protection
- No new endpoint added without authentication unless explicitly intended (and called out in the PR description)
- Secrets never go through Pino at
infolevel or above; sensitive fields are redacted (Pino'sredactoption, configured insrc/logger.ts)
Logger Usage
- Uses the project logger from
src/logger.ts(Pino), notconsole.log/console.error - Errors use
logger.error({ err })so Pino's serializer captures stack traces - No log lines that include credentials, tokens, or full request bodies that may contain secrets
Testing
- Unit tests follow
.claude/rules/unit-tests.md(assertion style, stubbing patterns, handler test structure, fake timers) - Integration tests follow
.claude/rules/integration-tests.md: colocated*.integration.test.ts,tagson outerdescribe, credential gating via early-return (NOTdescribe.skipIf, which still runs nested hooks in vitest 4) - Class-instance mocks use
createMockInstance(Class)from@tests/stubs/index.js - No
.onlyleft in test files - No
vi.mock(the project pattern isnode-deps.ts+vi.spyOn)
Use the /vitest skill to verify spy/mock APIs against current Vitest docs.
Step 6: Check PR Hygiene
- PR description follows the template: Summary of Changes, manual testing instructions (transports exercised, sample inputs/outputs, env vars), additional context
- Tests checkbox is honored: new behavior has new tests; updated behavior has updated tests
- CHANGELOG entry added when the change is user-facing (new tool, new config field, breaking change, security fix). Internal refactors do not need an entry.
- No unrelated changes bundled in (formatting passes, dependency bumps, etc., should ride their own PR unless they directly support the change)
- No secrets in the diff:
.env,.env.integration, real API keys, real cluster IDs
What NOT to Flag
Style Preferences (avoid nitpicking)
- Import statements:
import type { }vsimport { }- both valid - Formatting issues - Prettier and ESLint enforce these via the pre-commit hook
- Auto-removed unused imports during a refactor - handled by
eslint-plugin-unused-imports - Trailing-comma, single-quote, semi-colon preferences - Prettier owns these
- Minor naming preferences when the existing name is reasonable
Comment Preservation
- Never suggest deleting existing comments unless they are now actively misleading
- Comments explain "why" not "what"; they may look redundant but provide context the reviewer may not have
- When code is updated, preserve or update the comments rather than removing them
Output Format
For Self-Review
## Self-Review Summary
### Changes Overview
[Brief summary of what changed]
### Critical Requirements Checklist
- [ ] Tool Wiring (enum + handler + predicate + registry): [status, location of any gap]
- [ ] OpenAPI / Types Coupling: [status]
- [ ] ESM Stubbability (node-deps): [status]
- [ ] Type Safety: [status]
- [ ] Transport Security: [status, if applicable]
### Issues to Address Before PR
1. [High-priority issue with file:line]
2. [Medium-priority issue with file:line]
### Suggestions (Optional)
- [Nice-to-have improvements]
### Ready for Review?
[Yes / Not yet, with reasoning]
For Formal Review
## PR Review: #{number} - {title}
**Author:** {author}
**Branch:** {headRefName} → {baseRefName}
**Changes:** +{additions} / -{deletions} across {changedFiles} files
### Summary
[2-3 sentence summary of what the PR does and why]
### Changed Components
- [Categorized list of changed files, excluding auto-generated]
### Findings
#### Issues (Must Fix)
- [ ] **[category]**: [description] - `file:line`
#### Suggestions (Consider)
- [ ] **[category]**: [description] - `file:line`
#### Positive Observations
- [Good patterns, thorough tests, well-written code]
### Test Coverage Assessment
- **New tests added:** [Yes/No, list test files]
- **Coverage gaps:** [Untested paths or edge cases]
- **Unit vs integration balance:** [Assessment]
### Configuration & Security Notes
[Any concerns about transport security, secret handling, config surface, or CCloud auth]
### Recommendation
**[APPROVE / REQUEST CHANGES / NEEDS DISCUSSION]**
[Brief rationale]
Review Categories
Use these labels in findings:
| Category | Description |
|---|---|
tool-wiring |
Missing enum / handler / registry / predicate hookup |
openapi |
openapi.json and .d.ts out of sync, or hand-edited generated file |
stubbability |
New external I/O bypasses node-deps.ts; vi.mock introduced |
types |
Explicit any, missing Zod schema, raw fetch over openapi-fetch |
config |
YAML and env-var paths drift; missing example-file update |
transport |
Auth or DNS-rebinding regression; unauthenticated endpoint |
secrets |
Credentials in diff, logs, or fixtures |
testing |
Missing tests, wrong mocking pattern, .only left in |
logging |
console.* in src code, secret leaks in log lines |
docs |
CHANGELOG missing for user-facing change; stale README example |
style |
Naming, conventions where Prettier/ESLint do not already enforce |
performance |
Synchronous I/O in hot paths, unbounded fetches |
Common Issues to Watch For
Tool Wiring Gaps
- Handler class added but never imported into
tool-registry.ts - New enum entry never instantiated → unused enum value
enabledConnectionIdsreturns[]unconditionally → tool never enables
OpenAPI Drift
openapi.jsonupdated,openapi-schema.d.tsnot regenerated → callers still see the old typeopenapi-schema.d.tshand-edited because regeneration "broke things" - fix the spec instead
Test-Time Surprises
vi.mock(...)introduced as a shortcut → maintenance landmine; usenode-deps.tsinsteaddescribe.skipIf(!hasCreds)in integration tests → nested hooks still run on vitest 4- New external I/O imported directly at use site → tests cannot stub it without restructuring
Configuration Drift
- New field added to YAML schema but not to
buildConfigFromEnvAndCli- env-var users get a silently incomplete config .env.example/config.example.yamlnot updated → onboarding breakage
Security
- API key or PEM smuggled into a fixture or
.env.integrationexample - Pino log line that JSON-stringifies an object containing
Authorizationheaders - New transport endpoint without auth or with auth bypassed under
NODE_ENV=test
Performance
awaitin a tight loop wherePromise.allwould do- Unbounded list endpoints (no pagination, no limit) returning the full Confluent Cloud surface to a model context
Tips
- Start with the PR description and the PR template checklist
- Look at the test changes first to understand the intended behavior
- For new tools, trace
ToolNameenum → handler → registry → predicate; if any link is missing, the tool is dead code - Use
Taskwith the Explore agent for deeper codebase context (for example, "find all callers ofnodeDeps.readFileto estimate the blast radius of a signature change") - Use companion project skills to ground review in current docs:
/mcp-docsfor MCP protocol questions (tool annotations, resource shapes, transport spec)/vitestfor spy/mock APIs and Vitest 4 behavior
- When the diff touches
src/confluent/tools/**,**/*.test.ts, ortests/**, the matching.claude/rules/files (tool-handlers.md,unit-tests.md,integration-tests.md) auto-load with the canonical conventions - consult them rather than re-deriving the rules from the diff. - When suggesting a simplification, verify it with the relevant doc skill before posting - saves a back-and-forth when the "simpler" alternative does not actually exist on the current SDK version