mountly-mcp 4.2.0 → 4.4.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/GLOSSARY.md +1 -0
- package/README.md +165 -0
- package/dist/bridge/iframe-entry.js +7 -7
- package/dist/build/emit-meta.d.ts +3 -0
- package/dist/build/index.d.ts +3 -0
- package/dist/build/index.js +1 -1
- package/dist/chunk-4SZHJ4MK.js +9 -0
- package/dist/chunk-4SZHJ4MK.js.map +1 -0
- package/dist/{chunk-SXF23BZD.js → chunk-HIAIQVK5.js} +4 -7
- package/dist/chunk-HIAIQVK5.js.map +1 -0
- package/dist/{chunk-MTJDRWGD.js → chunk-IDGTP2NI.js} +4 -2
- package/dist/chunk-IDGTP2NI.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/dev/host-entry.js +6 -6
- package/dist/{doctor-B54H6CRN.js → doctor-NB3DE4CG.js} +45 -3
- package/dist/doctor-NB3DE4CG.js.map +1 -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/json-render/index.js +2 -1
- package/dist/json-render/index.js.map +1 -1
- package/dist/openai/server.d.ts +16 -0
- package/dist/openai/server.js +20 -0
- package/dist/openai/server.js.map +1 -0
- package/dist/react/index.js +4 -2
- package/dist/react/openai-extensions.d.ts +6 -0
- package/dist/react/openai-extensions.js +25 -0
- package/dist/react/openai-extensions.js.map +1 -0
- package/dist/server/index.js +11 -6
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/vite/index.js +1 -1
- package/package.json +20 -3
- 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-MTJDRWGD.js.map +0 -1
- package/dist/chunk-SXF23BZD.js.map +0 -1
- package/dist/doctor-B54H6CRN.js.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
3
|
+
export interface EventDefinition<I extends z.ZodType = z.ZodType, P extends z.ZodType = z.ZodType> {
|
|
4
|
+
description?: string;
|
|
5
|
+
/** Subscription arguments (e.g. `{ document_id }`). */
|
|
6
|
+
input: I;
|
|
7
|
+
/** Event `data`. Validated on `emit` when set. */
|
|
8
|
+
payload?: P;
|
|
9
|
+
}
|
|
10
|
+
export interface EventSubscription {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
arguments: Record<string, unknown>;
|
|
14
|
+
url: string;
|
|
15
|
+
secret: string;
|
|
16
|
+
/** Epoch ms. */
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
/** Principal captured at subscribe time; re-checked before every delivery. */
|
|
19
|
+
authInfo?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** A `Map` works. Persist across restarts in production (the spec requires it). */
|
|
22
|
+
export interface SubscriptionStore {
|
|
23
|
+
get(key: string): MaybePromise<EventSubscription | undefined>;
|
|
24
|
+
set(key: string, subscription: EventSubscription): unknown;
|
|
25
|
+
delete(key: string): unknown;
|
|
26
|
+
values(): MaybePromise<Iterable<EventSubscription>>;
|
|
27
|
+
}
|
|
28
|
+
export interface AuthorizeRequest {
|
|
29
|
+
name: string;
|
|
30
|
+
arguments: Record<string, unknown>;
|
|
31
|
+
authInfo?: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface DefineEventsOptions<E extends Record<string, EventDefinition>> {
|
|
34
|
+
events: E;
|
|
35
|
+
store?: SubscriptionStore;
|
|
36
|
+
/**
|
|
37
|
+
* Called on subscribe/unsubscribe with the caller, and before each delivery with
|
|
38
|
+
* the principal stored at subscribe time. Return `false` to deny; a denied
|
|
39
|
+
* delivery deletes the subscription.
|
|
40
|
+
*/
|
|
41
|
+
authorize?: (request: AuthorizeRequest) => MaybePromise<boolean>;
|
|
42
|
+
/** Default: `https:` only. Narrow further to block SSRF to internal hosts. */
|
|
43
|
+
allowCallbackUrl?: (url: URL) => boolean;
|
|
44
|
+
/** Default and maximum subscription lifetime. Default 30 days. */
|
|
45
|
+
maxTtlMs?: number;
|
|
46
|
+
/** Delivery attempts per event. Default 5. */
|
|
47
|
+
maxAttempts?: number;
|
|
48
|
+
/** First retry delay; doubles each attempt. Default 1000. */
|
|
49
|
+
retryBaseMs?: number;
|
|
50
|
+
fetch?: typeof fetch;
|
|
51
|
+
}
|
|
52
|
+
export interface EmitResult {
|
|
53
|
+
subscriptionId: string;
|
|
54
|
+
status: number;
|
|
55
|
+
delivered: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** JSON-RPC error; `code` is honoured by the MCP SDK when thrown from a handler. */
|
|
58
|
+
export declare class McpEventsError extends Error {
|
|
59
|
+
readonly code: number;
|
|
60
|
+
constructor(code: number, message: string);
|
|
61
|
+
}
|
|
62
|
+
/** Standard Webhooks `v1` signature (what ChatGPT verifies). */
|
|
63
|
+
export declare function signWebhook(input: {
|
|
64
|
+
secret: string;
|
|
65
|
+
id: string;
|
|
66
|
+
timestamp: number;
|
|
67
|
+
body: string;
|
|
68
|
+
}): string;
|
|
69
|
+
export declare function defineEvents<E extends Record<string, EventDefinition>>(options: DefineEventsOptions<E>): {
|
|
70
|
+
/** `events/list` result. */
|
|
71
|
+
list(): {
|
|
72
|
+
events: {
|
|
73
|
+
name: string;
|
|
74
|
+
description?: string | undefined;
|
|
75
|
+
delivery: string[];
|
|
76
|
+
inputSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
77
|
+
payloadSchema?: z.core.ZodStandardJSONSchemaPayload<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>> | undefined;
|
|
78
|
+
}[];
|
|
79
|
+
nextCursor: null;
|
|
80
|
+
};
|
|
81
|
+
/** `events/subscribe`, idempotent per (name, arguments, url). */
|
|
82
|
+
subscribe(params: unknown, ctx?: {
|
|
83
|
+
authInfo?: unknown;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
id: string;
|
|
86
|
+
refreshBefore: string;
|
|
87
|
+
cursor: null;
|
|
88
|
+
truncated: boolean;
|
|
89
|
+
}>;
|
|
90
|
+
/** `events/unsubscribe`, idempotent. */
|
|
91
|
+
unsubscribe(params: unknown, ctx?: {
|
|
92
|
+
authInfo?: unknown;
|
|
93
|
+
}): Promise<{}>;
|
|
94
|
+
/** Deliver one event to every live subscription for `name` whose arguments `match`. */
|
|
95
|
+
emit<N extends keyof E & string>(name: N, data: E[N]["payload"] extends z.ZodType ? z.input<E[N]["payload"]> : unknown, match?: (args: z.output<E[N]["input"]>) => boolean): Promise<EmitResult[]>;
|
|
96
|
+
};
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// src/events/index.ts
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
var McpEventsError = class extends Error {
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "McpEventsError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var INVALID_PARAMS = -32602;
|
|
12
|
+
var MAX_BODY_BYTES = 262144;
|
|
13
|
+
var SECRET = /^whsec_([A-Za-z0-9+/]+={0,2})$/;
|
|
14
|
+
var SubscribeParams = z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
arguments: z.record(z.string(), z.unknown()).default({}),
|
|
17
|
+
delivery: z.object({ mode: z.literal("webhook"), url: z.string(), secret: z.string() }),
|
|
18
|
+
cursor: z.string().nullish(),
|
|
19
|
+
ttlMs: z.number().int().positive().nullish()
|
|
20
|
+
});
|
|
21
|
+
var UnsubscribeParams = z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
arguments: z.record(z.string(), z.unknown()).default({}),
|
|
24
|
+
delivery: z.object({ mode: z.literal("webhook"), url: z.string() })
|
|
25
|
+
});
|
|
26
|
+
function signWebhook(input) {
|
|
27
|
+
const key = Buffer.from(input.secret.replace(/^whsec_/, ""), "base64");
|
|
28
|
+
const mac = createHmac("sha256", key).update(`${input.id}.${input.timestamp}.${input.body}`);
|
|
29
|
+
return `v1,${mac.digest("base64")}`;
|
|
30
|
+
}
|
|
31
|
+
function canonical(value) {
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
33
|
+
if (value && typeof value === "object") {
|
|
34
|
+
const obj = value;
|
|
35
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
function parse(schema, value, what) {
|
|
40
|
+
const result = schema.safeParse(value);
|
|
41
|
+
if (!result.success)
|
|
42
|
+
throw new McpEventsError(INVALID_PARAMS, `Invalid ${what}: ${z.prettifyError(result.error)}`);
|
|
43
|
+
return result.data;
|
|
44
|
+
}
|
|
45
|
+
var id = (prefix) => `${prefix}_${randomBytes(12).toString("hex")}`;
|
|
46
|
+
var keyOf = (name, args, url) => `${name}
|
|
47
|
+
${canonical(args)}
|
|
48
|
+
${url}`;
|
|
49
|
+
function defineEvents(options) {
|
|
50
|
+
const {
|
|
51
|
+
events,
|
|
52
|
+
store = /* @__PURE__ */ new Map(),
|
|
53
|
+
authorize,
|
|
54
|
+
allowCallbackUrl = (url) => url.protocol === "https:",
|
|
55
|
+
maxTtlMs = 30 * 24 * 60 * 60 * 1e3,
|
|
56
|
+
maxAttempts = 5,
|
|
57
|
+
retryBaseMs = 1e3,
|
|
58
|
+
fetch: fetchFn = fetch
|
|
59
|
+
} = options;
|
|
60
|
+
function definition(name) {
|
|
61
|
+
const def = Object.prototype.hasOwnProperty.call(events, name) ? events[name] : void 0;
|
|
62
|
+
if (!def) throw new McpEventsError(INVALID_PARAMS, `Unknown event: ${name}`);
|
|
63
|
+
return def;
|
|
64
|
+
}
|
|
65
|
+
async function allowed(request) {
|
|
66
|
+
return !authorize || await authorize(request);
|
|
67
|
+
}
|
|
68
|
+
function post(sub, webhookId, body) {
|
|
69
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
70
|
+
return fetchFn(sub.url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
redirect: "error",
|
|
73
|
+
signal: AbortSignal.timeout(1e4),
|
|
74
|
+
headers: {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
"webhook-id": webhookId,
|
|
77
|
+
"webhook-timestamp": String(timestamp),
|
|
78
|
+
"webhook-signature": signWebhook({ secret: sub.secret, id: webhookId, timestamp, body }),
|
|
79
|
+
"x-mcp-subscription-id": sub.id
|
|
80
|
+
},
|
|
81
|
+
body
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function verify(sub) {
|
|
85
|
+
const challenge = randomBytes(32).toString("base64url");
|
|
86
|
+
const res = await post(
|
|
87
|
+
sub,
|
|
88
|
+
id("msg_verification"),
|
|
89
|
+
JSON.stringify({ type: "verification", challenge })
|
|
90
|
+
).catch(() => void 0);
|
|
91
|
+
const echoed = res?.ok ? (await res.json().catch(() => ({}))).challenge : void 0;
|
|
92
|
+
const ok = typeof echoed === "string" && echoed.length === challenge.length && timingSafeEqual(Buffer.from(echoed), Buffer.from(challenge));
|
|
93
|
+
if (!ok) throw new McpEventsError(INVALID_PARAMS, "Callback verification failed");
|
|
94
|
+
}
|
|
95
|
+
async function deliver(key, sub, eventId, body) {
|
|
96
|
+
let status = 0;
|
|
97
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
98
|
+
status = (await post(sub, eventId, body).catch(() => void 0))?.status ?? 0;
|
|
99
|
+
if (status >= 200 && status < 300) return { subscriptionId: sub.id, status, delivered: true };
|
|
100
|
+
if (status === 410) await store.delete(key);
|
|
101
|
+
const transient = status === 0 || status === 408 || status === 429 || status >= 500;
|
|
102
|
+
if (!transient) break;
|
|
103
|
+
if (attempt < maxAttempts)
|
|
104
|
+
await new Promise((r) => setTimeout(r, retryBaseMs * 2 ** (attempt - 1)));
|
|
105
|
+
}
|
|
106
|
+
return { subscriptionId: sub.id, status, delivered: false };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
/** `events/list` result. */
|
|
110
|
+
list() {
|
|
111
|
+
return {
|
|
112
|
+
events: Object.entries(events).map(([name, def]) => ({
|
|
113
|
+
name,
|
|
114
|
+
...def.description ? { description: def.description } : {},
|
|
115
|
+
delivery: ["webhook"],
|
|
116
|
+
inputSchema: z.toJSONSchema(def.input),
|
|
117
|
+
...def.payload ? { payloadSchema: z.toJSONSchema(def.payload) } : {}
|
|
118
|
+
})),
|
|
119
|
+
nextCursor: null
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
/** `events/subscribe`, idempotent per (name, arguments, url). */
|
|
123
|
+
async subscribe(params, ctx = {}) {
|
|
124
|
+
const p = parse(SubscribeParams, params, "events/subscribe params");
|
|
125
|
+
const args = parse(
|
|
126
|
+
definition(p.name).input,
|
|
127
|
+
p.arguments,
|
|
128
|
+
`arguments for ${p.name}`
|
|
129
|
+
);
|
|
130
|
+
const url = URL.parse(p.delivery.url);
|
|
131
|
+
if (!url || !allowCallbackUrl(url))
|
|
132
|
+
throw new McpEventsError(INVALID_PARAMS, "Callback URL not allowed");
|
|
133
|
+
const secret = SECRET.exec(p.delivery.secret)?.[1];
|
|
134
|
+
const keyBytes = secret ? Buffer.from(secret, "base64").length : 0;
|
|
135
|
+
if (keyBytes < 24 || keyBytes > 64)
|
|
136
|
+
throw new McpEventsError(INVALID_PARAMS, "Secret must be whsec_ + base64 of 24\u201364 bytes");
|
|
137
|
+
if (!await allowed({ name: p.name, arguments: args, authInfo: ctx.authInfo })) {
|
|
138
|
+
throw new McpEventsError(INVALID_PARAMS, "Not authorized to subscribe");
|
|
139
|
+
}
|
|
140
|
+
const key = keyOf(p.name, args, url.href);
|
|
141
|
+
const existing = await store.get(key);
|
|
142
|
+
const ttl = Math.min(p.ttlMs ?? maxTtlMs, maxTtlMs);
|
|
143
|
+
const sub = {
|
|
144
|
+
id: existing?.id ?? id("sub"),
|
|
145
|
+
name: p.name,
|
|
146
|
+
arguments: args,
|
|
147
|
+
url: url.href,
|
|
148
|
+
secret: p.delivery.secret,
|
|
149
|
+
expiresAt: Date.now() + ttl,
|
|
150
|
+
authInfo: ctx.authInfo
|
|
151
|
+
};
|
|
152
|
+
if (existing?.secret !== sub.secret) await verify(sub);
|
|
153
|
+
await store.set(key, sub);
|
|
154
|
+
return {
|
|
155
|
+
id: sub.id,
|
|
156
|
+
refreshBefore: new Date(sub.expiresAt).toISOString(),
|
|
157
|
+
cursor: null,
|
|
158
|
+
truncated: false
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
/** `events/unsubscribe`, idempotent. */
|
|
162
|
+
async unsubscribe(params, ctx = {}) {
|
|
163
|
+
const p = parse(UnsubscribeParams, params, "events/unsubscribe params");
|
|
164
|
+
const args = parse(
|
|
165
|
+
definition(p.name).input,
|
|
166
|
+
p.arguments,
|
|
167
|
+
`arguments for ${p.name}`
|
|
168
|
+
);
|
|
169
|
+
if (!await allowed({ name: p.name, arguments: args, authInfo: ctx.authInfo })) {
|
|
170
|
+
throw new McpEventsError(INVALID_PARAMS, "Not authorized to unsubscribe");
|
|
171
|
+
}
|
|
172
|
+
const url = URL.parse(p.delivery.url);
|
|
173
|
+
if (url) await store.delete(keyOf(p.name, args, url.href));
|
|
174
|
+
return {};
|
|
175
|
+
},
|
|
176
|
+
/** Deliver one event to every live subscription for `name` whose arguments `match`. */
|
|
177
|
+
async emit(name, data, match = () => true) {
|
|
178
|
+
const def = definition(name);
|
|
179
|
+
const payload = def.payload ? parse(def.payload, data, `payload for ${name}`) : data;
|
|
180
|
+
const eventId = id("evt");
|
|
181
|
+
const body = JSON.stringify({
|
|
182
|
+
eventId,
|
|
183
|
+
name,
|
|
184
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
185
|
+
data: payload,
|
|
186
|
+
cursor: null
|
|
187
|
+
});
|
|
188
|
+
if (Buffer.byteLength(body) > MAX_BODY_BYTES)
|
|
189
|
+
throw new McpEventsError(INVALID_PARAMS, "Event body exceeds 256 KiB");
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
const deliveries = [];
|
|
192
|
+
for (const sub of [...await store.values()]) {
|
|
193
|
+
if (sub.name !== name) continue;
|
|
194
|
+
const key = keyOf(sub.name, sub.arguments, sub.url);
|
|
195
|
+
if (sub.expiresAt <= now) {
|
|
196
|
+
await store.delete(key);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!match(sub.arguments)) continue;
|
|
200
|
+
if (!await allowed({ name, arguments: sub.arguments, authInfo: sub.authInfo })) {
|
|
201
|
+
await store.delete(key);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
deliveries.push(deliver(key, sub, eventId, body));
|
|
205
|
+
}
|
|
206
|
+
return Promise.all(deliveries);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
McpEventsError,
|
|
212
|
+
defineEvents,
|
|
213
|
+
signWebhook
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/events/index.ts"],"sourcesContent":["/**\n * MCP Events over webhooks (protocol `2026-07-28`), the delivery mode ChatGPT uses.\n *\n * Wire `list`, `subscribe` and `unsubscribe` into any MCP 2.0 server as `events/*`\n * request handlers, then call `emit` when something changes. This entry tracks the\n * draft spec and stays experimental until the spec settles.\n */\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { z } from \"zod\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\nexport interface EventDefinition<I extends z.ZodType = z.ZodType, P extends z.ZodType = z.ZodType> {\n description?: string;\n /** Subscription arguments (e.g. `{ document_id }`). */\n input: I;\n /** Event `data`. Validated on `emit` when set. */\n payload?: P;\n}\n\nexport interface EventSubscription {\n id: string;\n name: string;\n arguments: Record<string, unknown>;\n url: string;\n secret: string;\n /** Epoch ms. */\n expiresAt: number;\n /** Principal captured at subscribe time; re-checked before every delivery. */\n authInfo?: unknown;\n}\n\n/** A `Map` works. Persist across restarts in production (the spec requires it). */\nexport interface SubscriptionStore {\n get(key: string): MaybePromise<EventSubscription | undefined>;\n set(key: string, subscription: EventSubscription): unknown;\n delete(key: string): unknown;\n values(): MaybePromise<Iterable<EventSubscription>>;\n}\n\nexport interface AuthorizeRequest {\n name: string;\n arguments: Record<string, unknown>;\n authInfo?: unknown;\n}\n\nexport interface DefineEventsOptions<E extends Record<string, EventDefinition>> {\n events: E;\n store?: SubscriptionStore;\n /**\n * Called on subscribe/unsubscribe with the caller, and before each delivery with\n * the principal stored at subscribe time. Return `false` to deny; a denied\n * delivery deletes the subscription.\n */\n authorize?: (request: AuthorizeRequest) => MaybePromise<boolean>;\n /** Default: `https:` only. Narrow further to block SSRF to internal hosts. */\n allowCallbackUrl?: (url: URL) => boolean;\n /** Default and maximum subscription lifetime. Default 30 days. */\n maxTtlMs?: number;\n /** Delivery attempts per event. Default 5. */\n maxAttempts?: number;\n /** First retry delay; doubles each attempt. Default 1000. */\n retryBaseMs?: number;\n fetch?: typeof fetch;\n}\n\nexport interface EmitResult {\n subscriptionId: string;\n status: number;\n delivered: boolean;\n}\n\n/** JSON-RPC error; `code` is honoured by the MCP SDK when thrown from a handler. */\nexport class McpEventsError extends Error {\n readonly code: number;\n constructor(code: number, message: string) {\n super(message);\n this.name = \"McpEventsError\";\n this.code = code;\n }\n}\n\nconst INVALID_PARAMS = -32602;\nconst MAX_BODY_BYTES = 262_144;\nconst SECRET = /^whsec_([A-Za-z0-9+/]+={0,2})$/;\n\nconst SubscribeParams = z.object({\n name: z.string(),\n arguments: z.record(z.string(), z.unknown()).default({}),\n delivery: z.object({ mode: z.literal(\"webhook\"), url: z.string(), secret: z.string() }),\n cursor: z.string().nullish(),\n ttlMs: z.number().int().positive().nullish(),\n});\n\nconst UnsubscribeParams = z.object({\n name: z.string(),\n arguments: z.record(z.string(), z.unknown()).default({}),\n delivery: z.object({ mode: z.literal(\"webhook\"), url: z.string() }),\n});\n\n/** Standard Webhooks `v1` signature (what ChatGPT verifies). */\nexport function signWebhook(input: {\n secret: string;\n id: string;\n timestamp: number;\n body: string;\n}): string {\n const key = Buffer.from(input.secret.replace(/^whsec_/, \"\"), \"base64\");\n const mac = createHmac(\"sha256\", key).update(`${input.id}.${input.timestamp}.${input.body}`);\n return `v1,${mac.digest(\"base64\")}`;\n}\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(\",\")}]`;\n if (value && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .sort()\n .map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`)\n .join(\",\")}}`;\n }\n return JSON.stringify(value);\n}\n\nfunction parse<S extends z.ZodType>(schema: S, value: unknown, what: string): z.output<S> {\n const result = schema.safeParse(value);\n if (!result.success)\n throw new McpEventsError(INVALID_PARAMS, `Invalid ${what}: ${z.prettifyError(result.error)}`);\n return result.data;\n}\n\nconst id = (prefix: string) => `${prefix}_${randomBytes(12).toString(\"hex\")}`;\n\nconst keyOf = (name: string, args: unknown, url: string) => `${name}\\n${canonical(args)}\\n${url}`;\n\nexport function defineEvents<E extends Record<string, EventDefinition>>(\n options: DefineEventsOptions<E>,\n) {\n const {\n events,\n store = new Map<string, EventSubscription>(),\n authorize,\n allowCallbackUrl = (url) => url.protocol === \"https:\",\n maxTtlMs = 30 * 24 * 60 * 60 * 1000,\n maxAttempts = 5,\n retryBaseMs = 1000,\n fetch: fetchFn = fetch,\n } = options;\n\n function definition(name: string): EventDefinition {\n const def = Object.prototype.hasOwnProperty.call(events, name) ? events[name] : undefined;\n if (!def) throw new McpEventsError(INVALID_PARAMS, `Unknown event: ${name}`);\n return def;\n }\n\n async function allowed(request: AuthorizeRequest) {\n return !authorize || (await authorize(request));\n }\n\n function post(sub: EventSubscription, webhookId: string, body: string) {\n const timestamp = Math.floor(Date.now() / 1000);\n return fetchFn(sub.url, {\n method: \"POST\",\n redirect: \"error\",\n signal: AbortSignal.timeout(10_000),\n headers: {\n \"content-type\": \"application/json\",\n \"webhook-id\": webhookId,\n \"webhook-timestamp\": String(timestamp),\n \"webhook-signature\": signWebhook({ secret: sub.secret, id: webhookId, timestamp, body }),\n \"x-mcp-subscription-id\": sub.id,\n },\n body,\n });\n }\n\n async function verify(sub: EventSubscription) {\n const challenge = randomBytes(32).toString(\"base64url\");\n const res = await post(\n sub,\n id(\"msg_verification\"),\n JSON.stringify({ type: \"verification\", challenge }),\n ).catch(() => undefined);\n const echoed = res?.ok\n ? ((await res.json().catch(() => ({}))) as { challenge?: unknown }).challenge\n : undefined;\n const ok =\n typeof echoed === \"string\" &&\n echoed.length === challenge.length &&\n timingSafeEqual(Buffer.from(echoed), Buffer.from(challenge));\n if (!ok) throw new McpEventsError(INVALID_PARAMS, \"Callback verification failed\");\n }\n\n async function deliver(\n key: string,\n sub: EventSubscription,\n eventId: string,\n body: string,\n ): Promise<EmitResult> {\n let status = 0;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n // Fresh timestamp + signature per attempt; same event id and body bytes.\n status = (await post(sub, eventId, body).catch(() => undefined))?.status ?? 0;\n if (status >= 200 && status < 300) return { subscriptionId: sub.id, status, delivered: true };\n if (status === 410) await store.delete(key);\n const transient = status === 0 || status === 408 || status === 429 || status >= 500;\n if (!transient) break;\n if (attempt < maxAttempts)\n await new Promise((r) => setTimeout(r, retryBaseMs * 2 ** (attempt - 1)));\n }\n return { subscriptionId: sub.id, status, delivered: false };\n }\n\n return {\n /** `events/list` result. */\n list() {\n return {\n events: Object.entries(events).map(([name, def]) => ({\n name,\n ...(def.description ? { description: def.description } : {}),\n delivery: [\"webhook\"],\n inputSchema: z.toJSONSchema(def.input),\n ...(def.payload ? { payloadSchema: z.toJSONSchema(def.payload) } : {}),\n })),\n nextCursor: null,\n };\n },\n\n /** `events/subscribe`, idempotent per (name, arguments, url). */\n async subscribe(params: unknown, ctx: { authInfo?: unknown } = {}) {\n const p = parse(SubscribeParams, params, \"events/subscribe params\");\n const args = parse(\n definition(p.name).input,\n p.arguments,\n `arguments for ${p.name}`,\n ) as Record<string, unknown>;\n\n const url = URL.parse(p.delivery.url);\n if (!url || !allowCallbackUrl(url))\n throw new McpEventsError(INVALID_PARAMS, \"Callback URL not allowed\");\n const secret = SECRET.exec(p.delivery.secret)?.[1];\n const keyBytes = secret ? Buffer.from(secret, \"base64\").length : 0;\n if (keyBytes < 24 || keyBytes > 64)\n throw new McpEventsError(INVALID_PARAMS, \"Secret must be whsec_ + base64 of 24–64 bytes\");\n if (!(await allowed({ name: p.name, arguments: args, authInfo: ctx.authInfo }))) {\n throw new McpEventsError(INVALID_PARAMS, \"Not authorized to subscribe\");\n }\n\n const key = keyOf(p.name, args, url.href);\n const existing = await store.get(key);\n const ttl = Math.min(p.ttlMs ?? maxTtlMs, maxTtlMs);\n const sub: EventSubscription = {\n id: existing?.id ?? id(\"sub\"),\n name: p.name,\n arguments: args,\n url: url.href,\n secret: p.delivery.secret,\n expiresAt: Date.now() + ttl,\n authInfo: ctx.authInfo,\n };\n if (existing?.secret !== sub.secret) await verify(sub);\n await store.set(key, sub);\n return {\n id: sub.id,\n refreshBefore: new Date(sub.expiresAt).toISOString(),\n cursor: null,\n truncated: false,\n };\n },\n\n /** `events/unsubscribe`, idempotent. */\n async unsubscribe(params: unknown, ctx: { authInfo?: unknown } = {}) {\n const p = parse(UnsubscribeParams, params, \"events/unsubscribe params\");\n const args = parse(\n definition(p.name).input,\n p.arguments,\n `arguments for ${p.name}`,\n ) as Record<string, unknown>;\n if (!(await allowed({ name: p.name, arguments: args, authInfo: ctx.authInfo }))) {\n throw new McpEventsError(INVALID_PARAMS, \"Not authorized to unsubscribe\");\n }\n const url = URL.parse(p.delivery.url);\n if (url) await store.delete(keyOf(p.name, args, url.href));\n return {};\n },\n\n /** Deliver one event to every live subscription for `name` whose arguments `match`. */\n async emit<N extends keyof E & string>(\n name: N,\n data: E[N][\"payload\"] extends z.ZodType ? z.input<E[N][\"payload\"]> : unknown,\n match: (args: z.output<E[N][\"input\"]>) => boolean = () => true,\n ): Promise<EmitResult[]> {\n const def = definition(name);\n const payload = def.payload ? parse(def.payload, data, `payload for ${name}`) : data;\n const eventId = id(\"evt\");\n const body = JSON.stringify({\n eventId,\n name,\n timestamp: new Date().toISOString(),\n data: payload,\n cursor: null,\n });\n if (Buffer.byteLength(body) > MAX_BODY_BYTES)\n throw new McpEventsError(INVALID_PARAMS, \"Event body exceeds 256 KiB\");\n\n const now = Date.now();\n const deliveries: Promise<EmitResult>[] = [];\n for (const sub of [...(await store.values())]) {\n if (sub.name !== name) continue;\n const key = keyOf(sub.name, sub.arguments, sub.url);\n if (sub.expiresAt <= now) {\n await store.delete(key);\n continue;\n }\n if (!match(sub.arguments as z.output<E[N][\"input\"]>)) continue;\n if (!(await allowed({ name, arguments: sub.arguments, authInfo: sub.authInfo }))) {\n await store.delete(key);\n continue;\n }\n deliveries.push(deliver(key, sub, eventId, body));\n }\n return Promise.all(deliveries);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/json-render/generative-view.tsx","../../src/json-render/index.ts","../../src/json-render/use-spec-stream.ts"],"sourcesContent":["import { createElement } from \"react\";\nimport { type ComponentMap, createRenderer } from \"@json-render/react\";\nimport type { Catalog, SchemaDefinition, Spec } from \"@json-render/core\";\nimport { createMcpView, useMcpApp, useToolResult } from \"../react/index.js\";\nimport type { App } from \"@modelcontextprotocol/ext-apps\";\nimport type { AdapterOptions } from \"mountly/adapter\";\nimport type { McpView } from \"../types.js\";\n\n/** The catalog-data constraint createRenderer infers against. */\ntype CatalogData = { components: Record<string, { props: unknown }> };\n\n/**\n * Route a generated UI's actions back to the MCP host. Called with the action\n * name, its (resolved) params, and the view-side `App` handle.\n */\nexport type ActionRouter = (\n name: string,\n params: Record<string, unknown> | undefined,\n mcp: App,\n) => void;\n\n/**\n * Default router: an `ask` action with a string `prompt` param sends a\n * follow-up user turn to the model via `App.sendMessage`. This is the agent\n * loop json-render alone cannot do — it has no concept of an MCP host.\n */\nexport const defaultActionRouter: ActionRouter = (name, params, mcp) => {\n if (name !== \"ask\") return;\n const prompt = params?.prompt;\n const text = typeof prompt === \"string\" ? prompt : \"\";\n if (text) {\n void mcp.sendMessage({ role: \"user\", content: [{ type: \"text\", text }] });\n }\n};\n\n/**\n * Identity helper that types a components map against a catalog (so it can be\n * defined once and reused by both `createGenerativeView` and `createRenderer`)\n * without losing per-component prop inference.\n *\n * ```ts\n * export const components = defineComponents(catalog, {\n * Metric: ({ element }) => <span>{element.props.value}</span>,\n * });\n * ```\n */\nexport function defineComponents<TDef extends SchemaDefinition, TCatalog extends CatalogData>(\n _catalog: Catalog<TDef, TCatalog>,\n components: ComponentMap<TCatalog[\"components\"]>,\n): ComponentMap<TCatalog[\"components\"]> {\n return components;\n}\n\nexport interface GenerativeViewOptions<\n TDef extends SchemaDefinition,\n TCatalog extends CatalogData,\n> extends AdapterOptions {\n /** The json-render catalog — the vocabulary the agent may compose. */\n catalog: Catalog<TDef, TCatalog>;\n /** Native component implementations for each catalog type. */\n components: ComponentMap<TCatalog[\"components\"]>;\n /**\n * How a generated UI's actions reach the agent. Defaults to\n * {@link defaultActionRouter} (`ask` → `App.sendMessage`).\n */\n onAction?: ActionRouter;\n}\n\ninterface ToolResultShape {\n structuredContent?: { spec?: Spec };\n}\n\n/**\n * Turn a json-render catalog + components into a mountly MCP Apps View.\n *\n * Reads the spec from the tool result, renders it natively (resolving `$state`\n * from `spec.state`), and routes actions back to the MCP host.\n *\n * ```ts\n * createGenerativeView({ catalog, components, styles, shadow: true });\n * ```\n */\nexport function createGenerativeView<TDef extends SchemaDefinition, TCatalog extends CatalogData>(\n options: GenerativeViewOptions<TDef, TCatalog>,\n): McpView {\n const { catalog, components, onAction = defaultActionRouter, ...adapter } = options;\n const Rendered = createRenderer(catalog, components);\n\n function GenerativeView() {\n const mcp = useMcpApp();\n const result = useToolResult<ToolResultShape>();\n const spec = result?.structuredContent?.spec ?? null;\n if (!spec) return null;\n return createElement(Rendered, {\n spec,\n state: spec.state,\n onAction: (name: string, params?: Record<string, unknown>) => onAction(name, params, mcp),\n });\n }\n\n return createMcpView(GenerativeView, adapter);\n}\n","export {\n type ActionRouter,\n type GenerativeViewOptions,\n createGenerativeView,\n defaultActionRouter,\n defineComponents,\n} from \"./generative-view.js\";\nexport {\n type JsonRenderToolOptions,\n type RegisterJsonRenderResourceOptions,\n type RegisterJsonRenderToolOptions,\n type CreateJsonRenderMcpAppOptions,\n createJsonRenderMcpApp,\n registerJsonRenderTool,\n registerJsonRenderResource,\n} from \"./mcp.js\";\nexport {\n type BuildAppHtmlOptions,\n type UseJsonRenderAppOptions,\n type UseJsonRenderAppReturn,\n buildAppHtml,\n useJsonRenderApp,\n} from \"./app.js\";\n\n// Convenience re-exports for rendering the same catalog natively (non-MCP) —\n// previews, tests, or a plain mountly feature. Pair with `mountly-react`'s\n// `createWidget` to mount a spec outside an MCP host.\nexport { createRenderer } from \"@json-render/react\";\nexport type { ComponentMap, ComponentRenderProps } from \"@json-render/react\";\n\n// Streaming primitives (json-render's own): replay a known spec as a patch\n// stream, compile patches back into a progressively-built spec, and the AI-SDK\n// transform that classifies a model's UI-message stream into spec patches.\nexport {\n compileSpecStream,\n compileTextStreamToSpecs,\n createJsonRenderTransform,\n createSpecStreamCompiler,\n parseSpecStreamLine,\n specToPatchLines,\n} from \"./spec-stream.js\";\nexport type { Spec } from \"@json-render/core\";\n\n// --- Two ways to stream ---\n//\n// Replay (known spec): `useSpecStream` takes a finished spec — e.g. the one an\n// MCP tool delivered as `structuredContent` — and replays it as a live build,\n// with the compiler, loading flag, and cancellation included.\nexport {\n type SpecStreamState,\n type UseSpecStreamOptions,\n useSpecStream,\n} from \"./use-spec-stream.js\";\n\n// Live (from a model): `useUIStream` / `useChatUI` are json-render's own client\n// hooks — POST a prompt to an endpoint that streams JSONL patches (see\n// `streamSpec` in `mountly-mcp/json-render/server`) and render the spec as it\n// builds. Re-exported so the live path is one import away.\nexport { useChatUI, useUIStream } from \"@json-render/react\";\nexport type { UseUIStreamOptions, UseUIStreamReturn } from \"@json-render/react\";\n","import { useEffect, useState } from \"react\";\nimport type { Spec } from \"@json-render/core\";\nimport { createSpecStreamCompiler } from \"@json-render/core\";\nimport { specToPatchLines } from \"./spec-stream.js\";\n\nexport interface UseSpecStreamOptions {\n /** Delay between patches, ms. Default 90. Set 0 to render instantly. */\n intervalMs?: number;\n}\n\nexport interface SpecStreamState {\n /** The progressively-built spec (partial while streaming, full when done). */\n spec: Spec | null;\n /** Convenience: `spec?.state`, to pass straight to the renderer. */\n state: Record<string, unknown> | undefined;\n /** True while patches are still being applied. */\n loading: boolean;\n}\n\n/**\n * Replay a finished spec as a live json-render stream — and own all the\n * ceremony. Give it the target spec; it scaffolds a `createSpecStreamCompiler`,\n * replays the patches with a delay, exposes the partial spec + `loading` after\n * each step, and cancels cleanly when the target changes (so a self-driving UI\n * that swaps specs mid-stream just works). Re-streams whenever `target` changes.\n *\n * ```tsx\n * const { spec, state, loading } = useSpecStream(specs[view]);\n * return <Renderer spec={spec} state={state} loading={loading} onAction={...} />;\n * ```\n */\nexport function useSpecStream(\n target: Spec | null | undefined,\n options: UseSpecStreamOptions = {},\n): SpecStreamState {\n const { intervalMs = 90 } = options;\n const [spec, setSpec] = useState<Spec | null>(null);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n if (!target) {\n setSpec(null);\n setLoading(false);\n return;\n }\n let cancelled = false;\n const compiler = createSpecStreamCompiler<Spec>();\n const lines = specToPatchLines(target);\n setLoading(true);\n\n void (async () => {\n for (const line of lines) {\n if (cancelled) return;\n const { result } = compiler.push(`${line}\\n`);\n setSpec({ ...result });\n if (intervalMs > 0) {\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n }\n if (cancelled) return;\n setSpec(compiler.getResult());\n setLoading(false);\n })();\n\n return () => {\n cancelled = true;\n };\n }, [target, intervalMs]);\n\n return { spec, state: spec?.state, loading };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAA4B,sBAAsB;AAyB3C,IAAM,sBAAoC,CAAC,MAAM,QAAQ,QAAQ;AACtE,MAAI,SAAS,MAAO;AACpB,QAAM,SAAS,QAAQ;AACvB,QAAM,OAAO,OAAO,WAAW,WAAW,SAAS;AACnD,MAAI,MAAM;AACR,SAAK,IAAI,YAAY,EAAE,MAAM,QAAQ,SAAS,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAE,CAAC;AAAA,EAC1E;AACF;AAaO,SAAS,iBACd,UACA,YACsC;AACtC,SAAO;AACT;AA+BO,SAAS,qBACd,SACS;AACT,QAAM,EAAE,SAAS,YAAY,WAAW,qBAAqB,GAAG,QAAQ,IAAI;AAC5E,QAAM,WAAW,eAAe,SAAS,UAAU;AAEnD,WAAS,iBAAiB;AACxB,UAAM,MAAM,UAAU;AACtB,UAAM,SAAS,cAA+B;AAC9C,UAAM,OAAO,QAAQ,mBAAmB,QAAQ;AAChD,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,cAAc,UAAU;AAAA,MAC7B;AAAA,MACA,OAAO,KAAK;AAAA,MACZ,UAAU,CAAC,MAAc,WAAqC,SAAS,MAAM,QAAQ,GAAG;AAAA,IAC1F,CAAC;AAAA,EACH;AAEA,SAAO,cAAc,gBAAgB,OAAO;AAC9C;;;AC1EA,SAAS,kBAAAA,uBAAsB;;;AC3B/B,SAAS,WAAW,gBAAgB;AAEpC,SAAS,4BAAAC,iCAAgC;AA6BlC,SAAS,cACd,QACA,UAAgC,CAAC,GAChB;AACjB,QAAM,EAAE,aAAa,GAAG,IAAI;AAC5B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX,cAAQ,IAAI;AACZ,iBAAW,KAAK;AAChB;AAAA,IACF;AACA,QAAI,YAAY;AAChB,UAAM,WAAWC,0BAA+B;AAChD,UAAM,QAAQ,iBAAiB,MAAM;AACrC,eAAW,IAAI;AAEf,UAAM,YAAY;AAChB,iBAAW,QAAQ,OAAO;AACxB,YAAI,UAAW;AACf,cAAM,EAAE,OAAO,IAAI,SAAS,KAAK,GAAG,IAAI;AAAA,CAAI;AAC5C,gBAAQ,EAAE,GAAG,OAAO,CAAC;AACrB,YAAI,aAAa,GAAG;AAClB,gBAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAAA,QACpD;AAAA,MACF;AACA,UAAI,UAAW;AACf,cAAQ,SAAS,UAAU,CAAC;AAC5B,iBAAW,KAAK;AAAA,IAClB,GAAG;AAEH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,UAAU,CAAC;AAEvB,SAAO,EAAE,MAAM,OAAO,MAAM,OAAO,QAAQ;AAC7C;;;ADZA,SAAS,WAAW,mBAAmB;","names":["createRenderer","createSpecStreamCompiler","createSpecStreamCompiler"]}
|
|
1
|
+
{"version":3,"sources":["../../src/json-render/generative-view.tsx","../../src/json-render/index.ts","../../src/json-render/use-spec-stream.ts"],"sourcesContent":["import { createElement } from \"react\";\nimport { type ComponentMap, createRenderer } from \"@json-render/react\";\nimport type { Catalog, SchemaDefinition, Spec } from \"@json-render/core\";\nimport { createMcpView, useMcpApp, useToolResult } from \"../react/index.js\";\nimport type { App } from \"@modelcontextprotocol/ext-apps\";\nimport type { AdapterOptions } from \"mountly/adapter\";\nimport type { McpView } from \"../types.js\";\n\n/** The catalog-data constraint createRenderer infers against. */\ntype CatalogData = { components: Record<string, { props: unknown }> };\n\n/**\n * Route a generated UI's actions back to the MCP host. Called with the action\n * name, its (resolved) params, and the view-side `App` handle.\n */\nexport type ActionRouter = (\n name: string,\n params: Record<string, unknown> | undefined,\n mcp: App,\n) => void;\n\n/**\n * Default router: an `ask` action with a string `prompt` param sends a\n * follow-up user turn to the model via `App.sendMessage`. This is the agent\n * loop json-render alone cannot do — it has no concept of an MCP host.\n */\nexport const defaultActionRouter: ActionRouter = (name, params, mcp) => {\n if (name !== \"ask\") return;\n const prompt = params?.prompt;\n const text = typeof prompt === \"string\" ? prompt : \"\";\n if (text) {\n void mcp.sendMessage({ role: \"user\", content: [{ type: \"text\", text }] });\n }\n};\n\n/**\n * Identity helper that types a components map against a catalog (so it can be\n * defined once and reused by both `createGenerativeView` and `createRenderer`)\n * without losing per-component prop inference.\n *\n * ```ts\n * export const components = defineComponents(catalog, {\n * Metric: ({ element }) => <span>{element.props.value}</span>,\n * });\n * ```\n */\nexport function defineComponents<TDef extends SchemaDefinition, TCatalog extends CatalogData>(\n _catalog: Catalog<TDef, TCatalog>,\n components: ComponentMap<TCatalog[\"components\"]>,\n): ComponentMap<TCatalog[\"components\"]> {\n return components;\n}\n\nexport interface GenerativeViewOptions<\n TDef extends SchemaDefinition,\n TCatalog extends CatalogData,\n> extends AdapterOptions {\n /** The json-render catalog — the vocabulary the agent may compose. */\n catalog: Catalog<TDef, TCatalog>;\n /** Native component implementations for each catalog type. */\n components: ComponentMap<TCatalog[\"components\"]>;\n /**\n * How a generated UI's actions reach the agent. Defaults to\n * {@link defaultActionRouter} (`ask` → `App.sendMessage`).\n */\n onAction?: ActionRouter;\n}\n\ninterface ToolResultShape {\n structuredContent?: { spec?: Spec };\n}\n\n/**\n * Turn a json-render catalog + components into a mountly MCP Apps View.\n *\n * Reads the spec from the tool result, renders it natively (resolving `$state`\n * from `spec.state`), and routes actions back to the MCP host.\n *\n * ```ts\n * createGenerativeView({ catalog, components, styles, shadow: true });\n * ```\n */\nexport function createGenerativeView<TDef extends SchemaDefinition, TCatalog extends CatalogData>(\n options: GenerativeViewOptions<TDef, TCatalog>,\n): McpView {\n const { catalog, components, onAction = defaultActionRouter, ...adapter } = options;\n const Rendered = createRenderer(catalog, components);\n\n function GenerativeView() {\n const mcp = useMcpApp();\n const result = useToolResult<ToolResultShape>();\n const spec = result?.structuredContent?.spec ?? null;\n if (!spec) return null;\n return createElement(Rendered, {\n spec,\n state: spec.state,\n onAction: (name: string, params?: Record<string, unknown>) => onAction(name, params, mcp),\n });\n }\n\n return createMcpView(GenerativeView, adapter);\n}\n","export {\n type ActionRouter,\n type GenerativeViewOptions,\n createGenerativeView,\n defaultActionRouter,\n defineComponents,\n} from \"./generative-view.js\";\nexport {\n type JsonRenderToolOptions,\n type RegisterJsonRenderResourceOptions,\n type RegisterJsonRenderToolOptions,\n type CreateJsonRenderMcpAppOptions,\n createJsonRenderMcpApp,\n registerJsonRenderTool,\n registerJsonRenderResource,\n} from \"./mcp.js\";\nexport {\n type BuildAppHtmlOptions,\n type UseJsonRenderAppOptions,\n type UseJsonRenderAppReturn,\n buildAppHtml,\n useJsonRenderApp,\n} from \"./app.js\";\n\n// Convenience re-exports for rendering the same catalog natively (non-MCP) —\n// previews, tests, or a plain mountly feature. Pair with `mountly-react`'s\n// `createWidget` to mount a spec outside an MCP host.\nexport { createRenderer } from \"@json-render/react\";\nexport type { ComponentMap, ComponentRenderProps } from \"@json-render/react\";\n\n// Streaming primitives (json-render's own): replay a known spec as a patch\n// stream, compile patches back into a progressively-built spec, and the AI-SDK\n// transform that classifies a model's UI-message stream into spec patches.\nexport {\n compileSpecStream,\n compileTextStreamToSpecs,\n createJsonRenderTransform,\n createSpecStreamCompiler,\n parseSpecStreamLine,\n specToPatchLines,\n} from \"./spec-stream.js\";\nexport type { Spec } from \"@json-render/core\";\n\n// --- Two ways to stream ---\n//\n// Replay (known spec): `useSpecStream` takes a finished spec — e.g. the one an\n// MCP tool delivered as `structuredContent` — and replays it as a live build,\n// with the compiler, loading flag, and cancellation included.\nexport {\n type SpecStreamState,\n type UseSpecStreamOptions,\n useSpecStream,\n} from \"./use-spec-stream.js\";\n\n// Live (from a model): `useUIStream` / `useChatUI` are json-render's own client\n// hooks — POST a prompt to an endpoint that streams JSONL patches (see\n// `streamSpec` in `mountly-mcp/json-render/server`) and render the spec as it\n// builds. Re-exported so the live path is one import away.\nexport { useChatUI, useUIStream } from \"@json-render/react\";\nexport type { UseUIStreamOptions, UseUIStreamReturn } from \"@json-render/react\";\n","import { useEffect, useState } from \"react\";\nimport type { Spec } from \"@json-render/core\";\nimport { createSpecStreamCompiler } from \"@json-render/core\";\nimport { specToPatchLines } from \"./spec-stream.js\";\n\nexport interface UseSpecStreamOptions {\n /** Delay between patches, ms. Default 90. Set 0 to render instantly. */\n intervalMs?: number;\n}\n\nexport interface SpecStreamState {\n /** The progressively-built spec (partial while streaming, full when done). */\n spec: Spec | null;\n /** Convenience: `spec?.state`, to pass straight to the renderer. */\n state: Record<string, unknown> | undefined;\n /** True while patches are still being applied. */\n loading: boolean;\n}\n\n/**\n * Replay a finished spec as a live json-render stream — and own all the\n * ceremony. Give it the target spec; it scaffolds a `createSpecStreamCompiler`,\n * replays the patches with a delay, exposes the partial spec + `loading` after\n * each step, and cancels cleanly when the target changes (so a self-driving UI\n * that swaps specs mid-stream just works). Re-streams whenever `target` changes.\n *\n * ```tsx\n * const { spec, state, loading } = useSpecStream(specs[view]);\n * return <Renderer spec={spec} state={state} loading={loading} onAction={...} />;\n * ```\n */\nexport function useSpecStream(\n target: Spec | null | undefined,\n options: UseSpecStreamOptions = {},\n): SpecStreamState {\n const { intervalMs = 90 } = options;\n const [spec, setSpec] = useState<Spec | null>(null);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n if (!target) {\n setSpec(null);\n setLoading(false);\n return;\n }\n let cancelled = false;\n const compiler = createSpecStreamCompiler<Spec>();\n const lines = specToPatchLines(target);\n setLoading(true);\n\n void (async () => {\n for (const line of lines) {\n if (cancelled) return;\n const { result } = compiler.push(`${line}\\n`);\n setSpec({ ...result });\n if (intervalMs > 0) {\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n }\n if (cancelled) return;\n setSpec(compiler.getResult());\n setLoading(false);\n })();\n\n return () => {\n cancelled = true;\n };\n }, [target, intervalMs]);\n\n return { spec, state: spec?.state, loading };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAA4B,sBAAsB;AAyB3C,IAAM,sBAAoC,CAAC,MAAM,QAAQ,QAAQ;AACtE,MAAI,SAAS,MAAO;AACpB,QAAM,SAAS,QAAQ;AACvB,QAAM,OAAO,OAAO,WAAW,WAAW,SAAS;AACnD,MAAI,MAAM;AACR,SAAK,IAAI,YAAY,EAAE,MAAM,QAAQ,SAAS,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAE,CAAC;AAAA,EAC1E;AACF;AAaO,SAAS,iBACd,UACA,YACsC;AACtC,SAAO;AACT;AA+BO,SAAS,qBACd,SACS;AACT,QAAM,EAAE,SAAS,YAAY,WAAW,qBAAqB,GAAG,QAAQ,IAAI;AAC5E,QAAM,WAAW,eAAe,SAAS,UAAU;AAEnD,WAAS,iBAAiB;AACxB,UAAM,MAAM,UAAU;AACtB,UAAM,SAAS,cAA+B;AAC9C,UAAM,OAAO,QAAQ,mBAAmB,QAAQ;AAChD,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,cAAc,UAAU;AAAA,MAC7B;AAAA,MACA,OAAO,KAAK;AAAA,MACZ,UAAU,CAAC,MAAc,WAAqC,SAAS,MAAM,QAAQ,GAAG;AAAA,IAC1F,CAAC;AAAA,EACH;AAEA,SAAO,cAAc,gBAAgB,OAAO;AAC9C;;;AC1EA,SAAS,kBAAAA,uBAAsB;;;AC3B/B,SAAS,WAAW,gBAAgB;AAEpC,SAAS,4BAAAC,iCAAgC;AA6BlC,SAAS,cACd,QACA,UAAgC,CAAC,GAChB;AACjB,QAAM,EAAE,aAAa,GAAG,IAAI;AAC5B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ;AACX,cAAQ,IAAI;AACZ,iBAAW,KAAK;AAChB;AAAA,IACF;AACA,QAAI,YAAY;AAChB,UAAM,WAAWC,0BAA+B;AAChD,UAAM,QAAQ,iBAAiB,MAAM;AACrC,eAAW,IAAI;AAEf,UAAM,YAAY;AAChB,iBAAW,QAAQ,OAAO;AACxB,YAAI,UAAW;AACf,cAAM,EAAE,OAAO,IAAI,SAAS,KAAK,GAAG,IAAI;AAAA,CAAI;AAC5C,gBAAQ,EAAE,GAAG,OAAO,CAAC;AACrB,YAAI,aAAa,GAAG;AAClB,gBAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAAA,QACpD;AAAA,MACF;AACA,UAAI,UAAW;AACf,cAAQ,SAAS,UAAU,CAAC;AAC5B,iBAAW,KAAK;AAAA,IAClB,GAAG;AAEH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,QAAQ,UAAU,CAAC;AAEvB,SAAO,EAAE,MAAM,OAAO,MAAM,OAAO,QAAQ;AAC7C;;;ADZA,SAAS,WAAW,mBAAmB;","names":["createRenderer","createSpecStreamCompiler","createSpecStreamCompiler"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT helpers that wrap `@openai/mcp-extensions/server`.
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { OpenAIExtensions, type OpenAIUiEntrypoint, type OpenAIUiResourceMetadata, type OpenAIUiToolMetadata } from "@openai/mcp-extensions/server";
|
|
6
|
+
/** Return the official OpenAI extensions surface for this MCP server. */
|
|
7
|
+
export declare function enableOpenAiExtensions(server: McpServer): OpenAIExtensions;
|
|
8
|
+
/** Build a `_meta` fragment with typed `"openai/ui"` tool metadata. */
|
|
9
|
+
export declare function openaiUiToolMeta(meta: OpenAIUiToolMetadata): {
|
|
10
|
+
"openai/ui": OpenAIUiToolMetadata;
|
|
11
|
+
};
|
|
12
|
+
/** Build a `_meta` fragment with typed `"openai/ui"` resource metadata. */
|
|
13
|
+
export declare function openaiUiResourceMeta(meta: OpenAIUiResourceMetadata): {
|
|
14
|
+
"openai/ui": OpenAIUiResourceMetadata;
|
|
15
|
+
};
|
|
16
|
+
export { OpenAIExtensions, type OpenAIUiEntrypoint, type OpenAIUiResourceMetadata, type OpenAIUiToolMetadata, };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/openai/server.ts
|
|
2
|
+
import {
|
|
3
|
+
OpenAIExtensions
|
|
4
|
+
} from "@openai/mcp-extensions/server";
|
|
5
|
+
function enableOpenAiExtensions(server) {
|
|
6
|
+
return new OpenAIExtensions(server);
|
|
7
|
+
}
|
|
8
|
+
function openaiUiToolMeta(meta) {
|
|
9
|
+
return { "openai/ui": meta };
|
|
10
|
+
}
|
|
11
|
+
function openaiUiResourceMeta(meta) {
|
|
12
|
+
return { "openai/ui": meta };
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
OpenAIExtensions,
|
|
16
|
+
enableOpenAiExtensions,
|
|
17
|
+
openaiUiResourceMeta,
|
|
18
|
+
openaiUiToolMeta
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/openai/server.ts"],"sourcesContent":["/**\n * ChatGPT helpers that wrap `@openai/mcp-extensions/server`.\n */\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport {\n OpenAIExtensions,\n type OpenAIUiEntrypoint,\n type OpenAIUiResourceMetadata,\n type OpenAIUiToolMetadata,\n} from \"@openai/mcp-extensions/server\";\n\n/** Return the official OpenAI extensions surface for this MCP server. */\nexport function enableOpenAiExtensions(server: McpServer): OpenAIExtensions {\n return new OpenAIExtensions(server);\n}\n\n/** Build a `_meta` fragment with typed `\"openai/ui\"` tool metadata. */\nexport function openaiUiToolMeta(\n meta: OpenAIUiToolMetadata,\n): { \"openai/ui\": OpenAIUiToolMetadata } {\n return { \"openai/ui\": meta };\n}\n\n/** Build a `_meta` fragment with typed `\"openai/ui\"` resource metadata. */\nexport function openaiUiResourceMeta(\n meta: OpenAIUiResourceMetadata,\n): { \"openai/ui\": OpenAIUiResourceMetadata } {\n return { \"openai/ui\": meta };\n}\n\nexport {\n OpenAIExtensions,\n type OpenAIUiEntrypoint,\n type OpenAIUiResourceMetadata,\n type OpenAIUiToolMetadata,\n};\n"],"mappings":";AAIA;AAAA,EACE;AAAA,OAIK;AAGA,SAAS,uBAAuB,QAAqC;AAC1E,SAAO,IAAI,iBAAiB,MAAM;AACpC;AAGO,SAAS,iBACd,MACuC;AACvC,SAAO,EAAE,aAAa,KAAK;AAC7B;AAGO,SAAS,qBACd,MAC2C;AAC3C,SAAO,EAAE,aAAa,KAAK;AAC7B;","names":[]}
|
package/dist/react/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
McpContext,
|
|
3
2
|
createMcpView,
|
|
4
3
|
useAutoResize,
|
|
5
4
|
useDisplayMode,
|
|
@@ -14,7 +13,10 @@ import {
|
|
|
14
13
|
useToolInputPartial,
|
|
15
14
|
useToolResult,
|
|
16
15
|
useUpdateModelContext
|
|
17
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-HIAIQVK5.js";
|
|
17
|
+
import {
|
|
18
|
+
McpContext
|
|
19
|
+
} from "../chunk-4SZHJ4MK.js";
|
|
18
20
|
import "../chunk-5L3QHYWK.js";
|
|
19
21
|
export {
|
|
20
22
|
McpContext,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { OpenAIExtensions } from "@openai/mcp-extensions/app";
|
|
2
|
+
/**
|
|
3
|
+
* Official ChatGPT extension surface for this View.
|
|
4
|
+
* One `OpenAIExtensions` per App handle. Call `message`, `files`, `deepLink`, and the rest on the returned instance.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useOpenAIExtensions(): OpenAIExtensions;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
McpContext
|
|
3
|
+
} from "../chunk-4SZHJ4MK.js";
|
|
4
|
+
|
|
5
|
+
// src/react/openai-extensions.ts
|
|
6
|
+
import { useContext, useRef } from "react";
|
|
7
|
+
import { OpenAIExtensions } from "@openai/mcp-extensions/app";
|
|
8
|
+
function useOpenAIExtensions() {
|
|
9
|
+
const ctx = useContext(McpContext);
|
|
10
|
+
if (!ctx) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"mountly-mcp/react: useOpenAIExtensions must be used inside a View wrapped with createMcpView()."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const app = ctx.app;
|
|
16
|
+
const ref = useRef(null);
|
|
17
|
+
if (ref.current?.app !== app) {
|
|
18
|
+
ref.current = { app, extensions: new OpenAIExtensions(app) };
|
|
19
|
+
}
|
|
20
|
+
return ref.current.extensions;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
useOpenAIExtensions
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=openai-extensions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/openai-extensions.ts"],"sourcesContent":["/**\n * ChatGPT extension hook backed by `@openai/mcp-extensions/app`.\n * Separate entry (`mountly-mcp/react/openai`) keeps the peer out of ordinary Views.\n */\nimport { useContext, useRef } from \"react\";\nimport type { App } from \"@modelcontextprotocol/ext-apps\";\nimport { OpenAIExtensions } from \"@openai/mcp-extensions/app\";\nimport { McpContext } from \"./context.js\";\n\n/**\n * Official ChatGPT extension surface for this View.\n * One `OpenAIExtensions` per App handle. Call `message`, `files`, `deepLink`, and the rest on the returned instance.\n */\nexport function useOpenAIExtensions(): OpenAIExtensions {\n const ctx = useContext(McpContext);\n if (!ctx) {\n throw new Error(\n \"mountly-mcp/react: useOpenAIExtensions must be used inside a View wrapped with createMcpView().\",\n );\n }\n const app = ctx.app;\n const ref = useRef<{ app: App; extensions: OpenAIExtensions } | null>(null);\n if (ref.current?.app !== app) {\n ref.current = { app, extensions: new OpenAIExtensions(app) };\n }\n return ref.current.extensions;\n}\n"],"mappings":";;;;;AAIA,SAAS,YAAY,cAAc;AAEnC,SAAS,wBAAwB;AAO1B,SAAS,sBAAwC;AACtD,QAAM,MAAM,WAAW,UAAU;AACjC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,MAAM,IAAI;AAChB,QAAM,MAAM,OAA0D,IAAI;AAC1E,MAAI,IAAI,SAAS,QAAQ,KAAK;AAC5B,QAAI,UAAU,EAAE,KAAK,YAAY,IAAI,iBAAiB,GAAG,EAAE;AAAA,EAC7D;AACA,SAAO,IAAI,QAAQ;AACrB;","names":[]}
|
package/dist/server/index.js
CHANGED
|
@@ -116,15 +116,20 @@ async function registerMcpApps(server, options) {
|
|
|
116
116
|
server.server.registerCapabilities({
|
|
117
117
|
extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } }
|
|
118
118
|
});
|
|
119
|
-
const resources = views.map(
|
|
120
|
-
|
|
119
|
+
const resources = views.map(({ artifact, html }) => {
|
|
120
|
+
const resourceMeta = {
|
|
121
|
+
ui: artifact.declaration._meta.ui
|
|
122
|
+
};
|
|
123
|
+
const openaiUi = artifact.declaration._meta["openai/ui"];
|
|
124
|
+
if (openaiUi !== void 0) resourceMeta["openai/ui"] = openaiUi;
|
|
125
|
+
return registerAppResource(
|
|
121
126
|
server,
|
|
122
127
|
artifact.name,
|
|
123
128
|
artifact.uri,
|
|
124
129
|
{
|
|
125
130
|
mimeType: artifact.declaration.mimeType,
|
|
126
131
|
description: artifact.declaration.description,
|
|
127
|
-
_meta:
|
|
132
|
+
_meta: resourceMeta
|
|
128
133
|
},
|
|
129
134
|
async () => ({
|
|
130
135
|
contents: [
|
|
@@ -132,12 +137,12 @@ async function registerMcpApps(server, options) {
|
|
|
132
137
|
uri: artifact.uri,
|
|
133
138
|
mimeType: artifact.declaration.mimeType,
|
|
134
139
|
text: html,
|
|
135
|
-
_meta:
|
|
140
|
+
_meta: resourceMeta
|
|
136
141
|
}
|
|
137
142
|
]
|
|
138
143
|
})
|
|
139
|
-
)
|
|
140
|
-
);
|
|
144
|
+
);
|
|
145
|
+
});
|
|
141
146
|
const tools = (options.tools ?? []).map((registration) => {
|
|
142
147
|
const { meta, withoutUi, visibility } = mergedToolMetadata(registration);
|
|
143
148
|
const config = registration.config ?? {};
|