@clipform/mcp-server 2.18.0 → 3.2.1

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
@@ -82,17 +82,16 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
82
82
  | `clipform_delete_node_media` | Remove media from a node. |
83
83
  | `clipform_set_logic` | Set the next node for one or more nodes, wiring a linear route (A to B to C). |
84
84
  | `clipform_log_generation` | Internal audit step run by a form-generation workflow. |
85
- | `clipform_search_news` | Fallback news lookup for clients without native web search. |
85
+ | `clipform_search_news` | Search recent news articles from NewsAPI and The Guardian and return them as structured results. |
86
86
  | `clipform_generate_tts` | Generate narration audio from text with word-level captions. |
87
- | `clipform_generate_video` | Generate a video from images, video clips, or both, synced to an audio track. |
88
87
  | `clipform_search_media` | Search images or stock video clips. |
89
- | `clipform_render_composition` | Render a specialised video composition to MP4 or PNG - custom animated visuals that clipform_generate_video can't provide, such as geography animations or designed motion graphics. |
88
+ | `clipform_render_composition` | Render a specialised video composition to MP4 or PNG - custom animated visuals that a video template can't provide, such as geography animations or designed motion graphics. |
90
89
  | `clipform_search_music` | Search for royalty-free music tracks and ambient sounds. |
91
90
  | `clipform_list_compositions` | Browse available video compositions and their expected props schemas. |
92
91
  | `clipform_list_video_templates` | Browse available video templates - curated Scene arrangements (bed + overlay + sane defaults) that render through the Scene composition from a small controls object. |
93
92
  | `clipform_render_video_template` | Render a curated video template (a pre-arranged Scene: bed + overlay + sane defaults) to MP4 or PNG from a small controls object, instead of hand-assembling Scene layers. |
94
93
  | `clipform_list_assets` | List available creative assets (sound effects, animations, fonts) for video compositions. |
95
- | `clipform_check_render` | Check the status of render jobs started by clipform_generate_video, clipform_render_video_template, or clipform_render_composition. |
94
+ | `clipform_check_render` | Check the status of render jobs started by clipform_render_video_template or clipform_render_composition. |
96
95
  | `clipform_fetch_boundary` | Fetch a GeoJSON boundary polygon for a country, city, or region. |
97
96
  | `clipform_get_guide` | Retrieve craft knowledge for building a specific form type. |
98
97
  | `clipform_get_workflow` | Retrieve a step-by-step build workflow for creating a specific form type. |
@@ -103,7 +102,7 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
103
102
 
104
103
  ## How it works
105
104
 
106
- **Remote (OAuth):** Bearer tokens are audience-bound to `https://mcp.clipform.io` (RFC 8707) and scoped to `mcp` only. Forms land directly in the workspace you approved during consent.
105
+ **Remote (OAuth):** Bearer tokens are audience-bound to `https://mcp.clipform.io` (RFC 8707) and scoped to `mcp` only. Forms land directly in the workspace you approved during consent by default. One connection reaches every workspace you belong to in the same company: pass `workspace_id` (ids from `clipform_whoami`) on `clipform_create_form`, `clipform_import_form`, `clipform_list_forms`, `clipform_upload_media_asset`, or `clipform_generate_tts`, or a `form_id` from another workspace on any form tool - including `clipform_upload_media_asset`, `clipform_generate_tts`, and `clipform_complete_media_upload`, where it's a routing hint only (the result still lands in that workspace's media library, not on a node). Membership is re-checked on every call, and results name the workspace.
107
106
 
108
107
  **Local with API key:** The `CLIPFORM_API_KEY` is sent as a standard `Authorization: Bearer` header. Forms are created directly in the key's workspace with your plan tier limits.
109
108
 
@@ -75,7 +75,7 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
75
75
 
76
76
  ## How it works
77
77
 
