Skip to content

feat: mcp-server-basti upgrade — 8 new tools, refactor, hardening - #4

Merged
Toqsick merged 7 commits into
mainfrom
feat/mcp-server-basti-upgrade
Aug 5, 2026
Merged

Toqsick merged 7 commits into
mainfrom
feat/mcp-server-basti-upgrade

Conversation

@Toqsick

@Toqsick Toqsick commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Comprehensive upgrade of mcp-server-basti (the local stdio FastMCP server in the agent-toolkit plugin): from 3 trivial tools to 11 read-only system-diagnostic tools, switched to standalone FastMCP ≥3.0, hardened with ToolAnnotations/tags/timeout, a @with_tool_logging decorator refactor, structured TypedDict returns, a firewall tool (with a prepared sudoers rule), docs/CI fixes, and full test coverage.

Type of change

  • feat — new tools
  • refactor — code change without new features
  • docs — documentation only
  • chore — maintenance (deps, config, scripts)

What changed

Refactor (commit 1)

  • Switched from the mcp[cli] shim to standalone fastmcp>=3.0 — the shim's @tool() lacks tags/output_schema/timeout/run_in_thread; standalone 3.4.5 supports all. Dropped mcp[cli] dep (mcp stays transitively via fastmcp's [server] extra).
  • Tools converted to sync def — fastmcp's default run_in_thread=True offloads sync tools to a threadpool, so asyncio.to_thread boilerplate is gone.
  • New src/mcp_server_basti/instrumentation.py — @with_tool_logging() decorator extracts the per-tool request_id/started_at/start+success+error logging boilerplate. Applied innermost; @mcp.tool(...) outermost. Error path logs is_error=True then re-raises (so ToolError still becomes wire isError).
  • get_repo_info → structured RepoInfo {branch, last_commit, detached} (was free-text).

8 new read-only system tools (commits 2–3)

Tool Command(s) Return
get_disk_status df -h --output=... DiskStatus{filesystems:[Filesystem]}
get_gpu_status nvidia-smi --query-gpu=... + -q -d POWER GpuStatus{...} (power_* may be None)
get_memory_status free -h + zramctl + swapon --show MemoryStatus{free,zram,swaps}
get_failed_units systemctl --failed FailedUnits{failed:[str],raw}
get_kernel_warnings journalctl -b -p warning str (raw)
get_boot_timing systemd-analyze blame + critical-chain BootTiming{blame:[BlameEntry],critical_chain}
get_power_profile powerprofilesctl get PowerProfile{profile}
get_firewall_state sudo -n ufw status verbose + ss -tlnp FirewallState{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 new src/mcp_server_basti/schemas.py — FastMCP derives the JSON schema automatically and returns structuredContent.

Firewall tool + sudoers (commit 3)

  • get_firewall_state uses sudo -n (non-interactive). Missing NOPASSWD grant → CalledProcessError → ToolError pointing at the setup doc (no silent partial result). Always advertised; runs only with the rule installed.
  • docs/mcp-server/SUDOERS_SETUP.md — narrow Cmnd_Alias MCP_BASTI_FW = /usr/sbin/ufw status verbose, /usr/bin/ss -tlnp + bratan ALL=(root) NOPASSWD: MCP_BASTI_FW snippet, 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 (raises ToolError → wire isError: true, not ToolResult is_error=True); fixed get_repo_info (git symbolic-ref --short HEAD + rev-parse --short fallback, structured {branch,last_commit,detached}).
  • USAGE.md — architecture/tool list → 11 tools; firewall sudoers prereq; --extra dev test invocation; BASTI_FW_TESTS gate hint.
  • README.md — basti-tools table row + Tools section → 11 tools; fixed hardcoded /home/bratan/ZCodeProject/my-agent-tools → ${CLAUDE_PROJECT_ROOT} (matches committed plugins/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] (deterministic select, not extend-select — avoids pulling in ruff's default ISC/SIM rules); tests/** per-file E501-ignore.
  • pytest-cov>=4.1 dev extra; addopts="--cov=src/mcp_server_basti --cov-report=term-missing --cov-report=xml" (report-only, no --cov-fail-under gate — threshold comes once a baseline is measured).
  • ci.yml — lint scope src/ → src/ tests/; coverage report-only; coverage.xml uploaded as artifact.
  • .gitignore — .coverage, coverage.xml, *.cover, htmlcov/.

Tests (commit 6)

  • 8 new mocked unit-test files (CI has no nvidia-smi/ufw/zram — must be reproducible): 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→ToolError mentioning SUDOERS_SETUP.md, non-password CPE, OSError→ToolError).
  • 3 new integration wire tests: test_system_status_integration, test_repo_info_integration, test_structured_output_integration (proves standalone-fastmcp structuredContent end-to-end via get_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_TOOLS 10 → 11.
  • Fixed test_wire_error_paths.py — import REPO_ROOT/STDIO_TIMEOUT from conftest (dedupe); moved not_git_wrapper fixture to tmp_path (stops writing .tmp-test-not-repo.py into 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-only BASTI_FW_TESTS-gated firewall tests). Coverage 96%.
  • bash scripts/check-mcp.sh — basti-tools smoke-test ok.
  • Manual stdio client: lists 11 tools, all readOnlyHint=True; call_tool("get_power_profile", {}) → structuredContent={'profile': 'power-saver'}.

Fallback note

If fastmcp>=3.0 ever regresses on stdio, revert imports to the mcp[cli] shim with structured_output=True on TypedDict returns (loses tags/timeout/explicit output_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

  • No secrets or tokens committed (.env is in .gitignore) — the sudoers snippet and docs contain no credentials.
  • CI passes (ruff, pytest w/ coverage, build) — see checks below.
  • N/A skill added (no skill change).
  • N/A .mcp.json env var (no new server; the firewall tool uses sudoers, not an env var).

Related

  • Continues the agent-toolkit plugin 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.
  • The firewall sudoers rule is installed separately by Basti per docs/mcp-server/SUDOERS_SETUP.md (user go-ahead, not part of this PR).

🤖 Generated with Claude Code

Toqsick and others added 6 commits August 5, 2026 08:13
…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>
Copilot AI lite review requested due to automatic review settings August 5, 2026 06:36
…<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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_logging instrumentation, 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`.
@Toqsick
Toqsick merged commit 592ec5c into main Aug 5, 2026
5 checks passed
Toqsick added a commit that referenced this pull request Aug 16, 2026
… from merged main

Follow-up zu 151b7f0: Der Remote-Merge (MCP-Server-PRs #1-#4) brachte 7 neue
:develop-Vorkommen in routing/, scripts/build_routing.py und der
mcp-health-check Workflow. Alle auf den selben SHA256-Digest gepinnt
(2d6c011e…) für Konsistenz mit dem Hauptfix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants