Skip to content

Commit d3d1003

Browse files
committed
feat(mcp): add diagnostics tools
1 parent 4078400 commit d3d1003

13 files changed

Lines changed: 129 additions & 9 deletions

File tree

backend/auth/buckets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
("GET", "/api/servers/<server_id>/logs"),
2323
("GET", "/api/servers/<server_id>/mods"),
2424
("GET", "/api/servers/<server_id>/install/progress"),
25+
("GET", "/api/servers/<server_id>/java-status"),
2526
("GET", "/api/servers/<server_id>/metrics"),
2627
("GET", "/api/metrics/system"),
2728
("GET", "/api/java/status"),

backend/server/routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,13 @@ def get_server_details(server_id, server):
498498
return jsonify(_augment_with_runtime(server))
499499

500500

501+
@server_bp.route('/servers/<server_id>/java-status', methods=['GET'])
502+
@require_server
503+
def get_server_java_status(server_id, server):
504+
"""Report the effective runtime and Java compatibility for one server."""
505+
return jsonify(_server_java_check_payload(server))
506+
507+
501508
@server_bp.route('/servers/<server_id>/logs', methods=['GET'])
502509
def get_server_logs(server_id):
503510
# type=int yields None on unparseable input, so fall back explicitly rather

mcp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ The token's scope decides this, and the **panel** enforces it — not this packa
6262

6363
| Scope | What it reaches |
6464
|---|---|
65-
| `read` | List servers and their status · read console output and crash logs · list installed mods and identify them against Modrinth by file hash · CPU and memory use · Java version checks · loader and Minecraft-version discovery · install progress and failure reasons · backup coverage and snapshot metadata · search Modrinth and check whether a mod has a build for your Minecraft version and loader |
65+
| `read` | List servers and their status · read console output and crash logs · list/identify installed mods and audit their compatible updates · batch-check Modrinth project compatibility · inspect exact Modrinth versions and search modpacks (catalog only) · CPU and memory use · server-specific Java/runtime and install diagnostics · loader and Minecraft-version discovery · backup coverage, schedules, and snapshot metadata · search Modrinth |
6666
| `manage` | Everything above, plus: start / stop / restart a server · start or retry server installation using its saved configuration · install or update one mod by Modrinth project id · delete installed mod jars |
6767

6868
**`read` is the documented default.** It answers every diagnostic question and cannot change

mcp/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fabricator-mcp"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = "MCP server for the Fabricator Minecraft panel"
55
readme = "README.md"
66
requires-python = ">=3.11"

mcp/src/fabricator_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99
__all__ = ["__version__"]
1010

11-
__version__ = "0.2.0"
11+
__version__ = "0.3.0"

mcp/src/fabricator_mcp/_panel_routes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from __future__ import annotations
1818

1919
#: Date of the last human re-audit of this snapshot.
20-
PANEL_TABLE_REVISION = "2026-08-06"
20+
PANEL_TABLE_REVISION = "2026-08-19"
2121

