From b77a9877d8d342fb8b6c60a0ecbaee1e7e880d9f Mon Sep 17 00:00:00 2001 From: 1998LJ <221638568+1998LJ@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:32:11 +0800 Subject: [PATCH 1/2] feat(discovery): add ~/.claude.json to home discovery (#1) --- src/mcplint/discovery.py | 1 + tests/test_parse.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/mcplint/discovery.py b/src/mcplint/discovery.py index 2610e0b..7e9259f 100644 --- a/src/mcplint/discovery.py +++ b/src/mcplint/discovery.py @@ -17,6 +17,7 @@ ] HOME_CONFIGS: list[tuple[str, str]] = [ + ("~/.claude.json", "claude-code"), ("~/.cursor/mcp.json", "cursor"), ("~/.codeium/windsurf/mcp_config.json", "windsurf"), ("~/.codex/config.toml", "codex"), diff --git a/tests/test_parse.py b/tests/test_parse.py index d295819..5cb7a98 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -84,3 +84,50 @@ def test_nested_configs_are_discovered(tmp_path: Path) -> None: assert ".mcp.json" in names assert "mcp.json" in names assert all("node_modules" not in str(path) for path in configs) + + +def test_claude_json_user_scope_discovery(tmp_path: Path, monkeypatch) -> None: + from mcplint.discovery import discover + + home = tmp_path / "home" + home.mkdir() + claude_json = home / ".claude.json" + claude_json.write_text( + """ + { + "numCachedChunks": 12, + "mcpServers": { + "sqlite": { "command": "uvx", "args": ["mcp-server-sqlite"] } + } + } + """, + encoding="utf-8", + ) + + monkeypatch.setenv("HOME", str(home)) + configs, _ = discover([], include_home=True) + assert any(c.name == ".claude.json" for c in configs) + + parsed = parse_config_file(claude_json) + assert parsed is not None + assert parsed.client == "claude-code" + assert len(parsed.servers) == 1 + assert parsed.servers[0].name == "sqlite" + assert parsed.servers[0].command == ["uvx", "mcp-server-sqlite"] + + +def test_claude_json_malformed_skipped(tmp_path: Path, monkeypatch) -> None: + from mcplint.discovery import discover + + home = tmp_path / "home" + home.mkdir() + claude_json = home / ".claude.json" + claude_json.write_text("{invalid json", encoding="utf-8") + + monkeypatch.setenv("HOME", str(home)) + configs, _ = discover([], include_home=True) + assert any(c.name == ".claude.json" for c in configs) + + # Parsing should return None safely without crashing + assert parse_config_file(claude_json) is None + From 2f53fb1610a74dc1fec87dfc76dd87494549af0a Mon Sep 17 00:00:00 2001 From: 1998LJ <221638568+1998LJ@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:46:01 +0800 Subject: [PATCH 2/2] feat(parse): add MAX_CONFIG_BYTES size cap and regression test for oversized configs --- src/mcplint/parse.py | 4 ++++ tests/test_parse.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/mcplint/parse.py b/src/mcplint/parse.py index a3dd0fc..50e63fd 100644 --- a/src/mcplint/parse.py +++ b/src/mcplint/parse.py @@ -12,6 +12,8 @@ SERVER_KEYS = ("mcpServers", "servers", "mcp", "mcp_servers") +MAX_CONFIG_BYTES = 2_000_000 # 2 MB safety limit for parsing configs + def strip_jsonc(text: str) -> str: """Remove // and /* */ comments plus trailing commas from JSONC.""" @@ -155,6 +157,8 @@ def _load_data(path: Path, text: str) -> dict[str, Any] | None: def parse_config_file(path: Path, client: str | None = None) -> MCPConfigFile | None: try: + if path.stat().st_size > MAX_CONFIG_BYTES: + return None text = path.read_text(encoding="utf-8", errors="replace") except OSError: return None diff --git a/tests/test_parse.py b/tests/test_parse.py index 5cb7a98..228d17f 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -131,3 +131,22 @@ def test_claude_json_malformed_skipped(tmp_path: Path, monkeypatch) -> None: # Parsing should return None safely without crashing assert parse_config_file(claude_json) is None + +def test_claude_json_oversized_skipped(tmp_path: Path, monkeypatch) -> None: + from mcplint.discovery import discover + from mcplint.parse import MAX_CONFIG_BYTES + + home = tmp_path / "home" + home.mkdir() + claude_json = home / ".claude.json" + + # Write a file exceeding MAX_CONFIG_BYTES + claude_json.write_bytes(b" " * (MAX_CONFIG_BYTES + 1)) + + monkeypatch.setenv("HOME", str(home)) + configs, _ = discover([], include_home=True) + assert any(c.name == ".claude.json" for c in configs) + + # Parsing oversized config should safely return None without reading/crashing + assert parse_config_file(claude_json) is None +