Skip to content

feat(doctor): static tool-hygiene lints as advisory warnings - #3753

Open
chelojimenez wants to merge 2 commits into
mainfrom
feat/doctor-tool-hygiene-lints
Open

feat(doctor): static tool-hygiene lints as advisory warnings#3753
chelojimenez wants to merge 2 commits into
mainfrom
feat/doctor-tool-hygiene-lints

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What

Adds a zero-traffic lint pass over the tools/list catalog to the shared doctor core. It ports the agent-facing failure modes PostHog surfaced from production MCP telemetry (their thread) into static connect-time checks — you see them in mcpjam server doctor before any agent ever hits the server.

Rules (all advisory)

Rule Catches Production analogue
inconsistent-param-naming same param spelled differently across tools (insightId vs insight_id) PostHog #72809 (id alias confusion)
unknowable-required-id required ID-shaped param whose prose never says where to get a value PostHog #73593 (required projectId agents never knew)
undocumented-constraint numeric schema bounds (maxLength etc.) the description never mentions PostHog #72814 (hidden 400-char cap)
unbounded-list-tool list/query-shaped tool with no limit/pagination param PostHog #71854 (trace responses in the tens of millions of tokens)
missing-description empty or trivial tool description PostHog #73584 (vague descriptions → validation errors)

How

  • New pure module sdk/src/tool-lints.tslintToolCatalog(tools) → ToolLintFinding[], dependency-free, defensive against malformed catalogs.
  • Wired once in buildConnectedServerDoctorState (server-doctor-core.ts), so the node-manager doctor, the browser/http doctor, and the hosted /servers/doctor route all get it.
  • New toolHygiene check + toolLints array on ServerDoctorResult; new "warn" check status.
  • Warnings never fail readiness: deriveDoctorStatus ignores warn, CLI exit codes unchanged (covered by a dedicated test).
  • CLI human output renders a Tool hygiene section (capped at 20 lines; full list in the --out JSON artifact).

Example output

- tools: ok (4 tools discovered.)
- toolHygiene: warn (5 hygiene warnings (advisory; does not affect readiness). See toolLints for details.)
Tool hygiene: 5 warnings (advisory; readiness is unaffected)
- [unknowable-required-id] insight-get · insightId: "insightId" is required and looks like an opaque identifier, but nothing says where to get one. Agents guess values or fail validation — name the tool that returns it, or make it optional with a default.
- [inconsistent-param-naming] insight-get, insight-update: The same parameter is spelled 2 ways across tools: "insightId" (insight-get), "insight_id" (insight-update). Agents mix spellings up and ping-pong between rejections — standardize on one, or accept the others as aliases.
...

Status stays ready, exit code 0.

