Machines API
The Machines API is a low level interface that provides access to the full range of platform services. It consists of a set of REST and a GraphQL interfaces. You can access these from any programming language capable of producing HTTP GET and POST requests and the ability to generate and parse JSON.
An example of when you would want to use the Machines API is when you want to create a Per-User Dev Environment
This guide presumes that you are running MacOS, Linux, or WSL2, and have curl installed.
Select an organization
All Fly.io users have a personal organization and may be a member of other organizations. Set an environment variable with your choice.
export ORG="personal"
Organization token
To get started, you need an organization token, which you can obtain via the dashboard.
Go to https://fly.io/dashboard, change the organization if necessary, select Tokens from the list on the left hand side of the page, optionally enter a token name or an expiration period and click on Create Organization token. Once complete you should see something like the following:
Click on Copy to clipboard, and then run the following command with your token pasted inside the quotes:
export FLY_API_TOKEN="FlyV1 fm2_IJPECAAA..."
Select an API hostname
The host name you chose depends on whether your application is running inside or outside your Fly.io private Wireguard network. If you are not sure, use the public base URL:
export FLY_API_HOSTNAME=https://api.machines.dev
Chose a hostname for your app
You are free to chose any available hostname for your application. The following will generate a name that is likely to be unique:
export APP_NAME=mcp-demo-$(uuidgen | cut -d '-' -f 5 | tr A-Z a-z)
Create your app
Now use the Create a Fly App API:
curl -i -X POST \
-H "Authorization: Bearer ${FLY_API_TOKEN}" \
-H "Content-Type: application/json" \
"${FLY_API_HOSTNAME}/v1/apps" \
-d "{ \
\"app_name\": \"${APP_NAME}\", \
\"org_slug\": \"${ORG}\" \
}"
Response should be a 200
Success, along with some JSON output.
Create IP addresses for your application
While the Fly.io GraphQL endpoint is public, our usage of it is open source, can be directly observed by setting LOG_LEVEL=debug
before running flyctl
commands, and there are no current plans to change it, be aware that this interface is not guaranteed to be stable and can change without notice. If this is a concern, consider using the fly ips
command instead.
The following will create a shared IPv4 address and a dedicated IPv6 address:
curl -i -X POST \
-H "Authorization: Bearer ${FLY_API_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.fly.io/graphql" \
-d @- <<EOF
{
"query": "mutation(\$input: AllocateIPAddressInput!) { allocateIpAddress(input: \$input) { app { sharedIpAddress } } }",
"variables": {
"input": {
"appId": "${APP_NAME}",
"type": "shared_v4",
"region": ""
}
}
}
EOF
curl -i -X POST \
-H "Authorization: Bearer ${FLY_API_TOKEN}" \
-H "Content-Type: application/json" \
"https://api.fly.io/graphql" \
-d @- <<EOF
{
"query": "mutation(\$input: AllocateIPAddressInput!) { allocateIpAddress(input: \$input) { ipAddress { id address type region createdAt } } }",
"variables": {
"input": {
"appId": "${APP_NAME}",
"type": "v6",
"region": ""
}
}
}
EOF
Other options include v4
and private_v6
.
Create a volume
This demo uses a volume. If your application doesn’t use a volume skip this step.
curl -i -X POST \
-H "Authorization: Bearer ${FLY_API_TOKEN}" \
-H "Content-Type: application/json" \
"${FLY_API_HOSTNAME}/v1/apps/${APP_NAME}/volumes" \
-d '{
"name": "data",
"region": "iad",
"size_gb": 1
}'
Adjust the region as necessary.
Create a machine
This next part contains a lot of properties, so first an overview:
- If you are using a volume,
region
selected must match a region in which you have an allocated but unattached volume. image
specifies the image we will be running. Fly.io provides an image capable of runningnpx
anduvx
, which is sufficient to run many MCPs. If you have a custom MCP with unique requirements, you can provide your own image.init
specifies the command we will be running.guest
specifies the size of the machine desired.mounts
specifies the volume, where it is to be mounted, and how it can grow.services
defined what network services your application provides.
curl -i -X POST \
-H "Authorization: Bearer ${FLY_API_TOKEN}" \
-H "Content-Type: application/json" \
"${FLY_API_HOSTNAME}/v1/apps/${APP_NAME}/machines" \
-d '{
"region": "iad",
"config": {
"image": "flyio/mcp:latest",
"init": {
"cmd": [
"npx",
"-f",
"@modelcontextprotocol/server-filesystem",
"/data/"
]
},
"guest": {
"cpu_kind": "shared",
"cpus": 1,
"memory_mb": 1024
},
"mounts": [
{
"volume": "data",
"path": "/data",
"extend_threshold_percent": 80,
"add_size_gb": 1,
"size_gb_limit": 100
}
],
"services": [
{
"protocol": "tcp",
"internal_port": 8080,
"autostop": "stop",
"autostart": true,
"ports": [
{
"port": 80,
"handlers": [
"http"
],
"force_https": true
},
{
"port": 443,
"handlers": [
"http",
"tls"
]
}
]
}
]
}
}'
Note: at this time the machine is created but not yet started. It will start once it receives its first request and stop when there is a period when there are no requests.
Accessing the MCP
Normally you would access the MCP using fly mcp proxy
to create a STDIO MCP server for your MCP client, even if you are using the Machines API to deploy an existing MCP server on a Fly Machine.
But for those interested in lower level details, fly mcp wrap
supports the asynchronous nature of MCP servers by requiring all requests be done via HTTP POST and all replies are returned in response to a HTTP GET request.
The following shell script will demonstrate that your MCP server is working:
curl -N https://${APP_NAME}.fly.dev/ &
process_id=$!
sleep 1
curl -i -X POST -H "Content-Type: application/json" https://${APP_NAME}.fly.dev/ -d '
{"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
{"jsonrpc":"2.0","method":"tools/list","id":1}
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_directory","arguments":{"path":"/data"}},"id":2}
'
sleep 1
kill $process_id
Note that even with the sleep
commands present, there is no retry logic in this script. What this means is that you may not see replies the first time you run the script, especially if your machine has not yet been started. Simply run the script again if this happens.
Performance Considerations
While the Machines API is the most performant way to create machines, there still are some considerations you need to be aware of.
- Differences in performance of otherwise similar operations:
- Starting a machine that is suspended is faster than starting a machine that is stopped.
- Starting a machine that is stopped is faster than updating a machine.
- Updating a machine is faster than creating a new machine.
- There are rate limits in place.
Taken together, if you are anticipating running hundreds or even thousands of machines, it makes sense to create them over a period of time and then having them start when needed.