frameshot-mcp 0.11.1 → 0.12.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.
@@ -1058,6 +1058,23 @@ document.getElementById("app").innerHTML = html;
1058
1058
  }
1059
1059
  const adapterPlugins = await adapter.getPlugins(project.root);
1060
1060
  const adapterAliases = adapter.getAliases(project.root, STUBS_DIR);
1061
+ const ORM_PACKAGES = [
1062
+ "@prisma/client",
1063
+ "drizzle-orm",
1064
+ "mongoose",
1065
+ "typeorm",
1066
+ "@mikro-orm/core",
1067
+ "sequelize",
1068
+ "knex",
1069
+ "objection",
1070
+ "bookshelf"
1071
+ ];
1072
+ const ormStub = join18(STUBS_DIR, "orm-proxy.js");
1073
+ for (const pkg of ORM_PACKAGES) {
1074
+ if (existsSync4(join18(project.root, "node_modules", ...pkg.split("/")))) {
1075
+ adapterAliases[pkg] = ormStub;
1076
+ }
1077
+ }
1061
1078
  const aliasEntries = [];
1062
1079
  for (const [key, value] of Object.entries(project.pathAliases)) {
1063
1080
  aliasEntries.push({
@@ -1411,7 +1428,7 @@ var DiffUseCase = class {
1411
1428
  };
1412
1429
 
1413
1430
  // src/use-cases/render.ts
1414
- import { readFileSync as readFileSync3 } from "fs";
1431
+ import { readFileSync as readFileSync3, writeFileSync } from "fs";
1415
1432
  import { extname as extname2 } from "path";
1416
1433
 
1417
1434
  // src/infrastructure/page-utils.ts
@@ -1510,9 +1527,10 @@ var RenderUseCase = class {
1510
1527
  css: opts.css,
1511
1528
  tailwindVersion: opts.tailwindVersion
1512
1529
  });
1513
- return Promise.all(
1530
+ const results = await Promise.all(
1514
1531
  opts.engines.map((engine) => this.renderHtml(engine, html, opts))
1515
1532
  );
1533
+ return this.maybeSave(results, opts.outputPath);
1516
1534
  }
1517
1535
  async renderFile(filePath, options = {}) {
1518
1536
  const opts = this.resolveOptions(options);
@@ -1525,10 +1543,10 @@ var RenderUseCase = class {
1525
1543
  props: options.props,
1526
1544
  projectRoot: options.projectRoot
1527
1545
  });
1528
- const results2 = await Promise.all(
1546
+ const raw2 = await Promise.all(
1529
1547
  opts.engines.map((engine) => this.renderUrl(engine, url, opts))
1530
1548
  );
1531
- return { results: results2, mode: "vite" };
1549
+ return { results: this.maybeSave(raw2, opts.outputPath), mode: "vite" };
1532
1550
  } catch (err) {
1533
1551
  fallbackReason = err instanceof Error ? err.message : String(err);
1534
1552
  process.stderr.write(
@@ -1540,8 +1558,12 @@ var RenderUseCase = class {
1540
1558
  fallbackReason = "Vite bundler not available";
1541
1559
  }
1542
1560
  const code = readFileSync3(filePath, "utf-8");
1543
- const results = await this.render(code, extFramework, options);
1544
- return { results, mode: "cdn", fallbackReason };
1561
+ const raw = await this.render(code, extFramework, options);
1562
+ return {
1563
+ results: this.maybeSave(raw, opts.outputPath),
1564
+ mode: "cdn",
1565
+ fallbackReason
1566
+ };
1545
1567
  }
1546
1568
  async renderInteraction(code, framework, interactions, options = {}) {
1547
1569
  const opts = this.resolveOptions(options);
@@ -1729,6 +1751,19 @@ var RenderUseCase = class {
1729
1751
  height: Math.max(measured.height + AUTO_FIT_PAD * 2, AUTO_FIT_MIN.height)
1730
1752
  };
1731
1753
  }
1754
+ /**
1755
+ * When outputPath is set, write the PNG to disk and replace the inline
1756
+ * base64 with an empty string to keep the MCP response small.
1757
+ */
1758
+ maybeSave(results, outputPath) {
1759
+ if (!outputPath) return results;
1760
+ return results.map((r) => {
1761
+ const filePath = results.length === 1 ? outputPath : outputPath.replace(/\.png$/i, `-${r.engine}.png`);
1762
+ const buf = Buffer.from(r.image, "base64");
1763
+ writeFileSync(filePath, buf);
1764
+ return { ...r, image: "", savedPath: filePath };
1765
+ });
1766
+ }
1732
1767
  resolveOptions(partial) {
1733
1768
  return {
1734
1769
  viewport: partial.viewport ?? DEFAULT_RENDER_OPTIONS.viewport,
@@ -1738,7 +1773,9 @@ var RenderUseCase = class {
1738
1773
  css: partial.css ?? DEFAULT_RENDER_OPTIONS.css,
1739
1774
  tailwindVersion: partial.tailwindVersion ?? DEFAULT_RENDER_OPTIONS.tailwindVersion,
1740
1775
  waitFor: partial.waitFor ?? DEFAULT_RENDER_OPTIONS.waitFor,
1741
- autoFit: partial.autoFit ?? DEFAULT_RENDER_OPTIONS.autoFit
1776
+ autoFit: partial.autoFit ?? DEFAULT_RENDER_OPTIONS.autoFit,
1777
+ mock: partial.mock,
1778
+ outputPath: partial.outputPath
1742
1779
  };
1743
1780
  }
1744
1781
  };
@@ -10,7 +10,7 @@ import {
10
10
  SnapshotStore,
11
11
  SnapshotUseCase,
12
12
  ViteBundler
13
- } from "./chunk-SP7UAIQL.js";
13
+ } from "./chunk-J5JI2IC5.js";
14
14
 
15
15
  // src/use-cases/watch.ts
16
16
  import { watch } from "chokidar";
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createContainer
4
- } from "./chunk-RXVC5ZDW.js";
4
+ } from "./chunk-SPOPEVPM.js";
5
5
  import {
6
6
  EXT_TO_FRAMEWORK
7
- } from "./chunk-SP7UAIQL.js";
7
+ } from "./chunk-J5JI2IC5.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { mkdirSync } from "fs";
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createContainer
4
- } from "./chunk-RXVC5ZDW.js";
4
+ } from "./chunk-SPOPEVPM.js";
5
5
  import {
6
6
  DEVICE_PRESETS,
7
7
  EXT_TO_FRAMEWORK,
8
8
  __export
9
- } from "./chunk-SP7UAIQL.js";
9
+ } from "./chunk-J5JI2IC5.js";
10
10
 
