Skip to content

Add watch-command-lsp MCP plugin for live error monitoring#251

Draft
nsheaps wants to merge 5 commits intomainfrom
claude/watch-command-lsp-plugin-OCUL5
Draft

Add watch-command-lsp MCP plugin for live error monitoring#251
nsheaps wants to merge 5 commits intomainfrom
claude/watch-command-lsp-plugin-OCUL5

Conversation

@nsheaps
Copy link
Copy Markdown
Owner

@nsheaps nsheaps commented Mar 15, 2026

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:

  • Spawning and managing watch processes in the background
  • Parsing tool-specific output formats into structured diagnostics
  • Exposing diagnostics via MCP tools that Claude can query at any time
  • Supporting multiple concurrent watchers (e.g., tests + linter + compiler simultaneously)

This enables TDD workflows, lint-as-you-go patterns, and live compiler error checking without manual re-runs.

How

The implementation includes:

  1. Core Server (src/index.ts): MCP server with 6 tools:

    • start_watcher: Launch a watch command with a specified parser
    • stop_watcher: Terminate a running watcher
    • list_watchers: Show active watchers and their status
    • get_diagnostics: Query parsed errors/warnings with optional filters (severity, file, watcher ID)
    • get_output: Retrieve raw output lines for debugging
    • clear_diagnostics: Reset accumulated diagnostics
  2. Watcher Manager (src/watcher.ts): Manages subprocess lifecycle, output buffering, and parser coordination

  3. Output Parsers (src/parsers/):

    • generic.ts: Matches file:line:col: severity: message patterns (GCC, Clang, Go, Rust, etc.)
    • jest.ts: Parses Jest test output with FAIL blocks and stack traces
    • eslint.ts: Handles ESLint default formatter output
    • tsc.ts: Parses TypeScript compiler errors with TS error codes
    • regex.ts: User-configurable regex parser with named capture groups
  4. Type System (src/types.ts): Diagnostic, WatcherConfig, OutputParser interfaces modeled after LSP

  5. Plugin Integration:

    • Setup hook (hooks/scripts/setup.sh) auto-approves MCP tool permissions on session start
    • Configuration files for MCP server registration and plugin metadata
    • Build configuration for Node.js/Bun

Validation steps

  • Parser implementations tested against tool-specific output formats (Jest FAIL blocks, ESLint grouped output, TSC error codes, generic patterns)
  • Watcher lifecycle management (start, stop, graceful shutdown on SIGINT/SIGTERM)
  • Diagnostic filtering by severity and file path
  • Output buffering with configurable limits (500 lines)
  • Multiple concurrent watchers support
  • Custom regex parser with named capture groups
  • MCP tool schema validation and error handling

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

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
@nsheaps nsheaps added the request-review Request a one-time review from the Claude review bot (label is removed after review starts) label Mar 15, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 15, 2026

Plugin Version Status

Versions are auto-bumped in PRs. Manual bumps to higher versions are preserved.

Plugin Base Current Action
watch-command-lsp 0.0.0 0.1.0 Already bumped

@henry-nsheaps henry-nsheaps Bot removed the request-review Request a one-time review from the Claude review bot (label is removed after review starts) label Mar 15, 2026
Copy link
Copy Markdown
Contributor

@henry-nsheaps henry-nsheaps Bot left a comment

Choose a reason for hiding this comment

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

👀 Solid new plugin with good architecture — a couple of correctness issues to address before merge

Code Quality Security Simplicity Confidence

⚠️ 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

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.json with 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-word enabled key)
  • .release-it.js extends base config
  • .mcp.json for 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_DIAGNOSTICS cap similar to MAX_OUTPUT_BUFFER_LINES
  • Consider exposing the shell config option in the MCP tool schema for start_watcher

Notes:12

Footnotes

  1. Workflow Run: https://github.com/nsheaps/ai-mktpl/actions/runs/23118967271/attempts/1

  2. PR: nsheaps/ai-mktpl#251

Comment on lines +20 to +61
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;
}
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.

⚠️ 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:

  1. Have the parser signal "new cycle" so the watcher can replace (not append) diagnostics, or
  2. Document that clear_diagnostics should be called before each get_diagnostics for tsc watchers, or
  3. 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).

Comment on lines +126 to +130
const newDiags = this.parser.parse(text, this.config.id);
if (newDiags.length > 0) {
this.diagnostics.push(...newDiags);
this.lastUpdated = new Date();
}
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.

⚠️ Diagnostics grow unbounded — no size limit like 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"));
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.

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.

Comment on lines +152 to +161
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);
}
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.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants