@clipform/mcp-server 4.5.0 → 4.7.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
@@ -102,9 +102,9 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
102
102
 
103
103
  **Local with API key:** The `CLIPFORM_API_KEY` is sent as a standard `Authorization: Bearer` header. Forms are created directly in the key's workspace with your plan tier limits.
104
104
 
105
- **Remote anonymous (no sign-in):** Connecting to `https://mcp.clipform.io` without signing in builds into a temporary account of the session's own. Forms stay drafts with a private preview link - nothing goes live - and while unclaimed, the form UUID is the credential to edit it. Build results include a one-time claim URL that signs you into that account (on the first form, periodically after, and at the form limit); free-plan limits apply. The local stdio server always needs `CLIPFORM_API_KEY`.
105
+ **Remote anonymous (no sign-in):** Connecting to `https://mcp.clipform.io` without signing in builds into a temporary account of the session's own. The form goes live at creation (after a content check), takes up to 3 responses and goes offline after 7 days unless you sign up; steps that need an account (payments, uploads, downloads, booking, outside links) are switched off until you sign up. Build results include a one-time sign-up link that moves the form into your new account (on the first form, periodically after, and at the form limit), or say "sign in" if you already have an account. A claimed form with those steps arrives as a draft - publish it to switch them on. While unclaimed, the form UUID is the credential to edit it; free-plan limits apply. The local stdio server always needs `CLIPFORM_API_KEY`.
106
106
 
107
- These tools need a signed-in Clipform account (OAuth, or an API key): `clipform_list_forms`, `clipform_whoami`, `clipform_get_results`, `clipform_get_responses`, `clipform_delete_form`. An anonymous remote session that calls one is asked to sign in instead. Everything else, including `clipform_get_guide`, `clipform_get_workflow` and `clipform_list_assets`, works without an account.
107
+ These tools need a signed-in Clipform account (OAuth, or an API key): `clipform_list_forms`, `clipform_whoami`, `clipform_get_results`, `clipform_get_responses`, `clipform_delete_form`. An anonymous remote session that calls one is asked to sign in instead (for the results tools: "Sign in to see this form's responses"). Everything else, including `clipform_get_guide`, `clipform_get_workflow` and `clipform_list_assets`, works without an account.
108
108
 
109
109
  Forms are created with a start node and end screen automatically - you just add the nodes in between.
110
110
 
@@ -75,9 +75,9 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
75
75
 
76
76
  **Local with API key:** The `CLIPFORM_API_KEY` is sent as a standard `Authorization: Bearer` header. Forms are created directly in the key's workspace with your plan tier limits.
77
77
 
78
- **Remote anonymous (no sign-in):** Connecting to `{{MCP_URL}}` without signing in builds into a temporary account of the session's own. Forms stay drafts with a private preview link - nothing goes live - and while unclaimed, the form UUID is the credential to edit it. Build results include a one-time claim URL that signs you into that account (on the first form, periodically after, and at the form limit); free-plan limits apply. The local stdio server always needs `CLIPFORM_API_KEY`.
78
+ **Remote anonymous (no sign-in):** Connecting to `{{MCP_URL}}` without signing in builds into a temporary account of the session's own. The form goes live at creation (after a content check), takes up to {{ANON_RESPONSE_CAP}} responses and goes offline after {{ANON_LIVE_DAYS}} days unless you sign up; steps that need an account (payments, uploads, downloads, booking, outside links) are switched off until you sign up. Build results include a one-time sign-up link that moves the form into your new account (on the first form, periodically after, and at the form limit), or say "sign in" if you already have an account. A claimed form with those steps arrives as a draft - publish it to switch them on. While unclaimed, the form UUID is the credential to edit it; free-plan limits apply. The local stdio server always needs `CLIPFORM_API_KEY`.
79
79
 
80
- These tools need a signed-in Clipform account (OAuth, or an API key): `clipform_list_forms`, `clipform_whoami`, `clipform_get_results`, `clipform_get_responses`, `clipform_delete_form`. An anonymous remote session that calls one is asked to sign in instead. Everything else, including `clipform_get_guide`, `clipform_get_workflow` and `clipform_list_assets`, works without an account.
80
+ These tools need a signed-in Clipform account (OAuth, or an API key): `clipform_list_forms`, `clipform_whoami`, `clipform_get_results`, `clipform_get_responses`, `clipform_delete_form`. An anonymous remote session that calls one is asked to sign in instead (for the results tools: "Sign in to see this form's responses"). Everything else, including `clipform_get_guide`, `clipform_get_workflow` and `clipform_list_assets`, works without an account.
81
81
 
82
82
  Forms are created with a start node and end screen automatically - you just add the nodes in between.
83
83
 
@@ -6,8 +6,8 @@ import {
6
6
  recoveryHint,
7
7
  structuredResult,
8
8
  textResult
9
- } from "./chunk-Q6OG6RXP.js";
10
- import "./chunk-BV3KA6HC.js";
9
+ } from "./chunk-XC3YMWCN.js";
10
+ import "./chunk-ENVV65JX.js";
11
11
  import "./chunk-PBQ5BQWD.js";
12
12
  import "./chunk-PZ5AY32C.js";
13
13
  export {
@@ -15,6 +15,7 @@ interface McpAuthContext {
15
15
  tool_name?: string;
16
16
  api_key?: string;
17
17
  is_anonymous?: boolean;
18
+ connector_guest?: boolean;
18
19
  auth_via?: "oauth";
19
20
  mcp_mint_proof?: string;
20
21
  mcp_session_proof?: string;
@@ -31,6 +32,7 @@ interface McpAuthContext {
31
32
  name: string;
32
33
  }[]>;
33
34
  adopted_form_ids?: string[];
35
+ adopted_draft_form_ids?: string[];
34
36
  default_workspace_id?: string;
35
37
  mcp_session_id?: string;
36
38
  loadInternalContent?: (kind: "guides" | "assets", params?: {
@@ -7,7 +7,7 @@ import {
7
7
  getMcpAuth,
8
8
  runWithMcpAuth,
9
9
  runWithMcpTool
10
- } from "./chunk-BV3KA6HC.js";
10
+ } from "./chunk-ENVV65JX.js";
11
11
  import "./chunk-PZ5AY32C.js";
12
12
  export {
13
13
  MCP_ANONYMOUS_HEADER,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  FULL_CAPS,
3
3
  getSessionContextWithAuth
4
- } from "./chunk-RN5ORN7O.js";
4
+ } from "./chunk-EFPJA3JE.js";
5
5
  import {
6
6
  FORM_TYPES,
7
7
  WORKFLOW_TYPES,
8
8
  getDiscoveryParams
9
- } from "./chunk-Q6OG6RXP.js";
9
+ } from "./chunk-XC3YMWCN.js";
10
10
  import {
11
11
  __export
12
12
  } from "./chunk-PZ5AY32C.js";
@@ -15231,4 +15231,4 @@ export {
15231
15231
  getWorkflowText,
15232
15232
  registerPrompts
15233
15233
  };
15234
- //# sourceMappingURL=chunk-TKNPSBNU.js.map
15234
+ //# sourceMappingURL=chunk-D4Q5ZWFO.js.map
@@ -2,10 +2,10 @@ import {
2
2
  CURRENT_PLAN_VERSIONS,
3
3
  PLAN_LIMITS,
4
4
  callApi
5
- } from "./chunk-Q6OG6RXP.js";
5
+ } from "./chunk-XC3YMWCN.js";
6
6
  import {
7
7
  getMcpAuth
8
- } from "./chunk-BV3KA6HC.js";
8
+ } from "./chunk-ENVV65JX.js";
9
9
 
10
10
  // src/lib/anon-notice.ts
11
11
  var ANON_PREVIEW_NOTICE = "Preview accounts are temporary - claim yours to keep your forms.";
@@ -13,18 +13,35 @@ function anonPreviewNotice() {
13
13
  return getMcpAuth()?.is_anonymous ? ANON_PREVIEW_NOTICE : null;
14
14
  }
15
15
  var ANON_PREVIEW_LINK_LABEL = "Preview (answers not saved)";
16
- var ANON_PREVIEW_ONLY_NOTICE = 'The form is ready as a preview. To go live, say "publish it" - that prompts sign-in and keeps this chat connected to your account. In the preview, answers are not saved: do not share the preview link with respondents.';
16
+ var ANON_PREVIEW_ONLY_NOTICE = "The form is ready as a preview. To put it live, call clipform_update_form with is_live:true - no sign-in is needed. In the preview, answers are not saved: do not share the preview link with respondents.";
17
+ var ANON_PREVIEW_ONLY_NOTICE_DRAFTS_ONLY = 'The form is ready as a preview. To go live, say "publish it" - that prompts sign-in and keeps this chat connected to your account. In the preview, answers are not saved: do not share the preview link with respondents.';
18
+ function isConnectorGuest() {
19
+ const auth = getMcpAuth();
20
+ return auth?.is_anonymous === true && auth.connector_guest === true;
21
+ }
22
+ function previewOnlyNotice() {
23
+ return isConnectorGuest() ? ANON_PREVIEW_ONLY_NOTICE : ANON_PREVIEW_ONLY_NOTICE_DRAFTS_ONLY;
24
+ }
17
25
  var PREVIEW_URL_DESCRIPTION = "Private preview of the form. Answers are not saved, so do not share it with respondents.";
18
- var CLAIM_URL_DESCRIPTION = "One-time link to sign up, keep this form and publish it.";
26
+ var CLAIM_URL_DESCRIPTION = "One-time link to sign up, keep this form and switch on its locked steps.";
19
27
  var ADOPTED_FORM_NOTICE = "Signed in - this form moved from the temporary preview account into your Clipform account, so it is yours now. The earlier claim link is no longer needed.";
