yuho 5.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 (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/mcp/server.py ADDED
@@ -0,0 +1,1452 @@
1
+ """
2
+ Yuho MCP Server implementation.
3
+
4
+ Provides MCP tools for parsing, transpiling, and analyzing Yuho code.
5
+ """
6
+
7
+ from typing import Dict, List, Optional, Any
8
+ from pathlib import Path
9
+ from enum import IntEnum
10
+ from dataclasses import dataclass, field
11
+ import json
12
+ import logging
13
+ import time
14
+ import threading
15
+
16
+ # Configure MCP logger
17
+ logger = logging.getLogger("yuho.mcp")
18
+
19
+
20
+ class LogVerbosity(IntEnum):
21
+ """Verbosity levels for MCP request logging."""
22
+ QUIET = 0 # No logging
23
+ MINIMAL = 1 # Log tool name only
24
+ STANDARD = 2 # Log tool name and execution time
25
+ VERBOSE = 3 # Log tool name, args summary, and execution time
26
+ DEBUG = 4 # Log everything including full args and responses
27
+
28
+
29
+ class RateLimitExceeded(Exception):
30
+ """Exception raised when rate limit is exceeded."""
31
+
32
+ def __init__(self, retry_after: float):
33
+ self.retry_after = retry_after
34
+ super().__init__(f"Rate limit exceeded. Retry after {retry_after:.1f}s")
35
+
36
+
37
+ @dataclass
38
+ class RateLimitConfig:
39
+ """Configuration for rate limiting."""
40
+
41
+ # Requests per second (token refill rate)
42
+ requests_per_second: float = 10.0
43
+
44
+ # Maximum burst size (bucket capacity)
45
+ burst_size: int = 20
46
+
47
+ # Per-client limits (by IP or client ID)
48
+ per_client_rps: float = 5.0
49
+ per_client_burst: int = 10
50
+
51
+ # Enable/disable rate limiting
52
+ enabled: bool = True
53
+
54
+ # Exempt tool names (no rate limiting)
55
+ exempt_tools: List[str] = field(default_factory=lambda: ["yuho_grammar", "yuho_types"])
56
+
57
+
58
+ class TokenBucket:
59
+ """Token bucket rate limiter implementation."""
60
+
61
+ def __init__(self, rate: float, capacity: int):
62
+ """
63
+ Initialize token bucket.
64
+
65
+ Args:
66
+ rate: Token refill rate (tokens per second)
67
+ capacity: Maximum bucket capacity
68
+ """
69
+ self.rate = rate
70
+ self.capacity = capacity
71
+ self.tokens = float(capacity)
72
+ self.last_refill = time.monotonic()
73
+ self._lock = threading.Lock()
74
+
75
+ def _refill(self) -> None:
76
+ """Refill tokens based on elapsed time."""
77
+ now = time.monotonic()
78
+ elapsed = now - self.last_refill
79
+ self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
80
+ self.last_refill = now
81
+
82
+ def acquire(self, tokens: int = 1) -> bool:
83
+ """
84
+ Try to acquire tokens from the bucket.
85
+
86
+ Args:
87
+ tokens: Number of tokens to acquire
88
+
89
+ Returns:
90
+ True if tokens acquired, False if rate limited
91
+ """
92
+ with self._lock:
93
+ self._refill()
94
+ if self.tokens >= tokens:
95
+ self.tokens -= tokens
96
+ return True
97
+ return False
98
+
99
+ def time_until_available(self, tokens: int = 1) -> float:
100
+ """
101
+ Calculate time until tokens will be available.
102
+
103
+ Args:
104
+ tokens: Number of tokens needed
105
+
106
+ Returns:
107
+ Seconds until tokens available (0 if available now)
108
+ """
109
+ with self._lock:
110
+ self._refill()
111
+ if self.tokens >= tokens:
112
+ return 0.0
113
+ deficit = tokens - self.tokens
114
+ return deficit / self.rate
115
+
116
+
117
+ class RateLimiter:
118
+ """Rate limiter for MCP server with per-client tracking."""
119
+
120
+ def __init__(self, config: RateLimitConfig):
121
+ self.config = config
122
+ self._global_bucket = TokenBucket(config.requests_per_second, config.burst_size)
123
+ self._client_buckets: Dict[str, TokenBucket] = {}
124
+ self._client_buckets_lock = threading.Lock()
125
+ self._stats = {
126
+ "total_requests": 0,
127
+ "rate_limited": 0,
128
+ "by_tool": {},
129
+ }
130
+
131
+ def _get_client_bucket(self, client_id: str) -> TokenBucket:
132
+ """Get or create a token bucket for a client."""
133
+ with self._client_buckets_lock:
134
+ if client_id not in self._client_buckets:
135
+ self._client_buckets[client_id] = TokenBucket(
136
+ self.config.per_client_rps,
137
+ self.config.per_client_burst
138
+ )
139
+ return self._client_buckets[client_id]
140
+
141
+ def check_rate_limit(
142
+ self,
143
+ tool_name: str,
144
+ client_id: Optional[str] = None,
145
+ ) -> None:
146
+ """
147
+ Check if request is rate limited.
148
+
149
+ Args:
150
+ tool_name: Name of the tool being called
151
+ client_id: Optional client identifier
152
+
153
+ Raises:
154
+ RateLimitExceeded: If rate limit is exceeded
155
+ """
156
+ if not self.config.enabled:
157
+ return
158
+
159
+ # Track stats
160
+ self._stats["total_requests"] += 1
161
+ self._stats["by_tool"][tool_name] = self._stats["by_tool"].get(tool_name, 0) + 1
162
+
163
+ # Check exempt tools
164
+ if tool_name in self.config.exempt_tools:
165
+ return
166
+
167
+ # Check global rate limit
168
+ if not self._global_bucket.acquire():
169
+ self._stats["rate_limited"] += 1
170
+ retry_after = self._global_bucket.time_until_available()
171
+ logger.warning(f"Global rate limit exceeded for {tool_name}")
172
+ raise RateLimitExceeded(retry_after)
173
+
174
+ # Check per-client rate limit if client_id provided
175
+ if client_id:
176
+ client_bucket = self._get_client_bucket(client_id)
177
+ if not client_bucket.acquire():
178
+ self._stats["rate_limited"] += 1
179
+ retry_after = client_bucket.time_until_available()
180
+ logger.warning(f"Client rate limit exceeded for {client_id} on {tool_name}")
181
+ raise RateLimitExceeded(retry_after)
182
+
183
+ def get_stats(self) -> Dict[str, Any]:
184
+ """Get rate limiting statistics."""
185
+ return {
186
+ **self._stats,
187
+ "global_tokens": self._global_bucket.tokens,
188
+ "active_clients": len(self._client_buckets),
189
+ }
190
+
191
+ def reset_stats(self) -> None:
192
+ """Reset rate limiting statistics."""
193
+ self._stats = {
194
+ "total_requests": 0,
195
+ "rate_limited": 0,
196
+ "by_tool": {},
197
+ }
198
+
199
+
200
+ class LogVerbosity(IntEnum):
201
+ """Verbosity levels for MCP request logging."""
202
+ QUIET = 0 # No logging
203
+ MINIMAL = 1 # Log tool name only
204
+ STANDARD = 2 # Log tool name and execution time
205
+ VERBOSE = 3 # Log tool name, args summary, and execution time
206
+ DEBUG = 4 # Log everything including full args and responses
207
+
208
+
209
+ class MCPRequestLogger:
210
+ """Logger for MCP requests with configurable verbosity."""
211
+
212
+ def __init__(self, verbosity: LogVerbosity = LogVerbosity.STANDARD):
213
+ self.verbosity = verbosity
214
+
215
+ def log_request(self, tool_name: str, args: Dict[str, Any]) -> float:
216
+ """Log incoming request, return start time."""
217
+ start = time.time()
218
+
219
+ if self.verbosity >= LogVerbosity.MINIMAL:
220
+ logger.info(f"MCP request: {tool_name}")
221
+
222
+ if self.verbosity >= LogVerbosity.VERBOSE:
223
+ args_summary = {k: f"{len(str(v))} chars" if len(str(v)) > 100 else v
224
+ for k, v in args.items()}
225
+ logger.info(f" Args: {args_summary}")
226
+
227
+ if self.verbosity >= LogVerbosity.DEBUG:
228
+ logger.debug(f" Full args: {args}")
229
+
230
+ return start
231
+
232
+ def log_response(self, tool_name: str, result: Any, start_time: float, error: Optional[Exception] = None) -> None:
233
+ """Log response after tool execution."""
234
+ elapsed = time.time() - start_time
235
+
236
+ if error:
237
+ logger.error(f"MCP error: {tool_name} failed after {elapsed:.3f}s - {error}")
238
+ return
239
+
240
+ if self.verbosity >= LogVerbosity.STANDARD:
241
+ logger.info(f"MCP response: {tool_name} completed in {elapsed:.3f}s")
242
+
243
+ if self.verbosity >= LogVerbosity.DEBUG:
244
+ result_str = str(result)
245
+ if len(result_str) > 500:
246
+ result_str = result_str[:500] + "..."
247
+ logger.debug(f" Result: {result_str}")
248
+
249
+
250
+ try:
251
+ from mcp.server import Server
252
+ from mcp.server.stdio import stdio_server
253
+ from mcp.types import Tool, TextContent, Resource
254
+ except ImportError:
255
+ # Provide mock classes for when mcp is not installed
256
+ class Server:
257
+ def __init__(self, name: str):
258
+ self.name = name
259
+
260
+ def tool(self):
261
+ def decorator(func):
262
+ return func
263
+ return decorator
264
+
265
+ def resource(self, uri: str):
266
+ def decorator(func):
267
+ return func
268
+ return decorator
269
+
270
+ def stdio_server():
271
+ raise ImportError("MCP dependencies not installed. Install with: pip install yuho[mcp]")
272
+
273
+
274
+ class YuhoMCPServer:
275
+ """
276
+ MCP Server exposing Yuho functionality.
277
+
278
+ Tools:
279
+ - yuho_check: Validate Yuho source
280
+ - yuho_transpile: Convert to other formats
281
+ - yuho_explain: Generate explanations
282
+ - yuho_parse: Get AST representation
283
+ - yuho_format: Format source code
284
+ - yuho_complete: Get completions
285
+ - yuho_hover: Get hover info
286
+ - yuho_definition: Find definition
287
+
288
+ Resources:
289
+ - yuho://grammar: Tree-sitter grammar
290
+ - yuho://types: Built-in types
291
+ - yuho://library/{section}: Statute by section
292
+ """
293
+
294
+ def __init__(
295
+ self,
296
+ verbosity: LogVerbosity = LogVerbosity.STANDARD,
297
+ rate_limit_config: Optional[RateLimitConfig] = None,
298
+ ):
299
+ self.server = Server("yuho-mcp")
300
+ self.request_logger = MCPRequestLogger(verbosity)
301
+ self.rate_limiter = RateLimiter(rate_limit_config or RateLimitConfig())
302
+ self._register_tools()
303
+ self._register_resources()
304
+ self._register_prompts()
305
+
306
+ def set_verbosity(self, verbosity: LogVerbosity) -> None:
307
+ """Set the logging verbosity level."""
308
+ self.request_logger.verbosity = verbosity
309
+ logger.info(f"MCP logging verbosity set to: {verbosity.name}")
310
+
311
+ def set_rate_limit_config(self, config: RateLimitConfig) -> None:
312
+ """Update rate limiting configuration."""
313
+ self.rate_limiter = RateLimiter(config)
314
+ logger.info(f"MCP rate limiting updated: {config.requests_per_second} req/s, burst={config.burst_size}")
315
+
316
+ def get_rate_limit_stats(self) -> Dict[str, Any]:
317
+ """Get rate limiting statistics."""
318
+ return self.rate_limiter.get_stats()
319
+
320
+ def _check_rate_limit(self, tool_name: str, client_id: Optional[str] = None) -> None:
321
+ """Check rate limit and raise exception if exceeded."""
322
+ self.rate_limiter.check_rate_limit(tool_name, client_id)
323
+
324
+ def _register_tools(self):
325
+ """Register MCP tools."""
326
+
327
+ @self.server.tool()
328
+ async def yuho_check(file_content: str, client_id: Optional[str] = None) -> Dict[str, Any]:
329
+ """
330
+ Validate Yuho source code.
331
+
332
+ Args:
333
+ file_content: The Yuho source code to validate
334
+ client_id: Optional client identifier for rate limiting
335
+
336
+ Returns:
337
+ {valid: bool, errors: list of error dicts}
338
+ """
339
+ try:
340
+ self._check_rate_limit("yuho_check", client_id)
341
+ except RateLimitExceeded as e:
342
+ return {"error": str(e), "retry_after": e.retry_after}
343
+
344
+ from yuho.parser import Parser
345
+ from yuho.ast import ASTBuilder
346
+
347
+ parser = Parser()
348
+ result = parser.parse(file_content)
349
+
350
+ if result.errors:
351
+ return {
352
+ "valid": False,
353
+ "errors": [
354
+ {
355
+ "message": err.message,
356
+ "line": err.location.line,
357
+ "col": err.location.col,
358
+ }
359
+ for err in result.errors
360
+ ],
361
+ }
362
+
363
+ try:
364
+ builder = ASTBuilder(file_content)
365
+ ast = builder.build(result.root_node)
366
+ return {
367
+ "valid": True,
368
+ "errors": [],
369
+ "stats": {
370
+ "statutes": len(ast.statutes),
371
+ "structs": len(ast.type_defs),
372
+ "functions": len(ast.function_defs),
373
+ },
374
+ }
375
+ except Exception as e:
376
+ return {
377
+ "valid": False,
378
+ "errors": [{"message": str(e), "line": 1, "col": 1}],
379
+ }
380
+
381
+ @self.server.tool()
382
+ async def yuho_transpile(file_content: str, target: str, client_id: Optional[str] = None) -> Dict[str, Any]:
383
+ """
384
+ Transpile Yuho source to another format.
385
+
386
+ Args:
387
+ file_content: The Yuho source code
388
+ target: Target format (json, jsonld, english, mermaid, alloy)
389
+ client_id: Optional client identifier for rate limiting
390
+
391
+ Returns:
392
+ {output: str} or {error: str}
393
+ """
394
+ try:
395
+ self._check_rate_limit("yuho_transpile", client_id)
396
+ except RateLimitExceeded as e:
397
+ return {"error": str(e), "retry_after": e.retry_after}
398
+
399
+ from yuho.parser import Parser
400
+ from yuho.ast import ASTBuilder
401
+ from yuho.transpile import TranspileTarget, get_transpiler
402
+
403
+ parser = Parser()
404
+ result = parser.parse(file_content)
405
+
406
+ if result.errors:
407
+ return {"error": f"Parse error: {result.errors[0].message}"}
408
+
409
+ try:
410
+ builder = ASTBuilder(file_content)
411
+ ast = builder.build(result.root_node)
412
+
413
+ transpile_target = TranspileTarget.from_string(target)
414
+ transpiler = get_transpiler(transpile_target)
415
+ output = transpiler.transpile(ast)
416
+
417
+ return {"output": output}
418
+ except ValueError as e:
419
+ return {"error": f"Invalid target: {e}"}
420
+ except Exception as e:
421
+ return {"error": str(e)}
422
+
423
+ @self.server.tool()
424
+ async def yuho_explain(file_content: str, section: Optional[str] = None, client_id: Optional[str] = None) -> Dict[str, Any]:
425
+ """
426
+ Generate natural language explanation.
427
+
428
+ Args:
429
+ file_content: The Yuho source code
430
+ section: Optional section number to explain
431
+ client_id: Optional client identifier for rate limiting
432
+
433
+ Returns:
434
+ {explanation: str} or {error: str}
435
+ """
436
+ try:
437
+ self._check_rate_limit("yuho_explain", client_id)
438
+ except RateLimitExceeded as e:
439
+ return {"error": str(e), "retry_after": e.retry_after}
440
+
441
+ from yuho.parser import Parser
442
+ from yuho.ast import ASTBuilder
443
+ from yuho.transpile import EnglishTranspiler
444
+
445
+ parser = Parser()
446
+ result = parser.parse(file_content)
447
+
448
+ if result.errors:
449
+ return {"error": f"Parse error: {result.errors[0].message}"}
450
+
451
+ try:
452
+ builder = ASTBuilder(file_content)
453
+ ast = builder.build(result.root_node)
454
+
455
+ # Filter to specific section if requested
456
+ if section:
457
+ from yuho.ast.nodes import ModuleNode
458
+ matching = [s for s in ast.statutes if section in s.section_number]
459
+ if not matching:
460
+ return {"error": f"Section {section} not found"}
461
+ ast = ModuleNode(
462
+ imports=ast.imports,
463
+ type_defs=ast.type_defs,
464
+ function_defs=ast.function_defs,
465
+ statutes=tuple(matching),
466
+ variables=ast.variables,
467
+ )
468
+
469
+ transpiler = EnglishTranspiler()
470
+ explanation = transpiler.transpile(ast)
471
+
472
+ return {"explanation": explanation}
473
+ except Exception as e:
474
+ return {"error": str(e)}
475
+
476
+ @self.server.tool()
477
+ async def yuho_parse(file_content: str) -> Dict[str, Any]:
478
+ """
479
+ Parse Yuho source and return AST.
480
+
481
+ Args:
482
+ file_content: The Yuho source code
483
+
484
+ Returns:
485
+ {ast: dict} or {error: str}
486
+ """
487
+ from yuho.parser import Parser
488
+ from yuho.ast import ASTBuilder
489
+ from yuho.transpile import JSONTranspiler
490
+
491
+ parser = Parser()
492
+ result = parser.parse(file_content)
493
+
494
+ if result.errors:
495
+ return {"error": f"Parse error: {result.errors[0].message}"}
496
+
497
+ try:
498
+ builder = ASTBuilder(file_content)
499
+ ast = builder.build(result.root_node)
500
+
501
+ # Use JSON transpiler to serialize AST
502
+ json_transpiler = JSONTranspiler(include_locations=False)
503
+ ast_json = json_transpiler.transpile(ast)
504
+
505
+ return {"ast": json.loads(ast_json)}
506
+ except Exception as e:
507
+ return {"error": str(e)}
508
+
509
+ @self.server.tool()
510
+ async def yuho_format(file_content: str) -> Dict[str, Any]:
511
+ """
512
+ Format Yuho source code.
513
+
514
+ Args:
515
+ file_content: The Yuho source code
516
+
517
+ Returns:
518
+ {formatted: str} or {error: str}
519
+ """
520
+ from yuho.parser import Parser
521
+ from yuho.ast import ASTBuilder
522
+ from yuho.cli.commands.fmt import _format_module
523
+
524
+ parser = Parser()
525
+ result = parser.parse(file_content)
526
+
527
+ if result.errors:
528
+ return {"error": f"Parse error: {result.errors[0].message}"}
529
+
530
+ try:
531
+ builder = ASTBuilder(file_content)
532
+ ast = builder.build(result.root_node)
533
+ formatted = _format_module(ast)
534
+
535
+ return {"formatted": formatted}
536
+ except Exception as e:
537
+ return {"error": str(e)}
538
+
539
+ @self.server.tool()
540
+ async def yuho_complete(file_content: str, line: int, col: int) -> Dict[str, Any]:
541
+ """
542
+ Get completions at position.
543
+
544
+ Args:
545
+ file_content: The Yuho source code
546
+ line: Line number (1-indexed)
547
+ col: Column number (1-indexed)
548
+
549
+ Returns:
550
+ {completions: list of completion items}
551
+ """
552
+ from yuho.parser import Parser
553
+ from yuho.ast import ASTBuilder
554
+
555
+ completions = []
556
+
557
+ # Keywords
558
+ keywords = [
559
+ "struct", "fn", "match", "case", "consequence", "pass", "return",
560
+ "statute", "definitions", "elements", "penalty", "illustration",
561
+ "import", "from", "TRUE", "FALSE",
562
+ ]
563
+ completions.extend({"label": kw, "kind": "keyword"} for kw in keywords)
564
+
565
+ # Types
566
+ types = ["int", "float", "bool", "string", "money", "percent", "date", "duration"]
567
+ completions.extend({"label": t, "kind": "type"} for t in types)
568
+
569
+ # Parse to get symbols
570
+ parser = Parser()
571
+ result = parser.parse(file_content)
572
+
573
+ if result.is_valid:
574
+ try:
575
+ builder = ASTBuilder(file_content)
576
+ ast = builder.build(result.root_node)
577
+
578
+ # Add struct names
579
+ for struct in ast.type_defs:
580
+ completions.append({"label": struct.name, "kind": "struct"})
581
+
582
+ # Add function names
583
+ for func in ast.function_defs:
584
+ completions.append({"label": func.name, "kind": "function"})
585
+
586
+ except Exception:
587
+ pass
588
+
589
+ return {"completions": completions}
590
+
591
+ @self.server.tool()
592
+ async def yuho_hover(file_content: str, line: int, col: int) -> Dict[str, Any]:
593
+ """
594
+ Get hover information at position.
595
+
596
+ Args:
597
+ file_content: The Yuho source code
598
+ line: Line number (1-indexed)
599
+ col: Column number (1-indexed)
600
+
601
+ Returns:
602
+ {info: str} or {info: null}
603
+ """
604
+ from yuho.parser import Parser
605
+ from yuho.ast import ASTBuilder
606
+
607
+ # Keywords and their docs
608
+ KEYWORD_DOCS = {
609
+ "struct": "Defines a structured type with named fields.",
610
+ "fn": "Defines a function.",
611
+ "match": "Pattern matching expression.",
612
+ "case": "Case arm in a match expression.",
613
+ "statute": "Defines a legal statute with elements and penalties.",
614
+ "elements": "Section containing the elements of an offense.",
615
+ "penalty": "Section specifying the punishment for an offense.",
616
+ "actus_reus": "Physical/conduct element of an offense (guilty act).",
617
+ "mens_rea": "Mental element of an offense (guilty mind).",
618
+ "circumstance": "Circumstantial element required for the offense.",
619
+ }
620
+
621
+ TYPE_DOCS = {
622
+ "int": "Integer number type (whole numbers).",
623
+ "float": "Floating-point number type (decimals).",
624
+ "bool": "Boolean type: TRUE or FALSE.",
625
+ "string": "Text string type.",
626
+ "money": "Monetary amount with currency (e.g., $1000.00 SGD).",
627
+ "percent": "Percentage value (0-100%).",
628
+ "date": "Calendar date (YYYY-MM-DD).",
629
+ "duration": "Time duration (years, months, days, etc.).",
630
+ "void": "No value type (for procedures).",
631
+ }
632
+
633
+ # Get word at position
634
+ lines = file_content.splitlines()
635
+ if line < 1 or line > len(lines):
636
+ return {"info": None}
637
+
638
+ target_line = lines[line - 1]
639
+ if col < 1 or col > len(target_line):
640
+ return {"info": None}
641
+
642
+ # Extract word at position
643
+ start = col - 1
644
+ end = col - 1
645
+ while start > 0 and (target_line[start - 1].isalnum() or target_line[start - 1] == '_'):
646
+ start -= 1
647
+ while end < len(target_line) and (target_line[end].isalnum() or target_line[end] == '_'):
648
+ end += 1
649
+
650
+ if start == end:
651
+ return {"info": None}
652
+
653
+ word = target_line[start:end]
654
+
655
+ # Check keywords
656
+ if word in KEYWORD_DOCS:
657
+ return {"info": f"**keyword** `{word}`\n\n{KEYWORD_DOCS[word]}"}
658
+
659
+ # Check types
660
+ if word in TYPE_DOCS:
661
+ return {"info": f"**type** `{word}`\n\n{TYPE_DOCS[word]}"}
662
+
663
+ # Parse for symbol info
664
+ parser = Parser()
665
+ result = parser.parse(file_content)
666
+
667
+ if result.is_valid:
668
+ try:
669
+ builder = ASTBuilder(file_content)
670
+ ast = builder.build(result.root_node)
671
+
672
+ # Check structs
673
+ for struct in ast.type_defs:
674
+ if struct.name == word:
675
+ fields = ", ".join(f"{f.name}: {f.type_annotation}" for f in struct.fields)
676
+ return {"info": f"**struct** `{struct.name}`\n\n```yuho\nstruct {struct.name} {{ {fields} }}\n```"}
677
+
678
+ # Check functions
679
+ for func in ast.function_defs:
680
+ if func.name == word:
681
+ params = ", ".join(f"{p.name}: {p.type_annotation}" for p in func.params)
682
+ ret = f" -> {func.return_type}" if func.return_type else ""
683
+ return {"info": f"**function** `{func.name}`\n\n```yuho\nfn {func.name}({params}){ret}\n```"}
684
+
685
+ # Check statutes
686
+ for statute in ast.statutes:
687
+ if statute.section_number == word or f"S{statute.section_number}" == word:
688
+ title = statute.title.value if statute.title else "Untitled"
689
+ info = f"**Statute Section {statute.section_number}**: {title}"
690
+ if statute.elements:
691
+ info += "\n\n**Elements:**\n"
692
+ for elem in statute.elements:
693
+ info += f"- {elem.element_type}: {elem.name}\n"
694
+ return {"info": info}
695
+
696
+ except Exception:
697
+ pass
698
+
699
+ return {"info": None}
700
+
701
+
702
+ @self.server.tool()
703
+ async def yuho_definition(file_content: str, line: int, col: int) -> Dict[str, Any]:
704
+ """
705
+ Find definition location.
706
+
707
+ Args:
708
+ file_content: The Yuho source code
709
+ line: Line number (1-indexed)
710
+ col: Column number (1-indexed)
711
+
712
+ Returns:
713
+ {location: {line, col}} or {location: null}
714
+ """
715
+ from yuho.parser import Parser
716
+ from yuho.ast import ASTBuilder
717
+
718
+ # Get word at position
719
+ lines = file_content.splitlines()
720
+ if line < 1 or line > len(lines):
721
+ return {"location": None}
722
+
723
+ target_line = lines[line - 1]
724
+ if col < 1 or col > len(target_line):
725
+ return {"location": None}
726
+
727
+ # Extract word at position
728
+ start = col - 1
729
+ end = col - 1
730
+ while start > 0 and (target_line[start - 1].isalnum() or target_line[start - 1] == '_'):
731
+ start -= 1
732
+ while end < len(target_line) and (target_line[end].isalnum() or target_line[end] == '_'):
733
+ end += 1
734
+
735
+ if start == end:
736
+ return {"location": None}
737
+
738
+ word = target_line[start:end]
739
+
740
+ # Parse for symbol definitions
741
+ parser = Parser()
742
+ result = parser.parse(file_content)
743
+
744
+ if result.is_valid:
745
+ try:
746
+ builder = ASTBuilder(file_content)
747
+ ast = builder.build(result.root_node)
748
+
749
+ # Check struct definitions
750
+ for struct in ast.type_defs:
751
+ if struct.name == word and struct.source_location:
752
+ return {
753
+ "location": {
754
+ "line": struct.source_location.line,
755
+ "col": struct.source_location.col,
756
+ }
757
+ }
758
+
759
+ # Check function definitions
760
+ for func in ast.function_defs:
761
+ if func.name == word and func.source_location:
762
+ return {
763
+ "location": {
764
+ "line": func.source_location.line,
765
+ "col": func.source_location.col,
766
+ }
767
+ }
768
+
769
+ # Check statute definitions (by section number)
770
+ for statute in ast.statutes:
771
+ if (statute.section_number == word or
772
+ f"S{statute.section_number}" == word) and statute.source_location:
773
+ return {
774
+ "location": {
775
+ "line": statute.source_location.line,
776
+ "col": statute.source_location.col,
777
+ }
778
+ }
779
+
780
+ except Exception:
781
+ pass
782
+
783
+ return {"location": None}
784
+
785
+
786
+ @self.server.tool()
787
+ async def yuho_references(file_content: str, line: int, col: int) -> Dict[str, Any]:
788
+ """
789
+ Find all references to symbol at position.
790
+
791
+ Args:
792
+ file_content: The Yuho source code
793
+ line: Line number (1-indexed)
794
+ col: Column number (1-indexed)
795
+
796
+ Returns:
797
+ {locations: list of {line, col, end_line, end_col}}
798
+ """
799
+ from yuho.parser import Parser
800
+ from yuho.ast import ASTBuilder
801
+
802
+ # Get word at position
803
+ lines = file_content.splitlines()
804
+ if line < 1 or line > len(lines):
805
+ return {"locations": []}
806
+
807
+ target_line = lines[line - 1]
808
+ if col < 1 or col > len(target_line):
809
+ return {"locations": []}
810
+
811
+ # Extract word at position
812
+ start = col - 1
813
+ end = col - 1
814
+ while start > 0 and (target_line[start - 1].isalnum() or target_line[start - 1] == '_'):
815
+ start -= 1
816
+ while end < len(target_line) and (target_line[end].isalnum() or target_line[end] == '_'):
817
+ end += 1
818
+
819
+ if start == end:
820
+ return {"locations": []}
821
+
822
+ word = target_line[start:end]
823
+
824
+ # Find all occurrences
825
+ locations = []
826
+ for i, ln in enumerate(lines, 1):
827
+ c = 0
828
+ while True:
829
+ pos = ln.find(word, c)
830
+ if pos == -1:
831
+ break
832
+ before_ok = pos == 0 or not (ln[pos - 1].isalnum() or ln[pos - 1] == '_')
833
+ after_pos = pos + len(word)
834
+ after_ok = after_pos >= len(ln) or not (ln[after_pos].isalnum() or ln[after_pos] == '_')
835
+ if before_ok and after_ok:
836
+ locations.append({
837
+ "line": i,
838
+ "col": pos + 1,
839
+ "end_line": i,
840
+ "end_col": after_pos + 1,
841
+ })
842
+ c = after_pos
843
+
844
+ return {"locations": locations}
845
+
846
+ @self.server.tool()
847
+ async def yuho_symbols(file_content: str) -> Dict[str, Any]:
848
+ """
849
+ Get all symbols in the document.
850
+
851
+ Args:
852
+ file_content: The Yuho source code
853
+
854
+ Returns:
855
+ {symbols: list of {name, kind, line, col}}
856
+ """
857
+ from yuho.parser import Parser
858
+ from yuho.ast import ASTBuilder
859
+
860
+ parser = Parser()
861
+ result = parser.parse(file_content)
862
+
863
+ if result.errors:
864
+ return {"symbols": [], "error": result.errors[0].message}
865
+
866
+ try:
867
+ builder = ASTBuilder(file_content)
868
+ ast = builder.build(result.root_node)
869
+
870
+ symbols = []
871
+
872
+ # Structs
873
+ for struct in ast.type_defs:
874
+ loc = struct.source_location
875
+ symbols.append({
876
+ "name": struct.name,
877
+ "kind": "struct",
878
+ "line": loc.line if loc else 0,
879
+ "col": loc.col if loc else 0,
880
+ })
881
+
882
+ # Functions
883
+ for func in ast.function_defs:
884
+ loc = func.source_location
885
+ symbols.append({
886
+ "name": func.name,
887
+ "kind": "function",
888
+ "line": loc.line if loc else 0,
889
+ "col": loc.col if loc else 0,
890
+ })
891
+
892
+ # Statutes
893
+ for statute in ast.statutes:
894
+ loc = statute.source_location
895
+ title = statute.title.value if statute.title else ""
896
+ symbols.append({
897
+ "name": f"S{statute.section_number}: {title}",
898
+ "kind": "statute",
899
+ "line": loc.line if loc else 0,
900
+ "col": loc.col if loc else 0,
901
+ })
902
+
903
+ return {"symbols": symbols}
904
+ except Exception as e:
905
+ return {"symbols": [], "error": str(e)}
906
+
907
+ @self.server.tool()
908
+ async def yuho_diagnostics(file_content: str) -> Dict[str, Any]:
909
+ """
910
+ Get diagnostics (errors, warnings) for the document.
911
+
912
+ Args:
913
+ file_content: The Yuho source code
914
+
915
+ Returns:
916
+ {diagnostics: list of {message, severity, line, col}}
917
+ """
918
+ from yuho.parser import Parser
919
+ from yuho.ast import ASTBuilder
920
+
921
+ diagnostics = []
922
+ parser = Parser()
923
+ result = parser.parse(file_content)
924
+
925
+ # Parse errors
926
+ for err in result.errors:
927
+ diagnostics.append({
928
+ "message": err.message,
929
+ "severity": "error",
930
+ "line": err.location.line,
931
+ "col": err.location.col,
932
+ })
933
+
934
+ # Try AST build for more diagnostics
935
+ if result.is_valid:
936
+ try:
937
+ builder = ASTBuilder(file_content)
938
+ ast = builder.build(result.root_node)
939
+
940
+ # TODO: Run semantic analysis for more diagnostics
941
+ except Exception as e:
942
+ diagnostics.append({
943
+ "message": str(e),
944
+ "severity": "error",
945
+ "line": 1,
946
+ "col": 1,
947
+ })
948
+
949
+ return {"diagnostics": diagnostics}
950
+
951
+ @self.server.tool()
952
+ async def yuho_validate_contribution(file_content: str, tests: List[str] = None) -> Dict[str, Any]:
953
+ """
954
+ Validate a statute file for contribution to the library.
955
+
956
+ Args:
957
+ file_content: The Yuho source code
958
+ tests: Optional list of test file contents
959
+
960
+ Returns:
961
+ {valid: bool, results: list}
962
+ """
963
+ from yuho.parser import Parser
964
+ from yuho.ast import ASTBuilder
965
+
966
+ tests = tests or []
967
+ results = []
968
+
969
+ # Check parsing
970
+ parser = Parser()
971
+ result = parser.parse(file_content)
972
+
973
+ if result.errors:
974
+ return {
975
+ "valid": False,
976
+ "results": [{
977
+ "check": "parse",
978
+ "passed": False,
979
+ "message": result.errors[0].message,
980
+ }],
981
+ }
982
+
983
+ results.append({"check": "parse", "passed": True, "message": "Parses successfully"})
984
+
985
+ # Check AST build
986
+ try:
987
+ builder = ASTBuilder(file_content)
988
+ ast = builder.build(result.root_node)
989
+ results.append({"check": "ast", "passed": True, "message": "AST builds successfully"})
990
+ except Exception as e:
991
+ return {
992
+ "valid": False,
993
+ "results": results + [{
994
+ "check": "ast",
995
+ "passed": False,
996
+ "message": str(e),
997
+ }],
998
+ }
999
+
1000
+ # Check has statutes
1001
+ if not ast.statutes:
1002
+ results.append({
1003
+ "check": "statute",
1004
+ "passed": False,
1005
+ "message": "No statutes defined",
1006
+ })
1007
+ else:
1008
+ results.append({
1009
+ "check": "statute",
1010
+ "passed": True,
1011
+ "message": f"Contains {len(ast.statutes)} statute(s)",
1012
+ })
1013
+
1014
+ # Check tests exist
1015
+ if not tests:
1016
+ results.append({
1017
+ "check": "tests",
1018
+ "passed": False,
1019
+ "message": "No test files provided",
1020
+ })
1021
+ else:
1022
+ results.append({
1023
+ "check": "tests",
1024
+ "passed": True,
1025
+ "message": f"{len(tests)} test file(s) provided",
1026
+ })
1027
+
1028
+ valid = all(r["passed"] for r in results)
1029
+ return {"valid": valid, "results": results}
1030
+
1031
+ @self.server.tool()
1032
+ async def yuho_library_search(query: str) -> Dict[str, Any]:
1033
+ """
1034
+ Search statute library by section number, title, or jurisdiction.
1035
+
1036
+ Args:
1037
+ query: Search query string
1038
+
1039
+ Returns:
1040
+ {statutes: list of {section, title, jurisdiction, path}}
1041
+ """
1042
+ # TODO: Implement proper library search using library index
1043
+ library_path = Path(__file__).parent.parent.parent.parent / "library"
1044
+ results = []
1045
+
1046
+ query_lower = query.lower()
1047
+
1048
+ if library_path.exists():
1049
+ for yh_file in library_path.glob("**/*.yh"):
1050
+ try:
1051
+ content = yh_file.read_text()
1052
+ # Simple search in content
1053
+ if query_lower in content.lower():
1054
+ results.append({
1055
+ "section": yh_file.stem,
1056
+ "title": yh_file.stem, # Simplified
1057
+ "jurisdiction": "unknown",
1058
+ "path": str(yh_file),
1059
+ })
1060
+ except Exception:
1061
+ continue
1062
+
1063
+ return {"statutes": results[:20]} # Limit results
1064
+
1065
+ @self.server.tool()
1066
+ async def yuho_library_get(section: str) -> Dict[str, Any]:
1067
+ """
1068
+ Get a statute from the library by section number.
1069
+
1070
+ Args:
1071
+ section: Section number (e.g., "299")
1072
+
1073
+ Returns:
1074
+ {statute: {section, title, content}} or {error: str}
1075
+ """
1076
+ library_path = Path(__file__).parent.parent.parent.parent / "library"
1077
+
1078
+ if library_path.exists():
1079
+ # Search for matching section
1080
+ for yh_file in library_path.glob("**/*.yh"):
1081
+ if section in yh_file.stem:
1082
+ try:
1083
+ content = yh_file.read_text()
1084
+ return {
1085
+ "statute": {
1086
+ "section": section,
1087
+ "title": yh_file.stem,
1088
+ "content": content,
1089
+ }
1090
+ }
1091
+ except Exception as e:
1092
+ return {"error": str(e)}
1093
+
1094
+ return {"error": f"Section {section} not found in library"}
1095
+
1096
+ @self.server.tool()
1097
+ async def yuho_statute_to_yuho(natural_text: str) -> Dict[str, Any]:
1098
+ """
1099
+ Convert natural language statute text to Yuho code using LLM.
1100
+
1101
+ Uses a structured prompt to convert legal statute descriptions
1102
+ into valid Yuho DSL code with proper elements, penalties, and definitions.
1103
+
1104
+ Args:
1105
+ natural_text: Natural language description of a statute
1106
+
1107
+ Returns:
1108
+ {yuho_code: str, valid: bool, errors: list} or {error: str}
1109
+ """
1110
+ import asyncio
1111
+
1112
+ try:
1113
+ from yuho.llm import get_provider, STATUTE_TO_YUHO_PROMPT
1114
+ from yuho.parser import Parser
1115
+
1116
+ # Get LLM provider
1117
+ provider = get_provider()
1118
+ if not provider.is_available():
1119
+ return {"error": "No LLM provider available. Configure Ollama or an API key."}
1120
+
1121
+ # Build prompt using the specialized template
1122
+ prompt = STATUTE_TO_YUHO_PROMPT.format(statute_text=natural_text)
1123
+
1124
+ # Run synchronous LLM call in executor to not block
1125
+ loop = asyncio.get_event_loop()
1126
+ response = await loop.run_in_executor(
1127
+ None, lambda: provider.generate(prompt, max_tokens=2000)
1128
+ )
1129
+
1130
+ # Extract code block if present
1131
+ yuho_code = response.strip()
1132
+ if "```" in yuho_code:
1133
+ # Extract code from markdown code block
1134
+ import re
1135
+ code_match = re.search(r"```(?:yuho)?\s*\n(.*?)```", yuho_code, re.DOTALL)
1136
+ if code_match:
1137
+ yuho_code = code_match.group(1).strip()
1138
+
1139
+ # Validate the generated code
1140
+ parser = Parser()
1141
+ result = parser.parse(yuho_code)
1142
+
1143
+ if result.errors:
1144
+ return {
1145
+ "yuho_code": yuho_code,
1146
+ "valid": False,
1147
+ "errors": [
1148
+ {
1149
+ "message": err.message,
1150
+ "line": err.location.line,
1151
+ "col": err.location.col,
1152
+ }
1153
+ for err in result.errors
1154
+ ],
1155
+ "note": "LLM generated code with syntax errors. Manual correction may be needed.",
1156
+ }
1157
+
1158
+ return {
1159
+ "yuho_code": yuho_code,
1160
+ "valid": True,
1161
+ "errors": [],
1162
+ }
1163
+
1164
+ except ImportError as e:
1165
+ missing = str(e).split("'")[-2] if "'" in str(e) else "llm dependencies"
1166
+ return {"error": f"LLM provider not configured. Missing: {missing}. Install with: pip install yuho[llm]"}
1167
+ except Exception as e:
1168
+ return {"error": f"LLM generation failed: {str(e)}"}
1169
+
1170
+ @self.server.tool()
1171
+ async def yuho_rate_limit_stats() -> Dict[str, Any]:
1172
+ """
1173
+ Get rate limiting statistics.
1174
+
1175
+ Returns:
1176
+ Statistics about rate limiting including total requests,
1177
+ rate-limited requests, per-tool breakdown, and current token counts.
1178
+ """
1179
+ return self.rate_limiter.get_stats()
1180
+
1181
+ def _register_resources(self):
1182
+ """Register MCP resources."""
1183
+
1184
+ @self.server.resource("yuho://grammar")
1185
+ async def get_grammar() -> str:
1186
+ """Return the tree-sitter grammar source."""
1187
+ grammar_path = Path(__file__).parent.parent.parent / "tree-sitter-yuho" / "grammar.js"
1188
+ if grammar_path.exists():
1189
+ return grammar_path.read_text()
1190
+ return "// Grammar not found"
1191
+
1192
+ @self.server.resource("yuho://types")
1193
+ async def get_types() -> str:
1194
+ """Return built-in type definitions."""
1195
+ return """
1196
+ Yuho Built-in Types:
1197
+
1198
+ int - Integer numbers (e.g., 42, -10)
1199
+ float - Floating point numbers (e.g., 3.14, -2.5)
1200
+ bool - Boolean values (TRUE, FALSE)
1201
+ string - Text strings (e.g., "hello")
1202
+ money - Monetary amounts with currency (e.g., $100.00, SGD1000)
1203
+ percent - Percentages 0-100 (e.g., 50%)
1204
+ date - ISO8601 dates (e.g., 2024-01-15)
1205
+ duration - Time periods (e.g., 3 years, 6 months)
1206
+ void - No value / null type
1207
+ """
1208
+
1209
+ @self.server.resource("yuho://library/{section}")
1210
+ async def get_statute(section: str) -> str:
1211
+ """Return statute source by section number."""
1212
+ library_path = Path(__file__).parent.parent.parent.parent / "library"
1213
+ if library_path.exists():
1214
+ for yh_file in library_path.glob("**/*.yh"):
1215
+ if section in yh_file.stem:
1216
+ try:
1217
+ return yh_file.read_text()
1218
+ except Exception:
1219
+ pass
1220
+ return f"// Statute {section} not found in library"
1221
+
1222
+ @self.server.resource("yuho://docs/{topic}")
1223
+ async def get_docs(topic: str) -> str:
1224
+ """Return reference documentation for a topic."""
1225
+ docs = {
1226
+ "overview": """
1227
+ # Yuho Language Overview
1228
+
1229
+ Yuho is a domain-specific language for encoding legal statutes in a machine-readable format.
1230
+
1231
+ ## Key Features
1232
+ - Structured statute representation
1233
+ - Elements: actus_reus, mens_rea, circumstance
1234
+ - Penalty specifications
1235
+ - Pattern matching
1236
+ - Type system
1237
+
1238
+ ## File Extension
1239
+ .yh files
1240
+ """,
1241
+ "syntax": """
1242
+ # Yuho Syntax Reference
1243
+
1244
+ ## Statute Declaration
1245
+ ```
1246
+ statute "Section.Number" {
1247
+ title: "Statute Title"
1248
+
1249
+ elements {
1250
+ actus_reus element_name: condition_expr
1251
+ mens_rea intent_name: intent_type
1252
+ circumstance circ_name: circ_expr
1253
+ }
1254
+
1255
+ penalty {
1256
+ imprisonment { max: 10 years }
1257
+ fine { max: $10000 SGD }
1258
+ }
1259
+ }
1260
+ ```
1261
+
1262
+ ## Struct Definition
1263
+ ```
1264
+ struct PersonInfo {
1265
+ name: string
1266
+ age: int
1267
+ }
1268
+ ```
1269
+
1270
+ ## Function Definition
1271
+ ```
1272
+ fn is_adult(age: int) -> bool {
1273
+ return age >= 18
1274
+ }
1275
+ ```
1276
+ """,
1277
+ "types": """
1278
+ # Yuho Type System
1279
+
1280
+ ## Primitive Types
1281
+ - int: Integer numbers
1282
+ - float: Floating point
1283
+ - bool: TRUE or FALSE
1284
+ - string: Text strings
1285
+
1286
+ ## Legal Domain Types
1287
+ - money: Currency amounts ($100.00 SGD)
1288
+ - percent: Percentages (50%)
1289
+ - date: ISO dates (2024-01-15)
1290
+ - duration: Time periods (3 years)
1291
+
1292
+ ## Composite Types
1293
+ - struct: Named record types
1294
+ - optional: Type? for nullable
1295
+ - array: [Type] for lists
1296
+ """,
1297
+ "elements": """
1298
+ # Statute Elements
1299
+
1300
+ ## actus_reus (Guilty Act)
1301
+ The physical or conduct element of an offense.
1302
+ ```
1303
+ actus_reus caused_death: victim.died && defendant.action.caused(victim.death)
1304
+ ```
1305
+
1306
+ ## mens_rea (Guilty Mind)
1307
+ The mental element - intent or knowledge required.
1308
+ ```
1309
+ mens_rea intent_to_kill: intent.purpose || intent.knowledge
1310
+ ```
1311
+
1312
+ ## circumstance
1313
+ Additional circumstances that must exist.
1314
+ ```
1315
+ circumstance victim_human: victim.is_human
1316
+ ```
1317
+ """,
1318
+ "penalty": """
1319
+ # Penalty Specification
1320
+
1321
+ ## Imprisonment
1322
+ ```
1323
+ imprisonment {
1324
+ min: 2 years
1325
+ max: 20 years
1326
+ }
1327
+ ```
1328
+
1329
+ ## Fine
1330
+ ```
1331
+ fine {
1332
+ max: $500000 SGD
1333
+ }
1334
+ ```
1335
+
1336
+ ## Supplementary
1337
+ Additional penalties like caning, disqualification, etc.
1338
+ ```
1339
+ supplementary {
1340
+ caning: true
1341
+ disqualification: "driving"
1342
+ }
1343
+ ```
1344
+ """,
1345
+ }
1346
+ return docs.get(topic.lower(), f"# Topic '{topic}' not found\n\nAvailable topics: {', '.join(docs.keys())}")
1347
+
1348
+ def _register_prompts(self):
1349
+ """Register MCP prompts."""
1350
+
1351
+ @self.server.prompt("explain_statute")
1352
+ async def explain_statute_prompt(file_content: str) -> str:
1353
+ """Prompt for explaining a statute in plain English."""
1354
+ return f"""You are a legal expert explaining a statute encoded in Yuho.
1355
+
1356
+ Analyze the following Yuho code and explain:
1357
+ 1. What offense it defines
1358
+ 2. What elements must be proven
1359
+ 3. What penalties apply
1360
+
1361
+ Yuho code:
1362
+ ```yuho
1363
+ {file_content}
1364
+ ```
1365
+
1366
+ Provide a clear, structured explanation suitable for legal professionals."""
1367
+
1368
+ @self.server.prompt("convert_to_yuho")
1369
+ async def convert_to_yuho_prompt(natural_text: str) -> str:
1370
+ """Prompt for converting natural language statute to Yuho."""
1371
+ return f"""You are an expert in legal DSLs, specifically Yuho.
1372
+
1373
+ Convert the following legal statute text into valid Yuho code.
1374
+
1375
+ Statute text:
1376
+ {natural_text}
1377
+
1378
+ Requirements:
1379
+ 1. Use proper statute declaration syntax
1380
+ 2. Identify and encode all elements (actus_reus, mens_rea, circumstance)
1381
+ 3. Include penalty section if mentioned
1382
+ 4. Add definitions section for legal terms
1383
+ 5. Use appropriate types (money, duration, percent)
1384
+
1385
+ Output only valid Yuho code with comments explaining design decisions."""
1386
+
1387
+ @self.server.prompt("analyze_coverage")
1388
+ async def analyze_coverage_prompt(file_content: str) -> str:
1389
+ """Prompt for analyzing test coverage of a statute."""
1390
+ return f"""You are a legal testing expert.
1391
+
1392
+ Analyze the following Yuho statute and identify:
1393
+ 1. All condition branches that need testing
1394
+ 2. Edge cases for each element
1395
+ 3. Suggested test scenarios
1396
+
1397
+ Yuho code:
1398
+ ```yuho
1399
+ {file_content}
1400
+ ```
1401
+
1402
+ Provide a comprehensive test plan with specific values for each test case."""
1403
+
1404
+ def health_check(self) -> Dict[str, Any]:
1405
+ """Return server health status."""
1406
+ return {
1407
+ "status": "healthy",
1408
+ "name": "yuho-mcp",
1409
+ "version": "5.0.0",
1410
+ "tools_registered": True,
1411
+ "resources_registered": True,
1412
+ }
1413
+
1414
+ def run_stdio(self):
1415
+ """Run the server using stdio transport."""
1416
+ import asyncio
1417
+ asyncio.run(self._run_stdio())
1418
+
1419
+ async def _run_stdio(self):
1420
+ """Async stdio runner."""
1421
+ async with stdio_server() as streams:
1422
+ await self.server.run(
1423
+ streams[0],
1424
+ streams[1],
1425
+ self.server.create_initialization_options(),
1426
+ )
1427
+
1428
+ def run_http(self, host: str = "127.0.0.1", port: int = 8080):
1429
+ """Run the server using HTTP transport."""
1430
+ import asyncio
1431
+ from aiohttp import web
1432
+
1433
+ async def handle_mcp(request):
1434
+ # Simple HTTP handler for MCP
1435
+ data = await request.json()
1436
+ # Process MCP request...
1437
+ return web.json_response({"status": "ok"})
1438
+
1439
+ async def handle_health(request):
1440
+ """Health check endpoint."""
1441
+ return web.json_response(self.health_check())
1442
+
1443
+ app = web.Application()
1444
+ app.router.add_post("/mcp", handle_mcp)
1445
+ app.router.add_get("/health", handle_health)
1446
+
1447
+ web.run_app(app, host=host, port=port)
1448
+
1449
+
1450
+ def create_server() -> YuhoMCPServer:
1451
+ """Create and return a YuhoMCPServer instance."""
1452
+ return YuhoMCPServer()