figura-mcp 0.3.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +21 -10
  2. package/dist/index.js +1 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
4
4
  the **Figura** visualization SaaS to MCP clients (Claude Code, Claude Desktop,
5
- etc.). It speaks to the Figura `/api/v1` HTTP API with a Bearer `fig_` token over
5
+ etc.). It speaks to the Figura `/v1` HTTP API with a Bearer `fig_` token over
6
6
  stdio.
7
7
 
8
8
  ## Token
@@ -13,7 +13,7 @@ The server reads the token from, in order:
13
13
  2. `~/.figura/config.json` (`{ "token": "fig_..." }`) — the same file the
14
14
  `figura-cli` `figura login` command writes.
15
15
 
16
- Base URL defaults to `https://figura.so`; override with `FIGURA_API_URL`.
16
+ Base URL defaults to `https://api.figura.so`; override with `FIGURA_API_URL`.
17
17
  All requests carry the `X-Figura-Source: mcp` header.
18
18
 
19
19
  ## Add to Claude Code
@@ -30,19 +30,30 @@ the server will fall back to `~/.figura/config.json`.
30
30
 
31
31
  ## Tools
32
32
 
33
- | Tool | Input |
34
- | ------------------- | -------------------------------------------------------------------------------------------------- |
35
- | `figura_whoami` | `{}` |
36
- | `figura_create_fig` | `html`, optional `title`, `description`, `area`, `tags[]`, `orientation`, `mode`, `model`, `issue` |
37
- | `figura_list_figs` | optional `area`, `tag`, `limit`, `cursor` |
38
- | `figura_get_fig` | `id` |
39
- | `figura_comment` | `fig_id`, `body` |
33
+ | Tool | Input |
34
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------- |
35
+ | `figura_whoami` | `{}` |
36
+ | `figura_set_brand` | `name`, optional `description`, `tokensBlock` |
37
+ | `figura_create_fig` | `html`, optional `title`, `description`, `area`, `tags[]`, `orientation`, `mode`, `model`, `issue`, `publish` |
38
+ | `figura_generate_fig` | `brief`, optional `title`, `area`, `orientation`, `mode`, `fidelity`, `publish` |
39
+ | `figura_revise_fig` | `id`, `html`, optional `title`, `description`, … |
40
+ | `figura_list_revisions`| `id` |
41
+ | `figura_update_fig` | `id`, optional `title`, `description`, `area`, `tags[]`, `status` |
42
+ | `figura_publish_fig` | `id`, optional `unpublish` |
43
+ | `figura_delete_fig` | `id`, optional `hard` |
44
+ | `figura_list_figs` | optional `area`, `tag`, `limit`, `cursor` |
45
+ | `figura_get_fig` | `id` |
46
+ | `figura_comment` | `fig_id`, `body` |
47
+ | `figura_request_feedback` | `id`, `from`, optional `note` |
48
+ | `figura_list_sources` | `{}` |
49
+ | `figura_sync_source` | optional `id` |
40
50
 
41
51
  Enum values:
42
52
 
43
- - `area`: `feed | profile | create | voices | capture | other | general`
53
+ - `area`: free text (a product-area hint, e.g. `feed`, `settings`, `other`) not a fixed enum.
44
54
  - `orientation`: `portrait | landscape | both`
45
55
  - `mode`: `single | compare | variations`
56
+ - `fidelity` (`figura_generate_fig`): `fast | high`
46
57
 
47
58
  Each tool returns its JSON result as a text content block; failures return
