quickshell-mcp 2.2.0__tar.gz → 2.4.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 (120) hide show
  1. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/PKG-INFO +1 -1
  2. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/pyproject.toml +1 -1
  3. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/assistant.py +2 -0
  4. quickshell_mcp-2.4.0/quickshell_mcp/capabilities/inspection.py +38 -0
  5. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/registry.py +17 -2
  6. quickshell_mcp-2.4.0/quickshell_mcp/capabilities/runtime.py +35 -0
  7. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/server.py +208 -0
  8. quickshell_mcp-2.4.0/quickshell_mcp/sources/runtime_profile.py +81 -0
  9. quickshell_mcp-2.4.0/quickshell_mcp/sources/runtime_session.py +287 -0
  10. quickshell_mcp-2.4.0/quickshell_mcp/sources/ui_runtime.py +277 -0
  11. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/scripts/smoke_test.py +15 -0
  12. quickshell_mcp-2.4.0/tests/fixtures/fake_qs.sh +13 -0
  13. quickshell_mcp-2.4.0/tests/fixtures/runtime-shell/main.qml +48 -0
  14. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_capabilities.py +19 -1
  15. quickshell_mcp-2.4.0/tests/test_runtime.py +201 -0
  16. quickshell_mcp-2.4.0/tests/test_ui_runtime.py +170 -0
  17. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.coderabbit.yaml +0 -0
  18. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  20. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/config_help.yml +0 -0
  21. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/pull_request_template.md +0 -0
  23. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/workflows/ci.yml +0 -0
  24. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.github/workflows/semantic-release.yml +0 -0
  25. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.gitignore +0 -0
  26. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/.releaserc.js +0 -0
  27. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/AGENTS.md +0 -0
  28. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/CLAUDE.md +0 -0
  29. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/CODE_OF_CONDUCT.md +0 -0
  30. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/CONTRIBUTING.md +0 -0
  31. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/Dockerfile +0 -0
  32. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/LICENSE +0 -0
  33. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/README.md +0 -0
  34. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/flake.lock +0 -0
  35. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/flake.nix +0 -0
  36. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/__init__.py +0 -0
  37. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/__main__.py +0 -0
  38. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/caches.py +0 -0
  39. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/__init__.py +0 -0
  40. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/debugging.py +0 -0
  41. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/generation.py +0 -0
  42. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/knowledge.py +0 -0
  43. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/migration.py +0 -0
  44. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/project.py +0 -0
  45. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/capabilities/validation.py +0 -0
  46. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/config.py +0 -0
  47. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/extraction.py +0 -0
  48. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/__init__.py +0 -0
  49. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/assistant.py +0 -0
  50. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/compat.py +0 -0
  51. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/docs.py +0 -0
  52. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/examples.py +0 -0
  53. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/explain_error.py +0 -0
  54. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/find_pattern.py +0 -0
  55. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/generate.py +0 -0
  56. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/implementations.py +0 -0
  57. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/migrate.py +0 -0
  58. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/project.py +0 -0
  59. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/project_intel.py +0 -0
  60. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/project_validate.py +0 -0
  61. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/qt_docs.py +0 -0
  62. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/refactor.py +0 -0
  63. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/search_all.py +0 -0
  64. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/style_match.py +0 -0
  65. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/sources/validate.py +0 -0
  66. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/utils.py +0 -0
  67. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/quickshell_mcp/versions.py +0 -0
  68. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/server.json +0 -0
  69. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/conftest.py +0 -0
  70. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/about.html +0 -0
  71. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/example_readme.md +0 -0
  72. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/examples_contents_root.json +0 -0
  73. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/examples_repo_info.json +0 -0
  74. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/guide_index.html +0 -0
  75. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/hyprland_monitor.html +0 -0
  76. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_caelestia_anchoranim.qml +0 -0
  77. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_caelestia_repo_info.json +0 -0
  78. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_caelestia_tree.json +0 -0
  79. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_dots_audio.qml +0 -0
  80. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_dots_repo_info.json +0 -0
  81. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_dots_tree.json +0 -0
  82. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_noctalia_nbutton.qml +0 -0
  83. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/impl_noctalia_tree.json +0 -0
  84. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/components/ClockWidget.qml +0 -0
  85. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/components/VolumeWidget.qml +0 -0
  86. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/config.toml +0 -0
  87. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/main.qml +0 -0
  88. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/medium/services/VolumeService.qml +0 -0
  89. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/config.json +0 -0
  90. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/main.qml +0 -0
  91. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/util.js +0 -0
  92. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/projects/small/widgets/VolumeWidget.qml +0 -0
  93. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/qml_language.html +0 -0
  94. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qml_qtquick_rectangle.html +0 -0
  95. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qtquick_controls_qmlmodule.html +0 -0
  96. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/fixtures/qt_qtquick_qmlmodule.html +0 -0
  97. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_assistant.py +0 -0
  98. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_code_quality.py +0 -0
  99. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_compat.py +0 -0
  100. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_content_search.py +0 -0
  101. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_diagnostics.py +0 -0
  102. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_disk_cache.py +0 -0
  103. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_errors_retry.py +0 -0
  104. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_examples.py +0 -0
  105. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_explain_error.py +0 -0
  106. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_extraction.py +0 -0
  107. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_find_pattern.py +0 -0
  108. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_generate.py +0 -0
  109. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_implementations.py +0 -0
  110. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_index_search.py +0 -0
  111. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_live.py +0 -0
  112. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_migrate.py +0 -0
  113. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_project.py +0 -0
  114. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_project_generate.py +0 -0
  115. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_project_intel.py +0 -0
  116. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_project_validate.py +0 -0
  117. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_qt_docs.py +0 -0
  118. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_search_all.py +0 -0
  119. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_validate.py +0 -0
  120. {quickshell_mcp-2.2.0 → quickshell_mcp-2.4.0}/tests/test_versions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickshell-mcp
