Troubleshooting
The errors that the library and the MCP SDK raise, what causes each one, and how to correct it.
Each message below comes from the library or from the MCP SDK it runs on. Find your message, then apply the correction.
Server errors
The host cannot connect at all
HTTP 400, JSON-RPC -32022 "Unsupported protocol version"
data.supported: ["2026-07-28"]
1.x speaks MCP 2026-07-28 only. The endpoint rejects a 2025-era client on the wire
(legacy: "reject"), therefore no tool ever runs. In the host this looks like a connector that
does not add, or one that lists no tools.
This is expected today. As of August 2026, claude.ai still connects with the 2025-11-25 revision. Keep a production connector on the 0.2.x line until the hosts you target roll out 2026-07-28. See migrate from 0.2.x.
Sealed state is invalid or expired
ng-mcp-ui: sealed state is invalid or expired
ctx.state.open() refused a token. The signature does not match, the lifetime ran out, the bind
context differs, or the token was minted for the other carrier. The SDK converts the throw into an
isError tool result, therefore the widget sees a failed call and not an exception.
The message names no reason on purpose. The usual causes are:
- The server restarted with no
state.key, thus with an ephemeral key. ttlSecondsis shorter than the time the widget stays on screen. Raise it, or re-seal on each response, because a re-seal restarts the lifetime.- An elicitation
requestStatetoken was given toopen(). The two carriers are not interchangeable.
In the widget, treat this result as "start over". See sealed state.
Invalid or expired requestState
JSON-RPC -32602 "Invalid or expired requestState"
Round two of an elicitation arrived with an echo that failed verification: tampered, expired, or minted for another purpose or another tool. The check runs before your handler, therefore the handler never sees the call.
Mint the echo with ctx.state.sealRequestState(...) in round one, and let the host send it back
unchanged. Do not build or edit the token yourself. See elicitation.
The client capabilities do not declare the required capability
JSON-RPC -32021 "Cannot request input 'confirm' (elicitation/create): the request's client
capabilities do not declare the required capability"
Your handler returned an inputRequired(...) with a form elicitation, but the request envelope
declared no elicitation.form capability. The error data lists the missing capabilities in
requiredCapabilities.
A host that supports elicitation declares the capability on each request. Therefore this error
means one of two things: the host cannot elicit, or a test request omitted the capability from the
_meta envelope. Give the tool a path that completes without the question.
state.key is required in production
ng-mcp-ui: `state.key` is required in production. Provide a stable secret of at least 32 bytes …
You constructed the server with a state option and no key, and NODE_ENV is production.
Construction throws, thus the process does not start.
Read the key from the environment, for example state: { key: process.env["NG_MCP_STATE_KEY"] }.
Each instance that can receive a token needs the same key.
In development the same case only writes a warning:
ng-mcp-ui: no `state.key` configured; using an ephemeral per-process key. …
The library then mints a key for the process. Each token dies on restart. That is correct for a dev loop, and it is the usual cause of "sealed state is invalid or expired" after a reload.
A tool renders a view but returned no content
ng-mcp-ui: tool "create_poll" renders a view but returned no `content`.
The handler returned structuredContent only. The view renders on an MCP Apps host and on an Apps
SDK host. Every other client, and the model's own reading of the result, sees nothing.
Return a short text summary next to structuredContent. The library writes this warning in
development only, and one time for each tool.
A view is already used by another tool
ng-mcp-ui: view "poll" is already used by tool "create_poll".
Tool "cast_vote" cannot also reference it — each view backs exactly one tool.
Each view belongs to one tool. This rule keeps the tool result and the rendered view together.
Give the second tool its own view, or let the second tool return a text result. A tool that only changes state does not need a view. The first tool re-renders with the new state.
Could not resolve the widget entry bundle
ng-mcp-ui: could not resolve the widget entry bundle from index.html.
IndexHtmlViewManifest read the index.html file of the widgets build, but it found no entry
script tag.
Run the widgets build first:
npm run build:widgets
Then confirm that dist/widgets/browser/index.html has a <script type="module"> tag.
Failed to read widgets index.html
ng-mcp-ui: failed to read widgets index.html at "…".
The path is wrong, or the build did not run. Run npm run build:widgets. If the path is wrong,
give the correct dist/widgets/browser/index.html path to IndexHtmlViewManifest.
The dev-server proxy accepts http only
ng-mcp-ui: the widgets dev-server proxy only supports http:// upstreams
You gave a devServerUrl value that is not an http:// URL. The proxy connects to your local
ng serve process, thus the upstream is always plain HTTP.
Set devServerUrl to the ng serve origin, for example http://localhost:4200.
View errors
An inject function throws outside an injection context
Angular throws this error when you call an inject* function from the wrong place. Each inject*
function reads the MCP_ADAPTOR token, therefore Angular DI must be active.
Call the function in a field initializer, in a constructor, or in runInInjectionContext. Do not
call it in a lifecycle hook, in an event handler, or in a setTimeout callback.
export class PollWidget {
private readonly state = injectViewState(); // correct
onClick() {
const state = injectViewState(); // wrong
}
}
File upload, download or selection is not supported in MCP App
File upload is not supported in MCP App.
An MCP Apps host has no file functions. Only an Apps SDK host has them.
Put the call in a try/catch block and give a fallback path. See
files and downloads and the
host support matrix.
setOpenInAppUrl is not implemented in MCP App
setOpenInAppUrl is not implemented in MCP App.
Only an Apps SDK host has this function. Call it in a try/catch block.
selectFiles is not supported by the current host version
selectFiles is not supported by the current host version.
The Apps SDK host is too old, and it does not show a file picker. Give the user another way to supply the file, for example a URL field.
The href parameter is required
The href parameter is required.
You called setOpenInAppUrl with an empty string, or with spaces only. Give a real URL, or do not
call the function.
Behavior that is correct but looks wrong
A call does nothing and writes a warning
Two functions do nothing on an Apps SDK host. They write a warning to the console.
injectRequestSize. An Apps SDK host controls the size of the iframe.injectRegisterViewTool. An Apps SDK host does not support view tools. The function returns an empty disposer, thus your cleanup code stays correct.
These are not errors. See the host support matrix.
download resolves with isError: true
injectDownload().download never throws. It resolves to { isError: true } when the host has no
download function. Test the result:
const result = await download({ contents });
if (result.isError) {
// Show a link instead.
}
An image or font does not load in the view
Two different causes give the same symptom.
- The URL points to the wrong origin. In the iframe, a relative path points to the host, not
to your server. Use the
mcpAssetpipe. - The CSP does not permit the origin. Add the origin to
resourceDomainsin thecspobject of the view config.
You often need both corrections. See Content Security Policy.