20
- function withAdoptedNotice(result, args) {
28
+ var ADOPTED_DRAFT_FORM_NOTICE = "Signed in - this form moved from the temporary preview account into your Clipform account, so it is yours now. It is a draft now: publish it (clipform_update_form with is_live:true) to switch its steps on. The earlier claim link is no longer needed.";
29
+ var ADOPTED_LIVE_FORM_NOTICE = "Signed in - this form moved from the temporary preview account into your Clipform account, so it is yours now. It is live with every step switched on. The earlier claim link is no longer needed.";
30
+ function withAdoptedNotice(result, args, toolName) {
21
31
  const formId = args?.form_id;
22
32
  if (typeof formId !== "string") return result;
23
- const adopted = getMcpAuth()?.adopted_form_ids;
24
- if (!adopted || !adopted.map((id) => id.toLowerCase()).includes(formId.toLowerCase())) return result;
33
+ const auth = getMcpAuth();
34
+ const id = formId.toLowerCase();
35
+ const has = (ids) => !!ids && ids.map((i) => i.toLowerCase()).includes(id);
36
+ if (!has(auth?.adopted_form_ids)) return result;
25
37
  const r = result;
26
38
  if (!r || r.isError || !Array.isArray(r.content)) return result;
27
- return { ...r, content: [{ type: "text", text: ADOPTED_FORM_NOTICE }, ...r.content] };
39
+ let notice = ADOPTED_FORM_NOTICE;
40
+ if (has(auth?.adopted_draft_form_ids)) {
41
+ const publishedNow = toolName === "clipform_update_form" && args?.is_live === true;
42
+ notice = publishedNow ? ADOPTED_LIVE_FORM_NOTICE : ADOPTED_DRAFT_FORM_NOTICE;
43
+ }
44
+ return { ...r, content: [{ type: "text", text: notice }, ...r.content] };
28
45
  }
29
46
  function isPreviewViewerUrl(url) {
30
47
  return typeof url === "string" && url.includes("mode=preview");
@@ -113,7 +130,8 @@ export {
113
130
  ANON_PREVIEW_NOTICE,
114
131
  anonPreviewNotice,
115
132
  ANON_PREVIEW_LINK_LABEL,
116
- ANON_PREVIEW_ONLY_NOTICE,
133
+ isConnectorGuest,
134
+ previewOnlyNotice,
117
135
  PREVIEW_URL_DESCRIPTION,
118
136
  CLAIM_URL_DESCRIPTION,
119
137
  withAdoptedNotice,
@@ -123,4 +141,4 @@ export {
123
141
  getSessionContextWithAuth,
124
142
  getSessionContext
125
143
  };
126
- //# sourceMappingURL=chunk-RN5ORN7O.js.map
144
+ //# sourceMappingURL=chunk-EFPJA3JE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/anon-notice.ts","../src/lib/caps.ts","../src/lib/session-context.ts"],"sourcesContent":["import { getMcpAuth } from \"./auth-context.js\";\n\n/**\n * #4650: anonymous (preview) accounts are temporary. Date-free by design - the\n * generic notice promises no day count. The 7-day offline expiry of a live\n * guest form IS enforced now (#4847; ANON_MCP_LIVE_TTL_DAYS, stated in the\n * create_form reply), while the account purge keeps its own schedule (#4662).\n * Defined once; every anonymous-lane surface (create/update_form text, whoami,\n * session resource) reuses it.\n */\nexport const ANON_PREVIEW_NOTICE =\n \"Preview accounts are temporary - claim yours to keep your forms.\";\n\n/** The notice for an anonymous MCP caller, or null for everyone else. */\nexport function anonPreviewNotice(): string | null {\n return getMcpAuth()?.is_anonymous ? ANON_PREVIEW_NOTICE : null;\n}\n\n/**\n * #4845: a preview link is private, never a respondent link (answers are not\n * saved). A guest build now goes live at creation (#4847); a preview is what\n * remains for a draft (moderation held it, or it was taken offline). Wording\n * defined once; create_form, get_form, the format-form link label and the\n * output-schema descriptions all reuse it.\n */\nexport const ANON_PREVIEW_LINK_LABEL = \"Preview (answers not saved)\";\n\nexport const ANON_PREVIEW_ONLY_NOTICE =\n 'The form is ready as a preview. To put it live, call clipform_update_form with is_live:true - no sign-in is needed. In the preview, answers are not saved: do not share the preview link with respondents.';\n\n/** The drafts-only lanes (bot, email, web demo - not the MCP connector): signing up is what publishes. */\nexport const ANON_PREVIEW_ONLY_NOTICE_DRAFTS_ONLY =\n 'The form is ready as a preview. To go live, say \"publish it\" - that prompts sign-in and keeps this chat connected to your account. In the preview, answers are not saved: do not share the preview link with respondents.';\n\n/** True on the MCP handler's anonymous connector lane (#4847). */\nexport function isConnectorGuest(): boolean {\n const auth = getMcpAuth();\n return auth?.is_anonymous === true && auth.connector_guest === true;\n}\n\n/** The preview-only notice for this caller's lane. */\nexport function previewOnlyNotice(): string {\n return isConnectorGuest() ? ANON_PREVIEW_ONLY_NOTICE : ANON_PREVIEW_ONLY_NOTICE_DRAFTS_ONLY;\n}\n\nexport const PREVIEW_URL_DESCRIPTION =\n \"Private preview of the form. Answers are not saved, so do not share it with respondents.\";\n\nexport const CLAIM_URL_DESCRIPTION =\n \"One-time link to sign up, keep this form and switch on its locked steps.\";\n\n/**\n * #5158: sign-in happens in a popup the model never sees, so after an adopt\n * (apps/api oauth-adopt.ts) the tool result tells it the form is now the\n * user's. Prepended by the server.ts registerTool wrapper.\n */\nexport const ADOPTED_FORM_NOTICE =\n \"Signed in - this form moved from the temporary preview account into your Clipform account, so it is yours now. The earlier claim link is no longer needed.\";\n\n/**\n * #4847: a claimed form with a locked step (payment, uploads, links ...) was\n * taken offline by the claim lock, so it arrives as a draft. Publishing it as\n * the new owner switches those steps on.\n */\nexport const ADOPTED_DRAFT_FORM_NOTICE =\n \"Signed in - this form moved from the temporary preview account into your Clipform account, so it is yours now. It is a draft now: publish it (clipform_update_form with is_live:true) to switch its steps on. The earlier claim link is no longer needed.\";\n\n/** The same call published the claimed form: every step is on. */\nexport const ADOPTED_LIVE_FORM_NOTICE =\n \"Signed in - this form moved from the temporary preview account into your Clipform account, so it is yours now. It is live with every step switched on. The earlier claim link is no longer needed.\";\n\n/**\n * Prepend the adopt notice to a successful result for a just-adopted form.\n * content only; structuredContent untouched. A form the claim lock drafted gets\n * the draft variant, or the live one when THIS call is update_form is_live:true.\n */\nexport function withAdoptedNotice<T>(result: T, args: unknown, toolName?: string): T {\n const formId = (args as { form_id?: unknown } | undefined)?.form_id;\n if (typeof formId !== \"string\") return result;\n const auth = getMcpAuth();\n const id = formId.toLowerCase();\n const has = (ids?: string[]) => !!ids && ids.map((i) => i.toLowerCase()).includes(id);\n if (!has(auth?.adopted_form_ids)) return result;\n const r = result as { isError?: boolean; content?: unknown } | undefined;\n if (!r || r.isError || !Array.isArray(r.content)) return result;\n let notice = ADOPTED_FORM_NOTICE;\n if (has(auth?.adopted_draft_form_ids)) {\n const publishedNow =\n toolName === \"clipform_update_form\" && (args as { is_live?: unknown } | undefined)?.is_live === true;\n notice = publishedNow ? ADOPTED_LIVE_FORM_NOTICE : ADOPTED_DRAFT_FORM_NOTICE;\n }\n return { ...r, content: [{ type: \"text\", text: notice }, ...r.content] } as T;\n}\n\n/** True for a viewer link in preview mode (an anonymous caller's draft). */\nexport function isPreviewViewerUrl(url: unknown): boolean {\n return typeof url === \"string\" && url.includes(\"mode=preview\");\n}\n","import type { McpToolset } from \"@vid-master/config\";\n\n/**\n * What a connected surface can actually do, derived from the connect-time\n * toolset filter (#3309) so the guidance layers (server instructions, prompt\n * recipes, `clipform_get_workflow`, craft guides) never reference a tool the\n * surface doesn't register (#3681).\n *\n * The Claude Connectors Directory surface (`mcp.clipform.io/forms`) is\n * account + forms only: no media generation and no research helpers. A\n * workflow that still says \"call clipform_generate_tts\" there sends the model\n * at a tool that doesn't exist, which shows up in review as failed tool\n * calls. Every guidance producer takes a ServerCaps and gates on it.\n */\nexport interface ServerCaps {\n /** Narration, video, image, music, render, and asset tools are registered. */\n media: boolean;\n /** News lookup and the generation audit log are registered. */\n research: boolean;\n /** Form tools (create, edit, publish) are registered. */\n forms: boolean;\n}\n\n/** Every toolset registered - stdio/npx and the bare remote endpoint. */\nexport const FULL_CAPS: ServerCaps = Object.freeze({ media: true, research: true, forms: true });\n\nexport function capsFromToolsets(toolsets?: readonly McpToolset[]): ServerCaps {\n if (toolsets === undefined) return FULL_CAPS;\n return {\n media: toolsets.includes(\"media\"),\n research: toolsets.includes(\"research\"),\n forms: toolsets.includes(\"forms\"),\n };\n}\n","import { BUSINESS, PLAN_LIMITS, CURRENT_PLAN_VERSIONS } from \"@vid-master/config\";\nimport { callApi } from \"./api-client.js\";\nimport { ANON_PREVIEW_NOTICE } from \"./anon-notice.js\";\nimport { getMcpAuth } from \"./auth-context.js\";\n\nexport type AuthMode = \"authenticated\" | \"anonymous\";\n\nexport interface SessionContextResult {\n text: string;\n authMode: AuthMode;\n}\n\nexport async function getSessionContextWithAuth(): Promise<SessionContextResult> {\n let me: any;\n const authCtx = getMcpAuth();\n if (authCtx?.loadInternalContent && !authCtx.api_key) {\n // #4657: the no-workspace anon lane (get_workflow with no mint) has no\n // workspace for /me to describe, so plan caps default to Free.\n const free = PLAN_LIMITS[CURRENT_PLAN_VERSIONS[0]];\n me = {\n auth_mode: \"anonymous\",\n plan: {\n name: free.name,\n tier: free.tier,\n node_limit: free.node_limit,\n form_limit: free.form_limit ?? null,\n custom_theme: free.custom_theme,\n },\n };\n } else {\n let result;\n try {\n result = await callApi(\"/me\", { method: \"GET\" });\n } catch {\n return { text: \"\", authMode: \"anonymous\" };\n }\n if (!result.ok) return { text: \"\", authMode: \"anonymous\" };\n me = result.data as any;\n }\n // The API's auth_mode is \"api_key\" | \"oauth\" | \"session\" | \"anonymous\".\n // Anything credentialed counts as authenticated - the old `=== \"oauth\"`\n // check matched nothing the API returns, so API-key sessions were labelled\n // anonymous with a \"sign in to unlock\" nudge right next to their real\n // plan (#412 F6).\n // The anon MCP lane runs as a minted bot key (/me says \"api_key\"), so the\n // auth context is the truth for anonymous (#4653).\n const apiAuthMode: string = getMcpAuth()?.is_anonymous\n ? \"anonymous\"\n : ((me.auth_mode as string) ?? \"anonymous\");\n const authMode: AuthMode = apiAuthMode === \"anonymous\" ? \"anonymous\" : \"authenticated\";\n const plan = me.plan;\n const lines: string[] = [];\n\n lines.push(\"## Your Session\");\n if (authMode === \"authenticated\") {\n lines.push(\n apiAuthMode === \"api_key\"\n ? \"Auth: API key (workspace-scoped)\"\n : \"Auth: signed in (connected to a Clipform account)\"\n );\n // Workspace NAME only - the workspace UUID and the raw user_id/company_id\n // are internal database identifiers the model never needs (response\n // hygiene / disclosure guard, #2976).\n lines.push(`Workspace: ${me.workspace?.name ?? \"Unknown\"}`);\n } else {\n lines.push(\"Auth: anonymous (not connected to a Clipform account)\");\n if (me.workspace) {\n lines.push(`Workspace: ${me.workspace.name}`);\n }\n }\n lines.push(`Plan: ${plan.name} (tier ${plan.tier})`);\n if (plan.node_limit !== null) {\n lines.push(`Question limit: ${plan.node_limit} per form`);\n } else {\n lines.push(\"Questions: unlimited\");\n }\n if (plan.form_limit !== null) {\n lines.push(`Form limit: ${plan.form_limit} forms`);\n } else {\n lines.push(\"Forms: unlimited\");\n }\n if (plan.custom_theme === false) {\n lines.push(\"Custom themes: not available on this plan\");\n }\n if (authMode === \"anonymous\") {\n // Neutral statement of fact, no upsell/CTA copy (commerce sweep, #2976):\n // an anonymous session's forms are not tied to an account until one signs in.\n lines.push(`\\nAnonymous session: forms are not tied to a Clipform account until you sign in.`);\n lines.push(ANON_PREVIEW_NOTICE);\n }\n\n return { text: lines.join(\"\\n\"), authMode };\n}\n\nexport async function getSessionContext(): Promise<string> {\n const { text } = await getSessionContextWithAuth();\n return text;\n}\n"],"mappings":"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","names":[]}
@@ -28,4 +28,4 @@ export {
28
28
  getMcpAuth,
29
29
  runWithMcpTool
30
30
  };
31
- //# sourceMappingURL=chunk-BV3KA6HC.js.map
31
+ //# sourceMappingURL=chunk-ENVV65JX.js.map
@@ -0,0 +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 // Set when the human behind this tool call is an anonymous Supabase\n // session (apps/api's app-chat lane, #2983/#2984) - api-client.ts's\n // getAuthHeaders reads it to mark the outgoing bot-key HTTP request so the\n // API can propagate the restriction even though the request itself\n // authenticates with an API key, not a JWT.\n is_anonymous?: boolean;\n // #4847: set ONLY by apps/api's MCP handler on its anonymous connector lane\n // (ChatGPT / Claude guests), never by the bot/email/web channels. It picks the\n // chat copy (\"goes live\" vs \"stays a preview\"); the API enforces go-live itself\n // regardless (marker header + mcp link), so this is never an access decision.\n connector_guest?: boolean;\n // Set on the OAuth branch of apps/api's mcp/handler.ts when this request's\n // bot API key was resolved on behalf of an OAuth-connected user (#3716) -\n // api-client.ts's getAuthHeaders reads it to mark the outgoing request so\n // /v1/me can report auth_mode \"oauth\" instead of the generic \"api_key\" the\n // bot-key lane would otherwise get, even though the request itself\n // authenticates with an API key, not an OAuth token.\n auth_via?: \"oauth\";\n // #3330 review fix (blocker 1): an HMAC proof (apps/api's mcp-mint-proof.ts)\n // that THIS exact request's #1415 anon lane freshly minted a brand-new anon\n // identity for `workspace_id` - set only on the mint branch of\n // resolveAnonymousMcpAuth, never when auth was resolved via an inbound\n // form_id (even the caller's own). Lets create-form.ts safely offer a\n // same-UID claim link without ever deriving \"who to bind it to\" from a\n // body-supplied id, which would let a batch smuggling another tenant's\n // form_id alongside a create_form call mint a claim token for THAT\n // tenant's identity instead of the caller's own.\n mcp_mint_proof?: string;\n // #4090: sibling of mcp_mint_proof, set when the anon lane REUSED a\n // server-verified session binding (#4652) instead of minting. HMAC-bound\n // to the bound {workspace, uid}; lets create-form.ts re-surface the claim\n // link on later forms in the session.\n mcp_session_proof?: string;\n // #3822: set ONLY on the OAuth branch of apps/api's mcp/handler.ts. Lets a\n // single tool call target another workspace the connected user belongs to\n // (per-call, never per-request - see the #3330 note in the package\n // CLAUDE.md). The host re-checks membership on every call and returns that\n // workspace's own bot key, or null to mean \"act in the default workspace\n // unchanged\" (target is the default, or a form_id the user can't reach -\n // the API then answers exactly as it did before targeting existed). Anon + stdio contexts never carry it, so they\n // stay pinned to one workspace.\n resolveWorkspace?: (target: {\n workspace_id?: string;\n form_id?: string;\n }) => Promise<{ workspace_id: string; workspace_name: string; api_key: string } | null>;\n // #3822: the workspaces the connected user may target (same set\n // resolveWorkspace accepts). OAuth lane only.\n listWorkspaces?: () => Promise<{ id: string; name: string }[]>;\n // #5158: OAuth lane only. The anonymous form ids THIS request just adopted\n // into the signed-in user's workspace (apps/api oauth-adopt.ts, #4638). The\n // sign-in happens in a popup the model never sees, so the tool wrapper uses\n // this to tell it the form is now the user's. INFORMATIONAL ONLY - derived\n // from the server-side adopt outcome of this same request, never from client\n // input, and never read for an access decision.\n adopted_form_ids?: string[];\n // #4847: the subset of adopted_form_ids the claim lock took offline (a locked\n // step - payment, uploads, links ...): they arrive as drafts the owner must\n // publish to switch those steps on. Same provenance and INFORMATIONAL-only\n // rule as adopted_form_ids.\n adopted_draft_form_ids?: string[];\n // #3822: the workspace picked at authorize time - what a call with no\n // `workspace_id` acts in. OAuth lane only (workspace_id above is swapped\n // per call when a target is resolved, this never is).\n default_workspace_id?: string;\n // #4657: set ONLY by apps/api's mcp/handler.ts for an anonymous request made\n // entirely of `anon_no_workspace` tools (get_guide / get_workflow /\n // list_assets). That lane has NO credential of any kind: the static content\n // is served in-process through this loader instead of an HTTP callApi, so no\n // secret or key ever sits in the anonymous context. Tools use it when\n // present and callApi otherwise (stdio / published behaviour unchanged).\n // #4657: decoded PostHog MCP `$session_id` from the inbound Mcp-Session-Id -\n // ANALYTICS ONLY (stamped on funnel events). api-client.ts forwards it as\n // MCP_SESSION_ID_HEADER; nothing about identity or access derives from it.\n mcp_session_id?: string;\n loadInternalContent?: (\n kind: \"guides\" | \"assets\",\n params?: { type?: string }\n ) => Promise<Record<string, unknown>>;\n}\n\n/**\n * Marker header the in-process MCP tool layer sets on its outgoing API\n * requests (api-client.ts) when `is_anonymous` is true, so an anonymous\n * app-chat session can't launder itself into an unrestricted API-key caller\n * by asking the bot to publish/mint credentials on its behalf. apps/api's\n * authMiddleware / workspaceAccessMiddleware / requireWorkspaceWriteAuth\n * read this on the API-key path and set isAnonymous accordingly - trusting\n * it is safe because it can only RESTRICT a caller: a real integration that\n * sets this header on its own API-key requests only denies itself, it can\n * never use it to gain anonymous-JWT privileges it doesn't already have.\n */\nexport const MCP_ANONYMOUS_HEADER = \"x-clipform-anonymous\";\n\n/**\n * Marker header the in-process MCP tool layer sets on its outgoing API\n * requests (api-client.ts) when `auth_via` is \"oauth\", so /v1/me can report\n * `auth_mode: \"oauth\"` for an OAuth-connected user instead of the generic\n * \"api_key\" the bot-key lane would otherwise get (#3716). Label-only: it\n * changes nothing but the reported string. Trusting it from the wire is\n * safe because a direct API-key caller who sets this header on their own\n * request only mislabels their own whoami output - they gain no privilege,\n * exactly the same reasoning as MCP_ANONYMOUS_HEADER above (except that\n * header can only restrict, and this one can only relabel).\n */\nexport const MCP_AUTH_VIA_HEADER = \"x-clipform-auth-via\";\n\n/**\n * Carries `mcp_mint_proof` on the same outgoing bot-key request. Unlike\n * MCP_ANONYMOUS_HEADER (a boolean that can only ever RESTRICT), this header's\n * VALUE matters - it names an identity a claim token gets bound to - so its\n * value is HMAC-signed (mcp-mint-proof.ts) rather than trusted raw: a direct\n * external caller reaching `POST /v1/forms` with any valid workspace API key\n * could otherwise hand-set an unsigned \"mint uid\" header to an arbitrary\n * victim's uid themselves (#3330 review).\n */\nexport const MCP_MINT_PROOF_HEADER = \"x-clipform-mcp-mint-proof\";\n\n/** #4090: carries `mcp_session_proof` (same trust model as MCP_MINT_PROOF_HEADER: HMAC-verified server-side, never trusted raw). */\nexport const MCP_SESSION_PROOF_HEADER = \"x-clipform-mcp-session-proof\";\n\n/** Carries `mcp_session_id` to the API for funnel analytics only (#4657). */\nexport const MCP_SESSION_ID_HEADER = \"x-clipform-mcp-session-id\";\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;AA6G3B,IAAM,uBAAuB;AAa7B,IAAM,sBAAsB;AAW5B,IAAM,wBAAwB;AAG9B,IAAM,2BAA2B;AAGjC,IAAM,wBAAwB;AAErC,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":[]}
@@ -29,7 +29,7 @@ import {
29
29
  union,
30
30
  unknown,
31
31
  url
32
- } from "./chunk-TKNPSBNU.js";
32
+ } from "./chunk-D4Q5ZWFO.js";
33
33
  import {
34
34
  GUIDE_TYPES,
35
35
  QUIZ_VARIANTS,
@@ -38,26 +38,29 @@ import {
38
38
  getGuideUri,
39
39
  guideFallbackText,
40
40
  registerResources
41
- } from "./chunk-WTYIKGXQ.js";
41
+ } from "./chunk-VSSV3IOU.js";
42
42
  import {
43
43
  ANON_PREVIEW_LINK_LABEL,
44
44
  ANON_PREVIEW_NOTICE,
45
- ANON_PREVIEW_ONLY_NOTICE,
46
45
  CLAIM_URL_DESCRIPTION,
47
46
  FULL_CAPS,
48
47
  PREVIEW_URL_DESCRIPTION,
49
48
  anonPreviewNotice,
50
49
  capsFromToolsets,
50
+ isConnectorGuest,
51
51
  isPreviewViewerUrl,
52
+ previewOnlyNotice,
52
53
  withAdoptedNotice
53
- } from "./chunk-RN5ORN7O.js";
54
+ } from "./chunk-EFPJA3JE.js";
54
55
  import {
55
56
  ACTIVE_NODE_TYPES,
57
+ ANON_MCP_BLOCKED_NODE_TYPES,
58
+ ANON_MCP_LIVE_TTL_DAYS,
59
+ ANON_MCP_RESPONSE_CAP,
56
60
  CONTACT_FIELDS,
57
61
  FEATURE_FLAGS,
58
62
  FORM_TYPES_LIST,
59
63
  FORM_TYPE_ALIASES,
60
- MCP_ANON_PUBLISH_GATE,
61
64
  MCP_TOOL_SECURITY,
62
65
  MCP_TOOL_TIERS,
63
66
  MCP_TOOL_TOOLSETS,
@@ -70,17 +73,18 @@ import {
70
73
  errorResult,
71
74
  exposedCompositionIds,
72
75
  gatedConfigKeys,
76
+ isAnonMcpLockedNode,
73
77
  isAnonymousAccountRequired,
74
78
  resolveFormType,
75
79
  stripGatedConfigKeys,
76
80
  structuredResult,
77
81
  textResult
78
- } from "./chunk-Q6OG6RXP.js";
82
+ } from "./chunk-XC3YMWCN.js";
79
83
  import {
80
84
  getMcpAuth,
81
85
  runWithMcpAuth,
82
86
  runWithMcpTool
83
- } from "./chunk-BV3KA6HC.js";
87
+ } from "./chunk-ENVV65JX.js";
84
88
 
85
89
  // ../../node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
86
90
  var __create = Object.create;
@@ -16096,6 +16100,7 @@ var INSTRUCTIONS_LEAD = `Clipform builds interactive video-style forms that othe
16096
16100
  // src/lib/onboarding-copy.ts
16097
16101
  var STARTERS = ACTIVE_JOB_LABELS.join(", ");
16098
16102
  var ONBOARDING_OFFER_CONTROL_VARIANT = "control";
16103
+ var ONBOARDING_OFFER_GUEST_VARIANT = "guest";
16099
16104
  var ONBOARDING_OFFER_COPY = {
16100
16105
  // #4089 review: this cohort has already completed OAuth to connect - they
16101
16106
  // have an account. Never claim "no account needed" or otherwise imply an
@@ -16103,7 +16108,12 @@ var ONBOARDING_OFFER_COPY = {
16103
16108
  // did and reads as broken. Acknowledge the connection instead, then offer
16104
16109
  // to build. (The anonymous/no-auth entry point is a different surface,
16105
16110
  // #4088 - not this cohort.)
16106
- [ONBOARDING_OFFER_CONTROL_VARIANT]: `When this is the start of a new conversation and the user has not yet said what they want to build, welcome them as someone who is now connected to Clipform and offer to build their first form together, then offer these starting points as plain text: ${STARTERS}, or something else - ask which one fits, or invite them to describe what they need instead. If the user's first message already states what they want to build, even loosely, skip that offer entirely and go straight into the brief-driven flow below.`
16111
+ [ONBOARDING_OFFER_CONTROL_VARIANT]: `When this is the start of a new conversation and the user has not yet said what they want to build, welcome them as someone who is now connected to Clipform and offer to build their first form together, then offer these starting points as plain text: ${STARTERS}, or something else - ask which one fits, or invite them to describe what they need instead. If the user's first message already states what they want to build, even loosely, skip that offer entirely and go straight into the brief-driven flow below.`,
16112
+ // #4847: a guest has NOT signed in, so the control copy's "now connected" is wrong.
16113
+ // Offer the sign-in path (the model's only trigger is the sign-in-only
16114
+ // clipform_whoami, which makes the host show its own Connect button) or the
16115
+ // same starting points.
16116
+ [ONBOARDING_OFFER_GUEST_VARIANT]: `When this is the start of a new conversation and the user has not yet said what they want to build, offer to build their first form together and give them two ways to start, as plain text: "Already have a Clipform account? Say 'sign in'." (when they say it, call clipform_whoami - it starts sign-in), or pick a starting point: ${STARTERS}, or something else - ask which one fits, or invite them to describe what they need instead. If the user's first message already states what they want to build, even loosely, skip that offer entirely and go straight into the brief-driven flow below.`
16107
16117
  };
16108
16118
  function buildOnboardingOffer(variant = ONBOARDING_OFFER_CONTROL_VARIANT) {
16109
16119
  return ONBOARDING_OFFER_COPY[variant] ?? ONBOARDING_OFFER_COPY[ONBOARDING_OFFER_CONTROL_VARIANT];
@@ -16362,13 +16372,36 @@ function mdLink(label, url2) {
16362
16372
  return `[${label}](${url2})`;
16363
16373
  }
16364
16374
 
16375
+ // src/lib/locked-steps.ts
16376
+ var LINKS_KIND = "outside links";
16377
+ function kindFor(type) {
16378
+ if (ANON_MCP_BLOCKED_NODE_TYPES.includes(type)) {
16379
+ const label = NODE_TYPES[type]?.label;
16380
+ return (label ?? type.replace(/_/g, " ")).toLowerCase();
16381
+ }
16382
+ return LINKS_KIND;
16383
+ }
16384
+ function lockedStepKinds(nodes) {
16385
+ const kinds = [];
16386
+ for (const node of nodes) {
16387
+ if (!isAnonMcpLockedNode(node)) continue;
16388
+ const kind = kindFor(node.type);
16389
+ if (!kinds.includes(kind)) kinds.push(kind);
16390
+ }
16391
+ return kinds;
16392
+ }
16393
+ function joinKinds(kinds) {
16394
+ if (kinds.length <= 1) return kinds.join("");
16395
+ return `${kinds.slice(0, -1).join(", ")} and ${kinds[kinds.length - 1]}`;
16396
+ }
16397
+
16365
16398
  // src/tools/create-form.ts
16366
16399
  function registerCreateFormTool(server) {
16367
16400
  server.registerTool(
16368
16401
  "clipform_create_form",
16369
16402
  {
16370
16403
  title: "Create Clipform",
16371
- description: `Create a new Clipform (interactive video-style form). Returns a viewer URL and form ID (an anonymous build's link is a private preview where answers are not saved). When connected via an authenticated MCP client (e.g. claude.ai), the form lands directly in the user's workspace. Anonymous sessions build into a temporary account of their own: forms stay drafts with a private preview link. Some results also include a one-time claim URL that signs the user into that account; relay it when present.
16404
+ description: `Create a new Clipform (interactive video-style form). Returns a viewer URL and form ID. When connected via an authenticated MCP client (e.g. claude.ai), the form lands directly in the user's workspace. Anonymous sessions build into a temporary account of their own: guests connected through a chat app (ChatGPT, Claude) get a form that goes live, takes up to ${ANON_MCP_RESPONSE_CAP} responses and goes offline after ${ANON_MCP_LIVE_TTL_DAYS} days unless the user signs up, with steps that need an account (such as payments and uploads) switched off until sign-up; other anonymous channels stay private previews. Some results also include a one-time claim URL that signs the user into that account; relay it when present.
16372
16405
 
16373
16406
  ${ONBOARDING_OFFER_TOOL_CUE}
16374
16407
 
@@ -16407,9 +16440,9 @@ Example: A form that asks a question, collects contact info, then finishes:
16407
16440
  // CONTRACT step drops the anonymous viewer_url / dashboard_url
16408
16441
  // duplicates. See SUBMISSION.md. Until (c), structuredContent stays
16409
16442
  // key-for-key what origin/main emitted (approved-schema.test.ts).
16410
- viewer_url: external_exports.string().describe("Form URL for respondents - the live public URL. For an anonymous build this is a private preview link: answers are not saved, do not share it with respondents."),
16411
- dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit). For an anonymous build this is a one-time claim link, not an editor."),
16412
- is_preview: external_exports.boolean().optional().describe("True when this is an anonymous preview build (answers are not saved), false otherwise."),
16443
+ viewer_url: external_exports.string().describe("Form URL for respondents - the live public URL. For a guest build held back from going live this is a private preview link instead: answers are not saved, do not share it with respondents."),
16444
+ dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit). For an anonymous build this is a one-time sign-up link, not an editor."),
16445
+ is_preview: external_exports.boolean().optional().describe("True when viewer_url is a private preview (answers are not saved), false when it is the live URL."),
16413
16446
  preview_url: external_exports.string().optional().describe(`Anonymous builds only. ${PREVIEW_URL_DESCRIPTION}`),
16414
16447
  claim_url: external_exports.string().optional().describe(`Anonymous builds only. ${CLAIM_URL_DESCRIPTION}`),
16415
16448
  nodes: external_exports.array(external_exports.object({ id: external_exports.string(), type: external_exports.string(), prompt: external_exports.string() })).describe("Created nodes in order - IDs for attach_node_media / update_node"),
@@ -16526,11 +16559,18 @@ Example: A form that asks a question, collects contact info, then finishes:
16526
16559
  body: { tags }
16527
16560
  });
16528
16561
  }
16562
+ const isGuest = !planContext?.is_authenticated && isConnectorGuest();
16563
+ const guestLive = isGuest && !isPreview;
16564
+ const liveRefusedReason = typeof data.live_refused_reason === "string" && data.live_refused_reason ? data.live_refused_reason : null;
16529
16565
  const truncate2 = (s, n = 60) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
16530
16566
  const lines = [
16531
16567
  isPreview ? `Form created as a preview draft.` : `Form created and live.`,
16532
16568
  // #4845: lead with the in-chat path (wording defined once in lib/anon-notice.ts).
16533
- ...isPreview ? [``, ANON_PREVIEW_ONLY_NOTICE] : [],
16569
+ ...isPreview ? [``, previewOnlyNotice()] : [],
16570
+ ...isPreview && isGuest && liveRefusedReason ? [
16571
+ ``,
16572
+ `It is not live yet: ${liveRefusedReason} Once that is sorted, clipform_update_form with is_live:true can retry going live.`
16573
+ ] : [],
16534
16574
  ``,
16535
16575
  `Title: ${title}`,
16536
16576
  ...planContext?.is_authenticated && planContext.workspace_name ? [`Workspace: ${planContext.workspace_name}`] : [],
@@ -16543,18 +16583,32 @@ Example: A form that asks a question, collects contact info, then finishes:
16543
16583
  ];
