claude-spotter 0.9.0 → 0.10.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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ **project scope `.mcp.json` 対応**。v0.9.0 は `~/.claude/.mcp.json` (user scope) だけを読んでいたため、プロジェクト直下の `.mcp.json` に登録された MCP サーバーの認証情報 (env / headers) を拾えなかった。`<projectRoot>/.mcp.json` も読んで user scope に merge (project 勝ち = Claude Code 本体の precedence と整合) するよう変更。
6
+
7
+ ### 変更点
8
+
9
+ - **編集 [src/tool-db/mcp-config.mjs](src/tool-db/mcp-config.mjs)**: `readMcpServers({projectRoot})` シグネチャへ変更。`projectRoot` が渡されれば user scope に project scope を merge して返す。missing file は空として扱う
10
+ - **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**: `listMcpServers` / `listMcpToolsAll` が `projectRoot` を受け取って伝搬
11
+ - **編集 [src/tool-db/refresh.mjs](src/tool-db/refresh.mjs)**: `buildInvestigationSnapshot` / `refresh` が `projectRoot` を伝搬 (CLI からは既に渡されている、経路が繋がった)
12
+ - **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: project-scope override + missing-file fallback の 2 ケース追加 (total 99 tests)
13
+
14
+ ### 非互換
15
+
16
+ - `readMcpServers()` → `readMcpServers({projectRoot})`: 引数なしも引き続き動く (user scope のみ) ので既存コードは影響なし
17
+
3
18
  ## 0.9.0
4
19
 
5
20
  **`.mcp.json` を真実源として読み込み、user-registered HTTP/stdio MCP の認証情報を live fetch に活用**。v0.8.0 で HTTP transport を実装したが、`claude mcp list` / `claude mcp get` は bearer token や headers を CLI 出力に含めないため、認証が必要な MCP サーバー (x-api) は依然 401 で落ちていた。`~/.claude/.mcp.json` を直接読んで env / headers を取得、stdio なら spawn 時の env に、HTTP なら fetch request header に渡す。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,7 +87,7 @@ export async function listToolsHttp({ url, serverName, headers: staticHeaders =
87
87
  params: {
88
88
  protocolVersion: PROTOCOL_VERSION,
89
89
  capabilities: {},
90
- clientInfo: { name: 'spotter', version: '0.9.0' },
90
+ clientInfo: { name: 'spotter', version: '0.10.0' },
91
91
  },
92
92
  });
93
93
  if (!initResult || initResult.error) {
@@ -40,8 +40,8 @@ export class McpInvestigationError extends Error {
40
40
 
41
41
  // Returns: Map<server-name, Array<{name, description}>>.
42
42
  // Skips servers that fail (logs the failure) so one broken server doesn't block the rest.
43
- export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude' } = {}) {
44
- const servers = await listMcpServers({ claudeBin });
43
+ export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude', projectRoot } = {}) {
44
+ const servers = await listMcpServers({ claudeBin, projectRoot });
45
45
  const out = new Map();
46
46
  for (const server of servers) {
47
47
  try {
@@ -64,10 +64,10 @@ export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude' }
64
64
  // full descriptor (with env/headers). Otherwise we fall back to the parsed CLI line,
65
65
  // which at minimum gives us name + transport + url (or triggers `claude mcp get` for
66
66
  // stdio command tokenisation).
67
- export async function listMcpServers({ claudeBin = 'claude' } = {}) {
67
+ export async function listMcpServers({ claudeBin = 'claude', projectRoot } = {}) {
68
68
  const [{ stdout }, mcpServers] = await Promise.all([
69
69
  execClaude(claudeBin, ['mcp', 'list'], { encoding: 'utf8' }),
70
- readMcpServers(),
70
+ readMcpServers({ projectRoot }),
71
71
  ]);
72
72
  const cliList = parseMcpListOutput(stdout);
73
73
  return cliList.map((cliEntry) => {
@@ -247,7 +247,7 @@ async function spawnAndQuery({ command, args, env = {} }, serverName) {
247
247
  await request('initialize', {
248
248
  protocolVersion: PROTOCOL_VERSION,
249
249
  capabilities: {},
250
- clientInfo: { name: 'spotter', version: '0.9.0' },
250
+ clientInfo: { name: 'spotter', version: '0.10.0' },
251
251
  });
252
252
  send({ jsonrpc: '2.0', method: 'notifications/initialized' });
253
253
  initializedSent = true;
@@ -6,10 +6,10 @@
6
6
  // of `claude mcp list` / `claude mcp get` hides those secrets. Without them, an HTTP
7
7
  // MCP server returns 401 and a stdio MCP server spawns without its API key.
8
8
  //
9
- // Scope: reads user-level `~/.claude/.mcp.json`. Project-level `.mcp.json` and
10
- // `settings.local.json` are not yet consulted — user scope covers the common case
11
- // (globally-installed MCP servers) and is the scope that Spotter's tool-db is
12
- // global-first anyway.
9
+ // Scope: merges user-level `~/.claude/.mcp.json` with optional project-level
10
+ // `<projectRoot>/.mcp.json`. Project scope overrides user scope on name collision
11
+ // (matches Claude Code's own precedence more-specific scope wins).
12
+ // `settings.local.json` (local scope) is not yet consulted.
13
13
  //
14
14
  // This file does NOT read ~/.claude/.credentials.json (Anthropic OAuth token). That
15
15
  // remains off-limits per the v0.8.0 design decision. `.mcp.json` is user-authored
@@ -23,11 +23,13 @@ export function userMcpConfigPath() {
23
23
  return join(homedir(), '.claude', '.mcp.json');
24
24
  }
25
25
 
26
- // Returns the raw `mcpServers` object from ~/.claude/.mcp.json, or {} if missing.
27
- // Throws only on malformed JSON (not on missing file).
28
- export async function readMcpServers() {
26
+ export function projectMcpConfigPath(projectRoot) {
27
+ return join(projectRoot, '.mcp.json');
28
+ }
29
+
30
+ async function readOne(path) {
29
31
  try {
30
- const text = await readFile(userMcpConfigPath(), 'utf8');
32
+ const text = await readFile(path, 'utf8');
31
33
  const data = JSON.parse(text);
32
34
  return data.mcpServers ?? {};
33
35
  } catch (err) {
@@ -36,6 +38,16 @@ export async function readMcpServers() {
36
38
  }
37
39
  }
38
40
 
41
+ // Returns the merged `mcpServers` object: user scope as base, project scope overrides
42
+ // on name collision. Missing files are treated as empty (not an error). If projectRoot
43
+ // is not supplied, only user scope is read.
44
+ export async function readMcpServers({ projectRoot } = {}) {
45
+ const user = await readOne(userMcpConfigPath());
46
+ if (!projectRoot) return user;
47
+ const project = await readOne(projectMcpConfigPath(projectRoot));
48
+ return { ...user, ...project };
49
+ }
50
+
39
51
  // Normalise an `.mcp.json` entry into a server descriptor the investigator can use.
40
52
  // Returns { name, transport: 'stdio'|'http'|'sse', ...transport-specific fields } or
41
53
  // null if the entry is not recognisable.
@@ -17,7 +17,7 @@ import { localDbPath, globalDbPath } from './loader.mjs';
17
17
  // - Deferred: from the hardcoded baseline
18
18
  //
19
19
  // Returns an in-memory snapshot the caller can use as the investigate() backend.
20
- export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin = 'claude' } = {}) {
20
+ export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin = 'claude', projectRoot } = {}) {
21
21
  const snapshot = new Map();
22
22
 
23
23
  // Deferred built-ins (Claude Code deferred tools like WebFetch, TodoWrite, etc.).
@@ -32,8 +32,9 @@ export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin =
32
32
  snapshot.set(name, getClaudeAiDescription(name));
33
33
  }
34
34
 
35
- // MCP servers (stdio + user-registered HTTP/SSE).
36
- const mcp = await listMcpToolsAll({ logFn, claudeBin });
35
+ // MCP servers (stdio + user-registered HTTP/SSE). projectRoot forwards for
36
+ // project-scope `.mcp.json` merge (see mcp-config.mjs).
37
+ const mcp = await listMcpToolsAll({ logFn, claudeBin, projectRoot });
37
38
  for (const [serverName, tools] of mcp.entries()) {
38
39
  for (const tool of tools) {
39
40
  if (!tool.description || tool.description.length === 0) continue;
@@ -47,7 +48,7 @@ export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin =
47
48
  // Refresh the tool-db. Discovers all currently available tools, resolves each via the
48
49
  // 3-tier lookup, writes through. Returns the resolved Map.
49
50
  export async function refresh({ projectRoot, logFn = () => {}, claudeBin = 'claude' }) {
50
- const snapshot = await buildInvestigationSnapshot({ logFn, claudeBin });
51
+ const snapshot = await buildInvestigationSnapshot({ logFn, claudeBin, projectRoot });
51
52
  const toolNames = Array.from(snapshot.keys());
52
53
  const investigate = async (name) => snapshot.get(name) ?? null;
53
54
 
package/src/version.mjs CHANGED
@@ -1 +1 @@
1
- export const version = '0.9.0';
1
+ export const version = '0.10.0';