48
59
  `isError: true` with the server's error message.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{promises as _,readFileSync as D}from"node:fs";import{homedir as M}from"node:os";import{join as U}from"node:path";import x from"axios";import{McpServer as l}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as m}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as K}from"zod";var N="https://api.figura.so",n="mcp",T=U(M(),".figura","config.json");function I(){return(process.env.FIGURA_API_URL||N).replace(/\/+$/,"")}async function R(){if(process.env.FIGURA_TOKEN)return process.env.FIGURA_TOKEN;try{let B=await _.readFile(T,"utf8"),J=JSON.parse(B);if(J&&typeof J==="object"&&typeof J.token==="string")return J.token}catch{}return}async function X(B,J={}){let Q=await R();if(!Q)throw Error("No Figura token. Set FIGURA_TOKEN or run `figura login` to create ~/.figura/config.json.");let P=`${I()}${B.startsWith("/")?"":"/"}${B}`,C={Accept:"application/json",Authorization:`Bearer ${Q}`,"X-Figura-Source":n},L;if(J.body!==void 0)C["Content-Type"]="application/json",L=JSON.stringify(J.body);let w=await x.request({url:P,method:J.method??"GET",headers:C,data:L,validateStatus:()=>!0,transformRequest:[(G)=>G],transformResponse:[(G)=>G]}),V=typeof w.data==="string"?w.data:"",$=void 0;if(V)try{$=JSON.parse(V)}catch{$=V}if(w.status<200||w.status>=300){let G=$&&typeof $==="object"&&$!==null&&"error"in $?String($.error):typeof $==="string"&&$?$:w.statusText||`HTTP ${w.status}`;throw Error(`${G} (HTTP ${w.status})`)}return $}function Y(B){return{content:[{type:"text",text:JSON.stringify(B,null,2)}]}}function Z(B){return{content:[{type:"text",text:B instanceof Error?B.message:String(B)}],isError:!0}}var H=["feed","profile","create","voices","capture","other","general"],j=["portrait","landscape","both"],q=["single","compare","variations"],O=(()=>{try{let B=JSON.parse(D(new URL("../package.json",import.meta.url),"utf8"));return typeof B.version==="string"?B.version:"0.0.0"}catch{return"0.0.0"}})(),W=new l({name:"figura",version:O});W.registerTool("figura_whoami",{title:"Figura: whoami",description:"Return the identity (team, actor, scopes) for the configured Figura token.",inputSchema:{}},async()=>{try{return Y(await X("/v1/whoami"))}catch(B){return Z(B)}});W.registerTool("figura_set_brand",{title:"Figura: set brand profile",description:'Set (upsert) the team brand profile that the fig generator renders against. Use this to "introduce a repo to Figura": scan the codebase for its design system, synthesize the brand, then push it here. `name` is required; `appDescription` is a one-clause product summary; `tokensBlock` is the freeform design-token reference (colors + hex + role, type scale + faces, spacing, the brand gradient, plus voice/conventions). This is a partial merge — fields you omit keep their saved value (pass an explicit empty string to clear one), so a name-only update will not wipe existing tokens. Editing the team brand — confirm with the user before calling.',inputSchema:{name:K.string().describe("Brand / app name (required)."),appDescription:K.string().optional().describe("One clause on what the product is + who it is for."),tokensBlock:K.string().optional().describe("Freeform plain-text design-token reference: colors (hex + role), type scale + faces, spacing, the brand gradient, and voice/conventions. Injected verbatim into the fig-generation prompt.")}},async(B)=>{try{let J={name:B.name};if(B.appDescription!==void 0)J.appDescription=B.appDescription;if(B.tokensBlock!==void 0)J.tokensBlock=B.tokensBlock;return Y(await X("/brand",{method:"PUT",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_create_fig",{title:"Figura: create fig",description:"Create a new visualization from an HTML document. Returns the created fig (id, slug); with publish:true it is published and the result includes a shareable public url.",inputSchema:{html:K.string().describe("The full HTML document for the visualization."),title:K.string().optional().describe("Title (otherwise derived from the HTML)."),description:K.string().optional(),area:K.enum(H).optional().describe("Product area the fig belongs to."),tags:K.array(K.string()).optional(),orientation:K.enum(j).optional(),mode:K.enum(q).optional(),model:K.string().optional().describe("Model attribution."),issue:K.number().optional().describe("Linked issue number."),publish:K.boolean().optional().describe("Publish to a shareable public URL on create (requires a paid plan).")}},async(B)=>{try{let J={html:B.html};for(let Q of["title","description","area","tags","orientation","mode","model","issue","publish"])if(B[Q]!==void 0)J[Q]=B[Q];return Y(await X("/v1/figs",{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_generate_fig",{title:"Figura: generate fig",description:"Generate a new fig server-side from a natural-language brief, then persist it. The engine runs Figura's on-brand generation — you do NOT write the HTML; just describe the screen. Returns the created fig (id, slug); publish:true publishes it with a shareable url. Requires an active paid subscription.",inputSchema:{prompt:K.string().describe("A natural-language brief describing the screen/UI to generate."),area:K.enum(H).optional().describe("Product area the fig belongs to."),orientation:K.enum(j).optional(),mode:K.enum(q).optional(),title:K.string().optional().describe("Title (otherwise derived from the generated HTML)."),fidelity:K.enum(["fast","high"]).optional().describe("Generation fidelity (default fast)."),publish:K.boolean().optional().describe("Publish to a shareable public URL on create (requires a paid plan).")}},async(B)=>{try{let J={prompt:B.prompt};for(let Q of["area","orientation","mode","title","fidelity","publish"])if(B[Q]!==void 0)J[Q]=B[Q];return Y(await X("/v1/figs/generate",{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_revise_fig",{title:"Figura: revise fig",description:"Create a new revision (version) of an existing fig from updated HTML. Returns the new fig (id, slug, version, rootFigId); publish:true publishes it with a shareable url. The new version joins the original fig's chain — it does not replace it in place.",inputSchema:{id:K.string().describe("The fig id to create a new revision of."),html:K.string().describe("The full updated HTML document."),title:K.string().optional().describe("Title (otherwise derived from the HTML)."),description:K.string().optional(),area:K.enum(H).optional().describe("Product area; inherited from the parent if omitted."),tags:K.array(K.string()).optional(),orientation:K.enum(j).optional().describe("Inherited from the parent if omitted."),mode:K.enum(q).optional(),model:K.string().optional().describe("Model attribution."),issue:K.number().optional().describe("Linked issue number."),publish:K.boolean().optional().describe("Publish the new revision to a shareable public URL (requires a paid plan).")}},async(B)=>{try{let J={html:B.html};for(let Q of["title","description","area","tags","orientation","mode","model","issue","publish"])if(B[Q]!==void 0)J[Q]=B[Q];return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/revise`,{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_list_revisions",{title:"Figura: list revisions",description:"List the full revision chain for a fig (all versions sharing a root), oldest to newest.",inputSchema:{id:K.string().describe("Any fig id in the chain.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/revisions`))}catch(J){return Z(J)}});W.registerTool("figura_publish_fig",{title:"Figura: publish fig",description:"Publish (or unpublish) an existing fig to its shareable public URL. Requires a paid plan. Returns { isPublic, url, publicId }.",inputSchema:{id:K.string().describe("The fig id to publish."),isPublic:K.boolean().optional().describe("Set false to unpublish (make private). Defaults to true (publish).")}},async(B)=>{try{let J={};if(B.isPublic!==void 0)J.isPublic=B.isPublic;return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/publish`,{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_list_figs",{title:"Figura: list figs",description:"List visualizations for the team, optionally filtered by area or tag.",inputSchema:{area:K.enum(H).optional(),tag:K.string().optional(),limit:K.number().optional(),cursor:K.string().optional()}},async(B)=>{try{let J=new URLSearchParams;if(B.area)J.set("area",B.area);if(B.tag)J.set("tag",B.tag);if(B.limit!==void 0)J.set("limit",String(B.limit));if(B.cursor)J.set("cursor",B.cursor);let Q=J.toString();return Y(await X(`/v1/figs${Q?`?${Q}`:""}`))}catch(J){return Z(J)}});W.registerTool("figura_get_fig",{title:"Figura: get fig",description:"Fetch a single visualization by id.",inputSchema:{id:K.string().describe("The fig id.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}`))}catch(J){return Z(J)}});W.registerTool("figura_comment",{title:"Figura: comment",description:"Add a comment to a visualization.",inputSchema:{fig_id:K.string().describe("The fig id to comment on."),body:K.string().describe("Comment text.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.fig_id)}/comments`,{method:"POST",body:{body:B.body}}))}catch(J){return Z(J)}});W.registerTool("figura_request_feedback",{title:"Figura: request feedback",description:"Ask a teammate to review a fig. Resolve `from` to a team member (by name or email); they get a notification (in-app, push, email) and the discussion happens in the fig's comment thread. Use this when the user says they want a specific person's feedback/eyes/review on a fig. Returns { ok, request: { to, status }, figUrl }.",inputSchema:{id:K.string().describe("The fig id to request feedback on."),from:K.string().describe("The teammate to ask, by name or email (resolved within the team)."),note:K.string().optional().describe("Optional message for them (e.g. what to look at).")}},async(B)=>{try{let J={from:B.from};if(B.note!==void 0)J.note=B.note;return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/request-feedback`,{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_list_sources",{title:"Figura: list sources",description:"List the company-brain source-spine connectors for the team, with event counts and last-synced times.",inputSchema:{}},async()=>{try{return Y(await X("/v1/sources"))}catch(B){return Z(B)}});W.registerTool("figura_sync_source",{title:"Figura: sync source",description:"Ingest new events from a connected source into the spine. Returns { ingested, fetched, nextCursor }.",inputSchema:{id:K.string().describe("The source id to sync (from figura_list_sources).")}},async(B)=>{try{return Y(await X(`/v1/sources/${encodeURIComponent(B.id)}/sync`,{method:"POST"}))}catch(J){return Z(J)}});W.registerTool("figura_update_fig",{title:"Figura: update fig",description:"Update a fig's metadata in place (title/description/area/tags/status) and optionally replace its HTML. Set status='archived' to soft-delete.",inputSchema:{id:K.string().describe("The fig id to update."),title:K.string().optional(),description:K.string().optional(),area:K.enum(H).optional(),tags:K.array(K.string()).optional().describe("Replaces the existing tags."),status:K.enum(["draft","published","archived"]).optional(),html:K.string().optional().describe("Replace the rendered HTML in place.")}},async(B)=>{try{let J={};if(B.title!==void 0)J.title=B.title;if(B.description!==void 0)J.description=B.description;if(B.area!==void 0)J.area=B.area;if(B.tags!==void 0)J.tags=B.tags;if(B.status!==void 0)J.status=B.status;if(B.html!==void 0)J.html=B.html;return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}`,{method:"PATCH",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_delete_fig",{title:"Figura: delete fig",description:"Archive a fig (soft-delete; leaves the library, public link stops resolving) or permanently delete it with hard=true.",inputSchema:{id:K.string().describe("The fig id to delete."),hard:K.boolean().optional().describe("Permanently delete instead of archive.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}${B.hard?"?hard=true":""}`,{method:"DELETE"}))}catch(J){return Z(J)}});await W.connect(new m);
