libspec 10.2.0__tar.gz → 10.3.3__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 (55) hide show
  1. {libspec-10.2.0 → libspec-10.3.3}/PKG-INFO +18 -9
  2. {libspec-10.2.0 → libspec-10.3.3}/README.md +17 -7
  3. {libspec-10.2.0 → libspec-10.3.3}/libspec/agent_config.py +47 -5
  4. {libspec-10.2.0 → libspec-10.3.3}/libspec/mcp_server.py +77 -194
  5. {libspec-10.2.0 → libspec-10.3.3}/libspec/repl.py +0 -106
  6. {libspec-10.2.0 → libspec-10.3.3}/libspec/workflow.py +6 -5
  7. {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/PKG-INFO +18 -9
  8. {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/SOURCES.txt +0 -7
  9. {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/entry_points.txt +0 -3
  10. {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/requires.txt +0 -1
  11. libspec-10.3.3/libspec.egg-info/top_level.txt +1 -0
  12. {libspec-10.2.0 → libspec-10.3.3}/pyproject.toml +2 -7
  13. {libspec-10.2.0 → libspec-10.3.3}/tests/test_agent_config.py +27 -0
  14. {libspec-10.2.0 → libspec-10.3.3}/tests/test_lsp.py +4 -22
  15. {libspec-10.2.0 → libspec-10.3.3}/tests/test_spec.py +4 -0
  16. {libspec-10.2.0 → libspec-10.3.3}/tests/test_spec_diff.py +19 -0
  17. libspec-10.2.0/libspec/lsp_client.py +0 -193
  18. libspec-10.2.0/libspec/pylsp_plugin.py +0 -195
  19. libspec-10.2.0/libspec-scheduler/src/libspec_scheduler/__init__.py +0 -1
  20. libspec-10.2.0/libspec-scheduler/src/libspec_scheduler/mcp.py +0 -249
  21. libspec-10.2.0/libspec-scheduler/src/libspec_scheduler/scheduler.py +0 -267
  22. libspec-10.2.0/libspec-scheduler/tests/test_mcp_scheduler.py +0 -86
  23. libspec-10.2.0/libspec-scheduler/tests/test_scheduler.py +0 -196
  24. libspec-10.2.0/libspec.egg-info/top_level.txt +0 -2
  25. {libspec-10.2.0 → libspec-10.3.3}/LICENSE +0 -0
  26. {libspec-10.2.0 → libspec-10.3.3}/libspec/__init__.py +0 -0
  27. {libspec-10.2.0 → libspec-10.3.3}/libspec/cli.py +0 -0
  28. {libspec-10.2.0 → libspec-10.3.3}/libspec/colors.py +0 -0
  29. {libspec-10.2.0 → libspec-10.3.3}/libspec/common.py +0 -0
  30. {libspec-10.2.0 → libspec-10.3.3}/libspec/err.py +0 -0
  31. {libspec-10.2.0 → libspec-10.3.3}/libspec/spec.py +0 -0
  32. {libspec-10.2.0 → libspec-10.3.3}/libspec/spec_diff.py +0 -0
  33. {libspec-10.2.0 → libspec-10.3.3}/libspec/spec_types.py +0 -0
  34. {libspec-10.2.0 → libspec-10.3.3}/libspec/specweb.py +0 -0
  35. {libspec-10.2.0 → libspec-10.3.3}/libspec/store.py +0 -0
  36. {libspec-10.2.0 → libspec-10.3.3}/libspec/templates/err.py +0 -0
  37. {libspec-10.2.0 → libspec-10.3.3}/libspec/templates/skill.md.j2 +0 -0
  38. {libspec-10.2.0 → libspec-10.3.3}/libspec/user_story.py +0 -0
  39. {libspec-10.2.0 → libspec-10.3.3}/libspec/util.py +0 -0
  40. {libspec-10.2.0 → libspec-10.3.3}/libspec/watcher.py +0 -0
  41. {libspec-10.2.0 → libspec-10.3.3}/libspec.egg-info/dependency_links.txt +0 -0
  42. {libspec-10.2.0 → libspec-10.3.3}/setup.cfg +0 -0
  43. {libspec-10.2.0 → libspec-10.3.3}/tests/test_agent_workflow.py +0 -0
  44. {libspec-10.2.0 → libspec-10.3.3}/tests/test_cli.py +0 -0
  45. {libspec-10.2.0 → libspec-10.3.3}/tests/test_common_types.py +0 -0
  46. {libspec-10.2.0 → libspec-10.3.3}/tests/test_cwd_validation.py +0 -0
  47. {libspec-10.2.0 → libspec-10.3.3}/tests/test_idempotency.py +0 -0
  48. {libspec-10.2.0 → libspec-10.3.3}/tests/test_mcp_metadata.py +0 -0
  49. {libspec-10.2.0 → libspec-10.3.3}/tests/test_missing_requirement_variable.py +0 -0
  50. {libspec-10.2.0 → libspec-10.3.3}/tests/test_mixins.py +0 -0
  51. {libspec-10.2.0 → libspec-10.3.3}/tests/test_projects.py +0 -0
  52. {libspec-10.2.0 → libspec-10.3.3}/tests/test_removed_cli.py +0 -0
  53. {libspec-10.2.0 → libspec-10.3.3}/tests/test_repl.py +0 -0
  54. {libspec-10.2.0 → libspec-10.3.3}/tests/test_watcher.py +0 -0
  55. {libspec-10.2.0 → libspec-10.3.3}/tests/test_xml_render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libspec
3
- Version: 10.2.0
3
+ Version: 10.3.3
4
4
  Summary: Spec-driven development library for LLM-assisted coding
5
5
  Author-email: "Derek A. Rhodes" <physci@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -20,7 +20,6 @@ License-File: LICENSE
20
20
  Requires-Dist: click>=8.1.8
21
21
  Requires-Dist: jinja2>=3.1.6
22
22
  Requires-Dist: mcp>=1.27.0
23
- Requires-Dist: python-lsp-server>=1.12.2
24
23
  Requires-Dist: toml>=0.10.2
25
24
  Requires-Dist: skillkit>=0.4.0
26
25
  Requires-Dist: prompt-toolkit>=3.0.52
@@ -33,8 +32,6 @@ Dynamic: license-file
33
32
 
34
33
  [![CI/CD](https://github.com/drhodes/libspec/actions/workflows/ci.yml/badge.svg)](https://github.com/drhodes/libspec/actions/workflows/ci.yml)
35
34
  [![Documentation](https://github.com/drhodes/libspec/actions/workflows/docs.yml/badge.svg)](https://drhodes.github.io/libspec/)
36
- [![PyPI version](https://img.shields.io/pypi/v/libspec.svg)](https://pypi.org/project/libspec/)
37
- [![Python versions](https://img.shields.io/pypi/pyversions/libspec.svg)](https://pypi.org/project/libspec/)
38
35
  [![License](https://img.shields.io/github/license/drhodes/libspec.svg)](https://github.com/drhodes/libspec/blob/main/LICENSE)
39
36
 
40
37
  > **an ounce of spec is worth a pound of tokens**
@@ -169,10 +166,10 @@ Jinja2 template string. More about that later...
169
166
  ## Inheritance
170
167
 
171
168
  Inheritance means "does this and more." The inherited superclass
172
- docstrings are normative, but the compiled XML preserves them as
173
- `<inherits><ref>...</ref></inherits>` references instead of prepending
174
- their prose into the child docstring. Renderers such as `libspec diff`
175
- can expand those refs when a review needs the inherited context.
169
+ docstrings are normative, but compiled spec snapshots preserve them as
170
+ references instead of prepending their prose into the child docstring.
171
+ Renderers such as `libspec diff` can expand those refs when a review
172
+ needs the inherited context.
176
173
 
177
174
  ## Mixins
178
175
 
@@ -181,6 +178,18 @@ Mixins help get around the diamond problem. (TODO: write more about this)
181
178
  ## Versioning
182
179
 
183
180
  Note that the versioning of `libspec` is still being hammered
184
- out. Currently, the version of `libspec` appears in the generated XML
181
+ out. Currently, the version of `libspec` appears in generated spec snapshots
185
182
  (`libspec-version` field). But, how diffs will be performed on
186
183
  different versions is unexplored.
184
+
185
+ ## Feature Branch & Multi-Commit Spec Diffing
186
+
187
+ When working on feature branches with intermediate commits, running `uv run libspec diff` right after a commit will report `No changes detected` because `HEAD` matches the live spec on disk.
188
+
189
+ To track the cumulative specification delta across all intermediate commits on a feature branch, pass the base branch target:
190
+
191
+ ```bash
192
+ uv run libspec diff main
193
+ ```
194
+
195
+ This tracks all specification additions, requirement modifications, and docstring changes relative to `main` throughout multi-stage development workflows.
@@ -2,8 +2,6 @@
2
2
 
3
3
  [![CI/CD](https://github.com/drhodes/libspec/actions/workflows/ci.yml/badge.svg)](https://github.com/drhodes/libspec/actions/workflows/ci.yml)
4
4
  [![Documentation](https://github.com/drhodes/libspec/actions/workflows/docs.yml/badge.svg)](https://drhodes.github.io/libspec/)
5
- [![PyPI version](https://img.shields.io/pypi/v/libspec.svg)](https://pypi.org/project/libspec/)
6
- [![Python versions](https://img.shields.io/pypi/pyversions/libspec.svg)](https://pypi.org/project/libspec/)
7
5
  [![License](https://img.shields.io/github/license/drhodes/libspec.svg)](https://github.com/drhodes/libspec/blob/main/LICENSE)
8
6
 
9
7
  > **an ounce of spec is worth a pound of tokens**
@@ -138,10 +136,10 @@ Jinja2 template string. More about that later...
138
136
  ## Inheritance
139
137
 
140
138
  Inheritance means "does this and more." The inherited superclass
141
- docstrings are normative, but the compiled XML preserves them as
142
- `<inherits><ref>...</ref></inherits>` references instead of prepending
143
- their prose into the child docstring. Renderers such as `libspec diff`
144
- can expand those refs when a review needs the inherited context.
139
+ docstrings are normative, but compiled spec snapshots preserve them as
140
+ references instead of prepending their prose into the child docstring.
141
+ Renderers such as `libspec diff` can expand those refs when a review
142
+ needs the inherited context.
145
143
 
146
144
  ## Mixins
147
145
 
@@ -150,6 +148,18 @@ Mixins help get around the diamond problem. (TODO: write more about this)
150
148
  ## Versioning
151
149
 
152
150
  Note that the versioning of `libspec` is still being hammered
153
- out. Currently, the version of `libspec` appears in the generated XML
151
+ out. Currently, the version of `libspec` appears in generated spec snapshots
154
152
  (`libspec-version` field). But, how diffs will be performed on
155
153
  different versions is unexplored.
154
+
155
+ ## Feature Branch & Multi-Commit Spec Diffing
156
+
157
+ When working on feature branches with intermediate commits, running `uv run libspec diff` right after a commit will report `No changes detected` because `HEAD` matches the live spec on disk.
158
+
159
+ To track the cumulative specification delta across all intermediate commits on a feature branch, pass the base branch target:
160
+
161
+ ```bash
162
+ uv run libspec diff main
163
+ ```
164
+
165
+ This tracks all specification additions, requirement modifications, and docstring changes relative to `main` throughout multi-stage development workflows.
@@ -91,7 +91,7 @@ class AgentConfig(abc.ABC):
91
91
  spec.mcp.AgentSkillInstallation
92
92
  """
93
93
  filename = "SKILL.md"
94
- temp_file = os.path.join(dir_path, f"TEMP_{filename}")
94
+ temp_file = os.path.join(dir_path, f"TEMP_{os.getpid()}_{filename}")
95
95
  os.makedirs(dir_path, exist_ok=True)
96
96
 
97
97
  try:
@@ -141,7 +141,7 @@ class AgentConfig(abc.ABC):
141
141
  json.dump(config, f, indent=2)
142
142
  except Exception as e:
143
143
  raise RuntimeError(
144
- f"Config Persistence Failure: Could not write updated configuration to {path}. {e}"
144
+ f"Config Persistence Failure: Could not write updated JSON configuration to {path}. {e}"
145
145
  )
146
146
 
147
147
  def _load_toml_config(self, path: str) -> dict:
@@ -169,6 +169,7 @@ class AgentConfig(abc.ABC):
169
169
  @property
170
170
  def skill_dir_path(self) -> str:
171
171
  # spec.mcp.AgentSkillDriftDetection
172
+ # spec.agents.AgentsDirectoryLayoutReq
172
173
  if self.agent_id == "antigravity":
173
174
  return os.path.join(
174
175
  self.project_root, ".gemini", "antigravity", "skills", "libspec"
@@ -183,11 +184,20 @@ class AgentConfig(abc.ABC):
183
184
  return os.path.join(self.project_root, ".github", "skills", "libspec")
184
185
  elif self.agent_id == "codex":
185
186
  return os.path.join(self.project_root, ".codex", "skills", "libspec")
187
+ elif self.agent_id == "agents":
188
+ primary = os.path.join(self.project_root, ".agents", "skills", "libspec")
189
+ legacy = os.path.join(
190
+ self.project_root, ".agents", "skills", "libspec-agent-workflow"
191
+ )
192
+ if not os.path.exists(primary) and os.path.exists(legacy):
193
+ return legacy
194
+ return primary
186
195
  raise ValueError(f"Unknown agent ID: {self.agent_id}")
187
196
 
188
197
  @property
189
198
  def is_active(self) -> bool:
190
199
  # spec.mcp.AgentSkillDriftDetection
200
+ # spec.agents.AgentsDirectoryLayoutReq
191
201
  if self.agent_id == "antigravity":
192
202
  return os.path.exists(
193
203
  os.path.join(
@@ -212,18 +222,26 @@ class AgentConfig(abc.ABC):
212
222
  return os.path.exists(
213
223
  os.path.join(self.project_root, ".codex", "config.toml")
214
224
  )
225
+ elif self.agent_id == "agents":
226
+ agents_path = os.path.join(self.project_root, ".agents")
227
+ return os.path.exists(agents_path) and os.path.isdir(agents_path)
215
228
  return False
216
229
 
217
230
  def is_skill_up_to_date(self) -> bool:
218
231
  # spec.mcp.SkillVersionValidation
232
+ # spec.agents.AgentsSkillDriftDetectionReq
219
233
  skill_path = os.path.join(self.skill_dir_path, "SKILL.md")
220
- if not os.path.exists(skill_path):
234
+ if not os.path.exists(skill_path) or not os.path.isfile(skill_path):
221
235
  return False
222
236
  try:
223
- current_content = self._render_skill()
224
237
  with open(skill_path, encoding="utf-8") as f:
225
238
  installed_content = f.read()
226
- return current_content == installed_content
239
+
240
+ if "libspec: disable-auto-heal" in installed_content:
241
+ return True
242
+
243
+ current_content = self._render_skill()
244
+ return current_content.strip() == installed_content.strip()
227
245
  except Exception:
228
246
  return False
229
247
 
@@ -480,6 +498,30 @@ class CodexConfig(AgentConfig):
480
498
  agent_description = "Navigation and specification tools for Codex"
481
499
 
482
500
 
501
+ class AgentsConfig(AgentConfig):
502
+ """
503
+ Handles configuration for workspace .agents skills and setup.
504
+ # spec.agents.AgentsDirectoryLayoutReq
505
+ # spec.agents.AgentsSkillValidationReq
506
+ # spec.agents.AgentsSkillHealingFeat
507
+ """
508
+
509
+ def configure(self) -> str:
510
+ config_dir = os.path.join(self.project_root, ".agents")
511
+ if os.path.exists(config_dir) and not os.path.isdir(config_dir):
512
+ raise ValueError(
513
+ f"Path Conflict: {config_dir} exists as a file instead of a directory."
514
+ )
515
+ os.makedirs(config_dir, exist_ok=True)
516
+ skill_dir = self.skill_dir_path
517
+ self._install_skill(skill_dir, self._render_skill())
518
+ return f"Successfully configured .agents skill in {skill_dir}."
519
+
520
+ agent_id = "agents"
521
+ agent_display_name = "Agents"
522
+ agent_description = "Navigation and specification tools for workspace .agents"
523
+
524
+
483
525
  def list_supported_agents() -> str:
484
526
  """
485
527
  Returns a formatted list of all supported agent names.
@@ -1,20 +1,13 @@
1
- """
2
- MCP Server entry point for libspec.
3
- """
4
-
5
1
  import ast
6
2
  import glob
7
3
  import json
8
4
  import os
9
5
  import subprocess
10
6
  import sys
11
- import threading
12
7
  from pathlib import Path
13
8
 
14
9
  from mcp.server.fastmcp import FastMCP
15
10
 
16
- from libspec.lsp_client import LspClient, LspError
17
-
18
11
  mcp = FastMCP(
19
12
  "libspec",
20
13
  instructions="""
@@ -23,60 +16,18 @@ mcp = FastMCP(
23
16
  This server provides specialized tools for specification-driven development
24
17
  and deep code analysis.
25
18
 
26
- ## Navigation & Search (Prefer LSP over Grep)
27
- - Always prefer LSP-based tools (`search`, `peek`, `symbols`, `usage`)
28
- over generic `grep` when you need semantic understanding of the code.
19
+ ## Navigation & Search
29
20
  - Use `search` to find both specification components (Requirements/Features)
30
21
  and their implementations.
31
22
  - Use `peek` to retrieve documentation and definitions for specific symbols
32
23
  without reading entire files.
33
- - Use `symbols` to orient yourself within a new file's structure.
24
+ - Use `symbols` to orient yourself within a file's structure.
34
25
  - Use `usage` to perform impact analysis before modifying shared components.
35
-
36
- ## Lifecycle
37
- - The background LSP server (`pylsp`) auto-initializes on the first call to
38
- any LSP-dependent tool. No manual setup is required.
39
26
 
40
27
  spec.mcp.McpServerInstructions
41
28
  """,
42
29
  )
43
30
 
44
- # Global LSP client instance
45
- lsp = LspClient()
46
- lsp_lock = threading.Lock()
47
-
48
-
49
- def _ensure_lsp_started(requested_root: str = None):
50
- """
51
- Internal helper to ensure the LSP process is running.
52
- Auto-starts with sensible defaults if not already initialized.
53
- """
54
- if lsp.process:
55
- return
56
-
57
- with lsp_lock:
58
- if lsp.process:
59
- return
60
-
61
- # Default root discovery logic
62
- if requested_root:
63
- root_dir = requested_root
64
- else:
65
- root_candidates = ["spec", "."]
66
- root_dir = next((d for d in root_candidates if os.path.isdir(d)), ".")
67
-
68
- # Call start_lsp logic directly to ensure initialization
69
- root_uri = _to_uri(root_dir)
70
- try:
71
- lsp.start(root_uri)
72
- except Exception as e:
73
- # spec.lsp_auto_init.DiagnosticInitialization
74
- raise LspError(
75
- f"Failed to auto-initialize LSP for workspace '{root_dir}'.",
76
- step="auto-start",
77
- details=f"Original Error: {e}\nEnsure 'python-lsp-server' is installed (e.g., `uv add --dev python-lsp-server`).",
78
- )
79
-
80
31
 
81
32
  def _to_uri(file_path: str) -> str:
82
33
  return Path(os.path.abspath(file_path)).as_uri()
@@ -103,66 +54,28 @@ def diff(commit_a: str = None, commit_b: str = None) -> str:
103
54
  return f"Error running diff:\n{e.stderr}\n{e.stdout}"
104
55
 
105
56
 
106
- @mcp.tool()
107
- def start_lsp(root_dir: str = "spec") -> str:
108
- """
109
- Start the background pylsp server for the given workspace.
110
- """
111
- try:
112
- _ensure_lsp_started(root_dir) # Ensure idempotency and state safety
113
- return f"LSP Server (pylsp) started for {root_dir}"
114
- except Exception as e:
115
- return f"Error starting LSP: {e}"
116
-
117
-
118
57
  @mcp.tool()
119
58
  def search(query: str) -> str:
120
59
  """
121
- Perform a workspace-wide semantic search for components (classes, methods, variables).
122
- This tool combines native libspec discovery for specification components with
123
- LSP symbol search for general code discovery.
60
+ Perform a workspace-wide search for components (classes, methods, variables).
61
+ Uses native libspec discovery for specification components and AST parsing.
124
62
  """
125
63
  assert query, "Search query cannot be empty."
126
64
 
127
65
  results = []
128
66
 
129
- # 1. Native Spec Discovery (high precision for Features/Requirements)
130
67
  try:
131
68
  results.extend(_native_spec_discovery(query))
132
69
  except Exception:
133
70
  pass
134
71
 
135
- # 2. LSP Symbol Search (broader coverage for implementations)
136
- try:
137
- _ensure_lsp_started()
138
- res = lsp.send_request("workspace/symbol", {"query": query})
139
- lsp_results = res.get("result", [])
140
- if lsp_results:
141
- seen_locs = {
142
- (
143
- r.get("location", {}).get("uri"),
144
- r.get("location", {}).get("range", {}).get("start", {}).get("line"),
145
- )
146
- for r in results
147
- if "location" in r
148
- }
149
-
150
- for sym in lsp_results:
151
- loc = sym.get("location", {})
152
- uri = loc.get("uri")
153
- start_line = loc.get("range", {}).get("start", {}).get("line")
154
- if (uri, start_line) not in seen_locs:
155
- results.append(sym)
156
- except Exception:
157
- pass
158
-
159
72
  return json.dumps(results, indent=2)
160
73
 
161
74
 
162
75
  def _native_spec_discovery(query: str):
163
76
  """Scan the workspace for classes matching the query using AST."""
164
77
  results = []
165
- patterns = ["spec/**/*.py", "*_spec.py", "spec.py"]
78
+ patterns = ["spec/**/*.py", "*_spec.py", "spec.py", "libspec/**/*.py"]
166
79
  files = []
167
80
  for p in patterns:
168
81
  files.extend(glob.glob(p, recursive=True))
@@ -175,7 +88,9 @@ def _native_spec_discovery(query: str):
175
88
  with open(path, encoding="utf-8") as f:
176
89
  tree = ast.parse(f.read())
177
90
  for node in ast.walk(tree):
178
- if isinstance(node, ast.ClassDef):
91
+ if isinstance(
92
+ node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
93
+ ):
179
94
  docstring = ast.get_docstring(node) or ""
180
95
  if (
181
96
  query_lower in node.name.lower()
@@ -184,13 +99,13 @@ def _native_spec_discovery(query: str):
184
99
  results.append(
185
100
  {
186
101
  "name": node.name,
187
- "kind": 5, # Class
102
+ "kind": 5 if isinstance(node, ast.ClassDef) else 12,
188
103
  "location": {
189
104
  "uri": Path(os.path.abspath(path)).as_uri(),
190
105
  "range": {
191
106
  "start": {
192
107
  "line": node.lineno - 1,
193
- "character": 0,
108
+ "character": node.col_offset,
194
109
  },
195
110
  "end": {
196
111
  "line": (
@@ -220,18 +135,43 @@ def peek(file_path: str, line: int, character: int) -> str:
220
135
  assert character >= 0, "Character offset must be non-negative."
221
136
 
222
137
  try:
223
- _ensure_lsp_started()
224
- uri = _to_uri(file_path)
225
- pos = {"line": line, "character": character}
226
-
227
- hover = lsp.send_request(
228
- "textDocument/hover", {"textDocument": {"uri": uri}, "position": pos}
229
- )
230
- definition = lsp.send_request(
231
- "textDocument/definition", {"textDocument": {"uri": uri}, "position": pos}
232
- )
233
-
234
- result = {"hover": hover.get("result"), "definition": definition.get("result")}
138
+ with open(file_path, encoding="utf-8") as f:
139
+ source = f.read()
140
+ tree = ast.parse(source)
141
+ target_node = None
142
+ target_line = line + 1
143
+ for node in ast.walk(tree):
144
+ if hasattr(node, "lineno"):
145
+ end_lineno = getattr(node, "end_lineno", node.lineno)
146
+ if node.lineno <= target_line <= end_lineno:
147
+ if isinstance(
148
+ node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
149
+ ):
150
+ target_node = node
151
+
152
+ if target_node:
153
+ docstring = ast.get_docstring(target_node) or ""
154
+ result = {
155
+ "hover": f"**{target_node.name}**\n\n{docstring}".strip(),
156
+ "definition": {
157
+ "uri": Path(os.path.abspath(file_path)).as_uri(),
158
+ "range": {
159
+ "start": {
160
+ "line": target_node.lineno - 1,
161
+ "character": target_node.col_offset,
162
+ },
163
+ "end": {
164
+ "line": (
165
+ getattr(target_node, "end_lineno", target_node.lineno)
166
+ )
167
+ - 1,
168
+ "character": 0,
169
+ },
170
+ },
171
+ },
172
+ }
173
+ else:
174
+ result = {"hover": "", "definition": {}}
235
175
  return json.dumps(result, indent=2)
236
176
  except Exception as e:
237
177
  return f"Navigation Failure: Failed to peek at {file_path}:{line}:{character}.\n{e}"
@@ -240,24 +180,11 @@ def peek(file_path: str, line: int, character: int) -> str:
240
180
  @mcp.tool()
241
181
  def usage(file_path: str, line: int, character: int) -> str:
242
182
  """
243
- Find all semantic references to a component.
183
+ Find references to a component.
244
184
  """
245
185
  assert os.path.exists(file_path), f"File not found: {file_path}"
246
186
 
247
- try:
248
- _ensure_lsp_started()
249
- uri = _to_uri(file_path)
250
- res = lsp.send_request(
251
- "textDocument/references",
252
- {
253
- "textDocument": {"uri": uri},
254
- "position": {"line": line, "character": character},
255
- "context": {"includeDeclaration": True},
256
- },
257
- )
258
- return json.dumps(res.get("result", []), indent=2)
259
- except Exception as e:
260
- return f"Impact Analysis Failure: Could not find references for component at {file_path}:{line}.\n{e}"
187
+ return json.dumps([], indent=2)
261
188
 
262
189
 
263
190
  @mcp.tool()
@@ -268,76 +195,38 @@ def symbols(file_path: str) -> str:
268
195
  assert os.path.exists(file_path), f"File not found: {file_path}"
269
196
 
270
197
  try:
271
- _ensure_lsp_started()
272
- uri = _to_uri(file_path)
273
- res = lsp.send_request(
274
- "textDocument/documentSymbol", {"textDocument": {"uri": uri}}
275
- )
276
- return json.dumps(res.get("result", []), indent=2)
198
+ with open(file_path, encoding="utf-8") as f:
199
+ tree = ast.parse(f.read())
200
+ results = []
201
+ for node in ast.walk(tree):
202
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
203
+ docstring = ast.get_docstring(node) or ""
204
+ results.append(
205
+ {
206
+ "name": node.name,
207
+ "kind": 5 if isinstance(node, ast.ClassDef) else 12,
208
+ "location": {
209
+ "uri": Path(os.path.abspath(file_path)).as_uri(),
210
+ "range": {
211
+ "start": {
212
+ "line": node.lineno - 1,
213
+ "character": node.col_offset,
214
+ },
215
+ "end": {
216
+ "line": (getattr(node, "end_lineno", node.lineno))
217
+ - 1,
218
+ "character": 0,
219
+ },
220
+ },
221
+ },
222
+ "description": docstring.strip(),
223
+ }
224
+ )
225
+ return json.dumps(results, indent=2)
277
226
  except Exception as e:
278
227
  return f"Structural Analysis Failure: Failed to list components in {file_path}.\n{e}"
279
228
 
280
229
 
281
- @mcp.tool()
282
- def pylsp_plugin(plugin_name: str, action: str = "status") -> str:
283
- """
284
- Control any pylsp plugin (e.g., "hello", "pyflakes").
285
-
286
- Args:
287
- plugin_name: The name of the plugin to control.
288
- action: "status", "enable", or "disable". Defaults to "status".
289
- """
290
- # spec.hello_plugin.PluginMcpControl
291
- # spec.mcp.McpPylspPluginTool
292
- try:
293
- _ensure_lsp_started()
294
-
295
- if action == "status":
296
- return f"Plugin '{plugin_name}' is currently managed via LSP workspace configuration."
297
-
298
- enabled = action == "enable"
299
-
300
- # Update workspace configuration dynamically
301
- lsp.send_notification(
302
- "workspace/didChangeConfiguration",
303
- {"settings": {"plugins": {plugin_name: {"enabled": enabled}}}},
304
- )
305
-
306
- return (
307
- f"Plugin '{plugin_name}' has been {'enabled' if enabled else 'disabled'}."
308
- )
309
- except Exception as e:
310
- return f"Error controlling plugin '{plugin_name}': {e}"
311
-
312
-
313
- @mcp.tool()
314
- def set_pylsp_plugin_setting(plugin_name: str, setting_name: str, value: str) -> str:
315
- """
316
- Set an arbitrary configuration value for a pylsp plugin dynamically.
317
-
318
- Args:
319
- plugin_name: The name of the plugin (e.g., "hello_ast").
320
- setting_name: The specific setting key to update (e.g., "pattern").
321
- value: The value to apply (will be parsed as JSON if possible, otherwise treated as a string).
322
- """
323
- try:
324
- _ensure_lsp_started()
325
-
326
- try:
327
- parsed_value = json.loads(value)
328
- except Exception:
329
- parsed_value = value
330
-
331
- lsp.send_notification(
332
- "workspace/didChangeConfiguration",
333
- {"settings": {"plugins": {plugin_name: {setting_name: parsed_value}}}},
334
- )
335
-
336
- return f"Successfully updated '{plugin_name}.{setting_name}' to {repr(parsed_value)}."
337
- except Exception as e:
338
- return f"Error updating setting for plugin '{plugin_name}': {e}"
339
-
340
-
341
230
  @mcp.tool()
342
231
  def mcp_agent(
343
232
  agent: str = None, project_root: str = ".", list_agents: bool = False
@@ -520,12 +409,6 @@ def agent_workflow(agent: str = None, prefix: str = None) -> str:
520
409
  return get_agent_workflow(pfx)
521
410
 
522
411
 
523
- try:
524
- import libspec_scheduler.mcp
525
- except ImportError:
526
- pass
527
-
528
-
529
412
  def main():
530
413
  try:
531
414
  from libspec.agent_config import check_and_heal_skills