Add watch-command-lsp MCP plugin for live error monitoring#251
Add watch-command-lsp MCP plugin for live error monitoring#251
Conversation
MCP server plugin that runs watch commands (jest, eslint, tsc, etc.) in the background and exposes parsed diagnostics as MCP tools. Acts as an interface layer so Claude can query live lint/test/compiler errors without re-running full suites or injecting via hooks. Includes: - Bun/TypeScript MCP server with stdio transport - Output parsers for generic, jest, eslint, tsc, and custom regex - WatcherManager for subprocess lifecycle and output capture - 6 MCP tools: start_watcher, stop_watcher, list_watchers, get_diagnostics, get_output, clear_diagnostics - SessionStart hook for auto-permissions and build - Skill documentation with workflow patterns https://claude.ai/code/session_012DQQLc4c7iSyVwek6ZgucZ
Plugin Version StatusVersions are auto-bumped in PRs. Manual bumps to higher versions are preserved.
|
There was a problem hiding this comment.
👀 Solid new plugin with good architecture — a couple of correctness issues to address before merge
⚠️ TscParser has dead code (currentDiagnostics written but never read) and stale diagnostics accumulate across recompilation cycles — see inline thread
⚠️ Diagnostics array grows unbounded (no size cap like the output buffer has) — see inline thread
❔ User-supplied regex patterns compiled without ReDoS protection — low risk for v0.1.0, see inline thread
✅ Clean plugin structure following all conventions (plugin.json, hooks, skills, lib symlinks, settings.yaml)
✅ Well-designed type system modeled after LSP DiagnosticSeverity
✅ Good process lifecycle management with graceful shutdown
✅ Comprehensive SKILL.md and README documentation
🖱️ Click to expand for full details
currentDiagnostics written but never read) and stale diagnostics accumulate across recompilation cycles — see inline thread❔ User-supplied regex patterns compiled without ReDoS protection — low risk for v0.1.0, see inline thread
✅ Clean plugin structure following all conventions (plugin.json, hooks, skills, lib symlinks, settings.yaml)
✅ Well-designed type system modeled after LSP DiagnosticSeverity
✅ Good process lifecycle management with graceful shutdown
✅ Comprehensive SKILL.md and README documentation
Code Quality (72%)
Stale diagnostics bug (TscParser): The currentDiagnostics field in src/parsers/tsc.ts is assigned but never read — it's dead code. More critically, the WatcherInstance in watcher.ts:128 only ever push()es new diagnostics, never replaces them. When tsc --watch starts a new compilation cycle and previously-fixed errors disappear from output, the old diagnostics still persist. This affects any parser whose underlying tool re-outputs all diagnostics per cycle (tsc, eslint). Score impact: -15%
Unbounded diagnostics growth: The output buffer is capped at 500 lines (MAX_OUTPUT_BUFFER_LINES), but this.diagnostics has no limit. For long-running watchers this could cause gradual memory growth. Score impact: -8%
Type assertions: Multiple as const assertions on string literals (type: "text" as const, type: "object" as const) throughout index.ts — these are required by the MCP SDK types so this is acceptable, not a score impact.
Security (80%)
The plugin's core purpose is to run arbitrary shell commands (spawn(shell, ["-c", command])). This is by design — the MCP tool caller is Claude, not untrusted user input, and users opt-in by installing the plugin. The setup.sh hook auto-approves mcp__watch-command-lsp__* permissions which means Claude can invoke these tools without per-call user confirmation. This is an acceptable trust model for this use case. Score impact: -10% for the broad auto-approve pattern.
The regex parser accepts user-supplied patterns without ReDoS protection. Low risk since Claude is the caller. Score impact: -10%
Simplicity (85%)
Clean separation of concerns: types, parsers, watcher manager, and MCP server layer are well-separated. The parser factory pattern is straightforward. The parser interface (parse/flush/reset) is minimal and well-defined.
The Jest parser has appropriate stateful parsing for its complex output format. Score impact: -5% for the TscParser's unused currentDiagnostics complexity.
Minor: shell is in WatcherConfig types but not exposed in the MCP tool schema — slight API surface mismatch. Score impact: -5%
No penalty for as const — it's SDK-required.
Plugin Structure
All conventions followed correctly:
plugin.jsonwith required fields (new file, allowed by CI)- External hooks file per
plugin-hooks-organization.md - Shared lib symlinks to
shared/lib/ - Settings use camelCase per
settings-key-naming.md(only single-wordenabledkey) .release-it.jsextends base config.mcp.jsonfor server registration
Test Coverage
PR description claims "Parser implementations tested against tool-specific output formats" but no test files are included in the PR. The parsers have non-trivial stateful logic (especially Jest and TSC) that would benefit from unit tests.
Recommended follow-ups (non-blocking):
- Add unit tests for parsers — especially the stateful Jest and TSC parsers which have complex buffering logic
- Address the stale diagnostics accumulation issue (TscParser cycle detection)
- Add a
MAX_DIAGNOSTICScap similar toMAX_OUTPUT_BUFFER_LINES - Consider exposing the
shellconfig option in the MCP tool schema forstart_watcher
Footnotes
| private currentDiagnostics: Diagnostic[] = []; | ||
|
|
||
| parse(output: string, source: string): Diagnostic[] { | ||
| this.buffer += output; | ||
| const lines = this.buffer.split("\n"); | ||
| this.buffer = lines.pop() ?? ""; | ||
|
|
||
| const diagnostics: Diagnostic[] = []; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed) continue; | ||
|
|
||
| // Check for new compilation cycle | ||
| if (TSC_WATCHING.test(trimmed) || TSC_SUMMARY.test(trimmed)) { | ||
| // A new cycle — replace accumulated diagnostics | ||
| if (diagnostics.length > 0 || this.currentDiagnostics.length > 0) { | ||
| this.currentDiagnostics = [...diagnostics]; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| for (const pattern of [TSC_PATTERN_PARENS, TSC_PATTERN_COLON]) { | ||
| const match = trimmed.match(pattern); | ||
| if (match?.groups) { | ||
| diagnostics.push({ | ||
| file: match.groups.file, | ||
| line: parseInt(match.groups.line, 10), | ||
| column: parseInt(match.groups.col, 10), | ||
| severity: | ||
| match.groups.sev === "error" ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, | ||
| message: match.groups.msg.trim(), | ||
| source, | ||
| code: match.groups.code, | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (diagnostics.length > 0) { | ||
| this.currentDiagnostics = diagnostics; | ||
| } |
There was a problem hiding this comment.
currentDiagnostics is written but never read — and stale diagnostics accumulate across recompilation cycles
The currentDiagnostics field (line 20) is assigned on lines 36 and 60, but is never read or returned anywhere. This appears to be dead code from an incomplete "replace diagnostics on new cycle" design.
More importantly, the WatcherInstance (in watcher.ts:128) calls this.diagnostics.push(...newDiags) — it only appends, never replaces. So when tsc --watch starts a new compilation cycle and previously-fixed errors are gone, the old diagnostics still persist in the watcher's accumulator. Users would need to manually call clear_diagnostics every time tsc recompiles to get accurate state.
Suggested fix: Either:
- Have the parser signal "new cycle" so the watcher can replace (not append) diagnostics, or
- Document that
clear_diagnosticsshould be called before eachget_diagnosticsfor tsc watchers, or - Have the TscParser's
parse()return a sentinel/flag indicating a fresh cycle
This is the same issue for any tool that re-outputs all diagnostics on each cycle (tsc, eslint in some modes).
| const newDiags = this.parser.parse(text, this.config.id); | ||
| if (newDiags.length > 0) { | ||
| this.diagnostics.push(...newDiags); | ||
| this.lastUpdated = new Date(); | ||
| } |
There was a problem hiding this comment.
outputBuffer
The output buffer has MAX_OUTPUT_BUFFER_LINES = 500 (line 5) to prevent memory growth, but this.diagnostics has no equivalent cap. For long-running watchers that produce frequent output (e.g., a flaky test suite rerunning every few seconds), this array will grow indefinitely.
Consider adding a MAX_DIAGNOSTICS limit with similar ring-buffer behavior, or at minimum document this as a known limitation for v0.1.0.
| private regexes: RegExp[]; | ||
|
|
||
| constructor(patterns: RegexPatternConfig[]) { | ||
| this.regexes = patterns.map((p) => new RegExp(p.pattern, p.flags ?? "gm")); |
There was a problem hiding this comment.
❔ User-supplied regex patterns — any concern about ReDoS?
new RegExp(p.pattern, p.flags) compiles user-provided patterns without validation. While the caller is Claude (not untrusted web input), a poorly-crafted pattern with catastrophic backtracking could hang the parser on certain output lines.
Low risk for v0.1.0 since the MCP tool caller is Claude, but worth considering a timeout or pattern complexity check if this is ever exposed more broadly.
| stop(): void { | ||
| if (this.process && this.exitCode === null) { | ||
| this.process.kill("SIGTERM"); | ||
| // Force kill after 5s | ||
| setTimeout(() => { | ||
| if (this.process && this.exitCode === null) { | ||
| this.process.kill("SIGKILL"); | ||
| } | ||
| }, 5000); | ||
| } |
There was a problem hiding this comment.
✅ Good: graceful shutdown with SIGTERM + SIGKILL fallback
Clean process lifecycle management with a 5-second grace period before force-killing. The exit handler on line 136 also correctly flushes remaining parser buffers.
Rewrites the watch-command-lsp plugin to use the Language Server Protocol instead of MCP. The server now pushes diagnostics proactively via textDocument/publishDiagnostics instead of requiring Claude to poll via MCP tools. Changes: - Replace @modelcontextprotocol/sdk with vscode-languageserver - New .lsp.json config (replaces .mcp.json) - Watchers configured via LSP initializationOptions - Diagnostic change callback on WatcherManager for push notifications - Remove MCP permission hooks (not needed for LSP) - Remove shared lib symlinks (no longer used) https://claude.ai/code/session_012DQQLc4c7iSyVwek6ZgucZ
What
Introduces
watch-command-lsp, a new MCP server plugin that runs watch-mode CLI tools (test runners, linters, compilers) in the background and exposes their parsed diagnostics as queryable MCP tools. This enables Claude to monitor for live errors, test failures, and lint warnings without re-running full suites.Why
When working on code, developers often run watch commands (e.g.,
jest --watch,tsc --watch,eslint --watch) to get real-time feedback. This plugin bridges that gap by:This enables TDD workflows, lint-as-you-go patterns, and live compiler error checking without manual re-runs.
How
The implementation includes:
Core Server (
src/index.ts): MCP server with 6 tools:start_watcher: Launch a watch command with a specified parserstop_watcher: Terminate a running watcherlist_watchers: Show active watchers and their statusget_diagnostics: Query parsed errors/warnings with optional filters (severity, file, watcher ID)get_output: Retrieve raw output lines for debuggingclear_diagnostics: Reset accumulated diagnosticsWatcher Manager (
src/watcher.ts): Manages subprocess lifecycle, output buffering, and parser coordinationOutput Parsers (
src/parsers/):generic.ts: Matchesfile:line:col: severity: messagepatterns (GCC, Clang, Go, Rust, etc.)jest.ts: Parses Jest test output with FAIL blocks and stack traceseslint.ts: Handles ESLint default formatter outputtsc.ts: Parses TypeScript compiler errors with TS error codesregex.ts: User-configurable regex parser with named capture groupsType System (
src/types.ts): Diagnostic, WatcherConfig, OutputParser interfaces modeled after LSPPlugin Integration:
hooks/scripts/setup.sh) auto-approves MCP tool permissions on session startValidation steps
Additional Context
This plugin is part of the nsheaps/ai-mktpl marketplace and integrates with Claude Code's plugin system via MCP. It requires Node.js 18+ or Bun and the watch-mode tools being monitored (jest, eslint, tsc, etc.).
https://claude.ai/code/session_012DQQLc4c7iSyVwek6ZgucZ