quickshell-mcp 2.0.0__tar.gz → 2.2.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.
Files changed (113) hide show
  1. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.gitignore +5 -0
  2. quickshell_mcp-2.2.0/.releaserc.js +61 -0
  3. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/AGENTS.md +65 -3
  4. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/PKG-INFO +17 -1
  5. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/README.md +16 -0
  6. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/pyproject.toml +1 -1
  7. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/__init__.py +36 -0
  8. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/assistant.py +28 -0
  9. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/debugging.py +16 -0
  10. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/generation.py +28 -0
  11. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/knowledge.py +71 -0
  12. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/migration.py +19 -0
  13. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/project.py +45 -0
  14. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/registry.py +167 -0
  15. quickshell_mcp-2.2.0/quickshell_mcp/capabilities/validation.py +32 -0
  16. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/server.py +309 -62
  17. quickshell_mcp-2.2.0/quickshell_mcp/sources/__init__.py +4 -0
  18. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/assistant.py +69 -9
  19. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/generate.py +136 -0
  20. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/migrate.py +13 -4
  21. quickshell_mcp-2.2.0/quickshell_mcp/sources/project.py +417 -0
  22. quickshell_mcp-2.2.0/quickshell_mcp/sources/project_intel.py +527 -0
  23. quickshell_mcp-2.2.0/quickshell_mcp/sources/project_validate.py +356 -0
  24. quickshell_mcp-2.2.0/quickshell_mcp/sources/refactor.py +222 -0
  25. quickshell_mcp-2.2.0/quickshell_mcp/sources/style_match.py +108 -0
  26. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/validate.py +17 -0
  27. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/scripts/smoke_test.py +14 -0
  28. quickshell_mcp-2.2.0/tests/fixtures/projects/medium/components/ClockWidget.qml +6 -0
  29. quickshell_mcp-2.2.0/tests/fixtures/projects/medium/components/VolumeWidget.qml +6 -0
  30. quickshell_mcp-2.2.0/tests/fixtures/projects/medium/config.toml +1 -0
  31. quickshell_mcp-2.2.0/tests/fixtures/projects/medium/main.qml +14 -0
  32. quickshell_mcp-2.2.0/tests/fixtures/projects/medium/services/VolumeService.qml +5 -0
  33. quickshell_mcp-2.2.0/tests/fixtures/projects/small/config.json +3 -0
  34. quickshell_mcp-2.2.0/tests/fixtures/projects/small/main.qml +12 -0
  35. quickshell_mcp-2.2.0/tests/fixtures/projects/small/util.js +3 -0
  36. quickshell_mcp-2.2.0/tests/fixtures/projects/small/widgets/VolumeWidget.qml +6 -0
  37. quickshell_mcp-2.2.0/tests/test_capabilities.py +227 -0
  38. quickshell_mcp-2.2.0/tests/test_diagnostics.py +202 -0
  39. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_migrate.py +9 -3
  40. quickshell_mcp-2.2.0/tests/test_project.py +427 -0
  41. quickshell_mcp-2.2.0/tests/test_project_generate.py +199 -0
  42. quickshell_mcp-2.2.0/tests/test_project_intel.py +206 -0
  43. quickshell_mcp-2.2.0/tests/test_project_validate.py +161 -0
  44. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_validate.py +1 -0
  45. quickshell_mcp-2.0.0/.releaserc.json +0 -55
  46. quickshell_mcp-2.0.0/quickshell_mcp/sources/__init__.py +0 -1
  47. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.coderabbit.yaml +0 -0
  48. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  49. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  50. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/ISSUE_TEMPLATE/config_help.yml +0 -0
  51. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  52. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/pull_request_template.md +0 -0
  53. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/workflows/ci.yml +0 -0
  54. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/.github/workflows/semantic-release.yml +0 -0
  55. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/CLAUDE.md +0 -0
  56. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/CODE_OF_CONDUCT.md +0 -0
  57. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/CONTRIBUTING.md +0 -0
  58. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/Dockerfile +0 -0
  59. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/LICENSE +0 -0
  60. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/flake.lock +0 -0
  61. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/flake.nix +0 -0
  62. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/__init__.py +0 -0
  63. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/__main__.py +0 -0
  64. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/caches.py +0 -0
  65. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/config.py +0 -0
  66. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/extraction.py +0 -0
  67. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/compat.py +0 -0
  68. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/docs.py +0 -0
  69. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/examples.py +0 -0
  70. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/explain_error.py +0 -0
  71. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/find_pattern.py +0 -0
  72. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/implementations.py +0 -0
  73. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/qt_docs.py +0 -0
  74. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/sources/search_all.py +0 -0
  75. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/utils.py +0 -0
  76. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/quickshell_mcp/versions.py +0 -0
  77. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/server.json +0 -0
  78. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/conftest.py +0 -0
  79. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/about.html +0 -0
  80. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/example_readme.md +0 -0
  81. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/examples_contents_root.json +0 -0
  82. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/examples_repo_info.json +0 -0
  83. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/guide_index.html +0 -0
  84. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/hyprland_monitor.html +0 -0
  85. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_caelestia_anchoranim.qml +0 -0
  86. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_caelestia_repo_info.json +0 -0
  87. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_caelestia_tree.json +0 -0
  88. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_dots_audio.qml +0 -0
  89. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_dots_repo_info.json +0 -0
  90. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_dots_tree.json +0 -0
  91. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_noctalia_nbutton.qml +0 -0
  92. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/impl_noctalia_tree.json +0 -0
  93. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/qml_language.html +0 -0
  94. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/qt_qml_qtquick_rectangle.html +0 -0
  95. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/qt_qtquick_controls_qmlmodule.html +0 -0
  96. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/fixtures/qt_qtquick_qmlmodule.html +0 -0
  97. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_assistant.py +0 -0
  98. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_code_quality.py +0 -0
  99. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_compat.py +0 -0
  100. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_content_search.py +0 -0
  101. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_disk_cache.py +0 -0
  102. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_errors_retry.py +0 -0
  103. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_examples.py +0 -0
  104. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_explain_error.py +0 -0
  105. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_extraction.py +0 -0
  106. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_find_pattern.py +0 -0
  107. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_generate.py +0 -0
  108. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_implementations.py +0 -0
  109. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_index_search.py +0 -0
  110. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_live.py +0 -0
  111. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_qt_docs.py +0 -0
  112. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_search_all.py +0 -0
  113. {quickshell_mcp-2.0.0 → quickshell_mcp-2.2.0}/tests/test_versions.py +0 -0
