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).
|
||||
|
|
|
|||
|
|
@ -31,6 +31,21 @@ const fullClientCaps: ClientCapabilities = {
|
|||
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", () => {
|
||||
it("renders server implementation fields under the heading", () => {
|
||||
renderWithMantine(
|
||||
|
|
@ -181,6 +196,96 @@ describe("ConnectionInfoContent", () => {
|
|||
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", () => {
|
||||
renderWithMantine(
|
||||
<ConnectionInfoContent
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import type {
|
|||
DiscoverResult,
|
||||
InitializeResult,
|
||||
ProtocolEra,
|
||||
ServerCapabilities,
|
||||
} from "@modelcontextprotocol/client";
|
||||
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 {
|
||||
CapabilityItem,
|
||||
|
|
@ -189,16 +191,62 @@ const CLIENT_CAPABILITY_KEYS: CapabilityKey[] = [
|
|||
export const CLEAR_OAUTH_STATE_AND_DISCONNECT_LABEL =
|
||||
"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(
|
||||
capabilities: Record<string, unknown>,
|
||||
knownKeys: CapabilityKey[],
|
||||
): { capability: CapabilityKey; supported: boolean }[] {
|
||||
return knownKeys.map((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({
|
||||
initializeResult,
|
||||
serverInfoReported = true,
|
||||
|
|
@ -229,7 +277,7 @@ export function ConnectionInfoContent({
|
|||
? serverInfo.version?.trim() || "—"
|
||||
: SERVER_INFO_NOT_REPORTED_LABEL;
|
||||
|
||||
const serverCaps = getCapabilityEntries(capabilities, SERVER_CAPABILITY_KEYS);
|
||||
const serverCaps = getServerCapabilityEntries(capabilities, protocolEra);
|
||||
const clientCaps = getCapabilityEntries(
|
||||
clientCapabilities,
|
||||
CLIENT_CAPABILITY_KEYS,
|
||||
|
|
|
|||
Loading…
Reference in a new issue