@formfeed/mcp 0.3.1 → 0.3.3

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
@@ -1,8 +1,8 @@
1
1
  # @formfeed/mcp
2
2
 
3
- Model Context Protocol server for Formfeed. Gives Claude, Cursor and other agents five tools:
3
+ Model Context Protocol server for Formfeed. Gives Claude, Cursor and other agents six tools:
4
4
  `list_templates`, `get_template_schema`, `validate_template` (offline, with the shared engine),
5
- `render` and `get_render`.
5
+ `render`, `convert_to_pdf` (Word, Excel, PowerPoint and OpenDocument to PDF) and `get_render`.
6
6
 
7
7
  ## stdio (Claude Desktop, Cursor, Claude Code)
8
8
 
@@ -77504,6 +77504,7 @@ function toDate2(value, tz) {
77504
77504
  function escapeHtml(value) {
77505
77505
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
77506
77506
  }
77507
+ var isNamedArgs = (v2) => !!v2 && typeof v2 === "object" && !Array.isArray(v2) && !(v2 instanceof Date);
77507
77508
  var str = (v2) => v2 === void 0 || v2 === null ? "" : String(v2);
77508
77509
  var opt = (v2, fallback) => v2 === void 0 || v2 === null || v2 === "" ? fallback : v2;
77509
77510
  var wordsByLang = {
@@ -77538,7 +77539,7 @@ var formatHelpers = [
77538
77539
  name: "number",
77539
77540
  doc: {
77540
77541
  signature: "number(value, decimals?, locale?)",
77541
- description: "Formats a number with grouping and a fixed number of decimals.",
77542
+ description: "Formats a number with grouping. With decimals, exactly that many (rounded half away from zero); without, the decimals the number has, at most three, so 1234.5 \u2192 1.234,5 and 2.34567 \u2192 2,346. Use number(0) for whole numbers.",
77542
77543
  example: "{{ qty | number(2) }} \u2192 1.234,00",
77543
77544
  category: "format"
77544
77545
  },
@@ -77632,15 +77633,26 @@ var formatHelpers = [
77632
77633
  {
77633
77634
  name: "round",
77634
77635
  doc: {
77635
- signature: "round(value, decimals = 0)",
77636
- description: "Rounds half away from zero.",
77637
- example: "{{ 2.345 | round(2) }} gives 2.35",
77636
+ signature: "round(value, decimals = 0, method = 'common')",
77637
+ description: "Rounds to the given decimals, as Jinja2's round does: 'common' rounds half away from zero, 'floor' always down and 'ceil' always up.",
77638
+ example: "{{ 2.345 | round(2) }} gives 2.35, {{ 2.349 | round(2, 'floor') }} gives 2.34",
77638
77639
  category: "format"
77639
77640
  },
77640
- fn: (_ctx, value, decimals) => {
77641
+ // `round(2, method='floor')` (Jinja2) and `round: 2, method: 'floor'` (Liquid) arrive as a
77642
+ // trailing object, as does a Handlebars hash
77643
+ fn: (_ctx, value, ...args2) => {
77641
77644
  const n = toNumber(value);
77642
- const f2 = 10 ** (decimals === void 0 || decimals === null ? 0 : Number(decimals));
77643
- return Number.isNaN(n) ? "" : Math.round((Math.abs(n) + Number.EPSILON) * f2) / f2 * Math.sign(n);
77645
+ if (Number.isNaN(n)) return "";
77646
+ const named = args2.find(isNamedArgs) ?? {};
77647
+ const [decimals, method] = args2.filter((a) => !isNamedArgs(a));
77648
+ const places = Number(opt(decimals, opt(named["precision"], opt(named["decimals"], 0))));
77649
+ const how = String(opt(method, opt(named["method"], "common")));
77650
+ const f2 = 10 ** places;
77651
+ const scaled = Number((Math.abs(n) * f2).toPrecision(15));
77652
+ if (how === "floor") return (n < 0 ? -Math.ceil(scaled) : Math.floor(scaled)) / f2;
77653
+ if (how === "ceil") return (n < 0 ? -Math.floor(scaled) : Math.ceil(scaled)) / f2;
77654
+ if (how !== "common") throw new Error(`round: method must be 'common', 'floor' or 'ceil', not '${how}'`);
77655
+ return Math.round(scaled + Number.EPSILON) / f2 * Math.sign(n);
77644
77656
  }
77645
77657
  },
77646
77658
  {
@@ -82779,7 +82791,8 @@ var encoder2 = new TextEncoder();
82779
82791
 
82780
82792
  // packages/mcp/src/lib/server.ts
82781
82793
  import { z as z2 } from "zod";
82782
- var SERVER_INFO = { name: "formfeed", version: "0.3.1" };
82794
+ var CONVERT_LIMIT_BYTES = 20 * 1024 * 1024;
82795
+ var SERVER_INFO = { name: "formfeed", version: "0.3.3" };
82783
82796
  var engineSchema = z2.enum(["jinja2", "liquid", "handlebars"]);
82784
82797
  var outputSchema = z2.enum(outputFormats);
82785
82798
  var kindSchema = z2.enum(["pdf", "image", "docx", "pptx"]);
@@ -82820,7 +82833,7 @@ function createFormfeedServer(options) {
82820
82833
  ...options.fetch ? { fetch: options.fetch } : {}
82821
82834
  });
82822
82835
  const server = new McpServer(SERVER_INFO, {
82823
- instructions: "Formfeed renders PDFs and images from HTML templates, and fills Word and PowerPoint templates (as DOCX, PPTX or PDF). Start with list_templates, read the data shape with get_template_schema, check data with validate_template, then call render and hand the download_url to the user."
82836
+ instructions: "Formfeed renders PDFs and images from HTML templates, and fills Word and PowerPoint templates (as DOCX, PPTX or PDF). Start with list_templates, read the data shape with get_template_schema, check data with validate_template, then call render and hand the download_url to the user. convert_to_pdf turns office documents into PDFs."
82824
82837
  });
82825
82838
  server.registerTool(
82826
82839
  "list_templates",
@@ -82961,6 +82974,69 @@ function createFormfeedServer(options) {
82961
82974
  }
82962
82975
  }
82963
82976
  );
82977
+ server.registerTool(
82978
+ "convert_to_pdf",
82979
+ {
82980
+ title: "Convert an office document to PDF",
82981
+ description: [
82982
+ "Converts a Word, Excel, PowerPoint, OpenDocument, RTF or HTML document (up to 20 MB) to a PDF and returns a download URL (Starter plan and above; units follow the PDF rule).",
82983
+ "Pass exactly one source: render_id, the render of a Word or PowerPoint template (output docx or pptx);",
82984
+ options.localFiles ? "path, a file on this machine; or file_base64 with file_name." : "or file_base64 with file_name, the document itself.",
82985
+ "The document is converted and not kept."
82986
+ ].join(" "),
82987
+ inputSchema: {
82988
+ render_id: z2.string().optional().describe("rnd_ id of a render whose output is docx or pptx"),
82989
+ ...options.localFiles ? { path: z2.string().optional().describe("Absolute path of a document on this machine") } : {},
82990
+ file_base64: z2.string().optional().describe("The document, base64 encoded"),
82991
+ file_name: z2.string().optional().describe("Name of the document, e.g. report.xlsx; the type is read from the content"),
82992
+ page_ranges: z2.string().regex(/^\d+(-\d+)?(,\d+(-\d+)?)*$/).optional().describe("Pages to convert, e.g. 1-3,5"),
82993
+ landscape: z2.boolean().optional().describe("Landscape for spreadsheets and documents without their own page setup"),
82994
+ single_page_sheets: z2.boolean().optional().describe("Each spreadsheet sheet on one page"),
82995
+ filename: z2.string().optional().describe("Name of the PDF")
82996
+ },
82997
+ annotations: { readOnlyHint: false, idempotentHint: false }
82998
+ },
82999
+ async (args2) => {
83000
+ try {
83001
+ const { render_id, file_base64, file_name, page_ranges, landscape, single_page_sheets, filename } = args2;
83002
+ const path = args2.path;
83003
+ const sources = [render_id, file_base64, path].filter((s) => s !== void 0 && s !== "");
83004
+ if (sources.length !== 1) return failure(new Error("Pass exactly one of render_id, file_base64" + (options.localFiles ? " or path" : "")));
83005
+ const convert = {
83006
+ meta: { source: "mcp" },
83007
+ ...page_ranges ? { page_ranges } : {},
83008
+ ...landscape !== void 0 ? { landscape } : {},
83009
+ ...single_page_sheets !== void 0 ? { single_page_sheets } : {},
83010
+ ...filename ? { filename } : {}
83011
+ };
83012
+ let render;
83013
+ if (render_id) {
83014
+ render = await client.pdf.convert(render_id, convert);
83015
+ } else {
83016
+ let data;
83017
+ let name;
83018
+ if (path) {
83019
+ const { readFile, stat } = await import("node:fs/promises");
83020
+ const { basename } = await import("node:path");
83021
+ if ((await stat(path)).size > CONVERT_LIMIT_BYTES) return failure(new Error(`${path} is larger than 20 MB, the limit for conversions`));
83022
+ data = new Uint8Array(await readFile(path));
83023
+ name = file_name ?? basename(path);
83024
+ } else {
83025
+ if (!file_name) return failure(new Error("file_name is required with file_base64"));
83026
+ data = Uint8Array.from(Buffer.from(file_base64, "base64"));
83027
+ if (data.length === 0) return failure(new Error("file_base64 is empty or not base64"));
83028
+ if (data.length > CONVERT_LIMIT_BYTES) return failure(new Error("The document is larger than 20 MB, the limit for conversions"));
83029
+ name = file_name;
83030
+ }
83031
+ render = await client.pdf.convert({ file: { data, name } }, { ...convert, ...filename ? {} : { filename: `${name.replace(/\.[^.]+$/, "")}.pdf` } });
83032
+ }
83033
+ if (render.status !== "succeeded" && render.status !== "failed") render = await client.renders.waitFor(render.id);
83034
+ return text(renderSummary(render));
83035
+ } catch (e) {
83036
+ return failure(e);
83037
+ }
83038
+ }
83039
+ );
82964
83040
  server.registerTool(
82965
83041
  "get_render",
82966
83042
  {
@@ -82982,7 +83058,7 @@ function createFormfeedServer(options) {
82982
83058
 
82983
83059
  // packages/mcp/src/lib/transports.ts
82984
83060
  async function startStdio(options) {
82985
- const server = createFormfeedServer(options);
83061
+ const server = createFormfeedServer({ localFiles: true, ...options });
82986
83062
  await server.connect(new StdioServerTransport());
82987
83063
  }
82988
83064
  async function startHttp(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Model Context Protocol server for Formfeed: list templates, read data schemas, validate and render documents from AI agents.",
5
5
  "license": "MIT",
6
6
  "author": {