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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- 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()
|