Skip to main content
Commit your team’s MCP servers in the repository the way Claude Code and Codex read them, and Aether tasks get them too: Claude Code’s .mcp.json and the [mcp_servers] tables of Codex’s .codex/config.toml, on either harness. Aether reads both files through GitHub at the commit the task’s base branch names when the task is created, and keeps that commit for the whole task. Values come from the project’s environment variables. The servers are the task’s own: nothing is added to the organization’s catalog. An entry Aether cannot use never blocks a task: it is skipped, and the task is created and runs without it. Every skip is shown with its file, its server and why: in the composer before you send, on the task page, in the CLI’s aether task create output, and as a line in the Slack and Linear replies that start the task. Only a config.toml server with required = true refuses the task instead.

Remote servers

A server with a url ("type": "http" in .mcp.json) is reached over Streamable HTTP through Aether’s MCP gateway, so its URL and headers never enter the workspace. ${VAR} and ${VAR:-default} in .mcp.json, and bearer_token_env_var and env_http_headers in config.toml, resolve from the project’s environment variables. A server that sets no Authorization header and has the URL of a catalog server the project enables uses that server’s connection, under the project’s credential policy. A server that sets no Authorization and has no catalog server’s URL is asked whether it needs a credential; if it does, it needs an org admin to add a catalog server with its URL. An optional server whose connection the task cannot use is skipped, saying whose connection is missing.

Local (stdio) servers

A server with a command runs inside the task’s workspace as a catalog local server does: Aether’s workspace service starts the process and both harnesses reach it as one more MCP server. The harness never starts it.

Supported commands

The command is a package runner with a package pinned to one release: -y (or --yes) is optional for npx; uvx --from <package>==<version> <package> is the same as uvx <package>@<version>. Arguments after the package go to the server unchanged.
  • The version is pinned. An npm package takes a full semver version (1.2.3), a PyPI package an exact PEP 440 version. A package with no version, latest, a tag, a range or a wildcard is skipped: the task would run whichever release is newest when it starts.
  • Other commands are skipped. node server.js, python -m … without uvx, docker, uv run, a binary or script in the repository, a path to npx, and package-runner options other than the ones above.

Environment

The process gets PATH, HOME, the npm and uv package caches, and the variables its file declares — nothing of the agent’s environment, and none of the project’s other environment variables.
  • .mcp.json: each env value expands ${VAR} and ${VAR:-default} from the project’s environment variables.
  • .codex/config.toml: env values are used as written (Codex expands nothing in them). Each name in env_vars is set from the project’s environment variable of that name, as Codex forwards it from the environment it runs in. A name may be in env or env_vars, not both.
A server referencing a variable the project does not set, with no default in the file, is skipped. A value that references a variable holds the project’s environment variables, so it is a secret: it is masked wherever the server’s output is logged. Literal values are not. command and args never reference a variable — a project’s environment variables reach a local server only through its env. A server with cwd in config.toml is skipped: a local server runs in the task’s repository checkout.
The process runs as the agent’s user in the workspace. It cannot inherit the agent’s environment, but it can read whatever the agent can read.

Required and disabled servers

A .mcp.json server is optional; one with "disabled": true is left out. A config.toml server is optional unless it sets required = true, as Codex reads it; one with enabled = false is left out. An optional server Aether cannot use is skipped, and one that does not start within 90 seconds is left out of the turn, which runs without it. A required server Aether cannot use refuses the task at creation with the code mcp_repo_config_invalid, naming the file and the server, and fails a later turn the same way; one that does not start within 90 seconds fails the turn, naming it.

When both files, or the organization, declare a server

  • The same name in both files is one server when both declare the same URL and headers, or the same command, arguments and environment (env_vars = ["TOKEN"] in config.toml is the same as "TOKEN": "${TOKEN}" in .mcp.json). Two different declarations under one name are not one server: the task is given .mcp.json’s, and config.toml’s is skipped. Names become server keys as the harnesses write them into tool names, lowercased.
  • A name the organization’s catalog already uses is the organization’s server: the task uses it instead, and its server list says why the repository’s copy was not given.

Skipped, and why

Each skip names its file and its server, or only the file when the file does not parse, and one of these reasons. The task runs without the server; fix the file or the project and the next task, or the task’s next turn for a variable, gets it. A variable removed from the project after a task was created, or a connection that went away, leaves its server out of the task’s next turn, and the task page names it as skipped; a connection made since gives the next turn the server again. A required server fails the turn instead, with mcp_repo_config_invalid and the same message.