@clipform/mcp-server 1.16.0 → 1.17.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 +2 -1
- package/dist/auth-context.d.ts +1 -0
- package/dist/auth-context.js +1 -1
- package/dist/{chunk-VWGIVJMQ.js → chunk-HCZI2UJ5.js} +1 -1
- package/dist/chunk-HCZI2UJ5.js.map +1 -0
- package/dist/{chunk-D64XBDOX.js → chunk-Y22DYUS3.js} +310 -547
- package/dist/chunk-Y22DYUS3.js.map +1 -0
- package/dist/index.js +6 -14
- package/dist/index.js.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-D64XBDOX.js.map +0 -1
- package/dist/chunk-VWGIVJMQ.js.map +0 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
__toESM,
|
|
5
5
|
getMcpAuth,
|
|
6
6
|
runWithMcpTool
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HCZI2UJ5.js";
|
|
8
8
|
|
|
9
9
|
// ../../node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/codegen/code.js
|
|
10
10
|
var require_code = __commonJS({
|
|
@@ -21277,23 +21277,22 @@ var NODE_TYPES = {
|
|
|
21277
21277
|
is_active: true,
|
|
21278
21278
|
show_in_results: false,
|
|
21279
21279
|
default_config: {
|
|
21280
|
-
|
|
21281
|
-
|
|
21282
|
-
|
|
21283
|
-
|
|
21284
|
-
],
|
|
21285
|
-
prompt: ""
|
|
21286
|
-
},
|
|
21280
|
+
fields: [
|
|
21281
|
+
{ id: "first_name", type: "first_name", label: "First Name", enabled: true, required: true },
|
|
21282
|
+
{ id: "email", type: "email", label: "Email", enabled: true, required: true }
|
|
21283
|
+
],
|
|
21287
21284
|
consent_items: [
|
|
21288
21285
|
{ id: "default-consent", name: "Privacy policy", label: "I agree to the privacy policy and terms of service", order: 0, type: "consent" }
|
|
21289
21286
|
]
|
|
21290
21287
|
},
|
|
21291
21288
|
config_schema: {
|
|
21292
21289
|
type: "object",
|
|
21290
|
+
required: ["fields"],
|
|
21293
21291
|
properties: {
|
|
21294
21292
|
title: { type: "string" },
|
|
21295
21293
|
fields: {
|
|
21296
21294
|
type: "array",
|
|
21295
|
+
minItems: 1,
|
|
21297
21296
|
items: {
|
|
21298
21297
|
type: "object",
|
|
21299
21298
|
required: ["id", "required"],
|
|
@@ -22140,16 +22139,21 @@ var CONTACT_FIELDS = [
|
|
|
22140
22139
|
{ id: "first_name", label: "First Name", type: "text", placeholder: "Enter first name", order: 1 },
|
|
22141
22140
|
{ id: "last_name", label: "Last Name", type: "text", placeholder: "Enter last name", order: 2 },
|
|
22142
22141
|
{ id: "email", label: "Email Address", type: "email", placeholder: "you@example.com", order: 3 },
|
|
22143
|
-
{ id: "phone", label: "Phone Number", type: "tel", placeholder: "(555) 123-4567", order: 4 }
|
|
22144
|
-
{ id: "company", label: "Company", type: "text", placeholder: "Company name", order: 5 },
|
|
22145
|
-
{ id: "job_title", label: "Job Title", type: "text", placeholder: "Your role", order: 6 },
|
|
22146
|
-
{ id: "website", label: "Website", type: "url", placeholder: "https://example.com", order: 7 },
|
|
22147
|
-
{ id: "linkedin", label: "LinkedIn Profile", type: "url", placeholder: "https://linkedin.com/in/username", order: 8 },
|
|
22148
|
-
{ id: "message", label: "Message", type: "textarea", placeholder: "Your message...", order: 9 }
|
|
22142
|
+
{ id: "phone", label: "Phone Number", type: "tel", placeholder: "(555) 123-4567", order: 4 }
|
|
22149
22143
|
];
|
|
22150
22144
|
var CONTACT_FIELDS_MAP = Object.fromEntries(
|
|
22151
22145
|
CONTACT_FIELDS.map((f) => [f.id, f])
|
|
22152
22146
|
);
|
|
22147
|
+
var KEN_BURNS_PRESETS = [
|
|
22148
|
+
"cinematic",
|
|
22149
|
+
"dramatic",
|
|
22150
|
+
"calm",
|
|
22151
|
+
"documentary",
|
|
22152
|
+
"dreamy",
|
|
22153
|
+
"moody"
|
|
22154
|
+
];
|
|
22155
|
+
var KEN_BURNS_EASINGS = ["ease-in-out", "ease-in", "ease-out", "linear"];
|
|
22156
|
+
var KEN_BURNS_FIT_MODES = ["cover", "blur-pad", "auto"];
|
|
22153
22157
|
|
|
22154
22158
|
// src/lib/schemas.ts
|
|
22155
22159
|
var ACTIVE_NODE_TYPES = Object.entries(NODE_TYPES).filter(([, def]) => def.is_active && !def.is_system).map(([type]) => type);
|
|
@@ -22198,8 +22202,15 @@ function generateConfigSummary(configSchema, typeKey) {
|
|
|
22198
22202
|
}
|
|
22199
22203
|
return parts.length > 0 ? `Config: ${parts.join(", ")}` : "";
|
|
22200
22204
|
}
|
|
22205
|
+
function formatDefaultConfig(defaultConfig) {
|
|
22206
|
+
if (!defaultConfig) return "";
|
|
22207
|
+
const filtered = { ...defaultConfig };
|
|
22208
|
+
delete filtered.options;
|
|
22209
|
+
if (Object.keys(filtered).length === 0) return "";
|
|
22210
|
+
return JSON.stringify(filtered);
|
|
22211
|
+
}
|
|
22201
22212
|
var NODE_TYPES_DESCRIPTION = (() => {
|
|
22202
|
-
const lines = ["Node types:"];
|
|
22213
|
+
const lines = ["Node types (omit config to use defaults where shown):"];
|
|
22203
22214
|
for (const type of ACTIVE_NODE_TYPES) {
|
|
22204
22215
|
const def = NODE_TYPES[type];
|
|
22205
22216
|
let line = `- ${type}: ${def.description || def.label}`;
|
|
@@ -22210,17 +22221,26 @@ var NODE_TYPES_DESCRIPTION = (() => {
|
|
|
22210
22221
|
if (configHint) {
|
|
22211
22222
|
line += `. ${configHint}`;
|
|
22212
22223
|
}
|
|
22224
|
+
const defaultStr = formatDefaultConfig(def.default_config);
|
|
22225
|
+
if (defaultStr) {
|
|
22226
|
+
line += `. Defaults: ${defaultStr}`;
|
|
22227
|
+
}
|
|
22213
22228
|
lines.push(line);
|
|
22214
22229
|
}
|
|
22215
22230
|
return lines.join("\n");
|
|
22216
22231
|
})();
|
|
22217
22232
|
var CONFIG_DESCRIPTION = (() => {
|
|
22218
|
-
const lines = ["Type-specific configuration. Per-type keys:"];
|
|
22233
|
+
const lines = ["Type-specific configuration. Omit to use defaults. Per-type keys:"];
|
|
22219
22234
|
for (const type of ACTIVE_NODE_TYPES) {
|
|
22220
22235
|
const def = NODE_TYPES[type];
|
|
22221
22236
|
const summary = generateConfigSummary(def.config_schema, type);
|
|
22222
22237
|
if (summary) {
|
|
22223
|
-
|
|
22238
|
+
let line = ` ${type}: ${summary}`;
|
|
22239
|
+
const defaultStr = formatDefaultConfig(def.default_config);
|
|
22240
|
+
if (defaultStr) {
|
|
22241
|
+
line += `. Defaults: ${defaultStr}`;
|
|
22242
|
+
}
|
|
22243
|
+
lines.push(line);
|
|
22224
22244
|
}
|
|
22225
22245
|
}
|
|
22226
22246
|
return lines.join("\n");
|
|
@@ -22253,83 +22273,33 @@ var NodeSchema = external_exports.object({
|
|
|
22253
22273
|
});
|
|
22254
22274
|
|
|
22255
22275
|
// src/lib/api-client.ts
|
|
22256
|
-
var
|
|
22257
|
-
|
|
22258
|
-
|
|
22259
|
-
|
|
22260
|
-
|
|
22261
|
-
function setApiKey(key) {
|
|
22262
|
-
_apiKey = key;
|
|
22276
|
+
var API_BASE_URL = process.env.API_URL;
|
|
22277
|
+
if (!API_BASE_URL) {
|
|
22278
|
+
throw new Error(
|
|
22279
|
+
"API_URL must be set (e.g. http://localhost:3003 or https://api.clipform.io)"
|
|
22280
|
+
);
|
|
22263
22281
|
}
|
|
22264
|
-
|
|
22265
|
-
const
|
|
22266
|
-
|
|
22267
|
-
if (
|
|
22268
|
-
|
|
22269
|
-
url += `?${searchParams.toString()}`;
|
|
22270
|
-
}
|
|
22271
|
-
const headers = {
|
|
22272
|
-
"Content-Type": "application/json"
|
|
22273
|
-
};
|
|
22274
|
-
const mcpAuth = getMcpAuth();
|
|
22275
|
-
if (mcpAuth && INTERNAL_SECRET) {
|
|
22276
|
-
headers["Authorization"] = `Bearer ${INTERNAL_SECRET}`;
|
|
22277
|
-
headers["X-Mcp-User"] = mcpAuth.user_id;
|
|
22278
|
-
headers["X-Mcp-Workspace"] = mcpAuth.workspace_id;
|
|
22279
|
-
if (mcpAuth.tool_name) headers["X-Mcp-Tool"] = mcpAuth.tool_name;
|
|
22280
|
-
} else if (_apiKey) {
|
|
22281
|
-
headers["Authorization"] = `Bearer ${_apiKey}`;
|
|
22282
|
-
}
|
|
22283
|
-
const fetchOptions = { method, headers };
|
|
22284
|
-
if (body && method !== "GET") {
|
|
22285
|
-
fetchOptions.body = JSON.stringify(body);
|
|
22282
|
+
function getAuthHeaders() {
|
|
22283
|
+
const authCtx = getMcpAuth();
|
|
22284
|
+
const apiKey = authCtx?.api_key || process.env.CLIPFORM_API_KEY;
|
|
22285
|
+
if (!apiKey) {
|
|
22286
|
+
throw new Error("CLIPFORM_API_KEY must be set.");
|
|
22286
22287
|
}
|
|
22287
|
-
|
|
22288
|
-
try {
|
|
22289
|
-
response = await fetch(url, fetchOptions);
|
|
22290
|
-
} catch (err) {
|
|
22291
|
-
return {
|
|
22292
|
-
ok: false,
|
|
22293
|
-
status: 0,
|
|
22294
|
-
error: `Failed to connect to dashboard API at ${url}. Make sure the dashboard is running.
|
|
22295
|
-
|
|
22296
|
-
Error: ${err instanceof Error ? err.message : String(err)}`
|
|
22297
|
-
};
|
|
22298
|
-
}
|
|
22299
|
-
if (response.status === 204) {
|
|
22300
|
-
return { ok: true, data: {} };
|
|
22301
|
-
}
|
|
22302
|
-
const data = await response.json();
|
|
22303
|
-
if (!response.ok) {
|
|
22304
|
-
return {
|
|
22305
|
-
ok: false,
|
|
22306
|
-
status: response.status,
|
|
22307
|
-
error: data.error || `API error (${response.status})`
|
|
22308
|
-
};
|
|
22309
|
-
}
|
|
22310
|
-
return { ok: true, data };
|
|
22288
|
+
return { "Authorization": `Bearer ${apiKey}` };
|
|
22311
22289
|
}
|
|
22312
|
-
async function
|
|
22313
|
-
const {
|
|
22314
|
-
|
|
22290
|
+
async function callApi(path, options = {}) {
|
|
22291
|
+
const { body, params } = options;
|
|
22292
|
+
const method = options.method || (body ? "POST" : "GET");
|
|
22293
|
+
const prefix = path.startsWith("/internal/") ? "" : "/v1";
|
|
22294
|
+
let url = `${API_BASE_URL}${prefix}${path}`;
|
|
22315
22295
|
if (params) {
|
|
22316
22296
|
const searchParams = new URLSearchParams(params);
|
|
22317
22297
|
url += `?${searchParams.toString()}`;
|
|
22318
22298
|
}
|
|
22319
22299
|
const headers = {
|
|
22320
|
-
"Content-Type": "application/json"
|
|
22300
|
+
"Content-Type": "application/json",
|
|
22301
|
+
...getAuthHeaders()
|
|
22321
22302
|
};
|
|
22322
|
-
const mcpAuth = getMcpAuth();
|
|
22323
|
-
if (mcpAuth && INTERNAL_SECRET) {
|
|
22324
|
-
headers["Authorization"] = `Bearer ${INTERNAL_SECRET}`;
|
|
22325
|
-
headers["X-Mcp-User"] = mcpAuth.user_id;
|
|
22326
|
-
headers["X-Mcp-Workspace"] = mcpAuth.workspace_id;
|
|
22327
|
-
if (mcpAuth.tool_name) headers["X-Mcp-Tool"] = mcpAuth.tool_name;
|
|
22328
|
-
} else if (_apiKey) {
|
|
22329
|
-
headers["Authorization"] = `Bearer ${_apiKey}`;
|
|
22330
|
-
} else if (INTERNAL_SECRET) {
|
|
22331
|
-
headers["Authorization"] = `Bearer ${INTERNAL_SECRET}`;
|
|
22332
|
-
}
|
|
22333
22303
|
const fetchOptions = { method, headers };
|
|
22334
22304
|
if (body && method !== "GET") {
|
|
22335
22305
|
fetchOptions.body = JSON.stringify(body);
|
|
@@ -22341,7 +22311,7 @@ async function callInternalApi(path, options = {}) {
|
|
|
22341
22311
|
return {
|
|
22342
22312
|
ok: false,
|
|
22343
22313
|
status: 0,
|
|
22344
|
-
error: `Failed to connect to
|
|
22314
|
+
error: `Failed to connect to API at ${url}.
|
|
22345
22315
|
|
|
22346
22316
|
Error: ${err instanceof Error ? err.message : String(err)}`
|
|
22347
22317
|
};
|
|
@@ -22354,7 +22324,7 @@ Error: ${err instanceof Error ? err.message : String(err)}`
|
|
|
22354
22324
|
return {
|
|
22355
22325
|
ok: false,
|
|
22356
22326
|
status: response.status,
|
|
22357
|
-
error: data.error || `
|
|
22327
|
+
error: data.error || `API error (${response.status})`
|
|
22358
22328
|
};
|
|
22359
22329
|
}
|
|
22360
22330
|
return { ok: true, data };
|
|
@@ -22422,7 +22392,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
22422
22392
|
const workspaceId = me.workspace?.id ?? null;
|
|
22423
22393
|
if (!workspaceId) {
|
|
22424
22394
|
return errorResult(
|
|
22425
|
-
"No workspace available.
|
|
22395
|
+
"No workspace available. This usually means authentication is misconfigured - check your API key or OAuth connection."
|
|
22426
22396
|
);
|
|
22427
22397
|
}
|
|
22428
22398
|
planContext = {
|
|
@@ -22430,14 +22400,27 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
22430
22400
|
workspace_id: workspaceId,
|
|
22431
22401
|
workspace_name: me.workspace?.name ?? null,
|
|
22432
22402
|
plan_name: me.plan?.name ?? "Free",
|
|
22433
|
-
node_limit: me.plan?.node_limit ?? null
|
|
22403
|
+
node_limit: me.plan?.node_limit ?? null,
|
|
22404
|
+
form_limit: me.plan?.form_limit ?? null
|
|
22434
22405
|
};
|
|
22435
|
-
const contentCount = nodes.filter((q) => q.type
|
|
22406
|
+
const contentCount = nodes.filter((q) => !NON_COUNTABLE_TYPES.includes(q.type)).length;
|
|
22436
22407
|
if (planContext.node_limit !== null && contentCount > planContext.node_limit) {
|
|
22437
22408
|
const upgradeUrl = `${BUSINESS.urls.dashboard}/billing`;
|
|
22438
|
-
const message = planContext.auth_mode === "oauth" ? `Your '${planContext.workspace_name}' workspace is on the ${planContext.plan_name} plan, capped at ${planContext.node_limit} nodes per form. You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes or upgrade at ${upgradeUrl}.` : `Anonymous sessions are capped at ${planContext.node_limit} nodes per form (${planContext.plan_name} plan). You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes, or
|
|
22409
|
+
const message = planContext.auth_mode === "oauth" ? `Your '${planContext.workspace_name}' workspace is on the ${planContext.plan_name} plan, capped at ${planContext.node_limit} nodes per form. You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes or upgrade at ${upgradeUrl}.` : `Anonymous sessions are capped at ${planContext.node_limit} nodes per form (${planContext.plan_name} plan). You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes, or sign in to your Clipform account so forms land in your workspace with your real plan limits.`;
|
|
22439
22410
|
return errorResult(message);
|
|
22440
22411
|
}
|
|
22412
|
+
if (planContext.form_limit !== null) {
|
|
22413
|
+
const formsResult = await callApi("/forms", { method: "GET" });
|
|
22414
|
+
if (formsResult.ok) {
|
|
22415
|
+
const formCount = Array.isArray(formsResult.data) ? formsResult.data.length : 0;
|
|
22416
|
+
if (formCount >= planContext.form_limit) {
|
|
22417
|
+
const upgradeUrl = `${BUSINESS.urls.dashboard}/billing`;
|
|
22418
|
+
return errorResult(
|
|
22419
|
+
`Your workspace has reached the ${planContext.plan_name} plan limit of ${planContext.form_limit} forms. Upgrade at ${upgradeUrl} for unlimited forms.`
|
|
22420
|
+
);
|
|
22421
|
+
}
|
|
22422
|
+
}
|
|
22423
|
+
}
|
|
22441
22424
|
const createResult = await callApi("/forms", {
|
|
22442
22425
|
method: "POST",
|
|
22443
22426
|
body: { title, workspace_id: workspaceId }
|
|
@@ -22462,6 +22445,11 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
22462
22445
|
});
|
|
22463
22446
|
}
|
|
22464
22447
|
for (const q of nodes) {
|
|
22448
|
+
if (q.type === "button" && (!q.options || q.options.length === 0)) {
|
|
22449
|
+
const buttonDefault = NODE_TYPES.button?.config_schema?.properties?.button_text?.default || "Continue";
|
|
22450
|
+
const label = q.config?.button_text || buttonDefault;
|
|
22451
|
+
q.options = [{ content: label }];
|
|
22452
|
+
}
|
|
22465
22453
|
const addResult = await callApi(`/forms/${formId}/nodes`, {
|
|
22466
22454
|
method: "POST",
|
|
22467
22455
|
body: { node: q }
|
|
@@ -22470,10 +22458,14 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
22470
22458
|
return errorResult(`Failed to add node "${q.prompt}": ${addResult.error}`);
|
|
22471
22459
|
}
|
|
22472
22460
|
}
|
|
22473
|
-
await callApi(`/forms/${formId}`, {
|
|
22461
|
+
const publishResult = await callApi(`/forms/${formId}`, {
|
|
22474
22462
|
method: "PATCH",
|
|
22475
22463
|
body: { is_live: true }
|
|
22476
22464
|
});
|
|
22465
|
+
if (!publishResult.ok) {
|
|
22466
|
+
return errorResult(`Form created but failed to publish: ${publishResult.error}. Form ID: ${formId}
|
|
22467
|
+
FORM URL: ${data.viewer_url}`);
|
|
22468
|
+
}
|
|
22477
22469
|
if (tags && tags.length > 0) {
|
|
22478
22470
|
await callApi(`/forms/${formId}/tags`, {
|
|
22479
22471
|
method: "PUT",
|
|
@@ -22494,11 +22486,13 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
22494
22486
|
}
|
|
22495
22487
|
lines.push(
|
|
22496
22488
|
``,
|
|
22497
|
-
`
|
|
22489
|
+
`CRITICAL: Copy the URLs above character-for-character into your response. Do NOT construct, guess, or invent any URLs. Only use the exact FORM URL and CLAIM URL provided here.`,
|
|
22498
22490
|
`Pass form_id on follow-up tools (get_form, add_node, upload_node_media, etc.).`
|
|
22499
22491
|
);
|
|
22500
22492
|
if (planContext) {
|
|
22501
|
-
const
|
|
22493
|
+
const nodePart = planContext.node_limit === null ? "unlimited nodes" : `up to ${planContext.node_limit} nodes per form`;
|
|
22494
|
+
const formPart = planContext.form_limit === null ? "unlimited forms" : `up to ${planContext.form_limit} forms`;
|
|
22495
|
+
const limitNote = `${formPart}, ${nodePart}`;
|
|
22502
22496
|
if (planContext.auth_mode === "oauth") {
|
|
22503
22497
|
lines.push(
|
|
22504
22498
|
``,
|
|
@@ -22507,7 +22501,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
22507
22501
|
} else {
|
|
22508
22502
|
lines.push(
|
|
22509
22503
|
``,
|
|
22510
|
-
`Plan: anonymous ${planContext.plan_name} (${limitNote}). Mention this once: the user can
|
|
22504
|
+
`Plan: anonymous ${planContext.plan_name} (${limitNote}). Mention this once: the user can sign in to their Clipform account so future forms land directly in their workspace - do not repeat on follow-up calls.`
|
|
22511
22505
|
);
|
|
22512
22506
|
}
|
|
22513
22507
|
}
|
|
@@ -22779,11 +22773,7 @@ function registerAddNodeTool(server) {
|
|
|
22779
22773
|
"clipform_add_node",
|
|
22780
22774
|
{
|
|
22781
22775
|
title: "Add Node",
|
|
22782
|
-
description: `Add a new node to an existing form.
|
|
22783
|
-
|
|
22784
|
-
${NODE_TYPES_DESCRIPTION}
|
|
22785
|
-
|
|
22786
|
-
All type definitions and config schemas are derived from @vid-master/config (answer-types).`,
|
|
22776
|
+
description: `Add a new node to an existing form. Inserted before the end screen by default. Use after_node_id to insert at a specific position. Node types and config schemas match clipform_create_form.`,
|
|
22787
22777
|
inputSchema: {
|
|
22788
22778
|
form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
|
|
22789
22779
|
node: NodeSchema.describe("The node to add"),
|
|
@@ -22932,6 +22922,7 @@ ${formState}` : confirmMsg);
|
|
|
22932
22922
|
}
|
|
22933
22923
|
|
|
22934
22924
|
// src/tools/upload-node-media.ts
|
|
22925
|
+
var MEDIA_SUPPORTED_TYPES = Object.entries(NODE_TYPES).filter(([, def]) => def.supports_media).map(([type]) => type);
|
|
22935
22926
|
var MediaItemSchema = external_exports.object({
|
|
22936
22927
|
node_id: external_exports.string().describe("The node ID"),
|
|
22937
22928
|
media_type: external_exports.enum(["video", "still"]).describe("Type of media"),
|
|
@@ -22960,7 +22951,7 @@ function registerUploadNodeMediaTool(server) {
|
|
|
22960
22951
|
title: "Upload Node Media",
|
|
22961
22952
|
description: `Upload media for one or more nodes. Pass one item or many (max 10). Multiple items upload sequentially.
|
|
22962
22953
|
|
|
22963
|
-
When a public URL is provided, the media is fetched and stored automatically. Only works on node types that support media (
|
|
22954
|
+
When a public URL is provided, the media is fetched and stored automatically. Only works on node types that support media (${MEDIA_SUPPORTED_TYPES.join(", ")}). For video: ingested via Mux. For image: stored in Supabase. When attaching TTS narration video, always include captions from clipform_generate_tts.`,
|
|
22964
22955
|
inputSchema: {
|
|
22965
22956
|
form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
|
|
22966
22957
|
items: external_exports.array(MediaItemSchema).min(1).max(10).describe("One or more media items to upload")
|
|
@@ -23149,44 +23140,6 @@ ${formState}` : confirmMsg);
|
|
|
23149
23140
|
);
|
|
23150
23141
|
}
|
|
23151
23142
|
|
|
23152
|
-
// src/tools/attach-node-audio.ts
|
|
23153
|
-
function registerAttachNodeAudioTool(server) {
|
|
23154
|
-
server.registerTool(
|
|
23155
|
-
"clipform_attach_audio",
|
|
23156
|
-
{
|
|
23157
|
-
title: "Attach Audio to Node",
|
|
23158
|
-
description: `Attach a sound effect or audio file to a node with existing media (stills only). The audio auto-plays once when a respondent reaches this node. Provide a public URL to the audio file (WAV, MP3, or OGG). The node must already have media uploaded (use clipform_upload_node_media first).`,
|
|
23159
|
-
inputSchema: {
|
|
23160
|
-
form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
|
|
23161
|
-
node_id: external_exports.string().describe("The node ID (must already have media attached)"),
|
|
23162
|
-
url: external_exports.string().url().describe("Public URL to the audio file (WAV, MP3, or OGG)")
|
|
23163
|
-
},
|
|
23164
|
-
annotations: {
|
|
23165
|
-
readOnlyHint: false,
|
|
23166
|
-
destructiveHint: false,
|
|
23167
|
-
idempotentHint: true,
|
|
23168
|
-
openWorldHint: true
|
|
23169
|
-
}
|
|
23170
|
-
},
|
|
23171
|
-
async ({ form_id, node_id, url }) => {
|
|
23172
|
-
const result = await callApi(
|
|
23173
|
-
`/forms/${form_id}/nodes/${node_id}/audio`,
|
|
23174
|
-
{
|
|
23175
|
-
method: "PUT",
|
|
23176
|
-
body: { url }
|
|
23177
|
-
}
|
|
23178
|
-
);
|
|
23179
|
-
if (!result.ok) {
|
|
23180
|
-
return errorResult(result.error);
|
|
23181
|
-
}
|
|
23182
|
-
return textResult(
|
|
23183
|
-
`Audio attached to node ${node_id}.
|
|
23184
|
-
Audio path: ${result.data.audio_storage_path}`
|
|
23185
|
-
);
|
|
23186
|
-
}
|
|
23187
|
-
);
|
|
23188
|
-
}
|
|
23189
|
-
|
|
23190
23143
|
// src/tools/log-generation.ts
|
|
23191
23144
|
function registerLogGenerationTool(server) {
|
|
23192
23145
|
server.registerTool(
|
|
@@ -23213,7 +23166,7 @@ function registerLogGenerationTool(server) {
|
|
|
23213
23166
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
23214
23167
|
},
|
|
23215
23168
|
async ({ form_id, summary, details }) => {
|
|
23216
|
-
const result = await
|
|
23169
|
+
const result = await callApi("/internal/log-generation", {
|
|
23217
23170
|
body: { form_id, summary, details }
|
|
23218
23171
|
});
|
|
23219
23172
|
if (!result.ok) return errorResult(result.error);
|
|
@@ -23246,7 +23199,7 @@ If neither native web search nor this tool is available and the topic is post-cu
|
|
|
23246
23199
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
23247
23200
|
},
|
|
23248
23201
|
async ({ query, count }) => {
|
|
23249
|
-
const result = await
|
|
23202
|
+
const result = await callApi("/internal/search-news", {
|
|
23250
23203
|
body: { query, count }
|
|
23251
23204
|
});
|
|
23252
23205
|
if (!result.ok) return errorResult(result.error);
|
|
@@ -23305,7 +23258,7 @@ DOES NOT WORK FOR:
|
|
|
23305
23258
|
}
|
|
23306
23259
|
},
|
|
23307
23260
|
async ({ url, lang, max_chars }) => {
|
|
23308
|
-
const result = await
|
|
23261
|
+
const result = await callApi("/internal/youtube-transcript", {
|
|
23309
23262
|
body: { url, lang, max_chars }
|
|
23310
23263
|
});
|
|
23311
23264
|
if (!result.ok) return errorResult(result.error);
|
|
@@ -23354,10 +23307,10 @@ Returns audio URL and word-level captions per item.`,
|
|
|
23354
23307
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
23355
23308
|
},
|
|
23356
23309
|
async ({ items }) => {
|
|
23357
|
-
const workspace_id =
|
|
23310
|
+
const workspace_id = getMcpAuth()?.workspace_id;
|
|
23358
23311
|
const results = await Promise.allSettled(
|
|
23359
23312
|
items.map(
|
|
23360
|
-
(item) =>
|
|
23313
|
+
(item) => callApi("/internal/tts", {
|
|
23361
23314
|
body: { text: item.text, voice: item.voice, workspace_id }
|
|
23362
23315
|
})
|
|
23363
23316
|
)
|
|
@@ -23395,101 +23348,9 @@ Returns audio URL and word-level captions per item.`,
|
|
|
23395
23348
|
);
|
|
23396
23349
|
}
|
|
23397
23350
|
|
|
23398
|
-
// ../../node_modules/uuid/dist/esm/stringify.js
|
|
23399
|
-
var byteToHex = [];
|
|
23400
|
-
for (let i = 0; i < 256; ++i) {
|
|
23401
|
-
byteToHex.push((i + 256).toString(16).slice(1));
|
|
23402
|
-
}
|
|
23403
|
-
function unsafeStringify(arr, offset = 0) {
|
|
23404
|
-
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
23405
|
-
}
|
|
23406
|
-
|
|
23407
|
-
// ../../node_modules/uuid/dist/esm/rng.js
|
|
23408
|
-
import { randomFillSync } from "crypto";
|
|
23409
|
-
var rnds8Pool = new Uint8Array(256);
|
|
23410
|
-
var poolPtr = rnds8Pool.length;
|
|
23411
|
-
function rng() {
|
|
23412
|
-
if (poolPtr > rnds8Pool.length - 16) {
|
|
23413
|
-
randomFillSync(rnds8Pool);
|
|
23414
|
-
poolPtr = 0;
|
|
23415
|
-
}
|
|
23416
|
-
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
23417
|
-
}
|
|
23418
|
-
|
|
23419
|
-
// ../../node_modules/uuid/dist/esm/native.js
|
|
23420
|
-
import { randomUUID } from "crypto";
|
|
23421
|
-
var native_default = { randomUUID };
|
|
23422
|
-
|
|
23423
|
-
// ../../node_modules/uuid/dist/esm/v4.js
|
|
23424
|
-
function v4(options, buf, offset) {
|
|
23425
|
-
if (native_default.randomUUID && !buf && !options) {
|
|
23426
|
-
return native_default.randomUUID();
|
|
23427
|
-
}
|
|
23428
|
-
options = options || {};
|
|
23429
|
-
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
23430
|
-
if (rnds.length < 16) {
|
|
23431
|
-
throw new Error("Random bytes length must be >= 16");
|
|
23432
|
-
}
|
|
23433
|
-
rnds[6] = rnds[6] & 15 | 64;
|
|
23434
|
-
rnds[8] = rnds[8] & 63 | 128;
|
|
23435
|
-
if (buf) {
|
|
23436
|
-
offset = offset || 0;
|
|
23437
|
-
if (offset < 0 || offset + 16 > buf.length) {
|
|
23438
|
-
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
23439
|
-
}
|
|
23440
|
-
for (let i = 0; i < 16; ++i) {
|
|
23441
|
-
buf[offset + i] = rnds[i];
|
|
23442
|
-
}
|
|
23443
|
-
return buf;
|
|
23444
|
-
}
|
|
23445
|
-
return unsafeStringify(rnds);
|
|
23446
|
-
}
|
|
23447
|
-
var v4_default = v4;
|
|
23448
|
-
|
|
23449
|
-
// src/lib/render-jobs.ts
|
|
23450
|
-
var jobs = /* @__PURE__ */ new Map();
|
|
23451
|
-
var MAX_AGE_MS = 30 * 60 * 1e3;
|
|
23452
|
-
var RENDER_TIMING = {
|
|
23453
|
-
expectedRange: "15-120 seconds",
|
|
23454
|
-
pollDelay: "~15 seconds"
|
|
23455
|
-
};
|
|
23456
|
-
function createJob(tool) {
|
|
23457
|
-
const job = {
|
|
23458
|
-
id: v4_default(),
|
|
23459
|
-
status: "rendering",
|
|
23460
|
-
tool,
|
|
23461
|
-
createdAt: Date.now()
|
|
23462
|
-
};
|
|
23463
|
-
jobs.set(job.id, job);
|
|
23464
|
-
return job;
|
|
23465
|
-
}
|
|
23466
|
-
function completeJob(id, result) {
|
|
23467
|
-
const job = jobs.get(id);
|
|
23468
|
-
if (job) {
|
|
23469
|
-
job.status = "complete";
|
|
23470
|
-
job.result = result;
|
|
23471
|
-
}
|
|
23472
|
-
}
|
|
23473
|
-
function failJob(id, error2) {
|
|
23474
|
-
const job = jobs.get(id);
|
|
23475
|
-
if (job) {
|
|
23476
|
-
job.status = "failed";
|
|
23477
|
-
job.error = error2;
|
|
23478
|
-
}
|
|
23479
|
-
}
|
|
23480
|
-
function getJob(id) {
|
|
23481
|
-
return jobs.get(id);
|
|
23482
|
-
}
|
|
23483
|
-
function pruneJobs() {
|
|
23484
|
-
const cutoff = Date.now() - MAX_AGE_MS;
|
|
23485
|
-
for (const [id, job] of jobs) {
|
|
23486
|
-
if (job.createdAt < cutoff) jobs.delete(id);
|
|
23487
|
-
}
|
|
23488
|
-
}
|
|
23489
|
-
|
|
23490
23351
|
// src/tools/generate-slideshow.ts
|
|
23491
23352
|
var slideshowStyleSchema = external_exports.object({
|
|
23492
|
-
preset: external_exports.enum(
|
|
23353
|
+
preset: external_exports.enum(KEN_BURNS_PRESETS).optional().describe("Style preset: cinematic (default), dramatic, calm, documentary, dreamy, moody."),
|
|
23493
23354
|
zoom: external_exports.object({
|
|
23494
23355
|
from: external_exports.number().optional().describe("Starting scale for zoom-in / ending scale for zoom-out. Default 1.0."),
|
|
23495
23356
|
to: external_exports.number().optional().describe("Ending scale for zoom-in / starting scale for zoom-out. Default 1.3 (cover) / 1.15 (blur-pad).")
|
|
@@ -23503,7 +23364,7 @@ var slideshowStyleSchema = external_exports.object({
|
|
|
23503
23364
|
to: external_exports.number().optional(),
|
|
23504
23365
|
rangeFraction: external_exports.number().optional().describe("Multiplier on pan.range for zoom-in-pan-* effects. Default 0.5.")
|
|
23505
23366
|
}).optional(),
|
|
23506
|
-
easing: external_exports.enum(
|
|
23367
|
+
easing: external_exports.enum(KEN_BURNS_EASINGS).optional().describe("Motion curve. 'ease-in-out' = cinematic default. 'linear' = mechanical/deliberate. 'ease-out' = fast-start slow-finish, good for reveals."),
|
|
23507
23368
|
blurPad: external_exports.object({
|
|
23508
23369
|
blurPx: external_exports.number().optional().describe("Blur strength on the background layer. Default 40. 60+ for heavy dreamy effect, 20 for just-barely."),
|
|
23509
23370
|
brightness: external_exports.number().optional().describe("Background brightness multiplier. Default 0.55 (dimmed). Raise to 0.8+ for a brighter bed; lower for moodier."),
|
|
@@ -23526,75 +23387,20 @@ function registerGenerateSlideshowTool(server) {
|
|
|
23526
23387
|
"clipform_generate_slideshow",
|
|
23527
23388
|
{
|
|
23528
23389
|
title: "Generate Slideshow",
|
|
23529
|
-
description:
|
|
23530
|
-
|
|
23531
|
-
Generate a slideshow video from images and audio. Creates a 9:16 (720x1280) video with smooth pan/zoom effects (Ken Burns style), eased motion, and crossfade transitions, synced to the audio duration. Uploaded to storage; returns a public URL.
|
|
23532
|
-
|
|
23533
|
-
You are the director. Every stylistic choice below is yours - defaults exist for convenience but override anything that fits your creative vision.
|
|
23534
|
-
|
|
23535
|
-
## Workflow
|
|
23536
|
-
|
|
23537
|
-
1. Source images (use clipform_search_media with kind: "image"). Prefer portrait sources for 9:16 output; landscape works too via blur-pad fallback.
|
|
23538
|
-
2. Produce narration audio (clipform_generate_tts).
|
|
23539
|
-
3. Call this tool with images + audio_url + your creative direction.
|
|
23540
|
-
4. Attach the returned public URL to a node via clipform_upload_media with media_type "video".
|
|
23541
|
-
|
|
23542
|
-
## Image framing (per-image)
|
|
23543
|
-
|
|
23544
|
-
- **aspect_ratio** (width/height): pass through from the image source so the composition can auto-pick framing. Landscape images with no aspect_ratio will cover-crop and may pixelate.
|
|
23545
|
-
- **fit**: 'cover' (fill frame, crop), 'blur-pad' (sharp contained foreground + blurred cover background for landscape sources), 'auto' (default: cover for portrait, blur-pad for landscape > 1.1:1).
|
|
23546
|
-
- **style**: per-image creative overrides (see Style Knobs below).
|
|
23547
|
-
|
|
23548
|
-
## Effects
|
|
23549
|
-
|
|
23550
|
-
zoom-in, zoom-out, pan-left, pan-right, pan-up, pan-down, zoom-in-pan-left, zoom-in-pan-right, random, static. Set 'random' or pass random_effects: true to shuffle per image.
|
|
23551
|
-
|
|
23552
|
-
## Transitions
|
|
23553
|
-
|
|
23554
|
-
fade (default), slide, wipe. Legacy names (fadeblack, slideleft, etc.) also work. duration is in seconds.
|
|
23555
|
-
|
|
23556
|
-
## Style presets (fastest path)
|
|
23390
|
+
description: `Deprecated: prefer clipform_generate_video which supports both images and video clips.
|
|
23557
23391
|
|
|
23558
|
-
|
|
23392
|
+
Generate a Ken Burns slideshow video (9:16, 720x1280) from images + audio. Blocks until complete. Returns a public URL.
|
|
23559
23393
|
|
|
23560
|
-
|
|
23561
|
-
- **dramatic** - zoom 1.0\u21921.4, ease-out, wider pan (0.07), strong vignette. Good for big-reveal content, competitions, climactic moments.
|
|
23562
|
-
- **calm** - zoom 1.0\u21921.08, linear easing, no vignette. Reflective topics, wellness, educational explainers.
|
|
23563
|
-
- **documentary** - near-static (zoom 1.0\u21921.05, tiny pan, no vignette). Talking-heads-style context shots, historical photos.
|
|
23564
|
-
- **dreamy** - heavy blur-pad (blurPx 60, brightness 0.75), soft vignette. Aspirational, romantic, nostalgic.
|
|
23565
|
-
- **moody** - desaturated blur-pad (brightness 0.35, saturate 0.7), strong vignette, dark wrapper color. Crime, mystery, somber topics.
|
|
23394
|
+
Workflow: 1) clipform_search_media (kind: "image") 2) clipform_generate_tts for narration 3) this tool 4) clipform_upload_node_media with media_type "video".
|
|
23566
23395
|
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
## Style Knobs (per-image via images[i].style, or global via default_style)
|
|
23570
|
-
|
|
23571
|
-
- **zoom**: { from, to } - push-in/pull-out range. Cinematic default 1.0\u21921.3. Drop to 1.0\u21921.05 for near-static; push to 1.0\u21921.4 for dramatic.
|
|
23572
|
-
- **pan**: { range, scale } - drift magnitude. 0.03 subtle, 0.05 default, 0.08+ sweeping.
|
|
23573
|
-
- **easing**: 'ease-in-out' (default, cinematic) | 'ease-in' (accelerate) | 'ease-out' (decelerate, good for reveals) | 'linear' (mechanical).
|
|
23574
|
-
- **blurPad**: { blurPx, brightness, saturate, overscale, foregroundScale } - only applies when blur-pad fit is active. Heavier blurPx (60) + lower brightness (0.35) for moody; foregroundScale: 1.2 to reduce letterbox at the cost of mild crop.
|
|
23575
|
-
- **vignette**: false to disable, or { opacity, innerRadiusPercent }. Stronger vignette (opacity 0.6) for dramatic focus; disable for bright, flat looks.
|
|
23576
|
-
- **backgroundColor**: hex color shown behind the image frame (default '#000').
|
|
23577
|
-
- **autoBlurPadThreshold**: when fit='auto', images wider than this ratio get blur-pad. Default 1.1.
|
|
23578
|
-
|
|
23579
|
-
## Global options
|
|
23580
|
-
|
|
23581
|
-
- **default_style**: style applied to every image unless the image overrides per-field. Per-image style merges over default_style at the field level.
|
|
23582
|
-
- **background_color**: viewport wrapper color, visible during cross-fades.
|
|
23583
|
-
- **random_effects**: shuffles effects across images when true.
|
|
23584
|
-
|
|
23585
|
-
## Creative guidance
|
|
23586
|
-
|
|
23587
|
-
- For the same 5-image quiz slideshow, vary the effects (one zoom-in on the opener, pan-left on a cityscape, zoom-out on the closer) rather than random_effects: true, unless you want the dice-roll.
|
|
23588
|
-
- Quiet/reflective topics: slow easing ('linear'), tight zoom (1.0\u21921.08), no vignette.
|
|
23589
|
-
- Energetic topics: punchy zoom (1.0\u21921.35), ease-out, wider pan.
|
|
23590
|
-
- Mixed orientation sets: rely on fit: 'auto' + pass aspect_ratio per image. The composition will cover-crop portraits and blur-pad landscapes without more work.`,
|
|
23396
|
+
Style presets via default_style.preset: cinematic (default), dramatic, calm, documentary, dreamy, moody. Per-image overrides merge on top. See parameter descriptions for details.`,
|
|
23591
23397
|
inputSchema: {
|
|
23592
23398
|
images: external_exports.array(
|
|
23593
23399
|
external_exports.object({
|
|
23594
23400
|
url: external_exports.string().url().describe("Image URL"),
|
|
23595
23401
|
effect: external_exports.string().optional().describe("Ken Burns effect name (see description). Pass 'random' to shuffle this image only."),
|
|
23596
23402
|
aspect_ratio: external_exports.number().positive().optional().describe("Source image width / height. Enables auto blur-pad for landscape images in the 9:16 viewport."),
|
|
23597
|
-
fit: external_exports.enum(
|
|
23403
|
+
fit: external_exports.enum(KEN_BURNS_FIT_MODES).optional().describe("Framing mode. 'auto' (default) picks cover for portrait, blur-pad for landscape."),
|
|
23598
23404
|
style: slideshowStyleSchema.optional()
|
|
23599
23405
|
})
|
|
23600
23406
|
).min(1).max(20).describe("Images for the slideshow (1-20)"),
|
|
@@ -23610,9 +23416,8 @@ Per-image preset wins over default_style preset. Per-field overrides (zoom, pan,
|
|
|
23610
23416
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
23611
23417
|
},
|
|
23612
23418
|
async ({ images, audio_url, random_effects, transition, default_style, background_color }) => {
|
|
23613
|
-
const workspace_id =
|
|
23614
|
-
const
|
|
23615
|
-
callInternalApi("/internal/slideshow", {
|
|
23419
|
+
const workspace_id = getMcpAuth()?.workspace_id;
|
|
23420
|
+
const result = await callApi("/internal/slideshow", {
|
|
23616
23421
|
body: {
|
|
23617
23422
|
images,
|
|
23618
23423
|
audio_url,
|
|
@@ -23622,23 +23427,19 @@ Per-image preset wins over default_style preset. Per-field overrides (zoom, pan,
|
|
|
23622
23427
|
background_color,
|
|
23623
23428
|
workspace_id
|
|
23624
23429
|
}
|
|
23625
|
-
}).then((result) => {
|
|
23626
|
-
if (!result.ok) {
|
|
23627
|
-
failJob(job.id, result.error);
|
|
23628
|
-
} else {
|
|
23629
|
-
completeJob(job.id, result.data);
|
|
23630
|
-
}
|
|
23631
|
-
}).catch((err) => {
|
|
23632
|
-
failJob(job.id, err instanceof Error ? err.message : String(err));
|
|
23633
23430
|
});
|
|
23431
|
+
if (!result.ok) return errorResult(result.error);
|
|
23432
|
+
const data = result.data;
|
|
23634
23433
|
return textResult(
|
|
23635
23434
|
[
|
|
23636
|
-
`Slideshow
|
|
23435
|
+
`Slideshow rendered (${images.length} image${images.length > 1 ? "s" : ""}).`,
|
|
23637
23436
|
``,
|
|
23638
|
-
`
|
|
23437
|
+
`Public URL: ${data.public_url}`,
|
|
23438
|
+
`Storage path: ${data.storage_path}`,
|
|
23439
|
+
data.duration_seconds ? `Duration: ${data.duration_seconds}s` : "",
|
|
23639
23440
|
``,
|
|
23640
|
-
`
|
|
23641
|
-
].join("\n")
|
|
23441
|
+
`Use clipform_upload_node_media with the public URL to attach to a node.`
|
|
23442
|
+
].filter(Boolean).join("\n")
|
|
23642
23443
|
);
|
|
23643
23444
|
}
|
|
23644
23445
|
);
|
|
@@ -23659,7 +23460,7 @@ function registerSearchMediaTool(server) {
|
|
|
23659
23460
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
23660
23461
|
},
|
|
23661
23462
|
async ({ query, kind, count }) => {
|
|
23662
|
-
const result = await
|
|
23463
|
+
const result = await callApi("/internal/search-media", {
|
|
23663
23464
|
body: { query, kind, count }
|
|
23664
23465
|
});
|
|
23665
23466
|
if (!result.ok) return errorResult(result.error);
|
|
@@ -23687,46 +23488,40 @@ function registerRenderCompositionTool(server) {
|
|
|
23687
23488
|
"clipform_render_composition",
|
|
23688
23489
|
{
|
|
23689
23490
|
title: "Render Composition",
|
|
23690
|
-
description: `Render a video composition to MP4 or PNG.
|
|
23491
|
+
description: `Render a video composition to MP4 or PNG. Blocks until complete and returns the public URL.
|
|
23691
23492
|
|
|
23692
23493
|
Output formats:
|
|
23693
23494
|
- mp4: Video file (H.264 codec, correct BT.709 colors, best for social media)
|
|
23694
23495
|
- png: Still image (single frame)
|
|
23695
23496
|
|
|
23696
|
-
For narrated quiz slideshows, prefer
|
|
23497
|
+
For narrated quiz slideshows, prefer clipform_generate_video which handles Ken Burns effects, audio sync, and storage upload. Use this tool for custom compositions like GlobeToCity, ScorecardQuiz, ShortFormQuiz, or PresenterDirected.`,
|
|
23697
23498
|
inputSchema: {
|
|
23698
|
-
compositionId: external_exports.string().describe("The composition ID (e.g. '
|
|
23499
|
+
compositionId: external_exports.string().describe("The composition ID (e.g. 'GlobeToCity', 'ScorecardQuiz', 'ShortFormQuiz')"),
|
|
23699
23500
|
outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
|
|
23700
23501
|
inputProps: external_exports.record(external_exports.unknown()).optional().describe("Props object matching the composition's expected schema")
|
|
23701
23502
|
},
|
|
23702
23503
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
23703
23504
|
},
|
|
23704
23505
|
async ({ compositionId, outputFormat, inputProps }) => {
|
|
23705
|
-
const
|
|
23706
|
-
callInternalApi("/internal/render", {
|
|
23506
|
+
const result = await callApi("/internal/render", {
|
|
23707
23507
|
body: {
|
|
23708
23508
|
compositionId,
|
|
23709
23509
|
outputFormat,
|
|
23710
23510
|
inputProps: inputProps ?? {}
|
|
23711
23511
|
}
|
|
23712
|
-
}).then((result) => {
|
|
23713
|
-
if (!result.ok) {
|
|
23714
|
-
failJob(job.id, result.error);
|
|
23715
|
-
} else {
|
|
23716
|
-
completeJob(job.id, result.data);
|
|
23717
|
-
}
|
|
23718
|
-
}).catch((err) => {
|
|
23719
|
-
failJob(job.id, err instanceof Error ? err.message : String(err));
|
|
23720
23512
|
});
|
|
23513
|
+
if (!result.ok) return errorResult(result.error);
|
|
23514
|
+
const data = result.data;
|
|
23721
23515
|
return textResult(
|
|
23722
23516
|
[
|
|
23723
|
-
`Render
|
|
23517
|
+
`Render complete.`,
|
|
23724
23518
|
``,
|
|
23725
23519
|
`Composition: ${compositionId}`,
|
|
23726
|
-
`Format: ${outputFormat}`,
|
|
23727
|
-
`
|
|
23520
|
+
`Format: ${data.format || outputFormat}`,
|
|
23521
|
+
`Public URL: ${data.public_url}`,
|
|
23522
|
+
`Storage path: ${data.storage_path}`,
|
|
23728
23523
|
``,
|
|
23729
|
-
`
|
|
23524
|
+
`Use clipform_upload_node_media with the public URL to attach to a node.`
|
|
23730
23525
|
].join("\n")
|
|
23731
23526
|
);
|
|
23732
23527
|
}
|
|
@@ -23751,7 +23546,7 @@ function registerSearchMusicTool(server) {
|
|
|
23751
23546
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
23752
23547
|
},
|
|
23753
23548
|
async ({ query, count, instrumentalOnly, minDuration, maxDuration, tags }) => {
|
|
23754
|
-
const result = await
|
|
23549
|
+
const result = await callApi("/internal/search-music", {
|
|
23755
23550
|
body: { query, count, instrumentalOnly, minDuration, maxDuration, tags }
|
|
23756
23551
|
});
|
|
23757
23552
|
if (!result.ok) return errorResult(result.error);
|
|
@@ -23783,7 +23578,7 @@ function registerListCompositionsTool(server) {
|
|
|
23783
23578
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
23784
23579
|
},
|
|
23785
23580
|
async () => {
|
|
23786
|
-
const result = await
|
|
23581
|
+
const result = await callApi("/internal/compositions", {
|
|
23787
23582
|
method: "GET"
|
|
23788
23583
|
});
|
|
23789
23584
|
if (!result.ok) {
|
|
@@ -23797,14 +23592,11 @@ function registerListCompositionsTool(server) {
|
|
|
23797
23592
|
const lines = [`Available compositions (${compositions.length}):
|
|
23798
23593
|
`];
|
|
23799
23594
|
for (const comp of compositions) {
|
|
23800
|
-
|
|
23801
|
-
lines.push(
|
|
23802
|
-
lines.push(` Size: ${comp.width}x${comp.height}`);
|
|
23803
|
-
if (comp.defaultProps) {
|
|
23804
|
-
lines.push(` Default props: ${JSON.stringify(comp.defaultProps, null, 2)}`);
|
|
23805
|
-
}
|
|
23806
|
-
lines.push("");
|
|
23595
|
+
const duration3 = (comp.durationInFrames / comp.fps).toFixed(0);
|
|
23596
|
+
lines.push(`- **${comp.id}** \u2014 ${comp.width}x${comp.height}, ${duration3}s`);
|
|
23807
23597
|
}
|
|
23598
|
+
lines.push("");
|
|
23599
|
+
lines.push("Use clipform_render_composition with a compositionId to render. Pass inputProps matching the composition's schema.");
|
|
23808
23600
|
return textResult(lines.join("\n"));
|
|
23809
23601
|
}
|
|
23810
23602
|
);
|
|
@@ -23823,7 +23615,7 @@ function registerListAssetsTool(server) {
|
|
|
23823
23615
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
23824
23616
|
},
|
|
23825
23617
|
async ({ type }) => {
|
|
23826
|
-
const result = await
|
|
23618
|
+
const result = await callApi("/internal/assets", {
|
|
23827
23619
|
method: "GET",
|
|
23828
23620
|
params: { type: type ?? "all" }
|
|
23829
23621
|
});
|
|
@@ -23861,7 +23653,7 @@ function registerListAssetsTool(server) {
|
|
|
23861
23653
|
|
|
23862
23654
|
// src/tools/generate-video.ts
|
|
23863
23655
|
var slideshowStyleSchema2 = external_exports.object({
|
|
23864
|
-
preset: external_exports.enum(
|
|
23656
|
+
preset: external_exports.enum(KEN_BURNS_PRESETS).optional(),
|
|
23865
23657
|
zoom: external_exports.object({ from: external_exports.number().optional(), to: external_exports.number().optional() }).optional(),
|
|
23866
23658
|
pan: external_exports.object({ range: external_exports.number().optional(), scale: external_exports.number().optional() }).optional(),
|
|
23867
23659
|
zoomPan: external_exports.object({
|
|
@@ -23869,7 +23661,7 @@ var slideshowStyleSchema2 = external_exports.object({
|
|
|
23869
23661
|
to: external_exports.number().optional(),
|
|
23870
23662
|
rangeFraction: external_exports.number().optional()
|
|
23871
23663
|
}).optional(),
|
|
23872
|
-
easing: external_exports.enum(
|
|
23664
|
+
easing: external_exports.enum(KEN_BURNS_EASINGS).optional(),
|
|
23873
23665
|
blurPad: external_exports.object({
|
|
23874
23666
|
blurPx: external_exports.number().optional(),
|
|
23875
23667
|
brightness: external_exports.number().optional(),
|
|
@@ -23889,43 +23681,11 @@ function registerGenerateVideoTool(server) {
|
|
|
23889
23681
|
"clipform_generate_video",
|
|
23890
23682
|
{
|
|
23891
23683
|
title: "Generate Video",
|
|
23892
|
-
description: `Generate a video from images, video clips, or
|
|
23893
|
-
|
|
23894
|
-
## Workflow
|
|
23895
|
-
|
|
23896
|
-
1. Source media with clipform_search_media (kind: "image" or "video")
|
|
23897
|
-
2. Source audio with clipform_generate_tts (narration) or clipform_search_music (background)
|
|
23898
|
-
3. Call this tool with items + audio_url
|
|
23899
|
-
4. Attach the returned URL to a node via clipform_upload_node_media with media_type "video"
|
|
23900
|
-
|
|
23901
|
-
## Items
|
|
23902
|
-
|
|
23903
|
-
Each item is an image or video clip:
|
|
23904
|
-
- **type: "image"** - still image with Ken Burns motion (pan/zoom). Supports effect, fit, and style overrides.
|
|
23905
|
-
- **type: "video"** - video clip, cover-cropped to fill 9:16 frame. Audio muted by default (set volume: 1 to mix in). Use start_from to trim.
|
|
23906
|
-
|
|
23907
|
-
## Style presets (image items)
|
|
23908
|
-
|
|
23909
|
-
Pick one via default_style.preset (all images) or per-image style.preset:
|
|
23910
|
-
- **cinematic** (default) - smooth ease-in-out, subtle zoom/pan, subtle vignette
|
|
23911
|
-
- **dramatic** - big zoom, wide pan, ease-out, strong vignette. Reveals, climactic moments.
|
|
23912
|
-
- **calm** - minimal motion, linear easing, no vignette. Educational, reflective.
|
|
23913
|
-
- **documentary** - near-static, tiny pan, no vignette. Historical photos, talking-heads context.
|
|
23914
|
-
- **dreamy** - heavy blur-pad, soft vignette. Aspirational, nostalgic.
|
|
23915
|
-
- **moody** - desaturated, dark, strong vignette. Mystery, somber.
|
|
23916
|
-
|
|
23917
|
-
## Effects (image items)
|
|
23918
|
-
|
|
23919
|
-
zoom-in, zoom-out, pan-left, pan-right, pan-up, pan-down, zoom-in-pan-left, zoom-in-pan-right, random, static.
|
|
23920
|
-
Set per-image or use random_effects: true to shuffle.
|
|
23921
|
-
|
|
23922
|
-
## Transitions
|
|
23923
|
-
|
|
23924
|
-
fade (default), slide, wipe, none. Duration in seconds.
|
|
23684
|
+
description: `Generate a video from images, video clips, or both, synced to audio. Creates 9:16 (720x1280) with Ken Burns effects and transitions. Blocks until complete. Returns a public URL.
|
|
23925
23685
|
|
|
23926
|
-
|
|
23686
|
+
Workflow: 1) clipform_search_media (kind: "image" or "video") 2) clipform_generate_tts or clipform_search_music for audio 3) this tool 4) clipform_upload_node_media with media_type "video".
|
|
23927
23687
|
|
|
23928
|
-
|
|
23688
|
+
Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by default). Style presets via default_style.preset: cinematic, dramatic, calm, documentary, dreamy, moody. Duration matches audio_url or set duration_seconds explicitly.`,
|
|
23929
23689
|
inputSchema: {
|
|
23930
23690
|
items: external_exports.array(
|
|
23931
23691
|
external_exports.object({
|
|
@@ -23933,7 +23693,7 @@ Matches audio when audio_url is provided. Use duration_seconds for videos withou
|
|
|
23933
23693
|
url: external_exports.string().url().describe("Media URL"),
|
|
23934
23694
|
effect: external_exports.string().optional().describe("Ken Burns effect (image only): zoom-in, zoom-out, pan-left, pan-right, pan-up, pan-down, zoom-in-pan-left, zoom-in-pan-right, random, static"),
|
|
23935
23695
|
aspect_ratio: external_exports.number().positive().optional().describe("Image width/height ratio (image only). Enables auto blur-pad for landscape images."),
|
|
23936
|
-
fit: external_exports.enum(
|
|
23696
|
+
fit: external_exports.enum(KEN_BURNS_FIT_MODES).optional().describe("Framing mode (image only). auto = cover for portrait, blur-pad for landscape."),
|
|
23937
23697
|
style: slideshowStyleSchema2.optional().describe("Creative style overrides (image only)"),
|
|
23938
23698
|
start_from: external_exports.number().min(0).optional().describe("Start time in seconds (video only). Trims the clip."),
|
|
23939
23699
|
volume: external_exports.number().min(0).max(1).optional().describe("Clip audio volume 0-1 (video only, default 0 = muted)")
|
|
@@ -23952,9 +23712,8 @@ Matches audio when audio_url is provided. Use duration_seconds for videos withou
|
|
|
23952
23712
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
23953
23713
|
},
|
|
23954
23714
|
async ({ items, audio_url, duration_seconds, random_effects, transition, default_style, background_color }) => {
|
|
23955
|
-
const workspace_id =
|
|
23956
|
-
const
|
|
23957
|
-
callInternalApi("/internal/generate-video", {
|
|
23715
|
+
const workspace_id = getMcpAuth()?.workspace_id;
|
|
23716
|
+
const result = await callApi("/internal/generate-video", {
|
|
23958
23717
|
body: {
|
|
23959
23718
|
items,
|
|
23960
23719
|
audio_url,
|
|
@@ -23965,76 +23724,19 @@ Matches audio when audio_url is provided. Use duration_seconds for videos withou
|
|
|
23965
23724
|
background_color,
|
|
23966
23725
|
workspace_id
|
|
23967
23726
|
}
|
|
23968
|
-
}).then((result) => {
|
|
23969
|
-
if (!result.ok) {
|
|
23970
|
-
failJob(job.id, result.error);
|
|
23971
|
-
} else {
|
|
23972
|
-
completeJob(job.id, result.data);
|
|
23973
|
-
}
|
|
23974
|
-
}).catch((err) => {
|
|
23975
|
-
failJob(job.id, err instanceof Error ? err.message : String(err));
|
|
23976
23727
|
});
|
|
23728
|
+
if (!result.ok) return errorResult(result.error);
|
|
23729
|
+
const data = result.data;
|
|
23977
23730
|
return textResult(
|
|
23978
23731
|
[
|
|
23979
|
-
`
|
|
23980
|
-
``,
|
|
23981
|
-
`Job ID: ${job.id}`,
|
|
23982
|
-
``,
|
|
23983
|
-
`Renders typically take ${RENDER_TIMING.expectedRange}. Use clipform_check_render with this job ID to check status.`
|
|
23984
|
-
].join("\n")
|
|
23985
|
-
);
|
|
23986
|
-
}
|
|
23987
|
-
);
|
|
23988
|
-
}
|
|
23989
|
-
|
|
23990
|
-
// src/tools/check-render.ts
|
|
23991
|
-
function registerCheckRenderTool(server) {
|
|
23992
|
-
server.registerTool(
|
|
23993
|
-
"clipform_check_render",
|
|
23994
|
-
{
|
|
23995
|
-
title: "Check Render Status",
|
|
23996
|
-
description: `Check the status of a render job started by clipform_generate_video, clipform_generate_slideshow, or clipform_render_composition.
|
|
23997
|
-
|
|
23998
|
-
Returns the current status and, when complete, the output URL. If still rendering, wait ${RENDER_TIMING.pollDelay} before checking again.`,
|
|
23999
|
-
inputSchema: {
|
|
24000
|
-
job_id: external_exports.string().uuid().describe("The job ID returned by the render tool")
|
|
24001
|
-
},
|
|
24002
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
24003
|
-
},
|
|
24004
|
-
async ({ job_id }) => {
|
|
24005
|
-
pruneJobs();
|
|
24006
|
-
const job = getJob(job_id);
|
|
24007
|
-
if (!job) {
|
|
24008
|
-
return errorResult(`No render job found with ID ${job_id}. Jobs expire after 30 minutes.`);
|
|
24009
|
-
}
|
|
24010
|
-
if (job.status === "rendering") {
|
|
24011
|
-
const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
|
|
24012
|
-
return textResult(
|
|
24013
|
-
[
|
|
24014
|
-
`Status: rendering (${elapsed}s elapsed)`,
|
|
24015
|
-
`Tool: ${job.tool}`,
|
|
24016
|
-
``,
|
|
24017
|
-
`Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
|
|
24018
|
-
].join("\n")
|
|
24019
|
-
);
|
|
24020
|
-
}
|
|
24021
|
-
if (job.status === "failed") {
|
|
24022
|
-
return errorResult(`Render failed: ${job.error}`);
|
|
24023
|
-
}
|
|
24024
|
-
const data = job.result;
|
|
24025
|
-
return textResult(
|
|
24026
|
-
[
|
|
24027
|
-
`Status: complete`,
|
|
24028
|
-
`Tool: ${job.tool}`,
|
|
23732
|
+
`Video rendered (${items.length} item${items.length > 1 ? "s" : ""}).`,
|
|
24029
23733
|
``,
|
|
24030
|
-
|
|
24031
|
-
|
|
24032
|
-
|
|
24033
|
-
...data.outputPath ? [`Output: ${data.outputPath}`] : [],
|
|
24034
|
-
...data.format ? [`Format: ${data.format}`] : [],
|
|
23734
|
+
`Public URL: ${data.public_url}`,
|
|
23735
|
+
`Storage path: ${data.storage_path}`,
|
|
23736
|
+
data.duration_seconds ? `Duration: ${data.duration_seconds}s` : "",
|
|
24035
23737
|
``,
|
|
24036
|
-
`Use clipform_upload_node_media with the public URL to attach
|
|
24037
|
-
].join("\n")
|
|
23738
|
+
`Use clipform_upload_node_media with the public URL to attach to a node.`
|
|
23739
|
+
].filter(Boolean).join("\n")
|
|
24038
23740
|
);
|
|
24039
23741
|
}
|
|
24040
23742
|
);
|
|
@@ -24177,45 +23879,72 @@ async function getSessionContext() {
|
|
|
24177
23879
|
} else {
|
|
24178
23880
|
lines.push("Questions: unlimited");
|
|
24179
23881
|
}
|
|
23882
|
+
if (plan.form_limit !== null) {
|
|
23883
|
+
lines.push(`Form limit: ${plan.form_limit} forms`);
|
|
23884
|
+
} else {
|
|
23885
|
+
lines.push("Forms: unlimited");
|
|
23886
|
+
}
|
|
24180
23887
|
if (plan.custom_theme === false) {
|
|
24181
23888
|
lines.push("Custom themes: not available on this plan");
|
|
24182
23889
|
}
|
|
24183
23890
|
if (me.auth_mode !== "oauth") {
|
|
24184
23891
|
lines.push(`
|
|
24185
|
-
To unlock your full plan:
|
|
23892
|
+
To unlock your full plan: sign in to your Clipform account.`);
|
|
24186
23893
|
}
|
|
24187
23894
|
return lines.join("\n");
|
|
24188
23895
|
}
|
|
24189
23896
|
|
|
24190
23897
|
// src/prompts.ts
|
|
23898
|
+
var MEDIA_STYLE_ENUM = ["text", "images", "video"];
|
|
23899
|
+
function resourceLink(name, uri, title) {
|
|
23900
|
+
return {
|
|
23901
|
+
role: "assistant",
|
|
23902
|
+
content: {
|
|
23903
|
+
type: "resource_link",
|
|
23904
|
+
uri,
|
|
23905
|
+
name,
|
|
23906
|
+
title,
|
|
23907
|
+
mimeType: "text/markdown"
|
|
23908
|
+
}
|
|
23909
|
+
};
|
|
23910
|
+
}
|
|
24191
23911
|
function registerPrompts(server) {
|
|
24192
23912
|
server.registerPrompt(
|
|
24193
23913
|
"create-quiz",
|
|
24194
23914
|
{
|
|
24195
23915
|
title: "Create a Quiz",
|
|
24196
|
-
description: "Build a scored knowledge quiz with narrated video questions"
|
|
23916
|
+
description: "Build a scored knowledge quiz with narrated video questions",
|
|
23917
|
+
argsSchema: {
|
|
23918
|
+
topic: external_exports.string().optional().describe("Quiz topic (e.g. 'Space exploration', 'Premier League')"),
|
|
23919
|
+
question_count: external_exports.number().optional().default(8).describe("Number of questions (default: 8)"),
|
|
23920
|
+
media_style: external_exports.enum(MEDIA_STYLE_ENUM).optional().default("video").describe("Media style: text only, still images, or slideshow video with narration (default: video)")
|
|
23921
|
+
}
|
|
24197
23922
|
},
|
|
24198
|
-
async () => {
|
|
23923
|
+
async (args) => {
|
|
24199
23924
|
const sessionContext = await getSessionContext();
|
|
23925
|
+
const topic = args.topic ? ` about "${args.topic}"` : "";
|
|
23926
|
+
const count = args.question_count ?? 8;
|
|
23927
|
+
const media = args.media_style ?? "video";
|
|
24200
23928
|
return {
|
|
24201
23929
|
messages: [
|
|
24202
23930
|
{
|
|
24203
23931
|
role: "user",
|
|
24204
23932
|
content: {
|
|
24205
23933
|
type: "text",
|
|
24206
|
-
text:
|
|
23934
|
+
text: `I want to create a quiz${topic}. ${count} questions, ${media} style.`
|
|
24207
23935
|
}
|
|
24208
23936
|
},
|
|
23937
|
+
resourceLink("guide-quiz", "clipform://guides/quiz", "Quiz Writing Guide"),
|
|
24209
23938
|
{
|
|
24210
23939
|
role: "assistant",
|
|
24211
23940
|
content: {
|
|
24212
23941
|
type: "text",
|
|
24213
|
-
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a great quiz with Clipform. Read the quiz
|
|
23942
|
+
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a great ${count}-question ${media} quiz${topic} with Clipform. Read the attached quiz guide for craft knowledge on question design, difficulty curves, and narration style.
|
|
24214
23943
|
|
|
24215
23944
|
## Workflow
|
|
24216
23945
|
|
|
24217
23946
|
1. **Research** the topic - find surprising facts, common misconceptions, myth-busters
|
|
24218
|
-
2. **Write questions** - follow the difficulty curve (easy start, hard middle, satisfying end). Target
|
|
23947
|
+
2. **Write questions** - follow the difficulty curve (easy start, hard middle, satisfying end). Target ${count} questions.
|
|
24219
23948
|
3. **Create the form** with clipform_create_form:
|
|
24220
23949
|
- show_step_counter: true
|
|
24221
23950
|
- disable_back_navigation: true
|
|
@@ -24224,12 +23953,12 @@ function registerPrompts(server) {
|
|
|
24224
23953
|
- randomise_options: true in config
|
|
24225
23954
|
- score: 1 on correct option, score: 0 on wrong
|
|
24226
23955
|
- 3-4 wrong answers per question
|
|
24227
|
-
5. **Generate narration** with clipform_generate_tts for each question. Tease the question - do NOT reveal the answer or read options aloud. Keep each narration 5-15 seconds
|
|
23956
|
+
5. **Generate narration** with clipform_generate_tts for each question. Tease the question - do NOT reveal the answer or read options aloud. Keep each narration 5-15 seconds.${media !== "text" ? `
|
|
24228
23957
|
6. **Build video** for each question:
|
|
24229
23958
|
- clipform_search_media (kind: "image") - 3 images per question
|
|
24230
23959
|
- clipform_generate_video - creates Ken Burns video synced to audio
|
|
24231
|
-
7. **Attach media** with clipform_upload_node_media. Include captions, set show_captions: true
|
|
24232
|
-
8. **Update end screen** with clipform_update_node - EVERY quiz must have:
|
|
23960
|
+
7. **Attach media** with clipform_upload_node_media. Include captions, set show_captions: true.` : ""}
|
|
23961
|
+
${media !== "text" ? "8" : "6"}. **Update end screen** with clipform_update_node - EVERY quiz must have:
|
|
24233
23962
|
- show_score: true, icon: "trophy"
|
|
24234
23963
|
- show_share_button: true (drives virality)
|
|
24235
23964
|
- cta_type: "restart", cta_text: a short challenge like "Beat your score?" or "Try again?"
|
|
@@ -24239,15 +23968,9 @@ function registerPrompts(server) {
|
|
|
24239
23968
|
{ "min": 3, "max": 5, "title": "Frequent Flyer", "message": "Not bad! You know your way around." },
|
|
24240
23969
|
{ "min": 6, "max": 8, "title": "World Explorer", "message": "Impressive - you really know your stuff." }
|
|
24241
23970
|
\`\`\`
|
|
24242
|
-
9. **Publish** with clipform_update_form
|
|
24243
|
-
10. **Tag the form** - pass tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words
|
|
24244
|
-
11. **Log** with clipform_log_generation (sources, images, attributions)
|
|
24245
|
-
|
|
24246
|
-
## Before building, ask
|
|
24247
|
-
|
|
24248
|
-
1. How many questions?
|
|
24249
|
-
2. Media style: text only, still images, or slideshow video with narration?
|
|
24250
|
-
3. Any topic or style preferences?`
|
|
23971
|
+
${media !== "text" ? "9" : "7"}. **Publish** with clipform_update_form
|
|
23972
|
+
${media !== "text" ? "10" : "8"}. **Tag the form** - pass tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words
|
|
23973
|
+
${media !== "text" ? "11" : "9"}. **Log** with clipform_log_generation (sources, images, attributions)`
|
|
24251
23974
|
}
|
|
24252
23975
|
}
|
|
24253
23976
|
]
|
|
@@ -24258,24 +23981,37 @@ function registerPrompts(server) {
|
|
|
24258
23981
|
"create-personality-quiz",
|
|
24259
23982
|
{
|
|
24260
23983
|
title: "Create a Personality Quiz",
|
|
24261
|
-
description: "Build a 'Which X are you?' personality quiz with category-based scoring and outcome screens"
|
|
23984
|
+
description: "Build a 'Which X are you?' personality quiz with category-based scoring and outcome screens",
|
|
23985
|
+
argsSchema: {
|
|
23986
|
+
topic: external_exports.string().optional().describe("Quiz theme (e.g. 'Which city are you?', 'What's your work style?')"),
|
|
23987
|
+
categories: external_exports.string().optional().describe("Comma-separated outcome categories (e.g. 'Creative, Analytical, Leader, Collaborator')"),
|
|
23988
|
+
question_count: external_exports.number().optional().default(8).describe("Number of questions (default: 8)"),
|
|
23989
|
+
media_style: external_exports.enum(MEDIA_STYLE_ENUM).optional().default("video").describe("Media style (default: video)")
|
|
23990
|
+
}
|
|
24262
23991
|
},
|
|
24263
|
-
async () => {
|
|
23992
|
+
async (args) => {
|
|
24264
23993
|
const sessionContext = await getSessionContext();
|
|
23994
|
+
const topic = args.topic ? ` - "${args.topic}"` : "";
|
|
23995
|
+
const count = args.question_count ?? 8;
|
|
23996
|
+
const media = args.media_style ?? "video";
|
|
23997
|
+
const categoriesNote = args.categories ? `
|
|
23998
|
+
|
|
23999
|
+
Outcome categories: ${args.categories}` : "";
|
|
24265
24000
|
return {
|
|
24266
24001
|
messages: [
|
|
24267
24002
|
{
|
|
24268
24003
|
role: "user",
|
|
24269
24004
|
content: {
|
|
24270
24005
|
type: "text",
|
|
24271
|
-
text:
|
|
24006
|
+
text: `I want to create a personality quiz${topic}. ${count} questions, ${media} style.${categoriesNote}`
|
|
24272
24007
|
}
|
|
24273
24008
|
},
|
|
24009
|
+
resourceLink("guide-personality-quiz", "clipform://guides/personality-quiz", "Personality Quiz Guide"),
|
|
24274
24010
|
{
|
|
24275
24011
|
role: "assistant",
|
|
24276
24012
|
content: {
|
|
24277
24013
|
type: "text",
|
|
24278
|
-
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a personality quiz with Clipform. Read the personality quiz guide
|
|
24014
|
+
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a ${count}-question personality quiz${topic} with Clipform. Read the attached personality quiz guide for craft knowledge on category design, option weighting, and outcome writing.
|
|
24279
24015
|
|
|
24280
24016
|
## How it differs from a knowledge quiz
|
|
24281
24017
|
|
|
@@ -24284,7 +24020,7 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24284
24020
|
## Workflow
|
|
24285
24021
|
|
|
24286
24022
|
1. **Define 3-5 outcome categories** - these are the "personalities" (e.g. "Creative", "Analytical", "Leader", "Collaborator"). More than 5 gets muddy.
|
|
24287
|
-
2. **Write questions** - each question should feel revealing but fun. Target
|
|
24023
|
+
2. **Write questions** - each question should feel revealing but fun. Target ${count} questions.
|
|
24288
24024
|
3. **Create the form** with clipform_create_form:
|
|
24289
24025
|
- show_step_counter: true
|
|
24290
24026
|
- disable_back_navigation: true
|
|
@@ -24315,13 +24051,7 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24315
24051
|
\`\`\`
|
|
24316
24052
|
8. **Publish** with clipform_update_form
|
|
24317
24053
|
9. **Tag the form** - pass tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words
|
|
24318
|
-
10. **Log** with clipform_log_generation
|
|
24319
|
-
|
|
24320
|
-
## Before building, ask
|
|
24321
|
-
|
|
24322
|
-
1. What are the possible outcomes/personalities? (3-5 categories)
|
|
24323
|
-
2. What's the theme? ("Which city are you?", "What's your work style?", "Which character are you?")
|
|
24324
|
-
3. Media style: text only, still images, or slideshow video with narration?`
|
|
24054
|
+
10. **Log** with clipform_log_generation`
|
|
24325
24055
|
}
|
|
24326
24056
|
}
|
|
24327
24057
|
]
|
|
@@ -24332,45 +24062,49 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24332
24062
|
"create-interview",
|
|
24333
24063
|
{
|
|
24334
24064
|
title: "Create an Interview",
|
|
24335
|
-
description: "Build a form to collect testimonials, case studies, async video interviews, or journalist responses"
|
|
24065
|
+
description: "Build a form to collect testimonials, case studies, async video interviews, or journalist responses",
|
|
24066
|
+
argsSchema: {
|
|
24067
|
+
purpose: external_exports.string().optional().describe("What you're collecting (e.g. 'testimonial', 'case study', 'job application')"),
|
|
24068
|
+
response_format: external_exports.enum(["video", "audio", "text", "all"]).optional().default("all").describe("How respondents reply (default: all - video, audio, and text)"),
|
|
24069
|
+
needs_consent: external_exports.boolean().optional().default(true).describe("Include a consent statement (default: true)")
|
|
24070
|
+
}
|
|
24336
24071
|
},
|
|
24337
|
-
async () => {
|
|
24072
|
+
async (args) => {
|
|
24338
24073
|
const sessionContext = await getSessionContext();
|
|
24074
|
+
const purpose = args.purpose ?? "responses";
|
|
24075
|
+
const format = args.response_format ?? "all";
|
|
24076
|
+
const consent = args.needs_consent ?? true;
|
|
24339
24077
|
return {
|
|
24340
24078
|
messages: [
|
|
24341
24079
|
{
|
|
24342
24080
|
role: "user",
|
|
24343
24081
|
content: {
|
|
24344
24082
|
type: "text",
|
|
24345
|
-
text:
|
|
24083
|
+
text: `I want to collect ${purpose} from people. Responses via ${format}.${consent ? " Include consent." : ""}`
|
|
24346
24084
|
}
|
|
24347
24085
|
},
|
|
24086
|
+
resourceLink("guide-interview", "clipform://guides/interview", "Interview & Testimonial Guide"),
|
|
24348
24087
|
{
|
|
24349
24088
|
role: "assistant",
|
|
24350
24089
|
content: {
|
|
24351
24090
|
type: "text",
|
|
24352
|
-
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build an interview
|
|
24091
|
+
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build an interview form for collecting ${purpose} with Clipform. Read the attached interview guide for craft knowledge on question design and pacing.
|
|
24353
24092
|
|
|
24354
24093
|
## Workflow
|
|
24355
24094
|
|
|
24356
|
-
1. **Identify the ask** - what do you need from respondents?
|
|
24095
|
+
1. **Identify the ask** - what do you need from respondents? ${purpose}.
|
|
24357
24096
|
2. **Create the form** with clipform_create_form:
|
|
24358
24097
|
- show_step_counter: true
|
|
24359
24098
|
- disable_back_navigation: false
|
|
24360
24099
|
3. **Add a warm-up question** - something easy: "Tell us your name and role" (type: "open")
|
|
24361
|
-
4. **Add core questions** (type: "open") - 2-3 max, one topic per question
|
|
24362
|
-
5. **Add contact collection** (type: "contact") - first name + email minimum
|
|
24363
|
-
6. **Add consent**
|
|
24100
|
+
4. **Add core questions** (type: "open") - 2-3 max, one topic per question.${format === "all" ? " Enable text + audio + video responses." : ` Enable ${format} responses.`}
|
|
24101
|
+
5. **Add contact collection** (type: "contact") - first name + email minimum${consent ? `
|
|
24102
|
+
6. **Add consent** - "I agree that my response may be used in [context]"` : ""}
|
|
24364
24103
|
7. **Update end screen** - set expectations: "Thanks! We'll be in touch."
|
|
24365
24104
|
8. **Optional: add narration** - warm, inviting tone. "We'd love to hear your story..."
|
|
24366
24105
|
9. **Publish** with clipform_update_form
|
|
24367
24106
|
10. **Tag the form** - pass tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words
|
|
24368
|
-
|
|
24369
|
-
## Before building, ask
|
|
24370
|
-
|
|
24371
|
-
1. What are you collecting? (testimonial, case study, interview, application)
|
|
24372
|
-
2. Should respondents reply with video, audio, text, or all three?
|
|
24373
|
-
3. Do you need a consent statement?`
|
|
24107
|
+
11. **Log** with clipform_log_generation`
|
|
24374
24108
|
}
|
|
24375
24109
|
}
|
|
24376
24110
|
]
|
|
@@ -24381,24 +24115,31 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24381
24115
|
"create-survey",
|
|
24382
24116
|
{
|
|
24383
24117
|
title: "Create a Survey",
|
|
24384
|
-
description: "Build a feedback survey, NPS form, or research questionnaire"
|
|
24118
|
+
description: "Build a feedback survey, NPS form, or research questionnaire",
|
|
24119
|
+
argsSchema: {
|
|
24120
|
+
topic: external_exports.string().optional().describe("What feedback you're collecting (e.g. 'NPS', 'event feedback', 'product satisfaction')"),
|
|
24121
|
+
anonymous: external_exports.boolean().optional().default(true).describe("Whether the survey is anonymous (default: true)")
|
|
24122
|
+
}
|
|
24385
24123
|
},
|
|
24386
|
-
async () => {
|
|
24124
|
+
async (args) => {
|
|
24387
24125
|
const sessionContext = await getSessionContext();
|
|
24126
|
+
const topic = args.topic ? ` for ${args.topic}` : "";
|
|
24127
|
+
const anonymous = args.anonymous ?? true;
|
|
24388
24128
|
return {
|
|
24389
24129
|
messages: [
|
|
24390
24130
|
{
|
|
24391
24131
|
role: "user",
|
|
24392
24132
|
content: {
|
|
24393
24133
|
type: "text",
|
|
24394
|
-
text:
|
|
24134
|
+
text: `I want to create a survey${topic}. ${anonymous ? "Anonymous" : "Identified"} respondents.`
|
|
24395
24135
|
}
|
|
24396
24136
|
},
|
|
24137
|
+
resourceLink("guide-survey", "clipform://guides/survey", "Survey & Feedback Guide"),
|
|
24397
24138
|
{
|
|
24398
24139
|
role: "assistant",
|
|
24399
24140
|
content: {
|
|
24400
24141
|
type: "text",
|
|
24401
|
-
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a survey with Clipform. Read the survey guide
|
|
24142
|
+
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a survey${topic} with Clipform. Read the attached survey guide for craft knowledge on question design and reducing respondent fatigue.
|
|
24402
24143
|
|
|
24403
24144
|
## Workflow
|
|
24404
24145
|
|
|
@@ -24408,19 +24149,14 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24408
24149
|
- disable_back_navigation: false
|
|
24409
24150
|
3. **Add key metric question first** (type: "choice" or "rating") - put it first while attention is highest
|
|
24410
24151
|
4. **Add one "why?" follow-up** (type: "open") - "What's the main reason for your score?"
|
|
24411
|
-
5. **Add 2-3 specific questions** (type: "choice") - only ask what you'll act on
|
|
24412
|
-
6. **Contact**
|
|
24152
|
+
5. **Add 2-3 specific questions** (type: "choice") - only ask what you'll act on${!anonymous ? `
|
|
24153
|
+
6. **Contact** - collect name + email for follow-up` : ""}
|
|
24413
24154
|
7. **Update end screen** - "Thanks for your feedback!"
|
|
24414
24155
|
8. **Publish** with clipform_update_form
|
|
24415
24156
|
9. **Tag the form** - pass tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words
|
|
24157
|
+
10. **Log** with clipform_log_generation
|
|
24416
24158
|
|
|
24417
|
-
## Key rule: 5 questions max. Every extra question costs completions
|
|
24418
|
-
|
|
24419
|
-
## Before building, ask
|
|
24420
|
-
|
|
24421
|
-
1. What feedback are you collecting? (NPS, satisfaction, event feedback, product research)
|
|
24422
|
-
2. Anonymous or identified?
|
|
24423
|
-
3. Any specific areas you want to ask about?`
|
|
24159
|
+
## Key rule: 5 questions max. Every extra question costs completions.`
|
|
24424
24160
|
}
|
|
24425
24161
|
}
|
|
24426
24162
|
]
|
|
@@ -24431,24 +24167,36 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24431
24167
|
"create-comprehension-quiz",
|
|
24432
24168
|
{
|
|
24433
24169
|
title: "Create a YouTube Comprehension Quiz",
|
|
24434
|
-
description: "Build a comprehension quiz from a YouTube video - tests whether the viewer actually watched and understood the content"
|
|
24170
|
+
description: "Build a comprehension quiz from a YouTube video - tests whether the viewer actually watched and understood the content",
|
|
24171
|
+
argsSchema: {
|
|
24172
|
+
youtube_url: external_exports.string().optional().describe("YouTube video URL to create the quiz from"),
|
|
24173
|
+
question_count: external_exports.number().optional().default(8).describe("Number of questions (default: 8)"),
|
|
24174
|
+
audience: external_exports.string().optional().describe("Target audience (e.g. 'children aged 5-8', 'university students', 'general')"),
|
|
24175
|
+
media_style: external_exports.enum(MEDIA_STYLE_ENUM).optional().default("video").describe("Media style (default: video)")
|
|
24176
|
+
}
|
|
24435
24177
|
},
|
|
24436
|
-
async () => {
|
|
24178
|
+
async (args) => {
|
|
24437
24179
|
const sessionContext = await getSessionContext();
|
|
24180
|
+
const count = args.question_count ?? 8;
|
|
24181
|
+
const media = args.media_style ?? "video";
|
|
24182
|
+
const urlNote = args.youtube_url ? ` Start by extracting the transcript from: ${args.youtube_url}` : "";
|
|
24183
|
+
const audienceNote = args.audience ? ` Target audience: ${args.audience}.` : "";
|
|
24438
24184
|
return {
|
|
24439
24185
|
messages: [
|
|
24440
24186
|
{
|
|
24441
24187
|
role: "user",
|
|
24442
24188
|
content: {
|
|
24443
24189
|
type: "text",
|
|
24444
|
-
text:
|
|
24190
|
+
text: `I want to create a comprehension quiz based on a YouTube video. ${count} questions, ${media} style.${urlNote}${audienceNote}`
|
|
24445
24191
|
}
|
|
24446
24192
|
},
|
|
24193
|
+
resourceLink("guide-quiz", "clipform://guides/quiz", "Quiz Writing Guide"),
|
|
24194
|
+
resourceLink("guide-comprehension-quiz", "clipform://guides/comprehension-quiz", "Comprehension Quiz Guide"),
|
|
24447
24195
|
{
|
|
24448
24196
|
role: "assistant",
|
|
24449
24197
|
content: {
|
|
24450
24198
|
type: "text",
|
|
24451
|
-
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a comprehension quiz from a YouTube video. Read the quiz
|
|
24199
|
+
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a ${count}-question comprehension quiz from a YouTube video. Read the attached quiz and comprehension guides for craft knowledge on question design and distractor techniques.
|
|
24452
24200
|
|
|
24453
24201
|
## Comprehension Quiz Workflow
|
|
24454
24202
|
|
|
@@ -24463,7 +24211,7 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24463
24211
|
- "What example does the video use to illustrate...?"
|
|
24464
24212
|
- Include 1-2 inference questions: "Based on the video, why does the presenter believe...?"
|
|
24465
24213
|
- Avoid questions answerable without watching (e.g., common knowledge about the topic)
|
|
24466
|
-
4. **Adapt to the audience
|
|
24214
|
+
4. **Adapt to the audience**${args.audience ? ` (${args.audience})` : ""} - simplify language for younger audiences, focus on concrete/visual details rather than abstract arguments
|
|
24467
24215
|
5. **Create the form** with clipform_create_form:
|
|
24468
24216
|
- show_step_counter: true
|
|
24469
24217
|
- disable_back_navigation: true
|
|
@@ -24472,12 +24220,12 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24472
24220
|
- randomise_options: true in config
|
|
24473
24221
|
- score: 1 on correct option, score: 0 on wrong
|
|
24474
24222
|
- 3-4 wrong answers per question - make distractors plausible (things someone might guess without watching)
|
|
24475
|
-
7. **Generate narration** with clipform_generate_tts - reference the video naturally: "If you watched closely, you'll know this one..." Keep each narration 5-10 seconds
|
|
24223
|
+
7. **Generate narration** with clipform_generate_tts - reference the video naturally: "If you watched closely, you'll know this one..." Keep each narration 5-10 seconds.${media !== "text" ? `
|
|
24476
24224
|
8. **Build video** for each question:
|
|
24477
24225
|
- clipform_search_media (kind: "image") - 3 images per question
|
|
24478
24226
|
- clipform_generate_video - Ken Burns video synced to audio
|
|
24479
|
-
9. **Attach media** with clipform_upload_node_media. Include captions, set show_captions: true
|
|
24480
|
-
10. **Update end screen** with clipform_update_node:
|
|
24227
|
+
9. **Attach media** with clipform_upload_node_media. Include captions, set show_captions: true.` : ""}
|
|
24228
|
+
${media !== "text" ? "10" : "8"}. **Update end screen** with clipform_update_node:
|
|
24481
24229
|
- show_score: true, icon: "trophy"
|
|
24482
24230
|
- show_share_button: true
|
|
24483
24231
|
- cta_type: "restart", cta_text: "Rewatch and try again?"
|
|
@@ -24487,9 +24235,9 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24487
24235
|
{ "min": 3, "max": 5, "title": "Casual Viewer", "message": "You caught the highlights but missed some details." },
|
|
24488
24236
|
{ "min": 6, "max": 8, "title": "Focused Student", "message": "You were paying attention - impressive." }
|
|
24489
24237
|
\`\`\`
|
|
24490
|
-
11. **Publish** with clipform_update_form
|
|
24491
|
-
12. **Tag** - tags: ["quiz", "comprehension", "youtube"] + 2-3 topic words from the video
|
|
24492
|
-
13. **Log** with clipform_log_generation - include the YouTube URL, video title, and channel as sources
|
|
24238
|
+
${media !== "text" ? "11" : "9"}. **Publish** with clipform_update_form
|
|
24239
|
+
${media !== "text" ? "12" : "10"}. **Tag** - tags: ["quiz", "comprehension", "youtube"] + 2-3 topic words from the video
|
|
24240
|
+
${media !== "text" ? "13" : "11"}. **Log** with clipform_log_generation - include the YouTube URL, video title, and channel as sources
|
|
24493
24241
|
|
|
24494
24242
|
## Question Types for Comprehension
|
|
24495
24243
|
|
|
@@ -24501,14 +24249,7 @@ There are NO correct answers. Each option maps to one or more outcome categories
|
|
|
24501
24249
|
| Contrast | "The video compares X and Y. What was the key difference?" | Comprehension depth |
|
|
24502
24250
|
| Conclusion | "What was the presenter's final point?" | Watched to the end |
|
|
24503
24251
|
|
|
24504
|
-
Wrong answers should sound right to someone who didn't watch but googled the topic. The quiz should be unfair to non-watchers and fair to watchers
|
|
24505
|
-
|
|
24506
|
-
## Before building, ask
|
|
24507
|
-
|
|
24508
|
-
1. What's the YouTube URL?
|
|
24509
|
-
2. How many questions? (default: 8)
|
|
24510
|
-
3. Who's the audience? (age, knowledge level)
|
|
24511
|
-
4. Media style: text only, still images, or slideshow video with narration?`
|
|
24252
|
+
Wrong answers should sound right to someone who didn't watch but googled the topic. The quiz should be unfair to non-watchers and fair to watchers.`
|
|
24512
24253
|
}
|
|
24513
24254
|
}
|
|
24514
24255
|
]
|
|
@@ -24519,46 +24260,50 @@ Wrong answers should sound right to someone who didn't watch but googled the top
|
|
|
24519
24260
|
"create-funnel",
|
|
24520
24261
|
{
|
|
24521
24262
|
title: "Create a Funnel",
|
|
24522
|
-
description: "Build a lead qualification funnel or product recommendation quiz with branching logic"
|
|
24263
|
+
description: "Build a lead qualification funnel or product recommendation quiz with branching logic",
|
|
24264
|
+
argsSchema: {
|
|
24265
|
+
outcomes: external_exports.string().optional().describe("What segments or recommendations exist (e.g. 'Basic/Pro/Enterprise', or product names)"),
|
|
24266
|
+
criteria: external_exports.string().optional().describe("What criteria determine routing (e.g. 'budget, team size, use case')"),
|
|
24267
|
+
needs_contact: external_exports.boolean().optional().default(true).describe("Include contact capture (default: true)")
|
|
24268
|
+
}
|
|
24523
24269
|
},
|
|
24524
|
-
async () => {
|
|
24270
|
+
async (args) => {
|
|
24525
24271
|
const sessionContext = await getSessionContext();
|
|
24272
|
+
const outcomes = args.outcomes ?? "different outcomes";
|
|
24273
|
+
const contact = args.needs_contact ?? true;
|
|
24274
|
+
const criteriaNote = args.criteria ? ` Routing criteria: ${args.criteria}.` : "";
|
|
24526
24275
|
return {
|
|
24527
24276
|
messages: [
|
|
24528
24277
|
{
|
|
24529
24278
|
role: "user",
|
|
24530
24279
|
content: {
|
|
24531
24280
|
type: "text",
|
|
24532
|
-
text:
|
|
24281
|
+
text: `I want to qualify leads or recommend products based on answers. Outcomes: ${outcomes}.${criteriaNote}`
|
|
24533
24282
|
}
|
|
24534
24283
|
},
|
|
24284
|
+
resourceLink("guide-funnel", "clipform://guides/funnel", "Lead Qualification & Funnel Guide"),
|
|
24535
24285
|
{
|
|
24536
24286
|
role: "assistant",
|
|
24537
24287
|
content: {
|
|
24538
24288
|
type: "text",
|
|
24539
|
-
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a qualification funnel with Clipform. Read the funnel guide
|
|
24289
|
+
text: `${sessionContext ? sessionContext + "\n\n" : ""}Here's how to build a qualification funnel with Clipform. Read the attached funnel guide for craft knowledge on branching logic and conversion.
|
|
24540
24290
|
|
|
24541
24291
|
## Workflow
|
|
24542
24292
|
|
|
24543
|
-
1. **Define outcomes** - what segments or recommendations exist? (
|
|
24293
|
+
1. **Define outcomes** - what segments or recommendations exist? (${outcomes})
|
|
24544
24294
|
2. **Create the form** with clipform_create_form:
|
|
24545
24295
|
- show_step_counter: false (funnels feel shorter without it)
|
|
24546
24296
|
- disable_back_navigation: true (prevents answer shopping that breaks scoring)
|
|
24547
24297
|
3. **Add hook question** (type: "choice") - "What best describes you?" or "What are you looking for?" This segments the user.
|
|
24548
24298
|
4. **Add qualifying questions** (type: "choice") - 2-3 questions that narrow down the need. Assign scores to each option.
|
|
24549
|
-
5. **Set branching logic** with clipform_set_logic - route based on answers
|
|
24550
|
-
6. **Add contact capture** (type: "contact") - name, email, phone. Place AFTER qualifying questions
|
|
24299
|
+
5. **Set branching logic** with clipform_set_logic - route based on answers${contact ? `
|
|
24300
|
+
6. **Add contact capture** (type: "contact") - name, email, phone. Place AFTER qualifying questions.` : ""}
|
|
24551
24301
|
7. **Update end screen** with score_ranges for personalised outcomes: "Based on your answers, we recommend..."
|
|
24552
24302
|
8. **Publish** with clipform_update_form
|
|
24553
24303
|
9. **Tag the form** - pass tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words
|
|
24304
|
+
10. **Log** with clipform_log_generation
|
|
24554
24305
|
|
|
24555
|
-
## Key rule: 3-5 questions max. Every extra step loses leads
|
|
24556
|
-
|
|
24557
|
-
## Before building, ask
|
|
24558
|
-
|
|
24559
|
-
1. What outcomes are you routing to? (products, plans, team members, messages)
|
|
24560
|
-
2. What criteria determine the routing?
|
|
24561
|
-
3. Do you need contact capture?`
|
|
24306
|
+
## Key rule: 3-5 questions max. Every extra step loses leads.`
|
|
24562
24307
|
}
|
|
24563
24308
|
}
|
|
24564
24309
|
]
|
|
@@ -24612,7 +24357,8 @@ function registerResources(server) {
|
|
|
24612
24357
|
"clipform://guides/quiz",
|
|
24613
24358
|
{
|
|
24614
24359
|
description: "Craft knowledge for writing engaging quizzes - difficulty curves, question psychology, narration style, scoring",
|
|
24615
|
-
mimeType: "text/markdown"
|
|
24360
|
+
mimeType: "text/markdown",
|
|
24361
|
+
annotations: { audience: ["assistant"], priority: 0.8 }
|
|
24616
24362
|
},
|
|
24617
24363
|
async () => ({
|
|
24618
24364
|
contents: [
|
|
@@ -24672,6 +24418,11 @@ Inspired by the Color Brain board game - every answer is identified by its colou
|
|
|
24672
24418
|
- If two answer options would produce identical swatches, don't use that question.
|
|
24673
24419
|
- Pair with a reveal composition (FlagReveal, image, or text) for the answer.
|
|
24674
24420
|
|
|
24421
|
+
## Settings
|
|
24422
|
+
|
|
24423
|
+
- show_step_counter: true
|
|
24424
|
+
- disable_back_navigation: true (prevent going back to change answers after seeing feedback)
|
|
24425
|
+
|
|
24675
24426
|
## Narration Style
|
|
24676
24427
|
|
|
24677
24428
|
You're a quiz master, not a question reader. Each question's narration should:
|
|
@@ -24697,7 +24448,8 @@ ${MEDIA_WORKFLOW}`
|
|
|
24697
24448
|
"clipform://guides/personality-quiz",
|
|
24698
24449
|
{
|
|
24699
24450
|
description: "Craft knowledge for building personality quizzes - category design, option weighting, outcome writing, no right/wrong answers",
|
|
24700
|
-
mimeType: "text/markdown"
|
|
24451
|
+
mimeType: "text/markdown",
|
|
24452
|
+
annotations: { audience: ["assistant"], priority: 0.8 }
|
|
24701
24453
|
},
|
|
24702
24454
|
async () => ({
|
|
24703
24455
|
contents: [
|
|
@@ -24747,6 +24499,11 @@ Each category needs a \`scoring_results\` entry on the end screen:
|
|
|
24747
24499
|
- **Message**: 2-3 sentences that feel like a personalised insight. Reference specific traits the quiz measured.
|
|
24748
24500
|
- **Optional CTA**: link to relevant content, product, or next step
|
|
24749
24501
|
|
|
24502
|
+
## Settings
|
|
24503
|
+
|
|
24504
|
+
- show_step_counter: true
|
|
24505
|
+
- disable_back_navigation: true (prevent re-answering which skews category scores)
|
|
24506
|
+
|
|
24750
24507
|
## Narration Style
|
|
24751
24508
|
|
|
24752
24509
|
Reflective and curious, not quizmaster-y:
|
|
@@ -24768,7 +24525,8 @@ ${MEDIA_WORKFLOW}`
|
|
|
24768
24525
|
"clipform://guides/comprehension-quiz",
|
|
24769
24526
|
{
|
|
24770
24527
|
description: "Craft knowledge for YouTube comprehension quizzes - extracting questions from transcripts, distractor design, audience adaptation",
|
|
24771
|
-
mimeType: "text/markdown"
|
|
24528
|
+
mimeType: "text/markdown",
|
|
24529
|
+
annotations: { audience: ["assistant"], priority: 0.8 }
|
|
24772
24530
|
},
|
|
24773
24531
|
async () => ({
|
|
24774
24532
|
contents: [
|
|
@@ -24819,6 +24577,11 @@ Make wrong answers plausible to someone who **didn't watch**:
|
|
|
24819
24577
|
|
|
24820
24578
|
For young children: focus on "What did you SEE?" and "Who did what?" rather than abstract arguments.
|
|
24821
24579
|
|
|
24580
|
+
## Settings
|
|
24581
|
+
|
|
24582
|
+
- show_step_counter: true
|
|
24583
|
+
- disable_back_navigation: true
|
|
24584
|
+
|
|
24822
24585
|
## Narration style
|
|
24823
24586
|
|
|
24824
24587
|
Reference the video naturally but don't spoil:
|
|
@@ -24839,7 +24602,8 @@ ${MEDIA_WORKFLOW}`
|
|
|
24839
24602
|
"clipform://guides/interview",
|
|
24840
24603
|
{
|
|
24841
24604
|
description: "Craft knowledge for building interview and testimonial collection forms - warm-up pacing, open questions, consent, video responses",
|
|
24842
|
-
mimeType: "text/markdown"
|
|
24605
|
+
mimeType: "text/markdown",
|
|
24606
|
+
annotations: { audience: ["assistant"], priority: 0.8 }
|
|
24843
24607
|
},
|
|
24844
24608
|
async () => ({
|
|
24845
24609
|
contents: [
|
|
@@ -24893,7 +24657,8 @@ ${MEDIA_WORKFLOW}`
|
|
|
24893
24657
|
"clipform://guides/survey",
|
|
24894
24658
|
{
|
|
24895
24659
|
description: "Craft knowledge for feedback surveys, NPS, and research forms - brevity, rating scales, respondent fatigue",
|
|
24896
|
-
mimeType: "text/markdown"
|
|
24660
|
+
mimeType: "text/markdown",
|
|
24661
|
+
annotations: { audience: ["assistant"], priority: 0.8 }
|
|
24897
24662
|
},
|
|
24898
24663
|
async () => ({
|
|
24899
24664
|
contents: [
|
|
@@ -24946,7 +24711,8 @@ ${WRITING_PRINCIPLES}`
|
|
|
24946
24711
|
"clipform://guides/funnel",
|
|
24947
24712
|
{
|
|
24948
24713
|
description: "Craft knowledge for lead qualification funnels and product recommendation quizzes - branching logic, progressive profiling, conversion",
|
|
24949
|
-
mimeType: "text/markdown"
|
|
24714
|
+
mimeType: "text/markdown",
|
|
24715
|
+
annotations: { audience: ["assistant"], priority: 0.8 }
|
|
24950
24716
|
},
|
|
24951
24717
|
async () => ({
|
|
24952
24718
|
contents: [
|
|
@@ -25057,7 +24823,6 @@ function createServer() {
|
|
|
25057
24823
|
registerGetNodeMediaTool(server);
|
|
25058
24824
|
registerDeleteNodeMediaTool(server);
|
|
25059
24825
|
registerSetNodeLogicTool(server);
|
|
25060
|
-
registerAttachNodeAudioTool(server);
|
|
25061
24826
|
registerLogGenerationTool(server);
|
|
25062
24827
|
registerSearchNewsTool(server);
|
|
25063
24828
|
registerYouTubeTranscriptTool(server);
|
|
@@ -25069,7 +24834,6 @@ function createServer() {
|
|
|
25069
24834
|
registerSearchMusicTool(server);
|
|
25070
24835
|
registerListCompositionsTool(server);
|
|
25071
24836
|
registerListAssetsTool(server);
|
|
25072
|
-
registerCheckRenderTool(server);
|
|
25073
24837
|
registerFetchBoundaryTool(server);
|
|
25074
24838
|
registerPrompts(server);
|
|
25075
24839
|
registerResources(server);
|
|
@@ -25078,7 +24842,6 @@ function createServer() {
|
|
|
25078
24842
|
|
|
25079
24843
|
export {
|
|
25080
24844
|
JSONRPCMessageSchema,
|
|
25081
|
-
setApiKey,
|
|
25082
24845
|
createServer
|
|
25083
24846
|
};
|
|
25084
|
-
//# sourceMappingURL=chunk-
|
|
24847
|
+
//# sourceMappingURL=chunk-Y22DYUS3.js.map
|