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

shell-session-mcp

v0.1.1

Published

MCP server for managing interactive shell/interpreter sessions (bash, gdb, radare2, python, node REPL, ...)

Downloads

299

Readme

shell-session-mcp

An MCP server that exposes interactive shell / interpreter sessions to an MCP client. Sessions run inside a real PTY, so programs that care about being on a terminal — bash, gdb, radare2, python REPL, node, psql, vim, htop — behave exactly as they would for a human user.

The server is designed for the case where a single tool-call-per-line model is a poor fit: long-running interpreters where you want to keep state across many turns, drive a TUI, or watch a process produce output asynchronously.

Features

  • PTY-backed processes via node-pty. Programs see isatty(stdin) == true, get proper window size, and receive signals like SIGWINCH.
  • Numeric session idsspawn returns an id; every other tool addresses sessions by it. Run any number of sessions concurrently.
  • Server-tracked read markread returns only what arrived since the previous read/read_to_file; the server remembers the position so the client never has to pass a cursor. The full session output is also retained (unbounded) and accessible via read({ all: true }).
  • Push notifications, opt-in per session, delivered as standard MCP notifications/message (logging) events:
    • {type: "output", id, chunk, cursor} — a chunk of new data. When notifications are enabled, each pushed chunk also advances the server-side read mark, so it won't be repeated by the next read (no duplication between push and pull).
    • {type: "exit", id, exitCode, signal} — the process ended
    • Exit notifications are always sent, regardless of the output toggle.
  • Context-overflow guard on read — by default refuses to ship more than 32 KiB inline. The size of the pending output is reported back so the caller can decide whether to bypass with force: true or drain to disk.
  • read_to_file — write pending output straight to a file path; only metadata returns over MCP, so the conversation context stays small even for multi-megabyte output.
  • Window resize, arbitrary signals (kill accepts SIGTERM, SIGINT, SIGKILL, …), and session listing.

Install

npm install

node-pty builds a native addon — you need a working C++ toolchain and Python on the host.

Run

The server speaks MCP over stdio:

node index.js
# or once installed
shell-session-mcp

Configuring it as an MCP server

{
  "mcpServers": {
    "shell": {
      "command": "node",
      "args": ["/abs/path/to/shell-session-mcp/index.js"]
    }
  }
}

To receive output / exit notifications the client must (a) declare the logging capability and (b) call logging/setLevel (any level — the server emits at info).

Tools

All tool responses are JSON encoded inside a single text content item.

spawn

Start a new PTY-backed process.

| field | type | required | notes | | ------- | ---------------- | -------- | ------------------------------------------------ | | command | string | yes | executable, e.g. bash, gdb, r2, python3 | | args | string[] | no | argument vector | | cwd | string | no | working directory | | env | record<string> | no | extra env vars, merged onto the server's env | | cols | int | no | PTY columns, default 120 | | rows | int | no | PTY rows, default 40 | | notify | bool | no | enable output notifications immediately |

Returns {id, pid}.

write

Send raw bytes to the PTY's input. You decide the line ending — append \n for shell-style submission, \r for some REPLs. Control characters pass through ( = Ctrl-C,  = Ctrl-D / EOF).

Args: {id, data}{id, written}.

read

By default returns output that arrived since the previous read / read_to_file and advances the server-side read mark. The server tracks the mark internally — there is no client-side cursor.

Pass all: true to return the full session output instead (everything since spawn). This still advances the mark to the end, so the next default read returns only what arrives afterwards.

| field | type | default | notes | | ---------- | ---- | ------- | -------------------------------------------------- | | id | int | — | | | all | bool | false | return full session output instead of only new | | wait_ms | int | 0 | block up to N ms for new output (ignored if all) | | max_bytes | int | 32768 | inline-output size guard (see below) | | force | bool | false | bypass the size guard |

Returns {id, all, data, bytes, cursor, exited, exitInfo} on success, or {id, oversized: true, all, available, max_bytes, hint, …} if the size guard tripped.

