MCP Server Reference
Every key accepted in an mcpServers entry. See MCP Integration for concepts and workflow.
File shape
{
"mcpServers": {
"<server-name>": { }
}
}The server name becomes part of every tool's namespaced name (mcp__<server-name>__<tool>), so keep it short and stable. Renaming a server renames every tool it provides, which the model sees as a different tool set.
Transport selection
| Condition | Transport |
|---|---|
command is set | stdio |
type is "http" or "streamable-http" | Streamable HTTP |
url is set and command is not | Streamable HTTP |
All keys
| Key | Type | Default | Applies to | Purpose |
|---|---|---|---|---|
type | string | inferred | both | "http" or "streamable-http" to force HTTP. Usually unnecessary |
command | string | — | stdio | The executable to run |
args | string[] | [] | stdio | Arguments passed to command |
env | object | {} | stdio | Environment variables for the subprocess |
url | string | — | HTTP | The server endpoint |
headers | object | {} | HTTP | Static request headers. Values support ${VAR} expansion |
oauth | object | — | HTTP | Enables the OAuth 2.1 flow. See below |
tools | string[] | all | both | Allowlist of tool names to register |
readOnly | boolean | false | both | Treat every tool from this server as safe-tier |
timeoutMs | number | 30000 | both | Per-request timeout. 0 uses the default |
required | boolean | false | both | Fail startup if this server does not connect |
stdio servers
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
"env": { "LOG_LEVEL": "warn" }
}
}
}The subprocess is started at agent startup and shut down when the agent exits. It inherits nothing from your environment except what you list in env.
HTTP servers
{
"mcpServers": {
"historian": {
"type": "http",
"url": "https://historian.internal/mcp",
"headers": {
"Authorization": "Bearer ${HISTORIAN_TOKEN}",
"X-Site": "plant-3"
},
"timeoutMs": 120000
}
}
}Environment expansion
${VAR} in a header value expands from the environment. An unset variable expands to empty with a warning. A missing token should produce a 401 you can read, not a startup crash that hides which header was wrong.
OAuth
{
"mcpServers": {
"tickets": {
"url": "https://tickets.example.com/mcp",
"oauth": {
"scopes": "tickets.read tickets.write"
}
}
}
}| Key | Type | Purpose |
|---|---|---|
scopes | string | Space-separated scopes to request |
client_id | string | A pre-registered client id. Omit to use dynamic registration (RFC 7591) |
client_secret | string | Only for a confidential client. Omit for the public-client flow |
An empty "oauth": {} object is valid and enables the flow with discovery-supplied defaults.
Standards implemented
| RFC / spec | Role |
|---|---|
| OAuth 2.1 | Authorisation-code flow, public client |
| PKCE (S256) | Mandatory. plain is not accepted |
| RFC 9728 | Protected Resource Metadata discovery |
| RFC 8414 | Authorization Server Metadata discovery |
| RFC 8707 | Resource indicator, binding the token to that server |
| RFC 7591 | Dynamic client registration, when no client_id is given |
Not implemented: the CIMD registration variant, device-code flow, token introspection. None is needed for the standard remote-server flow.
Token storage
| Path | ~/.config/oasis-agent/mcp-tokens.json |
| Override | OASIS_MCP_TOKEN_STORE, or XDG_CONFIG_HOME |
| Refresh | Automatic, one minute before actual expiry |
Run the browser flow once:
oasis-agent --mcp-config <path> --mcp-loginIt authenticates every configured HTTP server, reports, and exits. It never runs a turn.
Curation
tools: the allowlist
{ "tools": ["query_series", "list_tags"] }Only the named tools are registered. Everything else the server offers is ignored. This is the primary lever: a server that exposes forty tools costs forty tool schemas in your prompt prefix, and the model has to read all of them on every turn.
readOnly
{ "readOnly": true }Every tool from the server is treated as safe-tier and never prompts.
This is a claim you are making about the server
The agent has no way to verify it. Set it only on a server you know cannot change anything.
Without readOnly, MCP tools are treated as mutating and prompt according to the permission mode.
required
{ "required": true }| A server that fails to connect | |
|---|---|
false (default) | Skipped, with a message. The run continues |
true | Startup fails |
Use true when the run is meaningless without that server's tools, and to protect the prompt cache from a flaky server silently reshaping the schema.
Startup output
Each server reports as it loads:
| Event | Meaning |
|---|---|
| Server starting | Connection attempt begun |
| Server ready | Connected. Reports tool count and read-only status |
| Server error | Failed to connect. Skipped unless required |
| Tool renamed | A namespaced name collided and was disambiguated with a hash suffix |
A tool renamed message is worth reading. The model will see the renamed tool, not the obvious one, and a collision usually means two config entries that wanted to be the same server.
Tool naming rules
mcp__<sanitised-server>__<sanitised-tool>| Rule | Behaviour |
|---|---|
| Maximum length | 64 characters, truncated if longer |
| Sanitisation | Characters outside the allowed set are replaced, so "my server" and "my:server" can collide |
| Collision | Resolved with a short hash suffix and reported at startup |
Complete example
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
},
"historian": {
"type": "http",
"url": "https://historian.internal/mcp",
"headers": { "Authorization": "Bearer ${HISTORIAN_TOKEN}" },
"tools": ["query_series", "list_tags"],
"readOnly": true,
"timeoutMs": 120000,
"required": true
},
"tickets": {
"url": "https://tickets.example.com/mcp",
"oauth": { "scopes": "tickets.read tickets.write" },
"tools": ["create_ticket", "search_tickets"]
}
}
}