Minimalistic MCP Server in bash script
Let's create a minimalistic MCP server as a single, self-sufficient Bash script. GitHub code for this article: https://github.com/antonum/mcp-server-bash Model Context Protocol (MPC) is a hot topic nowadays. You can find many YouTube videos and articles guiding you through creating your first MPC server with Python, TypeScript, etc. Cool!!! But... All these examples rely on external libraries such as FastMCP for Python or McpServer for TypeScript. Essentially hiding the complexity (or the elegance if you will) of the underlying protocol from you. For me, it's a great learning opportunity to learn the inner workings of MCP, but you can think of different use cases, such as turning your existing bash scripts into tools for LLM without giving them access to the entire system via bash. Basic stdio server in bash First of all - MCP works either via HTTP request or via standard Input/Output. Let's use stdio here. The bare minimum stdio server looks like this: #!/bin/bash while read -r line; do echo $line done || break You can save this file as test.sh and, in the terminal, run bash test.sh. The server would just repeat whatever you type until you press Ctrl^C. JSONRPC Not exactly what we need, but we are getting there... Instead of arbitrary text, MCP use the jsonrpc protocol. Here are the basic request and response in jsonrpc: { "jsonrpc": "2.0", "method": "add", "params": [5, 3], "id": 1 } { "jsonrpc": "2.0", "result": 8, "id": 1 } Very simple. "jsonrpc": "2.0" tells us what specific protocol we are using and "id": 1 is an id of requests that must be matched by the server response. Now, let's adapt our server to match the sample request and response above: #!/bin/bash while read -r line; do method=$(echo "$line" | jq -r '.method' 2>/dev/null) id=$(echo "$line" | jq -r '.id' 2>/dev/null) if [[ "$method" == "add" ]]; then # Parse the parameters from the JSON input num1=$(echo "$line" | jq -r '.params[0]' 2>/dev/null) num2=$(echo "$line" | jq -r '.params[1]' 2>/dev/null) # Perform the addition sum=$((num1 + num2)) # Return the result as JSON echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"sum":'"$sum"'}}' else echo '{"jsonrpc":"2.0","id":'"$id"',"error":{"code":-32601,"message":"Method not found"}}' fi done || break You can test it by running the following in the terminal: echo '{"jsonrpc": "2.0","method": "add","params": [5, 3],"id": 1}' | bash test.sh {"jsonrpc":"2.0","id":1,"result":{"sum":8}} Almost there! We now do have a functioning jsonrps stdio server that even traps [some of] the errors. Model Context Protocol (MCP) There has been nothing specific to MCP so far. Just arbitrary jsonrpc. Let's get MCP working. The lifecycle of MCP server can be described in two phases. Initialization and Operation. The init phase messages look like this: # introductions: Client: {"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0} Server: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"math","version":"0.0.1"}}} Client: {"method":"notifications/initialized","jsonrpc":"2.0"} # gather server capabilities: Client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":1} Server: {"jsonrpc":"2.0","id":1,"result":{"resources":[]}} Client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2} Server: {"jsonrpc":"2.0","id":2,"result":{"tools":[]}} Client: {"method":"prompts/list","params":{},"jsonrpc":"2.0","id":3} Server: {"jsonrpc":"2.0","id":3,"result":{"prompts":[]}} In the example above, the client (AI Host such as Claude Desktop or mcphost) and server (your bash script) "shake hands" and inform each other of their capabilities. Hey, I'm Claude AI! Nice to meet you! I'm math server version 0.1.0 Cool! What are your resources? None How about tools? None Prompts? nah! Indeed, there's nothing in our server yet, but nevertheless if only you implement these calls in your server, you'll get a fully functioning "do nothing" server. Fully - functioning "do nothing" MCP Server Here is our updated bash script that speaks MCP and passes the initialization step: #!/bin/bash while read -r line; do method=$(echo "$line" | jq -r '.method' 2>/dev/null) id=$(echo "$line" | jq -r '.id' 2>/dev/null) if [[ "$method" == "initialize" ]]; then echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"math","version":"0.0.1"}

