Instead of manually setting each variable, use the Heroku CLI to pull the correct values.
export APP_NAME=<your-heroku-app-name>
heroku create $APP_NAME
heroku buildpacks:add --index 1 heroku-community/apt -a $APP_NAME
heroku buildpacks:add --index 2 heroku/python -a $APP_NAME
heroku config:set WEB_CONCURRENCY=1 -a $APP_NAME
# set a private API key that you create, for example:
heroku config:set API_KEY=$(openssl rand -hex 32) -a $APP_NAME
heroku config:set STDIO_MODE_ONLY=<true/false> -a $APP_NAME
Note: we recommend setting STDIO_MODE_ONLY
to true
for security and code execution isolation security.
Also put these config variables into a local .env file for local development:
heroku config -a $APP_NAME --shell | tee .env > /dev/null
Next, connect your app to your git repo:
heroku git:remote -a $APP_NAME
And deploy!
git push heroku main
View logs with:
heroku logs --tail -a $APP_NAME
One-time packages installation:
virtualenv venv
source venv/bin/activate
pip install -r requirements.txt
If you're testing SSE, in one terminal pane you'll need to start the server:
source venv/bin/activate
export API_KEY=$(heroku config:get API_KEY -a $APP_NAME)
uvicorn src.sse_server:app --reload
Running with --reload is optional, but great for local development
Next, in a new pane, you can try running some queries against your server:
First run:
export API_KEY=$(heroku config:get API_KEY -a $APP_NAME)
List tools:
python example_clients/test_sse.py mcp list_tools | jq
Example tool call request:
NOTE: this will intentionally NOT work if you have set STDIO_MODE_ONLY
to true
.
python example_clients/test_stdio.py mcp call_tool --args '{
"name": "code_exec_go",
"arguments": {
"code": "package main\nimport (\n \"github.com/fatih/color\"\n)\nfunc main() {\n color.NoColor = false\n color.Red(\"This should be red!\")\n}",
"packages": ["github.com/fatih/color"]
}
}' | jq -r '.content[0].text' | jq -r .stdout
There are two ways to easily test out your MCP server in STDIO mode:
List tools:
python example_clients/test_stdio.py mcp list_tools | jq
Example tool call request:
python example_clients/test_stdio.py mcp call_tool --args '{
"name": "code_exec_go",
"arguments": {
"code": "package main\nimport (\n \"fmt\"\n \"math/rand\"\n)\nfunc main() {\n for i := 0; i < 50; i++ {\n fmt.Printf(\"%f \", rand.Float64())\n }\n}",
"packages": []
}
}' | jq
Example tool call request:
cat <<EOF | python -m src.stdio_server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"0.1.0","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"code_exec_go","arguments":{"code":"package main\nimport (\n \"fmt\"\n \"math/rand\"\n)\nfunc main() {\n for i := 0; i < 50; i++ {\n fmt.Printf(\"%f \", rand.Float64())\n }\n}","packages":[]}}}
EOF
(Note that the server expects the client to send a shutdown request, so you can stop the connection with CTRL-C)
export API_KEY=$(heroku config:get API_KEY -a $APP_NAME)
export MCP_SERVER_URL=$(heroku info -s -a $APP_NAME | grep web_url | cut -d= -f2)
You can run the same queries as shown in the Local SSE - Example Requests testing section - because you've set MCP_SERVER_URL
, the client will call out to your deployed server.
There are two ways to test out your remote MCP server in STDIO mode:
To run against your deployed code, you can run the example client code on your deployed server inside a one-off dyno:
heroku run --app $APP_NAME -- bash -c 'python -m example_clients.test_stdio mcp list_tools | jq'
or:
heroku run --app "$APP_NAME" -- bash <<'EOF'
python -m example_clients.test_stdio mcp call_tool --args '{
"name": "code_exec_go",
"arguments": {
"code": "package main\nimport (\n \"fmt\"\n \"math/rand\"\n)\nfunc main() {\n for i := 0; i < 50; i++ {\n fmt.Printf(\"%f \", rand.Float64())\n }\n}",
"packages": []
}
}' | jq
EOF
Or, you can also run or simulate a client locally that sends your client-side requests to a one-off dyno:
heroku run --app "$APP_NAME" -- bash -c "python -m src.stdio_server 2> logs.txt" <<EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"0.1.0","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"code_exec_go","arguments":{"code":"package main\nimport (\n \"fmt\"\n \"math/rand\"\n)\nfunc main() {\n for i := 0; i < 50; i++ {\n fmt.Printf(\"%f \", rand.Float64())\n }\n}","packages":[]}}}
EOF
Again, note that since we're running our request through a single command, we're unable to simulate a client's shutdown request.
Soon, you'll also be able to connect up your MCP repo to Heroku's MCP Gateway, which will make streaming requests and responses from one-off MCP dynos simple!
The Heroku MCP Gateway will implement a rendezvous protocol so that you can easily talk to your MCP server one-off dynos (code execution isolation!) with seamless back-and-forth communication.