mcp-server-bash-sdk
MCP server SDK/example implemented entirely in bash — build a Model Context Protocol server with no runtime dependency beyond a POSIX shell.
Links
README
From the repo.
🐚 MCP Server in Bash
A lightweight, zero-overhead implementation of the Model Context Protocol (MCP) server in pure Bash — targeting the current 2026-07-28 revision.
📖 Documentation site · 🧩 Examples · 🧠 Architecture decisions · 🔬 Spikes
Why? Most MCP servers are just API wrappers with schema conversion. This implementation provides a zero-overhead alternative to Node.js, Python, or other heavy runtimes.
Why now? The 2026-07-28 revision made MCP stateless: no initialize handshake, no sessions, no server-initiated requests. One line in, one line out — which is exactly the shape of a shell read loop. Bash went from an awkward fit to a natural one.
📋 Features
- ✅ MCP 2026-07-28 — stateless, per-request version negotiation
- ✅ Both standard transports: stdio and Streamable HTTP, from the same server file
- ✅
server/discover,tools/list,tools/call - ✅ Dynamic tool discovery via function naming convention
- ✅ External configuration via JSON files
- ✅ Output validated against the official published JSON Schema
🔧 Requirements
- Bash 3.2 or newer (3.2 is what macOS ships as
/bin/bash) jqfor JSON processing (brew install jq/apt install jq/apk add jq)- (HTTP transport only)
socatpreferred, ornetcat— see the note under Transports - (optional) Python with
jsonschema, only to run the schema conformance test
Supported platforms
Verified by running the real test suites on each, not by inspection —
./scripts/test-linux.sh all reproduces the Linux rows on any machine with Docker.
| Platform | Shell | Unit | HTTP |
|---|---|---|---|
| macOS | bash 3.2 (/bin/bash) and 5.x | 31/31 | 19/19 |
| Debian / Ubuntu | bash 5.2 | 31/31 | 19/19 |
| Alpine (musl + busybox) | bash 5.3 | 31/31 | 19/19 |
Docker is needed only to verify other platforms — never to run a server.
🚀 Quick Start
- Clone the repo
git clone https://github.com/muthuishere/mcp-server-bash-sdk
cd mcp-server-bash-sdk
- Make scripts executable
chmod +x mcpserver_core.sh moviemcpserver.sh
- Try it out — every request carries its protocol version 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
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_movies","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' | ./moviemcpserver.sh
- Or run it as a local HTTP endpoint
./moviemcpserver.sh --http # http://127.0.0.1:3000/mcp
- Run the tests
./test_mcpserver_core.sh # 31 unit tests
./test_mcpserver_core.sh test_discover_shape # a single test
./test_conformance.sh # validate against the official schema
./test_http_transport.sh # 19 HTTP transport tests
./scripts/test-linux.sh all # run everything on Debian, Alpine and Ubuntu
🏗️ Architecture
┌─────────────┐ stdio ┌──────────────────────────────────────┐
│ MCP Host │◄─────────►│ Your server (moviemcpserver.sh) │
│ (AI System) │ │ │
│ │ HTTP │ ┌────────────────────────────────┐ │
│ │◄─────────►│ │ Transport │ │
└─────────────┘ │ │ run_mcp_server (stdio) │ │
│ │ mcpserver_http.sh (--http) │ │
│ └───────────────┬────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Protocol mcpserver_core.sh │ │
│ │ process_request() │ │
│ └───────────────┬────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Business logic tool_* funcs │ │
│ └───────────────┬────────────────┘ │
└──────────────────┼──────────────────-┘
▼
┌──────────────────────────────┐
│ Config JSON · external APIs │
└──────────────────────────────┘
Both transports call the same process_request, so they cannot drift in protocol behaviour.
- mcpserver_core.sh: JSON-RPC framing, MCP dispatch, version negotiation, result envelopes
- mcpserver_http.sh: the Streamable HTTP binding — headers, status codes,
Origin, listener - moviemcpserver.sh / examples/gitserver.sh: business logic — your
tool_*functions - assets/: discovery document and tool list
- spec/: the vendored official schema the conformance test validates against
- examples/: four runnable servers and a build-your-own walkthrough
- docs/adr/: why the SDK is shaped this way · docs/spikes/: the experiments behind those decisions
🔌 Creating Your Own MCP Server
Tool Function Guidelines
- Naming Convention: prefix every tool function with
tool_, matching the name in your tools JSON - Parameters: each function takes a single parameter
$1containing the arguments as JSON - Success: echo the result,
return 0 - Failure: echo an explanatory message,
return 1— the caller receives a successful response withisError: true, so the model can read the reason and retry. Tool failures are not transport errors. - Automatic Discovery: tools are dispatched by function name; the tools JSON controls what clients are told exists
Implementation Steps
- Create your business logic file (e.g.,
weatherserver.sh)
#!/bin/bash
# Weather API implementation
# Override configuration paths BEFORE sourcing the core
MCP_CONFIG_FILE="$(dirname "${BASH_SOURCE[0]}")/assets/weatherserver_config.json"
MCP_TOOLS_LIST_FILE="$(dirname "${BASH_SOURCE[0]}")/assets/weatherserver_tools.json"
MCP_LOG_FILE="$(dirname "${BASH_SOURCE[0]}")/logs/weatherserver.log"
source "$(dirname "${BASH_SOURCE[0]}")/mcpserver_core.sh"
API_KEY="${MCP_API_KEY:-default_key}"
# Tool: Get current weather for a location
tool_get_weather() {
local args="$1"
local location=$(echo "$args" | jq -r '.location')
if [[ -z "$location" || "$location" == "null" ]]; then
echo "Missing required parameter: location" # reaches the model as isError
return 1
fi
curl -s "https://api.example.com/weather?location=$location&apikey=$API_KEY"
return 0
}
# stdio by default; --http serves the same tools over Streamable HTTP.
case "${1:-}" in
--http) shift; run_mcp_http_server "$@" ;;
handle-connection) run_mcp_http_server handle-connection ;;
*) run_mcp_server "$@" ;;
esac
For the --http mode also source "$(dirname "${BASH_SOURCE[0]}")/mcpserver_http.sh" next to the core.
Four runnable examples are in examples/, each covering a different problem:
| Example | What it shows |
|---|---|
gitserver.sh | Shelling out safely — argument validation, structured output |
weatherserver.sh | Wrapping a third-party API — secrets in env, network failures, trimming the response |
fileserver.sh | Saying no — read-only filesystem access with a real path-traversal boundary |
moviemcpserver.sh | The minimum, over canned data |
- Create
assets/weatherserver_tools.json
{
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or coordinates"
}
},
"required": ["location"]
}
}
]
}
Keep the array order stable — the spec asks servers to return tools deterministically so clients (and LLM prompt caches) can cache them.
- Create
assets/weatherserver_config.json— this is now theserver/discoverbody, not aninitializeresult:
{
"supportedVersions": ["2026-07-28"],
"serverInfo": {
"name": "WeatherServer",
"version": "1.0.0"
},
"capabilities": {
"tools": {
"listChanged": false
}
},
"ttlMs": 3600000,
"cacheScope": "public",
"instructions": "This server provides weather information."
}
- Make your file executable
chmod +x weatherserver.sh
🔀 Transports
Both are the spec's standard bindings, and both run from the same server file.
stdio
What an editor or agent launches as a subprocess. This is the default and the one to use unless you specifically need HTTP.
./moviemcpserver.sh
Streamable HTTP
2026-07-28 removed sessions, the GET stream and SSE resumability, so this binding is now
just POST a message, get JSON back — which is why it fits in a shell script at all.
./moviemcpserver.sh --http # http://127.0.0.1:3000/mcp
MCP_HTTP_PORT=8080 ./moviemcpserver.sh --http
| Variable | Default | Purpose |
|---|---|---|
MCP_HTTP_PORT | 3000 | listening port |
MCP_HTTP_BIND | 127.0.0.1 | interface — leave it on loopback |
MCP_HTTP_PATH | /mcp | endpoint path |
MCP_ALLOWED_ORIGINS | http://localhost,http://127.0.0.1 | Origin allowlist; anything else gets 403 |
Every POST must carry MCP-Protocol-Version and Mcp-Method, plus Mcp-Name for
tools/call / resources/read / prompts/get, and each must match the request body —
a mismatch is 400 with -32020. That is a security control, not ceremony: an
intermediary may route on the header while the server executes the body.
⚠️ This is a local endpoint, not a web server. It binds loopback, has no TLS and no auth. Install
socat(brew install socat) and it forks per connection; without it thenetcatfallback serves one connection at a time, with a brief window between connections where the port is refused. To expose it beyond localhost, put a real reverse proxy in front. See ADR-0006 and spike 02.
🖥️ Using with an MCP client
"mcp": {
"servers": {
"my-weather-server": {
"type": "stdio",
"command": "/path/to/your/weatherserver.sh",
"args": [],
"env": {
"MCP_API_KEY": "your-api-key"
}
}
}
}
⚠️ The client must speak MCP 2026-07-28. This SDK implements that revision only and rejects older ones with -32022 (ADR-0002 explains the trade-off). Editors still on 2025-06-18 / 2025-11-25 will need a shim until they upgrade.
🚫 Limitations
- HTTP is a local endpoint: no TLS, no auth, and without
socatit serves one connection at a time - No SSE response mode, so no
notifications/progressstreaming and nosubscriptions/listen - No concurrency on stdio: one client, one process, ~30 requests/second
- No resources, prompts, MRTR input requests, or the tasks extension
- Clients older than
2026-07-28are rejected
For AI assistants and local tool execution, these aren't blocking issues.
🧭 Design notes
This SDK is documented as much by why as by how:
- Architecture decisions — seven ADRs, each recording what was rejected and why. ADR-0001 chose stdio-only and is now superseded in scope by ADR-0006; the reversal is recorded rather than edited away, because the protocol changed underneath it.
- Spikes — runnable experiments behind every number here. They are how
the two platform bugs were found:
read -Nmissing from macOS's bash 3.2, and Linux capping a singleargventry at 128 KB where macOS does not.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Blog : https://medium.com/@muthuishere/why-i-built-an-mcp-server-sdk-in-shell-yes-bash-6f2192072279
Community
Questions, ideas, or built something with this? Join AgentNexus — a Discord for people building with AI agents and open tools. This project lives in #mcp-bash-sdk.
Built and maintained by deemwar — we build messaging and browser automation, and the plumbing that keeps it delivering when it silently stops.
Need help with this, or something like it in production? io@deemwar.com
Collected info
- ★ 515 stars
- ⎇ 48 forks
- Language: Shell
- Source updated: 9/21/2026
Config for your environment
Replace {MCP_ENDPOINT_URL} with this MCP’s endpoint URL (from its repo or docs above). No API key — you connect directly.
Tool
OS
Config file: ~/.cursor/mcp.json
{
"mcpServers": {
"mcp-server": {
"url": "{MCP_ENDPOINT_URL}"
}
}
}Paste into mcpServers in the config file. Restart Cursor after saving.
If this MCP is also published on mcpchannel.ai, you can subscribe from Browse and use the gateway config there instead.