mountly-mcp 0.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +258 -20
- package/dist/artifact/index.d.ts +49 -0
- package/dist/artifact/index.js +21 -0
- package/dist/artifact/index.js.map +1 -0
- package/dist/bridge/iframe-entry.d.ts +1 -0
- package/dist/bridge/iframe-entry.js +251 -209
- package/dist/bridge/index.d.ts +17 -12
- package/dist/bridge/index.js +2 -2
- package/dist/build/emit-html.d.ts +30 -0
- package/dist/build/emit-meta.d.ts +17 -0
- package/dist/build/index.d.ts +32 -47
- package/dist/build/index.js +9 -135
- package/dist/build/index.js.map +1 -1
- package/dist/chunk-33DMFKKD.js +111 -0
- package/dist/chunk-33DMFKKD.js.map +1 -0
- package/dist/chunk-4HC3OWAQ.js +185 -0
- package/dist/chunk-4HC3OWAQ.js.map +1 -0
- package/dist/chunk-4WAKOYMK.js +196 -0
- package/dist/chunk-4WAKOYMK.js.map +1 -0
- package/dist/chunk-5EHNJCMF.js +58 -0
- package/dist/chunk-5EHNJCMF.js.map +1 -0
- package/dist/chunk-6QHKKFO3.js +30 -0
- package/dist/chunk-6QHKKFO3.js.map +1 -0
- package/dist/chunk-DKK26TRS.js +199 -0
- package/dist/chunk-DKK26TRS.js.map +1 -0
- package/dist/{chunk-A64ZILAS.js → chunk-DQLQOMDM.js} +2 -1
- package/dist/{chunk-A64ZILAS.js.map → chunk-DQLQOMDM.js.map} +1 -1
- package/dist/chunk-KRAS266U.js +260 -0
- package/dist/chunk-KRAS266U.js.map +1 -0
- package/dist/chunk-NLQDVNZP.js +115 -0
- package/dist/chunk-NLQDVNZP.js.map +1 -0
- package/dist/chunk-SNWNF2XI.js +171 -0
- package/dist/chunk-SNWNF2XI.js.map +1 -0
- package/dist/chunk-UECZA34Q.js +52 -0
- package/dist/chunk-UECZA34Q.js.map +1 -0
- package/dist/chunk-YJA4H6IB.js +13 -0
- package/dist/chunk-YJA4H6IB.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +286 -0
- package/dist/cli.js.map +1 -0
- package/dist/connect-server-7L4HGKKL.js +7 -0
- package/dist/connect-server-7L4HGKKL.js.map +1 -0
- package/dist/dev/connect-server.d.ts +13 -0
- package/dist/dev/host-entry.d.ts +13 -0
- package/dist/dev/host-entry.js +18330 -0
- package/dist/dev/index.d.ts +45 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/sandbox-entry.d.ts +7 -0
- package/dist/dev/sandbox-entry.js +80 -0
- package/dist/html.d.ts +4 -0
- package/dist/index.d.ts +6 -26
- package/dist/index.js +8 -3
- package/dist/json-render/app.d.ts +22 -0
- package/dist/json-render/app.js +10 -0
- package/dist/json-render/app.js.map +1 -0
- package/dist/json-render/index.d.ts +10 -0
- package/dist/json-render/index.js +119 -0
- package/dist/json-render/index.js.map +1 -0
- package/dist/json-render/mcp.d.ts +49 -0
- package/dist/json-render/mcp.js +11 -0
- package/dist/json-render/mcp.js.map +1 -0
- package/dist/json-render/server.d.ts +72 -0
- package/dist/json-render/server.js +98 -0
- package/dist/json-render/server.js.map +1 -0
- package/dist/json-render/spec-stream.d.ts +26 -0
- package/dist/json-render/use-spec-stream.d.ts +26 -0
- package/dist/json-render/widget.d.ts +59 -0
- package/dist/react/context.d.ts +14 -0
- package/dist/react/create-widget.d.ts +11 -0
- package/dist/react/hooks.d.ts +46 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +35 -0
- package/dist/react/index.js.map +1 -0
- package/dist/sandbox/index.d.ts +16 -0
- package/dist/sandbox/index.js +8 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/schema.d.ts +20 -0
- package/dist/server/index.d.ts +73 -0
- package/dist/server/index.js +235 -0
- package/dist/server/index.js.map +1 -0
- package/dist/svelte/index.d.ts +35 -0
- package/dist/svelte/index.js +9 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/testing/index.d.ts +27 -0
- package/dist/testing/index.js +312 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/render-artifact.d.ts +35 -0
- package/dist/types.d.ts +55 -0
- package/dist/vite/index.d.ts +23 -0
- package/dist/vite/index.js +211 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vue/index.d.ts +56 -0
- package/dist/vue/index.js +89 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +166 -20
- package/dist/chunk-BUGGLMNA.js +0 -132
- package/dist/chunk-BUGGLMNA.js.map +0 -1
- package/dist/types-B9JAWm4W.d.ts +0 -62
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte entry point: turn a Svelte component into an MCP App view.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { createMcpWidget } from "mountly-mcp/svelte";
|
|
6
|
+
* import Dashboard from "./Dashboard.svelte";
|
|
7
|
+
*
|
|
8
|
+
* const widget = createMcpWidget(Dashboard);
|
|
9
|
+
* (globalThis as { __mountlyMcpWidget__?: unknown }).__mountlyMcpWidget__ = widget;
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script lang="ts">
|
|
14
|
+
* import type { McpWidgetProps } from "mountly-mcp";
|
|
15
|
+
* let { mcp, toolResult, hostContext }: McpWidgetProps = $props();
|
|
16
|
+
* </script>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Unlike the React and Vue entry points there are no context helpers: Svelte's
|
|
20
|
+
* `setContext` has to run inside a compiled component, which would mean
|
|
21
|
+
* shipping a `.svelte` file and a compiler from this package. Props are already
|
|
22
|
+
* reactive via `$props()`, so pass them down where a nested component needs
|
|
23
|
+
* them.
|
|
24
|
+
*/
|
|
25
|
+
import { createWidget } from "mountly-svelte";
|
|
26
|
+
import type { AdapterOptions, WidgetModule } from "mountly/adapter";
|
|
27
|
+
import type { McpWidgetProps } from "../types.js";
|
|
28
|
+
/**
|
|
29
|
+
* Wraps a Svelte component as a mountly `WidgetModule` driven by the
|
|
30
|
+
* mountly-mcp bridge. The component receives `mcp`, `toolInput`,
|
|
31
|
+
* `toolInputPartial`, `toolResult` and `hostContext` as props, alongside any
|
|
32
|
+
* pass-through props.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createMcpWidget<P extends object>(Component: Parameters<typeof createWidget<P & McpWidgetProps>>[0], options?: AdapterOptions): WidgetModule;
|
|
35
|
+
export type { McpWidgetProps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/svelte/index.ts"],"sourcesContent":["/**\n * Svelte entry point: turn a Svelte component into an MCP App view.\n *\n * ```ts\n * import { createMcpWidget } from \"mountly-mcp/svelte\";\n * import Dashboard from \"./Dashboard.svelte\";\n *\n * const widget = createMcpWidget(Dashboard);\n * (globalThis as { __mountlyMcpWidget__?: unknown }).__mountlyMcpWidget__ = widget;\n * ```\n *\n * ```svelte\n * <script lang=\"ts\">\n * import type { McpWidgetProps } from \"mountly-mcp\";\n * let { mcp, toolResult, hostContext }: McpWidgetProps = $props();\n * </script>\n * ```\n *\n * Unlike the React and Vue entry points there are no context helpers: Svelte's\n * `setContext` has to run inside a compiled component, which would mean\n * shipping a `.svelte` file and a compiler from this package. Props are already\n * reactive via `$props()`, so pass them down where a nested component needs\n * them.\n */\nimport { createWidget } from \"mountly-svelte\";\nimport type { AdapterOptions, WidgetModule } from \"mountly/adapter\";\nimport type { McpWidgetProps } from \"../types.js\";\n\n/**\n * Wraps a Svelte component as a mountly `WidgetModule` driven by the\n * mountly-mcp bridge. The component receives `mcp`, `toolInput`,\n * `toolInputPartial`, `toolResult` and `hostContext` as props, alongside any\n * pass-through props.\n */\nexport function createMcpWidget<P extends object>(\n Component: Parameters<typeof createWidget<P & McpWidgetProps>>[0],\n options?: AdapterOptions,\n): WidgetModule {\n return createWidget<P & McpWidgetProps>(Component, options);\n}\n\nexport type { McpWidgetProps };\n"],"mappings":";AAwBA,SAAS,oBAAoB;AAUtB,SAAS,gBACd,WACA,SACc;AACd,SAAO,aAAiC,WAAW,OAAO;AAC5D;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ConformanceDiagnostic, type McpAppArtifact } from "../artifact/index.js";
|
|
2
|
+
export interface VerifyMcpAppsOptions {
|
|
3
|
+
/** Canonical manifest. Defaults to `dist/mountly-mcp.manifest.json`. */
|
|
4
|
+
manifestPath?: string;
|
|
5
|
+
/** Transitional single-View inputs when no manifest exists. */
|
|
6
|
+
htmlPaths?: ReadonlyArray<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Load each View in a real browser and confirm the widget mounts with content.
|
|
9
|
+
*
|
|
10
|
+
* Off by default: it needs Playwright and a Chromium download, which the
|
|
11
|
+
* static checks do not. Turn it on in CI. Everything else here inspects
|
|
12
|
+
* files, and a View can satisfy every file-level check while failing to
|
|
13
|
+
* render — a framework left external, a Node global in the bundle, a bridge
|
|
14
|
+
* that never completes the handshake all produce conforming HTML.
|
|
15
|
+
*/
|
|
16
|
+
render?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface McpAppConformanceReport {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Verification tier selected by the caller. */
|
|
21
|
+
mode: "static" | "browser";
|
|
22
|
+
artifacts: ReadonlyArray<McpAppArtifact>;
|
|
23
|
+
diagnostics: ReadonlyArray<ConformanceDiagnostic>;
|
|
24
|
+
}
|
|
25
|
+
/** Deterministic verification shared by the CLI and downstream CI. */
|
|
26
|
+
export declare function verifyMcpApps(options?: VerifyMcpAppsOptions): Promise<McpAppConformanceReport>;
|
|
27
|
+
export declare function formatConformanceReport(report: McpAppConformanceReport): string;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_APP_MANIFEST_FILE,
|
|
3
|
+
McpAppConformanceError,
|
|
4
|
+
readMcpAppArtifact,
|
|
5
|
+
readMcpAppManifest
|
|
6
|
+
} from "../chunk-DKK26TRS.js";
|
|
7
|
+
|
|
8
|
+
// src/testing/index.ts
|
|
9
|
+
import { access, readFile } from "fs/promises";
|
|
10
|
+
import { isDeepStrictEqual } from "util";
|
|
11
|
+
|
|
12
|
+
// src/testing/render-artifact.ts
|
|
13
|
+
function error(code, message, source) {
|
|
14
|
+
return { severity: "error", code, message, source };
|
|
15
|
+
}
|
|
16
|
+
function warning(code, message, source) {
|
|
17
|
+
return { severity: "warning", code, message, source };
|
|
18
|
+
}
|
|
19
|
+
async function loadAxeSource() {
|
|
20
|
+
const specifier = "axe-core";
|
|
21
|
+
try {
|
|
22
|
+
const mod = await import(specifier);
|
|
23
|
+
return mod.source ?? mod.default?.source;
|
|
24
|
+
} catch {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function auditAccessibility(view, artifact) {
|
|
29
|
+
const axeSource = await loadAxeSource();
|
|
30
|
+
if (!axeSource) return [];
|
|
31
|
+
let violations;
|
|
32
|
+
try {
|
|
33
|
+
await view.evaluate(axeSource);
|
|
34
|
+
violations = await view.evaluate(`
|
|
35
|
+
axe.run(document, { resultTypes: ["violations"] }).then((result) =>
|
|
36
|
+
result.violations.map((violation) => ({
|
|
37
|
+
id: violation.id,
|
|
38
|
+
impact: violation.impact ?? null,
|
|
39
|
+
help: violation.help,
|
|
40
|
+
nodes: violation.nodes.length,
|
|
41
|
+
target: String(violation.nodes[0]?.target?.[0] ?? "")
|
|
42
|
+
}))
|
|
43
|
+
)
|
|
44
|
+
`);
|
|
45
|
+
} catch (caught) {
|
|
46
|
+
return [
|
|
47
|
+
warning(
|
|
48
|
+
"render/a11y-unavailable",
|
|
49
|
+
`accessibility pass did not run: ${caught instanceof Error ? caught.message : String(caught)}`,
|
|
50
|
+
artifact.htmlPath
|
|
51
|
+
)
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
return violations.map(
|
|
55
|
+
(violation) => warning(
|
|
56
|
+
"render/a11y",
|
|
57
|
+
`${violation.id} (${violation.impact ?? "unknown"} impact): ${violation.help} \u2014 ${violation.nodes} element${violation.nodes === 1 ? "" : "s"}, first at \`${violation.target}\``,
|
|
58
|
+
artifact.htmlPath
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
async function launchBrowser() {
|
|
63
|
+
const specifiers = ["playwright", "@playwright/test"];
|
|
64
|
+
for (const specifier of specifiers) {
|
|
65
|
+
let mod;
|
|
66
|
+
try {
|
|
67
|
+
mod = await import(specifier);
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (mod.chromium) return mod.chromium.launch();
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
async function startHost(artifact) {
|
|
76
|
+
const { startDevHost } = await import("../dev/index.js");
|
|
77
|
+
return startDevHost({
|
|
78
|
+
htmlPath: artifact.htmlPath,
|
|
79
|
+
fixtures: { conformance: {} },
|
|
80
|
+
hostPort: 5480
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function renderMcpAppArtifactWith(artifact, dependencies) {
|
|
84
|
+
let browser;
|
|
85
|
+
try {
|
|
86
|
+
browser = await dependencies.launchBrowser();
|
|
87
|
+
} catch (caught) {
|
|
88
|
+
return [
|
|
89
|
+
error(
|
|
90
|
+
"render/unavailable",
|
|
91
|
+
`could not launch Chromium: ${caught instanceof Error ? caught.message : String(caught)}`,
|
|
92
|
+
artifact.htmlPath
|
|
93
|
+
)
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
if (!browser) {
|
|
97
|
+
return [
|
|
98
|
+
error(
|
|
99
|
+
"render/unavailable",
|
|
100
|
+
"render requested but Playwright is not installed (npm i -D playwright)",
|
|
101
|
+
artifact.htmlPath
|
|
102
|
+
)
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
let host;
|
|
106
|
+
const failures = [];
|
|
107
|
+
try {
|
|
108
|
+
host = await dependencies.startHost(artifact);
|
|
109
|
+
const page = await browser.newPage();
|
|
110
|
+
const pageErrors = [];
|
|
111
|
+
page.on("pageerror", (caught) => pageErrors.push(caught.message));
|
|
112
|
+
await page.goto(host.hostUrl);
|
|
113
|
+
const deadline = Date.now() + 2e4;
|
|
114
|
+
let mounted = false;
|
|
115
|
+
let renderedContent = false;
|
|
116
|
+
let mountedAt;
|
|
117
|
+
let boundary;
|
|
118
|
+
while (!boundary && Date.now() < deadline && pageErrors.length === 0) {
|
|
119
|
+
const view = page.frames().find((frame) => frame.url().includes("srcdoc"));
|
|
120
|
+
const state = view ? await view.evaluate(() => {
|
|
121
|
+
const failed = document.querySelector("[data-mountly-mcp-error]");
|
|
122
|
+
const root = document.querySelector("#mountly-mcp-root");
|
|
123
|
+
const bounds = root?.getBoundingClientRect();
|
|
124
|
+
return {
|
|
125
|
+
state: root?.getAttribute("data-mountly-mcp-state") ?? void 0,
|
|
126
|
+
html: root?.innerHTML ?? "",
|
|
127
|
+
shadowHtml: root?.shadowRoot?.innerHTML ?? "",
|
|
128
|
+
visibleHeight: bounds?.height ?? 0,
|
|
129
|
+
error: failed ? `${failed.getAttribute("data-mountly-mcp-error") ?? ""}: ${failed.textContent ?? ""}` : void 0
|
|
130
|
+
};
|
|
131
|
+
}).catch(() => void 0) : void 0;
|
|
132
|
+
boundary = state?.error;
|
|
133
|
+
mounted || (mounted = state?.state === "mounted");
|
|
134
|
+
if (mounted && mountedAt === void 0) mountedAt = Date.now();
|
|
135
|
+
renderedContent = (state?.html ?? "").trim().length > 0 || (state?.shadowHtml ?? "").trim().length > 0 || (state?.visibleHeight ?? 0) > 0;
|
|
136
|
+
if (mounted && renderedContent) break;
|
|
137
|
+
if (mountedAt !== void 0 && Date.now() - mountedAt >= 2e3) break;
|
|
138
|
+
await new Promise((done) => setTimeout(done, 100));
|
|
139
|
+
}
|
|
140
|
+
if (boundary) {
|
|
141
|
+
failures.push(
|
|
142
|
+
error("render/error-boundary", `View rendered an error: ${boundary}`, artifact.htmlPath)
|
|
143
|
+
);
|
|
144
|
+
} else if (pageErrors.length > 0) {
|
|
145
|
+
failures.push(
|
|
146
|
+
error("render/threw", `View threw while loading: ${pageErrors[0]}`, artifact.htmlPath)
|
|
147
|
+
);
|
|
148
|
+
} else if (!mounted) {
|
|
149
|
+
failures.push(
|
|
150
|
+
error(
|
|
151
|
+
"render/timeout",
|
|
152
|
+
"View never mounted: the handshake or widget mount did not complete",
|
|
153
|
+
artifact.htmlPath
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
} else if (!renderedContent) {
|
|
157
|
+
failures.push(
|
|
158
|
+
error("render/blank", "View mounted but rendered no content", artifact.htmlPath)
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
const view = page.frames().find((frame) => frame.url().includes("srcdoc"));
|
|
162
|
+
if (view) failures.push(...await auditAccessibility(view, artifact));
|
|
163
|
+
}
|
|
164
|
+
} catch (caught) {
|
|
165
|
+
failures.push(
|
|
166
|
+
error(
|
|
167
|
+
"render/failed",
|
|
168
|
+
caught instanceof Error ? caught.message : String(caught),
|
|
169
|
+
artifact.htmlPath
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await Promise.allSettled([browser.close(), host?.close() ?? Promise.resolve()]);
|
|
174
|
+
}
|
|
175
|
+
return failures;
|
|
176
|
+
}
|
|
177
|
+
function renderMcpAppArtifact(artifact) {
|
|
178
|
+
return renderMcpAppArtifactWith(artifact, { launchBrowser, startHost });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/testing/index.ts
|
|
182
|
+
function error2(code, message, source) {
|
|
183
|
+
return { severity: "error", code, message, source };
|
|
184
|
+
}
|
|
185
|
+
function warning2(code, message, source) {
|
|
186
|
+
return { severity: "warning", code, message, source };
|
|
187
|
+
}
|
|
188
|
+
async function verifyMcpApps(options = {}) {
|
|
189
|
+
const diagnostics = [];
|
|
190
|
+
const artifacts = [];
|
|
191
|
+
if (options.htmlPaths && options.htmlPaths.length > 0) {
|
|
192
|
+
for (const path of options.htmlPaths) {
|
|
193
|
+
try {
|
|
194
|
+
artifacts.push(await readMcpAppArtifact(path));
|
|
195
|
+
} catch (caught) {
|
|
196
|
+
if (caught instanceof McpAppConformanceError) diagnostics.push(...caught.diagnostics);
|
|
197
|
+
else
|
|
198
|
+
diagnostics.push(
|
|
199
|
+
error2("verify/load", caught instanceof Error ? caught.message : String(caught))
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
try {
|
|
205
|
+
const loaded = await readMcpAppManifest(
|
|
206
|
+
options.manifestPath ?? `dist/${MCP_APP_MANIFEST_FILE}`
|
|
207
|
+
);
|
|
208
|
+
for (const declared of loaded.artifacts) {
|
|
209
|
+
try {
|
|
210
|
+
const actual = await readMcpAppArtifact(declared.htmlPath, declared.uri);
|
|
211
|
+
if (actual.name !== declared.name) {
|
|
212
|
+
diagnostics.push(
|
|
213
|
+
error2(
|
|
214
|
+
"manifest/name-mismatch",
|
|
215
|
+
`manifest app '${declared.name}' points to artifact '${actual.name}'`,
|
|
216
|
+
loaded.path
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!isDeepStrictEqual(actual.declaration, declared.declaration)) {
|
|
221
|
+
diagnostics.push(
|
|
222
|
+
error2(
|
|
223
|
+
"manifest/declaration-mismatch",
|
|
224
|
+
`manifest declaration for '${declared.name}' does not match its metadata file`,
|
|
225
|
+
loaded.path
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
artifacts.push(actual);
|
|
230
|
+
} catch (caught) {
|
|
231
|
+
if (caught instanceof McpAppConformanceError) diagnostics.push(...caught.diagnostics);
|
|
232
|
+
else
|
|
233
|
+
diagnostics.push(
|
|
234
|
+
error2("verify/load", caught instanceof Error ? caught.message : String(caught))
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (caught) {
|
|
239
|
+
if (caught instanceof McpAppConformanceError) diagnostics.push(...caught.diagnostics);
|
|
240
|
+
else
|
|
241
|
+
diagnostics.push(
|
|
242
|
+
error2("verify/load", caught instanceof Error ? caught.message : String(caught))
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const names = /* @__PURE__ */ new Set();
|
|
247
|
+
const uris = /* @__PURE__ */ new Set();
|
|
248
|
+
for (const artifact of artifacts) {
|
|
249
|
+
if (names.has(artifact.name)) {
|
|
250
|
+
diagnostics.push(error2("manifest/duplicate-name", `duplicate app name '${artifact.name}'`));
|
|
251
|
+
}
|
|
252
|
+
if (uris.has(artifact.uri)) {
|
|
253
|
+
diagnostics.push(error2("manifest/duplicate-uri", `duplicate app uri '${artifact.uri}'`));
|
|
254
|
+
}
|
|
255
|
+
names.add(artifact.name);
|
|
256
|
+
uris.add(artifact.uri);
|
|
257
|
+
try {
|
|
258
|
+
await Promise.all([access(artifact.htmlPath), access(artifact.metaPath)]);
|
|
259
|
+
const html = await readFile(artifact.htmlPath, "utf8");
|
|
260
|
+
if (!html.includes("ui/initialize") || !html.includes("ui/notifications/initialized")) {
|
|
261
|
+
diagnostics.push(
|
|
262
|
+
error2(
|
|
263
|
+
"artifact/bridge",
|
|
264
|
+
"View HTML does not contain the MCP Apps initialization handshake",
|
|
265
|
+
artifact.htmlPath
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (options.render) diagnostics.push(...await renderMcpAppArtifact(artifact));
|
|
270
|
+
if (!artifact.declaration.description) {
|
|
271
|
+
diagnostics.push(
|
|
272
|
+
warning2(
|
|
273
|
+
"artifact/description",
|
|
274
|
+
`View '${artifact.name}' has no description`,
|
|
275
|
+
artifact.metaPath
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
} catch (caught) {
|
|
280
|
+
diagnostics.push(
|
|
281
|
+
error2(
|
|
282
|
+
"artifact/files",
|
|
283
|
+
caught instanceof Error ? caught.message : String(caught),
|
|
284
|
+
artifact.htmlPath
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
|
|
291
|
+
mode: options.render === true ? "browser" : "static",
|
|
292
|
+
artifacts,
|
|
293
|
+
diagnostics
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function formatConformanceReport(report) {
|
|
297
|
+
const lines = report.diagnostics.map((diagnostic) => {
|
|
298
|
+
const marker = diagnostic.severity === "error" ? "ERROR" : "WARN";
|
|
299
|
+
const source = diagnostic.source ? ` (${diagnostic.source})` : "";
|
|
300
|
+
return `${marker} ${diagnostic.code}: ${diagnostic.message}${source}`;
|
|
301
|
+
});
|
|
302
|
+
lines.push(
|
|
303
|
+
`${report.ok ? "PASS" : "FAIL"} ${report.artifacts.length} app artifact${report.artifacts.length === 1 ? "" : "s"} (${report.mode === "browser" ? "browser checks" : "static checks only; pass --render for runtime coverage"})`
|
|
304
|
+
);
|
|
305
|
+
return `${lines.join("\n")}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
formatConformanceReport,
|
|
310
|
+
verifyMcpApps
|
|
311
|
+
};
|
|
312
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts","../../src/testing/render-artifact.ts"],"sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport {\n MCP_APP_MANIFEST_FILE,\n McpAppConformanceError,\n readMcpAppArtifact,\n readMcpAppManifest,\n type ConformanceDiagnostic,\n type McpAppArtifact,\n} from \"../artifact/index.js\";\nimport { renderMcpAppArtifact } from \"./render-artifact.js\";\n\nexport interface VerifyMcpAppsOptions {\n /** Canonical manifest. Defaults to `dist/mountly-mcp.manifest.json`. */\n manifestPath?: string;\n /** Transitional single-View inputs when no manifest exists. */\n htmlPaths?: ReadonlyArray<string>;\n /**\n * Load each View in a real browser and confirm the widget mounts with content.\n *\n * Off by default: it needs Playwright and a Chromium download, which the\n * static checks do not. Turn it on in CI. Everything else here inspects\n * files, and a View can satisfy every file-level check while failing to\n * render — a framework left external, a Node global in the bundle, a bridge\n * that never completes the handshake all produce conforming HTML.\n */\n render?: boolean;\n}\n\nexport interface McpAppConformanceReport {\n ok: boolean;\n /** Verification tier selected by the caller. */\n mode: \"static\" | \"browser\";\n artifacts: ReadonlyArray<McpAppArtifact>;\n diagnostics: ReadonlyArray<ConformanceDiagnostic>;\n}\n\nfunction error(code: string, message: string, source?: string): ConformanceDiagnostic {\n return { severity: \"error\", code, message, source };\n}\n\nfunction warning(code: string, message: string, source?: string): ConformanceDiagnostic {\n return { severity: \"warning\", code, message, source };\n}\n\n/** Deterministic verification shared by the CLI and downstream CI. */\nexport async function verifyMcpApps(\n options: VerifyMcpAppsOptions = {},\n): Promise<McpAppConformanceReport> {\n const diagnostics: ConformanceDiagnostic[] = [];\n const artifacts: McpAppArtifact[] = [];\n if (options.htmlPaths && options.htmlPaths.length > 0) {\n for (const path of options.htmlPaths) {\n try {\n artifacts.push(await readMcpAppArtifact(path));\n } catch (caught) {\n if (caught instanceof McpAppConformanceError) diagnostics.push(...caught.diagnostics);\n else\n diagnostics.push(\n error(\"verify/load\", caught instanceof Error ? caught.message : String(caught)),\n );\n }\n }\n } else {\n try {\n const loaded = await readMcpAppManifest(\n options.manifestPath ?? `dist/${MCP_APP_MANIFEST_FILE}`,\n );\n for (const declared of loaded.artifacts) {\n try {\n const actual = await readMcpAppArtifact(declared.htmlPath, declared.uri);\n if (actual.name !== declared.name) {\n diagnostics.push(\n error(\n \"manifest/name-mismatch\",\n `manifest app '${declared.name}' points to artifact '${actual.name}'`,\n loaded.path,\n ),\n );\n }\n if (!isDeepStrictEqual(actual.declaration, declared.declaration)) {\n diagnostics.push(\n error(\n \"manifest/declaration-mismatch\",\n `manifest declaration for '${declared.name}' does not match its metadata file`,\n loaded.path,\n ),\n );\n }\n artifacts.push(actual);\n } catch (caught) {\n if (caught instanceof McpAppConformanceError) diagnostics.push(...caught.diagnostics);\n else\n diagnostics.push(\n error(\"verify/load\", caught instanceof Error ? caught.message : String(caught)),\n );\n }\n }\n } catch (caught) {\n if (caught instanceof McpAppConformanceError) diagnostics.push(...caught.diagnostics);\n else\n diagnostics.push(\n error(\"verify/load\", caught instanceof Error ? caught.message : String(caught)),\n );\n }\n }\n\n const names = new Set<string>();\n const uris = new Set<string>();\n for (const artifact of artifacts) {\n if (names.has(artifact.name)) {\n diagnostics.push(error(\"manifest/duplicate-name\", `duplicate app name '${artifact.name}'`));\n }\n if (uris.has(artifact.uri)) {\n diagnostics.push(error(\"manifest/duplicate-uri\", `duplicate app uri '${artifact.uri}'`));\n }\n names.add(artifact.name);\n uris.add(artifact.uri);\n try {\n await Promise.all([access(artifact.htmlPath), access(artifact.metaPath)]);\n const html = await readFile(artifact.htmlPath, \"utf8\");\n if (!html.includes(\"ui/initialize\") || !html.includes(\"ui/notifications/initialized\")) {\n diagnostics.push(\n error(\n \"artifact/bridge\",\n \"View HTML does not contain the MCP Apps initialization handshake\",\n artifact.htmlPath,\n ),\n );\n }\n if (options.render) diagnostics.push(...(await renderMcpAppArtifact(artifact)));\n if (!artifact.declaration.description) {\n diagnostics.push(\n warning(\n \"artifact/description\",\n `View '${artifact.name}' has no description`,\n artifact.metaPath,\n ),\n );\n }\n } catch (caught) {\n diagnostics.push(\n error(\n \"artifact/files\",\n caught instanceof Error ? caught.message : String(caught),\n artifact.htmlPath,\n ),\n );\n }\n }\n\n return {\n ok: !diagnostics.some((diagnostic) => diagnostic.severity === \"error\"),\n mode: options.render === true ? \"browser\" : \"static\",\n artifacts,\n diagnostics,\n };\n}\n\nexport function formatConformanceReport(report: McpAppConformanceReport): string {\n const lines = report.diagnostics.map((diagnostic) => {\n const marker = diagnostic.severity === \"error\" ? \"ERROR\" : \"WARN\";\n const source = diagnostic.source ? ` (${diagnostic.source})` : \"\";\n return `${marker} ${diagnostic.code}: ${diagnostic.message}${source}`;\n });\n lines.push(\n `${report.ok ? \"PASS\" : \"FAIL\"} ${report.artifacts.length} app artifact${report.artifacts.length === 1 ? \"\" : \"s\"} (${report.mode === \"browser\" ? \"browser checks\" : \"static checks only; pass --render for runtime coverage\"})`,\n );\n return `${lines.join(\"\\n\")}\\n`;\n}\n","import type { ConformanceDiagnostic, McpAppArtifact } from \"../artifact/index.js\";\n\ninterface FrameLike {\n url(): string;\n /**\n * A string expression is evaluated through the debugger protocol rather than\n * as page script, which is how axe reaches a View whose CSP withholds\n * `unsafe-eval` — the same CSP a host enforces in production.\n */\n evaluate<T>(fn: (() => T) | string): Promise<T>;\n}\n\ninterface PageLike {\n on(event: \"pageerror\", handler: (error: Error) => void): void;\n goto(url: string): Promise<unknown>;\n frames(): FrameLike[];\n}\n\nexport interface ConformanceBrowser {\n newPage(): Promise<PageLike>;\n close(): Promise<void>;\n}\n\nexport interface ConformanceHost {\n hostUrl: string;\n close(): Promise<void>;\n}\n\n/** @internal Test seam for browser and host lifecycle failures. */\nexport interface RenderMcpAppArtifactDependencies {\n launchBrowser(): Promise<ConformanceBrowser | undefined>;\n startHost(artifact: McpAppArtifact): Promise<ConformanceHost>;\n}\n\nfunction error(code: string, message: string, source: string): ConformanceDiagnostic {\n return { severity: \"error\", code, message, source };\n}\n\nfunction warning(code: string, message: string, source: string): ConformanceDiagnostic {\n return { severity: \"warning\", code, message, source };\n}\n\ninterface AxeViolation {\n id: string;\n impact: string | null;\n help: string;\n nodes: number;\n target: string;\n}\n\n/** axe-core is an optional peer, like Playwright: absent means the pass is skipped. */\nasync function loadAxeSource(): Promise<string | undefined> {\n const specifier: string = \"axe-core\";\n try {\n const mod = (await import(specifier)) as { source?: string; default?: { source?: string } };\n return mod.source ?? mod.default?.source;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Accessibility of the View as assembled, which component-level checks cannot\n * see: every part can be accessible and the composition still fail, because a\n * missing label or a contrast pairing only exists once the parts are together.\n *\n * Reported as warnings, so a run stays green unless `--strict` is on. They are\n * findings about the View you built, not proof the artifact is malformed.\n */\nasync function auditAccessibility(\n view: FrameLike,\n artifact: McpAppArtifact,\n): Promise<ConformanceDiagnostic[]> {\n const axeSource = await loadAxeSource();\n if (!axeSource) return [];\n\n let violations: AxeViolation[];\n try {\n await view.evaluate(axeSource);\n violations = await view.evaluate<AxeViolation[]>(`\n axe.run(document, { resultTypes: [\"violations\"] }).then((result) =>\n result.violations.map((violation) => ({\n id: violation.id,\n impact: violation.impact ?? null,\n help: violation.help,\n nodes: violation.nodes.length,\n target: String(violation.nodes[0]?.target?.[0] ?? \"\")\n }))\n )\n `);\n } catch (caught) {\n return [\n warning(\n \"render/a11y-unavailable\",\n `accessibility pass did not run: ${caught instanceof Error ? caught.message : String(caught)}`,\n artifact.htmlPath,\n ),\n ];\n }\n\n return violations.map((violation) =>\n warning(\n \"render/a11y\",\n `${violation.id} (${violation.impact ?? \"unknown\"} impact): ${violation.help} — ${violation.nodes} element${violation.nodes === 1 ? \"\" : \"s\"}, first at \\`${violation.target}\\``,\n artifact.htmlPath,\n ),\n );\n}\n\n/** Playwright is an optional peer — absent is a diagnostic, not a crash. */\nasync function launchBrowser(): Promise<ConformanceBrowser | undefined> {\n // Both specifiers are tried: under an isolated `node_modules` a project that\n // installed `@playwright/test` cannot resolve bare `playwright`, even though\n // one depends on the other. Specifiers stay `string`-typed so the optional\n // peer is optional at type-check time as well as at runtime.\n const specifiers: string[] = [\"playwright\", \"@playwright/test\"];\n for (const specifier of specifiers) {\n let mod: { chromium?: { launch(): Promise<ConformanceBrowser> } };\n try {\n mod = (await import(specifier)) as typeof mod;\n } catch {\n continue;\n }\n if (mod.chromium) return mod.chromium.launch();\n }\n return undefined;\n}\n\nasync function startHost(artifact: McpAppArtifact): Promise<ConformanceHost> {\n const { startDevHost } = await import(\"../dev/index.js\");\n return startDevHost({\n htmlPath: artifact.htmlPath,\n fixtures: { conformance: {} },\n hostPort: 5480,\n });\n}\n\n/**\n * Exercise an App artifact through the real two-origin host and browser.\n * Browser and host ownership stays local so every startup path is cleaned up.\n */\nexport async function renderMcpAppArtifactWith(\n artifact: McpAppArtifact,\n dependencies: RenderMcpAppArtifactDependencies,\n): Promise<ConformanceDiagnostic[]> {\n let browser: ConformanceBrowser | undefined;\n try {\n browser = await dependencies.launchBrowser();\n } catch (caught) {\n return [\n error(\n \"render/unavailable\",\n `could not launch Chromium: ${caught instanceof Error ? caught.message : String(caught)}`,\n artifact.htmlPath,\n ),\n ];\n }\n if (!browser) {\n return [\n error(\n \"render/unavailable\",\n \"render requested but Playwright is not installed (npm i -D playwright)\",\n artifact.htmlPath,\n ),\n ];\n }\n\n let host: ConformanceHost | undefined;\n const failures: ConformanceDiagnostic[] = [];\n try {\n host = await dependencies.startHost(artifact);\n const page = await browser.newPage();\n const pageErrors: string[] = [];\n page.on(\"pageerror\", (caught) => pageErrors.push(caught.message));\n await page.goto(host.hostUrl);\n\n const deadline = Date.now() + 20_000;\n let mounted = false;\n let renderedContent = false;\n let mountedAt: number | undefined;\n let boundary: string | undefined;\n while (!boundary && Date.now() < deadline && pageErrors.length === 0) {\n const view = page.frames().find((frame) => frame.url().includes(\"srcdoc\"));\n const state = view\n ? await view\n .evaluate(() => {\n const failed = document.querySelector(\"[data-mountly-mcp-error]\");\n const root = document.querySelector(\"#mountly-mcp-root\");\n const bounds = root?.getBoundingClientRect();\n return {\n state: root?.getAttribute(\"data-mountly-mcp-state\") ?? undefined,\n html: root?.innerHTML ?? \"\",\n shadowHtml: root?.shadowRoot?.innerHTML ?? \"\",\n visibleHeight: bounds?.height ?? 0,\n error: failed\n ? `${failed.getAttribute(\"data-mountly-mcp-error\") ?? \"\"}: ${failed.textContent ?? \"\"}`\n : undefined,\n };\n })\n .catch(() => undefined)\n : undefined;\n boundary = state?.error;\n // Sticky: the bridge's state only moves forward, and a View that mounted\n // stays mounted even if a later poll reads the frame mid-navigation.\n mounted ||= state?.state === \"mounted\";\n if (mounted && mountedAt === undefined) mountedAt = Date.now();\n renderedContent =\n (state?.html ?? \"\").trim().length > 0 ||\n (state?.shadowHtml ?? \"\").trim().length > 0 ||\n (state?.visibleHeight ?? 0) > 0;\n if (mounted && renderedContent) break;\n // Framework renderers may commit after mount() returns. Give them a\n // bounded window while still failing a genuinely empty View quickly.\n if (mountedAt !== undefined && Date.now() - mountedAt >= 2_000) break;\n await new Promise((done) => setTimeout(done, 100));\n }\n\n if (boundary) {\n failures.push(\n error(\"render/error-boundary\", `View rendered an error: ${boundary}`, artifact.htmlPath),\n );\n } else if (pageErrors.length > 0) {\n failures.push(\n error(\"render/threw\", `View threw while loading: ${pageErrors[0]}`, artifact.htmlPath),\n );\n } else if (!mounted) {\n failures.push(\n error(\n \"render/timeout\",\n \"View never mounted: the handshake or widget mount did not complete\",\n artifact.htmlPath,\n ),\n );\n } else if (!renderedContent) {\n failures.push(\n error(\"render/blank\", \"View mounted but rendered no content\", artifact.htmlPath),\n );\n } else {\n // Only a View that actually rendered can be audited — anything else has\n // already failed, and axe would just report on an empty document.\n const view = page.frames().find((frame) => frame.url().includes(\"srcdoc\"));\n if (view) failures.push(...(await auditAccessibility(view, artifact)));\n }\n } catch (caught) {\n failures.push(\n error(\n \"render/failed\",\n caught instanceof Error ? caught.message : String(caught),\n artifact.htmlPath,\n ),\n );\n } finally {\n await Promise.allSettled([browser.close(), host?.close() ?? Promise.resolve()]);\n }\n return failures;\n}\n\nexport function renderMcpAppArtifact(artifact: McpAppArtifact): Promise<ConformanceDiagnostic[]> {\n return renderMcpAppArtifactWith(artifact, { launchBrowser, startHost });\n}\n"],"mappings":"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","names":["error","warning"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ConformanceDiagnostic, McpAppArtifact } from "../artifact/index.js";
|
|
2
|
+
interface FrameLike {
|
|
3
|
+
url(): string;
|
|
4
|
+
/**
|
|
5
|
+
* A string expression is evaluated through the debugger protocol rather than
|
|
6
|
+
* as page script, which is how axe reaches a View whose CSP withholds
|
|
7
|
+
* `unsafe-eval` — the same CSP a host enforces in production.
|
|
8
|
+
*/
|
|
9
|
+
evaluate<T>(fn: (() => T) | string): Promise<T>;
|
|
10
|
+
}
|
|
11
|
+
interface PageLike {
|
|
12
|
+
on(event: "pageerror", handler: (error: Error) => void): void;
|
|
13
|
+
goto(url: string): Promise<unknown>;
|
|
14
|
+
frames(): FrameLike[];
|
|
15
|
+
}
|
|
16
|
+
export interface ConformanceBrowser {
|
|
17
|
+
newPage(): Promise<PageLike>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface ConformanceHost {
|
|
21
|
+
hostUrl: string;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/** @internal Test seam for browser and host lifecycle failures. */
|
|
25
|
+
export interface RenderMcpAppArtifactDependencies {
|
|
26
|
+
launchBrowser(): Promise<ConformanceBrowser | undefined>;
|
|
27
|
+
startHost(artifact: McpAppArtifact): Promise<ConformanceHost>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Exercise an App artifact through the real two-origin host and browser.
|
|
31
|
+
* Browser and host ownership stays local so every startup path is cleaned up.
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderMcpAppArtifactWith(artifact: McpAppArtifact, dependencies: RenderMcpAppArtifactDependencies): Promise<ConformanceDiagnostic[]>;
|
|
34
|
+
export declare function renderMcpAppArtifact(artifact: McpAppArtifact): Promise<ConformanceDiagnostic[]>;
|
|
35
|
+
export {};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec types come straight from `@modelcontextprotocol/ext-apps` under their
|
|
3
|
+
* own names — `McpUiDisplayMode`, `McpUiHostContext`, and the rest. mountly
|
|
4
|
+
* deliberately does not rename them: the names in the MCP Apps spec (SEP-1865)
|
|
5
|
+
* are the vocabulary users already learned, and every alias is one more thing
|
|
6
|
+
* to translate and one more place to drift.
|
|
7
|
+
*
|
|
8
|
+
* Only genuinely mountly-specific types live here: the props a widget's
|
|
9
|
+
* `mount()` receives, and the sidecar declaration written next to the built HTML.
|
|
10
|
+
*/
|
|
11
|
+
export type { App, McpUiAppCapabilities, McpUiDisplayMode, McpUiHostCapabilities, McpUiHostContext, McpUiHostContextChangedNotification, McpUiResourceCsp, McpUiResourceMeta, McpUiResourcePermissions, McpUiToolCancelledNotification, McpUiToolInputNotification, McpUiToolInputPartialNotification, McpUiToolMeta, McpUiToolResultNotification, McpUiToolVisibility, } from "@modelcontextprotocol/ext-apps";
|
|
12
|
+
import type { App, McpUiDisplayMode, McpUiHostContext, McpUiResourceMeta, McpUiToolInputNotification, McpUiToolInputPartialNotification, McpUiToolResultNotification } from "@modelcontextprotocol/ext-apps";
|
|
13
|
+
import type { MCP_APPS_MIME, MCP_APPS_PROTOCOL_VERSION } from "./schema.js";
|
|
14
|
+
/**
|
|
15
|
+
* Props passed to a `WidgetModule.mount(container, props)` call.
|
|
16
|
+
*
|
|
17
|
+
* The bridge surfaces the App handle plus the latest tool input/result and a
|
|
18
|
+
* snapshot of host context. Widgets receive new props through `update()` as
|
|
19
|
+
* notifications arrive — see `runBridge` in `bridge/index.ts`.
|
|
20
|
+
*/
|
|
21
|
+
export interface McpWidgetProps {
|
|
22
|
+
/** The view-side ext-apps App; exposes the full spec-compliant API surface. */
|
|
23
|
+
mcp: App;
|
|
24
|
+
/** Tool arguments (after `ui/notifications/tool-input`). */
|
|
25
|
+
toolInput?: McpUiToolInputNotification["params"];
|
|
26
|
+
/**
|
|
27
|
+
* Streaming tool arguments (`ui/notifications/tool-input-partial`), only when
|
|
28
|
+
* the bridge runs with `streamToolInput`. Best-effort recovered JSON: fields
|
|
29
|
+
* may be missing or change between notifications, so never drive a critical
|
|
30
|
+
* operation from it. Cleared once the complete `toolInput` arrives.
|
|
31
|
+
*/
|
|
32
|
+
toolInputPartial?: McpUiToolInputPartialNotification["params"];
|
|
33
|
+
/** Tool result (after `ui/notifications/tool-result`). */
|
|
34
|
+
toolResult?: McpUiToolResultNotification["params"];
|
|
35
|
+
/** Host context (theme, styles, displayMode, container dimensions, etc.). */
|
|
36
|
+
hostContext?: McpUiHostContext;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resource metadata written to the `.meta.json` sidecar next to the emitted
|
|
40
|
+
* HTML, consumed by `mountly-mcp/server` when registering the `ui://`
|
|
41
|
+
* resource with `_meta.ui = {...}`.
|
|
42
|
+
*/
|
|
43
|
+
export interface McpResourceDeclaration {
|
|
44
|
+
protocolVersion: typeof MCP_APPS_PROTOCOL_VERSION;
|
|
45
|
+
uri: string;
|
|
46
|
+
name: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
mimeType: typeof MCP_APPS_MIME;
|
|
49
|
+
/** Whether `runBridge` defers mount until the first tool-result arrives. */
|
|
50
|
+
awaitToolResult: boolean;
|
|
51
|
+
displayModes: ReadonlyArray<McpUiDisplayMode>;
|
|
52
|
+
_meta: {
|
|
53
|
+
ui: McpUiResourceMeta;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import type { BuildSelfContainedOptions } from "../build/index.js";
|
|
3
|
+
export interface MountlyMcpWidgetOptions extends Omit<BuildSelfContainedOptions, "entry" | "output" | "cssEntry"> {
|
|
4
|
+
entry: string;
|
|
5
|
+
output?: string;
|
|
6
|
+
cleanIntermediate?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface MountlyMcpWidgetsOptions {
|
|
9
|
+
apps: ReadonlyArray<MountlyMcpWidgetOptions>;
|
|
10
|
+
/** Defaults to `<outDir>/mountly-mcp.manifest.json`; false disables it. */
|
|
11
|
+
manifest?: string | false;
|
|
12
|
+
}
|
|
13
|
+
export type MountlyMcpViteOptions = MountlyMcpWidgetOptions | MountlyMcpWidgetsOptions;
|
|
14
|
+
export interface MountlyMcpViteApi {
|
|
15
|
+
apps: ReadonlyArray<MountlyMcpWidgetOptions>;
|
|
16
|
+
manifest?: string | false;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build one or many independent MCP Apps Views. Each View is a Vite build
|
|
20
|
+
* environment, so frameworks and CSS remain isolated while callers configure
|
|
21
|
+
* the collection once.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mountlyMcpWidget(options: MountlyMcpViteOptions): Plugin;
|