app-manager
OfficialAllows installing, detecting, and removing Docker on local and remote Linux targets via the distro package manager or vendor scripts, with progress tracking and package-database provenance.
Allows installing, detecting, and removing Ollama on local and remote Linux targets via the distro package manager or vendor scripts, with progress tracking and package-database provenance.
Manages Python packages via pip on Linux targets, including detection, version probing, installation, and removal, tracked under a dedicated pip namespace.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@app-managerinstall nginx on my remote server"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
app-manager
A Peckboard WASM plugin that lists, installs, and removes common applications on Linux targets — the local Peckboard host and any configured remote SSH hosts — via MCP tools. System apps come from the distro package manager (or a vendor script); Python packages come from pip, tracked as their own clearly-labelled namespace.
It ships both an MCP tool surface (catalog, target abstraction, app_*
tools) and an App Manager dashboard page reachable from the sidebar.
Permissions
provide_mcp_tools— theapp_*tools below.data_store— target registry and install/remove job records.process_exec_any— run commands on the local Peckboard host. This is a broad permission: it lets the plugin run any bare executable on PATH as the Peckboard host user. In practice this plugin only ever runs its own catalog's static recipes (seesrc/catalog.ts) — user input is validated against the catalog before anything reaches a shell — but the permission grant itself is not narrower than that.ssh— run commands on configured remote targets.ssh_keys— resolve a remote target's configured vault key by id (Auth::KeyRef) without this plugin ever seeing key material, and populate the page's key dropdown frompeckboard_ssh_key_list(metadata only).user_authority— serve the page's authenticated data routes under the signed-in user (http.request.authed).contribute_sidebar— the App Manager sidebar entry.models_read— the install picker's account+model catalog. Metadata only (ids, display names, tiers, account ids), already filtered server-side to thinking-capable models; never credentials or tokens.session_write— create the temporary AI install session (is_temp, in the shared~/peckboard-installs/app-managerfolder core registers).session_dispatch— dispatch the install prompt at that session.session_read— poll the session's slim event tail ({seq, kind, name}, never payloads) to render install progress.
Upgrading from 0.4.0 (or earlier) re-triggers the approval prompt. The
permission set grew again (models_read, session_write,
session_dispatch, session_read for AI-session installs), so Peckboard
loads the new version inert until you approve it again in Settings →
Plugins.
Related MCP server: Linux MCP Server
Dashboard Page
Sidebar → App Manager opens /plugin-api/v1/app-manager, served by
this plugin and framed in a sandboxed iframe (no allow-same-origin). It
talks only to its own authenticated routes, through the host's
postMessage fetch bridge:
Route | Purpose |
| the target dropdown (local + configured remotes) |
| vault key metadata for the key dropdown |
| distro banner + one grid row per catalog app |
| one app's live state + job progress |
| account+model picker options + stored default |
| remote-target CRUD |
| start an install (session/script) / remove job |
| cached dependency graph, trees + reverse view |
| re-resolve the graph from the package manager |
| system-wide reverse deps of one graph package |
(all under /api/plugin-ui/app-manager.)
The page itself is a single HTML string (src/page.ts) that cannot import
anything, so every display decision — badge text, action label, job headline,
and the prose an error is rendered as — is made server-side in src/view.ts
and shipped as plain data. That is also what the vitest suite covers; the page
is pure DOM plumbing on top.
Notes on the shape of it:
Target picker and SSH-key picker are
<select>elements — never free text. The page never accepts or displays private key material; a target stores only the vault key's id.Installs never block the UI:
POST /installreturns a job id and the page polls/statusevery 2s, streaming the log tail with a running / succeeded / failed state.Removal goes through a confirmation that states plainly that it runs a package-manager command as root on the target.
A target that isn't a usable Linux host renders as a refusal instead of an app grid; every error is a sentence, never raw JSON.
Local Target and Folder Scope
peckboard_exec_any pins its cwd to the caller's folder, and a global
sidebar page has no project or session to resolve one from. Core therefore
falls back to the app data dir when the caller holds full user authority and
carried no folder scope (see exec_impl in src/plugin/host.rs in the core
repo). An MCP tool call still refuses — its per-folder floor is what keeps a
plugin tool inside the calling session's reach.
Targets
localis always available and needs no configuration.Remote targets are records
{id, hostname, port, username, key_id}kept in this plugin's owndata_store(plugin data stores are hard-namespaced per plugin, so this plugin cannot see ssh-fleet's hosts, and vice versa). Only a vault key reference (key_id) is ever stored — never a password or private key. Populate the key dropdown from thepeckboard_ssh_key_listhost function.No MCP tool adds or removes a remote target: that is the dashboard page's job, through its own
POST /targets/POST /target-removeroutes on top of thesrc/targets.tsstore functions.
Catalog
src/catalog.ts is a plain data table — one entry per app, each with a
detect command, a version probe, per-package-manager install/remove
recipes, and (for apps not in any distro's repos) a vendor install/remove
script. Adding an app is a pure data change. Entries with
namespace: "pip" are Python packages, not system apps: one pip
recipe used on every distro, a pip_package name, and pip-based
detect/version probes (see "The pip Namespace" below).
Distro detection reads /etc/os-release on the target and maps
ID/ID_LIKE to one of apt (debian/ubuntu), dnf (fedora/rhel), pacman
(arch), zypper (suse). An unrecognised or non-Linux target is refused with
a clear message — never a guessed command.
Installs Are Detached Jobs — and Local Installs Run in an AI Session
app_install/app_remove don't block until completion — plugin calls are
synchronous and bounded by call_timeout_secs, and an ollama/docker
install can run for minutes.
Local installs (app_install on the local target) run through a
TEMPORARY AI SESSION instead of a detached script:
The user picks the account and model in the dashboard (a
<select>fed bypeckboard_list_models— thinking-capable models only, filtered server-side; the chosen id is validated against that same catalog before anything is created, and persisted as the default for next time).The plugin takes the BEFORE package-DB snapshot, creates a temp session (
Install <app>,is_temp, in~/peckboard-installs/app-manager) on that model, and dispatches an install prompt that mirrors the core install-session rules — includingsudo -Aso root steps raise the masked askpass dialog in the session tab.app_statuspolls the session's slim event tail ({seq, kind, name}— core never exposes event payloads to plugins), so the page shows tool-level activity plus an "Open install session" link. It is deliberately NOT a log; the real conversation lives in the session tab.When the run ends (
agent-end— emitted for completed and crashed runs alike), the plugin takes the AFTER snapshot and decides success by re-running the app's detect probe — never by trusting the agent's own account. A session that vanishes before its run ends (temp tab closed, cleared, killed) lands the job in a clear failed state with an "unknown" note — never a bogus empty delta recorded as success.
Remote installs and every removal stay deterministic scripts. An AI
session runs on the Peckboard host and has no path to a remote target's
SSH credentials; and removal is destructive, so a scripted apt remove is
preferred over an agent. Those paths keep the original shape:
The catalog recipe is wrapped and launched with `nohup sh -c '...' >
2>&1 &, its PID captured. 2. A job record {id, target_id, app_id, action, pid, logfile, status}is written to thedata_store; the tool call returns the job id immediately. 3. app_statuspolls: checks whether the pid is still alive and tails the logfile. The wrapped script also appends aPECKBOARD_EXIT: sentinel line on completion, soapp_status` can tell success from
failure without waiting on the process itself.
Session jobs reuse the same job records with kind: "session" plus the
session id, event cursor, and bounded activity lines (src/jobs.ts,
src/installSession.ts).
Install Provenance
What an install genuinely added is recorded by bracketing it with package-database snapshots — never by parsing installer output or asking an agent. Script installs take both snapshots inside the same detached script; AI-session installs take them in plugin code around the session's lifetime (before the prompt is dispatched, after the run ends). Either way:
snapshot(before) → install → snapshot(after) → delta = added packagesSnapshots dump name + version per line (dpkg-query -W, rpm -qa --qf,
pacman -Q) into /tmp files next to the job's logfile, through the same
target abstraction as everything else (src/exec.ts). When a poll first
observes the job's terminal state, the two files are read and deleted in
one exec and the delta becomes a record in the installs collection
(src/provenance.ts), keyed <target_id>:<app_id> — a re-install
supersedes, a successful remove deletes. app_list and the dashboard
surface it: the app row notes its package-DB version next to the probed
binary version, and the packages that arrived with it render as a
secondary "Installed with …" line, each with its version.
Honest edges, deliberately visible:
Vendor
curl | shinstallers (claude, cursor-agent, ollama) never touch the package database. Their rows say so — "not tracked by the package manager" — instead of showing an empty list that would read as "no dependencies". Prerequisites such an installer doesapt-get installDO land in the delta and are listed normally.A failed snapshot (unsupported package manager, permission denied, truncated output) degrades to an explicit "unknown", never to a silently-empty delta.
The record is provenance — "arrived during this job" — not a dependency graph: a shared library is attributed to whichever app's install pulled it in first. Real dependency edges must come from the package manager.
Dependency Graph
Provenance answers "what arrived during this job"; the dependency graph
answers "what does this app require right now" — and the edges are
queried from the package manager itself (apt-cache depends,
rpm -qR + --whatprovides, pacman -Qi), never inferred from the
install delta. The two live in separate data_store collections
(installs vs depgraphs) and never overwrite each other.
It is a DAG, not a tree: install git and node and both depend on
libssl3 — that node has two parents. The plugin honours that:
A shared dependency appears under every app that requires it, flagged
shared, instead of being attributed to whichever app's install pulled it in first.The remove confirmation states removal impact with autoremove semantics: only packages nothing else still depends on are listed as "would become unneeded"; a shared dependency another app needs is explicitly shown as kept, so the UI never contradicts what the package manager would actually do.
Cost control: resolution is seeded from the installed catalog apps'
packages plus the provenance delta set, expanded breadth-first one
batched exec per level, depth-limited (default 2, max 4, configurable
per refresh request) and capped at 600 nodes with a visible truncated
marker. The graph refreshes when an install/remove job settles and on
the explicit "Refresh dependencies" button — rendering only ever reads
the cached snapshot, which carries an at timestamp because dependency
sets drift with upgrades.
The dashboard grows a slim bar under the distro banner: resolution
state + refresh button, plus a reverse view — pick a library from the
dropdown and see which catalog apps require it, with an optional
system-wide rdepends query on demand (the package name is validated
against the stored graph before it goes anywhere near a shell). Each
installed app row gains a collapsed "Dependencies" toggle: name +
version + kind (app / library / binary) per node, shared nodes marked,
the app's own binaries listed under its root. The app_deps MCP tool
returns the same payload.
Honest limits, stated in the UI rather than papered over:
Vendor
curl | shinstalls (claude, cursor-agent, ollama) never enter the package database, so they have no dependency edges at all. Their rows say "not tracked by the package manager" — never an empty tree that would read as "no dependencies".pip/Python packages live in their own section. They are a different namespace from distro packages, so they are never merged into the system graph's nodes/edges — see "The pip Namespace" below.
kindis a display heuristic (catalog apps are "app";lib-named packages and.socapabilities are "library"; everything else renders "binary"), and on rpm systems capabilities resolve to their first provider.
The pip Namespace
pip packages are not dpkg/rpm/pacman packages: they live in pip's own database, are invisible to the snapshot bracket above, and must never be confused with system packages. The plugin treats them as a separate, explicitly-labelled namespace:
Catalog:
namespace: "pip"entries (today:graphifyy, the package the graphify plugin's tools need) install with one pip recipe on every distro — `PIP_BREAK_SYSTEM_PACKAGES=1 python3 -m pip install --user
— into the **user site**: no root, nothing outside$HOME. The env var lifts PEP 668's externally-managed refusal on modern distros and is ignored by older pips. python3andpipthemselves are ordinary system catalog entries (andpython3` deliberately has no remove
recipe — removing the system Python can dismantle the OS).
Probes are pip's own: presence via
pip show <pkg>, versions viapip list --format=freeze, dependency edges viapip show'sRequires:/Required-by:lines. Never via the distro package DB.Provenance: a pip install records
method: "pip"andtracking: "pip"(package_tracking: "pip"on the MCP surface) — the snapshot bracket is deliberately skipped, so an unrelated background distro change can never be attributed to a pip app.Dependency view: pip packages ride along on a dependency refresh as their own "Python packages (pip)" block (
pip_packagesin theapp_depspayload), never merged into the system graph's nodes/edges. A host without pip just leaves the block empty.UI: pip rows and entries carry a distinct
pipbadge.
One honest limit: the plugin only tracks pip's user/system site for
the target's python3 -m pip. Virtualenvs are invisible — in particular,
the graphify plugin's legacy self-install into a folder-root
.graphify-venv/ is neither seen nor managed here.
sudo
Recipes that need root use sudo -A, matching the core convention (see
src/service/askpass.rs and web/src/utils/installSession.ts).
AI-session installs (local): the agent runs
sudo -Ainside a real session, so the askpass bridge works — the password prompt appears as a masked dialog in the session tab (the dashboard flags it as "waiting for your answer" and links there).Script installs/removals: a plugin's own exec calls do not have the askpass bridge wired in, so
sudo -Afails cleanly with sudo's own stderr (e.g. "a password is required") rather than hanging — that message shows up in the job's log tail viaapp_status.
Build
./build.sh
# or: npm install && npm run buildRequires extism-js on PATH. Output: dist/plugin.wasm. Copy it to
<dataDir>/plugins/app-manager.wasm (the file stem is the plugin id)
and approve it in Settings → Plugins.
Renamed from linux-app-manager
Through 0.2.0 this plugin shipped as linux-app-manager, and the wasm file
stem is the plugin id. If an older copy is still staged, delete
<dataDir>/plugins/linux-app-manager.wasm when you stage
app-manager.wasm — two staged copies declare the same app_* tool
names, and core silently drops whichever set loads second. Core migrates the
plugin's stored data (configured remote targets, job records) from the old
plugin id to app-manager automatically at startup, provided the new id has
no data yet.
Test
npm testThis server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityDmaintenanceAn MCP server for managing Ubuntu/Linux systems, enabling AI assistants to execute commands, manage services, files, logs, and packages via local or SSH connection.
- Alicense-qualityBmaintenanceA read-only MCP server for Linux and macOS system administration, diagnostics, and troubleshooting, supporting remote SSH execution and multi-host management.Apache 2.0
- Flicense-qualityCmaintenanceMCP server for infrastructure discovery and remote management, enabling SSH command execution, file transfer, log tailing, and machine/service inventory with a companion web dashboard.1
- FlicenseAqualityBmaintenanceCross-platform MCP server for policy-controlled command execution on Linux and Windows, with no SSH dependency.3
Related MCP Connectors
Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.
A MCP server built for developers enabling Git based project management with project and personal…
An MCP server that let you interact with Cycloid.io Internal Development Portal and Platform
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/PeckBoard/app-manager'
If you have feedback or need assistance with the MCP directory API, please join our Discord server