CLI tool
muthuishere/mcp-server-bash-sdk avatar
muthuishere/mcp-server-bash-sdk

mcp-server-bash-sdk: An MCP Server That Is a Shell Script

MCP server SDK/example implemented entirely in bash — build a Model Context Protocol server with no runtime dependency beyond a POSIX shell.

516 stars48 forksShellMIT

At a glance

What is it?
A pure Bash implementation of the Model Context Protocol, targeting the stateless 2026-07-28 revision and shipping both stdio and Streamable HTTP transports. The interesting part is not the language choice but the dispatch convention: tool functions are discovered by name prefix, and the tools JSON only controls what clients are told exists.
Who is it for?
Adopt it if your tools are shell commands, your host already has jq, and you want an MCP server that is one file with no runtime to install. Do not adopt it if you need server-initiated requests or sessions, or if your tool logic is not naturally a shell function.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem is the runtime, not the protocol

Most MCP servers are API wrappers: a Node or Python process that exists to convert a schema into a function call and a result back into JSON. The README says this directly, calling the project a zero-overhead alternative to Node.js, Python, or other heavy runtimes. If the thing you actually want to expose is already a shell command, the wrapper is the entire cost.

The project targets engineers who write operational tooling in Bash and want it reachable by an agent without a package manager, a virtualenv, or a container. The stated floor is Bash 3.2, which the README notes is what macOS ships as /bin/bash, plus jq for JSON processing. That is a deliberately low bar, and it is the whole pitch: the server file is the artifact.

The timing argument in the README is worth taking seriously. The 2026-07-28 revision made MCP stateless: no initialize handshake, no sessions, no server-initiated requests. One line in, one line out. That shape maps onto a shell read loop far better than the older session-oriented protocol did. Bash did not become a good language for this. The protocol moved closer to what Bash is good at.

Two transports, one process_request

The architecture diagram in the README splits the server into three layers. The transport layer is either run_mcp_server for stdio or mcpserver_http.sh when the --http flag is passed. Below that sits mcpserver_core.sh, which handles JSON-RPC framing, MCP dispatch, version negotiation and result envelopes. At the bottom are the tool_ functions holding business logic.

The design decision that matters is stated plainly: both transports call the same process_request, so they cannot drift in protocol behaviour. That is a real constraint on how you extend the server. You cannot special-case the HTTP path without forking the core, and the core is the file you are least likely to want to touch.

Version negotiation is per-request rather than per-connection. Every request carries its protocol version in params._meta, under the key io.modelcontextprotocol/protocolVersion, alongside io.modelcontextprotocol/clientCapabilities. The README's own examples show this on both a server/discover call and a tools/call. There is no stored session state to consult, which is precisely why the stateless revision suits a read loop.

The repository layout separates concerns further than the diagram suggests: assets/ holds the discovery document and tool list, spec/ vendors the official schema that the conformance test validates against, docs/adr/ records why the SDK is shaped this way, and docs/spikes/ holds the experiments behind those decisions. The presence of an ADR directory is a signal worth noting for a project this small.

Tool dispatch is a naming convention, and that cuts both ways

The README's guidelines are explicit and short. Prefix every tool function with tool_, matching the name in your tools JSON. Each function takes a single parameter, $1, containing the arguments as JSON. On success, echo the result and return 0. On failure, echo an explanatory message and return 1.

The failure semantics are the part most likely to surprise. A return 1 does not produce a transport error. The caller receives a successful response with isError: true, so the model can read the reason and retry. The README states the reasoning directly: tool failures are not transport errors. That is a defensible line, and it means your error text is prompt content, not a log line. Write it for a model, not for grep.

The discovery mechanism is where the convention becomes a constraint. Tools are dispatched by function name, but the tools JSON controls what clients are told exists. Those two things can disagree. A function named tool_foo with no matching entry in the tools JSON is invisible to clients. An entry in the tools JSON with no matching function is advertised and then fails at call time. Nothing in the described mechanism reconciles them, so the tools JSON and the function set are two sources of truth you maintain by hand.

External configuration lives in JSON files, and the README lists dynamic tool discovery via function naming convention as a feature rather than a limitation. It is both. You get dispatch without a registry, and you give up any compile-time or startup-time check that the registry and the implementation match.

Getting a server running

The quick start is four steps. Clone the repository, then make the scripts executable:

chmod +x mcpserver_core.sh moviemcpserver.sh

Then pipe a request in. The README's example for discovery is a single JSON-RPC line with the protocol version and client capabilities nested in params._meta:

echo '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' | ./moviemcpserver.sh

The tools/call example follows the same shape, with name and arguments added inside params. For an HTTP endpoint instead of stdio:

./moviemcpserver.sh --http

The README gives the resulting address as http://127.0.0.1:3000/mcp. That transport needs socat, which the README prefers, or netcat as a fallback, and it points to a note under the Transports section for the difference. Both are listed as HTTP-transport-only requirements, so a stdio server needs neither.

Verification is where this project is unusually concrete. ./test_mcpserver_core.sh runs 31 unit tests, and passing a single test name such as test_discover_shape runs just that one. ./test_conformance.sh validates output against the official published JSON Schema, using Python with jsonschema, which the README marks as optional and needed only for that test. ./test_http_transport.sh runs 19 HTTP transport tests. ./scripts/test-linux.sh all reproduces the Linux results on any machine with Docker, and the README is careful to say Docker is needed only to verify other platforms, never to run a server.

The supported-platform table lists macOS on bash 3.2 and 5.x, Debian and Ubuntu on bash 5.2, and Alpine with musl and busybox on bash 5.3, each at 31/31 unit and 19/19 HTTP. Those numbers come from the README's own test suites. They are the project's claims about its own tests, not independent measurements.

Where a shell server stops being the right answer

The stateless revision is the enabling condition, and it is also the boundary. The README states that 2026-07-28 removed the initialize handshake, sessions, and server-initiated requests. A server built on this SDK therefore has no place to put state that outlives a request, and no way to push anything to the client unprompted. If your tool needs to hold a connection open, stream progress, or ask the client a question mid-call, this is the wrong tool and no amount of Bash cleverness changes that.

jq is a hard dependency for JSON processing. It is not bundled and it is not optional. On a minimal image without jq, the server does not degrade, it fails. The README's install lines are brew install jq, apt install jq, and apk add jq, which tells you the intended environments but also tells you the dependency is real.

HTTP transport adds a second external process. socat or netcat has to be present, and the README treats socat as preferred without spelling out in the excerpt shown here what the fallback costs you. If you plan to use --http in production, that note is the first thing to read.

There is a subtler failure mode in the dispatch convention. Because the tools JSON is what clients are told and the function name is what actually runs, a rename on one side and not the other produces a server that looks healthy during discovery and fails on invocation. The conformance test validates output against the schema, which is about shape, not about whether your advertised tools exist. Nothing described in the material performs that cross-check for you.

The alternative is a Python or Node MCP server, and the trade is real

The obvious comparison is any of the mainstream MCP server frameworks in Python or TypeScript. The difference is not speed, and the README does not claim a benchmark. The difference is what has to be installed and what has to be kept running.

A Python MCP server gives you a type system, a real JSON library, structured error handling, and a package ecosystem for whatever the tool actually does. It also gives you an interpreter version to pin, a dependency tree to audit, and a virtualenv or container to ship. A TypeScript server adds a build step. The Bash SDK gives you one file that runs wherever the shell runs, and asks you to express your tool logic as a function that takes a JSON string in $1 and echoes a result.

That trade is favourable exactly when the tool is already a shell command. Wrapping an existing CLI in a Python MCP server means writing a subprocess call and parsing its output. Wrapping it in this SDK means naming a function tool_something and echoing what the command printed. The second is less code and fewer moving parts, and it is the case the project was built for.

The trade turns against you when the tool logic is not shell-shaped. If you need to parse a nested JSON response, transform it, and return a structured object, you are doing that in jq, and jq programs of any size are their own maintenance burden. At that point the Python server is not heavier, it is just the right tool, and the shell version is a puzzle you built for yourself.

Maintenance, licence, and what the release cadence tells you

The project is MIT licensed, which places few restrictions on reuse and modification. That is a permissive licence, and it means you can vendor mcpserver_core.sh into your own repository without much ceremony. It does not mean the vendored copy stays current, and it does not mean the project owes you anything. This is not legal advice; read the LICENSE file for the actual terms.

The most recent release listed is v0.4.1, dated 2026-08-04, tagged with MCP 2026-07-28, both transports, and verified on Linux. The version number is worth reading as a signal: 0.4.x is pre-1.0, and the release notes tie a version bump to a protocol revision. When MCP revises again, this SDK's compatibility depends on someone updating the core and the vendored schema in spec/ together. The conformance test is the mechanism that would catch a mismatch, so its continued relevance depends on spec/ being refreshed alongside the code.

The last push recorded is 2026-09-09, roughly a month after v0.4.1. That is recent activity, not a guarantee of future cadence. The repository is not archived.

Upgrade cost is the thing to weigh before adopting. Because both transports funnel through process_request in mcpserver_core.sh, a protocol change lands in one file, which is the good case. The bad case is that your tool_ functions and your tools JSON are yours, and no upgrade touches them. The upgrade surface is small and the maintenance surface is yours. For a project at 0.4.x targeting a protocol revision that is itself dated, that split is the honest summary: the core is someone else's problem until it is not, and the tools are always yours.

Editorial conclusion

Adopt it if your tools are shell commands, your host already has jq, and you want an MCP server that is one file with no runtime to install. Do not adopt it if you need server-initiated requests or sessions, or if your tool logic is not naturally a shell function. Before wiring it into anything, run ./test_conformance.sh and confirm the vendored schema in spec/ matches the protocol revision your client actually negotiates, because the per-request _meta version field is what every dispatch decision hangs on.

Official sources

  1. License: MIT
  2. muthuishere/mcp-server-bash-sdk on GitHub
  3. Project website
  4. README
  5. Releases
Community notes

Community notes