11
11
  // src/index.ts
12
12
  import { execSync } from "child_process";
@@ -14531,6 +14531,9 @@ config(en_default());
14531
14531
  var mockSchema = external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe(
14532
14532
  "Mock network responses. Keys: URL pattern (path '/api/users', glob '**/api/*', or full URL). Values: response body (auto-serialized as JSON) or { status, contentType, body, headers }."
14533
14533
  );
14534
+ var outputPathSchema = external_exports.string().optional().describe(
14535
+ "Absolute path to save the PNG (e.g. '/tmp/button.png'). When set, returns a text summary instead of base64 image data \u2014 use this in long agent sessions to avoid context bloat."
14536
+ );
14534
14537
  function wrapHandler(handler) {
14535
14538
  return async (args) => {
14536
14539
  try {
@@ -14893,7 +14896,8 @@ function registerRenderFileTools(server2, useCase) {
14893
14896
  projectRoot: external_exports.string().optional().describe(
14894
14897
  "Project root directory override (defaults to auto-detect from file path)"
14895
14898
  ),
14896
- mock: mockSchema
14899
+ mock: mockSchema,
14900
+ outputPath: outputPathSchema
14897
14901
  },
14898
14902
  wrapHandler(
14899
14903
  async ({
@@ -14904,7 +14908,8 @@ function registerRenderFileTools(server2, useCase) {
14904
14908
  darkMode,
14905
14909
  tailwindVersion,
14906
14910
  projectRoot,
14907
- mock
14911
+ mock,
14912
+ outputPath
14908
14913
  }) => {
14909
14914
  const start = performance.now();
14910
14915
  const { results, mode, fallbackReason } = await useCase.renderFile(
@@ -14917,7 +14922,8 @@ function registerRenderFileTools(server2, useCase) {
14917
14922
  darkMode,
14918
14923
  tailwindVersion,
14919
14924
  projectRoot,
14920
- mock
14925
+ mock,
14926
+ outputPath
14921
14927
  }
14922
14928
  );
14923
14929
  const elapsed = Math.round(performance.now() - start);
@@ -14926,19 +14932,29 @@ function registerRenderFileTools(server2, useCase) {
14926
14932
  const fallbackNote = mode === "cdn" && fallbackReason ? `
14927
14933
  \u2139\uFE0F Fell back to CDN pipeline: ${fallbackReason}
14928
14934
  Imports from node_modules may not resolve. To fix: install vite as a devDep and add a minimal vite.config.` : "";
14929
- const content = results.flatMap((r) => [
14930
- {
14931
- type: "image",
14932
- data: r.image,
14933
- mimeType: "image/png"
14934
- },
14935
- {
14936
- type: "text",
14937
- text: `[${mode}/${framework}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
14935
+ const content = results.flatMap((r) => {
14936
+ if (r.savedPath) {
14937
+ return [
14938
+ {
14939
+ type: "text",
14940
+ text: `\u2713 Saved to ${r.savedPath} (${r.width}\xD7${r.height}, ${elapsed}ms)${fallbackNote}`
14941
+ }
14942
+ ];
14943
+ }
14944
+ return [
14945
+ {
14946
+ type: "image",
14947
+ data: r.image,
14948
+ mimeType: "image/png"
14949
+ },
14950
+ {
14951
+ type: "text",
14952
+ text: `[${mode}/${framework}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
14938
14953
  \u26A0\uFE0F Console errors:
14939
14954
  ${r.consoleErrors.join("\n")}` : ""}${fallbackNote}`
14940
- }
14941
- ]);
14955
+ }
14956
+ ];
14957
+ });
14942
14958
  return { content };
14943
14959
  }
14944
14960
  )
@@ -30,6 +30,15 @@ interface RenderOptions {
30
30
  * }
31
31
  */
32
32
  mock?: Record<string, unknown>;
33
+ /**
34
+ * Write the screenshot to disk instead of returning it inline as base64.
35
+ * When set, tools return a short text summary ("Saved to /path 400×300 12KB")
36
+ * instead of the full image data. This prevents large base64 blobs from
37
+ * filling the LLM's context window in multi-step agent workflows.
38
+ *
39
+ * @example outputPath: "/tmp/my-component.png"
40
+ */
41
+ outputPath?: string;
33
42
  }
34
43
  interface ScreenshotResult {
35
44
  engine: Engine;
@@ -37,6 +46,8 @@ interface ScreenshotResult {
37
46
  width: number;
38
47
  height: number;
39
48
  consoleErrors: string[];
49
+ /** Set when outputPath was provided — the image was saved here instead of inline. */
50
+ savedPath?: string;
40
51
  }
41
52
  interface DiffResult {
42
53
  before: string;
@@ -254,6 +265,11 @@ declare class RenderUseCase {
254
265
  private renderUrl;
255
266
  private navigateAndWait;
256
267
  private resolveAutoFitViewport;
268
+ /**
269
+ * When outputPath is set, write the PNG to disk and replace the inline
270
+ * base64 with an empty string to keep the MCP response small.
271
+ */
272
+ private maybeSave;
257
273
  private resolveOptions;
258
274
  }
259
275
 
package/dist/renderer.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  SnapshotStore,
14
14
  SnapshotUseCase,
15
15
  ViteBundler
16
- } from "./chunk-SP7UAIQL.js";
16
+ } from "./chunk-J5JI2IC5.js";
17
17
  export {
18
18
  AuditUseCase,
19
19
  BrowserPool,
@@ -1,4 +1,8 @@
1
- // Stub for next/headers — server-only APIs that must not crash during component preview
1
+ // Stub for next/headers — server-only APIs that must not crash during component preview.
2
+ //
3
+ // Next.js 15 made headers()/cookies() async. We export both sync and async
4
+ // variants so components compiled for either version work.
5
+
2
6
  const emptyMap = {
3
7
  get: () => undefined,
4
8
  getAll: () => [],
@@ -10,10 +14,23 @@ const emptyMap = {
10
14
  set: () => {},
11
15
  delete: () => {},
12
16
  toString: () => "",
17
+ toJSON: () => ({}),
18
+ append: () => {},
19
+ [Symbol.iterator]: () => [][Symbol.iterator](),
13
20
  };
14
- export const headers = () => emptyMap;
15
- export const cookies = () => emptyMap;
16
- export const draftMode = () => ({
21
+
22
+ // Next.js 15+: headers() and cookies() are async
23
+ export const headers = async () => emptyMap;
24
+ export const cookies = async () => emptyMap;
25
+
26
+ // Next.js 15 additions
27
+ export const connection = async () => {};
28
+ export const after = (fn) => {
29
+ if (typeof fn === "function") fn();
30
+ };
31
+
32
+ // Draft mode
33
+ export const draftMode = async () => ({
17
34
  isEnabled: false,
18
35
  enable: () => {},
19
36
  disable: () => {},
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Universal stub for server-only ORM/database packages.
3
+ *
4
+ * Prisma, Drizzle, Mongoose, TypeORM etc. require a running database and
5
+ * Node.js-only modules (net, tls, dns) that break in a browser/Vite context.
6
+ *
7
+ * This stub returns a Proxy that:
8
+ * - Returns empty arrays for .findMany(), .find(), .all() style calls
9
+ * - Returns null for .findFirst(), .findOne() style calls
10
+ * - Returns the input for .create(), .update(), .save() style calls
11
+ * - Returns { count: 0 } for .count(), .deleteMany()
12
+ * - Chains infinitely (every property returns another proxy)
13
+ * - Never throws — components render in their "empty data" state
14
+ */
15
+
16
+ function makeProxy(depth = 0) {
17
+ if (depth > 8) return undefined; // prevent infinite chain
18
+ return new Proxy(() => Promise.resolve([]), {
19
+ get(_, prop) {
20
+ if (prop === "then") return undefined; // not a thenable
21
+ if (prop === Symbol.toPrimitive) return () => "[ORM stub]";
22
+ if (prop === Symbol.iterator) return function* () {};
23
+ // Common result shapes
24
+ if (
25
+ typeof prop === "string" &&
26
+ /^(findMany|findAll|all|list|getAll|fetchAll)$/.test(prop)
27
+ )
28
+ return () => Promise.resolve([]);
29
+ if (
30
+ typeof prop === "string" &&
31
+ /^(findFirst|findOne|findUnique|get|fetch|findById)$/.test(prop)
32
+ )
33
+ return () => Promise.resolve(null);
34
+ if (
35
+ typeof prop === "string" &&
36
+ /^(count|countDocuments|estimatedDocumentCount)$/.test(prop)
37
+ )
38
+ return () => Promise.resolve(0);
39
+ if (
40
+ typeof prop === "string" &&
41
+ /^(create|insert|save|update|upsert|set)$/.test(prop)
42
+ )
43
+ return (data) => Promise.resolve(data ?? {});
44
+ if (
45
+ typeof prop === "string" &&
46
+ /^(delete|deleteMany|remove|destroy)$/.test(prop)
47
+ )
48
+ return () => Promise.resolve({ count: 0 });
49
+ return makeProxy(depth + 1);
50
+ },
51
+ apply() {
52
+ return Promise.resolve([]);
53
+ },
54
+ construct() {
55
+ return makeProxy(depth + 1);
56
+ },
57
+ });
58
+ }
59
+
60
+ // Default export and named exports both return a proxy
61
+ export default makeProxy();
62
+ export const PrismaClient = () => makeProxy();
63
+ export const drizzle = () => makeProxy();
64
+ export const createClient = () => makeProxy();
65
+ export const getConnection = () => makeProxy();
66
+ export const DataSource = () => makeProxy();
@@ -1,4 +1,8 @@
1
- // Stub for next/headers — server-only APIs that must not crash during component preview
1
+ // Stub for next/headers — server-only APIs that must not crash during component preview.
2
+ //
3
+ // Next.js 15 made headers()/cookies() async. We export both sync and async
4
+ // variants so components compiled for either version work.
5
+
2
6
  const emptyMap = {
3
7
  get: () => undefined,
4
8
  getAll: () => [],
@@ -10,10 +14,23 @@ const emptyMap = {
10
14
  set: () => {},
11
15
  delete: () => {},
12
16
  toString: () => "",
17
+ toJSON: () => ({}),
18
+ append: () => {},
19
+ [Symbol.iterator]: () => [][Symbol.iterator](),
13
20
  };
14
- export const headers = () => emptyMap;
15
- export const cookies = () => emptyMap;
16
- export const draftMode = () => ({
21
+
22
+ // Next.js 15+: headers() and cookies() are async
23
+ export const headers = async () => emptyMap;
24
+ export const cookies = async () => emptyMap;
25
+
26
+ // Next.js 15 additions
27
+ export const connection = async () => {};
28
+ export const after = (fn) => {
29
+ if (typeof fn === "function") fn();
30
+ };
31
+
32
+ // Draft mode
33
+ export const draftMode = async () => ({
17
34
  isEnabled: false,
18
35
  enable: () => {},
19
36
  disable: () => {},
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Universal stub for server-only ORM/database packages.
3
+ *
4
+ * Prisma, Drizzle, Mongoose, TypeORM etc. require a running database and
5
+ * Node.js-only modules (net, tls, dns) that break in a browser/Vite context.
6
+ *
7
+ * This stub returns a Proxy that:
8
+ * - Returns empty arrays for .findMany(), .find(), .all() style calls
9
+ * - Returns null for .findFirst(), .findOne() style calls
10
+ * - Returns the input for .create(), .update(), .save() style calls
11
+ * - Returns { count: 0 } for .count(), .deleteMany()
12
+ * - Chains infinitely (every property returns another proxy)
13
+ * - Never throws — components render in their "empty data" state
14
+ */
15
+
16
+ function makeProxy(depth = 0) {
17
+ if (depth > 8) return undefined; // prevent infinite chain
18
+ return new Proxy(() => Promise.resolve([]), {
19
+ get(_, prop) {
20
+ if (prop === "then") return undefined; // not a thenable
21
+ if (prop === Symbol.toPrimitive) return () => "[ORM stub]";
22
+ if (prop === Symbol.iterator) return function* () {};
23
+ // Common result shapes
24
+ if (
25
+ typeof prop === "string" &&
26
+ /^(findMany|findAll|all|list|getAll|fetchAll)$/.test(prop)
27
+ )
28
+ return () => Promise.resolve([]);
29
+ if (
30
+ typeof prop === "string" &&
31
+ /^(findFirst|findOne|findUnique|get|fetch|findById)$/.test(prop)
32
+ )
33
+ return () => Promise.resolve(null);
34
+ if (
35
+ typeof prop === "string" &&
36
+ /^(count|countDocuments|estimatedDocumentCount)$/.test(prop)
37
+ )
38
+ return () => Promise.resolve(0);
39
+ if (
40
+ typeof prop === "string" &&
41
+ /^(create|insert|save|update|upsert|set)$/.test(prop)
42
+ )
43
+ return (data) => Promise.resolve(data ?? {});
44
+ if (
45
+ typeof prop === "string" &&
46
+ /^(delete|deleteMany|remove|destroy)$/.test(prop)
47
+ )
48
+ return () => Promise.resolve({ count: 0 });
49
+ return makeProxy(depth + 1);
50
+ },
51
+ apply() {
52
+ return Promise.resolve([]);
53
+ },
54
+ construct() {
55
+ return makeProxy(depth + 1);
56
+ },
57
+ });
58
+ }
59
+
60
+ // Default export and named exports both return a proxy
61
+ export default makeProxy();
62
+ export const PrismaClient = () => makeProxy();
63
+ export const drizzle = () => makeProxy();
64
+ export const createClient = () => makeProxy();
65
+ export const getConnection = () => makeProxy();
66
+ export const DataSource = () => makeProxy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameshot-mcp",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Zero-config visual testing for AI agents. Render project components with full Vite dependency resolution — no stories, no config.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",