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 +31 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/tool-db/investigate-mcp.mjs +31 -7
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
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
|
107
|
-
if (
|
|
108
|
-
const name = trimmed.slice(0,
|
|
109
|
-
const rest = trimmed.slice(
|
|
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 —
|
|
119
|
-
|
|
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;
|