The repository selects an MCP server, and the operator allows it. A server file in .factorial/ is a request. The server runs only when the allowlist of the project names it.
The server file
One YAML file describes one server. A shared server is .factorial/mcp/<name>.yaml, and any agent can name it. A server for one agent is agents/<agent>/mcp/<name>.yaml. The file stem must equal name. The name is also the prefix of the tools, so a tool search_docs of the server product-docs is mcp__product-docs__search_docs. A name uses lowercase letters, digits and dashes.
The keys use snake_case, and a key that the table does not list is an error.
| Key | Required | What it holds |
|---|---|---|
specVersion | yes | factorial/v1. |
kind | yes | McpServer. |
name | yes | The server name. It equals the file stem. |
description | no | One line for people who read the file. |
transport | yes | http, sse or stdio. Prefer http. |
http | for http and sse | url and optional headers. The URL must start with https://, because the headers usually hold a credential. |
stdio | for stdio | command, optional args and optional env. Give the block that matches the transport, and no other. |
secrets | no | The keys that the server needs, each with a key and an optional description. See Give it a secret. |
tools | no | allow is a list of tool name patterns, for example search_*. Without it, the agent can call every tool of the server. Narrowing the tools is best effort, so do not use it as a hard limit. The allowlist is the control. |
required | no, default true | With true, a server that does not connect fails the run, and the failure gives the reason. With false, the run goes on without the server. |
Keep required as true when the prompt tells the agent to use the tool. A missing tool then shows as a failed run, and not as an agent that seems to work badly. Allow only read-only tools. Factorial checks no postcondition for a write to an outside system.
This is the sample server:
specVersion: factorial/v1
kind: McpServer
name: product-docs
description: Read-only search over the product documentation, to ground specs in what exists
transport: http
http:
url: https://mcp.example.com/product-docs/mcp
headers:
Authorization: "Bearer {secrets.PRODUCT_DOCS_TOKEN}"
secrets:
- key: PRODUCT_DOCS_TOKEN
description: Read-only API token for the documentation server
tools:
allow:
- search_docs
- get_doc
required: true # the agent is told to use these tools
Select it for an agent
An agent names a server in its mcp: block. The block has the same two lists as skills:.
mcp:
shared: # .factorial/mcp/<name>.yaml
- product-docs
local: # agents/<agent>/mcp/<name>.yaml
- repo-index
The block holds names only. The tools stay in the server file, so one policy has one home. A server that needs a different set of tools is a different server file. A name must be unique across both lists. Without the mcp: key, the agent has no MCP server. Factorial gives a run exactly the servers that its agent names and no others.
Permit it
An agent cannot run until the project allows each server that it names. The allowance is the mcp_allowlist setting of the project. It is in the database of Factorial, and no file in the repository can change it. An agent on a host runtime can reach that database, so use an E2B runtime for containment. See Security. A repository can only select. An empty list, or no key, allows no server. A server that the list does not cover blocks the agent from running, and the message names the mismatch.
The name and transport of an entry must match the server file. For http and sse, the url must match too, and the match ignores a trailing slash and the case of the scheme and host. For stdio, the command and args must match exactly. Each tool pattern that the file asks for must be covered by a pattern of the entry. For example, search_* covers search_docs. The entry for the sample server is:
{
"name": "product-docs",
"transport": "http",
"url": "https://mcp.example.com/product-docs/mcp",
"tools": ["search_docs", "get_doc"]
}
An http or sse entry has a url and no command or args. A stdio entry has a command, optional args and no url. An entry that leaves out tools covers every tool. Set the list with the API. PATCH replaces the whole settings object, so send every key that you want to keep:
curl -sS -X PATCH "$SF/api/projects/$PID" \
-H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"settings": {"mcp_allowlist": [
{"name": "product-docs", "transport": "http",
"url": "https://mcp.example.com/product-docs/mcp",
"tools": ["search_docs", "get_doc"]}
]}}'
The dashboard shows the list under MCP servers in the project configuration, and it cannot change the list. mcp_allowlist has the matching rules. Factorial also refuses a server that points at its own MCP address, even when an entry names it. Security explains why.
Pin stdio versions
A stdio server runs a command inside the container of the agent, next to the keys of the run. A floating version would change that code with no change to the config. Factorial therefore refuses a version such as @latest, @next or @main when it parses the file. Give an exact version.
transport: stdio
stdio:
command: npx
args: ["-y", "@acme/repo-index-mcp@1.4.2"]
env:
REPO_INDEX_TOKEN: "{secrets.REPO_INDEX_TOKEN}"
secrets:
- key: REPO_INDEX_TOKEN
The allowlist matches command and args exactly. A change of package or version is therefore a change that only an operator can make. Prefer an http server when you have the choice.
Give it a secret
A header or an environment value never holds a credential. It refers to one as {secrets.KEY}, which is the only substitution that these values allow. Declare the key in the secrets list of the server file. A reference to a key that the list lacks is an error.
- Write the reference, for example
Authorization: "Bearer {secrets.PRODUCT_DOCS_TOKEN}". - Declare
PRODUCT_DOCS_TOKENin thesecretslist of the server file. - Store the value in the Secrets section of the project. After the sync, the section shows the key as missing, with a field for the value. Give the agents their credentials shows the steps and the API call.
When a run starts, Factorial reads the stored value. It puts the value in the config of the server for that run. A key with no stored value blocks activation. When Factorial cannot find the value of a reference, the run does not start. Factorial never sends an unfilled Bearer {secrets.KEY}.