|
6 | 6 |
|
7 | 7 | import asyncio |
8 | 8 | import json |
| 9 | +import logging |
9 | 10 |
|
10 | 11 | from mcp.server import Server |
11 | 12 | from mcp.server.stdio import stdio_server |
|
21 | 22 | # Initialize MCP Server |
22 | 23 | server = Server("minecode-server") |
23 | 24 |
|
| 25 | +# logging |
| 26 | +logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") |
| 27 | +logger = logging.getLogger("minecode.server") |
| 28 | + |
| 29 | +# Load central configuration and assistant preprompt (if enabled) |
| 30 | +from pathlib import Path |
| 31 | + |
| 32 | +_pkg_dir = Path(__file__).resolve().parent |
| 33 | +_config_file = _pkg_dir / "config" / "config.json" |
| 34 | + |
| 35 | +# attach default_preprompt to server so other modules can access it |
| 36 | +server.default_preprompt = None |
| 37 | + |
| 38 | +def _load_preprompt_from_config(): |
| 39 | + try: |
| 40 | + if _config_file.exists(): |
| 41 | + cfg = json.loads(_config_file.read_text(encoding="utf-8")) |
| 42 | + preprompt_enabled = cfg.get("preprompt_enabled", False) |
| 43 | + preprompt_path = cfg.get("preprompt_path") |
| 44 | + if preprompt_enabled and preprompt_path: |
| 45 | + # Try multiple candidate locations for the preprompt path: |
| 46 | + candidates = [] |
| 47 | + p = Path(preprompt_path) |
| 48 | + # Absolute path as-given |
| 49 | + if p.is_absolute(): |
| 50 | + candidates.append(p) |
| 51 | + |
| 52 | + # Package-relative (e.g. "preprompts/assistant_preprompt.txt") |
| 53 | + candidates.append((_pkg_dir / preprompt_path)) |
| 54 | + |
| 55 | + # Workspace/root-relative if the config used "minecode/..." or similar |
| 56 | + repo_root = _pkg_dir.parent |
| 57 | + candidates.append(repo_root / preprompt_path) |
| 58 | + |
| 59 | + # If path contains a nested "minecode/", try the suffix relative to package |
| 60 | + if "minecode/" in preprompt_path: |
| 61 | + suffix = preprompt_path.split("minecode/", 1)[1] |
| 62 | + candidates.append(_pkg_dir / suffix) |
| 63 | + |
| 64 | + found = None |
| 65 | + for c in candidates: |
| 66 | + try: |
| 67 | + cc = c.resolve() |
| 68 | + except Exception: |
| 69 | + cc = c |
| 70 | + logger.debug(f"Checking preprompt candidate: {cc}") |
| 71 | + if cc.exists(): |
| 72 | + found = cc |
| 73 | + break |
| 74 | + |
| 75 | + if found: |
| 76 | + server.default_preprompt = found.read_text(encoding="utf-8") |
| 77 | + logger.info(f"Loaded assistant preprompt from {found}") |
| 78 | + else: |
| 79 | + logger.info(f"Assistant preprompt not found; tried {len(candidates)} locations") |
| 80 | + except Exception as e: |
| 81 | + logger.exception("Failed loading preprompt from config") |
| 82 | + |
| 83 | + |
| 84 | +_load_preprompt_from_config() |
| 85 | + |
| 86 | +def get_preprompt_messages(): |
| 87 | + if server.default_preprompt: |
| 88 | + return [{"role": "system", "content": server.default_preprompt}] |
| 89 | + return [] |
| 90 | + |
| 91 | +server.get_preprompt_messages = get_preprompt_messages |
| 92 | + |
24 | 93 |
|
25 | 94 | # Tool definitions |
26 | 95 | TOOLS = [ |
|
47 | 116 | "version": { |
48 | 117 | "type": "string", |
49 | 118 | "description": "Minecraft version (e.g., 1.20.1, latest)" |
| 119 | + }, |
| 120 | + "datapack_path": { |
| 121 | + "type": "string", |
| 122 | + "description": "Path to a datapack folder containing pack.mcmeta to infer version" |
50 | 123 | } |
51 | 124 | }, |
52 | | - "required": ["version"] |
| 125 | + "required": [] |
53 | 126 | } |
54 | 127 | ), |
55 | 128 | Tool( |
@@ -482,30 +555,62 @@ def handle_hello_world(name: str = None) -> str: |
482 | 555 | return "Hello, World! Welcome to MineCode - Your Minecraft Datapack Development Assistant" |
483 | 556 |
|
484 | 557 |
|
485 | | -def handle_get_minecraft_version(version: str) -> dict: |
486 | | - """Handle get_minecraft_version tool""" |
487 | | - # Simulated version info |
488 | | - versions = { |
489 | | - "1.20.1": { |
490 | | - "release_date": "2023-12-07", |
491 | | - "snapshot": False, |
492 | | - "features": ["Decorated pots", "Armor trims", "Smithing templates"] |
493 | | - }, |
494 | | - "1.20": { |
495 | | - "release_date": "2023-06-07", |
496 | | - "snapshot": False, |
497 | | - "features": ["Cherry logs", "Painting variants", "Camel mob"] |
498 | | - }, |
499 | | - "latest": { |
500 | | - "release_date": "2024-01-18", |
501 | | - "snapshot": False, |
502 | | - "version": "1.20.4" |
503 | | - } |
504 | | - } |
505 | | - |
506 | | - if version in versions: |
507 | | - return {"success": True, "data": versions[version]} |
508 | | - return {"success": False, "error": f"Version {version} not found in database"} |
| 558 | +def handle_get_minecraft_version(version: str = None, datapack_path: str = None) -> dict: |
| 559 | + """Handle get_minecraft_version tool. |
| 560 | +
|
| 561 | + If `datapack_path` is provided, attempt to read `pack.mcmeta` and infer |
| 562 | + the pack_format, then use `misode` metadata to find matching Minecraft |
| 563 | + versions. If `version` is provided, return the version info from `misode`. |
| 564 | + """ |
| 565 | + # If a datapack path is provided, try to read pack.mcmeta and infer versions |
| 566 | + if datapack_path: |
| 567 | + from pathlib import Path |
| 568 | + try: |
| 569 | + p = Path(datapack_path) |
| 570 | + # allow passing either the folder or the direct path to pack.mcmeta |
| 571 | + pp = p / "pack.mcmeta" if p.is_dir() else p |
| 572 | + if not pp.exists(): |
| 573 | + return {"success": False, "error": f"pack.mcmeta not found at {pp}"} |
| 574 | + |
| 575 | + import json as _json |
| 576 | + content = _json.loads(pp.read_text(encoding="utf-8")) |
| 577 | + pack = content.get("pack") or {} |
| 578 | + pack_format = pack.get("pack_format") |
| 579 | + if pack_format is None: |
| 580 | + return {"success": False, "error": "pack_format not found in pack.mcmeta"} |
| 581 | + |
| 582 | + # Find versions in misode that match this data_pack_version |
| 583 | + try: |
| 584 | + candidates = [] |
| 585 | + for vid in misode.list_versions(): |
| 586 | + info = misode.get_version_info(vid) or {} |
| 587 | + dpv = info.get("data_pack_version") or info.get("dataPackVersion") or info.get("pack_format") |
| 588 | + if dpv is None: |
| 589 | + continue |
| 590 | + # compare as ints/strings |
| 591 | + try: |
| 592 | + if int(dpv) == int(pack_format): |
| 593 | + candidates.append({"version": vid, "data_pack_version": dpv}) |
| 594 | + except Exception: |
| 595 | + if str(dpv) == str(pack_format): |
| 596 | + candidates.append({"version": vid, "data_pack_version": dpv}) |
| 597 | + |
| 598 | + if candidates: |
| 599 | + return {"success": True, "pack_format": pack_format, "matches": candidates} |
| 600 | + return {"success": False, "pack_format": pack_format, "matches": [], "note": "No matching versions found in misode metadata"} |
| 601 | + except Exception as e: |
| 602 | + return {"success": False, "error": f"Error querying misode metadata: {e}"} |
| 603 | + except Exception as e: |
| 604 | + return {"success": False, "error": f"Failed reading pack.mcmeta: {e}"} |
| 605 | + |
| 606 | + # Fallback: if a version string is provided, try to get info from misode |
| 607 | + if version: |
| 608 | + info = misode.get_version_info(version) |
| 609 | + if info: |
| 610 | + return {"success": True, "version": version, "info": info} |
| 611 | + return {"success": False, "error": f"Version {version} not found in misode database"} |
| 612 | + |
| 613 | + return {"success": False, "error": "Either `version` or `datapack_path` must be provided"} |
509 | 614 |
|
510 | 615 |
|
511 | 616 | def handle_validate_datapack(datapack_path: str, mc_version: str) -> dict: |
@@ -965,7 +1070,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: |
965 | 1070 | if name == "hello_world": |
966 | 1071 | result = handle_hello_world(arguments.get("name")) |
967 | 1072 | elif name == "get_minecraft_version": |
968 | | - result = handle_get_minecraft_version(arguments["version"]) |
| 1073 | + result = handle_get_minecraft_version(arguments.get("version"), arguments.get("datapack_path")) |
969 | 1074 | elif name == "validate_datapack": |
970 | 1075 | result = handle_validate_datapack(arguments["datapack_path"], arguments["mc_version"]) |
971 | 1076 | elif name == "search_wiki": |
@@ -1085,15 +1190,22 @@ async def list_tools(): |
1085 | 1190 | async def _run(): |
1086 | 1191 | """Run the MCP server""" |
1087 | 1192 | async with stdio_server() as (read_stream, write_stream): |
1088 | | - await server.run( |
1089 | | - read_stream, |
1090 | | - write_stream, |
1091 | | - server.create_initialization_options() |
1092 | | - ) |
| 1193 | + logger.info("MineCode MCP server starting (stdio mode)") |
| 1194 | + logger.info(f"Registering {len(TOOLS)} tools") |
| 1195 | + try: |
| 1196 | + await server.run( |
| 1197 | + read_stream, |
| 1198 | + write_stream, |
| 1199 | + server.create_initialization_options() |
| 1200 | + ) |
| 1201 | + finally: |
| 1202 | + logger.info("MineCode MCP server stopped") |
1093 | 1203 |
|
1094 | 1204 |
|
1095 | 1205 | def main(): |
1096 | 1206 | """Entry point for the MCP server""" |
| 1207 | + logger.info("Starting MineCode MCP server (main entry)") |
| 1208 | + logger.info(f"Config file: {_config_file}") |
1097 | 1209 | asyncio.run(_run()) |
1098 | 1210 |
|
1099 | 1211 |
|
|
0 commit comments