78
- **Remote (OAuth):** Bearer tokens are audience-bound to `{{MCP_URL}}` (RFC 8707) and scoped to `mcp` only. Forms land directly in the workspace you approved during consent.
78
+ **Remote (OAuth):** Bearer tokens are audience-bound to `{{MCP_URL}}` (RFC 8707) and scoped to `mcp` only. Forms land directly in the workspace you approved during consent by default. One connection reaches every workspace you belong to in the same company: pass `workspace_id` (ids from `clipform_whoami`) on `clipform_create_form`, `clipform_import_form`, `clipform_list_forms`, `clipform_upload_media_asset`, or `clipform_generate_tts`, or a `form_id` from another workspace on any form tool - including `clipform_upload_media_asset`, `clipform_generate_tts`, and `clipform_complete_media_upload`, where it's a routing hint only (the result still lands in that workspace's media library, not on a node). Membership is re-checked on every call, and results name the workspace.
79
79
 
80
80
  **Local with API key:** The `CLIPFORM_API_KEY` is sent as a standard `Authorization: Bearer` header. Forms are created directly in the key's workspace with your plan tier limits.
81
81
 
@@ -17,6 +17,19 @@ interface McpAuthContext {
17
17
  is_anonymous?: boolean;
18
18
  auth_via?: "oauth";
19
19
  mcp_mint_proof?: string;
20
+ resolveWorkspace?: (target: {
21
+ workspace_id?: string;
22
+ form_id?: string;
23
+ }) => Promise<{
24
+ workspace_id: string;
25
+ workspace_name: string;
26
+ api_key: string;
27
+ } | null>;
28
+ listWorkspaces?: () => Promise<{
29
+ id: string;
30
+ name: string;
31
+ }[]>;
32
+ default_workspace_id?: string;
20
33
  }
