Artifacts can no longer call a local (stdio) MCP server: `host:` server form refused with `capabilities.mcp: unavailable`

Status Open
Reported on v2.1.224
Maintainer reply None cached
Activity 4 comments · opened Aug 26, 2026

Summary

Published artifacts could call a local stdio MCP server through the previous
window.cowork.callMcpTool bridge. After the artifact runtime moved to the
claude.use('mcp') capability model, they cannot: the documented host:<name>
server form — the one intended for a server on the viewer's own device — is
refused at publish with

deploy 422: capabilities.mcp: unavailable

Two dashboards that had worked for weeks went dark simultaneously. Nothing on my
side changed; the runtime is served per page load, so the behaviour changed under
already-published pages.

What worked before

Both pages called a locally-registered stdio server (claude_desktop_config.json,
python3 .../server.py) via window.cowork.callMcpTool('mcp__<server>__<tool>', args).
Verified working as of 2026-07-29 by reading the live artifact's own source, and
in daily use after that until 2026-08-25.

That local stdio servers were reachable from artifacts is also implied by
#55788, which reports Live Artifacts failing to reach them on cold start,
with a documented workaround (call the tool once in Claude Chat, after which the
artifact path works for the rest of the session). A cold-start bug is not
possible in a capability that never existed.

What happens now

window.cowork is gone — a probe enumerating window inside a live artifact
returns cowork: undefined, claude: { use: function }. Declaring the
replacement for a local server fails:

{"mcp": {"servers": [{"server": "host:<local-server-name>",
                      "tools": ["<one_tool>"]}]}}

| Attempt | Result |
|---|---|
| host: form, full tool list, existing artifact | 422 capabilities.mcp: unavailable |
| Same, plus contract: "latest" (ruling out a stale contract pin) | same 422 |
| host: form, one tool, brand-new artifact, published from Claude Code | same 422 |
| host: form, one tool, brand-new artifact, published from the desktop app | same 422 |
| Bare server name, one tool, same account | publishes normally |
| {"downloads": true}, same account | publishes normally |

The control that isolates it

The last two rows are the point. Same account, same capability, same single
tool — the only difference is the host: prefix. So mcp is available
here; the host: server form is what is refused.

Which also makes the error message misleading: capabilities.mcp: unavailable
names the capability when the unavailable thing is the server inside it. That
wording sent three separate debugging sessions down the wrong path before the
control above isolated it. Naming the rejected server instead would have saved
all three.

Why I think this is a regression rather than intended

