Connectors for your AI apps: add them from a gallery, keep Claude and Codex in step, see what's broken and sign in.
paseo plugin add itsjustanks/paseo-mcp --ref 9ae26e191751975fa88ac41f43ba497acdfa7be9Git security status and fallback installation are pinned to commit 9ae26e191751.
From the plugin's README
Requires Paseo 0.8 or newer (0.1.0 is the last release for Paseo 0.7):
paseo plugin add itsjustanks/paseo-mcp
Updates are handled by Paseo:
paseo plugin update paseo-mcp
README
Manage MCP servers, project connections, credentials, and OAuth from Paseo.
In the app it is called Connectors (connectors are MCP servers): the sidebar row, its page, and the workspace and agent panels. Since 0.19.0 everything you read says "connector" where it said "server".
Requires Paseo 0.8 or newer (0.1.0 is the last release for Paseo 0.7):
paseo plugin add itsjustanks/paseo-mcp
Updates are handled by Paseo:
paseo plugin update paseo-mcp
.mcp.json files..mcp.json servers in a Connectors tab, available in both the workspace view and the Projects/Explorer view./mcp view does. A chip appears on a chat's composer only when a connector that chat loads is broken or needs sign-in (2 broken, 1 needs sign-in), and only on a running chat or one active in the last hour; a calm chat has none. Everything else that needs you shows as the sidebar row's dot, which opens a quick popover./connectors in a composer opens that panel.mcp__paseo__* tools the daemon adds to agents): for the whole host, per provider, and per tool.Servers → Add server opens a gallery. Search it, filter by category, and press Add on a card; Add by hand keeps the old form. Each card says who publishes it, whether it is Official, Library, Team or Community, whether it is on the Web or On this computer, whether you Sign in with your account, it Needs a key, or has No sign-in, and which library it came from.
| Shelf | Where it comes from |
|---|---|
| Recommended | 31 official servers shipped with the plugin (shared/catalog-curated.ts), each URL copied from the vendor's docs, which the card links to. Always shown; a library never hides one (see Merging) |
| Libraries | Every library in Libraries at the bottom of the gallery, most trusted first (see Merging). By default that is the MCP Gallery (https://raw.githubusercontent.com/itsjustanks/mcp-gallery/main/v0.2/servers.json; a saved v0.1 address, the default before 0.16.0, is read as v0.2). Add your own by address or file; the 0.12.0 team catalogue is now the library called Team |
| Registries | A library whose address is a registry, searched once you type two letters, latest versions only, cached a day per search, up to three pages of 100. The official MCP Registry (registry.modelcontextprotocol.io) is listed but off; switch it on in Libraries. Only remote servers are added in one click. A server that only ships a package (npm, PyPI, an image) is shown, marked "Runs code on this server", with its Repository and Add by hand (the form opens with its name filled in, nothing else) |
A library is JSON in the official MCP Registry's list shape: { "servers": [ { "server": <server.json>, "_meta": { … } } ], "metadata": { "count": N } }, each server a server.json
(name, description, version, remotes or packages). The gallery's curation is optional, in
_meta["io.github.itsjustanks/mcp-gallery"]: { "id", "displayName", "category", "publisher", "iconUrl", "auth": "oauth" | "token" | "none", "docsUrl", "verifiedAt" }. Without it the card uses title, websiteUrl, repository and
icons from server.json; a field that doesn't fit (an http icon, one over 2048 characters) is ignored on its own. The
category is one of the gallery filter's names (developer, data, docs, productivity, design, crm, support,
marketing, analytics, payments, automation); the older words map to the closest (database → data,
documentation → docs, observability and ai → developer, websites → design) and anything else is Other. A 0.12.0
team catalogue file (a list of entries) reads as a library too, its text cleaned of hidden characters like any other.
{
"servers": [
{
"server": {
"name": "com.acme/mcp",
"description": "Acme orders and invoices.",
"version": "2.1.0",
"remotes": [{
"type": "streamable-http",
"url": "https://mcp.acme.com/mcp",
"headers": [{ "name": "Authorization", "value": "Bearer {ACME_TOKEN}", "isRequired": true, "isSecret": true,
"variables": { "ACME_TOKEN": { "description": "Acme API token", "isRequired": true, "isSecret": true } } }]
}]
},
"_meta": { "io.github.itsjustanks/mcp-gallery": { "displayName": "Acme", "category": "payments", "auth": "token" } }
}
],
"metadata": { "count": 1 }
}
.json is read as one document; any other address is a
registry, searched through GET {address}/v0.1/servers?search=… (Read as picks either by hand). A path on the
host (absolute, or starting with ~/) is a private library that is never hosted. Plain http is read only from this
machine (localhost, 127.0.0.1). An address with a user name, a password or a key in its query is refused; a
private address takes a header name, and its value is set in the panel, write-only, kept in
$PASEO_HOME/plugin-settings/paseo-mcp/team-auth.json (0600), bound to the site of the address it was set for and
never sent back to the app. It goes only to that site; if the address moves to another site, the value is deleted.?via=…, an extra header, another version) is its own
Library card beside the Recommended one ("whoever can change that library can change this entry"). A library
card is never Official by itself.${…}, no control
characters, exact package versions (npx name@1.2.3, uvx name==1.2.3), no runner flags or runtime variables. One
that fails is refused, listed by name with the reason, and the rest still show. A package the plugin doesn't start
(an image, mcpb, NuGet) is shown with Add by hand. Header, env and argument values are {PLACEHOLDER} inputs
asked for when you add the server, and secret unless their name is on the short public list; a header may be a
template such as Bearer {token}. A library, and the plugin's cache of it, never holds a stored value. A document is
capped at 1 MB and 500 servers. A server that would not fit a card (an address over 2048 characters, more than 64
arguments, an argument over 1024) is refused with that reason, listed on the library's row.catalog.json and would show no team catalogue.
Paseo rewrites that file in place when it migrates, without a backup, so the plugin keeps the version 1 document
beside it as catalog.v1.json first. Before downgrading, copy it back:
cp $PASEO_HOME/plugin-settings/paseo-mcp/catalog.v1.json $PASEO_HOME/plugin-settings/paseo-mcp/catalog.json.
The Team key needs nothing; team-auth.json also keeps it in 0.12.0's shape.Anyone can publish to the registry. Official is kept for the Recommended shelf and for a registry entry whose every
address is a recommended one, since those addresses were checked against the vendor's own docs. Every other registry
entry is Community. Its card shows the namespace as plain text ("published as supabase.com", "published as
github.com/") with one line: the registry checks that the publisher owns that domain or GitHub account, not who
runs the service. com.supabase/mcp at mcp.supabase.com/mcp is Official; the same name at any other address is
Community. Community cards say what that costs:
"A third party relays your traffic and any key you give it", or "Runs code on this server" for a package. A registry
server that lists no headers reads "Sign-in unclear": the registry can't tell OAuth from no sign-in, so the card says to use
Connect OAuth if the server asks once it is added.
Adding asks where: My editors (the same editor configs as Apply) or One project (its .mcp.json). It shows the
exact change per file with secrets masked, and writes nothing until you press Add. A server that runs locally shows the
exact command line on its own line first (npx @playwright/mcp@latest). The install is bound to that preview: if the
entry or the change moved since you read it (a team file edited in between), nothing is written and the sheet asks you to
review again. Writes use the same writers as Import
(backup, atomic write, read back). A name that is already taken is never replaced; the sheet offers the next free name or
Skip. Afterwards it runs a health check, offers Open server to connect for an OAuth server, and says what the server
adds to the context budget.
A card you already have is hidden (0.15.0): the gallery shows what you don't have yet, and the line
under the search says how many are hidden ("8 you already have are hidden."). You have it when one of your
servers is at the same endpoint, is on the same host whatever the path (anything on mcp.zapier.com,
where the path carries a personal token; not a host many servers share, like vercel.app or localhost),
runs the same package (npm, PyPI, OCI; any version; not a runner like mcp-remote), or is named exactly
after the card's id or name, ignoring case, an -mcp ending and a prefix two or more of your servers
share (ikit-notion is Notion when you also have ikit-linear; old-github isn't GitHub). Show ones I already have brings them back, each saying "You have a Zapier server already,
called automations." A card at the same endpoint says Added (matched on the address or the package
and its ecosystem, not the name, so an npm package never counts as a PyPI one of the same name), and its
button reads Add to more and starts with only the missing places picked.
A project's .mcp.json is usually in git, so a key never goes into it. Claude Code expands ${VAR} in .mcp.json
(in command, args, env, url and headers), so a secret becomes a ${VAR} reference and the sheet names the
variable to set where Claude Code starts (for Paseo agents, the daemon's environment or the provider's env in Paseo's
settings); if one is already set on this host, the sheet says so (by name, never the value). The plugin names every
variable itself, so an entry can't choose which of your variables is sent to its server: MCP_<ID>_<INPUT> for a
recommended entry (for example MCP_HEROUI_PRO_HEROUI_PERSONAL_TOKEN), and MCP_TEAM__<ID>__<HASH6>__<INPUT> or
MCP_REG__… for a team or registry entry, where HASH6 comes from the SHA-256 of its address or package, so it can't reuse
a recommended entry's variable. A name Claude Code uses itself (MCP_CLIENT_SECRET, MCP_TIMEOUT …) is refused.
Inputs are secret by default: only one named on a short list (region, project, workspace, org, team, site, host, port,
database, schema, env, locale, timezone, mode) is written as typed, and not even then when it sits in a header or env
line named otherwise, anywhere in a team or registry entry's address or arguments, or when the value looks like a
credential (postgres://user:pass@…, sk-…, ghp_…, a Slack webhook). Codex does not read .mcp.json, and Add project servers to agents passes the reference on unexpanded.
The 0.12.0 team file shape still reads: a list of entries, or { "entries": [ … ] }:
[
{
"id": "team-n8n",
"name": "Team n8n",
"publisher": "Your team",
"description": "Our workflows as tools.",
"category": "automation",
"transport": "http",
"url": "https://n8n.example.com/mcp/team",
"headers": { "Authorization": "Bearer {N8N_TOKEN}" },
"inputs": [{ "id": "N8N_TOKEN", "label": "n8n token", "secret": true }],
"auth": "header",
"docs": "https://wiki.example.com/n8n-mcp"
}
]
transport is http (with url and headers) or stdio (with command, args and env); auth is oauth,
header, env or none; values the user fills in are {INPUT} placeholders declared in inputs (secret and required
unless they say otherwise). Addresses must be https. An entry holding a literal key, a password in its URL, a ${…}
reference, an undeclared placeholder, a control character, or a header name with a . is refused and listed with the
reason. A stdio entry that runs a package through npx, bunx, pnpx, npm exec, uvx or pipx run must name it
plainly with an exact version (acme-mcp@1.2.3, mcp-server-fetch==1.0.0): not latest, a range, *, an address or
no version, and no runner flag that picks another source or runs a shell command (-p, --registry, --index-url,
--with, -c, --call). Its env may not set what changes how the runner starts: NODE_OPTIONS, NPM_CONFIG_*,
UV_*, PIP_*, PYTHON*, PATH, LD_*, DYLD_*, HOME, SHELL, or a name the plugin reserves (AWS_*,
ANTHROPIC_*, HTTPS_PROXY …). Names, publishers, descriptions and labels are shown without bidi controls, zero-width
or control characters. An envVar on an input is ignored. The file must be under 1 MB. Copy as catalogue entry on any server card writes that server in this shape, templated so no
stored value is in it: of an address only the scheme and host stay (a host's first label becomes {HOST_PREFIX} unless
it is a plain word like mcp; a path that is not plain lowercase words becomes {PATH}; query keys become PARAM_1…n
with their values as inputs); every header and env value is a placeholder; of a command, only the command, its package
and flag names stay.
Some official servers can't be added in one click. A library entry says so in _meta["io.github.itsjustanks/mcp-gallery"].setup
(a recommended or team entry in a setup field), and the card shows a badge, the reason in one line, and How to set
it up (the vendor's own page):
kind |
Badge | What the gallery does | Examples in the MCP Gallery |
|---|---|---|---|
byo-oauth |
Needs setup | Set up opens a guided sheet: the vendor's steps with their links, the exact redirect address to register, the scopes to add, then the client ID and secret you copy back | Gmail, Google Calendar, Drive, Docs, Sheets, HubSpot, Zoom |
per-org |
Needs your address | Set up asks for your subdomain or org and fills the address in | Zendesk |
approved-clients |
Approved apps only | Only the vendor's approved AI apps can sign in. With clients, the approved apps the plugin writes to get it and the rest are skipped with why; without, it is shown, not added |
Dropbox (Claude Code, Codex), Slack, Canva |
admin |
Admin setup | Shown, not added: an administrator turns it on first | Box, Salesforce, Microsoft 365 |
Fields: kind, reason (one plain line, 200 characters at most), guideUrl (https), and by kind steps (up to 10
short lines; an https address in one is shown as a link), redirectHint (the vendor's name for the redirect field),
clients (claude, codex), scopes (space-separated), urlTemplate and label. It is untrusted text like the rest
of a library: cleaned of hidden characters, capped, https links only, and a field its kind doesn't use is an error. A
setup that doesn't check out makes the card "not installable" with the reason; it is never dropped, which would show a
gated server as one click. The rules are in shared/setup.ts.
Bring your own sign-in app (byo-oauth). The vendor accepts only an OAuth client you register (no dynamic
registration). The sheet shows http://localhost:33418/callback, the address Claude Code listens on when an entry's
oauth.callbackPort is 33418 ("Use pre-configured OAuth credentials", code.claude.com/docs/en/mcp); register exactly
that. One port for every app, so one Google client covers all five Google servers. For each Claude account you pick,
the entry is { "type": "http", "url": …, "oauth": { "clientId": …, "callbackPort": 33418, "scopes": … } }. The
client secret is never written to a file: Claude Code keeps it in its secure store (the macOS Keychain, or
~/.claude/.credentials.json elsewhere), and only its own claude mcp add / add-json --client-secret puts it there.
So for this one server the plugin backs the file up, refuses a name that's already there, runs claude mcp add-json --scope user --client-secret -- <name> <json> for that account (CLAUDE_CONFIG_DIR set to the folder of the file it
writes, unset for ~/.claude.json) with the secret in the child's environment as MCP_CLIENT_SECRET (never in its
arguments, never logged, never in an answer), then reads the file back and compares it with the plan. If Claude Code
fails, hangs (stopped after 30 s, killed 5 s later) or can't store the secret, the entry it had already written is taken
out again, so nothing is left that can't sign in. The preview is worked out without the secret (it only learns that one
was typed); the secret crosses to the daemon once, when you press Add.
Only vendors the plugin knows. Claude Code sends the client secret to whichever sign-in server the MCP server names,
so a library entry called "Gmail" at another address could collect a real one. byo-oauth is honoured only for a
vendor in the plugin's own BYO_OAUTH_VENDORS list (shared/setup.ts; never read from a library), at that vendor's
own MCP host: Google (<name>mcp.googleapis.com), HubSpot (mcp.hubspot.com) and Zoom (mcp.zoom.us). The guide and
every link in the steps must be https on that vendor's own sites. Anything else blocks the card, and the sheet names
the exact host next to the secret box. A new vendor takes a plugin release. It is user level only: the secret can't go in a project's file. Codex is skipped with the reason: its
[mcp_servers.<name>.oauth] table takes client_id, callback_url and callback_port but no secret
(developers.openai.com/codex/mcp), and these vendors' token endpoints require one. Kimi and Grok have no such setting.
After adding, sign in with Connect as for any OAuth server. To change the secret, remove the server and add it again.
Your own address (per-org). urlTemplate holds one {subdomain} or {org}, as the whole first label of the
vendor's host (https://{subdomain}.zendesk.com/api/mcp) or one whole path segment, and must be the server's own
address. What you type must be lowercase letters, digits and - (1 to 63, not starting or ending with -, not
something that reads like a key); the address it makes is checked again, and a host that isn't the template's is
refused. The value is written as typed at both levels (it names your organisation; it is not a secret), and a value
that fails is never shown in the preview's address.
Off by default. Turn it on under Settings → Plugins → Paseo MCP → Project servers, or run the
Add project servers to agents command. This is the plugin's own setting and is mainly for Codex,
which does not read .mcp.json. It is not Paseo's Enable Paseo tools (mcp.injectIntoAgents),
which adds Paseo's own mcp__paseo__* tools; that one is on the Paseo tools card.
| Setting | Default | Meaning |
|---|---|---|
| Add project servers to agents | off | Add the workspace's .mcp.json servers to every new agent |
| Providers | Codex | Codex, Claude Code, or both |
| Skip inline-credential servers | on | Leave out entries carrying tokens in env, headers, args, or the URL |
When an agent is created for a chosen provider, the plugin reads .mcp.json from the agent's
working directory (or its git root) and adds each server to the agent's MCP configuration. Servers
the agent already defines are kept as-is. Nothing is written to any config file, and a failure to
read leaves the agent unchanged. The daemon log shows injected N servers into <provider> agent.
Each agent also gets an MCP tab (and the MCP for this agent command) listing the workspace's project servers, with a line showing the agent's provider and whether project servers are added for it.
Settings live on the host at $PASEO_HOME/plugin-settings/paseo-mcp/injection.json
(~/.paseo by default; the file and its keys kept their 0.10 names). The hook reads that file
directly; if it is missing or invalid, the setting stays off.
Four tabs: Overview, Connectors, Projects and Help (0.19.0). The words are for people who have never heard of MCP: plain English, "connector" for what used to be called a server, and any term explained where it appears.
0.19.0 made it calmer at the user's request ("it just needs to work for dummies"):
Accordion / AccordionItem in client/ui.tsx.Since 0.17.0 it looks and reads like AI Router, the shared design standard of our Paseo plugins: one
type scale (15 px sentences, nothing below 13; TYPE in client/ui.tsx), a header with the plug icon,
"MCP servers" and the state with a coloured dot, underline tabs with icons (icons only, plus the active
label, when the labels don't fit), and on every tab its title, one or two plain sentences and "What you
can do here" (behind Learn more on a phone). The words are in shared/guide.ts, tested for plain
English. On Paseo 0.11 the page is a full screen with the app's own sidebar row; on 0.10 and later links
open in the system browser. Older apps keep the surface and sidebar item they had, decided at runtime
(shared/host-features.ts). requirements.paseo is >=0.9.0 since 0.19.2: Paseo accepts the manifest's description only from 0.9.0.
Copies go through the app's own clipboard (copyText, clipboardCopier in shared/host-features.ts;
0.20.0); React Native's deprecated Clipboard serves only an app without it, and a refused copy says
"Couldn't copy". Toasts come from one hook (useToast in client/ui.tsx): the app's, or inline lines on
an app without one. Everything a person sees that may quote a provider or a CLI (toasts, inline errors,
failure lists, previews) goes through one redactor, shared/redact.ts; CLI failures are a fixed sentence
plus the CLI's last line, redacted (cliFailure). A Copy button never offers a key: the saved config has
none while keys are shown, and previews are masked by the host, including TOML lines kept as written
(an oauth table's client_secret), for display only. The manifest has no name, icon or media yet: Paseo 0.11.0-beta.5's manifest
schema is strict and rejects them, so they wait until every host runs 0.11.1.
Overview is calm (0.18.0, AI Router 0.15.0's rules): a status card with the state in words
("Fix the server that isn't working", "All set: your servers are working"), three rows (health, AI
apps, sign-in), when health was last checked, and two buttons (the next step, then Browse servers or
Add a server). Which servers need a look, and why, folds behind a quiet link in the card. Below it, one
link, New to MCP servers? How it works, opens a single card with what MCP servers are, how it works
(four steps), how to use it and the words you'll see; it is open until the first server is added. AI
Router is one quiet line. Every other tab opens with its title and one or two sentences, with "What you
can do here" behind a small link. Spacing comes from one scale (SPACE in client/ui.tsx).
Copy to all my AI apps (0.15.0; it replaces Sync accounts) copies every user-level server into
every AI app and account on this host that doesn't have it: Claude → Codex and Kimi, Codex → Claude, and
between two accounts of one app. It opens a preview first: one row per server with where it will go,
which copy is used, "includes its saved key" when it carries one, and a tick box to leave it out; then
Copy N servers. It writes through the same writers as a server's Add to missing, one write and
one backup per file, in each file's own format, and reads every server back. It never removes or
overwrites: who has a server is decided by name, the plan is worked out again when you press Copy, and
the write refuses a name the file has by then ("it already has a different server called jam; left as it
is"). Between apps only the settings every app shares are copied; a switched-off server, an app-only
setting or a ${VAR} the other app wouldn't fill in is skipped with the reason. The result is per server
and per app, so a file that failed shows. Paseo's own server and project .mcp.json servers are left
out, and no sign-in is copied. Project trust isn't touched: Sync accounts copies that, and only where the
other account doesn't have it yet. It is on Overview, in the next step when servers are missing from some
apps, and on the Connectors tab's Missing from some apps filter.
Connectors is a gallery of the connectors you have, in the same style as the Add gallery (0.15.0). Search reads each server's name and description; the pills are All, Needs attention (health), Needs sign-in and Missing from some apps, each with its count. A card shows:
Above the gallery, Paseo's built-in tools ("Paseo's built-in tools · on · 61 tools") and the totals
("6 servers · 1 needs attention · 2 need sign-in") are two closed rows, so the gallery starts near the
top. One Refresh re-reads the settings, health, sign-in and every server's tool list. The logic is in
shared/servers.ts (serverGallery).
Settings files other programs write too (~/.claude.json, a project's .mcp.json, Codex's and Grok's
config.toml) are changed only through readStamped + replaceGuarded (server/safe-write.ts): the
inode, size, mtime and a SHA-256 of the text are recorded at the read; the new text goes to a temporary
file in the same folder; the file is stamped again right before the rename, and any difference deletes
the temporary file and throws a retryable ConcurrentChangeError ("Claude Code changed its settings file
while saving. Nothing was changed; try again."). The rename keeps the file's mode; the read-back must
equal the bytes written, and if it doesn't nothing more is written (no rollback) and the user is told
where the backup is. Temporary files are created 0600 and removed on any failure; the stamp hashes the
file's bytes, and a file that isn't valid UTF-8 is refused. Reveal to edit carries the connector's
version (entryVersionFromText); Save is refused if it changed since. The plugin's own files (settings copies, caches, saved keys)
and exports use the plain atomic writer.
Every surface uses the same plain labels (shared/scope.ts): Everywhere for a user-level config
(~/.claude.json's top level, ~/.codex/config.toml, the other apps' own files), This project ·
for a project's .mcp.json (and what the injection hook adds from it), and This project, just for you
for Claude Code's local scope (projects[<dir>].mcpServers in ~/.claude.json), shown read only as
"Claude Code · just for you": the plugin never writes those entries, and Remove gives the
claude mcp remove <name> -s local command instead. The workspace and agent
panels group by scope, project first. When a name is set up at several levels, loadFor keeps the
winner (local, then project, then user, Claude Code's documented order) and records what it hides
(shadows), and the row says which copy is used. Rows name sources without paths; Technical details
lists home-relative paths with Copy. Add starts on the scope being browsed (startingScope) and names
the file it writes. Codex's project .codex/config.toml is not read yet.
Each server's page offers three scopes, each a two-step confirm that names the files. Since 0.20.0 the
second step is Paseo's own dialog (Modal), with Cancel, rather than a box further down the page (an app
without Modal keeps it in the page). Each opening is a fresh session (shared/remove-dialog.ts): Cancel
forgets the app picked, and Confirm is single-use:
| Scope | What is written |
|---|---|
| this app | One AI app's config (pick which when the server is in several) |
| all apps | Every AI app config that defines it |
| everywhere | All apps plus every registered project's .mcp.json that defines it |
Only everywhere keeps the server gone: Claude Code reads a project's .mcp.json straight back, so
the other two scopes say which projects still define it. Project files are backed up first, rewritten
atomically, read back and must parse, and an emptied file is left as {"mcpServers":{}} rather than
deleted. The confirm warns that those files are usually version-controlled (the change shows in
git status) and that inline credentials in a deleted definition are lost; export first if in doubt.
The result is reported per target, so a partial failure is visible.
The workspace's Connectors panel and each agent's Connectors panel list the servers an agent there
loads, each tagged with its origin (user-level, this project's .mcp.json, local) and with a switch
where the editor has one. A switch takes effect when a new agent session starts; a running agent keeps
the servers it started with. State is read from the config on every refresh, so a /mcp disable done in
a terminal shows up here.
| Provider | Server origin | Lever |
|---|---|---|
| Claude Code | user-level, local | projects["<workspace dir>"].disabledMcpServers in ~/.claude.json — off for this workspace only, the definition is untouched. Same list /mcp disable writes. |
| Claude Code | this project's .mcp.json |
enabledMcpjsonServers / disabledMcpjsonServers for that directory (the approval lists). "Asks at launch" means neither list names it yet. |
| Codex | this project's .mcp.json (added by Add project servers to agents) |
The plugin's own $PASEO_HOME/plugin-settings/paseo-mcp/workspace-disabled.json; the agent.create hook leaves the server out for that directory. |
| Codex | user-level | No switch. Codex layers config.toml on top of what Paseo passes it, and enabled = false there is global. The row shows its state and says so. |
| Kimi, Grok | any | No switch. |
The project entry is keyed by the workspace's own directory, so a worktree workspace gets its own entry
rather than sharing the project root's. Writing ~/.claude.json is guarded: the file must parse, the new
document may differ only inside that one project entry (checked before the write), the write is a temp
file plus rename, a backup is taken first, and the result is read back and must hold the state just
written. Any failure refuses the write and reports why.
On by default. The host probes every MCP server on a timer: HTTP endpoints get the same JSON-RPC
initialize POST an MCP client opens a session with (configured headers and URL sent intact, so a
token in the query string counts), stdio commands are looked up on the login shell's PATH. The most recent result
is cached on the host, and every panel, pill, and the MCP surface reads that cached verdict, so
opening ten agents never probes your servers ten times. Refresh and Check now still run a
fresh probe and wait for it. Configure it under Settings → Plugins → Paseo MCP → Health checks, or run the
Configure MCP health checks command.
| Setting | Default | Meaning |
|---|---|---|
| Check servers in the background | on | Probe on a timer, not only when Refresh is pressed |
| Interval | 10 minutes | Time between background checks (1 to 1440 minutes) |
| Composer chip | on | Show a Connectors chip on a chat when one of its connectors is failing or needs sign-in |
| Chat notices: sign-in problems | on | Add one card to a chat when an MCP tool call there fails for lack of a sign-in (see In-chat sign-in card) |
Settings live on the host at $PASEO_HOME/plugin-settings/paseo-mcp/health.json; an unreadable or
invalid file means the defaults apply. The daemon log shows health check: N servers, M need attention, K OAuth
after each pass.
Background passes only run while a Paseo app is connected to the daemon (any RPC from the app counts;
the composer chip reads the cached verdict once a minute). With no app connected for 15 minutes the
log says health check: no app connected, pausing until one is and nothing is probed; the first read
after that answers with the last verdict and refreshes it in the background. A pass that fails is
retried after 1, 2, 4 … minutes, never more often than the interval. Probes run eight at a time.
The tool lists follow the same rules on six times the interval.
Opening a panel never waits on a probe. The cached read answers at once: with the last verdict when there is one, or with "checking" on a host that has none yet. When there is no verdict, only a saved one, or one older than the interval (after a pause), a pass starts in the background and the panel reads again every second until the verdict is in. If a read fails meanwhile, the panel backs off (2, 4, 8 … minutes, counted from the last successful read) rather than asking every second.
The last verdicts and tool lists are also kept on disk, so a plugin reload, an update or a daemon
restart does not start from nothing. After a restart the first read shows the saved verdict and tool
lists marked "as of HH:MM (saved before the plugin restarted)" and checks again straight away. The
file is $PASEO_HOME/plugin-data/paseo-mcp/cache.json (the same plugin-data/<id> place Shared
Browser uses), written atomically once per completed pass, 0600, and only while an app is connected.
It holds no URLs, query strings, header values, env values or command arguments: servers are keyed by
name and a hash of their URL, headers and command, and notes are the same redacted ones the panels
show. It does hold what the panels show next to each server: account labels (with the account's
email), config file paths and the stdio command name. Treat it like ~/.claude.json, which holds all
of that and more: don't share it. A tool list last read more than 7 days ago is not restored, and a
server that stops answering keeps its old list for at most 7 days. A corrupt file, or one from another
version, is ignored. Deleting it is safe.
Two calls back into the Paseo daemon sit on the panel's path: its project list and its provider
settings. A busy daemon takes seconds to answer them (paseo-mcp.auth took 10.0 s in a daemon log). Since
0.15.0 both go through one cache (server/daemon-cache.ts): a read answers from the last copy at once
and refreshes it in the background when it is older than 15 s; one call is in flight at a time; a failed
call keeps the last good copy and isn't retried from a read for 15 s. Only the very first read, or one
that needs a project registered since (adding a server to a project Paseo just learned about), waits.
Refresh in the Add gallery or on sign-in re-reads both in the background, and changing Paseo's tool
settings from the Connectors tab marks the provider settings out of date.
| Status | Meaning | Needs attention |
|---|---|---|
ok |
The endpoint answered initialize, or is alive and rejected the anonymous request the way the protocol says to (400, 405, 406, a redirect) |
no |
auth-required |
401 or 403: an OAuth server. This is how a working server answers a probe with no grant; the editor holds the sign-in. Shown as informational | no |
warn |
404 (wrong path, or an inactive n8n workflow) or a 5xx from the server | yes |
down |
Connection refused or reset, DNS failure, TLS rejected, or no answer in 5 seconds | yes |
binary-missing |
A stdio command not found on PATH | yes |
unknown |
No readable definition | no |
Health notes never contain a URL, query string, token, or header value; the verdict is redacted before it is cached, shown, or logged. Missing sign-ins are reported per account on each server's page (and counted on its card), which reads each editor's own grant list.
A chat's composer gets a Connectors chip only when a connector that chat loads needs you
(0.19.1; trimmed in 0.19.2): 2 broken or 1 needs sign-in, short enough not to be cut off. A
connector switched off for the chat's project (or everywhere), a tool that comes with the Codex app,
or a sign-in for a connector no settings file lists doesn't count, and only a running chat or one
active in the last hour gets a chip. Press it, or type /connectors in the composer, to open that
chat's Connectors panel: what needs a look (broken first), the connectors it loads with their
switches, and folded below them what it hasn't used, the project's own connectors and the technical
details. Paseo shows the chat's context use itself, so the panel doesn't estimate it. Turn the chip
off with the Composer chip setting.
Since 0.19.2 the plugin no longer registers /mcp (Paseo can't hide a slash command, and two entries
for one panel were noise), so the provider's own /mcp is untouched. Use /connectors.
The chip never makes a network call or starts a process: its estimate uses cached tool counts only.
Each server's card on the Servers section (the Tools disclosure) and the Tools card on its page
show what a server would hand an agent: every tool's name, title, description and the arguments it takes
(required ones starred), plus the server's own name and version from the handshake. The host asks
each HTTP server with the same two requests a client sends, initialize then tools/list, a few
servers at a time, and caches the answer; the section reads the cache, and Refresh asks again.
Nothing is guessed. A server that cannot be asked says why:
| Shown as | Meaning |
|---|---|
N tools |
The server answered tools/list; expand Tools on the card to read them |
sign in to list |
An OAuth server: it answers an anonymous request with 401 and lists its tools only to a signed-in editor |
runs on demand |
A stdio (command) server: its tools are only knowable while an agent has it running, and the plugin does not start processes |
not listed |
The endpoint answered but not with MCP (a web page at the URL, a JSON-RPC error, a timeout), with the redacted reason |
A server that listed its tools before and does not answer on a later pass (a timeout, a restart, an error page) keeps its earlier list, marked "as of HH:MM" with the reason the new ask failed, so a blip never shrinks the tool count on the chip. An edited URL starts over.
Tool descriptions are text the server controls; they are flattened to one capped line before they are stored or shown. The data carries each tool's name and argument list so a per-tool policy control can be added alongside it later.
Every Paseo daemon runs its own MCP server, paseo, and with daemon.mcp.injectIntoAgents on it
adds that server to every agent it starts. It hands each agent 39 tools for agents, terminals,
schedules, heartbeats and workspaces, plus 22 browser_* tools when browser tools are on: 61 in all
(Paseo 0.9.1). The Built-in tools fold-out at the bottom of Connectors shows and changes what agents get:
| Control | Daemon setting | Default |
|---|---|---|
| Add to every agent on this host | mcp.injectIntoAgents, the app's Settings → Orchestration → Enable Paseo tools |
off when unset in config.json |
| Per provider | providers.<id>.paseoTools.enabled |
on (absent means on) |
| Per tool | providers.<id>.paseoTools.disabledTools, bare names such as list_agents |
none |
| Turn off browser tools | browserTools.enabled |
off |
The host-wide switch changes every agent on the host, so it asks twice. The tool switches write to
every provider at once under All providers, or to one provider when one is picked. A tool that is
off for only some providers says which. mcp.enabled (the --no-mcp flag) turns the server off
altogether; the config API cannot change it, so the card only says so when it is off.
Every change goes through the daemon's config API (paseo.config.patch), never by editing
config.json. Only the fields that change are sent. disabledTools starts from the current list,
so names the plugin does not know are kept, and a provider patch carries only paseoTools, so
extends, env and every other field stay as they are. The config is read back after the write
and the card shows what was saved. The daemon copies the policy into an agent when it starts, so
a change applies to agents started after it; a running agent keeps the tools it started with.
Where the tool list comes from:
/mcp/agents is then open, so the host asks it for
tools/list at the address the daemon bound, which Paseo writes to listen in
$PASEO_HOME/paseo.pid. When that file is missing or has no address, it uses PASEO_LISTEN,
daemon.listen in config.json, or 127.0.0.1:$PORT (default 6767). The card says "Tool list live from this host".
The host asks once at most every ten minutes, in the background, or now when you press
Refresh. A failed ask is not retried before then, and a good list is kept until a newer one
replaces it.shared/paseo-tools.ts, copied from Paseo 0.9.1, otherwise. With a password
(PASEO_PASSWORD or daemon.auth.password), /mcp/agents wants a per-run token that only
agents get. The plugin never sends or guesses one. The card says "as of Paseo 0.9.1". When the host
runs another Paseo version, the card and the Overview line add: "This list is from Paseo 0.9.1;
this host runs X, so new tools may be missing and removed ones may still show." The version is
read from the @getpaseo/server package the daemon started the plugin from.The card also points to Add project servers to agents for servers from a project's .mcp.json.
The same count feeds everything else: the Overview has a Paseo tools line, the workspace and agent panels list Paseo tools (built in) among what an agent loads, and the composer chip counts it as one more server with its tools, for the agent's own provider only.
Each health result carries the configs that define the server: an editor's global config
(~/.claude.json, ~/.codex/config.toml, …) is user level and affects every workspace; a
.mcp.json in a registered project is project level. The workspace and agent panels list issues
under three headings — this project, user config, other project — and tag each project server row
with its health. A server defined at both levels is probed once, using the editor's copy, since that
is what the editor actually runs.
The Workspace connectors tab and each agent's Connectors tab lead with what an agent started in that workspace actually loads, counted from the same files the CLI reads:
.mcp.json (read natively by Claude Code, or added by Add project servers to agents for the chosen
providers),~/.claude.json, ~/.codex/config.toml, …), which loads in
every workspace.A name defined at two levels is counted once. The workspace tab shows the heaviest wired editor; the agent tab shows the agent's own provider.
The agent tab also lists servers the agent was actually started with that no editor config or Paseo
tools explain, tagged Added when created: other plugins add these from their own agent.create
hooks (Shared Browser's shared-browser, for one), and whoever creates an agent can pass its own. Each shows its name and transport; an HTTP one
shows its tool count from the same probe the Tools view uses (asked in the background, at most
every ten minutes), a stdio one says "runs on demand". They count towards that agent's budget, at
their real tool count where the probe has one and five otherwise. They come from the record the
daemon writes after every hook ran ($PASEO_HOME/agents/<project dir>/<agent id>.json, read-only);
Paseo's agent API does not return an agent's servers, and a hook only sees what plugins earlier in
id order added. The workspace tab has no agent, so it cannot know these and does not count them. Under the count: how many are stdio (a child
process per agent session) versus http (no local process), how many need attention here versus
elsewhere, and a Running now section listing the MCP server processes currently running for
this workspace with their resident memory. That section reads the daemon host's process table
(ps, /proc on Linux, lsof on macOS), attributing a server to the workspace when its agent, or
the server process itself, works in the workspace directory. When the table cannot be read the panel
says so rather than showing a zero.
The agent tab's Context section estimates what that agent's MCP tool definitions cost, the same number the chip shows:
| Server | Counted as |
|---|---|
| Listed HTTP server | Measured: each tool's name, description and input schema as JSON, ÷ 4, from the raw tools/list answer (before descriptions are cut for display) |
| Added when created, HTTP | Measured the same way from its own probe |
| Paseo tools (built in) | 135 tokens per tool, measured on Paseo 0.9.1's own tools/list (61 tools, about 8,250 tokens) |
| Anything not listed (stdio, OAuth before sign-in, unreachable) | 2,500: five tools at 500, an assumption between Paseo's measured 135 and the roughly 1,500 per tool Anthropic reported for large servers |
A server switched off for the workspace is not counted. Every number is an estimate and says "≈" or "~". When the agent has reported its context use, the section adds "This chat: 360k of 1M context; MCP definitions ≈38k of that." With tool search on, the definitions load on demand, so the chip says "deferred" and the panel says so instead of claiming a share. Heaviest servers lists each server, biggest first, with how it was estimated.
Below that, from the agent's own timeline: Used in this chat ("supabase ×4, linear ×1") and
Loaded but unused with their names. The host reads the timeline through Paseo's plugin API, the
newest 2,000 items at most, in the background, and keeps the answer a minute; the panel says when older
items were not read. Claude agents' tool calls are named mcp__<server>__<tool> and Codex agents'
<server>.<tool>; both are mapped to the servers the agent loads. A name two loaded servers could own
counts for both.
Turn off the unused ones for this workspace uses the same per-workspace switches as the rows below it. It asks first, names exactly the servers that change, and applies to new sessions only. It only offers servers that have a switch for this provider and are on now; unused servers without one are named and left alone. It is never offered when the chat is longer than the 2,000 items read or the last read failed; the panel says which. On confirm the host reads the whole chat again and switches off only if its own list matches the one you confirmed; otherwise it switches nothing and asks you to review again. A call to a server outside the loaded list counts as a use of any loaded server it could belong to.
With Chat notices: sign-in problems on (the default), when an MCP tool call in a chat fails with an auth-shaped error (401 or 403, "unauthorized", "invalid_token", "needs authentication"), the host adds one small card to that chat: "linear needs sign-in", with Connect. Connect opens the agent's MCP panel with that server's sign-in rows first, the same OAuth flow as its row. The card carries the server and provider names only, never the error text.
There is at most one card per server per chat, even across plugin restarts: the first time a chat is seen, its timeline is read back for an earlier card, and if that read fails nothing is added. The host checks each finished turn only while a Paseo app is connected; the panel's chat read catches failures from before that. Paseo's own server never gets a card.
Without tool search, every server's tool definitions are sent to the agent with its first prompt.
Cursor stops at 40 tools and warns that some models ignore more. The panel warns at 8 servers ("getting heavy", about 40
tools at five per server) and flags 16 or more as "over budget" (about 80 tools, well over 100K
tokens before any work, the range where Paseo-launched agents fail with "Prompt is too long"). The
warning lists the user-level servers the agent loads, since those are the ones that can be moved into
one project's .mcp.json or removed from the editor config, with a button to MCP management. The
thresholds live in shared/budget.ts.
The server lines assume five tools per server. Paseo's built-in server is counted by its real number of tools (61, or 39 without browser tools), so a second check counts tools: about 40 is "getting heavy" and 80 "over budget", the same lines as 8 and 16 servers at five each. The worse of the two checks wins. Without Paseo tools both checks always agree, so the numbers are unchanged.
Claude Code's MCP tool search is on by default: it loads tool names at the start and definitions
only when needed, so a long tool list costs little context. The host judges it per provider
(shared/tool-search.ts) and the panel says which applies:
| Verdict | When | Budget |
|---|---|---|
| on | A Claude-based provider with none of the settings below, or ENABLE_TOOL_SEARCH set to true, auto, or auto:N with N up to 10 (auto's own threshold) |
The tool check does not raise the tier; the server check still does (stdio processes and connections are real). One line: "Claude Code's tool search is on: tool definitions load up front only while they fit in 10% of the context window, and on demand past that." |
| off | A custom ANTHROPIC_BASE_URL (any host but api.anthropic.com), ENABLE_TOOL_SEARCH=false, or CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS, which Claude Code says keeps tool search off even with ENABLE_TOOL_SEARCH set, unless managed settings set it (source 5). Also every non-Claude CLI except Codex. |
As before, and the warning names the reason: "AI Router re-routes this provider through OmniRoute (custom ANTHROPIC_BASE_URL) whenever its endpoint is up, so Claude Code's tool search is off and all 76 tool definitions load with the first prompt." |
| unknown | Codex (it defers MCP tools only when the model supports tool search), Claude on Google Cloud's Agent Platform or Microsoft Foundry, ENABLE_TOOL_SEARCH=auto:N with N above 10 (tools load up front until they fill N% of the context window) or outside 0-100, or a provider whose CLI is not known |
As before, with the reason |
Sources, later ones winning:
The daemon's own environment.
The provider entry's env in the daemon config. A provider with extends: "claude" (or another
built-in) starts with that built-in's env and puts its own on top, as Paseo does.
AI Router. It routes the built-in claude provider while its routeAgents setting is on
(read-only from $PASEO_HOME/plugin-settings/ai-router/routing.json; a missing or unreadable
file, or a version other than 1, means not routed, as AI Router reads it). It always routes
its own ai-router provider. A routed session gets both ANTHROPIC_BASE_URL and
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1.
The env in Claude Code's own settings files: the user settings.json in the provider's
CLAUDE_CONFIG_DIR (or ~/.claude), then, on the workspace and agent panels, the workspace's
.claude/settings.json and .claude/settings.local.json. Claude Code's docs say a value there
"overwrites the same variable exported in your shell, and when more than one settings file sets
a variable, the highest-precedence one applies"
(settings reference, env); local
beats project beats user (settings precedence).
An empty value cancels one set lower down. A missing or broken file counts as not there.
Claude Code's managed settings, above everything: managed-settings.json and then every *.json
in managed-settings.d/ (alphabetical, hidden files skipped), in
/Library/Application Support/ClaudeCode/ on macOS, /etc/claude-code/ on Linux and WSL
(managed settings). Read for Claude-based
providers only. The value that keeps tool search on under CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS
and through a gateway (AI Router's routing) is a managed ENABLE_TOOL_SEARCH="force". The docs only
say "on Claude Code v2.1.227 or later, managed settings can keep tool search on"
(env vars); Claude Code 2.1.280 checks for exactly
force (isToolSearchForceOverride, first-party connections only, and a custom
ANTHROPIC_BASE_URL still counts as one). Tested on a fleet on 2026-09-25: with
{"env":{"ENABLE_TOOL_SEARCH":"force"}} in /etc/claude-code/managed-settings.json, routed chats
sent 13 tools up front instead of 70; a managed "true" changed nothing. So the panel says on for
force ("…set ENABLE_TOOL_SEARCH=force, which keeps tool search on through a gateway and under
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS (Claude Code v2.1.227 or later)"), and lets a managed
true/auto count only where the betas flag is not set. On a cloud provider (Bedrock, Mantle,
Claude Platform on AWS, Agent Platform, Foundry) force has no effect.
A missing or broken file counts as not there. The macOS configuration profile, the Windows
registry and server-managed settings are not files and are not read.
Health issues under the count are split the same way: servers this workspace loads first, each with an Open button that lands on that server in MCP management, and problems elsewhere folded away.
Claude Code records which servers each account still has to authorize in its own config dir, so its
sign-in state is a file read. Codex keeps MCP OAuth grants in the OS keyring, or, where there is none
(a Linux container, or mcp_oauth_credentials_store = "file"), in $CODEX_HOME/.credentials.json. The
plugin reads that file directly: a usable grant there shows as connected at once. Nothing from it
leaves the host except that yes/no; no token is read into a response or a log.
For everything the file cannot answer (keyring grants, whether a server wants OAuth at all), Codex is
asked with codex mcp list --json, which checks each server over the network and can take seconds. So
it never runs while a panel waits:
config.toml or grant file changed. Refresh and a
finished sign-in ask again.Codex prints this on every start when it cannot tidy $CODEX_HOME/tmp/arg0: something in there
belongs to another user, usually root, left by running codex through docker exec without --user.
It is harmless. The plugin captures Codex's output instead of passing it to its log, and explains the
warning once per account with the exact directory. To silence it, fix the ownership once, as root on
the daemon host (in the paseo-dev-stack container: docker compose exec --user root paseo chown -R paseo:paseo /home/paseo/.codex/tmp).
AgentLink is optional. Standard ~/.claude, ~/.codex, ~/.kimi-code, and ~/.grok setups work on their own.
When AgentLink account directories exist under ~/.agent-link or ~/.agent-auth, Paseo MCP discovers them automatically. It also reads hand-managed ~/.claude-accounts and ~/.codex-accounts directories. OAuth remains per account and is never copied.
npm install
npm run typecheck
npm test
paseo plugin add /absolute/path/to/paseo-mcp --link
npm run bench runs the load harness (tests/perf/harness.ts): the plugin in its own process against
a sandbox HOME with a fake codex, playing daemon and app, and prints processes started per minute,
event-loop stalls, and RPC timings with the panel closed and open. npm run bench -- --quick takes
about a minute. --daemon-delay 8 makes the fake daemon take 8 s to answer its project list and
provider settings, the way a busy daemon does (0.15.0).
Paseo's plugin host supplies the runtime. The npm dependencies are development types only.
Scanned 15 Oct 2026, 12:32 UTC from itsjustanks/paseo-mcp.