# MCP servers

> The Model Context Protocol (MCP) lets you connect external tool servers to kin. Their tools appear in the registry alongside the built-ins, gated by the same permission system.

The Model Context Protocol (MCP) lets you connect external tool servers to kin.
Their tools appear in the registry alongside the built-ins, gated by the same
permission system.

<!-- SOURCE: src/kin/harness/mcp/, src/kin/harness/mcp_hub.py, src/kin/harness/mcp_oauth.py, src/kin/harness/session/, src/kin/harness/approvals.py, src/kin/tui/mcp_lifecycle.py, src/kin/tui/commands_mixin.py, src/kin/tui/artifact_editor.py, src/kin/harness/artifact_edit.py, container/dashboard/mcp_api.py -->

## Outcome

Add an MCP server, review what will start, and make its tools available to the
current session without exposing resolved secrets in the UI.

## Fast path

1. Run `/mcp`.
2. Choose **Add server…** and select project or global scope.
3. Enter the stdio command or HTTP URL; use `${VAR}` for secret values.
4. Save and apply.
5. Approve the server's launch or endpoint identity.
6. Confirm the server is enabled and connected in the manager.

[![Kin MCP manager listing a connected local server and a disabled remote server with transport, scope, and redacted details.](/docs-assets/kin/assets/screenshots/mcp_manager.svg)](/docs-assets/kin/assets/screenshots/mcp_manager.svg)

*The manager owns quick add, toggle, and apply tasks. Resolved credentials stay
masked, and a changed launch identity asks again.*

## Configuring servers

kin reads MCP servers from an `mcp.json` file. Two locations are checked, and a
project entry wins on a name clash:

1. `<workdir>/.kin/mcp.json`
2. `~/.kin/mcp.json`