mcp.d.ts for contract 0.2.23 specifies host:<name> as a server "running on
the VIEWER'S DEVICE, reached through the Claude app that shows the page" —
routed by the app rather than the relay, which keeps the viewer-scoped model
intact rather than breaking it. It is written in the future tense, twice
("once the shell routes host: calls to the app"), and hedged ("only the
Artifact's owner can use host servers for now").

So the capability appears designed and not yet routed, rather than removed on
purpose. I'd not have filed this if the contract didn't describe the path.

I'm aware the artifacts documentation currently states that a published page
can't call local MCP servers, and that the relay architecture (claude.ai making
the call with the viewer's connectors, page never seeing credentials) is
deliberate and sensible. My point is narrower: that support existed, it went
away, and the specified replacement can't be declared.

Questions

  1. Is host: routing on the roadmap, and is there a rollout to opt into?
  2. Is the refusal a per-account gate, or the absence of the routing for

everyone? The answer decides whether waiting is rational or whether I should
re-architect onto a remote connector.

  1. Could the 422 name the rejected server rather than the capability?

Environment

  • Runtime contract 0.2.23
  • Publish surfaces tested: Claude Code 2.1.224, and the macOS desktop app
  • Artifact owner is the publisher in every test (host servers are documented as

owner-only)

  • Local MCP server: stdio, registered in claude_desktop_config.json

Possibly related

  • #55788 — Live Artifacts reject local stdio servers on cold start; the relay

receives a server name where a UUID is expected. Open, has repro.

  • #42453 — local MCP server tools disabled in Cowork while working in Claude

Chat Desktop. Closed as not planned, also labelled stale.

View original on GitHub ↗

3 Comments

gray-zeemee · 1 month ago

Re-tested on runtime contract 0.2.41 (originally reported on 0.2.23). Same repro: publish an artifact declaring the mcp capability with a host:-prefixed server.

Still refused — the functional gap is unchanged. But the error message has been fixed, and it's a real improvement worth acknowledging:

before (0.2.23):
  deploy 422: capabilities.mcp: unavailable

now (0.2.41):
  mcp manifest rejected: "host:data-pipeline-assistant" names a
  locally-configured MCP server, and host servers aren't available in
  this session — declare only claude.ai connectors (set "server" to the
  connector's display name), or to publish without connector access
  leave "mcp" out of capabilities (pass capabilities: {} to clear a
  stored declaration).

That's the specific thing that made this expensive to diagnose. The old message named the capability when the unavailable thing was the server inside it, which led us to conclude the account lacked an mcp entitlement — it didn't; the identical declaration with the prefix removed published fine. The new text says exactly which part is unavailable and what to do instead. Whoever changed that: thank you, it removes the misdiagnosis entirely.

Two things that remain true and are the reason this is still open:

  1. host: is documented as functional in the app. The capability reference shipped with contract 0.2.41 still describes it as "host:<name> for a local MCP server on the viewer's device (Claude app only; else server_not_connected)" — i.e. as something that works in the Claude app and merely fails elsewhere. It is refused at publish in the Claude app. So the contract documentation and the runtime still disagree.
  1. There is no substitute for this shape of use. Our two internal dashboards are artifacts that read live operational data from a local stdio MCP server, and they worked for weeks via the old window.cowork.callMcpTool bridge. The remaining route — a remote endpoint added as a claude.ai connector — requires an Owner to register it on a Team/Enterprise org, and the only OAuth provider our organisation will adopt has no Dynamic Client Registration endpoint. Neither of those is a technical problem you could ask us to solve differently; they're the reason host: mattered.

Happy to re-run the probe on any future contract if it would help confirm a fix. No action needed from us otherwise.

gray-zeemee · 1 month ago

Follow-up with a more actionable finding than my previous comment, from the same 0.2.41 re-test.

The bare-server-name path does not fail cleanly — it hangs, and none of the documented error codes arrive.

Repro: publish an artifact declaring capabilities: {mcp: {servers: [{server: "<a name that is not a connector>", tools: ["<tool>"]}]}}. Publish succeeds. Open it and call the namespace. Measured, with every leg timeboxed:

{
  "steps": {
    "use(mcp)":       { "ok": {},                     "elapsedMs": 2 },
    "listTools":      { "timedOutAfterMs": 15000,     "elapsedMs": 15000 },
    "callTool_bare":  { "timedOutAfterMs": 20000,     "elapsedMs": 20002 },
    "callTool_host":  { "timedOutAfterMs": 20000,     "elapsedMs": 20001 }
  },
  "mcpNamespace": "resolved"
}

(ok: {} is JSON.stringify dropping function properties, not an empty namespace — use() genuinely resolved.)

Three observations:

  1. claude.use('mcp') resolves in 2 ms. The capability is granted; the page is correctly set up. Nothing signals a problem at this stage.
  1. Every subsequent call hangs indefinitely rather than rejecting. The contract documents a per-code error vocabulary — server_not_connected, not_in_manifest, not_granted, server_unavailable — and mcp.d.ts instructs pages to "branch UX per error code, retry only retryable errors". In this scenario none of those codes is ever delivered. There is no rejection at all, only silence past 20 s.
  1. listTools() hangs too, which is the most diagnostic part. It takes no server argument, so it shouldn't depend on any particular server resolving. Its hanging suggests all calls are blocked behind the same unresolved consent — the viewer-facing "No matching connector found" dialog, which cannot be satisfied when no connector of that name exists.

Why this is worth separating from the host: issue above. A rejection carrying server_not_connected is a one-line thing for a page to handle. An indefinite hang is a failure mode every page has to defend against on its own, and a page written exactly as the contract instructs — branch on the error codes — will hang forever instead. That is not hypothetical: the probe that produced the table above hung silently on its first run because it had no timeouts, which is precisely what following the documented contract produces.

Suggested fix, in preference order:

  • Reject unresolvable server names with the existing server_not_connected code at first call, rather than blocking on a consent that cannot be granted.
  • Failing that, reject when the viewer dismisses or cannot satisfy the connector dialog.
  • At minimum, give callTool/listTools a bounded default timeout that rejects with a code, so the documented branch-per-code guidance is actually reachable.

Any of the three would turn this from a hang into something a page can render an error for.

Incidental data point in case it is useful: our own shared bridge survives this only because it races mcp.callTool() against a 45 s timeout and treats a codeless failure as a message string. Pages that trust the error vocabulary alone do not.

andyperez89 · 26 days ago

Second independent case, different stack, same refusal (contract 0.2.52, Windows)

Adding this because it's a different environment and a different reason a remote connector isn't an option, which I think is the crux of whether host: matters.

Environment

  • Claude Desktop / Cowork on Windows 11 Pro (10.0.26200)
  • Runtime contract 0.2.52
  • Local MCP server: read-only SQL Server connector (mssql-server) against an on-prem SAP Business One production database
  • Publisher is the artifact owner

What I tried
Publishing an AP aging dashboard that runs one aggregate query on open through the local connector:

{"mcp": {"servers": [{"server": "host:mssql-server", "tools": ["query"]}]}}

Result

mcp manifest rejected: "host:mssql-server" names a locally-configured MCP server,
and host servers aren't available in this session — declare only claude.ai
connectors (set "server" to the connector's display name), or to publish without
connector access leave "mcp" out of capabilities (pass capabilities: {} to clear
a stored declaration).

So the improved error text from 0.2.41 is still what ships in 0.2.52, and the functional gap is unchanged. The capability reference served in the same session still documents host:<name> as "a local MCP server on the viewer's device (Claude app only; else server_not_connected)", i.e. as working in the app. It is refused at publish in the app.

Why "use a remote connector" isn't available to us
The data source is an ERP production database on our LAN. Exposing it through a remote MCP endpoint reachable from claude.ai is a security posture change we will not make for a reporting dashboard, and it's the kind of thing most small/mid manufacturers running on-prem ERP will refuse. The old live artifact model was attractive precisely because the query ran on the viewer's machine through a connector that already existed, and nothing about the database's exposure changed.

Showing cached comments. Read the full discussion on GitHub ↗