Testing

  • New sdk/tests/tool-lints.test.ts (10 cases incl. false-positive guards: grid isn't an ID, minLength: 1 ignored, malformed entries skipped).
  • SDK full suite: 3838 passed. CLI: 561 passed + typecheck. Web servers-doctor route test passed. mcp workspace typecheck passed.
  • Smoke-tested live against a fixture stdio server reproducing all five problems.

Deliberately out of scope: the dynamic analyzers (bulk-gap loop detection, response-size p95) — those belong in swarm trace analysis, not a static connect-time pass.

🤖 Generated with Claude Code


Summary by cubic

Adds a zero-traffic, static hygiene lint pass over tools/list and surfaces advisory toolHygiene warnings in the doctor. Improves agent usability without affecting readiness or CLI exit codes.

  • New module sdk/src/tool-lints.ts with lintToolCatalog; exported from sdk and browser (ToolLintFinding, ToolLintRule).
  • Wired in buildConnectedServerDoctorState and reported via checks.toolHygiene; warn is advisory and does not change readiness or exit codes. Platform show-servers now accepts warn on primitive checks.
  • Findings are exposed as ServerDoctorResult.toolLints. CLI prints a “Tool hygiene” section (capped at 20 lines), with full details in --out JSON.
  • Rules: missing-description, unknowable-required-id, inconsistent-param-naming, undocumented-constraint, unbounded-list-tool.

Written for commit df63414. Summary will update on new commits.

Review in cubic


Note

Low Risk
Additive doctor diagnostics and CLI display only; warnings are explicitly excluded from readiness derivation and existing failure paths are unchanged.

Overview
Adds static, zero-traffic linting of the tools/list catalog during server doctor runs. A new lintToolCatalog module applies five advisory rules (missing descriptions, unknowable required IDs, inconsistent param naming, undocumented schema constraints, unbounded list/query tools) and returns structured toolLints findings.

Doctor results gain a toolHygiene check (with a new warn check status), a toolLints array on the report, and wiring in buildConnectedServerDoctorState so CLI, SDK, browser, and hosted doctor paths share the same behavior. Readiness and exit codes are unchangedderiveDoctorStatus still only treats error checks as failures.

The CLI human formatter prints a capped Tool hygiene section when warnings exist; full findings remain in JSON artifacts. lintToolCatalog and types are exported from the main SDK and browser entrypoints.

Reviewed by Cursor Bugbot for commit df63414. Bugbot is set up for automated code reviews on this repo. Configure here.

Add a zero-traffic lint pass over the tools/list catalog to the shared
doctor core, porting the agent-facing failure modes PostHog surfaced
from production MCP telemetry into connect-time checks:

- inconsistent-param-naming: same param spelled differently across
  tools (insightId vs insight_id)
- unknowable-required-id: required ID-shaped param whose prose never
  says where to get a value
- undocumented-constraint: numeric schema bounds (maxLength etc.) the
  description never mentions
- unbounded-list-tool: list/query-shaped tool with no limit or
  pagination param
- missing-description: empty or trivial tool description

Findings ride on ServerDoctorResult.toolLints plus a new toolHygiene
check with a new "warn" status. Warnings are advisory: deriveDoctorStatus
ignores them, so readiness and CLI exit codes are unaffected. Both
doctor variants (node manager + browser/http) get the pass via
buildConnectedServerDoctorState; the CLI human formatter renders a
capped "Tool hygiene" section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chelojimenez

chelojimenez commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@chelojimenez
chelojimenez marked this pull request as ready for review August 6, 2026 23:21
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Aug 6, 2026
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_c2f91935-3e24-4987-b5e0-c846a49a7a5e)

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8c7218bd-f7e1-41f6-a441-12c654b8a9d9)

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The SDK now lints tool catalogs for five advisory hygiene rules and stores findings in server-doctor results. Tool hygiene checks report skipped, passing, or warning states without changing readiness. The linter and its types are exported from SDK entrypoints. CLI output displays warning details, limits displayed findings to 20, and references the full JSON artifact for omitted findings. Tests cover lint rules, doctor integration, formatting, and updated result fixtures.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/lib/server-doctor.ts`:
- Around line 103-107: Sanitize server-controlled catalog text before rendering
it in the loop over toolLints: escape terminal control characters, including
ANSI escapes, carriage returns, and newlines, in both the constructed where
value and finding.message before pushing the formatted line. Add a formatting
test covering an ANSI escape sequence and newline embedded in a tool name, while
preserving the existing output structure for safe text.

In `@sdk/src/tool-lints.ts`:
- Around line 98-103: Update the findings sort comparator to replace
localeCompare calls for a.tools[0] and a.param with deterministic
locale-independent code-unit comparisons. Preserve the existing RULE_ORDER
priority and empty-string fallbacks while ensuring identical ordering across
hosts.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77920ae0-dd88-4afd-a55e-e9f6df256fc3

📥 Commits

Reviewing files that changed from the base of the PR and between e0d44c3 and df63414.

📒 Files selected for processing (10)
  • cli/src/lib/server-doctor.ts
  • cli/tests/debug-artifact.test.ts
  • cli/tests/server-doctor.test.ts
  • sdk/src/browser.ts
  • sdk/src/index.ts
  • sdk/src/platform/show-servers.ts
  • sdk/src/server-doctor-core.ts
  • sdk/src/tool-lints.ts
  • sdk/tests/server-doctor.test.ts
  • sdk/tests/tool-lints.test.ts

Comment on lines +103 to +107
for (const finding of toolLints.slice(0, MAX_TOOL_LINT_LINES)) {
const where = finding.param
? `${finding.tools.join(", ")} · ${finding.param}`
: finding.tools.join(", ");
lines.push(`- [${finding.rule}] ${where}: ${finding.message}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sanitize catalog text before terminal rendering.

finding.tools, finding.param, and finding.message contain server-controlled catalog text. A tool name or parameter name can include escape, carriage-return, or newline controls. These controls can forge CLI output or trigger terminal escape sequences.

Escape terminal control characters in both where and finding.message before adding them to lines. Add a formatting test with an ANSI escape sequence and a newline in a tool name.

