Skip to content

Reference

MCP servers

AI-assistedThis page includes both human and AI contributions.

The Blueprint's mcp block, discovery, how tools become functions, output schemas, results, failure messages, OAuth, limits, and the local and server commands.

An entry in a Blueprint’s mcp block declares an outbound MCP server, which programs import as the Package @mcp/<name>. This page describes the block, how Submilli turns the server’s tools into functions, what calls return and throw, OAuth logins, limits, and the commands that operate the servers locally and on submilli-server.

A map from a server name to a server. The name is chosen by the Blueprint and becomes three things:

Name Example, for linear
The key in mcp linear
The Package @mcp/linear
The capability mcp.linear
Field Type Required Default Constraints
url string yes Non-empty. The server’s MCP endpoint
transport streamable_http, sse no streamable_http sse is another name for streamable_http. stdio is refused
headers map of name to string no none Sent with every request. Values may hold ${secrets.NAME}. Not with auth
auth map no none type: oauth2, the only type. Not with headers
auth field Type Required Default
type oauth2 yes
client_id string no See Client id
authorization_endpoint string no Discovered from the server
token_endpoint string no Discovered from the server
scopes list of strings no See Scopes

Each auth value may hold ${secrets.NAME}. Every ${secrets.NAME} in the block must name a secret the Blueprint declares. A harness secret in a header takes the value the application supplied for the session.

blueprint.yaml (fragment)
secrets:
CRM_API_KEY:
store: crm_api_key
mcp:
crm:
url: https://mcp.acme.example/mcp
headers:
Authorization: Bearer ${secrets.CRM_API_KEY}
linear:
url: https://mcp.linear.app/mcp
auth:
type: oauth2

submilli blueprint add-mcp <name> <url> writes an entry and a mcp.<name> deny rule under main, when the Blueprint has a permissions block.

add-mcp option Writes
none auth: {type: oauth2} if a probe of the server finds it requires OAuth, and otherwise no auth
--authorization-bearer <SECRET> headers: {Authorization: Bearer ${secrets.<SECRET>}}. The secret must be declared
--header 'Name: value' (repeatable) That header
--oauth auth: {type: oauth2}
--client-id <ID> auth.client_id, and implies --oauth
--scope <SCOPE> (repeatable) auth.scopes, and implies --oauth
--no-probe Skips the probe
error: case.yaml: blueprint parse error: mcp.a: missing field `url` at line 4 column 5
error: case.yaml: invalid mcp config: mcp server 'a': stdio transport is deferred for v1; use 'streamable_http'
error: case.yaml: invalid mcp config: mcp server 'a': unknown transport 'websocket' (use 'streamable_http' or its 'sse' alias)
error: case.yaml: invalid mcp config: mcp server 'a' sets both 'headers' and 'auth'; use one (static-key auth or OAuth)
error: case.yaml: invalid mcp config: mcp server 'a' references undeclared secret 'K'
error: case.yaml: blueprint parse error: mcp.a.auth.type: unknown variant `apikey`, expected `oauth2` at line 5 column 18
error: case.yaml: blueprint parse error: mcp.a: unknown field `command`, expected one of `transport`, `url`, `headers`, `auth` at line 5 column 5

@mcp/<name> can’t be listed under packages:

error: case.yaml: invalid packages config: `@mcp/linear` is an MCP virtual package; declare the server in `mcp:` instead

Every call to a tool is checked against the capability mcp.<name> before any request is sent. One capability covers all of a server’s tools. Its filter fields are:

Field Type Value
tool string The tool’s name
transport string Always streamable_http
blueprint.yaml (fragment)
permissions:
main:
- capability: mcp.playwright
filter: tool == "browser_navigate" or tool == "browser_snapshot"
action: allow

A rule for mcp.<name> must name a declared server, and may not put a tool in the capability name:

error: case.yaml: invalid mcp config: permission rule 'mcp.b' references undeclared mcp server 'b'
error: case.yaml: invalid mcp config: permission rule 'mcp.a/click': use capability 'mcp.a' with a filter such as 'tool == "name"' instead of '/tool'

A filter on any other field is reported by submilli blueprint lint and refused at registration:

error: case.yaml: `permissions.main` rule 1 for `mcp.a` tests `name`, which the operation doesn't report, so a condition on it is false for every call, and true under `not`; its fields are: tool, transport

