Repository navigation
[MCP] Add the v1 MCP catalog with the McpApp component schema - #2945
Conversation
062292a to
5aa7661
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces the A2UI iframe catalog (@a2ui/catalog-iframe), enabling the embedding of sandboxed web applications via the WebAppFrameUrl and WebAppFrameSrcdoc components, and updates the MCP catalog to declare the McpApp component. The review feedback highlights several important improvements and bug fixes: resolving a potential runtime crash in the link interceptor script by checking if e.target is an Element before calling closest(), avoiding intrusive thread-blocking alerts in the security self-test by checking win.top?.location.href instead, ensuring that stale iframe content is cleared when a resource becomes null, and explicitly removing unused src or srcdoc attributes to prevent browser precedence conflicts when switching resource types.
I am having trouble creating individual review comments. Click here to see my feedback.
typescript/catalogs/shared/sandbox/sandbox.ts (327-333)
When updating the resource in the proxy iframe, if the previous resource was loaded via srcdoc and the new resource is loaded via src (or vice versa), the browser's precedence rules for srcdoc over src will prevent the new src from loading. To ensure the new resource loads correctly, we should explicitly remove the unused attribute (src or srcdoc) before setting the new one.
if (html !== undefined) {
inner.removeAttribute('src');
inner.onload = sendInit;
inner.srcdoc = html;
} else if (url !== undefined) {
inner.removeAttribute('srcdoc');
inner.onload = sendInit;
inner.src = url;
}
typescript/catalogs/shared/sandbox/sandbox.ts (284-296)
The security self-test uses win.top?.alert(...) to check if the top window is reachable. This is highly intrusive because if the sandbox is misconfigured during development, it pops up a modal alert dialog that blocks the main thread and requires user interaction to dismiss. We can perform a non-intrusive check by accessing a property on the top window (such as win.top?.location.href), which will throw a SecurityError if securely isolated (cross-origin) without showing any modal dialogs.
if (!disableSelfTest) {
// Security self-test: a proxy that can call into the top window is not isolated from it.
let topReachable = false;
try {
// Accessing a property on the top window throws a SecurityError if securely isolated (cross-origin).
const _ = win.top?.location.href;
topReachable = true;
} catch {
// Cross-origin access threw, which is the expected outcome.
}
if (topReachable) {
throw new Error('The sandbox is not setup securely.');
}
}typescript/catalogs/shared/sandbox/sandboxed_frame_element.ts (248-256)
If resource is null (e.g., because htmlContent or url was cleared), sendResource returns early and does not notify the proxy. This leaves the stale previous content visible in the iframe instead of clearing it. We should send an empty resource (e.g., {html: ''}) to the proxy when resource is null so it can clear the inner iframe.
private sendResource(resource: SandboxResource | null): void {
const session = this.session;
if (!session) {
return;
}
session.resource = resource;
if (!this.proxyReady) {
return;
}
sendSandboxResourceReady({
frame: session.frame,
sandboxOrigin: session.sandboxOrigin,
protocol: this.sandboxProtocol,
resource: resource ?? {html: ''},
});
}typescript/catalogs/iframe/src/components/srcdoc_content.ts (43)
In LINK_INTERCEPTOR_SCRIPT, e.target.closest('a') is called directly. However, e.target can be a Text node or Document which does not have the closest method, leading to potential runtime TypeError crashes. We should defensively check if e.target is an Element (e.g., e.target instanceof Element) before calling closest.
const anchor = (e.target instanceof Element) ? e.target.closest('a') : null;
fcef136 to
8670800
Compare
8670800 to
ce977a6
Compare
ce977a6 to
f12d965
Compare
f12d965 to
17556ef
Compare
17556ef to
2b63402
Compare
2b63402 to
76a31ee
Compare
ef8b7cd to
b9ef6f2
Compare
b9ef6f2 to
8f4b379
Compare
8f4b379 to
261d05d
Compare
e566bf4 to
5a5e4d4
Compare
5a5e4d4 to
07944f4
Compare
07944f4 to
31af1ec
Compare
31af1ec to
cc55b19
Compare
ditman
left a comment
There was a problem hiding this comment.
Thanks for putting this together! Two main pieces of feedback:
1. Register catalogs/mcp/v1/catalog.json in the v1.0 spec test runner
python3 specification/scripts/validate.py only validates message example files, so catalogs/mcp/v1/catalog.json isn't currently being checked against the v1.0 Catalog Schema Rules (specification/v1_0/docs/a2ui_protocol.md:552-625) or catalog_definition.json.
Please add ("catalogs/mcp/v1/catalog.json", os.path.join(REPO_ROOT, "catalogs/mcp/v1/catalog.json")) to catalogs_to_validate in specification/v1_0/test/run_tests.py inside:
validate_catalogs_structure()(L265–268)validate_catalogs_identifiers()(L309–312)validate_catalogs_ref_targets()(L413–416)
And run:
python3 specification/v1_0/test/run_tests.pyTo verify the test setup is working, you should initially see failures like these on the current catalogs/mcp/v1/catalog.json:
Validating catalog structural integrity against catalog_definition.json...
[FAIL] catalogs/mcp/v1/catalog.json
- instancePath: '/$defs', params: { additionalProperty: 'JsonSchema' } (and AllowedFunctions, DataPaths, McpAppCsp, McpAppPermissions)
- instancePath: '/functions/callMcpTool', params: { unevaluatedProperty: 'unevaluatedProperties' }
Validating catalog $ref targets against the allowed target list...
[FAIL] catalogs/mcp/v1/catalog.json
- Disallowed $ref target: 'https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentCommon'
- Disallowed $ref target: 'https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString'
- Disallowed $ref target: '#/$defs/AllowedFunctions'
(Fixing those to follow Rules 2–6—inlining the helper schemas into McpApp.properties, using relative common_types.json#/$defs/... refs, removing the redundant ComponentCommon wrapper and component/function-level unevaluatedProperties: false, and adding discriminator to anyComponent—should also let you drop the PUBLISHED_CATALOG_NOT_SELF_CONTAINED divergence in conformance_test.mjs.)
2. Separate McpApp / MCP catalog responsibilities from sample-specific data transformations
The MCP catalog (catalogs/mcp/v1/catalog.json:91-225) currently mixes MCP functionality (McpApp, callMcpTool) with general string manipulation (split), RE2 regex handling (regexCapture, regexReplace), JMESPath querying (jmespath), and path writes (updateDataModel).
Those five functions are not MCP specific. They were added for the a2ui-over-mcp-filesystem sample to parse raw plain-text CLI responses on the client. Bundling them into the shared MCP catalog:
- Forces the shared
@a2ui/catalog-mcppackage (typescript/catalogs/mcp/package.json:31-32) and every consumer (evena2ui-over-mcp-recipe, which doesn't use them) to pull injmespathandre2jsdependencies. - Would require every v1.0 SDK/renderer implementing the MCP catalog across languages to ship JMESPath and RE2 engines, while bloating LLM prompts with string-scraping schemas that
McpApp(data.paths) andcallMcpTool(targetDataPath) don't need.
Proposed split:
- In this PR: Keep
catalogs/mcp/v1/catalog.jsonminimal and strictly scoped to MCP (McpAppandcallMcpToolonly). - In the community sample: Move the string/RE2/JMESPath pipeline (
jmespath,split,regexCapture,regexReplace,updateDataModel) and thejmespath/re2jsdependencies out oftypescript/catalogs/mcp/and intosamples/community/mcp/a2ui-over-mcp-filesystem(which already registers a custom composite catalog), so the shared MCP catalog andMcpAppstay clean.
cc55b19 to
ba71a3d
Compare
|
Thanks for the review. Spec test registration. The Data functions. Agreed, the five data functions are not MCP specific. They already ship in the v0.9 catalog, hosts register functions one by one, and the Composer vendors all six from this catalog today, so I would move them in a follow-up to a separate transformations catalog (functions for function calling and for processing arguments and results) rather than into one sample. Tracked in #3153. This PR keeps the function set unchanged. |
ditman
left a comment
There was a problem hiding this comment.
About the extra functions:
They already ship in the v0.9 catalog, hosts register functions one by one, and the Composer vendors all six from this catalog today [...]
This is not accurate. They ship as part of a "community sample" app, introduced 3 weeks ago in #2650, but this PR makes them "first class citizens" of the "mcp/v1" catalog.
They rrrreally shouldn't land here, especially if we're not planning on addressing the new catalog as a fast follow.
Add catalogs/mcp/v1/catalog.json, the MCP catalog for A2UI protocol v1.0: the existing callMcpTool and data functions plus the McpApp component, whose allowedFunctions is a map of function name to JSON Schema. Move the component specification next to it, add examples, validate them in specification/scripts/validate.py, and publish the catalog at its $id URL from the docs workflow. The v0.9 catalog at catalogs/mcp/catalog.json is unchanged.
ba71a3d to
5a87480
Compare
|
You are right, I overstated that: the functions arrived with #2650 three weeks ago for the filesystem sample, and this PR is what would have made them part of a published v1 catalog. Following your comment on #3153 these extra functions will be extracted in #3171 as part of this stack. This catalog won't be released until then |
Summary
Adds
catalogs/mcp/v1/catalog.json(https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json), the v1.0 version of the MCP catalog, on top of the v1.0 protocol schemas. It carriescallMcpTooland the five data functions forward from the v0.9 root catalog (catalogs/mcp/catalog.json, kept intact) and adds theMcpAppcomponent schema for rendering interactive MCP Apps (io.modelcontextprotocol/ui) inside a double-iframe sandbox.Changes
catalogs/mcp/v1/catalog.json: v1.0 catalog (protocolVersion: "1.0") withMcpApp(htmlContent,title,allowedTools,allowedFunctions,data.paths,csp,permissions) andcallMcpTool+ data functions. The file follows the v1.0 Catalog Schema Rules:McpAppis a flat component (noComponentCommonallOf, nounevaluatedProperties), helper shapes are inlined into its properties, refs are relativecommon_types.json#/$defs/..., and$defsholds onlyanyComponent(withdiscriminator) andanyFunction. Moving the data functions to a separate transformations catalog is tracked in Split the data transformation functions out of the MCP catalog into a transformations catalog #3153.specification/v1_0/test/run_tests.py: registerscatalogs/mcp/v1/catalog.jsonin the structural, identifier and$reftarget checks.catalogs/mcp/v1/mcp_app_specification.md: moves theMcpAppcomponent specification from the Angular MCP calculator sample and updates it forv1.0and@modelcontextprotocol/ext-appsfields.conformance/core/catalog.yaml: registerscatalogs/mcp/v1/catalog.jsonintest_v10_published_mcp_catalog_is_self_contained(required bytest_catalog_conformance_covers_published_catalogsfor everycatalogs/*/v*/catalog.json)..github/workflows/docs.yml,catalogs/mcp/README.md,catalogs/README.md: publishescatalogs/mcp/v1at/specification/v1_0/catalogs/mcp/and documents the v1.0 catalog layout.Verification
python3 specification/scripts/validate.pypython3 specification/v1_0/test/run_tests.pypytest tests/test_conformance.pyinpython/a2ui_core./scripts/fix_format.sh --checkandpython3 scripts/fix_licenses.py --checkRelated to #3157