claude-spotter 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.5
4
+
5
+ **ECC プラグイン経由の MCP サーバー 6 件 (context7 / exa / github / memory / playwright / sequential-thinking) のツール群 (61 件) が Spotter のカタログから silent に欠落していた **二重構造バグ**を修正**。実プロジェクト (Web) で `spotter install` 実行時のログに ``mcp investigate failed for "plugin": Command failed: cmd.exe /c claude mcp get plugin`` が **6 連発** で出ていたのを契機に発見。これらのプラグイン MCP の呼び忘れを Spotter が検出できない状態だった (Web プロジェクトで rebuild すると 309 → 370 件、プラグイン由来 61 件が追加されることを確認)。
6
+
7
+ ### 変更点
8
+
9
+ - **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)** (2 段階):
10
+ 1. `parseMcpListOutput` の name 区切りを `indexOf(':')` (コロン単体) から `indexOf(': ')` (コロン + スペース) に変更。サーバー名側に literal `": "` (コロン + スペース) は CLI 仕様上現れない (CLI が name と rest の間に固定でこのペアを置くため) ので、空白入り名前 (`claude.ai Google Drive`) もコロン入り名前 (`plugin:everything-claude-code:context7`) も両立する
11
+ 2. stdio エントリの `command` / `args` を CLI 出力行 (`<name>: <command> <args...> - <status>`) から直接抽出するよう変更。プラグイン MCP は `claude mcp list` には出るが `claude mcp get <name>` では `No MCP server found` で引けない仕様 (実測で確認) のため、CLI 行を唯一の権威ソースとして扱う必要がある。既存 `listMcpToolsOne` の `hasFullConfig` 分岐がそのまま生かされ、`claude mcp get` 経路を skip して直接 spawn するようになる。tokenisation は既存 `splitArgs` と同じ素朴 (whitespace 区切り、quote 非対応) を踏襲、空白入りパスの制約は変わらず
12
+ - **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: 回帰テスト 3 件追加 + 既存 1 件の expectation 拡張 — プラグイン形式 stdio (`plugin:everything-claude-code:context7` を `npx ...` で登録、`command='npx'` / `args=['-y', '@upstash/context7-mcp@2.1.4']` を assert) / プラグイン形式 HTTP (`plugin:everything-claude-code:exa` を `(HTTP)` URL で登録) / 空白入り名前の継続パース (`claude.ai Google Drive` の sse 経路) / 既存 stdio エントリ (`caveat`) も command/args を返すことを assert
13
+
14
+ ### 背景
15
+
16
+ `claude mcp list` の出力フォーマットは `<name>: <url-or-command> [(HTTP)] - <status>` で、name 部分には許容文字に応じてスペースもコロンも入りうる。Spotter は v0.7.0 でこの text パースを導入したが、当時のサンプル (`caveat: ...`, `x-api: ...`) には内部コロンが無かったため `indexOf(':')` で素朴に切っていた。Claude Code 側でプラグイン経由 MCP の名前が `plugin:<plugin-id>:<server>` 形式になったことで、6 サーバー全てが name=`"plugin"` に折り畳まれ、`claude mcp get plugin` が `No MCP server found` で失敗、catalog 投入をスキップする経路に流れていた。
17
+
18
+ step 1 (name 区切り修正) でフルネームは取れるようになったが、Web プロジェクトでの局所実測で **`claude mcp get plugin:everything-claude-code:github` 等のフルネーム指定でも `No MCP server found with name: ...` を返す**ことが判明。プラグイン MCP は `mcp get` の対象外であり、`mcp list` 出力が唯一の権威ソース。step 2 で `parseMcpListOutput` を拡張して command/args を直接 tokenize、`hasFullConfig === true` で再 query を skip させた。
19
+
20
+ `indexOf(': ')` (コロン + スペース) は CLI が固定で挿入する 2 文字ペアであり、サーバー名内部にこのペアが現れることは構造的に無いので、name 内の任意の `:` (コロン単体) と ` ` (スペース単体) を許容しつつ name と rest を一意に切れる。
21
+
22
+ [docs/open-issues.md](docs/open-issues.md) P1 「`claude mcp list` text パースの脆弱性」全体は依然として残る (CLI フォーマット変更耐性は本修正でも上がらない、`--json` 出力が来たら全面切り替えしたい) が、コロン入り名前 + プラグイン MCP の具体例はこの版で塞がる。
23
+
24
+ ## 1.2.4
25
+
26
+ **v1.2.3 で `normalizeProjectPath` の挙動を変えた際に、対になる test の expectation 更新を漏らしたため macOS CI で fail していた hot-fix**。`normalizeProjectPath: separator / trailing slash / Windows case` ([test/tool-db.test.mjs:678](test/tool-db.test.mjs#L678)) は「backslash は常に forward slash になる」という旧仕様の expectation を残したまま v1.2.3 commit に取り込まれており、POSIX 上で `'C:\\Users\\u\\proj'` の入力に対して `'C:\\Users\\u\\proj'` (literal 保持) が返るのを `'C:/Users/u/proj'` で assert していた。Linux CI は v1.2.3 で緑化したが、v1.2.3 push 後の matrix 実行で macOS が同じ test で fail。
27
+
28
+ ### 変更点
29
+
30
+ - **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: `normalizeProjectPath: separator / trailing slash / Windows case` の POSIX 側 expectation を v1.2.3 で確定した「POSIX では backslash を literal に保つ」ルールに合わせ、`'C:\\Users\\u\\proj'` / `'C:/Users\\u/proj'` を変換せず返す挙動を assert。コメントで両 test (`normalizeProjectPath` 単体と `findLocalScopeServers: separator variant matches on Windows only`) の整合理由を明記
31
+
32
+ ソースは v1.2.3 から無変更、test 期待値だけの追従。
33
+
3
34
  ## 1.2.3
4
35
 
5
36
  **v1.2.1 で追加した `normalizeProjectPath` が Linux CI で Windows path key と POSIX path をマッチさせて test を落としていた回帰を修正**。`replace(/\\/g, '/')` をプラットフォーム条件なしで実行していたため、Linux 上で `'C:\Users\u\proj'` (Windows 表記の literal key) と `'C:/Users/u/proj'` (forward-slash 入力) が `C:/Users/u/proj` 同士に正規化されてマッチしてしまい、`findLocalScopeServers` が POSIX で意図しない命中を返していた。CI のみ赤、実運用 (Windows) は元から正しく動いていたので機能影響は無し。
package/README.ja.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/og.svg" alt="Spotter — Audit agent for Claude Code" width="100%">
2
+ <img src=".github/og.png" alt="Spotter — Audit agent for Claude Code" width="100%">
3
3
  </p>
4
4
 
5
5
  # Spotter
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/og.svg" alt="Spotter — Audit agent for Claude Code" width="100%">
2
+ <img src=".github/og.png" alt="Spotter — Audit agent for Claude Code" width="100%">
3
3
  </p>
4
4
 
5
5
  # Spotter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -96,17 +96,25 @@ export async function listMcpServers({ claudeBin = 'claude', projectRoot } = {})
96
96
  // "<name>: <command> <args...> - <status>"
97
97
  // We don't need full parsing — we just need the name and to know if it's an HTTP url
98
98
  // or a stdio command. For stdio we will re-query `claude mcp get <name>` for proper args.
99
+ //
100
+ // Splitter: ": " (colon + SPACE), not ":" alone. Plugin-style server names contain
101
+ // internal colons — e.g. "plugin:everything-claude-code:context7" — and a bare
102
+ // `indexOf(':')` collapses six distinct ECC plugin MCPs into the literal string
103
+ // "plugin", causing `claude mcp get plugin` to fail and silently dropping the
104
+ // servers' tools from the catalog. Server names cannot contain a literal ": "
105
+ // (colon + space) because the CLI uses that exact pair as the line delimiter, so
106
+ // `indexOf(': ')` is safe even for names with spaces (e.g. "claude.ai Google Drive").
99
107
  export function parseMcpListOutput(text) {
100
108
  const out = [];
101
109
  for (const line of text.split('\n')) {
102
110
  const trimmed = line.trim();
103
111
  if (trimmed.length === 0) continue;
104
112
  if (trimmed.startsWith('Checking') || trimmed.startsWith('Note:')) continue;
105
- // Format: "<name>: <rest>"
106
- const colonIdx = trimmed.indexOf(':');
107
- if (colonIdx <= 0) continue;
108
- const name = trimmed.slice(0, colonIdx).trim();
109
- const rest = trimmed.slice(colonIdx + 1).trim();
113
+ // Format: "<name>: <rest>" — split on ": " (colon + space), see comment above.
114
+ const sepIdx = trimmed.indexOf(': ');
115
+ if (sepIdx <= 0) continue;
116
+ const name = trimmed.slice(0, sepIdx).trim();
117
+ const rest = trimmed.slice(sepIdx + 2).trim();
110
118
  // skip the trailing " - ✓ Connected" / " - ✗ Failed"
111
119
  const dashIdx = rest.lastIndexOf(' - ');
112
120
  const beforeStatus = dashIdx > 0 ? rest.slice(0, dashIdx).trim() : rest;
@@ -115,8 +123,24 @@ export function parseMcpListOutput(text) {
115
123
  const url = beforeStatus.replace(/\s*\(HTTP\)$/, '').trim();
116
124
  out.push({ name, transport: isHttp ? 'http' : 'sse', url });
117
125
  } else {
118
- // stdio — defer to `claude mcp get` for properly-tokenized command/args.
119
- out.push({ name, transport: 'stdio' });
126
+ // stdio — extract command + args directly from the CLI line. Plugin-scoped
127
+ // servers (e.g. "plugin:everything-claude-code:context7") cannot be re-
128
+ // queried via `claude mcp get` (CLI returns "No MCP server found with
129
+ // name: ..." even though `mcp list` shows them), so the list line is the
130
+ // only authoritative source for these. Bare-name servers also work this
131
+ // way: tokenisation is naive (whitespace-only, matches splitArgs in
132
+ // getStdioConfig) and matches the existing constraint that command paths
133
+ // must not contain spaces.
134
+ const tokens = beforeStatus.split(/\s+/).filter((t) => t.length > 0);
135
+ // tokens.length === 0 means the CLI emitted "<name>: " followed by only
136
+ // status text (or nothing) — a malformed entry we cannot spawn anyway.
137
+ // We `continue` rather than throw so a single broken line cannot poison
138
+ // refresh for all the other healthy servers in the same `mcp list`. The
139
+ // fact that this entry was dropped is recoverable: the next refresh re-
140
+ // reads the CLI from scratch. This is the same "skip one server, log
141
+ // through listMcpToolsAll" treatment the rest of the path uses.
142
+ if (tokens.length === 0) continue;
143
+ out.push({ name, transport: 'stdio', command: tokens[0], args: tokens.slice(1) });
120
144
  }
121
145
  }
122
146
  return out;