This is page 1 of 4. Use http://codebase.md/utensils/mcp-nixos?lines=true&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:
--------------------------------------------------------------------------------
```
1 |
```
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
```
1 | use flake
2 |
```
--------------------------------------------------------------------------------
/website/.eslintignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | .next/
3 | out/
4 | public/
```
--------------------------------------------------------------------------------
/website/.prettierignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | .next/
3 | out/
4 | public/
5 | dist/
6 | coverage/
7 | .vscode/
8 | build/
9 | README.md
10 | *.yml
11 | *.yaml
```
--------------------------------------------------------------------------------
/website/.prettierrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "printWidth": 100,
6 | "trailingComma": "es5",
7 | "arrowParens": "avoid",
8 | "endOfLine": "auto"
9 | }
```
--------------------------------------------------------------------------------
/.mcp.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "nixos": {
4 | "type": "stdio",
5 | "command": "uv",
6 | "args": [
7 | "run",
8 | "--directory",
9 | "/Users/jamesbrink/Projects/utensils/mcp-nixos",
10 | "mcp-nixos"
11 | ]
12 | }
13 | }
14 | }
```
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
```
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 | .yarn/install-state.gz
6 |
7 | # next.js
8 | /.next/
9 | /out/
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | *.pem
15 | .env*
16 | !.env.example
17 |
18 | # debug
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # typescript
24 | *.tsbuildinfo
25 | next-env.d.ts
```
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.5.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: end-of-file-fixer
7 | - id: check-yaml
8 | - id: check-added-large-files
9 |
10 | - repo: https://github.com/astral-sh/ruff-pre-commit
11 | rev: v0.4.10
12 | hooks:
13 | - id: ruff
14 | args: ["--fix"]
15 | - id: ruff-format
16 |
17 | - repo: https://github.com/pre-commit/mirrors-mypy
18 | rev: v1.10.0
19 | hooks:
20 | - id: mypy
21 | additional_dependencies: [types-requests, types-beautifulsoup4]
22 | args: ["--strict", "--ignore-missing-imports"]
```
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
```
1 | # Git
2 | .git/
3 | .gitignore
4 | .gitattributes
5 |
6 | # GitHub
7 | .github/
8 |
9 | # Python
10 | __pycache__/
11 | *.py[cod]
12 | *$py.class
13 | *.so
14 | .Python
15 | *.egg-info/
16 | *.egg
17 | .eggs/
18 | dist/
19 | build/
20 | wheels/
21 | .pytest_cache/
22 | .mypy_cache/
23 | .ruff_cache/
24 | htmlcov/
25 | .coverage
26 | .coverage.*
27 | coverage.xml
28 | *.cover
29 | .hypothesis/
30 | .tox/
31 | .venv/
32 | venv/
33 | ENV/
34 | env/
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Nix
39 | result
40 | result-*
41 | .direnv/
42 | .envrc
43 |
44 | # IDE
45 | .vscode/
46 | .idea/
47 | *.swp
48 | *.swo
49 | *~
50 | .DS_Store
51 |
52 | # Documentation
53 | LICENSE
54 | docs/
55 | website/
56 | CLAUDE.md
57 | RELEASE_NOTES.md
58 | !README.md
59 |
60 | # Development
61 | .mcp.json
62 | CLAUDE.md
63 | tests/
64 | conftest.py
65 | *.test.py
66 | *_test.py
67 |
68 | # CI/CD
69 | .travis.yml
70 | .gitlab-ci.yml
71 | azure-pipelines.yml
72 |
73 | # Other
74 | .env
75 | .env.*
76 | *.log
77 | *.bak
78 | *.tmp
79 | node_modules/
80 | npm-debug.log*
81 | yarn-debug.log*
82 | yarn-error.log*
```
--------------------------------------------------------------------------------
/website/.eslintrc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:react-hooks/recommended",
7 | "plugin:@typescript-eslint/recommended"
8 | ],
9 | "plugins": [
10 | "react",
11 | "react-hooks",
12 | "@typescript-eslint"
13 | ],
14 | "parser": "@typescript-eslint/parser",
15 | "parserOptions": {
16 | "ecmaFeatures": {
17 | "jsx": true
18 | },
19 | "ecmaVersion": 2020,
20 | "sourceType": "module"
21 | },
22 | "rules": {
23 | "react/react-in-jsx-scope": "off",
24 | "react-hooks/rules-of-hooks": "error",
25 | "react-hooks/exhaustive-deps": "warn",
26 | "react/prop-types": "off",
27 | "@typescript-eslint/explicit-module-boundary-types": "off",
28 | "no-unused-vars": "off",
29 | "@typescript-eslint/no-unused-vars": ["warn", {
30 | "argsIgnorePattern": "^_",
31 | "varsIgnorePattern": "^_"
32 | }],
33 | "react/no-unknown-property": [
34 | "error",
35 | { "ignore": ["jsx"] }
36 | ]
37 | },
38 | "settings": {
39 | "react": {
40 | "version": "detect"
41 | }
42 | }
43 | }
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | build/
8 | develop-eggs/
9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 | MANIFEST
23 |
24 | # Virtual Environment
25 | .venv/
26 | venv/
27 | ENV/
28 |
29 | # Unit test / coverage reports
30 | htmlcov/
31 | .tox/
32 | .nox/
33 | .coverage
34 | .coverage.*
35 | .cache
36 | nosetests.xml
37 | coverage.xml
38 | *.cover
39 | .hypothesis/
40 | .pytest_cache/
41 | mcp_nixos_test_cache/
42 | *test_cache*/
43 |
44 | # Environments
45 | .env
46 | .env.local
47 | .venv
48 | env/
49 | venv/
50 | ENV/
51 | env.bak/
52 | venv.bak/
53 |
54 | # Nix
55 | .direnv/
56 | result
57 |
58 | # IDE
59 | .idea/
60 | *.swp
61 | *.swo
62 | *~
63 | .vscode/
64 |
65 | # MCP local configuration
66 | .mcp.json
67 | .mcp-nix.json
68 |
69 | # Misc
70 | temp
71 | tmp
72 | # Explicitly don't ignore uv.lock (we need to track it)
73 | uv-*.lock
74 | .aider*
75 | .pypirc
76 | mcp-completion-docs.md
77 | TODO.md
78 |
79 | # Wily
80 | .wily/
81 |
82 | # Logs
83 | *.log
84 | *.DS_Store
85 |
86 | # Website (Next.js)
87 | /website/node_modules/
88 | /website/.next/
89 | /website/out/
90 | /website/.vercel/
91 | /website/.env*.local
92 | /website/npm-debug.log*
93 | /website/yarn-debug.log*
94 | /website/yarn-error.log*
95 | /website/pnpm-debug.log*
96 | /website/.pnpm-store/
97 | /website/.DS_Store
98 | /website/*.pem
99 | reference-mcp-coroot/
100 |
```
--------------------------------------------------------------------------------
/website/public/favicon/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Favicon Files
2 |
3 | These favicon files are generated from the MCP-NixOS project logo.
4 |
5 | ## File Descriptions
6 |
7 | - `favicon.ico`: Multi-size ICO file containing 16x14 and 32x28 versions
8 | - `favicon-16x16.png`: 16x14 PNG for standard favicon
9 | - `favicon-32x32.png`: 32x28 PNG for standard favicon
10 | - `apple-touch-icon.png`: 180x156 PNG for iOS home screen
11 | - `android-chrome-192x192.png`: 192x167 PNG for Android
12 | - `android-chrome-512x512.png`: 512x444 PNG for Android
13 | - `safari-pinned-tab.svg`: Monochrome SVG for Safari pinned tabs
14 | - `mstile-150x150.png`: 150x130 PNG for Windows tiles
15 | - `browserconfig.xml`: Configuration for Microsoft browsers
16 | - `site.webmanifest`: Web app manifest for PWA support
17 |
18 | ## Generation Commands
19 |
20 | In a normal development environment, you can generate these files using ImageMagick:
21 |
22 | ```bash
23 | # Generate PNG files from the source PNG logo
24 | convert -background none -resize 16x16 ../images/mcp-nixos.png favicon-16x16.png
25 | convert -background none -resize 32x32 ../images/mcp-nixos.png favicon-32x32.png
26 | convert -background none -resize 180x180 ../images/mcp-nixos.png apple-touch-icon.png
27 | convert -background none -resize 192x192 ../images/mcp-nixos.png android-chrome-192x192.png
28 | convert -background none -resize 512x512 ../images/mcp-nixos.png android-chrome-512x512.png
29 | convert -background none -resize 150x150 ../images/mcp-nixos.png mstile-150x150.png
30 |
31 | # Generate ICO file (combines multiple sizes)
32 | convert favicon-16x16.png favicon-32x32.png favicon.ico
33 | ```
34 |
35 | ## Attribution
36 |
37 | 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
1 | # MCP-NixOS Website
2 |
3 | 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.
4 |
5 | ## Development
6 |
7 | This website is built with:
8 |
9 | - [Next.js 15.2](https://nextjs.org/) using the App Router
10 | - [TypeScript](https://www.typescriptlang.org/)
11 | - [Tailwind CSS](https://tailwindcss.com/) for styling
12 | - Static export for hosting on S3/CloudFront
13 |
14 | ## Getting Started
15 |
16 | ### Using Nix (Recommended)
17 |
18 | If you have Nix installed, you can use the dedicated website development shell:
19 |
20 | #### Option 1: Direct Website Shell Access
21 | ```bash
22 | # Enter the website development shell directly
23 | nix develop .#web
24 |
25 | # Use the menu commands or run directly:
26 | install # Install dependencies
27 | dev # Start development server
28 | build # Build for production
29 | lint # Lint code
30 | ```
31 |
32 | #### Option 2: From Main Development Shell
33 | ```bash
34 | # Enter the main development shell
35 | nix develop
36 |
37 | # Launch the website development shell
38 | web-dev # This opens the website shell with Node.js
39 | ```
40 |
41 | ### Manual Setup
42 |
43 | ```bash
44 | # Navigate to the website directory
45 | cd website
46 |
47 | # Install dependencies
48 | npm install
49 | # or
50 | yarn
51 | # or
52 | pnpm install
53 |
54 | # Start development server
55 | npm run dev
56 | # or
57 | yarn dev
58 | # or
59 | pnpm dev
60 |
61 | # Build for production
62 | npm run build
63 | # or
64 | yarn build
65 | # or
66 | pnpm build
67 | ```
68 |
69 | ## Project Structure
70 |
71 | - `app/` - Next.js app router pages
72 | - `components/` - Shared UI components
73 | - `public/` - Static assets
74 | - `tailwind.config.js` - Tailwind CSS configuration with NixOS color scheme
75 |
76 | ## Design Notes
77 |
78 | - The website follows NixOS brand colors:
79 | - Primary: #5277C3
80 | - Secondary: #7EBAE4
81 | - Dark Blue: #1C3E5A
82 | - Light Blue: #E6F0FA
83 |
84 | - Designed to be fully responsive for mobile, tablet, and desktop
85 | - SEO optimized with proper metadata
86 | - Follows accessibility guidelines (WCAG 2.1 AA)
87 |
88 | ## Code Quality
89 |
90 | The project includes comprehensive linting and type checking:
91 |
92 | ```bash
93 | # Run ESLint to check for issues
94 | npm run lint
95 |
96 | # Fix automatically fixable ESLint issues
97 | npm run lint:fix
98 |
99 | # Run TypeScript type checking
100 | npm run type-check
101 | ```
102 |
103 | VS Code settings are included for automatic formatting and linting.
104 |
105 | ## Next.js 15.2 Component Model
106 |
107 | Next.js 15.2 enforces a stricter separation between client and server components:
108 |
109 | ### Client Components
110 | - Must include `"use client";` at the top of the file
111 | - Can use React hooks (useState, useEffect, etc.)
112 | - Can include event handlers (onClick, onChange, etc.)
113 | - Can access browser APIs
114 |
115 | ### Server Components (Default)
116 | - Cannot use React hooks
117 | - Cannot include event handlers
118 | - Cannot access browser APIs
119 | - Can access backend resources directly
120 | - Keep sensitive information secure
121 |
122 | ### Best Practices
123 | - Mark components with interactive elements as client components
124 | - Use dynamic imports with `{ ssr: false }` for components with useLayoutEffect
125 | - Keep server components as the default when possible for better performance
126 | - Use client components only when necessary for interactivity
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP-NixOS - Because Your AI Assistant Shouldn't Hallucinate About Packages
2 |
3 | [](https://github.com/utensils/mcp-nixos/actions/workflows/ci.yml)
4 | [](https://codecov.io/gh/utensils/mcp-nixos)
5 | [](https://pypi.org/project/mcp-nixos/)
6 | [](https://pypi.org/project/mcp-nixos/)
7 | [](https://smithery.ai/server/@utensils/mcp-nixos)
8 | [](https://mseep.ai/app/99cc55fb-a5c5-4473-b315-45a6961b2e8c)
9 |
10 | > **🎉 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.
11 | >
12 | > **🚀 ASYNC UPDATE**: Version 1.0.1 migrated to FastMCP 2.x for modern async goodness. Because who doesn't love adding `await` to everything?
13 |
14 | ## Quick Start (Because You Want to Use It NOW)
15 |
16 | **🚨 No Nix/NixOS Required!** This tool works on any system - Windows, macOS, Linux. You're just querying web APIs.
17 |
18 | ### Option 1: Using uvx (Recommended for most users)
19 | [](https://cursor.com/install-mcp?name=nixos&config=eyJjb21tYW5kIjoidXZ4IG1jcC1uaXhvcyJ9)
20 | ```json
21 | {
22 | "mcpServers": {
23 | "nixos": {
24 | "command": "uvx",
25 | "args": ["mcp-nixos"]
26 | }
27 | }
28 | }
29 | ```
30 |
31 | ### Option 2: Using Nix (For Nix users)
32 | [](https://cursor.com/install-mcp?name=nixos&config=eyJjb21tYW5kIjoibml4IHJ1biBnaXRodWI6dXRlbnNpbHMvbWNwLW5peG9zIC0tIn0%3D)
33 | ```json
34 | {
35 | "mcpServers": {
36 | "nixos": {
37 | "command": "nix",
38 | "args": ["run", "github:utensils/mcp-nixos", "--"]
39 | }
40 | }
41 | }
42 | ```
43 |
44 | ### Option 3: Using Docker (Container lovers unite)
45 | [](https://cursor.com/install-mcp?name=nixos&config=eyJjb21tYW5kIjoiZG9ja2VyIiwiYXJncyI6WyJydW4iLCItLXJtIiwiLWkiLCJnaGNyLmlvL3V0ZW5zaWxzL21jcC1uaXhvcyJdfQ%3D%3D)
46 | ```json
47 | {
48 | "mcpServers": {
49 | "nixos": {
50 | "command": "docker",
51 | "args": ["run", "--rm", "-i", "ghcr.io/utensils/mcp-nixos"]
52 | }
53 | }
54 | }
55 | ```
56 |
57 | That's it. Your AI assistant now has access to real NixOS data instead of making things up. You're welcome.
58 |
59 | ## What Is This Thing?
60 |
61 | MCP-NixOS is a Model Context Protocol server that gives your AI assistant accurate, real-time information about:
62 | - **NixOS packages** (130K+ packages that actually exist)
63 | - **Configuration options** (22K+ ways to break your system)
64 | - **Home Manager settings** (4K+ options for the power users)
65 | - **nix-darwin configurations** (1K+ macOS settings Apple doesn't want you to touch)
66 | - **Package version history** via [NixHub.io](https://www.nixhub.io) (Find that ancient Ruby 2.6 with commit hashes)
67 |
68 | ## The Tools You Actually Care About
69 |
70 | ### 🔍 NixOS Tools
71 | - `nixos_search(query, type, channel)` - Search packages, options, or programs
72 | - `nixos_info(name, type, channel)` - Get detailed info about packages/options
73 | - `nixos_stats(channel)` - Package and option counts
74 | - `nixos_channels()` - List all available channels
75 | - `nixos_flakes_search(query)` - Search community flakes
76 | - `nixos_flakes_stats()` - Flake ecosystem statistics
77 |
78 | ### 📦 Version History Tools (NEW!)
79 | - `nixhub_package_versions(package, limit)` - Get version history with commit hashes
80 | - `nixhub_find_version(package, version)` - Smart search for specific versions
81 |
82 | ### 🏠 Home Manager Tools
83 | - `home_manager_search(query)` - Search user config options
84 | - `home_manager_info(name)` - Get option details (with suggestions!)
85 | - `home_manager_stats()` - See what's available
86 | - `home_manager_list_options()` - Browse all 131 categories
87 | - `home_manager_options_by_prefix(prefix)` - Explore options by prefix
88 |
89 | ### 🍎 Darwin Tools
90 | - `darwin_search(query)` - Search macOS options
91 | - `darwin_info(name)` - Get option details
92 | - `darwin_stats()` - macOS configuration statistics
93 | - `darwin_list_options()` - Browse all 21 categories
94 | - `darwin_options_by_prefix(prefix)` - Explore macOS options
95 |
96 | ## Installation Options
97 |
98 | **Remember: You DON'T need Nix/NixOS installed!** This tool runs anywhere Python runs.
99 |
100 | ### For Regular Humans (Windows/Mac/Linux)
101 | ```bash
102 | # Run directly with uvx (no installation needed)
103 | uvx mcp-nixos
104 |
105 | # Or install globally
106 | pip install mcp-nixos
107 | uv pip install mcp-nixos
108 | ```
109 |
110 | ### For Nix Users (You Know Who You Are)
111 | ```bash
112 | # Run without installing
113 | nix run github:utensils/mcp-nixos
114 |
115 | # Install to profile
116 | nix profile install github:utensils/mcp-nixos
117 | ```
118 |
119 | ## Features Worth Mentioning
120 |
121 | ### 🚀 Version 1.0.1: The Async Revolution (After The Great Simplification)
122 | - **Drastically less code** - v1.0.0 removed thousands of lines, v1.0.1 made them async
123 | - **100% functionality** - Everything still works, now with more `await`
124 | - **0% cache corruption** - Because we removed the cache entirely (still gone!)
125 | - **Stateless operation** - No files to clean up (async doesn't change this)
126 | - **Direct API access** - No abstraction nonsense (but now it's async nonsense)
127 | - **Modern MCP** - FastMCP 2.x because the old MCP was too synchronous
128 |
129 | ### 📊 What You Get
130 | - **Real-time data** - Always current, never stale
131 | - **Plain text output** - Human and AI readable
132 | - **Smart suggestions** - Helps when you typo option names
133 | - **Cross-platform** - Works on Linux, macOS, and yes, even Windows
134 | - **No configuration** - It just works™
135 |
136 | ### 🎯 Key Improvements
137 | - **Dynamic channel resolution** - `stable` always points to current stable
138 | - **Enhanced error messages** - Actually helpful when things go wrong
139 | - **Deduped flake results** - No more duplicate spam
140 | - **Version-aware searches** - Find that old Ruby version you need
141 | - **Category browsing** - Explore options systematically
142 |
143 | ## For Developers (The Brave Ones)
144 |
145 | ### Local Development Setup
146 |
147 | Want to test your changes in Claude Code or another MCP client? Create a `.mcp.json` file in your project directory:
148 |
149 | ```json
150 | {
151 | "mcpServers": {
152 | "nixos": {
153 | "type": "stdio",
154 | "command": "uv",
155 | "args": [
156 | "run",
157 | "--directory",
158 | "/home/hackerman/Projects/mcp-nixos",
159 | "mcp-nixos"
160 | ]
161 | }
162 | }
163 | }
164 | ```
165 |
166 | Replace `/home/hackerman/Projects/mcp-nixos` with your actual project path (yes, even you, Windows users with your `C:\Users\CoolDev\...` paths).
167 |
168 | This `.mcp.json` file:
169 | - **Automatically activates** when you launch Claude Code from the project directory
170 | - **Uses your local code** instead of the installed package
171 | - **Enables real-time testing** - just restart Claude Code after changes
172 | - **Already in .gitignore** so you won't accidentally commit your path
173 |
174 | ### With Nix (The Blessed Path)
175 | ```bash
176 | nix develop
177 | menu # Shows all available commands
178 |
179 | # Common tasks
180 | run # Start the server (now with FastMCP!)
181 | run-tests # Run all tests (now async!)
182 | lint # Format and check code (ruff replaced black/flake8)
183 | typecheck # Check types (mypy still judges you)
184 | build # Build the package
185 | publish # Upload to PyPI (requires credentials)
186 | ```
187 |
188 | ### Without Nix (The Path of Pain)
189 | ```bash
190 | # Install development dependencies
191 | uv pip install -e ".[dev]" # or pip install -e ".[dev]"
192 |
193 | # Run the server locally
194 | uv run mcp-nixos # or python -m mcp_nixos.server
195 |
196 | # Development commands
197 | pytest tests/ # Now with asyncio goodness
198 | ruff format mcp_nixos/ # black is so 2023
199 | ruff check mcp_nixos/ # flake8 is for boomers
200 | mypy mcp_nixos/ # Still pedantic as ever
201 |
202 | # Build and publish
203 | python -m build # Build distributions
204 | twine upload dist/* # Upload to PyPI
205 | ```
206 |
207 | ### Testing Philosophy
208 | - **367 tests** that actually test things (now async because why not)
209 | - **Real API calls** because mocks are for cowards (await real_courage())
210 | - **Plain text validation** ensuring no XML leaks through
211 | - **Cross-platform tests** because Windows users deserve pain too
212 | - **15 test files** down from 29 because organization is a virtue
213 |
214 | ## Environment Variables
215 |
216 | Just one. We're minimalists now:
217 |
218 | | Variable | Description | Default |
219 | |----------|-------------|---------|
220 | | `ELASTICSEARCH_URL` | NixOS API endpoint | https://search.nixos.org/backend |
221 |
222 | ## Troubleshooting
223 |
224 | ### Nix Sandbox Error
225 |
226 | If you encounter this error when running via Nix:
227 | ```
228 | error: derivation '/nix/store/...-python3.11-watchfiles-1.0.4.drv' specifies a sandbox profile,
229 | but this is only allowed when 'sandbox' is 'relaxed'
230 | ```
231 |
232 | **Solution**: Run with relaxed sandbox mode:
233 | ```bash
234 | nix run --option sandbox relaxed github:utensils/mcp-nixos --
235 | ```
236 |
237 | **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.
238 |
239 | **Permanent fix**: Add to your `/etc/nix/nix.conf`:
240 | ```
241 | sandbox = relaxed
242 | ```
243 |
244 | ## Acknowledgments
245 |
246 | This project queries data from several amazing services:
247 | - **[NixHub.io](https://www.nixhub.io)** - Provides package version history and commit tracking
248 | - **[search.nixos.org](https://search.nixos.org)** - Official NixOS package and option search
249 | - **[Jetify](https://www.jetify.com)** - Creators of [Devbox](https://www.jetify.com/devbox) and NixHub
250 |
251 | *Note: These services have not endorsed this tool. We're just grateful API consumers.*
252 |
253 | ## License
254 |
255 | MIT - Because sharing is caring, even if the code hurts.
256 |
257 | ---
258 |
259 | _Created by James Brink and maintained by masochists who enjoy Nix and async/await patterns._
260 |
261 | _Special thanks to the NixOS project for creating an OS that's simultaneously the best and worst thing ever._
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | 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.
8 |
9 | ## Key Architecture
10 |
11 | The project is a FastMCP 2.x server (async) with a single main module:
12 | - `mcp_nixos/server.py` - All MCP tools and API interactions (asyncio-based)
13 |
14 | Data sources:
15 | - NixOS packages/options: Elasticsearch API at search.nixos.org
16 | - Home Manager options: HTML parsing from official docs
17 | - nix-darwin options: HTML parsing from official docs
18 | - Package versions: NixHub.io API
19 | - Flakes: search.nixos.org flake index
20 |
21 | All responses are formatted as plain text for optimal LLM consumption.
22 |
23 | ## Development Commands
24 |
25 | ### With Nix Development Shell (Recommended)
26 |
27 | ```bash
28 | # Enter dev shell (auto-activates Python venv)
29 | nix develop
30 |
31 | # Core commands:
32 | run # Start the MCP server
33 | run-tests # Run all tests (with coverage in CI)
34 | run-tests --unit # Unit tests only
35 | run-tests --integration # Integration tests only
36 | lint # Check code with ruff
37 | format # Format code with ruff
38 | typecheck # Run mypy type checker
39 | build # Build package distributions
40 | publish # Upload to PyPI
41 | ```
42 |
43 | ### Without Nix
44 |
45 | ```bash
46 | # Install with development dependencies
47 | uv pip install -e ".[dev]" # or pip install -e ".[dev]"
48 |
49 | # Run server
50 | uv run mcp-nixos # or python -m mcp_nixos.server
51 |
52 | # Testing
53 | pytest tests/
54 | pytest tests/ --unit
55 | pytest tests/ --integration
56 |
57 | # Linting and formatting
58 | ruff format mcp_nixos/ tests/
59 | ruff check mcp_nixos/ tests/
60 | mypy mcp_nixos/
61 | ```
62 |
63 | ## Testing Approach
64 |
65 | - 367+ async tests using pytest-asyncio
66 | - Real API calls (no mocks) for integration tests
67 | - Unit tests marked with `@pytest.mark.unit`
68 | - Integration tests marked with `@pytest.mark.integration`
69 | - Tests ensure plain text output (no XML/JSON leakage)
70 |
71 | ## Local Development with MCP Clients
72 |
73 | Create `.mcp.json` in project root (already gitignored):
74 |
75 | ```json
76 | {
77 | "mcpServers": {
78 | "nixos": {
79 | "type": "stdio",
80 | "command": "uv",
81 | "args": [
82 | "run",
83 | "--directory",
84 | "/path/to/mcp-nixos",
85 | "mcp-nixos"
86 | ]
87 | }
88 | }
89 | }
90 | ```
91 |
92 | ## Important Implementation Notes
93 |
94 | 1. **Channel Resolution**: The server dynamically discovers available NixOS channels on startup. "stable" always maps to the current stable release.
95 |
96 | 2. **Error Handling**: All tools return helpful plain text error messages. API failures gracefully degrade with user-friendly messages.
97 |
98 | 3. **No Caching**: Version 1.0+ removed all caching for simplicity. All queries hit live APIs.
99 |
100 | 4. **Async Everything**: Version 1.0.1 migrated to FastMCP 2.x. All tools are async functions.
101 |
102 | 5. **Plain Text Output**: All responses are formatted as human-readable plain text. Never return raw JSON or XML to users.
103 |
104 | ## CI/CD Workflows
105 |
106 | - **CI**: Runs on all PRs - tests (unit + integration), linting, type checking
107 | - **Publish**: Automated PyPI releases on version tags (v*)
108 | - **Claude Code Review**: Reviews PRs using Claude
109 | - **Claude PR Assistant**: Helps with PR creation
110 |
111 | ## Environment Variables
112 |
113 | - `ELASTICSEARCH_URL`: Override NixOS API endpoint (default: https://search.nixos.org/backend)
```
--------------------------------------------------------------------------------
/website/public/robots.txt:
--------------------------------------------------------------------------------
```
1 | User-agent: *
2 | Allow: /
3 |
4 | Sitemap: https://mcp-nixos.io//sitemap.xml
5 |
```
--------------------------------------------------------------------------------
/website/postcss.config.js:
--------------------------------------------------------------------------------
```javascript
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
```
--------------------------------------------------------------------------------
/website/components/Footer.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import ClientFooter from './ClientFooter';
2 |
3 | export default function Footer() {
4 | return <ClientFooter />;
5 | }
```
--------------------------------------------------------------------------------
/website/components/Navbar.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import ClientNavbar from './ClientNavbar';
2 |
3 | export default function Navbar() {
4 | return <ClientNavbar />;
5 | }
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file
2 | # https://smithery.ai/docs/config
3 |
4 | startCommand:
5 | type: stdio
6 | command: python
7 | args: ["-m", "mcp_nixos"]
8 |
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Test utilities for MCP-NixOS tests."""
2 |
3 | # This file intentionally left minimal as the refactored server
4 | # doesn't need the complex test base classes from the old implementation
5 |
```
--------------------------------------------------------------------------------
/website/public/favicon/browserconfig.xml:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <browserconfig>
3 | <msapplication>
4 | <tile>
5 | <square150x150logo src="/favicon/mstile-150x150.png"/>
6 | <TileColor>#5277c3</TileColor>
7 | </tile>
8 | </msapplication>
9 | </browserconfig>
```
--------------------------------------------------------------------------------
/.claude/settings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "permissions": {
3 | "allow": [
4 | "Bash",
5 | "Task",
6 | "Glob",
7 | "Grep",
8 | "LS",
9 | "Read",
10 | "Edit",
11 | "MultiEdit",
12 | "Write",
13 | "WebFetch",
14 | "WebSearch"
15 | ]
16 | },
17 | "enabledMcpjsonServers": [
18 | "nixos"
19 | ]
20 | }
```
--------------------------------------------------------------------------------
/website/windsurf_deployment.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Windsurf Deploys Configuration (Beta)
2 | # This is an auto-generated file used to store your app deployment configuration. Do not modify.
3 | # The ID of the project (different from project name) on the provider's system. This is populated as a way to update existing deployments.
4 | project_id: 734e7842-9206-4765-b714-81e5541a3f91
5 | # The framework of the web application (examples: nextjs, react, vue, etc.)
6 | framework: nextjs
7 |
```
--------------------------------------------------------------------------------
/website/netlify.toml:
--------------------------------------------------------------------------------
```toml
1 | [build]
2 | command = "npm run build"
3 | publish = "out"
4 |
5 | [build.environment]
6 | NODE_VERSION = "18"
7 |
8 | [dev]
9 | command = "npm run dev"
10 | port = 3000
11 | targetPort = 3000
12 |
13 | [[redirects]]
14 | from = "/*"
15 | to = "/index.html"
16 | status = 200
17 |
18 | [build.processing]
19 | skip_processing = false
20 |
21 | [build.processing.css]
22 | bundle = true
23 | minify = true
24 |
25 | [build.processing.js]
26 | bundle = true
27 | minify = true
28 |
29 | [build.processing.images]
30 | compress = true
31 |
```
--------------------------------------------------------------------------------
/website/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": "explicit"
6 | },
7 | "eslint.validate": [
8 | "javascript",
9 | "javascriptreact",
10 | "typescript",
11 | "typescriptreact"
12 | ],
13 | "typescript.tsdk": "node_modules/typescript/lib",
14 | "typescript.enablePromptUseWorkspaceTsdk": true,
15 | "files.associations": {
16 | "*.css": "tailwindcss"
17 | }
18 | }
```
--------------------------------------------------------------------------------
/website/next.config.js:
--------------------------------------------------------------------------------
```javascript
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | output: 'export', // Enable static exports for S3/CloudFront
4 | distDir: 'out', // Output directory for static export
5 | images: {
6 | unoptimized: true, // Required for static export
7 | },
8 | reactStrictMode: true,
9 |
10 | // Allow cross-origin requests during development (for VS Code browser preview)
11 | allowedDevOrigins: [
12 | '127.0.0.1',
13 | ],
14 | };
15 |
16 | module.exports = nextConfig;
```
--------------------------------------------------------------------------------
/mcp_nixos/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | MCP-NixOS - Model Context Protocol server for NixOS, Home Manager, and nix-darwin resources.
3 |
4 | This package provides MCP resources and tools for interacting with NixOS packages,
5 | system options, Home Manager configuration options, and nix-darwin macOS configuration options.
6 | """
7 |
8 | from importlib.metadata import PackageNotFoundError, version
9 |
10 | try:
11 | __version__ = version("mcp-nixos")
12 | except PackageNotFoundError:
13 | # Package is not installed, use a default version
14 | __version__ = "1.0.1"
15 |
```
--------------------------------------------------------------------------------
/website/public/sitemap.xml:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3 | <url>
4 | <loc>https://mcp-nixos.io//</loc>
5 | <lastmod>2025-04-03</lastmod>
6 | <changefreq>monthly</changefreq>
7 | <priority>1.0</priority>
8 | </url>
9 | <url>
10 | <loc>https://mcp-nixos.io//about</loc>
11 | <lastmod>2025-04-03</lastmod>
12 | <changefreq>monthly</changefreq>
13 | <priority>0.8</priority>
14 | </url>
15 | <url>
16 | <loc>https://mcp-nixos.io//docs</loc>
17 | <lastmod>2025-04-03</lastmod>
18 | <changefreq>weekly</changefreq>
19 | <priority>0.9</priority>
20 | </url>
21 | </urlset>
22 |
```
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
```
1 | [pytest]
2 | testpaths = tests
3 | python_files = test_*.py
4 | python_classes = Test*
5 | python_functions = test_*
6 | addopts = --verbose
7 | asyncio_default_fixture_loop_scope = function
8 | markers =
9 | slow: marks tests as slow (deselect with '-m "not slow"')
10 | integration: marks tests that require external services or interact with external resources
11 | unit: marks tests as unit tests that don't require external services
12 | not_integration: explicitly marks tests that should be excluded from integration test runs
13 | asyncio: mark a test as an async test
14 | evals: marks tests as evaluation tests for MCP behavior
15 |
```
--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noEmit": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "bundler",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "incremental": true,
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "paths": {
26 | "@/*": [
27 | "./*"
28 | ]
29 | }
30 | },
31 | "include": [
32 | "**/*.ts",
33 | "**/*.tsx",
34 | ".next/types/**/*.ts",
35 | "next-env.d.ts",
36 | "out/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
```
--------------------------------------------------------------------------------
/website/tailwind.config.js:
--------------------------------------------------------------------------------
```javascript
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './app/**/*.{js,ts,jsx,tsx,mdx}',
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | colors: {
11 | 'nix-primary': '#5277C3', // NixOS primary blue
12 | 'nix-secondary': '#7EBAE4', // NixOS secondary blue
13 | 'nix-dark': '#1C3E5A', // Darker blue for contrast
14 | 'nix-light': '#E6F0FA', // Light blue background
15 | },
16 | fontFamily: {
17 | sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
18 | mono: ['Fira Code', 'ui-monospace', 'monospace'],
19 | },
20 | },
21 | },
22 | plugins: [],
23 | }
```
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
```python
1 | """Minimal test configuration for refactored MCP-NixOS."""
2 |
3 |
4 | def pytest_addoption(parser):
5 | """Add test filtering options."""
6 | parser.addoption("--unit", action="store_true", help="Run unit tests only")
7 | parser.addoption("--integration", action="store_true", help="Run integration tests only")
8 |
9 |
10 | def pytest_configure(config):
11 | """Configure pytest markers."""
12 | config.addinivalue_line("markers", "integration: mark test as integration test")
13 | config.addinivalue_line("markers", "slow: mark test as slow")
14 | config.addinivalue_line("markers", "asyncio: mark test as async")
15 |
16 | # Handle test filtering
17 | if config.getoption("--unit"):
18 | config.option.markexpr = "not integration"
19 | elif config.getoption("--integration"):
20 | config.option.markexpr = "integration"
21 |
```
--------------------------------------------------------------------------------
/.github/workflows/deploy-flakehub.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: "Publish tags to FlakeHub"
2 | on:
3 | push:
4 | tags:
5 | - "v?[0-9]+.[0-9]+.[0-9]+*"
6 | workflow_dispatch:
7 | inputs:
8 | tag:
9 | description: "The existing tag to publish to FlakeHub"
10 | type: "string"
11 | required: true
12 | jobs:
13 | flakehub-publish:
14 | runs-on: "ubuntu-latest"
15 | permissions:
16 | id-token: "write"
17 | contents: "read"
18 | steps:
19 | - uses: "actions/checkout@v5"
20 | with:
21 | persist-credentials: false
22 | ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
23 | - uses: "DeterminateSystems/determinate-nix-action@v3"
24 | - uses: "DeterminateSystems/flakehub-push@main"
25 | with:
26 | visibility: "public"
27 | name: "utensils/mcp-nixos"
28 | tag: "${{ inputs.tag }}"
29 | include-output-paths: true
30 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Multi-stage build for minimal final image
2 | FROM python:3.13-alpine3.22 AS builder
3 |
4 | # Install build dependencies
5 | RUN apk add --no-cache \
6 | gcc \
7 | musl-dev \
8 | libffi-dev
9 |
10 | # Set working directory
11 | WORKDIR /build
12 |
13 | # Copy project files
14 | COPY pyproject.toml README.md ./
15 | COPY mcp_nixos/ ./mcp_nixos/
16 |
17 | # Build wheel
18 | RUN pip wheel --no-cache-dir --wheel-dir /wheels .
19 |
20 | # Final stage
21 | FROM python:3.13-alpine3.22
22 |
23 | # Set environment variables
24 | ENV PYTHONUNBUFFERED=1 \
25 | PYTHONDONTWRITEBYTECODE=1 \
26 | PIP_NO_CACHE_DIR=1 \
27 | PIP_DISABLE_PIP_VERSION_CHECK=1
28 |
29 | # Install runtime dependencies
30 | RUN apk add --no-cache libffi
31 |
32 | # Create non-root user
33 | RUN adduser -D -h /app mcp
34 |
35 | # Set working directory
36 | WORKDIR /app
37 |
38 | # Copy wheels from builder
39 | COPY --from=builder /wheels /wheels
40 |
41 | # Install the package
42 | RUN pip install --no-cache-dir /wheels/* && \
43 | rm -rf /wheels
44 |
45 | # Switch to non-root user
46 | USER mcp
47 |
48 | # Run the MCP server
49 | ENTRYPOINT ["python", "-m", "mcp_nixos.server"]
```
--------------------------------------------------------------------------------
/.github/workflows/deploy-website.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Deploy Website
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths:
7 | - 'website/**'
8 | - '.github/workflows/deploy-website.yml'
9 |
10 | permissions:
11 | contents: read
12 | id-token: write
13 |
14 | jobs:
15 | deploy:
16 | runs-on: ubuntu-latest
17 | environment:
18 | name: AWS
19 | url: https://mcp-nixos.io/
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - name: Setup Node.js
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: '20'
28 | cache: 'npm'
29 | cache-dependency-path: website/package-lock.json
30 |
31 | - name: Build website
32 | run: |
33 | cd website
34 | npm install
35 | npm run build
36 |
37 | - name: Configure AWS credentials
38 | uses: aws-actions/configure-aws-credentials@v4
39 | with:
40 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
41 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
42 | aws-region: us-east-1
43 |
44 | - name: Deploy to S3
45 | run: |
46 | aws s3 sync website/out/ s3://urandom-mcp-nixos/ --delete
47 | aws cloudfront create-invalidation --distribution-id E1QS1G7FYYJ6TL --paths "/*"
```
--------------------------------------------------------------------------------
/website/public/images/attribution.md:
--------------------------------------------------------------------------------
```markdown
1 | # Attribution
2 |
3 | ## NixOS and NixOS Snowflake Logo
4 |
5 | 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.
6 |
7 | Specific logo files used:
8 | - `nixos-flake.svg`: The NixOS snowflake logo in blue (monochrome)
9 | - `nixos-snowflake-colour.svg`: The NixOS snowflake logo with gradient colors from commit 33856d7837cb8ba76c4fc9e26f91a659066ee31f
10 | - `mcp-nixos.png`: Project logo incorporating the NixOS snowflake design
11 | - Favicon files in `/favicon/` directory: Generated from the MCP-NixOS project logo
12 |
13 | 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.
14 |
15 | 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.
16 |
17 | ## Our Project
18 |
19 | 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
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 |
5 | interface CollapsibleSectionProps {
6 | title: string;
7 | children: React.ReactNode;
8 | }
9 |
10 | export default function CollapsibleSection({ title, children }: CollapsibleSectionProps) {
11 | const [isOpen, setIsOpen] = useState(false);
12 |
13 | return (
14 | <div className="mb-4 border border-nix-light rounded-lg overflow-hidden">
15 | <button
16 | onClick={() => setIsOpen(!isOpen)}
17 | 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"
18 | >
19 | <h5 className="text-md font-semibold text-nix-primary flex items-center">
20 | {title}
21 | </h5>
22 | <svg
23 | className={`w-5 h-5 text-nix-primary transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
24 | fill="none"
25 | stroke="currentColor"
26 | viewBox="0 0 24 24"
27 | xmlns="http://www.w3.org/2000/svg"
28 | >
29 | <path
30 | strokeLinecap="round"
31 | strokeLinejoin="round"
32 | strokeWidth={2}
33 | d="M19 9l-7 7-7-7"
34 | />
35 | </svg>
36 | </button>
37 | {isOpen && <div className="p-4">{children}</div>}
38 | </div>
39 | );
40 | }
41 |
```
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-nixos-website",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "type-check": "tsc --noEmit",
12 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
13 | "check-format": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\""
14 | },
15 | "dependencies": {
16 | "@types/react-syntax-highlighter": "^15.5.13",
17 | "next": "^15.2.4",
18 | "react": "^18.3.1",
19 | "react-dom": "^18.3.1",
20 | "react-syntax-highlighter": "^15.6.1",
21 | "rehype-pretty-code": "^0.14.1",
22 | "sharp": "^0.33.5",
23 | "shiki": "^3.2.1"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^20.17.30",
27 | "@types/react": "^18.3.20",
28 | "@types/react-dom": "^18.3.6",
29 | "@typescript-eslint/eslint-plugin": "^8.29.0",
30 | "@typescript-eslint/parser": "^8.29.0",
31 | "autoprefixer": "^10.4.21",
32 | "eslint": "^8.57.1",
33 | "eslint-config-next": "^15.2.4",
34 | "eslint-plugin-react": "^7.37.5",
35 | "eslint-plugin-react-hooks": "^5.2.0",
36 | "postcss": "^8.5.3",
37 | "prettier": "^3.5.3",
38 | "prettier-plugin-tailwindcss": "^0.6.11",
39 | "tailwindcss": "^3.4.17",
40 | "typescript": "^5.8.2"
41 | },
42 | "overrides": {
43 | "prismjs": "^1.30.0"
44 | }
45 | }
46 |
```
--------------------------------------------------------------------------------
/website/app/globals.css:
--------------------------------------------------------------------------------
```css
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* Global styles for prose content */
6 | p {
7 | @apply text-gray-800 leading-relaxed;
8 | }
9 |
10 | .prose p {
11 | @apply text-gray-800 leading-relaxed;
12 | }
13 |
14 | .prose li {
15 | @apply text-gray-800 font-medium;
16 | }
17 |
18 | /* Style for inline code, but not code blocks */
19 | .prose code:not(pre code) {
20 | @apply bg-gray-100 text-nix-dark px-1.5 py-0.5 rounded font-medium;
21 | }
22 |
23 | /* Ensure syntax highlighting in code blocks is not affected by global styles */
24 | .prose pre {
25 | @apply p-0 m-0 overflow-hidden rounded-lg;
26 | }
27 |
28 | .prose pre code {
29 | @apply bg-transparent p-0 text-inherit;
30 | }
31 |
32 | :root {
33 | --foreground-rgb: 0, 0, 0;
34 | --background-start-rgb: 240, 240, 245;
35 | --background-end-rgb: 255, 255, 255;
36 | }
37 |
38 | @media (prefers-color-scheme: dark) {
39 | :root {
40 | --foreground-rgb: 255, 255, 255;
41 | --background-start-rgb: 0, 0, 0;
42 | --background-end-rgb: 30, 30, 40;
43 | }
44 | }
45 |
46 | body {
47 | color: rgb(var(--foreground-rgb));
48 | background: linear-gradient(
49 | to bottom,
50 | transparent,
51 | rgb(var(--background-end-rgb))
52 | )
53 | rgb(var(--background-start-rgb));
54 | }
55 |
56 | @layer components {
57 | .btn-primary {
58 | @apply px-4 py-2 bg-nix-primary text-white rounded-md hover:bg-nix-dark transition-colors font-medium shadow-sm;
59 | }
60 |
61 | .btn-secondary {
62 | @apply px-4 py-2 border border-nix-primary text-nix-primary rounded-md hover:bg-nix-light transition-colors font-medium shadow-sm;
63 | }
64 |
65 | .container-custom {
66 | @apply container mx-auto px-4 md:px-6 lg:px-8 max-w-7xl;
67 | }
68 | }
```
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
```python
1 | """Tests for the main entry point in server module."""
2 |
3 | from unittest.mock import patch
4 |
5 | import pytest
6 | from mcp_nixos.server import main
7 |
8 |
9 | class TestMainModule:
10 | """Test the main entry point."""
11 |
12 | @patch("mcp_nixos.server.mcp")
13 | def test_main_normal_execution(self, mock_mcp):
14 | """Test normal server execution."""
15 | mock_mcp.run.return_value = None
16 |
17 | # Should not raise any exception
18 | main()
19 | mock_mcp.run.assert_called_once()
20 |
21 | @patch("mcp_nixos.server.mcp")
22 | def test_main_mcp_not_none(self, mock_mcp):
23 | """Test that mcp instance exists."""
24 | # Import to ensure mcp is available
25 | from mcp_nixos.server import mcp
26 |
27 | assert mcp is not None
28 |
29 |
30 | class TestServerImport:
31 | """Test server module imports."""
32 |
33 | def test_mcp_import_from_server(self):
34 | """Test that mcp is properly available in server."""
35 | from mcp_nixos.server import mcp
36 |
37 | assert mcp is not None
38 |
39 | def test_server_has_required_attributes(self):
40 | """Test that server module has required attributes."""
41 | from mcp_nixos import server
42 |
43 | assert hasattr(server, "mcp")
44 | assert hasattr(server, "main")
45 | assert hasattr(server, "nixos_search")
46 | assert hasattr(server, "nixos_info")
47 | assert hasattr(server, "home_manager_search")
48 | assert hasattr(server, "darwin_search")
49 |
50 |
51 | class TestIntegration:
52 | """Integration tests for main function."""
53 |
54 | def test_main_function_signature(self):
55 | """Test main function has correct signature."""
56 | from inspect import signature
57 |
58 | sig = signature(main)
59 |
60 | # Should take no parameters
61 | assert len(sig.parameters) == 0
62 |
63 | # Should be callable
64 | assert callable(main)
65 |
66 |
67 | if __name__ == "__main__":
68 | pytest.main([__file__, "-v", "--cov=mcp_nixos.server", "--cov-report=term-missing"])
69 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "mcp-nixos"
7 | version = "1.0.3"
8 | description = "Model Context Protocol server for NixOS, Home Manager, and nix-darwin resources"
9 | readme = "README.md"
10 | authors = [
11 | {name = "James Brink", email = "[email protected]"},
12 | ]
13 | requires-python = ">=3.11"
14 | license = {text = "MIT"}
15 | classifiers = [
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License",
19 | "Programming Language :: Python :: 3",
20 | "Programming Language :: Python :: 3.11",
21 | "Programming Language :: Python :: 3.12",
22 | "Programming Language :: Python :: 3.13",
23 | ]
24 | dependencies = [
25 | "fastmcp>=2.11.0",
26 | "requests>=2.32.4",
27 | "beautifulsoup4>=4.13.4",
28 | ]
29 |
30 | [project.optional-dependencies]
31 | dev = [
32 | "build>=1.2.2",
33 | "pytest>=8.4.1",
34 | "pytest-cov>=6.2.1",
35 | "pytest-asyncio>=1.1.0",
36 | "pytest-xdist>=3.6.0",
37 | "ruff>=0.12.4",
38 | "mypy>=1.17.0",
39 | "types-beautifulsoup4>=4.12.0.20250516",
40 | "types-requests>=2.32.4",
41 | "twine>=6.0.1",
42 | ]
43 | win = [
44 | "pywin32>=308.0", # Required for Windows-specific file operations and tests
45 | ]
46 |
47 | [project.scripts]
48 | mcp-nixos = "mcp_nixos.server:main"
49 |
50 | [tool.ruff]
51 | line-length = 120
52 | target-version = "py311"
53 | src = ["mcp_nixos", "tests"]
54 |
55 | [tool.ruff.lint]
56 | select = [
57 | "E", # pycodestyle errors
58 | "W", # pycodestyle warnings
59 | "F", # pyflakes
60 | "I", # isort
61 | "B", # flake8-bugbear
62 | "C4", # flake8-comprehensions
63 | "UP", # pyupgrade
64 | ]
65 | ignore = ["E402", "E203"]
66 |
67 | [tool.mypy]
68 | python_version = "3.11"
69 | strict = true
70 | warn_return_any = true
71 | warn_unused_configs = true
72 | no_implicit_reexport = true
73 | namespace_packages = true
74 | explicit_package_bases = true
75 |
76 | [[tool.mypy.overrides]]
77 | module = "tests.*"
78 | ignore_errors = true
79 |
80 | [tool.pytest.ini_options]
81 | asyncio_mode = "auto"
82 | testpaths = ["tests"]
83 | python_files = "test_*.py"
84 | addopts = "--cov=mcp_nixos --cov-report=term-missing"
85 |
86 | [tool.coverage.run]
87 | source = ["mcp_nixos"]
88 |
```
--------------------------------------------------------------------------------
/.github/workflows/claude.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Claude Code
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 | pull_request_review_comment:
7 | types: [created]
8 | issues:
9 | types: [opened, assigned]
10 | pull_request_review:
11 | types: [submitted]
12 |
13 | jobs:
14 | claude:
15 | if: |
16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20 | runs-on: ubuntu-latest
21 | permissions:
22 | contents: read
23 | pull-requests: read
24 | issues: read
25 | id-token: write
26 | actions: read # Required for Claude to read CI results on PRs
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 | with:
31 | fetch-depth: 1
32 |
33 | - name: Run Claude Code
34 | id: claude
35 | uses: anthropics/claude-code-action@beta
36 | with:
37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38 |
39 | # This is an optional setting that allows Claude to read CI results on PRs
40 | additional_permissions: |
41 | actions: read
42 |
43 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
44 | # model: "claude-opus-4-20250514"
45 |
46 | # Optional: Customize the trigger phrase (default: @claude)
47 | # trigger_phrase: "/claude"
48 |
49 | # Optional: Trigger when specific user is assigned to an issue
50 | # assignee_trigger: "claude-bot"
51 |
52 | # Optional: Allow Claude to run specific commands
53 | # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
54 |
55 | # Optional: Add custom instructions for Claude to customize its behavior for your project
56 | # custom_instructions: |
57 | # Follow our coding standards
58 | # Ensure all new code has tests
59 | # Use TypeScript for new files
60 |
61 | # Optional: Custom environment variables for Claude
62 | # claude_env: |
63 | # NODE_ENV: test
64 |
65 |
```
--------------------------------------------------------------------------------
/website/app/layout.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import type { Metadata, Viewport } from 'next';
2 | import './globals.css';
3 | import Navbar from '@/components/Navbar';
4 | import Footer from '@/components/Footer';
5 |
6 | export const metadata: Metadata = {
7 | title: 'MCP-NixOS | Model Context Protocol for NixOS',
8 | description: 'MCP resources and tools for NixOS packages, system options, Home Manager configuration, and nix-darwin macOS configuration.',
9 | keywords: ['NixOS', 'MCP', 'Model Context Protocol', 'Home Manager', 'nix-darwin', 'Claude', 'AI Assistant'],
10 | authors: [{ name: 'Utensils', url: 'https://utensils.io' }],
11 | creator: 'Utensils',
12 | publisher: 'Utensils',
13 | metadataBase: new URL('https://mcp-nixos.io'),
14 | alternates: {
15 | canonical: '/',
16 | },
17 | // Open Graph metadata
18 | openGraph: {
19 | type: 'website',
20 | locale: 'en_US',
21 | url: 'https://mcp-nixos.io',
22 | siteName: 'MCP-NixOS',
23 | title: 'MCP-NixOS | Model Context Protocol for NixOS',
24 | description: 'MCP resources and tools for NixOS packages, system options, Home Manager configuration, and nix-darwin macOS configuration.',
25 | images: [
26 | {
27 | url: '/images/og-image.png',
28 | width: 1200,
29 | height: 630,
30 | alt: 'MCP-NixOS - Model Context Protocol for NixOS',
31 | },
32 | ],
33 | },
34 | // Twitter Card metadata
35 | twitter: {
36 | card: 'summary_large_image',
37 | title: 'MCP-NixOS | Model Context Protocol for NixOS',
38 | description: 'MCP resources and tools for NixOS packages, system options, Home Manager configuration, and nix-darwin macOS configuration.',
39 | images: ['/images/og-image.png'],
40 | creator: '@utensils_io',
41 | },
42 | icons: {
43 | icon: [
44 | { url: '/favicon/favicon.ico' },
45 | { url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
46 | { url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' }
47 | ],
48 | apple: [
49 | { url: '/favicon/apple-touch-icon.png' }
50 | ],
51 | other: [
52 | {
53 | rel: 'mask-icon',
54 | url: '/favicon/safari-pinned-tab.svg',
55 | color: '#5277c3'
56 | }
57 | ]
58 | },
59 | manifest: '/favicon/site.webmanifest',
60 | };
61 |
62 | export const viewport: Viewport = {
63 | themeColor: '#5277c3',
64 | };
65 |
66 | export default function RootLayout({
67 | children,
68 | }: Readonly<{
69 | children: React.ReactNode;
70 | }>) {
71 | return (
72 | <html lang="en">
73 | <body className="min-h-screen flex flex-col">
74 | <Navbar />
75 | <main className="flex-grow">
76 | {children}
77 | </main>
78 | <Footer />
79 | </body>
80 | </html>
81 | );
82 | }
```
--------------------------------------------------------------------------------
/website/app/test-code-block/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import React from 'react';
4 | import CodeBlock from '../../components/CodeBlock';
5 | import AnchorHeading from '@/components/AnchorHeading';
6 |
7 | export default function TestCodeBlockPage() {
8 | const pythonCode = `def hello_world():
9 | # This is a comment
10 | print("Hello, world!")
11 | return 42
12 |
13 | # Call the function
14 | result = hello_world()
15 | print(f"The result is {result}")`;
16 |
17 | const nixCode = `{ config, pkgs, lib, ... }:
18 |
19 | # This is a NixOS configuration example
20 | {
21 | imports = [
22 | ./hardware-configuration.nix
23 | ];
24 |
25 | # Use the systemd-boot EFI boot loader
26 | boot.loader.systemd-boot.enable = true;
27 | boot.loader.efi.canTouchEfiVariables = true;
28 |
29 | # Define a user account
30 | users.users.alice = {
31 | isNormalUser = true;
32 | extraGroups = [ "wheel" "networkmanager" ];
33 | packages = with pkgs; [
34 | firefox
35 | git
36 | vscode
37 | ];
38 | };
39 |
40 | # Enable some services
41 | services.xserver = {
42 | enable = true;
43 | displayManager.gdm.enable = true;
44 | desktopManager.gnome.enable = true;
45 | };
46 |
47 | # This value determines the NixOS release
48 | system.stateVersion = "24.05";
49 | }`;
50 |
51 | const typescriptCode = `import { useState, useEffect } from 'react';
52 |
53 | interface User {
54 | id: number;
55 | name: string;
56 | email: string;
57 | }
58 |
59 | // This is a React hook that fetches user data
60 | export function useUserData(userId: number): User | null {
61 | const [user, setUser] = useState<User | null>(null);
62 | const [loading, setLoading] = useState<boolean>(true);
63 |
64 | useEffect(() => {
65 | async function fetchData() {
66 | try {
67 | const response = await fetch(\`/api/users/\${userId}\`);
68 | const data = await response.json();
69 | setUser(data);
70 | } catch (error) {
71 | console.error("Failed to fetch user:", error);
72 | } finally {
73 | setLoading(false);
74 | }
75 | }
76 |
77 | fetchData();
78 | }, [userId]);
79 |
80 | return user;
81 | }`;
82 |
83 | return (
84 | <div className="container mx-auto py-12 px-4">
85 | <AnchorHeading level={1} className="text-3xl font-bold mb-8 text-nix-primary">Code Block Test Page</AnchorHeading>
86 |
87 | <section className="mb-12">
88 | <AnchorHeading level={2} className="text-2xl font-semibold mb-4 text-nix-dark">Python Example</AnchorHeading>
89 | <CodeBlock code={pythonCode} language="python" showLineNumbers={true} />
90 | </section>
91 |
92 | <section className="mb-12">
93 | <AnchorHeading level={2} className="text-2xl font-semibold mb-4 text-nix-dark">Nix Example</AnchorHeading>
94 | <CodeBlock code={nixCode} language="nix" />
95 | </section>
96 |
97 | <section className="mb-12">
98 | <AnchorHeading level={2} className="text-2xl font-semibold mb-4 text-nix-dark">TypeScript Example</AnchorHeading>
99 | <CodeBlock code={typescriptCode} language="typescript" showLineNumbers={true} />
100 | </section>
101 | </div>
102 | );
103 | }
104 |
```
--------------------------------------------------------------------------------
/.github/workflows/claude-code-review.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Claude Code Review
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 | # Optional: Only run on specific file changes
7 | # paths:
8 | # - "src/**/*.ts"
9 | # - "src/**/*.tsx"
10 | # - "src/**/*.js"
11 | # - "src/**/*.jsx"
12 |
13 | jobs:
14 | claude-review:
15 | # Optional: Filter by PR author
16 | # if: |
17 | # github.event.pull_request.user.login == 'external-contributor' ||
18 | # github.event.pull_request.user.login == 'new-developer' ||
19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20 |
21 | runs-on: ubuntu-latest
22 | permissions:
23 | contents: read
24 | pull-requests: read
25 | issues: read
26 | id-token: write
27 |
28 | steps:
29 | - name: Checkout repository
30 | uses: actions/checkout@v4
31 | with:
32 | fetch-depth: 1
33 |
34 | - name: Run Claude Code Review
35 | id: claude-review
36 | uses: anthropics/claude-code-action@beta
37 | with:
38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39 |
40 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
41 | # model: "claude-opus-4-20250514"
42 |
43 | # Direct prompt for automated review (no @claude mention needed)
44 | direct_prompt: |
45 | Please review this pull request and provide feedback on:
46 | - Code quality and best practices
47 | - Potential bugs or issues
48 | - Performance considerations
49 | - Security concerns
50 | - Test coverage
51 |
52 | Be constructive and helpful in your feedback.
53 |
54 | # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
55 | use_sticky_comment: true
56 |
57 | # Optional: Customize review based on file types
58 | # direct_prompt: |
59 | # Review this PR focusing on:
60 | # - For TypeScript files: Type safety and proper interface usage
61 | # - For API endpoints: Security, input validation, and error handling
62 | # - For React components: Performance, accessibility, and best practices
63 | # - For tests: Coverage, edge cases, and test quality
64 |
65 | # Optional: Different prompts for different authors
66 | # direct_prompt: |
67 | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
68 | # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
69 | # 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
70 |
71 | # Optional: Add specific tools for running tests or linting
72 | # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
73 |
74 | # Optional: Skip review for certain conditions
75 | # if: |
76 | # !contains(github.event.pull_request.title, '[skip-review]') &&
77 | # !contains(github.event.pull_request.title, '[WIP]')
78 |
79 |
```
--------------------------------------------------------------------------------
/website/components/AnchorHeading.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import React, { useEffect } from 'react';
4 |
5 | interface AnchorHeadingProps {
6 | level: 1 | 2 | 3 | 4 | 5 | 6;
7 | children: React.ReactNode;
8 | className?: string;
9 | id?: string;
10 | }
11 |
12 | const AnchorHeading: React.FC<AnchorHeadingProps> = ({
13 | level,
14 | children,
15 | className = '',
16 | id
17 | }) => {
18 |
19 | // Extract text content for ID generation
20 | const extractTextContent = (node: React.ReactNode): string => {
21 | if (typeof node === 'string') return node;
22 | if (Array.isArray(node)) return node.map(extractTextContent).join(' ');
23 | if (React.isValidElement(node)) {
24 | const childContent = React.Children.toArray(node.props.children);
25 | return extractTextContent(childContent);
26 | }
27 | return '';
28 | };
29 |
30 | // Generate an ID from the children if none is provided
31 | const textContent = extractTextContent(children);
32 | const headingId = id || (textContent
33 | ? textContent.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
34 | : `heading-${Math.random().toString(36).substring(2, 9)}`);
35 |
36 | const handleAnchorClick = (e: React.MouseEvent) => {
37 | e.preventDefault();
38 | const hash = `#${headingId}`;
39 |
40 | // Update URL without page reload
41 | window.history.pushState(null, '', hash);
42 |
43 | // Scroll to the element
44 | const element = document.getElementById(headingId);
45 | if (element) {
46 | // Smooth scroll to the element
47 | element.scrollIntoView({ behavior: 'smooth', block: 'start' });
48 | }
49 | };
50 |
51 | // Handle initial load with hash in URL
52 | useEffect(() => {
53 | // Check if the current URL hash matches this heading
54 | if (typeof window !== 'undefined' && window.location.hash === `#${headingId}`) {
55 | // Add a small delay to ensure the page has fully loaded
56 | setTimeout(() => {
57 | const element = document.getElementById(headingId);
58 | if (element) {
59 | element.scrollIntoView({ behavior: 'smooth', block: 'start' });
60 | }
61 | }, 100);
62 | }
63 | }, [headingId]);
64 |
65 | const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
66 |
67 | // Check if the heading has text-center class
68 | const isCentered = className.includes('text-center');
69 |
70 | return (
71 | <HeadingTag id={headingId} className={`group relative ${className} scroll-mt-16`}>
72 | {isCentered ? (
73 | <div className="relative inline-flex items-center">
74 | {level !== 1 && (
75 | <a
76 | href={`#${headingId}`}
77 | onClick={handleAnchorClick}
78 | className="absolute -left-5 opacity-0 group-hover:opacity-100 transition-opacity text-nix-primary hover:text-nix-dark font-semibold"
79 | aria-label={`Link to ${textContent || 'this heading'}`}
80 | >
81 | #
82 | </a>
83 | )}
84 | {children}
85 | </div>
86 | ) : (
87 | <>
88 | {level !== 1 && (
89 | <a
90 | href={`#${headingId}`}
91 | onClick={handleAnchorClick}
92 | className="absolute -left-5 opacity-0 group-hover:opacity-100 transition-opacity text-nix-primary hover:text-nix-dark font-semibold"
93 | aria-label={`Link to ${textContent || 'this heading'}`}
94 | >
95 | #
96 | </a>
97 | )}
98 | {children}
99 | </>
100 | )}
101 | </HeadingTag>
102 | );
103 | };
104 |
105 | export default AnchorHeading;
106 |
```
--------------------------------------------------------------------------------
/RELEASE_WORKFLOW.md:
--------------------------------------------------------------------------------
```markdown
1 | # Release Workflow Guide for MCP-NixOS
2 |
3 | ## Overview
4 |
5 | This guide explains how to properly release a new version without triggering duplicate CI/CD runs.
6 |
7 | ## Improved CI/CD Features
8 |
9 | 1. **Documentation-only changes skip tests**: The workflow now detects if only docs (*.md, LICENSE, etc.) were changed and skips the test suite entirely.
10 |
11 | 2. **Smart change detection**: Uses `paths-filter` to categorize changes into:
12 | - `code`: Actual code changes that require testing
13 | - `docs`: Documentation changes that don't need tests
14 | - `website`: Website changes that only trigger deployment
15 |
16 | 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.
17 |
18 | ## Release Process
19 |
20 | ### Option 1: Automatic Release (Recommended)
21 |
22 | 1. **Update version in code**:
23 | ```bash
24 | # Update version in pyproject.toml
25 | # Update __init__.py fallback version if needed
26 | ```
27 |
28 | 2. **Update RELEASE_NOTES.md**:
29 | - Add release notes for the new version at the top
30 | - Follow the existing format
31 |
32 | 3. **Create PR as normal**:
33 | ```bash
34 | gh pr create --title "Release version 1.0.0"
35 | ```
36 |
37 | 4. **Merge with release trigger**:
38 | When merging the PR, edit the merge commit message to include:
39 | ```
40 | Merge pull request #28 from utensils/refactor
41 |
42 | release: v1.0.0
43 | ```
44 |
45 | 5. **Automatic steps**:
46 | - CI detects the `release:` keyword
47 | - Creates and pushes the git tag
48 | - Creates GitHub release with notes from RELEASE_NOTES.md
49 | - Triggers PyPI publishing
50 |
51 | ### Option 2: Manual Release (Traditional)
52 |
53 | 1. **Merge PR normally**
54 | 2. **Wait for CI to complete**
55 | 3. **Create and push tag manually**:
56 | ```bash
57 | git tag -a v1.0.0 -m "Release v1.0.0"
58 | git push origin v1.0.0
59 | ```
60 |
61 | ## Benefits of the New Workflow
62 |
63 | - **No duplicate runs**: The release process happens in the merge commit workflow
64 | - **Skip unnecessary tests**: Documentation changes don't trigger full test suite
65 | - **Atomic releases**: Tag, GitHub release, and PyPI publish happen together
66 | - **Clear audit trail**: Release intention is documented in the merge commit
67 |
68 | ## Testing the Workflow
69 |
70 | To test documentation-only changes:
71 | ```bash
72 | # Make changes only to *.md files
73 | git add README.md
74 | git commit -m "docs: update README"
75 | git push
76 | # CI will skip tests!
77 | ```
78 |
79 | To test the release process without actually releasing:
80 | 1. Create a test branch
81 | 2. Make a small change
82 | 3. Use the commit message pattern but with a test version
83 | 4. Verify the workflow runs correctly
84 | 5. Delete the test tag and release afterward
85 |
86 | ## Troubleshooting
87 |
88 | - If the release job fails, you can manually create the tag and it will trigger the publish job
89 | - The `paths-filter` action requires the full git history, so it uses `checkout@v4` without depth limits
90 | - The release extraction uses `awk` to parse RELEASE_NOTES.md, so maintain the heading format
91 |
92 | ## Example PR Description for Releases
93 |
94 | When creating a release PR, use this template:
95 | ```markdown
96 | ## Release v1.0.0
97 |
98 | This PR prepares the v1.0.0 release.
99 |
100 | ### Checklist
101 | - [ ] Version bumped in pyproject.toml
102 | - [ ] RELEASE_NOTES.md updated
103 | - [ ] All tests passing
104 | - [ ] Documentation updated
105 |
106 | ### Release Instructions
107 | When merging, use commit message:
108 | ```
109 | Merge pull request #XX from utensils/branch-name
110 |
111 | release: v1.0.0
112 | ```
113 | ```
```
--------------------------------------------------------------------------------
/.claude/commands/release.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | allowed-tools: Bash, Read, Edit, Glob, Grep, Write, TodoWrite
3 | description: Perform a version release with automated PyPI publishing and Docker image builds
4 | ---
5 |
6 | # Release
7 |
8 | Automate the release process: version bump, changelog, tag creation, and trigger CI/CD for PyPI and Docker deployments.
9 |
10 | ## Workflow
11 |
12 | 1. Review commits since last release
13 | 2. Determine version bump (patch/minor/major)
14 | 3. Update `pyproject.toml` and `RELEASE_NOTES.md`
15 | 4. Commit, tag, and create GitHub release
16 | 5. Verify PyPI and Docker deployments
17 |
18 | ## Key Files
19 |
20 | - `pyproject.toml` - Package version
21 | - `RELEASE_NOTES.md` - Release changelog
22 | - `.github/workflows/publish.yml` - PyPI & Docker publishing (triggered by GitHub release)
23 |
24 | ## Execute
25 |
26 | ### 1. Review Changes
27 |
28 | ```bash
29 | # Get current version and recent tags
30 | grep '^version = ' pyproject.toml
31 | git tag --list 'v*' --sort=-version:refname | head -5
32 |
33 | # Review commits since last release (replace with actual last tag)
34 | git log v1.0.2..HEAD --oneline
35 | ```
36 |
37 | ### 2. Update Version
38 |
39 | Version bump types:
40 | - **Patch** (x.y.Z): Bug fixes, CI/CD, docs
41 | - **Minor** (x.Y.0): New features, backward-compatible
42 | - **Major** (X.0.0): Breaking changes
43 |
44 | Edit `pyproject.toml`:
45 | ```toml
46 | version = "X.Y.Z"
47 | ```
48 |
49 | ### 3. Update Release Notes
50 |
51 | Add new section at top of `RELEASE_NOTES.md` following existing format:
52 |
53 | ```markdown
54 | # MCP-NixOS: vX.Y.Z Release Notes - [Title]
55 |
56 | ## Overview
57 | Brief description (1-2 sentences).
58 |
59 | ## Changes in vX.Y.Z
60 | ### 🚀 [Category]
61 | - **Feature**: Description
62 | ### 📦 Dependencies
63 | - Changes or "No changes from previous version"
64 |
65 | ## Installation
66 | [Standard installation commands]
67 |
68 | ## Migration Notes
69 | Breaking changes or "Drop-in replacement with no user-facing changes."
70 |
71 | ---
72 | ```
73 |
74 | ### 4. Commit and Tag
75 |
76 | ```bash
77 | # Commit changes
78 | git add pyproject.toml RELEASE_NOTES.md
79 | git commit -m "chore: Bump version to X.Y.Z"
80 | git commit -m "docs: Update RELEASE_NOTES.md for vX.Y.Z"
81 | git push
82 |
83 | # Create and push tag
84 | git tag -a vX.Y.Z -m "Release vX.Y.Z: [description]"
85 | git push origin vX.Y.Z
86 | ```
87 |
88 | ### 5. Create GitHub Release
89 |
90 | ```bash
91 | gh release create vX.Y.Z \
92 | --title "vX.Y.Z: [Title]" \
93 | --notes "## Overview
94 |
95 | [Brief description]
96 |
97 | ## Highlights
98 | - 🚀 [Key feature]
99 | - 🔒 [Important fix]
100 |
101 | ## Installation
102 | ```bash
103 | pip install mcp-nixos==X.Y.Z
104 | ```
105 |
106 | See [RELEASE_NOTES.md](https://github.com/utensils/mcp-nixos/blob/main/RELEASE_NOTES.md) for details."
107 | ```
108 |
109 | ### 6. Monitor Pipeline
110 |
111 | ```bash
112 | # Watch workflow execution
113 | gh run list --workflow=publish.yml --limit 3
114 | gh run watch <RUN_ID>
115 | ```
116 |
117 | ## Verify
118 |
119 | ### PyPI
120 | ```bash
121 | uvx [email protected] --help
122 | ```
123 |
124 | ### Docker Hub & GHCR
125 | ```bash
126 | docker pull utensils/mcp-nixos:X.Y.Z
127 | docker pull ghcr.io/utensils/mcp-nixos:X.Y.Z
128 | docker run --rm utensils/mcp-nixos:X.Y.Z --help
129 | ```
130 |
131 | ## Report
132 |
133 | Provide release summary:
134 |
135 | ```
136 | ✅ Release vX.Y.Z Complete!
137 |
138 | **Version:** vX.Y.Z
139 | **Release URL:** https://github.com/utensils/mcp-nixos/releases/tag/vX.Y.Z
140 |
141 | ### Verified Deployments
142 | - ✅ PyPI: https://pypi.org/project/mcp-nixos/X.Y.Z/
143 | - ✅ Docker Hub: utensils/mcp-nixos:X.Y.Z
144 | - ✅ GHCR: ghcr.io/utensils/mcp-nixos:X.Y.Z
145 | ```
146 |
147 | ## Troubleshooting
148 |
149 | **Workflow fails**: `gh run view <RUN_ID> --log-failed`
150 | **PyPI unavailable**: Wait 2-5 min for CDN, check Test PyPI first
151 | **Docker unavailable**: Wait 5-10 min for multi-arch builds
152 | **Tag exists**: Delete with `git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z`
153 |
```
--------------------------------------------------------------------------------
/website/metadata-checker.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html>
3 | <head>
4 | <title>Metadata Checker</title>
5 | <style>
6 | body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
7 | .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); }
8 | h1 { color: #5277C3; margin-top: 0; }
9 | h2 { color: #1C3E5A; margin-top: 20px; }
10 | pre { background-color: #E6F0FA; padding: 15px; border-radius: 4px; overflow-x: auto; }
11 | button { background-color: #5277C3; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-weight: 500; }
12 | button:hover { background-color: #1C3E5A; }
13 | .meta-list { margin-top: 10px; }
14 | .meta-item { margin-bottom: 5px; }
15 | </style>
16 | </head>
17 | <body>
18 | <div class="container">
19 | <h1>MCP-NixOS Metadata Checker</h1>
20 | <button id="checkMetadata">Check Metadata</button>
21 |
22 | <h2>General Metadata</h2>
23 | <div id="generalMetadata" class="meta-list"></div>
24 |
25 | <h2>Open Graph Metadata</h2>
26 | <div id="ogMetadata" class="meta-list"></div>
27 |
28 | <h2>Twitter Card Metadata</h2>
29 | <div id="twitterMetadata" class="meta-list"></div>
30 |
31 | <h2>Link Tags</h2>
32 | <div id="linkTags" class="meta-list"></div>
33 | </div>
34 |
35 | <script>
36 | document.getElementById('checkMetadata').addEventListener('click', async () => {
37 | try {
38 | // Fetch the page content
39 | const response = await fetch('http://localhost:3000');
40 | const html = await response.text();
41 |
42 | // Create a DOM parser
43 | const parser = new DOMParser();
44 | const doc = parser.parseFromString(html, 'text/html');
45 |
46 | // Extract metadata
47 | const metaTags = doc.querySelectorAll('meta');
48 | const linkTags = doc.querySelectorAll('link');
49 |
50 | // Clear previous results
51 | document.getElementById('generalMetadata').innerHTML = '';
52 | document.getElementById('ogMetadata').innerHTML = '';
53 | document.getElementById('twitterMetadata').innerHTML = '';
54 | document.getElementById('linkTags').innerHTML = '';
55 |
56 | // Process meta tags
57 | metaTags.forEach(tag => {
58 | const metaItem = document.createElement('div');
59 | metaItem.className = 'meta-item';
60 | metaItem.textContent = tag.outerHTML;
61 |
62 | if (tag.getAttribute('property') && tag.getAttribute('property').startsWith('og:')) {
63 | document.getElementById('ogMetadata').appendChild(metaItem);
64 | } else if (tag.getAttribute('name') && tag.getAttribute('name').startsWith('twitter:')) {
65 | document.getElementById('twitterMetadata').appendChild(metaItem);
66 | } else {
67 | document.getElementById('generalMetadata').appendChild(metaItem);
68 | }
69 | });
70 |
71 | // Process link tags
72 | linkTags.forEach(tag => {
73 | const linkItem = document.createElement('div');
74 | linkItem.className = 'meta-item';
75 | linkItem.textContent = tag.outerHTML;
76 | document.getElementById('linkTags').appendChild(linkItem);
77 | });
78 | } catch (error) {
79 | console.error('Error fetching metadata:', error);
80 | alert('Error fetching metadata. See console for details.');
81 | }
82 | });
83 | </script>
84 | </body>
85 | </html>
86 |
```
--------------------------------------------------------------------------------
/website/components/FeatureCard.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import React from 'react';
2 |
3 | interface FeatureCardProps {
4 | title: string;
5 | description: string;
6 | iconName: string;
7 | }
8 |
9 | const FeatureCard: React.FC<FeatureCardProps> = ({ title, description, iconName }) => {
10 | const renderIcon = () => {
11 | switch (iconName) {
12 | case 'package':
13 | return (
14 | <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">
15 | <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" />
16 | </svg>
17 | );
18 | case 'home':
19 | return (
20 | <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">
21 | <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" />
22 | </svg>
23 | );
24 | case 'apple':
25 | return (
26 | <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">
27 | <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" />
28 | </svg>
29 | );
30 | case 'bolt':
31 | return (
32 | <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">
33 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
34 | </svg>
35 | );
36 | case 'globe':
37 | return (
38 | <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">
39 | <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" />
40 | </svg>
41 | );
42 | case 'robot':
43 | return (
44 | <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">
45 | <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" />
46 | </svg>
47 | );
48 | default:
49 | return (
50 | <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">
51 | <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" />
52 | </svg>
53 | );
54 | }
55 | };
56 |
57 | return (
58 | <div className="p-6 border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow bg-white">
59 | <div className="mb-4">
60 | {renderIcon()}
61 | </div>
62 | <h3 className="text-xl font-semibold mb-2">{title}</h3>
63 | <p className="text-gray-600">{description}</p>
64 | </div>
65 | );
66 | };
67 |
68 | export default FeatureCard;
```
--------------------------------------------------------------------------------
/website/components/ClientNavbar.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import { useState } from 'react';
4 | import Link from 'next/link';
5 | import Image from 'next/image';
6 |
7 | export default function ClientNavbar() {
8 | const [isMenuOpen, setIsMenuOpen] = useState(false);
9 |
10 | const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
11 | const closeMenu = () => setIsMenuOpen(false);
12 |
13 | return (
14 | <nav className="bg-white shadow-md">
15 | <div className="container-custom mx-auto py-4">
16 | <div className="flex justify-between items-center">
17 | {/* Logo */}
18 | <div className="flex items-center space-x-2">
19 | <Image
20 | src="/images/nixos-snowflake-colour.svg"
21 | alt="NixOS Snowflake"
22 | width={32}
23 | height={32}
24 | className="h-8 w-8"
25 | />
26 | <Link href="/" className="text-2xl font-bold text-nix-primary">
27 | MCP-NixOS
28 | </Link>
29 | </div>
30 |
31 | {/* Desktop Navigation */}
32 | <div className="hidden md:flex space-x-8">
33 | <Link href="/" className="text-gray-700 hover:text-nix-primary">
34 | Home
35 | </Link>
36 | <Link href="/usage" className="text-gray-700 hover:text-nix-primary">
37 | Usage
38 | </Link>
39 | <Link href="/about" className="text-gray-700 hover:text-nix-primary">
40 | About
41 | </Link>
42 | <Link
43 | href="https://github.com/utensils/mcp-nixos"
44 | className="text-gray-700 hover:text-nix-primary"
45 | target="_blank"
46 | rel="noopener noreferrer"
47 | >
48 | GitHub
49 | </Link>
50 | </div>
51 |
52 | {/* Mobile menu button */}
53 | <div className="md:hidden">
54 | <button
55 | onClick={toggleMenu}
56 | className="text-gray-500 hover:text-nix-primary focus:outline-none"
57 | aria-label="Toggle menu"
58 | >
59 | {isMenuOpen ? (
60 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
61 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
62 | </svg>
63 | ) : (
64 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
65 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
66 | </svg>
67 | )}
68 | </button>
69 | </div>
70 | </div>
71 |
72 | {/* Mobile Menu */}
73 | {isMenuOpen && (
74 | <div className="md:hidden mt-4 pb-2">
75 | <div className="flex flex-col space-y-4">
76 | <Link
77 | href="/"
78 | className="text-gray-700 hover:text-nix-primary"
79 | onClick={closeMenu}
80 | >
81 | Home
82 | </Link>
83 | <Link
84 | href="/usage"
85 | className="text-gray-700 hover:text-nix-primary"
86 | onClick={closeMenu}
87 | >
88 | Usage
89 | </Link>
90 | <Link
91 | href="/about"
92 | className="text-gray-700 hover:text-nix-primary"
93 | onClick={closeMenu}
94 | >
95 | About
96 | </Link>
97 | <Link
98 | href="https://github.com/utensils/mcp-nixos"
99 | className="text-gray-700 hover:text-nix-primary"
100 | target="_blank"
101 | rel="noopener noreferrer"
102 | onClick={closeMenu}
103 | >
104 | GitHub
105 | </Link>
106 | </div>
107 | </div>
108 | )}
109 | </div>
110 | </nav>
111 | );
112 | }
```
--------------------------------------------------------------------------------
/.claude/agents/mcp-server-architect.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: mcp-server-architect
3 | 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.
4 | category: quality-security
5 | ---
6 |
7 | 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.
8 |
9 | ## When invoked:
10 |
11 | You should be used when there are needs to:
12 | - Design and implement new MCP servers from scratch using FastMCP 2.x
13 | - Build async servers with real API integrations (no caching/mocking)
14 | - Implement tool/resource/prompt definitions with proper annotations
15 | - Add completion support and argument suggestions
16 | - Configure session management and security measures
17 | - Enhance existing MCP servers with new capabilities
18 | - Format all outputs as plain text for optimal LLM consumption
19 | - Handle external API failures gracefully with user-friendly error messages
20 |
21 | ## Process:
22 |
23 | 1. **Analyze Requirements**: Thoroughly understand the domain and use cases before designing the server architecture
24 |
25 | 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
26 |
27 | 3. **Implement Real API Integrations**: Connect directly to live APIs without caching layers. Handle failures gracefully with meaningful error messages formatted as plain text
28 |
29 | 4. **Format for LLM Consumption**: Ensure all tool outputs are human-readable plain text, never raw JSON/XML. Structure responses for optimal LLM understanding
30 |
31 | 5. **Handle Async Operations**: Use proper asyncio patterns for all I/O operations. Implement concurrent API calls where beneficial
32 |
33 | 6. **Ensure Robust Error Handling**: Create custom exception classes, implement graceful degradation, and provide helpful user-facing error messages
34 |
35 | 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
36 |
37 | 8. **Optimize for Production**: Use efficient data structures, minimize API calls, and implement proper resource cleanup
38 |
39 | ## Provide:
40 |
41 | - **FastMCP 2.x Servers**: Complete, production-ready async MCP server implementations using FastMCP 2.x (≥2.11.0) with full type coverage
42 | - **Real API Integration Patterns**: Direct connections to external APIs (Elasticsearch, REST endpoints, HTML parsing) without caching layers
43 | - **Async Tool Implementations**: All tools as async functions using proper asyncio patterns for I/O operations
44 | - **Plain Text Formatting**: All outputs formatted as human-readable text, structured for optimal LLM consumption
45 | - **Robust Error Handling**: Custom exception classes (APIError, DocumentParseError) with graceful degradation and user-friendly messages
46 | - **Comprehensive Testing**: Async test suites using pytest-asyncio with real API calls, unit/integration test separation
47 | - **Production Patterns**: Proper resource cleanup, efficient data structures, concurrent API calls where beneficial
48 | - **Development Workflow**: Integration with Nix development shells, custom commands (run, run-tests, lint, format, typecheck)
49 |
50 | ## FastMCP 2.x Patterns:
51 |
52 | ```python
53 | from fastmcp import FastMCP
54 |
55 | mcp = FastMCP("server-name")
56 |
57 | @mcp.tool()
58 | async def search_items(query: str) -> str:
59 | """Search for items using external API."""
60 | try:
61 | # Direct API call, no caching
62 | response = await api_client.search(query)
63 | # Format as plain text for LLM
64 | return format_search_results(response)
65 | except APIError as e:
66 | return f"Search failed: {e.message}"
67 |
68 | if __name__ == "__main__":
69 | mcp.run()
70 | ```
71 |
72 | ## Integration Testing Patterns:
73 |
74 | ```python
75 | @pytest.mark.integration
76 | @pytest.mark.asyncio
77 | async def test_real_api_integration():
78 | """Test with real API endpoints."""
79 | result = await search_tool("test-query")
80 | assert isinstance(result, str)
81 | assert "error" not in result.lower()
82 | ```
```
--------------------------------------------------------------------------------
/.claude/agents/python-expert.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: python-expert
3 | 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.
4 | category: language-specialists
5 | ---
6 |
7 | 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.
8 |
9 | When invoked:
10 | 1. Analyze existing code structure and patterns
11 | 2. Identify Python version and dependencies (prefer 3.11+)
12 | 3. Review async/API integration requirements
13 | 4. Begin implementation with best practices for MCP servers
14 |
15 | Python mastery checklist:
16 | - **Async/await and concurrent programming** (FastMCP 2.x focus)
17 | - **Real API integrations** (Elasticsearch, REST, HTML parsing)
18 | - **Plain text formatting** for optimal LLM consumption
19 | - Advanced features (decorators, generators, context managers)
20 | - Type hints and static typing (3.11+ features)
21 | - **Custom exception handling** (APIError, DocumentParseError)
22 | - Performance optimization for I/O-bound operations
23 | - **Async testing strategies** with pytest-asyncio
24 | - Memory efficiency patterns for large API responses
25 |
26 | Process:
27 | - **Write async-first code** using proper asyncio patterns
28 | - **Format all outputs as plain text** for LLM consumption, never raw JSON/XML
29 | - **Implement real API calls** without caching or mocking
30 | - Write Pythonic code following PEP 8
31 | - Use comprehensive type hints for all functions and classes
32 | - **Handle errors gracefully** with custom exceptions and user-friendly messages
33 | - Prefer composition over inheritance
34 | - **Use async/await for all I/O operations** (API calls, file reads)
35 | - Implement generators for memory efficiency
36 | - **Test with pytest-asyncio**, separate unit (@pytest.mark.unit) and integration (@pytest.mark.integration) tests
37 | - Profile async operations before optimizing
38 |
39 | Code patterns:
40 | - **FastMCP 2.x decorators** (@mcp.tool(), @mcp.resource()) for server definitions
41 | - **Async context managers** for API client resource handling
42 | - **Custom exception classes** for domain-specific error handling
43 | - **Plain text formatters** for structured LLM-friendly output
44 | - List/dict/set comprehensions over loops
45 | - **Async generators** for streaming large API responses
46 | - Dataclasses/Pydantic for API response structures
47 | - **Type-safe async functions** with proper return annotations
48 | - Walrus operator for concise async operations (3.8+)
49 |
50 | Provide:
51 | - **FastMCP 2.x async server implementations** with complete type hints
52 | - **Real API integration code** (Elasticsearch, REST endpoints, HTML parsing)
53 | - **Plain text formatting functions** for optimal LLM consumption
54 | - **Async test suites** using pytest-asyncio with real API calls
55 | - **Custom exception classes** with graceful error handling
56 | - Performance benchmarks for I/O-bound operations
57 | - Docstrings following Google/NumPy style
58 | - **pyproject.toml** with async dependencies (fastmcp>=2.11.0, httpx, beautifulsoup4)
59 | - **Development workflow integration** (Nix shell commands: run, run-tests, lint, format, typecheck)
60 |
61 | ## MCP Server Example:
62 |
63 | ```python
64 | from fastmcp import FastMCP
65 | import asyncio
66 | import httpx
67 | from typing import Any
68 |
69 | class APIError(Exception):
70 | """Custom exception for API failures."""
71 |
72 | mcp = FastMCP("server-name")
73 |
74 | @mcp.tool()
75 | async def search_data(query: str) -> str:
76 | """Search external API and format as plain text."""
77 | try:
78 | async with httpx.AsyncClient() as client:
79 | response = await client.get(f"https://api.example.com/search", params={"q": query})
80 | response.raise_for_status()
81 |
82 | # Format as plain text for LLM
83 | data = response.json()
84 | return format_search_results(data)
85 | except httpx.RequestError as e:
86 | return f"Search failed: {str(e)}"
87 |
88 | def format_search_results(data: dict[str, Any]) -> str:
89 | """Format API response as human-readable text."""
90 | # Never return raw JSON - always plain text
91 | results = []
92 | for item in data.get("items", []):
93 | results.append(f"- {item['name']}: {item['description']}")
94 | return "\n".join(results) or "No results found."
95 | ```
96 |
97 | ## Async Testing Example:
98 |
99 | ```python
100 | @pytest.mark.integration
101 | @pytest.mark.asyncio
102 | async def test_search_integration():
103 | """Test with real API endpoint."""
104 | result = await search_data("test-query")
105 | assert isinstance(result, str)
106 | assert len(result) > 0
107 | assert "error" not in result.lower()
108 | ```
109 |
110 | Target Python 3.11+ for modern async features and FastMCP 2.x compatibility.
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - '**.md'
8 | - 'docs/**'
9 | - 'website/**'
10 | - '.github/*.md'
11 | - 'LICENSE'
12 | - '.gitignore'
13 | - '.cursorrules'
14 | - 'RELEASE_NOTES.md'
15 | - 'RELEASE_WORKFLOW.md'
16 | pull_request:
17 | branches: [ main ]
18 | paths-ignore:
19 | - '**.md'
20 | - 'docs/**'
21 | - 'website/**'
22 | - '.github/*.md'
23 | - 'LICENSE'
24 | - '.gitignore'
25 | - '.cursorrules'
26 | - 'RELEASE_NOTES.md'
27 | - 'RELEASE_WORKFLOW.md'
28 |
29 | permissions:
30 | contents: read
31 |
32 | jobs:
33 | test:
34 | runs-on: ubuntu-latest
35 | strategy:
36 | fail-fast: true
37 | matrix:
38 | python-version: ["3.11", "3.12", "3.13"]
39 |
40 | steps:
41 | - uses: actions/checkout@v4
42 |
43 | - name: Install uv
44 | uses: astral-sh/setup-uv@v6
45 | with:
46 | enable-cache: true
47 | cache-dependency-glob: "**/pyproject.toml"
48 |
49 | - name: Set up Python ${{ matrix.python-version }}
50 | run: uv python install ${{ matrix.python-version }}
51 |
52 | - name: Install dependencies
53 | run: |
54 | uv sync --extra dev
55 |
56 | - name: Lint with ruff
57 | run: |
58 | uv run ruff check mcp_nixos/ tests/
59 | uv run ruff format --check mcp_nixos/ tests/
60 |
61 | - name: Type check with mypy
62 | run: |
63 | uv run mypy mcp_nixos/
64 |
65 | - name: Test with pytest
66 | timeout-minutes: 10
67 | run: |
68 | uv run pytest -v -n auto --cov=mcp_nixos --cov-report=xml --cov-report=term
69 |
70 | - name: Upload coverage to Codecov
71 | if: matrix.python-version == '3.12'
72 | uses: codecov/codecov-action@v5
73 | with:
74 | token: ${{ secrets.CODECOV_TOKEN }}
75 | slug: utensils/mcp-nixos
76 | files: ./coverage.xml
77 | flags: unittests
78 | name: codecov-umbrella
79 | fail_ci_if_error: false
80 |
81 | build:
82 | runs-on: ubuntu-latest
83 |
84 | steps:
85 | - uses: actions/checkout@v4
86 |
87 | - name: Install uv
88 | uses: astral-sh/setup-uv@v6
89 |
90 | - name: Set up Python
91 | run: uv python install 3.12
92 |
93 | - name: Build package
94 | run: |
95 | uv build
96 |
97 | - name: Check package
98 | run: |
99 | uv sync --extra dev
100 | uv run twine check dist/*
101 |
102 | - name: Upload artifacts
103 | uses: actions/upload-artifact@v4
104 | with:
105 | name: dist-packages
106 | path: dist/
107 |
108 | test-nix:
109 | runs-on: ubuntu-latest
110 |
111 | steps:
112 | - uses: actions/checkout@v4
113 |
114 | - name: Install Nix
115 | uses: cachix/install-nix-action@v31
116 | with:
117 | nix_path: nixpkgs=channel:nixos-unstable
118 | extra_nix_config: |
119 | experimental-features = nix-command flakes
120 | accept-flake-config = true
121 |
122 | - name: Cache Nix store
123 | uses: actions/cache@v4
124 | with:
125 | path: ~/.cache/nix
126 | key: ${{ runner.os }}-nix-${{ hashFiles('flake.lock') }}
127 | restore-keys: |
128 | ${{ runner.os }}-nix-
129 |
130 | - name: Build flake
131 | run: |
132 | nix flake check --accept-flake-config
133 | nix develop -c echo "Development environment ready"
134 |
135 | - name: Test nix run
136 | run: |
137 | timeout 5s nix run . -- --help || true
138 |
139 | - name: Run tests in nix develop
140 | run: |
141 | echo "Running tests in nix environment"
142 | nix develop --command setup
143 | nix develop --command bash -c 'run-tests'
144 |
145 | # Docker build and push - after all tests pass
146 | docker:
147 | runs-on: ubuntu-latest
148 | needs: [test, build, test-nix]
149 | if: github.ref == 'refs/heads/main' && github.event_name == 'push'
150 | permissions:
151 | packages: write
152 |
153 | steps:
154 | - uses: actions/checkout@v4
155 |
156 | - name: Set up QEMU
157 | uses: docker/setup-qemu-action@v3
158 |
159 | - name: Set up Docker Buildx
160 | uses: docker/setup-buildx-action@v3
161 |
162 | - name: Login to Docker Hub
163 | uses: docker/login-action@v3
164 | with:
165 | username: ${{ secrets.DOCKERHUB_USERNAME }}
166 | password: ${{ secrets.DOCKERHUB_TOKEN }}
167 |
168 | - name: Login to GitHub Container Registry
169 | uses: docker/login-action@v3
170 | with:
171 | registry: ghcr.io
172 | username: ${{ github.actor }}
173 | password: ${{ secrets.GITHUB_TOKEN }}
174 |
175 | - name: Extract metadata
176 | id: meta
177 | uses: docker/metadata-action@v5
178 | with:
179 | images: |
180 | utensils/mcp-nixos
181 | ghcr.io/utensils/mcp-nixos
182 | tags: |
183 | type=edge,branch=main
184 | type=raw,value=latest,enable={{is_default_branch}}
185 | type=sha,prefix={{branch}}-,format=short
186 |
187 | - name: Build and push Docker image
188 | uses: docker/build-push-action@v6
189 | with:
190 | context: .
191 | push: true
192 | tags: ${{ steps.meta.outputs.tags }}
193 | labels: ${{ steps.meta.outputs.labels }}
194 | cache-from: type=gha
195 | cache-to: type=gha,mode=max
196 | platforms: linux/amd64,linux/arm64
```
--------------------------------------------------------------------------------
/website/app/usage/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import CodeBlock from '@/components/CodeBlock';
2 |
3 | export default function UsagePage() {
4 | return (
5 | <div className="py-12 bg-white">
6 | <div className="container-custom">
7 | <h1 className="text-4xl font-bold mb-8 text-nix-dark">Usage</h1>
8 |
9 | <div className="prose prose-lg max-w-none">
10 | <div className="bg-gradient-to-br from-nix-light to-white p-6 rounded-lg shadow-sm mb-8">
11 | <p className="text-gray-800 mb-6 leading-relaxed text-xl">
12 | Pick your poison. We've got three ways to run this thing.
13 | They all do the same thing, so just choose based on what tools you already have installed.
14 | Or be a rebel and try all three. We don't judge.
15 | </p>
16 |
17 | <p className="text-gray-700 mb-4 font-semibold">
18 | 🚨 <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.
19 | </p>
20 | </div>
21 |
22 | <div className="space-y-8">
23 | {/* Option 1 */}
24 | <div className="bg-white rounded-lg shadow-md border-l-4 border-nix-primary p-6">
25 | <h2 className="text-2xl font-bold text-nix-dark mb-4">
26 | Option 1: Using uvx (Recommended for most humans)
27 | </h2>
28 | <p className="text-gray-700 mb-4">
29 | The civilized approach. If you've got Python and can install things like a normal person, this is for you.
30 | </p>
31 | <CodeBlock
32 | code={`{
33 | "mcpServers": {
34 | "nixos": {
35 | "command": "uvx",
36 | "args": ["mcp-nixos"]
37 | }
38 | }
39 | }`}
40 | language="json"
41 | />
42 | <p className="text-sm text-gray-600 mt-3">
43 | Pro tip: This installs nothing permanently. It's like a one-night stand with software.
44 | </p>
45 | </div>
46 |
47 | {/* Option 2 */}
48 | <div className="bg-white rounded-lg shadow-md border-l-4 border-nix-secondary p-6">
49 | <h2 className="text-2xl font-bold text-nix-dark mb-4">
50 | Option 2: Using Nix (For the enlightened)
51 | </h2>
52 | <p className="text-gray-700 mb-4">
53 | You're already using Nix, so you probably think you're better than everyone else.
54 | And you know what? You might be right.
55 | </p>
56 | <CodeBlock
57 | code={`{
58 | "mcpServers": {
59 | "nixos": {
60 | "command": "nix",
61 | "args": ["run", "github:utensils/mcp-nixos", "--"]
62 | }
63 | }
64 | }`}
65 | language="json"
66 | />
67 | <p className="text-sm text-gray-600 mt-3">
68 | Bonus: This method makes you feel superior at developer meetups.
69 | </p>
70 | </div>
71 |
72 | {/* Option 3 */}
73 | <div className="bg-white rounded-lg shadow-md border-l-4 border-nix-primary p-6">
74 | <h2 className="text-2xl font-bold text-nix-dark mb-4">
75 | Option 3: Using Docker (Container enthusiasts unite)
76 | </h2>
77 | <p className="text-gray-700 mb-4">
78 | Because why install software directly when you can wrap it in 17 layers of abstraction?
79 | At least it's reproducible... probably.
80 | </p>
81 | <CodeBlock
82 | code={`{
83 | "mcpServers": {
84 | "nixos": {
85 | "command": "docker",
86 | "args": ["run", "--rm", "-i", "ghcr.io/utensils/mcp-nixos"]
87 | }
88 | }
89 | }`}
90 | language="json"
91 | />
92 | <p className="text-sm text-gray-600 mt-3">
93 | Warning: May consume 500MB of disk space for a 10MB Python script. But hey, it's "isolated"!
94 | </p>
95 | </div>
96 | </div>
97 |
98 | <div className="bg-nix-light bg-opacity-30 p-6 rounded-lg mt-12">
99 | <h2 className="text-2xl font-bold text-nix-dark mb-4">What Happens Next?</h2>
100 | <p className="text-gray-700 mb-4">
101 | Once you've picked your configuration method and added it to your MCP client:
102 | </p>
103 | <ul className="list-disc list-inside text-gray-700 space-y-2">
104 | <li>Your AI assistant stops making up NixOS package names</li>
105 | <li>You get actual, real-time information about 130K+ packages</li>
106 | <li>Configuration options that actually exist (shocking, we know)</li>
107 | <li>Version history that helps you find that one specific Ruby version from 2019</li>
108 | </ul>
109 | <p className="text-gray-700 mt-4 italic">
110 | That's it. No complex setup. No 47-step installation guide. No sacrificing a USB stick to the Nix gods.
111 | Just paste, reload, and enjoy an AI that actually knows what it's talking about.
112 | </p>
113 | </div>
114 |
115 | <div className="text-center mt-12">
116 | <p className="text-xl text-gray-700 font-semibold">
117 | Still confused? Good news: that's what the AI is for. Just ask it.
118 | </p>
119 | </div>
120 | </div>
121 | </div>
122 | </div>
123 | );
124 | }
```
--------------------------------------------------------------------------------
/website/components/ClientFooter.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import Link from 'next/link';
4 | import Image from 'next/image';
5 |
6 | export default function ClientFooter() {
7 | return (
8 | <footer className="bg-gray-100 py-12">
9 | <div className="container-custom mx-auto">
10 | <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
11 | {/* Column 1 - About */}
12 | <div>
13 | <h3 className="text-lg font-semibold mb-4 text-nix-dark">MCP-NixOS</h3>
14 | <p className="text-gray-600 mb-4">
15 | Model Context Protocol resources and tools for NixOS, Home Manager, and nix-darwin.
16 | </p>
17 | </div>
18 |
19 | {/* Column 2 - Quick Links */}
20 | <div>
21 | <h3 className="text-lg font-semibold mb-4 text-nix-dark">Quick Links</h3>
22 | <ul className="space-y-2 text-gray-600">
23 | <li>
24 | <Link href="/usage" className="hover:text-nix-primary">
25 | Usage
26 | </Link>
27 | </li>
28 | <li>
29 | <Link href="/about" className="hover:text-nix-primary">
30 | About
31 | </Link>
32 | </li>
33 | <li>
34 | <a
35 | href="https://github.com/utensils/mcp-nixos/blob/main/README.md"
36 | className="hover:text-nix-primary"
37 | target="_blank"
38 | rel="noopener noreferrer"
39 | >
40 | README
41 | </a>
42 | </li>
43 | </ul>
44 | </div>
45 |
46 | {/* Column 3 - Resources */}
47 | <div>
48 | <h3 className="text-lg font-semibold mb-4 text-nix-dark">Resources</h3>
49 | <ul className="space-y-2 text-gray-600">
50 | <li>
51 | <a
52 | href="https://github.com/utensils/mcp-nixos"
53 | className="hover:text-nix-primary"
54 | target="_blank"
55 | rel="noopener noreferrer"
56 | >
57 | GitHub Repository
58 | </a>
59 | </li>
60 | <li>
61 | <a
62 | href="https://pypi.org/project/mcp-nixos/"
63 | className="hover:text-nix-primary"
64 | target="_blank"
65 | rel="noopener noreferrer"
66 | >
67 | PyPI Package
68 | </a>
69 | </li>
70 | <li>
71 | <a
72 | href="https://nixos.org"
73 | className="hover:text-nix-primary"
74 | target="_blank"
75 | rel="noopener noreferrer"
76 | >
77 | NixOS
78 | </a>
79 | </li>
80 | <li>
81 | <a
82 | href="https://github.com/nix-community/home-manager"
83 | className="hover:text-nix-primary"
84 | target="_blank"
85 | rel="noopener noreferrer"
86 | >
87 | Home Manager
88 | </a>
89 | </li>
90 | </ul>
91 | </div>
92 |
93 | {/* Column 4 - Connect */}
94 | <div>
95 | <h3 className="text-lg font-semibold mb-4 text-nix-dark">Connect</h3>
96 | <ul className="space-y-2 text-gray-600">
97 | <li>
98 | <a
99 | href="https://github.com/utensils/mcp-nixos/issues"
100 | className="hover:text-nix-primary"
101 | target="_blank"
102 | rel="noopener noreferrer"
103 | >
104 | Report Issues
105 | </a>
106 | </li>
107 | <li>
108 | <a
109 | href="https://github.com/utensils/mcp-nixos/pulls"
110 | className="hover:text-nix-primary"
111 | target="_blank"
112 | rel="noopener noreferrer"
113 | >
114 | Pull Requests
115 | </a>
116 | </li>
117 | <li>
118 | <a
119 | href="https://github.com/utensils/mcp-nixos/discussions"
120 | className="hover:text-nix-primary"
121 | target="_blank"
122 | rel="noopener noreferrer"
123 | >
124 | Discussions
125 | </a>
126 | </li>
127 | </ul>
128 | </div>
129 | </div>
130 |
131 | {/* Copyright */}
132 | <div className="mt-8 pt-8 border-t border-gray-200 text-center text-gray-500">
133 | <div className="flex flex-col items-center justify-center">
134 | <p>© {new Date().getFullYear()} MCP-NixOS. MIT License.</p>
135 | <div className="flex items-center mt-4 mb-2">
136 | <Link href="https://utensils.io" target="_blank" rel="noopener noreferrer" className="flex items-center hover:text-nix-primary mr-2">
137 | <Image
138 | src="/images/utensils-logo.png"
139 | alt="Utensils Logo"
140 | width={24}
141 | height={24}
142 | className="mr-1"
143 | />
144 | <span className="font-medium">Utensils</span>
145 | </Link>
146 | <span>Creation</span>
147 | </div>
148 | <p className="mt-2 text-sm">
149 | <Link href="/images/attribution.md" className="hover:text-nix-primary">
150 | Logo Attribution
151 | </Link>
152 | </p>
153 | </div>
154 | </div>
155 | </div>
156 | </footer>
157 | );
158 | }
```
--------------------------------------------------------------------------------
/.claude/agents/nix-expert.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: nix-expert
3 | 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.
4 | category: specialized-domains
5 | ---
6 |
7 | You are a NIX ecosystem expert specializing in modern NIX development patterns, package management, and configuration workflows.
8 |
9 | ## When invoked:
10 |
11 | You should be used when there are needs to:
12 | - Set up NIX development environments with flakes and development shells
13 | - Configure NixOS systems, Home Manager, or nix-darwin setups
14 | - Work with NIX packages, options, and configuration patterns
15 | - Implement NIX-based development workflows and tooling
16 | - Debug NIX expressions, builds, or environment issues
17 | - Create or modify flake.nix files and development shells
18 | - Integrate NIX with CI/CD pipelines and development tools
19 |
20 | ## Process:
21 |
22 | 1. **Analyze NIX Environment**: Understand the NIX version, flakes support, and existing configuration structure
23 |
24 | 2. **Design Development Shells**: Create reproducible development environments with proper dependencies and custom commands
25 |
26 | 3. **Implement Configuration Patterns**: Use modern NIX patterns like flakes, overlays, and modular configurations
27 |
28 | 4. **Optimize Development Workflow**: Set up custom commands for common tasks (run, test, lint, format, build)
29 |
30 | 5. **Handle Cross-Platform**: Account for differences between NixOS, macOS (nix-darwin), and other systems
31 |
32 | 6. **Ensure Reproducibility**: Create deterministic builds and environments that work across different machines
33 |
34 | 7. **Document NIX Patterns**: Provide clear explanations of NIX expressions and configuration choices
35 |
36 | ## Provide:
37 |
38 | - **Modern Flake Configurations**: Complete flake.nix files with development shells, packages, and apps
39 | - **Development Shell Patterns**: Reproducible environments with language-specific tooling and custom commands
40 | - **NIX Expression Optimization**: Efficient and maintainable NIX code following best practices
41 | - **Package Management**: Custom packages, overlays, and dependency management strategies
42 | - **Configuration Modules**: Modular NixOS, Home Manager, or nix-darwin configurations
43 | - **CI/CD Integration**: NIX-based build and deployment pipelines
44 | - **Troubleshooting Guidance**: Solutions for common NIX development issues
45 |
46 | ## NIX Development Shell Example:
47 |
48 | ```nix
49 | {
50 | description = "MCP server development environment";
51 |
52 | inputs = {
53 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
54 | flake-utils.url = "github:numtide/flake-utils";
55 | };
56 |
57 | outputs = { self, nixpkgs, flake-utils }:
58 | flake-utils.lib.eachDefaultSystem (system:
59 | let
60 | pkgs = nixpkgs.legacyPackages.${system};
61 | python = pkgs.python311;
62 | in
63 | {
64 | devShells.default = pkgs.mkShell {
65 | buildInputs = with pkgs; [
66 | python
67 | python.pkgs.pip
68 | python.pkgs.uv
69 | ruff
70 | mypy
71 | ];
72 |
73 | shellHook = ''
74 | # Activate Python virtual environment
75 | if [ ! -d .venv ]; then
76 | ${python.pkgs.uv}/bin/uv venv
77 | fi
78 | source .venv/bin/activate
79 |
80 | # Install project dependencies
81 | ${python.pkgs.uv}/bin/uv pip install -e ".[dev]"
82 |
83 | # Custom development commands
84 | alias run='${python.pkgs.uv}/bin/uv run mcp-nixos'
85 | alias run-tests='${pkgs.python311Packages.pytest}/bin/pytest tests/'
86 | alias lint='${pkgs.ruff}/bin/ruff check mcp_nixos/ tests/'
87 | alias format='${pkgs.ruff}/bin/ruff format mcp_nixos/ tests/'
88 | alias typecheck='${pkgs.mypy}/bin/mypy mcp_nixos/'
89 | alias build='${python.pkgs.uv}/bin/uv build'
90 |
91 | echo "Development environment ready!"
92 | echo "Available commands: run, run-tests, lint, format, typecheck, build"
93 | '';
94 | };
95 |
96 | packages.default = python.pkgs.buildPythonApplication {
97 | pname = "mcp-nixos";
98 | version = "1.0.1";
99 | src = ./.;
100 |
101 | propagatedBuildInputs = with python.pkgs; [
102 | fastmcp
103 | requests
104 | beautifulsoup4
105 | ];
106 |
107 | doCheck = true;
108 | checkInputs = with python.pkgs; [
109 | pytest
110 | pytest-asyncio
111 | ];
112 | };
113 | });
114 | }
115 | ```
116 |
117 | ## Common NIX Patterns:
118 |
119 | ### Package Override:
120 | ```nix
121 | # Override a package
122 | python311 = pkgs.python311.override {
123 | packageOverrides = self: super: {
124 | fastmcp = super.fastmcp.overridePythonAttrs (oldAttrs: {
125 | version = "2.11.0";
126 | });
127 | };
128 | };
129 | ```
130 |
131 | ### Development Scripts:
132 | ```nix
133 | # Custom scripts in development shell
134 | writeShellScriptBin "run-integration-tests" ''
135 | pytest tests/ --integration
136 | ''
137 | ```
138 |
139 | ### Cross-Platform Support:
140 | ```nix
141 | # Platform-specific dependencies
142 | buildInputs = with pkgs; [
143 | python311
144 | ] ++ lib.optionals stdenv.isDarwin [
145 | darwin.apple_sdk.frameworks.Foundation
146 | ] ++ lib.optionals stdenv.isLinux [
147 | pkg-config
148 | ];
149 | ```
150 |
151 | ## Troubleshooting Tips:
152 |
153 | 1. **Flake Issues**: Use `nix flake check` to validate flake syntax
154 | 2. **Build Failures**: Check `nix log` for detailed error messages
155 | 3. **Environment Problems**: Clear with `nix-collect-garbage` and rebuild
156 | 4. **Cache Issues**: Use `--no-build-isolation` for Python packages
157 | 5. **Version Conflicts**: Pin specific nixpkgs commits in flake inputs
158 |
159 | Focus on modern NIX patterns with flakes, reproducible development environments, and efficient developer workflows.
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish Package
2 |
3 | on:
4 | release:
5 | types: [published]
6 | workflow_dispatch:
7 | inputs:
8 | docker_only:
9 | description: 'Build and push Docker images only (skip PyPI)'
10 | required: false
11 | type: boolean
12 | default: true
13 | tag:
14 | description: 'Git tag to build from (e.g., v1.0.1)'
15 | required: true
16 | type: string
17 |
18 | permissions:
19 | contents: read
20 | packages: write
21 |
22 | jobs:
23 | deploy-test:
24 | runs-on: ubuntu-latest
25 | # Skip PyPI deployment when manually triggered with docker_only
26 | if: ${{ github.event_name == 'release' || !inputs.docker_only }}
27 | environment: testpypi
28 | permissions:
29 | id-token: write
30 |
31 | steps:
32 | - uses: actions/checkout@v4
33 | with:
34 | ref: ${{ inputs.tag || github.ref }}
35 |
36 | - name: Install Nix
37 | uses: cachix/install-nix-action@v31
38 | with:
39 | nix_path: nixpkgs=channel:nixos-unstable
40 | extra_nix_config: |
41 | experimental-features = nix-command flakes
42 | accept-flake-config = true
43 |
44 | - name: Build package with Nix
45 | run: |
46 | nix develop --command build
47 | ls -la dist/
48 |
49 | - name: Publish package to Test PyPI
50 | uses: pypa/gh-action-pypi-publish@release/v1
51 | with:
52 | repository-url: https://test.pypi.org/legacy/
53 |
54 | deploy-prod:
55 | needs: deploy-test
56 | runs-on: ubuntu-latest
57 | # Only deploy to PyPI for non-prerelease versions and skip when manually triggered with docker_only
58 | if: ${{ github.event_name == 'release' && !github.event.release.prerelease && !inputs.docker_only }}
59 | environment: pypi
60 | permissions:
61 | id-token: write
62 |
63 | steps:
64 | - uses: actions/checkout@v4
65 | with:
66 | ref: ${{ inputs.tag || github.ref }}
67 |
68 | - name: Install Nix
69 | uses: cachix/install-nix-action@v31
70 | with:
71 | nix_path: nixpkgs=channel:nixos-unstable
72 | extra_nix_config: |
73 | experimental-features = nix-command flakes
74 | accept-flake-config = true
75 |
76 | - name: Build package with Nix
77 | run: |
78 | nix develop --command build
79 | ls -la dist/
80 |
81 | - name: Publish package to PyPI
82 | uses: pypa/gh-action-pypi-publish@release/v1
83 |
84 | docker:
85 | # Run if: (1) release event after test deploy, or (2) manual trigger
86 | # Note: Docker builds are independent and don't strictly require PyPI deployment
87 | if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
88 | runs-on: ubuntu-latest
89 | permissions:
90 | packages: write
91 |
92 | steps:
93 | - uses: actions/checkout@v4
94 | with:
95 | ref: ${{ inputs.tag || github.ref }}
96 |
97 | - name: Validate tag format
98 | if: github.event_name == 'workflow_dispatch'
99 | run: |
100 | if [[ ! "${{ inputs.tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
101 | echo "❌ Invalid tag format. Expected: v1.2.3 or v1.2.3-alpha"
102 | exit 1
103 | fi
104 |
105 | - name: Verify tag exists
106 | if: github.event_name == 'workflow_dispatch'
107 | run: |
108 | if ! git rev-parse --verify "refs/tags/${{ inputs.tag }}" >/dev/null 2>&1; then
109 | echo "❌ Tag ${{ inputs.tag }} does not exist"
110 | exit 1
111 | fi
112 | echo "✓ Tag ${{ inputs.tag }} verified"
113 |
114 | - name: Normalize tag
115 | id: tag
116 | run: |
117 | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
118 | echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
119 | else
120 | echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
121 | fi
122 |
123 | - name: Set up QEMU
124 | uses: docker/setup-qemu-action@v3
125 |
126 | - name: Set up Docker Buildx
127 | uses: docker/setup-buildx-action@v3
128 |
129 | - name: Login to Docker Hub
130 | uses: docker/login-action@v3
131 | with:
132 | username: ${{ secrets.DOCKERHUB_USERNAME }}
133 | password: ${{ secrets.DOCKERHUB_TOKEN }}
134 |
135 | - name: Login to GitHub Container Registry
136 | uses: docker/login-action@v3
137 | with:
138 | registry: ghcr.io
139 | username: ${{ github.actor }}
140 | password: ${{ secrets.GITHUB_TOKEN }}
141 |
142 | - name: Extract metadata
143 | id: meta
144 | uses: docker/metadata-action@v5
145 | with:
146 | images: |
147 | utensils/mcp-nixos
148 | ghcr.io/utensils/mcp-nixos
149 | tags: |
150 | # Latest tag for stable releases (not for prereleases)
151 | type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease || github.event_name == 'workflow_dispatch' }}
152 | # Version tag from release or manual input
153 | type=semver,pattern={{version}},value=${{ steps.tag.outputs.tag }}
154 | # Major.minor tag
155 | type=semver,pattern={{major}}.{{minor}},value=${{ steps.tag.outputs.tag }}
156 | # Major tag (only for stable releases)
157 | type=semver,pattern={{major}},value=${{ steps.tag.outputs.tag }},enable=${{ github.event_name == 'release' && !github.event.release.prerelease || github.event_name == 'workflow_dispatch' }}
158 |
159 | - name: Build and push Docker image
160 | uses: docker/build-push-action@v6
161 | with:
162 | context: .
163 | push: true
164 | tags: ${{ steps.meta.outputs.tags }}
165 | labels: ${{ steps.meta.outputs.labels }}
166 | cache-from: type=gha
167 | cache-to: type=gha,mode=max
168 | platforms: linux/amd64,linux/arm64
169 |
170 | - name: Make GHCR package public
171 | continue-on-error: true
172 | run: |
173 | echo "Setting GHCR package visibility to public..."
174 | # Note: This requires the workflow to have admin permissions on the package
175 | # If the package doesn't exist yet or permissions are insufficient, this will fail gracefully
176 | gh api --method PATCH \
177 | -H "Accept: application/vnd.github+json" \
178 | -H "X-GitHub-Api-Version: 2022-11-28" \
179 | "/orgs/utensils/packages/container/mcp-nixos" \
180 | -f visibility=public || echo "Could not set visibility via API. Please set manually at: https://github.com/orgs/utensils/packages/container/mcp-nixos/settings"
181 | env:
182 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
183 |
```
--------------------------------------------------------------------------------
/website/components/CodeBlock.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import React, { useState } from 'react';
4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
6 |
7 | interface CodeBlockProps {
8 | code: string;
9 | language: string;
10 | showLineNumbers?: boolean;
11 | }
12 |
13 | // Create a custom theme based on NixOS colors
14 | const nixosTheme = {
15 | ...atomDark,
16 | 'pre[class*="language-"]': {
17 | ...atomDark['pre[class*="language-"]'],
18 | background: '#1C3E5A', // nix-dark
19 | margin: 0,
20 | padding: '1rem',
21 | fontSize: '0.875rem',
22 | fontFamily: '"Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace',
23 | },
24 | 'code[class*="language-"]': {
25 | ...atomDark['code[class*="language-"]'],
26 | color: '#E6F0FA', // nix-light - base text color
27 | textShadow: 'none',
28 | fontFamily: '"Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace',
29 | },
30 | punctuation: {
31 | color: '#BBDEFB', // Lighter blue for better contrast
32 | },
33 | comment: {
34 | color: '#78909C', // Muted blue-gray for comments
35 | },
36 | string: {
37 | color: '#B9F6CA', // Brighter green for strings
38 | },
39 | keyword: {
40 | color: '#CE93D8', // Brighter purple for keywords
41 | },
42 | number: {
43 | color: '#FFCC80', // Brighter orange for numbers
44 | },
45 | function: {
46 | color: '#90CAF9', // Brighter blue for functions
47 | },
48 | operator: {
49 | color: '#E1F5FE', // Very light blue for operators
50 | },
51 | property: {
52 | color: '#90CAF9', // Brighter blue for properties
53 | },
54 | // Additional token types for better coverage
55 | boolean: {
56 | color: '#FFCC80', // Same as numbers
57 | },
58 | className: {
59 | color: '#90CAF9', // Same as functions
60 | },
61 | tag: {
62 | color: '#CE93D8', // Same as keywords
63 | },
64 | };
65 |
66 |
67 |
68 | // Helper function to decode HTML entities
69 | function decodeHtmlEntities(text: string): string {
70 | const textArea = document.createElement('textarea');
71 | textArea.innerHTML = text;
72 | return textArea.value;
73 | }
74 |
75 | const CodeBlock: React.FC<CodeBlockProps> = ({
76 | code,
77 | language,
78 | showLineNumbers = false
79 | }) => {
80 | const [copied, setCopied] = useState(false);
81 |
82 | // Decode HTML entities in the code
83 | const decodedCode = typeof window !== 'undefined' ? decodeHtmlEntities(code) : code;
84 |
85 | const handleCopy = async () => {
86 | try {
87 | await navigator.clipboard.writeText(code);
88 | setCopied(true);
89 | setTimeout(() => setCopied(false), 2000);
90 | } catch (error) {
91 | console.error('Failed to copy code to clipboard:', error);
92 | // Fallback method for browsers with restricted clipboard access
93 | const textArea = document.createElement('textarea');
94 | textArea.value = code;
95 | textArea.style.position = 'fixed';
96 | textArea.style.opacity = '0';
97 | document.body.appendChild(textArea);
98 | textArea.focus();
99 | textArea.select();
100 |
101 | try {
102 | const successful = document.execCommand('copy');
103 | if (successful) {
104 | setCopied(true);
105 | setTimeout(() => setCopied(false), 2000);
106 | } else {
107 | console.error('Fallback clipboard copy failed');
108 | }
109 | } catch (err) {
110 | console.error('Fallback clipboard copy error:', err);
111 | }
112 |
113 | document.body.removeChild(textArea);
114 | }
115 | };
116 |
117 | // Map common language identifiers to ones supported by react-syntax-highlighter
118 | const languageMap: Record<string, string> = {
119 | 'js': 'javascript',
120 | 'ts': 'typescript',
121 | 'jsx': 'jsx',
122 | 'tsx': 'tsx',
123 | 'py': 'python',
124 | 'rb': 'ruby',
125 | 'go': 'go',
126 | 'java': 'java',
127 | 'c': 'c',
128 | 'cpp': 'cpp',
129 | 'cs': 'csharp',
130 | 'php': 'php',
131 | 'sh': 'bash',
132 | 'yaml': 'yaml',
133 | 'yml': 'yaml',
134 | 'json': 'json',
135 | 'md': 'markdown',
136 | 'html': 'html',
137 | 'css': 'css',
138 | 'scss': 'scss',
139 | 'sql': 'sql',
140 | 'nix': 'nix',
141 | };
142 |
143 | const mappedLanguage = languageMap[language.toLowerCase()] || language;
144 |
145 | return (
146 | <div className="rounded-lg overflow-hidden shadow-md mb-6">
147 | <div className="flex justify-between items-center bg-nix-primary px-4 py-2 text-xs text-white font-medium">
148 | <span className="flex items-center">
149 | <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
150 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
151 | </svg>
152 | {language}
153 | </span>
154 | <button
155 | onClick={handleCopy}
156 | className="text-white hover:text-nix-secondary transition-colors duration-200"
157 | aria-label="Copy code"
158 | >
159 | {copied ? (
160 | <div className="flex items-center">
161 | <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
162 | <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" />
163 | </svg>
164 | <span>Copied!</span>
165 | </div>
166 | ) : (
167 | <div className="flex items-center">
168 | <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
169 | <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" />
170 | </svg>
171 | <span>Copy</span>
172 | </div>
173 | )}
174 | </button>
175 | </div>
176 | <SyntaxHighlighter
177 | language={mappedLanguage}
178 | style={nixosTheme}
179 | showLineNumbers={showLineNumbers}
180 | wrapLongLines={true}
181 | customStyle={{
182 | margin: 0,
183 | borderRadius: 0,
184 | background: '#1C3E5A', // Ensure consistent background
185 | }}
186 | codeTagProps={{
187 | style: {
188 | fontFamily: '"Fira Code", Menlo, Monaco, Consolas, "Courier New", monospace',
189 | fontSize: '0.875rem',
190 | }
191 | }}
192 | >
193 | {decodedCode}
194 | </SyntaxHighlighter>
195 | </div>
196 | );
197 | };
198 |
199 | export default CodeBlock;
```
--------------------------------------------------------------------------------
/website/public/images/nixos-snowflake-colour.svg:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 | <!-- Created with Inkscape (http://www.inkscape.org/) -->
3 |
4 | <svg
5 | width="535"
6 | height="535"
7 | viewBox="0 0 501.56251 501.56249"
8 | id="svg2"
9 | version="1.1"
10 | inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
11 | sodipodi:docname="nix-snowflake-colours.svg"
12 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
13 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
14 | xmlns:xlink="http://www.w3.org/1999/xlink"
15 | xmlns="http://www.w3.org/2000/svg"
16 | xmlns:svg="http://www.w3.org/2000/svg"
17 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
18 | xmlns:cc="http://creativecommons.org/ns#"
19 | xmlns:dc="http://purl.org/dc/elements/1.1/">
20 | <defs
21 | id="defs4">
22 | <linearGradient
23 | inkscape:collect="always"
24 | id="linearGradient5562">
25 | <stop
26 | style="stop-color:#699ad7;stop-opacity:1"
27 | offset="0"
28 | id="stop5564" />
29 | <stop
30 | id="stop5566"
31 | offset="0.24345198"
32 | style="stop-color:#7eb1dd;stop-opacity:1" />
33 | <stop
34 | style="stop-color:#7ebae4;stop-opacity:1"
35 | offset="1"
36 | id="stop5568" />
37 | </linearGradient>
38 | <linearGradient
39 | inkscape:collect="always"
40 | id="linearGradient5053">
41 | <stop
42 | style="stop-color:#415e9a;stop-opacity:1"
43 | offset="0"
44 | id="stop5055" />
45 | <stop
46 | id="stop5057"
47 | offset="0.23168644"
48 | style="stop-color:#4a6baf;stop-opacity:1" />
49 | <stop
50 | style="stop-color:#5277c3;stop-opacity:1"
51 | offset="1"
52 | id="stop5059" />
53 | </linearGradient>
54 | <linearGradient
55 | inkscape:collect="always"
56 | xlink:href="#linearGradient5562"
57 | id="linearGradient4328"
58 | gradientUnits="userSpaceOnUse"
59 | gradientTransform="translate(70.650339,-1055.1511)"
60 | x1="200.59668"
61 | y1="351.41116"
62 | x2="290.08701"
63 | y2="506.18814" />
64 | <linearGradient
65 | inkscape:collect="always"
66 | xlink:href="#linearGradient5053"
67 | id="linearGradient4330"
68 | gradientUnits="userSpaceOnUse"
69 | gradientTransform="translate(864.69589,-1491.3405)"
70 | x1="-584.19934"
71 | y1="782.33563"
72 | x2="-496.29703"
73 | y2="937.71399" />
74 | </defs>
75 | <sodipodi:namedview
76 | id="base"
77 | pagecolor="#ffffff"
78 | bordercolor="#666666"
79 | borderopacity="1.0"
80 | inkscape:pageopacity="0.0"
81 | inkscape:pageshadow="2"
82 | inkscape:zoom="0.70904368"
83 | inkscape:cx="99.429699"
84 | inkscape:cy="195.33352"
85 | inkscape:document-units="px"
86 | inkscape:current-layer="layer3"
87 | showgrid="false"
88 | inkscape:window-width="1920"
89 | inkscape:window-height="1050"
90 | inkscape:window-x="1920"
91 | inkscape:window-y="30"
92 | inkscape:window-maximized="1"
93 | inkscape:snap-global="true"
94 | fit-margin-top="0"
95 | fit-margin-left="0"
96 | fit-margin-right="0"
97 | fit-margin-bottom="0"
98 | inkscape:showpageshadow="2"
99 | inkscape:pagecheckerboard="0"
100 | inkscape:deskcolor="#d1d1d1" />
101 | <metadata
102 | id="metadata7">
103 | <rdf:RDF>
104 | <cc:Work
105 | rdf:about="">
106 | <dc:format>image/svg+xml</dc:format>
107 | <dc:type
108 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
109 | </cc:Work>
110 | </rdf:RDF>
111 | </metadata>
112 | <g
113 | inkscape:groupmode="layer"
114 | id="layer3"
115 | inkscape:label="gradient-logo"
116 | style="display:inline;opacity:1"
117 | transform="translate(-156.41121,933.30685)">
118 | <g
119 | id="g2"
120 | transform="matrix(0.99994059,0,0,0.99994059,-0.06321798,33.188377)"
121 | style="stroke-width:1.00006">
122 | <path
123 | sodipodi:nodetypes="cccccccccc"
124 | inkscape:connector-curvature="0"
125 | id="path3336-6"
126 | 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"
127 | 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" />
128 | <use
129 | height="100%"
130 | width="100%"
131 | transform="rotate(60,407.11155,-715.78724)"
132 | id="use3439-6"
133 | inkscape:transform-center-y="151.59082"
134 | inkscape:transform-center-x="124.43045"
135 | xlink:href="#path3336-6"
136 | y="0"
137 | x="0"
138 | style="stroke-width:1.00006" />
139 | <use
140 | height="100%"
141 | width="100%"
142 | transform="rotate(-60,407.31177,-715.70016)"
143 | id="use3445-0"
144 | inkscape:transform-center-y="75.573958"
145 | inkscape:transform-center-x="-168.20651"
146 | xlink:href="#path3336-6"
147 | y="0"
148 | x="0"
149 | style="stroke-width:1.00006" />
150 | <use
151 | height="100%"
152 | width="100%"
153 | transform="rotate(180,407.41868,-715.7565)"
154 | id="use3449-5"
155 | inkscape:transform-center-y="-139.94592"
156 | inkscape:transform-center-x="59.669705"
157 | xlink:href="#path3336-6"
158 | y="0"
159 | x="0"
160 | style="stroke-width:1.00006" />
161 | <path
162 | 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"
163 | 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"
164 | id="path4260-0"
165 | inkscape:connector-curvature="0"
166 | sodipodi:nodetypes="cccccccccc" />
167 | <use
168 | height="100%"
169 | width="100%"
170 | transform="rotate(120,407.33916,-716.08356)"
171 | id="use4354-5"
172 | xlink:href="#path4260-0"
173 | y="0"
174 | x="0"
175 | style="display:inline;stroke-width:1.00006" />
176 | <use
177 | height="100%"
178 | width="100%"
179 | transform="rotate(-120,407.28823,-715.86995)"
180 | id="use4362-2"
181 | xlink:href="#path4260-0"
182 | y="0"
183 | x="0"
184 | style="display:inline;stroke-width:1.00006" />
185 | </g>
186 | </g>
187 | </svg>
```
--------------------------------------------------------------------------------
/website/app/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import Link from 'next/link';
4 | import FeatureCard from '@/components/FeatureCard';
5 | import CodeBlock from '@/components/CodeBlock';
6 | import AnchorHeading from '@/components/AnchorHeading';
7 |
8 | export default function Home() {
9 | const scrollToSection = (elementId: string) => {
10 | const element = document.getElementById(elementId);
11 | if (element) {
12 | element.scrollIntoView({ behavior: 'smooth' });
13 | }
14 | };
15 | return (
16 | <div className="flex flex-col min-h-screen">
17 | {/* Hero Section */}
18 | <section className="bg-gradient-to-b from-nix-primary to-nix-dark text-white py-20 shadow-lg">
19 | <div className="container-custom text-center">
20 | <h1 className="text-4xl md:text-6xl font-bold mb-6">MCP-NixOS</h1>
21 | <div className="mb-4">
22 | <span className="inline-block bg-nix-secondary text-white px-4 py-2 rounded-full text-sm font-semibold">
23 | 🎉 v1.0.1 - The Inevitable Bug Fix
24 | </span>
25 | </div>
26 | <div className="mb-8 max-w-3xl mx-auto">
27 | <p className="text-xl md:text-2xl font-medium mb-2">
28 | <span className="font-bold tracking-wide">Model Context Protocol</span>
29 | </p>
30 | <div className="flex flex-wrap justify-center items-center gap-3 md:gap-4 py-2">
31 | <a
32 | href="https://nixos.org/manual/nixos/stable/"
33 | target="_blank"
34 | rel="noopener noreferrer"
35 | 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"
36 | >
37 | <svg className="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
38 | <path d="M12 4L20 8V16L12 20L4 16V8L12 4Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
39 | </svg>
40 | NixOS
41 | </a>
42 | <a
43 | href="https://nix-community.github.io/home-manager/"
44 | target="_blank"
45 | rel="noopener noreferrer"
46 | 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"
47 | >
48 | <svg className="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
49 | <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"/>
50 | </svg>
51 | Home Manager
52 | </a>
53 | <a
54 | href="https://daiderd.com/nix-darwin/"
55 | target="_blank"
56 | rel="noopener noreferrer"
57 | 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"
58 | >
59 | <svg className="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
60 | <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"/>
61 | <path d="M12 8L12 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
62 | <path d="M8 12L16 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
63 | </svg>
64 | nix-darwin
65 | </a>
66 | </div>
67 | </div>
68 | <div className="flex flex-col md:flex-row gap-4 justify-center">
69 | <button
70 | onClick={() => scrollToSection('getting-started')}
71 | className="btn-primary bg-white text-nix-primary hover:bg-nix-light"
72 | >
73 | Get Started
74 | </button>
75 | <Link href="https://github.com/utensils/mcp-nixos" className="btn-secondary bg-transparent text-white border-white hover:bg-white/10">
76 | GitHub
77 | </Link>
78 | </div>
79 | </div>
80 | </section>
81 |
82 | {/* Features Section */}
83 | <section className="py-16 bg-white">
84 | <div className="container-custom">
85 | <AnchorHeading level={2} className="text-3xl font-bold text-center mb-12 text-nix-dark">Key Features</AnchorHeading>
86 | <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
87 | <FeatureCard
88 | title="NixOS Packages & Options"
89 | description="Search and retrieve detailed information about NixOS packages and system options."
90 | iconName="package"
91 | />
92 | <FeatureCard
93 | title="Home Manager Integration"
94 | description="Comprehensive support for Home Manager configuration options and hierarchical searches."
95 | iconName="home"
96 | />
97 | <FeatureCard
98 | title="nix-darwin Support"
99 | description="Access to nix-darwin macOS configuration options and resources."
100 | iconName="apple"
101 | />
102 | <FeatureCard
103 | title="Fast & Stateless"
104 | description="Direct API calls with no caching complexity. Simple, reliable, and maintainable."
105 | iconName="bolt"
106 | />
107 | <FeatureCard
108 | title="Cross-Platform"
109 | description="Works seamlessly across Linux, macOS, and Windows environments."
110 | iconName="globe"
111 | />
112 | <FeatureCard
113 | title="Claude Integration"
114 | description="Perfect compatibility with Claude and other AI assistants via the MCP protocol."
115 | iconName="robot"
116 | />
117 | <FeatureCard
118 | title="Version History"
119 | description="Package version tracking with nixpkgs commit hashes via NixHub.io integration."
120 | iconName="history"
121 | />
122 | <FeatureCard
123 | title="Plain Text Output"
124 | description="Human-readable responses with no XML parsing needed. Just clear, formatted text."
125 | iconName="document"
126 | />
127 | </div>
128 | </div>
129 | </section>
130 |
131 | {/* Getting Started Section */}
132 | <section id="getting-started" className="py-16 bg-nix-light">
133 | <div className="container-custom">
134 | <AnchorHeading level={2} className="text-3xl font-bold text-center mb-12 text-nix-dark">Getting Started</AnchorHeading>
135 | <div className="max-w-2xl mx-auto">
136 | <AnchorHeading level={3} className="text-2xl font-bold mb-4 text-nix-primary">Configuration</AnchorHeading>
137 | <p className="mb-6 text-gray-800 font-medium">
138 | Add to your MCP configuration file:
139 | </p>
140 | <CodeBlock
141 | code={`{
142 | "mcpServers": {
143 | "nixos": {
144 | "command": "uvx",
145 | "args": ["mcp-nixos"]
146 | }
147 | }
148 | }`}
149 | language="json"
150 | />
151 | <p className="mt-6 text-gray-800 font-medium">
152 | Start leveraging NixOS package information and configuration options in your workflow!
153 | </p>
154 | <div className="text-center mt-12">
155 | <Link href="/usage" className="btn-primary">
156 | See All Configuration Options
157 | </Link>
158 | </div>
159 | </div>
160 | </div>
161 | </section>
162 | </div>
163 | );
164 | }
```