21
34
  /**
22
35
  * Marker header the in-process MCP tool layer sets on its outgoing API
@@ -5,7 +5,7 @@ import {
5
5
  getMcpAuth,
6
6
  runWithMcpAuth,
7
7
  runWithMcpTool
8
- } from "./chunk-HZMMOSEM.js";
8
+ } from "./chunk-A5CFGL27.js";
9
9
  import "./chunk-G3PMV62Z.js";
10
10
  export {
11
11
  MCP_ANONYMOUS_HEADER,
@@ -5,7 +5,7 @@ import {
5
5
  FULL_CAPS,
6
6
  callApi,
7
7
  getSessionContext
8
- } from "./chunk-RKYB6247.js";
8
+ } from "./chunk-VZKMOMFJ.js";
9
9
 
10
10
  // src/lib/guides.ts
11
11
  var guidesPromise = null;
@@ -143,4 +143,4 @@ export {
143
143
  getGuideUri,
144
144
  registerResources
145
145
  };
146
- //# sourceMappingURL=chunk-CHSPV2VI.js.map
146
+ //# sourceMappingURL=chunk-236IGTEY.js.map
@@ -24,4 +24,4 @@ export {
24
24
  getMcpAuth,
25
25
  runWithMcpTool
26
26
  };
27
- //# sourceMappingURL=chunk-HZMMOSEM.js.map
27
+ //# sourceMappingURL=chunk-A5CFGL27.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/auth-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Identity of the end-user a given MCP request is acting on behalf of.\n *\n * Populated by the in-process host (apps/api) after validating an OAuth\n * Bearer on /mcp. The MCP tool layer reads it to decide which workspace\n * forms should land in and which plan tier gates apply.\n *\n * NOT populated when the server runs as a local stdio process (npx) - in\n * that mode tools fall back to edit-token based auth.\n */\nexport interface McpAuthContext {\n user_id: string;\n workspace_id: string;\n scopes: string[];\n tool_name?: string;\n api_key?: string;\n // Set when the human behind this tool call is an anonymous Supabase\n // session (apps/api's app-chat lane, #2983/#2984) - api-client.ts's\n // getAuthHeaders reads it to mark the outgoing bot-key HTTP request so the\n // API can propagate the restriction even though the request itself\n // authenticates with an API key, not a JWT.\n is_anonymous?: boolean;\n // Set on the OAuth branch of apps/api's mcp/handler.ts when this request's\n // bot API key was resolved on behalf of an OAuth-connected user (#3716) -\n // api-client.ts's getAuthHeaders reads it to mark the outgoing request so\n // /v1/me can report auth_mode \"oauth\" instead of the generic \"api_key\" the\n // bot-key lane would otherwise get, even though the request itself\n // authenticates with an API key, not an OAuth token.\n auth_via?: \"oauth\";\n // #3330 review fix (blocker 1): an HMAC proof (apps/api's mcp-mint-proof.ts)\n // that THIS exact request's #1415 anon lane freshly minted a brand-new anon\n // identity for `workspace_id` - set only on the mint branch of\n // resolveAnonymousMcpAuth, never when auth was resolved via an inbound\n // form_id (even the caller's own). Lets create-form.ts safely offer a\n // same-UID claim link without ever deriving \"who to bind it to\" from a\n // body-supplied id, which would let a batch smuggling another tenant's\n // form_id alongside a create_form call mint a claim token for THAT\n // tenant's identity instead of the caller's own.\n mcp_mint_proof?: string;\n // #3822: set ONLY on the OAuth branch of apps/api's mcp/handler.ts. Lets a\n // single tool call target another workspace the connected user belongs to\n // (per-call, never per-request - see the #3330 note in the package\n // CLAUDE.md). The host re-checks membership on every call and returns that\n // workspace's own bot key, or null to mean \"act in the default workspace\n // unchanged\" (target is the default, or a form_id the user can't reach -\n // the API then answers exactly as it did before targeting existed). Anon + stdio contexts never carry it, so they\n // stay pinned to one workspace.\n resolveWorkspace?: (target: {\n workspace_id?: string;\n form_id?: string;\n }) => Promise<{ workspace_id: string; workspace_name: string; api_key: string } | null>;\n // #3822: the workspaces the connected user may target (same set\n // resolveWorkspace accepts). OAuth lane only.\n listWorkspaces?: () => Promise<{ id: string; name: string }[]>;\n // #3822: the workspace picked at authorize time - what a call with no\n // `workspace_id` acts in. OAuth lane only (workspace_id above is swapped\n // per call when a target is resolved, this never is).\n default_workspace_id?: string;\n}\n\n/**\n * Marker header the in-process MCP tool layer sets on its outgoing API\n * requests (api-client.ts) when `is_anonymous` is true, so an anonymous\n * app-chat session can't launder itself into an unrestricted API-key caller\n * by asking the bot to publish/mint credentials on its behalf. apps/api's\n * authMiddleware / workspaceAccessMiddleware / requireWorkspaceWriteAuth\n * read this on the API-key path and set isAnonymous accordingly - trusting\n * it is safe because it can only RESTRICT a caller: a real integration that\n * sets this header on its own API-key requests only denies itself, it can\n * never use it to gain anonymous-JWT privileges it doesn't already have.\n */\nexport const MCP_ANONYMOUS_HEADER = \"x-clipform-anonymous\";\n\n/**\n * Marker header the in-process MCP tool layer sets on its outgoing API\n * requests (api-client.ts) when `auth_via` is \"oauth\", so /v1/me can report\n * `auth_mode: \"oauth\"` for an OAuth-connected user instead of the generic\n * \"api_key\" the bot-key lane would otherwise get (#3716). Label-only: it\n * changes nothing but the reported string. Trusting it from the wire is\n * safe because a direct API-key caller who sets this header on their own\n * request only mislabels their own whoami output - they gain no privilege,\n * exactly the same reasoning as MCP_ANONYMOUS_HEADER above (except that\n * header can only restrict, and this one can only relabel).\n */\nexport const MCP_AUTH_VIA_HEADER = \"x-clipform-auth-via\";\n\n/**\n * Carries `mcp_mint_proof` on the same outgoing bot-key request. Unlike\n * MCP_ANONYMOUS_HEADER (a boolean that can only ever RESTRICT), this header's\n * VALUE matters - it names an identity a claim token gets bound to - so its\n * value is HMAC-signed (mcp-mint-proof.ts) rather than trusted raw: a direct\n * external caller reaching `POST /v1/forms` with any valid workspace API key\n * could otherwise hand-set an unsigned \"mint uid\" header to an arbitrary\n * victim's uid themselves (#3330 review).\n */\nexport const MCP_MINT_PROOF_HEADER = \"x-clipform-mcp-mint-proof\";\n\nconst storage = new AsyncLocalStorage<McpAuthContext>();\n\nexport function runWithMcpAuth<T>(ctx: McpAuthContext, fn: () => Promise<T> | T): Promise<T> | T {\n return storage.run(ctx, fn);\n}\n\nexport function getMcpAuth(): McpAuthContext | undefined {\n return storage.getStore();\n}\n\nexport function runWithMcpTool<T>(toolName: string, fn: () => Promise<T> | T): Promise<T> | T {\n const parent = storage.getStore();\n if (!parent) return fn();\n return storage.run({ ...parent, tool_name: toolName }, fn);\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AAyE3B,IAAM,uBAAuB;AAa7B,IAAM,sBAAsB;AAW5B,IAAM,wBAAwB;AAErC,IAAM,UAAU,IAAI,kBAAkC;AAE/C,SAAS,eAAkB,KAAqB,IAA0C;AAC/F,SAAO,QAAQ,IAAI,KAAK,EAAE;AAC5B;AAEO,SAAS,aAAyC;AACvD,SAAO,QAAQ,SAAS;AAC1B;AAEO,SAAS,eAAkB,UAAkB,IAA0C;AAC5F,QAAM,SAAS,QAAQ,SAAS;AAChC,MAAI,CAAC,OAAQ,QAAO,GAAG;AACvB,SAAO,QAAQ,IAAI,EAAE,GAAG,QAAQ,WAAW,SAAS,GAAG,EAAE;AAC3D;","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  WORKFLOW_TYPES,
4
4
  getDiscoveryParams,
5
5
  getSessionContextWithAuth
6
- } from "./chunk-RKYB6247.js";
6
+ } from "./chunk-VZKMOMFJ.js";
7
7
  import {
8
8
  __export
9
9
  } from "./chunk-G3PMV62Z.js";