Let's create a minimalistic MCP server as a single, self-sufficient Bash script.
GitHub code for this article: https://github.com/antonum/mcp-server-bash
Model Context Protocol (MPC) is a hot topic nowadays. You can find many YouTube videos and articles guiding you through creating your first MPC server with Python, TypeScript, etc.
Cool!!! But... All these examples rely on external libraries such as FastMCP
for Python or McpServer
for TypeScript. Essentially hiding the complexity (or the elegance if you will) of the underlying protocol from you.
For me, it's a great learning opportunity to learn the inner workings of MCP, but you can think of different use cases, such as turning your existing bash scripts into tools for LLM without giving them access to the entire system via bash.
Basic stdio server in bash
First of all - MCP works either via HTTP request or via standard Input/Output. Let's use stdio
here. The bare minimum stdio server
looks like this:
#!/bin/bash
while read -r line; do
echo $line
done || break
You can save this file as test.sh
and, in the terminal, run bash test.sh
. The server would just repeat whatever you type until you press Ctrl^C.
JSONRPC
Not exactly what we need, but we are getting there... Instead of arbitrary text, MCP use the jsonrpc
protocol. Here are the basic request and response in jsonrpc
:
{
"jsonrpc": "2.0",
"method": "add",
"params": [5, 3],
"id": 1
}
{
"jsonrpc": "2.0",
"result": 8,
"id": 1
}
Very simple. "jsonrpc": "2.0"
tells us what specific protocol we are using and "id": 1
is an id of requests that must be matched by the server response.
Now, let's adapt our server to match the sample request and response above:
#!/bin/bash
while read -r line; do
method=$(echo "$line" | jq -r '.method' 2>/dev/null)
id=$(echo "$line" | jq -r '.id' 2>/dev/null)
if [[ "$method" == "add" ]]; then
# Parse the parameters from the JSON input
num1=$(echo "$line" | jq -r '.params[0]' 2>/dev/null)
num2=$(echo "$line" | jq -r '.params[1]' 2>/dev/null)
# Perform the addition
sum=$((num1 + num2))
# Return the result as JSON
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"sum":'"$sum"'}}'
else
echo '{"jsonrpc":"2.0","id":'"$id"',"error":{"code":-32601,"message":"Method not found"}}'
fi
done || break
You can test it by running the following in the terminal:
echo '{"jsonrpc": "2.0","method": "add","params": [5, 3],"id": 1}' | bash test.sh
{"jsonrpc":"2.0","id":1,"result":{"sum":8}}
Almost there! We now do have a functioning jsonrps stdio
server that even traps [some of] the errors.
Model Context Protocol (MCP)
There has been nothing specific to MCP so far. Just arbitrary jsonrpc. Let's get MCP working. The lifecycle of MCP server can be described in two phases. Initialization and Operation.
The init phase messages look like this:
# introductions:
Client: {"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
Server: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"math","version":"0.0.1"}}}
Client: {"method":"notifications/initialized","jsonrpc":"2.0"}
# gather server capabilities:
Client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":1}
Server: {"jsonrpc":"2.0","id":1,"result":{"resources":[]}}
Client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2}
Server: {"jsonrpc":"2.0","id":2,"result":{"tools":[]}}
Client: {"method":"prompts/list","params":{},"jsonrpc":"2.0","id":3}
Server: {"jsonrpc":"2.0","id":3,"result":{"prompts":[]}}
In the example above, the client (AI Host such as Claude Desktop or mcphost) and server (your bash script) "shake hands" and inform each other of their capabilities.
- Hey, I'm Claude AI!
- Nice to meet you! I'm
math
server version 0.1.0 - Cool! What are your resources?
- None
- How about tools?
- None
- Prompts?
- nah!
Indeed, there's nothing in our server yet, but nevertheless if only you implement these calls in your server, you'll get a fully functioning "do nothing" server.
Fully - functioning "do nothing" MCP Server
Here is our updated bash script that speaks MCP and passes the initialization step:
#!/bin/bash
while read -r line; do
method=$(echo "$line" | jq -r '.method' 2>/dev/null)
id=$(echo "$line" | jq -r '.id' 2>/dev/null)
if [[ "$method" == "initialize" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"math","version":"0.0.1"}}}'
elif [[ "$method" == "notifications/initialized" ]]; then
: #do nothing
elif [[ "$method" == "tools/list" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"tools":[]}}'
elif [[ "$method" == "resources/list" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"resources":[]}}'
elif [[ "$method" == "prompts/list" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"prompts":[]}}'
else
echo '{"jsonrpc":"2.0","id":'"$id"',"error":{"code":-32601,"message":"Method not found"}}'
fi
done || break
Create update the test.sh
with the code above and make sure to add execute permissions to the file:
chmode +x test.sh
In order to test it you'll need to either update the existing or create a new configuration file for your LLM host.
{
"mcpServers": {
"math": {
"command": "/Users/anton/code/mcp-server-bash/test.sh",
"args": []
}
}
}
For Claude Desktop that file would be claude_desktop_config.json
. You can access it via Claude Menu -> Settings -> Developer - Edit Config. For mcphost you can specify this file right in the command line - add the JSON code above to the file mcp_bare.json
and add it to the arguments of mcphost alongside with any model that is capable of using tools. I'm using ollama with llama3.1 here.
mcphost -m ollama:llama3.1:latest --config /Users/anton/code/mcp-server-bash/mcp_bare.json
As you can see from the output, we successfully connected to the server and loaded exactly 0 tools. Which is expected.
Add a tool to add [two numbers]
And the final step - let's add a tool. This tool would accept two numbers as an input and output the sum of these two numbers. Exactly like we already did in the jsonrpc example above, but now as a proper MCP tool that you can call from the LLM.
To add a tool, you need to adjust the response to tools/list
method to let the Client know what this tool does and the method signature as well as implement corresponding logic for the tools/call
method.
Here is the updated response for tools/list
:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "addition",
"description": "addition of two numbers.\n\nArgs:\n num1, num2\n",
"inputSchema": {
"properties": {
"num1": {"title": "Number1","type":"string"},
"num2": {"title": "Number2","type": "string"}
},
"required": ["num1","num2"]
}
}
]
}
}
and request/response for the addition method:
Client: {"jsonrpc":"2.0","id":20, "method":"tools/call","params":{"name":"addition","arguments":{"num1":"1","num2":"2"}}}
Server: {"jsonrpc":"2.0","id":20,"result":{"content":[{"type":"text","text":"\n sum of two numbers is 3"}],"isError":false}}
Putting it all together
Here is the final result that incorporates all we learned. STDIO server, rpcjson payload going back and forth between client and server as valid MCP messages and simple kindergarden level math.
#!/bin/bash
while read -r line; do
# Parse JSON input using jq
method=$(echo "$line" | jq -r '.method' 2>/dev/null)
id=$(echo "$line" | jq -r '.id' 2>/dev/null)
if [[ "$method" == "initialize" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"math","version":"0.0.1"}}}'
elif [[ "$method" == "notifications/initialized" ]]; then
: #do nothing
elif [[ "$method" == "tools/list" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"tools":[{"name":"addition","description":"addition of two numbers.\n\nArgs:\n num1, num2\n","inputSchema":{"properties":{"num1":{"title":"Number1","type":"string"},"num2":{"title":"Number2","type":"string"}},"required":["num1", "num2"]}}]}}'
elif [[ "$method" == "resources/list" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"resources":[]}}'
elif [[ "$method" == "prompts/list" ]]; then
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"prompts":[]}}'
elif [[ "$method" == "tools/call" ]]; then
tool_method=$(echo "$line" | jq -r '.params.name' 2>/dev/null)
num1=$(echo "$line" | jq -r '.params.arguments.num1' 2>/dev/null)
num2=$(echo "$line" | jq -r '.params.arguments.num2' 2>/dev/null)
sum=$((num1 + num2))
echo '{"jsonrpc":"2.0","id":'"$id"',"result":{"content":[{"type":"text","text":"\n sum of two numbers is '"$sum"'"}],"isError":false}}'
else
echo '{"jsonrpc":"2.0","id":'"$id"',"error":{"code":-32601,"message":"Method not found"}}'
fi
done || break
Final result:
Here you have it. The fully functioning Model Context Protocol server in bash. No dependencies, no external libraries. Just under 30 lines of code.
References
- Model Context Protocol https://www.anthropic.com/news/model-context-protocol
- mcphost https://github.com/mark3labs/mcphost
- jsonrpc specification: https://www.jsonrpc.org/specification