@clipform/mcp-server 2.5.1 → 2.10.2
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 -0
- package/dist/{chunk-NYMC63ZK.js → chunk-4QF4KGPE.js} +107 -3
- package/dist/{chunk-NYMC63ZK.js.map → chunk-4QF4KGPE.js.map} +1 -1
- package/dist/{chunk-3BYQLPTT.js → chunk-IL26A4IN.js} +124 -4
- package/dist/chunk-IL26A4IN.js.map +1 -0
- package/dist/{chunk-3GBZFAEM.js → chunk-NCWQ42GI.js} +369 -20
- package/dist/chunk-NCWQ42GI.js.map +1 -0
- package/dist/{chunk-6EHOUAPE.js → chunk-UBJ7CZL2.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/instrument.d.ts +30 -0
- package/dist/instrument.js +5296 -0
- package/dist/instrument.js.map +1 -0
- package/dist/prompts.js +2 -2
- package/dist/resources.js +2 -2
- package/dist/server.js +4 -4
- package/package.json +6 -1
- package/dist/chunk-3BYQLPTT.js.map +0 -1
- package/dist/chunk-3GBZFAEM.js.map +0 -1
- /package/dist/{chunk-6EHOUAPE.js.map → chunk-UBJ7CZL2.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getWorkflowText,
|
|
7
7
|
objectType,
|
|
8
8
|
registerPrompts
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-4QF4KGPE.js";
|
|
10
10
|
import {
|
|
11
11
|
GUIDE_TYPES,
|
|
12
12
|
QUIZ_VARIANTS,
|
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
getGuideUri,
|
|
15
15
|
guideFallbackText,
|
|
16
16
|
registerResources
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-UBJ7CZL2.js";
|
|
18
18
|
import {
|
|
19
19
|
ACTIVE_NODE_TYPES,
|
|
20
20
|
CONTACT_FIELDS,
|
|
21
|
+
FEATURE_FLAGS,
|
|
21
22
|
FORM_TYPE_ALIASES,
|
|
22
23
|
KEN_BURNS_EFFECTS,
|
|
23
24
|
KEN_BURNS_FIT_MODES,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
resolveFormType,
|
|
36
37
|
structuredResult,
|
|
37
38
|
textResult
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-IL26A4IN.js";
|
|
39
40
|
import {
|
|
40
41
|
getMcpAuth,
|
|
41
42
|
runWithMcpTool
|
|
@@ -17002,6 +17003,7 @@ var EXPOSED_COMPOSITIONS = exposedCompositionIds(
|
|
|
17002
17003
|
);
|
|
17003
17004
|
|
|
17004
17005
|
// src/lib/schemas.ts
|
|
17006
|
+
var CUSTOM_SCORES_GATED_KEYS = /* @__PURE__ */ new Set(["show_score", "score_ranges", "scoring_results"]);
|
|
17005
17007
|
var ACTIVE_NODE_TYPES2 = ACTIVE_NODE_TYPES;
|
|
17006
17008
|
var CONTACT_FIELD_IDS = CONTACT_FIELDS.map(
|
|
17007
17009
|
(f) => f.id
|
|
@@ -17011,6 +17013,7 @@ function generateConfigSummary(configSchema, typeKey) {
|
|
|
17011
17013
|
const parts = [];
|
|
17012
17014
|
for (const [key, prop] of Object.entries(configSchema.properties)) {
|
|
17013
17015
|
if (key === "content_media_type") continue;
|
|
17016
|
+
if (!FEATURE_FLAGS.ENABLE_CUSTOM_SCORES && CUSTOM_SCORES_GATED_KEYS.has(key)) continue;
|
|
17014
17017
|
if (prop.enum) {
|
|
17015
17018
|
const vals = prop.enum.map((v) => `"${v}"`).join("|");
|
|
17016
17019
|
let s = `${key} (${vals}`;
|
|
@@ -17094,8 +17097,11 @@ var CaptionSchema = external_exports.object({
|
|
|
17094
17097
|
});
|
|
17095
17098
|
var OptionSchema = external_exports.object({
|
|
17096
17099
|
content: external_exports.string().describe("Option text"),
|
|
17100
|
+
is_correct: external_exports.boolean().optional().describe("Mark this as the correct answer for a quiz (maps to score 1; other options score 0). Use this OR score, not both."),
|
|
17097
17101
|
score: external_exports.number().optional().describe("Score value for this option (for scored quizzes). Use 1 for correct, 0 for wrong. Scored options automatically enable correct-answer marking (record_scores) on the node."),
|
|
17098
|
-
scores
|
|
17102
|
+
// `scores` (category-based) is gated behind custom_scores (planned, #767) -
|
|
17103
|
+
// omitted from the schema while off so the MCP doesn't accept a not-live field.
|
|
17104
|
+
...FEATURE_FLAGS.ENABLE_CUSTOM_SCORES ? { scores: external_exports.record(external_exports.number()).optional().describe("Category-based scores keyed by category name (personality quizzes).") } : {}
|
|
17099
17105
|
});
|
|
17100
17106
|
var OPTIONS_DESCRIPTION = (() => {
|
|
17101
17107
|
const hints = ["Answer options."];
|
|
@@ -17125,6 +17131,14 @@ function applyNodeDefaults(node) {
|
|
|
17125
17131
|
const label = node.config?.button_text || buttonDefault;
|
|
17126
17132
|
node.options = [{ content: label }];
|
|
17127
17133
|
}
|
|
17134
|
+
if (node.options) {
|
|
17135
|
+
for (const o of node.options) {
|
|
17136
|
+
if (o.score == null && typeof o.is_correct === "boolean") {
|
|
17137
|
+
o.score = o.is_correct ? 1 : 0;
|
|
17138
|
+
}
|
|
17139
|
+
delete o.is_correct;
|
|
17140
|
+
}
|
|
17141
|
+
}
|
|
17128
17142
|
const supportsRecordScores = def?.config_schema?.properties?.choice?.properties?.record_scores !== void 0;
|
|
17129
17143
|
const hasScoredOptions = node.options?.some((o) => o.score != null || o.scores != null);
|
|
17130
17144
|
if (supportsRecordScores && hasScoredOptions) {
|
|
@@ -17276,7 +17290,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17276
17290
|
body: { tags }
|
|
17277
17291
|
});
|
|
17278
17292
|
}
|
|
17279
|
-
const
|
|
17293
|
+
const truncate2 = (s, n = 60) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
17280
17294
|
const lines = [
|
|
17281
17295
|
isPreview ? `Form created as a preview draft.` : `Form created and live.`,
|
|
17282
17296
|
``,
|
|
@@ -17284,7 +17298,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17284
17298
|
`Form ID: ${formId}`,
|
|
17285
17299
|
``,
|
|
17286
17300
|
`Node IDs (use directly with attach_node_media / update_node - no get_form round-trip needed):`,
|
|
17287
|
-
...createdNodes.map((n) => `- [${n.type}] "${
|
|
17301
|
+
...createdNodes.map((n) => `- [${n.type}] "${truncate2(n.prompt)}": ${n.id}`),
|
|
17288
17302
|
``,
|
|
17289
17303
|
isPreview ? `PREVIEW URL (draft - only you can open this, not yet visible to respondents): ${data.viewer_url}` : `FORM URL (live - share this with respondents): ${data.viewer_url}`
|
|
17290
17304
|
];
|
|
@@ -17335,6 +17349,130 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17335
17349
|
);
|
|
17336
17350
|
}
|
|
17337
17351
|
|
|
17352
|
+
// src/tools/import-form.ts
|
|
17353
|
+
function detectImportPlatform(url) {
|
|
17354
|
+
let parsed;
|
|
17355
|
+
try {
|
|
17356
|
+
parsed = new URL(url);
|
|
17357
|
+
} catch {
|
|
17358
|
+
return null;
|
|
17359
|
+
}
|
|
17360
|
+
const hostname2 = parsed.hostname.toLowerCase();
|
|
17361
|
+
if (hostname2 === "docs.google.com" || hostname2 === "forms.gle") {
|
|
17362
|
+
return "google";
|
|
17363
|
+
}
|
|
17364
|
+
if (hostname2.includes("typeform.com") && parsed.pathname.includes("/to/")) {
|
|
17365
|
+
return "typeform";
|
|
17366
|
+
}
|
|
17367
|
+
if (hostname2.includes("tally.so")) {
|
|
17368
|
+
return "tally";
|
|
17369
|
+
}
|
|
17370
|
+
return null;
|
|
17371
|
+
}
|
|
17372
|
+
function pluralQuestions(n) {
|
|
17373
|
+
return `${n} question${n === 1 ? "" : "s"}`;
|
|
17374
|
+
}
|
|
17375
|
+
function registerImportFormTool(server) {
|
|
17376
|
+
server.registerTool(
|
|
17377
|
+
"clipform_import_form",
|
|
17378
|
+
{
|
|
17379
|
+
title: "Import Form",
|
|
17380
|
+
description: `Convert a public Google Form, Typeform, or Tally form into a new Clipform. Supported URLs: Google Forms (docs.google.com or forms.gle - must be shared as "Anyone with the link"), Typeform (form.typeform.com/to/...), and Tally (tally.so - must be public). The source form must be publicly accessible; a private form's questions cannot be read. The new Clipform is created as a DRAFT - review it, then republish with clipform_update_form (is_live: true) once it's ready. Some question types don't map cleanly to a Clipform node and are skipped rather than guessed at; the result always reports exactly what was imported vs skipped, and why.`,
|
|
17381
|
+
inputSchema: {
|
|
17382
|
+
url: external_exports.string().url().describe("Public URL of the Google Form, Typeform, or Tally form to import")
|
|
17383
|
+
},
|
|
17384
|
+
outputSchema: {
|
|
17385
|
+
form_id: external_exports.string().nullable().describe("New form's UUID - pass to follow-up tools. Null when no importable questions were found (nothing was created)."),
|
|
17386
|
+
viewer_url: external_exports.string().nullable().describe("The address this form WILL be live at once published (clipform_update_form with is_live: true) - it is a draft and is NOT viewable by anyone yet; opening it now shows a maintenance screen, not the form. Null when no importable questions were found."),
|
|
17387
|
+
import_summary: external_exports.object({
|
|
17388
|
+
title: external_exports.string().describe("Title of the source form"),
|
|
17389
|
+
total_questions: external_exports.number().describe("Total questions found in the source form"),
|
|
17390
|
+
imported: external_exports.number().describe("Questions successfully converted into Clipform nodes"),
|
|
17391
|
+
skipped: external_exports.number().describe("Questions that could not be converted"),
|
|
17392
|
+
skipped_details: external_exports.array(external_exports.record(external_exports.string(), external_exports.string())).describe("Per-skipped-question title, reason, and source question type"),
|
|
17393
|
+
warnings: external_exports.array(external_exports.string()).describe("Non-fatal issues surfaced during the import")
|
|
17394
|
+
})
|
|
17395
|
+
},
|
|
17396
|
+
annotations: {
|
|
17397
|
+
readOnlyHint: false,
|
|
17398
|
+
destructiveHint: false,
|
|
17399
|
+
idempotentHint: false,
|
|
17400
|
+
// Creates a new, publicly-reachable-once-published form (same
|
|
17401
|
+
// reasoning as clipform_create_form's own annotation).
|
|
17402
|
+
openWorldHint: true
|
|
17403
|
+
}
|
|
17404
|
+
},
|
|
17405
|
+
async ({ url }) => {
|
|
17406
|
+
const platform = detectImportPlatform(url);
|
|
17407
|
+
if (!platform) {
|
|
17408
|
+
return errorResult(
|
|
17409
|
+
"Unsupported URL. clipform_import_form only supports Google Forms (docs.google.com or forms.gle), Typeform (form.typeform.com/to/...), and Tally (tally.so) links."
|
|
17410
|
+
);
|
|
17411
|
+
}
|
|
17412
|
+
const meResult = await callApi("/me", { method: "GET" });
|
|
17413
|
+
if (!meResult.ok) {
|
|
17414
|
+
return errorResult(`Unable to determine workspace: ${meResult.error}`);
|
|
17415
|
+
}
|
|
17416
|
+
const me = meResult.data;
|
|
17417
|
+
const workspaceId = me.workspace?.id ?? null;
|
|
17418
|
+
if (!workspaceId) {
|
|
17419
|
+
return errorResult(
|
|
17420
|
+
"No workspace available. This usually means authentication is misconfigured - check your API key or OAuth connection."
|
|
17421
|
+
);
|
|
17422
|
+
}
|
|
17423
|
+
const importResult = platform === "google" ? await callApi("/forms/import/google", { method: "POST", body: { url, workspace_id: workspaceId } }) : platform === "typeform" ? await callApi("/forms/import/typeform", { method: "POST", body: { url, workspace_id: workspaceId } }) : await callApi("/forms/import/tally", { method: "POST", body: { url, workspace_id: workspaceId } });
|
|
17424
|
+
if (!importResult.ok) {
|
|
17425
|
+
return errorResult(importResult.error);
|
|
17426
|
+
}
|
|
17427
|
+
const data = importResult.data;
|
|
17428
|
+
const summary = data.import_summary;
|
|
17429
|
+
const formId = data.form_id;
|
|
17430
|
+
const viewerUrl = data.url;
|
|
17431
|
+
const lines = [];
|
|
17432
|
+
if (!formId) {
|
|
17433
|
+
lines.push(
|
|
17434
|
+
`No importable questions were found in "${summary.title}" (${pluralQuestions(summary.total_questions)} total) - nothing was created.`
|
|
17435
|
+
);
|
|
17436
|
+
} else {
|
|
17437
|
+
lines.push(
|
|
17438
|
+
`Imported "${summary.title}": ${summary.imported} of ${pluralQuestions(summary.total_questions)} converted, ${summary.skipped} skipped.`
|
|
17439
|
+
);
|
|
17440
|
+
}
|
|
17441
|
+
if (summary.skipped_details.length > 0) {
|
|
17442
|
+
lines.push(
|
|
17443
|
+
``,
|
|
17444
|
+
`Skipped:`,
|
|
17445
|
+
...summary.skipped_details.map((s) => `- "${s.title}": ${s.reason}`)
|
|
17446
|
+
);
|
|
17447
|
+
}
|
|
17448
|
+
if (summary.warnings.length > 0) {
|
|
17449
|
+
lines.push(``, `Warnings:`, ...summary.warnings.map((w) => `- ${w}`));
|
|
17450
|
+
}
|
|
17451
|
+
if (formId) {
|
|
17452
|
+
lines.push(
|
|
17453
|
+
``,
|
|
17454
|
+
`Form ID: ${formId}`,
|
|
17455
|
+
`FUTURE LIVE URL (not viewable yet - the form is a draft; opening this now shows a maintenance screen): ${viewerUrl}`,
|
|
17456
|
+
`This form was created as a DRAFT. Review it, then publish with clipform_update_form (is_live: true) - only then does the URL above start serving the form to respondents.`,
|
|
17457
|
+
`Pass form_id (the UUID above, not a share_id) to follow-up tools (get_form, add_node, update_node, etc.).`
|
|
17458
|
+
);
|
|
17459
|
+
}
|
|
17460
|
+
return structuredResult(lines.join("\n"), {
|
|
17461
|
+
form_id: formId,
|
|
17462
|
+
viewer_url: viewerUrl,
|
|
17463
|
+
import_summary: {
|
|
17464
|
+
title: summary.title,
|
|
17465
|
+
total_questions: summary.total_questions,
|
|
17466
|
+
imported: summary.imported,
|
|
17467
|
+
skipped: summary.skipped,
|
|
17468
|
+
skipped_details: summary.skipped_details,
|
|
17469
|
+
warnings: summary.warnings
|
|
17470
|
+
}
|
|
17471
|
+
});
|
|
17472
|
+
}
|
|
17473
|
+
);
|
|
17474
|
+
}
|
|
17475
|
+
|
|
17338
17476
|
// src/tools/list-forms.ts
|
|
17339
17477
|
function registerListFormsTool(server) {
|
|
17340
17478
|
server.registerTool(
|
|
@@ -17365,7 +17503,9 @@ function registerListFormsTool(server) {
|
|
|
17365
17503
|
share_id: external_exports.string(),
|
|
17366
17504
|
is_live: external_exports.boolean(),
|
|
17367
17505
|
has_unpublished_changes: external_exports.boolean(),
|
|
17368
|
-
tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() }))
|
|
17506
|
+
tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() })),
|
|
17507
|
+
builder_url: external_exports.string().optional().describe("Dashboard Build tab for this form. Not present for an anonymous caller."),
|
|
17508
|
+
viewer_url: external_exports.string().optional().describe("Live respondent link. Only present for a published (is_live) form, and not for an anonymous caller.")
|
|
17369
17509
|
})
|
|
17370
17510
|
).describe("Matching forms (empty if none)"),
|
|
17371
17511
|
next_cursor: external_exports.string().nullable().describe("Pass as cursor to fetch the next page, or null when no more")
|
|
@@ -17398,7 +17538,9 @@ function registerListFormsTool(server) {
|
|
|
17398
17538
|
share_id: f.share_id,
|
|
17399
17539
|
is_live: f.is_live,
|
|
17400
17540
|
has_unpublished_changes: f.has_unpublished_changes,
|
|
17401
|
-
tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color }))
|
|
17541
|
+
tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color })),
|
|
17542
|
+
...f.builder_url ? { builder_url: f.builder_url } : {},
|
|
17543
|
+
...f.viewer_url ? { viewer_url: f.viewer_url } : {}
|
|
17402
17544
|
})),
|
|
17403
17545
|
next_cursor: data.next_cursor
|
|
17404
17546
|
};
|
|
@@ -17410,7 +17552,8 @@ function registerListFormsTool(server) {
|
|
|
17410
17552
|
for (const f of data.forms) {
|
|
17411
17553
|
const status = f.is_live ? f.has_unpublished_changes ? "live, unpublished changes" : "live" : "draft";
|
|
17412
17554
|
const tagStr = f.tags.length > 0 ? ` [${f.tags.map((t) => t.name).join(", ")}]` : "";
|
|
17413
|
-
|
|
17555
|
+
const linkStr = f.builder_url ? ` \u2014 ${f.builder_url}` : "";
|
|
17556
|
+
lines.push(`- ${f.title || "(untitled)"} (${status})${tagStr} \u2014 ${f.id}${linkStr}`);
|
|
17414
17557
|
}
|
|
17415
17558
|
if (data.next_cursor) {
|
|
17416
17559
|
lines.push(
|
|
@@ -17446,6 +17589,26 @@ function projectConfig(nodeType, config2) {
|
|
|
17446
17589
|
}
|
|
17447
17590
|
return projected;
|
|
17448
17591
|
}
|
|
17592
|
+
var FORM_URL_KEYS = [
|
|
17593
|
+
"viewer",
|
|
17594
|
+
"builder",
|
|
17595
|
+
"summary",
|
|
17596
|
+
"responses",
|
|
17597
|
+
"analytics",
|
|
17598
|
+
"share",
|
|
17599
|
+
"embed",
|
|
17600
|
+
"dynamic_link"
|
|
17601
|
+
];
|
|
17602
|
+
function projectUrls(data) {
|
|
17603
|
+
const raw = data.urls;
|
|
17604
|
+
if (!raw) return void 0;
|
|
17605
|
+
const urls = {};
|
|
17606
|
+
for (const key of FORM_URL_KEYS) {
|
|
17607
|
+
const value = raw[key];
|
|
17608
|
+
if (typeof value === "string") urls[key] = value;
|
|
17609
|
+
}
|
|
17610
|
+
return Object.keys(urls).length > 0 ? urls : void 0;
|
|
17611
|
+
}
|
|
17449
17612
|
function getFormNodes(data) {
|
|
17450
17613
|
return data.node ?? data.nodes ?? [];
|
|
17451
17614
|
}
|
|
@@ -17472,11 +17635,13 @@ function projectNext(node, logicByNode) {
|
|
|
17472
17635
|
}
|
|
17473
17636
|
function projectFormState(data) {
|
|
17474
17637
|
const { nodes, logicByNode } = orderNodesWithLogic(data);
|
|
17638
|
+
const urls = projectUrls(data);
|
|
17475
17639
|
return {
|
|
17476
17640
|
form_id: String(data.form_id ?? ""),
|
|
17477
17641
|
title: String(data.title ?? ""),
|
|
17478
17642
|
is_live: Boolean(data.is_live),
|
|
17479
17643
|
has_unpublished_changes: Boolean(data.has_unpublished_changes),
|
|
17644
|
+
...urls ? { urls } : {},
|
|
17480
17645
|
nodes: nodes.map((q) => {
|
|
17481
17646
|
const options = q.node_options ?? q.options;
|
|
17482
17647
|
const config2 = projectConfig(q.type, q.config);
|
|
@@ -17505,6 +17670,18 @@ function formatFormState(data) {
|
|
|
17505
17670
|
`Unpublished changes: yes - respondents still see the last published version. Send is_live:true via clipform_update_form to publish.`
|
|
17506
17671
|
);
|
|
17507
17672
|
}
|
|
17673
|
+
const urls = projectUrls(data);
|
|
17674
|
+
if (urls) {
|
|
17675
|
+
lines.push(``, `Links (exact values - constructed or guessed URLs return 404):`);
|
|
17676
|
+
if (urls.viewer) lines.push(` Viewer (respondent view): ${urls.viewer}`);
|
|
17677
|
+
if (urls.builder) lines.push(` Builder: ${urls.builder}`);
|
|
17678
|
+
if (urls.summary) lines.push(` Summary: ${urls.summary}`);
|
|
17679
|
+
if (urls.responses) lines.push(` Responses: ${urls.responses}`);
|
|
17680
|
+
if (urls.analytics) lines.push(` Analytics: ${urls.analytics}`);
|
|
17681
|
+
if (urls.share) lines.push(` Share: ${urls.share}`);
|
|
17682
|
+
if (urls.embed) lines.push(` Embed: ${urls.embed}`);
|
|
17683
|
+
if (urls.dynamic_link) lines.push(` Dynamic link: ${urls.dynamic_link}`);
|
|
17684
|
+
}
|
|
17508
17685
|
lines.push(``, `Nodes (in order):`);
|
|
17509
17686
|
for (let i = 0; i < nodes.length; i++) {
|
|
17510
17687
|
const q = nodes[i];
|
|
@@ -17544,6 +17721,16 @@ function registerGetFormTool(server) {
|
|
|
17544
17721
|
title: external_exports.string().describe("Form title"),
|
|
17545
17722
|
is_live: external_exports.boolean().describe("Whether the form is published"),
|
|
17546
17723
|
has_unpublished_changes: external_exports.boolean().describe("Live form has edits not yet republished"),
|
|
17724
|
+
urls: external_exports.object({
|
|
17725
|
+
viewer: external_exports.string().optional().describe("Live respondent link, or a scoped preview link for an anonymous caller's draft. Omitted for a non-anonymous caller's unpublished draft."),
|
|
17726
|
+
builder: external_exports.string().optional().describe("Build tab - the node/canvas editor. Not present for an anonymous caller."),
|
|
17727
|
+
summary: external_exports.string().optional().describe("Summary tab - form overview and key stats. Not present for an anonymous caller."),
|
|
17728
|
+
responses: external_exports.string().optional().describe("Responses tab - individual submissions. Not present for an anonymous caller."),
|
|
17729
|
+
analytics: external_exports.string().optional().describe("Analytics tab - views, starts, completion rate, drop-off per node. Not present for an anonymous caller."),
|
|
17730
|
+
share: external_exports.string().optional().describe("Share tab - link, QR, social. Not present for an anonymous caller."),
|
|
17731
|
+
embed: external_exports.string().optional().describe("Embed setup for websites. Not present for an anonymous caller."),
|
|
17732
|
+
dynamic_link: external_exports.string().optional().describe("Dynamic/personalized link setup. Not present for an anonymous caller.")
|
|
17733
|
+
}).optional().describe("Dashboard/viewer deeplinks for this form. An anonymous caller gets only 'viewer'."),
|
|
17547
17734
|
nodes: external_exports.array(
|
|
17548
17735
|
external_exports.object({
|
|
17549
17736
|
id: external_exports.string(),
|
|
@@ -17875,7 +18062,8 @@ function registerGetResultsTool(server) {
|
|
|
17875
18062
|
})
|
|
17876
18063
|
).optional().describe("Most recent open-text answers, capped at 20 - open questions only")
|
|
17877
18064
|
})
|
|
17878
|
-
).describe("Nodes in form order. Contact/file/draw/camera/payment nodes carry only total_responses/total_views - no per-response detail.")
|
|
18065
|
+
).describe("Nodes in form order. Contact/file/draw/camera/payment nodes carry only total_responses/total_views - no per-response detail."),
|
|
18066
|
+
results_url: external_exports.string().optional().describe("Dashboard Responses tab for this form. Not present for an anonymous caller.")
|
|
17879
18067
|
},
|
|
17880
18068
|
annotations: {
|
|
17881
18069
|
readOnlyHint: true,
|
|
@@ -17893,13 +18081,13 @@ function registerGetResultsTool(server) {
|
|
|
17893
18081
|
const nodes = (data.nodes || []).map(projectNode);
|
|
17894
18082
|
const structured = {
|
|
17895
18083
|
total_responses: data.total_responses,
|
|
17896
|
-
nodes
|
|
18084
|
+
nodes,
|
|
18085
|
+
...data.results_url ? { results_url: data.results_url } : {}
|
|
17897
18086
|
};
|
|
17898
18087
|
if (data.total_responses === 0) {
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
);
|
|
18088
|
+
const lines2 = [`This form has no responses yet (${nodes.length} node(s)).`];
|
|
18089
|
+
if (data.results_url) lines2.push(`View in the dashboard: ${data.results_url}`);
|
|
18090
|
+
return structuredResult(lines2.join("\n"), structured);
|
|
17903
18091
|
}
|
|
17904
18092
|
const lines = [`${data.total_responses} total response(s) across ${nodes.length} node(s).`];
|
|
17905
18093
|
const highlights = nodes.filter((n) => "options" in n || "recent_answers" in n).slice(0, 3);
|
|
@@ -17911,11 +18099,157 @@ function registerGetResultsTool(server) {
|
|
|
17911
18099
|
lines.push(`- ${n.prompt}: ${n.recent_answers.length} recent open-text answer(s)`);
|
|
17912
18100
|
}
|
|
17913
18101
|
}
|
|
18102
|
+
if (data.results_url) lines.push(`View in the dashboard: ${data.results_url}`);
|
|
17914
18103
|
return structuredResult(lines.join("\n"), structured);
|
|
17915
18104
|
}
|
|
17916
18105
|
);
|
|
17917
18106
|
}
|
|
17918
18107
|
|
|
18108
|
+
// src/tools/get-responses.ts
|
|
18109
|
+
var MAX_ANSWER_LENGTH = 300;
|
|
18110
|
+
var DEFAULT_LIMIT = 20;
|
|
18111
|
+
var MAX_LIMIT = 50;
|
|
18112
|
+
var REDACTED_TYPES = {
|
|
18113
|
+
details: "[contact details - view in dashboard]",
|
|
18114
|
+
contact: "[contact details - view in dashboard]",
|
|
18115
|
+
file_upload: "[file upload - view in dashboard]",
|
|
18116
|
+
payment: "[payment - view in dashboard]",
|
|
18117
|
+
open: ""
|
|
18118
|
+
// open is redacted ONLY when the answer is a recording (handled below)
|
|
18119
|
+
};
|
|
18120
|
+
function truncate(s, n = MAX_ANSWER_LENGTH) {
|
|
18121
|
+
return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
18122
|
+
}
|
|
18123
|
+
function looksLikeMediaReference(display) {
|
|
18124
|
+
return /https?:\/\/|storage\/|\.(webm|mp4|mp3|wav|m4a|mov)\b/i.test(display);
|
|
18125
|
+
}
|
|
18126
|
+
function registerGetResponsesTool(server) {
|
|
18127
|
+
server.registerTool(
|
|
18128
|
+
"clipform_get_responses",
|
|
18129
|
+
{
|
|
18130
|
+
title: "List form responses",
|
|
18131
|
+
description: "List a Clipform form's individual responses (submissions) with time filtering - complements clipform_get_results (aggregates). Answers questions like 'what came in during the last 24 hours', 'what's the longest answer', 'what did people say'. Text answers only: contact details, file uploads, payments, and audio/video answers are redacted to placeholders - point the user at the dashboard for those. Responses are sorted newest first unless sort: 'oldest'.",
|
|
18132
|
+
inputSchema: {
|
|
18133
|
+
form_id: external_exports.string().describe("Form UUID (from clipform_list_forms / clipform_create_form)"),
|
|
18134
|
+
since: external_exports.string().datetime({ offset: true }).optional().describe("Only responses submitted at or after this ISO 8601 time (e.g. '2026-08-29T00:00:00Z')"),
|
|
18135
|
+
until: external_exports.string().datetime({ offset: true }).optional().describe("Only responses submitted at or before this ISO 8601 time"),
|
|
18136
|
+
query: external_exports.string().optional().describe("Only responses whose text answers contain this string (case-insensitive)"),
|
|
18137
|
+
sort: external_exports.enum(["newest", "oldest"]).optional().describe("Order by submission time. Default: newest"),
|
|
18138
|
+
limit: external_exports.number().int().min(1).max(MAX_LIMIT).optional().describe(`Max responses to return (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT})`)
|
|
18139
|
+
},
|
|
18140
|
+
outputSchema: {
|
|
18141
|
+
total_matching: external_exports.number().describe("Responses matching the filters (before the limit cap)"),
|
|
18142
|
+
returned: external_exports.number().describe("Responses in this result"),
|
|
18143
|
+
locked_over_plan_limit: external_exports.number().describe("Matching responses hidden by the plan's response limit"),
|
|
18144
|
+
responses: external_exports.array(
|
|
18145
|
+
external_exports.object({
|
|
18146
|
+
submitted_at: external_exports.string().describe("ISO 8601 submission time"),
|
|
18147
|
+
status: external_exports.string(),
|
|
18148
|
+
answers: external_exports.array(
|
|
18149
|
+
external_exports.object({
|
|
18150
|
+
question: external_exports.string(),
|
|
18151
|
+
type: external_exports.string(),
|
|
18152
|
+
answer: external_exports.string().describe("Text answer, or a redaction placeholder")
|
|
18153
|
+
})
|
|
18154
|
+
)
|
|
18155
|
+
})
|
|
18156
|
+
).describe("Matching responses, filtered/sorted/capped per the inputs")
|
|
18157
|
+
},
|
|
18158
|
+
annotations: {
|
|
18159
|
+
readOnlyHint: true,
|
|
18160
|
+
destructiveHint: false,
|
|
18161
|
+
idempotentHint: true,
|
|
18162
|
+
openWorldHint: false
|
|
18163
|
+
}
|
|
18164
|
+
},
|
|
18165
|
+
async ({ form_id, since, until, query, sort, limit }) => {
|
|
18166
|
+
const [subsResult, formResult] = await Promise.all([
|
|
18167
|
+
callApi(`/forms/${form_id}/submissions`),
|
|
18168
|
+
callApi(`/forms/${form_id}`)
|
|
18169
|
+
]);
|
|
18170
|
+
if (!subsResult.ok) {
|
|
18171
|
+
return errorResult(`Could not fetch responses: ${subsResult.error}`);
|
|
18172
|
+
}
|
|
18173
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
18174
|
+
if (formResult.ok) {
|
|
18175
|
+
const nodes = formResult.data?.nodes ?? [];
|
|
18176
|
+
for (const n of nodes) nodeById.set(n.id, n);
|
|
18177
|
+
}
|
|
18178
|
+
const rows = subsResult.data ?? [];
|
|
18179
|
+
const sinceMs = since ? Date.parse(since) : null;
|
|
18180
|
+
const untilMs = until ? Date.parse(until) : null;
|
|
18181
|
+
const needle = query?.toLowerCase() ?? null;
|
|
18182
|
+
let lockedCount = 0;
|
|
18183
|
+
const projected = [];
|
|
18184
|
+
for (const row of rows) {
|
|
18185
|
+
if (row.locked) {
|
|
18186
|
+
lockedCount++;
|
|
18187
|
+
continue;
|
|
18188
|
+
}
|
|
18189
|
+
const submittedAt = row.completed_at ?? row.created_at;
|
|
18190
|
+
if (!submittedAt) continue;
|
|
18191
|
+
const ms = Date.parse(submittedAt);
|
|
18192
|
+
if (sinceMs !== null && ms < sinceMs) continue;
|
|
18193
|
+
if (untilMs !== null && ms > untilMs) continue;
|
|
18194
|
+
const answers = [];
|
|
18195
|
+
for (const [nodeId, resp] of Object.entries(row.node_responses ?? {})) {
|
|
18196
|
+
const node = nodeById.get(nodeId);
|
|
18197
|
+
const type = node?.type ?? (typeof resp?.type === "string" ? resp.type : "unknown");
|
|
18198
|
+
const question = truncate(node?.prompt || type, 120);
|
|
18199
|
+
const placeholder = REDACTED_TYPES[type];
|
|
18200
|
+
if (placeholder) {
|
|
18201
|
+
answers.push({ question, type, answer: placeholder });
|
|
18202
|
+
continue;
|
|
18203
|
+
}
|
|
18204
|
+
const display = typeof resp?.display === "string" ? resp.display : "";
|
|
18205
|
+
if (!display) continue;
|
|
18206
|
+
if (looksLikeMediaReference(display)) {
|
|
18207
|
+
answers.push({ question, type, answer: "[recording - view in dashboard]" });
|
|
18208
|
+
continue;
|
|
18209
|
+
}
|
|
18210
|
+
answers.push({ question, type, answer: truncate(display) });
|
|
18211
|
+
}
|
|
18212
|
+
if (needle !== null) {
|
|
18213
|
+
const haystack = answers.map((a) => a.answer.toLowerCase()).join("\n");
|
|
18214
|
+
if (!haystack.includes(needle)) continue;
|
|
18215
|
+
}
|
|
18216
|
+
projected.push({
|
|
18217
|
+
submitted_at: submittedAt,
|
|
18218
|
+
submitted_ms: ms,
|
|
18219
|
+
status: row.status ?? "unknown",
|
|
18220
|
+
answers
|
|
18221
|
+
});
|
|
18222
|
+
}
|
|
18223
|
+
projected.sort(
|
|
18224
|
+
(a, b) => sort === "oldest" ? a.submitted_ms - b.submitted_ms : b.submitted_ms - a.submitted_ms
|
|
18225
|
+
);
|
|
18226
|
+
const totalMatching = projected.length;
|
|
18227
|
+
const capped = projected.slice(0, limit ?? DEFAULT_LIMIT).map(({ submitted_ms: _ms, ...rest }) => rest);
|
|
18228
|
+
const filterBits = [
|
|
18229
|
+
since ? `since ${since}` : null,
|
|
18230
|
+
until ? `until ${until}` : null,
|
|
18231
|
+
query ? `matching "${query}"` : null
|
|
18232
|
+
].filter(Boolean);
|
|
18233
|
+
const lines = [
|
|
18234
|
+
`${totalMatching} response(s)${filterBits.length ? ` ${filterBits.join(", ")}` : ""}${lockedCount > 0 ? ` (+${lockedCount} over the plan's response limit - not shown; upgrade or view counts in clipform_get_results)` : ""}. Showing ${capped.length}, ${sort === "oldest" ? "oldest" : "newest"} first.`,
|
|
18235
|
+
``,
|
|
18236
|
+
...capped.flatMap((r) => [
|
|
18237
|
+
`\u2014 ${r.submitted_at} [${r.status}]`,
|
|
18238
|
+
...r.answers.map((a) => ` ${a.question}: ${a.answer}`)
|
|
18239
|
+
]),
|
|
18240
|
+
``,
|
|
18241
|
+
`Contact details, files, payments, and recordings are redacted here - the dashboard shows full response detail. For aggregate stats use clipform_get_results.`
|
|
18242
|
+
];
|
|
18243
|
+
return structuredResult(lines.join("\n"), {
|
|
18244
|
+
total_matching: totalMatching,
|
|
18245
|
+
returned: capped.length,
|
|
18246
|
+
locked_over_plan_limit: lockedCount,
|
|
18247
|
+
responses: capped
|
|
18248
|
+
});
|
|
18249
|
+
}
|
|
18250
|
+
);
|
|
18251
|
+
}
|
|
18252
|
+
|
|
17919
18253
|
// src/tools/whoami.ts
|
|
17920
18254
|
function registerWhoamiTool(server) {
|
|
17921
18255
|
server.registerTool(
|
|
@@ -17934,7 +18268,11 @@ function registerWhoamiTool(server) {
|
|
|
17934
18268
|
node_limit: external_exports.number().nullable().describe("Max nodes per form, or null if unlimited"),
|
|
17935
18269
|
form_limit: external_exports.number().nullable().describe("Max forms in the workspace, or null if unlimited")
|
|
17936
18270
|
}).describe("Plan limits for the active workspace"),
|
|
17937
|
-
workspaces: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string() })).optional().describe("All workspaces in the caller's company - only present for session (dashboard) auth")
|
|
18271
|
+
workspaces: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string() })).optional().describe("All workspaces in the caller's company - only present for session (dashboard) auth"),
|
|
18272
|
+
links: external_exports.object({
|
|
18273
|
+
dashboard: external_exports.string().describe("Dashboard home for the active workspace."),
|
|
18274
|
+
forms: external_exports.string().describe("Forms list for the active workspace.")
|
|
18275
|
+
}).optional().describe("Workspace-level dashboard links - only present for a non-anonymous caller.")
|
|
17938
18276
|
},
|
|
17939
18277
|
annotations: {
|
|
17940
18278
|
readOnlyHint: true,
|
|
@@ -17968,9 +18306,16 @@ function registerWhoamiTool(server) {
|
|
|
17968
18306
|
if (Array.isArray(me.workspaces)) {
|
|
17969
18307
|
structured.workspaces = me.workspaces.map((w) => ({ id: w.id, name: w.name }));
|
|
17970
18308
|
}
|
|
18309
|
+
if (me.links && typeof me.links.dashboard === "string" && typeof me.links.forms === "string") {
|
|
18310
|
+
structured.links = { dashboard: me.links.dashboard, forms: me.links.forms };
|
|
18311
|
+
}
|
|
17971
18312
|
const workspaceLine = workspace ? `workspace '${workspace.name}'` : "no workspace (anonymous)";
|
|
17972
|
-
const
|
|
17973
|
-
|
|
18313
|
+
const lines = [`Auth: ${authMode}. ${workspaceLine}. Plan: ${plan.name}.`];
|
|
18314
|
+
if (structured.links) {
|
|
18315
|
+
const links = structured.links;
|
|
18316
|
+
lines.push(`Dashboard: ${links.dashboard}`, `Forms list: ${links.forms}`);
|
|
18317
|
+
}
|
|
18318
|
+
return structuredResult(lines.join("\n"), structured);
|
|
17974
18319
|
}
|
|
17975
18320
|
);
|
|
17976
18321
|
}
|
|
@@ -20185,7 +20530,9 @@ Optional args by type:
|
|
|
20185
20530
|
- funnel: outcomes, criteria, needs_contact (true)
|
|
20186
20531
|
- testimonial: use_case
|
|
20187
20532
|
- application: role
|
|
20188
|
-
- booking: event_name, event_type
|
|
20533
|
+
- booking: event_name, event_type
|
|
20534
|
+
- intake: use_case, response_format (all)
|
|
20535
|
+
- lead-capture: use_case`,
|
|
20189
20536
|
inputSchema: {
|
|
20190
20537
|
type: external_exports.string().describe("Form type to get the build workflow for (accepts aliases like 'feedback' \u2192 survey)"),
|
|
20191
20538
|
variant: external_exports.enum(QUIZ_VARIANTS).optional().describe("Quiz sub-variant. Only used when type is 'quiz'. Omit for a standard scored quiz."),
|
|
@@ -20281,12 +20628,14 @@ function createServer(options) {
|
|
|
20281
20628
|
);
|
|
20282
20629
|
};
|
|
20283
20630
|
registerCreateFormTool(server);
|
|
20631
|
+
registerImportFormTool(server);
|
|
20284
20632
|
registerListFormsTool(server);
|
|
20285
20633
|
registerWhoamiTool(server);
|
|
20286
20634
|
registerGetFormTool(server);
|
|
20287
20635
|
registerUpdateFormTool(server);
|
|
20288
20636
|
registerDeleteFormTool(server);
|
|
20289
20637
|
registerGetResultsTool(server);
|
|
20638
|
+
registerGetResponsesTool(server);
|
|
20290
20639
|
registerAddNodeTool(server);
|
|
20291
20640
|
registerUpdateNodeTool(server);
|
|
20292
20641
|
registerDeleteNodeTool(server);
|
|
@@ -20321,4 +20670,4 @@ export {
|
|
|
20321
20670
|
RENDER_TIMING,
|
|
20322
20671
|
createServer
|
|
20323
20672
|
};
|
|
20324
|
-
//# sourceMappingURL=chunk-
|
|
20673
|
+
//# sourceMappingURL=chunk-NCWQ42GI.js.map
|