Merge branch 'v2/main' into v2/chore/2156-use-mcp-apps

This commit is contained in:
Cliff Hall 2026-08-26 23:58:41 -04:00 • committed by GitHub
commit d79e96b0f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 885 additions and 59 deletions

View file

@ -91,6 +91,36 @@ The top-level `src/types/` is a separate sibling — ambient `.d.ts` module stub
Nothing _enforces_ the boundary — no path alias keys off it, and the coverage `include` in `vite.config.ts` lists both directories, so a move between them is coverage-neutral. It's a human-legible import-time signal. See [`AGENTS.md`](../../AGENTS.md) for the full rule (including the whitelist caveat — a module placed outside `components`/`lib`/`utils`/`server` falls out of the ≥90 gate).
## Core tab automation contract
The Tools, Resources, and Prompts screens each expose a `data-testid` plus a
small set of `data-*` attributes, so a headless driver can `waitForSelector` on
a deterministic signal rather than on visible copy. `scripts/smoke-web-tabs.mjs`
drives all three against `test-servers/configs/web-tabs-http.json` ([#2148](https://github.com/modelcontextprotocol/inspector/issues/2148)).
Treat them as a public contract, for the same reason as the Apps ones below:
| Attribute | Where | Meaning |
| ------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-testid="tools-screen"` | Tools screen root | The element carrying the two attributes below. |
| `data-tool-count` | on `tools-screen` | How many tools the list holds. `0` is a *populated screen with an empty list* — distinct from the screen being absent, which is what a driver waits out. |
| `data-call-status` | on `tools-screen` | `idle` → `pending` → `ok` / `error` for the current `tools/call`. Always a value; an absent call state reads `idle`, never empty. |
| `data-testid="structured-output"` | result panel | The `structuredContent` section of a tool result ([#1908](https://github.com/modelcontextprotocol/inspector/issues/1908)). Absent when a result carries none. |
| `data-testid="resources-screen"` | Resources root | The element carrying the three attributes below. |
| `data-resource-count` | on `resources-screen` | Entries from `resources/list`. |
| `data-template-count` | on `resources-screen` | Entries from `resources/templates/list` — a **separate** call that can fail on its own, so it is reported separately. |
| `data-read-status` | on `resources-screen` | `idle` → `pending` → `ok` / `error` for the current `resources/read`. |
| `data-testid="resource-preview"` | preview panel | The read resource's **rendered** contents. Assert this *and* `data-read-status`: the status flips as soon as the RPC resolves, so it alone stays true with the panel removed. |
| `data-testid="prompts-screen"` | Prompts root | The element carrying the two attributes below. |
| `data-prompt-count` | on `prompts-screen`| Entries from `prompts/list`. |
| `data-get-status` | on `prompts-screen`| `idle` → `pending` → `ok` / `error` for the current `prompts/get`. |
| `data-testid="prompt-messages"` | messages panel | The fetched prompt's **rendered** messages — the `prompts/get` counterpart of `resource-preview`, and asserted alongside `data-get-status` for the same reason. |
Why attributes rather than text: a smoke that waited on a label fails the next
time the label is reworded, which is noise rather than signal — and it fails as
an opaque timeout, because there is nothing to compare against. The screen tests
pin each attribute name for the same reason: a rename should fail loudly in a
unit test, not silently in a five-minute gate.
## MCP Apps screen automation contract
The Apps screen exposes a small, stable set of `data-testid` / `data-*` attributes so an automated driver (deep-link auto-open, CI review harness) can `waitForSelector` on a deterministic signal instead of sleeping. Treat these as a public contract — drivers depend on them staying stable:

View file

@ -64,3 +64,14 @@ describe("PromptMessagesDisplay", () => {
).not.toBeInTheDocument();
});
});
// The `data-testid` the headless tab smoke waits on (#2148). It asserts the
// messages RENDERED, which `data-get-status="ok"` alone does not — that flips
// as soon as `prompts/get` resolves. Pinned here so a rename fails loudly in a
// unit test rather than as an opaque timeout in a five-minute gate.
describe("automation contract (#2148)", () => {
it("exposes a stable data-testid on the panel", () => {
renderWithMantine(<PromptMessagesDisplay messages={messages} />);
expect(screen.getByTestId("prompt-messages")).toBeInTheDocument();
});
});

View file

@ -67,7 +67,10 @@ export function PromptMessagesDisplay({
onClose,
}: PromptMessagesDisplayProps) {
return (
<PanelStack>
// `data-testid` so a driver can wait on the messages having actually
// RENDERED, not merely on `prompts/get` having returned (#2148) — see the
// ResourcePreviewPanel counterpart for why the RPC status is not enough.
<PanelStack data-testid="prompt-messages">
<HeaderRow>
<HeaderLeft>
{onClose && (

View file

@ -566,3 +566,22 @@ describe("ResourcePreviewPanel", () => {
});
});
});
// The `data-testid` the headless tab smoke waits on (#2148) — see the
// PromptMessagesDisplay counterpart: `data-read-status="ok"` flips as soon as
// `resources/read` resolves, so only this proves the preview rendered.
describe("automation contract (#2148)", () => {
it("exposes a stable data-testid on the panel", () => {
renderWithMantine(
<ResourcePreviewPanel
resource={textResource}
contents={textContents}
isSubscribed={false}
onRefresh={vi.fn()}
onSubscribe={vi.fn()}
onUnsubscribe={vi.fn()}
/>,
);
expect(screen.getByTestId("resource-preview")).toBeInTheDocument();
});
});

View file

@ -222,7 +222,12 @@ export function ResourcePreviewPanel({
const sourceToggleable = isSourceToggleable(mimeType);
return (
<PanelStack>
// `data-testid` so a driver can wait on the preview having actually
// RENDERED, not merely on `resources/read` having returned (#2148). The
// read status flips as soon as the RPC resolves, so a smoke stopping there
// would stay green with this panel removed. A `data-*` attribute is not
// part of a component's typed props, so it is passed at the call site.
<PanelStack data-testid="resource-preview">
<HeaderRow>
<HeaderLeft>
{onClose && (

View file

@ -85,3 +85,15 @@ describe("StructuredOutputPanel", () => {
expect(screen.getByTestId("code-highlight")).toHaveTextContent("{}");
});
});
// The `data-testid` the headless tab smoke waits on (#2148). Asserting the
// section by its heading copy would make the smoke fail on a rewording, so the
// smoke keys off this attribute — pinned here so a rename fails loudly.
describe("automation contract (#2148)", () => {
it("exposes a stable data-testid on the section", () => {
renderWithMantine(
<StructuredOutputPanel structuredContent={structuredContent} />,
);
expect(screen.getByTestId("structured-output")).toBeInTheDocument();
});
});

View file

@ -72,7 +72,10 @@ export function StructuredOutputPanel({
const [expanded, setExpanded] = useState(defaultExpanded);
return (
<StructuredBox>
// `data-testid` so the headless tab smoke (#2148) can assert this section
// rendered without matching on its heading copy. A `data-*` attribute is
// not part of a component's typed props, so it is passed at the call site.
<StructuredBox data-testid="structured-output">
<StructuredInner>
<HeaderRow>
<StructuredHeader>Structured Output</StructuredHeader>

View file

@ -391,3 +391,34 @@ describe("PromptsScreen", () => {
});
});
});
// See the ToolsScreen counterpart: the `data-*` readiness contract the headless
// tab smoke drives (#2148), pinned here so a rename fails loudly.
describe("automation contract (#2148)", () => {
it("reports the prompt count and an idle get status", () => {
renderWithMantine(<PromptsScreen {...baseProps} />);
const root = screen.getByTestId("prompts-screen");
expect(root).toHaveAttribute(
"data-prompt-count",
String(promptsWithArgs.length),
);
expect(root).toHaveAttribute("data-get-status", "idle");
});
it("tracks the get status through to a result", () => {
renderWithMantine(
<PromptsScreen
{...baseProps}
getPromptState={{
status: "ok",
promptName: promptsWithArgs[0].name,
result: { messages: [] },
}}
/>,
);
expect(screen.getByTestId("prompts-screen")).toHaveAttribute(
"data-get-status",
"ok",
);
});
});

View file

@ -274,7 +274,13 @@ export function PromptsScreen({
}
return (
<ScreenLayout>
// `data-*` readiness contract for the headless tab smoke (#2148); see
// clients/web/README.md#core-tab-automation-contract.
<ScreenLayout
data-testid="prompts-screen"
data-prompt-count={prompts.length}
data-get-status={getPromptState?.status ?? "idle"}
>
<Sidebar>
<SidebarCard>
<PromptControls

View file

@ -403,3 +403,36 @@ describe("ResourcesScreen", () => {
).not.toBeInTheDocument();
});
});
// See the ToolsScreen counterpart: the `data-*` readiness contract the headless
// tab smoke drives (#2148), pinned here so a rename fails loudly.
describe("automation contract (#2148)", () => {
it("reports the resource and template counts separately", () => {
renderWithMantine(<ResourcesScreen {...baseProps} />);
const root = screen.getByTestId("resources-screen");
// Two attributes rather than one: `resources/list` and
// `resources/templates/list` are separate calls and either can fail alone.
expect(root).toHaveAttribute(
"data-resource-count",
String(resources.length),
);
expect(root).toHaveAttribute(
"data-template-count",
String(templates.length),
);
expect(root).toHaveAttribute("data-read-status", "idle");
});
it("tracks the read status through to a result", () => {
renderWithMantine(
<ResourcesScreen
{...baseProps}
readState={{ status: "ok", uri: "file:///x", result: okResult }}
/>,
);
expect(screen.getByTestId("resources-screen")).toHaveAttribute(
"data-read-status",
"ok",
);
});
});

View file

@ -319,7 +319,14 @@ export function ResourcesScreen({
}
return (
<ScreenLayout>
// `data-*` readiness contract for the headless tab smoke (#2148); see
// clients/web/README.md#core-tab-automation-contract.
<ScreenLayout
data-testid="resources-screen"
data-resource-count={resources.length}
data-template-count={templates.length}
data-read-status={readState?.status ?? "idle"}
>
<Sidebar>
<SidebarCard>
<ResourceControls

View file

@ -445,3 +445,41 @@ describe("ToolsScreen", () => {
expect(screen.getByText("invalid_header_tool")).toBeInTheDocument();
});
});
// The `data-*` readiness contract the headless tab smoke drives (#2148). It is
// a documented public contract (clients/web/README.md), and a smoke asserting
// it fails as an opaque 45s timeout rather than a mismatch — so the attribute
// names are pinned here, where a rename fails loudly instead.
describe("automation contract (#2148)", () => {
it("reports the tool count and an idle call status", () => {
renderWithMantine(<ToolsScreen {...baseProps} />);
const root = screen.getByTestId("tools-screen");
expect(root).toHaveAttribute("data-tool-count", String(tools.length));
// Absent call state reads `idle`, not empty: the smoke waits on a value.
expect(root).toHaveAttribute("data-call-status", "idle");
});
it("reports an empty list distinctly from a missing one", () => {
renderWithMantine(<ToolsScreen {...baseProps} tools={[]} />);
expect(screen.getByTestId("tools-screen")).toHaveAttribute(
"data-tool-count",
"0",
);
});
it("tracks the call status through to a result", () => {
renderWithMantine(
<ToolsScreen
{...baseProps}
callState={{
status: "ok",
result: { content: [{ type: "text", text: "done" }] },
}}
/>,
);
expect(screen.getByTestId("tools-screen")).toHaveAttribute(
"data-call-status",
"ok",
);
});
});

View file

@ -198,7 +198,15 @@ export function ToolsScreen({
};
return (
<ScreenLayout>
// `data-*` readiness contract for the headless tab smoke (#2148); see
// clients/web/README.md#core-tab-automation-contract. Attributes are not
// part of a component's typed props, so they are passed here rather than
// baked into the `ScreenLayout` constant.
<ScreenLayout
data-testid="tools-screen"
data-tool-count={tools.length}
data-call-status={callState?.status ?? "idle"}
>
<Sidebar>
<SidebarCard>
<ToolControls