test: isolate optional SDK role dependencies
This commit is contained in:
parent
43eed45404
commit
7664bb1010
11 changed files with 209 additions and 9 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import { cpSync, mkdirSync } from "node:fs";
|
import { cpSync, mkdirSync, rmSync } from "node:fs";
|
||||||
|
|
||||||
|
// Avoid publishing artifacts left behind by an earlier build or branch.
|
||||||
|
rmSync("dist", { recursive: true, force: true });
|
||||||
|
|
||||||
// Run TypeScript compiler for type declarations
|
// Run TypeScript compiler for type declarations
|
||||||
await $`tsc`;
|
await $`tsc`;
|
||||||
|
|
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -60,6 +60,12 @@
|
||||||
"zod": "^4.2.0"
|
"zod": "^4.2.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
"@modelcontextprotocol/client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@modelcontextprotocol/server": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
"prepack": "npm run build",
|
"prepack": "npm run build",
|
||||||
"build:all": "npm run examples:build",
|
"build:all": "npm run examples:build",
|
||||||
"test": "bun test src examples",
|
"test": "bun test src examples",
|
||||||
|
"test:dependency-isolation": "npm run build && node scripts/check-dependency-isolation.mjs",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:update": "playwright test --update-snapshots",
|
"test:e2e:update": "playwright test --update-snapshots",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
|
@ -118,6 +119,12 @@
|
||||||
"zod": "^4.2.0"
|
"zod": "^4.2.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
"@modelcontextprotocol/client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@modelcontextprotocol/server": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
159
scripts/check-dependency-isolation.mjs
Normal file
159
scripts/check-dependency-isolation.mjs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
mkdtempSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
const root = resolve(import.meta.dirname, "..");
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
readFileSync(join(root, "package.json"), "utf8"),
|
||||||
|
);
|
||||||
|
const client = "@modelcontextprotocol/client";
|
||||||
|
const server = "@modelcontextprotocol/server";
|
||||||
|
|
||||||
|
for (const role of [client, server]) {
|
||||||
|
if (!packageJson.peerDependencies?.[role]) {
|
||||||
|
throw new Error(`${role} must remain a peer dependency`);
|
||||||
|
}
|
||||||
|
if (packageJson.peerDependenciesMeta?.[role]?.optional !== true) {
|
||||||
|
throw new Error(`${role} must be an optional peer dependency`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-"));
|
||||||
|
try {
|
||||||
|
const npmEnvironment = {
|
||||||
|
...process.env,
|
||||||
|
npm_config_cache: join(temporaryRoot, "npm-cache"),
|
||||||
|
};
|
||||||
|
const packOutput = JSON.parse(
|
||||||
|
execFileSync(
|
||||||
|
"npm",
|
||||||
|
[
|
||||||
|
"pack",
|
||||||
|
"--ignore-scripts",
|
||||||
|
"--json",
|
||||||
|
"--pack-destination",
|
||||||
|
temporaryRoot,
|
||||||
|
],
|
||||||
|
{ cwd: root, encoding: "utf8", env: npmEnvironment },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const tarball = join(temporaryRoot, packOutput[0].filename);
|
||||||
|
|
||||||
|
const consumers = [
|
||||||
|
{
|
||||||
|
name: "app-only",
|
||||||
|
dependencies: {
|
||||||
|
"@types/node": packageJson.devDependencies["@types/node"],
|
||||||
|
[client]: packageJson.devDependencies[client],
|
||||||
|
"@modelcontextprotocol/ext-apps": `file:${tarball}`,
|
||||||
|
},
|
||||||
|
absent: server,
|
||||||
|
entry:
|
||||||
|
'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; console.log(App, AppBridge);',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server-only",
|
||||||
|
dependencies: {
|
||||||
|
"@types/node": packageJson.devDependencies["@types/node"],
|
||||||
|
"@modelcontextprotocol/ext-apps": `file:${tarball}`,
|
||||||
|
[server]: packageJson.devDependencies[server],
|
||||||
|
},
|
||||||
|
absent: client,
|
||||||
|
entry:
|
||||||
|
'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const consumer of consumers) {
|
||||||
|
const directory = join(temporaryRoot, consumer.name);
|
||||||
|
mkdirSync(directory);
|
||||||
|
writeFileSync(
|
||||||
|
join(directory, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
dependencies: consumer.dependencies,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
execFileSync(
|
||||||
|
"npm",
|
||||||
|
[
|
||||||
|
"install",
|
||||||
|
"--ignore-scripts",
|
||||||
|
"--package-lock=false",
|
||||||
|
"--no-audit",
|
||||||
|
"--no-fund",
|
||||||
|
],
|
||||||
|
{ cwd: directory, stdio: "pipe", env: npmEnvironment },
|
||||||
|
);
|
||||||
|
|
||||||
|
const absentPath = join(
|
||||||
|
directory,
|
||||||
|
"node_modules",
|
||||||
|
...consumer.absent.split("/"),
|
||||||
|
"package.json",
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
readFileSync(absentPath);
|
||||||
|
throw new Error(
|
||||||
|
`${consumer.name} unexpectedly installed ${consumer.absent}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== "ENOENT") throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(join(directory, "entry.ts"), consumer.entry);
|
||||||
|
writeFileSync(
|
||||||
|
join(directory, "tsconfig.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
compilerOptions: {
|
||||||
|
lib: ["ES2020", "DOM"],
|
||||||
|
module: "ESNext",
|
||||||
|
moduleResolution: "bundler",
|
||||||
|
noEmit: true,
|
||||||
|
skipLibCheck: false,
|
||||||
|
strict: true,
|
||||||
|
target: "ES2020",
|
||||||
|
},
|
||||||
|
include: ["entry.ts"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[join(root, "node_modules", "typescript", "bin", "tsc")],
|
||||||
|
{ cwd: directory, stdio: "inherit" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const metafile = join(directory, "bundle-meta.json");
|
||||||
|
execFileSync(
|
||||||
|
join(root, "node_modules", ".bin", "esbuild"),
|
||||||
|
[
|
||||||
|
"entry.ts",
|
||||||
|
"--bundle",
|
||||||
|
"--platform=browser",
|
||||||
|
"--outfile=bundle.js",
|
||||||
|
`--metafile=${metafile}`,
|
||||||
|
],
|
||||||
|
{ cwd: directory, stdio: "pipe" },
|
||||||
|
);
|
||||||
|
const bundleInputs = Object.keys(
|
||||||
|
JSON.parse(readFileSync(metafile, "utf8")).inputs,
|
||||||
|
).join("\n");
|
||||||
|
if (bundleInputs.includes(`/node_modules/${consumer.absent}/`)) {
|
||||||
|
throw new Error(
|
||||||
|
`${consumer.name} unexpectedly bundled ${consumer.absent}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Role peer dependency isolation checks passed.");
|
||||||
|
|
@ -89,8 +89,8 @@ import {
|
||||||
McpUiToolMeta,
|
McpUiToolMeta,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app";
|
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants";
|
||||||
import { RESOURCE_URI_META_KEY } from "./app";
|
import { RESOURCE_URI_META_KEY } from "./constants";
|
||||||
export { PostMessageTransport } from "./message-transport";
|
export { PostMessageTransport } from "./message-transport";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
type Transport,
|
type Transport,
|
||||||
} from "@modelcontextprotocol/client";
|
} from "@modelcontextprotocol/client";
|
||||||
import { EmptyResultSchema } from "@modelcontextprotocol/core";
|
import { EmptyResultSchema } from "@modelcontextprotocol/core";
|
||||||
|
export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants";
|
||||||
import { EventDispatcher } from "./events";
|
import { EventDispatcher } from "./events";
|
||||||
export { EventDispatcher } from "./events";
|
export { EventDispatcher } from "./events";
|
||||||
import { PostMessageTransport } from "./message-transport";
|
import { PostMessageTransport } from "./message-transport";
|
||||||
|
|
@ -158,7 +159,6 @@ export {
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIME type for MCP UI resources.
|
* MIME type for MCP UI resources.
|
||||||
|
|
@ -167,7 +167,6 @@ export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
||||||
*
|
*
|
||||||
* Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources.
|
* Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources.
|
||||||
*/
|
*/
|
||||||
export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for configuring {@link App `App`} behavior.
|
* Options for configuring {@link App `App`} behavior.
|
||||||
|
|
|
||||||
5
src/constants.ts
Normal file
5
src/constants.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/** Legacy metadata key associating an MCP tool with its App resource. */
|
||||||
|
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
||||||
|
|
||||||
|
/** MIME type for MCP App HTML resources. */
|
||||||
|
export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
||||||
20
src/core-types.ts
Normal file
20
src/core-types.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import {
|
||||||
|
CallToolResultSchema,
|
||||||
|
ContentBlockSchema,
|
||||||
|
EmbeddedResourceSchema,
|
||||||
|
ImplementationSchema,
|
||||||
|
RequestIdSchema,
|
||||||
|
ResourceLinkSchema,
|
||||||
|
ToolSchema,
|
||||||
|
} from "@modelcontextprotocol/core";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
|
// Infer shared wire types from the public role-neutral schemas so declarations
|
||||||
|
// used by every entrypoint do not acquire a client or server package edge.
|
||||||
|
export type CallToolResult = z.infer<typeof CallToolResultSchema>;
|
||||||
|
export type ContentBlock = z.infer<typeof ContentBlockSchema>;
|
||||||
|
export type EmbeddedResource = z.infer<typeof EmbeddedResourceSchema>;
|
||||||
|
export type Implementation = z.infer<typeof ImplementationSchema>;
|
||||||
|
export type RequestId = z.infer<typeof RequestIdSchema>;
|
||||||
|
export type ResourceLink = z.infer<typeof ResourceLinkSchema>;
|
||||||
|
export type Tool = z.infer<typeof ToolSchema>;
|
||||||
|
|
@ -32,13 +32,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RESOURCE_URI_META_KEY,
|
|
||||||
RESOURCE_MIME_TYPE,
|
|
||||||
McpUiResourceCsp,
|
McpUiResourceCsp,
|
||||||
McpUiResourceMeta,
|
McpUiResourceMeta,
|
||||||
McpUiToolMeta,
|
McpUiToolMeta,
|
||||||
McpUiClientCapabilities,
|
McpUiClientCapabilities,
|
||||||
} from "../app.js";
|
} from "../spec.types.js";
|
||||||
|
import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js";
|
||||||
import type {
|
import type {
|
||||||
ClientCapabilities,
|
ClientCapabilities,
|
||||||
McpServer,
|
McpServer,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import type {
|
||||||
RequestId,
|
RequestId,
|
||||||
ResourceLink,
|
ResourceLink,
|
||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/client";
|
} from "./core-types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current protocol version supported by this SDK.
|
* Current protocol version supported by this SDK.
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,14 @@ const config = {
|
||||||
ClientOptions: BASE_SDK_DOCS,
|
ClientOptions: BASE_SDK_DOCS,
|
||||||
ConnectOptions: BASE_SDK_DOCS,
|
ConnectOptions: BASE_SDK_DOCS,
|
||||||
DiscoverResult: BASE_SDK_DOCS,
|
DiscoverResult: BASE_SDK_DOCS,
|
||||||
|
"MessageExtraInfo.classification": BASE_SDK_DOCS,
|
||||||
ProtocolError: BASE_SDK_DOCS,
|
ProtocolError: BASE_SDK_DOCS,
|
||||||
"Protocol.close": BASE_SDK_DOCS,
|
"Protocol.close": BASE_SDK_DOCS,
|
||||||
"Protocol.notification": BASE_SDK_DOCS,
|
"Protocol.notification": BASE_SDK_DOCS,
|
||||||
ResponseCacheStore: BASE_SDK_DOCS,
|
ResponseCacheStore: BASE_SDK_DOCS,
|
||||||
SdkError: BASE_SDK_DOCS,
|
SdkError: BASE_SDK_DOCS,
|
||||||
"SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS,
|
"SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS,
|
||||||
|
"SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS,
|
||||||
"__type.enforceStrictCapabilities": BASE_SDK_DOCS,
|
"__type.enforceStrictCapabilities": BASE_SDK_DOCS,
|
||||||
},
|
},
|
||||||
"@modelcontextprotocol/server": {
|
"@modelcontextprotocol/server": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue