This is page 1 of 3. Use http://codebase.md/utensils/mcp-nixos?page={x} to view the full context.
# Directory Structure
```
├── .claude
│ ├── agents
│ │ ├── mcp-server-architect.md
│ │ ├── nix-expert.md
│ │ └── python-expert.md
│ ├── commands
│ │ └── release.md
│ └── settings.json
├── .dockerignore
├── .envrc
├── .github
│ └── workflows
│ ├── ci.yml
│ ├── claude-code-review.yml
│ ├── claude.yml
│ ├── deploy-flakehub.yml
│ ├── deploy-website.yml
│ └── publish.yml
├── .gitignore
├── .mcp.json
├── .pre-commit-config.yaml
├── CLAUDE.md
├── Dockerfile
├── flake.lock
├── flake.nix
├── LICENSE
├── MANIFEST.in
├── mcp_nixos
│ ├── __init__.py
│ └── server.py
├── pyproject.toml
├── pytest.ini
├── README.md
├── RELEASE_NOTES.md
├── RELEASE_WORKFLOW.md
├── smithery.yaml
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_channels.py
│ ├── test_edge_cases.py
│ ├── test_evals.py
│ ├── test_flakes.py
│ ├── test_integration.py
│ ├── test_main.py
│ ├── test_mcp_behavior.py
│ ├── test_mcp_tools.py
│ ├── test_nixhub.py
│ ├── test_nixos_stats.py
│ ├── test_options.py
│ ├── test_plain_text_output.py
│ ├── test_real_world_scenarios.py
│ ├── test_regression.py
│ └── test_server.py
├── uv.lock
└── website
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
│ └── settings.json
├── app
│ ├── about
│ │ └── page.tsx
│ ├── docs
│ │ └── claude.html
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ ├── test-code-block
│ │ └── page.tsx
│ └── usage
│ └── page.tsx
├── components
│ ├── AnchorHeading.tsx
│ ├── ClientFooter.tsx
│ ├── ClientNavbar.tsx
│ ├── CodeBlock.tsx
│ ├── CollapsibleSection.tsx
│ ├── FeatureCard.tsx
│ ├── Footer.tsx
│ └── Navbar.tsx
├── metadata-checker.html
├── netlify.toml
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── favicon
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── mstile-150x150.png
│ │ ├── README.md
│ │ └── site.webmanifest
│ ├── images
│ │ ├── .gitkeep
│ │ ├── attribution.md
│ │ ├── claude-logo.png
│ │ ├── JamesBrink.jpeg
│ │ ├── mcp-nixos.png
│ │ ├── nixos-snowflake-colour.svg
│ │ ├── og-image.png
│ │ ├── sean-callan.png
│ │ └── utensils-logo.png
│ ├── robots.txt
│ └── sitemap.xml
├── README.md
├── tailwind.config.js
├── tsconfig.json
└── windsurf_deployment.yaml
```
# Files
--------------------------------------------------------------------------------
/website/public/images/.gitkeep:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
```
use flake
```
--------------------------------------------------------------------------------
/website/.eslintignore:
--------------------------------------------------------------------------------
```
node_modules/
.next/
out/
public/
```
--------------------------------------------------------------------------------
/website/.prettierignore:
--------------------------------------------------------------------------------
```
node_modules/
.next/
out/
public/
dist/
coverage/
.vscode/
build/
README.md
*.yml
*.yaml
```
--------------------------------------------------------------------------------
/website/.prettierrc:
--------------------------------------------------------------------------------
```
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"printWidth": 100,
"trailingComma": "es5",
"arrowParens": "avoid",
"endOfLine": "auto"
}
```
--------------------------------------------------------------------------------
/.mcp.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"nixos": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--directory",
"/Users/jamesbrink/Projects/utensils/mcp-nixos",
"mcp-nixos"
]
}
}
}
```
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
```
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# next.js
/.next/
/out/
/build
# misc
.DS_Store
*.pem
.env*
!.env.example
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
```
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
```yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.10
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-beautifulsoup4]
args: ["--strict", "--ignore-missing-imports"]
```
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
```
# Git
.git/
.gitignore
.gitattributes
# GitHub
.github/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
*.egg
.eggs/
dist/
build/
wheels/
.pytest_cache/
.mypy_cache/
.ruff_cache/
htmlcov/
.coverage
.coverage.*
coverage.xml
*.cover
.hypothesis/
.tox/
.venv/
venv/
ENV/
env/
pip-log.txt
pip-delete-this-directory.txt
# Nix
result
result-*
.direnv/
.envrc
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Documentation
LICENSE
docs/
website/
CLAUDE.md
RELEASE_NOTES.md
!README.md
# Development
.mcp.json
CLAUDE.md
tests/
conftest.py
*.test.py
*_test.py
# CI/CD
.travis.yml
.gitlab-ci.yml
azure-pipelines.yml
# Other
.env
.env.*
*.log
*.bak
*.tmp
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
```
--------------------------------------------------------------------------------
/website/.eslintrc.json:
--------------------------------------------------------------------------------
```json
{
"extends": [
"next/core-web-vitals",
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": [
"react",
"react-hooks",
"@typescript-eslint"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/prop-types": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}],
"react/no-unknown-property": [
"error",
{ "ignore": ["jsx"] }
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual Environment
.venv/
venv/
ENV/
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
mcp_nixos_test_cache/
*test_cache*/
# Environments
.env
.env.local
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Nix
.direnv/
result
# IDE
.idea/
*.swp
*.swo
*~
.vscode/
# MCP local configuration
.mcp.json
.mcp-nix.json
# Misc
temp
tmp
# Explicitly don't ignore uv.lock (we need to track it)
uv-*.lock
.aider*
.pypirc
mcp-completion-docs.md
TODO.md
# Wily
.wily/
# Logs
*.log
*.DS_Store
# Website (Next.js)
/website/node_modules/
/website/.next/
/website/out/
/website/.vercel/
/website/.env*.local
/website/npm-debug.log*
/website/yarn-debug.log*
/website/yarn-error.log*
/website/pnpm-debug.log*
/website/.pnpm-store/
/website/.DS_Store
/website/*.pem
reference-mcp-coroot/
```
--------------------------------------------------------------------------------
/website/public/favicon/README.md:
--------------------------------------------------------------------------------
```markdown
# Favicon Files
These favicon files are generated from the MCP-NixOS project logo.
## File Descriptions
- `favicon.ico`: Multi-size ICO file containing 16x14 and 32x28 versions
- `favicon-16x16.png`: 16x14 PNG for standard favicon
- `favicon-32x32.png`: 32x28 PNG for standard favicon
- `apple-touch-icon.png`: 180x156 PNG for iOS home screen
- `android-chrome-192x192.png`: 192x167 PNG for Android
- `android-chrome-512x512.png`: 512x444 PNG for Android
- `safari-pinned-tab.svg`: Monochrome SVG for Safari pinned tabs
- `mstile-150x150.png`: 150x130 PNG for Windows tiles
- `browserconfig.xml`: Configuration for Microsoft browsers
- `site.webmanifest`: Web app manifest for PWA support
## Generation Commands
In a normal development environment, you can generate these files using ImageMagick:
```bash
# Generate PNG files from the source PNG logo
convert -background none -resize 16x16 ../images/mcp-nixos.png favicon-16x16.png
convert -background none -resize 32x32 ../images/mcp-nixos.png favicon-32x32.png
convert -background none -resize 180x180 ../images/mcp-nixos.png apple-touch-icon.png
convert -background none -resize 192x192 ../images/mcp-nixos.png android-chrome-192x192.png
convert -background none -resize 512x512 ../images/mcp-nixos.png android-chrome-512x512.png
convert -background none -resize 150x150 ../images/mcp-nixos.png mstile-150x150.png
# Generate ICO file (combines multiple sizes)
convert favicon-16x16.png favicon-32x32.png favicon.ico
```
## Attribution
These favicon files are derived from the NixOS snowflake logo and are used with attribution to the NixOS project. See the attribution.md file in the images directory for more details.
```
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP-NixOS Website
The official website for the MCP-NixOS project built with Next.js 15.2 and Tailwind CSS. Deployed automatically via CI/CD to AWS S3 and CloudFront.
## Development
This website is built with:
- [Next.js 15.2](https://nextjs.org/) using the App Router
- [TypeScript](https://www.typescriptlang.org/)
- [Tailwind CSS](https://tailwindcss.com/) for styling
- Static export for hosting on S3/CloudFront
## Getting Started
### Using Nix (Recommended)
If you have Nix installed, you can use the dedicated website development shell:
#### Option 1: Direct Website Shell Access
```bash
# Enter the website development shell directly
nix develop .#web
# Use the menu commands or run directly:
install # Install dependencies
dev # Start development server
build # Build for production
lint # Lint code
```
#### Option 2: From Main Development Shell
```bash
# Enter the main development shell
nix develop
# Launch the website development shell
web-dev # This opens the website shell with Node.js
```
### Manual Setup
```bash
# Navigate to the website directory
cd website
# Install dependencies
npm install
# or
yarn
# or
pnpm install
# Start development server
npm run dev
# or
yarn dev
# or
pnpm dev
# Build for production
npm run build
# or
yarn build
# or
pnpm build
```
## Project Structure
- `app/` - Next.js app router pages
- `components/` - Shared UI components
- `public/` - Static assets
- `tailwind.config.js` - Tailwind CSS configuration with NixOS color scheme
## Design Notes
- The website follows NixOS brand colors:
- Primary: #5277C3
- Secondary: #7EBAE4
- Dark Blue: #1C3E5A
- Light Blue: #E6F0FA
- Designed to be fully responsive for mobile, tablet, and desktop
- SEO optimized with proper metadata
- Follows accessibility guidelines (WCAG 2.1 AA)
## Code Quality
The project includes comprehensive linting and type checking:
```bash
# Run ESLint to check for issues
npm run lint
# Fix automatically fixable ESLint issues
npm run lint:fix
# Run TypeScript type checking
npm run type-check
```
VS Code settings are included for automatic formatting and linting.
## Next.js 15.2 Component Model
Next.js 15.2 enforces a stricter separation between client and server components:
### Client Components
- Must include `"use client";` at the top of the file
- Can use React hooks (useState, useEffect, etc.)
- Can include event handlers (onClick, onChange, etc.)
- Can access browser APIs
### Server Components (Default)
- Cannot use React hooks
- Cannot include event handlers
- Cannot access browser APIs
- Can access backend resources directly
- Keep sensitive information secure
### Best Practices
- Mark components with interactive elements as client components
- Use dynamic imports with `{ ssr: false }` for components with useLayoutEffect
- Keep server components as the default when possible for better performance
- Use client components only when necessary for interactivity
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP-NixOS - Because Your AI Assistant Shouldn't Hallucinate About Packages
[](https://github.com/utensils/mcp-nixos/actions/workflows/ci.yml)
[](https://codecov.io/gh/utensils/mcp-nixos)
[](https://pypi.org/project/mcp-nixos/)
[](https://pypi.org/project/mcp-nixos/)
[](https://smithery.ai/server/@utensils/mcp-nixos)
[](https://mseep.ai/app/99cc55fb-a5c5-4473-b315-45a6961b2e8c)
> **🎉 REFACTORED**: Version 1.0.0 represents a complete rewrite that drastically simplified everything. We removed all the complex caching, abstractions, and "enterprise" patterns. Because sometimes less is more, and more is just showing off.
>
> **🚀 ASYNC UPDATE**: Version 1.0.1 migrated to FastMCP 2.x for modern async goodness. Because who doesn't love adding `await` to everything?
## Quick Start (Because You Want to Use It NOW)
**🚨 No Nix/NixOS Required!** This tool works on any system - Windows, macOS, Linux. You're just querying web APIs.
### Option 1: Using uvx (Recommended for most users)
[](https://cursor.com/install-mcp?name=nixos&config=eyJjb21tYW5kIjoidXZ4IG1jcC1uaXhvcyJ9)
```json
{
"mcpServers": {
"nixos": {
"command": "uvx",
"args": ["mcp-nixos"]
}
}
}
```
### Option 2: Using Nix (For Nix users)
[](https://cursor.com/install-mcp?name=nixos&config=eyJjb21tYW5kIjoibml4IHJ1biBnaXRodWI6dXRlbnNpbHMvbWNwLW5peG9zIC0tIn0%3D)
```json
{
"mcpServers": {
"nixos": {
"command": "nix",
"args": ["run", "github:utensils/mcp-nixos", "--"]
}
}
}
```
### Option 3: Using Docker (Container lovers unite)
[](https://cursor.com/install-mcp?name=nixos&config=eyJjb21tYW5kIjoiZG9ja2VyIiwiYXJncyI6WyJydW4iLCItLXJtIiwiLWkiLCJnaGNyLmlvL3V0ZW5zaWxzL21jcC1uaXhvcyJdfQ%3D%3D)
```json
{
"mcpServers": {
"nixos": {
"command": "docker",
"args": ["run", "--rm", "-i", "ghcr.io/utensils/mcp-nixos"]
}
}
}
```
That's it. Your AI assistant now has access to real NixOS data instead of making things up. You're welcome.
## What Is This Thing?
MCP-NixOS is a Model Context Protocol server that gives your AI assistant accurate, real-time information about:
- **NixOS packages** (130K+ packages that actually exist)
- **Configuration options** (22K+ ways to break your system)
- **Home Manager settings** (4K+ options for the power users)
- **nix-darwin configurations** (1K+ macOS settings Apple doesn't want you to touch)
- **Package version history** via [NixHub.io](https://www.nixhub.io) (Find that ancient Ruby 2.6 with commit hashes)
## The Tools You Actually Care About
### 🔍 NixOS Tools
- `nixos_search(query, type, channel)` - Search packages, options, or programs
- `nixos_info(name, type, channel)` - Get detailed info about packages/options
- `nixos_stats(channel)` - Package and option counts
- `nixos_channels()` - List all available channels
- `nixos_flakes_search(query)` - Search community flakes
- `nixos_flakes_stats()` - Flake ecosystem statistics
### 📦 Version History Tools (NEW!)
- `nixhub_package_versions(package, limit)` - Get version history with commit hashes
- `nixhub_find_version(package, version)` - Smart search for specific versions
### 🏠 Home Manager Tools
- `home_manager_search(query)` - Search user config options
- `home_manager_info(name)` - Get option details (with suggestions!)
- `home_manager_stats()` - See what's available
- `home_manager_list_options()` - Browse all 131 categories
- `home_manager_options_by_prefix(prefix)` - Explore options by prefix
### 🍎 Darwin Tools
- `darwin_search(query)` - Search macOS options
- `darwin_info(name)` - Get option details
- `darwin_stats()` - macOS configuration statistics
- `darwin_list_options()` - Browse all 21 categories
- `darwin_options_by_prefix(prefix)` - Explore macOS options
## Installation Options
**Remember: You DON'T need Nix/NixOS installed!** This tool runs anywhere Python runs.
### For Regular Humans (Windows/Mac/Linux)
```bash
# Run directly with uvx (no installation needed)
uvx mcp-nixos
# Or install globally
pip install mcp-nixos
uv pip install mcp-nixos
```
### For Nix Users (You Know Who You Are)
```bash
# Run without installing
nix run github:utensils/mcp-nixos
# Install to profile
nix profile install github:utensils/mcp-nixos
```
## Features Worth Mentioning
### 🚀 Version 1.0.1: The Async Revolution (After The Great Simplification)
- **Drastically less code** - v1.0.0 removed thousands of lines, v1.0.1 made them async
- **100% functionality** - Everything still works, now with more `await`
- **0% cache corruption** - Because we removed the cache entirely (still gone!)
- **Stateless operation** - No files to clean up (async doesn't change this)
- **Direct API access** - No abstraction nonsense (but now it's async nonsense)
- **Modern MCP** - FastMCP 2.x because the old MCP was too synchronous
### 📊 What You Get
- **Real-time data** - Always current, never stale
- **Plain text output** - Human and AI readable
- **Smart suggestions** - Helps when you typo option names
- **Cross-platform** - Works on Linux, macOS, and yes, even Windows
- **No configuration** - It just works™
### 🎯 Key Improvements
- **Dynamic channel resolution** - `stable` always points to current stable
- **Enhanced error messages** - Actually helpful when things go wrong
- **Deduped flake results** - No more duplicate spam
- **Version-aware searches** - Find that old Ruby version you need
- **Category browsing** - Explore options systematically
## For Developers (The Brave Ones)
### Local Development Setup
Want to test your changes in Claude Code or another MCP client? Create a `.mcp.json` file in your project directory:
```json
{
"mcpServers": {
"nixos": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--directory",
"/home/hackerman/Projects/mcp-nixos",
"mcp-nixos"
]
}
}
}
```
Replace `/home/hackerman/Projects/mcp-nixos` with your actual project path (yes, even you, Windows users with your `C:\Users\CoolDev\...` paths).
This `.mcp.json` file:
- **Automatically activates** when you launch Claude Code from the project directory
- **Uses your local code** instead of the installed package
- **Enables real-time testing** - just restart Claude Code after changes
- **Already in .gitignore** so you won't accidentally commit your path
### With Nix (The Blessed Path)
```bash
nix develop
menu # Shows all available commands
# Common tasks
run # Start the server (now with FastMCP!)
run-tests # Run all tests (now async!)
lint # Format and check code (ruff replaced black/flake8)
typecheck # Check types (mypy still judges you)
build # Build the package
publish # Upload to PyPI (requires credentials)
```
### Without Nix (The Path of Pain)
```bash
# Install development dependencies
uv pip install -e ".[dev]" # or pip install -e ".[dev]"
# Run the server locally
uv run mcp-nixos # or python -m mcp_nixos.server
# Development commands
pytest tests/ # Now with asyncio goodness
ruff format mcp_nixos/ # black is so 2023
ruff check mcp_nixos/ # flake8 is for boomers
mypy mcp_nixos/ # Still pedantic as ever
# Build and publish
python -m build # Build distributions
twine upload dist/* # Upload to PyPI
```
### Testing Philosophy
- **367 tests** that actually test things (now async because why not)
- **Real API calls** because mocks are for cowards (await real_courage())
- **Plain text validation** ensuring no XML leaks through
- **Cross-platform tests** because Windows users deserve pain too
- **15 test files** down from 29 because organization is a virtue
## Environment Variables
Just one. We're minimalists now:
| Variable | Description | Default |
|----------|-------------|---------|
| `ELASTICSEARCH_URL` | NixOS API endpoint | https://search.nixos.org/backend |
## Troubleshooting
### Nix Sandbox Error
If you encounter this error when running via Nix:
```
error: derivation '/nix/store/...-python3.11-watchfiles-1.0.4.drv' specifies a sandbox profile,
but this is only allowed when 'sandbox' is 'relaxed'
```
**Solution**: Run with relaxed sandbox mode:
```bash
nix run --option sandbox relaxed github:utensils/mcp-nixos --
```
**Why this happens**: The `watchfiles` package (a transitive dependency via MCP) requires custom sandbox permissions for file system monitoring. This is only allowed when Nix's sandbox is in 'relaxed' mode instead of the default 'strict' mode.
**Permanent fix**: Add to your `/etc/nix/nix.conf`:
```
sandbox = relaxed
```
## Acknowledgments
This project queries data from several amazing services:
- **[NixHub.io](https://www.nixhub.io)** - Provides package version history and commit tracking
- **[search.nixos.org](https://search.nixos.org)** - Official NixOS package and option search
- **[Jetify](https://www.jetify.com)** - Creators of [Devbox](https://www.jetify.com/devbox) and NixHub
*Note: These services have not endorsed this tool. We're just grateful API consumers.*
## License
MIT - Because sharing is caring, even if the code hurts.
---
_Created by James Brink and maintained by masochists who enjoy Nix and async/await patterns._
_Special thanks to the NixOS project for creating an OS that's simultaneously the best and worst thing ever._
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
MCP-NixOS is a Model Context Protocol (MCP) server that provides accurate, real-time information about NixOS packages, configuration options, Home Manager, nix-darwin, and flakes. It prevents AI assistants from hallucinating about NixOS package names and configurations by querying official APIs and documentation.
## Key Architecture
The project is a FastMCP 2.x server (async) with a single main module:
- `mcp_nixos/server.py` - All MCP tools and API interactions (asyncio-based)
Data sources:
- NixOS packages/options: Elasticsearch API at search.nixos.org
- Home Manager options: HTML parsing from official docs
- nix-darwin options: HTML parsing from official docs
- Package versions: NixHub.io API
- Flakes: search.nixos.org flake index
All responses are formatted as plain text for optimal LLM consumption.
## Development Commands
### With Nix Development Shell (Recommended)
```bash
# Enter dev shell (auto-activates Python venv)
nix develop
# Core commands:
run # Start the MCP server
run-tests # Run all tests (with coverage in CI)
run-tests --unit # Unit tests only
run-tests --integration # Integration tests only
lint # Check code with ruff
format # Format code with ruff
typecheck # Run mypy type checker
build # Build package distributions
publish # Upload to PyPI
```
### Without Nix
```bash
# Install with development dependencies
uv pip install -e ".[dev]" # or pip install -e ".[dev]"
# Run server
uv run mcp-nixos # or python -m mcp_nixos.server
# Testing
pytest tests/
pytest tests/ --unit
pytest tests/ --integration
# Linting and formatting
ruff format mcp_nixos/ tests/
ruff check mcp_nixos/ tests/
mypy mcp_nixos/
```
## Testing Approach
- 367+ async tests using pytest-asyncio
- Real API calls (no mocks) for integration tests
- Unit tests marked with `@pytest.mark.unit`
- Integration tests marked with `@pytest.mark.integration`
- Tests ensure plain text output (no XML/JSON leakage)
## Local Development with MCP Clients
Create `.mcp.json` in project root (already gitignored):
```json
{
"mcpServers": {
"nixos": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/mcp-nixos",
"mcp-nixos"
]
}
}
}
```
## Important Implementation Notes
1. **Channel Resolution**: The server dynamically discovers available NixOS channels on startup. "stable" always maps to the current stable release.
2. **Error Handling**: All tools return helpful plain text error messages. API failures gracefully degrade with user-friendly messages.
3. **No Caching**: Version 1.0+ removed all caching for simplicity. All queries hit live APIs.
4. **Async Everything**: Version 1.0.1 migrated to FastMCP 2.x. All tools are async functions.
5. **Plain Text Output**: All responses are formatted as human-readable plain text. Never return raw JSON or XML to users.
## CI/CD Workflows
- **CI**: Runs on all PRs - tests (unit + integration), linting, type checking
- **Publish**: Automated PyPI releases on version tags (v*)
- **Claude Code Review**: Reviews PRs using Claude
- **Claude PR Assistant**: Helps with PR creation
## Environment Variables
- `ELASTICSEARCH_URL`: Override NixOS API endpoint (default: https://search.nixos.org/backend)
```
--------------------------------------------------------------------------------
/website/public/robots.txt:
--------------------------------------------------------------------------------
```
User-agent: *
Allow: /
Sitemap: https://mcp-nixos.io//sitemap.xml
```
--------------------------------------------------------------------------------
/website/postcss.config.js:
--------------------------------------------------------------------------------
```javascript
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
```
--------------------------------------------------------------------------------
/website/components/Footer.tsx:
--------------------------------------------------------------------------------
```typescript
import ClientFooter from './ClientFooter';
export default function Footer() {
return <ClientFooter />;
}
```
--------------------------------------------------------------------------------
/website/components/Navbar.tsx:
--------------------------------------------------------------------------------
```typescript
import ClientNavbar from './ClientNavbar';
export default function Navbar() {
return <ClientNavbar />;
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file
# https://smithery.ai/docs/config
startCommand:
type: stdio
command: python
args: ["-m", "mcp_nixos"]
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
"""Test utilities for MCP-NixOS tests."""
# This file intentionally left minimal as the refactored server
# doesn't need the complex test base classes from the old implementation
```
--------------------------------------------------------------------------------
/website/public/favicon/browserconfig.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicon/mstile-150x150.png"/>
<TileColor>#5277c3</TileColor>
</tile>
</msapplication>
</browserconfig>
```
--------------------------------------------------------------------------------
/.claude/settings.json:
--------------------------------------------------------------------------------
```json
{
"permissions": {
"allow": [
"Bash",
"Task",
"Glob",
"Grep",
"LS",
"Read",
"Edit",
"MultiEdit",
"Write",
"WebFetch",
"WebSearch"
]
},
"enabledMcpjsonServers": [
"nixos"
]
}
```
--------------------------------------------------------------------------------
/website/windsurf_deployment.yaml:
--------------------------------------------------------------------------------
```yaml
# Windsurf Deploys Configuration (Beta)
# This is an auto-generated file used to store your app deployment configuration. Do not modify.
# The ID of the project (different from project name) on the provider's system. This is populated as a way to update existing deployments.
project_id: 734e7842-9206-4765-b714-81e5541a3f91
# The framework of the web application (examples: nextjs, react, vue, etc.)
framework: nextjs
```
--------------------------------------------------------------------------------
/website/netlify.toml:
--------------------------------------------------------------------------------
```toml
[build]
command = "npm run build"
publish = "out"
[build.environment]
NODE_VERSION = "18"
[dev]
command = "npm run dev"
port = 3000
targetPort = 3000
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[build.processing]
skip_processing = false
[build.processing.css]
bundle = true
minify = true
[build.processing.js]
bundle = true
minify = true
[build.processing.images]
compress = true
```
--------------------------------------------------------------------------------
/website/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.associations": {
"*.css": "tailwindcss"
}
}
```
--------------------------------------------------------------------------------
/website/next.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // Enable static exports for S3/CloudFront
distDir: 'out', // Output directory for static export
images: {
unoptimized: true, // Required for static export
},
reactStrictMode: true,
// Allow cross-origin requests during development (for VS Code browser preview)
allowedDevOrigins: [
'127.0.0.1',
],
};
module.exports = nextConfig;
```
--------------------------------------------------------------------------------
/mcp_nixos/__init__.py:
--------------------------------------------------------------------------------
```python
"""
MCP-NixOS - Model Context Protocol server for NixOS, Home Manager, and nix-darwin resources.
This package provides MCP resources and tools for interacting with NixOS packages,
system options, Home Manager configuration options, and nix-darwin macOS configuration options.
"""
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("mcp-nixos")
except PackageNotFoundError:
# Package is not installed, use a default version
__version__ = "1.0.1"
```
--------------------------------------------------------------------------------
/website/public/sitemap.xml:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://mcp-nixos.io//</loc>
<lastmod>2025-04-03</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://mcp-nixos.io//about</loc>
<lastmod>2025-04-03</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://mcp-nixos.io//docs</loc>
<lastmod>2025-04-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>
```
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
```
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --verbose
asyncio_default_fixture_loop_scope = function
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests that require external services or interact with external resources
unit: marks tests as unit tests that don't require external services
not_integration: explicitly marks tests that should be excluded from integration test runs
asyncio: mark a test as an async test
evals: marks tests as evaluation tests for MCP behavior
```
--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next-env.d.ts",
"out/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
```
--------------------------------------------------------------------------------
/website/tailwind.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
'nix-primary': '#5277C3', // NixOS primary blue
'nix-secondary': '#7EBAE4', // NixOS secondary blue
'nix-dark': '#1C3E5A', // Darker blue for contrast
'nix-light': '#E6F0FA', // Light blue background
},
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'ui-monospace', 'monospace'],
},
},
},
plugins: [],
}
```
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
```python
"""Minimal test configuration for refactored MCP-NixOS."""
def pytest_addoption(parser):
"""Add test filtering options."""
parser.addoption("--unit", action="store_true", help="Run unit tests only")
parser.addoption("--integration", action="store_true", help="Run integration tests only")
def pytest_configure(config):
"""Configure pytest markers."""
config.addinivalue_line("markers", "integration: mark test as integration test")
config.addinivalue_line("markers", "slow: mark test as slow")
config.addinivalue_line("markers", "asyncio: mark test as async")
# Handle test filtering
if config.getoption("--unit"):
config.option.markexpr = "not integration"
elif config.getoption("--integration"):
config.option.markexpr = "integration"
```
--------------------------------------------------------------------------------
/.github/workflows/deploy-flakehub.yml:
--------------------------------------------------------------------------------
```yaml
name: "Publish tags to FlakeHub"
on:
push:
tags:
- "v?[0-9]+.[0-9]+.[0-9]+*"
workflow_dispatch:
inputs:
tag:
description: "The existing tag to publish to FlakeHub"
type: "string"
required: true
jobs:
flakehub-publish:
runs-on: "ubuntu-latest"
permissions:
id-token: "write"
contents: "read"
steps:
- uses: "actions/checkout@v5"
with:
persist-credentials: false
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
- uses: "DeterminateSystems/determinate-nix-action@v3"
- uses: "DeterminateSystems/flakehub-push@main"
with:
visibility: "public"
name: "utensils/mcp-nixos"
tag: "${{ inputs.tag }}"
include-output-paths: true
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Multi-stage build for minimal final image
FROM python:3.13-alpine3.22 AS builder
# Install build dependencies
RUN apk add --no-cache \
gcc \
musl-dev \
libffi-dev
# Set working directory
WORKDIR /build
# Copy project files
COPY pyproject.toml README.md ./
COPY mcp_nixos/ ./mcp_nixos/
# Build wheel
RUN pip wheel --no-cache-dir --wheel-dir /wheels .
# Final stage
FROM python:3.13-alpine3.22
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install runtime dependencies
RUN apk add --no-cache libffi
# Create non-root user
RUN adduser -D -h /app mcp
# Set working directory
WORKDIR /app
# Copy wheels from builder
COPY --from=builder /wheels /wheels
# Install the package
RUN pip install --no-cache-dir /wheels/* && \
rm -rf /wheels
# Switch to non-root user
USER mcp
# Run the MCP server
ENTRYPOINT ["python", "-m", "mcp_nixos.server"]
```
--------------------------------------------------------------------------------
/.github/workflows/deploy-website.yml:
--------------------------------------------------------------------------------
```yaml
name: Deploy Website
on:
push:
branches: [ main ]
paths:
- 'website/**'
- '.github/workflows/deploy-website.yml'
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: AWS
url: https://mcp-nixos.io/
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: website/package-lock.json
- name: Build website
run: |
cd website
npm install
npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync website/out/ s3://urandom-mcp-nixos/ --delete
aws cloudfront create-invalidation --distribution-id E1QS1G7FYYJ6TL --paths "/*"
```
--------------------------------------------------------------------------------
/website/public/images/attribution.md:
--------------------------------------------------------------------------------
```markdown
# Attribution
## NixOS and NixOS Snowflake Logo
The NixOS snowflake logo is used with attribution to the NixOS project. The logo is sourced from the official NixOS artwork repository at https://github.com/NixOS/nixos-artwork.
Specific logo files used:
- `nixos-flake.svg`: The NixOS snowflake logo in blue (monochrome)
- `nixos-snowflake-colour.svg`: The NixOS snowflake logo with gradient colors from commit 33856d7837cb8ba76c4fc9e26f91a659066ee31f
- `mcp-nixos.png`: Project logo incorporating the NixOS snowflake design
- Favicon files in `/favicon/` directory: Generated from the MCP-NixOS project logo
While no explicit license was found for the NixOS snowflake logo, we are using it under fair use for the purpose of indicating compatibility and integration with the NixOS ecosystem. The logo is used to accurately represent this project's connection to NixOS and its package ecosystem.
If you are the copyright holder of the NixOS snowflake logo and have concerns about its usage in this project, please contact us and we will address your concerns promptly.
## Our Project
MCP-NixOS is licensed under the MIT License. See the LICENSE file in the root directory for the full license text.
```
--------------------------------------------------------------------------------
/website/components/CollapsibleSection.tsx:
--------------------------------------------------------------------------------
```typescript
'use client';
import { useState } from 'react';
interface CollapsibleSectionProps {
title: string;
children: React.ReactNode;
}
export default function CollapsibleSection({ title, children }: CollapsibleSectionProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="mb-4 border border-nix-light rounded-lg overflow-hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full text-left px-4 py-3 bg-nix-light bg-opacity-20 flex justify-between items-center hover:bg-opacity-30 transition-colors duration-200"
>
<h5 className="text-md font-semibold text-nix-primary flex items-center">
{title}
</h5>
<svg
className={`w-5 h-5 text-nix-primary transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isOpen && <div className="p-4">{children}</div>}
</div>
);
}
```
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-nixos-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"type-check": "tsc --noEmit",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"check-format": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\""
},
"dependencies": {
"@types/react-syntax-highlighter": "^15.5.13",
"next": "^15.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.1",
"rehype-pretty-code": "^0.14.1",
"sharp": "^0.33.5",
"shiki": "^3.2.1"
},
"devDependencies": {
"@types/node": "^20.17.30",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"autoprefixer": "^10.4.21",
"eslint": "^8.57.1",
"eslint-config-next": "^15.2.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2"
},
"overrides": {
"prismjs": "^1.30.0"
}
}
```
--------------------------------------------------------------------------------
/website/app/globals.css:
--------------------------------------------------------------------------------
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Global styles for prose content */
p {
@apply text-gray-800 leading-relaxed;
}
.prose p {
@apply text-gray-800 leading-relaxed;
}
.prose li {
@apply text-gray-800 font-medium;
}
/* Style for inline code, but not code blocks */
.prose code:not(pre code) {
@apply bg-gray-100 text-nix-dark px-1.5 py-0.5 rounded font-medium;
}
/* Ensure syntax highlighting in code blocks is not affected by global styles */
.prose pre {
@apply p-0 m-0 overflow-hidden rounded-lg;
}
.prose pre code {
@apply bg-transparent p-0 text-inherit;
}
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 240, 240, 245;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 30, 30, 40;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer components {
.btn-primary {
@apply px-4 py-2 bg-nix-primary text-white rounded-md hover:bg-nix-dark transition-colors font-medium shadow-sm;
}
.btn-secondary {
@apply px-4 py-2 border border-nix-primary text-nix-primary rounded-md hover:bg-nix-light transition-colors font-medium shadow-sm;
}
.container-custom {
@apply container mx-auto px-4 md:px-6 lg:px-8 max-w-7xl;
}
}
```
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
```python
"""Tests for the main entry point in server module."""
from unittest.mock import patch
import pytest
from mcp_nixos.server import main
class TestMainModule:
"""Test the main entry point."""
@patch("mcp_nixos.server.mcp")
def test_main_normal_execution(self, mock_mcp):
"""Test normal server execution."""
mock_mcp.run.return_value = None
# Should not raise any exception
main()
mock_mcp.run.assert_called_once()
@patch("mcp_nixos.server.mcp")
def test_main_mcp_not_none(self, mock_mcp):
"""Test that mcp instance exists."""
# Import to ensure mcp is available
from mcp_nixos.server import mcp
assert mcp is not None
class TestServerImport:
"""Test server module imports."""
def test_mcp_import_from_server(self):
"""Test that mcp is properly available in server."""
from mcp_nixos.server import mcp
assert mcp is not None
def test_server_has_required_attributes(self):
"""Test that server module has required attributes."""
from mcp_nixos import server
assert hasattr(server, "mcp")
assert hasattr(server, "main")
assert hasattr(server, "nixos_search")
assert hasattr(server, "nixos_info")
assert hasattr(server, "home_manager_search")
assert hasattr(server, "darwin_search")
class TestIntegration:
"""Integration tests for main function."""
def test_main_function_signature(self):
"""Test main function has correct signature."""
from inspect import signature
sig = signature(main)
# Should take no parameters
assert len(sig.parameters) == 0
# Should be callable
assert callable(main)
if __name__ == "__main__":
pytest.main([__file__, "-v", "--cov=mcp_nixos.server", "--cov-report=term-missing"])
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mcp-nixos"
version = "1.0.3"
description = "Model Context Protocol server for NixOS, Home Manager, and nix-darwin resources"
readme = "README.md"
authors = [
{name = "James Brink", email = "[email protected]"},
]
requires-python = ">=3.11"
license = {text = "MIT"}
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"fastmcp>=2.11.0",
"requests>=2.32.4",
"beautifulsoup4>=4.13.4",
]
[project.optional-dependencies]
dev = [
"build>=1.2.2",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
"pytest-asyncio>=1.1.0",
"pytest-xdist>=3.6.0",
"ruff>=0.12.4",
"mypy>=1.17.0",
"types-beautifulsoup4>=4.12.0.20250516",
"types-requests>=2.32.4",
"twine>=6.0.1",
]
win = [
"pywin32>=308.0", # Required for Windows-specific file operations and tests
]
[project.scripts]
mcp-nixos = "mcp_nixos.server:main"
[tool.ruff]
line-length = 120
target-version = "py311"
src = ["mcp_nixos", "tests"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = ["E402", "E203"]
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
no_implicit_reexport = true
namespace_packages = true
explicit_package_bases = true
[[tool.mypy.overrides]]
module = "tests.*"
ignore_errors = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = "test_*.py"
addopts = "--cov=mcp_nixos --cov-report=term-missing"
[tool.coverage.run]
source = ["mcp_nixos"]
```
--------------------------------------------------------------------------------
/.github/workflows/claude.yml:
--------------------------------------------------------------------------------
```yaml
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test
```
--------------------------------------------------------------------------------
/website/app/layout.tsx:
--------------------------------------------------------------------------------
```typescript
import type { Metadata, Viewport } from 'next';
import './globals.css';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
export const metadata: Metadata = {
title: 'MCP-NixOS | Model Context Protocol for NixOS',
description: 'MCP resources and tools for NixOS packages, system options, Home Manager configuration, and nix-darwin macOS configuration.',
keywords: ['NixOS', 'MCP', 'Model Context Protocol', 'Home Manager', 'nix-darwin', 'Claude', 'AI Assistant'],
authors: [{ name: 'Utensils', url: 'https://utensils.io' }],
creator: 'Utensils',
publisher: 'Utensils',
metadataBase: new URL('https://mcp-nixos.io'),
alternates: {
canonical: '/',
},
// Open Graph metadata
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://mcp-nixos.io',
siteName: 'MCP-NixOS',
title: 'MCP-NixOS | Model Context Protocol for NixOS',
description: 'MCP resources and tools for NixOS packages, system options, Home Manager configuration, and nix-darwin macOS configuration.',
images: [
{
url: '/images/og-image.png',
width: 1200,
height: 630,
alt: 'MCP-NixOS - Model Context Protocol for NixOS',
},
],
},
// Twitter Card metadata
twitter: {
card: 'summary_large_image',
title: 'MCP-NixOS | Model Context Protocol for NixOS',
description: 'MCP resources and tools for NixOS packages, system options, Home Manager configuration, and nix-darwin macOS configuration.',
images: ['/images/og-image.png'],
creator: '@utensils_io',
},
icons: {
icon: [
{ url: '/favicon/favicon.ico' },
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' }
],
apple: [
{ url: '/favicon/apple-touch-icon.png' }
],
other: [
{
rel: 'mask-icon',
url: '/favicon/safari-pinned-tab.svg',
color: '#5277c3'
}
]
},
manifest: '/favicon/site.webmanifest',
};
export const viewport: Viewport = {
themeColor: '#5277c3',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="min-h-screen flex flex-col">
<Navbar />
<main className="flex-grow">
{children}
</main>
<Footer />
</body>
</html>
);
}
```
--------------------------------------------------------------------------------
/website/app/test-code-block/page.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import React from 'react';
import CodeBlock from '../../components/CodeBlock';
import AnchorHeading from '@/components/AnchorHeading';
export default function TestCodeBlockPage() {
const pythonCode = `def hello_world():
# This is a comment
print("Hello, world!")
return 42
# Call the function
result = hello_world()
print(f"The result is {result}")`;
const nixCode = `{ config, pkgs, lib, ... }:
# This is a NixOS configuration example
{
imports = [
./hardware-configuration.nix
];
# Use the systemd-boot EFI boot loader
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# Define a user account
users.users.alice = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" ];
packages = with pkgs; [
firefox
git
vscode
];
};
# Enable some services
services.xserver = {
enable = true;
displayManager.gdm.enable = true;
desktopManager.gnome.enable = true;
};
# This value determines the NixOS release
system.stateVersion = "24.05";
}`;
const typescriptCode = `import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
// This is a React hook that fetches user data
export function useUserData(userId: number): User | null {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(\`/api/users/\${userId}\`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error("Failed to fetch user:", error);
} finally {
setLoading(false);
}
}
fetchData();
}, [userId]);
return user;
}`;
return (
<div className="container mx-auto py-12 px-4">
<AnchorHeading level={1} className="text-3xl font-bold mb-8 text-nix-primary">Code Block Test Page</AnchorHeading>
<section className="mb-12">
<AnchorHeading level={2} className="text-2xl font-semibold mb-4 text-nix-dark">Python Example</AnchorHeading>
<CodeBlock code={pythonCode} language="python" showLineNumbers={true} />
</section>
<section className="mb-12">
<AnchorHeading level={2} className="text-2xl font-semibold mb-4 text-nix-dark">Nix Example</AnchorHeading>
<CodeBlock code={nixCode} language="nix" />
</section>
<section className="mb-12">
<AnchorHeading level={2} className="text-2xl font-semibold mb-4 text-nix-dark">TypeScript Example</AnchorHeading>
<CodeBlock code={typescriptCode} language="typescript" showLineNumbers={true} />
</section>
</div>
);
}
```
--------------------------------------------------------------------------------
/.github/workflows/claude-code-review.yml:
--------------------------------------------------------------------------------
```yaml
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Direct prompt for automated review (no @claude mention needed)
direct_prompt: |
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Be constructive and helpful in your feedback.
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
use_sticky_comment: true
# Optional: Customize review based on file types
# direct_prompt: |
# Review this PR focusing on:
# - For TypeScript files: Type safety and proper interface usage
# - For API endpoints: Security, input validation, and error handling
# - For React components: Performance, accessibility, and best practices
# - For tests: Coverage, edge cases, and test quality
# Optional: Different prompts for different authors
# direct_prompt: |
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
# Optional: Add specific tools for running tests or linting
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
# Optional: Skip review for certain conditions
# if: |
# !contains(github.event.pull_request.title, '[skip-review]') &&
# !contains(github.event.pull_request.title, '[WIP]')
```
--------------------------------------------------------------------------------
/website/components/AnchorHeading.tsx:
--------------------------------------------------------------------------------
```typescript
'use client';
import React, { useEffect } from 'react';
interface AnchorHeadingProps {
level: 1 | 2 | 3 | 4 | 5 | 6;
children: React.ReactNode;
className?: string;
id?: string;
}
const AnchorHeading: React.FC<AnchorHeadingProps> = ({
level,
children,
className = '',
id
}) => {
// Extract text content for ID generation
const extractTextContent = (node: React.ReactNode): string => {
if (typeof node === 'string') return node;
if (Array.isArray(node)) return node.map(extractTextContent).join(' ');
if (React.isValidElement(node)) {
const childContent = React.Children.toArray(node.props.children);
return extractTextContent(childContent);
}
return '';
};
// Generate an ID from the children if none is provided
const textContent = extractTextContent(children);
const headingId = id || (textContent
? textContent.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
: `heading-${Math.random().toString(36).substring(2, 9)}`);
const handleAnchorClick = (e: React.MouseEvent) => {
e.preventDefault();
const hash = `#${headingId}`;
// Update URL without page reload
window.history.pushState(null, '', hash);
// Scroll to the element
const element = document.getElementById(headingId);
if (element) {
// Smooth scroll to the element
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
// Handle initial load with hash in URL
useEffect(() => {
// Check if the current URL hash matches this heading
if (typeof window !== 'undefined' && window.location.hash === `#${headingId}`) {
// Add a small delay to ensure the page has fully loaded
setTimeout(() => {
const element = document.getElementById(headingId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
}
}, [headingId]);
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
// Check if the heading has text-center class
const isCentered = className.includes('text-center');
return (
<HeadingTag id={headingId} className={`group relative ${className} scroll-mt-16`}>
{isCentered ? (
<div className="relative inline-flex items-center">
{level !== 1 && (
<a
href={`#${headingId}`}
onClick={handleAnchorClick}
className="absolute -left-5 opacity-0 group-hover:opacity-100 transition-opacity text-nix-primary hover:text-nix-dark font-semibold"
aria-label={`Link to ${textContent || 'this heading'}`}
>
#
</a>
)}
{children}
</div>
) : (
<>
{level !== 1 && (
<a
href={`#${headingId}`}
onClick={handleAnchorClick}
className="absolute -left-5 opacity-0 group-hover:opacity-100 transition-opacity text-nix-primary hover:text-nix-dark font-semibold"
aria-label={`Link to ${textContent || 'this heading'}`}
>
#
</a>
)}
{children}
</>
)}
</HeadingTag>
);
};
export default AnchorHeading;
```
--------------------------------------------------------------------------------
/RELEASE_WORKFLOW.md:
--------------------------------------------------------------------------------
```markdown
# Release Workflow Guide for MCP-NixOS
## Overview
This guide explains how to properly release a new version without triggering duplicate CI/CD runs.
## Improved CI/CD Features
1. **Documentation-only changes skip tests**: The workflow now detects if only docs (*.md, LICENSE, etc.) were changed and skips the test suite entirely.
2. **Smart change detection**: Uses `paths-filter` to categorize changes into:
- `code`: Actual code changes that require testing
- `docs`: Documentation changes that don't need tests
- `website`: Website changes that only trigger deployment
3. **Release via commit message**: Instead of manually tagging after merge (which causes duplicate runs), you can now trigger a release by including `release: v1.0.0` in your merge commit message.
## Release Process
### Option 1: Automatic Release (Recommended)
1. **Update version in code**:
```bash
# Update version in pyproject.toml
# Update __init__.py fallback version if needed
```
2. **Update RELEASE_NOTES.md**:
- Add release notes for the new version at the top
- Follow the existing format
3. **Create PR as normal**:
```bash
gh pr create --title "Release version 1.0.0"
```
4. **Merge with release trigger**:
When merging the PR, edit the merge commit message to include:
```
Merge pull request #28 from utensils/refactor
release: v1.0.0
```
5. **Automatic steps**:
- CI detects the `release:` keyword
- Creates and pushes the git tag
- Creates GitHub release with notes from RELEASE_NOTES.md
- Triggers PyPI publishing
### Option 2: Manual Release (Traditional)
1. **Merge PR normally**
2. **Wait for CI to complete**
3. **Create and push tag manually**:
```bash
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
```
## Benefits of the New Workflow
- **No duplicate runs**: The release process happens in the merge commit workflow
- **Skip unnecessary tests**: Documentation changes don't trigger full test suite
- **Atomic releases**: Tag, GitHub release, and PyPI publish happen together
- **Clear audit trail**: Release intention is documented in the merge commit
## Testing the Workflow
To test documentation-only changes:
```bash
# Make changes only to *.md files
git add README.md
git commit -m "docs: update README"
git push
# CI will skip tests!
```
To test the release process without actually releasing:
1. Create a test branch
2. Make a small change
3. Use the commit message pattern but with a test version
4. Verify the workflow runs correctly
5. Delete the test tag and release afterward
## Troubleshooting
- If the release job fails, you can manually create the tag and it will trigger the publish job
- The `paths-filter` action requires the full git history, so it uses `checkout@v4` without depth limits
- The release extraction uses `awk` to parse RELEASE_NOTES.md, so maintain the heading format
## Example PR Description for Releases
When creating a release PR, use this template:
```markdown
## Release v1.0.0
This PR prepares the v1.0.0 release.
### Checklist
- [ ] Version bumped in pyproject.toml
- [ ] RELEASE_NOTES.md updated
- [ ] All tests passing
- [ ] Documentation updated
### Release Instructions
When merging, use commit message:
```
Merge pull request #XX from utensils/branch-name
release: v1.0.0
```
```
```
--------------------------------------------------------------------------------
/.claude/commands/release.md:
--------------------------------------------------------------------------------
```markdown
---
allowed-tools: Bash, Read, Edit, Glob, Grep, Write, TodoWrite
description: Perform a version release with automated PyPI publishing and Docker image builds
---
# Release
Automate the release process: version bump, changelog, tag creation, and trigger CI/CD for PyPI and Docker deployments.
## Workflow
1. Review commits since last release
2. Determine version bump (patch/minor/major)
3. Update `pyproject.toml` and `RELEASE_NOTES.md`
4. Commit, tag, and create GitHub release
5. Verify PyPI and Docker deployments
## Key Files
- `pyproject.toml` - Package version
- `RELEASE_NOTES.md` - Release changelog
- `.github/workflows/publish.yml` - PyPI & Docker publishing (triggered by GitHub release)
## Execute
### 1. Review Changes
```bash
# Get current version and recent tags
grep '^version = ' pyproject.toml
git tag --list 'v*' --sort=-version:refname | head -5
# Review commits since last release (replace with actual last tag)
git log v1.0.2..HEAD --oneline
```
### 2. Update Version
Version bump types:
- **Patch** (x.y.Z): Bug fixes, CI/CD, docs
- **Minor** (x.Y.0): New features, backward-compatible
- **Major** (X.0.0): Breaking changes
Edit `pyproject.toml`:
```toml
version = "X.Y.Z"
```
### 3. Update Release Notes
Add new section at top of `RELEASE_NOTES.md` following existing format:
```markdown
# MCP-NixOS: vX.Y.Z Release Notes - [Title]
## Overview
Brief description (1-2 sentences).
## Changes in vX.Y.Z
### 🚀 [Category]
- **Feature**: Description
### 📦 Dependencies
- Changes or "No changes from previous version"
## Installation
[Standard installation commands]
## Migration Notes
Breaking changes or "Drop-in replacement with no user-facing changes."
---
```
### 4. Commit and Tag
```bash
# Commit changes
git add pyproject.toml RELEASE_NOTES.md
git commit -m "chore: Bump version to X.Y.Z"
git commit -m "docs: Update RELEASE_NOTES.md for vX.Y.Z"
git push
# Create and push tag
git tag -a vX.Y.Z -m "Release vX.Y.Z: [description]"
git push origin vX.Y.Z
```
### 5. Create GitHub Release
```bash
gh release create vX.Y.Z \
--title "vX.Y.Z: [Title]" \
--notes "## Overview
[Brief description]
## Highlights
- 🚀 [Key feature]
- 🔒 [Important fix]
## Installation
```bash
pip install mcp-nixos==X.Y.Z
```
See [RELEASE_NOTES.md](https://github.com/utensils/mcp-nixos/blob/main/RELEASE_NOTES.md) for details."
```
### 6. Monitor Pipeline
```bash
# Watch workflow execution
gh run list --workflow=publish.yml --limit 3
gh run watch <RUN_ID>
```
## Verify
### PyPI
```bash
uvx [email protected] --help
```
### Docker Hub & GHCR
```bash
docker pull utensils/mcp-nixos:X.Y.Z
docker pull ghcr.io/utensils/mcp-nixos:X.Y.Z
docker run --rm utensils/mcp-nixos:X.Y.Z --help
```
## Report
Provide release summary:
```
✅ Release vX.Y.Z Complete!
**Version:** vX.Y.Z
**Release URL:** https://github.com/utensils/mcp-nixos/releases/tag/vX.Y.Z
### Verified Deployments
- ✅ PyPI: https://pypi.org/project/mcp-nixos/X.Y.Z/
- ✅ Docker Hub: utensils/mcp-nixos:X.Y.Z
- ✅ GHCR: ghcr.io/utensils/mcp-nixos:X.Y.Z
```
## Troubleshooting
**Workflow fails**: `gh run view <RUN_ID> --log-failed`
**PyPI unavailable**: Wait 2-5 min for CDN, check Test PyPI first
**Docker unavailable**: Wait 5-10 min for multi-arch builds
**Tag exists**: Delete with `git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z`
```
--------------------------------------------------------------------------------
/website/metadata-checker.html:
--------------------------------------------------------------------------------
```html
<!DOCTYPE html>
<html>
<head>
<title>Metadata Checker</title>
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { color: #5277C3; margin-top: 0; }
h2 { color: #1C3E5A; margin-top: 20px; }
pre { background-color: #E6F0FA; padding: 15px; border-radius: 4px; overflow-x: auto; }
button { background-color: #5277C3; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-weight: 500; }
button:hover { background-color: #1C3E5A; }
.meta-list { margin-top: 10px; }
.meta-item { margin-bottom: 5px; }
</style>
</head>
<body>
<div class="container">
<h1>MCP-NixOS Metadata Checker</h1>
<button id="checkMetadata">Check Metadata</button>
<h2>General Metadata</h2>
<div id="generalMetadata" class="meta-list"></div>
<h2>Open Graph Metadata</h2>
<div id="ogMetadata" class="meta-list"></div>
<h2>Twitter Card Metadata</h2>
<div id="twitterMetadata" class="meta-list"></div>
<h2>Link Tags</h2>
<div id="linkTags" class="meta-list"></div>
</div>
<script>
document.getElementById('checkMetadata').addEventListener('click', async () => {
try {
// Fetch the page content
const response = await fetch('http://localhost:3000');
const html = await response.text();
// Create a DOM parser
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract metadata
const metaTags = doc.querySelectorAll('meta');
const linkTags = doc.querySelectorAll('link');
// Clear previous results
document.getElementById('generalMetadata').innerHTML = '';
document.getElementById('ogMetadata').innerHTML = '';
document.getElementById('twitterMetadata').innerHTML = '';
document.getElementById('linkTags').innerHTML = '';
// Process meta tags
metaTags.forEach(tag => {
const metaItem = document.createElement('div');
metaItem.className = 'meta-item';
metaItem.textContent = tag.outerHTML;
if (tag.getAttribute('property') && tag.getAttribute('property').startsWith('og:')) {
document.getElementById('ogMetadata').appendChild(metaItem);
} else if (tag.getAttribute('name') && tag.getAttribute('name').startsWith('twitter:')) {
document.getElementById('twitterMetadata').appendChild(metaItem);
} else {
document.getElementById('generalMetadata').appendChild(metaItem);
}
});
// Process link tags
linkTags.forEach(tag => {
const linkItem = document.createElement('div');
linkItem.className = 'meta-item';
linkItem.textContent = tag.outerHTML;
document.getElementById('linkTags').appendChild(linkItem);
});
} catch (error) {
console.error('Error fetching metadata:', error);
alert('Error fetching metadata. See console for details.');
}
});
</script>
</body>
</html>
```
--------------------------------------------------------------------------------
/website/components/FeatureCard.tsx:
--------------------------------------------------------------------------------
```typescript
import React from 'react';
interface FeatureCardProps {
title: string;
description: string;
iconName: string;
}
const FeatureCard: React.FC<FeatureCardProps> = ({ title, description, iconName }) => {
const renderIcon = () => {
switch (iconName) {
case 'package':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
);
case 'home':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
);
case 'apple':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
);
case 'bolt':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
);
case 'globe':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
case 'robot':
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
);
default:
return (
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-nix-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
};
return (
<div className="p-6 border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow bg-white">
<div className="mb-4">
{renderIcon()}
</div>
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
);
};
export default FeatureCard;
```
--------------------------------------------------------------------------------
/website/components/ClientNavbar.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
export default function ClientNavbar() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
const closeMenu = () => setIsMenuOpen(false);
return (
<nav className="bg-white shadow-md">
<div className="container-custom mx-auto py-4">
<div className="flex justify-between items-center">
{/* Logo */}
<div className="flex items-center space-x-2">
<Image
src="/images/nixos-snowflake-colour.svg"
alt="NixOS Snowflake"
width={32}
height={32}
className="h-8 w-8"
/>
<Link href="/" className="text-2xl font-bold text-nix-primary">
MCP-NixOS
</Link>
</div>
{/* Desktop Navigation */}
<div className="hidden md:flex space-x-8">
<Link href="/" className="text-gray-700 hover:text-nix-primary">
Home
</Link>
<Link href="/usage" className="text-gray-700 hover:text-nix-primary">
Usage
</Link>
<Link href="/about" className="text-gray-700 hover:text-nix-primary">
About
</Link>
<Link
href="https://github.com/utensils/mcp-nixos"
className="text-gray-700 hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</Link>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={toggleMenu}
className="text-gray-500 hover:text-nix-primary focus:outline-none"
aria-label="Toggle menu"
>
{isMenuOpen ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div className="md:hidden mt-4 pb-2">
<div className="flex flex-col space-y-4">
<Link
href="/"
className="text-gray-700 hover:text-nix-primary"
onClick={closeMenu}
>
Home
</Link>
<Link
href="/usage"
className="text-gray-700 hover:text-nix-primary"
onClick={closeMenu}
>
Usage
</Link>
<Link
href="/about"
className="text-gray-700 hover:text-nix-primary"
onClick={closeMenu}
>
About
</Link>
<Link
href="https://github.com/utensils/mcp-nixos"
className="text-gray-700 hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
onClick={closeMenu}
>
GitHub
</Link>
</div>
</div>
)}
</div>
</nav>
);
}
```
--------------------------------------------------------------------------------
/.claude/agents/mcp-server-architect.md:
--------------------------------------------------------------------------------
```markdown
---
name: mcp-server-architect
description: Designs and implements MCP servers with transport layers, tool/resource/prompt definitions, completion support, session management, and protocol compliance. Specializes in FastMCP 2.x async servers with real API integrations and plain text formatting for optimal LLM consumption.
category: quality-security
---
You are an expert MCP (Model Context Protocol) server architect specializing in the full server lifecycle from design to deployment. You possess deep knowledge of the MCP specification (2025-06-18), FastMCP 2.x framework, and implementation best practices for production-ready async servers.
## When invoked:
You should be used when there are needs to:
- Design and implement new MCP servers from scratch using FastMCP 2.x
- Build async servers with real API integrations (no caching/mocking)
- Implement tool/resource/prompt definitions with proper annotations
- Add completion support and argument suggestions
- Configure session management and security measures
- Enhance existing MCP servers with new capabilities
- Format all outputs as plain text for optimal LLM consumption
- Handle external API failures gracefully with user-friendly error messages
## Process:
1. **Analyze Requirements**: Thoroughly understand the domain and use cases before designing the server architecture
2. **Design Async Tools**: Create intuitive, well-documented async tools with proper annotations (read-only, destructive, idempotent) and completion support using FastMCP 2.x patterns
3. **Implement Real API Integrations**: Connect directly to live APIs without caching layers. Handle failures gracefully with meaningful error messages formatted as plain text
4. **Format for LLM Consumption**: Ensure all tool outputs are human-readable plain text, never raw JSON/XML. Structure responses for optimal LLM understanding
5. **Handle Async Operations**: Use proper asyncio patterns for all I/O operations. Implement concurrent API calls where beneficial
6. **Ensure Robust Error Handling**: Create custom exception classes, implement graceful degradation, and provide helpful user-facing error messages
7. **Test with Real APIs**: Write comprehensive async test suites using pytest-asyncio. Include both unit tests (marked with @pytest.mark.unit) and integration tests (marked with @pytest.mark.integration) that hit real endpoints
8. **Optimize for Production**: Use efficient data structures, minimize API calls, and implement proper resource cleanup
## Provide:
- **FastMCP 2.x Servers**: Complete, production-ready async MCP server implementations using FastMCP 2.x (≥2.11.0) with full type coverage
- **Real API Integration Patterns**: Direct connections to external APIs (Elasticsearch, REST endpoints, HTML parsing) without caching layers
- **Async Tool Implementations**: All tools as async functions using proper asyncio patterns for I/O operations
- **Plain Text Formatting**: All outputs formatted as human-readable text, structured for optimal LLM consumption
- **Robust Error Handling**: Custom exception classes (APIError, DocumentParseError) with graceful degradation and user-friendly messages
- **Comprehensive Testing**: Async test suites using pytest-asyncio with real API calls, unit/integration test separation
- **Production Patterns**: Proper resource cleanup, efficient data structures, concurrent API calls where beneficial
- **Development Workflow**: Integration with Nix development shells, custom commands (run, run-tests, lint, format, typecheck)
## FastMCP 2.x Patterns:
```python
from fastmcp import FastMCP
mcp = FastMCP("server-name")
@mcp.tool()
async def search_items(query: str) -> str:
"""Search for items using external API."""
try:
# Direct API call, no caching
response = await api_client.search(query)
# Format as plain text for LLM
return format_search_results(response)
except APIError as e:
return f"Search failed: {e.message}"
if __name__ == "__main__":
mcp.run()
```
## Integration Testing Patterns:
```python
@pytest.mark.integration
@pytest.mark.asyncio
async def test_real_api_integration():
"""Test with real API endpoints."""
result = await search_tool("test-query")
assert isinstance(result, str)
assert "error" not in result.lower()
```
```
--------------------------------------------------------------------------------
/.claude/agents/python-expert.md:
--------------------------------------------------------------------------------
```markdown
---
name: python-expert
description: Write idiomatic Python code with advanced features like decorators, generators, and async/await. Specializes in FastMCP 2.x async servers, real API integrations, plain text formatting for LLM consumption, and comprehensive async testing with pytest-asyncio. Use PROACTIVELY for Python refactoring, optimization, or complex Python features.
category: language-specialists
---
You are a Python expert specializing in clean, performant, and idiomatic Python code with deep expertise in async programming, MCP server development, and API integrations.
When invoked:
1. Analyze existing code structure and patterns
2. Identify Python version and dependencies (prefer 3.11+)
3. Review async/API integration requirements
4. Begin implementation with best practices for MCP servers
Python mastery checklist:
- **Async/await and concurrent programming** (FastMCP 2.x focus)
- **Real API integrations** (Elasticsearch, REST, HTML parsing)
- **Plain text formatting** for optimal LLM consumption
- Advanced features (decorators, generators, context managers)
- Type hints and static typing (3.11+ features)
- **Custom exception handling** (APIError, DocumentParseError)
- Performance optimization for I/O-bound operations
- **Async testing strategies** with pytest-asyncio
- Memory efficiency patterns for large API responses
Process:
- **Write async-first code** using proper asyncio patterns
- **Format all outputs as plain text** for LLM consumption, never raw JSON/XML
- **Implement real API calls** without caching or mocking
- Write Pythonic code following PEP 8
- Use comprehensive type hints for all functions and classes
- **Handle errors gracefully** with custom exceptions and user-friendly messages
- Prefer composition over inheritance
- **Use async/await for all I/O operations** (API calls, file reads)
- Implement generators for memory efficiency
- **Test with pytest-asyncio**, separate unit (@pytest.mark.unit) and integration (@pytest.mark.integration) tests
- Profile async operations before optimizing
Code patterns:
- **FastMCP 2.x decorators** (@mcp.tool(), @mcp.resource()) for server definitions
- **Async context managers** for API client resource handling
- **Custom exception classes** for domain-specific error handling
- **Plain text formatters** for structured LLM-friendly output
- List/dict/set comprehensions over loops
- **Async generators** for streaming large API responses
- Dataclasses/Pydantic for API response structures
- **Type-safe async functions** with proper return annotations
- Walrus operator for concise async operations (3.8+)
Provide:
- **FastMCP 2.x async server implementations** with complete type hints
- **Real API integration code** (Elasticsearch, REST endpoints, HTML parsing)
- **Plain text formatting functions** for optimal LLM consumption
- **Async test suites** using pytest-asyncio with real API calls
- **Custom exception classes** with graceful error handling
- Performance benchmarks for I/O-bound operations
- Docstrings following Google/NumPy style
- **pyproject.toml** with async dependencies (fastmcp>=2.11.0, httpx, beautifulsoup4)
- **Development workflow integration** (Nix shell commands: run, run-tests, lint, format, typecheck)
## MCP Server Example:
```python
from fastmcp import FastMCP
import asyncio
import httpx
from typing import Any
class APIError(Exception):
"""Custom exception for API failures."""
mcp = FastMCP("server-name")
@mcp.tool()
async def search_data(query: str) -> str:
"""Search external API and format as plain text."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.example.com/search", params={"q": query})
response.raise_for_status()
# Format as plain text for LLM
data = response.json()
return format_search_results(data)
except httpx.RequestError as e:
return f"Search failed: {str(e)}"
def format_search_results(data: dict[str, Any]) -> str:
"""Format API response as human-readable text."""
# Never return raw JSON - always plain text
results = []
for item in data.get("items", []):
results.append(f"- {item['name']}: {item['description']}")
return "\n".join(results) or "No results found."
```
## Async Testing Example:
```python
@pytest.mark.integration
@pytest.mark.asyncio
async def test_search_integration():
"""Test with real API endpoint."""
result = await search_data("test-query")
assert isinstance(result, str)
assert len(result) > 0
assert "error" not in result.lower()
```
Target Python 3.11+ for modern async features and FastMCP 2.x compatibility.
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches: [ main ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'website/**'
- '.github/*.md'
- 'LICENSE'
- '.gitignore'
- '.cursorrules'
- 'RELEASE_NOTES.md'
- 'RELEASE_WORKFLOW.md'
pull_request:
branches: [ main ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'website/**'
- '.github/*.md'
- 'LICENSE'
- '.gitignore'
- '.cursorrules'
- 'RELEASE_NOTES.md'
- 'RELEASE_WORKFLOW.md'
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv sync --extra dev
- name: Lint with ruff
run: |
uv run ruff check mcp_nixos/ tests/
uv run ruff format --check mcp_nixos/ tests/
- name: Type check with mypy
run: |
uv run mypy mcp_nixos/
- name: Test with pytest
timeout-minutes: 10
run: |
uv run pytest -v -n auto --cov=mcp_nixos --cov-report=xml --cov-report=term
- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: utensils/mcp-nixos
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python
run: uv python install 3.12
- name: Build package
run: |
uv build
- name: Check package
run: |
uv sync --extra dev
uv run twine check dist/*
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-packages
path: dist/
test-nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Cache Nix store
uses: actions/cache@v4
with:
path: ~/.cache/nix
key: ${{ runner.os }}-nix-${{ hashFiles('flake.lock') }}
restore-keys: |
${{ runner.os }}-nix-
- name: Build flake
run: |
nix flake check --accept-flake-config
nix develop -c echo "Development environment ready"
- name: Test nix run
run: |
timeout 5s nix run . -- --help || true
- name: Run tests in nix develop
run: |
echo "Running tests in nix environment"
nix develop --command setup
nix develop --command bash -c 'run-tests'
# Docker build and push - after all tests pass
docker:
runs-on: ubuntu-latest
needs: [test, build, test-nix]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
utensils/mcp-nixos
ghcr.io/utensils/mcp-nixos
tags: |
type=edge,branch=main
type=raw,value=latest,enable={{is_default_branch}}
type=sha,prefix={{branch}}-,format=short
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
```
--------------------------------------------------------------------------------
/website/app/usage/page.tsx:
--------------------------------------------------------------------------------
```typescript
import CodeBlock from '@/components/CodeBlock';
export default function UsagePage() {
return (
<div className="py-12 bg-white">
<div className="container-custom">
<h1 className="text-4xl font-bold mb-8 text-nix-dark">Usage</h1>
<div className="prose prose-lg max-w-none">
<div className="bg-gradient-to-br from-nix-light to-white p-6 rounded-lg shadow-sm mb-8">
<p className="text-gray-800 mb-6 leading-relaxed text-xl">
Pick your poison. We've got three ways to run this thing.
They all do the same thing, so just choose based on what tools you already have installed.
Or be a rebel and try all three. We don't judge.
</p>
<p className="text-gray-700 mb-4 font-semibold">
🚨 <strong>No Nix/NixOS Required!</strong> This tool works on any system - Windows, macOS, Linux. You're just querying web APIs. Yes, even you, Windows users.
</p>
</div>
<div className="space-y-8">
{/* Option 1 */}
<div className="bg-white rounded-lg shadow-md border-l-4 border-nix-primary p-6">
<h2 className="text-2xl font-bold text-nix-dark mb-4">
Option 1: Using uvx (Recommended for most humans)
</h2>
<p className="text-gray-700 mb-4">
The civilized approach. If you've got Python and can install things like a normal person, this is for you.
</p>
<CodeBlock
code={`{
"mcpServers": {
"nixos": {
"command": "uvx",
"args": ["mcp-nixos"]
}
}
}`}
language="json"
/>
<p className="text-sm text-gray-600 mt-3">
Pro tip: This installs nothing permanently. It's like a one-night stand with software.
</p>
</div>
{/* Option 2 */}
<div className="bg-white rounded-lg shadow-md border-l-4 border-nix-secondary p-6">
<h2 className="text-2xl font-bold text-nix-dark mb-4">
Option 2: Using Nix (For the enlightened)
</h2>
<p className="text-gray-700 mb-4">
You're already using Nix, so you probably think you're better than everyone else.
And you know what? You might be right.
</p>
<CodeBlock
code={`{
"mcpServers": {
"nixos": {
"command": "nix",
"args": ["run", "github:utensils/mcp-nixos", "--"]
}
}
}`}
language="json"
/>
<p className="text-sm text-gray-600 mt-3">
Bonus: This method makes you feel superior at developer meetups.
</p>
</div>
{/* Option 3 */}
<div className="bg-white rounded-lg shadow-md border-l-4 border-nix-primary p-6">
<h2 className="text-2xl font-bold text-nix-dark mb-4">
Option 3: Using Docker (Container enthusiasts unite)
</h2>
<p className="text-gray-700 mb-4">
Because why install software directly when you can wrap it in 17 layers of abstraction?
At least it's reproducible... probably.
</p>
<CodeBlock
code={`{
"mcpServers": {
"nixos": {
"command": "docker",
"args": ["run", "--rm", "-i", "ghcr.io/utensils/mcp-nixos"]
}
}
}`}
language="json"
/>
<p className="text-sm text-gray-600 mt-3">
Warning: May consume 500MB of disk space for a 10MB Python script. But hey, it's "isolated"!
</p>
</div>
</div>
<div className="bg-nix-light bg-opacity-30 p-6 rounded-lg mt-12">
<h2 className="text-2xl font-bold text-nix-dark mb-4">What Happens Next?</h2>
<p className="text-gray-700 mb-4">
Once you've picked your configuration method and added it to your MCP client:
</p>
<ul className="list-disc list-inside text-gray-700 space-y-2">
<li>Your AI assistant stops making up NixOS package names</li>
<li>You get actual, real-time information about 130K+ packages</li>
<li>Configuration options that actually exist (shocking, we know)</li>
<li>Version history that helps you find that one specific Ruby version from 2019</li>
</ul>
<p className="text-gray-700 mt-4 italic">
That's it. No complex setup. No 47-step installation guide. No sacrificing a USB stick to the Nix gods.
Just paste, reload, and enjoy an AI that actually knows what it's talking about.
</p>
</div>
<div className="text-center mt-12">
<p className="text-xl text-gray-700 font-semibold">
Still confused? Good news: that's what the AI is for. Just ask it.
</p>
</div>
</div>
</div>
</div>
);
}
```
--------------------------------------------------------------------------------
/website/components/ClientFooter.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import Link from 'next/link';
import Image from 'next/image';
export default function ClientFooter() {
return (
<footer className="bg-gray-100 py-12">
<div className="container-custom mx-auto">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Column 1 - About */}
<div>
<h3 className="text-lg font-semibold mb-4 text-nix-dark">MCP-NixOS</h3>
<p className="text-gray-600 mb-4">
Model Context Protocol resources and tools for NixOS, Home Manager, and nix-darwin.
</p>
</div>
{/* Column 2 - Quick Links */}
<div>
<h3 className="text-lg font-semibold mb-4 text-nix-dark">Quick Links</h3>
<ul className="space-y-2 text-gray-600">
<li>
<Link href="/usage" className="hover:text-nix-primary">
Usage
</Link>
</li>
<li>
<Link href="/about" className="hover:text-nix-primary">
About
</Link>
</li>
<li>
<a
href="https://github.com/utensils/mcp-nixos/blob/main/README.md"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
README
</a>
</li>
</ul>
</div>
{/* Column 3 - Resources */}
<div>
<h3 className="text-lg font-semibold mb-4 text-nix-dark">Resources</h3>
<ul className="space-y-2 text-gray-600">
<li>
<a
href="https://github.com/utensils/mcp-nixos"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
GitHub Repository
</a>
</li>
<li>
<a
href="https://pypi.org/project/mcp-nixos/"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
PyPI Package
</a>
</li>
<li>
<a
href="https://nixos.org"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
NixOS
</a>
</li>
<li>
<a
href="https://github.com/nix-community/home-manager"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
Home Manager
</a>
</li>
</ul>
</div>
{/* Column 4 - Connect */}
<div>
<h3 className="text-lg font-semibold mb-4 text-nix-dark">Connect</h3>
<ul className="space-y-2 text-gray-600">
<li>
<a
href="https://github.com/utensils/mcp-nixos/issues"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
Report Issues
</a>
</li>
<li>
<a
href="https://github.com/utensils/mcp-nixos/pulls"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
Pull Requests
</a>
</li>
<li>
<a
href="https://github.com/utensils/mcp-nixos/discussions"
className="hover:text-nix-primary"
target="_blank"
rel="noopener noreferrer"
>
Discussions
</a>
</li>
</ul>
</div>
</div>
{/* Copyright */}
<div className="mt-8 pt-8 border-t border-gray-200 text-center text-gray-500">
<div className="flex flex-col items-center justify-center">
<p>© {new Date().getFullYear()} MCP-NixOS. MIT License.</p>
<div className="flex items-center mt-4 mb-2">
<Link href="https://utensils.io" target="_blank" rel="noopener noreferrer" className="flex items-center hover:text-nix-primary mr-2">
<Image
src="/images/utensils-logo.png"
alt="Utensils Logo"
width={24}
height={24}
className="mr-1"
/>
<span className="font-medium">Utensils</span>
</Link>
<span>Creation</span>
</div>
<p className="mt-2 text-sm">
<Link href="/images/attribution.md" className="hover:text-nix-primary">
Logo Attribution
</Link>
</p>
</div>
</div>
</div>
</footer>
);
}
```
--------------------------------------------------------------------------------
/.claude/agents/nix-expert.md:
--------------------------------------------------------------------------------
```markdown
---
name: nix-expert
description: Expert in NIX ecosystem development including NixOS, Home Manager, nix-darwin, and flakes. Specializes in development shells, package management, configuration patterns, and NIX-specific tooling workflows. Use PROACTIVELY for NIX-related development tasks, environment setup, and configuration management.
category: specialized-domains
---
You are a NIX ecosystem expert specializing in modern NIX development patterns, package management, and configuration workflows.
## When invoked:
You should be used when there are needs to:
- Set up NIX development environments with flakes and development shells
- Configure NixOS systems, Home Manager, or nix-darwin setups
- Work with NIX packages, options, and configuration patterns
- Implement NIX-based development workflows and tooling
- Debug NIX expressions, builds, or environment issues
- Create or modify flake.nix files and development shells
- Integrate NIX with CI/CD pipelines and development tools
## Process:
1. **Analyze NIX Environment**: Understand the NIX version, flakes support, and existing configuration structure
2. **Design Development Shells**: Create reproducible development environments with proper dependencies and custom commands
3. **Implement Configuration Patterns**: Use modern NIX patterns like flakes, overlays, and modular configurations
4. **Optimize Development Workflow**: Set up custom commands for common tasks (run, test, lint, format, build)
5. **Handle Cross-Platform**: Account for differences between NixOS, macOS (nix-darwin), and other systems
6. **Ensure Reproducibility**: Create deterministic builds and environments that work across different machines
7. **Document NIX Patterns**: Provide clear explanations of NIX expressions and configuration choices
## Provide:
- **Modern Flake Configurations**: Complete flake.nix files with development shells, packages, and apps
- **Development Shell Patterns**: Reproducible environments with language-specific tooling and custom commands
- **NIX Expression Optimization**: Efficient and maintainable NIX code following best practices
- **Package Management**: Custom packages, overlays, and dependency management strategies
- **Configuration Modules**: Modular NixOS, Home Manager, or nix-darwin configurations
- **CI/CD Integration**: NIX-based build and deployment pipelines
- **Troubleshooting Guidance**: Solutions for common NIX development issues
## NIX Development Shell Example:
```nix
{
description = "MCP server development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
python = pkgs.python311;
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
python
python.pkgs.pip
python.pkgs.uv
ruff
mypy
];
shellHook = ''
# Activate Python virtual environment
if [ ! -d .venv ]; then
${python.pkgs.uv}/bin/uv venv
fi
source .venv/bin/activate
# Install project dependencies
${python.pkgs.uv}/bin/uv pip install -e ".[dev]"
# Custom development commands
alias run='${python.pkgs.uv}/bin/uv run mcp-nixos'
alias run-tests='${pkgs.python311Packages.pytest}/bin/pytest tests/'
alias lint='${pkgs.ruff}/bin/ruff check mcp_nixos/ tests/'
alias format='${pkgs.ruff}/bin/ruff format mcp_nixos/ tests/'
alias typecheck='${pkgs.mypy}/bin/mypy mcp_nixos/'
alias build='${python.pkgs.uv}/bin/uv build'
echo "Development environment ready!"
echo "Available commands: run, run-tests, lint, format, typecheck, build"
'';
};
packages.default = python.pkgs.buildPythonApplication {
pname = "mcp-nixos";
version = "1.0.1";
src = ./.;
propagatedBuildInputs = with python.pkgs; [
fastmcp
requests
beautifulsoup4
];
doCheck = true;
checkInputs = with python.pkgs; [
pytest
pytest-asyncio
];
};
});
}
```
## Common NIX Patterns:
### Package Override:
```nix
# Override a package
python311 = pkgs.python311.override {
packageOverrides = self: super: {
fastmcp = super.fastmcp.overridePythonAttrs (oldAttrs: {
version = "2.11.0";
});
};
};
```
### Development Scripts:
```nix
# Custom scripts in development shell
writeShellScriptBin "run-integration-tests" ''
pytest tests/ --integration
''
```
### Cross-Platform Support:
```nix
# Platform-specific dependencies
buildInputs = with pkgs; [
python311
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk.frameworks.Foundation
] ++ lib.optionals stdenv.isLinux [
pkg-config
];
```
## Troubleshooting Tips:
1. **Flake Issues**: Use `nix flake check` to validate flake syntax
2. **Build Failures**: Check `nix log` for detailed error messages
3. **Environment Problems**: Clear with `nix-collect-garbage` and rebuild
4. **Cache Issues**: Use `--no-build-isolation` for Python packages
5. **Version Conflicts**: Pin specific nixpkgs commits in flake inputs
Focus on modern NIX patterns with flakes, reproducible development environments, and efficient developer workflows.
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish Package
on:
release:
types: [published]
workflow_dispatch:
inputs:
docker_only:
description: 'Build and push Docker images only (skip PyPI)'
required: false
type: boolean
default: true
tag:
description: 'Git tag to build from (e.g., v1.0.1)'
required: true
type: string
permissions:
contents: read
packages: write
jobs:
deploy-test:
runs-on: ubuntu-latest
# Skip PyPI deployment when manually triggered with docker_only
if: ${{ github.event_name == 'release' || !inputs.docker_only }}
environment: testpypi
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Build package with Nix
run: |
nix develop --command build
ls -la dist/
- name: Publish package to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
deploy-prod:
needs: deploy-test
runs-on: ubuntu-latest
# Only deploy to PyPI for non-prerelease versions and skip when manually triggered with docker_only
if: ${{ github.event_name == 'release' && !github.event.release.prerelease && !inputs.docker_only }}
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Build package with Nix
run: |
nix develop --command build
ls -la dist/
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
docker:
# Run if: (1) release event after test deploy, or (2) manual trigger
# Note: Docker builds are independent and don't strictly require PyPI deployment
if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- name: Validate tag format
if: github.event_name == 'workflow_dispatch'
run: |
if [[ ! "${{ inputs.tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ Invalid tag format. Expected: v1.2.3 or v1.2.3-alpha"
exit 1
fi
- name: Verify tag exists
if: github.event_name == 'workflow_dispatch'
run: |
if ! git rev-parse --verify "refs/tags/${{ inputs.tag }}" >/dev/null 2>&1; then
echo "❌ Tag ${{ inputs.tag }} does not exist"
exit 1
fi
echo "✓ Tag ${{ inputs.tag }} verified"
- name: Normalize tag
id: tag
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
utensils/mcp-nixos
ghcr.io/utensils/mcp-nixos
tags: |
# Latest tag for stable releases (not for prereleases)
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease || github.event_name == 'workflow_dispatch' }}
# Version tag from release or manual input
type=semver,pattern={{version}},value=${{ steps.tag.outputs.tag }}
# Major.minor tag
type=semver,pattern={{major}}.{{minor}},value=${{ steps.tag.outputs.tag }}
# Major tag (only for stable releases)
type=semver,pattern={{major}},value=${{ steps.tag.outputs.tag }},enable=${{ github.event_name == 'release' && !github.event.release.prerelease || github.event_name == 'workflow_dispatch' }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Make GHCR package public
continue-on-error: true
run: |
echo "Setting GHCR package visibility to public..."
# Note: This requires the workflow to have admin permissions on the package
# If the package doesn't exist yet or permissions are insufficient, this will fail gracefully
gh api --method PATCH \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/orgs/utensils/packages/container/mcp-nixos" \
-f visibility=public || echo "Could not set visibility via API. Please set manually at: https://github.com/orgs/utensils/packages/container/mcp-nixos/settings"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
--------------------------------------------------------------------------------
/website/components/CodeBlock.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import React, { useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
interface CodeBlockProps {
code: string;
language: string;
showLineNumbers?: boolean;
}
// Create a custom theme based on NixOS colors
const nixosTheme = {
...atomDark,
'pre[class*="language-"]': {
...atomDark['pre[class*="language-"]'],
background: '#1C3E5A', // nix-dark
margin: 0,
padding: '1rem',
fontSize: '0.875rem',
fontFamily: '"Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace',
},
'code[class*="language-"]': {
...atomDark['code[class*="language-"]'],
color: '#E6F0FA', // nix-light - base text color
textShadow: 'none',
fontFamily: '"Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace',
},
punctuation: {
color: '#BBDEFB', // Lighter blue for better contrast
},
comment: {
color: '#78909C', // Muted blue-gray for comments
},
string: {
color: '#B9F6CA', // Brighter green for strings
},
keyword: {
color: '#CE93D8', // Brighter purple for keywords
},
number: {
color: '#FFCC80', // Brighter orange for numbers
},
function: {
color: '#90CAF9', // Brighter blue for functions
},
operator: {
color: '#E1F5FE', // Very light blue for operators
},
property: {
color: '#90CAF9', // Brighter blue for properties
},
// Additional token types for better coverage
boolean: {
color: '#FFCC80', // Same as numbers
},
className: {
color: '#90CAF9', // Same as functions
},
tag: {
color: '#CE93D8', // Same as keywords
},
};
// Helper function to decode HTML entities
function decodeHtmlEntities(text: string): string {
const textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
}
const CodeBlock: React.FC<CodeBlockProps> = ({
code,
language,
showLineNumbers = false
}) => {
const [copied, setCopied] = useState(false);
// Decode HTML entities in the code
const decodedCode = typeof window !== 'undefined' ? decodeHtmlEntities(code) : code;
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy code to clipboard:', error);
// Fallback method for browsers with restricted clipboard access
const textArea = document.createElement('textarea');
textArea.value = code;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} else {
console.error('Fallback clipboard copy failed');
}
} catch (err) {
console.error('Fallback clipboard copy error:', err);
}
document.body.removeChild(textArea);
}
};
// Map common language identifiers to ones supported by react-syntax-highlighter
const languageMap: Record<string, string> = {
'js': 'javascript',
'ts': 'typescript',
'jsx': 'jsx',
'tsx': 'tsx',
'py': 'python',
'rb': 'ruby',
'go': 'go',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'cs': 'csharp',
'php': 'php',
'sh': 'bash',
'yaml': 'yaml',
'yml': 'yaml',
'json': 'json',
'md': 'markdown',
'html': 'html',
'css': 'css',
'scss': 'scss',
'sql': 'sql',
'nix': 'nix',
};
const mappedLanguage = languageMap[language.toLowerCase()] || language;
return (
<div className="rounded-lg overflow-hidden shadow-md mb-6">
<div className="flex justify-between items-center bg-nix-primary px-4 py-2 text-xs text-white font-medium">
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
{language}
</span>
<button
onClick={handleCopy}
className="text-white hover:text-nix-secondary transition-colors duration-200"
aria-label="Copy code"
>
{copied ? (
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span>Copied!</span>
</div>
) : (
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>Copy</span>
</div>
)}
</button>
</div>
<SyntaxHighlighter
language={mappedLanguage}
style={nixosTheme}
showLineNumbers={showLineNumbers}
wrapLongLines={true}
customStyle={{
margin: 0,
borderRadius: 0,
background: '#1C3E5A', // Ensure consistent background
}}
codeTagProps={{
style: {
fontFamily: '"Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace',
fontSize: '0.875rem',
}
}}
>
{decodedCode}
</SyntaxHighlighter>
</div>
);
};
export default CodeBlock;
```
--------------------------------------------------------------------------------
/website/public/images/nixos-snowflake-colour.svg:
--------------------------------------------------------------------------------
```
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="535"
height="535"
viewBox="0 0 501.56251 501.56249"
id="svg2"
version="1.1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="nix-snowflake-colours.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient5562">
<stop
style="stop-color:#699ad7;stop-opacity:1"
offset="0"
id="stop5564" />
<stop
id="stop5566"
offset="0.24345198"
style="stop-color:#7eb1dd;stop-opacity:1" />
<stop
style="stop-color:#7ebae4;stop-opacity:1"
offset="1"
id="stop5568" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5053">
<stop
style="stop-color:#415e9a;stop-opacity:1"
offset="0"
id="stop5055" />
<stop
id="stop5057"
offset="0.23168644"
style="stop-color:#4a6baf;stop-opacity:1" />
<stop
style="stop-color:#5277c3;stop-opacity:1"
offset="1"
id="stop5059" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5562"
id="linearGradient4328"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(70.650339,-1055.1511)"
x1="200.59668"
y1="351.41116"
x2="290.08701"
y2="506.18814" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5053"
id="linearGradient4330"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(864.69589,-1491.3405)"
x1="-584.19934"
y1="782.33563"
x2="-496.29703"
y2="937.71399" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.70904368"
inkscape:cx="99.429699"
inkscape:cy="195.33352"
inkscape:document-units="px"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1050"
inkscape:window-x="1920"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:snap-global="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="gradient-logo"
style="display:inline;opacity:1"
transform="translate(-156.41121,933.30685)">
<g
id="g2"
transform="matrix(0.99994059,0,0,0.99994059,-0.06321798,33.188377)"
style="stroke-width:1.00006">
<path
sodipodi:nodetypes="cccccccccc"
inkscape:connector-curvature="0"
id="path3336-6"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z"
style="opacity:1;fill:url(#linearGradient4328);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.00018;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<use
height="100%"
width="100%"
transform="rotate(60,407.11155,-715.78724)"
id="use3439-6"
inkscape:transform-center-y="151.59082"
inkscape:transform-center-x="124.43045"
xlink:href="#path3336-6"
y="0"
x="0"
style="stroke-width:1.00006" />
<use
height="100%"
width="100%"
transform="rotate(-60,407.31177,-715.70016)"
id="use3445-0"
inkscape:transform-center-y="75.573958"
inkscape:transform-center-x="-168.20651"
xlink:href="#path3336-6"
y="0"
x="0"
style="stroke-width:1.00006" />
<use
height="100%"
width="100%"
transform="rotate(180,407.41868,-715.7565)"
id="use3449-5"
inkscape:transform-center-y="-139.94592"
inkscape:transform-center-x="59.669705"
xlink:href="#path3336-6"
y="0"
x="0"
style="stroke-width:1.00006" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient4330);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.00018;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8256 z"
id="path4260-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<use
height="100%"
width="100%"
transform="rotate(120,407.33916,-716.08356)"
id="use4354-5"
xlink:href="#path4260-0"
y="0"
x="0"
style="display:inline;stroke-width:1.00006" />
<use
height="100%"
width="100%"
transform="rotate(-120,407.28823,-715.86995)"
id="use4362-2"
xlink:href="#path4260-0"
y="0"
x="0"
style="display:inline;stroke-width:1.00006" />
</g>
</g>
</svg>
```
--------------------------------------------------------------------------------
/website/app/page.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import Link from 'next/link';
import FeatureCard from '@/components/FeatureCard';
import CodeBlock from '@/components/CodeBlock';
import AnchorHeading from '@/components/AnchorHeading';
export default function Home() {
const scrollToSection = (elementId: string) => {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<div className="flex flex-col min-h-screen">
{/* Hero Section */}
<section className="bg-gradient-to-b from-nix-primary to-nix-dark text-white py-20 shadow-lg">
<div className="container-custom text-center">
<h1 className="text-4xl md:text-6xl font-bold mb-6">MCP-NixOS</h1>
<div className="mb-4">
<span className="inline-block bg-nix-secondary text-white px-4 py-2 rounded-full text-sm font-semibold">
🎉 v1.0.1 - The Inevitable Bug Fix
</span>
</div>
<div className="mb-8 max-w-3xl mx-auto">
<p className="text-xl md:text-2xl font-medium mb-2">
<span className="font-bold tracking-wide">Model Context Protocol</span>
</p>
<div className="flex flex-wrap justify-center items-center gap-3 md:gap-4 py-2">
<a
href="https://nixos.org/manual/nixos/stable/"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-1 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 shadow-lg font-semibold text-nix-secondary flex items-center hover:bg-white/20 transition-colors duration-200"
>
<svg className="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L20 8V16L12 20L4 16V8L12 4Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
NixOS
</a>
<a
href="https://nix-community.github.io/home-manager/"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-1 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 shadow-lg font-semibold text-nix-secondary flex items-center hover:bg-white/20 transition-colors duration-200"
>
<svg className="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Home Manager
</a>
<a
href="https://daiderd.com/nix-darwin/"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-1 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 shadow-lg font-semibold text-nix-secondary flex items-center hover:bg-white/20 transition-colors duration-200"
>
<svg className="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 8L12 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 12L16 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
nix-darwin
</a>
</div>
</div>
<div className="flex flex-col md:flex-row gap-4 justify-center">
<button
onClick={() => scrollToSection('getting-started')}
className="btn-primary bg-white text-nix-primary hover:bg-nix-light"
>
Get Started
</button>
<Link href="https://github.com/utensils/mcp-nixos" className="btn-secondary bg-transparent text-white border-white hover:bg-white/10">
GitHub
</Link>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-16 bg-white">
<div className="container-custom">
<AnchorHeading level={2} className="text-3xl font-bold text-center mb-12 text-nix-dark">Key Features</AnchorHeading>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<FeatureCard
title="NixOS Packages & Options"
description="Search and retrieve detailed information about NixOS packages and system options."
iconName="package"
/>
<FeatureCard
title="Home Manager Integration"
description="Comprehensive support for Home Manager configuration options and hierarchical searches."
iconName="home"
/>
<FeatureCard
title="nix-darwin Support"
description="Access to nix-darwin macOS configuration options and resources."
iconName="apple"
/>
<FeatureCard
title="Fast & Stateless"
description="Direct API calls with no caching complexity. Simple, reliable, and maintainable."
iconName="bolt"
/>
<FeatureCard
title="Cross-Platform"
description="Works seamlessly across Linux, macOS, and Windows environments."
iconName="globe"
/>
<FeatureCard
title="Claude Integration"
description="Perfect compatibility with Claude and other AI assistants via the MCP protocol."
iconName="robot"
/>
<FeatureCard
title="Version History"
description="Package version tracking with nixpkgs commit hashes via NixHub.io integration."
iconName="history"
/>
<FeatureCard
title="Plain Text Output"
description="Human-readable responses with no XML parsing needed. Just clear, formatted text."
iconName="document"
/>
</div>
</div>
</section>
{/* Getting Started Section */}
<section id="getting-started" className="py-16 bg-nix-light">
<div className="container-custom">
<AnchorHeading level={2} className="text-3xl font-bold text-center mb-12 text-nix-dark">Getting Started</AnchorHeading>
<div className="max-w-2xl mx-auto">
<AnchorHeading level={3} className="text-2xl font-bold mb-4 text-nix-primary">Configuration</AnchorHeading>
<p className="mb-6 text-gray-800 font-medium">
Add to your MCP configuration file:
</p>
<CodeBlock
code={`{
"mcpServers": {
"nixos": {
"command": "uvx",
"args": ["mcp-nixos"]
}
}
}`}
language="json"
/>
<p className="mt-6 text-gray-800 font-medium">
Start leveraging NixOS package information and configuration options in your workflow!
</p>
<div className="text-center mt-12">
<Link href="/usage" className="btn-primary">
See All Configuration Options
</Link>
</div>
</div>
</div>
</section>
</div>
);
}
```
--------------------------------------------------------------------------------
/tests/test_plain_text_output.py:
--------------------------------------------------------------------------------
```python
"""Test suite for plain text output validation."""
from unittest.mock import Mock, patch
import pytest
from mcp_nixos import server
from mcp_nixos.server import error
def get_tool_function(tool_name: str):
"""Get the underlying function from a FastMCP tool."""
tool = getattr(server, tool_name)
if hasattr(tool, "fn"):
return tool.fn
return tool
# Get the underlying functions for direct use
darwin_search = get_tool_function("darwin_search")
home_manager_info = get_tool_function("home_manager_info")
home_manager_list_options = get_tool_function("home_manager_list_options")
home_manager_search = get_tool_function("home_manager_search")
home_manager_stats = get_tool_function("home_manager_stats")
nixos_info = get_tool_function("nixos_info")
nixos_search = get_tool_function("nixos_search")
nixos_stats = get_tool_function("nixos_stats")
@pytest.fixture(autouse=True)
def mock_channel_cache():
"""Mock channel cache to avoid API calls during tests."""
with patch("mcp_nixos.server.channel_cache") as mock_cache:
# Mock the channel cache methods
mock_cache.get_resolved.return_value = {
"unstable": "latest-43-nixos-unstable",
"stable": "latest-43-nixos-25.05",
"25.05": "latest-43-nixos-25.05",
"24.11": "latest-43-nixos-24.11",
}
mock_cache.get_available.return_value = {
"latest-43-nixos-unstable": 150000,
"latest-43-nixos-25.05": 140000,
"latest-43-nixos-24.11": 130000,
}
yield mock_cache
class TestPlainTextOutput:
"""Validate all functions return plain text, not XML."""
def test_error_plain_text(self):
"""Test error returns plain text."""
result = error("Test message")
assert result == "Error (ERROR): Test message"
assert "<error>" not in result
def test_error_with_code_plain_text(self):
"""Test error with code returns plain text."""
result = error("Not found", "NOT_FOUND")
assert result == "Error (NOT_FOUND): Not found"
assert "<error>" not in result
@patch("mcp_nixos.server.requests.post")
@pytest.mark.asyncio
async def test_nixos_search_plain_text(self, mock_post):
"""Test nixos_search returns plain text."""
# Mock response
mock_response = Mock()
mock_response.json.return_value = {
"hits": {
"hits": [
{
"_source": {
"package_pname": "firefox",
"package_pversion": "123.0",
"package_description": "A web browser",
}
}
]
}
}
mock_response.raise_for_status = Mock()
mock_post.return_value = mock_response
result = await nixos_search("firefox", search_type="packages", limit=5)
assert "Found 1 packages matching 'firefox':" in result
assert "• firefox (123.0)" in result
assert " A web browser" in result
assert "<package>" not in result
assert "<name>" not in result
@patch("mcp_nixos.server.requests.post")
@pytest.mark.asyncio
async def test_nixos_info_plain_text(self, mock_post):
"""Test nixos_info returns plain text."""
# Mock response
mock_response = Mock()
mock_response.json.return_value = {
"hits": {
"hits": [
{
"_source": {
"package_pname": "firefox",
"package_pversion": "123.0",
"package_description": "A web browser",
"package_homepage": ["https://firefox.com"],
"package_license_set": ["MPL-2.0"],
}
}
]
}
}
mock_response.raise_for_status = Mock()
mock_post.return_value = mock_response
result = await nixos_info("firefox", type="package")
assert "Package: firefox" in result
assert "Version: 123.0" in result
assert "Description: A web browser" in result
assert "Homepage: https://firefox.com" in result
assert "License: MPL-2.0" in result
assert "<package_info>" not in result
@patch("mcp_nixos.server.requests.post")
@pytest.mark.asyncio
async def test_nixos_stats_plain_text(self, mock_post):
"""Test nixos_stats returns plain text."""
# Mock response
mock_response = Mock()
mock_response.json.return_value = {"count": 12345}
mock_response.raise_for_status = Mock()
mock_post.return_value = mock_response
result = await nixos_stats()
assert "NixOS Statistics for unstable channel:" in result
assert "• Packages: 12,345" in result
assert "• Options: 12,345" in result
assert "<nixos_stats>" not in result
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_home_manager_search_plain_text(self, mock_get):
"""Test home_manager_search returns plain text."""
# Mock HTML response
mock_response = Mock()
mock_response.content = """.encode("utf-8")
<html>
<dt>programs.git.enable</dt>
<dd>
<p>Enable git</p>
<span class="term">Type: boolean</span>
</dd>
</html>
"""
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = await home_manager_search("git", limit=5)
assert "Found 1 Home Manager options matching 'git':" in result
assert "• programs.git.enable" in result
assert " Type: boolean" in result
assert " Enable git" in result
assert "<option>" not in result
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_home_manager_info_plain_text(self, mock_get):
"""Test home_manager_info returns plain text."""
# Mock HTML response
mock_response = Mock()
mock_response.content = """.encode("utf-8")
<html>
<dt>programs.git.enable</dt>
<dd>
<p>Enable git</p>
<span class="term">Type: boolean</span>
</dd>
</html>
"""
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = await home_manager_info("programs.git.enable")
assert "Option: programs.git.enable" in result
assert "Type: boolean" in result
assert "Description: Enable git" in result
assert "<option_info>" not in result
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_home_manager_stats_plain_text(self, mock_parse):
"""Test home_manager_stats returns plain text."""
# Mock parsed options
mock_parse.return_value = [
{"name": "programs.git.enable", "type": "boolean", "description": "Enable git"},
{"name": "programs.zsh.enable", "type": "boolean", "description": "Enable zsh"},
{"name": "services.gpg-agent.enable", "type": "boolean", "description": "Enable GPG agent"},
{"name": "home.packages", "type": "list", "description": "Packages to install"},
{"name": "wayland.windowManager.sway.enable", "type": "boolean", "description": "Enable Sway"},
{"name": "xsession.enable", "type": "boolean", "description": "Enable X session"},
]
result = await home_manager_stats()
assert "Home Manager Statistics:" in result
assert "Total options:" in result
assert "Categories:" in result
assert "Top categories:" in result
assert "programs:" in result
assert "services:" in result
assert "<home_manager_stats>" not in result
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_home_manager_list_options_plain_text(self, mock_get):
"""Test home_manager_list_options returns plain text."""
# Mock HTML response
mock_response = Mock()
mock_response.content = """.encode("utf-8")
<html>
<dt>programs.git.enable</dt>
<dd><p>Enable git</p></dd>
<dt>services.ssh.enable</dt>
<dd><p>Enable SSH</p></dd>
</html>
"""
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = await home_manager_list_options()
assert "Home Manager option categories (2 total):" in result
assert "• programs (1 options)" in result
assert "• services (1 options)" in result
assert "<option_categories>" not in result
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_darwin_search_plain_text(self, mock_get):
"""Test darwin_search returns plain text."""
# Mock HTML response
mock_response = Mock()
mock_response.content = """.encode("utf-8")
<html>
<dt>system.defaults.dock.autohide</dt>
<dd>
<p>Auto-hide the dock</p>
<span class="term">Type: boolean</span>
</dd>
</html>
"""
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = await darwin_search("dock", limit=5)
assert "Found 1 nix-darwin options matching 'dock':" in result
assert "• system.defaults.dock.autohide" in result
assert " Type: boolean" in result
assert " Auto-hide the dock" in result
assert "<option>" not in result
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_no_results_plain_text(self, mock_get):
"""Test empty results return appropriate plain text."""
# Mock empty HTML response
mock_response = Mock()
mock_response.content = b"<html></html>"
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = await home_manager_search("nonexistent", limit=5)
assert result == "No Home Manager options found matching 'nonexistent'"
assert "<" not in result
@patch("mcp_nixos.server.requests.post")
@pytest.mark.asyncio
async def test_nixos_empty_search_plain_text(self, mock_post):
"""Test nixos_search with no results returns plain text."""
# Mock empty response
mock_response = Mock()
mock_response.json.return_value = {"hits": {"hits": []}}
mock_response.raise_for_status = Mock()
mock_post.return_value = mock_response
result = await nixos_search("nonexistent", search_type="packages")
assert result == "No packages found matching 'nonexistent'"
assert "<" not in result
```
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
```markdown
# MCP-NixOS: v1.0.3 Release Notes - Encoding Fix
## Overview
MCP-NixOS v1.0.3 fixes encoding errors when parsing Home Manager and nix-darwin documentation, ensuring robust operation with various HTML encodings from CDN edge servers.
## Changes in v1.0.3
### 🔧 Bug Fixes
- **HTML Encoding Support**: Fixed parsing errors with non-UTF-8 encodings (windows-1252, ISO-8859-1, UTF-8 with BOM) in documentation (#58)
- **CDN Resilience**: Enhanced robustness when fetching docs from different CDN edge nodes with varying configurations
- **Test Coverage**: Added comprehensive encoding tests for all HTML parsing functions
### 🛠️ Development Experience
- **Release Workflow**: Improved release command documentation with clearer formatting
- **Test Suite**: Updated 26 tests to properly handle byte content in mock responses
### 📦 Dependencies
- No changes from previous version
- Maintained compatibility with FastMCP 2.x
## Installation
```bash
# Install with pip
pip install mcp-nixos==1.0.3
# Install with uv
uv pip install mcp-nixos==1.0.3
# Install with uvx
uvx mcp-nixos==1.0.3
```
## Docker Images
```bash
# Pull from Docker Hub
docker pull utensils/mcp-nixos:1.0.3
# Pull from GitHub Container Registry
docker pull ghcr.io/utensils/mcp-nixos:1.0.3
```
## Migration Notes
This is a drop-in replacement for v1.0.2 with no user-facing changes. The fix resolves intermittent "unknown encoding: windows-1252" errors when fetching documentation.
## Contributors
- James Brink (@utensils) - Fixed encoding handling in HTML parser
---
# MCP-NixOS: v1.0.2 Release Notes - Infrastructure Improvements
## Overview
MCP-NixOS v1.0.2 is a maintenance release focused on CI/CD improvements, security fixes, and enhanced Docker support. This release adds manual workflow dispatch capabilities, GHCR package visibility automation, and improves the deployment pipeline.
## Changes in v1.0.2
### 🚀 CI/CD Enhancements
- **Manual Workflow Dispatch**: Added ability to manually trigger Docker builds for specific tags
- **GHCR Package Visibility**: Automated setting of GitHub Container Registry packages to public visibility
- **Continuous Docker Builds**: Docker images now build automatically on main branch pushes
- **FlakeHub Publishing**: Integrated automated FlakeHub deployment workflow
- **Workflow Separation**: Split website deployment into dedicated workflow for better CI/CD organization
### 🔧 Bug Fixes
- **Tag Validation**: Fixed regex character class in Docker tag validation
- **API Resilience**: Added fallback channels when NixOS API discovery fails (#52, #54)
- **Documentation Fixes**: Escaped quotes in usage page to fix ESLint errors
- **Security**: Patched PrismJS DOM Clobbering vulnerability
### 🛠️ Development Experience
- **Code Review Automation**: Enhanced Claude Code Review with sticky comments
- **Agent Support**: Added MCP and Python development subagents
- **CI Optimization**: Skip CI builds on documentation-only changes
- **Improved Docker Support**: Better multi-architecture builds (amd64, arm64)
### 📦 Dependencies
- All dependencies remain unchanged from v1.0.1
- Maintained compatibility with FastMCP 2.x
## Installation
```bash
# Install with pip
pip install mcp-nixos==1.0.2
# Install with uv
uv pip install mcp-nixos==1.0.2
# Install with uvx
uvx mcp-nixos==1.0.2
```
## Docker Images
```bash
# Pull from Docker Hub
docker pull utensils/mcp-nixos:1.0.2
# Pull from GitHub Container Registry
docker pull ghcr.io/utensils/mcp-nixos:1.0.2
```
## Migration Notes
This is a drop-in replacement for v1.0.1 with no user-facing changes. All improvements are infrastructure and workflow related.
## Contributors
- James Brink (@utensils) - Chief Infrastructure Engineer
---
# MCP-NixOS: v1.0.1 Release Notes - FastMCP 2.x Migration
## Overview
MCP-NixOS v1.0.1 completes the migration to FastMCP 2.x, bringing modern async/await patterns and improved MCP protocol compliance. This release maintains all existing functionality while modernizing the codebase for better performance and maintainability.
## Changes in v1.0.1
### 🚀 Major Updates
- **FastMCP 2.x Migration**: Migrated from MCP SDK to FastMCP 2.x for better async support
- **Async/Await Patterns**: All tools now use proper async/await patterns throughout
- **Improved Type Safety**: Enhanced type annotations with FastMCP's built-in types
- **Test Suite Overhaul**: Fixed all 334 tests to work with new async architecture
- **CI/CD Modernization**: Updated to use ruff for linting/formatting (replacing black/flake8/isort)
### 🔧 Technical Improvements
- **Tool Definitions**: Migrated from `@server.call_tool()` to `@mcp.tool()` decorators
- **Function Extraction**: Added `get_tool_function` helper for test compatibility
- **Mock Improvements**: Enhanced mock setup for async function testing
- **Channel Resolution**: Fixed channel cache mock configurations in tests
- **Error Messages**: Removed "await" from user-facing error messages for clarity
### 🧪 Testing Enhancements
- **Test File Consolidation**: Removed duplicate test classes from merged files
- **Async Test Support**: All tests now properly handle async/await patterns
- **Mock JSON Responses**: Fixed mock setup to return proper dictionaries instead of Mock objects
- **API Compatibility**: Updated test expectations to match current NixHub API data
- **Coverage Maintained**: All 334 tests passing with comprehensive coverage
### 🛠️ Development Experience
- **Ruff Integration**: Consolidated linting and formatting with ruff
- **Simplified Toolchain**: Removed black, flake8, and isort in favor of ruff
- **Faster CI/CD**: Improved CI pipeline efficiency with better caching
- **Type Checking**: Enhanced mypy configuration for FastMCP compatibility
### 📦 Dependencies
- **FastMCP**: Now using `fastmcp>=2.11.0` for modern MCP support
- **Other Dependencies**: Maintained compatibility with all existing dependencies
- **Development Tools**: Streamlined dev dependencies with ruff
## Installation
```bash
# Install with pip
pip install mcp-nixos==1.0.1
# Install with uv
uv pip install mcp-nixos==1.0.1
# Install with uvx
uvx mcp-nixos==1.0.1
```
## Migration Notes
This is a drop-in replacement for v1.0.1 with no user-facing changes. The migration to FastMCP 2.x is entirely internal and maintains full backward compatibility.
## Technical Details
The migration involved:
1. **Async Architecture**: Converted all tool functions to async with proper await usage
2. **Import Updates**: Changed from `mcp.server.Server` to `fastmcp.FastMCP`
3. **Decorator Migration**: Updated all tool decorators to FastMCP's `@mcp.tool()` pattern
4. **Test Compatibility**: Added function extraction helpers for test suite compatibility
5. **Mock Enhancements**: Improved mock setup for async testing patterns
## Contributors
- James Brink (@utensils) - Chief Modernizer
---
# MCP-NixOS: v1.0.0 Release Notes - The Great Simplification
## Overview
MCP-NixOS v1.0.0 is a complete rewrite that proves less is more. We've drastically simplified the codebase while maintaining 100% functionality and adding new features. This isn't just a refactor—it's a masterclass in minimalism.
## Changes in v1.0.0
### 🎯 The Nuclear Option
- **Complete Rewrite**: Drastically simplified the entire codebase
- **Stateless Operation**: No more cache directories filling up your disk
- **Direct API Calls**: Removed all abstraction layers—now it's just functions doing their job
- **Simplified Dependencies**: Reduced from 5 to 3 core dependencies (40% reduction)
- **Two-File Implementation**: Everything you need in just `server.py` and `__main__.py`
- **Resolves #22**: Completely eliminated pickle usage and the entire cache layer
### 🚀 Major Improvements
- **Plain Text Output**: All responses now return human-readable plain text (no XML!)
- **NixHub Integration**: Added package version history tools
- `nixhub_package_versions`: Get version history with nixpkgs commits
- `nixhub_find_version`: Smart search for specific versions
- **Dynamic Channel Resolution**: Auto-discovers current stable channel
- **Enhanced Error Messages**: Suggestions when exact matches fail
- **Flake Search**: Added deduplicated flake package search
- **Better Stats**: Accurate statistics for all tools
- **Zero Configuration**: Removed all the config options you weren't using anyway
- **Faster Startup**: No cache initialization, no state management, just pure functionality
- **100% Test Coverage**: Comprehensive test suite ensures everything works as advertised
### 💥 Breaking Changes
- **No More Caching**: All operations are now stateless (your internet better be working)
- **Environment Variables Removed**: Only `ELASTICSEARCH_URL` remains
- **No Pre-Cache Option**: The `--pre-cache` flag is gone (along with the cache itself)
- **No Interactive Shell**: The deprecated CLI has been completely removed
### 🧹 What We Removed
- `cache/` directory - Complex caching that nobody understood
- `clients/` directory - Abstract interfaces that abstracted nothing
- `contexts/` directory - Context managers for contexts that didn't exist
- `resources/` directory - MCP resource definitions (now inline)
- `tools/` directory - Tool implementations (now in server.py)
- `utils/` directory - "Utility" functions that weren't
- 45 files of over-engineered complexity
### 📊 The Numbers
- **Before**: Many files with layers of abstraction
- **After**: Just 2 core files that matter
- **Result**: Dramatically less code, zero reduction in functionality, more features added
## Installation
```bash
# Install with pip
pip install mcp-nixos==1.0.0
# Install with uv
uv pip install mcp-nixos==1.0.0
# Install with uvx
uvx mcp-nixos==1.0.0
```
## Migration Guide
If you're upgrading from v0.x:
1. **Remove cache-related environment variables** - They don't do anything anymore
2. **Remove `--pre-cache` from any scripts** - It's gone
3. **That's it** - Everything else just works
## Why This Matters
This release demonstrates that most "enterprise" code is just complexity for complexity's sake. By removing abstractions, caching layers, and "design patterns," we've created something that:
- Is easier to understand
- Has fewer bugs (less code = less bugs)
- Starts faster
- Uses less memory
- Is more reliable
Sometimes the best code is the code you delete.
## Contributors
- James Brink (@utensils) - Chief Code Deleter
---
# MCP-NixOS: v0.5.1 Release Notes
## Overview
MCP-NixOS v0.5.1 is a minor release that updates the Elasticsearch index references to ensure compatibility with the latest NixOS search API. This release updates the index references from `latest-42-` to `latest-43-` to maintain functionality with the NixOS search service.
## Changes in v0.5.1
### 🔧 Fixes & Improvements
- **Updated Elasticsearch Index References**: Fixed the Elasticsearch index references to ensure proper connectivity with the NixOS search API
- **Version Bump**: Bumped version from 0.5.0 to 0.5.1
## Installation
```bash
# Install with pip
pip install mcp-nixos==0.5.1
# Install with uv
uv pip install mcp-nixos==0.5.1
# Install with uvx
uvx mcp-nixos==0.5.1
```
## Configuration
Configure Claude to use the tool by adding it to your `~/.config/claude/config.json` file:
```json
{
"tools": [
{
"path": "mcp_nixos",
"default_enabled": true
}
]
}
```
## Contributors
- James Brink (@utensils)
# MCP-NixOS: v0.5.0 Release Notes
## Overview
MCP-NixOS v0.5.0 introduces support for the NixOS 25.05 Beta channel, enhancing the flexibility and forward compatibility of the tool. This release adds the ability to search and query packages and options from the upcoming NixOS 25.05 release while maintaining backward compatibility with existing channels.
## Changes in v0.5.0
### 🚀 Major Enhancements
- **NixOS 25.05 Beta Channel Support**: Added support for the upcoming NixOS 25.05 release
- **New "beta" Alias**: Added a "beta" alias that maps to the current beta channel (currently 25.05)
- **Comprehensive Channel Documentation**: Updated all docstrings to include information about the new beta channel
- **Enhanced Testing**: Added extensive tests to ensure proper channel functionality
### 🛠️ Implementation Details
- **Channel Validation**: Extended channel validation to include the new 25.05 Beta channel
- **Cache Management**: Ensured cache clearing behavior works correctly with the new channel
- **Alias Handling**: Implemented proper handling of the "beta" alias similar to the "stable" alias
- **Testing**: Comprehensive test suite to verify all aspects of channel switching and alias resolution
## Technical Details
The release implements the following key improvements:
1. **25.05 Beta Channel**: Added the Elasticsearch index mapping for the upcoming NixOS 25.05 release using the index name pattern `latest-43-nixos-25.05`
2. **Beta Alias**: Implemented a "beta" alias that will always point to the current beta channel, similar to how the "stable" alias points to the current stable release
3. **Extended Documentation**: Updated all function and parameter docstrings to include the new channel options, ensuring users know about the full range of available channels
4. **Future-Proofing**: Designed the implementation to make it easy to add new channels in the future when new NixOS releases are in development
## Installation
```bash
# Install with pip
pip install mcp-nixos==0.5.0
# Install with uv
uv pip install mcp-nixos==0.5.0
# Install with uvx
uvx mcp-nixos==0.5.0
```
## Usage
Configure Claude to use the tool by adding it to your `~/.config/claude/config.json` file:
```json
{
"tools": [
{
"path": "mcp_nixos",
"default_enabled": true
}
]
}
```
### Available Channels
The following channels are now available for all NixOS tools:
- `unstable` - The NixOS unstable development branch
- `25.05` - The NixOS 25.05 Beta release (upcoming)
- `beta` - Alias for the current beta channel (currently 25.05)
- `24.11` - The current stable NixOS release
- `stable` - Alias for the current stable release (currently 24.11)
Example usage:
```python
# Search packages in the beta channel
nixos_search(query="nginx", channel="beta")
# Get information about a package in the 25.05 channel
nixos_info(name="python3", type="package", channel="25.05")
```
## Contributors
- James Brink (@utensils)
- Sean Callan (Moral Support)
```
--------------------------------------------------------------------------------
/tests/test_mcp_tools.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
"""Comprehensive tests for all MCP NixOS tools to identify and fix issues."""
from unittest.mock import MagicMock, patch
import pytest
from mcp_nixos import server
def get_tool_function(tool_name: str):
"""Get the underlying function from a FastMCP tool."""
tool = getattr(server, tool_name)
if hasattr(tool, "fn"):
return tool.fn
return tool
# Get the underlying functions for direct use
darwin_stats = get_tool_function("darwin_stats")
home_manager_info = get_tool_function("home_manager_info")
home_manager_list_options = get_tool_function("home_manager_list_options")
home_manager_search = get_tool_function("home_manager_search")
home_manager_stats = get_tool_function("home_manager_stats")
nixos_info = get_tool_function("nixos_info")
nixos_search = get_tool_function("nixos_search")
class TestNixOSSearchIssues:
"""Test issues with nixos_search specifically for options."""
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_search_options_now_returns_relevant_results(self, mock_es):
"""Test that searching for 'services.nginx' returns relevant nginx options."""
# Mock proper nginx-related results
mock_es.return_value = [
{
"_source": {
"option_name": "services.nginx.enable",
"option_type": "boolean",
"option_description": "Whether to enable Nginx Web Server.",
}
},
{
"_source": {
"option_name": "services.nginx.package",
"option_type": "package",
"option_description": "Nginx package to use.",
}
},
]
result = await nixos_search("services.nginx", search_type="options", limit=2, channel="stable")
# After fix, should return nginx-related options
assert "services.nginx.enable" in result
assert "services.nginx.package" in result
# Should mention the search term
assert "services.nginx" in result
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_info_option_not_found(self, mock_es):
"""Test that nixos_info fails to find specific options like services.nginx.enable."""
mock_es.return_value = [] # Empty results
result = await nixos_info("services.nginx.enable", type="option", channel="stable")
assert "Error (NOT_FOUND)" in result
assert "services.nginx.enable" in result
class TestHomeManagerIssues:
"""Test issues with Home Manager tools."""
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_home_manager_list_options_incomplete(self, mock_parse):
"""Test that home_manager_list_options only returns 2 categories (incomplete)."""
# Mock returns only 2 categories as seen in the issue
mock_parse.return_value = [
{"name": "_module.args", "description": "", "type": ""},
{"name": "accounts.calendar.basePath", "description": "", "type": ""},
{"name": "accounts.email.enable", "description": "", "type": ""},
]
result = await home_manager_list_options()
assert "_module (1 options)" in result
assert "accounts (2 options)" in result
assert "programs" not in result # Missing many categories!
# Should have many more categories
assert "(2 total)" in result # Only 2 categories found
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_home_manager_stats_placeholder(self, mock_parse):
"""Test that home_manager_stats returns actual statistics."""
# Mock parsed options
mock_parse.return_value = [
{"name": "programs.git.enable", "type": "boolean", "description": "Enable git"},
{"name": "programs.zsh.enable", "type": "boolean", "description": "Enable zsh"},
{"name": "services.gpg-agent.enable", "type": "boolean", "description": "Enable GPG agent"},
{"name": "home.packages", "type": "list", "description": "Packages to install"},
{"name": "wayland.windowManager.sway.enable", "type": "boolean", "description": "Enable Sway"},
{"name": "xsession.enable", "type": "boolean", "description": "Enable X session"},
]
result = await home_manager_stats()
assert "Home Manager Statistics:" in result
assert "Total options: 6" in result
assert "Categories: 5" in result
assert "Top categories:" in result
assert "programs: 2 options" in result
assert "services: 1 options" in result
class TestDarwinIssues:
"""Test issues with nix-darwin tools."""
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_darwin_stats_placeholder(self, mock_parse):
"""Test that darwin_stats returns actual statistics."""
# Mock parsed options
mock_parse.return_value = [
{"name": "services.nix-daemon.enable", "type": "boolean", "description": "Enable nix-daemon"},
{"name": "system.defaults.dock.autohide", "type": "boolean", "description": "Auto-hide dock"},
{"name": "launchd.agents.test", "type": "attribute set", "description": "Launchd agents"},
{"name": "programs.zsh.enable", "type": "boolean", "description": "Enable zsh"},
{"name": "homebrew.enable", "type": "boolean", "description": "Enable Homebrew"},
]
result = await darwin_stats()
assert "nix-darwin Statistics:" in result
assert "Total options: 5" in result
assert "Categories: 5" in result
assert "Top categories:" in result
assert "services: 1 options" in result
assert "system: 1 options" in result
class TestHTMLParsingIssues:
"""Test issues with HTML parsing that affect both Home Manager and Darwin."""
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_parse_html_options_type_extraction(self, mock_get):
"""Test that type information is not properly extracted from HTML."""
# Mock HTML response with proper structure
mock_response = MagicMock()
mock_response.content = """.encode("utf-8")
<html>
<body>
<dt>programs.git.enable</dt>
<dd>
<p>Whether to enable Git.</p>
<span class="term">Type: boolean</span>
</dd>
<dt>programs.git.package</dt>
<dd>
<p>The git package to use.</p>
<span class="term">Type: package</span>
</dd>
</body>
</html>
"""
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response
result = await home_manager_info("programs.git.enable")
# Check if type info is properly extracted
assert "Type:" in result or "boolean" in result
if "Type:" not in result:
# Type extraction is failing
raise AssertionError("Type information not extracted from HTML")
class TestElasticsearchQueryIssues:
"""Test issues with Elasticsearch query construction."""
@patch("mcp_nixos.server.requests.post")
@pytest.mark.asyncio
async def test_es_query_field_names(self, mock_post):
"""Test that ES queries use correct field names."""
# Mock successful response
mock_response = MagicMock()
mock_response.json.return_value = {"hits": {"hits": []}}
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
# Test options search
await nixos_search("nginx", search_type="options", limit=1)
# Check the query sent to ES
call_args = mock_post.call_args
query_data = call_args[1]["json"]["query"]
# Verify correct field names are used
should_clauses = query_data["bool"]["should"]
field_names = []
for clause in should_clauses:
if "match" in clause:
field_names.extend(clause["match"].keys())
elif "wildcard" in clause:
field_names.extend(clause["wildcard"].keys())
# After fix, we use wildcard for option_name
assert "option_name" in field_names or any("option_name" in str(clause) for clause in should_clauses)
assert "option_description" in field_names
# Test exact match for nixos_info
mock_post.reset_mock()
await nixos_info("services.nginx.enable", type="option")
call_args = mock_post.call_args
query_data = call_args[1]["json"]["query"]
# Check for keyword field usage
must_clauses = query_data["bool"]["must"]
for clause in must_clauses:
if "term" in clause and "option_name" in str(clause):
# Should use keyword field for exact match
assert "option_name.keyword" in str(clause) or "option_name" in str(clause)
class TestPlainTextFormatting:
"""Test that all outputs are plain text without XML/HTML artifacts."""
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_search_strips_html(self, mock_es):
"""Test that HTML tags in descriptions are properly handled."""
mock_es.return_value = [
{
"_source": {
"option_name": "test.option",
"option_type": "boolean",
"option_description": (
"<rendered-html><p>Test description with <code>code</code></p></rendered-html>"
),
}
}
]
result = await nixos_search("test", search_type="options")
# Should not contain HTML tags
assert "<rendered-html>" not in result
assert "</p>" not in result
assert "<code>" not in result
# But should contain the actual text
assert "Test description" in result
class TestErrorHandling:
"""Test error handling across all tools."""
@pytest.mark.asyncio
async def test_nixos_search_invalid_parameters(self):
"""Test parameter validation in nixos_search."""
# Invalid type
result = await nixos_search("test", search_type="invalid")
assert "Error" in result
assert "Invalid type" in result
# Invalid channel
result = await nixos_search("test", channel="invalid")
assert "Error" in result
assert "Invalid channel" in result
# Invalid limit
result = await nixos_search("test", limit=0)
assert "Error" in result
assert "Limit must be 1-100" in result
@patch("mcp_nixos.server.requests.get")
@pytest.mark.asyncio
async def test_network_error_handling(self, mock_get):
"""Test handling of network errors."""
mock_get.side_effect = Exception("Network error")
result = await home_manager_search("test")
assert "Error" in result
assert "Failed to fetch docs" in result or "Network error" in result
class TestRealAPIBehavior:
"""Tests that verify actual API behavior (can be skipped in CI)."""
@pytest.mark.integration
@pytest.mark.asyncio
async def test_real_nixos_option_search(self):
"""Test real NixOS API option search behavior."""
# This would make actual API calls to verify the issue
result = await nixos_search("services.nginx.enable", search_type="options", channel="stable")
# The search should return nginx-related options, not random ones
if "appstream.enable" in result:
pytest.fail("Search returns unrelated options - API query issue confirmed")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_real_home_manager_parsing(self):
"""Test real Home Manager HTML parsing."""
result = await home_manager_list_options()
# Should have many categories, not just 2
if "(2 total)" in result:
pytest.fail("Only 2 categories found - HTML parsing issue confirmed")
# Additional test utilities
def count_lines(text: str) -> int:
"""Count non-empty lines in output."""
return len([line for line in text.split("\n") if line.strip()])
def has_plain_text_format(text: str) -> bool:
"""Check if text follows plain text format without XML/HTML."""
forbidden_patterns = [
"<rendered-html>",
"</rendered-html>",
"<p>",
"</p>",
"<code>",
"</code>",
"<a ",
"</a>",
"<?xml",
]
return not any(pattern in text for pattern in forbidden_patterns)
class TestOutputFormat:
"""Test output formatting consistency."""
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_search_result_format(self, mock_es):
"""Test consistent formatting of search results."""
mock_es.return_value = [
{
"_source": {
"package_pname": "nginx",
"package_pversion": "1.24.0",
"package_description": "A web server",
}
}
]
result = await nixos_search("nginx", search_type="packages", limit=1)
# Check format
assert "Found 1 packages matching" in result
assert "• nginx (1.24.0)" in result
assert " A web server" in result # Indented description
# Check plain text
assert has_plain_text_format(result)
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_home_manager_format_consistency(self, mock_parse):
"""Test Home Manager output format consistency."""
mock_parse.return_value = [
{"name": "programs.git.enable", "description": "Whether to enable Git.", "type": "boolean"}
]
result = await home_manager_search("git", limit=1)
# Check format matches nixos_search style
assert "Found 1 Home Manager options matching" in result
assert "• programs.git.enable" in result
assert " Type: boolean" in result
assert " Whether to enable Git." in result
if __name__ == "__main__":
pytest.main([__file__, "-v"])
```
--------------------------------------------------------------------------------
/tests/test_edge_cases.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
"""Comprehensive edge case tests for MCP-NixOS server."""
from unittest.mock import Mock, patch
import mcp_nixos.server as server
import pytest
import requests
from mcp_nixos.server import (
error,
es_query,
parse_html_options,
)
def get_tool_function(tool_name: str):
"""Get the underlying function from a FastMCP tool."""
tool = getattr(server, tool_name)
if hasattr(tool, "fn"):
return tool.fn
return tool
# Extract FastMCP tool functions
nixos_search = get_tool_function("nixos_search")
nixos_info = get_tool_function("nixos_info")
nixos_stats = get_tool_function("nixos_stats")
home_manager_search = get_tool_function("home_manager_search")
home_manager_info = get_tool_function("home_manager_info")
home_manager_list_options = get_tool_function("home_manager_list_options")
darwin_search = get_tool_function("darwin_search")
darwin_info = get_tool_function("darwin_info")
darwin_list_options = get_tool_function("darwin_list_options")
darwin_options_by_prefix = get_tool_function("darwin_options_by_prefix")
class TestEdgeCases:
"""Test edge cases and error conditions."""
def test_error_with_none_message(self):
"""Test error function with None message."""
result = error(None) # type: ignore
assert result == "Error (ERROR): "
def test_error_with_empty_string(self):
"""Test error function with empty string."""
result = error("")
assert result == "Error (ERROR): "
def test_error_with_unicode(self):
"""Test error function with unicode characters."""
result = error("Failed to parse: 你好世界 🌍")
assert result == "Error (ERROR): Failed to parse: 你好世界 🌍"
@patch("requests.post")
def test_es_query_malformed_response(self, mock_post):
"""Test es_query with malformed JSON response."""
mock_resp = Mock()
mock_resp.raise_for_status = Mock()
mock_resp.json = Mock(return_value={"unexpected": "structure"})
mock_post.return_value = mock_resp
result = es_query("test-index", {"query": {}})
assert result == []
@patch("requests.post")
def test_es_query_network_timeout(self, mock_post):
"""Test es_query with network timeout."""
mock_post.side_effect = requests.Timeout("Connection timed out")
with pytest.raises(Exception, match="API error: Connection timed out"):
es_query("test-index", {"query": {}})
@patch("requests.post")
def test_es_query_http_error(self, mock_post):
"""Test es_query with HTTP error status."""
mock_resp = Mock()
mock_resp.raise_for_status.side_effect = requests.HTTPError("503 Service Unavailable")
mock_post.return_value = mock_resp
with pytest.raises(Exception, match="API error: 503 Service Unavailable"):
es_query("test-index", {"query": {}})
@patch("requests.get")
def test_parse_html_options_large_document(self, mock_get):
"""Test parsing very large HTML documents."""
# Create a large HTML document with many options
large_html = (
"""
<html><body>
"""
+ "\n".join(
[
f"""
<dt><a id="opt-test.option{i}">test.option{i}</a></dt>
<dd>
<p>Description for option {i}</p>
<span class="term">Type: string</span>
</dd>
"""
for i in range(1000)
]
)
+ """
</body></html>
"""
)
mock_resp = Mock()
mock_resp.raise_for_status = Mock()
mock_resp.content = large_html.encode("utf-8")
mock_get.return_value = mock_resp
# Should respect limit
options = parse_html_options("http://test.com", limit=50)
assert len(options) == 50
assert options[0]["name"] == "test.option0"
assert options[49]["name"] == "test.option49"
@patch("requests.get")
def test_parse_html_options_malformed_html(self, mock_get):
"""Test parsing malformed HTML with missing tags."""
malformed_html = """
<html><body>
<dt><a id="opt-test.option1">test.option1</a>
<!-- Missing closing dt tag -->
<dd>Description without proper structure
<!-- Missing closing dd tag -->
<dt><a id="opt-test.option2">test.option2</a></dt>
<!-- Missing dd for this dt -->
<dt><a id="opt-test.option3">test.option3</a></dt>
<dd><p>Proper description</p></dd>
</body></html>
"""
mock_resp = Mock()
mock_resp.raise_for_status = Mock()
mock_resp.content = malformed_html.encode("utf-8")
mock_get.return_value = mock_resp
options = parse_html_options("http://test.com")
# Should handle malformed HTML gracefully
assert len(options) >= 1
assert any(opt["name"] == "test.option3" for opt in options)
@patch("requests.get")
def test_parse_html_options_special_characters(self, mock_get):
"""Test parsing options with special characters and HTML entities."""
html_with_entities = """
<html><body>
<dt><a id="opt-test.option<name>">test.option<name></a></dt>
<dd>
<p>Description with & entities "quoted" and 'apostrophes'</p>
<span class="term">Type: list of (attribute set)</span>
</dd>
<dt><a id="opt-programs.firefox.profiles._name_.search">programs.firefox.profiles.<name>.search</a></dt>
<dd><p>Firefox search configuration</p></dd>
</body></html>
"""
mock_resp = Mock()
mock_resp.raise_for_status = Mock()
mock_resp.content = html_with_entities.encode("utf-8")
mock_get.return_value = mock_resp
options = parse_html_options("http://test.com")
assert len(options) == 2
# BeautifulSoup should decode HTML entities
assert options[0]["description"] == "Description with & entities \"quoted\" and 'apostrophes'"
# The underscore replacement might change the name
assert "programs.firefox.profiles" in options[1]["name"]
assert "search" in options[1]["name"]
@pytest.mark.asyncio
async def test_nixos_search_invalid_parameters(self):
"""Test nixos_search with various invalid parameters."""
# Invalid type
result = await nixos_search("test", search_type="invalid")
assert "Error (ERROR): Invalid type 'invalid'" in result
# Invalid channel
result = await nixos_search("test", channel="nonexistent")
assert "Error (ERROR): Invalid channel 'nonexistent'" in result
# Invalid limit (too low)
result = await nixos_search("test", limit=0)
assert "Error (ERROR): Limit must be 1-100" in result
# Invalid limit (too high)
result = await nixos_search("test", limit=101)
assert "Error (ERROR): Limit must be 1-100" in result
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_search_with_empty_query(self, mock_es_query):
"""Test searching with empty query string."""
mock_es_query.return_value = []
result = await nixos_search("")
assert "No packages found matching ''" in result
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_search_programs_edge_case(self, mock_es_query):
"""Test programs search when program name doesn't match query."""
mock_es_query.return_value = [
{"_source": {"package_pname": "coreutils", "package_programs": ["ls", "cp", "mv", "rm"]}}
]
# Search for 'ls' should find it in programs
result = await nixos_search("ls", search_type="programs")
assert "ls (provided by coreutils)" in result
# Search for 'grep' should not show coreutils
result = await nixos_search("grep", search_type="programs")
assert "coreutils" not in result
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_info_with_missing_fields(self, mock_es_query):
"""Test nixos_info when response has missing fields."""
# Package with minimal fields
mock_es_query.return_value = [
{
"_source": {
"package_pname": "minimal-pkg"
# Missing version, description, homepage, license
}
}
]
result = await nixos_info("minimal-pkg", type="package")
assert "Package: minimal-pkg" in result
assert "Version: " in result # Empty version
# Should not crash on missing fields
@patch("mcp_nixos.server.es_query")
@pytest.mark.asyncio
async def test_nixos_info_option_html_stripping(self, mock_es_query):
"""Test HTML stripping in option descriptions."""
mock_es_query.return_value = [
{
"_source": {
"option_name": "test.option",
"option_type": "boolean",
"option_description": (
"<rendered-html><p>This is a <strong>test</strong> "
"option with <a href='#'>links</a></p></rendered-html>"
),
"option_default": "false",
}
}
]
result = await nixos_info("test.option", type="option")
assert "Description: This is a test option with links" in result
assert "<" not in result # No HTML tags
assert ">" not in result
@patch("requests.post")
@pytest.mark.asyncio
async def test_nixos_stats_partial_failure(self, mock_post):
"""Test nixos_stats when one count request fails."""
# First call succeeds
mock_resp1 = Mock()
mock_resp1.json = Mock(return_value={"count": 100000})
# Second call fails
mock_resp2 = Mock()
mock_resp2.json = Mock(side_effect=ValueError("Invalid JSON"))
mock_post.side_effect = [mock_resp1, mock_resp2]
result = await nixos_stats()
# With improved error handling, it should show 0 for failed count
assert "Options: 0" in result or "Error (ERROR):" in result
@pytest.mark.asyncio
async def test_home_manager_search_edge_cases(self):
"""Test home_manager_search with edge cases."""
# Invalid limit
result = await home_manager_search("test", limit=0)
assert "Error (ERROR): Limit must be 1-100" in result
result = await home_manager_search("test", limit=101)
assert "Error (ERROR): Limit must be 1-100" in result
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_home_manager_info_exact_match(self, mock_parse):
"""Test home_manager_info requires exact name match."""
mock_parse.return_value = [
{"name": "programs.git", "description": "Git program", "type": ""},
{"name": "programs.git.enable", "description": "Enable git", "type": "boolean"},
]
# Should find exact match
result = await home_manager_info("programs.git.enable")
assert "Option: programs.git.enable" in result
assert "Enable git" in result
# Should not find partial match
result = await home_manager_info("programs.git.en")
assert "Error (NOT_FOUND):" in result
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_home_manager_list_options_category_extraction(self, mock_parse):
"""Test category extraction from option names."""
mock_parse.return_value = [
{"name": "programs.git.enable", "description": "", "type": ""},
{"name": "programs.firefox.enable", "description": "", "type": ""},
{"name": "services.gpg-agent.enable", "description": "", "type": ""},
{"name": "xdg.configHome", "description": "", "type": ""},
{"name": "single", "description": "No category", "type": ""}, # Edge case: no dot
]
result = await home_manager_list_options()
assert "programs (2 options)" in result
assert "services (1 options)" in result
assert "xdg (1 options)" in result
assert "single (1 options)" in result
@patch("mcp_nixos.server.parse_html_options")
@pytest.mark.asyncio
async def test_darwin_options_by_prefix_sorting(self, mock_parse):
"""Test darwin_options_by_prefix sorts results."""
mock_parse.return_value = [
{"name": "system.defaults.c", "description": "Option C", "type": ""},
{"name": "system.defaults.a", "description": "Option A", "type": ""},
{"name": "system.defaults.b", "description": "Option B", "type": ""},
]
result = await darwin_options_by_prefix("system.defaults")
lines = result.split("\n")
# Find option lines (those starting with •)
option_lines = [line for line in lines if line.startswith("•")]
assert option_lines[0] == "• system.defaults.a"
assert option_lines[1] == "• system.defaults.b"
assert option_lines[2] == "• system.defaults.c"
@pytest.mark.asyncio
async def test_all_tools_handle_exceptions_gracefully(self):
"""Test that all tools handle exceptions and return error messages."""
with patch("requests.post", side_effect=Exception("Network error")):
result = await nixos_search("test")
assert "Error (ERROR):" in result
result = await nixos_info("test")
assert "Error (ERROR):" in result
result = await nixos_stats()
assert "Error (ERROR):" in result
with patch("requests.get", side_effect=Exception("Network error")):
result = await home_manager_search("test")
assert "Error (ERROR):" in result
result = await home_manager_info("test")
assert "Error (ERROR):" in result
result = await home_manager_list_options()
assert "Error (ERROR):" in result
result = await darwin_search("test")
assert "Error (ERROR):" in result
result = await darwin_info("test")
assert "Error (ERROR):" in result
result = await darwin_list_options()
assert "Error (ERROR):" in result
```