Repository navigation
feat: mcp-server-basti upgrade — 8 new tools, refactor, hardening - #4
Merged
Merged
Conversation
…l_logging
Ersetzt den mcp[cli]-Shim (mcp.server.fastmcp) durch standalone fastmcp>=3.0.
Der Shim ist ein striktes Subset ohne tags/output_schema/timeout/run_in_thread;
nur standalone liefert die moderne API. Tools werden sync def — FastMCP lagert
sie via run_in_thread=True in den Threadpool aus, sodass asyncio.to_thread entfällt.
- pyproject.toml: drop mcp[cli]>=1.0, bump fastmcp>=2.0 -> fastmcp>=3.0
(mcp bleibt transitiv via fastmcp [server] extra; Integrationstests mit
mcp.ClientSession/mcp.client.stdio funktionieren weiterhin)
- server.py: imports from fastmcp/fastmcp.exceptions, ToolAnnotations from mcp.types
- ToolAnnotations(readOnlyHint=True,...) + tags + timeout=15 auf allen Tools
- get_system_status -> SystemStatus {uptime: str} (TypedDict)
- get_repo_info -> RepoInfo {branch, last_commit, detached: bool} (TypedDict,
ersetzt das freie "Branch: X\nLetzter Commit: Y" Format)
- echo_tool bleibt roher str (Health-Check)
- Neu: src/mcp_server_basti/instrumentation.py (@with_tool_logging Dekorator,
kapselt request_id/started_at/tool_call_start/success/error Boilerplate)
- Neu: src/mcp_server_basti/schemas.py (TypedDicts)
- Tests: test_repo_info/test_system_status/test_echo_tool auf sync + neue
Structur aktualisiert; neu test_instrumentation.py; ruff-clean (unused
pytest imports, trailing newlines) in bestehenden Integrationstests
Wire-Error-Verhalten erhalten: ToolError -> CallToolResult(isError=True).
Fallback falls fastmcp>=3.0 auf stdio regressiert: shim + structured_output=True
(verliert tags/timeout/explicit output_schema). Low risk — fastmcp.run(transport=
stdio) stabil in 3.4.5.
Co-Authored-By: Claude <noreply@anthropic.com>
Fügt sieben strukturierte Read-Only-Tools für die tägliche System-Analyse
auf der Workstation hinzu. Alle sync def, @with_tool_logging, ToolAnnotations
(readOnlyHint=True), tags={system,read-only}, timeout=15. TypedDicts in
src/mcp_server_basti/schemas.py; _run-Helfer kapselt subprocess.run→ToolError.
- get_disk_status: df -h --output=... → {filesystems: [Filesystem{...}]}
(keine du-Home-Consumer-Analyse — zu langsam für ein MCP-Tool, dafür
existiert yuno-cleaner scan)
- get_gpu_status: nvidia-smi --query-gpu=... + -q -d POWER → GpuStatus mit
driver/temp/util/vram/pstate + power_limit/default/max (first-GPU-wins,
bei Multi-GPU-Output nimmt der erste Abschnitt GPU 0 den Vorrang)
- get_memory_status: free -h + zramctl + swapon --show als rohe Text-Blöcke
- get_failed_units: systemctl --failed --no-pager → {failed: [unit], raw}
(filtert Unit-Namen am Punkt-Suffix; Summary-Zeile ignoriert)
- get_kernel_warnings: journalctl -b -p warning → roher str (parsen ist fragil)
- get_boot_timing: systemd-analyze blame + critical-chain → {blame: [...], critical_chain}
- get_power_profile: powerprofilesctl get → {profile: str}
Tests: 7 neue Unit-Test-Files mit gemocktem subprocess.run (CI-reproduzierbar,
keine echten Binaries nötig); tests/unit/conftest.py mit fake_subprocess/completed
Fixtures. test_tool_discovery EXPECTED_TOOLS → 10 Tools.
Live verifiziert auf der Workstation: / 92%, /mnt/DATA 95%, GPU 595.84/40°C/
25W(default 80W, max 115W), power-saver, 0 failed units.
Co-Authored-By: Claude <noreply@anthropic.com>
- get_firewall_state tool: sudo -n ufw status verbose + ss -tlnp, returns
FirewallState{ufw, listening_ports}. Degrades cleanly to ToolError
(pointing at SUDOERS_SETUP.md) when the NOPASSWD sudoers rule is missing
— no silent partial result.
- FirewallState TypedDict in schemas.py.
- docs/mcp-server/SUDOERS_SETUP.md: the narrow Cmnd_Alias NOPASSWD snippet
for /usr/sbin/ufw status verbose + /usr/bin/ss -tlnp, install/validate/
verify commands, degradation notes. Host-paths verified (command -v).
- tests/unit/test_firewall_state.py: mocked success, no-sudoers→ToolError
mentioning SUDOERS_SETUP.md, non-password CPE, OSError→ToolError.
- tests/integration/test_firewall_integration.py: local-only (BASTI_FW_TESTS
env gate) happy-path over stdio.
- test_tool_discovery.py EXPECTED_TOOLS: 10 → 11 (firewall always advertised).
Co-Authored-By: Claude <noreply@anthropic.com>
- TOOL_REFERENCE.md: 11-tool table, structured return types, fix
Fehlerformat (raises ToolError → wire isError:true, NOT
ToolResult is_error=True), fix get_repo_info (symbolic-ref + rev-parse
fallback, now structured {branch,last_commit,detached}), add
get_firewall_state example + SUDOERS_SETUP.md cross-link.
- USAGE.md: architecture/tool list updated to 11 tools; firewall sudoers
prereq note; --extra dev test invocation; BASTI_FW_TESTS gate hint.
- README.md: basti-tools table row + Tools section → 11 tools with
structured-return note + firewall sudoers prereq; fix hardcoded
/home/bratan/ZCodeProject/my-agent-tools → ${CLAUDE_PROJECT_ROOT}
(matches committed plugins/agent-toolkit/.mcp.json); FastMCP bullet
→ standalone >=3.0; --extra dev test invocation.
Co-Authored-By: Claude <noreply@anthropic.com>
- pyproject.toml: [tool.ruff] line-length=100, target-version=py311; [tool.ruff.lint] select=[E,F,W,I,UP,B] (deterministic — nicht extend-select, um ruff-Default-Regeln wie ISC/SIM auszuschließen); tests/** per-file E501-ignore (lange Kommando-Strings/fixture-Dicts). dev-extra: pytest-cov>=4.1, ruff>=0.6. addopts: --cov=src/mcp_server_basti --cov-report=term-missing --cov-report=xml. - ci.yml: lint scope src/ → src/ tests/; pytest läuft mit Coverage (report-only, kein --cov-fail-under-Gate — Schwelle kommt mit Baseline); coverage.xml wird als Artefakt hochgeladen. - .gitignore: .coverage, coverage.xml, *.cover, htmlcov/. Co-Authored-By: Claude <noreply@anthropic.com>
- test_system_status_integration.py: get_system_status Happy-Path über
stdio — verifiziert structuredContent{uptime} (bisher fehlend).
- test_repo_info_integration.py: get_repo_info Happy-Path über stdio —
structuredContent{branch,last_commit,detached} mit detached: bool.
- test_structured_output_integration.py: beweist den
standalone-fastmcp output_schema/structuredContent-Pfad end-to-end via
get_memory_status (free -h ist auf ubuntu-latest garantiert).
- test_wire_error_paths.py: REPO_ROOT/STDIO_TIMEOUT aus conftest
importieren (dedupe); not_git_wrapper schreibt ins pytest tmp_path statt
.tmp-test-not-repo.py ins Repo-Root.
Co-Authored-By: Claude <noreply@anthropic.com>
…<3.12 CI (Python 3.11) failed during test collection with "PydanticUserError: Please use typing_extensions.TypedDict instead of typing.TypedDict on Python < 3.12" — pydantic 2.x needs the typing_extensions variant so __pydantic_core_schema__ is set, which FastMCP relies on for output-schema derivation. Local venv (3.13) didn't reproduce it because typing.TypedDict works on 3.12+. - schemas.py: from typing_extensions import TypedDict (superset on 3.12+). - pyproject.toml: explicit typing_extensions>=4.12 dep (transitive via pydantic, but pinned for clarity). Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR upgrades mcp-server-basti to standalone FastMCP (>=3.0), expands it from a few basic tools to 11 read-only system diagnostic tools with structured (TypedDict) outputs, and updates docs/CI/tests to match.
Changes:
- Migrate to standalone
fastmcp>=3.0, refactor tool implementations to sync + shared@with_tool_logginginstrumentation, and introduce TypedDict schemas for structured outputs. - Add 8 new read-only system diagnostic tools (disk/gpu/memory/systemd/journalctl/systemd-analyze/powerprofiles/firewall state) and wire/integration tests.
- Update docs, CI, and dev dependencies (ruff + pytest-cov), plus ignore coverage artifacts.
Reviewed changes
Copilot reviewed 29 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Updates locked dependencies (drops mcp[cli], adds pytest-cov, ruff, coverage, etc.). |
| pyproject.toml | Switches to fastmcp>=3.0, adds dev deps, and configures ruff + coverage addopts. |
| src/mcp_server_basti/server.py | Major refactor: new tools, new _run helper, tool annotations/tags/timeouts, structured outputs. |
| src/mcp_server_basti/schemas.py | Adds TypedDict schemas for structured tool returns. |
| src/mcp_server_basti/instrumentation.py | Introduces @with_tool_logging decorator for consistent per-tool structured logging. |
| README.md | Updates tool list/docs, fixes hardcoded path to ${CLAUDE_PROJECT_ROOT}, adds firewall sudoers note. |
| docs/mcp-server/USAGE.md | Updates usage docs for 11 tools, adds lint/test/BASTI_FW_TESTS guidance. |
| docs/mcp-server/TOOL_REFERENCE.md | Updates tool reference table/types and error-format explanation. |
| docs/mcp-server/SUDOERS_SETUP.md | Adds sudoers setup instructions for get_firewall_state. |
| .github/workflows/ci.yml | Expands ruff scope to tests/, runs pytest with coverage, uploads coverage.xml artifact. |
| .gitignore | Ignores coverage artifacts (.coverage, coverage.xml, etc.). |
| tests/unit/conftest.py | Adds shared fixtures to mock subprocess.run deterministically in unit tests. |
| tests/unit/test_system_status.py | Updates unit tests for structured {uptime: str} return. |
| tests/unit/test_repo_info.py | Updates unit tests for structured {branch,last_commit,detached} return. |
| tests/unit/test_echo_tool.py | Converts async tests to sync; documents raw-string contract. |
| tests/unit/test_disk_status.py | Adds mocked unit tests for get_disk_status. |
| tests/unit/test_gpu_status.py | Adds mocked unit tests for get_gpu_status. |
| tests/unit/test_memory_status.py | Adds mocked unit tests for get_memory_status. |
| tests/unit/test_failed_units.py | Adds mocked unit tests for get_failed_units. |
| tests/unit/test_kernel_warnings.py | Adds mocked unit tests for get_kernel_warnings. |
| tests/unit/test_boot_timing.py | Adds mocked unit tests for get_boot_timing. |
| tests/unit/test_power_profile.py | Adds mocked unit tests for get_power_profile. |
| tests/unit/test_firewall_state.py | Adds mocked unit tests for get_firewall_state (incl. sudoers-missing behavior). |
| tests/unit/test_instrumentation.py | Adds unit tests for the @with_tool_logging decorator behavior. |
| tests/integration/test_tool_discovery.py | Updates expected tool set from 3 to 11. |
| tests/integration/test_wire_error_paths.py | Adjusts integration fixture to avoid writing wrapper into repo root; dedupes constants import. |
| tests/integration/test_system_status_integration.py | Adds stdio integration test for get_system_status structuredContent happy-path. |
| tests/integration/test_repo_info_integration.py | Adds stdio integration test for get_repo_info structuredContent happy-path. |
| tests/integration/test_structured_output_integration.py | Adds stdio integration test proving structuredContent works end-to-end (memory tool). |
| tests/integration/test_firewall_integration.py | Adds local-only (env-gated) stdio integration test for firewall tool. |
| tests/integration/test_error_handling.py | Minor cleanup (removes unused import). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+200
to
+203
| for line in lines[1:]: | ||
| parts = line.split() | ||
| if len(parts) != 7: | ||
| continue |
Comment on lines
+239
to
+244
| ) | ||
| fields = [f.strip() for f in proc.stdout.strip().split(",")] | ||
| if len(fields) < 7: | ||
| raise ToolError( | ||
| f"nvidia-smi lieferte unerwartetes Format: {proc.stdout!r}" | ||
| ) |
Comment on lines
+82
to
+84
| except (OSError, subprocess.SubprocessError, UnicodeDecodeError) as exc: | ||
| cmd = " ".join(argv) | ||
| raise ToolError(f"Kommando '{cmd}' fehlgeschlagen: {exc}") from exc |
Comment on lines
+14
to
+16
| - Pro-Aufruf-Logging ist im ``@with_tool_logging``-Dekorator | ||
| (``instrumentation.py``) gekapselt; die Tool-Body selbst sind reine Logik. | ||
| - Alle Tools advertise ``readOnlyHint=True`` und ``tags={"system","read-only"}``. |
Comment on lines
+5
to
+8
| Alle Tools advertise `readOnlyHint=True` (keine Mutationen) und — außer `echo_tool` — | ||
| `tags={"system","read-only"}`. Strukturierte Rückgaben sind TypedDicts aus | ||
| `src/mcp_server_basti/schemas.py`; FastMCP leitet das JSON-Schema automatisch ab und | ||
| liefert das Ergebnis als `structuredContent`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Comprehensive upgrade of
mcp-server-basti(the local stdio FastMCP server in theagent-toolkitplugin): from 3 trivial tools to 11 read-only system-diagnostic tools, switched to standalone FastMCP ≥3.0, hardened withToolAnnotations/tags/timeout, a@with_tool_loggingdecorator refactor, structured TypedDict returns, a firewall tool (with a prepared sudoers rule), docs/CI fixes, and full test coverage.Type of change
feat— new toolsrefactor— code change without new featuresdocs— documentation onlychore— maintenance (deps, config, scripts)What changed
Refactor (commit 1)
mcp[cli]shim to standalonefastmcp>=3.0— the shim's@tool()lackstags/output_schema/timeout/run_in_thread; standalone 3.4.5 supports all. Droppedmcp[cli]dep (mcpstays transitively via fastmcp's[server]extra).def— fastmcp's defaultrun_in_thread=Trueoffloads sync tools to a threadpool, soasyncio.to_threadboilerplate is gone.src/mcp_server_basti/instrumentation.py—@with_tool_logging()decorator extracts the per-toolrequest_id/started_at/start+success+error logging boilerplate. Applied innermost;@mcp.tool(...)outermost. Error path logsis_error=Truethen re-raises (soToolErrorstill becomes wireisError).get_repo_info→ structuredRepoInfo {branch, last_commit, detached}(was free-text).8 new read-only system tools (commits 2–3)
get_disk_statusdf -h --output=...DiskStatus{filesystems:[Filesystem]}get_gpu_statusnvidia-smi --query-gpu=...+-q -d POWERGpuStatus{...}(power_* may beNone)get_memory_statusfree -h+zramctl+swapon --showMemoryStatus{free,zram,swaps}get_failed_unitssystemctl --failedFailedUnits{failed:[str],raw}get_kernel_warningsjournalctl -b -p warningstr(raw)get_boot_timingsystemd-analyze blame+critical-chainBootTiming{blame:[BlameEntry],critical_chain}get_power_profilepowerprofilesctl getPowerProfile{profile}get_firewall_statesudo -n ufw status verbose+ss -tlnpFirewallState{ufw,listening_ports}All sync,
@with_tool_logging(),ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True),tags={"system","read-only"}(firewall:{"security","read-only"}),timeout=15.0. TypedDicts in newsrc/mcp_server_basti/schemas.py— FastMCP derives the JSON schema automatically and returnsstructuredContent.Firewall tool + sudoers (commit 3)
get_firewall_stateusessudo -n(non-interactive). Missing NOPASSWD grant →CalledProcessError→ToolErrorpointing at the setup doc (no silent partial result). Always advertised; runs only with the rule installed.docs/mcp-server/SUDOERS_SETUP.md— narrowCmnd_Alias MCP_BASTI_FW = /usr/sbin/ufw status verbose, /usr/bin/ss -tlnp+bratan ALL=(root) NOPASSWD: MCP_BASTI_FWsnippet, install/validate/verify commands. Basti installs this via explicit go-ahead (the PR does not install it).Docs fixes (commit 4)
TOOL_REFERENCE.md— 11-tool table, structured return types; fixed Fehlerformat section (raisesToolError→ wireisError: true, notToolResult is_error=True); fixedget_repo_info(git symbolic-ref --short HEAD+rev-parse --shortfallback, structured{branch,last_commit,detached}).USAGE.md— architecture/tool list → 11 tools; firewall sudoers prereq;--extra devtest invocation;BASTI_FW_TESTSgate hint.README.md—basti-toolstable row + Tools section → 11 tools; fixed hardcoded/home/bratan/ZCodeProject/my-agent-tools→${CLAUDE_PROJECT_ROOT}(matches committedplugins/agent-toolkit/.mcp.json); FastMCP bullet → standalone ≥3.0.CI / quality (commit 5)
pyproject.toml[tool.ruff]—line-length=100,target-version=py311,select=[E,F,W,I,UP,B](deterministicselect, notextend-select— avoids pulling in ruff's default ISC/SIM rules);tests/**per-fileE501-ignore.pytest-cov>=4.1dev extra;addopts="--cov=src/mcp_server_basti --cov-report=term-missing --cov-report=xml"(report-only, no--cov-fail-undergate — threshold comes once a baseline is measured).ci.yml— lint scopesrc/→src/ tests/; coverage report-only;coverage.xmluploaded as artifact..gitignore—.coverage,coverage.xml,*.cover,htmlcov/.Tests (commit 6)
test_disk_status,test_gpu_status(incl. multi-GPU first-wins, bad-format→ToolError, no-nvidia→ToolError),test_memory_status,test_failed_units,test_kernel_warnings,test_boot_timing,test_power_profile,test_firewall_state(success, no-sudoers→ToolErrormentioningSUDOERS_SETUP.md, non-password CPE,OSError→ToolError).test_system_status_integration,test_repo_info_integration,test_structured_output_integration(proves standalone-fastmcpstructuredContentend-to-end viaget_memory_status/free -h, guaranteed on ubuntu-latest).test_firewall_integration.py— local-only (@pytest.mark.skipif(not os.environ.get("BASTI_FW_TESTS"))).test_tool_discovery.py—EXPECTED_TOOLS10 → 11.test_wire_error_paths.py— importREPO_ROOT/STDIO_TIMEOUTfromconftest(dedupe); movednot_git_wrapperfixture totmp_path(stops writing.tmp-test-not-repo.pyinto the repo root).Verification
ruff check src/ tests/— clean (under the new[tool.ruff]config).uv run --extra dev pytest tests/unit tests/integration -v— 54 passed, 2 skipped (the 2 skipped are the local-onlyBASTI_FW_TESTS-gated firewall tests). Coverage 96%.bash scripts/check-mcp.sh—basti-toolssmoke-testok.readOnlyHint=True;call_tool("get_power_profile", {})→structuredContent={'profile': 'power-saver'}.Fallback note
If
fastmcp>=3.0ever regresses on stdio, revert imports to themcp[cli]shim withstructured_output=Trueon TypedDict returns (losestags/timeout/explicitoutput_schema). Low risk —fastmcp.run(transport="stdio")is stable in 3.4.5; the 6 integration tests (incl. wire-error paths) pass against it.Checklist
.envis in.gitignore) — the sudoers snippet and docs contain no credentials..mcp.jsonenv var (no new server; the firewall tool uses sudoers, not an env var).Related
agent-toolkitplugin work from PR feat: 8 themed skill packs + /toolkit command + session hook + split roadmap #3 (skill-packs). This PR is server-internal — it does not bump the plugin version; the marketplace sync (git -C ~/.claude/plugins/marketplaces/my-agent-tools pull+claude plugin update agent-toolkit@my-agent-tools) happens after merge.docs/mcp-server/SUDOERS_SETUP.md(user go-ahead, not part of this PR).🤖 Generated with Claude Code