mcp-bash follows one rule: do the minimum necessary to translate MCP messages into predictable script executions. No daemons, no servers, no hidden state. The surface area stays small so every part can be inspected and understood.
Figure: High-level dataflow—MCP client sends JSON-RPC over stdio to mcp-bash core, which queries jq/gojq, dispatches into project tools/resources/prompts, and executes system commands.
~/.local/share/mcp-bash/ # or any directory if cloned manually
├─ bin/mcp-bash
├─ lib/
│ ├─ auth.sh
│ ├─ core.sh
│ ├─ completion.sh
│ ├─ elicitation.sh
│ ├─ handler_helpers.sh
│ ├─ hash.sh
│ ├─ json.sh
│ ├─ lock.sh
│ ├─ ids.sh
│ ├─ io.sh
│ ├─ paginate.sh
│ ├─ path.sh
│ ├─ policy.sh
│ ├─ progress.sh
│ ├─ progress-passthrough.sh
│ ├─ logging.sh
│ ├─ runtime.sh
│ ├─ registry.sh
│ ├─ require.sh
│ ├─ resource_content.sh
│ ├─ resource_providers.sh
│ ├─ resources.sh
│ ├─ roots.sh
│ ├─ rpc.sh
│ ├─ prompts.sh
│ ├─ tools.sh
│ ├─ tools_policy.sh
│ ├─ timeout.sh
│ ├─ uri.sh
│ ├─ validate.sh
│ ├─ spec.sh
│ └─ cli/
├─ handlers/
│ ├─ completion.sh
│ ├─ lifecycle.sh
│ ├─ ping.sh
│ ├─ logging.sh
│ ├─ roots.sh
│ ├─ tools.sh
│ ├─ resources.sh
│ └─ prompts.sh
├─ providers/
├─ sdk/
├─ server.d/ # optional project hooks/config
│ ├─ env.sh # environment overrides
│ ├─ register.json # preferred: data-only registration (no shell execution)
│ ├─ register.sh # hook registration (opt-in; executes shell code)
│ └─ policy.sh # optional: tool allow/deny hook
├─ scaffold/
├─ examples/
├─ docs/
├─ test/
└─ .registry/ (generated caches, typically gitignored in projects)
Stable modules live under bin/ and lib/, protocol handlers under handlers/, and dev assets under scaffold/ and examples/. Project extensions (tools/resources/prompts) live in your project tree; registries are written to .registry/.
bin/mcp-bashsources runtime, JSON, RPC, and core libraries, confirms stdout is a pipe or terminal, and entersmcp_core_run.- Each line from stdio is BOM-stripped, trimmed, and compacted with jq/gojq or validated through the minimal tokenizer before dispatch.
- Arrays are accepted when the negotiated protocol is
2025-03-26; newer protocols reject batch arrays unlessMCPBASH_COMPAT_BATCHES=true, in which case batches are decomposed into individual requests. - Dispatch routes lifecycle, ping, logging, tools, resources, prompts, and completion methods; unknown methods return
-32601and notifications remain server-owned. - Responses flow through
rpc_send_lineto guarantee single-line JSON with newline termination and carriage-return scrubbing.
- The main loop handles lifecycle/ping/logging synchronously; async methods (
tools/*,resources/*,prompts/get,completion/complete) spawn workers with per-request state under${TMPDIR}/mcpbash.state.<ppid>.<bashpid>.<seed>. - Workers run in isolated subshells with request-scoped env and use
lib/ids.shto encode ids, trackpid.*andcancelled.*markers, and clean up after completion. lib/lock.sh/lib/io.shenforce mkdir-based stdout locks under${TMPDIR}/mcpbash.locks, strip CR, and validate UTF-8 so each response emits exactly one JSON line.- Cancellation writes
notifications/cancelled, marks ids, and escalates TERM → KILL on the worker process group; cancellation checks happen while holding the stdout lock. - Minimal mode activates when JSON tooling is unavailable; tools/resources/prompts/completion decline requests while lifecycle/ping/logging stay available.
with_timeout <seconds> -- <command…>(fromlib/timeout.sh) runs a watchdog that sends TERM then KILL if a worker outlives the timeout.- Async paths honor
params.timeoutSecswhen jq/gojq is present and wrap tool/resource/prompt/completion handlers withwith_timeout; minimal mode skips per-request overrides. bin/mcp-bashtrapsEXIT INT TERMto runmcp_runtime_cleanup, removing${TMPDIR}/mcpbash.state.*and${TMPDIR}/mcpbash.locks.
handlers/lifecycle.shvalidatesinitialize, negotiates capabilities, waits fornotifications/initializedbefore non-lifecycle traffic, and managesshutdown/exit.handlers/ping.shreturns immediate{ "result": {} }responses as a connectivity check.- Core dispatch blocks non-lifecycle methods until initialization and emits explicit errors when uninitialized or shutting down.
handlers/tools.shimplementstools/listandtools/calland rejects both in minimal mode.lib/tools.shscans thetools/tree (skipping dotfiles), prefersNAME.meta.jsonover inline# mcp:annotations, writes.registry/tools.json, and computes hash/timestamp data for pagination and list_changed notifications.- Cursors are opaque base64url payloads with
ver,collection,offset,hash, andtimestamp;tools/listreturns deterministic slices withnextCursorand exposes the full count as an extension viaresult._meta["mcpbash/total"](not a top-level field) for strict-client compatibility. tools/callwires the SDK env, captures stdout/stderr, surfaces_meta.stderr, emits structured content when metadata declaresoutputSchema, and returnsisErroron tool exit codes.- Embedded resource content: tools can append to
MCP_TOOL_RESOURCES_FILE(JSON array or tab-separatedpath<TAB>mime<TAB>uri) to have the framework emit{type:"resource"}entries in the resultcontentarray; binary files are base64 encoded automatically. - Tool policy hook: if present,
server.d/policy.shdefinesmcp_tools_policy_check()and is invoked before every tool run (default implementation allows all tools). - Declarative registration:
server.d/register.jsoncan provide atoolsarray (and other kinds) without executing project shell code; if present, it takes precedence and invalid input fails loudly (no fallback to hooks). - Hook registration:
server.d/register.shcan return atoolsarray to replace auto-discovery, but it executes shell code and is opt-in (MCPBASH_ALLOW_PROJECT_HOOKS=trueplus safe ownership/permissions).
handlers/resources.shsupportsresources/list,resources/read,resources/subscribe, andresources/unsubscribe, declining them in minimal mode.lib/resources.shdiscovers entries underresources/, prefers metadata files, writes.registry/resources.json, and uses allow-listed providers with path normalization.- Pagination mirrors tools via
lib/paginate.sh, tracking registry hashes for list_changed notifications and returningresources, optionalnextCursor, and an extension count viaresult._meta["mcpbash/total"]. resources/readresolves URIs through providers (defaultproviders/file.sh), enforces roots allow lists, returns MIME hints and_metadiagnostics, and can subscribe; subscription polling (MCPBASH_RESOURCES_POLL_INTERVAL_SECS, default2, set0to disable) starts on firstresources/subscribeand pushes updates.- File providers translate
C:\prefixes into/c/...on Git-Bash/MSYS and honorMSYS2_ARG_CONV_EXCL. Git and HTTPS providers live inproviders/git.shandproviders/https.sh. server.d/register.jsoncan supplyresources/resourceTemplatesfor data-only overrides;server.d/register.shmay emit{ "tools": [...], "resources": [...], "resourceTemplates": [...], "prompts": [...], "completions": [...] }for hook-based overrides (opt-in).
handlers/prompts.shimplementsprompts/listandprompts/get, rejecting both in minimal mode.lib/prompts.shscansprompts/, writes.registry/prompts.json, paginates deterministically (returningprompts, optionalnextCursor, and an extension count viaresult._meta["mcpbash/total"]), and renders templates with argument schemas into structured and text content.- Manual overrides:
server.d/register.jsonorserver.d/register.shcan provide apromptsarray; preferregister.jsonunless you need dynamic behavior.
handlers/roots.shhandlesnotifications/roots/list_changedby re-requesting roots (debounced).lib/roots.shtracks client support, sendsroots/listafterinitialized, normalizes/percent-decodesfile://URIs, drops stale responses via generations, and falls back to env/config on errors/timeouts.- Tools block on
mcp_roots_wait_ready; when ready, env includesMCP_ROOTS_JSON,MCP_ROOTS_PATHS,MCP_ROOTS_COUNT. SDK helpers exposemcp_roots_list,mcp_roots_count, andmcp_roots_contains. - RPC callbacks in
lib/rpc.shroute responses to roots without touching existing file-based pending responses.
- Workers buffer progress and log notifications and flush after handler completion by default.
- Set
MCPBASH_ENABLE_LIVE_PROGRESS=trueto stream notifications mid-flight (starts a background flusher); adjust cadence withMCPBASH_PROGRESS_FLUSH_INTERVAL(seconds).
handlers/completion.shservescompletion/complete, declines in minimal mode, and caps suggestions at 100.lib/completion.shaggregates suggestions, reportshasMore, and reuses pagination helpers for cursor semantics.
handlers/logging.shenforces RFC-5424 levels vialogging/setLevel, rejects invalid inputs with-32602, and defaults toMCPBASH_LOG_LEVEL(orMCPBASH_LOG_LEVEL_DEFAULTtheninfo).lib/logging.shtracks the active level and filters SDK log notifications; worker subshells stream JSON logs per request andlib/core.shemits them after execution to keep stdout protocol-safe.
- No background daemons or hidden servers
- No long-lived mutable state beyond
.registry - No hidden watchers or background threads
- No magic: every dispatch path lives in
bin/,lib/, orhandlers/and is inspectable