The guard exists to protect the client's context window: when the result would exceed max_bytes the data is not embedded in the response, the read mark is not advanced, and the caller is told how big the result would be so they can either retry with force: true, raise max_bytes, or drain to disk via read_to_file.

read_to_file

Write output to a file on disk and return only metadata. The MCP message stream — and the model's context — never sees the data. Use it for build logs, gdb dumps, fuzzer output, or anything you only want to grep through later. Like read, defaults to only new bytes (and advances the mark); pass all: true for the full session.

Args: {id, path, all?, append?, wait_ms?}{id, path, all, bytes, cursor, append, exited, exitInfo}.

set_notifications

Toggle push notifications for new output on a session. Args: {id, enabled}.

When notifications are enabled, chunks emitted by the child are coalesced over a short debounce window (~50 ms) and then pushed as a single {type: "output", …} notification. The bytes covered by that notification are marked consumed, so a subsequent read won't return the same data again. (Anything buffered before notifications were enabled stays unread and is still returned by the next read.)

If a coalesced batch exceeds 32 KiB, the notification carries {oversized: true, available, hint} instead of the data, and the bytes are not marked consumed — the client must pull them explicitly via read (force: true or all: true) or read_to_file. This keeps push notifications from quietly dumping megabytes into the client's context.

Exit notifications fire regardless of this setting.

resize

{id, cols, rows} — propagates SIGWINCH to the child. Useful for full-screen TUIs.

kill

{id, signal?} — default SIGTERM. The session entry is kept after exit so you can still read final output and inspect exitInfo. Drop it with remove.

list

No args. Returns {sessions: [...]} with a summary for every known session (live and exited).

info

{id} → summary for one session.

remove

{id} — drop an exited session from the registry.

Notification payloads

All notifications are notifications/message (the MCP logging channel) with logger: "shell-session-mcp" and level: "info". The interesting bit is params.data:

// Coalesced new output (only if the session has notifications enabled).
// PTY chunks are debounced ~50ms and pushed as one notification. The bytes
// are marked consumed, so the next `read` will not return them again.
{ "type": "output", "id": 3, "chunk": "...bytes...", "cursor": 12345 }

// Heads-up that there is a big chunk waiting (size > 32 KiB). The data is
// NOT in the payload and is NOT consumed — call `read` (with `force: true`,
// or `all: true`) or `read_to_file` to fetch it.
{
  "type": "output", "id": 3, "oversized": true,
  "available": 75052, "cursor": 75144,
  "hint": "Pending output is 75052 bytes (>32768). Call read with force:true (or all:true), or read_to_file."
}

// Process exit (always)
{ "type": "exit", "id": 3, "exitCode": 0, "signal": null }

Example session

// 1. open a bash
spawn   {command: "bash", args: ["--norc", "--noprofile", "-i"], notify: true}
        → {id: 1, pid: 9999}

// 2. drive it
write   {id: 1, data: "ls /etc | head\n"}
read    {id: 1, wait_ms: 200}
        → {id: 1, data: "...", cursor: 412, ...}

// 3. expect a lot of output? drain to a file
write          {id: 1, data: "find / -name '*.so' 2>/dev/null\n"}
read_to_file   {id: 1, path: "/tmp/find.log", since: 412, wait_ms: 5000}
        → {id: 1, bytes: 1438201, cursor: 1438613, ...}

// 4. quit
write   {id: 1, data: "exit\n"}
// → notification: {type: "exit", id: 1, exitCode: 0, signal: null}
remove  {id: 1}

Limitations

  • Output is held as UTF-8 strings. Binary stdin/stdout is not directly representable; for hex-clean transport, base64 in your own protocol layer before write.
  • Output is retained for the entire session lifetime (no cap). For very long-running or chatty processes this can grow unbounded; spool to disk with read_to_file if you don't need it in memory.
  • One process per session. node-pty does not auto-respawn on crash.

License

MIT