invar-tools 1.8.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.
Files changed (117) hide show
  1. invar/__init__.py +8 -0
  2. invar/core/doc_edit.py +187 -0
  3. invar/core/doc_parser.py +563 -0
  4. invar/core/language.py +88 -0
  5. invar/core/models.py +106 -0
  6. invar/core/patterns/detector.py +6 -1
  7. invar/core/patterns/p0_exhaustive.py +15 -3
  8. invar/core/patterns/p0_literal.py +15 -3
  9. invar/core/patterns/p0_newtype.py +15 -3
  10. invar/core/patterns/p0_nonempty.py +15 -3
  11. invar/core/patterns/p0_validation.py +15 -3
  12. invar/core/patterns/registry.py +5 -1
  13. invar/core/patterns/types.py +5 -1
  14. invar/core/property_gen.py +4 -0
  15. invar/core/rules.py +84 -18
  16. invar/core/sync_helpers.py +27 -1
  17. invar/core/ts_parsers.py +286 -0
  18. invar/core/ts_sig_parser.py +310 -0
  19. invar/mcp/handlers.py +408 -0
  20. invar/mcp/server.py +288 -143
  21. invar/node_tools/MANIFEST +7 -0
  22. invar/node_tools/__init__.py +51 -0
  23. invar/node_tools/fc-runner/cli.js +77 -0
  24. invar/node_tools/quick-check/cli.js +28 -0
  25. invar/node_tools/ts-analyzer/cli.js +480 -0
  26. invar/shell/claude_hooks.py +35 -12
  27. invar/shell/commands/doc.py +409 -0
  28. invar/shell/commands/guard.py +41 -1
  29. invar/shell/commands/init.py +154 -16
  30. invar/shell/commands/perception.py +157 -33
  31. invar/shell/commands/skill.py +187 -0
  32. invar/shell/commands/template_sync.py +65 -13
  33. invar/shell/commands/uninstall.py +60 -12
  34. invar/shell/commands/update.py +6 -14
  35. invar/shell/contract_coverage.py +1 -0
  36. invar/shell/doc_tools.py +459 -0
  37. invar/shell/fs.py +67 -13
  38. invar/shell/pi_hooks.py +6 -0
  39. invar/shell/prove/crosshair.py +3 -0
  40. invar/shell/prove/guard_ts.py +902 -0
  41. invar/shell/skill_manager.py +355 -0
  42. invar/shell/template_engine.py +28 -4
  43. invar/shell/templates.py +4 -4
  44. invar/templates/claude-md/python/critical-rules.md +33 -0
  45. invar/templates/claude-md/python/quick-reference.md +24 -0
  46. invar/templates/claude-md/typescript/critical-rules.md +40 -0
  47. invar/templates/claude-md/typescript/quick-reference.md +24 -0
  48. invar/templates/claude-md/universal/check-in.md +25 -0
  49. invar/templates/claude-md/universal/skills.md +73 -0
  50. invar/templates/claude-md/universal/workflow.md +55 -0
  51. invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
  52. invar/templates/config/AGENT.md.jinja +58 -0
  53. invar/templates/config/CLAUDE.md.jinja +16 -209
  54. invar/templates/config/context.md.jinja +19 -0
  55. invar/templates/examples/{README.md → python/README.md} +2 -0
  56. invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
  57. invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
  58. invar/templates/examples/python/core_shell.py +227 -0
  59. invar/templates/examples/python/functional.py +613 -0
  60. invar/templates/examples/typescript/README.md +31 -0
  61. invar/templates/examples/typescript/contracts.ts +163 -0
  62. invar/templates/examples/typescript/core_shell.ts +374 -0
  63. invar/templates/examples/typescript/functional.ts +601 -0
  64. invar/templates/examples/typescript/workflow.md +95 -0
  65. invar/templates/hooks/PostToolUse.sh.jinja +10 -1
  66. invar/templates/hooks/PreToolUse.sh.jinja +38 -0
  67. invar/templates/hooks/Stop.sh.jinja +1 -1
  68. invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
  69. invar/templates/hooks/pi/invar.ts.jinja +9 -0
  70. invar/templates/manifest.toml +7 -6
  71. invar/templates/onboard/assessment.md.jinja +214 -0
  72. invar/templates/onboard/patterns/python.md +347 -0
  73. invar/templates/onboard/patterns/typescript.md +452 -0
  74. invar/templates/onboard/roadmap.md.jinja +168 -0
  75. invar/templates/protocol/INVAR.md.jinja +51 -0
  76. invar/templates/protocol/python/architecture-examples.md +41 -0
  77. invar/templates/protocol/python/contracts-syntax.md +56 -0
  78. invar/templates/protocol/python/markers.md +44 -0
  79. invar/templates/protocol/python/tools.md +24 -0
  80. invar/templates/protocol/python/troubleshooting.md +38 -0
  81. invar/templates/protocol/typescript/architecture-examples.md +52 -0
  82. invar/templates/protocol/typescript/contracts-syntax.md +73 -0
  83. invar/templates/protocol/typescript/markers.md +48 -0
  84. invar/templates/protocol/typescript/tools.md +65 -0
  85. invar/templates/protocol/typescript/troubleshooting.md +104 -0
  86. invar/templates/protocol/universal/architecture.md +36 -0
  87. invar/templates/protocol/universal/completion.md +14 -0
  88. invar/templates/protocol/universal/contracts-concept.md +37 -0
  89. invar/templates/protocol/universal/header.md +17 -0
  90. invar/templates/protocol/universal/session.md +17 -0
  91. invar/templates/protocol/universal/six-laws.md +10 -0
  92. invar/templates/protocol/universal/usbv.md +14 -0
  93. invar/templates/protocol/universal/visible-workflow.md +25 -0
  94. invar/templates/skills/develop/SKILL.md.jinja +85 -3
  95. invar/templates/skills/extensions/_registry.yaml +93 -0
  96. invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
  97. invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
  98. invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
  99. invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
  100. invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
  101. invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
  102. invar/templates/skills/extensions/security/SKILL.md +382 -0
  103. invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
  104. invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
  105. invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
  106. invar/templates/skills/review/SKILL.md.jinja +220 -248
  107. {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
  108. invar_tools-1.11.0.dist-info/RECORD +178 -0
  109. invar/templates/examples/core_shell.py +0 -127
  110. invar/templates/protocol/INVAR.md +0 -310
  111. invar_tools-1.8.0.dist-info/RECORD +0 -116
  112. /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
  113. {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
  114. {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
  115. {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
  116. {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
  117. {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
invar/mcp/handlers.py ADDED
@@ -0,0 +1,408 @@
1
+ """
2
+ MCP tool handlers for Invar.
3
+
4
+ DX-76: Extracted from server.py to manage file size.
5
+ Contains all _run_* handler functions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, Literal
15
+
16
+ from mcp.types import TextContent
17
+ from returns.result import Success
18
+
19
+
20
+ # @invar:allow shell_result: Pure validation helper, no I/O, returns tuple not Result
21
+ # @shell_complexity: Security validation requires multiple checks
22
+ def _validate_path(path: str) -> tuple[bool, str]:
23
+ """Validate path argument for safety.
24
+
25
+ Returns (is_valid, error_message).
26
+ Rejects paths that could be interpreted as shell commands or flags.
27
+ """
28
+ if not path:
29
+ return True, "" # Empty path defaults to "." in handlers
30
+
31
+ # Reject if looks like a flag (starts with -)
32
+ if path.startswith("-"):
33
+ return False, f"Invalid path: cannot start with '-': {path}"
34
+
35
+ # Reject shell metacharacters that could cause issues
36
+ dangerous_chars = [";", "&", "|", "$", "`", "\n", "\r"]
37
+ for char in dangerous_chars:
38
+ if char in path:
39
+ return False, f"Invalid path: contains forbidden character: {char!r}"
40
+
41
+ # Try to resolve path - this catches malformed paths
42
+ try:
43
+ Path(path).resolve()
44
+ except (OSError, ValueError) as e:
45
+ return False, f"Invalid path: {e}"
46
+
47
+ return True, ""
48
+
49
+
50
+ # @shell_orchestration: MCP handler - subprocess is called inside
51
+ # @shell_complexity: Guard command with multiple optional flags
52
+ # @invar:allow shell_result: MCP handler for guard tool
53
+ async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
54
+ """Run invar guard command."""
55
+ path = args.get("path", ".")
56
+ is_valid, error = _validate_path(path)
57
+ if not is_valid:
58
+ return [TextContent(type="text", text=f"Error: {error}")]
59
+
60
+ cmd = [sys.executable, "-m", "invar.shell.commands.guard", "guard"]
61
+ cmd.append(path)
62
+
63
+ if args.get("changed", True):
64
+ cmd.append("--changed")
65
+ if args.get("strict", False):
66
+ cmd.append("--strict")
67
+ # DX-37: Optional coverage collection
68
+ if args.get("coverage", False):
69
+ cmd.append("--coverage")
70
+ # DX-63: Contract coverage check only
71
+ if args.get("contracts_only", False):
72
+ cmd.append("--contracts-only")
73
+
74
+ # DX-26: TTY auto-detection - MCP runs in non-TTY, so agent JSON output is automatic
75
+ # No explicit flag needed
76
+
77
+ return await _execute_command(cmd)
78
+
79
+
80
+ # @shell_orchestration: MCP handler - subprocess is called inside
81
+ # @invar:allow shell_result: MCP handler for sig tool
82
+ async def _run_sig(args: dict[str, Any]) -> list[TextContent]:
83
+ """Run invar sig command."""
84
+ target = args.get("target", "")
85
+ if not target:
86
+ return [TextContent(type="text", text="Error: target is required")]
87
+
88
+ # Validate target (can be file path or file::symbol)
89
+ target_path = target.split("::")[0] if "::" in target else target
90
+ is_valid, error = _validate_path(target_path)
91
+ if not is_valid:
92
+ return [TextContent(type="text", text=f"Error: {error}")]
93
+
94
+ cmd = [sys.executable, "-m", "invar.shell.commands.guard", "sig", target, "--json"]
95
+ return await _execute_command(cmd)
96
+
97
+
98
+ # @shell_orchestration: MCP handler - subprocess is called inside
99
+ # @invar:allow shell_result: MCP handler for map tool
100
+ async def _run_map(args: dict[str, Any]) -> list[TextContent]:
101
+ """Run invar map command."""
102
+ path = args.get("path", ".")
103
+ is_valid, error = _validate_path(path)
104
+ if not is_valid:
105
+ return [TextContent(type="text", text=f"Error: {error}")]
106
+
107
+ cmd = [sys.executable, "-m", "invar.shell.commands.guard", "map"]
108
+ cmd.append(path)
109
+
110
+ top = args.get("top", 10)
111
+ cmd.extend(["--top", str(top)])
112
+
113
+ cmd.append("--json")
114
+ return await _execute_command(cmd)
115
+
116
+
117
+ # DX-76: Document query handlers
118
+ # @shell_orchestration: MCP handler - calls shell layer directly
119
+ # @shell_complexity: MCP input validation + result handling
120
+ # @invar:allow shell_result: MCP handler for doc_toc tool
121
+ async def _run_doc_toc(args: dict[str, Any]) -> list[TextContent]:
122
+ """Run invar_doc_toc - extract document structure."""
123
+ from dataclasses import asdict
124
+
125
+ from invar.shell.doc_tools import read_toc
126
+
127
+ file_path = args.get("file", "")
128
+ if not file_path:
129
+ return [TextContent(type="text", text="Error: file is required")]
130
+
131
+ is_valid, error = _validate_path(file_path)
132
+ if not is_valid:
133
+ return [TextContent(type="text", text=f"Error: {error}")]
134
+
135
+ path = Path(file_path)
136
+ result = read_toc(path)
137
+
138
+ if isinstance(result, Success):
139
+ toc = result.unwrap()
140
+ # Convert to JSON-serializable format
141
+ output = {
142
+ "sections": [_section_to_dict(s) for s in toc.sections],
143
+ "frontmatter": asdict(toc.frontmatter) if toc.frontmatter else None,
144
+ }
145
+ return [TextContent(type="text", text=json.dumps(output, indent=2))]
146
+ else:
147
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
148
+
149
+
150
+ # @invar:allow shell_result: Pure data transformation, no I/O
151
+ # @shell_orchestration: Helper for MCP response formatting
152
+ def _section_to_dict(section: Any) -> dict[str, Any]:
153
+ """Convert Section to JSON-serializable dict (recursive)."""
154
+ return {
155
+ "title": section.title,
156
+ "slug": section.slug,
157
+ "level": section.level,
158
+ "line_start": section.line_start,
159
+ "line_end": section.line_end,
160
+ "char_count": section.char_count,
161
+ "path": section.path,
162
+ "children": [_section_to_dict(c) for c in section.children],
163
+ }
164
+
165
+
166
+ # @shell_orchestration: MCP handler - calls shell layer directly
167
+ # @shell_complexity: MCP input validation + result handling
168
+ # @invar:allow shell_result: MCP handler for doc_read tool
169
+ async def _run_doc_read(args: dict[str, Any]) -> list[TextContent]:
170
+ """Run invar_doc_read - read a specific section."""
171
+ from invar.shell.doc_tools import read_section
172
+
173
+ file_path = args.get("file", "")
174
+ section_path = args.get("section", "")
175
+
176
+ if not file_path:
177
+ return [TextContent(type="text", text="Error: file is required")]
178
+ if not section_path:
179
+ return [TextContent(type="text", text="Error: section is required")]
180
+
181
+ is_valid, error = _validate_path(file_path)
182
+ if not is_valid:
183
+ return [TextContent(type="text", text=f"Error: {error}")]
184
+
185
+ path = Path(file_path)
186
+ result = read_section(path, section_path)
187
+
188
+ if isinstance(result, Success):
189
+ content = result.unwrap()
190
+ output = {"path": section_path, "content": content}
191
+ return [TextContent(type="text", text=json.dumps(output, indent=2))]
192
+ else:
193
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
194
+
195
+
196
+ # @shell_complexity: Multiple arg validation branches + error handling
197
+ # @invar:allow shell_result: MCP handler for doc_read_many tool
198
+ async def _run_doc_read_many(args: dict[str, Any]) -> list[TextContent]:
199
+ """Run invar_doc_read_many - read multiple sections."""
200
+ from invar.shell.doc_tools import read_sections_batch
201
+
202
+ file_path = args.get("file", "")
203
+ sections = args.get("sections", [])
204
+ include_children = args.get("include_children", True)
205
+
206
+ if not file_path:
207
+ return [TextContent(type="text", text="Error: file is required")]
208
+ if not sections:
209
+ return [TextContent(type="text", text="Error: sections list is required")]
210
+ if not isinstance(sections, list):
211
+ return [TextContent(type="text", text="Error: sections must be a list")]
212
+
213
+ is_valid, error = _validate_path(file_path)
214
+ if not is_valid:
215
+ return [TextContent(type="text", text=f"Error: {error}")]
216
+
217
+ path = Path(file_path)
218
+ result = read_sections_batch(path, sections, include_children)
219
+
220
+ if isinstance(result, Success):
221
+ sections_data = result.unwrap()
222
+ return [TextContent(type="text", text=json.dumps(sections_data, indent=2))]
223
+ else:
224
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
225
+
226
+
227
+ # @shell_orchestration: MCP handler - calls shell layer directly
228
+ # @shell_complexity: MCP input validation + result handling
229
+ # @invar:allow shell_result: MCP handler for doc_find tool
230
+ async def _run_doc_find(args: dict[str, Any]) -> list[TextContent]:
231
+ """Run invar_doc_find - find sections matching pattern."""
232
+ from invar.shell.doc_tools import find_sections
233
+
234
+ file_path = args.get("file", "")
235
+ pattern = args.get("pattern", "")
236
+ content_pattern = args.get("content")
237
+
238
+ if not file_path:
239
+ return [TextContent(type="text", text="Error: file is required")]
240
+ if not pattern:
241
+ return [TextContent(type="text", text="Error: pattern is required")]
242
+
243
+ is_valid, error = _validate_path(file_path)
244
+ if not is_valid:
245
+ return [TextContent(type="text", text=f"Error: {error}")]
246
+
247
+ path = Path(file_path)
248
+ result = find_sections(path, pattern, content_pattern)
249
+
250
+ if isinstance(result, Success):
251
+ sections = result.unwrap()
252
+ output = {
253
+ "matches": [
254
+ {
255
+ "path": s.path,
256
+ "title": s.title,
257
+ "level": s.level,
258
+ "line_start": s.line_start,
259
+ "line_end": s.line_end,
260
+ "char_count": s.char_count,
261
+ }
262
+ for s in sections
263
+ ]
264
+ }
265
+ return [TextContent(type="text", text=json.dumps(output, indent=2))]
266
+ else:
267
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
268
+
269
+
270
+ # DX-76 Phase A-2: Extended editing handlers
271
+ # @shell_orchestration: MCP handler - calls shell layer directly
272
+ # @shell_complexity: MCP input validation + result handling
273
+ # @invar:allow shell_result: MCP handler for doc_replace tool
274
+ async def _run_doc_replace(args: dict[str, Any]) -> list[TextContent]:
275
+ """Run invar_doc_replace - replace section content."""
276
+ from invar.shell.doc_tools import replace_section_content
277
+
278
+ file_path = args.get("file", "")
279
+ section_path = args.get("section", "")
280
+ content = args.get("content", "")
281
+ keep_heading = args.get("keep_heading", True)
282
+
283
+ if not file_path:
284
+ return [TextContent(type="text", text="Error: file is required")]
285
+ if not section_path:
286
+ return [TextContent(type="text", text="Error: section is required")]
287
+ if not content:
288
+ return [TextContent(type="text", text="Error: content is required")]
289
+
290
+ is_valid, error = _validate_path(file_path)
291
+ if not is_valid:
292
+ return [TextContent(type="text", text=f"Error: {error}")]
293
+
294
+ path = Path(file_path)
295
+ result = replace_section_content(path, section_path, content, keep_heading)
296
+
297
+ if isinstance(result, Success):
298
+ info = result.unwrap()
299
+ output = {"success": True, **info}
300
+ return [TextContent(type="text", text=json.dumps(output, indent=2))]
301
+ else:
302
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
303
+
304
+
305
+ # @shell_orchestration: MCP handler - calls shell layer directly
306
+ # @shell_complexity: MCP input validation + result handling
307
+ # @invar:allow shell_result: MCP handler for doc_insert tool
308
+ async def _run_doc_insert(args: dict[str, Any]) -> list[TextContent]:
309
+ """Run invar_doc_insert - insert content relative to section."""
310
+ from invar.shell.doc_tools import insert_section_content
311
+
312
+ file_path = args.get("file", "")
313
+ anchor_path = args.get("anchor", "")
314
+ content = args.get("content", "")
315
+ position = args.get("position", "after")
316
+
317
+ if not file_path:
318
+ return [TextContent(type="text", text="Error: file is required")]
319
+ if not anchor_path:
320
+ return [TextContent(type="text", text="Error: anchor is required")]
321
+ if not content:
322
+ return [TextContent(type="text", text="Error: content is required")]
323
+
324
+ valid_positions = ("before", "after", "first_child", "last_child")
325
+ if position not in valid_positions:
326
+ return [TextContent(type="text", text=f"Error: position must be one of {valid_positions}")]
327
+
328
+ is_valid, error = _validate_path(file_path)
329
+ if not is_valid:
330
+ return [TextContent(type="text", text=f"Error: {error}")]
331
+
332
+ path = Path(file_path)
333
+ # Cast position to Literal type for type safety
334
+ pos: Literal["before", "after", "first_child", "last_child"] = position
335
+ result = insert_section_content(path, anchor_path, content, pos)
336
+
337
+ if isinstance(result, Success):
338
+ info = result.unwrap()
339
+ output = {"success": True, **info}
340
+ return [TextContent(type="text", text=json.dumps(output, indent=2))]
341
+ else:
342
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
343
+
344
+
345
+ # @shell_orchestration: MCP handler - calls shell layer directly
346
+ # @shell_complexity: MCP input validation + result handling
347
+ # @invar:allow shell_result: MCP handler for doc_delete tool
348
+ async def _run_doc_delete(args: dict[str, Any]) -> list[TextContent]:
349
+ """Run invar_doc_delete - delete a section."""
350
+ from invar.shell.doc_tools import delete_section_content
351
+
352
+ file_path = args.get("file", "")
353
+ section_path = args.get("section", "")
354
+
355
+ if not file_path:
356
+ return [TextContent(type="text", text="Error: file is required")]
357
+ if not section_path:
358
+ return [TextContent(type="text", text="Error: section is required")]
359
+
360
+ is_valid, error = _validate_path(file_path)
361
+ if not is_valid:
362
+ return [TextContent(type="text", text=f"Error: {error}")]
363
+
364
+ path = Path(file_path)
365
+ result = delete_section_content(path, section_path)
366
+
367
+ if isinstance(result, Success):
368
+ info = result.unwrap()
369
+ output = {"success": True, **info}
370
+ return [TextContent(type="text", text=json.dumps(output, indent=2))]
371
+ else:
372
+ return [TextContent(type="text", text=f"Error: {result.failure()}")]
373
+
374
+
375
+ # @shell_complexity: Command execution with error handling branches
376
+ # @invar:allow shell_result: MCP subprocess wrapper utility
377
+ async def _execute_command(cmd: list[str], timeout: int = 600) -> list[TextContent]:
378
+ """Execute a command and return the result.
379
+
380
+ Args:
381
+ cmd: Command to execute
382
+ timeout: Maximum time in seconds (default: 600, accommodates full Guard cycle)
383
+ """
384
+ try:
385
+ result = subprocess.run(
386
+ cmd,
387
+ capture_output=True,
388
+ text=True,
389
+ timeout=timeout,
390
+ )
391
+
392
+ output = result.stdout
393
+ if result.stderr:
394
+ output += f"\n\nStderr:\n{result.stderr}"
395
+
396
+ # Try to parse as JSON for better formatting
397
+ try:
398
+ parsed = json.loads(result.stdout)
399
+ output = json.dumps(parsed, indent=2)
400
+ except json.JSONDecodeError:
401
+ pass
402
+
403
+ return [TextContent(type="text", text=output)]
404
+
405
+ except subprocess.TimeoutExpired:
406
+ return [TextContent(type="text", text=f"Error: Command timed out ({timeout}s)")]
407
+ except Exception as e:
408
+ return [TextContent(type="text", text=f"Error: {e}")]