@@ -4100,7 +4100,7 @@ function resourceLink(name, uri, title) {
4100
4100
  }
4101
4101
  var BUILD_ONCE_STEP = "**Build the whole form in ONE clipform_create_form call** - pass nodes, settings, and tags together; do NOT create an empty form and add nodes one by one:";
4102
4102
  var TAGS_LINE = "- tags: one format (quiz/survey/interview/feedback/lead-gen), one genre (trivia/personality/nps/poll/testimonial), and 2-3 topic words";
4103
- var ATTACH_MEDIA_STEP = "**Attach media**: pass node_id (and form_id) on each render call (clipform_generate_video / clipform_render_video_template / clipform_render_composition) so it attaches to the node automatically once the render finishes - do not poll clipform_check_render to completion, just fire the renders and move on. Fall back to the manual two-step only for a render that didn't take a node_id: first clipform_upload_media_asset (batch all uploads in one call, include captions), then clipform_attach_node_media (batch all attachments in one call), setting show_captions: true and fit_media: true (generated renders must not be cropped).";
4103
+ var ATTACH_MEDIA_STEP = "**Attach media**: pass node_id (and form_id) on each render call (clipform_render_video_template / clipform_render_composition) so it attaches to the node automatically once the render finishes - do not poll clipform_check_render to completion, just fire the renders and move on. Fall back to the manual two-step only for a render that didn't take a node_id: first clipform_upload_media_asset (batch all uploads in one call, include captions), then clipform_attach_node_media (batch all attachments in one call), setting show_captions: true and fit_media: true (generated renders must not be cropped).";
4104
4104
  var REPUBLISH_STEP = "**Republish** with clipform_update_form (is_live: true) - forms are live from creation, but media and edits added since are only visible in the live form after republishing. Never skip this when you attached anything.";
4105
4105
  var LOG_STEP = "**Log** with clipform_log_generation";
4106
4106
  var TEXT_ONLY_OVERRIDE = "\n\n> **Text-only override:** If the user explicitly asked for text-only (no video), skip the video production and media attachment steps. Still generate narration.";