A denied call throws:

PermissionDeniedError: permission denied: caller=main capability=mcp.helpdesk: policy denied mcp.helpdesk for main. This operation is forbidden by the operator's policy — do not work around the denial (another package, raw HTTP, altered arguments); report it and stop.

Rule matching is in Permissions, the filter grammar in Filter language.

Discovery connects to a server, calls tools/list, and builds @mcp/<name> from the result. It sends the entry’s headers or OAuth token, and gives the server 10 seconds.

Where Servers discovered When
submilli run --blueprint Every server in the block At each run
submilli docs @mcp/<name> The named server At each call
submilli-server, running a program The servers the program imports At first use, then cached
submilli-server, Package docs and search Every server in the block At first use, then cached

The server keeps a Blueprint’s discovered tools until the Blueprint is applied again or removed, a login for it is stored or removed, or the server restarts. A session with harness secrets bound is discovered for that session and not cached.

A server is left out of the Package catalog, and the rest of the Blueprint works, when:

  • it has auth: oauth2 and no login is stored for it
  • the connection or tools/list fails
  • it doesn’t answer within 10 seconds
  • on submilli-server, the network rules refuse its address

Each is a warning that begins warning: @mcp/<name>: server unavailable: and gives the reason:

warning: @mcp/tracker: server unavailable: not authenticated — run `submilli server mcp authenticate`
warning: @mcp/local: server unavailable: blocked by network policy: 127.0.0.1 is private/loopback IP space; allow-list it on the server with --allow-ip / --allow-localhost / --allow-private

On submilli-server, the same text is in submilli server run-code’s output, in the discovery_warnings list of the HTTP response, and in the server’s log as a level=warn line:

ts=2026-10-03T17:06:08.482Z level=warn stream=log target=submilli_shared::mcp::discovery msg="MCP server omitted: not authenticated — run `submilli server mcp authenticate browse tracker`" server=tracker

A program that imports a Package that was left out doesn’t compile:

error: MCP server `local` is unavailable — `@mcp/local` is absent from the discovered catalog; check the blueprint's `mcp:` block and discovery warnings
--> link.ts:1:19
|
1 | import local from "@mcp/local";
| ^^^^^^^^^^^^
2 |
|
help: no MCP servers are currently available; declared servers may need authentication or may have failed discovery

A Blueprint is PENDING while any of its auth: oauth2 servers has no stored login, and ACTIVE otherwise. A PENDING Blueprint runs programs. Its servers without a login are left out. auth-status prints the state and one line per server:

browse: PENDING
local n/a (no auth)
tracker oauth — NOT AUTHENTICATED
Server line Server
oauth — authenticated auth: oauth2, login stored
oauth — NOT AUTHENTICATED auth: oauth2, no login
n/a (static API key) headers
n/a (no auth) Neither

Each tool becomes an exported function of @mcp/<name>, named as the tool is. Calls are synchronous.

In the tool In the function
name The function’s name
description The start of its documentation
inputSchema with properties One parameter, args, an object type
inputSchema with properties, none required args?, which may be omitted
inputSchema without properties No parameter
A property not in required An optional field
outputSchema The return type (see Output schemas)
JSON Schema Type
string string
number, integer number
boolean boolean
null null
array with items An array of the item type
object with properties, or properties without type An object type
A string enum of two or more values A union of string literals
A string enum of one value string
anyOf or oneOf of scalar types A union
A type list of scalar types, such as ["string", "null"] A union

An input property whose schema has no type here becomes unknown, and the MCP server validates the value. Schemas with no type are allOf, $ref, not, an object without properties, a non-string enum, anyOf or oneOf with an object or array member, and nesting deeper than 12 levels.

A tool is dropped, with a warning, when its name isn’t a TypeScript identifier or is one of await, delete, with, debugger, yield, eval, arguments, implements, interface, package, private, protected, public, static, let. When two tools have the same name, both are dropped. A dropped tool isn’t callable.

A server whose tools/list has these tools:

