Merge remote-tracking branch 'origin/v2/fix/1919-rfc6570-uri-template-expansion' into v2/fix/1919-rfc6570-uri-template-expansion
This commit is contained in:
commit
1d29e4e71c
3 changed files with 163 additions and 2 deletions
|
|
@ -77,6 +77,14 @@ export const ModernEra: Story = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// SEP-2663 moved task support to the extension map, so the Tasks capability
|
||||||
|
// row must read it there on a modern connection — otherwise a tasks-capable
|
||||||
|
// server shows ✗ while listing the extension below (#1887).
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const tasksRow = canvas.getByText("Tasks");
|
||||||
|
await expect(tasksRow.previousElementSibling).toHaveTextContent("✓");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// A modern server that omitted the optional `_meta` serverInfo stamp (#1772).
|
// A modern server that omitted the optional `_meta` serverInfo stamp (#1772).
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,21 @@ const fullClientCaps: ClientCapabilities = {
|
||||||
experimental: {},
|
experimental: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The glyphs `CapabilityItem` renders beside each capability label.
|
||||||
|
const SUPPORTED_MARK = "✓";
|
||||||
|
const UNSUPPORTED_MARK = "✗";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the ✓/✗ a capability row is showing. The mark is the label's preceding
|
||||||
|
* sibling inside the row `Group`, so asserting on it — rather than on the
|
||||||
|
* label's mere presence — is what distinguishes "supported" from "listed".
|
||||||
|
*/
|
||||||
|
function capabilityMark(label: string): string | undefined {
|
||||||
|
return (
|
||||||
|
screen.getByText(label).previousElementSibling?.textContent ?? undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe("ConnectionInfoContent", () => {
|
describe("ConnectionInfoContent", () => {
|
||||||
it("renders server implementation fields under the heading", () => {
|
it("renders server implementation fields under the heading", () => {
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
|
|
@ -181,6 +196,96 @@ describe("ConnectionInfoContent", () => {
|
||||||
expect(screen.getByText("Sampling")).toBeInTheDocument();
|
expect(screen.getByText("Sampling")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks Tasks supported on a modern connection that advertises the tasks extension (#1887)", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ConnectionInfoContent
|
||||||
|
initializeResult={{
|
||||||
|
...fullResult,
|
||||||
|
capabilities: {
|
||||||
|
...fullResult.capabilities,
|
||||||
|
// SEP-2663: no top-level `tasks` key — support is the extension.
|
||||||
|
extensions: { "io.modelcontextprotocol/tasks": {} },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
clientCapabilities={fullClientCaps}
|
||||||
|
transport="streamable-http"
|
||||||
|
protocolEra="modern"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(capabilityMark("Tasks")).toBe(SUPPORTED_MARK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still marks Tasks unsupported on a modern connection without the extension (#1887)", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ConnectionInfoContent
|
||||||
|
initializeResult={fullResult}
|
||||||
|
clientCapabilities={fullClientCaps}
|
||||||
|
transport="streamable-http"
|
||||||
|
protocolEra="modern"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(capabilityMark("Tasks")).toBe(UNSUPPORTED_MARK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not read the tasks extension on a legacy connection (#1887)", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ConnectionInfoContent
|
||||||
|
initializeResult={{
|
||||||
|
...fullResult,
|
||||||
|
capabilities: {
|
||||||
|
...fullResult.capabilities,
|
||||||
|
extensions: { "io.modelcontextprotocol/tasks": {} },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
clientCapabilities={fullClientCaps}
|
||||||
|
transport="streamable-http"
|
||||||
|
protocolEra="legacy"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// Legacy tasks support is `capabilities.tasks`, which this server omits —
|
||||||
|
// an extension key alone must not turn the row green.
|
||||||
|
expect(capabilityMark("Tasks")).toBe(UNSUPPORTED_MARK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks Tasks supported on a legacy connection from capabilities.tasks", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ConnectionInfoContent
|
||||||
|
initializeResult={{
|
||||||
|
...fullResult,
|
||||||
|
capabilities: {
|
||||||
|
...fullResult.capabilities,
|
||||||
|
// `ServerTasksCapability`'s sub-capabilities are objects, not
|
||||||
|
// booleans — `{}` is the "supported, no sub-options" shape.
|
||||||
|
tasks: { list: {}, cancel: {} },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
clientCapabilities={fullClientCaps}
|
||||||
|
transport="streamable-http"
|
||||||
|
protocolEra="legacy"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(capabilityMark("Tasks")).toBe(SUPPORTED_MARK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not extension-promote a capability other than tasks (#1887)", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<ConnectionInfoContent
|
||||||
|
initializeResult={{
|
||||||
|
...fullResult,
|
||||||
|
capabilities: {
|
||||||
|
tools: { listChanged: true },
|
||||||
|
extensions: { "io.modelcontextprotocol/ui": {} },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
clientCapabilities={fullClientCaps}
|
||||||
|
transport="streamable-http"
|
||||||
|
protocolEra="modern"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(capabilityMark("Prompts")).toBe(UNSUPPORTED_MARK);
|
||||||
|
expect(capabilityMark("Tasks")).toBe(UNSUPPORTED_MARK);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders instructions when present", () => {
|
it("renders instructions when present", () => {
|
||||||
renderWithMantine(
|
renderWithMantine(
|
||||||
<ConnectionInfoContent
|
<ConnectionInfoContent
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ import type {
|
||||||
DiscoverResult,
|
DiscoverResult,
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
ProtocolEra,
|
ProtocolEra,
|
||||||
|
ServerCapabilities,
|
||||||
} from "@modelcontextprotocol/client";
|
} from "@modelcontextprotocol/client";
|
||||||
import type { ServerType } from "@inspector/core/mcp/types.js";
|
import type { ServerType } from "@inspector/core/mcp/types.js";
|
||||||
|
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
|
||||||
import type { OAuthClientRegistrationKind } from "@inspector/core/auth/types.js";
|
import type { OAuthClientRegistrationKind } from "@inspector/core/auth/types.js";
|
||||||
import {
|
import {
|
||||||
CapabilityItem,
|
CapabilityItem,
|
||||||
|
|
@ -189,16 +191,62 @@ const CLIENT_CAPABILITY_KEYS: CapabilityKey[] = [
|
||||||
export const CLEAR_OAUTH_STATE_AND_DISCONNECT_LABEL =
|
export const CLEAR_OAUTH_STATE_AND_DISCONNECT_LABEL =
|
||||||
"Clear OAuth state and disconnect";
|
"Clear OAuth state and disconnect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server capabilities the modern (2026-07-28) era expresses as a negotiated
|
||||||
|
* *extension* rather than a top-level `capabilities` key. `tasks` is the only
|
||||||
|
* one today: SEP-2663 moved task support to
|
||||||
|
* `capabilities.extensions["io.modelcontextprotocol/tasks"]`, so a modern
|
||||||
|
* tasks-capable server left the Tasks row showing a red ✗ while the very same
|
||||||
|
* extension id was listed under "Server Extensions" two sections below
|
||||||
|
* (#1887). Keyed the same way `InspectorClient.isTasksExtensionNegotiated()`
|
||||||
|
* gates the Tasks tab, so the checkmark and the tab agree.
|
||||||
|
*/
|
||||||
|
const MODERN_EXTENSION_BACKED_CAPABILITIES: Partial<
|
||||||
|
Record<CapabilityKey, string>
|
||||||
|
> = {
|
||||||
|
tasks: TASKS_EXTENSION_KEY,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isCapabilityPresent(
|
||||||
|
capabilities: Record<string, unknown>,
|
||||||
|
key: CapabilityKey,
|
||||||
|
): boolean {
|
||||||
|
return key in capabilities && capabilities[key] != null;
|
||||||
|
}
|
||||||
|
|
||||||
function getCapabilityEntries(
|
function getCapabilityEntries(
|
||||||
capabilities: Record<string, unknown>,
|
capabilities: Record<string, unknown>,
|
||||||
knownKeys: CapabilityKey[],
|
knownKeys: CapabilityKey[],
|
||||||
): { capability: CapabilityKey; supported: boolean }[] {
|
): { capability: CapabilityKey; supported: boolean }[] {
|
||||||
return knownKeys.map((key) => ({
|
return knownKeys.map((key) => ({
|
||||||
capability: key,
|
capability: key,
|
||||||
supported: key in capabilities && capabilities[key] != null,
|
supported: isCapabilityPresent(capabilities, key),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side capability entries. Same presence rule as the client column,
|
||||||
|
* plus the modern extension fallback above — gated on the negotiated era so a
|
||||||
|
* legacy connection is still judged purely on its `initialize` capabilities.
|
||||||
|
*/
|
||||||
|
function getServerCapabilityEntries(
|
||||||
|
capabilities: ServerCapabilities,
|
||||||
|
era: ProtocolEra | undefined,
|
||||||
|
): { capability: CapabilityKey; supported: boolean }[] {
|
||||||
|
const modern = isModernEra(era);
|
||||||
|
return SERVER_CAPABILITY_KEYS.map((key) => {
|
||||||
|
const extensionKey = MODERN_EXTENSION_BACKED_CAPABILITIES[key];
|
||||||
|
const viaExtension =
|
||||||
|
modern &&
|
||||||
|
extensionKey !== undefined &&
|
||||||
|
capabilities.extensions?.[extensionKey] != null;
|
||||||
|
return {
|
||||||
|
capability: key,
|
||||||
|
supported: isCapabilityPresent(capabilities, key) || viaExtension,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function ConnectionInfoContent({
|
export function ConnectionInfoContent({
|
||||||
initializeResult,
|
initializeResult,
|
||||||
serverInfoReported = true,
|
serverInfoReported = true,
|
||||||
|
|
@ -229,7 +277,7 @@ export function ConnectionInfoContent({
|
||||||
? serverInfo.version?.trim() || "—"
|
? serverInfo.version?.trim() || "—"
|
||||||
: SERVER_INFO_NOT_REPORTED_LABEL;
|
: SERVER_INFO_NOT_REPORTED_LABEL;
|
||||||
|
|
||||||
const serverCaps = getCapabilityEntries(capabilities, SERVER_CAPABILITY_KEYS);
|
const serverCaps = getServerCapabilityEntries(capabilities, protocolEra);
|
||||||
const clientCaps = getCapabilityEntries(
|
const clientCaps = getCapabilityEntries(
|
||||||
clientCapabilities,
|
clientCapabilities,
|
||||||
CLIENT_CAPABILITY_KEYS,
|
CLIENT_CAPABILITY_KEYS,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue