npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mdrv/zellijmcp

v0.1.5

Published

Agent-owned zellij sessions over MCP — no plugin, no main-session pollution.

Readme

zellijmcp

NPM License: MIT

Agent-owned zellij sessions over the Model Context Protocol.

Lets AI agents (OpenCode, Claude Code, Cursor, Zed, GitHub Copilot, mcphost, etc.) spin up and drive their own detached zellij sessions — without ever loading a plugin into the human user's interactive session, without permission prompts, without focus stealing.

The agent gets a workspace of tabs and panes it fully controls; the human's session is never modified.


Why this exists

Every other zellij-MCP bridge loads a .wasm plugin into the user's currently-focused interactive session — the one the human is using. That means:

  • A first-run permission prompt the user must babysit
  • Side effects on live panes (focus stealing, layout churn, plugin pane appearing)
  • Coupling between "what the agent is doing" and "where the human is working"

zellijmcp sidesteps all of it by giving the agent its own sessions, created detached via zellij attach --create-background. The agent's tabs/panes/output live in a session the human never has to see.

Install

bun add -g @mdrv/zellijmcp
# or
npm i -g @mdrv/zellijmcp

Requires:

  • Bun 1.2+ on PATH
  • zellij 0.42+ on PATH (zellijmcp doctor checks)

Verify:

zellijmcp --version    # zellijmcp v0.1.0
zellijmcp doctor       # checks zellij, paths, and default session

Quick start

Run the MCP server on stdio (this is what agent hosts spawn):

zellijmcp

Configure your agent host

OpenCode (opencode.jsonc):

{
	"mcp": {
		"zellij": {
			"type": "local",
			"command": ["zellijmcp"],
			"environment": {}
		}
	}
}

Claude Desktop / Cursor / Cline (claude_desktop_config.json):

{
	"mcpServers": {
		"zellij": {
			"command": "zellijmcp",
			"args": [],
			"env": {}
		}
	}
}

That's it. On first tool call, zellijmcp auto-creates a detached session named mcp in the background and the agent starts working in it.

Concepts an agent should know

Reading this section is enough to use zellijmcp productively. Full reference is in docs/.

Sessions

Every tool call targets a session. Resolution order:

  1. The tool's session parameter (if provided)
  2. --session / -s CLI flag, or ZELLIJ_MCP_SESSION_NAME env var
  3. Default: mcp (or mcp-<pid> with --pid-scope for concurrent-safe runs)

Sessions are agent-owned: tracked in ~/.zellijmcp/sessions.json. zellij_kill_session refuses to kill sessions not in the registry (unless force: true) — defense against touching the human's session.

To run parallel workspaces, give each one a name:

zellij_create_session({ name: 'build' })
zellij_create_session({ name: 'tests' })
zellij_run_command({ session: 'build', command: ['bun', 'run', 'b'] })
zellij_run_command({ session: 'tests', command: ['bun', 'test'] })

Tabs & panes

A session has tabs; each tab has panes. Pane ids are ephemeral per session — capture the return value of zellij_new_pane / zellij_run_command (they return { paneId }).

Shell

New panes launch bash by default, not the user's interactive shell (e.g. nushell). Override per-call with the shell parameter, or globally via --shell/$ZELLIJ_MCP_SHELL.

Errors

Tool failures return { isError: true, content: [{ type: 'text', text: '[KIND] message' }] } — they never throw to the host. Stable kinds include [SESSION_NOT_FOUND], [SESSION_NAME_INVALID], [PANE_NOT_FOUND], [ZELLIJ_NONZERO], [TIMEOUT]. Read the bracketed prefix and self-correct.

Tool reference (30 tools)

Every tool accepts an optional session: string. Read-only tools carry readOnlyHint; mutating tools carry destructiveHint or openWorldHint.

Session (6)

| Tool | Purpose | | ----------------------- | ------------------------------------------------------ | | zellij_list_sessions | Read-only. All zellij sessions visible to this user. | | zellij_create_session | Create a detached background session. Idempotent. | | zellij_kill_session | Kill one session. Registry-gated unless force: true. | | zellij_session_info | Read-only. Tab/pane/client counts for a session. | | zellij_rename_session | Rename. | | zellij_dump_layout | Read-only. Current layout as KDL text. |

Tabs (8)

| Tool | Purpose | | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | zellij_list_tabs | Read-only. JSON tab list. | | zellij_new_tab | Create a tab (name?, layout?, cwd?). | | zellij_go_to_tab | Focus by tabId, name, or index. | | zellij_rename_tab / zellij_close_tab / zellij_move_tab / zellij_tab_info / zellij_toggle_fullscreen_tab | As named. |

Panes (10)

| Tool | Purpose | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | zellij_list_panes | Read-only. JSON panes (with tabId? filter). | | zellij_new_pane | Open a pane (direction?, floating?, name?, cwd?, shell?). Returns paneId. | | zellij_run_command | Spawn a command in a new pane (command: string[], cwd?, name?, floating?, closeOnExit?, tabId?). Returns paneId. | | zellij_close_pane / zellij_focus_pane / zellij_rename_pane / zellij_resize_pane / zellij_move_pane / zellij_toggle_floating_panes / zellij_toggle_fullscreen / zellij_stack_panes | As named. |

Terminal I/O (5)

| Tool | Purpose | | -------------------- | --------------------------------------------------------------------------------------------- | | zellij_write_chars | Send text to a pane (no newline). | | zellij_send_keys | Send named keys: ['Enter'], ['Ctrl-c'], ['Escape'], ['Tab'], ['Up'], ['F1'], etc. | | zellij_read_pane | Capture pane viewport (full: true for scrollback, ansi: true for escapes). | | zellij_stream_pane | Subscribe to a pane for durationMs (default 2000, max 30000). Returns frames. | | zellij_run_command | (See Panes — it spawns + returns paneId.) |