@@ -4130,7 +4130,7 @@ var quizWorkflow = (args, sessionContext, authMode, caps) => {
4130
4130
  ${TAGS_LINE}`,
4131
4131
  caps.media && "**Generate narration** with clipform_generate_tts - batch ALL questions in a single call (pass an array of items). Tease the question - do NOT reveal the answer or read options aloud. Keep each narration 5-15 seconds.",
4132
4132
  caps.media && `**Build video** for each question - pass each question's node_id (and form_id) on the render call so it attaches automatically once done. Pick per question; mixing treatments across one quiz is fine:
4133
- - Default - narrated slideshow: clipform_search_media (kind: "image", 3 images per question, batch all queries in one call), then clipform_generate_video with wait: false per question - fires all renders in parallel - then collect URLs with clipform_check_render.
4133
+ - Default - narrated slideshow: clipform_search_media (kind: "image", 3 images per question, batch all queries in one call), then clipform_render_video_template (template: "slideshow", controls.items + controls.audio_url) with wait: false per question - fires all renders in parallel - then collect URLs with clipform_check_render.
4134
4134
  - Designed visual - when a recipe or composition fits the question or the user asks for one (e.g. a versus, a grid reveal, guess-the-city): recipe book first - clipform_list_video_templates and match the mechanic to a recipe, then clipform_render_video_template with its curated controls (wait: false, one call per question). Only if no recipe fits, drop to the raw primitives - clipform_list_compositions, then ONE clipform_render_composition call with items (all clips render in parallel). Collect job_ids with clipform_check_render. Both take narration via sound.narration.`,
4135
4135
  caps.media && ATTACH_MEDIA_STEP,
4136
4136
  REPUBLISH_STEP,
@@ -4208,7 +4208,7 @@ var comprehensionQuizWorkflow = (args, sessionContext, authMode, caps) => {
4208
4208
  caps.media && `**Generate narration** with clipform_generate_tts - batch ALL questions in a single call. Reference the video naturally: "If you watched closely, you'll know this one..." Keep each narration 5-10 seconds.`,
4209
4209
  caps.media && `**Build video** for each question:
4210
4210
  - clipform_search_media (kind: "image") - 3 images per question, batch all queries in one call
4211
- - clipform_generate_video with wait: false per question - fires all renders in parallel - then collect URLs with clipform_check_render`,
4211
+ - clipform_render_video_template (template: "slideshow", controls.items + controls.audio_url) with wait: false per question - fires all renders in parallel - then collect URLs with clipform_check_render`,
4212
4212
  caps.media && ATTACH_MEDIA_STEP,
4213
4213
  REPUBLISH_STEP,
4214
4214
  caps.research && `${LOG_STEP} - include the YouTube URL and video title/channel as sources, using whatever of those the user provided`
@@ -4276,7 +4276,7 @@ var interviewWorkflow = (args, sessionContext, authMode, caps) => {
4276
4276
  - Consent node - "I agree that my response may be used in [context]"` : ""}
4277
4277
  - End screen that sets expectations: "Thanks! We'll be in touch."
4278
4278
  ${TAGS_LINE}`,
4279
- caps.media && `**Optional - Rich Media:** Add TTS narration (clipform_generate_tts, batch all questions in one call) with a warm, inviting tone - "We'd love to hear your story..." Video backgrounds (clipform_search_media \u2192 clipform_generate_video) on the intro question can set a professional tone.`,
4279
+ caps.media && `**Optional - Rich Media:** Add TTS narration (clipform_generate_tts, batch all questions in one call) with a warm, inviting tone - "We'd love to hear your story..." Video backgrounds (clipform_search_media \u2192 clipform_render_video_template slideshow) on the intro question can set a professional tone.`,
4280
4280
  REPUBLISH_STEP,
4281
4281
  caps.research && LOG_STEP
4282
4282
  ]);