@@ -20,6 +20,11 @@ result
20
20
  result-*
21
21
  .direnv/
22
22
 
23
+ # Node (used only for local semantic-release debugging)
24
+ node_modules/
25
+ package-lock.json
26
+ package.json
27
+
23
28
  # Editors / OS
24
29
  .vscode/
25
30
  .idea/
@@ -0,0 +1,61 @@
1
+ const notesPattern = (sel) => new RegExp(`^[\\s|*]*(${sel}):\\s*(.*)`, "i");
2
+
3
+ module.exports = {
4
+ branches: ["main"],
5
+ tagFormat: "v${version}",
6
+ plugins: [
7
+ [
8
+ "@semantic-release/commit-analyzer",
9
+ {
10
+ parserOpts: {
11
+ headerPattern: "^([\\w.\\-*/]+):\\s?(.*)$",
12
+ headerCorrespondence: ["scope", "subject"],
13
+ noteKeywords: ["BREAKING CHANGE", "Semver"],
14
+ notesPattern,
15
+ },
16
+ releaseRules: [
17
+ { notes: [{ title: "Semver", text: "minor" }], release: "minor" },
18
+ { notes: [{ title: "Semver", text: "major" }], release: "major" },
19
+ ],
20
+ },
21
+ ],
22
+ [
23
+ "@semantic-release/release-notes-generator",
24
+ {
25
+ parserOpts: {
26
+ headerPattern: "^([\\w.\\-*/]+):\\s?(.*)$",
27
+ headerCorrespondence: ["scope", "subject"],
28
+ noteKeywords: null,
29
+ },
30
+ },
31
+ ],
32
+ [
33
+ "@google/semantic-release-replace-plugin",
34
+ {
35
+ replacements: [
36
+ {
37
+ files: ["pyproject.toml"],
38
+ from: "version = \"[0-9]+\\.[0-9]+\\.[0-9]+[^\"]*\"",
39
+ to: "version = \"${nextRelease.version}\"",
40
+ results: [{ file: "pyproject.toml", hasChanged: true }],
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ [
46
+ "@semantic-release/git",
47
+ {
48
+ assets: ["pyproject.toml"],
49
+ message: "release: v${nextRelease.version} [skip ci]",
50
+ },
51
+ ],
52
+ [
53
+ "@semantic-release/exec",
54
+ {
55
+ prepareCmd:
56
+ 'echo "released=true" >> "$GITHUB_OUTPUT" && echo "version=${nextRelease.version}" >> "$GITHUB_OUTPUT"',
57
+ },
58
+ ],
59
+ "@semantic-release/github",
60
+ ],
61
+ };
@@ -22,8 +22,9 @@ An MCP server that serves live Quickshell documentation from quickshell.org so L
22
22
  - `caches.py`: 30-minute in-memory cache per fetch, plus a disk layer that persists built indexes (guide/type/qt) across restarts (30-day default TTL); `QUICKSHELL_DOCS_MCP_DISK_CACHE` env var to disable/redirect
23
23
  - `utils.py`: logging, HTTP client, `_fetch_raw` (retry + stats), citation prefix; domain modules call `utils._fetch_raw` by module attribute so tests monkeypatch one seam
24
24
  - `versions.py`, `extraction.py`: runtime version discovery; HTML→Markdown with per-source strip rules
25
- - `sources/`: one logic module per domain capability (`docs`, `qt_docs`, `examples`, `implementations`, `compat`, `migrate`, `validate`, `generate`, `assistant`, `explain_error`, `find_pattern`, `search_all`); no MCP decorators here
26
- - `server.py`: FastMCP instance, every `@mcp.tool()` wrapper + docstring (the trigger surface), `main()`, and re-exports of helper names so tests keep addressing `srv.<helper>`
25
+ - `sources/`: shared-services layer — data-source access, index building, parsing, and low-level analysis (`docs`, `qt_docs`, `examples`, `implementations`, `compat`, `migrate`, `validate`, `generate`, `assistant`, `explain_error`, `find_pattern`, `search_all`); no MCP decorators here and it never imports `capabilities/` (dependencies point down)
26
+ - `capabilities/`: capability/domain layer between the tools and the sources — one module per capability (`knowledge`, `validation`, `generation`, `migration`, `debugging`, `assistant`), each declaring `CAPABILITY_NAME`/`CAPABILITY_TOOLS`/`CAPABILITY_DEPENDS_ON` and re-exporting the source entry points; `registry.py` aggregates the tool→capability map and a cycle-checked `dependency_order()`. Planned-but-unimplemented capabilities (project, runtime, inspection, testing, performance) are registry entries only — never empty placeholder modules
27
+ - `server.py`: FastMCP instance, every `@mcp.tool()` wrapper + docstring (the trigger surface), `main()`, and re-exports of helper names so tests keep addressing `srv.<helper>`; tool logic is routed through `capabilities/`
27
28
  - `tests/`: offline pytest suite driven by saved snapshots in `tests/fixtures/`. `conftest.py` provides `mock_fetch` (patches `utils._fetch_raw`), `docs_fixture_urls`, `http_404`, and an autouse cache/stats reset.
28
29
  - `scripts/smoke_test.py`: end-to-end stdio client; drives the server like a real MCP session against the live sites.
29
30
  - `flake.nix` / `Dockerfile` / `.github/workflows/ci.yml`: packaging + CI. `CLAUDE.md` is a symlink to this file.
@@ -118,6 +119,65 @@ Run checks when warranted, not reflexively. If none apply, say so instead of run
118
119
  5. **CI pipeline** (`.github/workflows/ci.yml`): flake check → nix build + dist/twine → lint → offline pytest with coverage → Dockerfile validation; live-smoke runs only on manual dispatch. If you add a check locally, wire the equivalent into CI.
119
120
  6. **Implementation references are not documentation.** Caelestia/Noctalia/dots-hyprland results carry `kind: real-world implementation` and every tool docstring says the docs win on API disputes. Noctalia's ref is pinned to its `legacy-v4` branch deliberately (its `main` moved to a C++ compositor); a GitHub `truncated: true` tree response is refused rather than silently under-indexed. dots-hyprland's QML root is nested under `dots/.config/quickshell/ii/` and paths are reported relative to it.
120
121
 
122
+ ## ProjectContext (`quickshell_mcp/sources/project.py`)
123
+
124
+ A reusable internal abstraction for representing a Quickshell project on disk.
125
+ It is **not** a capability or an MCP tool — it is shared infrastructure in the
126
+ `sources/` layer that capabilities consume.
127
+
128
+ ### Consumer pattern
129
+
130
+ ```python
131
+ from ..sources.project import _build_project_context
132
+
133
+ ctx = _build_project_context("/path/to/shell")
134
+ info = ctx.discover({"qml_files", "imports", "compositor"})
135
+ # info = {"qml_files": [...], "imports": [...], "compositor": ["Hyprland"], ...}
136
+ # ctx.detection_status("compositor") -> "inferred"
137
+ ```
138
+
139
+ ### Available fields (all lazy)
140
+
141
+ | Field | Status | Description |
142
+ |-------|--------|-------------|
143
+ | `qml_files` | detected | Every `*.qml` under the project root |
144
+ | `js_files` | detected | Every `*.js` under the project root |
145
+ | `entrypoints` | detected | QML files whose root object is a window type |
146
+ | `imports` | detected | All QML import statements across every file |
147
+ | `quickshell_modules` | detected / unknown | Imports starting with `Quickshell` |
148
+ | `quickshell_version` | inferred / unknown | Version string from `Quickshell` import versions |
149
+ | `qt_version` | inferred / unknown | Version string from `Qt*` module import versions |
150
+ | `compositor` | inferred / unknown | Quickshell integration namespaces (e.g. `Hyprland`) |
151
+ | `config_paths` | detected | Config files at the project root |
152
+ | `dependencies` | detected | All distinct import modules |
153
+ | `conventions` | inferred | Naming, layout, structure patterns |
154
+ | `components` | detected / unknown | QML files under `components/` or `widgets/` dirs |
155
+ | `services` | detected / inferred / unknown | `Quickshell.Services.*` imports + `*Service`-named objects |
156
+ | `runtime_dependencies` | detected / inferred / unknown | Runtime-hook QML types (Process, IpcHandler, ...) + config keyword hints |
157
+ | `environment` | inferred / unknown | `process.env.*` variables referenced in QML |
158
+
159
+ ### Status semantics
160
+
161
+ - **detected** — directly observed in the filesystem (file lists, import statements)
162
+ - **inferred** — derived from detected data (version strings, compositor, conventions)
163
+ - **unknown** — no data could be found (no QML files, no version-bearing imports)
164
+
165
+ ### Design rules
166
+
167
+ - **Never fabricates.** A field without data is `None` / `[]` with status `"unknown"`.
168
+ - **Lazy by default.** `discover({needs})` computes only the requested fields.
169
+ - **Instance-cached.** Repeated `discover()` calls on the same `_ProjectContext` reuse precomputed values.
170
+ - **Shared-cached.** The expensive file-tree scan is cached in the 30-minute in-memory cache keyed by the resolved root path, so a second `_build_project_context` for the same directory skips re-scanning.
171
+ - **No HTTP.** ProjectContext is purely local-filesystem: it walks the tree and parses QML text. Any version resolution against the live site is the caller's responsibility.
172
+ - **Compositor-independent.** The compositor field reports Quickshell integration namespaces (e.g. `Hyprland`) from `Quickshell.<Name>` imports after excluding core namespaces. No compositor-name list is hardcoded.
173
+
174
+ ### Adding a new field
175
+
176
+ 1. Add the field name to `_ALL_FIELDS` in `project.py`.
177
+ 2. Add a `_discover_<name>` method that calls `_set(...)` or `_set_inferred(...)`.
178
+ 3. Call `_scan()` for the raw file/import data; derive your field from it.
179
+ 4. Add a test in `tests/test_project.py`.
180
+
121
181
  ## MCP client configuration
122
182
 
123
183
  ```json
@@ -150,7 +210,9 @@ Scoped Commits (`<scope>: <description>`). Small diffs, one logical change per c
150
210
  > ⚠️ A semantic-release CI runs on every merge to `main`. Every commit defaults to a **patch** release. A new feature/tool must signal a minor bump explicitly — there is no type-prefix heuristic anymore. Forgetting `Semver: minor` on a new tool means no minor release.
151
211
 
152
212
  - `Semver: minor` — new feature, tool, or user-facing addition
153
- - `BREAKING CHANGE: <description>` — breaking API/config change
213
+ - `Semver: major` — breaking API/config change (preferred over `BREAKING CHANGE:`)
214
+
215
+ Both are matched as trailing lines in the commit body, e.g. a final `Semver: minor` line. Matching requires a literal colon after `Semver:`/`BREAKING CHANGE:`, so prose that merely *mentions* "BREAKING CHANGE" or "Semver" never triggers a bump.
154
216
 
155
217
  Example:
156
218
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickshell-mcp
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: MCP server that fetches live Quickshell docs from quickshell.org so the model reads real, current docs instead of hallucinating.
5
5
  Project-URL: Homepage, https://github.com/franklinnolasco7/quickshell-mcp
6
6
  Project-URL: Repository, https://github.com/franklinnolasco7/quickshell-mcp
@@ -417,6 +417,22 @@ Use `refresh=True` to bypass the short-lived cache where supported. The cache lo
417
417
 
418
418
  See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, workflow, and how to submit changes.
419
419
 
420
+ ### Internal architecture
421
+
422
+ The server is structured into four layers:
423
+
424
+ ```
425
+ MCP tool (server.py, tool registration + docstrings)
426
+ ↓
427
+ capabilities/ (domain layer: knowledge, validation, generation, migration, debugging, assistant)
428
+ ↓
429
+ sources/ (shared services: data-source access, index building, parsing, compatibility)
430
+ ↓
431
+ config, caches, utils, versions, extraction (infrastructure)
432
+ ```
433
+
434
+ Each capability module declares its tools and its explicit dependencies on other capabilities. The dependency graph is acyclic and verified by `tests/test_capabilities.py`. Planned capabilities (project, runtime, inspection, testing, performance) are documented in the registry; no empty modules are created until features land.
435
+
420
436
  ## Limitations
421
437
 
422
438
  - Validation is static and heuristic; it complements `qmlls`.
@@ -380,6 +380,22 @@ Use `refresh=True` to bypass the short-lived cache where supported. The cache lo
380
380
 
381
381
  See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, workflow, and how to submit changes.
382
382
 
383
+ ### Internal architecture
384
+
385
+ The server is structured into four layers:
386
+
387
+ ```
388
+ MCP tool (server.py, tool registration + docstrings)
389
+ ↓
390
+ capabilities/ (domain layer: knowledge, validation, generation, migration, debugging, assistant)
391
+ ↓
392
+ sources/ (shared services: data-source access, index building, parsing, compatibility)
393
+ ↓
394
+ config, caches, utils, versions, extraction (infrastructure)
395
+ ```
396
+
397
+ Each capability module declares its tools and its explicit dependencies on other capabilities. The dependency graph is acyclic and verified by `tests/test_capabilities.py`. Planned capabilities (project, runtime, inspection, testing, performance) are documented in the registry; no empty modules are created until features land.
398
+
383
399
  ## Limitations
384
400
 
385
401
  - Validation is static and heuristic; it complements `qmlls`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quickshell-mcp"
3
- version = "2.0.0"
3
+ version = "2.2.0"
4
4
  description = "MCP server that fetches live Quickshell docs from quickshell.org so the model reads real, current docs instead of hallucinating."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,36 @@
1
+ """Capability / domain layer.
2
+
3
+ Each capability module sits between the MCP tool wrappers in ``server.py``
4
+ and the shared services in ``sources/``. Capabilities own the tool-to-domain
5
+ mapping, declare explicit inter-capability dependencies, and re-export the
6
+ entry-point helpers that the tool wrappers call.
7
+
8
+ The dependency graph among implemented capabilities is acyclic:
9
+
10
+ knowledge (root)
11
+ ├── validation
12
+ ├── debugging
13
+ validation ──┬── generation
14
+ └── migration
15
+ knowledge, validation, debugging, generation, migration ── assistant
16
+ """
17
+
18
+ from . import (
19
+ assistant,
20
+ debugging,
21
+ generation,
22
+ knowledge,
23
+ migration,
24
+ registry,
25
+ validation,
26
+ )
27
+
28
+ __all__ = [
29
+ "assistant",
30
+ "debugging",
31
+ "generation",
32
+ "knowledge",
33
+ "migration",
34
+ "registry",
35
+ "validation",
36
+ ]
@@ -0,0 +1,28 @@
1
+ """The assistant capability: orchestrate a plain-language request across every
2
+ other capability.
3
+
4
+ Depends on: knowledge, validation, generation, migration, debugging.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ..sources.assistant import ( # noqa: F401
10
+ _classify_intent,
11
+ _coding_assistant,
12
+ _detect_compositor,
13
+ _project_info,
14
+ _resolve_version_hint,
15
+ _safe_step,
16
+ )
17
+ from ..sources.project import _build_project_context # noqa: F401
18
+
19
+ CAPABILITY_NAME = "assistant"
20
+ CAPABILITY_TOOLS = ("quickshell_coding_assistant",)
21
+ CAPABILITY_DEPENDS_ON = (
22
+ "knowledge",
23
+ "validation",
24
+ "generation",
25
+ "migration",
26
+ "debugging",
27
+ "project",
28
+ )
@@ -0,0 +1,16 @@
1
+ """The debugging capability: explain QML errors and suggest fixes.
2
+
3
+ Depends on: knowledge (debugging uses the docs and Qt indexes).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ..sources.explain_error import ( # noqa: F401
9
+ _categorize_error,
10
+ _explain_error,
11
+ _extract_type_from_code,
12
+ )
13
+
14
+ CAPABILITY_NAME = "debugging"
15
+ CAPABILITY_TOOLS = ("quickshell_explain_error",)
16
+ CAPABILITY_DEPENDS_ON = ("knowledge",)
@@ -0,0 +1,28 @@
1
+ """The generation capability: produce grounded QML component code.
2
+
3
+ Depends on: knowledge, validation (generation uses compat, find_pattern,
4
+ search_all, and validate).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ..sources.generate import ( # noqa: F401
10
+ _build_section,
11
+ _generate_component,
12
+ _generate_panel,
13
+ _generate_service,
14
+ _interpret_component_query,
15
+ )
16
+ from ..sources.refactor import _apply_patch, _refactor # noqa: F401
17
+ from ..sources.style_match import _style_match # noqa: F401
18
+
19
+ CAPABILITY_NAME = "generation"
20
+ CAPABILITY_TOOLS = (
21
+ "quickshell_generate_component",
22
+ "quickshell_generate_service",
23
+ "quickshell_generate_panel",
24
+ "quickshell_refactor",
25
+ "quickshell_apply_patch",
26
+ "quickshell_style_match",
27
+ )
28
+ CAPABILITY_DEPENDS_ON = ("knowledge", "validation")
@@ -0,0 +1,71 @@
1
+ """The knowledge capability: read and search Quickshell docs, Qt types,
2
+ official examples, and real-world implementations.
3
+
4
+ Depends on: (none in the capability layer — pulls directly from shared services).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ..sources.docs import ( # noqa: F401
10
+ GUIDE_LINK_RE,
11
+ TYPE_LINK_RE,
12
+ _build_index,
13
+ _guide_content_index,
14
+ _guide_page,
15
+ _resolve_version,
16
+ _search_guide_content,
17
+ _search_type_content,
18
+ _type_page,
19
+ )
20
+ from ..sources.examples import ( # noqa: F401
21
+ _example_file,
22
+ _examples_branch,
23
+ _examples_known_paths,
24
+ _examples_listing,
25
+ )
26
+ from ..sources.find_pattern import _find_pattern, _interpret_query # noqa: F401
27
+ from ..sources.implementations import ( # noqa: F401
28
+ _GITHUB_API,
29
+ _IMPL_QUERY_STOPWORDS,
30
+ _IMPL_TOPICS,
31
+ _impl_branch,
32
+ _impl_component,
33
+ _impl_entry_meta,
34
+ _impl_file,
35
+ _impl_repo_config,
36
+ _impl_topics_for_query,
37
+ _norm_source,
38
+ _search_implementations,
39
+ )
40
+ from ..sources.qt_docs import ( # noqa: F401
41
+ _QT_ANCHOR_RE,
42
+ _QT_MODULE_LINK_RE,
43
+ _QT_TYPE_LINK_RE,
44
+ _VALUE_TYPES_BUCKET,
45
+ _build_qt_index,
46
+ _normalize_qt_module,
47
+ _qt_type_page,
48
+ _resolve_qt_slug,
49
+ )
50
+ from ..sources.search_all import _search_everything # noqa: F401
51
+
52
+ CAPABILITY_NAME = "knowledge"
53
+ CAPABILITY_TOOLS = (
54
+ "quickshell_list_versions",
55
+ "quickshell_about",
56
+ "quickshell_changelog",
57
+ "quickshell_list_guide_pages",
58
+ "quickshell_get_guide_page",
59
+ "quickshell_list_types",
60
+ "quickshell_get_type",
61
+ "quickshell_search",
62
+ "quickshell_search_all",
63
+ "quickshell_find_pattern",
64
+ "quickshell_list_qt_types",
65
+ "quickshell_get_qt_type",
66
+ "quickshell_list_examples",
67
+ "quickshell_get_example",
68
+ "quickshell_search_implementations",
69
+ "quickshell_get_implementation",
70
+ )
71
+ CAPABILITY_DEPENDS_ON: tuple[str, ...] = ()
@@ -0,0 +1,19 @@
1
+ """The migration capability: analyse what code must change between versions.
2
+
3
+ Depends on: knowledge, validation (migration uses compat, docs, and validate).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ..sources.migrate import ( # noqa: F401
9
+ _behavioral_scan,
10
+ _collect_api_refs,
11
+ _import_diff,
12
+ _migrate,
13
+ _migration_plan,
14
+ _symbol_issue,
15
+ )
16
+
17
+ CAPABILITY_NAME = "migration"
18
+ CAPABILITY_TOOLS = ("quickshell_migrate",)
19
+ CAPABILITY_DEPENDS_ON = ("knowledge", "validation")
@@ -0,0 +1,45 @@
1
+ """The project capability: analyze a Quickshell project on disk.
2
+
3
+ Provides structured project intelligence built on the shared
4
+ ``ProjectContext``: an overview, a static relationship map, project-scoped
5
+ search, dependency classification, and config-convention detection. All
6
+ tools are read-only and never modify the project.
7
+
8
+ Depends on: knowledge (project search reuses the shared search/index
9
+ infrastructure).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ..sources.project import ( # noqa: F401
15
+ _ALL_FIELDS,
16
+ _build_project_context,
17
+ _ProjectContext,
18
+ )
19
+ from ..sources.project_intel import ( # noqa: F401
20
+ _analyze_project,
21
+ _config_conventions,
22
+ _map_project,
23
+ _project_dependencies,
24
+ _search_project,
25
+ )
26
+ from ..sources.project_validate import ( # noqa: F401
27
+ _lint_project,
28
+ _migrate_project,
29
+ _project_compatibility,
30
+ _validate_project,
31
+ )
32
+
33
+ CAPABILITY_NAME = "project"
34
+ CAPABILITY_TOOLS = (
35
+ "quickshell_project_analyze",
36
+ "quickshell_project_map",
37
+ "quickshell_project_find",
38
+ "quickshell_project_dependencies",
39
+ "quickshell_project_config",
40
+ "quickshell_project_validate",
41
+ "quickshell_project_lint",
42
+ "quickshell_project_compatibility",
43
+ "quickshell_project_migrate",
44
+ )
45
+ CAPABILITY_DEPENDS_ON = ("knowledge", "validation")
@@ -0,0 +1,167 @@
1
+ """Capability registry: the boundary map between MCP tools and the domain layer.
2
+
3
+ Each capability module (``knowledge``, ``validation``, ``generation``,
4
+ ``migration``, ``debugging``, ``assistant``) declares which tools it owns and
5
+ which other capabilities it depends on. This module aggregates those
6
+ declarations into a single map plus a cycle-checked topological ordering, so
7
+ server.py and tests can answer "which capability owns this tool?" and prove
8
+ the capability graph is acyclic.
9
+
10
+ Capabilities that are planned but not implemented yet (runtime, inspection,
11
+ testing, performance) are registered with ``status="planned"`` and no tools.
12
+ No modules are created for them until the features land.
13
+
14
+ Every capability carries a ``safety_level``:
15
+
16
+ * **read-only** — analysis only; no state changes, no file writes, no process
17
+ execution (default for all capabilities today).
18
+ * **mutating** — alters live state (runtime sessions, explicit file writes via
19
+ ``apply_patch``). Requires explicit invocation.
20
+ * **high-risk** — arbitrary evaluation or destructive filesystem operations.
21
+ Reserved for future eval/patch capabilities; not yet assigned.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass
27
+
28
+ from . import assistant, debugging, generation, knowledge, migration, project, validation
29
+
30
+ __all__ = [
31
+ "ALL_CAPABILITIES",
32
+ "CAPABILITIES",
33
+ "Capability",
34
+ "PLANNED_CAPABILITIES",
35
+ "SAFETY_LEVELS",
36
+ "capability_for_tool",
37
+ "classify_capability",
38
+ "dependency_order",
39
+ "safety_level_for_tool",
40
+ ]
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Capability:
45
+ name: str
46
+ tools: tuple[str, ...]
47
+ depends_on: tuple[str, ...]
48
+ status: str = "implemented"
49
+ safety_level: str = "read-only"
50
+
51
+
52
+ # Mutating capabilities alter live state rather than only reading project/docs.
53
+ # high-risk is not yet assigned to any capability; it is reserved for future
54
+ # evaluation/patch capabilities.
55
+ _MUTATING_CAPABILITIES = ("runtime", "testing")
56
+
57
+ # Per-tool safety overrides. A tool listed here overrides its owning
58
+ # capability's default safety level.
59
+ _TOOL_SAFETY_LEVELS: dict[str, str] = {
60
+ "quickshell_apply_patch": "mutating",
61
+ }
62
+
63
+
64
+ def _safety_level(name: str) -> str:
65
+ return "mutating" if name in _MUTATING_CAPABILITIES else "read-only"
66
+
67
+
68
+ _CAPABILITY_MODULES = (
69
+ knowledge,
70
+ validation,
71
+ generation,
72
+ migration,
73
+ debugging,
74
+ project,
75
+ assistant,
76
+ )
77
+
78
+ CAPABILITIES: dict[str, Capability] = {
79
+ mod.CAPABILITY_NAME: Capability(
80
+ name=mod.CAPABILITY_NAME,
81
+ tools=mod.CAPABILITY_TOOLS,
82
+ depends_on=mod.CAPABILITY_DEPENDS_ON,
83
+ safety_level=_safety_level(mod.CAPABILITY_NAME),
84
+ )
85
+ for mod in _CAPABILITY_MODULES
86
+ }
87
+
88
+ PLANNED_CAPABILITIES: dict[str, Capability] = {
89
+ name: Capability(
90
+ name=name,
91
+ tools=(),
92
+ depends_on=(),
93
+ status="planned",
94
+ safety_level=_safety_level(name),
95
+ )
96
+ for name in ("runtime", "inspection", "testing", "performance")
97
+ }
98
+
99
+ ALL_CAPABILITIES: dict[str, Capability] = {**CAPABILITIES, **PLANNED_CAPABILITIES}
100
+
101
+ # Convenience dict derived from the authoritative dataclass fields.
102
+ SAFETY_LEVELS: dict[str, str] = {name: cap.safety_level for name, cap in ALL_CAPABILITIES.items()}
103
+
104
+ # `quickshell_stats` reports session telemetry and lives entirely in server.py;
105
+ # it is not owned by any domain capability.
106
+ _SYSTEM_TOOLS = ("quickshell_stats",)
107
+
108
+
109
+ def capability_for_tool(tool: str) -> str | None:
110
+ """Return the capability that owns *tool*, or None for system tools."""
111
+ for cap in CAPABILITIES.values():
112
+ if tool in cap.tools:
113
+ return cap.name
114
+ return None
115
+
116
+
117
+ def classify_capability(name: str) -> str:
118
+ """Return the safety level of a registered capability.
119
+
120
+ Levels: ``read-only`` (analysis), ``mutating`` (changes live state),
121
+ ``high-risk`` (arbitrary evaluation or file mutation; not yet assigned).
122
+ """
123
+ cap = ALL_CAPABILITIES.get(name)
124
+ if cap is None:
125
+ raise ValueError(f"Unknown capability '{name}'")
126
+ return cap.safety_level
127
+
128
+
129
+ def safety_level_for_tool(tool: str) -> str:
130
+ """Return the safety level of the capability owning *tool*.
131
+
132
+ A per-tool override (``_TOOL_SAFETY_LEVELS``) wins; otherwise the level
133
+ is inherited from the owning capability. System tools (e.g.
134
+ ``quickshell_stats``) are telemetry and are always classified read-only.
135
+ """
136
+ if tool in _TOOL_SAFETY_LEVELS:
137
+ return _TOOL_SAFETY_LEVELS[tool]
138
+ cap = capability_for_tool(tool)
139
+ if cap is not None:
140
+ return classify_capability(cap)
141
+ if tool in _SYSTEM_TOOLS:
142
+ return "read-only"
143
+ raise ValueError(f"Unknown tool '{tool}'")
144
+
145
+
146
+ def dependency_order() -> list[str]:
147
+ """Topologically order the implemented capabilities (raises on a cycle)."""
148
+ graph = {name: set(cap.depends_on) for name, cap in CAPABILITIES.items()}
149
+ for name, deps in graph.items():
150
+ missing = deps - set(CAPABILITIES)
151
+ if missing:
152
+ raise ValueError(f"capability {name!r} depends on unregistered {sorted(missing)!r}")
153
+ order: list[str] = []
154
+ resolved: set[str] = set()
155
+ while len(order) < len(graph):
156
+ progress = False
157
+ for name, deps in graph.items():
158
+ if name in resolved:
159
+ continue
160
+ if deps <= resolved:
161
+ order.append(name)
162
+ resolved.add(name)
163
+ progress = True
164
+ if not progress:
165
+ cycle = sorted(set(graph) - resolved)
166
+ raise ValueError(f"circular capability dependency: {cycle}")
167
+ return order
@@ -0,0 +1,32 @@
1
+ """The validation capability: statically validate QML against the docs and
2
+ check version compatibility.
3
+
4
+ Depends on: knowledge (validation builds on the type/guide indexes).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ..sources.compat import ( # noqa: F401
10
+ _api_in_version,
11
+ _changelog_hits,
12
+ _changelog_sections,
13
+ _check_compatibility,
14
+ _compat_at,
15
+ _compat_from_code,
16
+ _incorporate_range,
17
+ _parse_api_ref,
18
+ _scan_versions,
19
+ )
20
+ from ..sources.validate import ( # noqa: F401
21
+ _parse_members,
22
+ _parse_structure,
23
+ _tokenize,
24
+ _validate,
25
+ )
26
+
27
+ CAPABILITY_NAME = "validation"
28
+ CAPABILITY_TOOLS = (
29
+ "quickshell_validate_qml",
30
+ "quickshell_check_compatibility",
31
+ )
32
+ CAPABILITY_DEPENDS_ON = ("knowledge",)