Editor (1)

| Tool | Purpose | | ------------------ | ------------------------------------------------------------------- | | zellij_edit_file | Open a file in $EDITOR inside a pane (paneId, path, line?). |

Common workflows

"Run this command and tell me what happened"

const { paneId } = await zellij_run_command({
  command: ['cargo', 'test', '--', '--nocapture'],
  cwd: '/path/to/repo',
  name: 'cargo-test',
})
const out = await zellij_read_pane({ paneId, full: true })

"Run a long-running server and tail it"

const { paneId } = await zellij_run_command({
  command: ['bun', 'run', 'dev'],
  cwd: '/path/to/app',
  name: 'dev-server',
})
// Now poll:
const viewport = await zellij_read_pane({ paneId })
// Or stream 5 seconds of output:
const { frames } = await zellij_stream_pane({ paneId, durationMs: 5000 })

"Drive an interactive TUI (lazygit, btop, claude)"

const { paneId } = await zellij_run_command({ command: ['lazygit'], cwd: '/repo' })
// Send keystrokes:
await zellij_send_keys({ paneId, keys: ['Enter'] })
await zellij_send_keys({ paneId, keys: ['q'] })           // quit
const final = await zellij_read_pane({ paneId, ansi: true })

"Multi-session workspace"

await zellij_create_session({ name: 'frontend' })
await zellij_create_session({ name: 'backend' })
await zellij_run_command({ session: 'frontend', command: ['bun', 'run', 'dev'], cwd: '/app/ui' })
await zellij_run_command({ session: 'backend',  command: ['go', 'run', '.'],     cwd: '/app/api' })
// Later:
const sessions = await zellij_list_sessions()

"Use nushell for one specific pane"

const { paneId } = await zellij_new_pane({ shell: 'nu' })

CLI

zellijmcp is a crustjs CLI. Bare invocation runs the MCP server. Subcommands exist for out-of-band inspection:

zellijmcp                              # run MCP server (default)
zellijmcp -h                           # help
zellijmcp -V                           # version

zellijmcp session list                 # list all zellij sessions
zellijmcp session list --mine          # only registry-tracked sessions
zellijmcp session kill <name>          # kill one (refuses untracked unless --force)
zellijmcp session kill-all             # kill everything we created
zellijmcp doctor                       # diagnostic check

# Common global flags (inherited by subcommands):
#   -s, --session <NAME>     Default target session (default: mcp)
#   -S, --shell <PATH>       Shell for new panes (default: bash)
#       --pid-scope          Use mcp-<pid> as default session name
#   -d, --debug              Verbose logging (stderr only; stdout is JSON-RPC)
#       --trace              Trace logging
#       --log-file [<dir>]   Tee logs to ~/.zellijmcp/logs/zellijmcp-<pid>.log
#       --json               Emit JSON from subcommands (where supported)

Environment variables

| Var | Purpose | Default | | ------------------------------- | ------------------------------------ | -------------------------------------------- | | ZELLIJ_MCP_BIN | Path to zellij binary | zellij (PATH lookup) | | ZELLIJ_MCP_SESSION_NAME | Default session name | mcp | | ZELLIJ_MCP_SHELL | Shell for new panes/sessions | bash | | ZELLIJ_MCP_NO_ENSURE_SESSION | Skip boot-time session creation | 0 | | ZELLIJ_MCP_KILL_ON_SHUTDOWN | Kill registry sessions on MCP exit | 0 | | ZELLIJ_MCP_LOG_DIR | Log directory | ~/.zellijmcp/logs/ | | ZELLIJ_MCP_REGISTRY | Registry path | ~/.zellijmcp/sessions.json | | ZELLIJ_MCP_SESSION_BLOCKLIST | Colon-separated extra blocked names | (includes main and $ZELLIJ_SESSION_NAME) | | ZELLIJ_MCP_ALLOW_USER_SESSION | Let the agent see the user's session | 0 |

How it works (under the hood)

No plugins, no daemons, no IPC. Every tool call is one or two zellij CLI invocations with --session <name>:

  • zellij --session mcp action list-panes --json → typed JSON
  • zellij --session mcp action dump-screen --pane-id 3 → pane viewport as text
  • zellij --session mcp run --cwd /repo --name build -- make test → new pane, returns terminal_43
  • zellij --session mcp subscribe --pane-id 3 --format json → pane render frames

Stdout of zellijmcp is reserved for JSON-RPC; all logging goes to stderr (or a file) so it can't corrupt the protocol. The agent's session is addressed exclusively via --session, so the human's session is never touched.

Full internals: docs/01-architecture.md.

Project layout

zellijmcp/
├── src/
│   ├── cli/             crustjs entry + commands + handlers
│   ├── mcp/             McpServer setup + safe-tool wrapper + 30 tools
│   ├── zellij/          spawn + parse + high-level client
│   ├── session/         registry + lifecycle + default-name resolution
│   ├── config.ts        env + flag → ResolvedConfig
│   ├── errors.ts        typed discriminated-union errors
│   ├── log.ts           logtape (stderr-only)
│   └── index.ts         public API
├── test/                145 bun tests + fixtures/fake-zellij.sh
├── docs/                9 design + reference docs
└── .github/workflows/   ci.yml + release.yml

Documentation

Development

git clone https://github.com/mdrv/zellijmcp
cd zellijmcp
bun install
bun test               # 145 tests
bun run typecheck      # tsc --noEmit
bun run fc             # dprint check
bun b                  # build to dist/

License

MIT © Umar Alfarouk