claude-spotter 0.7.0 → 0.8.0
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,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
**HTTP/SSE MCP transport 対応 + Windows `.cmd` 経路の ENOENT fix + claude.ai 系 MCP の hardcoded baseline**。v0.7.0 を実測したら Windows で `spotter db refresh` が `spawn claude ENOENT` で起動すらせず、fix した上で動かしたら今度は Gmail / Google Calendar / Google Drive / x-api が丸ごと抜け落ちて Haiku の視野に入らない状態だった。この 3 本を同時に潰した。
|
|
6
|
+
|
|
7
|
+
### 事の発端
|
|
8
|
+
|
|
9
|
+
v0.7.0 リリース直後、新規セッションで `spotter db refresh` を打ったら即失敗:
|
|
10
|
+
|
|
11
|
+
1. **Windows で `spawn claude ENOENT`**: Node の `execFile` / `spawn` は Windows で `.cmd` 拡張子のラッパーを直接起動できない。`doctor` は `cmd.exe /c claude` 経由で回避していたが、新規コード [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs) は `execFileP(claudeBin, ...)` をそのまま使っていて全 Windows 環境で DB refresh 不可。
|
|
12
|
+
2. **HTTP/SSE transport 全滅**: v0.7.0 は stdio しか実装していなかったため、`claude.ai Google Drive` / `claude.ai Google Calendar` / `claude.ai Gmail` / `x-api` の 4 サーバーが `sse transport not yet supported` / `http transport not yet supported` でスキップされ、Haiku は Gmail や Calendar のツールを推奨できない状態だった。
|
|
13
|
+
3. **claude.ai 系 MCP は `claude mcp get` で取得不可**: `claude mcp get "claude.ai Gmail"` は `No MCP server found` を返す。Anthropic 提供の MCP は `.mcp.json` に登録されず、Claude Code は `~/.claude/.credentials.json` の OAuth token を使って `mcp-proxy.anthropic.com` に直接アクセスしている。Spotter は credentials を読まない方針なので、これらは **hardcoded baseline** でカバーする。
|
|
14
|
+
|
|
15
|
+
### 変更点
|
|
16
|
+
|
|
17
|
+
- **新規 [src/tool-db/investigate-mcp-http.mjs](src/tool-db/investigate-mcp-http.mjs)**: MCP Streamable HTTP transport 実装。POST + `Content-Type: application/json` + `Accept: application/json, text/event-stream` で JSON-RPC 往復、`Mcp-Session-Id` header で session 維持、SSE 形式レスポンスも parse。`initialize` → `notifications/initialized` → `tools/list` を 10 秒 timeout で実行
|
|
18
|
+
- **新規 [src/tool-db/claude-ai-baseline.mjs](src/tool-db/claude-ai-baseline.mjs)**: `claude.ai Gmail` (10) / `claude.ai Google Calendar` (8) / `claude.ai Google Drive` (7) の合計 25 件の {name, description} を手書き baseline として保持。deferred-baseline と同じ設計パターン
|
|
19
|
+
- **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**:
|
|
20
|
+
- `execClaude()` helper 追加: Windows では `execFileP('cmd.exe', ['/c', claudeBin, ...args])` 経由、他は `execFileP(claudeBin, args)` そのまま。`listMcpServers` と `getStdioConfig` の 2 箇所で利用
|
|
21
|
+
- `buildStdioSpawn()` helper 追加: MCP stdio サーバーの Command が Windows で `.cmd` / `.bat` 拡張子なら `cmd.exe /c` 経由で spawn (caveat 等 `.exe` は影響なし)
|
|
22
|
+
- `listMcpToolsOne` の HTTP/SSE 分岐を `listToolsHttp` に dispatch
|
|
23
|
+
- **編集 [src/tool-db/refresh.mjs](src/tool-db/refresh.mjs)**: `buildInvestigationSnapshot` に claude.ai baseline を deferred の直後に merge。後続の live HTTP investigate が成功すれば上書き
|
|
24
|
+
- **編集 [package.json](package.json), [src/version.mjs](src/version.mjs)**: `0.7.0` → `0.8.0`
|
|
25
|
+
|
|
26
|
+
### 実測
|
|
27
|
+
|
|
28
|
+
`spotter db rebuild` で **48 tools resolved**:
|
|
29
|
+
- deferred baseline: 17 (Claude Code 組込み遅延ツール)
|
|
30
|
+
- claude.ai baseline: 25 (Gmail 10 + Calendar 8 + Drive 7)
|
|
31
|
+
- stdio MCP (Caveat): 6
|
|
32
|
+
|
|
33
|
+
`claude.ai Gmail/Calendar/Drive` への live HTTP fetch は **HTTP 403 Forbidden** (認証 token なしで直接叩けない、想定通り) → baseline がカバー。`x-api` は **HTTP 401 Unauthorized** (ユーザー設定 MCP で Authorization header 未対応、次回課題)。`caveat` は stdio で正常 fetch。
|
|
34
|
+
|
|
35
|
+
### 残る課題
|
|
36
|
+
|
|
37
|
+
- **ユーザー設定 HTTP MCP の認証 header**: `claude mcp get <name>` の出力から Authorization 等を抽出して fetch に付与する仕組みが未実装。v0.8.0 では x-api が落ちる (baseline でも救えない、公開情報でない)
|
|
38
|
+
- **claude.ai baseline の手動メンテ**: Anthropic が tool を追加・変更したら手で追従。deferred baseline と同じ trade-off
|
|
39
|
+
|
|
3
40
|
## 0.7.0
|
|
4
41
|
|
|
5
42
|
**カタログを tool-db に置き換え**。手書きの `tools.yaml` (5 つの抽象ツール) を捨て、**実際にセッションで使えるツール (MCP + Claude Code 組込み 遅延ツール) の name + description を自動収集してキャッシュする** 仕組みに置き換え。
|
package/package.json
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Hardcoded description baseline for Anthropic-provided `claude.ai ...` MCP servers
|
|
2
|
+
// (Gmail / Google Calendar / Google Drive).
|
|
3
|
+
//
|
|
4
|
+
// Why hardcoded, not fetched:
|
|
5
|
+
// These servers are NOT registered in local `.mcp.json`. Claude Code calls them via
|
|
6
|
+
// its own mcp-proxy.anthropic.com endpoint using the OAuth token in
|
|
7
|
+
// ~/.claude/.credentials.json. Spotter deliberately does NOT read credentials — so
|
|
8
|
+
// we cannot dynamically invoke tools/list against the proxy. The tool surface is
|
|
9
|
+
// public (the same deferred-tool list Claude Code exposes) and changes slowly.
|
|
10
|
+
//
|
|
11
|
+
// Maintenance:
|
|
12
|
+
// When Anthropic adds new tools to these MCP servers, append them here. The daemon
|
|
13
|
+
// merges this baseline into the tool-db during `spotter db refresh`.
|
|
14
|
+
//
|
|
15
|
+
// Bell-visible names follow the convention `mcp__<server-id>__<tool-name>` where
|
|
16
|
+
// <server-id> is the `claude.ai ...` server name with non-[A-Za-z0-9_-] chars replaced
|
|
17
|
+
// by `_` — e.g. "claude.ai Gmail" → "claude_ai_Gmail".
|
|
18
|
+
|
|
19
|
+
const GMAIL = {
|
|
20
|
+
mcp__claude_ai_Gmail__search_threads:
|
|
21
|
+
'Search Gmail threads matching a query (e.g. from:alice subject:invoice, is:unread, after:2024-01-01). Use when the user asks to find, read, or reference specific mail.',
|
|
22
|
+
mcp__claude_ai_Gmail__get_thread:
|
|
23
|
+
'Fetch the full body of a Gmail thread by id — all messages, headers, and text. Use after search_threads to read the actual content.',
|
|
24
|
+
mcp__claude_ai_Gmail__list_drafts:
|
|
25
|
+
'List existing Gmail drafts. Use before create_draft when the user asks about pending unsent mail or wants to resume a draft.',
|
|
26
|
+
mcp__claude_ai_Gmail__create_draft:
|
|
27
|
+
'Create a new Gmail draft (to, subject, body, optional cc/bcc). Does NOT send. Use when the user asks Claude to compose a message for later review.',
|
|
28
|
+
mcp__claude_ai_Gmail__list_labels:
|
|
29
|
+
'List all Gmail labels (both system and user-created). Use before label_message / label_thread to find the correct label id.',
|
|
30
|
+
mcp__claude_ai_Gmail__create_label:
|
|
31
|
+
'Create a new Gmail label. Use when the user wants to organize mail into a category that does not yet exist.',
|
|
32
|
+
mcp__claude_ai_Gmail__label_message:
|
|
33
|
+
'Apply a label to a single Gmail message by id.',
|
|
34
|
+
mcp__claude_ai_Gmail__unlabel_message:
|
|
35
|
+
'Remove a label from a single Gmail message by id.',
|
|
36
|
+
mcp__claude_ai_Gmail__label_thread:
|
|
37
|
+
'Apply a label to an entire Gmail thread (all messages in the thread).',
|
|
38
|
+
mcp__claude_ai_Gmail__unlabel_thread:
|
|
39
|
+
'Remove a label from an entire Gmail thread.',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CALENDAR = {
|
|
43
|
+
mcp__claude_ai_Google_Calendar__list_calendars:
|
|
44
|
+
'List all Google Calendars the user has access to (primary, shared, subscribed). Use first when the user does not specify which calendar.',
|
|
45
|
+
mcp__claude_ai_Google_Calendar__list_events:
|
|
46
|
+
'List events in a calendar over a time range. Use when the user asks about upcoming meetings, schedule conflicts, or "what is on my calendar".',
|
|
47
|
+
mcp__claude_ai_Google_Calendar__get_event:
|
|
48
|
+
'Fetch a single event by id with full details (attendees, description, attachments).',
|
|
49
|
+
mcp__claude_ai_Google_Calendar__create_event:
|
|
50
|
+
'Create a new Google Calendar event (title, start/end, attendees, optional location/description). Use when the user asks to schedule something.',
|
|
51
|
+
mcp__claude_ai_Google_Calendar__update_event:
|
|
52
|
+
'Modify an existing event (reschedule, change attendees, update description). Use when the user asks to move or edit a meeting.',
|
|
53
|
+
mcp__claude_ai_Google_Calendar__delete_event:
|
|
54
|
+
'Delete an event from Google Calendar. Use when the user asks to cancel a meeting.',
|
|
55
|
+
mcp__claude_ai_Google_Calendar__respond_to_event:
|
|
56
|
+
'RSVP to an event invitation (accept / decline / tentative).',
|
|
57
|
+
mcp__claude_ai_Google_Calendar__suggest_time:
|
|
58
|
+
'Find free time slots across multiple calendars / attendees. Use when the user asks to find a mutually available meeting time.',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const DRIVE = {
|
|
62
|
+
mcp__claude_ai_Google_Drive__search_files:
|
|
63
|
+
'Search Google Drive for files matching a query (name, content, mimeType). Use when the user asks to find a doc/sheet/slide by topic or title.',
|
|
64
|
+
mcp__claude_ai_Google_Drive__list_recent_files:
|
|
65
|
+
'List recently accessed Google Drive files. Use when the user asks "what was I just working on in Drive?" or similar.',
|
|
66
|
+
mcp__claude_ai_Google_Drive__get_file_metadata:
|
|
67
|
+
'Fetch metadata (name, mimeType, owner, modified time, sharing) for a Drive file by id — NOT the content. Use before read_file_content to verify the file type.',
|
|
68
|
+
mcp__claude_ai_Google_Drive__read_file_content:
|
|
69
|
+
'Read the textual content of a Drive file (Docs, Sheets, plain text). Use when the user asks Claude to reference or summarize a file.',
|
|
70
|
+
mcp__claude_ai_Google_Drive__download_file_content:
|
|
71
|
+
'Download binary file content from Drive (PDF, image, zip). Use when the user needs the raw bytes rather than extracted text.',
|
|
72
|
+
mcp__claude_ai_Google_Drive__create_file:
|
|
73
|
+
'Create a new file in Google Drive (text content or uploaded blob). Use when the user asks to save something to Drive.',
|
|
74
|
+
mcp__claude_ai_Google_Drive__get_file_permissions:
|
|
75
|
+
'List who has access to a Drive file and their role (reader/commenter/writer/owner). Use when the user asks who can see or edit a file.',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const ALL = { ...GMAIL, ...CALENDAR, ...DRIVE };
|
|
79
|
+
|
|
80
|
+
export function listClaudeAiNames() {
|
|
81
|
+
return Object.keys(ALL);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getClaudeAiDescription(name) {
|
|
85
|
+
return ALL[name] ?? null;
|
|
86
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// HTTP (Streamable HTTP transport) MCP investigation.
|
|
2
|
+
//
|
|
3
|
+
// Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http
|
|
4
|
+
// Wire:
|
|
5
|
+
// POST <url> with JSON-RPC body + Content-Type: application/json
|
|
6
|
+
// Accept: application/json, text/event-stream (server may reply with either)
|
|
7
|
+
// First response may set Mcp-Session-Id; echo it on subsequent requests.
|
|
8
|
+
//
|
|
9
|
+
// We only need initialize → notifications/initialized → tools/list; the handshake is
|
|
10
|
+
// plain request/response, no long-lived SSE stream required.
|
|
11
|
+
//
|
|
12
|
+
// Auth: `claude mcp get <name>` does NOT expose bearer tokens for user-configured
|
|
13
|
+
// servers. If a server requires auth beyond public URL access, investigate will fail
|
|
14
|
+
// with a descriptive error — caller logs and skips that server.
|
|
15
|
+
|
|
16
|
+
const PROTOCOL_VERSION = '2025-03-26';
|
|
17
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
19
|
+
export class McpHttpError extends Error {
|
|
20
|
+
constructor(message, server) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'McpHttpError';
|
|
23
|
+
this.server = server;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function listToolsHttp({ url, serverName }) {
|
|
28
|
+
let sessionId = null;
|
|
29
|
+
|
|
30
|
+
const post = async (body) => {
|
|
31
|
+
const ctl = new AbortController();
|
|
32
|
+
const t = setTimeout(() => ctl.abort(), REQUEST_TIMEOUT_MS);
|
|
33
|
+
try {
|
|
34
|
+
const headers = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Accept': 'application/json, text/event-stream',
|
|
37
|
+
};
|
|
38
|
+
if (sessionId) headers['Mcp-Session-Id'] = sessionId;
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers,
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
signal: ctl.signal,
|
|
44
|
+
});
|
|
45
|
+
const sid = res.headers.get('mcp-session-id');
|
|
46
|
+
if (sid && !sessionId) sessionId = sid;
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new McpHttpError(`HTTP ${res.status} ${res.statusText}`, serverName);
|
|
49
|
+
}
|
|
50
|
+
// Response may be application/json or text/event-stream.
|
|
51
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
52
|
+
if (contentType.includes('text/event-stream')) {
|
|
53
|
+
return parseSseSingle(await res.text(), serverName);
|
|
54
|
+
}
|
|
55
|
+
if (contentType.includes('application/json')) {
|
|
56
|
+
return await res.json();
|
|
57
|
+
}
|
|
58
|
+
// Some servers omit content-type; try JSON.
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
if (text.trim().length === 0) return null;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new McpHttpError(`unexpected response content-type "${contentType}"`, serverName);
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(t);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const postNotification = async (body) => {
|
|
72
|
+
// Notifications may return 202 Accepted with empty body; swallow the result.
|
|
73
|
+
await post(body).catch((err) => {
|
|
74
|
+
// Some servers reject notifications with no body response as non-2xx; treat as OK
|
|
75
|
+
// only if the error is clearly a parse issue on empty body.
|
|
76
|
+
if (err instanceof McpHttpError) throw err;
|
|
77
|
+
throw err;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 1. initialize
|
|
82
|
+
const initResult = await post({
|
|
83
|
+
jsonrpc: '2.0',
|
|
84
|
+
id: 1,
|
|
85
|
+
method: 'initialize',
|
|
86
|
+
params: {
|
|
87
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
88
|
+
capabilities: {},
|
|
89
|
+
clientInfo: { name: 'spotter', version: '0.8.0' },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!initResult || initResult.error) {
|
|
93
|
+
throw new McpHttpError(`initialize failed: ${JSON.stringify(initResult?.error ?? initResult)}`, serverName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. notifications/initialized
|
|
97
|
+
await postNotification({
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
method: 'notifications/initialized',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 3. tools/list
|
|
103
|
+
const listResult = await post({
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
id: 2,
|
|
106
|
+
method: 'tools/list',
|
|
107
|
+
params: {},
|
|
108
|
+
});
|
|
109
|
+
if (!listResult || listResult.error) {
|
|
110
|
+
throw new McpHttpError(`tools/list failed: ${JSON.stringify(listResult?.error ?? listResult)}`, serverName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tools = (listResult.result?.tools ?? []).map((t) => ({
|
|
114
|
+
name: t.name,
|
|
115
|
+
description: typeof t.description === 'string' ? t.description : '',
|
|
116
|
+
}));
|
|
117
|
+
return tools;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// SSE responses wrap a single JSON message in one `data:` event. Parse it out.
|
|
121
|
+
function parseSseSingle(sseText, serverName) {
|
|
122
|
+
for (const rawLine of sseText.split('\n')) {
|
|
123
|
+
const line = rawLine.trim();
|
|
124
|
+
if (line.startsWith('data:')) {
|
|
125
|
+
const payload = line.slice('data:'.length).trim();
|
|
126
|
+
if (payload.length === 0) continue;
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(payload);
|
|
129
|
+
} catch {
|
|
130
|
+
throw new McpHttpError(`failed to parse SSE data payload`, serverName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw new McpHttpError(`no data event in SSE response`, serverName);
|
|
135
|
+
}
|
|
@@ -12,12 +12,23 @@
|
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { execFile } from 'node:child_process';
|
|
14
14
|
import { promisify } from 'node:util';
|
|
15
|
+
import { listToolsHttp } from './investigate-mcp-http.mjs';
|
|
15
16
|
|
|
16
17
|
const execFileP = promisify(execFile);
|
|
17
18
|
|
|
18
19
|
const PROTOCOL_VERSION = '2025-03-26'; // MCP protocol version we claim to speak.
|
|
19
20
|
const HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
20
21
|
|
|
22
|
+
// On Windows, `claude` is a .cmd shim; Node's execFile cannot locate it directly without
|
|
23
|
+
// going through cmd.exe. Matches the pattern in src/daemon/haiku-caller.mjs buildSpawnArgs.
|
|
24
|
+
// We use cmd.exe /c rather than shell:true to avoid DEP0190 on Node 24+.
|
|
25
|
+
async function execClaude(claudeBin, args, opts) {
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
return execFileP('cmd.exe', ['/c', claudeBin, ...args], opts);
|
|
28
|
+
}
|
|
29
|
+
return execFileP(claudeBin, args, opts);
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export class McpInvestigationError extends Error {
|
|
22
33
|
constructor(message, server) {
|
|
23
34
|
super(message);
|
|
@@ -45,7 +56,7 @@ export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude' }
|
|
|
45
56
|
// Parse `claude mcp list` output. Returns array of {name, transport, command, url}.
|
|
46
57
|
// stdio servers have command, http/sse servers have url.
|
|
47
58
|
export async function listMcpServers({ claudeBin = 'claude' } = {}) {
|
|
48
|
-
const { stdout } = await
|
|
59
|
+
const { stdout } = await execClaude(claudeBin, ['mcp', 'list'], { encoding: 'utf8' });
|
|
49
60
|
return parseMcpListOutput(stdout);
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -89,17 +100,20 @@ export async function listMcpToolsOne({ server, logFn = () => {}, claudeBin = 'c
|
|
|
89
100
|
return spawnAndQuery(config, server.name);
|
|
90
101
|
}
|
|
91
102
|
if (server.transport === 'http' || server.transport === 'sse') {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
103
|
+
// Streamable HTTP transport. For `claude.ai ...` servers, `claude mcp get` rejects
|
|
104
|
+
// them (they are not in local .mcp.json) so server.url is unavailable — those are
|
|
105
|
+
// covered by src/tool-db/claude-ai-baseline.mjs at a higher layer.
|
|
106
|
+
if (!server.url) {
|
|
107
|
+
throw new McpInvestigationError(`no URL available for ${server.transport} server`, server.name);
|
|
108
|
+
}
|
|
109
|
+
return listToolsHttp({ url: server.url, serverName: server.name });
|
|
96
110
|
}
|
|
97
111
|
throw new McpInvestigationError(`unknown transport: ${server.transport}`, server.name);
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
// Parse `claude mcp get <name>` to extract Command + Args for stdio servers.
|
|
101
115
|
async function getStdioConfig({ name, claudeBin }) {
|
|
102
|
-
const { stdout } = await
|
|
116
|
+
const { stdout } = await execClaude(claudeBin, ['mcp', 'get', name], { encoding: 'utf8' });
|
|
103
117
|
let command = null;
|
|
104
118
|
let argsRaw = null;
|
|
105
119
|
for (const rawLine of stdout.split('\n')) {
|
|
@@ -119,9 +133,19 @@ function splitArgs(s) {
|
|
|
119
133
|
return s.split(/\s+/).filter((t) => t.length > 0);
|
|
120
134
|
}
|
|
121
135
|
|
|
136
|
+
// On Windows, `.cmd` / `.bat` shims cannot be spawned directly without cmd.exe.
|
|
137
|
+
// Unix-like paths or `.exe` binaries go through spawn as-is.
|
|
138
|
+
function buildStdioSpawn(command, args) {
|
|
139
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(command)) {
|
|
140
|
+
return { cmd: 'cmd.exe', cmdArgs: ['/c', command, ...args] };
|
|
141
|
+
}
|
|
142
|
+
return { cmd: command, cmdArgs: args };
|
|
143
|
+
}
|
|
144
|
+
|
|
122
145
|
async function spawnAndQuery({ command, args }, serverName) {
|
|
123
146
|
return new Promise((resolve, reject) => {
|
|
124
|
-
const
|
|
147
|
+
const { cmd, cmdArgs } = buildStdioSpawn(command, args);
|
|
148
|
+
const child = spawn(cmd, cmdArgs, {
|
|
125
149
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
150
|
windowsHide: true,
|
|
127
151
|
});
|
|
@@ -199,7 +223,7 @@ async function spawnAndQuery({ command, args }, serverName) {
|
|
|
199
223
|
await request('initialize', {
|
|
200
224
|
protocolVersion: PROTOCOL_VERSION,
|
|
201
225
|
capabilities: {},
|
|
202
|
-
clientInfo: { name: 'spotter', version: '0.
|
|
226
|
+
clientInfo: { name: 'spotter', version: '0.8.0' },
|
|
203
227
|
});
|
|
204
228
|
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
205
229
|
initializedSent = true;
|
package/src/tool-db/refresh.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { resolveAll } from './lookup.mjs';
|
|
9
9
|
import { listMcpToolsAll, bellVisibleName } from './investigate-mcp.mjs';
|
|
10
10
|
import { getDeferredDescription, listDeferredNames } from './deferred-baseline.mjs';
|
|
11
|
+
import { getClaudeAiDescription, listClaudeAiNames } from './claude-ai-baseline.mjs';
|
|
11
12
|
import { localDbPath, globalDbPath } from './loader.mjs';
|
|
12
13
|
|
|
13
14
|
// Build the (name → description) map for an investigation pass:
|
|
@@ -19,12 +20,19 @@ import { localDbPath, globalDbPath } from './loader.mjs';
|
|
|
19
20
|
export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin = 'claude' } = {}) {
|
|
20
21
|
const snapshot = new Map();
|
|
21
22
|
|
|
22
|
-
// Deferred built-ins.
|
|
23
|
+
// Deferred built-ins (Claude Code deferred tools like WebFetch, TodoWrite, etc.).
|
|
23
24
|
for (const name of listDeferredNames()) {
|
|
24
25
|
snapshot.set(name, getDeferredDescription(name));
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// MCP servers
|
|
28
|
+
// Anthropic-provided `claude.ai ...` MCP servers — hardcoded because the OAuth proxy
|
|
29
|
+
// is not reachable without reading ~/.claude/.credentials.json (deliberately avoided).
|
|
30
|
+
// If a live HTTP investigate for the same name later succeeds below, it overrides.
|
|
31
|
+
for (const name of listClaudeAiNames()) {
|
|
32
|
+
snapshot.set(name, getClaudeAiDescription(name));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MCP servers (stdio + user-registered HTTP/SSE).
|
|
28
36
|
const mcp = await listMcpToolsAll({ logFn, claudeBin });
|
|
29
37
|
for (const [serverName, tools] of mcp.entries()) {
|
|
30
38
|
for (const tool of tools) {
|
package/src/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.8.0';
|