A root `<workdir>/.mcp.json` is **not** read: that spelling is the file other
harnesses (Claude Code, codex, …) load and write, so kin treats it as theirs —
auto-loading it would execute foreign configuration, the posture kin already
rejects for `.claude/skills`. When one exists, kin points at it in a launch
note and the [`/mcp` manager](#managing-servers) lists its servers as
`foreign (not loaded)` rows you can adopt explicitly.

The shape follows the cross-tool `mcpServers` convention:

```json
{
  "mcpServers": {
    "files": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
    },
    "api": {
      "type": "http",
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer ${MY_TOKEN}" }
    }
  }
}
```

| Field | Notes |
|-------|-------|
| `type` | `stdio` (default when `command` is set), `http` / `streamable-http`, or `sse`. `sse` is parsed but never connected — it's warned as deprecated and skipped in favor of `http`; `ws` is skipped outright (not in the spec) |
| `command` / `args` | The process to spawn for an stdio server |
| `url` / `headers` | The endpoint and headers for an http server |
| `env` | Extra environment variables, merged over the SDK's safe baseline — never your full environment |
| `timeout` | Per-tool execution timeout in milliseconds |
| `disabled` | `true` turns a server off without deleting it — it's skipped on load (the `/mcp` manager toggles this) |
| `allowed_tools` | A per-server capability allowlist (also accepted as `allowedTools`). When present, **only** the listed tool names may register — anything else the server exposes is skipped with a warning, including tools it tries to add later via `tools/list_changed`. Fail-closed: an empty list exposes nothing |
| `auth` | `"oauth"` opts an `http` server into the OAuth 2.1 sign-in flow — see [OAuth for remote servers](#oauth-for-remote-servers) |
| `oauth` | Optional parameters for that flow: `clientId`, `callbackHost`, `callbackPort`, `scopes` (snake_case spellings also accepted). Present on an `http` server, it implies `auth: "oauth"`. Needed by providers that don't register clients dynamically — see [Providers that need a registered client](#providers-that-need-a-registered-client) |

Values support `${VAR}` and `${VAR:-default}` expansion, so secrets stay in
your environment rather than in the file. Malformed entries are skipped with a
warning rather than failing the launch. Both `stdio` and `http` /
`streamable-http` are fully supported live transports (including
[OAuth](#oauth-for-remote-servers) on the http side); `sse` and `ws` are not.

## Connecting & trust

A server's `command` is spawned only after you approve it. On first run kin shows
the approval modal with the server's transport and command; choose allow-once or
allow-always. "Always" persists the grant to `~/.kin/mcp_trust.json`, so the
server connects silently on later launches.

The grant is keyed by the server name **and** a hash of its launch-affecting
fields (command, args, url, env). If any of those change, the key no longer
matches and kin re-prompts — a rug-pull defense, so a server can't quietly swap
in a different command or env after you've trusted it. A cloned repo's poisoned
`.kin/mcp.json` therefore can't execute anything on launch without your
approval (and a cloned repo's root `.mcp.json` is never loaded at all).

## Namespacing

Each server's tools are flattened into the registry namespaced as
`mcp__<server>__<tool>` — for example `mcp__files__read_file`. (A name that would
exceed the 64-character tool-name limit is truncated with a short hash suffix so
two long names can't collide.) Tool descriptions are sanitized before the model
sees them: zero-width and bidi control characters are stripped, the text is
length-capped, and injected-override phrasing is flagged — defenses against tool
poisoning.

## Permissions

MCP tools **ask for approval by default** (they carry the `MCP` risk kind, so the
gate prompts on each first use). A tool is auto-allowed as read-only only when its
approved server declares `readOnlyHint` for it in the server's initial tool list.
A tool that appears later via a `tools/list_changed` notification is always
treated as unapproved and re-vetted on first call, even if it claims to be
read-only — so a server can't slip in a "read-only" tool to dodge the gate.

Every MCP tool result is wrapped in untrusted-content framing before it enters
the conversation, since server output is attacker-influenceable. See
[Modes & permissions](/docs/kin/guide/modes-and-permissions/) for the gate, and
[Tools](/docs/kin/guide/tools/) for how tools surface.

An "always allow" grant on an MCP tool is scoped to that tool's **name only**,
kept deliberately separate from shell-command grants — an MCP tool that
declares a `command` parameter can never piggyback on (or feed) a standing
`shell` grant.

If a server **redefines** a tool mid-session (same name, changed description,
schema, or read-only hint), kin treats it as a rug-pull signal, not routine
churn: a ⚠ system note and a warning toast name the redefined tools, any
standing "always allow" grant for those names is revoked, and the tool must be
approved again on its next use. kin also compares the running `mcp` SDK
version against the one this build was vetted with and warns loudly on drift
(a supply-chain posture: a stale or unexpectedly-upgraded SDK should be
re-vetted, not silently trusted).

## Elicitation

An MCP server can ask *you* for input mid-call (the spec's
`elicitation/create`). kin bridges each request onto the same question modal
the `ask` tool uses — one prompt per schema field, with the header naming
**which server** is asking. Enum fields become option lists, booleans become
Yes/No, free-form fields take typed text; press ++escape++ at any point to
decline the whole request (nothing partial is ever sent). Requests whose
schema falls outside the spec's flat-primitives subset are declined
automatically, and kin never auto-answers: in a [headless run](/docs/kin/guide/headless/)
an elicitation is recorded as **needs-human** (exit 2) and declined.

URL-mode elicitation (the newer spec revision, used for sensitive out-of-band
flows like sign-ins) shows you the full URL and asks for consent — kin never
opens or fetches the URL itself; you visit it out of band if you choose.

## Resources

When a connected server declares the `resources` capability, kin registers two
read-only tools: `list_mcp_resources` (enumerate URIs, optionally per server)
and `read_mcp_resource` (fetch one by URI). Both are `read` kind — resources
are read-only by protocol and the server already cleared the first-run
approval gate — and everything they return is wrapped in untrusted-content
framing, with binary content summarized rather than decoded and long text
truncated.

## OAuth for remote servers

Set `"auth": "oauth"` on an `http` server to authenticate with OAuth 2.1
instead of a static header:

```json
{
  "mcpServers": {
    "notion": { "type": "http", "url": "https://mcp.notion.com/mcp", "auth": "oauth" }
  }
}
```

On first connect kin runs the MCP authorization flow — discovery, dynamic
client registration where offered, then **authorization code + PKCE** with a
one-shot loopback callback on `127.0.0.1`. The sign-in URL is posted to the
transcript and a browser open is attempted; finish the consent screen and the
connect completes (first-time OAuth connects get a 300 s handshake budget).
Tokens (and the client info) persist under `~/.kin/mcp_auth/` in `0600` files,
so later launches reconnect silently via the stored/refreshed token — the token
itself never appears in the transcript, journal, or `/mcp` status (which shows
the auth mode and, when set, the public client id and loopback redirect).

Stored state is keyed by the endpoint *identity*: the URL plus the server's
non-secret headers. Services that serve many tenants from one URL and select
between them with headers therefore get one token file each, and rotating a
bearer header never strands an OAuth token.

Auth-code + PKCE is the flow the MCP spec and the hosted-server ecosystem
actually support (device flow appears in no spec revision and no major hosted
server). On a box with no reachable browser — Outpost, a bare SSH host —
prefer a PAT/bearer token in `headers` (`"Authorization": "Bearer ${MY_PAT}"`),
which GitHub, Linear, Atlassian and Stripe all document for exactly this case.

### Providers that need a registered client

Some authorization servers don't implement dynamic client registration, and
match the redirect URI exactly. Microsoft Entra ID is the common one. For those,
an administrator registers an application once and you name it in the `oauth`
block:

| Key | Notes |
|-----|-------|
| `clientId` | The pre-registered application id. Supports `${VAR}`. Without it kin attempts dynamic registration, which such providers reject |
| `callbackPort` | The fixed loopback port in the registered redirect URI. Omitted, kin uses an ephemeral port, which only works where any port is accepted |
| `callbackHost` | `127.0.0.1` (default) or `localhost`. Use whichever spelling the redirect URI was registered with; `localhost` requires `callbackPort` and binds both IPv4 and IPv6 |
| `scopes` | Exact scopes to request, replacing the ones the server advertises. Use this when the advertised set is missing something you need — most often `offline_access`, without which the provider issues no refresh token and every launch re-opens the browser |

Only loopback hosts are accepted: the authorization code is a credential, and a
non-loopback redirect would hand it to another machine.

### Example: Dynamics 365 Business Central

Business Central serves every tenant from one URL and selects the environment
with headers. Register an application in the [Microsoft Entra admin
center](https://entra.microsoft.com) — multi-tenant, a **Mobile and desktop
applications** redirect URI of `http://localhost:33418/callback`, delegated
`Dynamics 365 Business Central → Financials.ReadWrite.All`, admin-consented —
then:

```json
{
  "mcpServers": {
    "businesscentral": {
      "type": "http",
      "url": "https://mcp.businesscentral.dynamics.com",
      "headers": {
        "TenantId": "${BC_TENANT_ID}",
        "EnvironmentName": "Production",
        "Company": "CRONUS USA, Inc."
      },
      "auth": "oauth",
      "oauth": {
        "clientId": "${BC_MCP_CLIENT_ID}",
        "callbackHost": "localhost",
        "callbackPort": 33418,
        "scopes": [
          "https://mcp.businesscentral.dynamics.com/Financials.ReadWrite.All",
          "offline_access"
        ]
      }
    }
  }
}
```

The port here must match the registered redirect URI. A `Company` or
`ConfigurationName` containing non-ASCII characters has to be sent Base64-encoded
in the `=?base64?…?=` form the provider documents. Microsoft's own
[setup guide for non-Microsoft hosts](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/ai/use-mcp-server-non-microsoft)
covers the registration steps in full. The server exposes read-only access to
its API pages by default; write operations are enabled administratively on the
Business Central side, not here.

## Managing servers

Run `/mcp` to open the interactive server manager. The left pane lists every
configured server (and an "Add server…" row); the right pane shows the selected
server's details with secrets redacted — the endpoint with query tokens and
credentials stripped, and http headers with secret values masked (`Bearer ●●●●`,
never the token). Resolved secrets are never shown.

If the workspace has a foreign root `.mcp.json`, its servers appear as
`foreign (not loaded)` rows. `enter` on one **adopts** it: the raw spec
(unexpanded `${VAR}` spellings and all) is copied into the project scope, so
saving writes it to `.kin/mcp.json`. The foreign file itself is never modified,
and an adopted server still goes through the ordinary first-run approval on
connect — adoption moves config, never trust. Every other key on a foreign row
asks you to adopt first.

Keys in the manager:

| Key | Action |
|-----|--------|
| `a` | Add a server (opens a form: name, scope, transport, command/args/env or url/headers) |
| `enter` / `e` | Edit the selected server (on a `foreign` row, `enter` adopts it into the project scope) |
| `d` / `x` | Enable / disable the selected server (flips its `disabled` field) |
| `t` | Test — a live connect isn't run from the modal (that must stay on the one MCP lifecycle task); save + apply instead, and watch for the connect note |
| `delete` | Remove the selected server (confirms first) |
| `ctrl+s` | Save & apply — writes the changed mcp.json file(s) and reconnects live |
| `esc` | Cancel (discards changes) |

For larger edits, `/edit-mcp [instruction]` lets kin draft the whole
`.kin/mcp.json` for you and shows a diff to approve — see
[Editing config artifacts](/docs/kin/guide/slash-commands/#editing-config-artifacts). The
manager (`/mcp`) is the quick toggle/add path; `/edit-mcp` is the
draft-from-a-description path. Both reconnect live on apply.

On save, kin writes each touched scope's file (`<workdir>/.kin/mcp.json` or
`~/.kin/mcp.json`) and reconnects the MCP servers **live** — no restart. The
reconnect re-checks approvals (an unchanged server you've trusted reconnects
silently; a changed command re-prompts).

`/reload` re-reads both mcp.json scopes plus `settings.toml` and applies them live
(reconnecting MCP and, if the wire changed, swapping the backend). It's refused
while a turn is running.

Revoke a standing trust grant with `/grants` (which also clears the persistent
trust entry); see [Slash commands](/docs/kin/guide/slash-commands/). To put MCP server config
in a TOML settings file context, see
[Settings (TOML)](/docs/kin/reference/settings-toml/).

## Adding servers from the Outpost Dashboard

The [Outpost Dashboard](/docs/kin/guide/outpost/) offers a second, host-level surface for the
**global** `~/.kin/mcp.json` — useful when you want every workspace's sessions
on the box to adopt the same server without opening `/mcp` in each one. The
**MCP Servers** card lists configured servers (name, transport, masked
endpoint, enabled/disabled), with an enable/disable toggle and a delete
button per row, plus an "Add server" form: paste the same `mcpServers` entry
shape shown above (one server's JSON snippet, copied straight from its
README) and save. Every read goes through the same `safe_endpoint` /
`masked_headers` redaction described above — a resolved `${VAR}` secret never
reaches the browser — and every write persists the raw, unexpanded snippet,
never a secret-resolved copy. It complements, not replaces, the in-TUI `/mcp`
manager (which is project-scoped and live-reconnects); a server added or
toggled from the dashboard takes effect for sessions started **after** the
change — an already-open terminal needs a restart to pick it up.

## Troubleshooting

- **A trusted server asks again:** its command, arguments, URL, or environment
  changed, so the stored identity hash no longer matches.
- **A server you put in `.mcp.json` never appears:** kin reads
  `.kin/mcp.json`, not the root `.mcp.json` (the root spelling is the
  cross-tool convention — other harnesses write it, and so did kin itself
  before the `.kin/` move). Open `/mcp` and adopt the `foreign (not loaded)`
  row, or move the entry yourself.
- **A tool is missing:** check `disabled`, `allowed_tools`, and whether a
  late-added or redefined tool is waiting for fresh approval.
- **OAuth cannot complete on a remote host:** use the provider's documented
  PAT/bearer header path or complete the flow where the loopback callback is
  reachable.
- **Sign-in fails on a redirect URI mismatch** (Entra reports `AADSTS50011`):
  the `oauth.callbackPort` and `callbackHost` must render exactly the redirect
  URI registered with the provider. `/mcp` shows what kin will send.
- **Client registration is rejected or 404s:** the provider doesn't support
  dynamic registration — set `oauth.clientId` to a registered application id.
- **You are asked to sign in every launch:** the provider issued no refresh
  token. Add the scope that grants one (`offline_access` for Entra) to
  `oauth.scopes`.
- **The callback port is already in use:** free it, or change `callbackPort`
  *and* the registered redirect URI together.
- **A removed `clientId` is still being used:** client info persists per
  endpoint. Delete that endpoint's file under `~/.kin/mcp_auth/` to start the
  flow over from registration.
- **An edit did not take effect:** use **Save & apply** in `/mcp`; dashboard
  changes affect newly started sessions.

## Reference

- [Modes & approvals](/docs/kin/guide/modes-and-permissions/)
- [Tools](/docs/kin/guide/tools/)
- [Commands](/docs/kin/guide/slash-commands/)
- [Outpost operations](/docs/kin/internals/outpost-operations/)
