invar-tools 1.0.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 (64) hide show
  1. invar/__init__.py +68 -0
  2. invar/contracts.py +152 -0
  3. invar/core/__init__.py +8 -0
  4. invar/core/contracts.py +375 -0
  5. invar/core/extraction.py +172 -0
  6. invar/core/formatter.py +281 -0
  7. invar/core/hypothesis_strategies.py +454 -0
  8. invar/core/inspect.py +154 -0
  9. invar/core/lambda_helpers.py +190 -0
  10. invar/core/models.py +289 -0
  11. invar/core/must_use.py +172 -0
  12. invar/core/parser.py +276 -0
  13. invar/core/property_gen.py +383 -0
  14. invar/core/purity.py +369 -0
  15. invar/core/purity_heuristics.py +184 -0
  16. invar/core/references.py +180 -0
  17. invar/core/rule_meta.py +203 -0
  18. invar/core/rules.py +435 -0
  19. invar/core/strategies.py +267 -0
  20. invar/core/suggestions.py +324 -0
  21. invar/core/tautology.py +137 -0
  22. invar/core/timeout_inference.py +114 -0
  23. invar/core/utils.py +364 -0
  24. invar/decorators.py +94 -0
  25. invar/invariant.py +57 -0
  26. invar/mcp/__init__.py +10 -0
  27. invar/mcp/__main__.py +13 -0
  28. invar/mcp/server.py +251 -0
  29. invar/py.typed +0 -0
  30. invar/resource.py +99 -0
  31. invar/shell/__init__.py +8 -0
  32. invar/shell/cli.py +358 -0
  33. invar/shell/config.py +248 -0
  34. invar/shell/fs.py +112 -0
  35. invar/shell/git.py +85 -0
  36. invar/shell/guard_helpers.py +324 -0
  37. invar/shell/guard_output.py +235 -0
  38. invar/shell/init_cmd.py +289 -0
  39. invar/shell/mcp_config.py +171 -0
  40. invar/shell/perception.py +125 -0
  41. invar/shell/property_tests.py +227 -0
  42. invar/shell/prove.py +460 -0
  43. invar/shell/prove_cache.py +133 -0
  44. invar/shell/prove_fallback.py +183 -0
  45. invar/shell/templates.py +443 -0
  46. invar/shell/test_cmd.py +117 -0
  47. invar/shell/testing.py +297 -0
  48. invar/shell/update_cmd.py +191 -0
  49. invar/templates/CLAUDE.md.template +58 -0
  50. invar/templates/INVAR.md +134 -0
  51. invar/templates/__init__.py +1 -0
  52. invar/templates/aider.conf.yml.template +29 -0
  53. invar/templates/context.md.template +51 -0
  54. invar/templates/cursorrules.template +28 -0
  55. invar/templates/examples/README.md +21 -0
  56. invar/templates/examples/contracts.py +111 -0
  57. invar/templates/examples/core_shell.py +121 -0
  58. invar/templates/pre-commit-config.yaml.template +44 -0
  59. invar/templates/proposal.md.template +93 -0
  60. invar_tools-1.0.0.dist-info/METADATA +321 -0
  61. invar_tools-1.0.0.dist-info/RECORD +64 -0
  62. invar_tools-1.0.0.dist-info/WHEEL +4 -0
  63. invar_tools-1.0.0.dist-info/entry_points.txt +2 -0
  64. invar_tools-1.0.0.dist-info/licenses/LICENSE +21 -0
