typed

MCP servers in typed

MCP (the Model Context Protocol) is how a typed session reaches tools that are not built in: a database, an issue tracker, a documentation index, your own scripts. Each MCP server you configure is started or connected when a session begins, its tools are offered to the model alongside typed's built-in ones, and the model calls them the same way it calls Read or Bash, through the same permission gates. Any MCP server works -- one you wrote, one a vendor ships, one from a package registry -- over any of the three standard transports. If you would rather not manage servers one at a time, typed also knows how to find Yaw MCP, a broker that manages many servers behind one entry (described further down); it is optional, and everything on this page works without it.

Quick start

Add a server, then start a session. Servers are resolved when a session starts, so every edit below applies from the next launch.

A stdio server is anything you can run from a shell, passed after --:

typed mcp add <name> -- <command> [args...]

A remote server over Streamable HTTP (use --transport sse for the legacy SSE transport):

typed mcp add <name> --transport http https://host/mcp

One of the curated Yaw servers, with no package name to remember:

typed mcp add --yaw context7

See everything typed will read, with the scope each entry comes from, or take one out again:

typed mcp list
typed mcp remove <name>

Sign in to a remote server that wants OAuth rather than a static token, or drop those credentials again:

typed mcp login <name>
typed mcp logout <name>

Inside a session every one of these verbs runs as a slash command -- /mcp list, /mcp add ..., /mcp remove ..., /mcp login <name> and /mcp logout <name> -- because the slash form hands the rest of the line to the same command it runs in a shell; no verb is shell-only. Quote any path or header value that contains a space. typed mcp with no action is typed mcp list, and rm is accepted for remove.

Once a server is connected, its tools appear to the model as mcp__<server>__<tool>. That name is what your permission rules match: mcp__github__* in settings.json allows (or asks about, or denies) every tool one server offers, and mcp__github__create_issue names one tool. See the permission gates in the CLI reference for the rule syntax.

Each session says what it connected. A successful server prints (mcp: <name> -- N tools) in the startup notes, a failed one prints ! mcp <name> failed: <reason>, and (connecting to N MCP servers...) appears on stderr while the handshakes run. Nothing is spawned silently.

Where typed reads servers from

typed reads the same mcpServers maps the reference CLI reads, plus one file of its own. Every file has the same shape:

{
  "mcpServers": {
    "<name>": { "command": "npx", "args": ["-y", "<package>"] }
  }
}

