foro (Python)
The Python SDK and CLI for foro.sh — the fastest path from
"I want to build an MCP server" to a deployed https://<slug>.foro.sh URL.
Quickstart
uvx foro init my-server && cd my-server
uvx foro dev
foro init scaffolds a working FastMCP server;
foro dev runs it exactly as foro.sh will and confirms it would pass the
platform's health check. Once it looks good:
git init && git add -A && git commit -m "init" && gh repo create --push
Then, on the foro.sh dashboard: sign in with GitHub, pick the repo, add any secrets your tools need, and deploy.
CLI
Install once with uv tool install foro, or run ad-hoc with
uvx foro ... — no need to add it to your project's own dependencies.
| Command | What it does |
|---|---|
foro init [name] |
Scaffold a new project, or record an existing one's foro settings in its pyproject.toml (run with no argument). --yes takes every default without prompting, for CI and coding agents |
foro check [path] |
Validate a repo against foro.sh's deploy contract before you push |
foro dev [path] |
Run the server locally exactly as foro.sh will, and confirm it would pass the health check. --once verifies and exits instead of staying up, for CI and coding agents |
foro verify <url> |
Prove a deployed server actually serves MCP, by opening a session and listing its tools |
foro auth <login|status|logout|token> |
Sign in to foro.sh, so the CLI can act on your behalf |
foro deploy [path] |
Deploy this directory and stream the build until it's live |
foro logs [path] |
Tail the running server (-f), or read a deployment's --deploy / --build log |
foro projects [show] |
List your projects, or show the one this directory is linked to |
foro link <slug> / foro unlink |
Adopt a project created in the dashboard, or forget the link |
foro open |
Open the deployed URL in a browser |
foro check mirrors the platform's own validation rule for rule, so a repo
it passes will deploy and one it flags will not — the same reason code,
surfaced locally instead of as a 60-second health-check timeout.
foro verify applies foro dev's standard to a deployed server: a URL that
answers HTTP is not the same as one serving MCP, and a green deploy only means
the container passed a TCP probe. It runs the same handshake foro dev does —
initialize, then tools/list — and exits non-zero when that fails, so a
script or a CI step can branch on it.
$ foro verify https://swift-harbor-a3f2.foro.sh
✓ https://swift-harbor-a3f2.foro.sh/mcp is serving MCP
Tools: get_forecast, list_cities
The /mcp path is appended when you leave it off, so the URL foro deploy
printed works as-is.
Signing in
init, check and dev are entirely local and need no account. foro auth
is for everything that talks to foro.sh:
$ foro auth login
! First copy your one-time code: 7A2F-K9QP
Press Enter to open foro.sh in your browser...
- Waiting for authorization... 6s
✓ Logged in as danielsteman (workspace: acme)
A browser approves the code, the CLI polls until you do — the same device flow
gh auth login uses, so it works over SSH where a loopback redirect wouldn't.
- The token is workspace-scoped, picked when you approve it. Access to a second workspace means logging in again for a second token; there is no workspace-switch command.
foro auth statusvalidates against the API rather than reporting that a file exists, so a revoked token shows as broken. It exits 1 when you aren't authenticated, which is what a script should branch on.foro auth logoutrevokes the token server-side and deletes it here. If revocation fails it still deletes locally and tells you to finish the job on/account.foro auth tokenprints the raw token and nothing else, forcurl -H "Authorization: Bearer $(foro auth token)".
Credentials live in ~/.config/foro/hosts.yml ($XDG_CONFIG_HOME is honoured;
%APPDATA%\foro on Windows) at mode 0600, keyed by host. Two environment
variables override it:
| Variable | Effect |
|---|---|
FORO_TOKEN |
Use this token instead of the stored one, and never write it to disk — the GH_TOKEN convention. foro auth status says when it's in play |
FORO_HOST |
Point the CLI at another instance (localhost:3001 for a native dev stack). Defaults to foro.sh |
For CI, skip the browser entirely:
$ echo "$FORO_TOKEN" | foro auth login --with-token
Signing in
init, check and dev are entirely local and need no account. foro auth
is for everything that talks to foro.sh:
$ foro auth login
! First copy your one-time code: 7A2F-K9QP
Press Enter to open foro.sh in your browser...
- Waiting for authorization... 6s
✓ Logged in as danielsteman (workspace: acme)
A browser approves the code, the CLI polls until you do — the same device flow
gh auth login uses, so it works over SSH where a loopback redirect wouldn't.
- The token is workspace-scoped, picked when you approve it. Access to a second workspace means logging in again for a second token; there is no workspace-switch command.
foro auth statusvalidates against the API rather than reporting that a file exists, so a revoked token shows as broken. It exits 1 when you aren't authenticated, which is what a script should branch on.foro auth logoutrevokes the token server-side and deletes it here. If revocation fails it still deletes locally and tells you to finish the job on/account.foro auth tokenprints the raw token and nothing else, forcurl -H "Authorization: Bearer $(foro auth token)".
Credentials live in ~/.config/foro/hosts.yml ($XDG_CONFIG_HOME is honoured;
%APPDATA%\foro on Windows) at mode 0600, keyed by host. Two environment
variables override it:
| Variable | Effect |
|---|---|
FORO_TOKEN |
Use this token instead of the stored one, and never write it to disk — the GH_TOKEN convention. foro auth status says when it's in play |
FORO_HOST |
Point the CLI at another instance (localhost:3001 for a native dev stack). Defaults to foro.sh |
For CI, skip the browser entirely:
$ echo "$FORO_TOKEN" | foro auth login --with-token
Deploying
$ foro deploy
- packaged 14 files (0.1 MiB)
- created swift-harbor-a3f2
- deploying swift-harbor-a3f2 (a1b2c3d4)
cloning build context
│ #8 exporting layers
health check passed
✓ live at https://swift-harbor-a3f2.foro.sh
foro check runs first, so nothing that can't build gets uploaded. The
indented lines are raw docker build output, streamed alongside the deploy
narrative because that's usually where a failure's real cause is. Ctrl+C
detaches without cancelling the deploy.
What gets deployed depends on the project's source, and the CLI won't guess wrong quietly:
| Situation | What happens |
|---|---|
| Unlinked directory | Creates an upload project from the working tree, and links this directory to it |
Linked, source: upload |
Uploads the working tree again, then deploys |
Linked, source: github |
Builds from the repo branch — and warns about uncommitted or unpushed work, which will not be in that build |
--upload / --repo force either path; --detach skips the streaming;
--project <slug> acts on a project this directory isn't linked to.
The archive is what git would track — .gitignore is honoured, and .git/,
.venv/, __pycache__/, node_modules/, dist/ and .env* are always
excluded, so a first deploy can't ship a secret or a 400 MB virtualenv.
foro.yaml must be at the root of the directory you deploy, which is where
foro init puts it.
The link lives in .foro/project.json (gitignored), not in foro.yaml —
the manifest is the shared, committed build contract, while the slug is
platform-generated and workspace-scoped, so baking it into a committed file
would make a fork deploy into someone else's project.
$ foro logs -f # tail the running server
$ foro logs --build # raw docker output from the last deploy
$ foro logs --deploy --json # the deploy narrative, one JSON object per line
$ foro projects # everything in this token's workspace
$ foro open # the deployed URL, in a browser
Runtime log retention is plan-gated (24h on Free, up to 90d on Enterprise), so an empty history on a quiet server is normal rather than broken.
Runtime
One import, used from your server's entrypoint:
from fastmcp import FastMCP
import foro
mcp = FastMCP("my-server")
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
foro.run(mcp) # streamable HTTP, host 0.0.0.0, port $PORT - identical locally and deployed
| Function | What it does |
|---|---|
foro.run(server, *, port=None) |
The one correct way to start a server for foro.sh. Accepts any FastMCP-shaped server (standalone fastmcp.FastMCP, mcp.server.fastmcp.FastMCP, or a low-level Server) — it's duck-typed, not tied to a specific class. |
foro.secret(name) |
Read a required secret from the environment, raising a dashboard-actionable error if it's missing. Set secrets in your project's Secrets tab on the dashboard; they arrive as env vars at deploy time. |
Bare foro (what a deployed container installs — no [cli] extra) stays
dependency-free, so a deployed container never pulls in CLI tooling it
doesn't use.
Project config
There is no foro-specific manifest. A repo's pyproject.toml (Python) or
package.json (Node) already names the project and points at the file that
starts it, and that is what foro.sh reads:
| What foro needs | Python | Node |
|---|---|---|
| display name | [project].name |
name |
| runtime | a pyproject.toml is here |
a package.json is here |
| interpreter version | requires-python, resolved to the newest supported version it allows |
engines.node, same |
| entry file | server.py, main.py, src/server.py or app.py |
main, then bin, then index.js |
| dependency manager | the lockfile (uv, PDM, Poetry, pipenv, requirements.txt) |
the lockfile (npm, pnpm, yarn) |
The rest is optional, and only for what those files can't say:
# pyproject.toml - every key optional, most projects have no such table
[tool.foro]
entrypoint = "cmd/serve.py" # only when it isn't one of the names above
runtime_version = "3.13" # only to pin against what requires-python allows
port = 9000 # only when your server can't listen on $PORT
dependency_manager = "poetry" # only when a repo is genuinely ambiguous
egress = ["example.com:443"] # only to lock down outbound traffic
egress is absent by default, which keeps today's permissive outbound
traffic. Declaring it - even as [] - switches the container to
deny-by-default outbound, restricted to exactly the listed
<destination>:<port> entries (an IPv4 address, an IPv4 CIDR, or a
hostname). foro dev does not enforce it locally; it's a deploy-time
restriction only.
// package.json - the same keys, under "foro"
{ "foro": { "port": 9000 } }
At runtime the container gets PORT and every project secret as its own env
var. Public traffic arrives at
https://<slug>.foro.sh, so your server must listen on 0.0.0.0:$PORT —
which is exactly what foro.run() does for you.
Example
foro-sh/todo-mcp is a small stateful
todo-list server built the way described above — every new foro.sh workspace
gets a deployed copy of it to poke at before deploying anything of your own.
Develop this package
uv sync
uv run pytest
Release files for foro 0.15.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| foro-0.15.0.tar.gz | 421.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| foro-0.15.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 483.1 kB
Release files / foro-0.15.0.tar.gz
| Download URL | foro-0.15.0.tar.gz |
|---|---|
| Size | 421.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c323c68f7d73ed45948d5a23780ab3e3c3cf1e1e15cd84f7ce1ee53eb587c626
|
|
BLAKE2b-256 checksum How to use checksums |
f885f34c98d6fafe8f374ba918ab39be95058f5312d7842fd6cb36e6917b230f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / foro-0.15.0-py3-none-any.whl
| Download URL | foro-0.15.0-py3-none-any.whl |
|---|---|
| Size | 61.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bc7fe61bff5ce685b1c9d46045741d5fc2ef660797035233540aeb0366faeea8
|
|
BLAKE2b-256 checksum How to use checksums |
51eb073e851ee51f36e73b824da6bf45498587d44b33d2a49a876cfcb27d25fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log