invar-tools 1.10.0__py3-none-any.whl → 1.12.0__py3-none-any.whl
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.
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/ts_sig_parser.py +6 -3
- invar/mcp/handlers.py +436 -0
- invar/mcp/server.py +351 -156
- invar/node_tools/ts-query.js +396 -0
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +29 -0
- invar/shell/commands/init.py +72 -13
- invar/shell/commands/perception.py +302 -6
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +15 -14
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +13 -10
- invar/shell/py_refs.py +156 -0
- invar/shell/skill_manager.py +17 -15
- invar/shell/ts_compiler.py +238 -0
- invar/templates/examples/typescript/patterns.md +193 -0
- invar/templates/skills/develop/SKILL.md.jinja +46 -0
- invar/templates/skills/review/SKILL.md.jinja +205 -493
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/METADATA +58 -8
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/RECORD +27 -18
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.12.0.dist-info}/licenses/NOTICE +0 -0
invar/mcp/server.py
CHANGED
|
@@ -4,70 +4,64 @@ Invar MCP Server implementation.
|
|
|
4
4
|
Exposes invar guard, sig, and map as first-class MCP tools.
|
|
5
5
|
Part of DX-16: Agent Tool Enforcement.
|
|
6
6
|
DX-52: Added Phase 2 smart re-spawn for project Python compatibility.
|
|
7
|
+
DX-76: Added doc_toc, doc_read, doc_find for structured document queries.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
|
-
import json
|
|
12
12
|
import os
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
13
|
from pathlib import Path
|
|
16
14
|
from typing import Any
|
|
17
15
|
|
|
18
16
|
from mcp.server import Server
|
|
19
17
|
from mcp.types import TextContent, Tool
|
|
20
18
|
|
|
19
|
+
from invar.mcp.handlers import (
|
|
20
|
+
_run_doc_delete,
|
|
21
|
+
_run_doc_find,
|
|
22
|
+
_run_doc_insert,
|
|
23
|
+
_run_doc_read,
|
|
24
|
+
_run_doc_read_many,
|
|
25
|
+
_run_doc_replace,
|
|
26
|
+
_run_doc_toc,
|
|
27
|
+
_run_guard,
|
|
28
|
+
_run_map,
|
|
29
|
+
_run_refs,
|
|
30
|
+
_run_sig,
|
|
31
|
+
)
|
|
21
32
|
from invar.shell.subprocess_env import should_respawn
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
# @invar:allow shell_result: Pure validation helper, no I/O, returns tuple not Result
|
|
25
|
-
# @shell_complexity: Security validation requires multiple checks
|
|
26
|
-
def _validate_path(path: str) -> tuple[bool, str]:
|
|
27
|
-
"""Validate path argument for safety.
|
|
28
|
-
|
|
29
|
-
Returns (is_valid, error_message).
|
|
30
|
-
Rejects paths that could be interpreted as shell commands or flags.
|
|
31
|
-
"""
|
|
32
|
-
if not path:
|
|
33
|
-
return True, "" # Empty path defaults to "." in handlers
|
|
34
|
-
|
|
35
|
-
# Reject if looks like a flag (starts with -)
|
|
36
|
-
if path.startswith("-"):
|
|
37
|
-
return False, f"Invalid path: cannot start with '-': {path}"
|
|
38
|
-
|
|
39
|
-
# Reject shell metacharacters that could cause issues
|
|
40
|
-
dangerous_chars = [";", "&", "|", "$", "`", "\n", "\r"]
|
|
41
|
-
for char in dangerous_chars:
|
|
42
|
-
if char in path:
|
|
43
|
-
return False, f"Invalid path: contains forbidden character: {char!r}"
|
|
44
|
-
|
|
45
|
-
# Try to resolve path - this catches malformed paths
|
|
46
|
-
try:
|
|
47
|
-
Path(path).resolve()
|
|
48
|
-
except (OSError, ValueError) as e:
|
|
49
|
-
return False, f"Invalid path: {e}"
|
|
50
|
-
|
|
51
|
-
return True, ""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26)
|
|
34
|
+
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26 + DX-76 + DX-78)
|
|
55
35
|
INVAR_INSTRUCTIONS = """
|
|
56
36
|
## Invar Tool Usage (MANDATORY)
|
|
57
37
|
|
|
58
38
|
This project uses Invar for all code verification and analysis.
|
|
59
39
|
The following rules are MANDATORY, not suggestions.
|
|
60
40
|
|
|
61
|
-
###
|
|
41
|
+
### Check-In (REQUIRED)
|
|
62
42
|
|
|
63
|
-
|
|
43
|
+
Your first message MUST display:
|
|
44
|
+
```
|
|
45
|
+
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
46
|
+
```
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
**Actions:** Read `.invar/context.md`, then show status.
|
|
49
|
+
**Do NOT run guard at Check-In.**
|
|
67
50
|
|
|
68
|
-
|
|
51
|
+
Run guard only when:
|
|
52
|
+
- Entering VALIDATE phase of USBV workflow
|
|
53
|
+
- User explicitly requests verification
|
|
54
|
+
- After making code changes
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
### Tool x Language Support
|
|
57
|
+
|
|
58
|
+
| Tool | Python | TypeScript | Notes |
|
|
59
|
+
|------|--------|------------|-------|
|
|
60
|
+
| `invar_guard` | ✅ Full | ⚠️ Partial | TS: tsc + eslint + vitest |
|
|
61
|
+
| `invar_sig` | ✅ Full | ✅ Full | TS: TS Compiler API |
|
|
62
|
+
| `invar_map` | ✅ Full | ✅ Full | TS: With reference counts |
|
|
63
|
+
| `invar_refs` | ✅ Full | ✅ Full | Cross-file reference finding |
|
|
64
|
+
| `invar_doc_*` | ✅ Full | ✅ Full | Language-agnostic |
|
|
71
65
|
|
|
72
66
|
### Tool Substitution Rules (ENFORCED)
|
|
73
67
|
|
|
@@ -77,6 +71,27 @@ Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
|
77
71
|
| Symbolic verification | `Bash("crosshair ...")` | `invar_guard` (included by default) |
|
|
78
72
|
| Understand file structure | `Read` entire .py file | `invar_sig` |
|
|
79
73
|
| Find entry points | `Grep` for "def " | `invar_map` |
|
|
74
|
+
| Find symbol references | Manual grep | `invar_refs` |
|
|
75
|
+
| View document structure | `Read` entire .md file | `invar_doc_toc` |
|
|
76
|
+
| Read document section | `Read` with manual line counting | `invar_doc_read` |
|
|
77
|
+
| Read multiple sections | Multiple `invar_doc_read` calls | `invar_doc_read_many` |
|
|
78
|
+
| Find sections by pattern | `Grep` in markdown files | `invar_doc_find` |
|
|
79
|
+
|
|
80
|
+
### Document Tools (DX-76)
|
|
81
|
+
|
|
82
|
+
| I want to... | Use |
|
|
83
|
+
|--------------|-----|
|
|
84
|
+
| View document structure | `invar_doc_toc(file="path.md")` |
|
|
85
|
+
| Read specific section | `invar_doc_read(file="path.md", section="slug")` |
|
|
86
|
+
| Read multiple sections | `invar_doc_read_many(file="path.md", sections=["slug1", "slug2"])` |
|
|
87
|
+
| Search sections by title | `invar_doc_find(file="path.md", pattern="*auth*")` |
|
|
88
|
+
| Replace section content | `invar_doc_replace(file="path.md", section="slug", content="...")` |
|
|
89
|
+
| Insert new section | `invar_doc_insert(file="path.md", anchor="slug", content="...")` |
|
|
90
|
+
| Delete section | `invar_doc_delete(file="path.md", section="slug")` |
|
|
91
|
+
|
|
92
|
+
**Section addressing:** slug path (`requirements/auth`), fuzzy (`auth`), index (`#0/#1`), line (`@48`)
|
|
93
|
+
|
|
94
|
+
**Workflow:** ALWAYS call `invar_doc_toc` first to understand document structure before editing.
|
|
80
95
|
|
|
81
96
|
### Common Mistakes to AVOID
|
|
82
97
|
|
|
@@ -86,12 +101,14 @@ Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
|
86
101
|
❌ `Read("src/foo.py")` just to see signatures - Use invar_sig instead
|
|
87
102
|
❌ `Grep` for function definitions - Use invar_map instead
|
|
88
103
|
❌ `Bash("invar guard ...")` - Use invar_guard MCP tool instead
|
|
104
|
+
❌ `Read("docs/file.md")` to understand structure - Use invar_doc_toc instead
|
|
105
|
+
❌ `Grep` in markdown files - Use invar_doc_find instead
|
|
89
106
|
|
|
90
107
|
### Task Completion
|
|
91
108
|
|
|
92
109
|
A task is complete ONLY when:
|
|
93
|
-
-
|
|
94
|
-
- Final `invar_guard` passed
|
|
110
|
+
- Check-In displayed at session start
|
|
111
|
+
- Final `invar_guard` passed (in VALIDATE phase)
|
|
95
112
|
- User requirement satisfied
|
|
96
113
|
|
|
97
114
|
### Why This Matters
|
|
@@ -99,19 +116,29 @@ A task is complete ONLY when:
|
|
|
99
116
|
1. **invar_guard** = Smart Guard (static + doctests + CrossHair + Hypothesis)
|
|
100
117
|
2. **invar_sig** shows @pre/@post contracts that Read misses
|
|
101
118
|
3. **invar_map** includes reference counts for importance ranking
|
|
119
|
+
4. **invar_doc_toc** shows document structure that Read doesn't parse
|
|
102
120
|
|
|
103
121
|
### Correct Usage Examples
|
|
104
122
|
|
|
105
123
|
```
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
# Check-In (REQUIRED at session start)
|
|
125
|
+
# Display: ✓ Check-In: Invar | main | clean
|
|
126
|
+
# Then read .invar/context.md
|
|
109
127
|
|
|
110
|
-
#
|
|
111
|
-
|
|
128
|
+
# Explore codebase (when needed)
|
|
129
|
+
invar_map(top=10)
|
|
112
130
|
|
|
113
131
|
# Understand a file's structure
|
|
114
132
|
invar_sig(target="src/invar/core/parser.py")
|
|
133
|
+
|
|
134
|
+
# Understand a document's structure
|
|
135
|
+
invar_doc_toc(file="docs/proposals/DX-76.md")
|
|
136
|
+
|
|
137
|
+
# Read specific section
|
|
138
|
+
invar_doc_read(file="docs/proposals/DX-76.md", section="phase-a")
|
|
139
|
+
|
|
140
|
+
# VALIDATE phase: Verify code after changes
|
|
141
|
+
invar_guard(changed=true)
|
|
115
142
|
```
|
|
116
143
|
|
|
117
144
|
IMPORTANT: Using Bash commands for Invar operations bypasses
|
|
@@ -125,6 +152,7 @@ def _get_guard_tool() -> Tool:
|
|
|
125
152
|
"""Define the invar_guard tool."""
|
|
126
153
|
return Tool(
|
|
127
154
|
name="invar_guard",
|
|
155
|
+
title="Smart Guard",
|
|
128
156
|
description=(
|
|
129
157
|
"Smart Guard: Verify code quality with static analysis + doctests. "
|
|
130
158
|
"Use this INSTEAD of Bash('pytest ...') or Bash('crosshair ...'). "
|
|
@@ -149,6 +177,7 @@ def _get_sig_tool() -> Tool:
|
|
|
149
177
|
"""Define the invar_sig tool."""
|
|
150
178
|
return Tool(
|
|
151
179
|
name="invar_sig",
|
|
180
|
+
title="Show Signatures",
|
|
152
181
|
description=(
|
|
153
182
|
"Show function signatures and contracts (@pre/@post). "
|
|
154
183
|
"Use this INSTEAD of Read('file.py') when you want to understand structure."
|
|
@@ -169,6 +198,7 @@ def _get_map_tool() -> Tool:
|
|
|
169
198
|
"""Define the invar_map tool."""
|
|
170
199
|
return Tool(
|
|
171
200
|
name="invar_map",
|
|
201
|
+
title="Symbol Map",
|
|
172
202
|
description=(
|
|
173
203
|
"Symbol map with reference counts. "
|
|
174
204
|
"Use this INSTEAD of Grep for 'def ' to find functions."
|
|
@@ -183,6 +213,246 @@ def _get_map_tool() -> Tool:
|
|
|
183
213
|
)
|
|
184
214
|
|
|
185
215
|
|
|
216
|
+
|
|
217
|
+
# @shell_orchestration: MCP tool factory - creates tool definition for framework
|
|
218
|
+
# @invar:allow shell_result: MCP tool factory for refs command
|
|
219
|
+
def _get_refs_tool() -> Tool:
|
|
220
|
+
"""Define the invar_refs tool.
|
|
221
|
+
|
|
222
|
+
DX-78: Cross-file reference finding.
|
|
223
|
+
"""
|
|
224
|
+
return Tool(
|
|
225
|
+
name="invar_refs",
|
|
226
|
+
title="Find References",
|
|
227
|
+
description=(
|
|
228
|
+
"Find all references to a symbol. "
|
|
229
|
+
"Supports Python (via jedi) and TypeScript (via TS Compiler API). "
|
|
230
|
+
"Use this to understand symbol usage across the codebase."
|
|
231
|
+
),
|
|
232
|
+
inputSchema={
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"target": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Target format: 'file.py::symbol' or 'file.ts::symbol'",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
"required": ["target"],
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# DX-76: Document query tools
|
|
246
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
247
|
+
# @invar:allow shell_result: MCP tool factory for doc_toc command
|
|
248
|
+
def _get_doc_toc_tool() -> Tool:
|
|
249
|
+
"""Define the invar_doc_toc tool."""
|
|
250
|
+
return Tool(
|
|
251
|
+
name="invar_doc_toc",
|
|
252
|
+
title="Markdown TOC",
|
|
253
|
+
description=(
|
|
254
|
+
"Extract document structure (Table of Contents) from markdown files. "
|
|
255
|
+
"Shows headings hierarchy with line numbers and character counts. "
|
|
256
|
+
"Use this INSTEAD of Read() to understand markdown structure."
|
|
257
|
+
),
|
|
258
|
+
inputSchema={
|
|
259
|
+
"type": "object",
|
|
260
|
+
"properties": {
|
|
261
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
262
|
+
"depth": {
|
|
263
|
+
"type": "integer",
|
|
264
|
+
"description": "Maximum heading depth to include (1-6)",
|
|
265
|
+
"default": 6,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
"required": ["file"],
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
274
|
+
# @invar:allow shell_result: MCP tool factory for doc_read command
|
|
275
|
+
def _get_doc_read_tool() -> Tool:
|
|
276
|
+
"""Define the invar_doc_read tool."""
|
|
277
|
+
return Tool(
|
|
278
|
+
name="invar_doc_read",
|
|
279
|
+
title="Read Markdown Section",
|
|
280
|
+
description=(
|
|
281
|
+
"Read a specific section from a markdown document. "
|
|
282
|
+
"Supports multiple addressing formats: slug path, fuzzy match, "
|
|
283
|
+
"index (#0/#1), or line anchor (@48). "
|
|
284
|
+
"Use this INSTEAD of Read() with manual line counting."
|
|
285
|
+
),
|
|
286
|
+
inputSchema={
|
|
287
|
+
"type": "object",
|
|
288
|
+
"properties": {
|
|
289
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
290
|
+
"section": {
|
|
291
|
+
"type": "string",
|
|
292
|
+
"description": (
|
|
293
|
+
"Section path: slug ('requirements/auth'), "
|
|
294
|
+
"fuzzy ('auth'), index ('#0/#1'), or line ('@48')"
|
|
295
|
+
),
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
"required": ["file", "section"],
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
304
|
+
# @invar:allow shell_result: MCP tool factory for doc_read_many command
|
|
305
|
+
def _get_doc_read_many_tool() -> Tool:
|
|
306
|
+
"""Define the invar_doc_read_many tool."""
|
|
307
|
+
return Tool(
|
|
308
|
+
name="invar_doc_read_many",
|
|
309
|
+
title="Read Multiple Markdown Sections",
|
|
310
|
+
description=(
|
|
311
|
+
"Read multiple sections from a markdown document in one call. "
|
|
312
|
+
"Reduces tool calls by batching section reads. "
|
|
313
|
+
"Use this INSTEAD of multiple invar_doc_read() calls."
|
|
314
|
+
),
|
|
315
|
+
inputSchema={
|
|
316
|
+
"type": "object",
|
|
317
|
+
"properties": {
|
|
318
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
319
|
+
"sections": {
|
|
320
|
+
"type": "array",
|
|
321
|
+
"items": {"type": "string"},
|
|
322
|
+
"description": (
|
|
323
|
+
"List of section paths (slug, fuzzy, index, or line anchor)"
|
|
324
|
+
),
|
|
325
|
+
},
|
|
326
|
+
"include_children": {
|
|
327
|
+
"type": "boolean",
|
|
328
|
+
"description": "Include child sections in output",
|
|
329
|
+
"default": True,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
"required": ["file", "sections"],
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
338
|
+
# @invar:allow shell_result: MCP tool factory for doc_find command
|
|
339
|
+
def _get_doc_find_tool() -> Tool:
|
|
340
|
+
"""Define the invar_doc_find tool."""
|
|
341
|
+
return Tool(
|
|
342
|
+
name="invar_doc_find",
|
|
343
|
+
title="Find Markdown Sections",
|
|
344
|
+
description=(
|
|
345
|
+
"Find sections in markdown documents matching a pattern. "
|
|
346
|
+
"Supports glob patterns for titles and optional content search. "
|
|
347
|
+
"Use this INSTEAD of Grep in markdown files."
|
|
348
|
+
),
|
|
349
|
+
inputSchema={
|
|
350
|
+
"type": "object",
|
|
351
|
+
"properties": {
|
|
352
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
353
|
+
"pattern": {
|
|
354
|
+
"type": "string",
|
|
355
|
+
"description": "Title pattern (glob-style, e.g., '*auth*')",
|
|
356
|
+
},
|
|
357
|
+
"content": {
|
|
358
|
+
"type": "string",
|
|
359
|
+
"description": "Optional content search pattern",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
"required": ["file", "pattern"],
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# DX-76 Phase A-2: Extended editing tools
|
|
368
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
369
|
+
# @invar:allow shell_result: MCP tool factory for doc_replace command
|
|
370
|
+
def _get_doc_replace_tool() -> Tool:
|
|
371
|
+
"""Define the invar_doc_replace tool."""
|
|
372
|
+
return Tool(
|
|
373
|
+
name="invar_doc_replace",
|
|
374
|
+
title="Replace Markdown Section",
|
|
375
|
+
description=(
|
|
376
|
+
"Replace a section's content in a markdown document. "
|
|
377
|
+
"Use this INSTEAD of Edit()/Write() for section replacement."
|
|
378
|
+
),
|
|
379
|
+
inputSchema={
|
|
380
|
+
"type": "object",
|
|
381
|
+
"properties": {
|
|
382
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
383
|
+
"section": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"description": "Section path to replace (slug, fuzzy, index, or line anchor)",
|
|
386
|
+
},
|
|
387
|
+
"content": {"type": "string", "description": "New content to replace the section with"},
|
|
388
|
+
"keep_heading": {
|
|
389
|
+
"type": "boolean",
|
|
390
|
+
"description": "If true, preserve the original heading line",
|
|
391
|
+
"default": True,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
"required": ["file", "section", "content"],
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
400
|
+
# @invar:allow shell_result: MCP tool factory for doc_insert command
|
|
401
|
+
def _get_doc_insert_tool() -> Tool:
|
|
402
|
+
"""Define the invar_doc_insert tool."""
|
|
403
|
+
return Tool(
|
|
404
|
+
name="invar_doc_insert",
|
|
405
|
+
title="Insert Markdown Section",
|
|
406
|
+
description=(
|
|
407
|
+
"Insert new content relative to a section in a markdown document. "
|
|
408
|
+
"Use this INSTEAD of Edit()/Write() for section insertion."
|
|
409
|
+
),
|
|
410
|
+
inputSchema={
|
|
411
|
+
"type": "object",
|
|
412
|
+
"properties": {
|
|
413
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
414
|
+
"anchor": {
|
|
415
|
+
"type": "string",
|
|
416
|
+
"description": "Section path for the anchor (slug, fuzzy, index, or line anchor)",
|
|
417
|
+
},
|
|
418
|
+
"content": {"type": "string", "description": "Content to insert (include heading if new section)"},
|
|
419
|
+
"position": {
|
|
420
|
+
"type": "string",
|
|
421
|
+
"description": "Where to insert: 'before', 'after', 'first_child', 'last_child'",
|
|
422
|
+
"default": "after",
|
|
423
|
+
"enum": ["before", "after", "first_child", "last_child"],
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
"required": ["file", "anchor", "content"],
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
432
|
+
# @invar:allow shell_result: MCP tool factory for doc_delete command
|
|
433
|
+
def _get_doc_delete_tool() -> Tool:
|
|
434
|
+
"""Define the invar_doc_delete tool."""
|
|
435
|
+
return Tool(
|
|
436
|
+
name="invar_doc_delete",
|
|
437
|
+
title="Delete Markdown Section",
|
|
438
|
+
description=(
|
|
439
|
+
"Delete a section from a markdown document. "
|
|
440
|
+
"Use this INSTEAD of Edit()/Write() for section deletion."
|
|
441
|
+
),
|
|
442
|
+
inputSchema={
|
|
443
|
+
"type": "object",
|
|
444
|
+
"properties": {
|
|
445
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
446
|
+
"section": {
|
|
447
|
+
"type": "string",
|
|
448
|
+
"description": "Section path to delete (slug, fuzzy, index, or line anchor)",
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
"required": ["file", "section"],
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
186
456
|
# @shell_orchestration: MCP server setup - registers handlers with framework
|
|
187
457
|
# @invar:allow shell_result: MCP framework API returns Server
|
|
188
458
|
def create_server() -> Server:
|
|
@@ -191,11 +461,39 @@ def create_server() -> Server:
|
|
|
191
461
|
|
|
192
462
|
@server.list_tools()
|
|
193
463
|
async def list_tools() -> list[Tool]:
|
|
194
|
-
return [
|
|
464
|
+
return [
|
|
465
|
+
_get_guard_tool(),
|
|
466
|
+
_get_sig_tool(),
|
|
467
|
+
_get_map_tool(),
|
|
468
|
+
_get_refs_tool(), # DX-78: Reference finding
|
|
469
|
+
# DX-76: Document query tools
|
|
470
|
+
_get_doc_toc_tool(),
|
|
471
|
+
_get_doc_read_tool(),
|
|
472
|
+
_get_doc_read_many_tool(), # DX-77: Batch section reading
|
|
473
|
+
_get_doc_find_tool(),
|
|
474
|
+
# DX-76 Phase A-2: Document editing tools
|
|
475
|
+
_get_doc_replace_tool(),
|
|
476
|
+
_get_doc_insert_tool(),
|
|
477
|
+
_get_doc_delete_tool(),
|
|
478
|
+
]
|
|
195
479
|
|
|
196
480
|
@server.call_tool()
|
|
197
481
|
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
198
|
-
handlers = {
|
|
482
|
+
handlers = {
|
|
483
|
+
"invar_guard": _run_guard,
|
|
484
|
+
"invar_sig": _run_sig,
|
|
485
|
+
"invar_map": _run_map,
|
|
486
|
+
"invar_refs": _run_refs, # DX-78: Reference finding
|
|
487
|
+
# DX-76: Document query handlers
|
|
488
|
+
"invar_doc_toc": _run_doc_toc,
|
|
489
|
+
"invar_doc_read": _run_doc_read,
|
|
490
|
+
"invar_doc_read_many": _run_doc_read_many, # DX-77: Batch reading
|
|
491
|
+
"invar_doc_find": _run_doc_find,
|
|
492
|
+
# DX-76 Phase A-2: Document editing handlers
|
|
493
|
+
"invar_doc_replace": _run_doc_replace,
|
|
494
|
+
"invar_doc_insert": _run_doc_insert,
|
|
495
|
+
"invar_doc_delete": _run_doc_delete,
|
|
496
|
+
}
|
|
199
497
|
handler = handlers.get(name)
|
|
200
498
|
if handler:
|
|
201
499
|
return await handler(arguments)
|
|
@@ -204,109 +502,6 @@ def create_server() -> Server:
|
|
|
204
502
|
return server
|
|
205
503
|
|
|
206
504
|
|
|
207
|
-
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
208
|
-
# @shell_complexity: Guard command with multiple optional flags
|
|
209
|
-
# @invar:allow shell_result: MCP handler for guard tool
|
|
210
|
-
async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
|
|
211
|
-
"""Run invar guard command."""
|
|
212
|
-
path = args.get("path", ".")
|
|
213
|
-
is_valid, error = _validate_path(path)
|
|
214
|
-
if not is_valid:
|
|
215
|
-
return [TextContent(type="text", text=f"Error: {error}")]
|
|
216
|
-
|
|
217
|
-
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "guard"]
|
|
218
|
-
cmd.append(path)
|
|
219
|
-
|
|
220
|
-
if args.get("changed", True):
|
|
221
|
-
cmd.append("--changed")
|
|
222
|
-
if args.get("strict", False):
|
|
223
|
-
cmd.append("--strict")
|
|
224
|
-
# DX-37: Optional coverage collection
|
|
225
|
-
if args.get("coverage", False):
|
|
226
|
-
cmd.append("--coverage")
|
|
227
|
-
# DX-63: Contract coverage check only
|
|
228
|
-
if args.get("contracts_only", False):
|
|
229
|
-
cmd.append("--contracts-only")
|
|
230
|
-
|
|
231
|
-
# DX-26: TTY auto-detection - MCP runs in non-TTY, so agent JSON output is automatic
|
|
232
|
-
# No explicit flag needed
|
|
233
|
-
|
|
234
|
-
return await _execute_command(cmd)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
238
|
-
# @invar:allow shell_result: MCP handler for sig tool
|
|
239
|
-
async def _run_sig(args: dict[str, Any]) -> list[TextContent]:
|
|
240
|
-
"""Run invar sig command."""
|
|
241
|
-
target = args.get("target", "")
|
|
242
|
-
if not target:
|
|
243
|
-
return [TextContent(type="text", text="Error: target is required")]
|
|
244
|
-
|
|
245
|
-
# Validate target (can be file path or file::symbol)
|
|
246
|
-
target_path = target.split("::")[0] if "::" in target else target
|
|
247
|
-
is_valid, error = _validate_path(target_path)
|
|
248
|
-
if not is_valid:
|
|
249
|
-
return [TextContent(type="text", text=f"Error: {error}")]
|
|
250
|
-
|
|
251
|
-
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "sig", target, "--json"]
|
|
252
|
-
return await _execute_command(cmd)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
256
|
-
# @invar:allow shell_result: MCP handler for map tool
|
|
257
|
-
async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
258
|
-
"""Run invar map command."""
|
|
259
|
-
path = args.get("path", ".")
|
|
260
|
-
is_valid, error = _validate_path(path)
|
|
261
|
-
if not is_valid:
|
|
262
|
-
return [TextContent(type="text", text=f"Error: {error}")]
|
|
263
|
-
|
|
264
|
-
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "map"]
|
|
265
|
-
cmd.append(path)
|
|
266
|
-
|
|
267
|
-
top = args.get("top", 10)
|
|
268
|
-
cmd.extend(["--top", str(top)])
|
|
269
|
-
|
|
270
|
-
cmd.append("--json")
|
|
271
|
-
return await _execute_command(cmd)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# @shell_complexity: Command execution with error handling branches
|
|
275
|
-
# @invar:allow shell_result: MCP subprocess wrapper utility
|
|
276
|
-
async def _execute_command(cmd: list[str], timeout: int = 600) -> list[TextContent]:
|
|
277
|
-
"""Execute a command and return the result.
|
|
278
|
-
|
|
279
|
-
Args:
|
|
280
|
-
cmd: Command to execute
|
|
281
|
-
timeout: Maximum time in seconds (default: 600, accommodates full Guard cycle)
|
|
282
|
-
"""
|
|
283
|
-
try:
|
|
284
|
-
result = subprocess.run(
|
|
285
|
-
cmd,
|
|
286
|
-
capture_output=True,
|
|
287
|
-
text=True,
|
|
288
|
-
timeout=timeout,
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
output = result.stdout
|
|
292
|
-
if result.stderr:
|
|
293
|
-
output += f"\n\nStderr:\n{result.stderr}"
|
|
294
|
-
|
|
295
|
-
# Try to parse as JSON for better formatting
|
|
296
|
-
try:
|
|
297
|
-
parsed = json.loads(result.stdout)
|
|
298
|
-
output = json.dumps(parsed, indent=2)
|
|
299
|
-
except json.JSONDecodeError:
|
|
300
|
-
pass
|
|
301
|
-
|
|
302
|
-
return [TextContent(type="text", text=output)]
|
|
303
|
-
|
|
304
|
-
except subprocess.TimeoutExpired:
|
|
305
|
-
return [TextContent(type="text", text=f"Error: Command timed out ({timeout}s)")]
|
|
306
|
-
except Exception as e:
|
|
307
|
-
return [TextContent(type="text", text=f"Error: {e}")]
|
|
308
|
-
|
|
309
|
-
|
|
310
505
|
# @shell_orchestration: MCP server entry point - runs async server
|
|
311
506
|
def run_server() -> None:
|
|
312
507
|
"""Run the Invar MCP server.
|
|
@@ -315,6 +510,8 @@ def run_server() -> None:
|
|
|
315
510
|
to ensure C extensions are compatible with project's Python version.
|
|
316
511
|
"""
|
|
317
512
|
import asyncio
|
|
513
|
+
import subprocess
|
|
514
|
+
import sys
|
|
318
515
|
|
|
319
516
|
from mcp.server.stdio import stdio_server
|
|
320
517
|
|
|
@@ -324,8 +521,6 @@ def run_server() -> None:
|
|
|
324
521
|
|
|
325
522
|
if do_respawn and project_python is not None:
|
|
326
523
|
# Re-spawn with project Python (has both invar AND project deps)
|
|
327
|
-
import subprocess
|
|
328
|
-
import sys
|
|
329
524
|
|
|
330
525
|
if os.name == "nt":
|
|
331
526
|
# Windows: execv doesn't replace process, use subprocess + exit
|
|
@@ -339,7 +534,7 @@ def run_server() -> None:
|
|
|
339
534
|
)
|
|
340
535
|
|
|
341
536
|
# Phase 1 fallback: Continue with uvx + PYTHONPATH injection
|
|
342
|
-
async def main():
|
|
537
|
+
async def main() -> None:
|
|
343
538
|
server = create_server()
|
|
344
539
|
async with stdio_server() as (read_stream, write_stream):
|
|
345
540
|
await server.run(
|