When two sources define a server of the same name, the one later in this list wins. <config dir> is CLAUDE_CONFIG_DIR, or ~/.claude when that is unset.

  1. ~/.mcp.json
  2. <config dir>/.mcp.json
  3. top-level mcpServers in ~/.claude.json, then in <config dir>/.claude.json (the reference CLI's user scope)
  4. ~/.config/typed/mcp.json -- typed's own user-scope file
  5. <project>/.mcp.json (project scope, shipped with the repository, trust-gated -- see below)
  6. projects["<project>"].mcpServers in ~/.claude.json, then in <config dir>/.claude.json (the reference CLI's local scope: private to you, bound to this directory)

So local scope beats project scope, and project scope beats user scope, as in the reference CLI. The projects key is matched with / and \ treated alike, and case-insensitively on Windows. typed mcp list prints every entry these files define with its scope, before the trust gate and without the Yaw MCP preload, so what it lists is the configuration rather than what one particular session connected.

A listing prints each entry's command, arguments or url as written, with any ${VAR} left unexpanded -- a url like https://host/mcp?key=${API_KEY} shows that template, not the key it expands to. Each remote server's line ends with how it authenticates, and never with a credential:

mcp: 3 server(s) configured
  docs [user]  npx -y docs-mcp
  issues [user]  http https://mcp.example.com/mcp  auth: oauth (logged in; token expires in 58 min)
  metrics [project]  sse https://metrics.example.com/sse  auth: header

auth: none means no credential is configured for that server at all.

What typed mcp add writes, and where. Without --project, a new server goes to ~/.config/typed/mcp.json, and so does an update to an existing user-scope server, wherever the older copy lived. That file is typed's own because every shared location is wrong for a durable entry: <config dir> can be a temporary overlay that disappears when a terminal pane closes, the reference CLI reads ~/.mcp.json as a project file rather than a user one, and a same-named entry in .claude.json would otherwise outrank yours. Sitting above both .claude.json user maps and below the project file, ~/.config/typed/mcp.json is where a user-scope server you add is guaranteed to load unless a project or local entry deliberately overrides it. With --project, the server is written to <project>/.mcp.json instead, the file you commit for teammates.

When an add would be shadowed by a higher-precedence entry of the same name, the command says so and names the command that removes the other copy; it never reports a success that will not load.

What typed mcp remove edits. Without --project it removes the name from the first of typed's user-scope files that holds it: ~/.config/typed/mcp.json, then <config dir>/.mcp.json, then ~/.mcp.json. With --project it edits <project>/.mcp.json. If the name is not in any file the command manages but is still configured somewhere else, it tells you where to look instead of reporting "no such server"; and if another copy survives the removal, it names that copy's scope so the server does not quietly keep loading next session.

Both commands refuse to touch a file they cannot parse. A malformed file is reported by path for you to fix by hand, never reset -- it may hold servers you wrote yourself.

Transports

typed speaks all three transports the reference CLI does. The type field selects one; a missing type means stdio.

stdio -- typed starts the command and talks to it over its stdin and stdout. command is required; args and env are optional.

{
  "mcpServers": {
    "local-tools": {
      "command": "node",
      "args": ["/home/me/tools/mcp-server.mjs"],
      "env": { "TOOLS_ROOT": "/home/me/tools" }
    }
  }
}

typed mcp add <name> -- <command> [args...] writes command and args. It has no flag for env; to pass environment variables to a stdio server, add the env map to the entry by hand.

http -- Streamable HTTP, the current remote transport. url is required; headers is optional.

{
  "mcpServers": {
    "remote": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${MCP_EXAMPLE_TOKEN}" }
    }
  }
}

sse -- the legacy HTTP+SSE transport, with the same url and headers fields as http and "type": "sse".

From the command line, --transport http or --transport sse selects the remote form, and each --header "Key: Value" adds one header (the value is split at the first colon; repeat the flag for more headers). --header is refused on a stdio server rather than silently ignored, and a command after -- is refused on a remote one, so a flag can never be accepted and then do nothing.

typed mcp add remote --transport http https://mcp.example.com/mcp \
  --header 'Authorization: Bearer ${MCP_EXAMPLE_TOKEN}'

Variables. ${VAR} and ${VAR:-default} expand from the environment in every string value: command, each of args, each env value, url and each header.

  • ${VAR} takes the variable's value. A variable that is unset or empty with no default is an error: the server is skipped with a ! startup warning naming the server, the field (args[1], env.API_KEY, headers.Authorization, url, command) and the variable. Nothing is ever sent half-blank -- an unset token used to ship a header as Bearer with nothing after it, and the only symptom was the server's 401.
  • ${VAR:-default} uses the default when the variable is unset or empty, so ${TOKEN:-} is how you say an empty value is fine here.
  • $${ is a literal ${, the one escape: $${HOME} is the literal text ${HOME}. A lone $ is literal.
  • Nesting is not supported. ${A_${B}}, an unterminated ${, and a ${A:-${B}} default are each skipped with a warning that says so.
  • Windows %VAR% is never expanded by typed. For a .cmd or .bat server, cmd.exe may still expand one itself when it starts the shim.
  • A warning names the variable, never the expanded value.

A url containing ${ is stored as written by typed mcp add, since it can only be validated after expansion -- but a malformed template is refused at that point rather than written and skipped every session after, and typed mcp add also names any variable the entry needs that is not set in your current environment.

cwd in an entry is not supported and is ignored with a warning: a stdio server starts in the session's working directory. Wrap the command in a script that changes directory if it needs another.

Entries typed cannot use are skipped with a ! startup warning that names the server and the reason -- an unknown type (the message lists the three valid ones), a missing command or url, a field of the wrong shape -- as is a file that exists but cannot be read or parsed. Headless runs write these warnings to stderr.

Authorizing a remote server

A remote (http or sse) server that wants a credential can be given one two ways.

A static token, as a header. The form above: --header 'Authorization: Bearer ${MCP_EXAMPLE_TOKEN}'. The header is stored with the template intact and expanded from your environment at load time, so the token itself never goes into a config file.

OAuth, with typed mcp login <name>. Use it when the server answers 401 with an OAuth challenge rather than accepting a token you can paste. It runs the MCP authorization flow -- OAuth 2.1 authorization code with PKCE -- end to end: it reads the server's challenge for its protected-resource metadata (falling back to /.well-known/oauth-protected-resource), reads the authorization server's metadata, registers typed as a client if the server supports dynamic registration, opens your browser, and receives the redirect on a http://127.0.0.1:<random port>/callback listener with a state check.

typed mcp login issues                 # opens your browser
typed mcp login issues --no-browser    # just print the URL
typed mcp login issues --client-id <id>  # a client the server's operator registered for you
typed mcp logout issues                # delete the stored tokens

Tokens are stored at ~/.config/typed/mcp-auth/<server>.json, one file per server, mode 0600 on macOS and Linux. The file is named after the server, never after a token. typed attaches the bearer when a session connects and refreshes it silently once it expires; a server that rejects a token it issued is retried once with a refreshed one. typed mcp logout deletes the file and leaves the server configured.

A stored login is tied to the url it was issued for. If you later point the entry somewhere else, the old token is not sent to the new host -- typed mcp list shows auth: none with the reason, and the next connect says the same. Log in again to re-authorize.

Only typed mcp login ever opens a browser. A session that meets a 401 or 403 does not start a sign-in -- an interactive session would interrupt your work, and a headless (typed -p) or typed acp run has no browser to show. It fails that one server with a line naming the server, the status, the challenge, which credential was tried and the fix, and the rest of the session continues.

A configured Authorization header wins over a stored login, and typed mcp login refuses while one is configured, so the two can never quietly disagree about which credential is in use.

Trust: servers a project ships

A project's .mcp.json travels with the repository and can start any command, so typed reads it only in a project you have trusted.

In the interactive session, the first time you launch in a project that ships any of this configuration (a .mcp.json, a CLAUDE.md or AGENTS.md, a .claude/ directory), typed shows what it found -- including mcp servers: N -- and asks Trust this project's instructions? [y/n, Enter = not now]. An explicit yes or no is saved to ~/.config/typed/trust.json (delete the entry to be asked again); a bare Enter runs this session untrusted and asks again next launch, so a reflexive keypress never silently disables a project's servers for good.

In a headless run (typed -p) and under typed acp, typed never prompts. The project file is read only with a saved decision trusting the project, or with TYPED_CLI_TRUST_PROJECT=1 set.

An untrusted project's .mcp.json is not read at all, so its servers can neither start nor displace a same-named server of your own. The .claude.json scopes and ~/.config/typed/mcp.json are your own files and are never trust-gated.

Servers that live in .claude.json

Entries in .claude.json -- the file the reference CLI's claude mcp add writes, and where Yaw Terminal registers its bundled Yaw MCP -- are read but not written by typed. typed mcp list shows them; typed mcp add and typed mcp remove do not edit them. To change one, use the reference CLI (claude mcp remove <name>, with -s local for a local-scope entry), or add a same-named server with typed mcp add, which lands in ~/.config/typed/mcp.json and outranks the user-scope copy. A local-scope .claude.json entry still outranks that, as it would in the reference CLI, and typed mcp add tells you when that is the case.

Yaw MCP: the recommended broker

Every server you configure adds its tools to every request, and a session that needs eight services does not want all of them connected from the first prompt. Yaw MCP is our answer to that: one stdio server that knows many others, and loads them on demand. It registers a small set of mcp_connect_* tools -- mcp_connect_discover lists the servers it has, mcp_connect_activate and mcp_connect_dispatch load one for the current task, mcp_connect_deactivate unloads it, and mcp_connect_exec, mcp_connect_find_tool and mcp_connect_read_tool call a loaded tool by name -- so the model picks up a service when it needs one and puts it down afterwards. It also keeps the credentials those servers need in a local vault, referenced from their configuration as ${secret:NAME} and listed by name (never by value) through mcp_connect_secrets, so a token does not have to live in a config file. If that is the shape you want, install it and read its own documentation at yaw.sh/mcp.

It is optional. typed treats it as one more MCP server, and the rest of this page applies to it unchanged. What typed does add is that it looks for it:

  • Preload. When no configured server already runs Yaw MCP, typed preloads it from whatever is installed and says so in a startup note: the yaw-mcp bin on PATH, or else, when the ~/.yaw-mcp state directory exists, npx -y @yawlabs/mcp@latest. A configured server counts as Yaw MCP when its command or arguments name the @yawlabs/mcp package or a yaw-mcp bin, or when it is named mcp or yaw; a match on the name alone also suppresses the preload, with a note naming the server when a preload was otherwise available. TYPED_CLI_NO_YAW_MCP=1 turns the preload off.
  • Its name is mcp. The preload registers as server mcp, so its tools are mcp__mcp__<tool>, and the mcp__mcp__* permission rule Yaw MCP's own installer writes covers the preloaded tools too. typed CLI 1.5.0 and earlier registered it as yaw; a rule written for mcp__yaw__... no longer matches anything, so rewrite it as mcp__mcp__....
  • Stale installs update themselves. A yaw-mcp bin on PATH older than 0.75.0, the first release that starts in gateway mode, is still preloaded, with a note, and typed runs npm i -g @yawlabs/mcp@latest in the background so the next session starts the updated bin; the current session keeps the old one. TYPED_NO_YAW_AUTO_UPDATE=1, or the broader TYPED_NO_UPDATE_CHECK=1, stops that update.
  • Installing it for typed. yaw-mcp install typed writes Yaw MCP's entry, named mcp, into ~/.config/typed/mcp.json, where it outranks the mcp entry Yaw Terminal keeps in ~/.claude.json.
  • Curated servers. typed mcp add --yaw <name> adds one of the individual Yaw servers -- npm, lemonsqueezy or context7 -- as an ordinary npx -y @yawlabs/<name>-mcp stdio entry, without the package name to remember. These are plain servers, not the broker, and nothing here loads unless you add it.
  • Tools that arrive mid-session. A server can change its tool list mid-session and say so with notifications/tools/list_changed, which is what Yaw MCP does when it loads a service. typed then re-lists that server before the next request, through the same --tools allowlist and per-server cap startup used, and the model sees the change from its next turn; a real change is announced in one line, and a call to a tool the server has withdrawn is refused. TYPED_CLI_MCP_LIST_CHANGED=off keeps every server's startup tools for the whole session, which is right for a reproducible test and otherwise a regression -- a server that adds tools in the middle of a long task is the norm.

Tool budgets: what happens with many tools

Every tool a session registers -- name, description and JSON schema -- is sent with every request. On the hosted tiers (typed++ and typed--) a large MCP surface is a cost; on the free local tier, with a window of a few tens of thousands of tokens, it can be the whole window. typed measures this before the first prompt and tells you.

The startup preflight estimates the static prefix (tools plus system prompt) against the context window. Above half the window it prints one line saying so and carries on. Above ninety percent it prints the breakdown -- how many tokens each MCP server's tools cost, how many the built-ins cost, how many the system prompt costs -- followed by the fixes, cheapest first: drop the largest server (the note names it and where to remove it), cap it with TYPED_CLI_MAX_MCP_TOOLS=<n>, narrow --tools to register only what you need, shorten CLAUDE.md, or move to a tier with a larger window. A headless run exits on that verdict; the interactive session stays up, since the figure is an estimate that errs high and a session just past the line can still answer a short prompt, and shows the fix list again with your first submission. Every figure is labelled an estimate, since the model's own tokenizer is not available at startup.

TYPED_CLI_MAX_MCP_TOOLS=<n> caps the tools registered per server on any tier; 0 means no cap, which is the hosted default. A cap keeps a server's tools in the order the server advertised them -- not smallest-first, which was measured to drop exactly the gateway tools that make the rest of a broker reachable -- and, for a Yaw MCP gateway, keeps mcp_connect_exec, mcp_connect_find_tool and mcp_connect_read_tool first so a capped broker can still route to what it loads. Whatever a cap drops is listed in a ! startup note; a silently truncated tool set would make the model fail to call something it was never told about.

The free local tier connects no MCP servers at all, and preloads no Yaw MCP, unless TYPED_CLI_LOCAL_MCP=1 is set; when servers are configured, a startup note says how many it skipped, and /mcp inside the session reminds you that its entries apply from the next launch. With the opt-in set, each server is capped at 3 tools unless TYPED_CLI_MAX_MCP_TOOLS says otherwise, and the cap is announced.

--tools interacts with all of this: a --tools list that cannot match any mcp__ tool skips the server spawns entirely, with a note, since nothing they offer could be registered.

Headless and ACP

typed -p reads the same files and applies the same precedence. It never prompts, so a project's .mcp.json needs TYPED_CLI_TRUST_PROJECT=1 or a saved trust decision, and every ! warning goes to stderr, leaving stdout for the result.

typed acp loads MCP from typed's own configuration as well, and it also connects the mcpServers an editor sends with session/new -- in the same batch as your configured ones, under the same connect guard, tool budget and mid-session re-listing. An editor's entries are the setting of a program you chose rather than something a repository ships, so they carry user scope and the project trust gate does not apply to them, and their values are passed on as the editor wrote them: ${VAR} expansion is a contract of the config files above, and the editor has already resolved its own settings. They are session-only -- nothing is written to a file, typed mcp list does not show them, and they are gone when the process exits. An editor's server wins a name collision with a configured server of the same name, and the one it replaced is named on the diagnostic channel. One entry typed cannot read fails the whole session/new, naming the entry and the shapes it accepts, rather than being dropped in silence; and because the list is connected once per process, a later session/new naming a different one is refused with the difference rather than quietly left on the first.

Troubleshooting

mcp <name> connect timed out after 30000ms. MCP_TIMEOUT is the startup budget, in milliseconds, for spawning, initialising and listing one server; the default is 30 seconds. A server launched through npx gets 90 seconds when MCP_TIMEOUT is unset, because resolving the package can take that long on a cold cache. Raise it for a server that is slow to start; an invalid value is reported and the default used. It is a startup budget only -- lowering it to fail a hung server fast never shortens a tool call, which has its own deadline.

MCP request tools/call ... timed out. Running a tool is a separate wait from starting a server, and MCP_TOOL_TIMEOUT is its own knob, in milliseconds. The default is two minutes, chosen so that the slowest thing a gateway server routinely does before a call can start -- loading the upstream server that owns the tool, on first use -- finishes comfortably inside it; a raised MCP_TIMEOUT raises that default with it, so widening the startup budget for a slow server never leaves its tool calls on the shorter wait. Raise it for a tool that is legitimately slower than that; lower it to give up on a wedged tool sooner. Whatever you set is what applies, in both directions, and an invalid value is reported rather than quietly ignored.

A server that reports progress keeps its call alive: typed asks every tool call for progress updates, and each one it receives restarts the deadline with a fresh window, so a tool that is visibly working is never cut off for taking a while. That extension is bounded -- ten times the deadline, and at most ten minutes -- so a server that reports progress forever without ever finishing still ends. The failure message says which of the three happened: the server reported nothing, it reported and then went quiet, or it kept reporting past the limit. Tools that report no progress at all, which includes running a tool through Yaw MCP's mcp_connect_exec, simply get the full deadline.

failed to start: spawn npx ENOENT on Windows. typed resolves npx, node and other commands through PATH and PATHEXT itself, so the standard "command": "npx" shape works. If a command still cannot be found, give it as an absolute path.

sent an oversized line. A stdio server must write one JSON-RPC message per line. A server that streams without a newline, or writes a single message over 16 MB, is disconnected rather than buffered without limit; the fix is on the server side.

The server started but its tools are missing. Check --tools (a list without an mcp__ match skips MCP entirely), the per-server cap note, and on the free local tier TYPED_CLI_LOCAL_MCP. /mcp list in the session, or typed mcp list in a shell, shows what is configured and from which scope; the startup notes show what this session connected.

A server I removed keeps loading. It has another copy in a higher-precedence source. typed mcp remove names the surviving scope; typed mcp list shows every entry. A copy in .claude.json is removed with the reference CLI.

A remote server answers 401 or 403. The failure line says which credential was tried and what to do: run typed mcp login <name> when none was configured, check the token when a header was rejected, or log in again when a stored token was. If the server was skipped before it was ever contacted, the startup warning names the ${VAR} in its url or a header that is unset -- set it, or write ${VAR:-} if an empty value is really intended.

The full list of environment variables, the permission rule syntax and the rest of the CLI are in the CLI reference.