antono's personal Nix package set — the custom packages used by my personal NixOS/home-manager flake, in one repo instead of one fork per project.
{
inputs.sneg = {
url = "github:antono/sneg";
inputs.nixpkgs.follows = "nixpkgs";
};
}Then either take the overlay, which makes everything available as pkgs.<name>:
nixpkgs.overlays = [ inputs.sneg.overlays.default ];or reach for a single package directly:
inputs.sneg.packages.${system}.deplexity| Attribute | What |
|---|---|
deplexity |
Export Perplexity AI conversations, spaces and profile to JSON/Markdown/PDF |
deplexity-with-chromium |
Same, bundling Chromium so deplexity login works out of the box (linux only) |
tolaria |
Tolaria desktop app bundled with its MCP server (linux only — WebKitGTK 4.1) |
tolaria-mcp |
Just the Tolaria MCP server: vault tools over stdio + a WebSocket bridge |
tolaria-node-modules |
Tolaria's pnpm dependency closure, exposed so the hash can be rebuilt on its own |
tolaria-src |
Tolaria's fetched source, exposed so it can be realised on its own |
hacktv |
Analogue TV signal generator for SDR hardware |
hyperframes |
HyperFrames CLI: HTML compositions rendered to deterministic video, wired to nixpkgs' ffmpeg and headless Chrome |
hyperframes-plugin |
HyperFrames' 21 agent skills as a read-only plugin whose launcher runs the CLI above |
mcphub |
Self-hosted MCP gateway: fronts many MCP servers behind one endpoint, with a dashboard |
argocd-mcp |
MCP server for Argo CD |
blender-mcp |
MCP server for Blender (pairs with an extension installed into Blender) |
freecad-mcp |
MCP server for FreeCAD (pairs with an addon installed into FreeCAD) |
greenhouse-mcp |
MCP server for the Greenhouse Harvest API |
kicad-mcp |
MCP server for KiCAD (wraps a KiCAD install: its pcbnew bindings, CLI and libraries) |
konnect |
MCP server for KiCAD 10, the Rust rewrite of the above — one binary, over KiCAD's IPC API |
mcp-musescore |
MCP server for MuseScore (pairs with a QML plugin) |
signoz-mcp-server |
MCP server for SigNoz |
telegram-mcp |
MCP server for Telegram, acting as your user account (not a bot) via Telethon |
The servers above are the ones mcp-servers-nix
does not ship. There is no fork of it: sneg's servers plug into upstream's
module system, and lib.mkConfig is a drop-in for upstream's that knows about
both sides.
programs.mcp.configFile = inputs.sneg.lib.mkConfig pkgs {
programs = {
# from mcp-servers-nix
chrome-devtools.enable = true;
context7.enable = true;
nixos.enable = true;
playwright.enable = true;
terraform.enable = true;
# from sneg
argocd = {
enable = true;
baseUrl = "https://argocd.example.com";
passwordCommand.ARGOCD_API_TOKEN = [ "cat" "/run/secrets/argocd" ];
};
signoz.enable = true;
};
};Secrets go through envFile or passwordCommand, never env or args —
everything in /nix/store is world-readable. That is why sneg's modules expose
hosts and URLs as options but never tokens.
Prefer upstream's version of a server whenever it gains one: when
chrome-devtools landed there, sneg's copy was deleted rather than kept.
The two KiCAD servers are the ones that are not self-contained: both wrap a
pkgs.kicad of their own, because kicad-cli and the KICAD<major>_*_DIR
library paths are otherwise set only by the kicad wrapper, which nothing here
runs under. kicad-mcp needs more of it — pcbnew is a compiled module that
ships inside kicad rather than in nixpkgs' python set, so the interpreter is
wrapped too. Each has a README beside its package, and .override { kicad = ...; } moves everything together.
They are alternatives, not duplicates: konnect is the author's Rust rewrite —
AGPL-3.0, KiCAD 10 only, one binary over the IPC API — while kicad-mcp stays
MIT and works against 8 and 9. Enabling both is fine.
Upstream's lib.evalModule takes the nixpkgs instance it resolves server
packages from, plus one module. Both are seams:
- extending nixpkgs with
overlays.mcp-serversmakesprograms.<name>.packageresolve sneg's servers by name, exactly as it resolves upstream's; - the module can
importssneg's server modules, which are ordinary mcp-servers-nix modules built on upstream'smkServerModulespecialArg.
lib/default.nix does both. If you would rather wire it yourself:
inputs.mcp-servers-nix.lib.mkConfig (pkgs.extend inputs.sneg.overlays.mcp-servers) {
imports = inputs.sneg.lib.serverModules;
programs.signoz.enable = true;
}checks.<system>.mcp-servers renders every server from both sides into one
config file, so a bad package name or a clash with an upstream module fails
here rather than at the consumer.
mcphub is not another server. It is a
gateway: it starts the servers listed in its settings file and re-exports all of
them over one HTTP origin — with a dashboard, bearer keys and a CLI on top — so
a client that cannot spawn stdio children gets a single stable address instead
of N processes of its own.
homeManagerModules.mcphub runs it as a user service (systemd on linux,
launchd on darwin):
{
imports = [ inputs.sneg.homeManagerModules.mcphub ];
# Declared once. Direct clients read this; so does the hub.
programs.mcp.servers.nixos = { ... };
services.mcphub = {
enable = true;
# Servers only the hub should see, in mcphub's own schema. Merged after
# programs.mcp, so a name repeated here wins.
servers.deploy-notes = {
type = "streamable-http";
url = "https://notes.example.com/mcp";
# Expanded by mcphub from its own environment, so the token itself never
# reaches the store.
headers.Authorization = "Bearer \${NOTES_TOKEN}";
};
environmentFile = "/run/secrets/mcphub.env";
};
}The thing to understand about mcphub is that it owns its settings file. There
is exactly one — mcp_settings.json — and mcphub does not merely read it: the
admin password hash lands there on first boot, every bearer key and OAuth
client the dashboard mints is appended to it, and every server toggled in the
UI is written back. A store symlink fails on the first write; a file
regenerated on every home-manager switch throws away the credentials that
make the dashboard reachable.
So the module treats it as state and merges into it at every start, under one
rule: every top-level key the module declares replaces that key in the live
file, and every key it does not declare is left alone. servers and
useHomeManagerServers declare mcpServers; settings declares whatever it
names. users, bearerKeys, groups, oauthClients, oauthTokens, prompts
and resources therefore survive untouched — and mcpServers, once declared,
is declared entirely, so a server added through the dashboard is dropped at
the next restart. Declare nothing and the dashboard owns the list.
The merge is shallow on purpose. A recursive one would leave half-overwritten
systemConfig subtrees matching neither what Nix said nor what the dashboard
said, so settings.systemConfig must be restated whole. It is a jq program in
its own store file, which is what lets checks.<system>.mcphub-module run it
against fabricated inputs rather than grepping the shell command that invokes
it.
useHomeManagerServers (on by default) folds ~/.config/mcp/mcp.json in as the
first source of servers, read when the service starts rather than when the
configuration is built. The two schemas line up better than the names suggest:
type, command, args, env, url, headers and — unlike the hub this
replaced — enabled all mean the same thing on both sides, so a server switched
off in programs.mcp is switched off in the hub too.
The one thing that does not survive the trip is env.<VAR>.file, which
home-manager renders as the literal string {file:/path} for its per-client
modules to rewrite. mcphub has no such placeholder and hands the token text to
the server as its secret. That warns. The fix is to restate the server under
services.mcphub.servers with env.<VAR> = "${VAR}": mcphub expands ${VAR}
and $VAR from its own environment across a server's env, args, headers
and url, so supplying the variable through environmentFile keeps the secret
out of the store — the same rule the server modules follow, and the reason
environment is the wrong place for one.
mcphub is configured by environment variable rather than by argument: the module
exports PORT, BASE_PATH and MCPHUB_SETTING_PATH itself and leaves the rest
— ADMIN_PASSWORD, DISABLE_WEB, READONLY, the timeouts — to environment
and environmentFile. Note that it binds every interface, with no upstream
option to narrow that, and that settings.systemConfig.routing.skipAuth turns
every caller on that port into an admin who can register a stdio server. The
module warns about the second; the first is on you.
New generations reach a running hub by restarting it. The start script carries
the whole configuration in its own store path, so the unit's ExecStart and the
agent's ProgramArguments differ whenever anything does — that is enough for
sd-switch on linux and for home-manager's plist comparison on darwin, with no
generation marker needed. Unit.X-Restart-Triggers covers the one input that is
not baked in: the servers home-manager writes to a stable ~/.config path.
mcphub re-reads its settings when the mtime moves, but the merge that moves it
runs at start, so the restart is the reload.
This module is independent of homeManagerModules.default — the bridge writes
programs.mcp.servers, the hub merges the file that option generates — so
importing both is fine and is the normal case: the bridge feeds sneg's servers
into programs.mcp, and the hub picks them up from there.
modules/mcphub.nix the service module
pkgs/mcphub/package.nix
tests/mcphub-module.nix checks.<system>.mcphub-module — the rendered unit,
the start script, and the merge rule actually run
homeManagerModules.hyperframes installs the CLI and hands the skills to
Claude Code and Codex as a plugin:
{
imports = [ inputs.sneg.homeManagerModules.hyperframes ];
programs.hyperframes.enable = true;
# On by default whenever the agent's own module is enabled.
# programs.hyperframes.enableClaudeCodeIntegration = true;
# programs.hyperframes.enableCodexIntegration = true;
# ~/.agents/skills, for codex, opencode, gemini-cli and anything else that
# follows the Agent Skills convention. Off by default.
programs.hyperframes.enableAgentsSkills = true;
}In Claude Code the router is /hyperframes:hyperframes.
enableAgentsSkills links each skill to ~/.agents/skills/<name>, one link per
skill so the directory stays shared with skills installed by other means. It
also writes ~/.agents/plugin.json, which is not an accident: see below. Codex
reads ~/.agents/skills as well as its plugins, so turning this on switches
enableCodexIntegration off by default, and forcing both on warns.
The skills go in as a plugin, never as loose skill directories, because the
router decides how to behave by looking for a plugin manifest two levels above
its own SKILL.md. With one, it treats the install as read-only and runs every
CLI call through skills/hyperframes/scripts/plugin-cli.mjs, which sneg patches
to run the Nix-built binary instead of npx hyperframes@<version>. Without one
(~/.claude/skills/<name>), it falls back to standalone mode: it downloads a
second CLI from npm and runs hyperframes skills update against skills that
live in the store. claude-code and codex take plugins natively. For
~/.agents/skills the module keeps the same shape by hand: with the manifest at
~/.agents/plugin.json, two levels above every
~/.agents/skills/<name>/SKILL.md, the router finds a plugin root there too.
The launcher is not the only way the skills reach for npx: media-use's helpers
(transcribe, resolve, TTS) and pr-to-video's preflight spawn npx hyperframes
from the user's project, where it would download the latest release. Those are
patched to the same binary, and checks.<system>.hyperframes-module fails if a
bump adds a script that spawns npx directly.
The CLI is wrapped with --set-default, so each of these can still be
overridden from the environment:
HYPERFRAMES_BROWSER_PATH: Playwright's patchedchrome-headless-shell(linux). It keeps the fast BeginFrame capture path, which full chromium lacks. On darwin the CLI downloads its own copy on first use.HYPERFRAMES_FFMPEG_PATH/HYPERFRAMES_FFPROBE_PATH:ffmpeg-headless.HYPERFRAMES_NO_UPDATE_CHECK,HYPERFRAMES_NO_AUTO_INSTALL,HYPERFRAMES_SKIP_SKILLS: Nix owns updates and skills.initignores its own--skip-skillsflag and only honors the variable.HYPERFRAMES_NO_TELEMETRY: opt-out by default.
Optional extras the skills can use (whisper-cpp for transcription, Kokoro TTS,
MusicGen) are not bundled. hyperframes doctor lists what is missing. Bundled
helper scripts run under the agent's own node, so the plugin expects one on
PATH.
The CLI is built from the npm tarball (upstream's tsup bundle) with a vendored
lockfile, so a bump means regenerating that lockfile. The steps are at the
bottom of pkgs/hyperframes/package.nix. The plugin follows the CLI's version
and only needs its own src hash refreshed.
lib/default.nix mkConfig / evalModule / serverModules
modules/mcp-servers/<name>.nix one file per server, auto-discovered
pkgs/mcp-servers/<name>/package.nix
tests/mcp-servers.nix the composition check
Package attribute names must match the packageName its module passes to
mkServerModule — that is the only thing tying the two halves together.
crane reads tolaria's Cargo.lock and Cargo.toml out of the fetched tree, so
merely evaluating tolaria requires its source to already be in the store.
Upstream's flake avoids this only because its src is a local path.
Consequences, all of them handled in .github/workflows/ci.yml:
nix flake check --no-buildcannot instantiate tolaria on a cold store. Runnix build --no-link .#tolaria-srcfirst.--all-systemscannot work: evaluatingpackages.aarch64-*would need an aarch64 source derivation realised on an x86_64 machine.
flake.nix packages.<system>.*, overlays.default, checks, devShell
overlay.nix final: _prev: import ./pkgs final
pkgs/default.nix the package list — one line per package
pkgs/<name>/package.nix
pkgs/default.nix is the single source of truth: overlays.default and
packages.<system> are both derived from it.
A package that ships several outputs from one source tree (tolaria) gets a
directory with a default.nix returning a set, which pkgs/default.nix splices
into the package set under its final names.
Two rules the overlay has to obey:
- Anything deciding which attributes exist must read
prev, notfinal. Readingfinal.stdenvthere sends the nixpkgs stdenv bootstrap into infinite recursion. - Extra flake inputs (
fenix,crane— both only for tolaria) reach packages through theinputsargument threaded fromflake.nixviaoverlay.nix. Prefer packages that need nothing beyond nixpkgs.
pkgs/<name>/package.nix— a plaincallPackage-able derivation. Keep it nixpkgs-shaped (fetchFromGitHubpinned to a release tag, no local paths, no flake inputs) so it can be sent upstream as-is later.- One line in
pkgs/default.nix. git addit — flakes ignore untracked files — thennix flake check -L.
Starting from scratch? nix run nixpkgs#nix-init -- pkgs/<name>/package.nix
generates a first draft.
nix-update --flake deplexity # rewrites version, src hash and vendorHash
nix flake check -Lnix develop # nix-update, nix-init, nixfmt-tree
nix fmt
nix build .#deplexity && ./result/bin/deplexity version