code2schema 0.1.7__tar.gz → 0.1.9__tar.gz

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 (25) hide show
  1. {code2schema-0.1.7 → code2schema-0.1.9}/PKG-INFO +11 -9
  2. {code2schema-0.1.7 → code2schema-0.1.9}/README.md +8 -5
  3. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/__init__.py +1 -1
  4. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/cqrs.py +1 -3
  5. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/events.py +7 -11
  6. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/graph.py +11 -2
  7. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/cli.py +3 -6
  8. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/core/extractor.py +3 -9
  9. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/core/models.py +1 -3
  10. code2schema-0.1.9/code2schema/mcp_server.py +305 -0
  11. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/PKG-INFO +11 -9
  12. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/SOURCES.txt +3 -1
  13. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/requires.txt +2 -1
  14. {code2schema-0.1.7 → code2schema-0.1.9}/pyproject.toml +14 -7
  15. code2schema-0.1.9/tests/test_event_handlers.py +30 -0
  16. {code2schema-0.1.7 → code2schema-0.1.9}/LICENSE +0 -0
  17. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/__init__.py +0 -0
  18. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/codegen/__init__.py +0 -0
  19. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/codegen/visualizer.py +0 -0
  20. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/core/__init__.py +0 -0
  21. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/dependency_links.txt +0 -0
  22. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/entry_points.txt +0 -0
  23. {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/top_level.txt +0 -0
  24. {code2schema-0.1.7 → code2schema-0.1.9}/setup.cfg +0 -0
  25. {code2schema-0.1.7 → code2schema-0.1.9}/tests/test_code2schema.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2schema
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema
5
- License: Apache-2.0
5
+ License-Expression: Apache-2.0
6
6
  Keywords: ast,cqrs,static-analysis,code-quality,schema
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Programming Language :: Python :: 3
@@ -24,9 +24,8 @@ Requires-Dist: pytest>=8; extra == "dev"
24
24
  Requires-Dist: pytest-cov; extra == "dev"
25
25
  Requires-Dist: black; extra == "dev"
26
26
  Requires-Dist: ruff; extra == "dev"
27
- Requires-Dist: goal>=2.1.0; extra == "dev"
28
27
  Requires-Dist: costs>=0.1.20; extra == "dev"
29
- Requires-Dist: pfix>=0.1.60; extra == "dev"
28
+ Requires-Dist: pfix>=0.1.60; python_version >= "3.10" and extra == "dev"
30
29
  Dynamic: license-file
31
30
 
32
31
  # code2schema
@@ -34,13 +33,13 @@ Dynamic: license-file
34
33
 
35
34
  ## AI Cost Tracking
36
35
 
37
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.7-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
38
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-4.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.77-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
39
38
 
40
- - 🤖 **LLM usage:** $0.7500 (5 commits)
41
- - 👤 **Human dev:** ~$430 (4.3h @ $100/h, 30min dedup)
39
+ - 🤖 **LLM usage:** $6.7712 (15 commits)
40
+ - 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
42
41
 
43
- Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
+ Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
44
43
 
45
44
  ---
46
45
 
@@ -270,3 +269,6 @@ MIT
270
269
  ## License
271
270
 
272
271
  Licensed under Apache-2.0.
272
+ ## Dependency maintenance
273
+
274
+ See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
@@ -3,13 +3,13 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.7-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-4.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.77-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $0.7500 (5 commits)
10
- - 👤 **Human dev:** ~$430 (4.3h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $6.7712 (15 commits)
10
+ - 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
@@ -239,3 +239,6 @@ MIT
239
239
  ## License
240
240
 
241
241
  Licensed under Apache-2.0.
242
+ ## Dependency maintenance
243
+
244
+ See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
@@ -13,4 +13,4 @@ __all__ = [
13
13
  "extract_module",
14
14
  "analyze",
15
15
  ]
16
- __version__ = "0.1.7"
16
+ __version__ = "0.1.9"
@@ -92,9 +92,7 @@ def build_workflows(modules: List[ModuleIR]) -> List[WorkflowIR]:
92
92
  workflow = WorkflowIR(
93
93
  name=f"workflow_{f.name}",
94
94
  entry=f.qualified_name,
95
- steps=[
96
- WorkflowStep(callee=c, is_async=f.is_async) for c in f.calls
97
- ],
95
+ steps=[WorkflowStep(callee=c, is_async=f.is_async) for c in f.calls],
98
96
  )
99
97
  workflows.append(workflow)
100
98
  return workflows
@@ -62,9 +62,7 @@ class EventModel:
62
62
  lines.append("\nDomain Events:")
63
63
  for ev in self.events[:15]:
64
64
  handlers = ", ".join(ev.handled_by[:3]) or "—"
65
- lines.append(
66
- f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]"
67
- )
65
+ lines.append(f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]")
68
66
  if self.commands:
69
67
  lines.append("\nCommand Handlers (top 10):")
70
68
  for cmd in self.commands[:10]:
@@ -91,12 +89,14 @@ def _find_emitters(modules: List[ModuleIR]) -> List[DomainEvent]:
91
89
 
92
90
  def _find_handlers(events: List[DomainEvent], modules: List[ModuleIR]) -> None:
93
91
  """Krok 2: dopasuj handlery do zdarzeń (mutuje events in-place)."""
92
+ # Event keywords are invariant for this pass; normalize each only once.
93
+ keywords = [(ev, ev.name.lower().replace("event", "").strip()) for ev in events]
94
94
  for mod in modules:
95
95
  for f in mod.functions:
96
96
  if _EVENT_HANDLER_PATTERNS.search(f.name):
97
- for ev in events:
98
- keyword = ev.name.lower().replace("event", "").strip()
99
- if keyword and keyword in f.name.lower():
97
+ handler_name = f.name.lower()
98
+ for ev, keyword in keywords:
99
+ if keyword and keyword in handler_name:
100
100
  ev.handled_by.append(f.qualified_name)
101
101
 
102
102
 
@@ -106,11 +106,7 @@ def _find_command_handlers(modules: List[ModuleIR]) -> List[CommandHandler]:
106
106
  for mod in modules:
107
107
  for f in mod.functions:
108
108
  if f.role == CQRSRole.COMMAND or _AGGREGATE_PATTERNS.search(f.name):
109
- emits = [
110
- _derive_event_name(c)
111
- for c in f.calls
112
- if _EVENT_EMIT_PATTERNS.search(c)
113
- ]
109
+ emits = [_derive_event_name(c) for c in f.calls if _EVENT_EMIT_PATTERNS.search(c)]
114
110
  if emits or f.role == CQRSRole.COMMAND:
115
111
  commands.append(
116
112
  CommandHandler(
@@ -54,10 +54,19 @@ def build_rich_graph(schema: SchemaIR) -> nx.DiGraph:
54
54
 
55
55
 
56
56
  def centrality_report(G: nx.DiGraph, top_n: int = 10) -> List[Tuple[str, float]]:
57
- """PageRank — funkcje najważniejsze architektonicznie."""
57
+ """PageRank — funkcje najważniejsze architektonicznie.
58
+
59
+ Falls back to in-degree ranking when scipy is unavailable
60
+ (networkx pagerank delegates to scipy on some versions).
61
+ """
58
62
  if G.number_of_edges() == 0:
59
63
  return []
60
- pr = nx.pagerank(G, alpha=0.85)
64
+ try:
65
+ pr = nx.pagerank(G, alpha=0.85)
66
+ except ModuleNotFoundError:
67
+ pr = dict(G.in_degree())
68
+ total = sum(pr.values()) or 1
69
+ pr = {node: deg / total for node, deg in pr.items()}
61
70
  return sorted(pr.items(), key=lambda x: x[1], reverse=True)[:top_n]
62
71
 
63
72
 
@@ -91,9 +91,7 @@ def _build_parser() -> argparse.ArgumentParser:
91
91
  parser.add_argument("--no-rules", action="store_true")
92
92
  parser.add_argument("--exclude", nargs="*", default=[])
93
93
  parser.add_argument("-q", "--quiet", action="store_true")
94
- parser.add_argument(
95
- "-V", "--version", action="version", version=f"%(prog)s {__version__}"
96
- )
94
+ parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
97
95
  return parser
98
96
 
99
97
 
@@ -209,7 +207,7 @@ def _print_summary(modules, schema, G, t0, out_path, proto_path, md_path, html_p
209
207
  if html_path:
210
208
  outputs.append(f" → {html_path.name}")
211
209
  print(
212
- f"\n✅ Gotowe ({time.perf_counter()-t0:.2f}s)\n"
210
+ f"\n✅ Gotowe ({time.perf_counter() - t0:.2f}s)\n"
213
211
  f" Modules : {len(modules)}\n"
214
212
  f" Functions: {len(funcs)}\n"
215
213
  f" Queries : {len(schema.queries())}\n"
@@ -217,8 +215,7 @@ def _print_summary(modules, schema, G, t0, out_path, proto_path, md_path, html_p
217
215
  f" Orchest. : {len(schema.orchestrators())}\n"
218
216
  f" Workflows: {len(schema.workflows)}\n"
219
217
  f" Rules : {len(schema.rules)}\n"
220
- f" Graph : {G.number_of_nodes()}N / {G.number_of_edges()}E\n"
221
- + "\n".join(outputs)
218
+ f" Graph : {G.number_of_nodes()}N / {G.number_of_edges()}E\n" + "\n".join(outputs)
222
219
  )
223
220
 
224
221
 
@@ -87,9 +87,7 @@ class _FunctionVisitor(ast.NodeVisitor):
87
87
  self._process_func(node, is_async=True)
88
88
  self.generic_visit(node)
89
89
 
90
- def _process_func(
91
- self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool
92
- ) -> None:
90
+ def _process_func(self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool) -> None:
93
91
  calls = self._collect_calls(node)
94
92
  side_effects = self._detect_side_effects(node, calls)
95
93
  docstring = ast.get_docstring(node)
@@ -100,9 +98,7 @@ class _FunctionVisitor(ast.NodeVisitor):
100
98
  calls=list(dict.fromkeys(calls)), # deduplicate, preserve order
101
99
  fan_out=len(set(calls)),
102
100
  side_effects=side_effects,
103
- lines=(
104
- node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0
105
- ),
101
+ lines=(node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0),
106
102
  is_async=is_async,
107
103
  docstring=docstring,
108
104
  )
@@ -162,9 +158,7 @@ def extract_module(path: Path) -> ModuleIR | None:
162
158
  imports = [
163
159
  alias.name
164
160
  for node in ast.walk(tree)
165
- for alias in (
166
- node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else []
167
- )
161
+ for alias in (node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else [])
168
162
  ]
169
163
 
170
164
  return ModuleIR(
@@ -81,9 +81,7 @@ class RuleIR(BaseModel):
81
81
  class SchemaIR(BaseModel):
82
82
  """Korzeń modelu semantycznego całego projektu."""
83
83
 
84
- system: dict = Field(
85
- default_factory=lambda: {"type": "code2schema", "version": "0.1"}
86
- )
84
+ system: dict = Field(default_factory=lambda: {"type": "code2schema", "version": "0.1"})
87
85
  modules: List[ModuleIR] = Field(default_factory=list)
88
86
  workflows: List[WorkflowIR] = Field(default_factory=list)
89
87
  rules: List[RuleIR] = Field(default_factory=list)
@@ -0,0 +1,305 @@
1
+ """code2schema MCP server — dependency-free stdio JSON-RPC.
2
+
3
+ Adopts wellmanifest/poa (typed tools, closed inputSchema, fail-closed
4
+ dispatch) and wellmanifest/logs (append-only hash-chained JSONL event
5
+ stream under $XDG_STATE_HOME/code2schema/mcp-events.jsonl, overridable
6
+ with CODE2SCHEMA_MCP_EVENT_LOG).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import sys
15
+ import time
16
+ import uuid
17
+ from pathlib import Path
18
+ from typing import Any, Callable
19
+
20
+ _PROTOCOL_VERSION = "2024-11-05"
21
+ _NOTIFICATIONS = frozenset({"notifications/initialized", "notifications/cancelled"})
22
+
23
+ SERVER_NAME = "code2schema"
24
+ try:
25
+ from importlib.metadata import version as _pkg_version
26
+
27
+ SERVER_VERSION = _pkg_version("code2schema")
28
+ except Exception:
29
+ SERVER_VERSION = "0.0.0"
30
+
31
+ _ZERO_HASH = "0" * 64
32
+
33
+
34
+ def _event_log_path() -> Path:
35
+ override = os.environ.get("CODE2SCHEMA_MCP_EVENT_LOG")
36
+ if override:
37
+ return Path(override)
38
+ state_home = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
39
+ return state_home / "code2schema" / "mcp-events.jsonl"
40
+
41
+
42
+ def _emit_event(tool: str, status: str, duration_ms: int, detail: str = "") -> None:
43
+ """Append one hash-chained event; logging failure never breaks a tool call."""
44
+ try:
45
+ path = _event_log_path()
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ prev = _ZERO_HASH
48
+ if path.exists() and path.stat().st_size:
49
+ with path.open("rb") as fh:
50
+ fh.seek(0, os.SEEK_END)
51
+ tail = fh.read(65536).decode("utf-8", errors="replace")
52
+ last = tail.rstrip().rsplit("\n", 1)[-1]
53
+ prev = json.loads(last).get("event_hash", _ZERO_HASH)
54
+ event = {
55
+ "schema": "code2schema.mcp/event/v1",
56
+ "event_id": f"event:{uuid.uuid4().hex[:24]}",
57
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
58
+ "actor": "agent:mcp",
59
+ "tool": tool,
60
+ "status": status,
61
+ "duration_ms": duration_ms,
62
+ "detail": detail[:200],
63
+ "prev_hash": prev,
64
+ }
65
+ body = json.dumps(event, sort_keys=True)
66
+ event["event_hash"] = hashlib.sha256(body.encode()).hexdigest()
67
+ with path.open("a", encoding="utf-8") as fh:
68
+ fh.write(json.dumps(event) + "\n")
69
+ except Exception:
70
+ pass
71
+
72
+
73
+ def _load_graph(path: str, exclude: list[str] | None):
74
+ from code2schema.analyzer.cqrs import analyze
75
+ from code2schema.analyzer.graph import build_rich_graph
76
+ from code2schema.core.extractor import extract_project
77
+
78
+ root = Path(path)
79
+ if not root.exists():
80
+ raise FileNotFoundError(f"Path does not exist: {path}")
81
+ modules = extract_project(root, exclude=exclude or None)
82
+ if not modules:
83
+ raise ValueError(f"No .py files found under: {path}")
84
+ schema = analyze(modules)
85
+ return modules, schema, build_rich_graph(schema)
86
+
87
+
88
+ def _tool_analyze(args: dict[str, Any]) -> str:
89
+ from code2schema.codegen import to_json
90
+
91
+ _, schema, _ = _load_graph(args["path"], args.get("exclude"))
92
+ return to_json(schema)
93
+
94
+
95
+ def _tool_summary(args: dict[str, Any]) -> str:
96
+ from code2schema.analyzer.events import infer_event_model
97
+ from code2schema.analyzer.graph import detect_cycles, graph_summary
98
+
99
+ modules, schema, G = _load_graph(args["path"], args.get("exclude"))
100
+ cycles = detect_cycles(G)
101
+ result = {
102
+ "modules": len(modules),
103
+ "functions": len(schema.all_functions()),
104
+ "queries": len(schema.queries()),
105
+ "commands": len(schema.commands()),
106
+ "orchestrators": len(schema.orchestrators()),
107
+ "workflows": len(schema.workflows),
108
+ "rules": len(schema.rules),
109
+ "graph_nodes": G.number_of_nodes(),
110
+ "graph_edges": G.number_of_edges(),
111
+ "cycles": len(cycles),
112
+ "graph_summary": graph_summary(G, schema),
113
+ "event_model": infer_event_model(modules).summary(),
114
+ }
115
+ return json.dumps(result, indent=2, default=str)
116
+
117
+
118
+ def _tool_cycles(args: dict[str, Any]) -> str:
119
+ from code2schema.analyzer.graph import detect_cycles
120
+
121
+ _, _, G = _load_graph(args["path"], args.get("exclude"))
122
+ return json.dumps({"cycles": detect_cycles(G)}, indent=2)
123
+
124
+
125
+ def _tool_proto(args: dict[str, Any]) -> str:
126
+ from code2schema.codegen import to_proto
127
+
128
+ _, schema, _ = _load_graph(args["path"], args.get("exclude"))
129
+ return to_proto(schema)
130
+
131
+
132
+ _PATH_PROPERTIES = {
133
+ "path": {"type": "string", "description": "Project directory to analyze"},
134
+ "exclude": {
135
+ "type": "array",
136
+ "items": {"type": "string"},
137
+ "description": "Directory/file names to exclude",
138
+ },
139
+ }
140
+
141
+ TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
142
+ "code2schema_analyze": {
143
+ "name": "code2schema_analyze",
144
+ "description": "Compile a Python project to the full CQRS SchemaIR JSON (commands, queries, workflows, rules).",
145
+ "inputSchema": {
146
+ "type": "object",
147
+ "properties": _PATH_PROPERTIES,
148
+ "required": ["path"],
149
+ },
150
+ },
151
+ "code2schema_summary": {
152
+ "name": "code2schema_summary",
153
+ "description": "Compact analysis summary: module/function/command counts, graph size, cycles, event model.",
154
+ "inputSchema": {
155
+ "type": "object",
156
+ "properties": _PATH_PROPERTIES,
157
+ "required": ["path"],
158
+ },
159
+ },
160
+ "code2schema_cycles": {
161
+ "name": "code2schema_cycles",
162
+ "description": "Detect dependency cycles in the CQRS graph.",
163
+ "inputSchema": {
164
+ "type": "object",
165
+ "properties": _PATH_PROPERTIES,
166
+ "required": ["path"],
167
+ },
168
+ },
169
+ "code2schema_proto": {
170
+ "name": "code2schema_proto",
171
+ "description": "Generate proto3 definition from the CQRS schema.",
172
+ "inputSchema": {
173
+ "type": "object",
174
+ "properties": _PATH_PROPERTIES,
175
+ "required": ["path"],
176
+ },
177
+ },
178
+ }
179
+
180
+ TOOL_HANDLERS: dict[str, Callable[[dict[str, Any]], str]] = {
181
+ "code2schema_analyze": _tool_analyze,
182
+ "code2schema_summary": _tool_summary,
183
+ "code2schema_cycles": _tool_cycles,
184
+ "code2schema_proto": _tool_proto,
185
+ }
186
+
187
+
188
+ def _handle_initialize(request_id: Any, params: dict[str, Any] | None) -> dict[str, Any]:
189
+ client_version = (params or {}).get("protocolVersion", _PROTOCOL_VERSION)
190
+ return {
191
+ "jsonrpc": "2.0",
192
+ "id": request_id,
193
+ "result": {
194
+ "protocolVersion": client_version,
195
+ "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
196
+ "capabilities": {"tools": {}},
197
+ },
198
+ }
199
+
200
+
201
+ def _handle_tools_list(request_id: Any) -> dict[str, Any]:
202
+ tools = [
203
+ {
204
+ "name": schema["name"],
205
+ "description": schema["description"],
206
+ "inputSchema": schema["inputSchema"],
207
+ }
208
+ for schema in TOOL_SCHEMAS.values()
209
+ ]
210
+ return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": tools}}
211
+
212
+
213
+ def _handle_tools_call(request_id: Any, params: dict[str, Any]) -> dict[str, Any]:
214
+ tool_name = params.get("name")
215
+ arguments = params.get("arguments", {}) or {}
216
+
217
+ if tool_name not in TOOL_HANDLERS:
218
+ return {
219
+ "jsonrpc": "2.0",
220
+ "id": request_id,
221
+ "error": {"code": -32601, "message": f"Tool '{tool_name}' not found"},
222
+ }
223
+
224
+ start = time.monotonic()
225
+ try:
226
+ result = TOOL_HANDLERS[tool_name](arguments)
227
+ _emit_event(tool_name, "ok", int((time.monotonic() - start) * 1000))
228
+ return {
229
+ "jsonrpc": "2.0",
230
+ "id": request_id,
231
+ "result": {"content": [{"type": "text", "text": result}]},
232
+ }
233
+ except Exception as exc:
234
+ _emit_event(
235
+ tool_name, "error", int((time.monotonic() - start) * 1000), str(exc)
236
+ )
237
+ return {
238
+ "jsonrpc": "2.0",
239
+ "id": request_id,
240
+ "error": {"code": -32603, "message": f"Tool execution failed: {exc}"},
241
+ }
242
+
243
+
244
+ def handle_request(request: dict[str, Any]) -> dict[str, Any]:
245
+ method = request.get("method", "")
246
+ params = request.get("params", {}) or {}
247
+ request_id = request.get("id")
248
+
249
+ if method in _NOTIFICATIONS:
250
+ return {}
251
+ if method == "initialize":
252
+ return _handle_initialize(request_id, params)
253
+ if method == "tools/list":
254
+ return _handle_tools_list(request_id)
255
+ if method == "tools/call":
256
+ return _handle_tools_call(request_id, params)
257
+
258
+ return {
259
+ "jsonrpc": "2.0",
260
+ "id": request_id,
261
+ "error": {"code": -32601, "message": f"Method '{method}' not found"},
262
+ }
263
+
264
+
265
+ def run_server() -> None:
266
+ print("code2schema MCP Server started", file=sys.stderr)
267
+ print(f"Available tools: {', '.join(sorted(TOOL_SCHEMAS))}", file=sys.stderr)
268
+
269
+ for line in sys.stdin:
270
+ line = line.strip()
271
+ if not line:
272
+ continue
273
+ try:
274
+ request = json.loads(line)
275
+ response = handle_request(request)
276
+ if response:
277
+ print(json.dumps(response), flush=True)
278
+ except json.JSONDecodeError as exc:
279
+ print(
280
+ json.dumps(
281
+ {
282
+ "jsonrpc": "2.0",
283
+ "error": {"code": -32700, "message": f"Parse error: {exc}"},
284
+ }
285
+ ),
286
+ flush=True,
287
+ )
288
+ except Exception as exc:
289
+ print(
290
+ json.dumps(
291
+ {
292
+ "jsonrpc": "2.0",
293
+ "error": {"code": -32603, "message": f"Internal error: {exc}"},
294
+ }
295
+ ),
296
+ flush=True,
297
+ )
298
+
299
+
300
+ def main() -> None:
301
+ run_server()
302
+
303
+
304
+ if __name__ == "__main__":
305
+ main()
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2schema
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema
5
- License: Apache-2.0
5
+ License-Expression: Apache-2.0
6
6
  Keywords: ast,cqrs,static-analysis,code-quality,schema
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Programming Language :: Python :: 3
@@ -24,9 +24,8 @@ Requires-Dist: pytest>=8; extra == "dev"
24
24
  Requires-Dist: pytest-cov; extra == "dev"
25
25
  Requires-Dist: black; extra == "dev"
26
26
  Requires-Dist: ruff; extra == "dev"
27
- Requires-Dist: goal>=2.1.0; extra == "dev"
28
27
  Requires-Dist: costs>=0.1.20; extra == "dev"
29
- Requires-Dist: pfix>=0.1.60; extra == "dev"
28
+ Requires-Dist: pfix>=0.1.60; python_version >= "3.10" and extra == "dev"
30
29
  Dynamic: license-file
31
30
 
32
31
  # code2schema
@@ -34,13 +33,13 @@ Dynamic: license-file
34
33
 
35
34
  ## AI Cost Tracking
36
35
 
37
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.7-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
38
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.75-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-4.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.77-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
39
38
 
40
- - 🤖 **LLM usage:** $0.7500 (5 commits)
41
- - 👤 **Human dev:** ~$430 (4.3h @ $100/h, 30min dedup)
39
+ - 🤖 **LLM usage:** $6.7712 (15 commits)
40
+ - 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
42
41
 
43
- Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
+ Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
44
43
 
45
44
  ---
46
45
 
@@ -270,3 +269,6 @@ MIT
270
269
  ## License
271
270
 
272
271
  Licensed under Apache-2.0.
272
+ ## Dependency maintenance
273
+
274
+ See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  code2schema/__init__.py
5
5
  code2schema/cli.py
6
+ code2schema/mcp_server.py
6
7
  code2schema.egg-info/PKG-INFO
7
8
  code2schema.egg-info/SOURCES.txt
8
9
  code2schema.egg-info/dependency_links.txt
@@ -18,4 +19,5 @@ code2schema/codegen/visualizer.py
18
19
  code2schema/core/__init__.py
19
20
  code2schema/core/extractor.py
20
21
  code2schema/core/models.py
21
- tests/test_code2schema.py
22
+ tests/test_code2schema.py
23
+ tests/test_event_handlers.py
@@ -7,8 +7,9 @@ pytest>=8
7
7
  pytest-cov
8
8
  black
9
9
  ruff
10
- goal>=2.1.0
11
10
  costs>=0.1.20
11
+
12
+ [dev:python_version >= "3.10"]
12
13
  pfix>=0.1.60
13
14
 
14
15
  [neo4j]
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2schema"
7
- version = "0.1.7"
7
+ version = "0.1.9"
8
8
  description = "Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -19,18 +19,19 @@ dependencies = [
19
19
  "networkx>=3.2",
20
20
  "pydantic>=2.5",
21
21
  ]
22
-
23
- [project.license]
24
- text = "Apache-2.0"
22
+ license = "Apache-2.0"
25
23
 
26
24
  [project.optional-dependencies]
27
25
  proto = ["grpcio-tools>=1.60"]
28
26
  neo4j = ["neo4j>=5.15"]
29
27
  viz = ["pyvis>=0.3"]
30
- dev = ["pytest>=8", "pytest-cov", "black", "ruff",
31
- "goal>=2.1.0",
28
+ dev = [
29
+ "pytest>=8",
30
+ "pytest-cov",
31
+ "black",
32
+ "ruff",
32
33
  "costs>=0.1.20",
33
- "pfix>=0.1.60",
34
+ "pfix>=0.1.60; python_version >= '3.10'",
34
35
  ]
35
36
 
36
37
  [project.scripts]
@@ -75,3 +76,9 @@ max_commits = 500
75
76
 
76
77
  # Cost thresholds for badge colors (USD)
77
78
  badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
79
+
80
+ [tool.uv.dependency-groups]
81
+ automation = {requires-python = ">=3.12"}
82
+
83
+ [dependency-groups]
84
+ automation = ["goal>=2.2.0"]
@@ -0,0 +1,30 @@
1
+ import copy
2
+ import re
3
+
4
+ from code2schema.analyzer.events import DomainEvent, _find_handlers
5
+ from code2schema.core.models import FunctionIR, ModuleIR
6
+
7
+
8
+ def test_handler_matching_preserves_order_duplicates_case_and_empty_keywords():
9
+ names = ["UserEvent", "UserEvent", "Event", "Entity1Event", "Entity10Event", "ŻółćEvent"]
10
+ events = [DomainEvent(name=n, emitted_by=str(i), handled_by=["existing"]) for i,n in enumerate(names)]
11
+ modules = [ModuleIR(name=m, path=m+".py", functions=[FunctionIR(name=n,module=m) for n in [
12
+ "handle_user", "ON_USER", "handle_entity10", "on_żółć", "plain_user", "consumer_user"]]) for m in ["a","b"]]
13
+ expected = copy.deepcopy(events)
14
+ for _ in range(2):
15
+ for mod in modules:
16
+ for f in mod.functions:
17
+ if re.search(r"(on_|handle_|listener|subscriber|consumer|process_|receive_)", f.name, re.I):
18
+ for ev in expected:
19
+ keyword = ev.name.lower().replace("event", "").strip()
20
+ if keyword and keyword in f.name.lower():
21
+ ev.handled_by.append(f.qualified_name)
22
+ _find_handlers(events, modules)
23
+ assert events == expected
24
+
25
+
26
+ def test_empty_events_and_modules_are_noops():
27
+ events = [DomainEvent(name="UserEvent", emitted_by="emit")]
28
+ _find_handlers(events, [])
29
+ _find_handlers([], [])
30
+ assert events[0].handled_by == []
File without changes
File without changes