16544
16584
  if (formUrl) {
16545
16585
  lines.push(
16546
- isPreview ? `CLAIM URL (fallback - or open this one-time link to sign up in the browser): ${mdLink(LINK_LABELS.claim, formUrl)}` : `DASHBOARD URL (sign in to edit and manage): ${mdLink(LINK_LABELS.edit, formUrl)}`
16586
+ isPreview ? `CLAIM URL (fallback - or open this one-time link to sign up in the browser): ${mdLink(LINK_LABELS.claim, formUrl)}` : isGuest ? `SIGN-UP URL (one-time link - signs the user up and moves this form into their new account): ${mdLink(LINK_LABELS.claim, formUrl)}` : `DASHBOARD URL (sign in to edit and manage): ${mdLink(LINK_LABELS.edit, formUrl)}`
16547
16587
  );
16548
16588
  }
16549
16589
  if (ignoredNotes.length > 0) lines.push(``, ...ignoredNotes);
16550
16590
  const firstUrlLabel = isPreview ? "PREVIEW" : "FORM";
16551
- const secondUrlLabel = isPreview ? "CLAIM" : "DASHBOARD";
16591
+ const secondUrlLabel = isPreview ? "CLAIM" : isGuest ? "SIGN-UP" : "DASHBOARD";
16552
16592
  lines.push(
16553
16593
  ``,
16554
- isPreview ? `This form is a preview, not live. When the user says "publish it", call clipform_update_form (is_live: true); it prompts them to sign in. The PREVIEW URL never becomes public on its own.` : `The form is live and accessible at the FORM URL above. Edits and media attached AFTER this point are NOT visible to respondents until you re-send is_live: true via clipform_update_form - republishing is the expected final step of a build, never skip it after attaching media/narration.`,
16594
+ isPreview ? isGuest ? `This form is a preview draft, not live. Call clipform_update_form (is_live: true) to put it live - no sign-in is needed. The PREVIEW URL never becomes public on its own.` : `This form is a preview, not live. When the user says "publish it", call clipform_update_form (is_live: true); it prompts them to sign in. The PREVIEW URL never becomes public on its own.` : `The form is live and accessible at the FORM URL above. Edits and media attached AFTER this point are NOT visible to respondents until you re-send is_live: true via clipform_update_form - republishing is the expected final step of a build, never skip it after attaching media/narration.`,
16555
16595
  formUrl ? `The URLs above are exact values; constructed or guessed URLs return 404. Use the ${firstUrlLabel} URL and ${secondUrlLabel} URL as shown, keeping each link's label rather than pasting the bare URL.` : `The ${firstUrlLabel} URL above is an exact value; constructed or guessed URLs return 404. Keep the link's label rather than pasting the bare URL.`,
16556
16596
  `Pass form_id on follow-up tools (get_form, add_node, attach_node_media, etc.).`
16557
16597
  );
