Skip to main content

Plugin-based MCP server framework — routes any CLI tool through Source → Executor → Sink pipelines

Project description

mcpipe

PyPI Python License CI coverage

Plugin-based MCP server that keeps CLI output out of your context window.

Any command-line tool can be exposed as an MCP tool. Output gets cached to disk — the LLM gets back a handle and uses generic framework tools (view, search) to read what it needs instead of having the full dump shoved into the conversation.

Zero dependencies. Python 3.12+.

How it works

LLM calls:  git_log(since="1week")
Returns:    { handle: "git_log_1716000000_a1b2c3d4", lines: 847, preview: "..." }

LLM calls:  view(handle="git_log_1716000000_a1b2c3d4", _search="auth")
Returns:    matching lines only

One tool produces. Generic tools consume. Plugins don't implement search or pagination.

Install

# From PyPI
pip install mcpipe

# From source
pip install .

Usage

MCP server (for LLM clients)

mcpipe server

Speaks JSON-RPC 2.0 over stdio. Point your MCP client at it.

CLI

mcpipe run git_log since="1 week ago"
mcpipe run docker_ps all=true
mcpipe view <handle> -T search pattern="error"
mcpipe list

Configuration

mcpipe reads configuration from (highest priority first):

  1. Environment variables (MCPIPE_*)
  2. Config file: $XDG_CONFIG_HOME/mcpipe/config.toml (default: ~/.config/mcpipe/config.toml)
  3. Defaults
[cache]
dir = "/custom/cache/path"     # default: $XDG_RUNTIME_DIR/mcpipe
ttl = 7200                     # seconds, default: 3600
inline_threshold = 100         # lines, default: 50

[authoring]
enabled = true                 # default: false

[paths]
allowed = ["/home/user/code", "/data"]  # default: CWD only
Environment variable Config key Description
MCPIPE_CACHE_DIR cache.dir Cache directory
MCPIPE_CACHE_TTL cache.ttl Default cache TTL (seconds)
MCPIPE_INLINE_THRESHOLD cache.inline_threshold Lines below which output is inline
MCPIPE_ENABLE_AUTHORING authoring.enabled Enable authoring tools (1 to enable)
FS_ROOTS paths.allowed Colon-separated allowed filesystem roots

Built-in plugins

Git

git_status, git_log, git_diff, git_diff_unstaged, git_diff_staged, git_show, git_branch, git_add, git_commit, git_reset, git_create_branch, git_checkout, git_fetch, git_pull, git_push, git_stash_push, git_stash_pop, git_stash_list, git_tag, git_blame, git_cherry_pick, git_revert, git_remote, git_merge

Docker

docker_ps, docker_logs, docker_images

Docker Compose

compose_ps, compose_logs, compose_up, compose_down, compose_restart, compose_stop, compose_start, compose_config, compose_top, compose_images, compose_pull, compose_build, compose_exec, compose_run

Filesystem

fs_read, fs_ls, fs_stat, fs_find, fs_grep, fs_roots, fs_write, fs_mkdir, fs_rm, fs_mv, fs_cp

Access is restricted to allowed directory trees. Configure via paths.allowed in your config file, or set FS_ROOTS to a colon-separated list of paths.

Transforms

Output post-processing is pluggable. Transforms are pure functions — lines in, lines out. They run after caching and never mutate the cache.

Built-in transforms

All built-ins can be overridden — a user @transform with the same name replaces them (user extensions load after builtins).

Transform Description Params
search Filter lines by regex pattern (case-insensitive) pattern: str
limit Return at most N lines from the start n: int = 50
offset Skip the first N lines n: int = 0
head Return the first N lines n: int = 10
tail Return the last N lines n: int = 10

Meta-params

Any tool call can include transform meta-params prefixed with _:

{ "name": "git_log", "arguments": { "since": "1week", "_search": "fix", "_limit": 10 } }

These are desugared into transform steps before dispatch — plugins never see them.

Custom transforms

Custom transforms use the @transform decorator and live in ~/.config/mcpipe/transforms/:

from typing import Annotated
from mcpipe import transform

@transform("Sort lines alphabetically")
def sort(
    lines: list[str],
    reverse: Annotated[bool, "Sort in reverse order"] = False,
) -> list[str]:
    return sorted(lines, reverse=reverse)