2222
PANEL_READ: frozenset[tuple[str, str]] = frozenset({
2323
("GET", "/api/auth/status"),
@@ -48,6 +48,7 @@
4848
("GET", "/api/servers/<server_id>/backup-configs"),
4949
("GET", "/api/servers/<server_id>/backup-summary"),
5050
("GET", "/api/servers/<server_id>/install/progress"),
51+
("GET", "/api/servers/<server_id>/java-status"),
5152
("GET", "/api/servers/<server_id>/logs"),
5253
("GET", "/api/servers/<server_id>/metrics"),
5354
("GET", "/api/servers/<server_id>/mods"),

mcp/src/fabricator_mcp/routes.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@
5656
"get_install_progress": (
5757
("GET", "/api/servers/<server_id>/install/progress"),
5858
),
59+
"search_modpacks": (("GET", "/api/modrinth/modpacks/search"),),
60+
"get_mod_version": (("GET", "/api/modrinth/version/<version_id>"),),
61+
"check_installed_mod_updates": (
62+
("GET", "/api/servers/<server_id>"),
63+
("GET", "/api/servers/<server_id>/mods"),
64+
("GET", "/api/modrinth/servers/<server_id>/resolve-installed"),
65+
("GET", "/api/modrinth/project/<project_id>/resolve-version"),
66+
),
67+
"check_mods_compatibility": (("GET", "/api/modrinth/project/<project_id>/resolve-version"),),
68+
"get_server_runtime_diagnostics": (
69+
("GET", "/api/servers/<server_id>/java-status"),
70+
("GET", "/api/servers/<server_id>/install/progress"),
71+
),
72+
"list_backup_configs": (("GET", "/api/servers/<server_id>/backup-configs"),),
5973
"check_panel": (
6074
("GET", "/api/health"),
6175
("GET", "/api/auth/status"),
@@ -101,6 +115,12 @@
101115
"get_backup_status": READ,
102116
"list_snapshots": READ,
103117
"get_install_progress": READ,
118+
"search_modpacks": READ,
119+
"get_mod_version": READ,
120+
"check_installed_mod_updates": READ,
121+
"check_mods_compatibility": READ,
122+
"get_server_runtime_diagnostics": READ,
123+
"list_backup_configs": READ,
104124
"check_panel": READ,
105125
"search_modrinth": READ,
106126
"get_mod_info": READ,

mcp/src/fabricator_mcp/server.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,36 @@ async def check_mod_compatibility(
134134
server as configured.
135135
"""
136136
return await read_tools.check_mod_compatibility(client, project_id, mc_version, loader)
137+
@server.tool()
138+
async def search_modpacks(query: str, mc_version: str | None = None, loader: str | None = None, limit: int = 10) -> dict[str, Any]:
139+
"""Search Modrinth modpacks. Installing a modpack remains panel-UI only."""
140+
return await read_tools.search_modpacks(client, query, mc_version, loader, limit)
141+
142+
@server.tool()
143+
async def get_mod_version(version_id: str) -> dict[str, Any]:
144+
"""Inspect one exact Modrinth version before choosing an update."""
145+
return await read_tools.get_mod_version(client, version_id)
146+
147+
@server.tool()
148+
async def check_installed_mod_updates(server_id: str) -> dict[str, Any]:
149+
"""Find compatible updates for identified installed mods; reports at most 25."""
150+
return await read_tools.check_installed_mod_updates(client, server_id)
151+
152+
@server.tool()
153+
async def check_mods_compatibility(project_ids: list[str], mc_version: str, loader: str | None = None) -> dict[str, Any]:
154+
"""Batch-check up to 25 Modrinth projects for a target loader and Minecraft version."""
155+
return await read_tools.check_mods_compatibility(client, project_ids, mc_version, loader)
156+
157+
@server.tool()
158+
async def get_server_runtime_diagnostics(server_id: str) -> dict[str, Any]:
159+
"""Show this server's effective Java compatibility and current install progress."""
160+
return await read_tools.get_server_runtime_diagnostics(client, server_id)
161+
162+
@server.tool()
163+
async def list_backup_configs(server_id: str) -> dict[str, Any]:
164+
"""List backup schedules and retention without exposing storage paths or mutation."""
165+
return await read_tools.list_backup_configs(client, server_id)
166+
137167

138168

139169
def register_manage_tools(server, client: PanelClient) -> None:

mcp/src/fabricator_mcp/tools/read.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,67 @@ async def get_install_progress(client: PanelClient, server_id: str) -> dict[str,
214214
})
215215

216216

217+
async def search_modpacks(client: PanelClient, query: str, mc_version: str | None = None, loader: str | None = None, limit: int = DEFAULT_SEARCH_LIMIT) -> dict[str, Any]:
218+
"""Search the catalog only; installing modpacks remains intentionally unavailable."""
219+
if not isinstance(query, str) or not query.strip():
220+
raise ValueError("query is required")
221+
payload = await client.get("/api/modrinth/modpacks/search", params={"query": query.strip()[:128], "mc_version": mc_version, "loader": loader, "limit": clamp(limit, 1, 50, DEFAULT_SEARCH_LIMIT)})
222+
hits = payload.get("hits") if isinstance(payload, dict) else []
223+
hits = hits if isinstance(hits, list) else []
224+
return {"results": [drop_empty({"projectId": h.get("project_id"), "slug": h.get("slug"), "title": h.get("title"), "description": h.get("description"), "downloads": h.get("downloads")}) for h in hits if isinstance(h, dict)]}
225+
226+
227+
async def get_mod_version(client: PanelClient, version_id: str) -> dict[str, Any]:
228+
version_id = _require(version_id, "version_id")
229+
payload = await client.get(f"/api/modrinth/version/{version_id}")
230+
payload = payload if isinstance(payload, dict) else {}
231+
return drop_empty({"versionId": payload.get("id"), "projectId": payload.get("project_id"), "versionNumber": payload.get("version_number"), "gameVersions": payload.get("game_versions"), "loaders": payload.get("loaders"), "name": payload.get("name")})
232+
233+
234+
async def check_mods_compatibility(client: PanelClient, project_ids: list[str], mc_version: str, loader: str | None = None) -> dict[str, Any]:
235+
if not isinstance(project_ids, list) or not project_ids:
236+
raise ValueError("project_ids is required")
237+
if not isinstance(mc_version, str) or not mc_version.strip():
238+
raise ValueError("mc_version is required")
239+
results = []
240+
for project_id in project_ids[:25]:
241+
project_id = _require(project_id, "project_id")
242+
result = await check_mod_compatibility(client, project_id, mc_version, loader)
243+
results.append({"projectId": project_id, **result})
244+
return {"minecraftVersion": mc_version.strip(), "loader": loader, "results": results, "truncated": len(project_ids) > 25}
245+
246+
247+
async def check_installed_mod_updates(client: PanelClient, server_id: str) -> dict[str, Any]:
248+
server_id = _require(server_id, "server_id")
249+
server = await client.get(f"/api/servers/{server_id}")
250+
server = server if isinstance(server, dict) else {}
251+
listing = await list_installed_mods(client, server_id, identify=True)
252+
version, loader = server.get("version"), server.get("loader")
253+
results = []
254+
for mod in listing.get("mods", [])[:25]:
255+
project_id = mod.get("projectId") if isinstance(mod, dict) else None
256+
if not project_id or not version:
257+
continue
258+
compatible = await check_mod_compatibility(client, project_id, version, loader)
259+
target = compatible.get("versionId")
260+
results.append(drop_empty({"name": mod.get("name"), "projectId": project_id, "currentVersionId": mod.get("versionId"), "currentVersionNumber": mod.get("versionNumber"), "targetVersionId": target, "targetVersionNumber": compatible.get("versionNumber"), "updateAvailable": bool(target and target != mod.get("versionId")), "compatible": compatible.get("compatible")}))
261+
return {"minecraftVersion": version, "loader": loader, "updates": results, "identified": listing.get("identified", False), "truncated": len(listing.get("mods", [])) > 25}
262+
263+
264+
async def get_server_runtime_diagnostics(client: PanelClient, server_id: str) -> dict[str, Any]:
265+
server_id = _require(server_id, "server_id")
266+
java, install = await client.get(f"/api/servers/{server_id}/java-status"), await client.get(f"/api/servers/{server_id}/install/progress")
267+
java, install = (java if isinstance(java, dict) else {}), (install if isinstance(install, dict) else {})
268+
return drop_empty({"requiredMajor": java.get("required_java"), "detectedMajor": java.get("detected_java"), "meetsRequirement": java.get("meets_requirement"), "enforcementSkipped": java.get("java_enforcement_skipped"), "install": drop_empty({"active": install.get("active"), "phase": install.get("phase"), "error": install.get("error"), "updatedAt": install.get("updated_at")})})
269+
270+
271+
async def list_backup_configs(client: PanelClient, server_id: str) -> dict[str, Any]:
272+
server_id = _require(server_id, "server_id")
273+
payload = await client.get(f"/api/servers/{server_id}/backup-configs")
274+
configs = payload if isinstance(payload, list) else []
275+
return {"configs": [drop_empty({"id": c.get("id"), "name": c.get("name"), "enabled": c.get("enabled"), "schedule": c.get("schedule"), "retention": c.get("retention"), "nextRunTime": c.get("next_run_time")}) for c in configs if isinstance(c, dict)]}
276+
277+
217278
#: What the panel reports when it has no release marker to read — a source
218279
#: checkout has no .fabricator_version, only a built release does.
219280
_UNKNOWN_VERSION = "unknown"

mcp/tests/test_tool_routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,4 @@ def test_the_tool_set_is_narrower_than_the_ceiling():
7272
"""Curation, stated as a fact: tools use a strict subset of the permitted routes."""
7373
used = {route for routes in TOOL_ROUTES.values() for route in routes}
7474
assert used < PANEL_TOKEN_REACHABLE
75-
assert len(TOOL_ROUTES) == 19
75+
assert len(TOOL_ROUTES) == 25

0 commit comments

Comments
 (0)