3
- Version: 2.2.0
3
+ Version: 2.4.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quickshell-mcp"
3
- version = "2.2.0"
3
+ version = "2.4.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"
@@ -25,4 +25,6 @@ CAPABILITY_DEPENDS_ON = (
25
25
  "migration",
26
26
  "debugging",
27
27
  "project",
28
+ "runtime",
29
+ "inspection",
28
30
  )
@@ -0,0 +1,38 @@
1
+ """The inspection capability: read and drive the live UI of a managed runtime
2
+ session.
3
+
4
+ Read-only by default (windows, screenshots, ui_tree, ui_find, ui_get_property,
5
+ screenshot_diff). ui_set_property and ui_invoke are mutating; ui_eval is
6
+ high-risk. All operate on a managed runtime session, never on unrelated
7
+ user processes.
8
+
9
+ Depends on: knowledge, project, runtime.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ..sources.ui_runtime import ( # noqa: F401
15
+ _screenshot,
16
+ _screenshot_diff,
17
+ _ui_eval,
18
+ _ui_find,
19
+ _ui_get_property,
20
+ _ui_invoke,
21
+ _ui_set_property,
22
+ _ui_tree,
23
+ _ui_windows,
24
+ )
25
+
26
+ CAPABILITY_NAME = "inspection"
27
+ CAPABILITY_TOOLS = (
28
+ "quickshell_windows",
29
+ "quickshell_screenshot",
30
+ "quickshell_screenshot_diff",
31
+ "quickshell_ui_tree",
32
+ "quickshell_ui_find",
33
+ "quickshell_ui_get_property",
34
+ "quickshell_ui_set_property",
35
+ "quickshell_ui_invoke",
36
+ "quickshell_ui_eval",
37
+ )
38
+ CAPABILITY_DEPENDS_ON = ("knowledge", "project", "runtime")
@@ -25,7 +25,17 @@ from __future__ import annotations
25
25
 
26
26
  from dataclasses import dataclass
27
27
 
28
- from . import assistant, debugging, generation, knowledge, migration, project, validation
28
+ from . import (
29
+ assistant,
30
+ debugging,
31
+ generation,
32
+ inspection,
33
+ knowledge,
34
+ migration,
35
+ project,
36
+ runtime,
37
+ validation,
38
+ )
29
39
 
30
40
  __all__ = [
31
41
  "ALL_CAPABILITIES",
@@ -58,6 +68,9 @@ _MUTATING_CAPABILITIES = ("runtime", "testing")
58
68
  # capability's default safety level.
59
69
  _TOOL_SAFETY_LEVELS: dict[str, str] = {
60
70
  "quickshell_apply_patch": "mutating",
71
+ "quickshell_ui_set_property": "mutating",
72
+ "quickshell_ui_invoke": "mutating",
73
+ "quickshell_ui_eval": "high-risk",
61
74
  }
62
75
 
63
76
 
@@ -72,6 +85,8 @@ _CAPABILITY_MODULES = (
72
85
  migration,
73
86
  debugging,
74
87
  project,
88
+ runtime,
89
+ inspection,
75
90
  assistant,
76
91
  )
77
92
 
@@ -93,7 +108,7 @@ PLANNED_CAPABILITIES: dict[str, Capability] = {
93
108
  status="planned",
94
109
  safety_level=_safety_level(name),
95
110
  )
96
- for name in ("runtime", "inspection", "testing", "performance")
111
+ for name in ("testing", "performance")
97
112
  }
98
113
 
99
114
  ALL_CAPABILITIES: dict[str, Capability] = {**CAPABILITIES, **PLANNED_CAPABILITIES}
@@ -0,0 +1,35 @@
1
+ """The runtime capability: manage isolated Quickshell runtime sessions.
2
+
3
+ Sessions are launched from explicit runtime profiles with isolated XDG dirs,
4
+ tracked in a registry, and inspectable via status, logs, and ping. The
5
+ capability is mutating: starting, stopping, and resetting sessions alter
6
+ process state and must be explicitly invoked.
7
+
8
+ Depends on: knowledge, project (runtime tools use project detection and
9
+ documentation for diagnostics).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ..sources.runtime_profile import _RuntimeProfile # noqa: F401
15
+ from ..sources.runtime_session import ( # noqa: F401
16
+ _SESSION_REGISTRY,
17
+ _logs,
18
+ _ping,
19
+ _qs_binary,
20
+ _reset_session,
21
+ _start_session,
22
+ _status_session,
23
+ _stop_session,
24
+ )
25
+
26
+ CAPABILITY_NAME = "runtime"
27
+ CAPABILITY_TOOLS = (
28
+ "quickshell_runtime_start",
29
+ "quickshell_runtime_stop",
30
+ "quickshell_runtime_reset",
31
+ "quickshell_runtime_status",
32
+ "quickshell_runtime_logs",
33
+ "quickshell_runtime_ping",
34
+ )
35
+ CAPABILITY_DEPENDS_ON = ("knowledge", "project")
@@ -61,6 +61,17 @@ from .capabilities.generation import ( # noqa: F401
61
61
  _refactor,
62
62
  _style_match,
63
63
  )
