pyDMNrules-enhanced 1.5.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.
@@ -0,0 +1,632 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ pyDMNrules MCP Server - DMN Decision Engine with FastMCP
4
+
5
+ pyDMNrules 엔진을 사용하여 DMN XML 파일을 로드하고 의사결정을 실행하는 MCP 서버입니다.
6
+ DMN XML 파일을 관리하고, 규칙별 입력 스키마를 제공하며, LLM이 적절한 형태로 추론을 호출할 수 있도록 합니다.
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Union
15
+
16
+ import aiofiles
17
+ from fastmcp import FastMCP
18
+ from pydantic import BaseModel, Field
19
+
20
+ # pyDMNrules import
21
+ try:
22
+ from pyDMNrules import DMN
23
+ PYDMNRULES_AVAILABLE = True
24
+ except ImportError:
25
+ PYDMNRULES_AVAILABLE = False
26
+ print("Error: pyDMNrules not available. Please install it: pip install pydmnrules-enhanced")
27
+
28
+
29
+ class DecisionResult(BaseModel):
30
+ """의사결정 결과를 나타내는 모델"""
31
+ result: Dict[str, Any] = Field(description="의사결정 결과")
32
+ trace: List[Dict[str, Any]] = Field(description="의사결정 경로 추적")
33
+ input_context: Dict[str, Any] = Field(description="입력 컨텍스트 (key-value)")
34
+ rule_name: str = Field(description="사용된 규칙 이름")
35
+ execution_time: Optional[float] = Field(None, description="실행 시간 (초)")
36
+ rule_schema: Optional[Dict[str, Any]] = Field(None, description="규칙별 입력 스키마")
37
+ engine_used: Optional[str] = Field(None, description="사용된 엔진 (pyDMNrules)")
38
+
39
+
40
+ class PyDMNrulesEngine:
41
+ """pyDMNrules 엔진 - DMN XML 파싱 및 실행"""
42
+
43
+ def __init__(self):
44
+ self.dmn_models: Dict[str, DMN] = {}
45
+ self.model_metadata: Dict[str, Dict[str, Any]] = {}
46
+
47
+ def load_dmn_xml(self, rule_name: str, xml_content: str) -> Dict[str, Any]:
48
+ """DMN XML을 로드하고 규칙 엔진을 구축합니다"""
49
+
50
+ if not PYDMNRULES_AVAILABLE:
51
+ raise RuntimeError("pyDMNrules is not available. Please install it.")
52
+
53
+ try:
54
+ # DMN 모델 인스턴스 생성
55
+ dmn_model = DMN()
56
+
57
+ # XML 문자열로부터 모델 로드
58
+ status = dmn_model.useXML(xml_content)
59
+
60
+ # 에러 체크
61
+ if 'errors' in status and len(status['errors']) > 0:
62
+ return {
63
+ "status": "error",
64
+ "message": f"Failed to load rule '{rule_name}'",
65
+ "errors": status['errors']
66
+ }
67
+
68
+ # 모델 저장
69
+ self.dmn_models[rule_name] = dmn_model
70
+
71
+ # 메타데이터 추출 - Glossary로부터 입력/출력 변수 정보 추출
72
+ metadata = self._extract_metadata(dmn_model, rule_name)
73
+ self.model_metadata[rule_name] = metadata
74
+
75
+ return {
76
+ "status": "success",
77
+ "message": f"Rule '{rule_name}' loaded successfully with pyDMNrules",
78
+ "engine_type": "pyDMNrules",
79
+ "inputs": metadata.get("inputs", {}),
80
+ "outputs": metadata.get("outputs", {}),
81
+ "decision_tables": metadata.get("decision_tables", [])
82
+ }
83
+
84
+ except Exception as e:
85
+ return {
86
+ "status": "error",
87
+ "message": f"Failed to load DMN XML: {str(e)}",
88
+ "errors": [str(e)]
89
+ }
90
+
91
+ def _extract_metadata(self, dmn_model: DMN, rule_name: str) -> Dict[str, Any]:
92
+ """DMN 모델로부터 메타데이터를 추출합니다"""
93
+ metadata = {
94
+ "loaded_at": time.time(),
95
+ "engine_type": "pyDMNrules",
96
+ "rule_name": rule_name,
97
+ "inputs": {},
98
+ "outputs": {},
99
+ "decision_tables": [],
100
+ "glossary": {},
101
+ "variable_names": [] # LLM이 사용해야 할 정확한 변수 이름 목록
102
+ }
103
+
104
+ # Glossary 정보 추출
105
+ if hasattr(dmn_model, 'glossary') and dmn_model.glossary:
106
+ for variable, info in dmn_model.glossary.items():
107
+ item = info.get('item', '')
108
+ concept = info.get('concept', '')
109
+
110
+ glossary_entry = {
111
+ "variable": variable,
112
+ "item": item,
113
+ "concept": concept,
114
+ "type": "string", # 기본 타입 (pyDMNrules에서 타입 정보가 제한적임)
115
+ "feel_name": item # FEEL 표현식에서 사용되는 이름
116
+ }
117
+
118
+ metadata["glossary"][variable] = glossary_entry
119
+ metadata["variable_names"].append(variable)
120
+
121
+ # 입력/출력 구분은 decision tables를 분석해야 정확하지만,
122
+ # 일단 모든 glossary 항목을 입력으로 간주
123
+ metadata["inputs"][variable] = glossary_entry
124
+
125
+ # Decision Tables 정보 추출
126
+ if hasattr(dmn_model, 'decisionTables') and dmn_model.decisionTables:
127
+ for table_name, table_info in dmn_model.decisionTables.items():
128
+ decision_table = {
129
+ "name": table_name,
130
+ "hit_policy": table_info.get('hitPolicy', 'U'),
131
+ "description": table_info.get('name', table_name)
132
+ }
133
+ metadata["decision_tables"].append(decision_table)
134
+
135
+ # Decisions 정보 추출
136
+ if hasattr(dmn_model, 'decisions') and dmn_model.decisions:
137
+ metadata["decisions"] = []
138
+ for decision in dmn_model.decisions:
139
+ if isinstance(decision, (list, tuple)) and len(decision) > 2:
140
+ metadata["decisions"].append({
141
+ "description": decision[1] if len(decision) > 1 else "",
142
+ "table": decision[2] if len(decision) > 2 else ""
143
+ })
144
+
145
+ return metadata
146
+
147
+ def execute_dmn_rule(self, rule_name: str, input_context: Dict[str, Any]) -> DecisionResult:
148
+ """DMN 규칙을 실행합니다"""
149
+ start_time = time.time()
150
+
151
+ if rule_name not in self.dmn_models:
152
+ execution_time = time.time() - start_time
153
+ return DecisionResult(
154
+ result={"error": f"Rule '{rule_name}' not loaded"},
155
+ trace=[{"step": 1, "rule": "error", "condition": "rule_not_loaded", "result": "error"}],
156
+ input_context=input_context,
157
+ rule_name=rule_name,
158
+ execution_time=execution_time,
159
+ rule_schema={"error": "Rule not loaded"},
160
+ engine_used="pyDMNrules"
161
+ )
162
+
163
+ try:
164
+ dmn_model = self.dmn_models[rule_name]
165
+
166
+ # pyDMNrules의 decide() 함수 호출
167
+ (status, result_data) = dmn_model.decide(input_context)
168
+
169
+ # 에러 체크
170
+ if 'errors' in status and len(status['errors']) > 0:
171
+ execution_time = time.time() - start_time
172
+ return DecisionResult(
173
+ result={"error": "Decision execution failed", "details": status['errors']},
174
+ trace=[{"step": 1, "error": str(status['errors'])}],
175
+ input_context=input_context,
176
+ rule_name=rule_name,
177
+ execution_time=execution_time,
178
+ rule_schema=self.model_metadata.get(rule_name, {}),
179
+ engine_used="pyDMNrules"
180
+ )
181
+
182
+ # 결과 파싱 및 trace 생성
183
+ trace = self._build_trace(result_data, input_context)
184
+ parsed_result = self._parse_result(result_data)
185
+
186
+ execution_time = time.time() - start_time
187
+
188
+ return DecisionResult(
189
+ result=parsed_result,
190
+ trace=trace,
191
+ input_context=input_context,
192
+ rule_name=rule_name,
193
+ execution_time=execution_time,
194
+ rule_schema=self.model_metadata.get(rule_name, {}),
195
+ engine_used="pyDMNrules"
196
+ )
197
+
198
+ except Exception as e:
199
+ execution_time = time.time() - start_time
200
+ return DecisionResult(
201
+ result={"error": f"Rule execution failed: {str(e)}"},
202
+ trace=[{"step": 1, "rule": "error", "condition": "execution_failed", "result": str(e)}],
203
+ input_context=input_context,
204
+ rule_name=rule_name,
205
+ execution_time=execution_time,
206
+ rule_schema=self.model_metadata.get(rule_name, {}),
207
+ engine_used="pyDMNrules"
208
+ )
209
+
210
+ def _parse_result(self, result_data: Any) -> Dict[str, Any]:
211
+ """pyDMNrules 결과를 파싱합니다"""
212
+
213
+ # result_data는 단일 decision dict이거나 decision dict의 list일 수 있음
214
+ if isinstance(result_data, list):
215
+ # 여러 decision tables가 실행된 경우
216
+ # 마지막 결과를 primary result로, 전체를 all_results로 저장
217
+ all_results = []
218
+ for item in result_data:
219
+ if isinstance(item, dict) and 'Result' in item:
220
+ all_results.append(item['Result'])
221
+
222
+ return {
223
+ "final_result": all_results[-1] if all_results else {},
224
+ "all_results": all_results,
225
+ "decision_count": len(all_results)
226
+ }
227
+
228
+ elif isinstance(result_data, dict):
229
+ # 단일 decision table 결과
230
+ if 'Result' in result_data:
231
+ return result_data['Result']
232
+ else:
233
+ return result_data
234
+
235
+ else:
236
+ return {"raw_result": str(result_data)}
237
+
238
+ def _build_trace(self, result_data: Any, input_context: Dict[str, Any]) -> List[Dict[str, Any]]:
239
+ """의사결정 경로 추적 정보를 생성합니다"""
240
+ trace = []
241
+
242
+ # 입력 정보 추가
243
+ trace.append({
244
+ "step": 1,
245
+ "action": "input",
246
+ "data": input_context,
247
+ "description": "Input context provided"
248
+ })
249
+
250
+ # result_data 분석
251
+ if isinstance(result_data, list):
252
+ # 여러 decision tables가 실행된 경우
253
+ for idx, item in enumerate(result_data):
254
+ step_info = {
255
+ "step": idx + 2,
256
+ "action": "decision_table",
257
+ "index": idx
258
+ }
259
+
260
+ if isinstance(item, dict):
261
+ if 'Executed Rule' in item:
262
+ executed_rule = item['Executed Rule']
263
+ if isinstance(executed_rule, tuple) and len(executed_rule) >= 3:
264
+ step_info["description"] = executed_rule[0]
265
+ step_info["table"] = executed_rule[1]
266
+ step_info["rule_id"] = executed_rule[2]
267
+
268
+ if 'Result' in item:
269
+ step_info["result"] = item['Result']
270
+
271
+ if 'RuleAnnotations' in item:
272
+ step_info["annotations"] = item['RuleAnnotations']
273
+
274
+ trace.append(step_info)
275
+
276
+ elif isinstance(result_data, dict):
277
+ # 단일 decision table 결과
278
+ step_info = {
279
+ "step": 2,
280
+ "action": "decision_table"
281
+ }
282
+
283
+ if 'Executed Rule' in result_data:
284
+ executed_rule = result_data['Executed Rule']
285
+ if isinstance(executed_rule, tuple) and len(executed_rule) >= 3:
286
+ step_info["description"] = executed_rule[0]
287
+ step_info["table"] = executed_rule[1]
288
+ step_info["rule_id"] = executed_rule[2]
289
+
290
+ if 'Result' in result_data:
291
+ step_info["result"] = result_data['Result']
292
+
293
+ if 'RuleAnnotations' in result_data:
294
+ step_info["annotations"] = result_data['RuleAnnotations']
295
+
296
+ trace.append(step_info)
297
+
298
+ return trace
299
+
300
+ def get_rule_schema(self, rule_name: str) -> Dict[str, Any]:
301
+ """규칙의 스키마 정보를 반환합니다"""
302
+ if rule_name in self.model_metadata:
303
+ metadata = self.model_metadata[rule_name]
304
+
305
+ # LLM이 이해하기 쉬운 형태로 스키마 구성
306
+ schema = {
307
+ "rule_name": rule_name,
308
+ "engine_type": "pyDMNrules",
309
+ "loaded_at": metadata.get("loaded_at", 0),
310
+ "inputs": {},
311
+ "outputs": {},
312
+ "decision_tables": metadata.get("decision_tables", []),
313
+ "example_input": {},
314
+ "variable_names": metadata.get("variable_names", []),
315
+ "important_note": "⚠️ Use EXACT variable names from 'variable_names' list as dictionary keys in context_input"
316
+ }
317
+
318
+ # 입력 필드 정보
319
+ for var_name, var_info in metadata.get("inputs", {}).items():
320
+ schema["inputs"][var_name] = {
321
+ "variable_name": var_name, # 정확한 변수 이름 (공백 포함 가능)
322
+ "feel_name": var_info.get('feel_name', ''), # FEEL 표현식 이름
323
+ "description": f"{var_info.get('concept', '')}.{var_info.get('item', '')}",
324
+ "type": var_info.get("type", "string"),
325
+ "required": True
326
+ }
327
+ schema["example_input"][var_name] = None
328
+
329
+ # 출력 필드 정보
330
+ for var_name, var_info in metadata.get("outputs", {}).items():
331
+ schema["outputs"][var_name] = {
332
+ "variable_name": var_name,
333
+ "feel_name": var_info.get('feel_name', ''),
334
+ "description": f"{var_info.get('concept', '')}.{var_info.get('item', '')}",
335
+ "type": var_info.get("type", "string")
336
+ }
337
+
338
+ return schema
339
+ else:
340
+ return {
341
+ "error": f"Rule '{rule_name}' not loaded",
342
+ "inputs": {},
343
+ "outputs": {}
344
+ }
345
+
346
+
347
+ class DMNModel:
348
+ """DMN 모델을 관리하는 클래스 (pyDMNrules)"""
349
+
350
+ def __init__(self, rules_dir: str = "rules"):
351
+ self.rules_dir = Path(rules_dir)
352
+ self.rules_dir.mkdir(exist_ok=True)
353
+ self.dmn_engine = PyDMNrulesEngine()
354
+
355
+ async def load_rule(self, rule_name: str) -> str:
356
+ """DMN 규칙을 로드합니다"""
357
+ rule_file = self.rules_dir / f"{rule_name}.dmn"
358
+
359
+ if not rule_file.exists():
360
+ # .dmn.xml 확장자도 시도
361
+ rule_file = self.rules_dir / f"{rule_name}.dmn.xml"
362
+ if not rule_file.exists():
363
+ raise FileNotFoundError(f"Rule '{rule_name}' not found (tried .dmn and .dmn.xml)")
364
+
365
+ async with aiofiles.open(rule_file, 'r', encoding='utf-8') as f:
366
+ content = await f.read()
367
+
368
+ # pyDMNrules 엔진으로 XML 로드
369
+ result = self.dmn_engine.load_dmn_xml(rule_name, content)
370
+
371
+ if result.get("status") == "error":
372
+ error_msg = result.get("message", "Unknown error")
373
+ errors = result.get("errors", [])
374
+ raise ValueError(f"{error_msg}: {errors}")
375
+
376
+ return result["message"]
377
+
378
+ async def save_rule(self, rule_name: str, xml_content: str) -> str:
379
+ """DMN 규칙을 저장합니다"""
380
+ rule_file = self.rules_dir / f"{rule_name}.dmn"
381
+
382
+ # XML 유효성 검사
383
+ try:
384
+ import xml.etree.ElementTree as ET
385
+ ET.fromstring(xml_content)
386
+ except ET.ParseError as e:
387
+ raise ValueError(f"Invalid DMN XML: {e}")
388
+
389
+ async with aiofiles.open(rule_file, 'w', encoding='utf-8') as f:
390
+ await f.write(xml_content)
391
+
392
+ # 저장 후 즉시 로드 시도
393
+ try:
394
+ self.dmn_engine.load_dmn_xml(rule_name, xml_content)
395
+ except Exception as e:
396
+ print(f"Warning: Failed to load saved XML: {e}")
397
+
398
+ return f"Rule '{rule_name}' saved successfully"
399
+
400
+ async def list_rules(self) -> List[str]:
401
+ """저장된 규칙 목록을 반환합니다"""
402
+ if not self.rules_dir.exists():
403
+ return []
404
+
405
+ rules = set()
406
+
407
+ # .dmn 파일 찾기
408
+ for file_path in self.rules_dir.glob("*.dmn"):
409
+ if not file_path.name.endswith('.xml'):
410
+ rules.add(file_path.stem)
411
+
412
+ # .dmn.xml 파일 찾기
413
+ for file_path in self.rules_dir.glob("*.dmn.xml"):
414
+ rule_name = file_path.name.replace('.dmn.xml', '')
415
+ rules.add(rule_name)
416
+
417
+ return sorted(list(rules))
418
+
419
+ async def delete_rule(self, rule_name: str) -> str:
420
+ """DMN 규칙을 삭제합니다"""
421
+ deleted = False
422
+
423
+ # .dmn 파일 삭제 시도
424
+ rule_file = self.rules_dir / f"{rule_name}.dmn"
425
+ if rule_file.exists():
426
+ rule_file.unlink()
427
+ deleted = True
428
+
429
+ # .dmn.xml 파일 삭제 시도
430
+ rule_file_xml = self.rules_dir / f"{rule_name}.dmn.xml"
431
+ if rule_file_xml.exists():
432
+ rule_file_xml.unlink()
433
+ deleted = True
434
+
435
+ if not deleted:
436
+ raise FileNotFoundError(f"Rule '{rule_name}' not found")
437
+
438
+ # 엔진에서도 제거
439
+ if rule_name in self.dmn_engine.dmn_models:
440
+ del self.dmn_engine.dmn_models[rule_name]
441
+ if rule_name in self.dmn_engine.model_metadata:
442
+ del self.dmn_engine.model_metadata[rule_name]
443
+
444
+ return f"Rule '{rule_name}' deleted successfully"
445
+
446
+ async def get_rule_schema(self, rule_name: str) -> Dict[str, Any]:
447
+ """규칙의 입력 스키마를 반환합니다"""
448
+ return self.dmn_engine.get_rule_schema(rule_name)
449
+
450
+ async def evaluate_decision(self, rule_name: str, input_context: Dict[str, Any]) -> DecisionResult:
451
+ """DMN 규칙을 실행하여 의사결정을 수행합니다"""
452
+ return self.dmn_engine.execute_dmn_rule(rule_name, input_context)
453
+
454
+
455
+ # FastMCP 서버 초기화
456
+ mcp = FastMCP("pydmnrules-mcp-server")
457
+ dmn_model = DMNModel()
458
+
459
+
460
+ @mcp.tool()
461
+ async def load_rule(rule_name: str) -> str:
462
+ """
463
+ 지정된 이름의 DMN XML 규칙을 로드합니다. pyDMNrules 엔진을 사용합니다.
464
+
465
+ Args:
466
+ rule_name: 로드할 규칙의 이름 (확장자 제외)
467
+
468
+ Returns:
469
+ 로드 결과 메시지
470
+ """
471
+ try:
472
+ return await dmn_model.load_rule(rule_name)
473
+ except Exception as e:
474
+ return f"Error loading rule '{rule_name}': {str(e)}"
475
+
476
+
477
+ @mcp.tool()
478
+ async def save_rule(rule_name: str, xml_content: str) -> str:
479
+ """
480
+ 새로운 DMN 규칙을 저장합니다.
481
+
482
+ Args:
483
+ rule_name: 저장할 규칙의 이름
484
+ xml_content: DMN XML 내용
485
+
486
+ Returns:
487
+ 저장 결과 메시지
488
+ """
489
+ try:
490
+ return await dmn_model.save_rule(rule_name, xml_content)
491
+ except Exception as e:
492
+ return f"Error saving rule '{rule_name}': {str(e)}"
493
+
494
+
495
+ @mcp.tool()
496
+ async def list_rules() -> List[str]:
497
+ """
498
+ 등록된 DMN 규칙 목록을 조회합니다.
499
+
500
+ Returns:
501
+ 규칙 이름 목록
502
+ """
503
+ try:
504
+ return await dmn_model.list_rules()
505
+ except Exception as e:
506
+ return [f"Error listing rules: {str(e)}"]
507
+
508
+
509
+ @mcp.tool()
510
+ async def delete_rule(rule_name: str) -> str:
511
+ """
512
+ DMN 규칙을 삭제합니다.
513
+
514
+ Args:
515
+ rule_name: 삭제할 규칙의 이름
516
+
517
+ Returns:
518
+ 삭제 결과 메시지
519
+ """
520
+ try:
521
+ return await dmn_model.delete_rule(rule_name)
522
+ except Exception as e:
523
+ return f"Error deleting rule '{rule_name}': {str(e)}"
524
+
525
+
526
+ @mcp.tool()
527
+ async def get_rule_schema(rule_name: str) -> Dict[str, Any]:
528
+ """
529
+ 규칙의 입력 스키마를 조회합니다.
530
+
531
+ Args:
532
+ rule_name: 규칙 이름
533
+
534
+ Returns:
535
+ 규칙의 입력 스키마 (엔진별 메타데이터 포함)
536
+ """
537
+ try:
538
+ return await dmn_model.get_rule_schema(rule_name)
539
+ except Exception as e:
540
+ return {"error": f"Error getting schema for rule '{rule_name}': {str(e)}"}
541
+
542
+
543
+ @mcp.tool()
544
+ async def infer_decision(rule_name: str, context_input: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
545
+ """
546
+ 지정된 DMN 규칙을 기반으로 의사결정을 실행합니다. pyDMNrules 엔진을 사용합니다.
547
+
548
+ Args:
549
+ rule_name: 사용할 DMN 규칙의 이름
550
+ context_input: key-value 딕셔너리 형태의 입력 데이터
551
+
552
+ Returns:
553
+ 의사결정 결과 (result, trace, input_context, rule_name, execution_time, rule_schema, engine_used 포함)
554
+ """
555
+ try:
556
+ # 입력 컨텍스트 생성
557
+ if isinstance(context_input, str):
558
+ try:
559
+ input_context = json.loads(context_input)
560
+ except json.JSONDecodeError:
561
+ input_context = {"raw_input": context_input}
562
+ else:
563
+ input_context = context_input
564
+
565
+ # 의사결정 실행
566
+ result = await dmn_model.evaluate_decision(rule_name, input_context)
567
+
568
+ return {
569
+ "result": result.result,
570
+ "trace": result.trace,
571
+ "input_context": result.input_context,
572
+ "rule_name": result.rule_name,
573
+ "execution_time": result.execution_time,
574
+ "rule_schema": result.rule_schema,
575
+ "engine_used": result.engine_used
576
+ }
577
+ except Exception as e:
578
+ return {
579
+ "error": f"Error executing decision: {str(e)}",
580
+ "result": {},
581
+ "trace": [],
582
+ "input_context": {},
583
+ "rule_name": rule_name,
584
+ "execution_time": 0,
585
+ "rule_schema": None,
586
+ "engine_used": "error"
587
+ }
588
+
589
+
590
+ @mcp.tool()
591
+ async def check_engine_status() -> Dict[str, Any]:
592
+ """
593
+ pyDMNrules 엔진의 상태를 확인합니다.
594
+
595
+ Returns:
596
+ 엔진 상태 정보
597
+ """
598
+ return {
599
+ "pydmnrules_available": PYDMNRULES_AVAILABLE,
600
+ "message": f"pyDMNrules Engine - Available: {PYDMNRULES_AVAILABLE}",
601
+ "loaded_models": list(dmn_model.dmn_engine.dmn_models.keys()),
602
+ "total_loaded_models": len(dmn_model.dmn_engine.dmn_models),
603
+ "rules_directory": str(dmn_model.rules_dir.absolute())
604
+ }
605
+
606
+
607
+ def main():
608
+ """메인 함수 - 서버 실행"""
609
+ __version__ = "1.0.0"
610
+
611
+ print(f"Starting pyDMNrules MCP Server v{__version__}")
612
+ print(f"pyDMNrules available: {PYDMNRULES_AVAILABLE}")
613
+ print("Supports DMN XML files (.dmn, .dmn.xml)")
614
+ print(f"Rules directory: {dmn_model.rules_dir.absolute()}")
615
+
616
+ if not PYDMNRULES_AVAILABLE:
617
+ print("\n⚠️ WARNING: pyDMNrules is not installed!")
618
+ print("Install with: pip install pydmnrules-enhanced")
619
+ return
620
+
621
+ try:
622
+ # stdio 모드로 실행
623
+ mcp.run()
624
+ except KeyboardInterrupt:
625
+ print("\nServer stopped by user")
626
+ except Exception as e:
627
+ print(f"\nServer error: {e}")
628
+
629
+
630
+ if __name__ == "__main__":
631
+ main()
632
+