The first argument must be lines: list[str], and the function must return list[str]. Additional parameters become the transform's config schema.

Writing plugins

A plugin is a Python file in mcpipe/plugins/ (built-in) or ~/.config/mcpipe/plugins/ (user).

Public API

Everything a plugin needs is importable from mcpipe:

from mcpipe import tool, Cmd, transform, TransformStep, ToolOutput

@tool(description, *, ...)

Decorator that registers a function as an MCP tool. The function name becomes the tool name. Type hints generate the JSON Schema.

The decorated function must return Cmd (to run a subprocess) or str (direct output).

@tool(
    description: str,           # Tool description shown to LLM
    *,
    read_only: bool = False,    # Tool only reads, never modifies
    destructive: bool = True,   # Tool may cause irreversible changes
    idempotent: bool = False,   # Safe to call repeatedly with same args
    open_world: bool = True,    # Tool interacts with external world
    ttl: int | None = None,     # Cache TTL in seconds (None = default)
    output_filter: list[TransformStep] | None = None,  # Default transforms
    meta_params: bool = True,   # Inject _search/_limit/etc. into schema
)

Cmd(*args: str)

Return from a @tool function to run a subprocess. Args are passed to asyncio.create_subprocess_exec.

Cmd("git", "-C", repo_path, "log", "--max-count=10")

ToolOutput

Structured result from tool execution (returned by execute()). You don't construct this directly — the framework builds it.

Field Type Description
handle str Cache handle for the output
total_lines int Total line count before transforms
text str | None Full output (inline if small or transformed)
preview str | None First few lines (large non-transformed output)
is_error bool Whether the tool failed

@transform(description)

Decorator that registers a transform function. Must accept lines: list[str] as the first argument and return list[str]. If a transform with the same name already exists, it is overwritten.

@transform(
    description: str,  # Transform description
)

Example:

@transform("Sort lines alphabetically")
def sort(lines: list[str], reverse: bool = False) -> list[str]:
    return sorted(lines, reverse=reverse)

TransformStep(name, params)

A single transform invocation, used in output_filter:

output_filter=[TransformStep("head", {"n": 10})]

Examples

from typing import Annotated
from mcpipe import Cmd, tool

@tool("List running containers", read_only=True, destructive=False, idempotent=True)
def docker_ps(
    all: Annotated[bool, "Show all containers (including stopped)"] = False,
    format: Annotated[str, "Go template for output format"] = "",
) -> Cmd:
    args = ["docker", "ps"]
    if all:
        args.append("--all")
    if format:
        args.extend(["--format", format])
    return Cmd(*args)

Return Cmd to run a subprocess, or str for direct output. Type hints generate the MCP input schema automatically. Use Annotated[type, "description"] to add descriptions to parameters — these appear in the tool's JSON Schema.

Pure Python tools

Return str instead of Cmd for tools that don't need a subprocess:

from typing import Annotated
from mcpipe import tool

@tool("Count lines in a file", read_only=True, destructive=False)
def count_lines(
    path: Annotated[str, "File path"],
) -> str:
    with open(path) as f:
        return str(sum(1 for _ in f))

Fuller example — git log

A more complete example showing a helper function, input validation, and how Annotated args map to the subprocess call:

from typing import Annotated
from mcpipe import Cmd, tool

def _git(*args: str, repo_path: str = ".") -> Cmd:
    """Build a git Cmd with -C repo_path prefix."""
    return Cmd("git", "-C", repo_path, *args)

def _validate_ref(value: str, label: str = "value") -> None:
    """Reject refs starting with '-' to prevent flag injection."""
    if value.startswith("-"):
        raise ValueError(f"Invalid {label}: '{value}' — cannot start with '-'")

@tool("Show commit log", read_only=True, destructive=False, idempotent=True)
def git_log(
    repo_path: Annotated[str, "Path to the git repository"] = ".",
    max_count: Annotated[int, "Number of commits to show"] = 10,
    since: Annotated[str, "Show commits after this date (e.g. '1 week ago')"] = "",
    path: Annotated[str, "Limit to commits touching this path"] = "",
) -> Cmd:
    args = ["log", f"--max-count={max_count}"]
    if since:
        _validate_ref(since, "since")
        args.extend(["--since", since])
    if path:
        _validate_ref(path, "path")
        args.extend(["--", path])
    return _git(*args, repo_path=repo_path)

