# MCP servers

> Connecting Shelley to remote MCP servers, logging in with OAuth, and how the agent calls their tools.

Shelley can use tools from [Model Context Protocol](https://modelcontextprotocol.io)
servers. Two things work differently from many MCP clients:

- **HTTP only.** Shelley connects to servers over Streamable HTTP. It doesn't
  launch local stdio servers (the `npx some-mcp-server` kind).
- **Not model tools.** MCP tools aren't added to the model's tool list. The
  agent reaches them by running `shelley mcp` with its bash tool, guided by
  the built-in `mcp` [skill](/docs/skills). A server costs one line in the
  system prompt until it's needed, and results can be piped through `jq`
  like any other command output.

## Adding a server

From a terminal:

```sh
shelley mcp add tracker -d 'Issue tracker for the web team' https://mcp.example.com/mcp
```

For a server that takes an API key, pass it as a header:

```sh
shelley mcp add search -H 'Authorization: Bearer sk-...' https://example.com/mcp
```

Or use **MCP Servers** in the conversation's **More options** menu, or ask
Shelley to add it. The name is what you type in commands: letters, digits,
`_` and `-`, starting with a letter or digit, and it can't be changed later.
The description goes in the system prompt, so write it for the model: what
the server is for, and when to reach for it.

Servers live in Shelley's SQLite database, not in a config file. A different
`--db` means a different set of servers. Headers are stored as plain text
there too, which is one more reason to keep the database private.

## Logging in with OAuth

A server without an `Authorization` header can log you in with OAuth. You
don't do anything at `add` time. The first time the agent (or you) uses the
server and it answers `401`, the command fails with a link:

```text
shelley mcp: MCP server "tracker" needs you to log in: open /mcp/login/tracker
```

The link is relative to wherever you reach Shelley, e.g.
`http://localhost:9000/mcp/login/tracker`. The agent is told to hand it to you
and wait. Opening it:

1. registers Shelley as a client with the server's authorization server
   (dynamic client registration), with the redirect URI
   `<your Shelley address>/mcp/oauth/callback`;
2. sends your browser there to log in;
3. brings you back to Shelley and stores the tokens in the database. Shelley
   refreshes them when they expire.

A started login waits up to ten minutes for you. `shelley mcp auth NAME`
shows the login state and link; the MCP Servers dialog has **Log in** and
**Log out** buttons.

Some servers only accept clients they've approved and refuse the
registration; the error says so. Use a token header for those.

Editing a server's URL keeps its login if only the path or query changes.
Changing the scheme or host, or adding an `Authorization` header, drops it.

## How the agent uses them

A new conversation's system prompt lists the registered servers with their
descriptions. If you add or remove one, or change its URL or headers, while
the agent is working, Shelley tells it mid-turn. Idle conversations aren't
interrupted; their system prompt keeps the old list, but `shelley mcp list`
is always current.

A typical exchange from the agent's side:

```sh
shelley mcp search issue label
shelley mcp list tracker.search_issues -schema
shelley mcp call tracker.search_issues query='login bug' limit=5
```

Each conversation gets its own session with each server, so server-side state
lasts across calls. Forks and subagents start fresh, and an unused session
closes after 30 minutes.

This relies on two things:

- `shelley` on the `PATH` that Shelley runs with.
- The Unix socket. Shelley exports its path to the agent's commands as
  `SHELLEY_SOCKET`; with `serve -socket none`, `shelley mcp` from the agent
  can't find the server.

## Commands

`shelley mcp` talks to a running Shelley. By default that's the server whose
socket is in `$SHELLEY_SOCKET`, else `~/.config/shelley/shelley.sock`
(`$XDG_CONFIG_HOME/shelley/shelley.sock` if that's set). Point it elsewhere
with `-url`, which goes before the command:

```sh
shelley mcp -url http://localhost:9001 list
```

| Command | Does |
|---|---|
| `list` | Lists servers with their login state, without connecting. |
| `list SERVER` | Shows the server's instructions and its tools as TypeScript-style signatures. |
| `list SERVER.TOOL [-schema]` | Shows one tool; `-schema` adds its JSON schemas. |
| `list -json [SERVER[.TOOL]]` | The same, as JSON. |
| `search [SERVER] TERM...` | Tools matching every term in their name, description, or parameters, across all servers unless one is named. `-json` for JSON. |
| `call SERVER.TOOL [KEY=VALUE...]` | Calls a tool. Values are used as is for string parameters and parsed as JSON otherwise. |
| `call SERVER.TOOL -` | Reads the arguments as a JSON object from stdin. |
| `add NAME [-d DESC] [-H 'K: V']... URL` | Registers a server. `-H` repeats. |
| `rm NAME` | Removes a server and its login. |
| `auth NAME` | Shows the OAuth login state and, if needed, the login link. |
| `restart NAME` | Ends this conversation's session with the server. |

`call` takes `-json` (print the raw `CallToolResult`) and `-timeout`
(default `1m`; a call that timed out may still have run). Images and other
binary results are saved to temporary files and the output says where. If the
tool reports an error, it goes to stderr and the exit status is 1.

Subcommand flags can go before or after the arguments:

```sh
shelley mcp call tracker.get_issue id=ENG-123 -json | jq .structuredContent
echo '{"query": "login bug"}' | shelley mcp call tracker.search_issues -
```

## Troubleshooting

`/debug/mcp` on your Shelley shows the registered servers, open sessions,
and recent events such as logins.

The code is in [mcp/](https://github.com/boldsoftware/shelley/tree/main/mcp),
[server/mcp.go](https://github.com/boldsoftware/shelley/blob/main/server/mcp.go),
and [client/mcp.go](https://github.com/boldsoftware/shelley/blob/main/client/mcp.go).
