mountly-mcp 4.3.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/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 |
package/dist/cli.js CHANGED
@@ -349,7 +349,7 @@ async function main() {
349
349
  return addView(parseAddArgs(rest));
350
350
  }
351
351
  if (command === "doctor") {
352
- const { doctorCommand } = await import("./doctor-4MTC6Y4X.js");
352
+ const { doctorCommand } = await import("./doctor-NB3DE4CG.js");
353
353
  return doctorCommand();
354
354
  }
355
355
  if (command === "dev") return dev(parseArgs(rest));
@@ -6,7 +6,7 @@ import {
6
6
  // src/doctor.ts
7
7
  import { existsSync, readFileSync, readdirSync } from "fs";
8
8
  import { createRequire } from "module";
9
- import { join, resolve } from "path";
9
+ import { dirname, join, resolve } from "path";
10
10
  function nodeMajor() {
11
11
  return Number(process.versions.node.split(".")[0] ?? 0);
12
12
  }
@@ -18,6 +18,13 @@ function tryResolve(specifier, fromDir) {
18
18
  return null;
19
19
  }
20
20
  }
21
+ function findInstalled(pkg, fromDir) {
22
+ for (let dir = resolve(fromDir); ; dir = dirname(dir)) {
23
+ const path = join(dir, "node_modules", pkg, "package.json");
24
+ if (existsSync(path)) return path;
25
+ if (dirname(dir) === dir) return null;
26
+ }
27
+ }
21
28
  var OPENAI_USAGE = /openai\/ui|useOpenAIExtensions|mountly-mcp\/openai|mountly-mcp\/react\/openai|@openai\/mcp-extensions|openaiUi\b|openaiUiToolMeta|enableOpenAiExtensions/;
22
29
  function projectSourcesMentionOpenAi(cwd, viteConfig) {
23
30
  const candidates = [];
@@ -116,7 +123,7 @@ function runDoctor(cwd = process.cwd()) {
116
123
  }
117
124
  }
118
125
  if (projectSourcesMentionOpenAi(cwd, viteConfig)) {
119
- const openai = tryResolve("@openai/mcp-extensions/package.json", cwd);
126
+ const openai = findInstalled("@openai/mcp-extensions", cwd);
120
127
  checks.push({
121
128
  id: "openai-extensions",
122
129
  ok: Boolean(openai),
@@ -154,4 +161,4 @@ export {
154
161
  formatDoctorReport,
155
162
  runDoctor
156
163
  };
157
- //# sourceMappingURL=doctor-4MTC6Y4X.js.map
164
+ //# sourceMappingURL=doctor-NB3DE4CG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/doctor.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { CLI_ERROR_CODES, cliError } from \"./errors.js\";\n\nexport interface DoctorCheck {\n id: string;\n ok: boolean;\n detail: string;\n}\n\nexport interface DoctorReport {\n ok: boolean;\n checks: DoctorCheck[];\n}\n\nfunction nodeMajor(): number {\n return Number(process.versions.node.split(\".\")[0] ?? 0);\n}\n\nfunction tryResolve(specifier: string, fromDir: string): string | null {\n try {\n const require = createRequire(resolve(fromDir, \"package.json\"));\n return require.resolve(specifier);\n } catch {\n return null;\n }\n}\n\n/**\n * Path to `pkg` in the project's node_modules, walking up from `fromDir`. Reads the\n * filesystem, so packages whose `exports` omit `./package.json` resolve too.\n */\nfunction findInstalled(pkg: string, fromDir: string): string | null {\n for (let dir = resolve(fromDir); ; dir = dirname(dir)) {\n const path = join(dir, \"node_modules\", pkg, \"package.json\");\n if (existsSync(path)) return path;\n if (dirname(dir) === dir) return null;\n }\n}\n\nconst OPENAI_USAGE =\n /openai\\/ui|useOpenAIExtensions|mountly-mcp\\/openai|mountly-mcp\\/react\\/openai|@openai\\/mcp-extensions|openaiUi\\b|openaiUiToolMeta|enableOpenAiExtensions/;\n\nfunction projectSourcesMentionOpenAi(cwd: string, viteConfig?: string): boolean {\n const candidates: string[] = [];\n if (viteConfig) candidates.push(viteConfig);\n for (const name of [\"server.ts\", \"server.mjs\", \"server.js\", \"src/server.ts\", \"src/view.tsx\"]) {\n const path = resolve(cwd, name);\n if (existsSync(path)) candidates.push(path);\n }\n const srcDir = resolve(cwd, \"src\");\n if (existsSync(srcDir)) {\n try {\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n if (entry.isFile() && /\\.(tsx?|jsx?|mjs|cjs)$/.test(entry.name)) {\n candidates.push(join(srcDir, entry.name));\n }\n }\n } catch {\n // ignore unreadable src\n }\n }\n for (const path of candidates) {\n try {\n if (OPENAI_USAGE.test(readFileSync(path, \"utf8\"))) return true;\n } catch {\n // skip\n }\n }\n return false;\n}\n\n/**\n * Check the local project for Mountly MCP readiness. Exits nonzero via the\n * CLI when `ok` is false.\n */\nexport function runDoctor(cwd = process.cwd()): DoctorReport {\n const checks: DoctorCheck[] = [];\n\n const major = nodeMajor();\n checks.push({\n id: \"node\",\n ok: major >= 20,\n detail:\n major >= 20\n ? `Node ${process.versions.node} (ok; 20+ required, 22+ recommended)`\n : `Node ${process.versions.node} is below 20. Upgrade Node, then re-run mountly-mcp doctor.`,\n });\n\n const viteConfigCandidates = [\n \"vite.config.ts\",\n \"vite.config.mts\",\n \"vite.config.js\",\n \"vite.config.mjs\",\n ].map((name) => resolve(cwd, name));\n const viteConfig = viteConfigCandidates.find((path) => existsSync(path));\n checks.push({\n id: \"vite-config\",\n ok: Boolean(viteConfig),\n detail: viteConfig\n ? `Found ${viteConfig}`\n : \"No vite.config.* in this directory. Run from the View project, or: npx mountly-mcp create my-app --framework react\",\n });\n\n if (viteConfig) {\n const source = readFileSync(viteConfig, \"utf8\");\n const hasPlugin = /mountlyMcpViews\\s*\\(/.test(source);\n checks.push({\n id: \"vite-plugin\",\n ok: hasPlugin,\n detail: hasPlugin\n ? \"vite config imports mountlyMcpViews()\"\n : `Add mountlyMcpViews() from \"mountly-mcp/vite\" to ${viteConfig}`,\n });\n }\n\n const manifest = resolve(cwd, \"dist/mountly-mcp.manifest.json\");\n checks.push({\n id: \"manifest\",\n ok: existsSync(manifest),\n detail: existsSync(manifest)\n ? `Found ${manifest}`\n : \"No dist/mountly-mcp.manifest.json yet. Run: npx mountly-mcp build\",\n });\n\n const playwright = tryResolve(\"playwright\", cwd) ?? tryResolve(\"@playwright/test\", cwd);\n checks.push({\n id: \"playwright\",\n ok: Boolean(playwright),\n detail: playwright\n ? `Playwright resolvable for verify --render (${playwright})`\n : \"Playwright not installed. For CI render checks: pnpm add -D playwright && npx playwright install chromium\",\n });\n\n const pkgPath = resolve(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as {\n dependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n if (deps.react) {\n const reactPath = tryResolve(\"react/package.json\", cwd);\n if (reactPath) {\n const reactPkg = JSON.parse(readFileSync(reactPath, \"utf8\")) as { version?: string };\n const reactMajor = Number((reactPkg.version ?? \"0\").split(\".\")[0]);\n checks.push({\n id: \"react\",\n ok: reactMajor >= 19,\n detail:\n reactMajor >= 19\n ? `React ${reactPkg.version} (ok)`\n : `React ${reactPkg.version} is below 19. mountly-mcp/react and mountly-react require React 19.`,\n });\n }\n }\n if (deps.vite) {\n const vitePath = tryResolve(\"vite/package.json\", cwd);\n if (vitePath) {\n const vitePkg = JSON.parse(readFileSync(vitePath, \"utf8\")) as { version?: string };\n const viteMajor = Number((vitePkg.version ?? \"0\").split(\".\")[0]);\n checks.push({\n id: \"vite\",\n ok: viteMajor >= 8,\n detail:\n viteMajor >= 8\n ? `Vite ${vitePkg.version} (ok)`\n : `Vite ${vitePkg.version} is below 8. mountly-mcp/vite is built and tested against Vite 8.`,\n });\n }\n }\n }\n\n if (projectSourcesMentionOpenAi(cwd, viteConfig)) {\n const openai = findInstalled(\"@openai/mcp-extensions\", cwd);\n checks.push({\n id: \"openai-extensions\",\n ok: Boolean(openai),\n detail: openai\n ? `@openai/mcp-extensions resolvable (${openai})`\n : 'Project references OpenAI MCP extensions but @openai/mcp-extensions is not installed. Run: pnpm add @openai/mcp-extensions',\n });\n }\n\n // Manifest missing is advisory for a brand-new scaffold before first build.\n const hard = checks.filter((c) => c.id !== \"manifest\" && c.id !== \"playwright\");\n const ok = hard.every((c) => c.ok);\n\n return { ok, checks };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n const lines = [\"mountly-mcp doctor\", \"\"];\n for (const check of report.checks) {\n lines.push(`${check.ok ? \"ok\" : \"FAIL\"} ${check.id.padEnd(14)} ${check.detail}`);\n }\n lines.push(\"\");\n lines.push(report.ok ? \"Ready for mountly-mcp build / dev / verify.\" : \"Fix FAIL items above.\");\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function doctorCommand(cwd = process.cwd()): void {\n const report = runDoctor(cwd);\n process.stdout.write(formatDoctorReport(report));\n if (!report.ok) {\n throw cliError(\n CLI_ERROR_CODES.DOCTOR_FAILED,\n \"one or more doctor checks failed\",\n \"Fix the FAIL lines above, then re-run: mountly-mcp doctor\",\n );\n }\n}\n"],"mappings":"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","names":["require"]}
@@ -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":[]}
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
+ import {
2
+ sandboxProxyHtml
3
+ } from "./chunk-6QHKKFO3.js";
1
4
  import {
2
5
  MCP_VIEW_GLOBAL_KEY,
3
6
  getPublishedMcpView,
4
7
  publishMcpView
5
8
  } from "./chunk-5L3QHYWK.js";
6
- import {
7
- sandboxProxyHtml
8
- } from "./chunk-6QHKKFO3.js";
9
9
  import {
10
10
  App,
11
11
  PostMessageTransport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mountly-mcp",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Build MCP Apps (SEP-1865) views from React, Vue or Svelte components — new or existing. One build emits the ui:// resource, its sidecar, and the MCP server wiring.",
5
5
  "keywords": [
6
6
  "chatgpt",
@@ -69,6 +69,10 @@
69
69
  "types": "./dist/openai/server.d.ts",
70
70
  "import": "./dist/openai/server.js"
71
71
  },
72
+ "./events": {
73
+ "types": "./dist/events/index.d.ts",
74
+ "import": "./dist/events/index.js"
75
+ },
72
76
  "./json-render": {
73
77
  "types": "./dist/json-render/index.d.ts",
74
78
  "import": "./dist/json-render/index.js"
@@ -9,6 +9,8 @@ pnpm install
9
9
  pnpm dev # sandboxed AppBridge host on :5179 (uses --server ./server.mjs)
10
10
  pnpm verify # static conformance
11
11
  pnpm verify:render # + Chromium mount check (needs Playwright)
12
+ pnpm typecheck # TypeScript 7 (native tsc, a.k.a. tsgo)
13
+ pnpm lint # oxlint, type-aware via tsgolint
12
14
  ```
13
15
 
14
16
  | File | Role |
@@ -78,6 +80,7 @@ jobs:
78
80
  - uses: actions/setup-node@v4
79
81
  with: { node-version: 22, cache: pnpm }
80
82
  - run: pnpm install
83
+ - run: pnpm typecheck && pnpm lint
81
84
  - run: pnpm exec mountly-mcp build
82
85
  - run: pnpm exec playwright install chromium
83
86
  - run: pnpm exec mountly-mcp verify --strict --render
@@ -10,7 +10,8 @@
10
10
  "verify:strict": "mountly-mcp verify --strict",
11
11
  "verify:render": "mountly-mcp verify --strict --render",
12
12
  "doctor": "mountly-mcp doctor",
13
- "typecheck": "tsc --noEmit"
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "oxlint --type-aware"
14
15
  },
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -22,8 +23,10 @@
22
23
  "devDependencies": {
23
24
  "@types/react": "^19.2.18",
24
25
  "@types/react-dom": "^19.2.4",
25
- "@vitejs/plugin-react": "^5.0.0",
26
- "typescript": "^5.9.0",
26
+ "@vitejs/plugin-react": "^6.1.1",
27
+ "oxlint": "^1.86.0",
28
+ "oxlint-tsgolint": "^7.0.2003",
29
+ "typescript": "^7.0.2",
27
30
  "vite": "^8.2.1"
28
31
  }
29
32
  }
@@ -9,6 +9,8 @@ pnpm install
9
9
  pnpm dev
10
10
  pnpm verify
11
11
  pnpm verify:render
12
+ pnpm typecheck # TypeScript 7 (native tsc, a.k.a. tsgo)
13
+ pnpm lint # oxlint, type-aware via tsgolint
12
14
  ```
13
15
 
14
16
  | File | Role |
@@ -10,7 +10,8 @@
10
10
  "verify:strict": "mountly-mcp verify --strict",
11
11
  "verify:render": "mountly-mcp verify --strict --render",
12
12
  "doctor": "mountly-mcp doctor",
13
- "typecheck": "svelte-check --tsconfig ./tsconfig.json"
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "oxlint --type-aware"
14
15
  },
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -21,8 +22,9 @@
21
22
  },
22
23
  "devDependencies": {
23
24
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
24
- "svelte-check": "^4.0.0",
25
- "typescript": "^5.9.0",
25
+ "oxlint": "^1.86.0",
26
+ "oxlint-tsgolint": "^7.0.2003",
27
+ "typescript": "^7.0.2",
26
28
  "vite": "^8.2.1"
27
29
  }
28
30
  }
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // Types `.svelte` imports for `tsc --noEmit` on TypeScript 7. Switch typecheck to
4
+ // `svelte-check` and delete this file once svelte-check supports TypeScript 7.
5
+ declare module "*.svelte" {
6
+ import type { Component } from "svelte";
7
+
8
+ const component: Component<Record<string, unknown>>;
9
+ export default component;
10
+ }
@@ -9,6 +9,8 @@ pnpm install
9
9
  pnpm dev
10
10
  pnpm verify
11
11
  pnpm verify:render
12
+ pnpm typecheck # TypeScript 7 (native tsc, a.k.a. tsgo)
13
+ pnpm lint # oxlint, type-aware via tsgolint
12
14
  ```
13
15
 
14
16
  | File | Role |
@@ -10,7 +10,8 @@
10
10
  "verify:strict": "mountly-mcp verify --strict",
11
11
  "verify:render": "mountly-mcp verify --strict --render",
12
12
  "doctor": "mountly-mcp doctor",
13
- "typecheck": "tsc --noEmit"
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "oxlint --type-aware"
14
15
  },
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -18,7 +19,9 @@
18
19
  "zod": "^4.4.3"
19
20
  },
20
21
  "devDependencies": {
21
- "typescript": "^5.9.0",
22
+ "oxlint": "^1.86.0",
23
+ "oxlint-tsgolint": "^7.0.2003",
24
+ "typescript": "^7.0.2",
22
25
  "vite": "^8.2.1"
23
26
  }
24
27
  }
@@ -9,6 +9,8 @@ pnpm install
9
9
  pnpm dev
10
10
  pnpm verify
11
11
  pnpm verify:render
12
+ pnpm typecheck # TypeScript 7 (native tsc, a.k.a. tsgo)
13
+ pnpm lint # oxlint, type-aware via tsgolint
12
14
  ```
13
15
 
14
16
  | File | Role |
@@ -10,7 +10,8 @@
10
10
  "verify:strict": "mountly-mcp verify --strict",
11
11
  "verify:render": "mountly-mcp verify --strict --render",
12
12
  "doctor": "mountly-mcp doctor",
13
- "typecheck": "vue-tsc --noEmit"
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "oxlint --type-aware"
14
15
  },
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -21,8 +22,9 @@
21
22
  },