invar/core/utils.py ADDED
@@ -0,0 +1,364 @@
1
+ """
2
+ Pure utility functions.
3
+
4
+ These functions were moved from Shell to Core because they contain
5
+ no I/O operations - they are pure data transformations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import fnmatch
11
+ from typing import Any
12
+
13
+ from deal import post, pre
14
+
15
+ from invar.core.models import GuardReport, RuleConfig, RuleExclusion
16
+
17
+
18
+ @pre(lambda report, strict: isinstance(report, GuardReport))
19
+ @post(lambda result: result in (0, 1))
20
+ def get_exit_code(report: GuardReport, strict: bool) -> int:
21
+ """
22
+ Determine exit code based on report and strict mode.
23
+
24
+ Examples:
25
+ >>> from invar.core.models import GuardReport
26
+ >>> get_exit_code(GuardReport(files_checked=1), strict=False)
27
+ 0
28
+ >>> report = GuardReport(files_checked=1)
29
+ >>> report.errors = 1
30
+ >>> get_exit_code(report, strict=False)
31
+ 1
32
+ """
33
+ if report.errors > 0:
34
+ return 1
35
+ if strict and report.warnings > 0:
36
+ return 1
37
+ return 0
38
+
39
+
40
+ @pre(lambda data, source: isinstance(data, dict) and isinstance(source, str))
41
+ @post(lambda result: isinstance(result, dict))
42
+ def extract_guard_section(data: dict[str, Any], source: str) -> dict[str, Any]:
43
+ """
44
+ Extract guard config section based on source type.
45
+
46
+ Examples:
47
+ >>> extract_guard_section({"tool": {"invar": {"guard": {"x": 1}}}}, "pyproject")
48
+ {'x': 1}
49
+ >>> extract_guard_section({"guard": {"y": 2}}, "invar")
50
+ {'y': 2}
51
+ >>> extract_guard_section({}, "default")
52
+ {}
53
+ >>> extract_guard_section({"guard": 0}, "invar") # Non-dict value returns empty
54
+ {}
55
+ """
56
+ if source == "pyproject":
57
+ result = data.get("tool", {})
58
+ if not isinstance(result, dict):
59
+ return {}
60
+ result = result.get("invar", {})
61
+ if not isinstance(result, dict):
62
+ return {}
63
+ result = result.get("guard", {})
64
+ return result if isinstance(result, dict) else {}
65
+ # invar.toml and .invar/config.toml use [guard] directly
66
+ result = data.get("guard", {})
67
+ return result if isinstance(result, dict) else {}
68
+
69
+
70
+ @pre(lambda config, key: isinstance(config, dict) and isinstance(key, str))
71
+ @post(lambda result: result is None or isinstance(result, bool))
72
+ def _get_bool(config: dict[str, Any], key: str) -> bool | None:
73
+ """
74
+ Safely extract a boolean from config, returning None if invalid.
75
+
76
+ >>> _get_bool({"a": True}, "a")
77
+ True
78
+ >>> _get_bool({"a": "not bool"}, "a") is None
79
+ True
80
+ """
81
+ val = config.get(key)
82
+ if isinstance(val, bool):
83
+ return bool(val) # Convert to ensure real Python bool
84
+ return None
85
+
86
+
87
+ @pre(lambda config, key: isinstance(config, dict) and isinstance(key, str))
88
+ @post(lambda result: result is None or isinstance(result, int))
89
+ def _get_int(config: dict[str, Any], key: str) -> int | None:
90
+ """
91
+ Safely extract an integer from config, returning None if invalid.
92
+
93
+ >>> _get_int({"a": 42}, "a")
94
+ 42
95
+ >>> _get_int({"a": "not int"}, "a") is None
96
+ True
97
+ """
98
+ val = config.get(key)
99
+ if isinstance(val, int) and not isinstance(val, bool):
100
+ return int(val) # Convert to ensure real Python int
101
+ return None
102
+
103
+
104
+ @pre(lambda config, key: isinstance(config, dict) and isinstance(key, str))
105
+ @post(lambda result: result is None or isinstance(result, float))
106
+ def _get_float(config: dict[str, Any], key: str) -> float | None:
107
+ """
108
+ Safely extract a float from config, returning None if invalid.
109
+
110
+ >>> _get_float({"a": 3.14}, "a")
111
+ 3.14
112
+ >>> _get_float({"a": 10}, "a")
113
+ 10.0
114
+ >>> _get_float({"a": "not float"}, "a") is None
115
+ True
116
+ """
117
+ val = config.get(key)
118
+ if isinstance(val, (int, float)) and not isinstance(val, bool):
119
+ return float(val)
120
+ return None
121
+
122
+
123
+ @pre(lambda config, key: isinstance(config, dict) and isinstance(key, str))
124
+ @post(lambda result: result is None or isinstance(result, list))
125
+ def _get_str_list(config: dict[str, Any], key: str) -> list[str] | None:
126
+ """
127
+ Safely extract a list of strings from config.
128
+
129
+ >>> _get_str_list({"a": ["x", "y"]}, "a")
130
+ ['x', 'y']
131
+ >>> _get_str_list({"a": "not list"}, "a") is None
132
+ True
133
+ """
134
+ val = config.get(key)
135
+ if isinstance(val, (list, tuple)):
136
+ return [str(x) for x in val if isinstance(x, str)]
137
+ return None
138
+
139
+
140
+ @pre(lambda config: isinstance(config, dict))
141
+ @post(lambda result: result is None or isinstance(result, list))
142
+ def _parse_rule_exclusions(config: dict[str, Any]) -> list[RuleExclusion] | None:
143
+ """
144
+ Parse rule_exclusions from config.
145
+
146
+ >>> excl = _parse_rule_exclusions({"rule_exclusions": [{"pattern": "**/gen/**", "rules": ["*"]}]})
147
+ >>> excl[0].pattern
148
+ '**/gen/**'
149
+ >>> _parse_rule_exclusions({}) is None
150
+ True
151
+ """
152
+ raw = config.get("rule_exclusions")
153
+ if not isinstance(raw, list):
154
+ return None
155
+ exclusions = []
156
+ for excl in raw:
157
+ if isinstance(excl, dict) and "pattern" in excl and "rules" in excl:
158
+ pattern, rules = excl["pattern"], excl["rules"]
159
+ if isinstance(pattern, str) and isinstance(rules, list):
160
+ exclusions.append(RuleExclusion(pattern=str(pattern), rules=[str(r) for r in rules]))
161
+ return exclusions if exclusions else None
162
+
163
+
164
+ @pre(lambda config: isinstance(config, dict))
165
+ @post(lambda result: result is None or isinstance(result, dict))
166
+ def _parse_severity_overrides(config: dict[str, Any]) -> dict[str, str] | None:
167
+ """
168
+ Parse severity_overrides from config (merge with defaults).
169
+
170
+ >>> _parse_severity_overrides({"severity_overrides": {"foo": "off"}})
171
+ {'redundant_type_contract': 'off', 'foo': 'off'}
172
+ >>> _parse_severity_overrides({}) is None
173
+ True
174
+ """
175
+ raw = config.get("severity_overrides")
176
+ if not isinstance(raw, dict):
177
+ return None
178
+ defaults: dict[str, str] = {"redundant_type_contract": "off"}
179
+ for k, v in raw.items():
180
+ if isinstance(k, str) and isinstance(v, str):
181
+ defaults[str(k)] = str(v)
182
+ return defaults
183
+
184
+
185
+ @pre(lambda guard_config: isinstance(guard_config, dict))
186
+ @post(lambda result: isinstance(result, RuleConfig))
187
+ def parse_guard_config(guard_config: dict[str, Any]) -> RuleConfig:
188
+ """
189
+ Parse configuration from guard section.
190
+
191
+ Examples:
192
+ >>> cfg = parse_guard_config({"max_file_lines": 400})
193
+ >>> cfg.max_file_lines
194
+ 400
195
+ >>> cfg = parse_guard_config({})
196
+ >>> cfg.max_file_lines # Phase 9 P1: Default is now 500
197
+ 500
198
+ >>> cfg = parse_guard_config({"rule_exclusions": [{"pattern": "**/gen/**", "rules": ["*"]}]})
199
+ >>> len(cfg.rule_exclusions)
200
+ 1
201
+ >>> cfg.rule_exclusions[0].pattern
202
+ '**/gen/**'
203
+ >>> cfg = parse_guard_config({"use_code_lines": "invalid"}) # Invalid type ignored
204
+ >>> cfg.use_code_lines # Falls back to model default (False)
205
+ False
206
+ """
207
+ kwargs: dict[str, Any] = {}
208
+
209
+ # Int fields
210
+ for key in ("max_file_lines", "max_function_lines"):
211
+ if (val := _get_int(guard_config, key)) is not None:
212
+ kwargs[key] = val
213
+
214
+ # Bool fields
215
+ for key in ("require_contracts", "require_doctests", "strict_pure", "use_code_lines", "exclude_doctest_lines"):
216
+ if (val := _get_bool(guard_config, key)) is not None:
217
+ kwargs[key] = val
218
+
219
+ # Float fields
220
+ if (val := _get_float(guard_config, "size_warning_threshold")) is not None:
221
+ kwargs["size_warning_threshold"] = val
222
+
223
+ # List fields (convert to tuple for forbidden_imports)
224
+ if (val := _get_str_list(guard_config, "forbidden_imports")) is not None:
225
+ kwargs["forbidden_imports"] = tuple(val)
226
+ for key in ("purity_pure", "purity_impure"):
227
+ if (val := _get_str_list(guard_config, key)) is not None:
228
+ kwargs[key] = val
229
+
230
+ # Complex fields
231
+ if (val := _parse_rule_exclusions(guard_config)) is not None:
232
+ kwargs["rule_exclusions"] = val
233
+ if (val := _parse_severity_overrides(guard_config)) is not None:
234
+ kwargs["severity_overrides"] = val
235
+
236
+ try:
237
+ return RuleConfig(**kwargs)
238
+ except Exception:
239
+ return RuleConfig()
240
+
241
+
242
+ @pre(lambda file_path, patterns: isinstance(file_path, str) and isinstance(patterns, list))
243
+ def matches_pattern(file_path: str, patterns: list[str]) -> bool:
244
+ """
245
+ Check if a file path matches any of the glob patterns.
246
+
247
+ Examples:
248
+ >>> matches_pattern("src/domain/models.py", ["**/domain/**"])
249
+ True
250
+ >>> matches_pattern("src/api/views.py", ["**/domain/**"])
251
+ False
252
+ >>> matches_pattern("src/core/logic.py", ["src/core/**", "**/models/**"])
253
+ True
254
+ """
255
+ for pattern in patterns:
256
+ if fnmatch.fnmatch(file_path, pattern):
257
+ return True
258
+ # Also check with leading path component for ** patterns
259
+ if pattern.startswith("**/"):
260
+ # Match anywhere in path
261
+ if fnmatch.fnmatch(file_path, pattern[3:]):
262
+ return True
263
+ # Try matching each subpath
264
+ parts = file_path.split("/")
265
+ for i in range(len(parts)):
266
+ subpath = "/".join(parts[i:])
267
+ if fnmatch.fnmatch(subpath, pattern[3:]):
268
+ return True
269
+ return False
270
+
271
+
272
+ @pre(lambda file_path, prefixes: isinstance(file_path, str) and isinstance(prefixes, list))
273
+ def matches_path_prefix(file_path: str, prefixes: list[str]) -> bool:
274
+ """
275
+ Check if file_path starts with any of the given prefixes.
276
+
277
+ Examples:
278
+ >>> matches_path_prefix("src/core/logic.py", ["src/core", "src/domain"])
279
+ True
280
+ >>> matches_path_prefix("src/shell/cli.py", ["src/core", "src/domain"])
281
+ False
282
+ """
283
+ return any(file_path.startswith(p) for p in prefixes)
284
+
285
+
286
+ @post(lambda result: isinstance(result, bool))
287
+ def match_glob_pattern(file_path: str, pattern: str) -> bool:
288
+ """
289
+ Check if file path matches a glob pattern with ** support.
290
+
291
+ Uses fnmatch for single-segment wildcards, handles ** for multi-segment.
292
+
293
+ Examples:
294
+ >>> match_glob_pattern("src/generated/foo.py", "**/generated/**")
295
+ True
296
+ >>> match_glob_pattern("generated/foo.py", "**/generated/**")
297
+ True
298
+ >>> match_glob_pattern("src/core/calc.py", "**/generated/**")
299
+ False
300
+ >>> match_glob_pattern("src/core/data.py", "src/core/data.py")
301
+ True
302
+ >>> match_glob_pattern("src/core/calc.py", "src/core/*.py")
303
+ True
304
+ >>> match_glob_pattern("src/core/sub/calc.py", "src/core/*.py")
305
+ False
306
+ >>> match_glob_pattern("src/core/sub/calc.py", "src/core/**/*.py")
307
+ True
308
+ """
309
+ file_path = file_path.replace("\\", "/")
310
+ pattern = pattern.replace("\\", "/")
311
+ if "**" not in pattern:
312
+ if file_path.count("/") != pattern.count("/"):
313
+ return False
314
+ return fnmatch.fnmatch(file_path, pattern)
315
+ path_parts = file_path.split("/")
316
+ if pattern.startswith("**/") and pattern.endswith("/**"):
317
+ middle = pattern[3:-3]
318
+ if "/" not in middle and "*" not in middle:
319
+ return middle in path_parts[:-1]
320
+ if pattern.startswith("**/") and not pattern.endswith("/**"):
321
+ suffix = pattern[3:]
322
+ for i in range(len(path_parts)):
323
+ if fnmatch.fnmatch("/".join(path_parts[i:]), suffix):
324
+ return True
325
+ return False
326
+ if pattern.endswith("/**") and not pattern.startswith("**/"):
327
+ prefix = pattern[:-3]
328
+ return file_path.startswith(prefix + "/") or file_path == prefix
329
+ parts = pattern.split("**/")
330
+ if len(parts) == 2:
331
+ prefix, suffix = parts[0].rstrip("/"), parts[1].lstrip("/").rstrip("/**")
332
+ for i in range(len(path_parts) + 1):
333
+ head = "/".join(path_parts[:i]) if i > 0 else ""
334
+ tail = "/".join(path_parts[i:])
335
+ if (not prefix or fnmatch.fnmatch(head, prefix)) and (
336
+ not suffix or fnmatch.fnmatch(tail, suffix) or fnmatch.fnmatch(tail, "*/" + suffix)
337
+ ):
338
+ return True
339
+ return False
340
+
341
+
342
+ @pre(lambda file_path, config: isinstance(config, RuleConfig))
343
+ def get_excluded_rules(file_path: str, config: RuleConfig) -> set[str]:
344
+ """
345
+ Get the set of rules to exclude for a given file path.
346
+
347
+ Examples:
348
+ >>> from invar.core.models import RuleConfig, RuleExclusion
349
+ >>> excl = RuleExclusion(pattern="**/generated/**", rules=["*"])
350
+ >>> cfg = RuleConfig(rule_exclusions=[excl])
351
+ >>> get_excluded_rules("src/generated/foo.py", cfg)
352
+ {'*'}
353
+ >>> get_excluded_rules("src/core/calc.py", cfg)
354
+ set()
355
+ >>> excl2 = RuleExclusion(pattern="**/data/**", rules=["file_size"])
356
+ >>> cfg2 = RuleConfig(rule_exclusions=[excl, excl2])
357
+ >>> sorted(get_excluded_rules("src/data/big.py", cfg2))
358
+ ['file_size']
359
+ """
360
+ excluded: set[str] = set()
361
+ for exclusion in config.rule_exclusions:
362
+ if match_glob_pattern(file_path, exclusion.pattern):
363
+ excluded.update(exclusion.rules)
364
+ return excluded
invar/decorators.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ Invar contract decorators.
3
+
4
+ Provides decorators that extend deal's contract system for additional
5
+ static analysis by Guard.
6
+
7
+ DX-12-B: Added @strategy for custom Hypothesis strategies.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable
13
+ from typing import Any, TypeVar
14
+
15
+ F = TypeVar("F", bound=Callable)
16
+
17
+
18
+ def must_use(reason: str | None = None) -> Callable[[F], F]:
19
+ """
20
+ Mark a function's return value as must-use.
21
+
22
+ Guard will warn if calls to this function discard the return value.
23
+ Inspired by Move's lack of drop ability and Rust's #[must_use].
24
+
25
+ Args:
26
+ reason: Explanation of why the return value must be used.
27
+
28
+ Returns:
29
+ A decorator that marks the function.
30
+
31
+ >>> @must_use("Error must be handled")
32
+ ... def may_fail() -> int:
33
+ ... return 42
34
+ >>> may_fail.__invar_must_use__
35
+ 'Error must be handled'
36
+
37
+ >>> @must_use()
38
+ ... def important() -> str:
39
+ ... return "result"
40
+ >>> important.__invar_must_use__
41
+ 'Return value must be used'
42
+ """
43
+
44
+ def decorator(func: F) -> F:
45
+ func.__invar_must_use__ = reason or "Return value must be used" # type: ignore[attr-defined]
46
+ return func
47
+
48
+ return decorator
49
+
50
+
51
+ def strategy(**param_strategies: Any) -> Callable[[F], F]:
52
+ """
53
+ Specify custom Hypothesis strategies for function parameters.
54
+
55
+ DX-12-B: Escape hatch when automatic strategy inference fails or
56
+ when you need precise control over generated values.
57
+
58
+ Args:
59
+ **param_strategies: Mapping of parameter names to Hypothesis strategies.
60
+
61
+ Returns:
62
+ A decorator that attaches strategies to the function.
63
+
64
+ Examples:
65
+ >>> from hypothesis import strategies as st
66
+ >>> @strategy(x=st.floats(min_value=1e-10, max_value=1e10))
67
+ ... def sqrt(x: float) -> float:
68
+ ... return x ** 0.5
69
+ >>> hasattr(sqrt, '__invar_strategies__')
70
+ True
71
+ >>> 'x' in sqrt.__invar_strategies__
72
+ True
73
+
74
+ >>> # NumPy array with specific shape
75
+ >>> @strategy(arr="arrays(dtype=float64, shape=(10,))")
76
+ ... def normalize(arr):
77
+ ... return arr / arr.sum()
78
+ >>> 'arr' in normalize.__invar_strategies__
79
+ True
80
+
81
+ Note:
82
+ Strategies can be either:
83
+ - Hypothesis strategy objects (e.g., st.floats())
84
+ - String representations (e.g., "floats(min_value=0)")
85
+
86
+ String representations are useful when you don't want to import
87
+ Hypothesis at module load time.
88
+ """
89
+
90
+ def decorator(func: F) -> F:
91
+ func.__invar_strategies__ = param_strategies # type: ignore[attr-defined]
92
+ return func
93
+
94
+ return decorator
invar/invariant.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ Loop invariant support for Invar.
3
+
4
+ Provides runtime-checked loop invariants inspired by Dafny.
5
+ Checking controlled by INVAR_CHECK environment variable (default: ON).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+
12
+
13
+ class InvariantViolation(Exception):
14
+ """Raised when a loop invariant is violated."""
15
+
16
+ pass
17
+
18
+
19
+ # Read once at module load time - effectively a constant
20
+ _INVAR_CHECK = os.environ.get("INVAR_CHECK", "1") == "1"
21
+
22
+
23
+ def invariant(condition: bool, message: str = "") -> None:
24
+ """
25
+ Assert loop invariant. Checked at runtime when INVAR_CHECK=1.
26
+
27
+ Place at the START of loop body to check condition each iteration.
28
+ Invariants document what must remain true throughout loop execution.
29
+
30
+ Args:
31
+ condition: Boolean condition that must hold
32
+ message: Optional message describing the invariant
33
+
34
+ Raises:
35
+ InvariantViolation: When condition is False and INVAR_CHECK=1
36
+
37
+ Examples:
38
+ >>> invariant(True) # OK
39
+
40
+ >>> invariant(True, "x is positive") # OK with message
41
+
42
+ >>> try:
43
+ ... invariant(False, "x must be positive")
44
+ ... except InvariantViolation as e:
45
+ ... print(str(e))
46
+ Loop invariant violated: x must be positive
47
+
48
+ Typical usage in a binary search:
49
+
50
+ while lo < hi:
51
+ invariant(0 <= lo <= hi <= len(arr))
52
+ invariant(target not in arr[:lo]) # Already searched
53
+ ...
54
+ """
55
+ if _INVAR_CHECK and not condition:
56
+ msg = f"Loop invariant violated: {message}" if message else "Loop invariant violated"
57
+ raise InvariantViolation(msg)
invar/mcp/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Invar MCP Server.
3
+
4
+ Provides Invar tools as first-class MCP tools for AI agents.
5
+ Part of DX-16: Agent Tool Enforcement.
6
+ """
7
+
8
+ from invar.mcp.server import create_server, run_server
9
+
10
+ __all__ = ["create_server", "run_server"]
invar/mcp/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Entry point for running Invar MCP server.
3
+
4
+ Usage:
5
+ python -m invar.mcp
6
+
7
+ This starts the MCP server using stdio transport.
8
+ """
9
+
10
+ from invar.mcp.server import run_server
11
+
12
+ if __name__ == "__main__":
13
+ run_server()