When an LLM calls git_log(max_count=5, since="1 week ago"), this builds and runs: git -C . log --max-count=5 --since '1 week ago'.

Default output filters

Tools can declare default transforms that apply when the caller doesn't send any _meta params. Useful for keeping verbose output short by default:

from mcpipe import Cmd, tool
from mcpipe.transform import TransformStep

@tool(
    "Push commits to remote",
    read_only=False,
    output_filter=[TransformStep("head", {"n": 10})],
)
def git_push(...) -> Cmd:
    ...

Caller-provided transforms replace defaults entirely — no merging.

Opting out of meta-params

By default, every tool gets transform meta-params (_search, _limit, etc.) injected into its schema. Tools that shouldn't be filtered (config, help, authoring) can opt out:

@tool("Show help text", read_only=True, meta_params=False)
def my_help() -> str:
    return "..."

LLM self-authoring

An LLM connected to mcpipe can create its own tools and transforms at runtime — no restarts, no manual file editing. This is the core design: if a tool doesn't exist yet, the LLM writes it, reloads, and uses it immediately.

Authoring tools are disabled by default. Enable with authoring.enabled = true in your config or MCPIPE_ENABLE_AUTHORING=1.

How it works

mcpipe exposes framework tools for managing user extensions:

Tool Purpose
authoring_help Returns the full plugin/transform API guide
write_plugin Creates or overwrites a user plugin file
write_transform Creates or overwrites a user transform file
read_extension Reads a user plugin/transform source file
list_user_extensions Lists files in the user config dirs
delete_plugin / delete_transform Removes a file
reload Hot-reloads all modules to pick up changes

User extensions live in ~/.config/mcpipe/ (or $XDG_CONFIG_HOME/mcpipe/):

  • plugins/*.py — tools
  • transforms/*.py — output transforms

Example: LLM creates a kubectl plugin

User: "I need to check my Kubernetes pods"

LLM:  1. Calls authoring_help(topic="plugin") — reads the API
      2. Calls write_plugin(name="kubectl", content="...")
      3. Calls reload() — new tools are live
      4. Calls kubectl_get_pods(namespace="production")
      5. Calls view(handle="...", _search="CrashLoopBackOff")

The plugin persists across sessions. Next time the LLM connects, kubectl_get_pods is already available.

Development

uv sync --dev          # install deps
uv run poe hooks       # install git hooks
uv run poe check       # lint + typecheck + tests
uv run poe test        # tests only
uv run poe lint        # ruff
uv run poe format      # ruff format
uv run poe release     # bump version, changelog, tag, push

Commits follow Conventional Commits. Versioning and changelogs are managed by commitizen.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcpipe-0.1.1.tar.gz (104.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcpipe-0.1.1-py3-none-any.whl (44.5 kB view details)

Uploaded Python 3

File details

Details for the file mcpipe-0.1.1.tar.gz.

File metadata

  • Download URL: mcpipe-0.1.1.tar.gz
  • Upload date:
  • Size: 104.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mcpipe-0.1.1.tar.gz
Algorithm Hash digest
SHA256 0327e243a28713296abb8ed304c6543799ef990594e79133ef3b2cead3429fae
MD5 f333ab80fc1acb4baca7fce553d3979f
BLAKE2b-256 d469b3328b1b1ef6885507bae6cf7bc1e134fb69ae83a5c1e667797fa289a2f5

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcpipe-0.1.1.tar.gz:

Publisher: publish.yml on davetothek/mcpipe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mcpipe-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: mcpipe-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 44.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mcpipe-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4d5b642f88c27f42dafa7ee5be581e124f7428f622b48019306b95883010636a
MD5 8b2135d86e216f0580b9ff236d63efab
BLAKE2b-256 b98ac3c524fc0e3a1af987197bcfbb80ba033f6c4ed256a75b098ab538ac51f6

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcpipe-0.1.1-py3-none-any.whl:

Publisher: publish.yml on davetothek/mcpipe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page