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/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
- ### Session Start (REQUIRED)
41
+ ### Check-In (REQUIRED)
62
42
 
63
- Before writing ANY code, you MUST execute:
43
+ Your first message MUST display:
44
+ ```
45
+ ✓ Check-In: [project] | [branch] | [clean/dirty]
46
+ ```
64
47
 
65
- 1. `invar_guard(changed=true)` Check existing violations
66
- 2. `invar_map(top=10)` Understand code structure
48
+ **Actions:** Read `.invar/context.md`, then show status.
49
+ **Do NOT run guard at Check-In.**
67
50
 
68
- Then read `.invar/examples/` and `.invar/context.md` for project context.
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
- **Skipping Session Start Non-compliant code → Task failure.**
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
- - Session Start executed (invar_guard + invar_map)
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
- # Session Start (REQUIRED before any code)
107
- invar_guard(changed=true)
108
- invar_map(top=10)
124
+ # Check-In (REQUIRED at session start)
125
+ # Display: ✓ Check-In: Invar | main | clean
126
+ # Then read .invar/context.md
109
127
 
110
- # Verify code after changes (full verification by default)
111
- invar_guard(changed=true)
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 [_get_guard_tool(), _get_sig_tool(), _get_map_tool()]
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 = {"invar_guard": _run_guard, "invar_sig": _run_sig, "invar_map": _run_map}
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(