26 Commits
Author SHA1 Message Date
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
32 changed files with 35045 additions and 1735 deletions
+8
View File
@@ -43,6 +43,14 @@ packages/
overlays/
└── 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.lock # Locked dependency versions
```
+21 -2
View File
@@ -9,10 +9,10 @@ This repository provides Nix derivations for MCP (Model Context Protocol) server
| Package | Description | Language |
| -------------------------------- | -------------------------------------------------------------------- | ---------- |
| 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 |
| duckduckgo-mcp-server | Web search and content fetching via DuckDuckGo | 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 |
| manim-mcp-server | Execute Manim animation code and return videos | Python |
| mcp-arr-server | MCP server for *arr suite — Sonarr, Radarr, Lidarr, Prowlarr | TypeScript |
@@ -22,11 +22,12 @@ This repository provides Nix derivations for MCP (Model Context Protocol) server
| pdf-reader-mcp | Production‑ready PDF processing with parallel processing | TypeScript |
| rust-research-mcp | Accumulate and organize academic knowledge | Rust |
| 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 |
| sabnzbd-mcp | MCP server for SABnzbd download management | Python |
| jellyfin-mcp | MCP server for Jellyfin — search, playback, metadata, admin | Go |
| zotero-mcp | Connect Zotero research library with AI assistants | Python |
| annas-mcp | Search/download documents from Anna's Archive | Go |
| 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 |
@@ -35,6 +36,24 @@ This repository provides Nix derivations for MCP (Model Context Protocol) server
| slskd-mcp | slskd (Soulseek) search, download, peer browsing, private messages | 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
### 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.
Generated
+1 -18
View File
@@ -18,24 +18,7 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-nixpkgs": "rust-nixpkgs"
}
},
"rust-nixpkgs": {
"locked": {
"lastModified": 1786599213,
"narHash": "sha256-yNJd40f11EzXBjSByCB7IPpeFFAdeoSKKM67dGkfFoU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0e251e24a4f24e036a084b6b4b2d2491af4167f4",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0e251e24a4f24e036a084b6b4b2d2491af4167f4",
"type": "github"
"nixpkgs": "nixpkgs"
}
}
},
+15
View File
@@ -24,9 +24,23 @@
"aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
mcpGatewayModule = import ./modules/mcp-gateway.nix;
sshMcpModule = import ./modules/ssh-mcp.nix;
in
{
overlays.default = import ./overlays/default.nix;
homeManagerModules = {
mcp-gateway = mcpGatewayModule;
ssh-mcp = sshMcpModule;
default = {
imports = [
mcpGatewayModule
sshMcpModule
];
};
};
}
// {
packages = forAllSystems (
@@ -60,6 +74,7 @@
packages = builtins.attrValues mcpPackages ++ [ pkgs.mcp-nixos pkgs.git ];
shellHook = ''
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"],
"enabled": true
},
"mcp-searxng": {
"type": "local",
"command": ["mcp-searxng"],
"enabled": true,
"environment": {
"SEARXNG_URL": "https://search.bueso.eu"
}
},
"rust-research": {
"type": "local",
"command": ["rust-research-mcp"],
@@ -96,6 +104,11 @@
"type": "local",
"command": ["mcp-score", "serve"],
"enabled": true
},
"docx": {
"type": "local",
"command": ["docx-mcp"],
"enabled": true
}
}
}
+7 -8
View File
@@ -7,16 +7,16 @@
buildGoModule rec {
pname = "annas-mcp";
version = "0.1.0-unstable-2026-06-26";
version = "0.0.14";
src = fetchFromGitHub {
owner = "iosifache";
owner = "SokolskyNikita";
repo = "annas-mcp";
rev = "c095963ad5d10d33da3c614495246b6ec7f680d4";
hash = "sha256-4gH5175yA9YYPVifXX3n9WCTRug0gOOfpQBLaGbvBcU=";
rev = "9e720ad93df0fdfb25d4de6afcba11a920b34af4";
hash = "sha256-LcnWYpvSpSHYo1zMcLGTZgxZgo7JOXF4dESD4l+qVhI=";
};
vendorHash = "sha256-2NdG5p2XfrhVgi388dRDBUSGwg6ybnzfn9495TWNGsA=";
vendorHash = "sha256-SCTznvZ8DvDN+iPDcTxiqpNYJwRLS8zIomz7f8S1eD0=";
nativeBuildInputs = [ makeWrapper ];
@@ -28,9 +28,8 @@ buildGoModule rec {
meta = with lib; {
description = "MCP server and CLI tool for searching and downloading documents from Anna's Archive";
homepage = "https://github.com/iosifache/annas-mcp";
license = licenses.mit;
homepage = "https://github.com/SokolskyNikita/annas-mcp";
mainProgram = "annas-mcp";
platforms = platforms.linux;
platforms = platforms.all;
};
}
+2 -2
View File
@@ -6,14 +6,14 @@
python3Packages.buildPythonApplication rec {
pname = "applemusic-mcp";
version = "0.18.3";
version = "0.20.1";
pyproject = true;
src = fetchFromGitHub {
owner = "epheterson";
repo = "applemusic-mcp";
rev = "v${version}";
hash = "sha256-VHW7ct824hW+Gv3x6tMDGnvx3Hj/0ZDqGjc7yQOaf7U=";
hash = "sha256-T956sblq/4TrAXkY4cwIFDPW6a2bjzyeyRyejYB4ARc=";
};
build-system = with python3Packages; [
+4
View File
@@ -22,6 +22,7 @@
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 { };
@@ -29,5 +30,8 @@
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 { };
# example-mcp-server = pkgs.callPackage ./example-mcp-server/package.nix { };
}
+15 -3
View File
@@ -2,18 +2,28 @@
lib,
python3Packages,
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 {
pname = "duckduckgo-mcp-server";
version = "0.4.0";
version = "0.7.0";
pyproject = true;
src = fetchFromGitHub {
owner = "nickclyde";
repo = "duckduckgo-mcp-server";
rev = "v${version}";
hash = "sha256-g1mpBrhuCt3g/XnUfHoY8lJDMizINypikj8Lqk2LVuE=";
hash = "sha256-wI6Q4lBZYzU4VORLwGZowv8D5UfhKaMeqBIQ7u6eRxc=";
};
build-system = [ python3Packages.hatchling ];
@@ -21,10 +31,12 @@ python3Packages.buildPythonApplication rec {
dependencies = with python3Packages; [
beautifulsoup4
httpx
mcp
httpcore
mcp2.mcp
];
doCheck = false;
dontCheckRuntimeDeps = true;
pythonImportsCheck = [ "duckduckgo_mcp_server" ];
+3 -3
View File
@@ -6,16 +6,16 @@
buildGoModule rec {
pname = "jellyfin-mcp";
version = "2026.604.2";
version = "2026.1015.1";
src = fetchFromGitHub {
owner = "jaredtrent";
repo = "jellyfin-mcp";
tag = "v${version}";
hash = "sha256-Ks54txfOAkNEWfhl5ZXEIaFzAbmCnwhHtN1+FIa7dgo=";
hash = "sha256-5wSYB/peTbvNYDlDBd2M0nA7JreP93nQe1G9rW3hv2I=";
};
vendorHash = "sha256-cDy+PpZM6xL7ur/xq81y4vlyaDnSOpGkyVV2hh8qI7o=";
vendorHash = "sha256-L39HdshrVfVXD+rV3prsKQRosvidynwIlA9J6dtfqmU=";
meta = with lib; {
description = "MCP server for Jellyfin media server — search, playback, metadata, admin";
@@ -71,14 +71,14 @@ let
in
python313Packages.buildPythonApplication rec {
pname = "kindly-web-search-mcp-server";
version = "0.1.8-unstable";
version = "0.1.9-unstable";
pyproject = true;
src = fetchFromGitHub {
owner = "Shelpuk-AI-Technology-Consulting";
repo = "kindly-web-search-mcp-server";
rev = "2b76afb37bf311facbde6750d0a0c0cbdfb8fec2";
hash = "sha256-90QHr2FPiMSwKXmjlZ2W9NIQvj3LDXzYIq6m/Z+sJMw=";
rev = "52a4e480d8041f8d4f388cee1ddecbd8e1ab01c7";
hash = "sha256-Mf3PMUic3XiNkiABRy6DrEhoGwcqq/xThU4ROO4S8dY=";
};
build-system = with python313Packages; [
@@ -95,10 +95,18 @@ python313Packages.buildPythonApplication rec {
pymupdf
fastmcp
trafilatura
starlette
uvicorn
];
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
postInstall = ''
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"
];
};
}
+3 -3
View File
@@ -6,14 +6,14 @@
python3Packages.buildPythonApplication rec {
pname = "mcp-email-server";
version = "1.4.1";
version = "1.9.1";
pyproject = true;
src = fetchFromGitHub {
owner = "Wh1isper";
repo = "mcp-email-server";
rev = "a1668a912db08373cc109b7482363129c75fd646";
hash = "sha256-Zt+198bQMthp6Mxmmsd7eTh9iybqOXWuVqLylzfUiIg=";
rev = "1.9.1";
hash = "sha256-FGjpRkVOFLkqY45Wl1lBuSWZvUnkyESZPeAB+d+1KYw=";
};
build-system = with python3Packages; [
+5 -5
View File
@@ -6,10 +6,10 @@
}:
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 {
pname = "music21";
version = "9.9.2";
version = "10.5.0";
format = "wheel";
src = fetchPypi {
@@ -17,7 +17,7 @@ let
format = "wheel";
dist = "py3";
python = "py3";
hash = "sha256-ZIAQq40TKNCxB0bO8UV+hLEidM0xYkgifpbSHielGMo=";
hash = "sha256-mSTv9fv1hJDmfL87eKWLpTBA53woGTDTVK8yp0fpRgY=";
};
propagatedBuildInputs = with python313Packages; [
@@ -50,8 +50,8 @@ python313Packages.buildPythonApplication rec {
src = fetchFromGitHub {
owner = "tskovlund";
repo = "mcp-score";
rev = "3ff9d4b6504fcee5032c44fdcf7dc1d7a21a4b5d";
hash = "sha256-zrpvzxi8AbF5/etuvMZLni4HSFBYGRe55rpKJJEAg60=";
rev = "b2155a59119103c39a37f14166d0d6920e564c76";
hash = "sha256-XYZjGa7pWaVmZ4dF/vOLkrYjSjNXP/148DQjujMdvIU=";
};
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 {
pname = "n8n-mcp";
version = "2.35.1";
version = "2.87.0";
src = fetchzip {
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 = ''
cp ${./package-lock.json} package-lock.json
+2 -2
View File
@@ -6,11 +6,11 @@
buildNpmPackage rec {
pname = "navidrome-mcp";
version = "2.2.0";
version = "2.3.0";
src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-1wZyM0GJUrQfzBK1tWqqIcVk90KUv/9wt2ZpRobQp/A=";
hash = "sha256-npUgzJpB0DaFuzU/1SuOIOVvd2gqZtb1Kos7NC8/Pd4=";
};
npmDepsHash = "sha256-58OZ4qVfeN+0LVzcSJQHQqiY66gPSGWI6ht/4fMp0oQ=";
+21 -8
View File
@@ -3,9 +3,18 @@
python3Packages,
fetchFromGitHub,
fetchPypi,
fetchurl,
}:
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
# CRITICAL: pname MUST use underscores, not hyphens, for PyPI wheel filenames
fetchWheel = { pname, version, hash, propagatedBuildInputs ? [ ], meta ? { } }:
@@ -132,23 +141,24 @@ let
in
python3Packages.buildPythonApplication rec {
pname = "nextcloud-mcp-server";
version = "0.173.0";
version = "0.195.0";
pyproject = true;
src = fetchFromGitHub {
owner = "cbcoutinho";
repo = "nextcloud-mcp-server";
rev = "v${version}";
hash = "sha256-wN0LUoKD4qR00H1eTeIHEoAmSEC7YY+sQ2HtY6oerjE=";
hash = "sha256-k8WeVe1ScWN7Lh+trowzn+UVuQBgfgI1Klfz4w5F8do=";
};
build-system = [ python3Packages.uv-build ];
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
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
googleapis-common-protos opentelemetry-api opentelemetry-proto
@@ -163,13 +173,10 @@ python3Packages.buildPythonApplication rec {
postPatch = ''
substituteInPlace pyproject.toml \
--replace-quiet '"uv_build>=0.12.1,<0.13.0"' '"uv_build"' \
--replace-quiet '" (>=1.29,<1.30)"' '""' \
--replace-quiet '" (>=2.1,<3)"' '""' \
--replace-quiet '" (>=0.28.1,<0.29.0)"' '""' \
--replace-quiet '" (>=10.3.0,<12.0.0)"' '""' \
--replace-quiet '" (>=7.2.0,<7.3.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 '" (>=7.3.0,<7.4.0)"' '""' \
--replace-quiet '">=3.0.1,<4.0"' '""' \
--replace-quiet '">=3.8.0,<4.0"' '""' \
--replace-quiet '">=3.2.13,<4.0"' '""' \
@@ -189,6 +196,12 @@ python3Packages.buildPythonApplication rec {
dontCheckRuntimeDeps = true;
catchConflicts = false;
pythonImportsCheck = [
"nextcloud_mcp_server"
"mcp"
"pymupdf"
];
meta = with lib; {
description = "MCP server for Nextcloud — files, calendar, contacts, notes, deck, talk, sharing";
homepage = "https://github.com/cbcoutinho/nextcloud-mcp-server";
+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,
buildNpmPackage,
fetchzip,
autoPatchelfHook,
stdenv,
}:
buildNpmPackage rec {
pname = "pdf-reader-mcp";
version = "2.3.0";
version = "4.1.3";
src = fetchzip {
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 = ''
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;
meta = with lib; {
+152 -37
View File
@@ -1,5 +1,28 @@
#!/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 sys
@@ -20,13 +43,76 @@ HEADERS = {"Authorization": f"Bearer {PINCHFLAT_TOKEN}", "Content-Type": "applic
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:
"""Make a GET request to the Pinchflat API."""
with httpx.Client(timeout=30) as client:
r = client.get(f"{BASE}{path}", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
return _unwrap(r)
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:
r = client.post(f"{BASE}{path}", headers=HEADERS, json=json)
r.raise_for_status()
return r.json()
return _unwrap(r)
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:
r = client.put(f"{BASE}{path}", headers=HEADERS, json=json)
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."""
with httpx.Client(timeout=30) as client:
r = client.delete(f"{BASE}{path}", headers=HEADERS)
with httpx.Client(timeout=timeout) as client:
r = client.delete(f"{BASE}{path}", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
return _unwrap(r)
# --- Sources ---
@mcp.tool()
@tool()
def list_sources() -> dict:
"""List all configured media sources (YouTube channels, playlists, etc.)."""
return _get("/sources")
@mcp.tool()
@tool()
def get_source(source_id: int) -> dict:
"""Get details of a specific source by ID, including its pending tasks."""
return _get(f"/sources/{source_id}")
@mcp.tool()
@tool()
def create_source(
original_url: str,
media_profile_id: int,
collection_type: str = "channel",
custom_name: Optional[str] = None,
download_cutoff_date: Optional[str] = None,
retention_period_days: Optional[int] = None,
) -> dict:
"""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'
custom_name: Optional custom name for the source
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 = {
"media_profile_id": media_profile_id,
@@ -93,18 +183,30 @@ def create_source(
source["custom_name"] = custom_name
if 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})
@mcp.tool()
@tool()
def update_source(
source_id: int,
custom_name: Optional[str] = None,
download_cutoff_date: Optional[str] = None,
enabled: Optional[bool] = None,
index_frequency_minutes: Optional[int] = None,
retention_period_days: Optional[int] = None,
prevent_culling: Optional[bool] = None,
download_media: Optional[bool] = None,
) -> 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 = {}
if custom_name is not None:
updates["custom_name"] = custom_name
@@ -114,34 +216,46 @@ def update_source(
updates["enabled"] = enabled
if index_frequency_minutes is not None:
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})
@mcp.tool()
@tool()
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}")
@mcp.tool()
@tool()
def force_download_pending(source_id: int) -> dict:
"""Force download of all pending media items for a source."""
return _post(f"/sources/{source_id}/force_download_pending")
@mcp.tool()
@tool()
def force_redownload(source_id: int) -> dict:
"""Force re-download of all existing media for a source."""
return _post(f"/sources/{source_id}/force_redownload")
@mcp.tool()
@tool()
def force_index(source_id: int) -> dict:
"""Force re-index of a source."""
return _post(f"/sources/{source_id}/force_index")
@mcp.tool()
@tool()
def force_metadata_refresh(source_id: int) -> dict:
"""Force metadata refresh for a source."""
return _post(f"/sources/{source_id}/force_metadata_refresh")
@@ -149,7 +263,8 @@ def force_metadata_refresh(source_id: int) -> dict:
# --- Media Items ---
@mcp.tool()
@tool()
def list_media(
source_id: Optional[int] = None,
limit: int = 50,
@@ -172,25 +287,25 @@ def list_media(
return _get("/media", params=params)
@mcp.tool()
@tool()
def search_media(query: str) -> dict:
"""Search media items by title or description."""
return _get("/media/search", params={"q": query})
@mcp.tool()
@tool()
def get_media(media_id: int) -> dict:
"""Get details of a specific media item."""
return _get(f"/media/{media_id}")
@mcp.tool()
@tool()
def force_download_media(media_id: int) -> dict:
"""Force download of a specific media item."""
return _post(f"/media/{media_id}/force_download")
@mcp.tool()
@tool()
def delete_media(media_id: int, prevent_download: bool = False) -> dict:
"""Delete a media item's downloaded files from disk.
@@ -201,21 +316,19 @@ def delete_media(media_id: int, prevent_download: bool = False) -> dict:
params = {}
if prevent_download:
params["prevent_download"] = "true"
with httpx.Client(timeout=120) as client:
r = client.delete(f"{BASE}/media/{media_id}", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
return _delete(f"/media/{media_id}", params=params, timeout=120)
# --- Media Profiles ---
@mcp.tool()
@tool()
def list_media_profiles() -> dict:
"""List all media profiles (download quality/container settings)."""
return _get("/media_profiles")
@mcp.tool()
@tool()
def get_media_profile(profile_id: int) -> dict:
"""Get details of a specific media profile."""
return _get(f"/media_profiles/{profile_id}")
@@ -223,7 +336,8 @@ def get_media_profile(profile_id: int) -> dict:
# --- Tasks ---
@mcp.tool()
@tool()
def list_tasks(source_id: Optional[int] = None) -> dict:
"""List all tasks. Optionally filter by source_id."""
params = {}
@@ -232,21 +346,22 @@ def list_tasks(source_id: Optional[int] = None) -> dict:
return _get("/tasks", params=params)
@mcp.tool()
@tool()
def get_task(task_id: int) -> dict:
"""Get details of a specific task."""
return _get(f"/tasks/{task_id}")
# --- Settings & Info ---
# --- Settings & Info (admin tier — not exposed by default) ---
@mcp.tool()
@tool("admin")
def get_settings() -> dict:
"""Get current Pinchflat settings."""
"""Get current Pinchflat settings (credential values are redacted)."""
return _get("/settings")
@mcp.tool()
@tool("admin")
def get_app_info() -> dict:
"""Get Pinchflat app info (version, environment, etc.)."""
return _get("/app_info")
@@ -254,4 +369,4 @@ def get_app_info() -> dict:
if __name__ == "__main__":
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 = [ ];
};
}
+3 -3
View File
@@ -6,13 +6,13 @@
let
wheel = fetchurl {
url = "https://files.pythonhosted.org/packages/6b/9c/dc73804f0f62208d59794080d009fb86b7ac6e9d4b5ad452791355dcfb51/transmission_mcp-1.0.1-py3-none-any.whl";
hash = "sha256-1TKb75iNKgMqw0YVZ+rDEac4IyCuj94y1OMzqQROCrI=";
url = "https://files.pythonhosted.org/packages/7e/81/fb2a2f2073d22b0a18c4c96b9c9ff386c3d0b49b6263097bf646d8b72800/transmission_mcp-1.1.0-py3-none-any.whl";
hash = "sha256-J22L3lY+XVdLdZeu8KuFdly0BkFUoAbX4GE/9a5kKSU=";
};
in
python3Packages.buildPythonApplication rec {
pname = "transmission-mcp";
version = "1.0.1";
version = "1.1.0";
format = "wheel";
src = wheel;
+33 -22
View File
@@ -10,7 +10,7 @@ let
# Internal dependency: pyzotero (not in nixpkgs)
pyzotero = python3Packages.buildPythonPackage rec {
pname = "pyzotero";
version = "1.13.5";
version = "1.14.0";
format = "wheel";
src = fetchPypi {
@@ -18,7 +18,7 @@ let
format = "wheel";
dist = "py3";
python = "py3";
hash = "sha256-KoHXv+ix6kRzvLUPOPBeAeAJaO/3bBxHi1PwUa5rDcg=";
hash = "sha256-OUoU8HTEFwVfzqopcLWUzpJaYJTJ09zIpzTgdCbs9BU=";
};
propagatedBuildInputs = with python3Packages; [
@@ -62,21 +62,21 @@ let
in
python3Packages.buildPythonApplication rec {
pname = "zotero-mcp";
version = "0.9.1";
version = "0.13.0";
pyproject = true;
src = fetchFromGitHub {
owner = "54yyyu";
repo = "zotero-mcp";
rev = "v${version}";
hash = "sha256-1A4aKGLFUmY+abLKhxmnSd3KaJtC8onda4/MkjrybIg=";
hash = "sha256-xL54SbaAczkwk4tkan+wVVTv9ACKIC6hcAADI2GTnxk=";
};
build-system = with python3Packages; [
hatchling
];
# Core dependencies (from pyproject.toml v0.9.1)
# Core dependencies (from pyproject.toml v0.13.0)
dependencies = with python3Packages; [
pyzotero
python-dotenv
@@ -88,25 +88,36 @@ python3Packages.buildPythonApplication rec {
httpx
unidecode
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
ebooklib
# Runtime deps for the semantic-search tools (upstream "semantic" extra).
# 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
sentence-transformers
openai
google-genai
tiktoken
];
# Optional extras (semantic search, PDF, scite)
optional-dependencies = with python3Packages; {
semantic = [
chromadb
sentence-transformers
openai
google-genai
tiktoken
];
pdf = [
pymupdf
ebooklib
];
scite = [
requests
];
};
# `zotero_get_pdf_outline` reads a PDF's table of contents in a throwaway
# `sys.executable -c ...` child (tools/write.py, _extract_pdf_toc). Under the
# Nix wrapper this package's dependencies are injected into the *parent*
# interpreter via site.addsitedir, never exported as PYTHONPATH, so the child
# gets a bare sys.path and its `import pymupdf` fails. The tool then reports
# "PyMuPDF (fitz) is required..." even though PyMuPDF is a build input.
# Forward the parent's sys.path so the child resolves the same modules.
# Mirrors nixpkgs' python-modules/vllm/0003-propagate-pythonpath.patch.
postPatch = ''
substituteInPlace src/zotero_mcp/tools/write.py \
--replace-fail \
'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" ];