Sign in

    mcpbox

    A self-contained MCP client CLI for discovering and calling Model Context Protocol servers

    mcp
    model-context-protocol
    cli
    client
    agent
    moonbit
    Download zip
    Author
    Version
    0.1.6
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    26

    #mcpbox 📦 — carry MCP tools into scripts, CLIs and agents

    CI npm License

    mcpbox is a self-contained Model Context Protocol client and command line, written in MoonBit. It is for developers and coding agents that want the MCP tools they already have configured — from Cursor, Claude Code/Desktop, Windsurf, VS Code or an mcpbox file — available in a terminal, a script or a generated agent.

    It is deliberately small: one-shot by default, with an optional keep-alive daemon for repeated commands. No Node runtime is needed by mcpbox itself.

    #Install

    Try it without installing anything:

    npx @tomasko/mcpbox --version

    Install it for repeated use:

    npm install -g @tomasko/mcpbox # or curl -LsSf https://raw.githubusercontent.com/tomasky/mcpbox/master/install.sh | bash # or, with the MoonBit toolchain: builds from source into ~/.moon/bin moon install kupas/mcpbox/cmd/mcpbox

    #Quick start

    List the servers mcpbox found. This only reads configuration and starts nothing:

    mcpbox list

    Start them and show the tools each one advertises:

    mcpbox list --all

    Call a tool. The first argument is <server>.<tool>; the rest are arguments:

    mcpbox call context7.resolve-library-id query="React hooks docs" libraryName=react

    Point it at a server you have not configured yet:

    mcpbox list --http-url https://mcp.context7.com/mcp mcpbox call adhoc.echo --stdio "python3 echo_server.py" name=hi count=5

    #What makes it pleasant

    • Schema-aware arguments. count=5 becomes the JSON number 5, flag=true a boolean and tags=a,b an array, using the tool's inputSchema. You only reach for JSON when you want to: meta:={"any":"json"}.
    • Fast discovery. mcpbox list prints configured server names without starting any of them. mcpbox list --all starts them in parallel and shows their tools.
    • Documented discovery. mcpbox list <server> and mcpbox list --all show tool descriptions and parameter docs, including choices and defaults. Use --no-docs for bare signatures.
    • Optional keep-alive. -b keeps initialized sessions in a background process, avoiding repeated stdio server startup.
    • Stable JSON. --json produces output meant to be parsed, while the default is meant to be read.
    • Real exit codes. A failed call exits non-zero, so a script or an agent can tell the difference.
    • Two transports. stdio child processes and Streamable HTTP, including text/event-stream responses, session ids, gzip decoding, and same-origin redirects that keep POST and DELETE semantics.

    #Commands

    CommandPurpose
    mcpbox list [server ...]List configured server names; --all starts them and shows tools
    mcpbox call <server>.<tool> [key=value ...]Call a tool
    mcpbox config [filter ...]Show config files and the resolved server set
    mcpbox stop <server>Close kept-alive sessions with this name
    mcpbox stop allClose all kept-alive sessions and exit the daemon
    mcpbox versionPrint the version
    mcpbox helpShow help

    Useful flags: --all, -b / --background, --no-docs, --quiet / -q, --json, --brief, --raw, --timeout <seconds>, --http-url <url>, --stdio "<command>". A -- stops option parsing so option-like words after it remain tool arguments. config filters match name substrings, with * matching any sequence and ? one character; repeat the filter to match any of several names.

    #Documentation and startup

    mcpbox list is configuration-only and instant: it prints server names without starting anything. Naming a server (mcpbox list ct7) or passing --all starts the selected servers. Human-readable tool listings then include JSDoc-style descriptions and @param comments, plus server instructions when supplied. JSON output keeps its existing structured format; --brief stays compact.

    While servers start, a terminal gets a one-line spinner with elapsed seconds on stderr. Pipes and CI logs get plain mcpbox: connecting... lines instead; --quiet hides progress, and --json suppresses it automatically so machine-readable output stays clean.

    #Keep servers warm

    mcpbox list ct7 -b # start once; keep the MCP session alive mcpbox call ct7.query-docs libraryId=/facebook/react query="hooks" mcpbox list ct7 # reuse the running service automatically mcpbox stop ct7 # stop every warm session named ct7 mcpbox stop all # stop every session and the service itself

    Without -b and without an existing service, commands remain one-shot. The service is per-user; sessions are isolated by working directory, config override, and server name. Changed transport settings or environment replace the warm session. Each command sends its resolved config and environment rather than relying on the service's original working directory.

    The daemon serializes requests to avoid concurrent readers on an MCP stream. A request that loses its response is not retried, since a tool may already have performed side effects. After a transport error, the affected session is dropped; the next command can open a new one.

    IPC uses loopback TCP authenticated by a random secret in a private (0700) directory: $XDG_RUNTIME_DIR/mcpbox, or ~/.cache/mcpbox/run when unset. Server stderr is appended to daemon.log there. Treat this log as potentially sensitive. The daemon runs until stop all; stop <server> leaves it available. Shutdown closes stdin, then sends TERM/KILL if a stdio child ignores EOF. Background mode currently requires POSIX (Linux/macOS).

    An agent skill is available at skills/mcpbox/SKILL.md. The standalone lifecycle regression script accepts an already-built binary: bash tests/daemon.sh /absolute/path/to/mcpbox. It does not build anything.

    #Configuration

    mcpbox reads the same mcpServers blocks the other MCP clients use, so an existing configuration works as-is. Files are JSONC: // and /* */ comments and trailing commas are accepted.

    A minimal mcpbox.json:

    { "mcpServers": { "context7": { "url": "https://mcp.context7.com/mcp" }, "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "env": { "TOKEN": "${FS_TOKEN}" } } } }

    Files are read in precedence order, with the first definition of a name winning. MCPBOX_CONFIG is trimmed and a leading ~ is expanded, so a quoted "~/mcp.json" behaves like the layers below.

    1. $MCPBOX_CONFIG
    2. ./mcpbox.json, ./.mcpbox.json, ./config/mcpbox.json, ./.mcpbox/config.json
    3. ./.cursor/mcp.json, ./.vscode/mcp.json
    4. ~/.mcpbox.json
    5. ~/.config/mcpbox/config.json
    6. ~/.cursor/mcp.json, ~/.claude.json, ~/.codeium/windsurf/mcp_config.json, ~/.config/Claude/claude_desktop_config.json, ~/Library/Application Support/Claude/claude_desktop_config.json

    String values support ${VAR} and ${VAR:-fallback}, including placeholders embedded inside a longer value. An unset variable with no fallback expands to the empty string. An explicit stdio cwd is resolved against the config file that declares it, so relative paths do not depend on where mcpbox was launched.

    #Build from source

    moon build --target native --release _build/native/release/build/cmd/mcpbox/mcpbox.exe --help

    The release binaries are linked statically against musl on Linux, so they run on any distribution regardless of its glibc version.

    #Development

    moon check --target native --deny-warn moon test --target native moon info && moon fmt

    The stdio and HTTP transports are exercised end to end against the fixture server in tests/fixtures/echo_server.py:

    moon build --target native _build/native/debug/build/cmd/mcpbox/mcpbox.exe list \ --stdio "python3 tests/fixtures/echo_server.py"

    #License

    Apache-2.0

    McpError

    pub(all) suberror McpError {
    McpError(String)
    }

    The single error type the library raises. Everything user-facing — a bad config, an unreachable server, a failed tool — arrives here as a message that the CLI prints as-is.

    CallResult

    pub(all) struct CallResult {
    content : Array[Content]
    is_error : Bool
    structured : Json?
    } derive(
    Debug
    )

    The result of a tools/call.

    Content

    pub(all) enum Content {
    Text(String)
    Image(String, String)
    Audio(String, String)
    EmbeddedResource(Json)
    Unknown(Json)
    } derive(
    Debug
    )

    One block of a tool result. The first two kinds are the ones every client understands; the rest are kept verbatim so nothing is silently dropped.

    Content::to_repr

    HttpSpec

    pub(all) struct HttpSpec {
    url : String
    headers : Map[String, String]
    } derive(
    Debug
    )

    A Streamable HTTP endpoint, with any static headers the server needs.

    HttpSpec::to_repr

    LiveSession

    pub struct LiveSession {
    session : Session
    // private fields
    }

    A session that outlives the call that created it, for the resident daemon.

    with_server ties a session to a callback so cleanup is automatic. The daemon instead keeps one LiveSession per server between commands, which is where the startup saving comes from: the child process is spawned once rather than once per mcpbox list or mcpbox call.

    A stdio child is started as an orphan, so it stays alive when the task that spawned it returns. Shutdown closes stdin first, then escalates to signals if the child does not exit voluntarily.

    LiveSession::close

    async fn LiveSession::close(self : LiveSession) -> Unit

    Tear the transport down and reap the child, escalating if it ignores EOF. Calling this twice is harmless.

    LiveSession::session

    fn LiveSession::session(self : LiveSession) -> Session

    The session inside, for listing tools and making calls.

    RpcError

    pub(all) struct RpcError {
    code : Int
    message : String
    data : Json?
    } derive(
    Debug
    )

    A JSON-RPC 2.0 error object.

    RpcError::to_repr

    RpcMessage

    pub(all) enum RpcMessage {
    Response(Int, Json?, RpcError?)
    ServerRequest(Json, String, Json?)
    Notification(String, Json?)
    } derive(
    Debug
    )

    One decoded JSON-RPC message from a server.

    Server

    pub(all) struct Server {
    name : String
    kind : ServerKind
    note : String
    } derive(
    Debug
    )

    One configured MCP server.

    Server::to_repr

    ServerInfo

    pub(all) struct ServerInfo {
    name : String
    version : String
    protocol_version : String
    instructions : String
    } derive(
    Debug
    )

    What a server reports about itself during initialize.

    ServerKind

    pub(all) enum ServerKind {
    Stdio(StdioSpec)
    Http(HttpSpec)
    } derive(
    Debug
    )

    The transport half of a server definition.

    Session

    pub struct Session {
    // private fields
    }

    A live, initialized MCP session.

    Session::call_tool

    async fn Session::call_tool(self : Session, name : String, arguments : Json, timeout_ms : Int) -> CallResult

    Call one tool. arguments is the already-built JSON object.

    Session::list_tools

    async fn Session::list_tools(self : Session, timeout_ms : Int) -> Array[Tool]

    List the server's tools.

    Session::name

    fn Session::name(self : Session) -> String

    The server's name, for messages.

    Session::server_info

    fn Session::server_info(self : Session) -> ServerInfo

    What the server reported during initialize.

    StdioSpec

    pub(all) struct StdioSpec {
    command : String
    args : Array[String]
    env : Map[String, String]
    cwd : String
    } derive(
    Debug
    )

    How mcpbox reaches a stdio server: a child process that speaks newline-delimited JSON-RPC on its standard input and output.

    Tool

    pub(all) struct Tool {
    name : String
    title : String
    description : String
    input_schema : Json
    } derive(
    Debug
    )

    A tool advertised by a server's tools/list.

    Tool::to_repr

    build_arguments

    fn build_arguments(pairs : Array[String], schema : Json) -> Json raise McpError

    Build the arguments object for a tool call.

    call_result_to_json

    fn call_result_to_json(result : CallResult) -> Json

    The JSON view of a tool call result.

    coerce_value

    fn coerce_value(raw : String, schema : Json?) -> Json

    Coerce a key=value argument using the tool's schema. This is where mcpbox improves on a plain string passthrough: count=5 becomes a JSON number and tags=a,b a JSON array when the schema says so, so callers do not have to hand-write JSON for ordinary values. Anything that does not parse falls back to the string, and key:=<json> is always available for the exact form.

    config_files

    async fn config_files() -> Array[(String, Int)] raise McpError

    The config files that exist on this machine, in precedence order, with the number of servers each contributes. Used by mcpbox config.

    content_to_json

    fn content_to_json(content : Content) -> Json

    The JSON view of one content block, in the shape MCP uses.

    decode_rpc_message

    fn decode_rpc_message(json : Json) -> RpcMessage? raise McpError

    Decode one JSON-RPC message. Returns None for a well-formed object that is neither a response nor a notification mcpbox cares about, so the caller can simply skip it.

    expand_env

    fn expand_env(text : String) -> String

    Expand ${VAR} and ${VAR:-fallback} in a string. An unset variable with no fallback expands to the empty string rather than failing, so a missing optional token does not make the whole config unreadable.

    fail

    fn fail(message : String) -> McpError

    Raise a McpError with a formatted message.

    first_line

    fn first_line(text : String) -> String

    The first line of a description, so a table stays one line per tool.

    help_text

    fn help_text() -> String

    The mcpbox help text.

    load_servers

    async fn load_servers() -> Array[Server] raise McpError

    Load every server mcpbox can find. Names are deduplicated with the first definition winning, matching the precedence of config_candidates.

    main_cli

    async fn main_cli() -> Unit

    Entry point for the mcpbox executable. A failure is reported once, on stderr, and turns into a non-zero exit status.

    mcpbox_exit

    fn mcpbox_exit(code : Int) -> Unit

    Terminate the process with the given status. Used once the CLI has printed its result so scripts and agents see a meaningful exit code.

    open_live_session

    async fn open_live_session(server : Server, timeout_ms : Int, inherit_env? : Bool) -> LiveSession

    Connect and handshake without owning the transport for a callback's lifetime. The caller must eventually call LiveSession::close; the daemon does so when a server is stopped.

    parse_call_result

    fn parse_call_result(result : Json) -> CallResult raise McpError

    Decode a tools/call result.

    parse_content

    fn parse_content(json : Json) -> Content

    Decode one content block.

    parse_jsonc

    fn parse_jsonc(text : String) -> Json raise McpError

    Parse JSONC text into a Json value.

    parse_tool

    fn parse_tool(json : Json) -> Tool raise McpError

    Decode one tool descriptor, tolerating servers that omit optional fields.

    parse_tools

    fn parse_tools(result : Json) -> Array[Tool] raise McpError

    Decode a tools/list result.

    protocol_version

    let protocol_version : String

    The MCP revision mcpbox advertises first. Servers that predate it answer with the revision they do support, and mcpbox accepts whatever comes back: the wire format is unchanged across the revisions in use.

    render_call_result

    fn render_call_result(result : CallResult) -> String

    The text a human sees for a tool call: the text blocks joined, images and audio summarised, and structured content printed as JSON.

    render_instructions

    fn render_instructions(text : String, indent : String) -> Array[String]

    A server's instructions clipped to one readable line, or no lines when it sent none.

    render_params

    fn render_params(schema : Json) -> String

    Render a tool's parameters as name: type, optional?: type, with the names sorted so the output is stable across runs.

    render_tool_doc

    fn render_tool_doc(tool : Tool, indent : String) -> Array[String]

    A JSDoc-style comment for a tool: the description, then one @param line per documented argument. This is the shape a TypeScript MCP client shows, and it turns a bare signature into something a reader can act on. Tools with nothing to say render no lines at all.

    render_tool_signature

    fn render_tool_signature(tool : Tool) -> String

    A one-line signature for a tool: name(params).

    rpc_error_response

    fn rpc_error_response(id : Json, code : Int, message : String) -> Json

    Build a JSON-RPC error response, echoing the request's id.

    rpc_notification

    fn rpc_notification(method_name : String, params : Json?) -> Json

    Build a JSON-RPC 2.0 notification: a request with no id, which the server must not answer.

    rpc_request

    fn rpc_request(id : Int, method_name : String, params : Json?) -> Json

    Build a JSON-RPC 2.0 request object. The repr keeps ids integral in the encoded form instead of printing them as 1.0.

    rpc_result_response

    fn rpc_result_response(id : Json, result : Json) -> Json

    Build the response to a server-initiated request, echoing its id.

    run

    async fn run(args : Array[String]) -> Unit

    The dispatch table.

    schema_choices

    fn schema_choices(schema : Json) -> String

    The allowed values of an enum schema, comma separated.

    schema_description

    fn schema_description(schema : Json) -> String

    The description of a schema node, if it has one.

    schema_properties

    fn schema_properties(schema : Json) -> Map[String, Json]

    The properties map of a tool schema, if any.

    schema_type

    fn schema_type(schema : Json) -> String

    The JSON Schema type of a property, rendered for a signature.

    server_info_to_json

    fn server_info_to_json(info : ServerInfo) -> Json

    The JSON view of a server's initialize result.

    server_to_json

    fn server_to_json(server : Server) -> Json

    The JSON view of a server definition.

    servers_from_document

    fn servers_from_document(json : Json, key : String, source : String) -> Array[Server] raise McpError

    Extract the servers from one parsed config document. Returns an empty array when the file has no server block, so an unrelated mcp.json is skipped rather than reported as broken.

    tool_to_json

    fn tool_to_json(tool : Tool) -> Json

    The JSON view of a tool.

    transport_label

    fn transport_label(server : Server) -> String

    A short transport label, e.g. http or stdio.

    transport_target

    fn transport_target(server : Server) -> String

    Where a server connects to, for a listing.

    version

    let version : String

    The mcpbox version, kept in sync with moon.mod and npmjs/package.json. It is also what the client reports to servers during initialize.

    with_server

    async fn with_server(server : Server, timeout_ms : Int, f : async (Session) -> Unit) -> Unit

    Connect to a server, run the initialize handshake, hand the session to f, and tear the transport down again. The server's child process is owned by the enclosing task group, so an error anywhere still reaps it.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io