Get started
Project config reference
foro has no config file of its own. It reads the pyproject.toml or package.json your project already has: the name, the interpreter range, the entry file. The handful of things neither can say live in an optional [tool.foro] table (or a "foro" key in package.json), and most projects declare none of them. Validation happens at deploy time - a bad value fails the deploy with the exact field and rule in the deploy log.
# pyproject.toml - everything foro reads, and nothing it requires [project] name = "grocery-list" requires-python = ">=3.12" # Optional. Only for what [project] cannot say. [tool.foro] entrypoint = "src/server.py" # default: server.py, main.py, src/server.py, app.py build_path = "." # default port = 8000 # default
{ "name": "grocery-list", "main": "dist/index.js", "engines": { "node": ">=22" }, "foro": { "port": 8000 } }
name
- read from
[project].name/name- rule
- never rejected - normalised to 3–48 characters of a–z, 0–9 and dashes
The server's display name in the dashboard. It is not the URL - the <slug>.foro.sh subdomain is generated at project creation and never changes, so renaming your package never breaks an agent's config. A name that can't be spelled that way - a scope, an underscore, a capital - is normalised rather than rejected (@acme/My_Server shows as my-server). Refreshed on every deploy.
entrypoint
- read from
[tool.foro].entrypoint, else inferred- rule
- a relative path inside the build directory - no leading /, no ..
The file that starts your MCP server. For Python, foro looks for server.py, main.py, src/server.py and app.py, in that order. For Node it reads main, then bin, then an index.js that exists. Set entrypoint when your entry file is somewhere else. It is resolved against the build directory and run with your project's package manager (e.g. uv run) or with node inside the container.
runtime
- read from
- which config file is present
- rule
pythonornode
The language the server is built and run as. It picks the base image, the install commands (uv, Poetry, pipenv and PDM for Python; npm, pnpm and yarn for Node) and the allowlist runtime_version is judged against. A pyproject.toml means Python and a package.json means Node; declare runtime explicitly only to break the tie when a directory has both, and only in the file that is actually the server.
runtime_version
- read from
requires-python/engines.node- rule
- one of
3.11,3.12,3.13for Python; one of22,24for Node
The interpreter version the container runs. foro resolves the range your project already declares to the newest version inside it that foro supports - >=3.11,<3.13 runs 3.12, ^22 || ^24 runs 24. A range that admits no supported version fails the deploy rather than quietly running an interpreter you said you don't support; a range foro can't read falls back to the default (3.12 / 24). Pin one exactly with [tool.foro].runtime_version.
build_path
- read from
[tool.foro].build_path- rule
- a relative path inside the repo - letters, digits, `.`, `_`, `-` per segment, no `..`
Where the project lives when it isn't beside its config file. Defaults to the directory the config was read from, which is what a monorepo subdirectory already gives you. The build runs there: the lockfile and the entrypoint are both resolved against it.
port
- read from
[tool.foro].port- rule
- an integer 1024–65535, except
8001and8002(both reserved for the platform)
The port your server listens on inside the container, 8000 by default. foro injects PORT, so a server that reads it never needs this field. You never expose the port yourself: the public URL reaches a platform proxy on 8002, which checks your server's token, counts the call and forwards it here. 8001 is that same proxy's health endpoint.
dependency_manager
- read from
[tool.foro].dependency_manager, else the lockfile- rule
uv,uv-pip,poetry,pipenv,pdmfor Python;npm,pnpm,yarnfor Node
How dependencies are installed. Detected from the lockfile beside your config - uv.lock, poetry.lock, pnpm-lock.yaml and so on. Set this only when a repo carries more than one and the wrong one wins. The two vocabularies are disjoint: a Python manager on a Node project is rejected, and the reverse too.
Which fields do I have to declare?
None of foro's own. A pyproject.toml with a [project] name and a server.py beside it deploys as-is, and so does a package.json with a main. The [tool.foro] table (or the "foro" key in package.json) exists only for what neither file can say - a custom entry file, a port, a build directory, a dependency manager. Any key in that block that foro does not read is rejected rather than ignored, so a typo fails the deploy instead of silently changing what runs.
How does foro know whether my project is Python or Node?
By which config file is there: a pyproject.toml means Python, a package.json means Node. If a directory has both - a Python server whose repo also carries a package.json for tooling is the common case - set runtime in the [tool.foro] table or the "foro" key of whichever one is the MCP server, and only that one. foro never guesses between the two.
What happened to foro.yaml?
It is gone. foro reads the config your project already has instead of a second file that repeated it. A repo carrying only a foro.yaml fails validation with a message naming the block to add: move entrypoint, port, build_path and dependency_manager into [tool.foro], and drop name and python_version - the package name and requires-python already say those.
What happens if the config is invalid?
The deploy fails at the validation stage, before any image is built, and the deploy log names the exact field and the rule it broke. Nothing is started and the previously running version keeps serving.
Where does foro look?
In the directory you deploy from on the branch you deploy from - the repo root unless you picked a subdirectory when creating the project. If neither config file is there, foro proposes a pyproject.toml pre-filled for your repo: commit it, push, and rescan.
Anything in the foro block that isn't listed here is rejected, not ignored. Secrets and environment variables are managed in the dashboard, never committed to your config.