59 Commits
Author SHA1 Message Date
hermes-agent b55c7af097 romm-mcp: pin the real source and npmDeps hashes
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 25s
Both come from the build, and both were non-obvious:

- fetchzip hashes the *unpacked* tree, not the .tgz bytes, so a hash computed
  from the published tarball does not match; the derivation reports the NAR hash
  of the extracted contents instead.
- npmDepsHash only exists once npm has materialised the dependency tree from the
  committed lockfile.

Verified with nix build .#romm-mcp: the derivation builds and ./result/bin/romm-mcp
starts and prints 'RomM MCP server running on stdio'.
2026-11-02 10:13:46 +01:00
hermes-agent e9dc6f91b7 Add romm-mcp package
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 22s
RomM MCP server: browse, search and administrate a self-hosted ROM library,
including triggering library scans (POST /api/tasks/scan), which RomM 5.x only
exposes through its API.

Upstream ships no package-lock.json, so the lock committed here is generated from
the published 1.0.5 tarball (npm install --package-lock-only --ignore-scripts),
pinning exactly the tree that shipped with that version. dist/ is prebuilt in the
tarball, hence dontNpmBuild.

npmDepsHash is a placeholder: it can only be derived from the lockfile by a build,
so CI (nix flake check, which builds every package) reports the real value.
2026-11-02 10:09:42 +01:00
luis e797c592cd Merge branch 'main' of gitea.bueso.eu:luis/mcp-servers
NixOS Configuration CI / Nix Flake Check (push) Successful in 15s
2026-10-22 17:12:44 +02:00
luis e9946694e6 msg 2026-10-22 17:12:12 +02:00
hermes-agent 4709b47c1e Merge pull request 'pinchflat-mcp: stop leaking the API token, gate admin tools, allow retention' (#17) from fix/pinchflat-mcp-secrets into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 12s
2026-10-18 15:21:51 +02:00
hermes-agent 80d0352d1e pinchflat-mcp: redact credentials, gate admin tools, expose retention
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 15s
2026-10-18 15:18:18 +02:00
hermes-agent e4e6d083e5 Merge pull request 'Bump jellyfin-mcp to 2026.1015.1 (Jellyfin 12 auth header)' (#16) from bump/jellyfin-mcp-2026.1015.1 into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
2026-10-18 13:50:31 +02:00
hermes-agent ceaaec2068 jellyfin-mcp: real vendorHash for 2026.1015.1
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 1m29s
2026-10-18 13:47:03 +02:00
hermes-agent 560d2d102f Bump jellyfin-mcp to 2026.1015.1
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 19s
2026-10-18 13:44:17 +02:00
luis 81c7f99174 msg
NixOS Configuration CI / Nix Flake Check (push) Successful in 25s
2026-10-01 16:44:32 +02:00
luis d7c952cf4d Merge remote-tracking branch 'origin/main'
NixOS Configuration CI / Nix Flake Check (push) Successful in 34s
2026-09-30 16:07:33 +02:00
luis 810820658a msg 2026-09-30 16:07:13 +02:00
luis 42206f8328 Merge pull request 'Add python-ssh-mcp package' (#15) from add-python-ssh-mcp into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 25s
Reviewed-on: #15
2026-09-30 09:07:53 +02:00
hermes-agent a525c2a1d0 Register python-ssh-mcp in package aggregator
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 14s
2026-09-29 23:43:24 +02:00
hermes-agent bd75822e2d Add python-ssh-mcp package 2026-09-29 23:43:14 +02:00
luis 23da3cf87f msg
NixOS Configuration CI / Nix Flake Check (push) Successful in 2m46s
2026-09-29 12:17:23 +02:00
luis 55fdf722e4 add libgen-mcp package
NixOS Configuration CI / Nix Flake Check (push) Successful in 10s
Library Genesis MCP server (jmrplens/libgen-mcp v2.0.1): federated book/paper search, download and read across LibGen and open-access sources. No account, API key or cookie required, unlike annas-mcp.

The flake's pinned nixpkgs only ships Go 1.26 and upstream needs Go 1.27, so package the prebuilt static release binaries (same approach as yarr).
2026-09-25 18:32:00 +02:00
luis 67c7e829f3 annas-mcp: repoint to SokolskyNikita fork v0.0.14
NixOS Configuration CI / Nix Flake Check (push) Successful in 8s
Upstream iosifache/annas-mcp is unmaintained since 2026-06-26 and returns HTTP 403 on all requests because Anna's Archive added a DDoS-Guard browser check. The fork is 24 commits ahead, passes DDoS-Guard via ANNAS_ACCOUNT_COOKIE, fixes pagination/filters, and updates the MCP SDK.
2026-09-25 18:06:13 +02:00
luis 66e3a17aa1 Revert "msg"
NixOS Configuration CI / Nix Flake Check (push) Successful in 30s
This reverts commit bc7e34472e.
2026-09-25 12:03:52 +02:00
luis 058a009f65 Revert "msg"
NixOS Configuration CI / Nix Flake Check (push) Successful in 2m14s
This reverts commit 30717f8021.
2026-09-25 12:01:03 +02:00
luis bc7e34472e msg
NixOS Configuration CI / Nix Flake Check (push) Successful in 11s
2026-09-25 11:47:02 +02:00
luis 30717f8021 msg
NixOS Configuration CI / Nix Flake Check (push) Successful in 11s
2026-09-23 12:52:51 +02:00
luis f23109857b msg
NixOS Configuration CI / Nix Flake Check (push) Successful in 26s
2026-09-23 12:25:32 +02:00
luis 471a7b4fbf update
NixOS Configuration CI / Nix Flake Check (push) Successful in 8s
2026-09-23 12:04:12 +02:00
hermes-agent eab27798b2 pdf-mcp: use fetchFromGitea for hermes-agent fork
NixOS Configuration CI / Nix Flake Check (push) Successful in 11s
2026-09-09 15:06:05 +00:00
hermes-agent b732981049 pdf-mcp: point at hermes-agent fork with bbox crop param
NixOS Configuration CI / Nix Flake Check (push) Failing after 17s
Use the hermes-agent/pdf-mcp fork (commit 356e476) which adds a bbox
crop parameter to get_page_image so it can render just a figure region
as a small PNG.
2026-09-09 15:00:29 +00:00
hermes-agent 32dd8e8d2f Fix pdf-mcp source hash (unpacked tree hash)
NixOS Configuration CI / Nix Flake Check (push) Successful in 38s
2026-09-09 14:02:27 +00:00
hermes-agent 58eb312a77 Add pdf-mcp package (I-CAN-hack/pdf-mcp)
NixOS Configuration CI / Nix Flake Check (push) Failing after 1m9s
MCP server for reading, rendering, and searching PDF files using PyMuPDF.
Exposes get_page_text, get_page_image (render page as PNG), search_text.
Python, so it spawns reliably under the mcp-gateway stdio backend.
2026-09-09 13:59:41 +00:00
luis 9e26156949 Merge pull request 'yarr: use pre-built binary instead of building from source' (#14) from fix/yarr-prebuilt into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 12s
Reviewed-on: #14
2026-08-18 08:48:23 +02:00
hermes-agent 279b4e5caf yarr: set correct fetchzip hash for pre-built binary
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 14s
2026-08-18 06:12:47 +00:00
hermes-agent 0d0ca4aa8e yarr: use pre-built binary instead of building from source
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 19s
Building yarr from source (Rust) takes ~5+ min and times out the
config-nixos CI runner (3h limit) when combined with the full
perlis+wilkes+relay flake check. Use the pre-built x86_64-linux binary
from GitHub releases instead — fast fetch, no Rust toolchain needed.

Removes the rust-nixpkgs flake input (no longer needed).
2026-08-18 06:11:19 +00:00
luis de417511c7 Merge pull request 'overlay: include yarr (consuming nixpkgs has rustc 1.97.1)' (#13) from fix/yarr-overlay into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 14s
Reviewed-on: #13
2026-08-17 14:10:04 +02:00
hermes-agent 1a06666b77 overlay: include yarr (consuming nixpkgs has rustc 1.97.1)
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 13s
yarr needs rustc 1.97.1. The mcp-servers flake's own nixpkgs has 1.95,
but consuming flakes (config-nixos) pin a newer nixpkgs with 1.97.1.
Since rustPkgs defaults to the consuming pkgs, yarr builds fine in the
overlay there. Remove the removeAttrs exclusion.
2026-08-17 12:05:47 +00:00
luis e9c4ae7273 Merge pull request 'Add yarr: Rust MCP server for media automation fleets' (#12) from feat/yarr into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
2026-08-17 14:03:39 +02:00
hermes-agent 9cf77387fd yarr: skip cargo test phase (hangs on network)
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 6m46s
The cargo test phase hangs at 0% CPU with no rustc processes — likely a
test blocking on network. Skip tests; we only need the binary.
2026-08-17 10:26:07 +00:00
hermes-agent e2b5eac582 flake.lock: add rust-nixpkgs input for yarr
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 3h1m3s
2026-08-17 09:54:30 +00:00
hermes-agent 98ba683632 yarr: add flake input for rustc 1.97.1, exclude from overlay
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 3h7m5s
yarr needs rustc 1.97.1 but the main nixpkgs input (pinned by flake.lock)
has rustc 1.95.0. Bumping the main input breaks aioimaplib (email server
dep), so add a separate rust-nixpkgs input pinned to a newer rev.

yarr is excluded from the overlay (which uses the main nixpkgs) — build
it via 'nix build .#yarr' on the flake directly.
2026-08-15 09:17:05 +00:00
hermes-agent e5db294f18 Add yarr: Rust MCP server for media automation fleets (Sonarr, Radarr, Prowlarr, Jellyfin, SABnzbd, qBittorrent, Plex)
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 1m0s
Replaces the Node.js-based mcp-arr-server and jellyfin-mcp backends
in the gateway with a single Rust binary that covers both *arr and
Jellyfin. Rust binaries work through the gateway's stdio spawn
(where Node.js ones fail with 401).
2026-08-15 08:42:53 +00:00
luis 9bcb95b262 Merge pull request 'ovhcloud-mcp: add bin wrapper (upstream has no bin field)' (#11) from fix/ovhcloud-bin-wrapper into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 9s
2026-08-13 22:13:44 +02:00
hermes-agent 017683f9e2 ovhcloud-mcp: add bin wrapper (upstream has no bin field)
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 9s
The upstream package.json has no 'bin' field, so buildNpmPackage produced
no executable and the gateway could not spawn it ('No such file or
directory'). Add a makeWrapper that runs node dist/index.js.
2026-08-13 20:09:14 +00:00
luis 377d7534ff Merge pull request 'Bump applemusic, zotero, nextcloud MCP servers to latest' (#10) from feat/bump-stale-mcps into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 8s
2026-08-13 18:31:11 +02:00
hermes-agent fcd3f6d0f0 Bump applemusic, zotero, nextcloud MCP servers to latest
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 25s
- applemusic-mcp: 0.15.1 -> 0.18.3 (adds keyring dep)
- zotero-mcp: 0.5.0 -> 0.9.1 (pyzotero 1.13.5, vendored pdf-inspector,
  updated dep set per pyproject)
- nextcloud-mcp-server: 0.121.3 -> 0.173.0 (adds anyio, uvicorn,
  cryptography, lxml, botocore, numpy, packaging, pypdfium2; updated
  postPatch for new uv_build/icalendar constraints)

All hashes computed via nix build on wilkes; nix flake check passes.
2026-08-13 16:28:47 +00:00
luis b4b26c6ef2 Merge pull request 'Add MCP server packages: annas, bitwarden, email, namecheap, navidrome, ovhcloud, slskd' (#9) from feat/package-all-inuse into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 1m10s
2026-08-13 18:01:24 +02:00
hermes-agent 80035de8f3 Add MCP server packages: annas, bitwarden, email, namecheap, navidrome, ovhcloud, slskd
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 9s
Packages MCP servers currently used by Hermes that were not yet in this
repo. Real hashes computed via nix build on wilkes; nix flake check passes.

- annas-mcp: Go, buildGoModule (re-added, previously dropped as broken)
- bitwarden-mcp: npm @bitwarden/mcp-server
- mcp-email-server: Python, buildPythonApplication
- namecheap-mcp: npm namecheap-api-mcp
- navidrome-mcp: npm navidrome-mcp
- ovhcloud-mcp: npm from GitHub, tsc build step
- slskd-mcp: Python, fastmcp wrapper
2026-08-13 15:50:38 +00:00
luis fd19b3519f Merge pull request 'feat(mcp-arr-server): bump to 1.7.3 for HTTP transport' (#8) from feat/arr-http-transport into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 8m32s
Reviewed-on: #8
2026-08-12 12:01:56 +02:00
hermes-agent 633dc74563 set npmDepsHash for 1.7.3
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 11m44s
2026-08-12 09:36:46 +00:00
hermes-agent 483e87b0a8 set src hash for 1.7.3 2026-08-12 09:35:57 +00:00
hermes-agent 9383fe1d8d feat(mcp-arr-server): bump to 1.7.3 for HTTP transport support 2026-08-12 09:34:50 +00:00
hermes-agent 2d7dedd1a2 Merge pull request 'fix: repoint sabnzbd-mcp to jasonpaulso fork, drop broken annas-mcp' (#7) from fix/sabnzbd-mcp-source into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
2026-08-04 16:13:04 +02:00
hermes-agent 7af3d89e5d fix: repoint sabnzbd-mcp to jasonpaulso fork, drop broken annas-mcp
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 15s
- sabnzbd-mcp: original upstream (jmagar/sabnzbd-mcp) was deleted from
  GitHub (404). Repoint to jasonpaulso/sabnzbd-mcp fork, which preserves
  the same file layout and the /sabnzbd/api string postPatch rewrites.
- annas-mcp: remove package that was never completed (lib.fakeHash
  placeholders) and is not referenced anywhere in config-nixos. It was
  breaking 'nix flake check'.
2026-08-04 14:10:54 +00:00
luis 84bb4f4425 docx
NixOS Configuration CI / Nix Flake Check (push) Failing after 18s
2026-07-13 12:37:11 +02:00
luis 9f9001299c Merge pull request 'add: annas-mcp package for Anna's Archive search/download' (#6) from add/annas-mcp into main
NixOS Configuration CI / Nix Flake Check (push) Failing after 13s
Reviewed-on: #6
2026-07-12 21:02:10 +02:00
hermes-agent 8dc25624ff add: annas-mcp package (Go, fakeHash placeholders)
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 20s
2026-07-12 18:58:19 +00:00
luis d276fe1ef0 Merge pull request 'pinchflat-mcp: add pagination, downloaded filter, and delete_media tool' (#5) from fix/pinchflat-mcp-pagination into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
Reviewed-on: #5
2026-07-12 13:19:27 +02:00
hermes-agent 676d06f8ca audiobookshelf-mcp: fix vendorHash (from CI)
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 46s
2026-07-12 11:16:46 +00:00
hermes-agent 4856b98429 fix: remove committed pyc, add to gitignore
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 14s
2026-07-12 11:14:52 +00:00
hermes-agent d009717854 audiobookshelf-mcp: fix src hash (from CI), still need vendorHash
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 14s
2026-07-12 11:14:43 +00:00
hermes-agent 617bb9cab5 pinchflat-mcp: add pagination, downloaded filter, and delete_media tool
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 21s
- list_media: add offset and downloaded params for API pagination
- delete_media: new tool to delete downloaded files (with prevent_download option)
- Bump timeout on delete to 120s
2026-07-12 11:09:19 +00:00
hermes-agent 15906893c0 Add audiobookshelf-mcp package
Go binary from michaeldvinci/audiobookshelf-mcp (v0.0.5).
Covers libraries, audiobooks, podcasts, authors, collections,
playlists, and users. Uses buildGoModule with fakeHash placeholders
to be filled on first nix build.
2026-07-04 22:24:46 +00:00
49 changed files with 59711 additions and 1810 deletions

No files matched your search

+1
View File
@@ -27,3 +27,4 @@ result-*
# Local environment overrides # Local environment overrides
.envrc.local .envrc.local
/.dir-locals.el /.dir-locals.el
__pycache__/
+8
View File
@@ -43,6 +43,14 @@ packages/
overlays/ overlays/
└── default.nix # Nix overlay that exports all MCP packages └── default.nix # Nix overlay that exports all MCP packages
modules/
├── mcp-gateway.nix # Home Manager module: services.mcp-gateway
└── ssh-mcp.nix # Home Manager module: services.ssh-mcp
docs/
├── mcp-gateway.md # Gateway module usage
└── ssh-mcp.md # ssh-mcp module usage
flake.nix # Main flake definition flake.nix # Main flake definition
flake.lock # Locked dependency versions flake.lock # Locked dependency versions
``` ```
+27 -1
View File
@@ -9,10 +9,10 @@ This repository provides Nix derivations for MCP (Model Context Protocol) server
| Package | Description | Language | | Package | Description | Language |
| -------------------------------- | -------------------------------------------------------------------- | ---------- | | -------------------------------- | -------------------------------------------------------------------- | ---------- |
| academic-search-mcp-server | Search academic papers via Semantic Scholar and Crossref | Python | | academic-search-mcp-server | Search academic papers via Semantic Scholar and Crossref | Python |
| annas-mcp | Anna's Archive MCP server | Python |
| applemusic-mcp | Apple Music — manage playlists, search catalog, browse library | Python | | applemusic-mcp | Apple Music — manage playlists, search catalog, browse library | Python |
| duckduckgo-mcp-server | Web search and content fetching via DuckDuckGo | Python | | duckduckgo-mcp-server | Web search and content fetching via DuckDuckGo | Python |
| jellyfin-mcp | MCP server for Jellyfin media server | Python | | jellyfin-mcp | MCP server for Jellyfin media server | Python |
| libgen-mcp | Library Genesis + open-access search/download/read (no account needed) | Go |
| kindly-web-search-mcp-server | Web search + content retrieval via SearxNG, Tavily, or SearXNG | Python | | kindly-web-search-mcp-server | Web search + content retrieval via SearxNG, Tavily, or SearXNG | Python |
| manim-mcp-server | Execute Manim animation code and return videos | Python | | manim-mcp-server | Execute Manim animation code and return videos | Python |
| mcp-arr-server | MCP server for *arr suite — Sonarr, Radarr, Lidarr, Prowlarr | TypeScript | | mcp-arr-server | MCP server for *arr suite — Sonarr, Radarr, Lidarr, Prowlarr | TypeScript |
@@ -22,12 +22,38 @@ This repository provides Nix derivations for MCP (Model Context Protocol) server
| pdf-reader-mcp | Production‑ready PDF processing with parallel processing | TypeScript | | pdf-reader-mcp | Production‑ready PDF processing with parallel processing | TypeScript |
| rust-research-mcp | Accumulate and organize academic knowledge | Rust | | rust-research-mcp | Accumulate and organize academic knowledge | Rust |
| simple-web-search-mcp | Zero-config web search and page reading for LLMs | TypeScript | | simple-web-search-mcp | Zero-config web search and page reading for LLMs | TypeScript |
| mcp-searxng | Privacy-respecting web search and URL reading via SearXNG | TypeScript |
| transmission-mcp | MCP server for the Transmission BitTorrent client | Python | | transmission-mcp | MCP server for the Transmission BitTorrent client | Python |
| sabnzbd-mcp | MCP server for SABnzbd download management | Python | | sabnzbd-mcp | MCP server for SABnzbd download management | Python |
| jellyfin-mcp | MCP server for Jellyfin — search, playback, metadata, admin | Go | | jellyfin-mcp | MCP server for Jellyfin — search, playback, metadata, admin | Go |
| zotero-mcp | Connect Zotero research library with AI assistants | Python | | zotero-mcp | Connect Zotero research library with AI assistants | Python |
| annas-mcp | Anna's Archive search/download (SokolskyNikita fork); needs cookie/API key | Go |
| bitwarden-mcp | Official Bitwarden vault, login, Send, org management | TypeScript |
| mcp-email-server | Send, receive, search email over IMAP/SMTP | Python |
| namecheap-mcp | Namecheap DNS, domains, contacts, registrar lock | TypeScript |
| navidrome-mcp | Navidrome music library search, playlists, radio, playback | TypeScript |
| ovhcloud-mcp | OVHcloud VPS, domains, DNS, billing, SSH, API explorer | TypeScript |
| slskd-mcp | slskd (Soulseek) search, download, peer browsing, private messages | Python |
| mcp-score | AI-driven music score generation and MuseScore live control | Python | | mcp-score | AI-driven music score generation and MuseScore live control | Python |
## Home Manager Modules
The flake also exports reusable Home Manager modules:
```nix
home-manager.sharedModules = [
mcp-servers.homeManagerModules.mcp-gateway
mcp-servers.homeManagerModules.ssh-mcp
];
```
- `services.mcp-gateway` renders `gateway.yaml` from Nix options.
- `services.ssh-mcp.instances.<name>` renders per-instance `hosts.toml` and
`known_hosts` files and exposes a `gatewayBackend` helper.
See [docs/mcp-gateway.md](docs/mcp-gateway.md) and
[docs/ssh-mcp.md](docs/ssh-mcp.md).
## Usage ## Usage
### Using the Overlay (Recommended) ### Using the Overlay (Recommended)
+62
View File
@@ -0,0 +1,62 @@
# MCP Gateway Home Manager Module
`homeManagerModules.mcp-gateway` exposes `services.mcp-gateway` and renders
`~/.config/mcp-gateway/gateway.yaml` from Nix options.
## Import
```nix
home-manager.sharedModules = [
mcp-servers.homeManagerModules.mcp-gateway
];
```
## Minimal example
```nix
services.mcp-gateway = {
enable = true;
server = {
host = "100.64.0.19";
port = 39401;
};
environmentFile = "/run/secrets/rendered/mcp_gateway_env";
auth.apiKeys.dev = {
key = "env:DEV_GATEWAY_KEY";
backends = [ "nixos" "fetch" ];
};
backends = {
nixos.command = "${pkgs.mcp-nixos}/bin/mcp-nixos";
fetch.command = "${pkgs.mcp-server-fetch}/bin/mcp-server-fetch";
};
};
```
`services.mcp-gateway.url` is read-only and resolves to
`http://<server.host>:<server.port>/mcp`, for use by MCP clients.
## Backends
Backends are keyed by name and support:
- `command` for stdio backends, or `httpUrl` for HTTP backends
- `cwd`, `timeout`, `env`, `headers`
- `passthrough`, `allowCleartextCredentials`
- `resolveEnv = true` to wrap commands that read literal `${env.VAR}`
placeholders
- `extraConfig` for fields not modelled by the module
## API keys
API keys are keyed by client name. The key may use the gateway's `env:VAR`,
`keychain:NAME`, or `oauth:PROVIDER` indirection. Avoid literal secrets in the
Nix store.
## Routing profiles
`routingProfiles.<name>` supports `allowBackends`, `denyBackends`,
`allowTools`, and `denyTools`. Set `defaultRoutingProfile` to the profile used
when a session has not selected one.
+77
View File
@@ -0,0 +1,77 @@
# ssh-mcp Home Manager Module
`homeManagerModules.ssh-mcp` exposes `services.ssh-mcp.instances.<name>`.
Each instance renders its own SSH host policy and known_hosts file under
`~/.config/ssh-mcp/<instance>/`.
## Import
```nix
home-manager.sharedModules = [
mcp-servers.homeManagerModules.ssh-mcp
];
```
## Example
```nix
services.ssh-mcp.instances.admin = {
enable = true;
description = "Read-only systemd/journalctl over SSH";
defaults = {
user = "ai-agent";
port = 22;
platform = "linux";
persistentSession = false;
auth = {
method = "key";
key = "/run/secrets/ssh_ai_agent_key";
};
};
hosts = {
perlis.hostname = "100.64.0.4";
ravel.hostname = "100.64.0.19";
};
knownHosts = {
"100.64.0.4" = "ssh-ed25519 AAAA...";
"100.64.0.19" = "ssh-ed25519 AAAA...";
};
settings = {
enabledGroups = [ "host" "systemctl" ];
allowLowAccessTools = false;
allowDangerousTools = false;
allowSudo = false;
};
};
```
## Gateway integration
Each instance exposes `gatewayBackend`, a plain attrset shaped like a
`services.mcp-gateway.backends.<name>` entry:
```nix
services.mcp-gateway.backends = {
ssh-mcp = config.services.ssh-mcp.instances.admin.gatewayBackend;
ssh-mcp-media = config.services.ssh-mcp.instances.media.gatewayBackend;
};
```
Read-only paths are also available:
- `config.services.ssh-mcp.instances.<name>.hostsFile`
- `config.services.ssh-mcp.instances.<name>.knownHostsFile`
## Notes
- The default `package` is `pkgs.python-ssh-mcp`, so apply
`mcp-servers.overlays.default` on the host or set `package` explicitly.
- `SSH_ENABLED_GROUPS` is server-wide, not per-host. Run separate instances
when different catalogs are needed.
- Tier flags (`allowLowAccessTools`, `allowDangerousTools`, `allowSudo`) are
hard security gates. Group filters only change what is advertised.
- Empty path or command allowlists mean denied, not unrestricted.
+15
View File
@@ -24,9 +24,23 @@
"aarch64-darwin" "aarch64-darwin"
]; ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems; forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
mcpGatewayModule = import ./modules/mcp-gateway.nix;
sshMcpModule = import ./modules/ssh-mcp.nix;
in in
{ {
overlays.default = import ./overlays/default.nix; overlays.default = import ./overlays/default.nix;
homeManagerModules = {
mcp-gateway = mcpGatewayModule;
ssh-mcp = sshMcpModule;
default = {
imports = [
mcpGatewayModule
sshMcpModule
];
};
};
} }
// { // {
packages = forAllSystems ( packages = forAllSystems (
@@ -60,6 +74,7 @@
packages = builtins.attrValues mcpPackages ++ [ pkgs.mcp-nixos pkgs.git ]; packages = builtins.attrValues mcpPackages ++ [ pkgs.mcp-nixos pkgs.git ];
shellHook = '' shellHook = ''
export SEARXNG_BASE_URL="https://search.bueso.eu" export SEARXNG_BASE_URL="https://search.bueso.eu"
export SEARXNG_URL="https://search.bueso.eu"
''; '';
}; };
} }
+606
View File
@@ -0,0 +1,606 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkOption
mkIf
types
;
cfg = config.services.mcp-gateway;
# Gateway configuration is YAML. `pkgs.formats.yaml` builds it via
# structured attrs, which also drops null-valued attributes, so unset
# optional fields simply disappear from the rendered file.
yaml = pkgs.formats.yaml { };
# The gateway passes `${env.VAR}` placeholders VERBATIM to every backend
# subprocess -- it does NOT resolve them. Some MCP servers resolve the
# placeholder themselves; others (yarr, slskd-mcp, nextcloud-mcp-server,
# gitea-mcp, ovhcloud-mcp, pinchflat-mcp) read the literal `${env.VAR}`
# string as the credential and fail auth. This wrapper scans every env var
# and resolves any literal `${env.X}` placeholder from the rendered gateway
# environment file before exec'ing the real binary.
resolveEnv = pkgs.writeShellScript "mcp-gateway-resolve-env" ''
env_file=${lib.escapeShellArg (if cfg.environmentFile == null then "" else cfg.environmentFile)}
for line in $(env | cut -d= -f1); do
val="$(printenv "$line")"
if [[ "$val" == \$\{env.* ]]; then
name="''${val#\$\{env.}"
name="''${name%\}}"
export "$line"="$(grep "^$name=" "$env_file" | head -1 | cut -d= -f2-)"
fi
done
exec "$@"
'';
apiKeyType = types.submodule (
{ name, ... }:
{
options = {
key = mkOption {
type = types.str;
description = ''
API key value. Prefer an indirection such as `env:VAR_NAME`,
`keychain:NAME`, or `oauth:PROVIDER` instead of a literal secret.
'';
};
name = mkOption {
type = types.str;
default = name;
description = "Human-readable client name. Defaults to the attribute name.";
};
rateLimit = mkOption {
type = types.ints.unsigned;
default = 0;
description = "Requests per minute. 0 means unlimited.";
};
backends = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Backends this key may use. Empty means every backend.";
};
allowedTools = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "If set, only these tools (glob patterns allowed) are accessible.";
};
deniedTools = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "Tools (glob patterns allowed) blocked for this key.";
};
admin = mkOption {
type = types.bool;
default = false;
description = "Whether this key may use admin-only management tools.";
};
};
}
);
routingProfileType = types.submodule {
options = {
description = mkOption {
type = types.str;
default = "";
description = "Human-readable profile description.";
};
allowBackends = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "If set, only these backends are reachable in this profile.";
};
denyBackends = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "Backends denied in this profile.";
};
allowTools = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "If set, only these tools (glob patterns allowed) are reachable.";
};
denyTools = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "Tools (glob patterns allowed) denied in this profile.";
};
};
};
surfacedToolType = types.submodule {
options = {
server = mkOption {
type = types.str;
description = "Backend name.";
};
tool = mkOption {
type = types.str;
description = "Tool name to surface directly in tools/list.";
};
};
};
backendType = types.submodule (
{ name, ... }:
{
options = {
description = mkOption {
type = types.str;
default = "";
description = "Human-readable backend description.";
};
enabled = mkOption {
type = types.bool;
default = true;
description = "Whether the backend is enabled.";
};
command = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Shell command for a stdio backend. Mutually exclusive with
`httpUrl`. Set `resolveEnv = true` if the backend reads literal
`''${env.VAR}` placeholders.
'';
};
cwd = mkOption {
type = types.nullOr types.str;
default = null;
description = "Working directory for a stdio backend.";
};
httpUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = "URL for an HTTP backend. Mutually exclusive with `command`.";
};
streamableHttp = mkOption {
type = types.bool;
default = true;
description = "Use Streamable HTTP instead of the SSE handshake for `httpUrl`.";
};
timeout = mkOption {
type = types.str;
default = "30s";
description = "Per-backend request timeout, in humantime syntax.";
};
env = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Environment variables passed to the backend process.";
};
headers = mkOption {
type = types.attrsOf types.str;
default = { };
description = "HTTP headers for an HTTP backend.";
};
passthrough = mkOption {
type = types.bool;
default = false;
description = ''
Skip input sanitization on this backend's tools/call requests.
Only use for fully trusted internal backends.
'';
};
allowCleartextCredentials = mkOption {
type = types.bool;
default = false;
description = ''
Permit credentials over cleartext HTTP to a non-loopback host.
This is a security exception, not a convenience toggle.
'';
};
resolveEnv = mkOption {
type = types.bool;
default = false;
description = ''
Prefix `command` with the module's env-placeholder resolver. This
makes backends that read literal `''${env.VAR}` strings resolve
them from `environmentFile` before starting.
'';
};
extraConfig = mkOption {
type = types.attrsOf types.anything;
default = { };
description = "Escape hatch merged into this backend's YAML map.";
};
};
}
);
# Render an environment-like value the way gateway.yaml expects it:
# booleans as true/false, lists as comma-separated strings.
renderEnvValue =
value:
if builtins.isBool value then
(if value then "true" else "false")
else if builtins.isList value then
lib.concatStringsSep "," value
else
toString value;
apiSettings = {
enabled = cfg.auth.enable;
bearer_token = cfg.auth.bearerToken;
public_paths = cfg.auth.publicPaths;
single_user = cfg.auth.singleUser;
api_keys = lib.mapAttrsToList (_name: key: {
inherit (key) key name backends admin;
rate_limit = key.rateLimit;
allowed_tools = key.allowedTools;
denied_tools = key.deniedTools;
}) cfg.auth.apiKeys;
};
metaMcpSettings = {
enabled = cfg.metaMcp.enable;
cache_tools = cfg.metaMcp.cacheTools;
cache_ttl = cfg.metaMcp.cacheTtl;
prompts_resources_fetch_timeout = cfg.metaMcp.promptsResourcesFetchTimeout;
warm_start = cfg.metaMcp.warmStart;
surfaced_tools = cfg.metaMcp.surfacedTools;
projection_mode = cfg.metaMcp.projectionMode;
exposed_meta_tools = cfg.metaMcp.exposedMetaTools;
expose_stats_tool = cfg.metaMcp.exposeStatsTool;
};
routingSettings = lib.mapAttrs (_name: profile: {
inherit (profile) description;
allow_backends = profile.allowBackends;
deny_backends = profile.denyBackends;
allow_tools = profile.allowTools;
deny_tools = profile.denyTools;
}) cfg.routingProfiles;
backendSettings = lib.mapAttrs (
name: backend:
{
inherit (backend) description enabled timeout env headers;
}
// lib.optionalAttrs (backend.command != null) {
command =
if backend.resolveEnv then
"${resolveEnv} ${backend.command}"
else
backend.command;
}
// lib.optionalAttrs (backend.cwd != null) {
inherit (backend) cwd;
}
// lib.optionalAttrs (backend.httpUrl != null) {
http_url = backend.httpUrl;
streamable_http = backend.streamableHttp;
}
// lib.optionalAttrs backend.passthrough {
passthrough = true;
}
// lib.optionalAttrs backend.allowCleartextCredentials {
allow_cleartext_credentials = true;
}
// backend.extraConfig
) cfg.backends;
settings = {
server = {
inherit (cfg.server) host port;
modern_protocol = cfg.server.modernProtocol;
request_timeout = cfg.server.requestTimeout;
shutdown_timeout = cfg.server.shutdownTimeout;
max_body_size = cfg.server.maxBodySize;
public_url = cfg.server.publicUrl;
ws_port = cfg.server.wsPort;
allow_unauthenticated_network_bind = cfg.server.allowUnauthenticatedNetworkBind;
};
auth = apiSettings;
meta_mcp = metaMcpSettings;
default_routing_profile = cfg.defaultRoutingProfile;
routing_profiles = routingSettings;
backends = backendSettings;
}
// lib.optionalAttrs (cfg.envFiles != [ ]) {
env_files = cfg.envFiles;
}
// cfg.extraConfig;
referencedBackends =
lib.concatMap (key: key.backends) (lib.attrValues cfg.auth.apiKeys)
++ lib.concatMap (
profile:
lib.optionals (profile.allowBackends != null) profile.allowBackends
++ lib.optionals (profile.denyBackends != null) profile.denyBackends
) (lib.attrValues cfg.routingProfiles)
++ cfg.metaMcp.warmStart
++ map (tool: tool.server) cfg.metaMcp.surfacedTools;
missingBackends = lib.filter (name: !(builtins.hasAttr name cfg.backends)) referencedBackends;
invalidBackends = lib.filter (
backend: (backend.command == null) == (backend.httpUrl == null)
) (lib.attrValues cfg.backends);
in
{
options.services.mcp-gateway = {
enable = mkEnableOption "the local MCP gateway";
package = mkOption {
type = types.package;
default = pkgs.mcp-gateway;
description = "MCP gateway package.";
};
environmentFile = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Optional systemd EnvironmentFile containing backend credentials and
`env:` API keys. Also the file the `resolveEnv` wrapper consults.
'';
};
environment = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Extra environment variables for the gateway process.";
};
envFiles = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Extra env files loaded by the gateway itself (gateway `env_files`).";
};
server = {
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Address to bind to. Use this host's Tailscale address where remote
clients must reach it. Never use 0.0.0.0 on a host with a public
interface.
'';
};
port = mkOption {
type = types.port;
default = 39401;
description = "TCP port to listen on.";
};
modernProtocol = mkOption {
type = types.bool;
default = true;
description = "Serve the newest MCP protocol revision and negotiate down.";
};
requestTimeout = mkOption {
type = types.str;
default = "30s";
description = "Request timeout in humantime syntax.";
};
shutdownTimeout = mkOption {
type = types.str;
default = "30s";
description = "Graceful shutdown timeout in humantime syntax.";
};
maxBodySize = mkOption {
type = types.ints.positive;
default = 10485760;
description = "Maximum request body size in bytes.";
};
publicUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = "Externally reachable base URL when running behind a proxy.";
};
wsPort = mkOption {
type = types.nullOr types.port;
default = null;
description = "Optional WebSocket listener port.";
};
allowUnauthenticatedNetworkBind = mkOption {
type = types.bool;
default = false;
description = ''
Permit an unauthenticated bind on a non-loopback address. Only set
when authentication terminates in front of the gateway.
'';
};
};
auth = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable API-key authentication.";
};
bearerToken = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional single bearer token in addition to API keys.";
};
publicPaths = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Paths allowed without authentication.";
};
singleUser = mkOption {
type = types.bool;
default = false;
description = "Treat all callers as a single user.";
};
apiKeys = mkOption {
type = types.attrsOf apiKeyType;
default = { };
description = "API keys keyed by client name.";
};
};
metaMcp = {
enable = mkOption {
type = types.bool;
default = true;
description = "Expose the compact meta-MCP surface.";
};
cacheTools = mkOption {
type = types.bool;
default = true;
description = "Cache backend tool lists.";
};
cacheTtl = mkOption {
type = types.str;
default = "5m";
description = "Tool cache TTL in humantime syntax.";
};
promptsResourcesFetchTimeout = mkOption {
type = types.str;
default = "10s";
description = "Per-backend bound for prompts/resources aggregation.";
};
warmStart = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Backends to warm-start on gateway startup.";
};
surfacedTools = mkOption {
type = types.listOf surfacedToolType;
default = [ ];
description = "Backend tools to expose directly alongside the meta-tools.";
};
projectionMode = mkOption {
type = types.enum [ "off" "on" "experimental" ];
default = "off";
description = "Canonical response-projection rollout mode.";
};
exposedMetaTools = mkOption {
type = types.listOf types.str;
default = [ ];
description = "If non-empty, only these meta-tools are exposed.";
};
exposeStatsTool = mkOption {
type = types.bool;
default = false;
description = "List `gateway_get_stats` in tools/list.";
};
};
defaultRoutingProfile = mkOption {
type = types.str;
default = "default";
description = "Routing profile used when a session does not select one.";
};
routingProfiles = mkOption {
type = types.attrsOf routingProfileType;
default = { };
description = "Session-scoped tool/backend access profiles.";
};
backends = mkOption {
type = types.attrsOf backendType;
default = { };
description = "MCP backends keyed by backend name.";
};
extraConfig = mkOption {
type = types.attrsOf types.anything;
default = { };
description = "Escape hatch merged over the generated top-level YAML.";
};
url = mkOption {
type = types.str;
readOnly = true;
default = "http://${cfg.server.host}:${toString cfg.server.port}/mcp";
description = "MCP endpoint URL derived from `server.host` and `server.port`.";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = missingBackends == [ ];
message = "services.mcp-gateway references unknown backends: ${lib.concatStringsSep ", " missingBackends}";
}
{
assertion = invalidBackends == [ ];
message = "services.mcp-gateway backends must set exactly one of `command` or `httpUrl`";
}
{
assertion =
cfg.environmentFile != null
|| !(lib.any ( backend: backend.resolveEnv ) (lib.attrValues cfg.backends));
message = "services.mcp-gateway: `resolveEnv = true` requires `environmentFile`";
}
];
xdg.configFile."mcp-gateway/gateway.yaml".source =
yaml.generate "gateway.yaml" settings;
systemd.user.services.mcp-gateway = {
Unit = {
Description = "Local MCP gateway";
After = [ "network.target" ];
};
Service = {
ExecStart = "${cfg.package}/bin/mcp-gateway serve --config ${config.xdg.configHome}/mcp-gateway/gateway.yaml";
EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
Environment = [
"NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"HOME=%h"
]
++ lib.mapAttrsToList (name: value: "${name}=${value}") cfg.environment;
Restart = "always";
RestartSec = 5;
};
Install.WantedBy = [ "default.target" ];
};
};
}
+521
View File
@@ -0,0 +1,521 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkOption
mkIf
types
;
cfg = config.services.ssh-mcp;
hostConfig = config;
toml = pkgs.formats.toml { };
renderEnvValue =
value:
if builtins.isBool value then
(if value then "true" else "false")
else if builtins.isList value then
lib.concatStringsSep "," value
else
toString value;
authType = types.submodule {
options = {
method = mkOption {
type = types.enum [ "agent" "key" "password" ];
default = "agent";
description = "SSH authentication method.";
};
key = mkOption {
type = types.nullOr types.str;
default = null;
description = "Private key path for `method = \"key\"`.";
};
identityFingerprint = mkOption {
type = types.nullOr types.str;
default = null;
description = "Pin one agent identity by SHA-256 fingerprint.";
};
identitiesOnly = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Offer only the pinned identity to the server.";
};
identityAgent = mkOption {
type = types.nullOr types.str;
default = null;
description = "SSH agent socket to use instead of SSH_AUTH_SOCK.";
};
passphraseCmd = mkOption {
type = types.nullOr types.str;
default = null;
description = "Local command that prints the key passphrase.";
};
extraConfig = mkOption {
type = types.attrsOf types.anything;
default = { };
description = "Escape hatch merged into this auth table.";
};
};
};
defaultsType = types.submodule {
options = {
user = mkOption {
type = types.str;
default = "root";
description = "Default SSH user.";
};
port = mkOption {
type = types.port;
default = 22;
description = "Default SSH port.";
};
platform = mkOption {
type = types.str;
default = "linux";
description = "Default target platform.";
};
persistentSession = mkOption {
type = types.bool;
default = false;
description = "Allow persistent shell sessions by default.";
};
pathAllowlist = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Default path roots. Empty means no paths are allowed.";
};
restrictedPaths = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Default paths carved out of the allowlist.";
};
commandAllowlist = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Default exec command allowlist.";
};
auth = mkOption {
type = authType;
default = { };
description = "Default SSH authentication policy.";
};
extraConfig = mkOption {
type = types.attrsOf types.anything;
default = { };
description = "Escape hatch merged into the `[defaults]` table.";
};
};
};
hostType = types.submodule {
options = {
hostname = mkOption {
type = types.str;
description = "Canonical hostname or address used on the wire.";
};
user = mkOption {
type = types.nullOr types.str;
default = null;
description = "Per-host SSH user override.";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "Per-host SSH port override.";
};
platform = mkOption {
type = types.nullOr types.str;
default = null;
description = "Per-host platform override.";
};
persistentSession = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Per-host persistent-session override.";
};
pathAllowlist = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "Per-host path roots.";
};
restrictedPaths = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "Per-host paths carved out of the allowlist.";
};
commandAllowlist = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = "Per-host exec command allowlist.";
};
auth = mkOption {
type = types.nullOr authType;
default = null;
description = "Per-host authentication policy override.";
};
extraConfig = mkOption {
type = types.attrsOf types.anything;
default = { };
description = "Escape hatch merged into this host's table.";
};
};
};
settingsType = types.submodule {
options = {
enabledGroups = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Tool groups to expose. Empty exposes all groups.";
};
hostsAllowlist = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Additional hostnames allowed by the server.";
};
hostsBlocklist = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Hostnames denied by the server. Deny wins.";
};
pathAllowlist = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Server-wide path allowlist.";
};
restrictedPaths = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Server-wide restricted paths.";
};
commandAllowlist = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Server-wide exec command allowlist.";
};
allowLowAccessTools = mkOption {
type = types.bool;
default = false;
description = "Enable SFTP-mediated file mutation tools.";
};
allowDangerousTools = mkOption {
type = types.bool;
default = false;
description = "Enable arbitrary command execution tools.";
};
allowSudo = mkOption {
type = types.bool;
default = false;
description = "Enable sudo-tier tools. Also requires dangerous tools.";
};
allowAnyCommand = mkOption {
type = types.bool;
default = false;
description = "Permit arbitrary commands when no command allowlist is set.";
};
allowPasswordAuth = mkOption {
type = types.bool;
default = false;
description = "Permit password authentication.";
};
pingIncludesNotes = mkOption {
type = types.bool;
default = false;
description = "Include operator notes in host ping results.";
};
pingIncludesAgentNotes = mkOption {
type = types.bool;
default = false;
description = "Include agent notes in host ping results.";
};
otelEnabled = mkOption {
type = types.bool;
default = false;
description = "Enable OpenTelemetry export.";
};
localTransferRoots = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Local paths the server may read for uploads and transfers.";
};
dockerCmd = mkOption {
type = types.nullOr types.str;
default = null;
description = "Docker-compatible CLI to use, e.g. `podman`.";
};
configFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional OpenSSH config file to fill in unset host fields.";
};
extraEnv = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Extra environment variables for the ssh-mcp process.";
};
};
};
renderAuth =
auth:
{ inherit (auth) method; }
// lib.optionalAttrs (auth.key != null) { inherit (auth) key; }
// lib.optionalAttrs (auth.identityFingerprint != null) {
identity_fingerprint = auth.identityFingerprint;
}
// lib.optionalAttrs (auth.identitiesOnly != null) {
identities_only = auth.identitiesOnly;
}
// lib.optionalAttrs (auth.identityAgent != null) {
identity_agent = auth.identityAgent;
}
// lib.optionalAttrs (auth.passphraseCmd != null) {
passphrase_cmd = auth.passphraseCmd;
}
// auth.extraConfig;
renderDefaults =
defaults:
{
inherit (defaults) user port platform;
persistent_session = defaults.persistentSession;
path_allowlist = defaults.pathAllowlist;
restricted_paths = defaults.restrictedPaths;
command_allowlist = defaults.commandAllowlist;
auth = renderAuth defaults.auth;
}
// defaults.extraConfig;
renderHost =
host:
{ inherit (host) hostname; }
// lib.optionalAttrs (host.user != null) { inherit (host) user; }
// lib.optionalAttrs (host.port != null) { inherit (host) port; }
// lib.optionalAttrs (host.platform != null) { inherit (host) platform; }
// lib.optionalAttrs (host.persistentSession != null) {
persistent_session = host.persistentSession;
}
// lib.optionalAttrs (host.pathAllowlist != null) {
path_allowlist = host.pathAllowlist;
}
// lib.optionalAttrs (host.restrictedPaths != null) {
restricted_paths = host.restrictedPaths;
}
// lib.optionalAttrs (host.commandAllowlist != null) {
command_allowlist = host.commandAllowlist;
}
// lib.optionalAttrs (host.auth != null) {
auth = renderAuth host.auth;
}
// host.extraConfig;
renderInstanceSettings =
instance:
{
ALLOW_LOW_ACCESS_TOOLS = renderEnvValue instance.settings.allowLowAccessTools;
ALLOW_DANGEROUS_TOOLS = renderEnvValue instance.settings.allowDangerousTools;
ALLOW_SUDO = renderEnvValue instance.settings.allowSudo;
ALLOW_ANY_COMMAND = renderEnvValue instance.settings.allowAnyCommand;
ALLOW_PASSWORD_AUTH = renderEnvValue instance.settings.allowPasswordAuth;
SSH_PING_INCLUDES_NOTES = renderEnvValue instance.settings.pingIncludesNotes;
SSH_PING_INCLUDES_AGENT_NOTES = renderEnvValue instance.settings.pingIncludesAgentNotes;
OTEL_ENABLED = renderEnvValue instance.settings.otelEnabled;
}
// lib.optionalAttrs (instance.settings.enabledGroups != [ ]) {
SSH_ENABLED_GROUPS = renderEnvValue instance.settings.enabledGroups;
}
// lib.optionalAttrs (instance.settings.hostsAllowlist != [ ]) {
SSH_HOSTS_ALLOWLIST = renderEnvValue instance.settings.hostsAllowlist;
}
// lib.optionalAttrs (instance.settings.hostsBlocklist != [ ]) {
SSH_HOSTS_BLOCKLIST = renderEnvValue instance.settings.hostsBlocklist;
}
// lib.optionalAttrs (instance.settings.pathAllowlist != [ ]) {
SSH_PATH_ALLOWLIST = renderEnvValue instance.settings.pathAllowlist;
}
// lib.optionalAttrs (instance.settings.restrictedPaths != [ ]) {
SSH_RESTRICTED_PATHS = renderEnvValue instance.settings.restrictedPaths;
}
// lib.optionalAttrs (instance.settings.commandAllowlist != [ ]) {
SSH_COMMAND_ALLOWLIST = renderEnvValue instance.settings.commandAllowlist;
}
// lib.optionalAttrs (instance.settings.localTransferRoots != [ ]) {
SSH_LOCAL_TRANSFER_ROOTS = renderEnvValue instance.settings.localTransferRoots;
}
// lib.optionalAttrs (instance.settings.dockerCmd != null) {
SSH_DOCKER_CMD = instance.settings.dockerCmd;
}
// lib.optionalAttrs (instance.settings.configFile != null) {
SSH_CONFIG_FILE = instance.settings.configFile;
}
// instance.settings.extraEnv;
instanceType = types.submodule (
{ name, config, ... }:
{
options = {
enable = mkEnableOption "the ssh-mcp instance ${name}";
package = mkOption {
type = types.package;
default = pkgs.python-ssh-mcp;
description = "ssh-mcp package for this instance.";
};
description = mkOption {
type = types.str;
default = "ssh-mcp instance ${name}";
description = "Backend description used by `gatewayBackend`.";
};
hostsFileName = mkOption {
type = types.str;
default = "hosts.toml";
description = "Generated hosts policy filename under `~/.config/ssh-mcp/${name}`.";
};
knownHostsFileName = mkOption {
type = types.str;
default = "known_hosts";
description = "Generated known_hosts filename under `~/.config/ssh-mcp/${name}`.";
};
defaults = mkOption {
type = defaultsType;
default = { };
description = "Default host policy for this instance.";
};
hosts = mkOption {
type = types.attrsOf hostType;
default = { };
description = "Hosts reachable through this instance, keyed by alias.";
};
knownHosts = mkOption {
type = types.attrsOf types.str;
default = { };
description = ''
Pinned host keys keyed by canonical hostname. Each value is the
key type and public key, e.g. `ssh-ed25519 AAAA...`.
'';
};
settings = mkOption {
type = settingsType;
default = { };
description = "ssh-mcp server-wide environment settings.";
};
hostsFile = mkOption {
type = types.str;
readOnly = true;
default = "${hostConfig.xdg.configHome}/ssh-mcp/${name}/${config.hostsFileName}";
description = "Runtime path of the generated hosts policy.";
};
knownHostsFile = mkOption {
type = types.str;
readOnly = true;
default = "${hostConfig.xdg.configHome}/ssh-mcp/${name}/${config.knownHostsFileName}";
description = "Runtime path of the generated known_hosts file.";
};
gatewayBackend = mkOption {
type = types.attrsOf types.anything;
readOnly = true;
default = {
command = "${config.package}/bin/ssh-mcp";
inherit (config) description;
env = {
SSH_HOSTS_FILE = config.hostsFile;
SSH_KNOWN_HOSTS = config.knownHostsFile;
}
// renderInstanceSettings config;
};
description = "Backend attrset ready for `services.mcp-gateway.backends`.";
};
};
}
);
in
{
options.services.ssh-mcp = {
instances = mkOption {
type = types.attrsOf instanceType;
default = { };
description = "Independent ssh-mcp server instances.";
};
};
config = {
xdg.configFile = lib.mkMerge (
lib.mapAttrsToList (
name: instance:
mkIf instance.enable {
"ssh-mcp/${name}/${instance.hostsFileName}".source = toml.generate "ssh-mcp-${name}-hosts.toml" {
defaults = renderDefaults instance.defaults;
hosts = lib.mapAttrs (_host: host: renderHost host) instance.hosts;
};
"ssh-mcp/${name}/${instance.knownHostsFileName}".text =
lib.optionalString (instance.knownHosts != { }) (
lib.concatStringsSep "\n" (
lib.mapAttrsToList (host: key: "${host} ${key}") instance.knownHosts
)
+ "\n"
);
}
) cfg.instances
);
};
}
+13
View File
@@ -39,6 +39,14 @@
"command": ["simple-web-search-mcp"], "command": ["simple-web-search-mcp"],
"enabled": true "enabled": true
}, },
"mcp-searxng": {
"type": "local",
"command": ["mcp-searxng"],
"enabled": true,
"environment": {
"SEARXNG_URL": "https://search.bueso.eu"
}
},
"rust-research": { "rust-research": {
"type": "local", "type": "local",
"command": ["rust-research-mcp"], "command": ["rust-research-mcp"],
@@ -96,6 +104,11 @@
"type": "local", "type": "local",
"command": ["mcp-score", "serve"], "command": ["mcp-score", "serve"],
"enabled": true "enabled": true
},
"docx": {
"type": "local",
"command": ["docx-mcp"],
"enabled": true
} }
} }
} }
+1 -2
View File
@@ -15,9 +15,8 @@
# # Now pkgs.manim-mcp-server is available # # Now pkgs.manim-mcp-server is available
# }; # };
# } # }
#
final: prev: final: prev:
let let
mcpPackages = import ../packages { pkgs = final; }; mcpPackages = import ../packages { pkgs = final; };
in in
mcpPackages mcpPackages
+15 -9
View File
@@ -2,27 +2,33 @@
lib, lib,
buildGoModule, buildGoModule,
fetchFromGitHub, fetchFromGitHub,
makeWrapper,
}: }:
buildGoModule rec { buildGoModule rec {
pname = "annas-mcp"; pname = "annas-mcp";
version = "0.0.5"; version = "0.0.14";
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "iosifache"; owner = "SokolskyNikita";
repo = "annas-mcp"; repo = "annas-mcp";
rev = "v${version}"; rev = "9e720ad93df0fdfb25d4de6afcba11a920b34af4";
hash = "sha256-XicM7tU5jD8B8n7JJDQ/84koBiLb8XF4+WBQ4LCUoRU="; hash = "sha256-LcnWYpvSpSHYo1zMcLGTZgxZgo7JOXF4dESD4l+qVhI=";
}; };
vendorHash = "sha256-2NdG5p2XfrhVgi388dRDBUSGwg6ybnzfn9495TWNGsA="; vendorHash = "sha256-SCTznvZ8DvDN+iPDcTxiqpNYJwRLS8zIomz7f8S1eD0=";
subPackages = [ "cmd/annas-mcp" ]; nativeBuildInputs = [ makeWrapper ];
# The MCP server mode is invoked with "mcp" subcommand
postInstall = ''
wrapProgram $out/bin/annas-mcp \
--add-flags "mcp"
'';
meta = with lib; { meta = with lib; {
description = "MCP server and CLI for searching and downloading documents from Anna's Archive"; description = "MCP server and CLI tool for searching and downloading documents from Anna's Archive";
homepage = "https://github.com/iosifache/annas-mcp"; homepage = "https://github.com/SokolskyNikita/annas-mcp";
license = licenses.mit;
mainProgram = "annas-mcp"; mainProgram = "annas-mcp";
platforms = platforms.all; platforms = platforms.all;
}; };
+3 -2
View File
@@ -6,14 +6,14 @@
python3Packages.buildPythonApplication rec { python3Packages.buildPythonApplication rec {
pname = "applemusic-mcp"; pname = "applemusic-mcp";
version = "0.15.1"; version = "0.20.1";
pyproject = true; pyproject = true;
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "epheterson"; owner = "epheterson";
repo = "applemusic-mcp"; repo = "applemusic-mcp";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-N97bZyT8TWOrt1WS7W31OJaICwoUxFfcDDuRs+TweTQ="; hash = "sha256-T956sblq/4TrAXkY4cwIFDPW6a2bjzyeyRyejYB4ARc=";
}; };
build-system = with python3Packages; [ build-system = with python3Packages; [
@@ -26,6 +26,7 @@ python3Packages.buildPythonApplication rec {
requests requests
cryptography cryptography
playwright playwright
keyring
]; ];
pythonImportsCheck = [ "applemusic_mcp" ]; pythonImportsCheck = [ "applemusic_mcp" ];
+27
View File
@@ -0,0 +1,27 @@
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule rec {
pname = "audiobookshelf-mcp";
version = "unstable-2026-06-27";
src = fetchFromGitHub {
owner = "michaeldvinci";
repo = "audiobookshelf-mcp";
rev = "e790d9c75085772180c95f2ec1376acc98e9282c";
hash = "sha256-vQR+Ej3Qtg8ehIUWnXkSayNYpqnOZPWV0TpTVPYh+io=";
};
vendorHash = "sha256-W8hlCGf4QdFbKc3QFc9pa4MWBhnp5A5GvWFNzg0BEhw=";
meta = with lib; {
description = "MCP server for Audiobookshelf — libraries, audiobooks, podcasts, authors, collections, playlists";
homepage = "https://github.com/michaeldvinci/audiobookshelf-mcp";
license = licenses.gpl3Only;
mainProgram = "abs-mcp";
platforms = platforms.all;
};
}
File diff suppressed because it is too large. Load diff
+33
View File
@@ -0,0 +1,33 @@
{
lib,
buildNpmPackage,
fetchzip,
makeWrapper,
}:
buildNpmPackage rec {
pname = "bitwarden-mcp";
version = "2026.7.0";
src = fetchzip {
url = "https://registry.npmjs.org/@bitwarden/mcp-server/-/mcp-server-${version}.tgz";
hash = "sha256-uvytqAXW0G9HCshJ987j0U3a+c0p8xUG1m0BsU9Q7AQ=";
};
npmDepsHash = "sha256-X3mXupbs2Sx6E3mYRvtuWyT+FhfGmYyQZAK1iPAStM0=";
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
# dist/ is already built in the npm tarball
dontNpmBuild = true;
meta = with lib; {
description = "Official Bitwarden MCP server - vault, login, folder, Send, and organization management";
homepage = "https://github.com/bitwarden/mcp-server";
license = licenses.bsd3;
mainProgram = "bitwarden-mcp";
platforms = platforms.all;
};
}
+16 -2
View File
@@ -8,6 +8,7 @@
zotero-mcp = pkgs.callPackage ./zotero-mcp/package.nix { }; zotero-mcp = pkgs.callPackage ./zotero-mcp/package.nix { };
pdf-reader-mcp = pkgs.callPackage ./pdf-reader-mcp/package.nix { }; pdf-reader-mcp = pkgs.callPackage ./pdf-reader-mcp/package.nix { };
kindly-web-search-mcp-server = pkgs.callPackage ./kindly-web-search-mcp-server/package.nix { }; kindly-web-search-mcp-server = pkgs.callPackage ./kindly-web-search-mcp-server/package.nix { };
docx-mcp = pkgs.callPackage ./docx-mcp/package.nix { };
duckduckgo-mcp-server = pkgs.callPackage ./duckduckgo-mcp-server/package.nix { }; duckduckgo-mcp-server = pkgs.callPackage ./duckduckgo-mcp-server/package.nix { };
n8n-mcp = pkgs.callPackage ./n8n-mcp/package.nix { }; n8n-mcp = pkgs.callPackage ./n8n-mcp/package.nix { };
simple-web-search-mcp = pkgs.callPackage ./simple-web-search-mcp/package.nix { }; simple-web-search-mcp = pkgs.callPackage ./simple-web-search-mcp/package.nix { };
@@ -16,9 +17,22 @@
sabnzbd-mcp = pkgs.callPackage ./sabnzbd-mcp/package.nix { }; sabnzbd-mcp = pkgs.callPackage ./sabnzbd-mcp/package.nix { };
jellyfin-mcp = pkgs.callPackage ./jellyfin-mcp/package.nix { }; jellyfin-mcp = pkgs.callPackage ./jellyfin-mcp/package.nix { };
nextcloud-mcp-server = pkgs.callPackage ./nextcloud-mcp-server/package.nix { }; nextcloud-mcp-server = pkgs.callPackage ./nextcloud-mcp-server/package.nix { };
annas-mcp = pkgs.callPackage ./annas-mcp/package.nix { };
mcp-score = pkgs.callPackage ./mcp-score/package.nix { }; mcp-score = pkgs.callPackage ./mcp-score/package.nix { };
applemusic-mcp = pkgs.callPackage ./applemusic-mcp/package.nix { }; applemusic-mcp = pkgs.callPackage ./applemusic-mcp/package.nix { };
pinchflat-mcp = pkgs.callPackage ./pinchflat-mcp/package.nix { }; pinchflat-mcp = pkgs.callPackage ./pinchflat-mcp/package.nix { };
audiobookshelf-mcp = pkgs.callPackage ./audiobookshelf-mcp/package.nix { };
annas-mcp = pkgs.callPackage ./annas-mcp/package.nix { };
libgen-mcp = pkgs.callPackage ./libgen-mcp/package.nix { };
bitwarden-mcp = pkgs.callPackage ./bitwarden-mcp/package.nix { };
mcp-email-server = pkgs.callPackage ./mcp-email-server/package.nix { };
namecheap-mcp = pkgs.callPackage ./namecheap-mcp/package.nix { };
navidrome-mcp = pkgs.callPackage ./navidrome-mcp/package.nix { };
ovhcloud-mcp = pkgs.callPackage ./ovhcloud-mcp/package.nix { };
slskd-mcp = pkgs.callPackage ./slskd-mcp/package.nix { };
yarr = pkgs.callPackage ./yarr/package.nix { };
pdf-mcp = pkgs.callPackage ./pdf-mcp/package.nix { };
mcp-searxng = pkgs.callPackage ./mcp-searxng/package.nix { };
python-ssh-mcp = pkgs.callPackage ./python-ssh-mcp/package.nix { };
romm-mcp = pkgs.callPackage ./romm-mcp/package.nix { };
# example-mcp-server = pkgs.callPackage ./example-mcp-server/package.nix { }; # example-mcp-server = pkgs.callPackage ./example-mcp-server/package.nix { };
} }
+39
View File
@@ -0,0 +1,39 @@
{
lib,
python3Packages,
fetchFromGitHub,
}:
python3Packages.buildPythonApplication rec {
pname = "docx-mcp";
version = "2.0.0-unstable-2026-07-13";
src = fetchFromGitHub {
owner = "Rookie0x80";
repo = "docx-mcp";
rev = "b6188eaa57d2694b8f70217e8f6a15980b22ec16";
hash = "sha256-JjcQjwTiJvO6f0f+iiLgm24Rjms8+xehXHsAD6L+TEY=";
};
pyproject = true;
build-system = with python3Packages; [ setuptools ];
dependencies = with python3Packages; [
python-docx
fastmcp
typing-extensions
];
doCheck = false;
pythonImportsCheck = [ "docx_mcp" ];
meta = with lib; {
description = "MCP server for Microsoft Word .docx operations using python-docx (table-focused)";
homepage = "https://github.com/Rookie0x80/docx-mcp";
license = licenses.mit;
mainProgram = "docx-mcp";
platforms = platforms.all;
maintainers = [ ];
};
}
+15 -3
View File
@@ -2,18 +2,28 @@
lib, lib,
python3Packages, python3Packages,
fetchFromGitHub, fetchFromGitHub,
fetchurl,
}: }:
let
# Upstream 0.7.0 was ported to the MCP SDK 2.x API
# (`mcp.server.mcpserver.MCPServer`). nixpkgs pins the SDK at 1.x, so use the
# per-package 2.x stack from packages/lib/mcp2.nix.
mcp2 = import ../lib/mcp2.nix {
inherit lib fetchurl;
pythonPackages = python3Packages;
};
in
python3Packages.buildPythonApplication rec { python3Packages.buildPythonApplication rec {
pname = "duckduckgo-mcp-server"; pname = "duckduckgo-mcp-server";
version = "0.4.0"; version = "0.7.0";
pyproject = true; pyproject = true;
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "nickclyde"; owner = "nickclyde";
repo = "duckduckgo-mcp-server"; repo = "duckduckgo-mcp-server";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-g1mpBrhuCt3g/XnUfHoY8lJDMizINypikj8Lqk2LVuE="; hash = "sha256-wI6Q4lBZYzU4VORLwGZowv8D5UfhKaMeqBIQ7u6eRxc=";
}; };
build-system = [ python3Packages.hatchling ]; build-system = [ python3Packages.hatchling ];
@@ -21,10 +31,12 @@ python3Packages.buildPythonApplication rec {
dependencies = with python3Packages; [ dependencies = with python3Packages; [
beautifulsoup4 beautifulsoup4
httpx httpx
mcp httpcore
mcp2.mcp
]; ];
doCheck = false; doCheck = false;
dontCheckRuntimeDeps = true;
pythonImportsCheck = [ "duckduckgo_mcp_server" ]; pythonImportsCheck = [ "duckduckgo_mcp_server" ];
+3 -3
View File
@@ -6,16 +6,16 @@
buildGoModule rec { buildGoModule rec {
pname = "jellyfin-mcp"; pname = "jellyfin-mcp";
version = "2026.604.2"; version = "2026.1015.1";
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "jaredtrent"; owner = "jaredtrent";
repo = "jellyfin-mcp"; repo = "jellyfin-mcp";
tag = "v${version}"; tag = "v${version}";
hash = "sha256-Ks54txfOAkNEWfhl5ZXEIaFzAbmCnwhHtN1+FIa7dgo="; hash = "sha256-5wSYB/peTbvNYDlDBd2M0nA7JreP93nQe1G9rW3hv2I=";
}; };
vendorHash = "sha256-cDy+PpZM6xL7ur/xq81y4vlyaDnSOpGkyVV2hh8qI7o="; vendorHash = "sha256-L39HdshrVfVXD+rV3prsKQRosvidynwIlA9J6dtfqmU=";
meta = with lib; { meta = with lib; {
description = "MCP server for Jellyfin media server — search, playback, metadata, admin"; description = "MCP server for Jellyfin media server — search, playback, metadata, admin";
@@ -71,14 +71,14 @@ let
in in
python313Packages.buildPythonApplication rec { python313Packages.buildPythonApplication rec {
pname = "kindly-web-search-mcp-server"; pname = "kindly-web-search-mcp-server";
version = "0.1.8-unstable"; version = "0.1.9-unstable";
pyproject = true; pyproject = true;
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "Shelpuk-AI-Technology-Consulting"; owner = "Shelpuk-AI-Technology-Consulting";
repo = "kindly-web-search-mcp-server"; repo = "kindly-web-search-mcp-server";
rev = "2b76afb37bf311facbde6750d0a0c0cbdfb8fec2"; rev = "52a4e480d8041f8d4f388cee1ddecbd8e1ab01c7";
hash = "sha256-90QHr2FPiMSwKXmjlZ2W9NIQvj3LDXzYIq6m/Z+sJMw="; hash = "sha256-Mf3PMUic3XiNkiABRy6DrEhoGwcqq/xThU4ROO4S8dY=";
}; };
build-system = with python313Packages; [ build-system = with python313Packages; [
@@ -95,10 +95,18 @@ python313Packages.buildPythonApplication rec {
pymupdf pymupdf
fastmcp fastmcp
trafilatura trafilatura
starlette
uvicorn
]; ];
nativeBuildInputs = [ makeWrapper ]; nativeBuildInputs = [ makeWrapper ];
# Upstream pins floors newer than nixpkgs currently ships (starlette 1.6,
# pydantic 2.13, beautifulsoup4 4.15, trafilatura 2.2, ...). The APIs this
# server actually uses are present in the nixpkgs versions, so the strict
# runtime-deps floor check is skipped here.
dontCheckRuntimeDeps = true;
# Wrap with Chromium path for headless browser content extraction # Wrap with Chromium path for headless browser content extraction
postInstall = '' postInstall = ''
wrapProgram $out/bin/kindly-web-search-mcp-server \ wrapProgram $out/bin/kindly-web-search-mcp-server \
+98
View File
@@ -0,0 +1,98 @@
# mcp 2.x Python SDK stack
#
# nixpkgs pins the MCP Python SDK at 1.x, whose server class is
# `mcp.server.fastmcp.FastMCP`. SDK 2.x renamed it to
# `mcp.server.mcpserver.MCPServer` and dropped the old module, so packages that
# upstream has ported to the 2.x API need their own copy of the SDK rather than
# the nixpkgs attribute. Derivations are isolated, so this stack coexists with
# the 1.x packages in the same repository without affecting them.
#
# The three pure-Python wheels below (`mcp-types`, `httpx2`, `httpcore2`) are
# not in nixpkgs; everything else resolves from the pinned nixpkgs.
{
lib,
pythonPackages,
fetchurl,
}:
let
ps = pythonPackages;
mk = { pname, version, url, hash, deps ? [ ] }:
ps.buildPythonPackage {
inherit pname version;
format = "wheel";
src = fetchurl { inherit url hash; };
propagatedBuildInputs = deps;
doCheck = false;
# Upstream floors track the latest releases, which routinely sit ahead of
# nixpkgs. The build is pure-Python and import-checked below, so the
# strict floor check is not useful here.
dontCheckRuntimeDeps = true;
pythonImportsCheck = [ (builtins.replaceStrings [ "-" ] [ "_" ] pname) ];
meta = with lib; {
description = "MCP Python SDK ${version} dependency: ${pname}";
homepage = "https://github.com/modelcontextprotocol/python-sdk";
license = licenses.mit;
};
};
mcp-types = mk {
pname = "mcp-types";
version = "2.1.1";
url = "https://files.pythonhosted.org/packages/71/d0/242e63c510f4a17381f55b1549a3f94f5687a0595984febd2b6f87a687a0/mcp_types-2.1.1-py3-none-any.whl";
hash = "sha256-Jvn38D8qVzBxeluY4qt+tkCsNS0FoAzcclwxGGR3gpU=";
deps = with ps; [ pydantic typing-extensions ];
};
httpcore2 = mk {
pname = "httpcore2";
version = "2.5.0";
url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl";
hash = "sha256-XONRiN5GHTHo0AC/uO+L8ixsFlh6IR5Vcd6qXpvfhCo=";
deps = with ps; [ h11 truststore ];
};
httpx2 = mk {
pname = "httpx2";
version = "2.5.0";
url = "https://files.pythonhosted.org/packages/31/22/859d8252dad9bc9adee34b52e62cde621ece07b042ccb2ab4da1be46695f/httpx2-2.5.0-py3-none-any.whl";
hash = "sha256-PS1NnPS2HxofRqlZR8/bR+gMtWovkcYlasj1jkiR30E=";
deps = with ps; [
anyio
httpcore2
idna
truststore
];
};
in
{
inherit mcp-types httpcore2 httpx2;
mcp = mk {
pname = "mcp";
version = "2.1.1";
url = "https://files.pythonhosted.org/packages/50/af/8644cc5fa26a59afd2df2e98eeb19e72926887fa4b7441aba4ff661140db/mcp-2.1.1-py3-none-any.whl";
hash = "sha256-HGwxxdZHHFjbdq86+K9n9G0R0B8KWQd9CjCMvbPT6RU=";
deps = with ps; [
anyio
httpx2
jsonschema
mcp-types
opentelemetry-api
pydantic
pyjwt
python-multipart
sse-starlette
starlette
typing-extensions
typing-inspection
uvicorn
] ++ (with ps; [
# mcp[cli] console entry point
python-dotenv
rich
typer
]);
};
}
+54
View File
@@ -0,0 +1,54 @@
{
lib,
stdenv,
fetchurl,
}:
stdenv.mkDerivation rec {
pname = "libgen-mcp";
version = "2.0.1";
src = fetchurl (
{
x86_64-linux = {
url = "https://github.com/jmrplens/libgen-mcp/releases/download/v${version}/libgen-mcp-linux-amd64";
hash = "sha256-MN6DQ76AmGfaz/Xsyuk6EgFbHxf4TEcg5HXyVomn8G8=";
};
aarch64-linux = {
url = "https://github.com/jmrplens/libgen-mcp/releases/download/v${version}/libgen-mcp-linux-arm64";
hash = "sha256-qIkFGqEaohEnL99vbjM86JjFaHbV+A7vKNVSZg0G5Cg=";
};
x86_64-darwin = {
url = "https://github.com/jmrplens/libgen-mcp/releases/download/v${version}/libgen-mcp-darwin-amd64";
hash = "sha256-eeLfugTMCbvuyT3NzVQmBovarz0tOtOMsbNzAVKUTco=";
};
aarch64-darwin = {
url = "https://github.com/jmrplens/libgen-mcp/releases/download/v${version}/libgen-mcp-darwin-arm64";
hash = "sha256-xmKMeMJtOrqZwgTv7CvXiLEMib/XLvkQ3rCFTOyGU/Y=";
};
}
.${stdenv.hostPlatform.system} or (throw "libgen-mcp: unsupported system ${stdenv.hostPlatform.system}")
);
dontUnpack = true;
installPhase = ''
runHook preInstall
mkdir -p $out/bin
install -m755 $src $out/bin/${pname}
runHook postInstall
'';
meta = with lib; {
description = "MCP server for federated search, download and reading of books, papers, comics and standards across Library Genesis and open-access sources";
homepage = "https://github.com/jmrplens/libgen-mcp";
license = licenses.mit;
mainProgram = pname;
platforms = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
};
}
+127 -87
View File
@@ -1,15 +1,15 @@
{ {
"name": "mcp-arr-server", "name": "mcp-arr-server",
"version": "1.5.4", "version": "1.7.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mcp-arr-server", "name": "mcp-arr-server",
"version": "1.5.4", "version": "1.7.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2" "@modelcontextprotocol/sdk": "^1.30.0"
}, },
"bin": { "bin": {
"mcp-arr": "dist/index.js" "mcp-arr": "dist/index.js"
@@ -23,24 +23,24 @@
} }
}, },
"node_modules/@hono/node-server": { "node_modules/@hono/node-server": {
"version": "1.19.11", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz",
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=20"
}, },
"peerDependencies": { "peerDependencies": {
"hono": "^4" "hono": "^4"
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.28.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
"integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.9", "@hono/node-server": "^1.19.9 || ^2.0.5",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"content-type": "^1.0.5", "content-type": "^1.0.5",
@@ -75,9 +75,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.37", "version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -98,9 +98,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "8.18.0", "version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@@ -131,20 +131,20 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.2", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^2.0.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.1",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.2",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.1", "qs": "^6.15.2",
"raw-body": "^3.0.1", "raw-body": "^3.0.2",
"type-is": "^2.0.1" "type-is": "^2.1.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -154,6 +154,19 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/body-parser/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -193,9 +206,9 @@
} }
}, },
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.0.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -337,9 +350,9 @@
} }
}, },
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -376,9 +389,9 @@
} }
}, },
"node_modules/eventsource-parser": { "node_modules/eventsource-parser": {
"version": "3.0.6", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@@ -428,12 +441,13 @@
} }
}, },
"node_modules/express-rate-limit": { "node_modules/express-rate-limit": {
"version": "8.3.1", "version": "8.6.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz",
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ip-address": "10.1.0" "debug": "^4.4.3",
"ip-address": "^10.2.0"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -452,9 +466,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.0", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -577,9 +591,9 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -589,9 +603,9 @@
} }
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "4.12.9", "version": "4.13.1",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz",
"integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
@@ -618,9 +632,9 @@
} }
}, },
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.7.2", "version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -640,9 +654,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "10.1.0", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 12" "node": ">= 12"
@@ -670,9 +684,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/jose": { "node_modules/jose": {
"version": "6.2.2", "version": "6.2.8",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz",
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
@@ -700,12 +714,16 @@
} }
}, },
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
@@ -821,9 +839,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.4.0", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -853,12 +871,13 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.0", "version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -868,12 +887,16 @@
} }
}, },
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
@@ -995,14 +1018,14 @@
} }
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"object-inspect": "^1.13.3", "object-inspect": "^1.13.4",
"side-channel-list": "^1.0.0", "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1", "side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2" "side-channel-weakmap": "^1.0.2"
}, },
@@ -1014,13 +1037,13 @@
} }
}, },
"node_modules/side-channel-list": { "node_modules/side-channel-list": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"object-inspect": "^1.13.3" "object-inspect": "^1.13.4"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -1085,17 +1108,34 @@
} }
}, },
"node_modules/type-is": { "node_modules/type-is": {
"version": "2.0.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"content-type": "^1.0.5", "content-type": "^2.0.0",
"media-typer": "^1.1.0", "media-typer": "^1.1.0",
"mime-types": "^3.0.0" "mime-types": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/type-is/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
@@ -1159,9 +1199,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "4.3.6", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
+3 -3
View File
@@ -6,14 +6,14 @@
buildNpmPackage rec { buildNpmPackage rec {
pname = "mcp-arr-server"; pname = "mcp-arr-server";
version = "1.5.4"; version = "1.7.3";
src = fetchzip { src = fetchzip {
url = "https://registry.npmjs.org/mcp-arr-server/-/mcp-arr-server-${version}.tgz"; url = "https://registry.npmjs.org/mcp-arr-server/-/mcp-arr-server-${version}.tgz";
hash = "sha256-4eSL1eQmPfm5XlQuxykRDotTHvq9oScJOdXOZrYs6sU="; hash = "sha256-n2wbAvkssh1OuxAZDcixOYkc2fdmhRkKJWsb+zoEEmY=";
}; };
npmDepsHash = "sha256-s3/NbuHGqp+wAs7ysKcdBk6X0DNcvfMce+RbsB8Nt14="; npmDepsHash = "sha256-fG9reQSzNb3QvqvrHKhtOjijTKNEKH7I27rpr0t2gio=";
postPatch = '' postPatch = ''
cp ${./package-lock.json} package-lock.json cp ${./package-lock.json} package-lock.json
+57
View File
@@ -0,0 +1,57 @@
{
lib,
python3Packages,
fetchFromGitHub,
}:
python3Packages.buildPythonApplication rec {
pname = "mcp-email-server";
version = "1.9.1";
pyproject = true;
src = fetchFromGitHub {
owner = "Wh1isper";
repo = "mcp-email-server";
rev = "1.9.1";
hash = "sha256-FGjpRkVOFLkqY45Wl1lBuSWZvUnkyESZPeAB+d+1KYw=";
};
build-system = with python3Packages; [
hatchling
];
dependencies = with python3Packages; [
aioimaplib
aiosmtplib
anyio
beautifulsoup4
click
jinja2
keyring
loguru
mcp
pydantic
pydantic-settings
starlette
tomli-w
typer
email-validator
filelock
uvicorn
];
doCheck = false;
# mcp transitively requires filelock>=3.32.2 but nixpkgs ships 3.29.7;
# the runtime-deps check is too strict, so skip it (same as nextcloud-mcp-server).
dontCheckRuntimeDeps = true;
pythonImportsCheck = [ "mcp_email_server" ];
meta = with lib; {
description = "MCP server for email - send, receive, search, and manage mail over IMAP/SMTP";
homepage = "https://github.com/Wh1isper/mcp-email-server";
license = licenses.mit;
mainProgram = "mcp-email-server";
platforms = platforms.all;
};
}
+5 -5
View File
@@ -6,10 +6,10 @@
}: }:
let let
# music21 is not in nixpkgs; vendor it (upstream mcp-score pins >=9.0,<10) # music21 is not in nixpkgs; vendor it (upstream mcp-score pins >=10.5,<11)
music21 = python313Packages.buildPythonPackage rec { music21 = python313Packages.buildPythonPackage rec {
pname = "music21"; pname = "music21";
version = "9.9.2"; version = "10.5.0";
format = "wheel"; format = "wheel";
src = fetchPypi { src = fetchPypi {
@@ -17,7 +17,7 @@ let
format = "wheel"; format = "wheel";
dist = "py3"; dist = "py3";
python = "py3"; python = "py3";
hash = "sha256-ZIAQq40TKNCxB0bO8UV+hLEidM0xYkgifpbSHielGMo="; hash = "sha256-mSTv9fv1hJDmfL87eKWLpTBA53woGTDTVK8yp0fpRgY=";
}; };
propagatedBuildInputs = with python313Packages; [ propagatedBuildInputs = with python313Packages; [
@@ -50,8 +50,8 @@ python313Packages.buildPythonApplication rec {
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "tskovlund"; owner = "tskovlund";
repo = "mcp-score"; repo = "mcp-score";
rev = "3ff9d4b6504fcee5032c44fdcf7dc1d7a21a4b5d"; rev = "b2155a59119103c39a37f14166d0d6920e564c76";
hash = "sha256-zrpvzxi8AbF5/etuvMZLni4HSFBYGRe55rpKJJEAg60="; hash = "sha256-XYZjGa7pWaVmZ4dF/vOLkrYjSjNXP/148DQjujMdvIU=";
}; };
build-system = with python313Packages; [ build-system = with python313Packages; [
File diff suppressed because it is too large. Load diff
+32
View File
@@ -0,0 +1,32 @@
{
lib,
buildNpmPackage,
fetchzip,
}:
buildNpmPackage rec {
pname = "mcp-searxng";
version = "2.4.0";
src = fetchzip {
url = "https://registry.npmjs.org/mcp-searxng/-/mcp-searxng-${version}.tgz";
hash = "sha256-WB1p3+IDPpRzXh1WSFbWA3ezX8Ih2FsZlV3D3od/v1A=";
};
npmDepsHash = "sha256-srchzmiehwh4wOzhXfDbHiT7t4i8FPRSlakdUGIF/Ks=";
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
# dist/ is already built in the npm tarball
dontNpmBuild = true;
meta = with lib; {
description = "MCP server integrating SearXNG for privacy-respecting web search and URL reading";
homepage = "https://github.com/ihor-sokoliuk/mcp-searxng";
license = licenses.mit;
mainProgram = pname;
platforms = platforms.all;
};
}
+24788 -230
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -8,14 +8,14 @@
buildNpmPackage rec { buildNpmPackage rec {
pname = "n8n-mcp"; pname = "n8n-mcp";
version = "2.35.1"; version = "2.87.0";
src = fetchzip { src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz"; url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-CWHH1ACcMCWkLahV5fTO0/k5OtfVUKNldHseDP8EfEk="; hash = "sha256-YEINthnVgy5nc0KPPHWhXBiiA5vR/ThKqie9lJ6d3co=";
}; };
npmDepsHash = "sha256-x9ziuYxZPQMqkB2HA4HmGkpge7NGp9t4t4aNTi0XXn4="; npmDepsHash = "sha256-oy3aR16bTuyztbY3rgTo2z66/4kCk2BNE4BLZjhtki8=";
postPatch = '' postPatch = ''
cp ${./package-lock.json} package-lock.json cp ${./package-lock.json} package-lock.json
File diff suppressed because it is too large. Load diff
+33
View File
@@ -0,0 +1,33 @@
{
lib,
buildNpmPackage,
fetchzip,
}:
buildNpmPackage rec {
pname = "namecheap-mcp";
version = "1.0.1";
src = fetchzip {
# npm package is "namecheap-api-mcp"; pname is the Nix attr name.
url = "https://registry.npmjs.org/namecheap-api-mcp/-/namecheap-api-mcp-${version}.tgz";
hash = "sha256-+/MwPgrxJiWmObkLF30YqXWGwesbp98qzGiv/W86WYw=";
};
npmDepsHash = "sha256-l0i/zzy3I29ilF8vZMBrTlBQdvrISC9Vr/iQJXDVKmw=";
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
# dist/ is already built in the npm tarball
dontNpmBuild = true;
meta = with lib; {
description = "Namecheap API MCP server - DNS, domains, contacts, and registrar lock management";
homepage = "https://github.com/lanternrow/namecheap-api-mcp";
license = licenses.mit;
mainProgram = "namecheap-api-mcp";
platforms = platforms.all;
};
}
File diff suppressed because it is too large. Load diff
+32
View File
@@ -0,0 +1,32 @@
{
lib,
buildNpmPackage,
fetchzip,
}:
buildNpmPackage rec {
pname = "navidrome-mcp";
version = "2.3.0";
src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-npUgzJpB0DaFuzU/1SuOIOVvd2gqZtb1Kos7NC8/Pd4=";
};
npmDepsHash = "sha256-58OZ4qVfeN+0LVzcSJQHQqiY66gPSGWI6ht/4fMp0oQ=";
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
# dist/ is already built in the npm tarball
dontNpmBuild = true;
meta = with lib; {
description = "MCP server for Navidrome - search music library, playlists, radio stations, and playback control";
homepage = "https://github.com/Blakeem/Navidrome-MCP";
license = licenses.mit;
mainProgram = "navidrome-mcp";
platforms = platforms.all;
};
}
+23 -9
View File
@@ -3,9 +3,18 @@
python3Packages, python3Packages,
fetchFromGitHub, fetchFromGitHub,
fetchPypi, fetchPypi,
fetchurl,
}: }:
let let
# Upstream >=0.185.0 was ported to the MCP SDK 2.x API
# (`mcp.server.mcpserver.MCPServer`). nixpkgs pins the SDK at 1.x, so use the
# per-package 2.x stack from packages/lib/mcp2.nix.
mcp2 = import ../lib/mcp2.nix {
inherit lib fetchurl;
pythonPackages = python3Packages;
};
# Helper: buildPythonPackage for a wheel fetched from PyPI # Helper: buildPythonPackage for a wheel fetched from PyPI
# CRITICAL: pname MUST use underscores, not hyphens, for PyPI wheel filenames # CRITICAL: pname MUST use underscores, not hyphens, for PyPI wheel filenames
fetchWheel = { pname, version, hash, propagatedBuildInputs ? [ ], meta ? { } }: fetchWheel = { pname, version, hash, propagatedBuildInputs ? [ ], meta ? { } }:
@@ -132,22 +141,24 @@ let
in in
python3Packages.buildPythonApplication rec { python3Packages.buildPythonApplication rec {
pname = "nextcloud-mcp-server"; pname = "nextcloud-mcp-server";
version = "0.121.3"; version = "0.195.0";
pyproject = true; pyproject = true;
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "cbcoutinho"; owner = "cbcoutinho";
repo = "nextcloud-mcp-server"; repo = "nextcloud-mcp-server";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-U6BG5anTEllNnJzta4VV6QOfn86+N15q6v/SfLGM7mc="; hash = "sha256-k8WeVe1ScWN7Lh+trowzn+UVuQBgfgI1Klfz4w5F8do=";
}; };
build-system = [ python3Packages.uv-build ]; build-system = [ python3Packages.uv-build ];
dependencies = with python3Packages; [ dependencies = with python3Packages; [
mcp httpx pillow icalendar pydantic click pyjwt mcp2.mcp httpx pillow icalendar pydantic click pyjwt
aiosqlite alembic authlib fastembed anthropic boto3 starlette aiosqlite alembic authlib fastembed anthropic boto3 starlette
jinja2 markdownify pymupdf pymupdf4llm openai dynaconf sqlalchemy jinja2 markdownify pymupdf pymupdf4llm openai dynaconf sqlalchemy
anyio uvicorn cryptography lxml botocore numpy packaging pypdfium2
olefile openpyxl python-docx python-pptx
pythonvcard4 prometheus-client python-json-logger importlib-metadata pythonvcard4 prometheus-client python-json-logger importlib-metadata
googleapis-common-protos opentelemetry-api opentelemetry-proto googleapis-common-protos opentelemetry-api opentelemetry-proto
@@ -161,14 +172,11 @@ python3Packages.buildPythonApplication rec {
postPatch = '' postPatch = ''
substituteInPlace pyproject.toml \ substituteInPlace pyproject.toml \
--replace-quiet '"uv_build>=0.11.5,<0.12.0"' '"uv_build"' \ --replace-quiet '"uv_build>=0.12.1,<0.13.0"' '"uv_build"' \
--replace-quiet '" (>=1.27,<1.28)"' '""' \ --replace-quiet '" (>=2.1,<3)"' '""' \
--replace-quiet '" (>=0.28.1,<0.29.0)"' '""' \ --replace-quiet '" (>=0.28.1,<0.29.0)"' '""' \
--replace-quiet '" (>=10.3.0,<12.0.0)"' '""' \ --replace-quiet '" (>=10.3.0,<12.0.0)"' '""' \
--replace-quiet '" (>=7.1.0,<7.2.0)"' '""' \ --replace-quiet '" (>=7.3.0,<7.4.0)"' '""' \
--replace-quiet '" (>=3.0.1,<4.0)"' '""' \
--replace-quiet '" (>=1.28.2,<2.0.0)"' '""' \
--replace-quiet '" (>=2.0.0,<3.0.0)"' '""' \
--replace-quiet '">=3.0.1,<4.0"' '""' \ --replace-quiet '">=3.0.1,<4.0"' '""' \
--replace-quiet '">=3.8.0,<4.0"' '""' \ --replace-quiet '">=3.8.0,<4.0"' '""' \
--replace-quiet '">=3.2.13,<4.0"' '""' \ --replace-quiet '">=3.2.13,<4.0"' '""' \
@@ -188,6 +196,12 @@ python3Packages.buildPythonApplication rec {
dontCheckRuntimeDeps = true; dontCheckRuntimeDeps = true;
catchConflicts = false; catchConflicts = false;
pythonImportsCheck = [
"nextcloud_mcp_server"
"mcp"
"pymupdf"
];
meta = with lib; { meta = with lib; {
description = "MCP server for Nextcloud — files, calendar, contacts, notes, deck, talk, sharing"; description = "MCP server for Nextcloud — files, calendar, contacts, notes, deck, talk, sharing";
homepage = "https://github.com/cbcoutinho/nextcloud-mcp-server"; homepage = "https://github.com/cbcoutinho/nextcloud-mcp-server";
File diff suppressed because it is too large. Load diff
+46
View File
@@ -0,0 +1,46 @@
{
lib,
buildNpmPackage,
fetchFromGitHub,
makeWrapper,
nodejs,
}:
buildNpmPackage rec {
pname = "ovhcloud-mcp";
version = "2.0.1";
src = fetchFromGitHub {
owner = "hlebtkachenko";
repo = "ovhcloud-mcp";
rev = "ccadc85bd4aab35434a24732bc77ef2b6f100b4c";
hash = "sha256-FYJSKOU/lCutGCUJhaAtKWBLJOBUyWvdkSnMWbxwhI4=";
};
npmDepsHash = "sha256-myeqksb3mjJ1GJej/4D7iuyOOTsGJBdkka9zUpH+flY=";
nativeBuildInputs = [ makeWrapper ];
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
# dist/ is NOT committed to the repo; buildNpmPackage runs "npm run build"
# (tsc) by default, which emits dist/index.js.
#
# The upstream package.json has NO "bin" field, so buildNpmPackage produces
# no executable. Create a wrapper that runs node dist/index.js.
postInstall = ''
mkdir -p $out/bin
makeWrapper ${nodejs}/bin/node $out/bin/ovhcloud-mcp \
--add-flags "$out/lib/node_modules/ovhcloud-mcp/dist/index.js"
'';
meta = with lib; {
description = "OVHcloud MCP server - VPS, domains, DNS, billing, SSH, and API explorer";
homepage = "https://github.com/hlebtkachenko/ovhcloud-mcp";
license = licenses.mit;
mainProgram = "ovhcloud-mcp";
platforms = platforms.all;
};
}
+40
View File
@@ -0,0 +1,40 @@
{
lib,
python3Packages,
fetchFromGitea,
}:
python3Packages.buildPythonApplication rec {
pname = "pdf-mcp";
version = "0.1.1-unstable-2026-07-30";
src = fetchFromGitea {
domain = "gitea.bueso.eu";
owner = "hermes-agent";
repo = "pdf-mcp";
rev = "356e476d1dd0d2d9eb866df7c6f82a21018351c8";
hash = "sha256-whfXjMQIUt94VEeo6Fdvdr96WV3HNNNVuL1gtkt4g7M=";
};
pyproject = true;
build-system = with python3Packages; [ hatchling ];
dependencies = with python3Packages; [
mcp
pymupdf
pymupdf4llm
];
doCheck = false;
pythonImportsCheck = [ "pdf_mcp" ];
meta = with lib; {
description = "MCP server for reading, rendering, and searching PDF files (PyMuPDF)";
homepage = "https://gitea.bueso.eu/hermes-agent/pdf-mcp";
license = licenses.mit;
mainProgram = "pdf-mcp";
platforms = platforms.all;
maintainers = [ ];
};
}
+3740 -1379
View File
File diff suppressed because it is too large. Load diff
+13 -4
View File
@@ -2,24 +2,33 @@
lib, lib,
buildNpmPackage, buildNpmPackage,
fetchzip, fetchzip,
autoPatchelfHook,
stdenv,
}: }:
buildNpmPackage rec { buildNpmPackage rec {
pname = "pdf-reader-mcp"; pname = "pdf-reader-mcp";
version = "2.3.0"; version = "4.1.3";
src = fetchzip { src = fetchzip {
url = "https://registry.npmjs.org/@sylphx/pdf-reader-mcp/-/pdf-reader-mcp-${version}.tgz"; url = "https://registry.npmjs.org/@sylphx/pdf-reader-mcp/-/pdf-reader-mcp-${version}.tgz";
hash = "sha256-nLtTYE5pSLigAQqsWx3TvntOign30vUJ4jaCDScm9oY="; hash = "sha256-oxEL7lwEJdhPoA4p/HiQeMp9TP7al1z2KpiPTUPqn6c=";
}; };
npmDepsHash = "sha256-Z+C1pGJb1gCUR4qlM9ZSjhevqWPdCOVhL631GFaehbc="; npmDepsHash = "sha256-3XohzskNUMGkIgZvQie9DA8DfHxJZGD86n+uVZPxMdg=";
# The runtime launcher spawns a prebuilt Rust binary shipped in the matching
# optional platform package. On Linux it is dynamically linked against glibc,
# so its interpreter and RPATH must be patched for NixOS.
nativeBuildInputs = lib.optional stdenv.isLinux autoPatchelfHook;
buildInputs = lib.optional stdenv.isLinux stdenv.cc.cc.lib;
postPatch = '' postPatch = ''
cp ${./package-lock.json} package-lock.json cp ${./package-lock.json} package-lock.json
''; '';
# dist/ is already built in the npm tarball # dist/ and the native Rust binary ship prebuilt; there is no build step.
dontNpmBuild = true; dontNpmBuild = true;
meta = with lib; { meta = with lib; {
+181 -35
View File
@@ -1,5 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Pinchflat MCP server — exposes the Pinchflat REST API as MCP tools.""" """Pinchflat MCP server — exposes the Pinchflat REST API as MCP tools.
Tool tiers
----------
Two tiers of tools exist and only one is exposed by default:
* ``subscriptions`` (default) — sources, media items, media profiles, tasks.
This is what an assistant needs to manage subscriptions and their files.
* ``admin`` — global application state: ``get_settings``, ``get_app_info``.
These describe the installation rather than a subscription and are NOT
exposed unless explicitly enabled.
Select the tier with ``PINCHFLAT_MCP_TOOLSET``:
PINCHFLAT_MCP_TOOLSET=subscriptions # default
PINCHFLAT_MCP_TOOLSET=admin
PINCHFLAT_MCP_TOOLSET=all
Responses are scrubbed of secrets on the way out (see ``_scrub``), because the
Pinchflat settings record embeds the application's own ``api_token``. Exposing
it to a tool caller hands out a credential that outlives the tool session, so
secret-bearing keys are redacted at the transport boundary rather than left to
individual tools to remember.
"""
import os import os
import sys import sys
@@ -20,13 +43,76 @@ HEADERS = {"Authorization": f"Bearer {PINCHFLAT_TOKEN}", "Content-Type": "applic
mcp = FastMCP("pinchflat") mcp = FastMCP("pinchflat")
# --- Tool tiers -------------------------------------------------------------
_TIERS = {
"subscriptions": {"subscriptions"},
"admin": {"subscriptions", "admin"},
"all": {"subscriptions", "admin"},
}
_TOOLSET = os.environ.get("PINCHFLAT_MCP_TOOLSET", "subscriptions").strip().lower()
ENABLED_TIERS = _TIERS.get(_TOOLSET, _TIERS["subscriptions"])
def tool(tier: str = "subscriptions"):
"""Register a tool only when its tier is enabled."""
def decorator(fn):
if tier in ENABLED_TIERS:
return mcp.tool()(fn)
return fn
return decorator
# --- Response scrubbing -----------------------------------------------------
# Keys whose values are credentials and must never cross the tool boundary.
SECRET_KEYS = frozenset(
{
"api_token",
"api_key",
"token",
"secret",
"password",
"password_hash",
"youtube_api_key",
"apprise_server",
}
)
REDACTED = "[redacted]"
def _scrub(obj: Any) -> Any:
"""Recursively redact credential-bearing values from an API response."""
if isinstance(obj, dict):
out = {}
for k, v in obj.items():
if isinstance(k, str) and k.strip().lower() in SECRET_KEYS and v:
out[k] = REDACTED
else:
out[k] = _scrub(v)
return out
if isinstance(obj, list):
return [_scrub(v) for v in obj]
return obj
# --- HTTP helpers -----------------------------------------------------------
def _unwrap(r: httpx.Response) -> Any:
"""Decode a response and scrub secrets before it reaches the caller."""
body = r.json()
return _scrub(body)
def _get(path: str, params: Optional[dict] = None) -> Any: def _get(path: str, params: Optional[dict] = None) -> Any:
"""Make a GET request to the Pinchflat API.""" """Make a GET request to the Pinchflat API."""
with httpx.Client(timeout=30) as client: with httpx.Client(timeout=30) as client:
r = client.get(f"{BASE}{path}", headers=HEADERS, params=params) r = client.get(f"{BASE}{path}", headers=HEADERS, params=params)
r.raise_for_status() r.raise_for_status()
return r.json() return _unwrap(r)
def _post(path: str, json: Optional[dict] = None) -> Any: def _post(path: str, json: Optional[dict] = None) -> Any:
@@ -34,7 +120,7 @@ def _post(path: str, json: Optional[dict] = None) -> Any:
with httpx.Client(timeout=30) as client: with httpx.Client(timeout=30) as client:
r = client.post(f"{BASE}{path}", headers=HEADERS, json=json) r = client.post(f"{BASE}{path}", headers=HEADERS, json=json)
r.raise_for_status() r.raise_for_status()
return r.json() return _unwrap(r)
def _put(path: str, json: dict) -> Any: def _put(path: str, json: dict) -> Any:
@@ -42,38 +128,40 @@ def _put(path: str, json: dict) -> Any:
with httpx.Client(timeout=30) as client: with httpx.Client(timeout=30) as client:
r = client.put(f"{BASE}{path}", headers=HEADERS, json=json) r = client.put(f"{BASE}{path}", headers=HEADERS, json=json)
r.raise_for_status() r.raise_for_status()
return r.json() return _unwrap(r)
def _delete(path: str) -> Any: def _delete(path: str, params: Optional[dict] = None, timeout: int = 30) -> Any:
"""Make a DELETE request to the Pinchflat API.""" """Make a DELETE request to the Pinchflat API."""
with httpx.Client(timeout=30) as client: with httpx.Client(timeout=timeout) as client:
r = client.delete(f"{BASE}{path}", headers=HEADERS) r = client.delete(f"{BASE}{path}", headers=HEADERS, params=params)
r.raise_for_status() r.raise_for_status()
return r.json() return _unwrap(r)
# --- Sources --- # --- Sources ---
@mcp.tool()
@tool()
def list_sources() -> dict: def list_sources() -> dict:
"""List all configured media sources (YouTube channels, playlists, etc.).""" """List all configured media sources (YouTube channels, playlists, etc.)."""
return _get("/sources") return _get("/sources")
@mcp.tool() @tool()
def get_source(source_id: int) -> dict: def get_source(source_id: int) -> dict:
"""Get details of a specific source by ID, including its pending tasks.""" """Get details of a specific source by ID, including its pending tasks."""
return _get(f"/sources/{source_id}") return _get(f"/sources/{source_id}")
@mcp.tool() @tool()
def create_source( def create_source(
original_url: str, original_url: str,
media_profile_id: int, media_profile_id: int,
collection_type: str = "channel", collection_type: str = "channel",
custom_name: Optional[str] = None, custom_name: Optional[str] = None,
download_cutoff_date: Optional[str] = None, download_cutoff_date: Optional[str] = None,
retention_period_days: Optional[int] = None,
) -> dict: ) -> dict:
"""Create a new media source (e.g. add a YouTube channel to monitor). """Create a new media source (e.g. add a YouTube channel to monitor).
@@ -83,6 +171,8 @@ def create_source(
collection_type: 'channel', 'playlist', or 'video' collection_type: 'channel', 'playlist', or 'video'
custom_name: Optional custom name for the source custom_name: Optional custom name for the source
download_cutoff_date: Only download media after this date (YYYY-MM-DD) download_cutoff_date: Only download media after this date (YYYY-MM-DD)
retention_period_days: Delete downloaded media this many days after
publication; omit to keep everything
""" """
source = { source = {
"media_profile_id": media_profile_id, "media_profile_id": media_profile_id,
@@ -93,18 +183,30 @@ def create_source(
source["custom_name"] = custom_name source["custom_name"] = custom_name
if download_cutoff_date: if download_cutoff_date:
source["download_cutoff_date"] = download_cutoff_date source["download_cutoff_date"] = download_cutoff_date
if retention_period_days is not None:
source["retention_period_days"] = retention_period_days
return _post("/sources", json={"source": source}) return _post("/sources", json={"source": source})
@mcp.tool() @tool()
def update_source( def update_source(
source_id: int, source_id: int,
custom_name: Optional[str] = None, custom_name: Optional[str] = None,
download_cutoff_date: Optional[str] = None, download_cutoff_date: Optional[str] = None,
enabled: Optional[bool] = None, enabled: Optional[bool] = None,
index_frequency_minutes: Optional[int] = None, index_frequency_minutes: Optional[int] = None,
retention_period_days: Optional[int] = None,
prevent_culling: Optional[bool] = None,
download_media: Optional[bool] = None,
) -> dict: ) -> dict:
"""Update an existing source. Only provided fields will be changed.""" """Update an existing source. Only provided fields will be changed.
Args:
retention_period_days: Delete downloaded media this many days after
publication (0 keeps everything)
prevent_culling: If True, never delete this source's downloads
download_media: If False, index the source but download nothing
"""
updates = {} updates = {}
if custom_name is not None: if custom_name is not None:
updates["custom_name"] = custom_name updates["custom_name"] = custom_name
@@ -114,34 +216,46 @@ def update_source(
updates["enabled"] = enabled updates["enabled"] = enabled
if index_frequency_minutes is not None: if index_frequency_minutes is not None:
updates["index_frequency_minutes"] = index_frequency_minutes updates["index_frequency_minutes"] = index_frequency_minutes
if retention_period_days is not None:
updates["retention_period_days"] = retention_period_days
if prevent_culling is not None:
updates["prevent_culling"] = prevent_culling
if download_media is not None:
updates["download_media"] = download_media
if not updates:
return {"data": {"message": "No fields to update."}}
return _put(f"/sources/{source_id}", json={"source": updates}) return _put(f"/sources/{source_id}", json={"source": updates})
@mcp.tool() @tool()
def delete_source(source_id: int) -> dict: def delete_source(source_id: int) -> dict:
"""Delete a source (marks it for deletion).""" """Delete a source (marks it for deletion).
Note: this removes the source and its media records, but does NOT delete
the files from disk. Delete the media items first if space is the goal.
"""
return _delete(f"/sources/{source_id}") return _delete(f"/sources/{source_id}")
@mcp.tool() @tool()
def force_download_pending(source_id: int) -> dict: def force_download_pending(source_id: int) -> dict:
"""Force download of all pending media items for a source.""" """Force download of all pending media items for a source."""
return _post(f"/sources/{source_id}/force_download_pending") return _post(f"/sources/{source_id}/force_download_pending")
@mcp.tool() @tool()
def force_redownload(source_id: int) -> dict: def force_redownload(source_id: int) -> dict:
"""Force re-download of all existing media for a source.""" """Force re-download of all existing media for a source."""
return _post(f"/sources/{source_id}/force_redownload") return _post(f"/sources/{source_id}/force_redownload")
@mcp.tool() @tool()
def force_index(source_id: int) -> dict: def force_index(source_id: int) -> dict:
"""Force re-index of a source.""" """Force re-index of a source."""
return _post(f"/sources/{source_id}/force_index") return _post(f"/sources/{source_id}/force_index")
@mcp.tool() @tool()
def force_metadata_refresh(source_id: int) -> dict: def force_metadata_refresh(source_id: int) -> dict:
"""Force metadata refresh for a source.""" """Force metadata refresh for a source."""
return _post(f"/sources/{source_id}/force_metadata_refresh") return _post(f"/sources/{source_id}/force_metadata_refresh")
@@ -149,42 +263,72 @@ def force_metadata_refresh(source_id: int) -> dict:
# --- Media Items --- # --- Media Items ---
@mcp.tool()
def list_media(source_id: Optional[int] = None, limit: int = 50) -> dict: @tool()
"""List media items. Optionally filter by source_id.""" def list_media(
params = {} source_id: Optional[int] = None,
limit: int = 50,
offset: int = 0,
downloaded: Optional[bool] = None,
) -> dict:
"""List media items with pagination. Optionally filter by source_id or download status.
Args:
source_id: Filter to a specific source
limit: Max items to return (default 50)
offset: Skip first N items for pagination (default 0)
downloaded: If True, only return downloaded media; if False, only undownloaded
"""
params = {"limit": limit, "offset": offset}
if source_id: if source_id:
params["source_id"] = source_id params["source_id"] = source_id
if downloaded is not None:
params["downloaded"] = "true" if downloaded else "false"
return _get("/media", params=params) return _get("/media", params=params)
@mcp.tool() @tool()
def search_media(query: str) -> dict: def search_media(query: str) -> dict:
"""Search media items by title or description.""" """Search media items by title or description."""
return _get("/media/search", params={"q": query}) return _get("/media/search", params={"q": query})
@mcp.tool() @tool()
def get_media(media_id: int) -> dict: def get_media(media_id: int) -> dict:
"""Get details of a specific media item.""" """Get details of a specific media item."""
return _get(f"/media/{media_id}") return _get(f"/media/{media_id}")
@mcp.tool() @tool()
def force_download_media(media_id: int) -> dict: def force_download_media(media_id: int) -> dict:
"""Force download of a specific media item.""" """Force download of a specific media item."""
return _post(f"/media/{media_id}/force_download") return _post(f"/media/{media_id}/force_download")
@tool()
def delete_media(media_id: int, prevent_download: bool = False) -> dict:
"""Delete a media item's downloaded files from disk.
Args:
media_id: The media item ID to delete
prevent_download: If True, prevent the item from being re-downloaded in the future
"""
params = {}
if prevent_download:
params["prevent_download"] = "true"
return _delete(f"/media/{media_id}", params=params, timeout=120)
# --- Media Profiles --- # --- Media Profiles ---
@mcp.tool()
@tool()
def list_media_profiles() -> dict: def list_media_profiles() -> dict:
"""List all media profiles (download quality/container settings).""" """List all media profiles (download quality/container settings)."""
return _get("/media_profiles") return _get("/media_profiles")
@mcp.tool() @tool()
def get_media_profile(profile_id: int) -> dict: def get_media_profile(profile_id: int) -> dict:
"""Get details of a specific media profile.""" """Get details of a specific media profile."""
return _get(f"/media_profiles/{profile_id}") return _get(f"/media_profiles/{profile_id}")
@@ -192,7 +336,8 @@ def get_media_profile(profile_id: int) -> dict:
# --- Tasks --- # --- Tasks ---
@mcp.tool()
@tool()
def list_tasks(source_id: Optional[int] = None) -> dict: def list_tasks(source_id: Optional[int] = None) -> dict:
"""List all tasks. Optionally filter by source_id.""" """List all tasks. Optionally filter by source_id."""
params = {} params = {}
@@ -201,21 +346,22 @@ def list_tasks(source_id: Optional[int] = None) -> dict:
return _get("/tasks", params=params) return _get("/tasks", params=params)
@mcp.tool() @tool()
def get_task(task_id: int) -> dict: def get_task(task_id: int) -> dict:
"""Get details of a specific task.""" """Get details of a specific task."""
return _get(f"/tasks/{task_id}") return _get(f"/tasks/{task_id}")
# --- Settings & Info --- # --- Settings & Info (admin tier — not exposed by default) ---
@mcp.tool()
@tool("admin")
def get_settings() -> dict: def get_settings() -> dict:
"""Get current Pinchflat settings.""" """Get current Pinchflat settings (credential values are redacted)."""
return _get("/settings") return _get("/settings")
@mcp.tool() @tool("admin")
def get_app_info() -> dict: def get_app_info() -> dict:
"""Get Pinchflat app info (version, environment, etc.).""" """Get Pinchflat app info (version, environment, etc.)."""
return _get("/app_info") return _get("/app_info")
@@ -223,4 +369,4 @@ def get_app_info() -> dict:
if __name__ == "__main__": if __name__ == "__main__":
transport = os.environ.get("PINCHFLAT_MCP_TRANSPORT", "stdio") transport = os.environ.get("PINCHFLAT_MCP_TRANSPORT", "stdio")
mcp.run(transport=transport if transport in ("stdio", "http") else "stdio") mcp.run(transport=transport if transport in ("stdio", "http") else "stdio")
+86
View File
@@ -0,0 +1,86 @@
{
lib,
python313Packages,
fetchFromGitHub,
fetchPypi,
}:
# Upstream pins `requires-python = ">=3.11,<3.14"`, so this must NOT be built
# with the default `python3Packages` (which is 3.14.x on nixos-unstable and
# would be rejected by the build backend's requires-python check). Pin the
# 3.13 package set explicitly.
#
# `fastmcp[tasks]` is a HARD runtime dependency: `src/ssh_mcp/tools/exec_tools.py`
# does `from fastmcp.server.tasks import TaskConfig` at import time, and the
# decorator validates at import. nixpkgs' `fastmcp` installs only the
# client+server extras (optional-dependencies are passthru-only in nix and
# install nothing), so the `tasks` extra's backend -- pydocket -- has to be
# listed explicitly or the server crashes at startup.
let
# Internal dependency: upstream pins `unidiff>=0.7.5,<0.8.0`, but nixpkgs only
# carries 1.0.0. The 0.7.x API surface used by the ssh_edit tool
# (`PatchSet.from_string` and hunk iteration) is unchanged in 1.0.0, but
# buildPythonApplication's runtime-dep check enforces the specifier, so the
# pinned version still has to be provided. 0.7.5 is sdist-only on PyPI
# (no wheel), so this is a source build.
unidiff = python313Packages.buildPythonPackage rec {
pname = "unidiff";
version = "0.7.5";
pyproject = true;
build-system = [ python313Packages.setuptools ];
src = fetchPypi {
inherit pname version;
hash = "sha256-Ll8BYgUiSJRrnwlwpA6eEkI2v4bIK3CCEUOm/B3qJXQ=";
};
doCheck = false;
pythonImportsCheck = [ "unidiff" ];
meta = with lib; {
description = "Unified diff parsing and application";
homepage = "https://github.com/matiasb/python-unidiff";
license = licenses.mit;
};
};
in
python313Packages.buildPythonApplication rec {
pname = "python-ssh-mcp";
version = "1.5.2";
pyproject = true;
src = fetchFromGitHub {
owner = "Nightreaver";
repo = "python-ssh-mcp";
rev = "73c00c9b0d26add59798bc7b39dff88ac547b15a"; # v1.5.2
hash = "sha256-OfwOiBan5sqGEs/zzHHC4vJdcALOLWaIr8uENCbQ4FU=";
};
build-system = [ python313Packages.hatchling ];
dependencies = with python313Packages; [
fastmcp
pydocket # the fastmcp[tasks] extra
asyncssh
pydantic
pydantic-settings
unidiff
];
# Upstream ships no importable test suite in the sdist workflow used here
# (its 1649 tests need the dockerized sshd fixtures); skip and rely on the
# import check + the smoke test below.
doCheck = false;
pythonImportsCheck = [ "ssh_mcp" ];
meta = with lib; {
description = "SSH MCP server for multi-host systemd/docker/apt administration (read-only by default, tiered)";
homepage = "https://github.com/Nightreaver/python-ssh-mcp";
license = licenses.gpl3Only;
mainProgram = "ssh-mcp";
platforms = platforms.all;
maintainers = [ ];
};
}
+1267
View File
File diff suppressed because it is too large. Load diff
+38
View File
@@ -0,0 +1,38 @@
{
lib,
buildNpmPackage,
fetchzip,
}:
buildNpmPackage rec {
pname = "romm-mcp";
version = "1.0.5";
src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-SOYzG0CtNSvIrxOYJq3UDXRlXylqbDjo2f5qfpbiYmI=";
};
# Upstream publishes no package-lock.json (only package.json), and
# buildNpmPackage requires one to compute npmDepsHash. This lock is generated
# from the published tarball itself -- `npm install --package-lock-only
# --ignore-scripts` inside the extracted 1.0.5 package -- so it locks exactly
# the dependency tree that shipped with this version.
npmDepsHash = "sha256-oSlECdIf2ZP8ZZhOUChkcJAmUTj6TFTdeTJLY5RwLGo=";
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
# dist/*.js (tsc output) ships inside the npm tarball, so there is nothing to
# compile here -- same arrangement as navidrome-mcp and n8n-mcp.
dontNpmBuild = true;
meta = with lib; {
description = "MCP server for RomM: browse, search and administrate a self-hosted ROM library";
homepage = "https://github.com/rodrigosiviero/romm-mcp";
license = licenses.mit;
mainProgram = pname;
platforms = platforms.all;
};
}
+7 -3
View File
@@ -21,10 +21,14 @@ stdenv.mkDerivation {
version = "0.1.0-unstable"; version = "0.1.0-unstable";
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "jmagar"; # Original upstream (jmagar/sabnzbd-mcp) was deleted from GitHub.
# Using jasonpaulso's fork, which preserves the same file layout
# (client.py + sabnzbd-mcp-server.py at repo root) and the
# /sabnzbd/api string that postPatch below rewrites.
owner = "jasonpaulso";
repo = "sabnzbd-mcp"; repo = "sabnzbd-mcp";
rev = "9d691ed70b72397fddec03ca4ec68c81b30c79c3"; rev = "8755e70e2634c02c24471f32962224833877dc81";
hash = "sha256-1sEnHH40cDVT5oiZD2Xywl7S9vJThii/ce+6r+XmpSw="; hash = "sha256-y46jTXBsOXMoFnnwyv0n+8uq2Z/kw2HbnZC85knWpUw=";
}; };
nativeBuildInputs = [ makeWrapper ]; nativeBuildInputs = [ makeWrapper ];
+49
View File
@@ -0,0 +1,49 @@
{
lib,
stdenv,
fetchFromGitHub,
makeWrapper,
python3,
}:
let
python = python3.withPackages (
ps: with ps; [
fastmcp
httpx
]
);
in
stdenv.mkDerivation rec {
pname = "slskd-mcp";
version = "0.1.0-unstable-2026-07-28";
src = fetchFromGitHub {
owner = "abl030";
repo = "slskd-mcp";
rev = "08e4373f41fddcceff8ea3240323e0c6258a373e";
hash = "sha256-ELUoAOOWmNoxoTjPxxAY00exN56lngSPs4wtn/GadQU=";
};
nativeBuildInputs = [ makeWrapper ];
# generated/server.py is tracked in git; no build step needed.
dontBuild = true;
installPhase = ''
runHook preInstall
mkdir -p $out/lib/slskd-mcp $out/bin
cp -r $src/generated $out/lib/slskd-mcp/
makeWrapper ${python}/bin/fastmcp $out/bin/slskd-mcp \
--add-flags "run $out/lib/slskd-mcp/generated/server.py"
runHook postInstall
'';
meta = with lib; {
description = "MCP server for slskd (Soulseek daemon) - search, download, browse peers, private messages";
homepage = "https://github.com/abl030/slskd-mcp";
license = licenses.mit;
mainProgram = "slskd-mcp";
platforms = platforms.all;
};
}
+3 -3
View File
@@ -6,13 +6,13 @@
let let
wheel = fetchurl { wheel = fetchurl {
url = "https://files.pythonhosted.org/packages/6b/9c/dc73804f0f62208d59794080d009fb86b7ac6e9d4b5ad452791355dcfb51/transmission_mcp-1.0.1-py3-none-any.whl"; url = "https://files.pythonhosted.org/packages/7e/81/fb2a2f2073d22b0a18c4c96b9c9ff386c3d0b49b6263097bf646d8b72800/transmission_mcp-1.1.0-py3-none-any.whl";
hash = "sha256-1TKb75iNKgMqw0YVZ+rDEac4IyCuj94y1OMzqQROCrI="; hash = "sha256-J22L3lY+XVdLdZeu8KuFdly0BkFUoAbX4GE/9a5kKSU=";
}; };
in in
python3Packages.buildPythonApplication rec { python3Packages.buildPythonApplication rec {
pname = "transmission-mcp"; pname = "transmission-mcp";
version = "1.0.1"; version = "1.1.0";
format = "wheel"; format = "wheel";
src = wheel; src = wheel;
+29
View File
@@ -0,0 +1,29 @@
{
lib,
stdenv,
fetchzip,
}:
stdenv.mkDerivation rec {
pname = "yarr";
version = "2.2.2";
src = fetchzip {
url = "https://github.com/dinglebear-ai/yarr/releases/download/v${version}/yarr-x86_64.tar.gz";
hash = "sha256-2L1slCN/zigS4eMIXK7vexAyLNQZs8Q4BMdA7GHIIiM=";
};
installPhase = ''
mkdir -p $out/bin
cp yarr $out/bin/
chmod +x $out/bin/yarr
'';
meta = with lib; {
description = "Rust MCP server for media automation fleets - Sonarr, Radarr, Prowlarr, Jellyfin, SABnzbd, qBittorrent, Plex";
homepage = "https://github.com/dinglebear-ai/yarr";
license = licenses.agpl3Only;
mainProgram = "yarr";
platforms = [ "x86_64-linux" ];
};
}
+53 -24
View File
@@ -3,13 +3,14 @@
python3Packages, python3Packages,
fetchFromGitHub, fetchFromGitHub,
fetchPypi, fetchPypi,
fetchurl,
}: }:
let let
# Internal dependency: pyzotero (not in nixpkgs) # Internal dependency: pyzotero (not in nixpkgs)
pyzotero = python3Packages.buildPythonPackage rec { pyzotero = python3Packages.buildPythonPackage rec {
pname = "pyzotero"; pname = "pyzotero";
version = "1.11.0"; version = "1.14.0";
format = "wheel"; format = "wheel";
src = fetchPypi { src = fetchPypi {
@@ -17,7 +18,7 @@ let
format = "wheel"; format = "wheel";
dist = "py3"; dist = "py3";
python = "py3"; python = "py3";
hash = "sha256-VDSMczLHnNGs9THyVI/I2MbqMeiemMQgIy2XoZOtlbI="; hash = "sha256-OUoU8HTEFwVfzqopcLWUzpJaYJTJ09zIpzTgdCbs9BU=";
}; };
propagatedBuildInputs = with python3Packages; [ propagatedBuildInputs = with python3Packages; [
@@ -36,38 +37,66 @@ let
license = licenses.blueOak100; license = licenses.blueOak100;
}; };
}; };
# pdf-inspector: Rust with prebuilt abi3 wheels, not in nixpkgs.
# Pinned exactly (pre-1.0); no Python dependencies of its own.
pdf-inspector = python3Packages.buildPythonPackage rec {
pname = "pdf-inspector";
version = "0.2.6";
format = "wheel";
src = fetchurl {
url = "https://files.pythonhosted.org/packages/59/af/be72ab2bd310b6532e4311cf06175eb722802ccea29dbd9281f66c645bd8/pdf_inspector-0.2.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";
hash = "sha256-33bdEAUEtwXOku8sZo8VLwUnfZQqvZSn0/JSy1RI1W0=";
};
doCheck = false;
pythonImportsCheck = [ "pdf_inspector" ];
meta = with lib; {
description = "PDF text extraction (Rust, abi3 wheels)";
homepage = "https://github.com/54yyyu/pdf-inspector";
license = licenses.mit;
};
};
in in
python3Packages.buildPythonApplication rec { python3Packages.buildPythonApplication rec {
pname = "zotero-mcp"; pname = "zotero-mcp";
version = "0.5.0"; version = "0.13.0";
pyproject = true; pyproject = true;
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "54yyyu"; owner = "54yyyu";
repo = "zotero-mcp"; repo = "zotero-mcp";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-ioy6bdP+jFioUGBKiiYtD4nbdFe6v2E3nCi6s9+dTT0="; hash = "sha256-xL54SbaAczkwk4tkan+wVVTv9ACKIC6hcAADI2GTnxk=";
}; };
build-system = with python3Packages; [ build-system = with python3Packages; [
hatchling hatchling
]; ];
# Core dependencies # Core dependencies (from pyproject.toml v0.13.0)
dependencies = with python3Packages; [ dependencies = with python3Packages; [
pyzotero pyzotero
mcp
python-dotenv python-dotenv
markitdown pdf-inspector
markdownify
pydantic pydantic
requests requests
fastmcp fastmcp
httpx
unidecode unidecode
# PDF support bibtexparser
# Runtime deps for the annotation/layout/read tools (upstream "pdf" extra).
# nix's optional-dependencies is passthru-only and installs nothing, so
# anything needed at runtime must live in this list.
pymupdf pymupdf
ebooklib ebooklib
pdfplumber # Runtime deps for the semantic-search tools (upstream "semantic" extra).
# Semantic search # zotero_mcp.semantic_search imports chromadb unconditionally at module
# scope (embeddings/providers/__init__.py, chroma_client.py), so these are
# hard requirements, not truly optional at runtime.
chromadb chromadb
sentence-transformers sentence-transformers
openai openai
@@ -75,20 +104,20 @@ python3Packages.buildPythonApplication rec {
tiktoken tiktoken
]; ];
# Optional: semantic search dependencies # `zotero_get_pdf_outline` reads a PDF's table of contents in a throwaway
optional-dependencies = with python3Packages; { # `sys.executable -c ...` child (tools/write.py, _extract_pdf_toc). Under the
semantic = [ # Nix wrapper this package's dependencies are injected into the *parent*
chromadb # interpreter via site.addsitedir, never exported as PYTHONPATH, so the child
sentence-transformers # gets a bare sys.path and its `import pymupdf` fails. The tool then reports
openai # "PyMuPDF (fitz) is required..." even though PyMuPDF is a build input.
google-genai # Forward the parent's sys.path so the child resolves the same modules.
tiktoken # Mirrors nixpkgs' python-modules/vllm/0003-propagate-pythonpath.patch.
]; postPatch = ''
pdf = [ substituteInPlace src/zotero_mcp/tools/write.py \
pymupdf --replace-fail \
ebooklib 'child_env.setdefault("PYTHONUTF8", "1")' \
]; 'child_env.setdefault("PYTHONUTF8", "1"); child_env["PYTHONPATH"] = os.pathsep.join(p for p in sys.path if p)'
}; '';
pythonImportsCheck = [ "zotero_mcp" ]; pythonImportsCheck = [ "zotero_mcp" ];