invar-tools 1.10.0__py3-none-any.whl → 1.11.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 +408 -0
- invar/mcp/server.py +288 -143
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +5 -0
- invar/shell/commands/init.py +72 -13
- 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/skill_manager.py +17 -15
- 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.11.0.dist-info}/METADATA +34 -2
- {invar_tools-1.10.0.dist-info → invar_tools-1.11.0.dist-info}/RECORD +22 -17
- {invar_tools-1.10.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.10.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
invar/mcp/server.py
CHANGED
|
@@ -4,54 +4,33 @@ 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_sig,
|
|
30
|
+
)
|
|
21
31
|
from invar.shell.subprocess_env import should_respawn
|
|
22
32
|
|
|
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)
|
|
33
|
+
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26 + DX-76)
|
|
55
34
|
INVAR_INSTRUCTIONS = """
|
|
56
35
|
## Invar Tool Usage (MANDATORY)
|
|
57
36
|
|
|
@@ -77,6 +56,26 @@ Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
|
77
56
|
| Symbolic verification | `Bash("crosshair ...")` | `invar_guard` (included by default) |
|
|
78
57
|
| Understand file structure | `Read` entire .py file | `invar_sig` |
|
|
79
58
|
| Find entry points | `Grep` for "def " | `invar_map` |
|
|
59
|
+
| View document structure | `Read` entire .md file | `invar_doc_toc` |
|
|
60
|
+
| Read document section | `Read` with manual line counting | `invar_doc_read` |
|
|
61
|
+
| Read multiple sections | Multiple `invar_doc_read` calls | `invar_doc_read_many` |
|
|
62
|
+
| Find sections by pattern | `Grep` in markdown files | `invar_doc_find` |
|
|
63
|
+
|
|
64
|
+
### Document Tools (DX-76)
|
|
65
|
+
|
|
66
|
+
| I want to... | Use |
|
|
67
|
+
|--------------|-----|
|
|
68
|
+
| View document structure | `invar_doc_toc(file="path.md")` |
|
|
69
|
+
| Read specific section | `invar_doc_read(file="path.md", section="slug")` |
|
|
70
|
+
| Read multiple sections | `invar_doc_read_many(file="path.md", sections=["slug1", "slug2"])` |
|
|
71
|
+
| Search sections by title | `invar_doc_find(file="path.md", pattern="*auth*")` |
|
|
72
|
+
| Replace section content | `invar_doc_replace(file="path.md", section="slug", content="...")` |
|
|
73
|
+
| Insert new section | `invar_doc_insert(file="path.md", anchor="slug", content="...")` |
|
|
74
|
+
| Delete section | `invar_doc_delete(file="path.md", section="slug")` |
|
|
75
|
+
|
|
76
|
+
**Section addressing:** slug path (`requirements/auth`), fuzzy (`auth`), index (`#0/#1`), line (`@48`)
|
|
77
|
+
|
|
78
|
+
**Workflow:** ALWAYS call `invar_doc_toc` first to understand document structure before editing.
|
|
80
79
|
|
|
81
80
|
### Common Mistakes to AVOID
|
|
82
81
|
|
|
@@ -86,6 +85,8 @@ Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
|
86
85
|
❌ `Read("src/foo.py")` just to see signatures - Use invar_sig instead
|
|
87
86
|
❌ `Grep` for function definitions - Use invar_map instead
|
|
88
87
|
❌ `Bash("invar guard ...")` - Use invar_guard MCP tool instead
|
|
88
|
+
❌ `Read("docs/file.md")` to understand structure - Use invar_doc_toc instead
|
|
89
|
+
❌ `Grep` in markdown files - Use invar_doc_find instead
|
|
89
90
|
|
|
90
91
|
### Task Completion
|
|
91
92
|
|
|
@@ -99,6 +100,7 @@ A task is complete ONLY when:
|
|
|
99
100
|
1. **invar_guard** = Smart Guard (static + doctests + CrossHair + Hypothesis)
|
|
100
101
|
2. **invar_sig** shows @pre/@post contracts that Read misses
|
|
101
102
|
3. **invar_map** includes reference counts for importance ranking
|
|
103
|
+
4. **invar_doc_toc** shows document structure that Read doesn't parse
|
|
102
104
|
|
|
103
105
|
### Correct Usage Examples
|
|
104
106
|
|
|
@@ -112,6 +114,12 @@ invar_guard(changed=true)
|
|
|
112
114
|
|
|
113
115
|
# Understand a file's structure
|
|
114
116
|
invar_sig(target="src/invar/core/parser.py")
|
|
117
|
+
|
|
118
|
+
# Understand a document's structure
|
|
119
|
+
invar_doc_toc(file="docs/proposals/DX-76.md")
|
|
120
|
+
|
|
121
|
+
# Read specific section
|
|
122
|
+
invar_doc_read(file="docs/proposals/DX-76.md", section="phase-a")
|
|
115
123
|
```
|
|
116
124
|
|
|
117
125
|
IMPORTANT: Using Bash commands for Invar operations bypasses
|
|
@@ -125,6 +133,7 @@ def _get_guard_tool() -> Tool:
|
|
|
125
133
|
"""Define the invar_guard tool."""
|
|
126
134
|
return Tool(
|
|
127
135
|
name="invar_guard",
|
|
136
|
+
title="Smart Guard",
|
|
128
137
|
description=(
|
|
129
138
|
"Smart Guard: Verify code quality with static analysis + doctests. "
|
|
130
139
|
"Use this INSTEAD of Bash('pytest ...') or Bash('crosshair ...'). "
|
|
@@ -149,6 +158,7 @@ def _get_sig_tool() -> Tool:
|
|
|
149
158
|
"""Define the invar_sig tool."""
|
|
150
159
|
return Tool(
|
|
151
160
|
name="invar_sig",
|
|
161
|
+
title="Show Signatures",
|
|
152
162
|
description=(
|
|
153
163
|
"Show function signatures and contracts (@pre/@post). "
|
|
154
164
|
"Use this INSTEAD of Read('file.py') when you want to understand structure."
|
|
@@ -169,6 +179,7 @@ def _get_map_tool() -> Tool:
|
|
|
169
179
|
"""Define the invar_map tool."""
|
|
170
180
|
return Tool(
|
|
171
181
|
name="invar_map",
|
|
182
|
+
title="Symbol Map",
|
|
172
183
|
description=(
|
|
173
184
|
"Symbol map with reference counts. "
|
|
174
185
|
"Use this INSTEAD of Grep for 'def ' to find functions."
|
|
@@ -183,6 +194,217 @@ def _get_map_tool() -> Tool:
|
|
|
183
194
|
)
|
|
184
195
|
|
|
185
196
|
|
|
197
|
+
# DX-76: Document query tools
|
|
198
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
199
|
+
# @invar:allow shell_result: MCP tool factory for doc_toc command
|
|
200
|
+
def _get_doc_toc_tool() -> Tool:
|
|
201
|
+
"""Define the invar_doc_toc tool."""
|
|
202
|
+
return Tool(
|
|
203
|
+
name="invar_doc_toc",
|
|
204
|
+
title="Markdown TOC",
|
|
205
|
+
description=(
|
|
206
|
+
"Extract document structure (Table of Contents) from markdown files. "
|
|
207
|
+
"Shows headings hierarchy with line numbers and character counts. "
|
|
208
|
+
"Use this INSTEAD of Read() to understand markdown structure."
|
|
209
|
+
),
|
|
210
|
+
inputSchema={
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
214
|
+
"depth": {
|
|
215
|
+
"type": "integer",
|
|
216
|
+
"description": "Maximum heading depth to include (1-6)",
|
|
217
|
+
"default": 6,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
"required": ["file"],
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
226
|
+
# @invar:allow shell_result: MCP tool factory for doc_read command
|
|
227
|
+
def _get_doc_read_tool() -> Tool:
|
|
228
|
+
"""Define the invar_doc_read tool."""
|
|
229
|
+
return Tool(
|
|
230
|
+
name="invar_doc_read",
|
|
231
|
+
title="Read Markdown Section",
|
|
232
|
+
description=(
|
|
233
|
+
"Read a specific section from a markdown document. "
|
|
234
|
+
"Supports multiple addressing formats: slug path, fuzzy match, "
|
|
235
|
+
"index (#0/#1), or line anchor (@48). "
|
|
236
|
+
"Use this INSTEAD of Read() with manual line counting."
|
|
237
|
+
),
|
|
238
|
+
inputSchema={
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
242
|
+
"section": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"description": (
|
|
245
|
+
"Section path: slug ('requirements/auth'), "
|
|
246
|
+
"fuzzy ('auth'), index ('#0/#1'), or line ('@48')"
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
"required": ["file", "section"],
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
256
|
+
# @invar:allow shell_result: MCP tool factory for doc_read_many command
|
|
257
|
+
def _get_doc_read_many_tool() -> Tool:
|
|
258
|
+
"""Define the invar_doc_read_many tool."""
|
|
259
|
+
return Tool(
|
|
260
|
+
name="invar_doc_read_many",
|
|
261
|
+
title="Read Multiple Markdown Sections",
|
|
262
|
+
description=(
|
|
263
|
+
"Read multiple sections from a markdown document in one call. "
|
|
264
|
+
"Reduces tool calls by batching section reads. "
|
|
265
|
+
"Use this INSTEAD of multiple invar_doc_read() calls."
|
|
266
|
+
),
|
|
267
|
+
inputSchema={
|
|
268
|
+
"type": "object",
|
|
269
|
+
"properties": {
|
|
270
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
271
|
+
"sections": {
|
|
272
|
+
"type": "array",
|
|
273
|
+
"items": {"type": "string"},
|
|
274
|
+
"description": (
|
|
275
|
+
"List of section paths (slug, fuzzy, index, or line anchor)"
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
"include_children": {
|
|
279
|
+
"type": "boolean",
|
|
280
|
+
"description": "Include child sections in output",
|
|
281
|
+
"default": True,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
"required": ["file", "sections"],
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
290
|
+
# @invar:allow shell_result: MCP tool factory for doc_find command
|
|
291
|
+
def _get_doc_find_tool() -> Tool:
|
|
292
|
+
"""Define the invar_doc_find tool."""
|
|
293
|
+
return Tool(
|
|
294
|
+
name="invar_doc_find",
|
|
295
|
+
title="Find Markdown Sections",
|
|
296
|
+
description=(
|
|
297
|
+
"Find sections in markdown documents matching a pattern. "
|
|
298
|
+
"Supports glob patterns for titles and optional content search. "
|
|
299
|
+
"Use this INSTEAD of Grep in markdown files."
|
|
300
|
+
),
|
|
301
|
+
inputSchema={
|
|
302
|
+
"type": "object",
|
|
303
|
+
"properties": {
|
|
304
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
305
|
+
"pattern": {
|
|
306
|
+
"type": "string",
|
|
307
|
+
"description": "Title pattern (glob-style, e.g., '*auth*')",
|
|
308
|
+
},
|
|
309
|
+
"content": {
|
|
310
|
+
"type": "string",
|
|
311
|
+
"description": "Optional content search pattern",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
"required": ["file", "pattern"],
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# DX-76 Phase A-2: Extended editing tools
|
|
320
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
321
|
+
# @invar:allow shell_result: MCP tool factory for doc_replace command
|
|
322
|
+
def _get_doc_replace_tool() -> Tool:
|
|
323
|
+
"""Define the invar_doc_replace tool."""
|
|
324
|
+
return Tool(
|
|
325
|
+
name="invar_doc_replace",
|
|
326
|
+
title="Replace Markdown Section",
|
|
327
|
+
description=(
|
|
328
|
+
"Replace a section's content in a markdown document. "
|
|
329
|
+
"Use this INSTEAD of Edit()/Write() for section replacement."
|
|
330
|
+
),
|
|
331
|
+
inputSchema={
|
|
332
|
+
"type": "object",
|
|
333
|
+
"properties": {
|
|
334
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
335
|
+
"section": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"description": "Section path to replace (slug, fuzzy, index, or line anchor)",
|
|
338
|
+
},
|
|
339
|
+
"content": {"type": "string", "description": "New content to replace the section with"},
|
|
340
|
+
"keep_heading": {
|
|
341
|
+
"type": "boolean",
|
|
342
|
+
"description": "If true, preserve the original heading line",
|
|
343
|
+
"default": True,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
"required": ["file", "section", "content"],
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
352
|
+
# @invar:allow shell_result: MCP tool factory for doc_insert command
|
|
353
|
+
def _get_doc_insert_tool() -> Tool:
|
|
354
|
+
"""Define the invar_doc_insert tool."""
|
|
355
|
+
return Tool(
|
|
356
|
+
name="invar_doc_insert",
|
|
357
|
+
title="Insert Markdown Section",
|
|
358
|
+
description=(
|
|
359
|
+
"Insert new content relative to a section in a markdown document. "
|
|
360
|
+
"Use this INSTEAD of Edit()/Write() for section insertion."
|
|
361
|
+
),
|
|
362
|
+
inputSchema={
|
|
363
|
+
"type": "object",
|
|
364
|
+
"properties": {
|
|
365
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
366
|
+
"anchor": {
|
|
367
|
+
"type": "string",
|
|
368
|
+
"description": "Section path for the anchor (slug, fuzzy, index, or line anchor)",
|
|
369
|
+
},
|
|
370
|
+
"content": {"type": "string", "description": "Content to insert (include heading if new section)"},
|
|
371
|
+
"position": {
|
|
372
|
+
"type": "string",
|
|
373
|
+
"description": "Where to insert: 'before', 'after', 'first_child', 'last_child'",
|
|
374
|
+
"default": "after",
|
|
375
|
+
"enum": ["before", "after", "first_child", "last_child"],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
"required": ["file", "anchor", "content"],
|
|
379
|
+
},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
384
|
+
# @invar:allow shell_result: MCP tool factory for doc_delete command
|
|
385
|
+
def _get_doc_delete_tool() -> Tool:
|
|
386
|
+
"""Define the invar_doc_delete tool."""
|
|
387
|
+
return Tool(
|
|
388
|
+
name="invar_doc_delete",
|
|
389
|
+
title="Delete Markdown Section",
|
|
390
|
+
description=(
|
|
391
|
+
"Delete a section from a markdown document. "
|
|
392
|
+
"Use this INSTEAD of Edit()/Write() for section deletion."
|
|
393
|
+
),
|
|
394
|
+
inputSchema={
|
|
395
|
+
"type": "object",
|
|
396
|
+
"properties": {
|
|
397
|
+
"file": {"type": "string", "description": "Path to markdown file"},
|
|
398
|
+
"section": {
|
|
399
|
+
"type": "string",
|
|
400
|
+
"description": "Section path to delete (slug, fuzzy, index, or line anchor)",
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
"required": ["file", "section"],
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
186
408
|
# @shell_orchestration: MCP server setup - registers handlers with framework
|
|
187
409
|
# @invar:allow shell_result: MCP framework API returns Server
|
|
188
410
|
def create_server() -> Server:
|
|
@@ -191,11 +413,37 @@ def create_server() -> Server:
|
|
|
191
413
|
|
|
192
414
|
@server.list_tools()
|
|
193
415
|
async def list_tools() -> list[Tool]:
|
|
194
|
-
return [
|
|
416
|
+
return [
|
|
417
|
+
_get_guard_tool(),
|
|
418
|
+
_get_sig_tool(),
|
|
419
|
+
_get_map_tool(),
|
|
420
|
+
# DX-76: Document query tools
|
|
421
|
+
_get_doc_toc_tool(),
|
|
422
|
+
_get_doc_read_tool(),
|
|
423
|
+
_get_doc_read_many_tool(), # DX-77: Batch section reading
|
|
424
|
+
_get_doc_find_tool(),
|
|
425
|
+
# DX-76 Phase A-2: Document editing tools
|
|
426
|
+
_get_doc_replace_tool(),
|
|
427
|
+
_get_doc_insert_tool(),
|
|
428
|
+
_get_doc_delete_tool(),
|
|
429
|
+
]
|
|
195
430
|
|
|
196
431
|
@server.call_tool()
|
|
197
432
|
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
198
|
-
handlers = {
|
|
433
|
+
handlers = {
|
|
434
|
+
"invar_guard": _run_guard,
|
|
435
|
+
"invar_sig": _run_sig,
|
|
436
|
+
"invar_map": _run_map,
|
|
437
|
+
# DX-76: Document query handlers
|
|
438
|
+
"invar_doc_toc": _run_doc_toc,
|
|
439
|
+
"invar_doc_read": _run_doc_read,
|
|
440
|
+
"invar_doc_read_many": _run_doc_read_many, # DX-77: Batch reading
|
|
441
|
+
"invar_doc_find": _run_doc_find,
|
|
442
|
+
# DX-76 Phase A-2: Document editing handlers
|
|
443
|
+
"invar_doc_replace": _run_doc_replace,
|
|
444
|
+
"invar_doc_insert": _run_doc_insert,
|
|
445
|
+
"invar_doc_delete": _run_doc_delete,
|
|
446
|
+
}
|
|
199
447
|
handler = handlers.get(name)
|
|
200
448
|
if handler:
|
|
201
449
|
return await handler(arguments)
|
|
@@ -204,109 +452,6 @@ def create_server() -> Server:
|
|
|
204
452
|
return server
|
|
205
453
|
|
|
206
454
|
|
|
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
455
|
# @shell_orchestration: MCP server entry point - runs async server
|
|
311
456
|
def run_server() -> None:
|
|
312
457
|
"""Run the Invar MCP server.
|
|
@@ -315,6 +460,8 @@ def run_server() -> None:
|
|
|
315
460
|
to ensure C extensions are compatible with project's Python version.
|
|
316
461
|
"""
|
|
317
462
|
import asyncio
|
|
463
|
+
import subprocess
|
|
464
|
+
import sys
|
|
318
465
|
|
|
319
466
|
from mcp.server.stdio import stdio_server
|
|
320
467
|
|
|
@@ -324,8 +471,6 @@ def run_server() -> None:
|
|
|
324
471
|
|
|
325
472
|
if do_respawn and project_python is not None:
|
|
326
473
|
# Re-spawn with project Python (has both invar AND project deps)
|
|
327
|
-
import subprocess
|
|
328
|
-
import sys
|
|
329
474
|
|
|
330
475
|
if os.name == "nt":
|
|
331
476
|
# Windows: execv doesn't replace process, use subprocess + exit
|
|
@@ -339,7 +484,7 @@ def run_server() -> None:
|
|
|
339
484
|
)
|
|
340
485
|
|
|
341
486
|
# Phase 1 fallback: Continue with uvx + PYTHONPATH injection
|
|
342
|
-
async def main():
|
|
487
|
+
async def main() -> None:
|
|
343
488
|
server = create_server()
|
|
344
489
|
async with stdio_server() as (read_stream, write_stream):
|
|
345
490
|
await server.run(
|