22
23
  "devDependencies": {
23
24
  "@vitejs/plugin-vue": "^6.0.8",
24
- "typescript": "^5.9.0",
25
- "vite": "^8.2.1",
26
- "vue-tsc": "^2.2.0"
25
+ "oxlint": "^1.86.0",
26
+ "oxlint-tsgolint": "^7.0.2003",
27
+ "typescript": "^7.0.2",
28
+ "vite": "^8.2.1"
27
29
  }
28
30
  }
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // Types `.vue` imports for `tsc --noEmit` on TypeScript 7. Switch typecheck to
4
+ // `vue-tsc --noEmit` and delete this file once vue-tsc supports TypeScript 7.
5
+ declare module "*.vue" {
6
+ import type { DefineComponent } from "vue";
7
+
8
+ const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
9
+ export default component;
10
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/doctor.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { join, resolve } from \"node:path\";\nimport { CLI_ERROR_CODES, cliError } from \"./errors.js\";\n\nexport interface DoctorCheck {\n id: string;\n ok: boolean;\n detail: string;\n}\n\nexport interface DoctorReport {\n ok: boolean;\n checks: DoctorCheck[];\n}\n\nfunction nodeMajor(): number {\n return Number(process.versions.node.split(\".\")[0] ?? 0);\n}\n\nfunction tryResolve(specifier: string, fromDir: string): string | null {\n try {\n const require = createRequire(resolve(fromDir, \"package.json\"));\n return require.resolve(specifier);\n } catch {\n return null;\n }\n}\n\nconst OPENAI_USAGE =\n /openai\\/ui|useOpenAIExtensions|mountly-mcp\\/openai|mountly-mcp\\/react\\/openai|@openai\\/mcp-extensions|openaiUi\\b|openaiUiToolMeta|enableOpenAiExtensions/;\n\nfunction projectSourcesMentionOpenAi(cwd: string, viteConfig?: string): boolean {\n const candidates: string[] = [];\n if (viteConfig) candidates.push(viteConfig);\n for (const name of [\"server.ts\", \"server.mjs\", \"server.js\", \"src/server.ts\", \"src/view.tsx\"]) {\n const path = resolve(cwd, name);\n if (existsSync(path)) candidates.push(path);\n }\n const srcDir = resolve(cwd, \"src\");\n if (existsSync(srcDir)) {\n try {\n for (const entry of readdirSync(srcDir, { withFileTypes: true })) {\n if (entry.isFile() && /\\.(tsx?|jsx?|mjs|cjs)$/.test(entry.name)) {\n candidates.push(join(srcDir, entry.name));\n }\n }\n } catch {\n // ignore unreadable src\n }\n }\n for (const path of candidates) {\n try {\n if (OPENAI_USAGE.test(readFileSync(path, \"utf8\"))) return true;\n } catch {\n // skip\n }\n }\n return false;\n}\n\n/**\n * Check the local project for Mountly MCP readiness. Exits nonzero via the\n * CLI when `ok` is false.\n */\nexport function runDoctor(cwd = process.cwd()): DoctorReport {\n const checks: DoctorCheck[] = [];\n\n const major = nodeMajor();\n checks.push({\n id: \"node\",\n ok: major >= 20,\n detail:\n major >= 20\n ? `Node ${process.versions.node} (ok; 20+ required, 22+ recommended)`\n : `Node ${process.versions.node} is below 20. Upgrade Node, then re-run mountly-mcp doctor.`,\n });\n\n const viteConfigCandidates = [\n \"vite.config.ts\",\n \"vite.config.mts\",\n \"vite.config.js\",\n \"vite.config.mjs\",\n ].map((name) => resolve(cwd, name));\n const viteConfig = viteConfigCandidates.find((path) => existsSync(path));\n checks.push({\n id: \"vite-config\",\n ok: Boolean(viteConfig),\n detail: viteConfig\n ? `Found ${viteConfig}`\n : \"No vite.config.* in this directory. Run from the View project, or: npx mountly-mcp create my-app --framework react\",\n });\n\n if (viteConfig) {\n const source = readFileSync(viteConfig, \"utf8\");\n const hasPlugin = /mountlyMcpViews\\s*\\(/.test(source);\n checks.push({\n id: \"vite-plugin\",\n ok: hasPlugin,\n detail: hasPlugin\n ? \"vite config imports mountlyMcpViews()\"\n : `Add mountlyMcpViews() from \"mountly-mcp/vite\" to ${viteConfig}`,\n });\n }\n\n const manifest = resolve(cwd, \"dist/mountly-mcp.manifest.json\");\n checks.push({\n id: \"manifest\",\n ok: existsSync(manifest),\n detail: existsSync(manifest)\n ? `Found ${manifest}`\n : \"No dist/mountly-mcp.manifest.json yet. Run: npx mountly-mcp build\",\n });\n\n const playwright = tryResolve(\"playwright\", cwd) ?? tryResolve(\"@playwright/test\", cwd);\n checks.push({\n id: \"playwright\",\n ok: Boolean(playwright),\n detail: playwright\n ? `Playwright resolvable for verify --render (${playwright})`\n : \"Playwright not installed. For CI render checks: pnpm add -D playwright && npx playwright install chromium\",\n });\n\n const pkgPath = resolve(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as {\n dependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n if (deps.react) {\n const reactPath = tryResolve(\"react/package.json\", cwd);\n if (reactPath) {\n const reactPkg = JSON.parse(readFileSync(reactPath, \"utf8\")) as { version?: string };\n const reactMajor = Number((reactPkg.version ?? \"0\").split(\".\")[0]);\n checks.push({\n id: \"react\",\n ok: reactMajor >= 19,\n detail:\n reactMajor >= 19\n ? `React ${reactPkg.version} (ok)`\n : `React ${reactPkg.version} is below 19. mountly-mcp/react and mountly-react require React 19.`,\n });\n }\n }\n if (deps.vite) {\n const vitePath = tryResolve(\"vite/package.json\", cwd);\n if (vitePath) {\n const vitePkg = JSON.parse(readFileSync(vitePath, \"utf8\")) as { version?: string };\n const viteMajor = Number((vitePkg.version ?? \"0\").split(\".\")[0]);\n checks.push({\n id: \"vite\",\n ok: viteMajor >= 8,\n detail:\n viteMajor >= 8\n ? `Vite ${vitePkg.version} (ok)`\n : `Vite ${vitePkg.version} is below 8. mountly-mcp/vite is built and tested against Vite 8.`,\n });\n }\n }\n }\n\n if (projectSourcesMentionOpenAi(cwd, viteConfig)) {\n const openai = tryResolve(\"@openai/mcp-extensions/package.json\", cwd);\n checks.push({\n id: \"openai-extensions\",\n ok: Boolean(openai),\n detail: openai\n ? `@openai/mcp-extensions resolvable (${openai})`\n : 'Project references OpenAI MCP extensions but @openai/mcp-extensions is not installed. Run: pnpm add @openai/mcp-extensions',\n });\n }\n\n // Manifest missing is advisory for a brand-new scaffold before first build.\n const hard = checks.filter((c) => c.id !== \"manifest\" && c.id !== \"playwright\");\n const ok = hard.every((c) => c.ok);\n\n return { ok, checks };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n const lines = [\"mountly-mcp doctor\", \"\"];\n for (const check of report.checks) {\n lines.push(`${check.ok ? \"ok\" : \"FAIL\"} ${check.id.padEnd(14)} ${check.detail}`);\n }\n lines.push(\"\");\n lines.push(report.ok ? \"Ready for mountly-mcp build / dev / verify.\" : \"Fix FAIL items above.\");\n lines.push(\"\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport function doctorCommand(cwd = process.cwd()): void {\n const report = runDoctor(cwd);\n process.stdout.write(formatDoctorReport(report));\n if (!report.ok) {\n throw cliError(\n CLI_ERROR_CODES.DOCTOR_FAILED,\n \"one or more doctor checks failed\",\n \"Fix the FAIL lines above, then re-run: mountly-mcp doctor\",\n );\n }\n}\n"],"mappings":"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","names":["require"]}