@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 +2 -2
- package/bin/formfeed-mcp.mjs +86 -10
- package/package.json +1 -1
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
|
|
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
|
|
package/bin/formfeed-mcp.mjs
CHANGED
|
@@ -77504,6 +77504,7 @@ function toDate2(value, tz) {
|
|
|
77504
77504
|
function escapeHtml(value) {
|
|
77505
77505
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77643
|
-
|
|
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
|
|
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