64
+ from .capabilities.inspection import ( # noqa: F401
65
+ _screenshot,
66
+ _screenshot_diff,
67
+ _ui_eval,
68
+ _ui_find,
69
+ _ui_get_property,
70
+ _ui_invoke,
71
+ _ui_set_property,
72
+ _ui_tree,
73
+ _ui_windows,
74
+ )
64
75
  from .capabilities.knowledge import ( # noqa: F401
65
76
  _GITHUB_API,
66
77
  _IMPL_QUERY_STOPWORDS,
@@ -117,6 +128,17 @@ from .capabilities.project import ( # noqa: F401
117
128
  _search_project,
118
129
  _validate_project,
119
130
  )
131
+ from .capabilities.runtime import ( # noqa: F401
132
+ _SESSION_REGISTRY,
133
+ _logs,
134
+ _ping,
135
+ _qs_binary,
136
+ _reset_session,
137
+ _RuntimeProfile,
138
+ _start_session,
139
+ _status_session,
140
+ _stop_session,
141
+ )
120
142
  from .capabilities.validation import ( # noqa: F401
121
143
  _api_in_version,
122
144
  _changelog_hits,
@@ -163,6 +185,14 @@ from .versions import ( # noqa: F401
163
185
  mcp = FastMCP("quickshell-mcp")
164
186
 
165
187
 
188
+ def _require_session(session_id: str):
189
+ """Look up a tracked runtime session, raising ValueError if unknown."""
190
+ session = _SESSION_REGISTRY.get(session_id)
191
+ if session is None:
192
+ raise ValueError(f"Unknown runtime session '{session_id}'")
193
+ return session
194
+
195
+
166
196
  @mcp.tool()
167
197
  def quickshell_list_versions(refresh: bool = False) -> dict:
168
198
  """List all Quickshell documentation versions currently published on
@@ -1016,6 +1046,184 @@ def quickshell_project_migrate(project: str, from_version: str, to_version: str)
1016
1046
  return _migrate_project(project, from_version=from_version, to_version=to_version)
1017
1047
 
1018
1048
 
1049
+ @mcp.tool()
1050
+ def quickshell_runtime_start(
1051
+ project: str,
1052
+ entrypoint: str | None = None,
1053
+ config_dir: str | None = None,
1054
+ environment: dict[str, str] | None = None,
1055
+ compositor: str | None = None,
1056
+ arguments: list[str] | None = None,
1057
+ ) -> dict:
1058
+ """Start a managed, isolated Quickshell runtime session for a project.
1059
+
1060
+ Launches ``qs`` with isolated XDG directories so it never touches your
1061
+ real desktop session or other quickshell instances. Returns a session id
1062
+ and tracks the process for later status, logs, ping, stop, and reset.
1063
+ This is a mutating operation: it launches a process."""
1064
+ _record_tool("quickshell_runtime_start")
1065
+ profile = _RuntimeProfile(
1066
+ project_root=project,
1067
+ entrypoint=entrypoint,
1068
+ config_dir=config_dir,
1069
+ environment=environment or {},
1070
+ compositor=compositor,
1071
+ arguments=arguments or [],
1072
+ )
1073
+ return _start_session(profile).to_dict()
1074
+
1075
+
1076
+ @mcp.tool()
1077
+ def quickshell_runtime_stop(session_id: str) -> dict:
1078
+ """Stop a managed runtime session safely (SIGTERM, then SIGKILL on timeout).
1079
+
1080
+ Handles already-exited and orphaned processes; stops only the tracked
1081
+ session's process group, never unrelated user processes. Mutating."""
1082
+ _record_tool("quickshell_runtime_stop")
1083
+ session = _require_session(session_id)
1084
+ _stop_session(session)
1085
+ return session.to_dict()
1086
+
1087
+
1088
+ @mcp.tool()
1089
+ def quickshell_runtime_reset(session_id: str) -> dict:
1090
+ """Reset a managed runtime session to a clean state.
1091
+
1092
+ Stops the current session, cleans up its isolated temp dirs, and starts a
1093
+ fresh session with the same profile under a new session id. Mutating."""
1094
+ _record_tool("quickshell_runtime_reset")
1095
+ session = _require_session(session_id)
1096
+ fresh = _reset_session(session)
1097
+ return fresh.to_dict()
1098
+
1099
+
1100
+ @mcp.tool()
1101
+ def quickshell_runtime_status(session_id: str) -> dict:
1102
+ """Return structured status for a runtime session: session id, running
1103
+ state, PID, startup duration, exit code, and profile identity. Read-only."""
1104
+ _record_tool("quickshell_runtime_status")
1105
+ session = _require_session(session_id)
1106
+ return _status_session(session)
1107
+
1108
+
1109
+ @mcp.tool()
1110
+ def quickshell_runtime_logs(
1111
+ session_id: str,
1112
+ stream: str | None = None,
1113
+ severity: str | None = None,
1114
+ text: str | None = None,
1115
+ limit: int = 200,
1116
+ ) -> dict:
1117
+ """Return structured logs from a runtime session with optional filtering
1118
+ by stream (stdout/stderr), text, and a bounded limit. Read-only."""
1119
+ _record_tool("quickshell_runtime_logs")
1120
+ session = _require_session(session_id)
1121
+ lines = _logs(session, stream=stream, severity=severity, text=text, limit=limit)
1122
+ return {"session_id": session_id, "logs": lines, "count": len(lines)}
1123
+
1124
+
1125
+ @mcp.tool()
1126
+ def quickshell_runtime_ping(session_id: str) -> dict:
1127
+ """Lightweight readiness/health check for a runtime session.
1128
+
1129
+ Distinguishes: process_running, exited (with exit code), or unhealthy.
1130
+ Fast, read-only."""
1131
+ _record_tool("quickshell_runtime_ping")
1132
+ session = _require_session(session_id)
1133
+ return _ping(session)
1134
+
1135
+
1136
+ @mcp.tool()
1137
+ def quickshell_windows(session_id: str) -> dict:
1138
+ """Enumerate windows/surfaces belonging to a managed Quickshell runtime
1139
+ session. Read-only. Requires a compositor adapter; when unavailable
1140
+ returns an empty list with an explanatory note (never fails)."""
1141
+ _record_tool("quickshell_windows")
1142
+ return _ui_windows(session_id)
1143
+
1144
+
1145
+ @mcp.tool()
1146
+ def quickshell_screenshot(session_id: str) -> dict:
1147
+ """Capture a screenshot from a managed Quickshell runtime session.
1148
+
1149
+ Requires the ``grim`` compositor screenshot tool on PATH. Returns the
1150
+ image path or an "unavailable" note. Read-only."""
1151
+ _record_tool("quickshell_screenshot")
1152
+ return _screenshot(session_id)
1153
+
1154
+
1155
+ @mcp.tool()
1156
+ def quickshell_screenshot_diff(baseline: str, actual: str, output: str | None = None) -> dict:
1157
+ """Compare two runtime screenshots with ImageMagick ``compare``.
1158
+
1159
+ Returns whether they differ, the diff image path, and a metric.
1160
+ Deterministic for CI. Read-only."""
1161
+ _record_tool("quickshell_screenshot_diff")
1162
+ return _screenshot_diff(baseline, actual, output=output)
1163
+
1164
+
1165
+ @mcp.tool()
1166
+ def quickshell_ui_tree(session_id: str, depth: int = 3) -> dict:
1167
+ """Inspect the live QML object tree of a managed runtime session.
1168
+
1169
+ Returns a compact, depth-limited hierarchy from the injected
1170
+ 'inspector' IpcHandler target. Read-only."""
1171
+ _record_tool("quickshell_ui_tree")
1172
+ return _ui_tree(session_id, depth=depth)
1173
+
1174
+
1175
+ @mcp.tool()
1176
+ def quickshell_ui_find(session_id: str, query: str) -> dict:
1177
+ """Search the live QML object tree of a managed runtime session by name,
1178
+ type, text, or property. Returns stable references for later runtime
1179
+ tools. Read-only."""
1180
+ _record_tool("quickshell_ui_find")
1181
+ return _ui_find(session_id, query)
1182
+
1183
+
1184
+ @mcp.tool()
1185
+ def quickshell_ui_get_property(session_id: str, target: str, property_name: str) -> dict:
1186
+ """Read a live QML property value from a managed runtime object via
1187
+ ``qs ipc prop get``. Validates the object/property exist. Read-only."""
1188
+ _record_tool("quickshell_ui_get_property")
1189
+ return _ui_get_property(session_id, target, property_name)
1190
+
1191
+
1192
+ @mcp.tool()
1193
+ def quickshell_ui_set_property(
1194
+ session_id: str, target: str, property_name: str, value: str
1195
+ ) -> dict:
1196
+ """Set a live QML property on a managed runtime object. Mutating.
1197
+
1198
+ Requires an explicit runtime session, validates the property exists,
1199
+ returns the old and new values, and never modifies project files."""
1200
+ _record_tool("quickshell_ui_set_property")
1201
+ return _ui_set_property(session_id, target, property_name, value)
1202
+
1203
+
1204
+ @mcp.tool()
1205
+ def quickshell_ui_invoke(
1206
+ session_id: str, target: str, method: str, arguments: list[str] | None = None
1207
+ ) -> dict:
1208
+ """Invoke a QML method on a managed runtime object via ``qs ipc call``.
1209
+ Mutating. Validates the method and arguments; enforces runtime session
1210
+ boundaries; no arbitrary process/system calls."""
1211
+ _record_tool("quickshell_ui_invoke")
1212
+ return _ui_invoke(session_id, target, method, arguments=arguments)
1213
+
1214
+
1215
+ @mcp.tool()
1216
+ def quickshell_ui_eval(session_id: str, js_code: str, timeout: int = 5) -> dict:
1217
+ """HIGH-RISK: evaluate QML/JavaScript in a managed runtime session.
1218
+
1219
+ Explicitly opt-in, session-scoped, with an execution timeout and output
1220
+ limits. No filesystem or process access is granted. Prefer
1221
+ quickshell_ui_get_property / quickshell_ui_invoke for controlled reads
1222
+ and calls."""
1223
+ _record_tool("quickshell_ui_eval")
1224
+ return _ui_eval(session_id, js_code, timeout=timeout)
1225
+
1226
+
1019
1227
  @mcp.tool()
1020
1228
  def quickshell_stats() -> dict:
1021
1229
  """Report session usage stats for this MCP server: per-tool call counts,
@@ -0,0 +1,81 @@
1
+ """Runtime profiles: explicit, inspectable launch configuration for managed
2
+ Quickshell runtime sessions.
3
+
4
+ A profile describes *how* to run a Quickshell project in isolation: the
5
+ project root, entrypoint, config directory, environment overrides, compositor
6
+ or backend, command-line arguments, and optional fixture data. Profiles are
7
+ pure data — nothing here launches processes.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+
15
+
16
+ @dataclass
17
+ class _RuntimeProfile:
18
+ """Explicit launch configuration for a managed Quickshell session."""
19
+
20
+ project_root: str
21
+ entrypoint: str | None = None
22
+ config_dir: str | None = None
23
+ environment: dict[str, str] = field(default_factory=dict)
24
+ compositor: str | None = None
25
+ arguments: list[str] = field(default_factory=list)
26
+ fixture_data: dict[str, str] = field(default_factory=dict)
27
+
28
+ def resolved_entrypoint(self) -> str:
29
+ """The QML entrypoint: the configured one, or the project's detected
30
+ entrypoint, or ``shell.qml`` / ``main.qml`` under the project root."""
31
+ if self.entrypoint:
32
+ return str(Path(self.entrypoint).expanduser().resolve())
33
+ root = Path(self.project_root).expanduser().resolve()
34
+ for name in ("shell.qml", "main.qml", "config.qml"):
35
+ candidate = root / name
36
+ if candidate.is_file():
37
+ return str(candidate)
38
+ raise ValueError(
39
+ f"No entrypoint found under {root}; pass entrypoint= or add shell.qml/main.qml."
40
+ )
41
+
42
+ def isolated_environment(self, instance_id: str) -> dict[str, str]:
43
+ """Environment for launching the session: inherited variables plus
44
+ profile overrides and isolated XDG dirs for the instance."""
45
+ import os
46
+ import tempfile
47
+
48
+ base = dict(os.environ)
49
+ base.update(self.environment)
50
+ prefix = Path(tempfile.gettempdir()) / f"qs-mcp-{instance_id}"
51
+ # Isolate per-session state so a managed shell never touches the
52
+ # user's real XDG dirs or other quickshell instances.
53
+ base.update(
54
+ {
55
+ "QUICKSHELL_INSTANCE_ID": instance_id,
56
+ "XDG_CONFIG_HOME": str(prefix / "config"),
57
+ "XDG_CACHE_HOME": str(prefix / "cache"),
58
+ "XDG_DATA_HOME": str(prefix / "data"),
59
+ "XDG_STATE_HOME": str(prefix / "state"),
60
+ }
61
+ )
62
+ return base
63
+
64
+ def to_dict(self) -> dict[str, object]:
65
+ root = Path(self.project_root).expanduser().resolve()
66
+ entrypoint: str | None
67
+ if self.entrypoint or root.is_dir():
68
+ try:
69
+ entrypoint = self.resolved_entrypoint()
70
+ except ValueError:
71
+ entrypoint = self.entrypoint
72
+ else:
73
+ entrypoint = self.entrypoint
74
+ return {
75
+ "project_root": self.project_root,
76
+ "entrypoint": entrypoint,
77
+ "config_dir": self.config_dir,
78
+ "compositor": self.compositor,
79
+ "arguments": self.arguments,
80
+ "fixture_data": self.fixture_data,
81
+ }