Skip to content

Commit 3956b25

Browse files
authored
feat: Migrate to FastMCP 2.x and fix IntelliJ/timeout issues (#39)
* chore: update MCP SDK to 1.12.3 to fix IntelliJ compatibility - Upgraded from MCP 1.6.0 to 1.12.3 - Fixes BrokenPipeError when IntelliJ disconnects after tools/list - IntelliJ MCP client closes connection immediately after receiving tools list * fix: IntelliJ MCP integration and remove Anthropic tests - Fixed __main__.py double sys.exit() issue causing IntelliJ crashes - Updated MCP SDK to 1.12.3 for better compatibility - Removed .mcp.json from repo (now in .gitignore for local dev) - Added .mcp-nix.json to .gitignore for local nix testing - Removed all Anthropic evaluation tests and dependencies - Cleaned up pyproject.toml, pytest.ini, and CI workflows - Updated CI/CD to publish to TestPyPI before PyPI - Bumped version to 1.0.1 - Removed JetBrains troubleshooting section from README - Fixed unit tests by mocking channel discovery The server now works correctly with IntelliJ IDEA's MCP client. * fix: restore minimal __main__.py implementation for IntelliJ compatibility The excessive error handling in __main__.py was interfering with FastMCP's event loop, causing the server to exit immediately after handling the first request. This minimal implementation lets FastMCP handle all lifecycle management properly. * fix: migrate to FastMCP 2.x for proper IntelliJ compatibility - Remove __main__.py file (entry point now in server.py) - Update to fastmcp>=2.10.6 from older mcp>=1.12.3 - Make all tool functions async to match FastMCP 2.x pattern - Update type annotations to Python 3.9+ style (dict instead of Dict) - Update pyproject.toml entry point to mcp_nixos.server:main - Add reference-mcp-coroot to .gitignore - Clean implementation matches working mcp-coroot pattern * chore: remove unnecessary comment from server.py - Remove 'formatters.py functions were inlined' comment - All async functions already have proper type annotations - No debugging artifacts found - Code is clean and production-ready * chore: modernize dependencies and tooling to match coroot project - Update all dependencies to latest versions: - fastmcp: 2.10.6 → 2.11.0 - requests: 2.32.3 → 2.32.4 - beautifulsoup4: 4.13.3 → 4.13.4 - pytest: 8.3.5 → 8.4.1 - pytest-cov: 6.0.0 → 6.2.1 - pytest-asyncio: 0.26.0 → 1.1.0 - build: 1.0.0 → 1.2.2 - pywin32: 306.0 → 308.0 - Replace old linting tools with modern alternatives: - Remove black, flake8, pylint - Add ruff for linting and formatting - Add mypy for type checking - Update tool configurations to use ruff - Fix type annotations: - Add missing return type annotations - Fix generic type parameters - Add explicit type annotations for dict/list comprehensions - Fix async function call (nixos_flakes_search) - Update Nix flake: - Replace black/flake8/pylint with ruff - Add mypy support - Update lint/format/typecheck commands - Run ruff auto-fixes: - Fix import ordering - Fix unused loop variables - Remove unnecessary list() call in sorted() - Replace assert False with raise AssertionError This brings the project in line with modern Python tooling standards and matches the approach used in the mcp-coroot project. * fix: partial async conversion for test files - Add pytest import and @pytest.mark.asyncio decorators - Convert sync function calls to async/await pattern - Files updated: test_flake_search.py, test_flakes_stats_eval.py (partial test_mcp_behavior_comprehensive.py) - Still need to complete remaining test files * fix: complete async conversion for main test files - Fixed test_nixhub_find_version.py: added pytest import and async/await - Fixed test_nixhub_integration.py: added pytest import and async/await - Fixed test_nixos_stats_regression.py: added pytest import and async/await - Fixed test_option_info_improvements.py: added pytest import and async/await - Fixed test_package_counts_eval.py: added pytest import and async/await - Fixed test_real_world_scenarios.py: added pytest import and async/await Used efficient perl regex patterns to add @pytest.mark.asyncio decorators and await keywords to all async function calls. Most of the requested test files are now complete. Only test_mcp_behavior_comprehensive.py remains partially fixed. * fix: complete async conversion for all test files - Completed test_mcp_behavior_comprehensive.py async conversion - Added @pytest.mark.asyncio decorators to all test methods - Added await keywords to all async function calls - Used efficient perl regex patterns for batch modifications - All test files now properly handle async function calls Summary of all files fixed: - test_flake_search.py (6 test methods) - test_flakes_stats_eval.py (6 test methods) - test_mcp_behavior_comprehensive.py (13 test methods) - test_nixhub_find_version.py (10 test methods) - test_nixhub_integration.py (13 test methods) - test_nixos_stats_regression.py (3 test methods) - test_option_info_improvements.py (9 test methods) - test_package_counts_eval.py (5 test methods) - test_real_world_scenarios.py (10 test methods) All async functions now correctly await: - nixos_search, nixos_info, nixos_channels, nixos_stats - home_manager_search, home_manager_info, home_manager_stats - home_manager_list_options, home_manager_options_by_prefix - darwin_search, darwin_info, darwin_stats - darwin_list_options, darwin_options_by_prefix - nixos_flakes_search, nixos_flakes_stats - nixhub_package_versions, nixhub_find_version All test files validated with ast.parse() - syntax is correct. * refactor: clean up dependencies and modernize tooling in flake.nix - Remove unused dependencies: python-dotenv, psutil from propagatedBuildInputs - Update comments to clarify that ruff replaces black, flake8, isort, and more - Update dependency verification to check for fastmcp instead of python-dotenv - Add explanatory comments for better clarity - All actual dependencies now match what's used in the source code This cleanup aligns the flake.nix with the modern tooling already configured in pyproject.toml (ruff for linting/formatting, mypy for type checking) and removes dependencies that are no longer used. * fix: update all tests for async/await compatibility - Add pytest imports to all test files - Add @pytest.mark.asyncio decorators to async tests - Convert test methods to async where needed - Add await keywords for all async function calls - Fix 75 test methods across 9 test files All test logic remains unchanged - only async syntax updated * fix: add missing async decorators in two test files - test_nixos_stats_regression.py: 3 methods - test_package_counts_eval.py: 3 methods These were missed in the initial async conversion * test: reorganize test files for better structure - Consolidated 29 test files down to 15 for better organization - Combined related test files: - test_evals.py: merged with test_evals_comprehensive.py - test_mcp_behavior.py: merged behavior and comprehensive tests - test_flakes.py: combined all flake-related tests - test_nixhub.py: consolidated nixhub integration tests - test_options.py: merged option info tests - test_channels.py: combined channel handling tests - test_integration.py: merged real and advanced integration - test_nixos_stats.py: combined stats regression tests - Renamed test_server_comprehensive.py to test_server.py - Renamed test_mcp_tools_comprehensive.py to test_mcp_tools.py - Renamed test_fixes_comprehensive.py to test_regression.py - Fixed missing imports (dataclass, patch) after file merging - All test files now compile without syntax errors * WiP * chore: update dependency management and Nix flake - Updated uv.lock to sync with pyproject.toml - Simplified flake.nix dependencies - removed attempt to build fastmcp in Nix - Added pytest-asyncio to Nix dev environment - Fixed pip list verification to check for fastmcp instead of mcp - Verified app starts correctly with: uv run mcp-nixos - Verified tests run correctly with: uv run pytest - Note: fastmcp dependency is managed via pip/uv, not nixpkgs * fix: make MockAssistant.use_tool async in test_mcp_behavior.py * fix: convert remaining test methods to async in test_mcp_behavior.py * fix: convert all remaining test methods to async in test_mcp_behavior.py * chore: clean up project and update CLAUDE.md - Updated CLAUDE.md to v1.0.1 reflecting current project state: - Removed __main__.py reference (entry point now in server.py) - Updated file structure to show reorganized tests (15 files) - Updated dependencies to show fastmcp>=2.11.0 - Added async requirement for new tools - Removed references to pylint and black (using ruff) - Deleted unused files: - requirements.txt (outdated, using pyproject.toml) - setup.py (outdated, using pyproject.toml) - configuration.nix (test file) - pyrightconfig.json (using mypy instead) - wily.cfg (unused config) - claude-desktop.log (log file) - Removed code2prompt and llm from flake.nix (replaced with gh) - Fixed async implementation in test_mcp_behavior.py MockAssistant * docs: update README.md with v1.0.1 async changes - Added v1.0.1 ASYNC UPDATE section about FastMCP 2.x migration - Updated feature section title to "The Async Revolution" - Added notes about ruff replacing black/flake8 - Updated testing philosophy to mention async tests and file reduction - Kept the sarcastic tone throughout ("await real_courage()") - Updated maintainer tagline to include async/await masochism * chore: update pre-commit hooks and clean up dotfiles - Updated .pre-commit-config.yaml to use ruff instead of black/flake8 - Added ruff and ruff-format hooks - Added mypy hook with type stubs - Removed wily hook (unused) - Removed outdated dotfiles: - .flake8 (replaced by ruff) - .pylintrc (replaced by ruff) - .env (unnecessary) - Note: .mcp.json already in .gitignore for local dev use - Added get_tool_function helper to test files for FastMCP compatibility * fix: add get_tool_function helper to test_server.py * docs: remove connection timeouts section from README - This section is no longer relevant with modern package management - Connection timeouts were more of an issue with older setups - Also added get_tool_function helper to remaining test files * docs: update developer section in README with local testing guide - Added comprehensive local development setup section - Included .mcp.json example with "hackerman" path (for the culture) - Explained how .mcp.json enables real-time testing in Claude Code - Updated both Nix and non-Nix development instructions - Added build and publish commands for completeness - Kept the sarcastic tone with Windows path joke - Also added get_tool_function helper to test_nixhub.py * fix: resolve test failures after FastMCP 2.x migration Major fixes to adapt tests for FastMCP 2.x async architecture: Server Changes: - Refactored nixos_flakes_search to avoid tool-to-tool calls - Created _nixos_flakes_search_impl internal function - nixos_search now calls implementation directly, not the tool Test Infrastructure: - Added get_tool_function helper to extract underlying functions from FastMCP tools - Applied helper to: test_edge_cases.py, test_channels.py, test_flakes.py, test_integration.py, test_nixhub.py, test_options.py, test_server.py - Made test_evals.py framework fully async-compatible Channel Test Fixes: - Fixed mocking to include both get_available() and get_resolved() - Updated test expectations to match current output format - Ensured proper channel discovery simulation Key Insights: - FastMCP tools are wrapped and cannot be called directly from code - Must use get_tool_function helper or refactor to avoid tool-to-tool calls - All async functions need proper await handling Result: All 32 previously failing tests now pass * fix: handle 'await' prefix in eval test framework tool call parsing The eval framework was not correctly parsing expected tool calls that included the 'await' prefix, causing test failures. Now strips 'await ' before extracting the tool name for comparison. * feat: complete FastMCP 2.x migration and fix all tests - Migrated from MCP SDK to FastMCP 2.x with async/await patterns - Fixed all 334 tests to work with new async architecture - Added get_tool_function helper for test compatibility - Removed duplicate test classes from merged files - Fixed mock setup for async function testing - Updated test expectations to match current API data - Enhanced CI/CD with ruff for linting/formatting - Updated RELEASE_NOTES.md for v1.0.1 This is a drop-in replacement maintaining full backward compatibility while modernizing the codebase for better performance and maintainability. * fix: sync flake with uv dependencies and use Python 3.12 - Update from Python 3.11 to 3.12 to use fastmcp from nixpkgs - Update nixpkgs to latest unstable with fastmcp package - Remove unused pyproject-nix input - Ensure nix run and nix develop work correctly - All dependencies (fastmcp, requests, beautifulsoup4) now from nixpkgs * ci: add Python matrix builds for 3.11, 3.12, and 3.13 - Split test job into test-python (matrix) and test-nix - test-python uses uv and tests on Python 3.11, 3.12, and 3.13 - test-nix only tests with Python 3.12 via nix flake - Update README badges to show all supported Python versions - Update publish jobs to depend on both test jobs * fix: remove pkg_resources fallback for Python 3.11+ compatibility - Remove pkg_resources import that was causing mypy errors - Since we require Python 3.11+, importlib.metadata is always available - Fixes CI type checking failures * fix: update fallback version to match pyproject.toml - Change fallback version from 1.0.0 to 1.0.1 to match actual package version - This ensures consistency when package metadata is not available
1 parent 48fa97c commit 3956b25

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+6910
-8261
lines changed

.env.example

Lines changed: 0 additions & 8 deletions
This file was deleted.

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

.flake8

Lines changed: 0 additions & 5 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# .github/workflows/ci.yml
22
#
33
# Simplified CI/CD workflow that actually works:
4-
# - Always excludes eval tests (expensive - run locally when needed)
54
# - Skips tests for documentation-only changes (on branches only)
65
# - Publishes to PyPI on any version tag (v*)
76

@@ -60,11 +59,14 @@ jobs:
6059
website:
6160
- 'website/**'
6261
63-
# Test job - runs for all tags and for code changes on branches
64-
test:
65-
name: Test & Lint
62+
# Python matrix test job - tests on multiple Python versions
63+
test-python:
64+
name: Test Python ${{ matrix.python-version }}
6665
runs-on: ubuntu-latest
6766
needs: [changes]
67+
strategy:
68+
matrix:
69+
python-version: ["3.11", "3.12", "3.13"]
6870
# Always evaluate this condition, even if changes job was skipped
6971
if: |
7072
always() && (
@@ -77,48 +79,41 @@ jobs:
7779
- name: Checkout code
7880
uses: actions/checkout@v4
7981

80-
- name: Install Nix
81-
uses: cachix/install-nix-action@v27
82+
- name: Set up Python ${{ matrix.python-version }}
83+
uses: actions/setup-python@v5
8284
with:
83-
nix_path: nixpkgs=channel:nixos-unstable
84-
extra_nix_config: |
85-
experimental-features = nix-command flakes
86-
accept-flake-config = true
85+
python-version: ${{ matrix.python-version }}
8786

88-
- name: Cache Nix store
89-
uses: actions/cache@v4
87+
- name: Install uv
88+
uses: astral-sh/setup-uv@v4
9089
with:
91-
path: ~/.cache/nix
92-
key: ${{ runner.os }}-nix-${{ hashFiles('flake.lock') }}
93-
restore-keys: |
94-
${{ runner.os }}-nix-
90+
enable-cache: true
91+
cache-dependency-glob: "pyproject.toml"
9592

96-
- name: Build flake
93+
- name: Install dependencies
9794
run: |
98-
nix flake check --accept-flake-config
99-
nix develop -c echo "Development environment ready"
95+
uv pip install --system -e ".[dev]"
10096
10197
- name: Format check
102-
run: nix develop --command format --check
98+
run: ruff format --check mcp_nixos/ tests/
10399

104100
- name: Lint
105-
run: nix develop --command lint
101+
run: ruff check mcp_nixos/ tests/
106102

107103
- name: Type check
108-
run: nix develop --command typecheck
104+
run: mypy mcp_nixos/
109105

110-
- name: Run tests (always excludes eval tests)
106+
- name: Run tests
111107
run: |
112-
echo "Running tests excluding eval tests (eval tests are expensive - run locally when needed)"
113-
nix develop --command setup
114-
nix develop --command bash -c 'run-tests -m "not anthropic" --ignore-glob="**/test_*eval*.py"'
108+
pytest tests/ -v --cov=mcp_nixos --cov-report=term --cov-report=xml --junitxml=junit.xml
115109
116110
- name: Upload coverage to Codecov
117111
if: always()
118112
uses: codecov/codecov-action@v4
119113
with:
120114
files: ./coverage.xml
121115
fail_ci_if_error: false
116+
flags: python-${{ matrix.python-version }}
122117
env:
123118
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
124119

@@ -129,6 +124,54 @@ jobs:
129124
token: ${{ secrets.CODECOV_TOKEN }}
130125
files: ./junit.xml
131126

127+
# Nix flake test job - only tests with Python 3.12
128+
test-nix:
129+
name: Test Nix Flake
130+
runs-on: ubuntu-latest
131+
needs: [changes]
132+
# Always evaluate this condition, even if changes job was skipped
133+
if: |
134+
always() && (
135+
startsWith(github.ref, 'refs/tags/') ||
136+
(needs.changes.result == 'success' && needs.changes.outputs.code == 'true' &&
137+
(github.event_name == 'pull_request' ||
138+
(github.event_name == 'push' && !contains(github.event.head_commit.message, 'Merge pull request'))))
139+
)
140+
steps:
141+
- name: Checkout code
142+
uses: actions/checkout@v4
143+
144+
- name: Install Nix
145+
uses: cachix/install-nix-action@v27
146+
with:
147+
nix_path: nixpkgs=channel:nixos-unstable
148+
extra_nix_config: |
149+
experimental-features = nix-command flakes
150+
accept-flake-config = true
151+
152+
- name: Cache Nix store
153+
uses: actions/cache@v4
154+
with:
155+
path: ~/.cache/nix
156+
key: ${{ runner.os }}-nix-${{ hashFiles('flake.lock') }}
157+
restore-keys: |
158+
${{ runner.os }}-nix-
159+
160+
- name: Build flake
161+
run: |
162+
nix flake check --accept-flake-config
163+
nix develop -c echo "Development environment ready"
164+
165+
- name: Test nix run
166+
run: |
167+
timeout 5s nix run . -- --help || true
168+
169+
- name: Run tests in nix develop
170+
run: |
171+
echo "Running tests in nix environment"
172+
nix develop --command setup
173+
nix develop --command bash -c 'run-tests'
174+
132175
# Website deployment - only on main branch pushes with changes
133176
deploy-website:
134177
name: Deploy Website
@@ -175,13 +218,52 @@ jobs:
175218
aws s3 sync website/out/ s3://urandom-mcp-nixos/ --delete
176219
aws cloudfront create-invalidation --distribution-id E1QS1G7FYYJ6TL --paths "/*"
177220
178-
# PyPI publishing - only on version tags
221+
# Publish to TestPyPI - on all pushes to main and on tags
222+
publish-test:
223+
name: Publish to TestPyPI
224+
needs: [test-python, test-nix]
225+
if: |
226+
always() &&
227+
needs.test-python.result == 'success' &&
228+
needs.test-nix.result == 'success' &&
229+
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
230+
runs-on: ubuntu-latest
231+
environment:
232+
name: testpypi
233+
url: https://test.pypi.org/p/mcp-nixos
234+
permissions:
235+
id-token: write
236+
steps:
237+
- name: Checkout code
238+
uses: actions/checkout@v4
239+
240+
- name: Install Nix
241+
uses: cachix/install-nix-action@v27
242+
with:
243+
nix_path: nixpkgs=channel:nixos-unstable
244+
extra_nix_config: |
245+
experimental-features = nix-command flakes
246+
accept-flake-config = true
247+
248+
- name: Build package
249+
run: |
250+
nix develop --command build
251+
ls -l dist/
252+
253+
- name: Publish to TestPyPI
254+
uses: pypa/gh-action-pypi-publish@release/v1
255+
with:
256+
repository-url: https://test.pypi.org/legacy/
257+
258+
# PyPI publishing - only on version tags after TestPyPI succeeds
179259
publish:
180260
name: Publish to PyPI
181-
needs: [test]
261+
needs: [test-python, test-nix, publish-test]
182262
if: |
183263
always() &&
184-
needs.test.result == 'success' &&
264+
needs.test-python.result == 'success' &&
265+
needs.test-nix.result == 'success' &&
266+
needs.publish-test.result == 'success' &&
185267
startsWith(github.ref, 'refs/tags/v')
186268
runs-on: ubuntu-latest
187269
environment:

.github/workflows/eval-tests.yml.disabled

Lines changed: 0 additions & 80 deletions
This file was deleted.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ result
6262
*~
6363
.vscode/
6464

65+
# MCP local configuration
66+
.mcp.json
67+
.mcp-nix.json
68+
6569
# Misc
6670
temp
6771
tmp
@@ -92,3 +96,4 @@ TODO.md
9296
/website/.pnpm-store/
9397
/website/.DS_Store
9498
/website/*.pem
99+
reference-mcp-coroot/

.mcp.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
"command": "uv",
66
"args": [
77
"run",
8-
"-m",
9-
"mcp_nixos"
10-
],
11-
"env": {}
8+
"--directory",
9+
"/Users/jamesbrink/Projects/utensils/mcp-nixos",
10+
"mcp-nixos"
11+
]
1212
}
1313
}
1414
}

.pre-commit-config.yaml

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,16 @@ repos:
77
- id: check-yaml
88
- id: check-added-large-files
99

10-
- repo: https://github.com/psf/black
11-
rev: 24.1.1
10+
- repo: https://github.com/astral-sh/ruff-pre-commit
11+
rev: v0.4.10
1212
hooks:
13-
- id: black
14-
args: ["--line-length", "120"]
13+
- id: ruff
14+
args: ["--fix"]
15+
- id: ruff-format
1516

16-
- repo: https://github.com/pycqa/flake8
17-
rev: 7.0.0
17+
- repo: https://github.com/pre-commit/mirrors-mypy
18+
rev: v1.10.0
1819
hooks:
19-
- id: flake8
20-
args: ["--max-line-length", "120", "--ignore", "E402,E203"]
21-
22-
- repo: local
23-
hooks:
24-
- id: wily
25-
name: wily
26-
entry: wily diff
27-
verbose: true
28-
language: python
29-
additional_dependencies: [wily]
20+
- id: mypy
21+
additional_dependencies: [types-requests, types-beautifulsoup4]
22+
args: ["--strict", "--ignore-missing-imports"]

0 commit comments

Comments
 (0)