graphlens-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikita Rybalchenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.3
2
+ Name: graphlens-mcp
3
+ Version: 0.1.0
4
+ Summary: Semantic code graph MCP server for coding agents
5
+ Author: Neko1313
6
+ Author-email: Neko1313 <nikita.ribalchencko@yandex.ru>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 Nikita Rybalchenko
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Requires-Dist: aiosqlite>=0.22.1
29
+ Requires-Dist: click>=8.4.2
30
+ Requires-Dist: fastmcp>=3.4.2
31
+ Requires-Dist: graphlens[go,python,rust,typescript,php]>=0.7.0
32
+ Requires-Dist: questionary>=2.0.1
33
+ Requires-Dist: tomlkit>=0.15.0
34
+ Requires-Dist: watchfiles>=0.24
35
+ Requires-Python: >=3.13
36
+ Description-Content-Type: text/markdown
37
+
38
+ # graphlens-mcp
39
+
40
+ [![CI](https://github.com/Neko1313/graphlens-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Neko1313/graphlens-mcp/actions/workflows/ci.yml)
41
+ [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://neko1313.github.io/graphlens-mcp/)
42
+ [![Python](https://img.shields.io/badge/python-%E2%89%A53.13-blue)](https://www.python.org/)
43
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
44
+
45
+ A free, MIT-licensed [MCP](https://modelcontextprotocol.io) server that gives coding
46
+ agents (Claude Code, Cursor, and compatible clients) a **semantic code graph** of your
47
+ project — symbols, cross-file calls, references, imports and cross-language boundaries.
48
+
49
+ Instead of reading files top-to-bottom or grepping for names, the agent **navigates the
50
+ structure**: *who calls this function*, *what does it depend on*, *what breaks if I change
51
+ its signature*. It is a thin runtime layer over the
52
+ [`graphlens`](https://github.com/Neko1313/graphlens) analysis engine: `graphlens` provides
53
+ the mechanisms (parsing, stable node identity, resolvers); `graphlens-mcp` owns the storage,
54
+ freshness and the agent-facing surface.
55
+
56
+ 📖 **Documentation:** <https://neko1313.github.io/graphlens-mcp/>
57
+
58
+ > Status: early. The core navigation works; see [Known limitations](#known-limitations).
59
+
60
+ ## Why
61
+
62
+ A `filesystem`/grep MCP makes the agent read whole files and match text — slow, noisy, and
63
+ blind to which of three modules actually calls `OrderService.create`. Bare tree-sitter
64
+ gives single-file syntax but cannot resolve links *between* files. `graphlens-mcp` answers
65
+ the cross-file questions — call graphs and impact analysis — and keeps the graph fresh as
66
+ you edit, then teaches the agent to use it via a bundled navigation skill.
67
+
68
+ ## Install
69
+
70
+ Requires **Python ≥ 3.13** (a constraint inherited from `graphlens`).
71
+
72
+ ```bash
73
+ uv tool install graphlens-mcp # or: pipx install graphlens-mcp
74
+ ```
75
+
76
+ Python language analysis works out of the box (the `ty` type engine ships as a
77
+ dependency). Other languages parse immediately and unlock full cross-file semantics once
78
+ their toolchain is present (Node for TypeScript, the Go toolchain, etc.); without it that
79
+ language is reported as `degraded` rather than blocking `init`.
80
+
81
+ ## Quickstart (two commands)
82
+
83
+ ```bash
84
+ uv tool install graphlens-mcp # 1. install
85
+ cd your-project && graphlens-mcp init # 2. index + configure your agent
86
+ ```
87
+
88
+ `init` detects the project's languages, indexes the code into a local graph, writes the
89
+ MCP server entry into your agent's config and installs the navigation skill. You do **not**
90
+ run `serve` yourself — your agent launches it from the config. Restart the agent and ask
91
+ it something like *"what breaks if I change the signature of `create_order`?"*.
92
+
93
+ ## Commands
94
+
95
+ | Command | What it does |
96
+ |---|---|
97
+ | `graphlens-mcp init` | Detect languages → toolchain doctor → full index → configure agents → install skill |
98
+ | `graphlens-mcp serve` | Start the MCP server over stdio. **Launched by the agent**, not by you |
99
+ | `graphlens-mcp status` | Show detected languages, toolchain status, and graph size/freshness |
100
+ | `graphlens-mcp reindex` | Force a full rebuild (e.g. after installing a new toolchain) |
101
+ | `graphlens-mcp remove` | Deregister from agents and (with `--purge-db`) delete the local graph |
102
+
103
+ Useful `init` flags: `--root <dir>`, `--agent claude_code --agent cursor` (repeatable),
104
+ `--no-agent`, `--no-skills`, `--db <path>`.
105
+
106
+ The graph lives at `<project>/.graphlens/graph.db` (SQLite). It is a regenerable cache —
107
+ safe to delete; `reindex` rebuilds it. Add `.graphlens/` to your VCS ignore (the bundled
108
+ `init` flow assumes it is not committed).
109
+
110
+ ## Supported languages
111
+
112
+ | Language | Engine | Out-of-box |
113
+ |---|---|---|
114
+ | Python | `ty` (bundled) | Full semantics immediately |
115
+ | TypeScript | Node bridge | `degraded` without Node; full semantics with Node installed |
116
+ | Go | Go toolchain | `degraded` without toolchain |
117
+ | Rust | SCIP / rust-analyzer | `degraded` without toolchain |
118
+ | PHP | PHP parser | `degraded` without toolchain |
119
+
120
+ `graphlens-mcp status` reports the actual resolver status per language. When a toolchain is
121
+ missing, that language is reported as **degraded** (parsed structure, calls/types not fully
122
+ resolved) with an install hint — it never blocks `init`.
123
+
124
+ ## Agent tools
125
+
126
+ Each response carries a graph-quality status (`ok` | `degraded`) so the agent never mistakes
127
+ a partial answer for a complete one.
128
+
129
+ | Tool | Purpose |
130
+ |---|---|
131
+ | `search_symbols` | Full-text search over symbol names — **start here** |
132
+ | `get_node_info` | Source snippet + signature + location for a node |
133
+ | `get_file_structure` | Symbol outline of a file |
134
+ | `get_callees` | What a function calls (outgoing, up to `max_depth`) |
135
+ | `get_callers` | Who calls a function — primary impact-analysis tool |
136
+ | `get_neighbors` | Nodes within N hops in any direction |
137
+ | `find_references` | Non-call usages (type annotations, assignments) |
138
+ | `get_cross_language_calls` | Connections across service boundaries (HTTP/gRPC/queues) |
139
+
140
+ ## Freshness model
141
+
142
+ A single mechanism keeps the graph current: a **filesystem watcher** (`serve` starts it by
143
+ default; disable with `--no-watch`). When a file changes on disk the server re-indexes the
144
+ **connected set** — the changed file plus the files that import it and the files it imports —
145
+ with one full analyze, so cross-file edges are rebuilt correctly rather than left partial.
146
+ Deleting a file prunes its symbols and refreshes its importers. There is no polling and no
147
+ structure-only "skeleton" phase: every (re)index produces the full graph the resolver can
148
+ give. As a backstop, a tool that touches a file the watcher hasn't processed yet triggers the
149
+ same connected re-index on access.
150
+
151
+ Files created, deleted or edited *while the server was down* are invisible to an event-based
152
+ watcher, so `serve` runs a one-shot **reconcile** at startup: it scans the project, indexes
153
+ new files, prunes vanished ones, and refreshes any that changed — then hands off to the
154
+ watcher.
155
+
156
+ ## Known limitations
157
+
158
+ - **Whole-project re-link:** the watcher re-links the *connected set* of a change, not the
159
+ entire project. A rename that ripples through many indirection layers — or creating a file
160
+ that an *unchanged* file already imports — may need a full `reindex` for an exact graph.
161
+ - **Cross-language edges on incremental edits:** synthesized `COMMUNICATES_WITH` edges are
162
+ rebuilt on a full `reindex`; they can erode across incremental edits (the boundary-based
163
+ query still resolves connections). Run `reindex` for an exact cross-language view.
164
+
165
+ ## Uninstall
166
+
167
+ `graphlens-mcp remove` deregisters the server from your agents; add `--purge-db` to also
168
+ delete the local `.graphlens/` cache.
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ uv sync --all-groups # install lint + test tooling
174
+ task check # ruff + format-check + ty + bandit + pytest (the CI gate)
175
+ task docs:serve # preview the docs site locally (needs Node + pnpm)
176
+ ```
177
+
178
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for the design and invariants, or the
179
+ [documentation site](https://neko1313.github.io/graphlens-mcp/) for the full guide.
180
+
181
+ ## License
182
+
183
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,146 @@
1
+ # graphlens-mcp
2
+
3
+ [![CI](https://github.com/Neko1313/graphlens-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Neko1313/graphlens-mcp/actions/workflows/ci.yml)
4
+ [![Docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://neko1313.github.io/graphlens-mcp/)
5
+ [![Python](https://img.shields.io/badge/python-%E2%89%A53.13-blue)](https://www.python.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
+
8
+ A free, MIT-licensed [MCP](https://modelcontextprotocol.io) server that gives coding
9
+ agents (Claude Code, Cursor, and compatible clients) a **semantic code graph** of your
10
+ project — symbols, cross-file calls, references, imports and cross-language boundaries.
11
+
12
+ Instead of reading files top-to-bottom or grepping for names, the agent **navigates the
13
+ structure**: *who calls this function*, *what does it depend on*, *what breaks if I change
14
+ its signature*. It is a thin runtime layer over the
15
+ [`graphlens`](https://github.com/Neko1313/graphlens) analysis engine: `graphlens` provides
16
+ the mechanisms (parsing, stable node identity, resolvers); `graphlens-mcp` owns the storage,
17
+ freshness and the agent-facing surface.
18
+
19
+ 📖 **Documentation:** <https://neko1313.github.io/graphlens-mcp/>
20
+
21
+ > Status: early. The core navigation works; see [Known limitations](#known-limitations).
22
+
23
+ ## Why
24
+
25
+ A `filesystem`/grep MCP makes the agent read whole files and match text — slow, noisy, and
26
+ blind to which of three modules actually calls `OrderService.create`. Bare tree-sitter
27
+ gives single-file syntax but cannot resolve links *between* files. `graphlens-mcp` answers
28
+ the cross-file questions — call graphs and impact analysis — and keeps the graph fresh as
29
+ you edit, then teaches the agent to use it via a bundled navigation skill.
30
+
31
+ ## Install
32
+
33
+ Requires **Python ≥ 3.13** (a constraint inherited from `graphlens`).
34
+
35
+ ```bash
36
+ uv tool install graphlens-mcp # or: pipx install graphlens-mcp
37
+ ```
38
+
39
+ Python language analysis works out of the box (the `ty` type engine ships as a
40
+ dependency). Other languages parse immediately and unlock full cross-file semantics once
41
+ their toolchain is present (Node for TypeScript, the Go toolchain, etc.); without it that
42
+ language is reported as `degraded` rather than blocking `init`.
43
+
44
+ ## Quickstart (two commands)
45
+
46
+ ```bash
47
+ uv tool install graphlens-mcp # 1. install
48
+ cd your-project && graphlens-mcp init # 2. index + configure your agent
49
+ ```
50
+
51
+ `init` detects the project's languages, indexes the code into a local graph, writes the
52
+ MCP server entry into your agent's config and installs the navigation skill. You do **not**
53
+ run `serve` yourself — your agent launches it from the config. Restart the agent and ask
54
+ it something like *"what breaks if I change the signature of `create_order`?"*.
55
+
56
+ ## Commands
57
+
58
+ | Command | What it does |
59
+ |---|---|
60
+ | `graphlens-mcp init` | Detect languages → toolchain doctor → full index → configure agents → install skill |
61
+ | `graphlens-mcp serve` | Start the MCP server over stdio. **Launched by the agent**, not by you |
62
+ | `graphlens-mcp status` | Show detected languages, toolchain status, and graph size/freshness |
63
+ | `graphlens-mcp reindex` | Force a full rebuild (e.g. after installing a new toolchain) |
64
+ | `graphlens-mcp remove` | Deregister from agents and (with `--purge-db`) delete the local graph |
65
+
66
+ Useful `init` flags: `--root <dir>`, `--agent claude_code --agent cursor` (repeatable),
67
+ `--no-agent`, `--no-skills`, `--db <path>`.
68
+
69
+ The graph lives at `<project>/.graphlens/graph.db` (SQLite). It is a regenerable cache —
70
+ safe to delete; `reindex` rebuilds it. Add `.graphlens/` to your VCS ignore (the bundled
71
+ `init` flow assumes it is not committed).
72
+
73
+ ## Supported languages
74
+
75
+ | Language | Engine | Out-of-box |
76
+ |---|---|---|
77
+ | Python | `ty` (bundled) | Full semantics immediately |
78
+ | TypeScript | Node bridge | `degraded` without Node; full semantics with Node installed |
79
+ | Go | Go toolchain | `degraded` without toolchain |
80
+ | Rust | SCIP / rust-analyzer | `degraded` without toolchain |
81
+ | PHP | PHP parser | `degraded` without toolchain |
82
+
83
+ `graphlens-mcp status` reports the actual resolver status per language. When a toolchain is
84
+ missing, that language is reported as **degraded** (parsed structure, calls/types not fully
85
+ resolved) with an install hint — it never blocks `init`.
86
+
87
+ ## Agent tools
88
+
89
+ Each response carries a graph-quality status (`ok` | `degraded`) so the agent never mistakes
90
+ a partial answer for a complete one.
91
+
92
+ | Tool | Purpose |
93
+ |---|---|
94
+ | `search_symbols` | Full-text search over symbol names — **start here** |
95
+ | `get_node_info` | Source snippet + signature + location for a node |
96
+ | `get_file_structure` | Symbol outline of a file |
97
+ | `get_callees` | What a function calls (outgoing, up to `max_depth`) |
98
+ | `get_callers` | Who calls a function — primary impact-analysis tool |
99
+ | `get_neighbors` | Nodes within N hops in any direction |
100
+ | `find_references` | Non-call usages (type annotations, assignments) |
101
+ | `get_cross_language_calls` | Connections across service boundaries (HTTP/gRPC/queues) |
102
+
103
+ ## Freshness model
104
+
105
+ A single mechanism keeps the graph current: a **filesystem watcher** (`serve` starts it by
106
+ default; disable with `--no-watch`). When a file changes on disk the server re-indexes the
107
+ **connected set** — the changed file plus the files that import it and the files it imports —
108
+ with one full analyze, so cross-file edges are rebuilt correctly rather than left partial.
109
+ Deleting a file prunes its symbols and refreshes its importers. There is no polling and no
110
+ structure-only "skeleton" phase: every (re)index produces the full graph the resolver can
111
+ give. As a backstop, a tool that touches a file the watcher hasn't processed yet triggers the
112
+ same connected re-index on access.
113
+
114
+ Files created, deleted or edited *while the server was down* are invisible to an event-based
115
+ watcher, so `serve` runs a one-shot **reconcile** at startup: it scans the project, indexes
116
+ new files, prunes vanished ones, and refreshes any that changed — then hands off to the
117
+ watcher.
118
+
119
+ ## Known limitations
120
+
121
+ - **Whole-project re-link:** the watcher re-links the *connected set* of a change, not the
122
+ entire project. A rename that ripples through many indirection layers — or creating a file
123
+ that an *unchanged* file already imports — may need a full `reindex` for an exact graph.
124
+ - **Cross-language edges on incremental edits:** synthesized `COMMUNICATES_WITH` edges are
125
+ rebuilt on a full `reindex`; they can erode across incremental edits (the boundary-based
126
+ query still resolves connections). Run `reindex` for an exact cross-language view.
127
+
128
+ ## Uninstall
129
+
130
+ `graphlens-mcp remove` deregisters the server from your agents; add `--purge-db` to also
131
+ delete the local `.graphlens/` cache.
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ uv sync --all-groups # install lint + test tooling
137
+ task check # ruff + format-check + ty + bandit + pytest (the CI gate)
138
+ task docs:serve # preview the docs site locally (needs Node + pnpm)
139
+ ```
140
+
141
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for the design and invariants, or the
142
+ [documentation site](https://neko1313.github.io/graphlens-mcp/) for the full guide.
143
+
144
+ ## License
145
+
146
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "graphlens-mcp"
3
+ version = "0.1.0"
4
+ description = "Semantic code graph MCP server for coding agents"
5
+ readme = { file = "README.md", content-type = "text/markdown" }
6
+ license = { file = "LICENSE" }
7
+ authors = [
8
+ { name = "Neko1313", email = "nikita.ribalchencko@yandex.ru" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "aiosqlite>=0.22.1",
13
+ "click>=8.4.2",
14
+ "fastmcp>=3.4.2",
15
+ "graphlens[go,python,rust,typescript,php]>=0.7.0",
16
+ "questionary>=2.0.1",
17
+ "tomlkit>=0.15.0",
18
+ "watchfiles>=0.24",
19
+ ]
20
+
21
+ [project.scripts]
22
+ graphlens-mcp = "graphlens_mcp.cli:main"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.11.3,<0.12.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ # Mirrors the graphlens engine: a lint group and a test group, synced together via
30
+ # `uv sync --all-groups`. Ruff/ty config lives in ruff.toml / ty.toml (engine parity).
31
+ lint = [
32
+ "bandit>=1.9.3",
33
+ "ruff>=0.15.0",
34
+ "ty>=0.0.15",
35
+ ]
36
+ test = [
37
+ "pytest>=8.3",
38
+ "pytest-asyncio>=0.24",
39
+ "pytest-cov>=5.0",
40
+ "pytest-timeout>=2.3",
41
+ ]
42
+
43
+ # ----------------------------------------------------------------------
44
+ # Tooling
45
+ # ----------------------------------------------------------------------
46
+
47
+ [tool.bandit]
48
+ exclude_dirs = ["tests"]
49
+ # B608: the only f-string SQL builds an IN(...) from a count of '?' placeholders or a
50
+ # fixed table allowlist — never user data. Ruff's S608 (enabled via ALL) is the active
51
+ # per-site guard that will flag any genuinely unsafe new query.
52
+ skips = ["B608"]
53
+
54
+ [tool.pytest.ini_options]
55
+ asyncio_mode = "auto"
56
+ testpaths = ["tests"]
57
+ # A per-test ceiling so a single hang (e.g. a stalled DB worker) fails fast with a
58
+ # traceback instead of stalling the whole run; integration analyze stays well under it.
59
+ addopts = "-q --strict-markers --timeout=120"
60
+ markers = [
61
+ "unit: pure tests, no external processes",
62
+ "integration: real graphlens analyze / filesystem / CLI",
63
+ "store: SqliteStore behavior",
64
+ "workspace: indexing & freshness",
65
+ "agents: agent registry (de)registration",
66
+ "cli: command-line interface",
67
+ "tools: MCP tool layer",
68
+ "golden: batch == incremental invariant",
69
+ "paths: path normalization",
70
+ ]
@@ -0,0 +1,3 @@
1
+ """graphlens-mcp: semantic code graph MCP server for coding agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,11 @@
1
+ """Per-agent MCP config registry (de/registration of the graphlens server)."""
2
+
3
+ from graphlens_mcp.agents.base import (
4
+ SERVER_KEY,
5
+ AgentSpec,
6
+ configure,
7
+ deregister,
8
+ )
9
+ from graphlens_mcp.agents.registry import REGISTRY
10
+
11
+ __all__ = ["REGISTRY", "SERVER_KEY", "AgentSpec", "configure", "deregister"]
@@ -0,0 +1,150 @@
1
+ """
2
+ Agent registry primitives: a data-driven spec per MCP client + (de)register.
3
+
4
+ Each coding agent stores MCP server config differently — Claude Code and
5
+ Cursor use a ``mcpServers`` map, VS Code uses ``servers`` with a ``type``
6
+ field, configs live at different paths and some are global.
7
+ :class:`AgentSpec` captures those differences so adding an agent is data,
8
+ not code, and ``init``/``remove`` operate on any spec uniformly.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import tempfile
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ import tomlkit
21
+ import tomlkit.exceptions
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable
25
+
26
+ # Key under which we register our server in each agent's config.
27
+ SERVER_KEY = "graphlens"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class AgentSpec:
32
+ """Declarative description of how to (de)register graphlens here."""
33
+
34
+ name: str # registry key / --agent value
35
+ label: str # human display name
36
+ scope: str # 'project' or 'global'
37
+ servers_key: (
38
+ str # top-level config key: 'mcpServers' | 'servers' | 'mcp_servers'
39
+ )
40
+ stdio_type: bool # emit {"type": "stdio", ...} in the entry (VS Code)
41
+ path_fn: Callable[[Path], Path] # config file location for a project root
42
+ detect_fn: Callable[[Path], bool] # is this agent plausibly used here?
43
+ fmt: str = "json" # config file format: 'json' | 'toml' (Codex)
44
+ install_skill: Callable[[Path], Path | None] | None = None
45
+
46
+ def config_path(self, project_root: Path) -> Path:
47
+ """Return this agent's MCP config file location for *project_root*."""
48
+ return self.path_fn(project_root.resolve())
49
+
50
+ def detect(self, project_root: Path) -> bool:
51
+ """Return True if this agent looks used for *project_root*."""
52
+ try:
53
+ return bool(self.detect_fn(project_root.resolve()))
54
+ except OSError:
55
+ return False
56
+
57
+
58
+ def _server_entry(spec: AgentSpec, project_root: Path, db_path: Path) -> dict:
59
+ # Pass absolute --db and --root so the entry does not depend on the agent's
60
+ # working directory (clients launch the server with varying cwd).
61
+ entry: dict = {
62
+ "command": "graphlens-mcp",
63
+ "args": ["serve", "--db", str(db_path), "--root", str(project_root)],
64
+ }
65
+ if spec.stdio_type:
66
+ return {"type": "stdio", **entry}
67
+ return entry
68
+
69
+
70
+ def configure(spec: AgentSpec, project_root: Path, db_path: Path) -> Path:
71
+ """Write/update the graphlens MCP entry in *spec*'s config. Idempotent."""
72
+ project_root = project_root.resolve()
73
+ path = spec.config_path(project_root)
74
+ entry = _server_entry(spec, project_root, db_path.resolve())
75
+
76
+ if spec.fmt == "toml":
77
+ doc = _load_toml(path)
78
+ table = doc.get(spec.servers_key)
79
+ if not isinstance(table, dict):
80
+ table = tomlkit.table()
81
+ doc[spec.servers_key] = table
82
+ table[SERVER_KEY] = entry
83
+ _atomic_write_text(path, tomlkit.dumps(doc))
84
+ else:
85
+ cfg = _load_json(path)
86
+ cfg.setdefault(spec.servers_key, {})[SERVER_KEY] = entry
87
+ _atomic_write_text(path, json.dumps(cfg, indent=2) + "\n")
88
+ return path
89
+
90
+
91
+ def deregister(spec: AgentSpec, project_root: Path) -> bool:
92
+ """Remove graphlens entry from *spec*'s config. True if removed."""
93
+ path = spec.config_path(project_root.resolve())
94
+ if not path.exists():
95
+ return False
96
+
97
+ if spec.fmt == "toml":
98
+ doc = _load_toml(path)
99
+ table = doc.get(spec.servers_key)
100
+ if not isinstance(table, dict) or SERVER_KEY not in table:
101
+ return False
102
+ del table[SERVER_KEY]
103
+ if len(table) == 0:
104
+ del doc[spec.servers_key]
105
+ _atomic_write_text(path, tomlkit.dumps(doc))
106
+ return True
107
+
108
+ cfg = _load_json(path)
109
+ servers = cfg.get(spec.servers_key)
110
+ if not isinstance(servers, dict) or SERVER_KEY not in servers:
111
+ return False
112
+ del servers[SERVER_KEY]
113
+ if not servers:
114
+ cfg.pop(spec.servers_key, None)
115
+ _atomic_write_text(path, json.dumps(cfg, indent=2) + "\n")
116
+ return True
117
+
118
+
119
+ def _load_json(path: Path) -> dict:
120
+ if path.exists():
121
+ try:
122
+ data = json.loads(path.read_text())
123
+ return data if isinstance(data, dict) else {}
124
+ except (OSError, json.JSONDecodeError):
125
+ return {}
126
+ return {}
127
+
128
+
129
+ def _load_toml(path: Path) -> tomlkit.TOMLDocument:
130
+ if path.exists():
131
+ try:
132
+ return tomlkit.parse(path.read_text())
133
+ except (OSError, tomlkit.exceptions.TOMLKitError):
134
+ return tomlkit.document()
135
+ return tomlkit.document()
136
+
137
+
138
+ def _atomic_write_text(path: Path, text: str) -> None:
139
+ path.parent.mkdir(parents=True, exist_ok=True)
140
+ fd, tmp = tempfile.mkstemp(
141
+ dir=path.parent, prefix=".graphlens-", suffix=".tmp"
142
+ )
143
+ tmp_path = Path(tmp)
144
+ try:
145
+ with os.fdopen(fd, "w") as f:
146
+ f.write(text)
147
+ tmp_path.replace(path)
148
+ finally:
149
+ if tmp_path.exists():
150
+ tmp_path.unlink()
@@ -0,0 +1,90 @@
1
+ """
2
+ The set of supported coding agents.
3
+
4
+ Only agents whose MCP config format and location are known are included, so we
5
+ never write a config an agent cannot read. Adding another agent is a single
6
+ :class:`AgentSpec` entry. Codex uses TOML (``~/.codex/config.toml``); the
7
+ rest use JSON. Zed and Cline use distinct schemas/locations and are not
8
+ registered yet.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import shutil
14
+ from pathlib import Path
15
+
16
+ from graphlens_mcp.agents.base import AgentSpec
17
+
18
+ _SKILL_SRC = (
19
+ Path(__file__).parent.parent
20
+ / "skills"
21
+ / "graphlens-navigation"
22
+ / "SKILL.md"
23
+ )
24
+
25
+
26
+ def _install_claude_skill(_project_root: Path) -> Path | None:
27
+ if not _SKILL_SRC.exists():
28
+ return None
29
+ dest_dir = Path.home() / ".claude" / "skills" / "graphlens-navigation"
30
+ dest_dir.mkdir(parents=True, exist_ok=True)
31
+ dest = dest_dir / "SKILL.md"
32
+ shutil.copy2(_SKILL_SRC, dest)
33
+ return dest
34
+
35
+
36
+ REGISTRY: dict[str, AgentSpec] = {
37
+ "claude_code": AgentSpec(
38
+ name="claude_code",
39
+ label="Claude Code",
40
+ scope="project",
41
+ servers_key="mcpServers",
42
+ stdio_type=False,
43
+ path_fn=lambda r: r / ".mcp.json",
44
+ detect_fn=lambda r: (
45
+ (r / ".claude").is_dir()
46
+ or (r / ".mcp.json").exists()
47
+ or (r / "CLAUDE.md").exists()
48
+ ),
49
+ install_skill=_install_claude_skill,
50
+ ),
51
+ "cursor": AgentSpec(
52
+ name="cursor",
53
+ label="Cursor",
54
+ scope="project",
55
+ servers_key="mcpServers",
56
+ stdio_type=False,
57
+ path_fn=lambda r: r / ".cursor" / "mcp.json",
58
+ detect_fn=lambda r: (r / ".cursor").is_dir(),
59
+ ),
60
+ "windsurf": AgentSpec(
61
+ name="windsurf",
62
+ label="Windsurf",
63
+ scope="global",
64
+ servers_key="mcpServers",
65
+ stdio_type=False,
66
+ path_fn=lambda _r: (
67
+ Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
68
+ ),
69
+ detect_fn=lambda _r: (Path.home() / ".codeium" / "windsurf").is_dir(),
70
+ ),
71
+ "vscode": AgentSpec(
72
+ name="vscode",
73
+ label="VS Code (Copilot)",
74
+ scope="project",
75
+ servers_key="servers",
76
+ stdio_type=True,
77
+ path_fn=lambda r: r / ".vscode" / "mcp.json",
78
+ detect_fn=lambda r: (r / ".vscode").is_dir(),
79
+ ),
80
+ "codex": AgentSpec(
81
+ name="codex",
82
+ label="Codex CLI",
83
+ scope="global",
84
+ servers_key="mcp_servers",
85
+ stdio_type=False,
86
+ fmt="toml",
87
+ path_fn=lambda _r: Path.home() / ".codex" / "config.toml",
88
+ detect_fn=lambda _r: (Path.home() / ".codex").is_dir(),
89
+ ),
90
+ }