These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

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 wantsUseWho 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 headersecret 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_...
  1. 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.
  2. 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
VisibilityWho can mcp call
owner (default)Creator only
groupCreator + --group NAME
membersAny 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
CommandWhat it does
mcp connect NAME --url URL [--timeout SECS]Owner. Print OAuth URL; then mcp connect poll SESSION_ID
mcp connect NAME --url URL --waitBlock 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 '{}' --backgroundLong job, when the server supports MCP tasks
mcp task list SERVERBackground tasks on that server
mcp task get SERVER TASK_IDOne task's status, without waiting
mcp task wait SERVER TASK_ID [--timeout SECS]Block until that task finishes
mcp reloadRe-read config from the database and reset pooled connections
mcp show NAMEConfig (redacted if you cannot use it)
mcp chmod NAME --visibility owner|group|members [--group NAME]Change who can call it
mcp remove NAMEDelete (creator or repository owner)
mcp import [PATH]One-time import from .sails/mcp.json (owner)
mcp resource list SERVER / read SERVER URI / templates SERVERRead-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 seeWhat it meansWhat to do
mcp connect deniedOwner-onlyAn owner runs it, or use mcp add with an API key
mcp connect cannot start OAuthProvider has no dynamic client registrationGet an API key; secret open --origin ...; mcp add --header
Poll times outNobody finished the browser stepmcp connect again; new session id
secret open or secret set errors on a missing flag--origin is requiredAdd --origin https://HOST (scheme and host, no path)
Call fails closed / header emptySecret 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 worksServer is shared, secret is still owner-onlysecret chmod ssec_... --visibility members (or group)
mcp list omits the serverVisibility, or config not reloadedmcp show NAME; mcp chmod; mcp reload
No such toolWrong server name, or list is stalemcp reload; mcp list --server NAME
Private IP / URL blockedSame outbound policy as curlPublic HTTPS URL only
You started a stdio processNot supportedHTTP streamable URL only
You committed the API keyWrong storesecret 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.