Docs navigation

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
python or node

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.13 for Python; one of 22, 24 for 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 8001 and 8002 (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, pdm for Python; npm, pnpm, yarn for 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.