Extensions: Connection Info both eras + io.modelcontextprotocol/ui advertisement (Phase 3 of #1633) (#1744)

* Extensions: Connection Info both eras + io.modelcontextprotocol/ui advertisement (Phase 3 of #1633)

Completes the Extensions surfacing and advertises the MCP Apps UI extension.

core/mcp/extensions.ts:
- Add UI_EXTENSION_KEY ("io.modelcontextprotocol/ui") and MCP_APP_MIME_TYPE
  ("text/html;profile=mcp-app") — mirrors of ext-apps' EXTENSION_ID /
  RESOURCE_MIME_TYPE (hardcoded: the constants live on the package's /server
  subpath or an extensionless re-export that doesn't resolve under NodeNext).
- Registry entries gain an optional `advertisement` value; the builder stamps
  it (default {}). Add the `ui` entry, default-advertised with
  `{ mimeTypes: ["text/html;profile=mcp-app"] }` — the Inspector always renders
  App tools. No ext-apps cast needed: the extensions value type is `object`.

ConnectionInfoContent:
- Source server extensions from the negotiated `capabilities.extensions`
  (getServerCapabilities, era-transparent) instead of the modern-only
  discoverResult, so a LEGACY server advertising extensions renders them too.
- Add a two-column "Server Extensions" / "Advertised Extensions" section (the
  Inspector's own advertised map), mirroring the server/client capability
  columns. Drop the now-redundant Extensions row from the modern Discovery
  section (Supported versions stays).

Tests: extensions.test.ts covers the ui entry + advertisement value and the
per-extension enable/disable; ConnectionInfoContent.test.tsx covers legacy
server extensions (no discovery), the advertised-extensions column, and the
em-dash empty states. npm run ci green (the one InspectorView localStorage
failure is a known flake, passes on re-run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Extensions: address Phase 3 review (mime drift-guard, story, exact em-dashes)

- extensions-mimetype.test.ts (integration/node): assert MCP_APP_MIME_TYPE
  equals ext-apps' real RESOURCE_MIME_TYPE (imported) and that the client
  advertises it — a genuine drift guard, replacing the tautological literal
  check in the unit test (which now just asserts the type).
- ConnectionInfoContent Modern story: carry the tasks extension on
  initializeResult.capabilities too, so the Server Extensions row demonstrates
  it (the row now reads negotiated server capabilities, not discoverResult).
- Tighten the em-dash assertions from ">= N" to exact counts (3/3/2) now that
  the two extension sections em-dash by default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Extensions: drift-guard UI_EXTENSION_KEY against ext-apps EXTENSION_ID (review)

Address the Phase 3 re-review nit: the extension id was the other half of the
same drift risk (a server keys its Apps lookup on it). Import ext-apps'
EXTENSION_ID from the /server subpath in the node integration test (fine there;
the browser build is what avoids /server) and assert UI_EXTENSION_KEY === it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Extensions: clone the registry advertisement so builds never alias it (review)

Address the Phase 3 re-review's optional defensive nit: buildClientExtensions
returned the same object reference as the ADVERTISABLE_EXTENSIONS entry for the
ui advertisement, so an in-place mutation of a stamped value (e.g.
extensions[ui].mimeTypes) could corrupt the registry for later connections.
structuredClone the advertisement on stamp; add a test asserting a mutated
build doesn't leak into the next one. Nothing mutates it today — purely
defensive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* Extensions: add #1740 Connection Info proof screenshots

End-to-end smoke test in the web client against the legacy
advertised-extensions-http.json server: the Connection Info modal shows the new
era-transparent Server Extensions / Advertised Extensions section, with
Advertised Extensions listing io.modelcontextprotocol/tasks + io.modelcontextprotocol/ui
(the new ui advertisement), and only ui after toggling Tasks off and reconnecting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-23 10:11:36 -04:00 • committed by GitHub
commit e2c75da5ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -57,7 +57,9 @@ describe("ConnectionInfoContent", () => {
transport="stdio"
/>,
);
expect(screen.getByText("—")).toBeInTheDocument();
// Exactly three em dashes: the missing server version, plus the two
// extension sections (the fixtures advertise none on either side).
expect(screen.getAllByText("—")).toHaveLength(3);
});
it("renders server and client capability sections", () => {
@ -151,12 +153,9 @@ describe("ConnectionInfoContent", () => {
expect(screen.getByText("Sessionless")).toBeInTheDocument();
expect(screen.getByText("Discovery")).toBeInTheDocument();
expect(screen.getByText("2026-07-28, 2025-11-25")).toBeInTheDocument();
expect(
screen.getByText("io.modelcontextprotocol/tasks"),
).toBeInTheDocument();
});
it("renders em-dashes for empty supported versions and absent extensions", () => {
it("renders an em-dash for empty supported versions in Discovery", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
@ -171,9 +170,74 @@ describe("ConnectionInfoContent", () => {
/>,
);
expect(screen.getByText("Supported versions")).toBeInTheDocument();
expect(screen.getByText("Extensions")).toBeInTheDocument();
// Both the empty-versions and no-extensions values render as an em dash.
expect(screen.getAllByText("—").length).toBeGreaterThanOrEqual(2);
// Extensions moved out of Discovery into their own era-transparent section.
expect(screen.queryByText("Discovery")).toBeInTheDocument();
// Exactly three em dashes: empty supported versions, plus the two extension
// sections (the fixtures advertise none).
expect(screen.getAllByText("—")).toHaveLength(3);
});
it("shows server extensions on a LEGACY connection (no discovery), from server capabilities (#1740)", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={{
...fullResult,
capabilities: {
...fullResult.capabilities,
extensions: { "io.modelcontextprotocol/tasks": {} },
},
}}
clientCapabilities={fullClientCaps}
transport="streamable-http"
protocolEra="legacy"
/>,
);
// No discoverResult (legacy) but the server's extension still renders,
// sourced from the negotiated server capabilities rather than discovery.
expect(screen.queryByText("Discovery")).not.toBeInTheDocument();
expect(screen.getByText("Server Extensions")).toBeInTheDocument();
expect(
screen.getByText("io.modelcontextprotocol/tasks"),
).toBeInTheDocument();
});
it("shows the Inspector's own advertised extensions (#1740)", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
clientCapabilities={{
...fullClientCaps,
extensions: {
"io.modelcontextprotocol/tasks": {},
"io.modelcontextprotocol/ui": { mimeTypes: ["text/html"] },
},
}}
transport="streamable-http"
protocolEra="legacy"
/>,
);
expect(screen.getByText("Advertised Extensions")).toBeInTheDocument();
expect(
screen.getByText(
"io.modelcontextprotocol/tasks, io.modelcontextprotocol/ui",
),
).toBeInTheDocument();
});
it("renders em-dashes for the extensions sections when neither side advertises any (#1740)", () => {
renderWithMantine(
<ConnectionInfoContent
initializeResult={fullResult}
clientCapabilities={fullClientCaps}
transport="streamable-http"
protocolEra="legacy"
/>,
);
expect(screen.getByText("Server Extensions")).toBeInTheDocument();
expect(screen.getByText("Advertised Extensions")).toBeInTheDocument();
// Exactly two em dashes: the two extension sections (the server version is
// present in the fixture, so it does not em-dash).
expect(screen.getAllByText("—")).toHaveLength(2);
});
it("renders client registration kind when provided", () => {