mountly-mcp 4.3.0 → 4.5.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.
Files changed (58) hide show
  1. package/README.md +89 -0
  2. package/dist/artifact/declaration.d.ts +61 -0
  3. package/dist/artifact/index.d.ts +2 -42
  4. package/dist/artifact/index.js +9 -5
  5. package/dist/chunk-6T5EIQGE.js +104 -0
  6. package/dist/chunk-6T5EIQGE.js.map +1 -0
  7. package/dist/chunk-BLT2MK32.js +35 -0
  8. package/dist/chunk-BLT2MK32.js.map +1 -0
  9. package/dist/{chunk-HIAIQVK5.js → chunk-QIA76O6M.js} +9 -1
  10. package/dist/chunk-QIA76O6M.js.map +1 -0
  11. package/dist/{chunk-DKK26TRS.js → chunk-RLDSMU5M.js} +9 -70
  12. package/dist/chunk-RLDSMU5M.js.map +1 -0
  13. package/dist/chunk-UMQ5OULD.js +243 -0
  14. package/dist/chunk-UMQ5OULD.js.map +1 -0
  15. package/dist/cli.js +1 -1
  16. package/dist/dev/host-entry.js +23 -0
  17. package/dist/{doctor-4MTC6Y4X.js → doctor-OTOROLPI.js} +118 -9
  18. package/dist/doctor-OTOROLPI.js.map +1 -0
  19. package/dist/doctor.d.ts +4 -0
  20. package/dist/download.d.ts +17 -0
  21. package/dist/events/index.d.ts +97 -0
  22. package/dist/events/index.js +215 -0
  23. package/dist/events/index.js.map +1 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +7 -3
  26. package/dist/json-render/index.js +2 -1
  27. package/dist/json-render/index.js.map +1 -1
  28. package/dist/react/hooks.d.ts +7 -0
  29. package/dist/react/index.js +4 -1
  30. package/dist/server/core.d.ts +84 -0
  31. package/dist/server/index.d.ts +2 -72
  32. package/dist/server/index.js +19 -219
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/server/workers.d.ts +7 -0
  35. package/dist/server/workers.js +14 -0
  36. package/dist/server/workers.js.map +1 -0
  37. package/dist/testing/index.js +5 -3
  38. package/dist/testing/index.js.map +1 -1
  39. package/dist/vite/index.d.ts +15 -1
  40. package/dist/vite/index.js +133 -11
  41. package/dist/vite/index.js.map +1 -1
  42. package/dist/vue/index.d.ts +6 -0
  43. package/dist/vue/index.js +8 -0
  44. package/dist/vue/index.js.map +1 -1
  45. package/package.json +18 -6
  46. package/templates/react/README.md.tmpl +3 -0
  47. package/templates/react/package.json.tmpl +6 -3
  48. package/templates/svelte/README.md.tmpl +2 -0
  49. package/templates/svelte/package.json.tmpl +5 -3
  50. package/templates/svelte/src/env.d.ts.tmpl +10 -0
  51. package/templates/vanilla/README.md.tmpl +2 -0
  52. package/templates/vanilla/package.json.tmpl +5 -2
  53. package/templates/vue/README.md.tmpl +2 -0
  54. package/templates/vue/package.json.tmpl +6 -4
  55. package/templates/vue/src/env.d.ts.tmpl +10 -0
  56. package/dist/chunk-DKK26TRS.js.map +0 -1
  57. package/dist/chunk-HIAIQVK5.js.map +0 -1
  58. package/dist/doctor-4MTC6Y4X.js.map +0 -1
@@ -0,0 +1,243 @@
1
+ import {
2
+ parseMcpAppSource
3
+ } from "./chunk-6T5EIQGE.js";
4
+
5
+ // src/server/core.ts
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
9
+ import {
10
+ EXTENSION_ID,
11
+ RESOURCE_MIME_TYPE,
12
+ getUiCapability,
13
+ registerAppResource,
14
+ registerAppTool
15
+ } from "@modelcontextprotocol/ext-apps/server";
16
+ import { z } from "zod";
17
+ function normalizeVisibility(visibility) {
18
+ if (visibility === void 0) return void 0;
19
+ return typeof visibility === "string" ? [visibility] : [...visibility];
20
+ }
21
+ function isSchemaLibraryValue(value) {
22
+ return typeof value === "object" && value !== null && ("~standard" in value || "_def" in value);
23
+ }
24
+ function normalizeToolSchema(schema) {
25
+ if (!schema || Object.keys(schema).length === 0) return schema;
26
+ const candidate = schema;
27
+ if (isSchemaLibraryValue(candidate) || Object.values(candidate).some(isSchemaLibraryValue)) {
28
+ return schema;
29
+ }
30
+ const jsonSchemaKeys = [
31
+ "$schema",
32
+ "$ref",
33
+ "$defs",
34
+ "type",
35
+ "properties",
36
+ "required",
37
+ "additionalProperties",
38
+ "allOf",
39
+ "anyOf",
40
+ "oneOf"
41
+ ];
42
+ return jsonSchemaKeys.some((key) => key in candidate) ? z.fromJSONSchema(schema) : schema;
43
+ }
44
+ function withTextFallback(result) {
45
+ if (result.content && result.content.length > 0) return result;
46
+ if (result.structuredContent === void 0) return result;
47
+ return {
48
+ ...result,
49
+ content: [{ type: "text", text: JSON.stringify(result.structuredContent) }]
50
+ };
51
+ }
52
+ function withoutUiMetadata(meta) {
53
+ const copy = { ...meta };
54
+ delete copy.ui;
55
+ delete copy["ui/resourceUri"];
56
+ return copy;
57
+ }
58
+ function mergedToolMetadata(tool) {
59
+ const base = { ...tool.config?._meta ?? {} };
60
+ const existingUi = typeof base.ui === "object" && base.ui !== null ? base.ui : {};
61
+ const visibility = normalizeVisibility(tool.visibility) ?? [
62
+ ...existingUi.visibility ?? ["model", "app"]
63
+ ];
64
+ const meta = {
65
+ ...base,
66
+ ui: { ...existingUi, resourceUri: tool.resourceUri, visibility }
67
+ };
68
+ return { meta, withoutUi: withoutUiMetadata(base), visibility };
69
+ }
70
+ async function loadViews(registrations) {
71
+ const loaded = [];
72
+ const names = /* @__PURE__ */ new Set();
73
+ const uris = /* @__PURE__ */ new Set();
74
+ for (const registration of registrations) {
75
+ if (registration.uri !== void 0 && !registration.uri.startsWith("ui://")) {
76
+ throw new Error(
77
+ `mountly-mcp/server: registration uri must use the 'ui://' scheme (received '${registration.uri}')`
78
+ );
79
+ }
80
+ const view = await loadView(registration);
81
+ const artifact = view.artifact;
82
+ if (names.has(artifact.name))
83
+ throw new Error(`mountly-mcp: duplicate app name '${artifact.name}'`);
84
+ if (uris.has(artifact.uri)) throw new Error(`mountly-mcp: duplicate app uri '${artifact.uri}'`);
85
+ names.add(artifact.name);
86
+ uris.add(artifact.uri);
87
+ loaded.push(view);
88
+ }
89
+ return loaded;
90
+ }
91
+ var diskLoader;
92
+ function installDiskViewLoader(loader) {
93
+ diskLoader = loader;
94
+ }
95
+ async function loadView(registration) {
96
+ const supplied = registration.artifact;
97
+ if (typeof supplied !== "string" && "html" in supplied) {
98
+ const source = parseMcpAppSource(supplied, registration.uri);
99
+ return { artifact: source, html: source.html };
100
+ }
101
+ if (!diskLoader) {
102
+ throw new Error(
103
+ "mountly-mcp/server: this runtime has no filesystem to read Views from. Build with mountlyMcpViews({ module: true, \u2026 }) and register the Views it exports: views.map((artifact) => ({ artifact }))."
104
+ );
105
+ }
106
+ return diskLoader(supplied, registration.uri);
107
+ }
108
+ async function registerMcpApps(server, options) {
109
+ if (server.isConnected()) {
110
+ throw new Error("mountly-mcp: registerMcpApps() must run before the server connects");
111
+ }
112
+ if (options.views.length === 0) {
113
+ throw new Error("mountly-mcp: registerMcpApps() requires at least one View");
114
+ }
115
+ const views = await loadViews(options.views);
116
+ const byUri = new Map(views.map((view) => [view.artifact.uri, view]));
117
+ for (const tool of options.tools ?? []) {
118
+ if (!byUri.has(tool.resourceUri)) {
119
+ throw new Error(
120
+ `mountly-mcp: tool '${tool.name}' references undeclared View '${tool.resourceUri}'`
121
+ );
122
+ }
123
+ }
124
+ server.server.registerCapabilities({
125
+ extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } }
126
+ });
127
+ const resources = views.map(({ artifact, html }) => {
128
+ const resourceMeta = {
129
+ ui: artifact.declaration._meta.ui
130
+ };
131
+ const openaiUi = artifact.declaration._meta["openai/ui"];
132
+ if (openaiUi !== void 0) resourceMeta["openai/ui"] = openaiUi;
133
+ return registerAppResource(
134
+ server,
135
+ artifact.name,
136
+ artifact.uri,
137
+ {
138
+ mimeType: artifact.declaration.mimeType,
139
+ description: artifact.declaration.description,
140
+ _meta: resourceMeta
141
+ },
142
+ async () => ({
143
+ contents: [
144
+ {
145
+ uri: artifact.uri,
146
+ mimeType: artifact.declaration.mimeType,
147
+ text: html,
148
+ _meta: resourceMeta
149
+ }
150
+ ]
151
+ })
152
+ );
153
+ });
154
+ const tools = (options.tools ?? []).map((registration) => {
155
+ const { meta, withoutUi, visibility } = mergedToolMetadata(registration);
156
+ const config = registration.config ?? {};
157
+ const registered = registerAppTool(
158
+ server,
159
+ registration.name,
160
+ {
161
+ ...config,
162
+ inputSchema: normalizeToolSchema(config.inputSchema),
163
+ outputSchema: normalizeToolSchema(config.outputSchema),
164
+ _meta: meta
165
+ },
166
+ (async (args, extra) => withTextFallback(await registration.handler(args, extra)))
167
+ );
168
+ return { registration, registered, metadataWithoutUi: withoutUi, visibility };
169
+ });
170
+ const previousInitialized = server.server.oninitialized;
171
+ const handleInitialized = () => {
172
+ previousInitialized?.();
173
+ const ui = getUiCapability(server.server.getClientCapabilities());
174
+ if (ui?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) return;
175
+ for (const resource of resources) resource.remove();
176
+ for (const tool of tools) {
177
+ if (tool.visibility.includes("model")) {
178
+ tool.registered.update({ _meta: tool.metadataWithoutUi });
179
+ } else {
180
+ tool.registered.remove();
181
+ }
182
+ }
183
+ };
184
+ server.server.oninitialized = handleInitialized;
185
+ return {
186
+ artifacts: views.map((view) => view.artifact),
187
+ remove() {
188
+ for (const resource of resources) resource.remove();
189
+ for (const tool of tools) tool.registered.remove();
190
+ if (server.server.oninitialized === handleInitialized) {
191
+ server.server.oninitialized = previousInitialized;
192
+ }
193
+ }
194
+ };
195
+ }
196
+ function createConfiguredServer(options) {
197
+ return new McpServer(
198
+ { name: options.name, version: options.version },
199
+ { capabilities: { resources: {}, tools: {} } }
200
+ );
201
+ }
202
+ function createMcpAppServer(options) {
203
+ let server;
204
+ async function build() {
205
+ const next = createConfiguredServer(options);
206
+ await registerMcpApps(next, { views: options.views, tools: options.tools });
207
+ server = next;
208
+ return next;
209
+ }
210
+ return {
211
+ async connectInProcess(connectOptions) {
212
+ const next = await build();
213
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
214
+ await next.connect(serverTransport);
215
+ const client = new Client(
216
+ { name: `${options.name}-inprocess-client`, version: options.version },
217
+ connectOptions?.ui ?? true ? {
218
+ capabilities: {
219
+ extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } }
220
+ }
221
+ } : void 0
222
+ );
223
+ await client.connect(clientTransport);
224
+ return { client };
225
+ },
226
+ async listen(transport) {
227
+ const next = await build();
228
+ await next.connect(transport);
229
+ },
230
+ async close() {
231
+ await server?.close();
232
+ }
233
+ };
234
+ }
235
+
236
+ export {
237
+ EXTENSION_ID,
238
+ RESOURCE_MIME_TYPE,
239
+ installDiskViewLoader,
240
+ registerMcpApps,
241
+ createMcpAppServer
242
+ };
243
+ //# sourceMappingURL=chunk-UMQ5OULD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/core.ts"],"sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport {\n EXTENSION_ID,\n RESOURCE_MIME_TYPE,\n getUiCapability,\n registerAppResource,\n registerAppTool,\n type McpUiAppToolConfig,\n} from \"@modelcontextprotocol/ext-apps/server\";\nimport type { McpUiToolMeta, McpUiToolVisibility } from \"@modelcontextprotocol/ext-apps\";\nimport { z } from \"zod\";\nimport {\n parseMcpAppSource,\n type McpAppArtifact,\n type McpAppSource,\n} from \"../artifact/declaration.js\";\n\nexport interface McpAppToolResult {\n structuredContent?: unknown;\n content?: ReadonlyArray<{ type: string; [key: string]: unknown }>;\n isError?: boolean;\n}\n\n/** Complete official tool configuration, plus JSON Schema support at Mountly's seam. */\nexport interface McpAppToolConfig extends Omit<\n McpUiAppToolConfig,\n \"_meta\" | \"inputSchema\" | \"outputSchema\"\n> {\n inputSchema?: object;\n outputSchema?: object;\n /** Additional metadata is preserved; Mountly only merges `_meta.ui`. */\n _meta?: Record<string, unknown>;\n}\n\n/** A View is independent of the tools that reference it. */\nexport interface McpAppViewRegistration {\n /**\n * Built artifact or the path to its self-contained HTML — or, on a runtime\n * with no filesystem such as Cloudflare Workers, the View itself, as\n * `mountly-mcp.views.js` exports it.\n */\n artifact: McpAppArtifact | McpAppSource | string;\n /** Optional assertion used by migration adapters; artifacts already own identity. */\n uri?: string;\n}\n\n/** A tool links to a separately declared View through the protocol resource URI. */\nexport interface McpAppToolRegistration<Args = unknown> {\n name: string;\n resourceUri: string;\n config?: McpAppToolConfig;\n /** Convenience override for `_meta.ui.visibility`. */\n visibility?: McpUiToolVisibility | ReadonlyArray<McpUiToolVisibility>;\n handler: (args: Args, extra?: unknown) => Promise<McpAppToolResult>;\n}\n\nexport interface RegisterMcpAppsOptions {\n views: ReadonlyArray<McpAppViewRegistration>;\n tools?: ReadonlyArray<McpAppToolRegistration>;\n}\n\n/**\n * `createMcpAppServer` declares Views and tools exactly as `registerMcpApps`\n * does; it only owns the `McpServer` for you. One vocabulary, two entry points.\n */\nexport interface CreateMcpAppServerOptions extends RegisterMcpAppsOptions {\n name: string;\n version: string;\n}\n\ninterface LoadedView {\n artifact: McpAppArtifact | McpAppSource;\n html: string;\n}\n\ninterface InstalledTool {\n registration: McpAppToolRegistration;\n registered: { remove(): void; update(u: { _meta?: object }): void };\n metadataWithoutUi: Record<string, unknown>;\n visibility: ReadonlyArray<McpUiToolVisibility>;\n}\n\nexport interface RegisteredMcpApps {\n /** File-backed Views carry `htmlPath`; in-memory ones carry `html`. */\n artifacts: ReadonlyArray<McpAppArtifact | McpAppSource>;\n /** Remove every resource and tool installed by this registration. */\n remove(): void;\n}\n\nfunction normalizeVisibility(\n visibility: McpAppToolRegistration[\"visibility\"],\n): McpUiToolVisibility[] | undefined {\n if (visibility === undefined) return undefined;\n return typeof visibility === \"string\" ? [visibility] : [...visibility];\n}\n\nfunction isSchemaLibraryValue(value: unknown): boolean {\n return typeof value === \"object\" && value !== null && (\"~standard\" in value || \"_def\" in value);\n}\n\n/** Convert documented JSON Schema while preserving Zod and Standard Schema values. */\nfunction normalizeToolSchema(schema: object | undefined): object | undefined {\n if (!schema || Object.keys(schema).length === 0) return schema;\n const candidate = schema as Record<string, unknown>;\n if (isSchemaLibraryValue(candidate) || Object.values(candidate).some(isSchemaLibraryValue)) {\n return schema;\n }\n const jsonSchemaKeys = [\n \"$schema\",\n \"$ref\",\n \"$defs\",\n \"type\",\n \"properties\",\n \"required\",\n \"additionalProperties\",\n \"allOf\",\n \"anyOf\",\n \"oneOf\",\n ];\n return jsonSchemaKeys.some((key) => key in candidate)\n ? z.fromJSONSchema(schema as never)\n : schema;\n}\n\nfunction withTextFallback(result: McpAppToolResult): McpAppToolResult {\n if (result.content && result.content.length > 0) return result;\n if (result.structuredContent === undefined) return result;\n return {\n ...result,\n content: [{ type: \"text\", text: JSON.stringify(result.structuredContent) }],\n };\n}\n\nfunction withoutUiMetadata(meta: Record<string, unknown>): Record<string, unknown> {\n const copy = { ...meta };\n delete copy.ui;\n delete copy[\"ui/resourceUri\"];\n return copy;\n}\n\nfunction mergedToolMetadata(tool: McpAppToolRegistration): {\n meta: Record<string, unknown> & { ui: McpUiToolMeta };\n withoutUi: Record<string, unknown>;\n visibility: McpUiToolVisibility[];\n} {\n const base = { ...(tool.config?._meta ?? {}) };\n const existingUi =\n typeof base.ui === \"object\" && base.ui !== null ? (base.ui as Partial<McpUiToolMeta>) : {};\n const visibility = normalizeVisibility(tool.visibility) ?? [\n ...(existingUi.visibility ?? [\"model\", \"app\"]),\n ];\n const meta = {\n ...base,\n ui: { ...existingUi, resourceUri: tool.resourceUri, visibility },\n };\n return { meta, withoutUi: withoutUiMetadata(base), visibility };\n}\n\nasync function loadViews(\n registrations: ReadonlyArray<McpAppViewRegistration>,\n): Promise<ReadonlyArray<LoadedView>> {\n const loaded: LoadedView[] = [];\n const names = new Set<string>();\n const uris = new Set<string>();\n for (const registration of registrations) {\n if (registration.uri !== undefined && !registration.uri.startsWith(\"ui://\")) {\n throw new Error(\n `mountly-mcp/server: registration uri must use the 'ui://' scheme (received '${registration.uri}')`,\n );\n }\n const view = await loadView(registration);\n const artifact = view.artifact;\n if (names.has(artifact.name))\n throw new Error(`mountly-mcp: duplicate app name '${artifact.name}'`);\n if (uris.has(artifact.uri)) throw new Error(`mountly-mcp: duplicate app uri '${artifact.uri}'`);\n names.add(artifact.name);\n uris.add(artifact.uri);\n loaded.push(view);\n }\n return loaded;\n}\n\n/** Reads a View from disk. Only the Node entry installs one; a Worker has no disk. */\nexport type DiskViewLoader = (\n artifact: McpAppArtifact | string,\n uri: string | undefined,\n) => Promise<{ artifact: McpAppArtifact; html: string }>;\n\nlet diskLoader: DiskViewLoader | undefined;\n\n/** @internal Called once by the Node entry. */\nexport function installDiskViewLoader(loader: DiskViewLoader): void {\n diskLoader = loader;\n}\n\nasync function loadView(registration: McpAppViewRegistration): Promise<LoadedView> {\n const supplied = registration.artifact;\n if (typeof supplied !== \"string\" && \"html\" in supplied) {\n const source = parseMcpAppSource(supplied, registration.uri);\n return { artifact: source, html: source.html };\n }\n if (!diskLoader) {\n throw new Error(\n \"mountly-mcp/server: this runtime has no filesystem to read Views from. Build with \" +\n \"mountlyMcpViews({ module: true, … }) and register the Views it exports: \" +\n \"views.map((artifact) => ({ artifact })).\",\n );\n }\n return diskLoader(supplied, registration.uri);\n}\n\n/**\n * Install Mountly Views and their independently declared tools into an existing,\n * unconnected MCP server. The caller retains ownership of identity, auth,\n * transports, prompts, ordinary tools, and deployment.\n */\nexport async function registerMcpApps(\n server: McpServer,\n options: RegisterMcpAppsOptions,\n): Promise<RegisteredMcpApps> {\n if (server.isConnected()) {\n throw new Error(\"mountly-mcp: registerMcpApps() must run before the server connects\");\n }\n if (options.views.length === 0) {\n throw new Error(\"mountly-mcp: registerMcpApps() requires at least one View\");\n }\n const views = await loadViews(options.views);\n const byUri = new Map(views.map((view) => [view.artifact.uri, view]));\n for (const tool of options.tools ?? []) {\n if (!byUri.has(tool.resourceUri)) {\n throw new Error(\n `mountly-mcp: tool '${tool.name}' references undeclared View '${tool.resourceUri}'`,\n );\n }\n }\n\n server.server.registerCapabilities({\n extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } },\n } as never);\n\n const resources = views.map(({ artifact, html }) => {\n const resourceMeta: Record<string, unknown> = {\n ui: artifact.declaration._meta.ui,\n };\n const openaiUi = artifact.declaration._meta[\"openai/ui\"];\n if (openaiUi !== undefined) resourceMeta[\"openai/ui\"] = openaiUi;\n return registerAppResource(\n server,\n artifact.name,\n artifact.uri,\n {\n mimeType: artifact.declaration.mimeType,\n description: artifact.declaration.description,\n _meta: resourceMeta,\n },\n async () => ({\n contents: [\n {\n uri: artifact.uri,\n mimeType: artifact.declaration.mimeType,\n text: html,\n _meta: resourceMeta,\n },\n ],\n }),\n );\n });\n\n const tools: InstalledTool[] = (options.tools ?? []).map((registration) => {\n const { meta, withoutUi, visibility } = mergedToolMetadata(registration);\n const config = registration.config ?? {};\n const registered = registerAppTool(\n server,\n registration.name,\n {\n ...config,\n inputSchema: normalizeToolSchema(config.inputSchema) as never,\n outputSchema: normalizeToolSchema(config.outputSchema) as never,\n _meta: meta,\n },\n (async (args: unknown, extra: unknown) =>\n withTextFallback(await registration.handler(args, extra))) as never,\n );\n return { registration, registered, metadataWithoutUi: withoutUi, visibility };\n });\n\n const previousInitialized = server.server.oninitialized;\n const handleInitialized = () => {\n previousInitialized?.();\n const ui = getUiCapability(server.server.getClientCapabilities());\n if (ui?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) return;\n for (const resource of resources) resource.remove();\n for (const tool of tools) {\n if (tool.visibility.includes(\"model\")) {\n tool.registered.update({ _meta: tool.metadataWithoutUi });\n } else {\n tool.registered.remove();\n }\n }\n };\n server.server.oninitialized = handleInitialized;\n\n return {\n artifacts: views.map((view) => view.artifact),\n remove() {\n for (const resource of resources) resource.remove();\n for (const tool of tools) tool.registered.remove();\n if (server.server.oninitialized === handleInitialized) {\n server.server.oninitialized = previousInitialized;\n }\n },\n };\n}\n\nexport interface RunningMcpAppServer {\n connectInProcess(options?: { ui?: boolean }): Promise<{ client: Client }>;\n listen(transport: Transport): Promise<void>;\n close(): Promise<void>;\n}\n\nfunction createConfiguredServer(options: CreateMcpAppServerOptions): McpServer {\n return new McpServer(\n { name: options.name, version: options.version },\n { capabilities: { resources: {}, tools: {} } },\n );\n}\n\n/** Convenience adapter that retains the original one-call server experience. */\nexport function createMcpAppServer(options: CreateMcpAppServerOptions): RunningMcpAppServer {\n let server: McpServer | undefined;\n async function build(): Promise<McpServer> {\n const next = createConfiguredServer(options);\n await registerMcpApps(next, { views: options.views, tools: options.tools });\n server = next;\n return next;\n }\n return {\n async connectInProcess(connectOptions) {\n const next = await build();\n const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();\n await next.connect(serverTransport);\n const client = new Client(\n { name: `${options.name}-inprocess-client`, version: options.version },\n (connectOptions?.ui ?? true)\n ? ({\n capabilities: {\n extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } },\n },\n } as never)\n : undefined,\n );\n await client.connect(clientTransport);\n return { client };\n },\n async listen(transport) {\n const next = await build();\n await next.connect(transport);\n },\n async close() {\n await server?.close();\n },\n };\n}\n\nexport { EXTENSION_ID, RESOURCE_MIME_TYPE };\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -349,7 +349,7 @@ async function main() {
349
349
  return addView(parseAddArgs(rest));
350
350
  }
351
351
  if (command === "doctor") {
352
- const { doctorCommand } = await import("./doctor-4MTC6Y4X.js");
352
+ const { doctorCommand } = await import("./doctor-OTOROLPI.js");
353
353
  return doctorCommand();
354
354
  }
355
355
  if (command === "dev") return dev(parseArgs(rest));
@@ -18212,6 +18212,8 @@ container holding the app. Specify either width or maxWidth, and either height o
18212
18212
  }
18213
18213
  var capabilities = {
18214
18214
  openLinks: {},
18215
+ // The host side of `downloadFile()`, so a View exercises the real path in dev.
18216
+ downloadFile: {},
18215
18217
  logging: {},
18216
18218
  // A view's `sendMessage` asks the host to post the user's next turn to the agent.
18217
18219
  message: { text: {} },
@@ -18236,6 +18238,27 @@ container holding the app. Specify either width or maxWidth, and either height o
18236
18238
  window.open(url2, "_blank", "noopener,noreferrer");
18237
18239
  return {};
18238
18240
  };
18241
+ bridge.ondownloadfile = async ({ contents }) => {
18242
+ for (const item of contents) {
18243
+ if (item.type === "resource_link") {
18244
+ log("in", `ui/download-file ${item.uri}`);
18245
+ window.open(item.uri, "_blank", "noopener,noreferrer");
18246
+ continue;
18247
+ }
18248
+ const resource = item.resource;
18249
+ const name = decodeURIComponent(resource.uri.split("/").pop() || "download");
18250
+ log("in", `ui/download-file ${name}`);
18251
+ const blob = "blob" in resource ? new Blob([Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0))], {
18252
+ type: resource.mimeType
18253
+ }) : new Blob([resource.text], { type: resource.mimeType });
18254
+ const link = document.createElement("a");
18255
+ link.href = URL.createObjectURL(blob);
18256
+ link.download = name;
18257
+ link.click();
18258
+ setTimeout(() => URL.revokeObjectURL(link.href), 1e3);
18259
+ }
18260
+ return {};
18261
+ };
18239
18262
  bridge.onsizechange = ({ height }) => {
18240
18263
  if (height !== void 0) hostSandbox.style.height = `${Math.max(80, height)}px`;
18241
18264
  };
@@ -6,7 +6,7 @@ import {
6
6
  // src/doctor.ts
7
7
  import { existsSync, readFileSync, readdirSync } from "fs";
8
8
  import { createRequire } from "module";
9
- import { join, resolve } from "path";
9
+ import { dirname, join, relative, resolve } from "path";
10
10
  function nodeMajor() {
11
11
  return Number(process.versions.node.split(".")[0] ?? 0);
12
12
  }
@@ -18,6 +18,13 @@ function tryResolve(specifier, fromDir) {
18
18
  return null;
19
19
  }
20
20
  }
21
+ function findInstalled(pkg, fromDir) {
22
+ for (let dir = resolve(fromDir); ; dir = dirname(dir)) {
23
+ const path = join(dir, "node_modules", pkg, "package.json");
24
+ if (existsSync(path)) return path;
25
+ if (dirname(dir) === dir) return null;
26
+ }
27
+ }
21
28
  var OPENAI_USAGE = /openai\/ui|useOpenAIExtensions|mountly-mcp\/openai|mountly-mcp\/react\/openai|@openai\/mcp-extensions|openaiUi\b|openaiUiToolMeta|enableOpenAiExtensions/;
22
29
  function projectSourcesMentionOpenAi(cwd, viteConfig) {
23
30
  const candidates = [];
@@ -45,6 +52,102 @@ function projectSourcesMentionOpenAi(cwd, viteConfig) {
45
52
  }
46
53
  return false;
47
54
  }
55
+ var SANDBOX_HAZARDS = [
56
+ {
57
+ id: "storage",
58
+ pattern: /\b(?:localStorage|sessionStorage|indexedDB)\b/,
59
+ fix: "throws in a sandbox without allow-same-origin. Wrap each access in try/catch, or keep the state in memory or on the server."
60
+ },
61
+ {
62
+ id: "router",
63
+ pattern: /\b(?:BrowserRouter|createBrowserRouter)\b/,
64
+ fix: "a ui:// View has no URL to route on. Render the page directly, or use a MemoryRouter."
65
+ },
66
+ {
67
+ id: "download",
68
+ pattern: /\.download\s*=|\bdownload=|\bsaveAs\s*\(|\bXLSX\.writeFile(?:XLSX)?\s*\(/,
69
+ fix: "hosts block <a download>. Use useDownload() from mountly-mcp/react, or downloadFile() from mountly-mcp."
70
+ },
71
+ {
72
+ id: "worker",
73
+ pattern: /new\s+(?:Shared)?Worker\s*\((?!\s*new\s+URL\s*\()/,
74
+ fix: 'only new Worker(new URL("./w.ts", import.meta.url)) is bundled into the View. Start the worker that way.'
75
+ },
76
+ {
77
+ id: "remote-import",
78
+ pattern: /(?:import\s*\(|\bfrom)\s*["'`]https?:\/\//,
79
+ fix: "remote modules load only from origins in csp.resourceDomains. Install and bundle the package instead."
80
+ },
81
+ {
82
+ id: "hostname",
83
+ pattern: /\blocation\.(?:hostname|host|origin)\b/,
84
+ fix: "in a host iframe this is the sandbox's origin, not your site. Pass configuration in through the tool result."
85
+ },
86
+ {
87
+ id: "relative-fetch",
88
+ pattern: /\bfetch\s*\(\s*[`'"]\/(?!\/)/,
89
+ fix: "a View has no origin to resolve /api against. Call an app-only server tool with callServerTool(), or fetch an absolute URL listed in csp.connectDomains."
90
+ },
91
+ {
92
+ id: "window-open",
93
+ pattern: /\bwindow\.open\s*\(/,
94
+ fix: "popups are blocked. Use app.openLink({ url }) from useMcpApp()."
95
+ }
96
+ ];
97
+ function sourceFiles(dir) {
98
+ const out = [];
99
+ let entries;
100
+ try {
101
+ entries = readdirSync(dir, { withFileTypes: true });
102
+ } catch {
103
+ return out;
104
+ }
105
+ for (const entry of entries) {
106
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
107
+ continue;
108
+ }
109
+ const path = join(dir, entry.name);
110
+ if (entry.isDirectory()) out.push(...sourceFiles(path));
111
+ else if (/\.(?:[cm]?[jt]sx?|vue|svelte)$/.test(entry.name) && !/\.(?:test|spec)\./.test(entry.name)) {
112
+ out.push(path);
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ function scanSandboxHazards(cwd) {
118
+ const hits = /* @__PURE__ */ new Map();
119
+ for (const file of sourceFiles(resolve(cwd, "src"))) {
120
+ let text;
121
+ try {
122
+ text = readFileSync(file, "utf8");
123
+ } catch {
124
+ continue;
125
+ }
126
+ for (const hazard of SANDBOX_HAZARDS) {
127
+ for (const match of text.matchAll(new RegExp(hazard.pattern.source, "g"))) {
128
+ const line = text.slice(0, match.index).split("\n").length;
129
+ const at = hits.get(hazard.id) ?? [];
130
+ at.push(`${relative(cwd, file)}:${line}`);
131
+ hits.set(hazard.id, at);
132
+ }
133
+ }
134
+ }
135
+ if (hits.size === 0) {
136
+ return [
137
+ { id: "sandbox", ok: true, detail: "no sandbox hazards found in src/", advisory: true }
138
+ ];
139
+ }
140
+ return SANDBOX_HAZARDS.filter((hazard) => hits.has(hazard.id)).map((hazard) => {
141
+ const at = hits.get(hazard.id) ?? [];
142
+ const more = at.length > 3 ? ` (+${at.length - 3} more)` : "";
143
+ return {
144
+ id: `sandbox/${hazard.id}`,
145
+ ok: false,
146
+ advisory: true,
147
+ detail: `${at.slice(0, 3).join(", ")}${more}: ${hazard.fix}`
148
+ };
149
+ });
150
+ }
48
151
  function runDoctor(cwd = process.cwd()) {
49
152
  const checks = [];
50
153
  const major = nodeMajor();
@@ -78,12 +181,16 @@ function runDoctor(cwd = process.cwd()) {
78
181
  checks.push({
79
182
  id: "manifest",
80
183
  ok: existsSync(manifest),
184
+ // A brand-new scaffold has not built yet.
185
+ advisory: true,
81
186
  detail: existsSync(manifest) ? `Found ${manifest}` : "No dist/mountly-mcp.manifest.json yet. Run: npx mountly-mcp build"
82
187
  });
83
188
  const playwright = tryResolve("playwright", cwd) ?? tryResolve("@playwright/test", cwd);
84
189
  checks.push({
85
190
  id: "playwright",
86
191
  ok: Boolean(playwright),
192
+ // Only `verify --render` needs it.
193
+ advisory: true,
87
194
  detail: playwright ? `Playwright resolvable for verify --render (${playwright})` : "Playwright not installed. For CI render checks: pnpm add -D playwright && npx playwright install chromium"
88
195
  });
89
196
  const pkgPath = resolve(cwd, "package.json");
@@ -97,8 +204,8 @@ function runDoctor(cwd = process.cwd()) {
97
204
  const reactMajor = Number((reactPkg.version ?? "0").split(".")[0]);
98
205
  checks.push({
99
206
  id: "react",
100
- ok: reactMajor >= 19,
101
- detail: reactMajor >= 19 ? `React ${reactPkg.version} (ok)` : `React ${reactPkg.version} is below 19. mountly-mcp/react and mountly-react require React 19.`
207
+ ok: reactMajor >= 18,
208
+ detail: reactMajor >= 18 ? `React ${reactPkg.version} (ok)` : `React ${reactPkg.version} is below 18. mountly-mcp/react and mountly-react need createRoot, from React 18.`
102
209
  });
103
210
  }
104
211
  }
@@ -116,21 +223,22 @@ function runDoctor(cwd = process.cwd()) {
116
223
  }
117
224
  }
118
225
  if (projectSourcesMentionOpenAi(cwd, viteConfig)) {
119
- const openai = tryResolve("@openai/mcp-extensions/package.json", cwd);
226
+ const openai = findInstalled("@openai/mcp-extensions", cwd);
120
227
  checks.push({
121
228
  id: "openai-extensions",
122
229
  ok: Boolean(openai),
123
230
  detail: openai ? `@openai/mcp-extensions resolvable (${openai})` : "Project references OpenAI MCP extensions but @openai/mcp-extensions is not installed. Run: pnpm add @openai/mcp-extensions"
124
231
  });
125
232
  }
126
- const hard = checks.filter((c) => c.id !== "manifest" && c.id !== "playwright");
127
- const ok = hard.every((c) => c.ok);
233
+ checks.push(...scanSandboxHazards(cwd));
234
+ const ok = checks.every((c) => c.ok || c.advisory);
128
235
  return { ok, checks };
129
236
  }
130
237
  function formatDoctorReport(report) {
131
238
  const lines = ["mountly-mcp doctor", ""];
132
239
  for (const check of report.checks) {
133
- lines.push(`${check.ok ? "ok" : "FAIL"} ${check.id.padEnd(14)} ${check.detail}`);
240
+ const status = check.ok ? "ok" : check.advisory ? "warn" : "FAIL";
241
+ lines.push(`${status.padEnd(4)} ${check.id.padEnd(22)} ${check.detail}`);
134
242
  }
135
243
  lines.push("");
136
244
  lines.push(report.ok ? "Ready for mountly-mcp build / dev / verify." : "Fix FAIL items above.");
@@ -152,6 +260,7 @@ function doctorCommand(cwd = process.cwd()) {
152
260
  export {
153
261
  doctorCommand,
154
262
  formatDoctorReport,
155
- runDoctor
263
+ runDoctor,
264
+ scanSandboxHazards
156
265
  };
157
- //# sourceMappingURL=doctor-4MTC6Y4X.js.map
266
+ //# sourceMappingURL=doctor-OTOROLPI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/doctor.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { CLI_ERROR_CODES, cliError } from \"./errors.js\";\n\nexport interface DoctorCheck {\n id: string;\n ok: boolean;\n detail: string;\n /** Worth fixing, but never fails the run: printed as `warn`, not `FAIL`. */\n advisory?: boolean;\n}\n\nexport interface DoctorReport {\n ok: boolean;\n checks: DoctorCheck[];\n}\n\nfunction nodeMajor(): number {\n return Number(process.versions.node.split(\".\")[0] ?? 0);\n}\n\nfunction tryResolve(specifier: string, fromDir: string): string | null {\n try {\n const require = createRequire(resolve(fromDir, \"package.json\"));\n return require.resolve(specifier);\n } catch {\n return null;\n }\n}\n\n/**\n * Path to `pkg` in the project's node_modules, walking up from `fromDir`. Reads the\n * filesystem, so packages whose `exports` omit `./package.json` resolve too.\n */\nfunction findInstalled(pkg: string, fromDir: string): string | null {\n for (let dir = resolve(fromDir); ; dir = dirname(dir)) {\n const path = join(dir, \"node_modules\", pkg, \"package.json\");\n if (existsSync(path)) return path;\n if (dirname(dir) === dir) return null;\n }\n}\n\nconst OPENAI_USAGE =\n /openai\\/ui|useOpenAIExtensions|mountly-mcp\\/openai|mountly-mcp\\/react\\/openai|@openai\\/mcp-extensions|openaiUi\\b|openaiUiToolMeta|enableOpenAiExtensions/;\n\nfunction projectSourcesMentionOpenAi(cwd: string, viteConfig?: string): boolean {\n const candidates: string[] = [];\n if (viteConfig) candidates.push(viteConfig);\n for (const name of [\"server.ts\", \"server.mjs\", \"server.js\", \"src/server.ts\", \"src/view.tsx\"]) {\n const path = resolve(cwd, name);\n if (existsSync(path)) candidates.push(path);\n }\n const srcDir = resolve(cwd, \"src\");\n if (existsSync(srcDir)) {\n try {\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n if (entry.isFile() && /\\.(tsx?|jsx?|mjs|cjs)$/.test(entry.name)) {\n candidates.push(join(srcDir, entry.name));\n }\n }\n } catch {\n // ignore unreadable src\n }\n }\n for (const path of candidates) {\n try {\n if (OPENAI_USAGE.test(readFileSync(path, \"utf8\"))) return true;\n } catch {\n // skip\n }\n }\n return false;\n}\n\n/**\n * Habits of an ordinary web app that break once it runs as a View, inside a\n * host's sandboxed iframe with no origin of its own. Text patterns, not\n * analysis: each hit marks a line to check.\n */\nconst SANDBOX_HAZARDS: ReadonlyArray<{ id: string; pattern: RegExp; fix: string }> = [\n {\n id: \"storage\",\n pattern: /\\b(?:localStorage|sessionStorage|indexedDB)\\b/,\n fix: \"throws in a sandbox without allow-same-origin. Wrap each access in try/catch, or keep the state in memory or on the server.\",\n },\n {\n id: \"router\",\n pattern: /\\b(?:BrowserRouter|createBrowserRouter)\\b/,\n fix: \"a ui:// View has no URL to route on. Render the page directly, or use a MemoryRouter.\",\n },\n {\n id: \"download\",\n pattern: /\\.download\\s*=|\\bdownload=|\\bsaveAs\\s*\\(|\\bXLSX\\.writeFile(?:XLSX)?\\s*\\(/,\n fix: \"hosts block <a download>. Use useDownload() from mountly-mcp/react, or downloadFile() from mountly-mcp.\",\n },\n {\n id: \"worker\",\n pattern: /new\\s+(?:Shared)?Worker\\s*\\((?!\\s*new\\s+URL\\s*\\()/,\n fix: 'only new Worker(new URL(\"./w.ts\", import.meta.url)) is bundled into the View. Start the worker that way.',\n },\n {\n id: \"remote-import\",\n pattern: /(?:import\\s*\\(|\\bfrom)\\s*[\"'`]https?:\\/\\//,\n fix: \"remote modules load only from origins in csp.resourceDomains. Install and bundle the package instead.\",\n },\n {\n id: \"hostname\",\n pattern: /\\blocation\\.(?:hostname|host|origin)\\b/,\n fix: \"in a host iframe this is the sandbox's origin, not your site. Pass configuration in through the tool result.\",\n },\n {\n id: \"relative-fetch\",\n pattern: /\\bfetch\\s*\\(\\s*[`'\"]\\/(?!\\/)/,\n fix: \"a View has no origin to resolve /api against. Call an app-only server tool with callServerTool(), or fetch an absolute URL listed in csp.connectDomains.\",\n },\n {\n id: \"window-open\",\n pattern: /\\bwindow\\.open\\s*\\(/,\n fix: \"popups are blocked. Use app.openLink({ url }) from useMcpApp().\",\n },\n];\n\nfunction sourceFiles(dir: string): string[] {\n const out: string[] = [];\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.name === \"node_modules\" || entry.name === \"dist\" || entry.name.startsWith(\".\")) {\n continue;\n }\n const path = join(dir, entry.name);\n if (entry.isDirectory()) out.push(...sourceFiles(path));\n else if (\n /\\.(?:[cm]?[jt]sx?|vue|svelte)$/.test(entry.name) &&\n !/\\.(?:test|spec)\\./.test(entry.name)\n ) {\n out.push(path);\n }\n }\n return out;\n}\n\n/** One advisory check per hazard found under `src/`, naming where to look. */\nexport function scanSandboxHazards(cwd: string): DoctorCheck[] {\n const hits = new Map<string, string[]>();\n for (const file of sourceFiles(resolve(cwd, \"src\"))) {\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n continue;\n }\n // Whole-file, not per line: `new Worker(\\n new URL(…)` is one call.\n for (const hazard of SANDBOX_HAZARDS) {\n for (const match of text.matchAll(new RegExp(hazard.pattern.source, \"g\"))) {\n const line = text.slice(0, match.index).split(\"\\n\").length;\n const at = hits.get(hazard.id) ?? [];\n at.push(`${relative(cwd, file)}:${line}`);\n hits.set(hazard.id, at);\n }\n }\n }\n if (hits.size === 0) {\n return [\n { id: \"sandbox\", ok: true, detail: \"no sandbox hazards found in src/\", advisory: true },\n ];\n }\n return SANDBOX_HAZARDS.filter((hazard) => hits.has(hazard.id)).map((hazard) => {\n const at = hits.get(hazard.id) ?? [];\n const more = at.length > 3 ? ` (+${at.length - 3} more)` : \"\";\n return {\n id: `sandbox/${hazard.id}`,\n ok: false,\n advisory: true,\n detail: `${at.slice(0, 3).join(\", \")}${more}: ${hazard.fix}`,\n };\n });\n}\n\n/**\n * Check the local project for Mountly MCP readiness. Exits nonzero via the\n * CLI when `ok` is false.\n */\nexport function runDoctor(cwd = process.cwd()): DoctorReport {\n const checks: DoctorCheck[] = [];\n\n const major = nodeMajor();\n checks.push({\n id: \"node\",\n ok: major >= 20,\n detail:\n major >= 20\n ? `Node ${process.versions.node} (ok; 20+ required, 22+ recommended)`\n : `Node ${process.versions.node} is below 20. Upgrade Node, then re-run mountly-mcp doctor.`,\n });\n\n const viteConfigCandidates = [\n \"vite.config.ts\",\n \"vite.config.mts\",\n \"vite.config.js\",\n \"vite.config.mjs\",\n ].map((name) => resolve(cwd, name));\n const viteConfig = viteConfigCandidates.find((path) => existsSync(path));\n checks.push({\n id: \"vite-config\",\n ok: Boolean(viteConfig),\n detail: viteConfig\n ? `Found ${viteConfig}`\n : \"No vite.config.* in this directory. Run from the View project, or: npx mountly-mcp create my-app --framework react\",\n });\n\n if (viteConfig) {\n const source = readFileSync(viteConfig, \"utf8\");\n const hasPlugin = /mountlyMcpViews\\s*\\(/.test(source);\n checks.push({\n id: \"vite-plugin\",\n ok: hasPlugin,\n detail: hasPlugin\n ? \"vite config imports mountlyMcpViews()\"\n : `Add mountlyMcpViews() from \"mountly-mcp/vite\" to ${viteConfig}`,\n });\n }\n\n const manifest = resolve(cwd, \"dist/mountly-mcp.manifest.json\");\n checks.push({\n id: \"manifest\",\n ok: existsSync(manifest),\n // A brand-new scaffold has not built yet.\n advisory: true,\n detail: existsSync(manifest)\n ? `Found ${manifest}`\n : \"No dist/mountly-mcp.manifest.json yet. Run: npx mountly-mcp build\",\n });\n\n const playwright = tryResolve(\"playwright\", cwd) ?? tryResolve(\"@playwright/test\", cwd);\n checks.push({\n id: \"playwright\",\n ok: Boolean(playwright),\n // Only `verify --render` needs it.\n advisory: true,\n detail: playwright\n ? `Playwright resolvable for verify --render (${playwright})`\n : \"Playwright not installed. For CI render checks: pnpm add -D playwright && npx playwright install chromium\",\n });\n\n const pkgPath = resolve(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as {\n dependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n if (deps.react) {\n const reactPath = tryResolve(\"react/package.json\", cwd);\n if (reactPath) {\n const reactPkg = JSON.parse(readFileSync(reactPath, \"utf8\")) as { version?: string };\n const reactMajor = Number((reactPkg.version ?? \"0\").split(\".\")[0]);\n checks.push({\n id: \"react\",\n ok: reactMajor >= 18,\n detail:\n reactMajor >= 18\n ? `React ${reactPkg.version} (ok)`\n : `React ${reactPkg.version} is below 18. mountly-mcp/react and mountly-react need createRoot, from React 18.`,\n });\n }\n }\n if (deps.vite) {\n const vitePath = tryResolve(\"vite/package.json\", cwd);\n if (vitePath) {\n const vitePkg = JSON.parse(readFileSync(vitePath, \"utf8\")) as { version?: string };\n const viteMajor = Number((vitePkg.version ?? \"0\").split(\".\")[0]);\n checks.push({\n id: \"vite\",\n ok: viteMajor >= 8,\n detail:\n viteMajor >= 8\n ? `Vite ${vitePkg.version} (ok)`\n : `Vite ${vitePkg.version} is below 8. mountly-mcp/vite is built and tested against Vite 8.`,\n });\n }\n }\n }\n\n if (projectSourcesMentionOpenAi(cwd, viteConfig)) {\n const openai = findInstalled(\"@openai/mcp-extensions\", cwd);\n checks.push({\n id: \"openai-extensions\",\n ok: Boolean(openai),\n detail: openai\n ? `@openai/mcp-extensions resolvable (${openai})`\n : \"Project references OpenAI MCP extensions but @openai/mcp-extensions is not installed. Run: pnpm add @openai/mcp-extensions\",\n });\n }\n\n checks.push(...scanSandboxHazards(cwd));\n\n const ok = checks.every((c) => c.ok || c.advisory);\n\n return { ok, checks };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n const lines = [\"mountly-mcp doctor\", \"\"];\n for (const check of report.checks) {\n const status = check.ok ? \"ok\" : check.advisory ? \"warn\" : \"FAIL\";\n lines.push(`${status.padEnd(4)} ${check.id.padEnd(22)} ${check.detail}`);\n }\n lines.push(\"\");\n lines.push(report.ok ? \"Ready for mountly-mcp build / dev / verify.\" : \"Fix FAIL items above.\");\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function doctorCommand(cwd = process.cwd()): void {\n const report = runDoctor(cwd);\n process.stdout.write(formatDoctorReport(report));\n if (!report.ok) {\n throw cliError(\n CLI_ERROR_CODES.DOCTOR_FAILED,\n \"one or more doctor checks failed\",\n \"Fix the FAIL lines above, then re-run: mountly-mcp doctor\",\n );\n }\n}\n"],"mappings":"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","names":["require"]}
package/dist/doctor.d.ts CHANGED
@@ -2,11 +2,15 @@ export interface DoctorCheck {
2
2
  id: string;
3
3
  ok: boolean;
4
4
  detail: string;
5
+ /** Worth fixing, but never fails the run: printed as `warn`, not `FAIL`. */
6
+ advisory?: boolean;
5
7
  }
6
8
  export interface DoctorReport {
7
9
  ok: boolean;
8
10
  checks: DoctorCheck[];
9
11
  }
12
+ /** One advisory check per hazard found under `src/`, naming where to look. */
13
+ export declare function scanSandboxHazards(cwd: string): DoctorCheck[];
10
14
  /**
11
15
  * Check the local project for Mountly MCP readiness. Exits nonzero via the
12
16
  * CLI when `ok` is false.
@@ -0,0 +1,17 @@
1
+ import type { App } from "@modelcontextprotocol/ext-apps";
2
+ export interface DownloadableFile {
3
+ /** The name the user saves it as. */
4
+ name: string;
5
+ data: Blob | string;
6
+ /** Defaults to the Blob's own type, or `text/plain` for a string. */
7
+ mimeType?: string;
8
+ }
9
+ /**
10
+ * Hand the user a file. A View's iframe usually cannot download on its own, so
11
+ * a host that supports `ui/download-file` is asked to save it, with its own
12
+ * confirmation. Anywhere else — a host without it, the dev harness, an ordinary
13
+ * page — an `<a download>` is clicked, which a strict sandbox may still block.
14
+ *
15
+ * Resolves `false` when the host declined or the user cancelled.
16
+ */
17
+ export declare function downloadFile(app: App | undefined, file: DownloadableFile): Promise<boolean>;