๐Ÿš MCP Server in Bash

August 5, 2026 ยท View on GitHub

MCP Release Platforms License Discord

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)
  • jq for JSON processing (brew install jq / apt install jq / apk add jq)
  • (HTTP transport only) socat preferred, or netcat โ€” 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.

PlatformShellUnitHTTP
macOSbash 3.2 (/bin/bash) and 5.x31/3119/19
Debian / Ubuntubash 5.231/3119/19
Alpine (musl + busybox)bash 5.331/3119/19

Docker is needed only to verify other platforms โ€” never to run a server.


๐Ÿš€ Quick Start

  1. Clone the repo
git clone https://github.com/muthuishere/mcp-server-bash-sdk
cd mcp-server-bash-sdk
  1. Make scripts executable
chmod +x mcpserver_core.sh moviemcpserver.sh
  1. 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
  1. Or run it as a local HTTP endpoint
./moviemcpserver.sh --http        # http://127.0.0.1:3000/mcp
  1. 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

  1. Naming Convention: prefix every tool function with tool_, matching the name in your tools JSON
  2. Parameters: each function takes a single parameter $1 containing the arguments as JSON
  3. Success: echo the result, return 0
  4. Failure: echo an explanatory message, return 1 โ€” the caller receives a successful response with isError: true, so the model can read the reason and retry. Tool failures are not transport errors.
  5. Automatic Discovery: tools are dispatched by function name; the tools JSON controls what clients are told exists

Implementation Steps

  1. 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:

ExampleWhat it shows
gitserver.shShelling out safely โ€” argument validation, structured output
weatherserver.shWrapping a third-party API โ€” secrets in env, network failures, trimming the response
fileserver.shSaying no โ€” read-only filesystem access with a real path-traversal boundary
moviemcpserver.shThe minimum, over canned data
  1. 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.

  1. Create assets/weatherserver_config.json โ€” this is now the server/discover body, not an initialize result:
{
  "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."
}
  1. 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
VariableDefaultPurpose
MCP_HTTP_PORT3000listening port
MCP_HTTP_BIND127.0.0.1interface โ€” leave it on loopback
MCP_HTTP_PATH/mcpendpoint path
MCP_ALLOWED_ORIGINShttp://localhost,http://127.0.0.1Origin 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 the netcat fallback 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 socat it serves one connection at a time
  • No SSE response mode, so no notifications/progress streaming and no subscriptions/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-28 are 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 -N missing from macOS's bash 3.2, and Linux capping a single argv entry 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.