16598
+ if (guestLive) {
16599
+ const lockedKinds = lockedStepKinds(nodes);
16600
+ if (lockedKinds.length > 0) {
16601
+ lines.push(
16602
+ ``,
16603
+ formUrl ? `These parts of this form are switched off for now: ${joinKinds(lockedKinds)}. Tell the user they switch on after they sign up, and give them the SIGN-UP URL above: ${mdLink(LINK_LABELS.claim, formUrl)}` : `These parts of this form are switched off for now: ${joinKinds(lockedKinds)}. Tell the user they switch on after they sign up - they can say "sign in" and you will start that.`
16604
+ );
16605
+ }
16606
+ lines.push(
16607
+ ``,
16608
+ `Guest limits: this form takes up to ${ANON_MCP_RESPONSE_CAP} responses and goes offline after ${ANON_MCP_LIVE_TTL_DAYS} days unless the user signs up.`,
16609
+ `Offer once: "Already have a Clipform account? Say 'sign in' and I'll move this form into it." (when they say it, call clipform_whoami - it starts sign-in).`
16610
+ );
16611
+ }
16558
16612
  const anonNotice = isPreview ? anonPreviewNotice() : null;
16559
16613
  if (anonNotice) lines.push(``, anonNotice);
16560
16614
  if (planContext) {
@@ -17043,7 +17097,7 @@ function formatFormState(data) {
17043
17097
  if (urls.embed) lines.push(` Embed: ${urls.embed}`);
17044
17098
  if (urls.dynamic_link) lines.push(` Dynamic link: ${urls.dynamic_link}`);
17045
17099
  }
17046
- if (isPreviewViewerUrl(urls?.viewer)) lines.push(``, ANON_PREVIEW_ONLY_NOTICE);
17100
+ if (isPreviewViewerUrl(urls?.viewer)) lines.push(``, previewOnlyNotice());
17047
17101
  lines.push(``, `Nodes (in order):`);
17048
17102
  for (let i = 0; i < nodes.length; i++) {
17049
17103
  const q = nodes[i];
@@ -17126,11 +17180,6 @@ function registerGetFormTool(server) {
17126
17180
  }
17127
17181
 
17128
17182
  // src/tools/update-form.ts
17129
- if (MCP_ANON_PUBLISH_GATE.tool !== "clipform_update_form") {
17130
- throw new Error(
17131
- `update-form.ts registers "clipform_update_form" but MCP_ANON_PUBLISH_GATE.tool is "${MCP_ANON_PUBLISH_GATE.tool}" - update both sides together (packages/config/index.js and this file's registerTool call) or the Claude lazy-auth HTTP gate silently stops matching this tool.`
17132
- );
17133
- }
17134
17183
  function registerUpdateFormTool(server) {
17135
17184
  server.registerTool(
17136
17185
  "clipform_update_form",
@@ -17193,10 +17242,21 @@ function registerUpdateFormTool(server) {
17193
17242
  body
17194
17243
  });
17195
17244
  if (!result.ok) {
17196
- return isAnonymousAccountRequired(result) ? errorResult(
17197
- result.error,
17198
- authChallengeMeta("insufficient_scope", result.apiMessage ?? result.error)
17199
- ) : errorResult(result.error);
17245
+ if (isAnonymousAccountRequired(result)) {
17246
+ return errorResult(
17247
+ result.error,
17248
+ authChallengeMeta("insufficient_scope", result.apiMessage ?? result.error)
17249
+ );
17250
+ }
17251
+ if (is_live === true && isConnectorGuest() && (result.status === 403 || result.status === 503)) {
17252
+ const others = Object.keys(body).some((k) => k !== "is_live");
17253
+ return errorResult(
17254
+ `${result.error}
17255
+
17256
+ ${others ? "The other changes in this call were saved to the draft, but the form was not republished." : "The form was not republished."}`
17257
+ );
17258
+ }
17259
+ return errorResult(result.error);
17200
17260
  }
17201
17261
  }
17202
17262
  if (tags) {
@@ -20107,10 +20167,10 @@ var MCP_SERVER_CARD_IDENTITY = Object.freeze({
20107
20167
  icons: serverJson.icons,
20108
20168
  repository: serverJson.repository
20109
20169
  });
20110
- function buildInstructions(caps) {
20170
+ function buildInstructions(caps, signedIn) {
20111
20171
  return [
20112
20172
  INSTRUCTIONS_LEAD,
20113
- buildOnboardingOffer(),
20173
+ buildOnboardingOffer(signedIn === false ? ONBOARDING_OFFER_GUEST_VARIANT : void 0),
20114
20174
  "When the brief is clear, build the form directly; when it's vague, ask one or two short questions first.",
20115
20175
  caps.media && "Prefer rich media: video over images over plain text.",
20116
20176
  caps.media ? "Before building, derive a register from the topic and audience (e.g. hype, playful, warm, considered, elegant, sombre); if the user states a mood explicitly, that wins. State the register once, then apply it to every rendering choice - narration tone, motion preset, music, pacing. Never mix registers in one form." : "Before building, derive a register from the topic and audience (e.g. hype, playful, warm, considered, elegant, sombre); if the user states a mood explicitly, that wins. State the register once, then apply it to every question, option, and end-screen line. Never mix registers in one form.",
@@ -20139,7 +20199,7 @@ function createServer(options) {
20139
20199
  const caps = capsFromToolsets(options?.toolsets);
20140
20200
  const server = new McpServer(
20141
20201
  { ...MCP_SERVER_IDENTITY },
20142
- { instructions: buildInstructions(caps) }
20202
+ { instructions: buildInstructions(caps, options?.signedIn) }
20143
20203
  );
20144
20204
  const rawServer = server.server;
20145
20205
  const nativeSetRequestHandler = rawServer.setRequestHandler.bind(rawServer);
@@ -20180,7 +20240,7 @@ function createServer(options) {
20180
20240
  (...args) => runWithMcpTool(name, async () => {
20181
20241
  try {
20182
20242
  const result = await runWithWorkspaceTarget(args[0], () => handler(...args));
20183
- return withAdoptedNotice(result, args[0]);
20243
+ return withAdoptedNotice(result, args[0], name);
20184
20244
  } catch (err) {
20185
20245
  const error = err instanceof Error ? err : new Error(String(err));
20186
20246
  options?.onToolError?.(name, error);
@@ -20290,4 +20350,4 @@ export {
20290
20350
  * MIT Licensed
20291
20351
  *)
20292
20352
  */
20293
- //# sourceMappingURL=chunk-OSOUHA2R.js.map
20353
+ //# sourceMappingURL=chunk-HV5FRLYK.js.map