@clipform/mcp-server 2.2.2 → 2.2.3

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.
@@ -0,0 +1,60 @@
1
+ // src/lib/telemetry.ts
2
+ function isTelemetryEnabled() {
3
+ return process.env.DO_NOT_TRACK !== "1" && process.env.CLIPFORM_TELEMETRY !== "off";
4
+ }
5
+ var mcpVersion = "unknown";
6
+ function setMcpVersion(version) {
7
+ mcpVersion = version;
8
+ }
9
+ function getApiBaseUrl() {
10
+ return process.env.API_URL || null;
11
+ }
12
+ var customReporter = null;
13
+ function setErrorReporter(reporter) {
14
+ customReporter = reporter;
15
+ }
16
+ function reportError(report) {
17
+ if (customReporter) {
18
+ customReporter(report);
19
+ return;
20
+ }
21
+ if (!isTelemetryEnabled()) return;
22
+ const base = getApiBaseUrl();
23
+ if (!base) return;
24
+ const payload = {
25
+ ...report,
26
+ error_message: report.error_message.slice(0, 500),
27
+ mcp_version: mcpVersion,
28
+ runtime: detectRuntime(),
29
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
30
+ };
31
+ fetch(`${base}/v1/telemetry/errors`, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify(payload),
35
+ signal: AbortSignal.timeout(2e3)
36
+ }).catch(() => {
37
+ });
38
+ }
39
+ function isServerFault(status) {
40
+ return status >= 500;
41
+ }
42
+ function isAuthFault(status, code) {
43
+ if (status === 401) return true;
44
+ if (status === 403) return code === "FORBIDDEN";
45
+ return false;
46
+ }
47
+ function detectRuntime() {
48
+ if (process.env.CLAUDE_CODE) return "claude-code";
49
+ if (process.env.CURSOR_SESSION_ID) return "cursor";
50
+ return "stdio";
51
+ }
52
+
53
+ export {
54
+ setMcpVersion,
55
+ setErrorReporter,
56
+ reportError,
57
+ isServerFault,
58
+ isAuthFault
59
+ };
60
+ //# sourceMappingURL=chunk-PBQ5BQWD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/telemetry.ts"],"sourcesContent":["// Evaluated per-call (not frozen at module load) so a runtime env change is\n// actually observed - also what makes the ordering fix below testable.\nfunction isTelemetryEnabled(): boolean {\n return process.env.DO_NOT_TRACK !== \"1\" && process.env.CLIPFORM_TELEMETRY !== \"off\";\n}\n\nlet mcpVersion = \"unknown\";\n\nexport function setMcpVersion(version: string) {\n mcpVersion = version;\n}\n\nfunction getApiBaseUrl(): string | null {\n return process.env.API_URL || null;\n}\n\nexport interface ErrorReport {\n error_type: string;\n error_message: string;\n tool_name?: string;\n status_code?: number;\n api_path?: string;\n}\n\nexport type ErrorReporter = (report: ErrorReport) => void;\n\n// Overridable delivery mechanism (#2599 follow-up). The default below POSTs\n// to /v1/telemetry/errors - correct for the external stdio MCP client, which\n// runs as its own process talking to a real remote API over HTTP. The\n// in-process host (apps/api, via packages/mcp-client) runs this same package\n// IN the API process itself, so that default would have the API POST\n// itself: mcp_version is only ever set by the stdio bin (src/index.ts), so\n// the endpoint's version-format check 400s every in-process report, and\n// /v1/telemetry/errors' 10/min IP rate limit is shared across every\n// in-process channel on one loopback IP. The in-process host installs its\n// own reporter (apps/api/src/lib/mcp-telemetry-bridge.ts) that captures\n// straight through its own PostHog client instead. Exported so that bridge\n// can install it; pass null to restore the default HTTP reporter.\nlet customReporter: ErrorReporter | null = null;\n\nexport function setErrorReporter(reporter: ErrorReporter | null): void {\n customReporter = reporter;\n}\n\nexport function reportError(report: ErrorReport) {\n // An installed reporter (the in-process host) takes precedence over the\n // opt-out gate below - DO_NOT_TRACK/CLIPFORM_TELEMETRY are an end-user\n // choice for the EXTERNAL stdio client and have no server-side meaning;\n // whatever those vars happen to be set to in the API's own environment\n // must never silently suppress the AI channels' own error capture (#2599\n // minor - the gate used to run before this check).\n if (customReporter) {\n customReporter(report);\n return;\n }\n\n if (!isTelemetryEnabled()) return;\n\n const base = getApiBaseUrl();\n if (!base) return;\n\n const payload = {\n ...report,\n error_message: report.error_message.slice(0, 500),\n mcp_version: mcpVersion,\n runtime: detectRuntime(),\n timestamp: new Date().toISOString(),\n };\n\n fetch(`${base}/v1/telemetry/errors`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(2_000),\n }).catch(() => {});\n}\n\nexport function isServerFault(status: number): boolean {\n return status >= 500;\n}\n\n/**\n * Genuine auth failures - unlike 404/422 these are never ordinary user/agent\n * error, so they're worth reporting even though they're 4xx (#2599). A 401\n * has no other legitimate cause in this API, so it always reports. A 403,\n * though, is NOT always an auth fault here: PLAN_LIMIT and other\n * business-logic denials (feature gating) also respond 403, and those are\n * ordinary user error - exactly the noise this stays silent for. Only the\n * API's own `code` on the JSON error body (from `{ code, error }`, e.g.\n * `create-form.ts`'s ApiError) tells them apart, so a 403 only counts as an\n * auth fault when the code says so; an unrecognized/missing code on a 403\n * stays silent rather than risk misclassifying a new business-logic denial.\n */\nexport function isAuthFault(status: number, code?: string): boolean {\n if (status === 401) return true;\n if (status === 403) return code === \"FORBIDDEN\";\n return false;\n}\n\nfunction detectRuntime(): string {\n if (process.env.CLAUDE_CODE) return \"claude-code\";\n if (process.env.CURSOR_SESSION_ID) return \"cursor\";\n return \"stdio\";\n}\n"],"mappings":";AAEA,SAAS,qBAA8B;AACrC,SAAO,QAAQ,IAAI,iBAAiB,OAAO,QAAQ,IAAI,uBAAuB;AAChF;AAEA,IAAI,aAAa;AAEV,SAAS,cAAc,SAAiB;AAC7C,eAAa;AACf;AAEA,SAAS,gBAA+B;AACtC,SAAO,QAAQ,IAAI,WAAW;AAChC;AAwBA,IAAI,iBAAuC;AAEpC,SAAS,iBAAiB,UAAsC;AACrE,mBAAiB;AACnB;AAEO,SAAS,YAAY,QAAqB;AAO/C,MAAI,gBAAgB;AAClB,mBAAe,MAAM;AACrB;AAAA,EACF;AAEA,MAAI,CAAC,mBAAmB,EAAG;AAE3B,QAAM,OAAO,cAAc;AAC3B,MAAI,CAAC,KAAM;AAEX,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,eAAe,OAAO,cAAc,MAAM,GAAG,GAAG;AAAA,IAChD,aAAa;AAAA,IACb,SAAS,cAAc;AAAA,IACvB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AAEA,QAAM,GAAG,IAAI,wBAAwB;AAAA,IACnC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,IAC5B,QAAQ,YAAY,QAAQ,GAAK;AAAA,EACnC,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACnB;AAEO,SAAS,cAAc,QAAyB;AACrD,SAAO,UAAU;AACnB;AAcO,SAAS,YAAY,QAAgB,MAAwB;AAClE,MAAI,WAAW,IAAK,QAAO;AAC3B,MAAI,WAAW,IAAK,QAAO,SAAS;AACpC,SAAO;AACT;AAEA,SAAS,gBAAwB;AAC/B,MAAI,QAAQ,IAAI,YAAa,QAAO;AACpC,MAAI,QAAQ,IAAI,kBAAmB,QAAO;AAC1C,SAAO;AACT;","names":[]}
@@ -0,0 +1,21 @@
1
+ // src/lib/auth-context.ts
2
+ import { AsyncLocalStorage } from "async_hooks";
3
+ var storage = new AsyncLocalStorage();
4
+ function runWithMcpAuth(ctx, fn) {
5
+ return storage.run(ctx, fn);
6
+ }
7
+ function getMcpAuth() {
8
+ return storage.getStore();
9
+ }
10
+ function runWithMcpTool(toolName, fn) {
11
+ const parent = storage.getStore();
12
+ if (!parent) return fn();
13
+ return storage.run({ ...parent, tool_name: toolName }, fn);
14
+ }
15
+
16
+ export {
17
+ runWithMcpAuth,
18
+ getMcpAuth,
19
+ runWithMcpTool
20
+ };
21
+ //# sourceMappingURL=chunk-V4L5RQWK.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/auth-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Identity of the end-user a given MCP request is acting on behalf of.\n *\n * Populated by the in-process host (apps/api) after validating an OAuth\n * Bearer on /mcp. The MCP tool layer reads it to decide which workspace\n * forms should land in and which plan tier gates apply.\n *\n * NOT populated when the server runs as a local stdio process (npx) - in\n * that mode tools fall back to edit-token based auth.\n */\nexport interface McpAuthContext {\n user_id: string;\n workspace_id: string;\n scopes: string[];\n tool_name?: string;\n api_key?: string;\n}\n\nconst storage = new AsyncLocalStorage<McpAuthContext>();\n\nexport function runWithMcpAuth<T>(ctx: McpAuthContext, fn: () => Promise<T> | T): Promise<T> | T {\n return storage.run(ctx, fn);\n}\n\nexport function getMcpAuth(): McpAuthContext | undefined {\n return storage.getStore();\n}\n\nexport function runWithMcpTool<T>(toolName: string, fn: () => Promise<T> | T): Promise<T> | T {\n const parent = storage.getStore();\n if (!parent) return fn();\n return storage.run({ ...parent, tool_name: toolName }, fn);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,yBAAyB;AAoBlC,IAAM,UAAU,IAAI,kBAAkC;AAE/C,SAAS,eAAkB,KAAqB,IAA0C;AAC/F,SAAO,QAAQ,IAAI,KAAK,EAAE;AAC5B;AAEO,SAAS,aAAyC;AACvD,SAAO,QAAQ,SAAS;AAC1B;AAEO,SAAS,eAAkB,UAAkB,IAA0C;AAC5F,QAAM,SAAS,QAAQ,SAAS;AAChC,MAAI,CAAC,OAAQ,QAAO,GAAG;AACvB,SAAO,QAAQ,IAAI,EAAE,GAAG,QAAQ,WAAW,SAAS,GAAG,EAAE;AAC3D;","names":[]}
1
+ {"version":3,"sources":["../src/lib/auth-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Identity of the end-user a given MCP request is acting on behalf of.\n *\n * Populated by the in-process host (apps/api) after validating an OAuth\n * Bearer on /mcp. The MCP tool layer reads it to decide which workspace\n * forms should land in and which plan tier gates apply.\n *\n * NOT populated when the server runs as a local stdio process (npx) - in\n * that mode tools fall back to edit-token based auth.\n */\nexport interface McpAuthContext {\n user_id: string;\n workspace_id: string;\n scopes: string[];\n tool_name?: string;\n api_key?: string;\n}\n\nconst storage = new AsyncLocalStorage<McpAuthContext>();\n\nexport function runWithMcpAuth<T>(ctx: McpAuthContext, fn: () => Promise<T> | T): Promise<T> | T {\n return storage.run(ctx, fn);\n}\n\nexport function getMcpAuth(): McpAuthContext | undefined {\n return storage.getStore();\n}\n\nexport function runWithMcpTool<T>(toolName: string, fn: () => Promise<T> | T): Promise<T> | T {\n const parent = storage.getStore();\n if (!parent) return fn();\n return storage.run({ ...parent, tool_name: toolName }, fn);\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AAoBlC,IAAM,UAAU,IAAI,kBAAkC;AAE/C,SAAS,eAAkB,KAAqB,IAA0C;AAC/F,SAAO,QAAQ,IAAI,KAAK,EAAE;AAC5B;AAEO,SAAS,aAAyC;AACvD,SAAO,QAAQ,SAAS;AAC1B;AAEO,SAAS,eAAkB,UAAkB,IAA0C;AAC5F,QAAM,SAAS,QAAQ,SAAS;AAChC,MAAI,CAAC,OAAQ,QAAO,GAAG;AACvB,SAAO,QAAQ,IAAI,EAAE,GAAG,QAAQ,WAAW,SAAS,GAAG,EAAE;AAC3D;","names":[]}
@@ -2,10 +2,10 @@ import {
2
2
  WORKFLOW_TYPES,
3
3
  getDiscoveryParams,
4
4
  getSessionContextWithAuth
5
- } from "./chunk-GZRTOCD2.js";
5
+ } from "./chunk-YZJZ5NPL.js";
6
6
  import {
7
7
  __export
8
- } from "./chunk-HCZI2UJ5.js";
8
+ } from "./chunk-G3PMV62Z.js";
9
9
 
10
10
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
11
11
  var external_exports = {};
@@ -4708,4 +4708,4 @@ export {
4708
4708
  getWorkflowText,
4709
4709
  registerPrompts
4710
4710
  };
4711
- //# sourceMappingURL=chunk-T6L5Z4J5.js.map
4711
+ //# sourceMappingURL=chunk-WCPHQ4GU.js.map
@@ -4,7 +4,7 @@ import {
4
4
  FORM_TYPE_KEYS,
5
5
  callApi,
6
6
  getSessionContext
7
- } from "./chunk-GZRTOCD2.js";
7
+ } from "./chunk-YZJZ5NPL.js";
8
8
 
9
9
  // src/lib/guides.ts
10
10
  var guidesPromise = null;
@@ -133,4 +133,4 @@ export {
133
133
  getGuideUri,
134
134
  registerResources
135
135
  };
136
- //# sourceMappingURL=chunk-EOAGENT6.js.map
136
+ //# sourceMappingURL=chunk-WNXTF76M.js.map
@@ -1,6 +1,11 @@
1
1
  import {
2
2
  getMcpAuth
3
- } from "./chunk-HCZI2UJ5.js";
3
+ } from "./chunk-V4L5RQWK.js";
4
+ import {
5
+ isAuthFault,
6
+ isServerFault,
7
+ reportError
8
+ } from "./chunk-PBQ5BQWD.js";
4
9
 
5
10
  // ../config/features.js
6
11
  var FEATURES = {
@@ -331,9 +336,9 @@ var FEATURES = {
331
336
  status: "planned",
332
337
  category: "sharing"
333
338
  },
334
- embed_slots: {
335
- name: "Embed slots",
336
- description: "Named slots that point an embed at a form, so content can be swapped from the dashboard without changing the embed code",
339
+ dynamic_link: {
340
+ name: "Dynamic links",
341
+ description: "A named link that points at a form, so the target can be swapped from the dashboard without changing the embed code or printed link",
337
342
  enabled: false,
338
343
  status: "beta",
339
344
  category: "sharing"
@@ -1049,7 +1054,10 @@ var NODE_TYPES = {
1049
1054
  type: "object",
1050
1055
  properties: {
1051
1056
  max_files: { max: 20, min: 1, type: "number", label: "Max files", default: 5, description: "Maximum number of files respondents can upload" },
1052
- max_file_size_mb: { max: 500, min: 1, type: "number", label: "Max file size (MB)", default: 50, description: "Maximum size per file (1-500 MB)" },
1057
+ // Capped at 50, not 500: respondent file uploads go straight to the Supabase
1058
+ // `file-uploads` bucket, and SUPABASE_STORAGE_MAX_BYTES is a hard ceiling there.
1059
+ // Anything above it passed validation and then 500'd at the storage layer (#2556).
1060
+ max_file_size_mb: { max: 50, min: 1, type: "number", label: "Max file size (MB)", default: 50, description: "Maximum size per file (1-50 MB)" },
1053
1061
  allowed_media_types: {
1054
1062
  type: "array",
1055
1063
  items: { enum: ["images", "videos", "documents", "all"], type: "string" },
@@ -2195,6 +2203,7 @@ var BUSINESS = {
2195
2203
  urls: getUrls()
2196
2204
  };
2197
2205
  var MEDIA_DIRECT_MAX_BYTES = 20 * 1024 * 1024;
2206
+ var SUPABASE_STORAGE_MAX_BYTES = 50 * 1024 * 1024;
2198
2207
  var VIDEO_COMPRESS_FLOOR_BYTES = 1 * 1024 * 1024;
2199
2208
  var HELP_LINKS = buildHelpLinks(BUSINESS.urls.docs);
2200
2209
  var CONTACT_FIELDS = [
@@ -2279,50 +2288,17 @@ function exposedCompositionIds(includeNonPublic) {
2279
2288
  var SCENE_LAYER_COMPOSITION_IDS = COMPOSITION_REGISTRY.filter((c) => c.scene_layer).map((c) => c.id);
2280
2289
  var MCP_TOOL_TIERS = {};
2281
2290
 
2282
- // src/lib/telemetry.ts
2283
- var telemetryEnabled = process.env.DO_NOT_TRACK !== "1" && process.env.CLIPFORM_TELEMETRY !== "off";
2284
- var mcpVersion = "unknown";
2285
- function setMcpVersion(version) {
2286
- mcpVersion = version;
2287
- }
2288
- function getApiBaseUrl() {
2289
- return process.env.API_URL || null;
2290
- }
2291
- function reportError(report) {
2292
- if (!telemetryEnabled) return;
2293
- const base = getApiBaseUrl();
2294
- if (!base) return;
2295
- const payload = {
2296
- ...report,
2297
- error_message: report.error_message.slice(0, 500),
2298
- mcp_version: mcpVersion,
2299
- runtime: detectRuntime(),
2300
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2301
- };
2302
- fetch(`${base}/v1/telemetry/errors`, {
2303
- method: "POST",
2304
- headers: { "Content-Type": "application/json" },
2305
- body: JSON.stringify(payload),
2306
- signal: AbortSignal.timeout(2e3)
2307
- }).catch(() => {
2308
- });
2309
- }
2310
- function isServerFault(status) {
2311
- return status >= 500;
2312
- }
2313
- function detectRuntime() {
2314
- if (process.env.CLAUDE_CODE) return "claude-code";
2315
- if (process.env.CURSOR_SESSION_ID) return "cursor";
2316
- return "stdio";
2317
- }
2318
-
2319
2291
  // src/lib/api-client.ts
2320
- function getApiBaseUrl2() {
2292
+ function getApiBaseUrl() {
2321
2293
  const url = process.env.API_URL;
2322
2294
  if (!url) {
2323
- throw new Error(
2324
- "API_URL must be set (e.g. http://localhost:3003 or https://api.clipform.io)"
2325
- );
2295
+ const message = "API_URL must be set (e.g. http://localhost:3003 or https://api.clipform.io)";
2296
+ reportError({
2297
+ error_type: "config_error",
2298
+ error_message: message,
2299
+ tool_name: getMcpAuth()?.tool_name
2300
+ });
2301
+ throw new Error(message);
2326
2302
  }
2327
2303
  return url;
2328
2304
  }
@@ -2330,14 +2306,20 @@ function getAuthHeaders() {
2330
2306
  const authCtx = getMcpAuth();
2331
2307
  const apiKey = authCtx?.api_key || process.env.CLIPFORM_API_KEY;
2332
2308
  if (!apiKey) {
2333
- throw new Error("CLIPFORM_API_KEY must be set.");
2309
+ const message = "CLIPFORM_API_KEY must be set.";
2310
+ reportError({
2311
+ error_type: "config_error",
2312
+ error_message: message,
2313
+ tool_name: authCtx?.tool_name
2314
+ });
2315
+ throw new Error(message);
2334
2316
  }
2335
2317
  return { "Authorization": `Bearer ${apiKey}` };
2336
2318
  }
2337
2319
  async function callApi(path, options = {}) {
2338
2320
  const { body, params, timeoutMs = 3e4 } = options;
2339
2321
  const method = options.method || (body ? "POST" : "GET");
2340
- const base = getApiBaseUrl2();
2322
+ const base = getApiBaseUrl();
2341
2323
  const prefix = path.startsWith("/internal/") ? "" : "/v1";
2342
2324
  let url = `${base}${prefix}${path}`;
2343
2325
  if (params) {
@@ -2383,7 +2365,8 @@ Error: ${message}`
2383
2365
  const hint = recoveryHint(response.status, path);
2384
2366
  const apiMsg = data.error || `API error (${response.status})`;
2385
2367
  const errorMsg = hint ? `${apiMsg} - ${hint}` : apiMsg;
2386
- if (isServerFault(response.status)) {
2368
+ const errorCode = typeof data.code === "string" ? data.code : void 0;
2369
+ if (isServerFault(response.status) || isAuthFault(response.status, errorCode)) {
2387
2370
  reportError({
2388
2371
  error_type: `http_${response.status}`,
2389
2372
  error_message: errorMsg,
@@ -2504,7 +2487,6 @@ export {
2504
2487
  SLIDESHOW_TRANSITIONS,
2505
2488
  exposedCompositionIds,
2506
2489
  MCP_TOOL_TIERS,
2507
- setMcpVersion,
2508
2490
  callApi,
2509
2491
  errorResult,
2510
2492
  textResult,
@@ -2512,4 +2494,4 @@ export {
2512
2494
  getSessionContextWithAuth,
2513
2495
  getSessionContext
2514
2496
  };
2515
- //# sourceMappingURL=chunk-GZRTOCD2.js.map
2497
+ //# sourceMappingURL=chunk-YZJZ5NPL.js.map