Proposed fix
+function escapeTerminalText(value: string): string {
+  return value.replace(/[\u0000-\u001F\u007F-\u009F]/g, (character) =>
+    `\\u${character.codePointAt(0)!.toString(16).padStart(4, "0")}`,
+  );
+}
+
 for (const finding of toolLints.slice(0, MAX_TOOL_LINT_LINES)) {
   const where = finding.param
-    ? `${finding.tools.join(", ")} · ${finding.param}`
-    : finding.tools.join(", ");
-  lines.push(`- [${finding.rule}] ${where}: ${finding.message}`);
+    ? `${finding.tools.map(escapeTerminalText).join(", ")} · ${escapeTerminalText(finding.param)}`
+    : finding.tools.map(escapeTerminalText).join(", ");
+  lines.push(
+    `- [${finding.rule}] ${where}: ${escapeTerminalText(finding.message)}`,
+  );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const finding of toolLints.slice(0, MAX_TOOL_LINT_LINES)) {
const where = finding.param
? `${finding.tools.join(", ")} · ${finding.param}`
: finding.tools.join(", ");
lines.push(`- [${finding.rule}] ${where}: ${finding.message}`);
function escapeTerminalText(value: string): string {
return value.replace(/[\u0000-\u001F\u007F-\u009F]/g, (character) =>
`\\u${character.codePointAt(0)!.toString(16).padStart(4, "0")}`,
);
}
for (const finding of toolLints.slice(0, MAX_TOOL_LINT_LINES)) {
const where = finding.param
? `${finding.tools.map(escapeTerminalText).join(", ")} · ${escapeTerminalText(finding.param)}`
: finding.tools.map(escapeTerminalText).join(", ");
lines.push(
`- [${finding.rule}] ${where}: ${escapeTerminalText(finding.message)}`,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/lib/server-doctor.ts` around lines 103 - 107, Sanitize
server-controlled catalog text before rendering it in the loop over toolLints:
escape terminal control characters, including ANSI escapes, carriage returns,
and newlines, in both the constructed where value and finding.message before
pushing the formatted line. Add a formatting test covering an ANSI escape
sequence and newline embedded in a tool name, while preserving the existing
output structure for safe text.

Comment thread sdk/src/tool-lints.ts
Comment on lines +98 to +103
return findings.sort(
(a, b) =>
RULE_ORDER[a.rule] - RULE_ORDER[b.rule] ||
(a.tools[0] ?? "").localeCompare(b.tools[0] ?? "") ||
(a.param ?? "").localeCompare(b.param ?? "")
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use locale-independent finding ordering.

localeCompare() without an explicit locale uses the host default locale. The same catalog can produce different finding orders in JSON artifacts on different hosts. Use a fixed code-unit comparison for the tool and parameter sort keys.

Proposed fix
+function compareStableStrings(left: string, right: string): number {
+  return left === right ? 0 : left < right ? -1 : 1;
+}
+
 return findings.sort(
   (a, b) =>
     RULE_ORDER[a.rule] - RULE_ORDER[b.rule] ||
-    (a.tools[0] ?? "").localeCompare(b.tools[0] ?? "") ||
-    (a.param ?? "").localeCompare(b.param ?? "")
+    compareStableStrings(a.tools[0] ?? "", b.tools[0] ?? "") ||
+    compareStableStrings(a.param ?? "", b.param ?? "")
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return findings.sort(
(a, b) =>
RULE_ORDER[a.rule] - RULE_ORDER[b.rule] ||
(a.tools[0] ?? "").localeCompare(b.tools[0] ?? "") ||
(a.param ?? "").localeCompare(b.param ?? "")
);
function compareStableStrings(left: string, right: string): number {
return left === right ? 0 : left < right ? -1 : 1;
}
return findings.sort(
(a, b) =>
RULE_ORDER[a.rule] - RULE_ORDER[b.rule] ||
compareStableStrings(a.tools[0] ?? "", b.tools[0] ?? "") ||
compareStableStrings(a.param ?? "", b.param ?? "")
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/tool-lints.ts` around lines 98 - 103, Update the findings sort
comparator to replace localeCompare calls for a.tools[0] and a.param with
deterministic locale-independent code-unit comparisons. Preserve the existing
RULE_ORDER priority and empty-string fallbacks while ensuring identical ordering
across hosts.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-3753.up.railway.app
Deployed commit: 68c6b5f
PR head commit: df63414
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant