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.
- package/README.md +89 -0
- package/dist/artifact/declaration.d.ts +61 -0
- package/dist/artifact/index.d.ts +2 -42
- package/dist/artifact/index.js +9 -5
- package/dist/chunk-6T5EIQGE.js +104 -0
- package/dist/chunk-6T5EIQGE.js.map +1 -0
- package/dist/chunk-BLT2MK32.js +35 -0
- package/dist/chunk-BLT2MK32.js.map +1 -0
- package/dist/{chunk-HIAIQVK5.js → chunk-QIA76O6M.js} +9 -1
- package/dist/chunk-QIA76O6M.js.map +1 -0
- package/dist/{chunk-DKK26TRS.js → chunk-RLDSMU5M.js} +9 -70
- package/dist/chunk-RLDSMU5M.js.map +1 -0
- package/dist/chunk-UMQ5OULD.js +243 -0
- package/dist/chunk-UMQ5OULD.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/dev/host-entry.js +23 -0
- package/dist/{doctor-4MTC6Y4X.js → doctor-OTOROLPI.js} +118 -9
- package/dist/doctor-OTOROLPI.js.map +1 -0
- package/dist/doctor.d.ts +4 -0
- package/dist/download.d.ts +17 -0
- package/dist/events/index.d.ts +97 -0
- package/dist/events/index.js +215 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/json-render/index.js +2 -1
- package/dist/json-render/index.js.map +1 -1
- package/dist/react/hooks.d.ts +7 -0
- package/dist/react/index.js +4 -1
- package/dist/server/core.d.ts +84 -0
- package/dist/server/index.d.ts +2 -72
- package/dist/server/index.js +19 -219
- package/dist/server/index.js.map +1 -1
- package/dist/server/workers.d.ts +7 -0
- package/dist/server/workers.js +14 -0
- package/dist/server/workers.js.map +1 -0
- package/dist/testing/index.js +5 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/vite/index.d.ts +15 -1
- package/dist/vite/index.js +133 -11
- package/dist/vite/index.js.map +1 -1
- package/dist/vue/index.d.ts +6 -0
- package/dist/vue/index.js +8 -0
- package/dist/vue/index.js.map +1 -1
- package/package.json +18 -6
- package/templates/react/README.md.tmpl +3 -0
- package/templates/react/package.json.tmpl +6 -3
- package/templates/svelte/README.md.tmpl +2 -0
- package/templates/svelte/package.json.tmpl +5 -3
- package/templates/svelte/src/env.d.ts.tmpl +10 -0
- package/templates/vanilla/README.md.tmpl +2 -0
- package/templates/vanilla/package.json.tmpl +5 -2
- package/templates/vue/README.md.tmpl +2 -0
- package/templates/vue/package.json.tmpl +6 -4
- package/templates/vue/src/env.d.ts.tmpl +10 -0
- package/dist/chunk-DKK26TRS.js.map +0 -1
- package/dist/chunk-HIAIQVK5.js.map +0 -1
- package/dist/doctor-4MTC6Y4X.js.map +0 -1
package/README.md
CHANGED
|
@@ -207,10 +207,98 @@ function Library() {
|
|
|
207
207
|
}
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
**Entrypoint types** place one View on several ChatGPT surfaces. Other hosts
|
|
211
|
+
ignore `openai/ui` and render the View inline:
|
|
212
|
+
|
|
213
|
+
| `type` | ChatGPT surface | Extra fields |
|
|
214
|
+
| ---------- | ----------------------------------------------- | -------------------------------- |
|
|
215
|
+
| `global` | Persistent sidebar panel | `quickAction` (sidebar shortcut) |
|
|
216
|
+
| `thread` | Conversation panel beside the chat | none |
|
|
217
|
+
| `file` | File viewer/editor for matching extensions | `extensions` |
|
|
218
|
+
| `settings` | Plugin settings page | `searchTerms` |
|
|
219
|
+
|
|
220
|
+
A file viewer receives the opened file as tool input. Parse it with the SDK schema:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { OpenAIFileEntrypointInputSchema } from "@openai/mcp-extensions/server";
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
name: "parts.open_cad",
|
|
227
|
+
resourceUri: "ui://parts/cad-viewer",
|
|
228
|
+
config: {
|
|
229
|
+
inputSchema: OpenAIFileEntrypointInputSchema.shape,
|
|
230
|
+
_meta: openaiUiToolMeta({
|
|
231
|
+
entrypoints: [{ type: "file", extensions: [".step", ".stl"] }],
|
|
232
|
+
}),
|
|
233
|
+
},
|
|
234
|
+
handler: async ({ file }) => ({ structuredContent: { name: file.name, uri: file.resourceUri } }),
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
210
238
|
Smoke in ChatGPT: serve Streamable HTTP at `/mcp`, enable Developer mode, add the
|
|
211
239
|
connector, then open a `global` entrypoint from the Desktop sidebar. See the
|
|
212
240
|
[host matrix](https://mountly.dev/mcp-apps/host-matrix/).
|
|
213
241
|
|
|
242
|
+
## MCP Events (experimental)
|
|
243
|
+
|
|
244
|
+
`mountly-mcp/events` lets your server wake a ChatGPT conversation when something
|
|
245
|
+
changes, such as a finished deploy or a new comment. It implements webhook
|
|
246
|
+
delivery from the draft MCP Events spec: Standard Webhooks signing, callback
|
|
247
|
+
challenge verification, retries with backoff, and `410` cleanup. The API follows
|
|
248
|
+
the draft spec.
|
|
249
|
+
|
|
250
|
+
You need an **MCP 2.0** server (`@modelcontextprotocol/server` v2, protocol
|
|
251
|
+
`2026-07-28`). Register the helpers as custom methods:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
255
|
+
import { defineEvents } from "mountly-mcp/events";
|
|
256
|
+
import { z } from "zod";
|
|
257
|
+
|
|
258
|
+
const events = defineEvents({
|
|
259
|
+
events: {
|
|
260
|
+
"deploy.finished": {
|
|
261
|
+
description: "A deploy for the app finished.",
|
|
262
|
+
input: z.object({ app: z.string() }),
|
|
263
|
+
payload: z.object({ app: z.string(), id: z.string(), status: z.enum(["ok", "failed"]) }),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
store: subscriptionsTable, // get/set/delete/values; a Map is fine for dev
|
|
267
|
+
authorize: ({ arguments: args, authInfo }) => canSeeApp(authInfo, args.app),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const handler = createMcpHandler(() => {
|
|
271
|
+
const server = new McpServer({ name: "deploys", version: "1.0.0" });
|
|
272
|
+
server.server.registerCapabilities({ events: {} });
|
|
273
|
+
const params = z.looseObject({});
|
|
274
|
+
server.server.setRequestHandler("events/list", { params }, () => events.list());
|
|
275
|
+
server.server.setRequestHandler("events/subscribe", { params }, (p, ctx) =>
|
|
276
|
+
events.subscribe(p, { authInfo: ctx.http?.authInfo }));
|
|
277
|
+
server.server.setRequestHandler("events/unsubscribe", { params }, (p, ctx) =>
|
|
278
|
+
events.unsubscribe(p, { authInfo: ctx.http?.authInfo }));
|
|
279
|
+
return server;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Later, when it happens:
|
|
283
|
+
await events.emit(
|
|
284
|
+
"deploy.finished",
|
|
285
|
+
{ app: "web", id: "d_42", status: "ok" },
|
|
286
|
+
(args) => args.app === "web",
|
|
287
|
+
);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Give the events server at least one tool. ChatGPT creates a connector once
|
|
291
|
+
`tools/list` succeeds.
|
|
292
|
+
|
|
293
|
+
`authorize` runs on subscribe and again before each delivery, using the principal
|
|
294
|
+
stored at subscribe time, so revoking access stops delivery. Callback URLs use
|
|
295
|
+
`https:`; pass `allowCallbackUrl` to restrict them further. Keep `authInfo`
|
|
296
|
+
serializable when your store persists subscriptions.
|
|
297
|
+
|
|
298
|
+
ChatGPT subscribes from Work chats and processes events asynchronously. See the
|
|
299
|
+
[`mcp-chatgpt-plugin` example](https://github.com/jagreehal/mountly/tree/main/docs/examples/mcp-chatgpt-plugin) for a
|
|
300
|
+
full walkthrough.
|
|
301
|
+
|
|
214
302
|
## Verify
|
|
215
303
|
|
|
216
304
|
```bash
|
|
@@ -281,6 +369,7 @@ the view sends the agent its next turn with the current page. The
|
|
|
281
369
|
| `mountly-mcp/artifact` | Manifest APIs |
|
|
282
370
|
| `mountly-mcp/server` | `registerMcpApps` |
|
|
283
371
|
| `mountly-mcp/openai/server` | ChatGPT helpers (`enableOpenAiExtensions`, `openaiUiToolMeta`) |
|
|
372
|
+
| `mountly-mcp/events` | Experimental MCP Events (`defineEvents`, `signWebhook`) |
|
|
284
373
|
| `mountly-mcp/dev` | Local host helpers |
|
|
285
374
|
| `mountly-mcp/testing` | `verifyMcpApps` |
|
|
286
375
|
| `mountly-mcp/json-render` | Generative path |
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of an artifact that need no filesystem: its types, and the checks
|
|
3
|
+
* a resource declaration must pass. Kept apart so a server on a runtime without
|
|
4
|
+
* `node:fs` (Cloudflare Workers) can register Views held in memory.
|
|
5
|
+
*/
|
|
6
|
+
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/ext-apps";
|
|
7
|
+
import type { McpResourceDeclaration } from "../types.js";
|
|
8
|
+
export declare const MCP_APP_MANIFEST_VERSION: 1;
|
|
9
|
+
export declare const MCP_APP_MANIFEST_FILE = "mountly-mcp.manifest.json";
|
|
10
|
+
export interface McpAppArtifact {
|
|
11
|
+
/** Developer-facing unique key. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Protocol identity. */
|
|
14
|
+
uri: string;
|
|
15
|
+
/** Absolute path to the self-contained View HTML. */
|
|
16
|
+
htmlPath: string;
|
|
17
|
+
/** Absolute path to the transitional per-View sidecar. */
|
|
18
|
+
metaPath: string;
|
|
19
|
+
declaration: McpResourceDeclaration;
|
|
20
|
+
}
|
|
21
|
+
export interface McpAppManifestEntry {
|
|
22
|
+
name: string;
|
|
23
|
+
uri: string;
|
|
24
|
+
/** Path relative to the manifest, unless explicitly absolute. */
|
|
25
|
+
html: string;
|
|
26
|
+
/** Path relative to the manifest, unless explicitly absolute. */
|
|
27
|
+
meta: string;
|
|
28
|
+
declaration: McpResourceDeclaration;
|
|
29
|
+
}
|
|
30
|
+
export interface McpAppManifest {
|
|
31
|
+
formatVersion: typeof MCP_APP_MANIFEST_VERSION;
|
|
32
|
+
protocolVersion: typeof LATEST_PROTOCOL_VERSION;
|
|
33
|
+
apps: ReadonlyArray<McpAppManifestEntry>;
|
|
34
|
+
}
|
|
35
|
+
export type ConformanceSeverity = "error" | "warning";
|
|
36
|
+
export interface ConformanceDiagnostic {
|
|
37
|
+
severity: ConformanceSeverity;
|
|
38
|
+
code: string;
|
|
39
|
+
message: string;
|
|
40
|
+
source?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class McpAppConformanceError extends Error {
|
|
43
|
+
readonly diagnostics: ReadonlyArray<ConformanceDiagnostic>;
|
|
44
|
+
constructor(diagnostics: ReadonlyArray<ConformanceDiagnostic>);
|
|
45
|
+
}
|
|
46
|
+
export declare function object(value: unknown): Record<string, unknown> | undefined;
|
|
47
|
+
export declare function inspectMcpResourceDeclaration(value: unknown, source?: string): ReadonlyArray<ConformanceDiagnostic>;
|
|
48
|
+
export declare function parseMcpResourceDeclaration(value: unknown, source?: string): McpResourceDeclaration;
|
|
49
|
+
/**
|
|
50
|
+
* A View held in memory rather than on disk: what `mountly-mcp build` writes to
|
|
51
|
+
* `mountly-mcp.views.js` for servers that cannot read files.
|
|
52
|
+
*/
|
|
53
|
+
export interface McpAppSource {
|
|
54
|
+
name: string;
|
|
55
|
+
uri: string;
|
|
56
|
+
declaration: McpResourceDeclaration;
|
|
57
|
+
/** The self-contained View HTML. */
|
|
58
|
+
html: string;
|
|
59
|
+
}
|
|
60
|
+
/** Check an in-memory View the way `readMcpAppArtifact` checks one on disk. */
|
|
61
|
+
export declare function parseMcpAppSource(value: McpAppSource, expectedUri?: string): McpAppSource;
|
package/dist/artifact/index.d.ts
CHANGED
|
@@ -1,45 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare const MCP_APP_MANIFEST_VERSION: 1;
|
|
4
|
-
export declare const MCP_APP_MANIFEST_FILE = "mountly-mcp.manifest.json";
|
|
5
|
-
export interface McpAppArtifact {
|
|
6
|
-
/** Developer-facing unique key. */
|
|
7
|
-
name: string;
|
|
8
|
-
/** Protocol identity. */
|
|
9
|
-
uri: string;
|
|
10
|
-
/** Absolute path to the self-contained View HTML. */
|
|
11
|
-
htmlPath: string;
|
|
12
|
-
/** Absolute path to the transitional per-View sidecar. */
|
|
13
|
-
metaPath: string;
|
|
14
|
-
declaration: McpResourceDeclaration;
|
|
15
|
-
}
|
|
16
|
-
export interface McpAppManifestEntry {
|
|
17
|
-
name: string;
|
|
18
|
-
uri: string;
|
|
19
|
-
/** Path relative to the manifest, unless explicitly absolute. */
|
|
20
|
-
html: string;
|
|
21
|
-
/** Path relative to the manifest, unless explicitly absolute. */
|
|
22
|
-
meta: string;
|
|
23
|
-
declaration: McpResourceDeclaration;
|
|
24
|
-
}
|
|
25
|
-
export interface McpAppManifest {
|
|
26
|
-
formatVersion: typeof MCP_APP_MANIFEST_VERSION;
|
|
27
|
-
protocolVersion: typeof LATEST_PROTOCOL_VERSION;
|
|
28
|
-
apps: ReadonlyArray<McpAppManifestEntry>;
|
|
29
|
-
}
|
|
30
|
-
export type ConformanceSeverity = "error" | "warning";
|
|
31
|
-
export interface ConformanceDiagnostic {
|
|
32
|
-
severity: ConformanceSeverity;
|
|
33
|
-
code: string;
|
|
34
|
-
message: string;
|
|
35
|
-
source?: string;
|
|
36
|
-
}
|
|
37
|
-
export declare class McpAppConformanceError extends Error {
|
|
38
|
-
readonly diagnostics: ReadonlyArray<ConformanceDiagnostic>;
|
|
39
|
-
constructor(diagnostics: ReadonlyArray<ConformanceDiagnostic>);
|
|
40
|
-
}
|
|
41
|
-
export declare function inspectMcpResourceDeclaration(value: unknown, source?: string): ReadonlyArray<ConformanceDiagnostic>;
|
|
42
|
-
export declare function parseMcpResourceDeclaration(value: unknown, source?: string): McpResourceDeclaration;
|
|
1
|
+
import { type McpAppArtifact, type McpAppManifest } from "./declaration.js";
|
|
2
|
+
export { MCP_APP_MANIFEST_FILE, MCP_APP_MANIFEST_VERSION, McpAppConformanceError, inspectMcpResourceDeclaration, parseMcpAppSource, parseMcpResourceDeclaration, type ConformanceDiagnostic, type ConformanceSeverity, type McpAppArtifact, type McpAppManifest, type McpAppManifestEntry, type McpAppSource, } from "./declaration.js";
|
|
43
3
|
export declare function readMcpAppArtifact(htmlPath: string, expectedUri?: string): Promise<McpAppArtifact>;
|
|
44
4
|
export declare function writeMcpAppManifest(path: string, artifacts: ReadonlyArray<McpAppArtifact>): Promise<McpAppManifest>;
|
|
45
5
|
export declare function readMcpAppManifest(path: string): Promise<{
|
package/dist/artifact/index.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readMcpAppArtifact,
|
|
3
|
+
readMcpAppManifest,
|
|
4
|
+
writeMcpAppManifest
|
|
5
|
+
} from "../chunk-RLDSMU5M.js";
|
|
1
6
|
import {
|
|
2
7
|
MCP_APP_MANIFEST_FILE,
|
|
3
8
|
MCP_APP_MANIFEST_VERSION,
|
|
4
9
|
McpAppConformanceError,
|
|
5
10
|
inspectMcpResourceDeclaration,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
writeMcpAppManifest
|
|
10
|
-
} from "../chunk-DKK26TRS.js";
|
|
11
|
+
parseMcpAppSource,
|
|
12
|
+
parseMcpResourceDeclaration
|
|
13
|
+
} from "../chunk-6T5EIQGE.js";
|
|
11
14
|
export {
|
|
12
15
|
MCP_APP_MANIFEST_FILE,
|
|
13
16
|
MCP_APP_MANIFEST_VERSION,
|
|
14
17
|
McpAppConformanceError,
|
|
15
18
|
inspectMcpResourceDeclaration,
|
|
19
|
+
parseMcpAppSource,
|
|
16
20
|
parseMcpResourceDeclaration,
|
|
17
21
|
readMcpAppArtifact,
|
|
18
22
|
readMcpAppManifest,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/artifact/declaration.ts
|
|
2
|
+
import {
|
|
3
|
+
McpUiDisplayModeSchema,
|
|
4
|
+
McpUiResourceMetaSchema,
|
|
5
|
+
RESOURCE_MIME_TYPE,
|
|
6
|
+
LATEST_PROTOCOL_VERSION
|
|
7
|
+
} from "@modelcontextprotocol/ext-apps";
|
|
8
|
+
var MCP_APP_MANIFEST_VERSION = 1;
|
|
9
|
+
var MCP_APP_MANIFEST_FILE = "mountly-mcp.manifest.json";
|
|
10
|
+
var McpAppConformanceError = class extends Error {
|
|
11
|
+
constructor(diagnostics) {
|
|
12
|
+
super(diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
13
|
+
this.name = "McpAppConformanceError";
|
|
14
|
+
this.diagnostics = diagnostics;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function object(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
19
|
+
}
|
|
20
|
+
function inspectMcpResourceDeclaration(value, source) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const add = (code, message) => {
|
|
23
|
+
diagnostics.push({ severity: "error", code, message, source });
|
|
24
|
+
};
|
|
25
|
+
const candidate = object(value);
|
|
26
|
+
if (!candidate) {
|
|
27
|
+
add("artifact/not-object", "resource declaration must be an object");
|
|
28
|
+
return diagnostics;
|
|
29
|
+
}
|
|
30
|
+
if (candidate.protocolVersion !== LATEST_PROTOCOL_VERSION) {
|
|
31
|
+
add(
|
|
32
|
+
"artifact/protocol-version",
|
|
33
|
+
`sidecar protocolVersion must be '${LATEST_PROTOCOL_VERSION}' (received '${String(candidate.protocolVersion)}')`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (typeof candidate.uri !== "string" || !candidate.uri.startsWith("ui://")) {
|
|
37
|
+
add("artifact/uri", `uri must use the 'ui://' scheme (received '${String(candidate.uri)}')`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof candidate.name !== "string" || candidate.name.trim() === "") {
|
|
40
|
+
add("artifact/name", "name must be a non-empty string");
|
|
41
|
+
}
|
|
42
|
+
if (candidate.mimeType !== RESOURCE_MIME_TYPE) {
|
|
43
|
+
add(
|
|
44
|
+
"artifact/mime-type",
|
|
45
|
+
`sidecar mimeType must be '${RESOURCE_MIME_TYPE}' (received '${String(candidate.mimeType)}')`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (typeof candidate.awaitToolResult !== "boolean") {
|
|
49
|
+
add("artifact/await-tool-result", "awaitToolResult must be a boolean");
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(candidate.displayModes) || candidate.displayModes.length === 0 || candidate.displayModes.some((mode) => !McpUiDisplayModeSchema.safeParse(mode).success)) {
|
|
52
|
+
add("artifact/display-modes", "displayModes must contain supported MCP Apps display modes");
|
|
53
|
+
}
|
|
54
|
+
const meta = object(candidate._meta);
|
|
55
|
+
const ui = meta && object(meta.ui);
|
|
56
|
+
if (!ui || !McpUiResourceMetaSchema.safeParse(ui).success) {
|
|
57
|
+
add("artifact/resource-meta", "_meta.ui must be valid MCP Apps resource metadata");
|
|
58
|
+
}
|
|
59
|
+
return diagnostics;
|
|
60
|
+
}
|
|
61
|
+
function parseMcpResourceDeclaration(value, source) {
|
|
62
|
+
const diagnostics = inspectMcpResourceDeclaration(value, source);
|
|
63
|
+
if (diagnostics.length > 0) throw new McpAppConformanceError(diagnostics);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseMcpAppSource(value, expectedUri) {
|
|
67
|
+
const source = value.name ? `view '${value.name}'` : "in-memory view";
|
|
68
|
+
if (typeof value.html !== "string" || !/^\s*<!doctype html>/i.test(value.html)) {
|
|
69
|
+
throw new McpAppConformanceError([
|
|
70
|
+
{
|
|
71
|
+
severity: "error",
|
|
72
|
+
code: "artifact/html5",
|
|
73
|
+
message: "View HTML must start with an HTML5 doctype",
|
|
74
|
+
source
|
|
75
|
+
}
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
const declaration = parseMcpResourceDeclaration(value.declaration, source);
|
|
79
|
+
if (declaration.name !== value.name || declaration.uri !== value.uri) {
|
|
80
|
+
throw new Error(`mountly-mcp: ${source} identity does not match its declaration`);
|
|
81
|
+
}
|
|
82
|
+
if (expectedUri !== void 0 && declaration.uri !== expectedUri) {
|
|
83
|
+
throw new McpAppConformanceError([
|
|
84
|
+
{
|
|
85
|
+
severity: "error",
|
|
86
|
+
code: "artifact/uri-mismatch",
|
|
87
|
+
message: `registration uri '${expectedUri}' does not match declared uri '${declaration.uri}'`,
|
|
88
|
+
source
|
|
89
|
+
}
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
MCP_APP_MANIFEST_VERSION,
|
|
97
|
+
MCP_APP_MANIFEST_FILE,
|
|
98
|
+
McpAppConformanceError,
|
|
99
|
+
object,
|
|
100
|
+
inspectMcpResourceDeclaration,
|
|
101
|
+
parseMcpResourceDeclaration,
|
|
102
|
+
parseMcpAppSource
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-6T5EIQGE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/artifact/declaration.ts"],"sourcesContent":["/**\n * The parts of an artifact that need no filesystem: its types, and the checks\n * a resource declaration must pass. Kept apart so a server on a runtime without\n * `node:fs` (Cloudflare Workers) can register Views held in memory.\n */\nimport {\n McpUiDisplayModeSchema,\n McpUiResourceMetaSchema,\n RESOURCE_MIME_TYPE,\n LATEST_PROTOCOL_VERSION,\n} from \"@modelcontextprotocol/ext-apps\";\nimport type { McpResourceDeclaration } from \"../types.js\";\n\nexport const MCP_APP_MANIFEST_VERSION = 1 as const;\nexport const MCP_APP_MANIFEST_FILE = \"mountly-mcp.manifest.json\";\n\nexport interface McpAppArtifact {\n /** Developer-facing unique key. */\n name: string;\n /** Protocol identity. */\n uri: string;\n /** Absolute path to the self-contained View HTML. */\n htmlPath: string;\n /** Absolute path to the transitional per-View sidecar. */\n metaPath: string;\n declaration: McpResourceDeclaration;\n}\n\nexport interface McpAppManifestEntry {\n name: string;\n uri: string;\n /** Path relative to the manifest, unless explicitly absolute. */\n html: string;\n /** Path relative to the manifest, unless explicitly absolute. */\n meta: string;\n declaration: McpResourceDeclaration;\n}\n\nexport interface McpAppManifest {\n formatVersion: typeof MCP_APP_MANIFEST_VERSION;\n protocolVersion: typeof LATEST_PROTOCOL_VERSION;\n apps: ReadonlyArray<McpAppManifestEntry>;\n}\n\nexport type ConformanceSeverity = \"error\" | \"warning\";\n\nexport interface ConformanceDiagnostic {\n severity: ConformanceSeverity;\n code: string;\n message: string;\n source?: string;\n}\n\nexport class McpAppConformanceError extends Error {\n readonly diagnostics: ReadonlyArray<ConformanceDiagnostic>;\n\n constructor(diagnostics: ReadonlyArray<ConformanceDiagnostic>) {\n super(diagnostics.map((d) => `${d.code}: ${d.message}`).join(\"\\n\"));\n this.name = \"McpAppConformanceError\";\n this.diagnostics = diagnostics;\n }\n}\n\nexport function object(value: unknown): Record<string, unknown> | undefined {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\nexport function inspectMcpResourceDeclaration(\n value: unknown,\n source?: string,\n): ReadonlyArray<ConformanceDiagnostic> {\n const diagnostics: ConformanceDiagnostic[] = [];\n const add = (code: string, message: string): void => {\n diagnostics.push({ severity: \"error\", code, message, source });\n };\n const candidate = object(value);\n if (!candidate) {\n add(\"artifact/not-object\", \"resource declaration must be an object\");\n return diagnostics;\n }\n if (candidate.protocolVersion !== LATEST_PROTOCOL_VERSION) {\n add(\n \"artifact/protocol-version\",\n `sidecar protocolVersion must be '${LATEST_PROTOCOL_VERSION}' (received '${String(candidate.protocolVersion)}')`,\n );\n }\n if (typeof candidate.uri !== \"string\" || !candidate.uri.startsWith(\"ui://\")) {\n add(\"artifact/uri\", `uri must use the 'ui://' scheme (received '${String(candidate.uri)}')`);\n }\n if (typeof candidate.name !== \"string\" || candidate.name.trim() === \"\") {\n add(\"artifact/name\", \"name must be a non-empty string\");\n }\n if (candidate.mimeType !== RESOURCE_MIME_TYPE) {\n add(\n \"artifact/mime-type\",\n `sidecar mimeType must be '${RESOURCE_MIME_TYPE}' (received '${String(candidate.mimeType)}')`,\n );\n }\n if (typeof candidate.awaitToolResult !== \"boolean\") {\n add(\"artifact/await-tool-result\", \"awaitToolResult must be a boolean\");\n }\n if (\n !Array.isArray(candidate.displayModes) ||\n candidate.displayModes.length === 0 ||\n candidate.displayModes.some((mode) => !McpUiDisplayModeSchema.safeParse(mode).success)\n ) {\n add(\"artifact/display-modes\", \"displayModes must contain supported MCP Apps display modes\");\n }\n const meta = object(candidate._meta);\n const ui = meta && object(meta.ui);\n if (!ui || !McpUiResourceMetaSchema.safeParse(ui).success) {\n add(\"artifact/resource-meta\", \"_meta.ui must be valid MCP Apps resource metadata\");\n }\n return diagnostics;\n}\n\nexport function parseMcpResourceDeclaration(\n value: unknown,\n source?: string,\n): McpResourceDeclaration {\n const diagnostics = inspectMcpResourceDeclaration(value, source);\n if (diagnostics.length > 0) throw new McpAppConformanceError(diagnostics);\n return value as McpResourceDeclaration;\n}\n\n/**\n * A View held in memory rather than on disk: what `mountly-mcp build` writes to\n * `mountly-mcp.views.js` for servers that cannot read files.\n */\nexport interface McpAppSource {\n name: string;\n uri: string;\n declaration: McpResourceDeclaration;\n /** The self-contained View HTML. */\n html: string;\n}\n\n/** Check an in-memory View the way `readMcpAppArtifact` checks one on disk. */\nexport function parseMcpAppSource(value: McpAppSource, expectedUri?: string): McpAppSource {\n const source = value.name ? `view '${value.name}'` : \"in-memory view\";\n if (typeof value.html !== \"string\" || !/^\\s*<!doctype html>/i.test(value.html)) {\n throw new McpAppConformanceError([\n {\n severity: \"error\",\n code: \"artifact/html5\",\n message: \"View HTML must start with an HTML5 doctype\",\n source,\n },\n ]);\n }\n const declaration = parseMcpResourceDeclaration(value.declaration, source);\n if (declaration.name !== value.name || declaration.uri !== value.uri) {\n throw new Error(`mountly-mcp: ${source} identity does not match its declaration`);\n }\n if (expectedUri !== undefined && declaration.uri !== expectedUri) {\n throw new McpAppConformanceError([\n {\n severity: \"error\",\n code: \"artifact/uri-mismatch\",\n message: `registration uri '${expectedUri}' does not match declared uri '${declaration.uri}'`,\n source,\n },\n ]);\n }\n return value;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/download.ts
|
|
2
|
+
async function base64(blob) {
|
|
3
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
6
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
7
|
+
}
|
|
8
|
+
return btoa(binary);
|
|
9
|
+
}
|
|
10
|
+
async function downloadFile(app, file) {
|
|
11
|
+
const text = typeof file.data === "string";
|
|
12
|
+
const mimeType = file.mimeType || (text ? "text/plain" : file.data.type) || "application/octet-stream";
|
|
13
|
+
if (app?.getHostCapabilities()?.downloadFile) {
|
|
14
|
+
const uri = `file:///${encodeURIComponent(file.name)}`;
|
|
15
|
+
const resource = text ? { uri, mimeType, text: file.data } : { uri, mimeType, blob: await base64(file.data) };
|
|
16
|
+
const { isError } = await app.downloadFile({ contents: [{ type: "resource", resource }] });
|
|
17
|
+
return !isError;
|
|
18
|
+
}
|
|
19
|
+
const blob = text ? new Blob([file.data], { type: mimeType }) : file.data;
|
|
20
|
+
const url = URL.createObjectURL(blob);
|
|
21
|
+
const link = document.createElement("a");
|
|
22
|
+
link.href = url;
|
|
23
|
+
link.download = file.name;
|
|
24
|
+
link.hidden = true;
|
|
25
|
+
document.body.append(link);
|
|
26
|
+
link.click();
|
|
27
|
+
link.remove();
|
|
28
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
downloadFile
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=chunk-BLT2MK32.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/download.ts"],"sourcesContent":["import type { App } from \"@modelcontextprotocol/ext-apps\";\n\nexport interface DownloadableFile {\n /** The name the user saves it as. */\n name: string;\n data: Blob | string;\n /** Defaults to the Blob's own type, or `text/plain` for a string. */\n mimeType?: string;\n}\n\nasync function base64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n // Chunked: spreading a whole large file into one call overflows the stack.\n for (let i = 0; i < bytes.length; i += 0x8000) {\n binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));\n }\n return btoa(binary);\n}\n\n/**\n * Hand the user a file. A View's iframe usually cannot download on its own, so\n * a host that supports `ui/download-file` is asked to save it, with its own\n * confirmation. Anywhere else — a host without it, the dev harness, an ordinary\n * page — an `<a download>` is clicked, which a strict sandbox may still block.\n *\n * Resolves `false` when the host declined or the user cancelled.\n */\nexport async function downloadFile(app: App | undefined, file: DownloadableFile): Promise<boolean> {\n const text = typeof file.data === \"string\";\n const mimeType =\n file.mimeType || (text ? \"text/plain\" : (file.data as Blob).type) || \"application/octet-stream\";\n\n if (app?.getHostCapabilities()?.downloadFile) {\n const uri = `file:///${encodeURIComponent(file.name)}`;\n const resource = text\n ? { uri, mimeType, text: file.data as string }\n : { uri, mimeType, blob: await base64(file.data as Blob) };\n const { isError } = await app.downloadFile({ contents: [{ type: \"resource\", resource }] });\n return !isError;\n }\n\n const blob = text ? new Blob([file.data], { type: mimeType }) : (file.data as Blob);\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = file.name;\n link.hidden = true;\n document.body.append(link);\n link.click();\n link.remove();\n // Revoking on the same tick can cancel the download in some browsers.\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n return true;\n}\n"],"mappings":";AAUA,eAAe,OAAO,MAA6B;AACjD,QAAM,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AACrD,MAAI,SAAS;AAEb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,OAAQ;AAC7C,cAAU,OAAO,aAAa,GAAG,MAAM,SAAS,GAAG,IAAI,KAAM,CAAC;AAAA,EAChE;AACA,SAAO,KAAK,MAAM;AACpB;AAUA,eAAsB,aAAa,KAAsB,MAA0C;AACjG,QAAM,OAAO,OAAO,KAAK,SAAS;AAClC,QAAM,WACJ,KAAK,aAAa,OAAO,eAAgB,KAAK,KAAc,SAAS;AAEvE,MAAI,KAAK,oBAAoB,GAAG,cAAc;AAC5C,UAAM,MAAM,WAAW,mBAAmB,KAAK,IAAI,CAAC;AACpD,UAAM,WAAW,OACb,EAAE,KAAK,UAAU,MAAM,KAAK,KAAe,IAC3C,EAAE,KAAK,UAAU,MAAM,MAAM,OAAO,KAAK,IAAY,EAAE;AAC3D,UAAM,EAAE,QAAQ,IAAI,MAAM,IAAI,aAAa,EAAE,UAAU,CAAC,EAAE,MAAM,YAAY,SAAS,CAAC,EAAE,CAAC;AACzF,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,OAAO,OAAO,IAAI,KAAK,CAAC,KAAK,IAAI,GAAG,EAAE,MAAM,SAAS,CAAC,IAAK,KAAK;AACtE,QAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK;AACrB,OAAK,SAAS;AACd,WAAS,KAAK,OAAO,IAAI;AACzB,OAAK,MAAM;AACX,OAAK,OAAO;AAEZ,aAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,GAAI;AAC/C,SAAO;AACT;","names":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
McpContext
|
|
3
3
|
} from "./chunk-4SZHJ4MK.js";
|
|
4
|
+
import {
|
|
5
|
+
downloadFile
|
|
6
|
+
} from "./chunk-BLT2MK32.js";
|
|
4
7
|
import {
|
|
5
8
|
publishMcpView
|
|
6
9
|
} from "./chunk-5L3QHYWK.js";
|
|
@@ -71,6 +74,10 @@ function useUpdateModelContext() {
|
|
|
71
74
|
await app.updateModelContext(params);
|
|
72
75
|
};
|
|
73
76
|
}
|
|
77
|
+
function useDownload() {
|
|
78
|
+
const { app } = useEnsuredContext();
|
|
79
|
+
return (file) => downloadFile(app, file);
|
|
80
|
+
}
|
|
74
81
|
function useAutoResize(...args) {
|
|
75
82
|
return useAutoResizeBase(...args);
|
|
76
83
|
}
|
|
@@ -110,7 +117,8 @@ export {
|
|
|
110
117
|
useHostStyles,
|
|
111
118
|
useDocumentTheme,
|
|
112
119
|
useUpdateModelContext,
|
|
120
|
+
useDownload,
|
|
113
121
|
useAutoResize,
|
|
114
122
|
createMcpView
|
|
115
123
|
};
|
|
116
|
-
//# sourceMappingURL=chunk-
|
|
124
|
+
//# sourceMappingURL=chunk-QIA76O6M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/hooks.ts","../src/react/create-view.ts"],"sourcesContent":["import { useContext } from \"react\";\nimport {\n useAutoResize as useAutoResizeBase,\n useDocumentTheme as useDocumentThemeBase,\n useHostFonts as useHostFontsBase,\n useHostStyleVariables as useHostStyleVariablesBase,\n useHostStyles as useHostStylesBase,\n} from \"@modelcontextprotocol/ext-apps/react\";\nimport type {\n App,\n McpUiDisplayMode,\n McpUiHostContext,\n McpUiUpdateModelContextRequest,\n} from \"@modelcontextprotocol/ext-apps\";\nimport { downloadFile, type DownloadableFile } from \"../download.js\";\nimport { McpContext } from \"./context.js\";\n\nfunction useEnsuredContext() {\n const ctx = useContext(McpContext);\n if (!ctx) {\n throw new Error(\n \"mountly-mcp/react: useMcpApp/useToolInput/etc must be used inside a View wrapped with createMcpView().\",\n );\n }\n return ctx;\n}\n\n/** The view-side ext-apps App. Use this to call server tools, send messages, open links, etc. */\nexport function useMcpApp(): App {\n return useEnsuredContext().app;\n}\n\n/** Complete notification params; read the tool arguments from `.arguments`. */\nexport function useToolInput<T = unknown>(): T | undefined {\n return useEnsuredContext().toolInput as T | undefined;\n}\n\n/**\n * Streaming tool arguments (`ui/notifications/tool-input-partial`), present only\n * when the bridge runs with `streamToolInput`. Best-effort recovered JSON —\n * fields may be missing or change between notifications, so render loading\n * states from it, never critical operations. Undefined once the complete\n * {@link useToolInput} arrives.\n */\nexport function useToolInputPartial<T = unknown>(): Partial<T> | undefined {\n return useEnsuredContext().toolInputPartial as Partial<T> | undefined;\n}\n\n/** Complete MCP tool result; structured data is available at `.structuredContent`. */\nexport function useToolResult<T = unknown>(): T | undefined {\n return useEnsuredContext().toolResult as T | undefined;\n}\n\n/** Latest host context snapshot (theme, styles, displayMode, container dimensions, etc.). */\nexport function useHostContext(): McpUiHostContext | undefined {\n return useEnsuredContext().hostContext;\n}\n\n/** Current display mode reported by the host. */\nexport function useDisplayMode(): McpUiDisplayMode | undefined {\n return useHostContext()?.displayMode;\n}\n\n/**\n * Request a display mode change, resolving to the mode the host actually\n * applied — which the spec allows to differ from the one asked for.\n *\n * Modes the host didn't list in `availableDisplayModes` are never requested\n * (the spec requires views to check first); the current mode comes back\n * unchanged instead.\n */\nexport function useRequestDisplayMode(): (\n mode: McpUiDisplayMode,\n) => Promise<McpUiDisplayMode | undefined> {\n const { app, hostContext } = useEnsuredContext();\n return async (mode) => {\n const available = hostContext?.availableDisplayModes;\n if (available && !available.includes(mode)) return hostContext?.displayMode;\n const result = await app.requestDisplayMode({ mode });\n return result.mode;\n };\n}\n\n/** Apply host CSS variables (and react to host-context changes). */\nexport function useHostStyleVariables(): void {\n const { app, hostContext } = useEnsuredContext();\n useHostStyleVariablesBase(app, hostContext ?? null);\n}\n\n/** Apply host-provided `@font-face`/`@import` rules. */\nexport function useHostFonts(): void {\n const { app, hostContext } = useEnsuredContext();\n useHostFontsBase(app, hostContext ?? null);\n}\n\n/** One-shot helper: apply both host style variables and host fonts. */\nexport function useHostStyles(): void {\n const { app, hostContext } = useEnsuredContext();\n useHostStylesBase(app, hostContext ?? null);\n}\n\n/** Track the document theme reactively. */\nexport function useDocumentTheme() {\n return useDocumentThemeBase();\n}\n\n/**\n * Return a function that calls `app.updateModelContext(params)`.\n *\n * Context updates are available to the model in future turns without\n * triggering an immediate response. Only the last update is kept.\n */\nexport function useUpdateModelContext(): (\n params: McpUiUpdateModelContextRequest[\"params\"],\n) => Promise<void> {\n const { app } = useEnsuredContext();\n return async (params) => {\n await app.updateModelContext(params);\n };\n}\n\n/**\n * Return a function that hands the user a file: through the host's\n * `ui/download-file` where it has one, else an ordinary `<a download>`.\n * Resolves `false` when the host declined or the user cancelled.\n */\nexport function useDownload(): (file: DownloadableFile) => Promise<boolean> {\n const { app } = useEnsuredContext();\n return (file) => downloadFile(app, file);\n}\n\n/** Manually control auto-resize (rarely needed — the bridge calls setupSizeChangedNotifications by default). */\nexport function useAutoResize(...args: Parameters<typeof useAutoResizeBase>) {\n return useAutoResizeBase(...args);\n}\n","/**\n * Wraps a React component as an MCP Apps View and publishes it for the bridge.\n * One call is enough — no manual global assign.\n */\nimport { createElement, type ComponentType } from \"react\";\nimport { createWidget } from \"mountly-react\";\nimport type { AdapterOptions } from \"mountly/adapter\";\nimport type { McpView, McpViewProps } from \"../types.js\";\nimport { publishMcpView } from \"../publish.js\";\nimport { McpContext, type McpContextValue } from \"./context.js\";\n\nexport function createMcpView<P extends object>(\n Component: ComponentType<P>,\n options?: AdapterOptions,\n): McpView {\n const Wrapped = (props: P & McpViewProps) => {\n const { mcp, toolInput, toolInputPartial, toolResult, hostContext, ...rest } =\n props as McpViewProps & P;\n const value: McpContextValue = {\n app: mcp,\n toolInput,\n toolInputPartial,\n toolResult,\n hostContext,\n };\n return createElement(\n McpContext.Provider,\n { value },\n createElement(Component as ComponentType, rest as P),\n );\n };\n return publishMcpView(createWidget(Wrapped as ComponentType<P & McpViewProps>, options));\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,kBAAkB;AAC3B;AAAA,EACE,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,yBAAyB;AAAA,EACzB,iBAAiB;AAAA,OACZ;AAUP,SAAS,oBAAoB;AAC3B,QAAM,MAAM,WAAW,UAAU;AACjC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,YAAiB;AAC/B,SAAO,kBAAkB,EAAE;AAC7B;AAGO,SAAS,eAA2C;AACzD,SAAO,kBAAkB,EAAE;AAC7B;AASO,SAAS,sBAA2D;AACzE,SAAO,kBAAkB,EAAE;AAC7B;AAGO,SAAS,gBAA4C;AAC1D,SAAO,kBAAkB,EAAE;AAC7B;AAGO,SAAS,iBAA+C;AAC7D,SAAO,kBAAkB,EAAE;AAC7B;AAGO,SAAS,iBAA+C;AAC7D,SAAO,eAAe,GAAG;AAC3B;AAUO,SAAS,wBAE2B;AACzC,QAAM,EAAE,KAAK,YAAY,IAAI,kBAAkB;AAC/C,SAAO,OAAO,SAAS;AACrB,UAAM,YAAY,aAAa;AAC/B,QAAI,aAAa,CAAC,UAAU,SAAS,IAAI,EAAG,QAAO,aAAa;AAChE,UAAM,SAAS,MAAM,IAAI,mBAAmB,EAAE,KAAK,CAAC;AACpD,WAAO,OAAO;AAAA,EAChB;AACF;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,KAAK,YAAY,IAAI,kBAAkB;AAC/C,4BAA0B,KAAK,eAAe,IAAI;AACpD;AAGO,SAAS,eAAqB;AACnC,QAAM,EAAE,KAAK,YAAY,IAAI,kBAAkB;AAC/C,mBAAiB,KAAK,eAAe,IAAI;AAC3C;AAGO,SAAS,gBAAsB;AACpC,QAAM,EAAE,KAAK,YAAY,IAAI,kBAAkB;AAC/C,oBAAkB,KAAK,eAAe,IAAI;AAC5C;AAGO,SAAS,mBAAmB;AACjC,SAAO,qBAAqB;AAC9B;AAQO,SAAS,wBAEG;AACjB,QAAM,EAAE,IAAI,IAAI,kBAAkB;AAClC,SAAO,OAAO,WAAW;AACvB,UAAM,IAAI,mBAAmB,MAAM;AAAA,EACrC;AACF;AAOO,SAAS,cAA4D;AAC1E,QAAM,EAAE,IAAI,IAAI,kBAAkB;AAClC,SAAO,CAAC,SAAS,aAAa,KAAK,IAAI;AACzC;AAGO,SAAS,iBAAiB,MAA4C;AAC3E,SAAO,kBAAkB,GAAG,IAAI;AAClC;;;AClIA,SAAS,qBAAyC;AAClD,SAAS,oBAAoB;AAMtB,SAAS,cACd,WACA,SACS;AACT,QAAM,UAAU,CAAC,UAA4B;AAC3C,UAAM,EAAE,KAAK,WAAW,kBAAkB,YAAY,aAAa,GAAG,KAAK,IACzE;AACF,UAAM,QAAyB;AAAA,MAC7B,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,WAAO;AAAA,MACL,WAAW;AAAA,MACX,EAAE,MAAM;AAAA,MACR,cAAc,WAA4B,IAAS;AAAA,IACrD;AAAA,EACF;AACA,SAAO,eAAe,aAAa,SAA4C,OAAO,CAAC;AACzF;","names":[]}
|
|
@@ -1,70 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_APP_MANIFEST_VERSION,
|
|
3
|
+
McpAppConformanceError,
|
|
4
|
+
object,
|
|
5
|
+
parseMcpResourceDeclaration
|
|
6
|
+
} from "./chunk-6T5EIQGE.js";
|
|
7
|
+
|
|
1
8
|
// src/artifact/index.ts
|
|
2
9
|
import { readFile, writeFile } from "fs/promises";
|
|
3
10
|
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
4
|
-
import {
|
|
5
|
-
McpUiDisplayModeSchema,
|
|
6
|
-
McpUiResourceMetaSchema,
|
|
7
|
-
RESOURCE_MIME_TYPE,
|
|
8
|
-
LATEST_PROTOCOL_VERSION
|
|
9
|
-
} from "@modelcontextprotocol/ext-apps";
|
|
10
|
-
var MCP_APP_MANIFEST_VERSION = 1;
|
|
11
|
-
var MCP_APP_MANIFEST_FILE = "mountly-mcp.manifest.json";
|
|
12
|
-
var McpAppConformanceError = class extends Error {
|
|
13
|
-
constructor(diagnostics) {
|
|
14
|
-
super(diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
15
|
-
this.name = "McpAppConformanceError";
|
|
16
|
-
this.diagnostics = diagnostics;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
function object(value) {
|
|
20
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
21
|
-
}
|
|
22
|
-
function inspectMcpResourceDeclaration(value, source) {
|
|
23
|
-
const diagnostics = [];
|
|
24
|
-
const add = (code, message) => {
|
|
25
|
-
diagnostics.push({ severity: "error", code, message, source });
|
|
26
|
-
};
|
|
27
|
-
const candidate = object(value);
|
|
28
|
-
if (!candidate) {
|
|
29
|
-
add("artifact/not-object", "resource declaration must be an object");
|
|
30
|
-
return diagnostics;
|
|
31
|
-
}
|
|
32
|
-
if (candidate.protocolVersion !== LATEST_PROTOCOL_VERSION) {
|
|
33
|
-
add(
|
|
34
|
-
"artifact/protocol-version",
|
|
35
|
-
`sidecar protocolVersion must be '${LATEST_PROTOCOL_VERSION}' (received '${String(candidate.protocolVersion)}')`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
if (typeof candidate.uri !== "string" || !candidate.uri.startsWith("ui://")) {
|
|
39
|
-
add("artifact/uri", `uri must use the 'ui://' scheme (received '${String(candidate.uri)}')`);
|
|
40
|
-
}
|
|
41
|
-
if (typeof candidate.name !== "string" || candidate.name.trim() === "") {
|
|
42
|
-
add("artifact/name", "name must be a non-empty string");
|
|
43
|
-
}
|
|
44
|
-
if (candidate.mimeType !== RESOURCE_MIME_TYPE) {
|
|
45
|
-
add(
|
|
46
|
-
"artifact/mime-type",
|
|
47
|
-
`sidecar mimeType must be '${RESOURCE_MIME_TYPE}' (received '${String(candidate.mimeType)}')`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (typeof candidate.awaitToolResult !== "boolean") {
|
|
51
|
-
add("artifact/await-tool-result", "awaitToolResult must be a boolean");
|
|
52
|
-
}
|
|
53
|
-
if (!Array.isArray(candidate.displayModes) || candidate.displayModes.length === 0 || candidate.displayModes.some((mode) => !McpUiDisplayModeSchema.safeParse(mode).success)) {
|
|
54
|
-
add("artifact/display-modes", "displayModes must contain supported MCP Apps display modes");
|
|
55
|
-
}
|
|
56
|
-
const meta = object(candidate._meta);
|
|
57
|
-
const ui = meta && object(meta.ui);
|
|
58
|
-
if (!ui || !McpUiResourceMetaSchema.safeParse(ui).success) {
|
|
59
|
-
add("artifact/resource-meta", "_meta.ui must be valid MCP Apps resource metadata");
|
|
60
|
-
}
|
|
61
|
-
return diagnostics;
|
|
62
|
-
}
|
|
63
|
-
function parseMcpResourceDeclaration(value, source) {
|
|
64
|
-
const diagnostics = inspectMcpResourceDeclaration(value, source);
|
|
65
|
-
if (diagnostics.length > 0) throw new McpAppConformanceError(diagnostics);
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
11
|
+
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/ext-apps";
|
|
68
12
|
async function readMcpAppArtifact(htmlPath, expectedUri) {
|
|
69
13
|
const absoluteHtmlPath = resolve(htmlPath);
|
|
70
14
|
const metaPath = `${absoluteHtmlPath}.meta.json`;
|
|
@@ -187,13 +131,8 @@ async function readMcpAppManifest(path) {
|
|
|
187
131
|
}
|
|
188
132
|
|
|
189
133
|
export {
|
|
190
|
-
MCP_APP_MANIFEST_VERSION,
|
|
191
|
-
MCP_APP_MANIFEST_FILE,
|
|
192
|
-
McpAppConformanceError,
|
|
193
|
-
inspectMcpResourceDeclaration,
|
|
194
|
-
parseMcpResourceDeclaration,
|
|
195
134
|
readMcpAppArtifact,
|
|
196
135
|
writeMcpAppManifest,
|
|
197
136
|
readMcpAppManifest
|
|
198
137
|
};
|
|
199
|
-
//# sourceMappingURL=chunk-
|
|
138
|
+
//# sourceMappingURL=chunk-RLDSMU5M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/artifact/index.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, isAbsolute, relative, resolve } from \"node:path\";\nimport { LATEST_PROTOCOL_VERSION } from \"@modelcontextprotocol/ext-apps\";\nimport {\n MCP_APP_MANIFEST_VERSION,\n McpAppConformanceError,\n object,\n parseMcpResourceDeclaration,\n type McpAppArtifact,\n type McpAppManifest,\n} from \"./declaration.js\";\n\nexport {\n MCP_APP_MANIFEST_FILE,\n MCP_APP_MANIFEST_VERSION,\n McpAppConformanceError,\n inspectMcpResourceDeclaration,\n parseMcpAppSource,\n parseMcpResourceDeclaration,\n type ConformanceDiagnostic,\n type ConformanceSeverity,\n type McpAppArtifact,\n type McpAppManifest,\n type McpAppManifestEntry,\n type McpAppSource,\n} from \"./declaration.js\";\n\nexport async function readMcpAppArtifact(\n htmlPath: string,\n expectedUri?: string,\n): Promise<McpAppArtifact> {\n const absoluteHtmlPath = resolve(htmlPath);\n const metaPath = `${absoluteHtmlPath}.meta.json`;\n const [html, raw] = await Promise.all([\n readFile(absoluteHtmlPath, \"utf8\"),\n readFile(metaPath, \"utf8\"),\n ]);\n if (!/^\\s*<!doctype html>/i.test(html)) {\n throw new McpAppConformanceError([\n {\n severity: \"error\",\n code: \"artifact/html5\",\n message: \"View HTML must start with an HTML5 doctype\",\n source: absoluteHtmlPath,\n },\n ]);\n }\n let decoded: unknown;\n try {\n decoded = JSON.parse(raw);\n } catch (error) {\n throw new McpAppConformanceError([\n {\n severity: \"error\",\n code: \"artifact/meta-json\",\n message: `metadata is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,\n source: metaPath,\n },\n ]);\n }\n const declaration = parseMcpResourceDeclaration(decoded, metaPath);\n if (expectedUri !== undefined && declaration.uri !== expectedUri) {\n throw new McpAppConformanceError([\n {\n severity: \"error\",\n code: \"artifact/uri-mismatch\",\n message: `registration uri '${expectedUri}' does not match sidecar uri '${declaration.uri}'`,\n source: metaPath,\n },\n ]);\n }\n return {\n name: declaration.name,\n uri: declaration.uri,\n htmlPath: absoluteHtmlPath,\n metaPath,\n declaration,\n };\n}\n\nfunction manifestPath(path: string, base: string): string {\n return isAbsolute(path) ? path : resolve(base, path);\n}\n\nexport async function writeMcpAppManifest(\n path: string,\n artifacts: ReadonlyArray<McpAppArtifact>,\n): Promise<McpAppManifest> {\n const absolutePath = resolve(path);\n const base = dirname(absolutePath);\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 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 }\n const manifest: McpAppManifest = {\n formatVersion: MCP_APP_MANIFEST_VERSION,\n protocolVersion: LATEST_PROTOCOL_VERSION,\n apps: artifacts.map((artifact) => ({\n name: artifact.name,\n uri: artifact.uri,\n html: relative(base, artifact.htmlPath),\n meta: relative(base, artifact.metaPath),\n declaration: artifact.declaration,\n })),\n };\n await writeFile(absolutePath, JSON.stringify(manifest, null, 2), \"utf8\");\n return manifest;\n}\n\nexport async function readMcpAppManifest(path: string): Promise<{\n path: string;\n manifest: McpAppManifest;\n artifacts: ReadonlyArray<McpAppArtifact>;\n}> {\n const absolutePath = resolve(path);\n const base = dirname(absolutePath);\n let decoded: unknown;\n try {\n decoded = JSON.parse(await readFile(absolutePath, \"utf8\"));\n } catch (error) {\n throw new Error(\n `mountly-mcp: could not read manifest '${absolutePath}': ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n const candidate = object(decoded);\n if (\n candidate?.formatVersion !== MCP_APP_MANIFEST_VERSION ||\n candidate.protocolVersion !== LATEST_PROTOCOL_VERSION ||\n !Array.isArray(candidate.apps)\n ) {\n throw new Error(\n `mountly-mcp: '${absolutePath}' is not a supported mountly-mcp manifest (expected formatVersion ${MCP_APP_MANIFEST_VERSION})`,\n );\n }\n const manifest = decoded as McpAppManifest;\n const names = new Set<string>();\n const uris = new Set<string>();\n const artifacts: McpAppArtifact[] = [];\n for (const entry of manifest.apps) {\n if (names.has(entry.name)) throw new Error(`mountly-mcp: duplicate app name '${entry.name}'`);\n if (uris.has(entry.uri)) throw new Error(`mountly-mcp: duplicate app uri '${entry.uri}'`);\n names.add(entry.name);\n uris.add(entry.uri);\n const declaration = parseMcpResourceDeclaration(entry.declaration, absolutePath);\n if (declaration.name !== entry.name || declaration.uri !== entry.uri) {\n throw new Error(\n `mountly-mcp: manifest identity does not match declaration for '${entry.name}'`,\n );\n }\n artifacts.push({\n name: entry.name,\n uri: entry.uri,\n htmlPath: manifestPath(entry.html, base),\n metaPath: manifestPath(entry.meta, base),\n declaration,\n });\n }\n return { path: absolutePath, manifest, artifacts };\n}\n"],"mappings":"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","names":[]}
|