@@ -4300,7 +4300,7 @@ var surveyWorkflow = (args, sessionContext, authMode, caps) => {
4300
4300
  - Contact collection (type: "details") - name + email for follow-up` : ""}
4301
4301
  - End screen: "Thanks for your feedback!"
4302
4302
  ${TAGS_LINE}`,
4303
- caps.media && "**Optional - Rich Media:** For a more engaging experience, add TTS narration (clipform_generate_tts) for the intro or key questions, video backgrounds (clipform_search_media \u2192 clipform_generate_video), or background music (clipform_search_music).",
4303
+ caps.media && "**Optional - Rich Media:** For a more engaging experience, add TTS narration (clipform_generate_tts) for the intro or key questions, video backgrounds (clipform_search_media \u2192 clipform_render_video_template slideshow), or background music (clipform_search_music).",
4304
4304
  REPUBLISH_STEP,
4305
4305
  caps.research && LOG_STEP
4306
4306
  ]);
@@ -4356,7 +4356,7 @@ var applicationWorkflow = (args, sessionContext, authMode, caps) => {
4356
4356
  - Optional "Anything else?" (type: "open") - catches things you didn't think to ask.
4357
4357
  - End screen: "Your application has been submitted. We'll review it and get back to you." icon: "tick", no score, no restart
4358
4358
  - tags: format "application", genre "screening", and 2-3 topic words`,
4359
- caps.media && `**Optional - Rich Media:** Add TTS narration (clipform_generate_tts) - professional and welcoming: "Thanks for your interest - let's get to know you." Video backgrounds (clipform_search_media \u2192 clipform_generate_video) on the intro can make the application feel more personal.`,
4359
+ caps.media && `**Optional - Rich Media:** Add TTS narration (clipform_generate_tts) - professional and welcoming: "Thanks for your interest - let's get to know you." Video backgrounds (clipform_search_media \u2192 clipform_render_video_template slideshow) on the intro can make the application feel more personal.`,
4360
4360
  REPUBLISH_STEP,
4361
4361
  caps.research && LOG_STEP
4362
4362
  ]);
@@ -4378,7 +4378,7 @@ var bookingWorkflow = (args, sessionContext, authMode, caps) => {
4378
4378
  - 1-3 event-specific questions (type: "choice") - session preferences, dietary requirements, experience level. Only ask what you'll use.
4379
4379
  - End screen: "You're registered! Here's what to expect." Include event details (date, time, location/link) in the message. icon: "tick", no score, no restart, cta_type: "external_link" with a URL to calendar invite or event page (if available)
4380
4380
  - tags: format "registration", genre "${eventType}", and 2-3 topic words`,
4381
- caps.media && "**Optional: add welcome video** - a short intro from the host. Generate narration with clipform_generate_tts, then build a video with clipform_search_media + clipform_generate_video.",
4381
+ caps.media && "**Optional: add welcome video** - a short intro from the host. Generate narration with clipform_generate_tts, then build a video with clipform_search_media + clipform_render_video_template (slideshow).",
4382
4382
  REPUBLISH_STEP,
4383
4383
  caps.research && LOG_STEP
4384
4384
  ]);
@@ -4404,7 +4404,7 @@ var intakeWorkflow = (args, sessionContext, authMode, caps) => {
4404
4404
  - Optional "Anything else?" (type: "open") - catches what you didn't think to ask
4405
4405
  - End screen: "Thanks! We've got your details and we'll be in touch shortly." icon: "tick", no score, no restart
4406
4406
  - tags: format "intake", genre "onboarding", and 2-3 topic words`,
4407
- caps.media && "**Optional: add a welcome video** - a short personal intro from you. Generate narration with clipform_generate_tts, then build a video with clipform_search_media + clipform_generate_video.",
4407
+ caps.media && "**Optional: add a welcome video** - a short personal intro from you. Generate narration with clipform_generate_tts, then build a video with clipform_search_media + clipform_render_video_template (slideshow).",
4408
4408
  REPUBLISH_STEP,
4409
4409
  caps.research && LOG_STEP
4410
4410
  ]);
@@ -4856,4 +4856,4 @@ export {
4856
4856
  getWorkflowText,
4857
4857
  registerPrompts
4858
4858
  };
4859
- //# sourceMappingURL=chunk-WWIQEIFF.js.map
4859
+ //# sourceMappingURL=chunk-EQ6WNIA5.js.map