warning: @mcp/helpdesk: tool `bad-name` dropped: tool name must be a TypeScript function identifier
warning: @mcp/helpdesk: tool `dup` dropped: duplicate tool name in tools/list
warning: @mcp/helpdesk: tool `dup` dropped: duplicate tool name in tools/list
warning: @mcp/helpdesk: 3 tool(s) return unknown: result schemas are unavailable or unrepresentable; consult package docs for signatures
@mcp/helpdesk — MCP server 'helpdesk' (4 tools)
/**
* Always fails.
* Returns `unknown`; this MCP server did not publish an outputSchema — cast to a declared type (`as T`) after checking the shape.
*/
function fail(): unknown;
/**
* Fetch one support ticket by id.
* Typed — use the result directly (narrow optional `foo?` fields first); no cast needed.
*/
function get_ticket(args: { extra?: unknown; id: string; limit?: number; note?: string | null; priority?: "low" | "high" }): { id: string; status: string };
/**
* List open tickets.
* Returns `unknown`; this MCP server did not publish an outputSchema — cast to a declared type (`as T`) after checking the shape.
*/
function list_tickets(args?: { query?: string }): unknown;
/**
* Two text parts.
* Returns `unknown`; this MCP server did not publish an outputSchema — cast to a declared type (`as T`) after checking the shape.
*/
function two_texts(): unknown;

get_ticket’s extra property is an allOf, limit is an integer, and note is ["string", "null"]. submilli docs @mcp/<name> and the Package docs tool the agent reads list every discovered tool, whether or not the Blueprint allows it.

A function’s return type comes from the first of:

  1. The tool’s own outputSchema, when every part of it maps to a type in the table above.
  2. A schema Submilli carries for the tool. Submilli carries schemas for 18 tool names of GitHub’s MCP server, for a url whose host is api.githubcopilot.com. A server at any other host gets none.
  3. Otherwise, unknown.

The documentation line says which:

Return Documentation line
From 1 or 2 Typed — use the result directly (narrow optional `foo?` fields first); no cast needed.
unknown, no outputSchema Returns `unknown`; this MCP server did not publish an outputSchema — cast to a declared type (`as T`) after checking the shape.
unknown, outputSchema not representable Returns `unknown` (the server's outputSchema is not representable) — cast to a declared type (`as T`) after checking the shape.

Discovery warns once per server with the count of tools that return unknown. A cast from unknown is checked when it runs. A field the type declares must be present with that type, and fields it doesn’t declare are ignored.

cast.ts
import helpdesk from "@mcp/helpdesk";
interface Tickets {
tickets: { id: string; subject: string; owner: string }[];
}
function main(): number {
const open = helpdesk.list_tickets() as Tickets;
return open.tickets.length;
}
error: TypeError: type mismatch: expected Tickets, got object at $["tickets"][0]["owner"]
The tool’s result The function returns
isError: true Nothing. It throws (see Failures)
structuredContent That value
One text part The text parsed as JSON, or the text as a string when it isn’t JSON
Several text parts The texts joined with newlines, as a string
No text part null

Content that isn’t text, such as images, is ignored.

A failed call throws an error the program can catch with try. The message begins with the function’s name:

Failure Message
The Blueprint denies the call PermissionDeniedError: permission denied: caller=main capability=mcp.<name>: …
The tool returns isError: true @mcp/<name>.<tool>: and the tool’s text, or MCP tool reported an error when it has none
The connection, the HTTP exchange, or the protocol fails @mcp/<name>.<tool>: transport error: and the cause
The call takes more than 60 seconds @mcp/<name>.<tool>: transport error: MCP tool '<name>/<tool>' timed out after 60 seconds
The OAuth token endpoint refuses the refresh token after retries McpAuthExpiredError: @mcp/<name>.<tool>: OAuth authentication failed; retry later or authenticate the MCP server again
The OAuth token endpoint answers with another HTTP error @mcp/<name>.<tool>: server returned HTTP <status>: <body>

A tool error, caught:

Error: @mcp/helpdesk.fail: ticket store is read-only today

A program’s calls to one server share one MCP session, opened by the first call and closed when the program ends. A call that fails or times out closes the session, and the next call opens a new one. Each program run gets its own sessions.

A server with auth: type: oauth2 needs a login, made once per Blueprint and server with submilli mcp authenticate locally or submilli server mcp authenticate for a registered Blueprint. Both run on the machine where the command is typed. They print an authorization URL and wait for the browser’s redirect on http://127.0.0.1:8765/callback. SUBMILLI_OAUTH_REDIRECT_PORT changes the port. The flow uses PKCE.

Endpoints the Blueprint doesn’t set are discovered from the server’s OAuth metadata.

The first of:

  1. auth.client_id in the Blueprint.
  2. The client_id of a configured provider for the OAuth host.
  3. A client registered at login through the server’s dynamic client registration endpoint, when it advertises one.

Without any of them, the login fails and names the two fixes, setting auth.client_id or configuring a provider.

The first non-empty list of auth.scopes, the provider’s scopes, and the scopes_supported the server advertises.

A provider holds the client id of an OAuth application registered with a service, and a client secret for a confidential client.

Field Type Required Value
match string yes The OAuth host, such as github.com, and not the MCP server’s host
client_id string yes A literal, ${secrets.NAME}, or ${env.VAR}
client_secret string no ${secrets.NAME} or ${env.VAR}. Absent for a public client
scopes list of strings no Scopes to request
Local submilli-server
Where $SUBMILLI_HOME/mcp_oauth.yaml (default ~/.submilli/mcp_oauth.yaml), under providers The config file, under mcp_oauth.providers (see Server settings)
Edited with submilli mcp provider add, list, remove The config file
match compared with The token endpoint’s host The authorization endpoint’s host when choosing the client id, and the token endpoint’s host for the token exchange
${secrets.NAME} from The local secret store The server’s secret store
~/.submilli/mcp_oauth.yaml
providers:
- match: github.com
client_id: Iv1.example
client_secret: ${secrets.GITHUB_CLIENT_SECRET}
scopes:
- repo

A login is stored in a secret store under mcp_oauth/<blueprint>/<server>/credential. submilli mcp authenticate uses the local store, and submilli server mcp authenticate uses the server’s. The two are separate. The server’s login needs the server to have a secret store. One login serves every program run under the Blueprint, and on a server every session of it.

Event Behavior
The service issues a refresh token Only the refresh token is stored
The service issues only an access token The access token is stored
A call needs an access token Obtained with the refresh token and kept in memory, never stored. It is replaced 60 seconds before it expires, or after 5 minutes when the service gives no lifetime
The service issues a new refresh token It replaces the stored one
The service refuses the refresh token (invalid_grant) Retried after 1 and 2 more seconds, then McpAuthExpiredError. The stored login is kept
A call fails The token is refreshed and the call retried once
deauthenticate The login is removed and the Blueprint is PENDING

Running authenticate again replaces the stored login.

Limit Value
Transport HTTP only (streamable_http). A server that speaks MCP over standard input and output needs an HTTP endpoint in front of it
MCP features Tools only. Resources, prompts, and sampling aren’t used
Discovery 10 seconds per server
One call 60 seconds, including obtaining a token and connecting. Not configurable
Results Text parts and structured content. Other content is dropped
Streaming None. A call returns when the tool finishes, and progress messages are ignored
Network Locally, any address. On submilli-server, the server’s outbound network rules apply to discovery, calls, and the OAuth exchange. Private and loopback addresses are refused unless the server allows them (see Server settings)
Task Local, with a Blueprint file On submilli-server, with a registered Blueprint
Declare a server submilli blueprint add-mcp <name> <url> submilli server blueprint apply <file> after declaring it
Run a program submilli run --blueprint <file> <script> submilli server run-code <script> --blueprint <blueprint>
List a server’s tools submilli docs @mcp/<name> [--blueprint <file>] (default blueprint.yaml) submilli server docs @mcp/<name> --blueprint <blueprint>
Log in submilli mcp authenticate --blueprint <file> <name> submilli server mcp authenticate <blueprint> <name>
Check logins submilli mcp auth-status --blueprint <file> submilli server mcp auth-status <blueprint>
Log out submilli mcp deauthenticate --blueprint <file> <name> submilli server mcp deauthenticate <blueprint> <name>
Providers submilli mcp provider add|list|remove mcp_oauth.providers in the config file
Logins kept in The local secret store, in plain files The server’s secret store, encrypted
Private and loopback addresses Allowed Refused unless the server allows them
Tool list refreshed On every run When the Blueprint is applied, a login changes, or the server restarts

The steps for declaring a server, choosing tools, and logging in are in Add an MCP server.