2
+ import{promises as _,readFileSync as D}from"node:fs";import{homedir as M}from"node:os";import{join as U}from"node:path";import x from"axios";import{McpServer as A}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as R}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as K}from"zod";var l="https://api.figura.so",m="mcp",N=U(M(),".figura","config.json");function n(){return(process.env.FIGURA_API_URL||l).replace(/\/+$/,"")}async function E(){if(process.env.FIGURA_TOKEN)return process.env.FIGURA_TOKEN;try{let B=await _.readFile(N,"utf8"),J=JSON.parse(B);if(J&&typeof J==="object"&&typeof J.token==="string")return J.token}catch{}return}async function X(B,J={}){let Q=await E();if(!Q)throw Error("No Figura token. Set FIGURA_TOKEN or run `figura login` to create ~/.figura/config.json.");let P=`${n()}${B.startsWith("/")?"":"/"}${B}`,C={Accept:"application/json",Authorization:`Bearer ${Q}`,"X-Figura-Source":m},L;if(J.body!==void 0)C["Content-Type"]="application/json",L=JSON.stringify(J.body);let w=await x.request({url:P,method:J.method??"GET",headers:C,data:L,validateStatus:()=>!0,transformRequest:[(G)=>G],transformResponse:[(G)=>G]}),V=typeof w.data==="string"?w.data:"",$=void 0;if(V)try{$=JSON.parse(V)}catch{$=V}if(w.status<200||w.status>=300){let G=$&&typeof $==="object"&&$!==null&&"error"in $?String($.error):typeof $==="string"&&$?$:w.statusText||`HTTP ${w.status}`;throw Error(`${G} (HTTP ${w.status})`)}return $}function Y(B){return{content:[{type:"text",text:JSON.stringify(B,null,2)}]}}function Z(B){return{content:[{type:"text",text:B instanceof Error?B.message:String(B)}],isError:!0}}var H=()=>K.string().optional().describe("Product area / surface the fig belongs to, e.g. 'dashboard', 'settings', 'onboarding'. Free text."),j=["portrait","landscape","both"],q=["single","compare","variations"],T=(()=>{try{let B=JSON.parse(D(new URL("../package.json",import.meta.url),"utf8"));return typeof B.version==="string"?B.version:"0.0.0"}catch{return"0.0.0"}})(),W=new A({name:"figura",version:T});W.registerTool("figura_whoami",{title:"Figura: whoami",description:"Return the identity (team, actor, scopes) for the configured Figura token.",inputSchema:{}},async()=>{try{return Y(await X("/v1/whoami"))}catch(B){return Z(B)}});W.registerTool("figura_set_brand",{title:"Figura: set brand profile",description:'Set (upsert) the team brand profile that the fig generator renders against. Use this to "introduce a repo to Figura": scan the codebase for its design system, synthesize the brand, then push it here. `name` is required; `appDescription` is a one-clause product summary; `tokensBlock` is the freeform design-token reference (colors + hex + role, type scale + faces, spacing, the brand gradient, plus voice/conventions). This is a partial merge — fields you omit keep their saved value (pass an explicit empty string to clear one), so a name-only update will not wipe existing tokens. Editing the team brand — confirm with the user before calling.',inputSchema:{name:K.string().describe("Brand / app name (required)."),appDescription:K.string().optional().describe("One clause on what the product is + who it is for."),tokensBlock:K.string().optional().describe("Freeform plain-text design-token reference: colors (hex + role), type scale + faces, spacing, the brand gradient, and voice/conventions. Injected verbatim into the fig-generation prompt.")}},async(B)=>{try{let J={name:B.name};if(B.appDescription!==void 0)J.appDescription=B.appDescription;if(B.tokensBlock!==void 0)J.tokensBlock=B.tokensBlock;return Y(await X("/brand",{method:"PUT",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_create_fig",{title:"Figura: create fig",description:"Create a new visualization from an HTML document. Returns the created fig (id, slug); with publish:true it is published and the result includes a shareable public url.",inputSchema:{html:K.string().describe("The full HTML document for the visualization."),title:K.string().optional().describe("Title (otherwise derived from the HTML)."),description:K.string().optional(),area:H(),tags:K.array(K.string()).optional(),orientation:K.enum(j).optional(),mode:K.enum(q).optional(),model:K.string().optional().describe("Model attribution."),issue:K.number().optional().describe("Linked issue number."),publish:K.boolean().optional().describe("Publish to a shareable public URL on create (requires a paid plan).")}},async(B)=>{try{let J={html:B.html};for(let Q of["title","description","area","tags","orientation","mode","model","issue","publish"])if(B[Q]!==void 0)J[Q]=B[Q];return Y(await X("/v1/figs",{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_generate_fig",{title:"Figura: generate fig",description:"Generate a new fig server-side from a natural-language brief, then persist it. The engine runs Figura's on-brand generation — you do NOT write the HTML; just describe the screen. Returns the created fig (id, slug); publish:true publishes it with a shareable url. Requires an active paid subscription.",inputSchema:{prompt:K.string().describe("A natural-language brief describing the screen/UI to generate."),area:H(),orientation:K.enum(j).optional(),mode:K.enum(q).optional(),title:K.string().optional().describe("Title (otherwise derived from the generated HTML)."),fidelity:K.enum(["fast","high"]).optional().describe("Generation fidelity (default fast)."),publish:K.boolean().optional().describe("Publish to a shareable public URL on create (requires a paid plan).")}},async(B)=>{try{let J={prompt:B.prompt};for(let Q of["area","orientation","mode","title","fidelity","publish"])if(B[Q]!==void 0)J[Q]=B[Q];return Y(await X("/v1/figs/generate",{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_revise_fig",{title:"Figura: revise fig",description:"Create a new revision (version) of an existing fig from updated HTML. Returns the new fig (id, slug, version, rootFigId); publish:true publishes it with a shareable url. The new version joins the original fig's chain — it does not replace it in place.",inputSchema:{id:K.string().describe("The fig id to create a new revision of."),html:K.string().describe("The full updated HTML document."),title:K.string().optional().describe("Title (otherwise derived from the HTML)."),description:K.string().optional(),area:K.string().optional().describe("Product area / surface; free text. Inherited from the parent if omitted."),tags:K.array(K.string()).optional(),orientation:K.enum(j).optional().describe("Inherited from the parent if omitted."),mode:K.enum(q).optional(),model:K.string().optional().describe("Model attribution."),issue:K.number().optional().describe("Linked issue number."),publish:K.boolean().optional().describe("Publish the new revision to a shareable public URL (requires a paid plan).")}},async(B)=>{try{let J={html:B.html};for(let Q of["title","description","area","tags","orientation","mode","model","issue","publish"])if(B[Q]!==void 0)J[Q]=B[Q];return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/revise`,{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_list_revisions",{title:"Figura: list revisions",description:"List the full revision chain for a fig (all versions sharing a root), oldest to newest.",inputSchema:{id:K.string().describe("Any fig id in the chain.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/revisions`))}catch(J){return Z(J)}});W.registerTool("figura_publish_fig",{title:"Figura: publish fig",description:"Publish (or unpublish) an existing fig to its shareable public URL. Requires a paid plan. Returns { isPublic, url, publicId }.",inputSchema:{id:K.string().describe("The fig id to publish."),isPublic:K.boolean().optional().describe("Set false to unpublish (make private). Defaults to true (publish).")}},async(B)=>{try{let J={};if(B.isPublic!==void 0)J.isPublic=B.isPublic;return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/publish`,{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_list_figs",{title:"Figura: list figs",description:"List visualizations for the team, optionally filtered by area or tag.",inputSchema:{area:H(),tag:K.string().optional(),limit:K.number().optional(),cursor:K.string().optional()}},async(B)=>{try{let J=new URLSearchParams;if(B.area)J.set("area",B.area);if(B.tag)J.set("tag",B.tag);if(B.limit!==void 0)J.set("limit",String(B.limit));if(B.cursor)J.set("cursor",B.cursor);let Q=J.toString();return Y(await X(`/v1/figs${Q?`?${Q}`:""}`))}catch(J){return Z(J)}});W.registerTool("figura_get_fig",{title:"Figura: get fig",description:"Fetch a single visualization by id.",inputSchema:{id:K.string().describe("The fig id.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}`))}catch(J){return Z(J)}});W.registerTool("figura_comment",{title:"Figura: comment",description:"Add a comment to a visualization.",inputSchema:{fig_id:K.string().describe("The fig id to comment on."),body:K.string().describe("Comment text.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.fig_id)}/comments`,{method:"POST",body:{body:B.body}}))}catch(J){return Z(J)}});W.registerTool("figura_request_feedback",{title:"Figura: request feedback",description:"Ask a teammate to review a fig. Resolve `from` to a team member (by name or email); they get a notification (in-app, push, email) and the discussion happens in the fig's comment thread. Use this when the user says they want a specific person's feedback/eyes/review on a fig. Returns { ok, request: { to, status }, figUrl }.",inputSchema:{id:K.string().describe("The fig id to request feedback on."),from:K.string().describe("The teammate to ask, by name or email (resolved within the team)."),note:K.string().optional().describe("Optional message for them (e.g. what to look at).")}},async(B)=>{try{let J={from:B.from};if(B.note!==void 0)J.note=B.note;return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}/request-feedback`,{method:"POST",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_list_sources",{title:"Figura: list sources",description:"List the company-brain source-spine connectors for the team, with event counts and last-synced times.",inputSchema:{}},async()=>{try{return Y(await X("/v1/sources"))}catch(B){return Z(B)}});W.registerTool("figura_sync_source",{title:"Figura: sync source",description:"Ingest new events from a connected source into the spine. Returns { ingested, fetched, nextCursor }.",inputSchema:{id:K.string().describe("The source id to sync (from figura_list_sources).")}},async(B)=>{try{return Y(await X(`/v1/sources/${encodeURIComponent(B.id)}/sync`,{method:"POST"}))}catch(J){return Z(J)}});W.registerTool("figura_update_fig",{title:"Figura: update fig",description:"Update a fig's metadata in place (title/description/area/tags/status) and optionally replace its HTML. Set status='archived' to soft-delete.",inputSchema:{id:K.string().describe("The fig id to update."),title:K.string().optional(),description:K.string().optional(),area:H(),tags:K.array(K.string()).optional().describe("Replaces the existing tags."),status:K.enum(["draft","published","archived"]).optional(),html:K.string().optional().describe("Replace the rendered HTML in place.")}},async(B)=>{try{let J={};if(B.title!==void 0)J.title=B.title;if(B.description!==void 0)J.description=B.description;if(B.area!==void 0)J.area=B.area;if(B.tags!==void 0)J.tags=B.tags;if(B.status!==void 0)J.status=B.status;if(B.html!==void 0)J.html=B.html;return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}`,{method:"PATCH",body:J}))}catch(J){return Z(J)}});W.registerTool("figura_delete_fig",{title:"Figura: delete fig",description:"Archive a fig (soft-delete; leaves the library, public link stops resolving) or permanently delete it with hard=true.",inputSchema:{id:K.string().describe("The fig id to delete."),hard:K.boolean().optional().describe("Permanently delete instead of archive.")}},async(B)=>{try{return Y(await X(`/v1/figs/${encodeURIComponent(B.id)}${B.hard?"?hard=true":""}`,{method:"DELETE"}))}catch(J){return Z(J)}});await W.connect(new R);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figura-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Model Context Protocol server for the Figura visualization SaaS",
5
5
  "license": "MIT",
6
6
  "type": "module",