* 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>
127 lines
3.4 KiB
TypeScript
127 lines
3.4 KiB
TypeScript
import type {
|
|
ClientCapabilities,
|
|
InitializeResult,
|
|
} from "@modelcontextprotocol/client";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import { ConnectionInfoContent } from "./ConnectionInfoContent";
|
|
|
|
const fullResult: InitializeResult = {
|
|
protocolVersion: "2025-03-26",
|
|
serverInfo: { name: "Everything Server", version: "2.1.0" },
|
|
capabilities: {
|
|
tools: { listChanged: true },
|
|
resources: { subscribe: true, listChanged: true },
|
|
prompts: { listChanged: true },
|
|
logging: {},
|
|
completions: {},
|
|
},
|
|
instructions:
|
|
"This server provides access to the project management system. Use the list_projects tool first to discover available projects before querying tasks. Rate limiting applies: max 60 requests per minute.",
|
|
};
|
|
|
|
const fullClientCaps: ClientCapabilities = {
|
|
roots: { listChanged: true },
|
|
sampling: {},
|
|
elicitation: {},
|
|
experimental: {},
|
|
};
|
|
|
|
const meta: Meta<typeof ConnectionInfoContent> = {
|
|
title: "Groups/ConnectionInfoContent",
|
|
component: ConnectionInfoContent,
|
|
};
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof ConnectionInfoContent>;
|
|
|
|
export const FullCapabilities: Story = {
|
|
args: {
|
|
initializeResult: fullResult,
|
|
clientCapabilities: fullClientCaps,
|
|
transport: "stdio",
|
|
},
|
|
};
|
|
|
|
export const ModernEra: Story = {
|
|
args: {
|
|
initializeResult: {
|
|
protocolVersion: "2026-07-28",
|
|
serverInfo: { name: "Modern Server", version: "2.0.0" },
|
|
capabilities: {
|
|
tools: { listChanged: true },
|
|
resources: { subscribe: true },
|
|
// The Server Extensions row reads the negotiated server capabilities
|
|
// (era-transparent), not discoverResult — carry it here too so the
|
|
// modern story demonstrates the extension. (#1740)
|
|
extensions: {
|
|
"io.modelcontextprotocol/tasks": {},
|
|
},
|
|
},
|
|
},
|
|
clientCapabilities: fullClientCaps,
|
|
transport: "streamable-http",
|
|
protocolEra: "modern",
|
|
discoverResult: {
|
|
supportedVersions: ["2026-07-28", "2025-11-25"],
|
|
serverInfo: { name: "Modern Server", version: "2.0.0" },
|
|
capabilities: {
|
|
tools: { listChanged: true },
|
|
resources: { subscribe: true },
|
|
extensions: {
|
|
"io.modelcontextprotocol/tasks": {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
export const MinimalCapabilities: Story = {
|
|
args: {
|
|
initializeResult: {
|
|
protocolVersion: "2025-03-26",
|
|
serverInfo: { name: "Simple Server", version: "1.0.0" },
|
|
capabilities: {
|
|
tools: { listChanged: false },
|
|
},
|
|
},
|
|
clientCapabilities: {
|
|
roots: { listChanged: true },
|
|
},
|
|
transport: "streamable-http",
|
|
},
|
|
};
|
|
|
|
export const WithInstructions: Story = {
|
|
args: {
|
|
initializeResult: fullResult,
|
|
clientCapabilities: {
|
|
roots: { listChanged: true },
|
|
sampling: {},
|
|
},
|
|
transport: "stdio",
|
|
},
|
|
};
|
|
|
|
export const WithOAuth: Story = {
|
|
args: {
|
|
initializeResult: {
|
|
protocolVersion: "2025-03-26",
|
|
serverInfo: { name: "Authenticated Server", version: "3.0.0" },
|
|
capabilities: {
|
|
tools: { listChanged: true },
|
|
resources: { subscribe: true },
|
|
},
|
|
},
|
|
clientCapabilities: {
|
|
roots: { listChanged: true },
|
|
},
|
|
transport: "streamable-http",
|
|
oauth: {
|
|
protocol: "standard",
|
|
authorized: true,
|
|
authUrl: "https://auth.example.com/oauth2/authorize",
|
|
scopes: ["read", "write", "admin"],
|
|
accessToken: "eyJhbGciOiJSUzI1NiIs...truncated",
|
|
},
|
|
},
|
|
};
|