@clipform/mcp-server 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,7 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
64
64
  | Tool | Description |
65
65
  |------|-------------|
66
66
  | `clipform_create_form` | Create a new Clipform (interactive video-style form). |
67
+ | `clipform_import_form` | Convert a public Google Form, Typeform, or Tally form into a new Clipform. |
67
68
  | `clipform_list_forms` | List forms in your workspace with optional filtering. |
68
69
  | `clipform_whoami` | Show the current identity: auth mode (api key, session, or anonymous), active workspace, and plan limits. |
69
70
  | `clipform_get_form` | Retrieve a form's details including all nodes in sequential order and their routing. |
@@ -6,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-NYMC63ZK.js";
9
+ } from "./chunk-WMLM53A3.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -14,7 +14,7 @@ import {
14
14
  getGuideUri,
15
15
  guideFallbackText,
16
16
  registerResources
17
- } from "./chunk-6EHOUAPE.js";
17
+ } from "./chunk-L3DIBX4G.js";
18
18
  import {
19
19
  ACTIVE_NODE_TYPES,
20
20
  CONTACT_FIELDS,
@@ -35,7 +35,7 @@ import {
35
35
  resolveFormType,
36
36
  structuredResult,
37
37
  textResult
38
- } from "./chunk-3BYQLPTT.js";
38
+ } from "./chunk-YAI5LQPF.js";
39
39
  import {
40
40
  getMcpAuth,
41
41
  runWithMcpTool
@@ -17335,6 +17335,130 @@ Example: A form that asks a question, collects contact info, then finishes:
17335
17335
  );
17336
17336
  }
17337
17337
 
17338
+ // src/tools/import-form.ts
17339
+ function detectImportPlatform(url) {
17340
+ let parsed;
17341
+ try {
17342
+ parsed = new URL(url);
17343
+ } catch {
17344
+ return null;
17345
+ }
17346
+ const hostname2 = parsed.hostname.toLowerCase();
17347
+ if (hostname2 === "docs.google.com" || hostname2 === "forms.gle") {
17348
+ return "google";
17349
+ }
17350
+ if (hostname2.includes("typeform.com") && parsed.pathname.includes("/to/")) {
17351
+ return "typeform";
17352
+ }
17353
+ if (hostname2.includes("tally.so")) {
17354
+ return "tally";
17355
+ }
17356
+ return null;
17357
+ }
17358
+ function pluralQuestions(n) {
17359
+ return `${n} question${n === 1 ? "" : "s"}`;
17360
+ }
17361
+ function registerImportFormTool(server) {
17362
+ server.registerTool(
17363
+ "clipform_import_form",
17364
+ {
17365
+ title: "Import Form",
17366
+ 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.`,
17367
+ inputSchema: {
17368
+ url: external_exports.string().url().describe("Public URL of the Google Form, Typeform, or Tally form to import")
17369
+ },
17370
+ outputSchema: {
17371
+ 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)."),
17372
+ 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."),
17373
+ import_summary: external_exports.object({
17374
+ title: external_exports.string().describe("Title of the source form"),
17375
+ total_questions: external_exports.number().describe("Total questions found in the source form"),
17376
+ imported: external_exports.number().describe("Questions successfully converted into Clipform nodes"),
17377
+ skipped: external_exports.number().describe("Questions that could not be converted"),
17378
+ skipped_details: external_exports.array(external_exports.record(external_exports.string(), external_exports.string())).describe("Per-skipped-question title, reason, and source question type"),
17379
+ warnings: external_exports.array(external_exports.string()).describe("Non-fatal issues surfaced during the import")
17380
+ })
17381
+ },
17382
+ annotations: {
17383
+ readOnlyHint: false,
17384
+ destructiveHint: false,
17385
+ idempotentHint: false,
17386
+ // Creates a new, publicly-reachable-once-published form (same
17387
+ // reasoning as clipform_create_form's own annotation).
17388
+ openWorldHint: true
17389
+ }
17390
+ },
17391
+ async ({ url }) => {
17392
+ const platform = detectImportPlatform(url);
17393
+ if (!platform) {
17394
+ return errorResult(
17395
+ "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."
17396
+ );
17397
+ }
17398
+ const meResult = await callApi("/me", { method: "GET" });
17399
+ if (!meResult.ok) {
17400
+ return errorResult(`Unable to determine workspace: ${meResult.error}`);
17401
+ }
17402
+ const me = meResult.data;
17403
+ const workspaceId = me.workspace?.id ?? null;
17404
+ if (!workspaceId) {
17405
+ return errorResult(
17406
+ "No workspace available. This usually means authentication is misconfigured - check your API key or OAuth connection."
17407
+ );
17408
+ }
17409
+ 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 } });
17410
+ if (!importResult.ok) {
17411
+ return errorResult(importResult.error);
17412
+ }
17413
+ const data = importResult.data;
17414
+ const summary = data.import_summary;
17415
+ const formId = data.form_id;
17416
+ const viewerUrl = data.url;
17417
+ const lines = [];
17418
+ if (!formId) {
17419
+ lines.push(
17420
+ `No importable questions were found in "${summary.title}" (${pluralQuestions(summary.total_questions)} total) - nothing was created.`
17421
+ );
17422
+ } else {
17423
+ lines.push(
17424
+ `Imported "${summary.title}": ${summary.imported} of ${pluralQuestions(summary.total_questions)} converted, ${summary.skipped} skipped.`
17425
+ );
17426
+ }
17427
+ if (summary.skipped_details.length > 0) {
17428
+ lines.push(
17429
+ ``,
17430
+ `Skipped:`,
17431
+ ...summary.skipped_details.map((s) => `- "${s.title}": ${s.reason}`)
17432
+ );
17433
+ }
17434
+ if (summary.warnings.length > 0) {
17435
+ lines.push(``, `Warnings:`, ...summary.warnings.map((w) => `- ${w}`));
17436
+ }
17437
+ if (formId) {
17438
+ lines.push(
17439
+ ``,
17440
+ `Form ID: ${formId}`,
17441
+ `FUTURE LIVE URL (not viewable yet - the form is a draft; opening this now shows a maintenance screen): ${viewerUrl}`,
17442
+ `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.`,
17443
+ `Pass form_id (the UUID above, not a share_id) to follow-up tools (get_form, add_node, update_node, etc.).`
17444
+ );
17445
+ }
17446
+ return structuredResult(lines.join("\n"), {
17447
+ form_id: formId,
17448
+ viewer_url: viewerUrl,
17449
+ import_summary: {
17450
+ title: summary.title,
17451
+ total_questions: summary.total_questions,
17452
+ imported: summary.imported,
17453
+ skipped: summary.skipped,
17454
+ skipped_details: summary.skipped_details,
17455
+ warnings: summary.warnings
17456
+ }
17457
+ });
17458
+ }
17459
+ );
17460
+ }
17461
+
17338
17462
  // src/tools/list-forms.ts
17339
17463
  function registerListFormsTool(server) {
17340
17464
  server.registerTool(
@@ -20281,6 +20405,7 @@ function createServer(options) {
20281
20405
  );
20282
20406
  };
20283
20407
  registerCreateFormTool(server);
20408
+ registerImportFormTool(server);
20284
20409
  registerListFormsTool(server);
20285
20410
  registerWhoamiTool(server);
20286
20411
  registerGetFormTool(server);
@@ -20321,4 +20446,4 @@ export {
20321
20446
  RENDER_TIMING,
20322
20447
  createServer
20323
20448
  };
20324
- //# sourceMappingURL=chunk-3GBZFAEM.js.map
20449
+ //# sourceMappingURL=chunk-44FHF2MF.js.map