Connect an MCP server
External MCP tools show up as bash: mcp call SERVER TOOL. Config lives in the repository database, not in git, so it is platform metadata rather than a workspace file. Transport is HTTP streamable only. There is no stdio server.
This is not a published *.mcp.sh on your site. Those are tools you expose. This page is inbound: you call someone else's server.
Done looks like
mcp list --server NAME prints lines like mcp call NAME some_tool. mcp call NAME some_tool --json '{…}' returns that tool's result. mcp show NAME prints the name, URL, visibility, and auth_type (for example oauth). The connection still works after you leave the chat.
Pick a path
| The server wants | Use | Who can run it |
|---|---|---|
| A browser login (OAuth with dynamic client registration) | mcp connect (worked example A) | Repository owner |
| An API key or Bearer token in a header | secret open or secret set, then mcp add (worked example B) | Anyone with write access |
If the provider does OAuth but does not support dynamic client registration, mcp connect cannot finish. Ask the provider for an API key and use path B.
Worked example A: OAuth server (Resend)
Job: the server wants a browser login. You are a repository owner.
mcp connect resend --url https://mcp.resend.com
https://mcp.resend.com/mcp works too. OAuth uses the canonical resource URL from the server's metadata.
Expected shape:
Open this URL to authorize MCP server `resend`:
https://…/sails/mcp/oauth/start/mcpsess_...
Session: mcpsess_...
Then run: mcp connect poll mcpsess_...
- Open the URL. Finish the provider login. The provider redirects back to the MCP host, and the done (or error) page is on the Sails admin app.
- Back in the shell:
mcp connect poll mcpsess_...
Expected shape:
Connected MCP server `resend`. Use `mcp list --server resend` or `mcp call resend TOOL`.
mcp list --server resend
mcp call resend list-api-keys --json '{}'
Pick tool names from mcp list. Do not pass --wait from bash_exec. Output is buffered until the command ends, so you never see the URL. Print it, then run mcp connect poll SESSION_ID. This is the same rule as secret open --wait.
--timeout SECS applies to mcp connect. OAuth tokens are stored encrypted and refresh when a connection is established. If authorization expires, run mcp connect again.
Worked example B: API key header (Exa)
Job: Exa wants its key in an x-api-key header. Store the key first, bound to the MCP server's origin. See Store a secret.
The origin is the scheme and host of the --url you will register, with no path. For https://mcp.exa.ai/mcp that is https://mcp.exa.ai.
Browser intake (preferred, so the key never passes through chat):
secret open --origin https://mcp.exa.ai exa-key
# prints an intake URL and an intake id; submit the key in the browser
secret poll INTAKE_ID
# prints: ssec_...
Or, when you already hold the value in the shell:
secret set --origin https://mcp.exa.ai --stdin < exa-key.txt
Register the server with the token, not the raw key:
mcp add exa --url https://mcp.exa.ai/mcp \
--header "x-api-key=ssec_..." \
--visibility owner
Bearer style (GitHub-like servers), with the secret bound to that server's origin:
secret set --origin https://mcp.example.com --stdin < token.txt
mcp add github --url https://mcp.example.com/mcp \
--header "Authorization=Bearer ssec_..." \
--visibility owner
The platform swaps in the real value only when it calls a URL on the secret's origin. If the origin does not match, the secret has no origin, or you cannot use the token, the call fails closed and the error never shows the value.
mcp reload
mcp list --server exa
mcp call exa search --json '{"query":"sails static site"}'
Pick the real tool name from mcp list. mcp help also lists --auth K=ssec_... on mcp add. The skill's examples use --header, so start there.
mcp add needs write access. Default --visibility owner means only the creator can call it. Repository owners can mcp chmod and mcp remove any server but cannot call it unless they are the creator or inside the visibility scope.
Who can use the server
mcp chmod exa --visibility members
mcp chmod exa --visibility group --group writers
mcp show exa
mcp remove exa
| Visibility | Who can mcp call |
|---|---|
owner (default) | Creator only |
group | Creator + --group NAME |
members | Any repository member |
Sharing the server does not share the key. The ssec_... in its header is a separate secret with its own visibility (owner-only by default). When you widen the server, widen the secret too, or other members' calls fail closed:
secret chmod ssec_... --visibility members
mcp remove is allowed for the creator or a repository owner. Migrate a one-time .sails/mcp.json with mcp import ~/.sails/mcp.json (owner).
Commands
mcp help
| Command | What it does |
|---|---|
mcp connect NAME --url URL [--timeout SECS] | Owner. Print OAuth URL; then mcp connect poll SESSION_ID |
mcp connect NAME --url URL --wait | Block until authorized. Avoid inside bash_exec |
mcp add NAME --url URL [--header K=V]... [--auth K=ssec_...]... [--visibility ...] [--group NAME] | Register static headers with ssec_... tokens (write access) |
mcp list [--server NAME] | Tools you can use (mcp call SERVER TOOL lines) |
mcp call SERVER TOOL [--json '{}'] | Invoke a tool |
mcp call SERVER TOOL --json '{}' --background | Long job, when the server supports MCP tasks |
mcp task list SERVER | Background tasks on that server |
mcp task get SERVER TASK_ID | One task's status, without waiting |
mcp task wait SERVER TASK_ID [--timeout SECS] | Block until that task finishes |
mcp reload | Re-read config from the database and reset pooled connections |
mcp show NAME | Config (redacted if you cannot use it) |
mcp chmod NAME --visibility owner|group|members [--group NAME] | Change who can call it |
mcp remove NAME | Delete (creator or repository owner) |
mcp import [PATH] | One-time import from .sails/mcp.json (owner) |
mcp resource list SERVER / read SERVER URI / templates SERVER | Read-only resources |
mcp prompt list SERVER / get SERVER NAME [--json '{}'] | Prompts |
mcp resource list github
mcp resource read github "file://docs/README.md"
mcp prompt list github
mcp prompt get github summarize --json '{"text":"..."}'
mcp call github heavy-job --json '{}' --background
mcp task list github
mcp task get github TASK_ID
mcp task wait github TASK_ID --timeout 600
Connections are pooled per server with a 5 minute idle timeout. mcp reload resets them, which helps after you change a header or visibility.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
mcp connect denied | Owner-only | An owner runs it, or use mcp add with an API key |
mcp connect cannot start OAuth | Provider has no dynamic client registration | Get an API key; secret open --origin ...; mcp add --header |
| Poll times out | Nobody finished the browser step | mcp connect again; new session id |
secret open or secret set errors on a missing flag | --origin is required | Add --origin https://HOST (scheme and host, no path) |
| Call fails closed / header empty | Secret origin does not match the server URL, it has no origin, or you cannot use that ssec_... | secret list to see the allowed origin; secret chmod ssec_... --origin https://HOST; secret chmod ssec_... --visibility ... if it should be shared |
| Teammate's call fails but yours works | Server is shared, secret is still owner-only | secret chmod ssec_... --visibility members (or group) |
mcp list omits the server | Visibility, or config not reloaded | mcp show NAME; mcp chmod; mcp reload |
| No such tool | Wrong server name, or list is stale | mcp reload; mcp list --server NAME |
| Private IP / URL blocked | Same outbound policy as curl | Public HTTPS URL only |
| You started a stdio process | Not supported | HTTP streamable URL only |
| You committed the API key | Wrong store | secret open --origin ..., put ssec_... in --header, rotate the leaked key |
Sampling and elicitation are declined (headless bash). Progress for long calls prints [mcp progress] … on stderr.
Site tools you publish yourself are *.mcp.sh, not this command. Secrets how-to: Store a secret.