Merge branch 'v2/main' into v2/fix/1919-rfc6570-uri-template-expansion

This commit is contained in:
Cliff Hall 2026-08-16 23:16:27 -04:00 • committed by GitHub
commit 63250a6c52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2391 additions and 126 deletions

View file

@ -31,5 +31,32 @@ export default defineConfig([
"react-refresh/only-export-components": "off",
},
},
{
// Type-aware pass for `no-floating-promises` (#1959). The rule needs type
// information, which `tseslint.configs.recommended` does not provide, and
// the parser needs a project that literally contains the linted file — so
// all four leaf projects are listed (`tsconfig.json` itself is a
// solution file with `files: []`, so it contains nothing).
//
// `.d.ts` is excluded: `vitest.shims.d.ts` is pulled in through a `types`
// reference rather than an `include`, so no project owns it, and a
// declaration file cannot float a promise anyway.
files: ["**/*.{ts,tsx}"],
ignores: ["**/*.d.ts"],
languageOptions: {
parserOptions: {
project: [
"./tsconfig.app.json",
"./tsconfig.node.json",
"./tsconfig.storybook.json",
"./tsconfig.test.json",
],
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-floating-promises": "error",
},
},
...storybook.configs["flat/recommended"],
]);

View file

@ -9,7 +9,6 @@ import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { randomBytes } from "node:crypto";
import open from "open";
import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { Hono } from "hono";
@ -22,6 +21,7 @@ import type { WebServerConfig } from "./web-server-config.js";
import {
webServerConfigToInitialPayload,
printServerBanner,
openBrowser,
} from "./web-server-config.js";
import type { WebServerHandle } from "./types.js";
@ -125,7 +125,9 @@ export async function startHonoServer(
sandboxUrl ?? undefined,
);
if (config.autoOpen) {
open(url);
// Never a bare `open(url)`: this callback is synchronous, so a
// rejection would escape it. See openBrowser.
openBrowser(url);
}
},
);

View file

@ -9,7 +9,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Plugin } from "vite";
import open from "open";
// Direct leaf import — the `core/mcp/remote/node/index.ts` barrel re-exports
// from `../constants.js`, which would otherwise drag the broader
// `core/mcp/remote/index.ts` barrel (including `createRemoteLogger`, with its
@ -22,6 +21,7 @@ import type { WebServerConfig } from "./web-server-config.js";
import {
webServerConfigToInitialPayload,
printServerBanner,
openBrowser,
} from "./web-server-config.js";
export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
@ -114,7 +114,9 @@ export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
);
if (config.autoOpen) {
open(url);
// Never a bare `open(url)`: `logBanner` is synchronous, so a
// rejection would escape it. See openBrowser.
openBrowser(url);
}
};

View file

@ -2,6 +2,7 @@
* Config object for the web server (dev and prod). Passed in-process; no env handoff.
*/
import open from "open";
import pino from "pino";
import type { Logger } from "pino";
import type { MCPConfig, MCPServerConfig } from "../../../core/mcp/types.ts";
@ -203,6 +204,24 @@ export function printServerBanner(
return url;
}
/**
* Launch the user's browser at `url`, reporting a failure rather than throwing.
*
* Both callers — the prod Hono server's `serve()` listen callback and the dev
* plugin's `logBanner` — are synchronous, so a rejection has nowhere to go and
* escapes as an unhandled rejection (#1959). On a headless or otherwise
* browser-less host that rejection is the ordinary outcome, not an edge case,
* and by the time it happens the server is already listening and
* `printServerBanner` has printed a perfectly usable URL. So warn and carry
* on. Shared between the two servers deliberately: two copies of a one-line
* `.catch` is two chances for one of them to drift back to a bare `open(url)`.
*/
export function openBrowser(url: string): void {
open(url).catch((err: unknown) => {
console.warn("Could not open a browser automatically:", err);
});
}
export interface BuildWebServerConfigOptions {
initialMcpConfig?: MCPServerConfig | null;
/** Catalog/session file the backend should serve; see {@link WebServerConfig.mcpConfigPath}. */

View file

@ -64,6 +64,7 @@ import { ResourceSubscriptionsState } from "@inspector/core/mcp/state/resourceSu
import {
cleanRoots,
oauthAuthorizationParamsFromSettings,
oauthEndpointOverridesFromSettings,
serializeMcpConfig,
} from "@inspector/core/mcp/serverList.js";
import type { ClientConfig } from "@inspector/core/client/types.js";
@ -2315,12 +2316,16 @@ function App() {
const serverAuthorizationParams = savedSettings
? oauthAuthorizationParamsFromSettings(savedSettings)
: undefined;
const serverEndpointOverrides = savedSettings
? oauthEndpointOverridesFromSettings(savedSettings)
: undefined;
const oauthFromServer =
savedSettings &&
(savedSettings.oauthClientId ||
savedSettings.oauthClientSecret ||
savedSettings.oauthScopes ||
serverAuthorizationParams ||
serverEndpointOverrides ||
savedSettings.enterpriseManaged)
? {
...(savedSettings.oauthClientId && {
@ -2335,6 +2340,7 @@ function App() {
...(serverAuthorizationParams && {
authorizationParams: serverAuthorizationParams,
}),
...serverEndpointOverrides,
...(savedSettings.enterpriseManaged && {
enterpriseManaged: true,
}),

View file

@ -14,7 +14,7 @@ export const Transport: Story = {
args: { category: "transport" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("transport")).toBeInTheDocument();
await expect(canvas.getByText("transport")).toBeInTheDocument();
},
};
@ -22,6 +22,6 @@ export const Auth: Story = {
args: { category: "auth" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("auth")).toBeInTheDocument();
await expect(canvas.getByText("auth")).toBeInTheDocument();
},
};

View file

@ -24,7 +24,7 @@ export const FullSize: Story = {
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Row 1")).toBeInTheDocument();
await expect(canvas.getByText("Row 1")).toBeInTheDocument();
},
};
@ -38,6 +38,6 @@ export const Embedded: Story = {
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Row 1")).toBeInTheDocument();
await expect(canvas.getByText("Row 1")).toBeInTheDocument();
},
};

View file

@ -15,7 +15,9 @@ export const Collapsed: Story = {
args: { expanded: false },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByRole("button", { name: "Expand" })).toBeInTheDocument();
await expect(
canvas.getByRole("button", { name: "Expand" }),
).toBeInTheDocument();
},
};
@ -23,7 +25,7 @@ export const Expanded: Story = {
args: { expanded: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByRole("button", { name: "Collapse" }),
).toBeInTheDocument();
},

View file

@ -14,7 +14,7 @@ export const HeaderMismatch: Story = {
args: { code: -32020, name: "HeaderMismatch" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("-32020 HeaderMismatch")).toBeInTheDocument();
await expect(canvas.getByText("-32020 HeaderMismatch")).toBeInTheDocument();
},
};
@ -22,7 +22,7 @@ export const MissingCapability: Story = {
args: { code: -32021, name: "MissingRequiredClientCapability" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByText("-32021 MissingRequiredClientCapability"),
).toBeInTheDocument();
},
@ -32,7 +32,7 @@ export const UnsupportedVersion: Story = {
args: { code: -32022, name: "UnsupportedProtocolVersion" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByText("-32022 UnsupportedProtocolVersion"),
).toBeInTheDocument();
},
@ -47,6 +47,6 @@ export const MethodNotFound: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("-32601 MethodNotFound")).toBeInTheDocument();
await expect(canvas.getByText("-32601 MethodNotFound")).toBeInTheDocument();
},
};

View file

@ -14,7 +14,7 @@ export const Outgoing: Story = {
args: { direction: "outgoing" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("client → server")).toBeInTheDocument();
await expect(canvas.getByText("client → server")).toBeInTheDocument();
},
};
@ -22,6 +22,6 @@ export const Incoming: Story = {
args: { direction: "incoming" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("server → client")).toBeInTheDocument();
await expect(canvas.getByText("server → client")).toBeInTheDocument();
},
};

View file

@ -14,7 +14,7 @@ export const ToolsList: Story = {
args: { method: "tools/list" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("tools/list")).toBeInTheDocument();
await expect(canvas.getByText("tools/list")).toBeInTheDocument();
},
};
@ -22,6 +22,6 @@ export const TasksList: Story = {
args: { method: "tasks/list" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("tasks/list")).toBeInTheDocument();
await expect(canvas.getByText("tasks/list")).toBeInTheDocument();
},
};

View file

@ -15,7 +15,7 @@ export const Closed: Story = {
args: { open: false },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByRole("button", { name: "Open monitoring sidebar" }),
).toBeInTheDocument();
},
@ -25,7 +25,7 @@ export const Open: Story = {
args: { open: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByRole("button", { name: "Close monitoring sidebar" }),
).toBeInTheDocument();
},

View file

@ -15,7 +15,9 @@ export const Unpinned: Story = {
args: { pinned: false },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByRole("button", { name: "Pin" })).toBeInTheDocument();
await expect(
canvas.getByRole("button", { name: "Pin" }),
).toBeInTheDocument();
},
};
@ -23,6 +25,8 @@ export const Pinned: Story = {
args: { pinned: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByRole("button", { name: "Unpin" })).toBeInTheDocument();
await expect(
canvas.getByRole("button", { name: "Unpin" }),
).toBeInTheDocument();
},
};

View file

@ -14,6 +14,8 @@ type Story = StoryObj<typeof ReplayButton>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByRole("button", { name: "Replay" })).toBeInTheDocument();
await expect(
canvas.getByRole("button", { name: "Replay" }),
).toBeInTheDocument();
},
};

View file

@ -44,7 +44,7 @@ export const Default: Story = {
render: () => <ResizeDemo />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByRole("separator", { name: "Resize demo panel" }),
).toBeInTheDocument();
},

View file

@ -48,7 +48,7 @@ export const FillsHostWidth: Story = {
// sharper discriminator: a row flex leaves the layer full-width while
// collapsing the child to the single "x").
const layer = child.parentElement as HTMLElement;
expect(child.getBoundingClientRect().width).toBeCloseTo(
await expect(child.getBoundingClientRect().width).toBeCloseTo(
layer.getBoundingClientRect().width,
0,
);
@ -77,11 +77,11 @@ export const FillVariant: Story = {
// Width still stretches onto the child (relative to the layer, as above);
// the `fill` height comes from `bottom: 0` making the layer span the host.
const layer = child.parentElement as HTMLElement;
expect(child.getBoundingClientRect().width).toBeCloseTo(
await expect(child.getBoundingClientRect().width).toBeCloseTo(
layer.getBoundingClientRect().width,
0,
);
expect(layer.getBoundingClientRect().height).toBeGreaterThan(160 - 4);
await expect(layer.getBoundingClientRect().height).toBeGreaterThan(160 - 4);
},
};
@ -97,6 +97,6 @@ export const Inactive: Story = {
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.queryByTestId("stage-child")).not.toBeInTheDocument();
await expect(canvas.queryByTestId("stage-child")).not.toBeInTheDocument();
},
};

View file

@ -114,8 +114,10 @@ export const ServerInfoNotReported: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.queryByText("my-catalog-name")).not.toBeInTheDocument();
expect(canvas.getAllByText(SERVER_INFO_NOT_REPORTED_LABEL)).toHaveLength(2);
await expect(canvas.queryByText("my-catalog-name")).not.toBeInTheDocument();
await expect(
canvas.getAllByText(SERVER_INFO_NOT_REPORTED_LABEL),
).toHaveLength(2);
},
};
@ -195,11 +197,11 @@ export const WithOAuthIdToken: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("ID Token")).toBeInTheDocument();
await expect(canvas.getByText("ID Token")).toBeInTheDocument();
// Only the ID token is a JWT here, so exactly one decode toggle is offered.
await userEvent.click(
canvas.getByRole("button", { name: "Decode JWT for ID Token" }),
);
expect(canvas.getByText(/"sub": "user-42"/)).toBeInTheDocument();
await expect(canvas.getByText(/"sub": "user-42"/)).toBeInTheDocument();
},
};

View file

@ -77,6 +77,6 @@ export const WithEntries: Story = {
},
// List scrollbar reserves a gutter and stays hidden when idle (#1474).
play: async ({ canvasElement }) => {
expectScrollbarGutterIdleHidden(canvasElement);
await expectScrollbarGutterIdleHidden(canvasElement);
},
};

View file

@ -31,10 +31,10 @@ export const AllVisible: Story = {
args: { visibleDirections: { client: true, server: true } },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
await expect(
canvas.getByRole("button", { name: "client → server" }),
).toBeInTheDocument();
expect(
await expect(
canvas.getByRole("button", { name: "Deselect All" }),
).toBeInTheDocument();
},

View file

@ -20,7 +20,7 @@ type Story = StoryObj<typeof MonitoringControls>;
export const AllTabs: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByRole("radio", { name: "Logs" })).toBeChecked();
await expect(canvas.getByRole("radio", { name: "Logs" })).toBeChecked();
},
};
@ -28,6 +28,6 @@ export const StdioServer: Story = {
args: { tabs: ["Logs", "Protocol"], value: "Protocol" },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull();
await expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull();
},
};

Some files were not shown because too many files have changed in this diff Show more