@formfeed/mcp 0.3.20 → 0.3.22

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
@@ -3,12 +3,13 @@
3
3
  [![formfeed MCP server – quality and maintenance score on Glama](https://glama.ai/mcp/servers/formfeed-dev/formfeed/badges/score.svg)](https://glama.ai/mcp/servers/formfeed-dev/formfeed)
4
4
 
5
5
  Model Context Protocol server for [Formfeed](https://formfeed.dev/?utm_source=npm&utm_content=mcp).
6
- Gives Claude, Cursor and other agents eight tools: `list_templates`, `get_template_schema`,
6
+ Gives Claude, Cursor and other agents ten tools: `list_templates`, `get_template_schema`,
7
7
  `validate_template` (free: the data against the template's schema, ad-hoc HTML offline), `check_invoice` (free and offline: the
8
8
  `_invoice` block of a ZUGFeRD / Factur-X e-invoice against EN 16931), `render` (with `einvoice`, the PDF becomes that e-invoice; an
9
9
  image comes back as a picture the agent can look at),
10
- `convert_to_pdf` (Word, Excel, PowerPoint and OpenDocument to PDF), `get_render` and `get_workspace` (which workspace the
11
- server works in, and the units left).
10
+ `convert_to_pdf` (Word, Excel, PowerPoint and OpenDocument to PDF), `get_render`, `get_workspace` (which workspace the
11
+ server works in, and the units left), `get_template_source` and `save_template` (a new template or a change as a draft,
12
+ which a person publishes in Formfeed; needs `template:write` on the key, or the person's permission for a signed-in agent).
12
13
 
13
14
  ## stdio (Claude Desktop, Cursor, Claude Code)
14
15
 
@@ -70732,7 +70732,7 @@ function toNumber(value) {
70732
70732
  // packages/engine/src/lib/codes/epc.ts
70733
70733
  function epcPayload(input) {
70734
70734
  const amount = input.amount === void 0 || input.amount === "" ? "" : `EUR${toNumber(input.amount).toFixed(2)}`;
70735
- return [
70735
+ const elements = [
70736
70736
  "BCD",
70737
70737
  "002",
70738
70738
  "1",
@@ -70744,8 +70744,12 @@ function epcPayload(input) {
70744
70744
  (input.purpose ?? "").slice(0, 4),
70745
70745
  (input.reference ?? "").slice(0, 35),
70746
70746
  input.reference ? "" : (input.text ?? "").slice(0, 140),
70747
+ // the beneficiary to originator information, which a payment here has no field for
70747
70748
  ""
70748
- ].join("\n");
70749
+ ];
70750
+ while (elements.length > 0 && elements[elements.length - 1] === "")
70751
+ elements.pop();
70752
+ return elements.join("\n");
70749
70753
  }
70750
70754
  var normaliseIban = (iban2) => iban2.replace(/\s/g, "").toUpperCase();
70751
70755
  function ibanChecksum(iban2) {
@@ -83878,7 +83882,7 @@ function base64(bytes) {
83878
83882
  binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
83879
83883
  return btoa(binary);
83880
83884
  }
83881
- var SERVER_INFO = { name: "formfeed", version: "0.3.20" };
83885
+ var SERVER_INFO = { name: "formfeed", version: "0.3.22" };
83882
83886
  var READS = {
83883
83887
  readOnlyHint: true,
83884
83888
  destructiveHint: false,
@@ -83912,6 +83916,9 @@ var renderOutput = {
83912
83916
  ),
83913
83917
  error: z3.object({ code: z3.string().optional(), message: z3.string().optional() }).nullable().describe("Why a failed render failed"),
83914
83918
  accessibility: z3.record(z3.string(), z3.unknown()).optional().describe("The PDF/UA-1 verdict, when the template declares it"),
83919
+ pdfa: z3.record(z3.string(), z3.unknown()).optional().describe(
83920
+ "The PDF/A verdict (PDF/A-2b or PDF/A-3b), when the template declares it"
83921
+ ),
83915
83922
  einvoice: z3.record(z3.string(), z3.unknown()).optional().describe("The e-invoice report, when the render made one")
83916
83923
  };
83917
83924
  var diagnosticOutput = z3.object({
@@ -83948,13 +83955,57 @@ var renderSummary = (r) => ({
83948
83955
  // the validator's verdict of a render whose template declares PDF/UA-1 (plan 21): whether the
83949
83956
  // file passed, and for each failed rule what to change and in which elements
83950
83957
  ...r.accessibility ? { accessibility: r.accessibility } : {},
83958
+ // the verdict of a render whose template declares PDF/A (plan 26): the level and the validator
83959
+ ...r.pdfa ? { pdfa: r.pdfa } : {},
83951
83960
  // the report of an e-invoice (spec 17): valid, the findings, the XML's link, the hashes it checked
83952
83961
  ...r.einvoice ? { einvoice: r.einvoice } : {}
83953
83962
  });
83963
+ var diagnosticOf = (d) => ({
83964
+ severity: d.severity,
83965
+ code: d.code,
83966
+ message: d.message,
83967
+ ...d.path ? { path: d.path } : {},
83968
+ ...d.location ? { line: d.location.line, column: d.location.column } : {},
83969
+ ...d.part ? { part: d.part, paragraph: d.paragraph } : {}
83970
+ });
83971
+ var slugSchema = z3.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/).describe(
83972
+ "New in the workspace: lowercase letters, digits and dashes, e.g. invoice-de"
83973
+ );
83974
+ var sourceParts = {
83975
+ html: z3.string().max(2 * 1024 * 1024).optional().describe(
83976
+ "The body markup in the template language, without <html>, <head> or <body>; up to 2 MB"
83977
+ ),
83978
+ css: z3.string().max(1024 * 1024).optional().describe("The stylesheet; up to 1 MB"),
83979
+ head: z3.string().max(256 * 1024).optional().describe("Extra markup for <head>, such as font links"),
83980
+ settings: z3.record(z3.string(), z3.unknown()).optional().describe(
83981
+ "Paper, margins, header, footer, fonts and the other settings, as get_template_source returns them; replaces the stored settings as a whole"
83982
+ ),
83983
+ sample_data: z3.record(z3.string(), z3.unknown()).optional().describe(
83984
+ "The data the editor previews with and the draft is checked with; never used by a render"
83985
+ ),
83986
+ data_sets: z3.record(z3.string(), z3.unknown()).optional().describe("Named sample data sets besides sample_data (name \u2192 data)"),
83987
+ data_schema: z3.record(z3.string(), z3.unknown()).nullable().optional().describe("JSON Schema of data; null removes the stored one"),
83988
+ i18n: z3.record(z3.string(), z3.unknown()).nullable().optional().describe("Translations: locale \u2192 key \u2192 text, read by the t helper"),
83989
+ partials: z3.record(z3.string(), z3.string()).optional().describe("Partials the template includes (name \u2192 source)")
83990
+ };
83991
+ function appOrigin(baseUrl2) {
83992
+ if (!baseUrl2) return "https://app.formfeed.dev";
83993
+ try {
83994
+ return /^api(-[a-z]+)?\.formfeed\.dev$/.test(new URL(baseUrl2).hostname) ? "https://app.formfeed.dev" : null;
83995
+ } catch {
83996
+ return null;
83997
+ }
83998
+ }
83999
+ function slugIn(account, part) {
84000
+ const value = account?.[part];
84001
+ const slug = typeof value === "object" && value !== null ? value["slug"] : null;
84002
+ return typeof slug === "string" ? slug : null;
84003
+ }
83954
84004
  var EINVOICE_PROBLEMS = /* @__PURE__ */ new Set([
83955
84005
  "einvoice_data_invalid",
83956
84006
  "einvoice_display_mismatch",
83957
- "pdfa_conversion_failed"
84007
+ "pdfa_conversion_failed",
84008
+ "einvoice_validator_error"
83958
84009
  ]);
83959
84010
  function text3(data) {
83960
84011
  return {
@@ -83965,11 +84016,13 @@ function text3(data) {
83965
84016
  function failure(e) {
83966
84017
  const message2 = e instanceof FormfeedError ? `${e.code}: ${e.message}` : e instanceof Error ? e.message : String(e);
83967
84018
  const verdict = e instanceof FormfeedError && e.code === "pdfua_validation_failed" ? e.problem?.["accessibility"] : void 0;
84019
+ const pdfa = e instanceof FormfeedError && (e.code === "pdfa_conversion_failed" || e.code === "pdfua_validation_failed") ? e.problem?.["pdfa"] : void 0;
83968
84020
  const invoice = e instanceof FormfeedError && EINVOICE_PROBLEMS.has(e.code) ? Object.fromEntries(
83969
84021
  ["errors", "einvoice"].filter((key) => e.problem?.[key] !== void 0).map((key) => [key, e.problem?.[key]])
83970
84022
  ) : {};
83971
84023
  const details = {
83972
84024
  ...verdict ? { accessibility: verdict } : {},
84025
+ ...pdfa ? { pdfa } : {},
83973
84026
  ...invoice
83974
84027
  };
83975
84028
  return {
@@ -84000,7 +84053,7 @@ function createFormfeedServer(options) {
84000
84053
  };
84001
84054
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
84002
84055
  const server = new McpServer(SERVER_INFO, {
84003
- instructions: (client ? "" : `${NO_KEY_INSTRUCTION} `) + "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. An image render also returns the picture itself, so look at it before handing it on. For an e-invoice (ZUGFeRD / Factur-X), put the invoice into data._invoice, check it with check_invoice, and call render with einvoice set; the template prints the same block. convert_to_pdf turns office documents into PDFs. get_workspace says which workspace this server works in and what the period has left."
84056
+ instructions: (client ? "" : `${NO_KEY_INSTRUCTION} `) + "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. An image render also returns the picture itself, so look at it before handing it on. For an e-invoice (ZUGFeRD / Factur-X), put the invoice into data._invoice, check it with check_invoice, and call render with einvoice set; the template prints the same block. convert_to_pdf turns office documents into PDFs. To change a template or write a new one, read it with get_template_source and save it with save_template: it stays a draft until a person publishes it in Formfeed. get_workspace says which workspace this server works in and what the period has left."
84004
84057
  });
84005
84058
  const picture = async (render) => {
84006
84059
  const mimeType = IMAGE_TYPES[render.output];
@@ -84103,13 +84156,92 @@ function createFormfeedServer(options) {
84103
84156
  }
84104
84157
  }
84105
84158
  );
84159
+ server.registerTool(
84160
+ "get_template_source",
84161
+ {
84162
+ title: "Read the source of a template",
84163
+ description: "Returns what a template version is made of: the body markup (html), css, head, settings (paper, margins, header, footer, fonts), sample data, data sets, data schema, translations and partials, with the version's checksum. Call it before save_template to change a template the user named, and save what you edited with that checksum as base_checksum. Reads the latest version by default, the newest draft a change builds on; published reads what renders use. Word and PowerPoint templates are documents: for them it returns the settings and data, not the document. Read-only and free.",
84164
+ inputSchema: {
84165
+ template: z3.string().describe(
84166
+ "Slug (e.g. invoice) or tpl_ id, as list_templates returns them"
84167
+ ),
84168
+ version: z3.union([z3.string().min(1).max(32), z3.number().int().positive()]).optional().describe(
84169
+ "latest (default), published, a version number or a channel name"
84170
+ )
84171
+ },
84172
+ outputSchema: {
84173
+ template: z3.object({
84174
+ id: z3.string().describe("tpl_ id"),
84175
+ slug: z3.string(),
84176
+ name: z3.string(),
84177
+ kind: z3.string().describe("pdf, image, docx (Word) or pptx (PowerPoint)"),
84178
+ engine: z3.string().describe("jinja2, liquid or handlebars"),
84179
+ published_version: z3.number().nullable()
84180
+ }),
84181
+ version: z3.object({
84182
+ number: z3.number(),
84183
+ status: z3.string().describe("draft, published or archived"),
84184
+ checksum: z3.string().describe("Pass it to save_template as base_checksum"),
84185
+ change_note: z3.string().nullable()
84186
+ }),
84187
+ html: z3.string(),
84188
+ css: z3.string(),
84189
+ head: z3.string(),
84190
+ settings: z3.record(z3.string(), z3.unknown()),
84191
+ sample_data: z3.record(z3.string(), z3.unknown()),
84192
+ data_sets: z3.record(z3.string(), z3.unknown()),
84193
+ data_schema: z3.record(z3.string(), z3.unknown()).nullable(),
84194
+ i18n: z3.record(z3.string(), z3.unknown()).nullable(),
84195
+ partials: z3.record(z3.string(), z3.string())
84196
+ },
84197
+ annotations: READS
84198
+ },
84199
+ async ({ template, version }) => {
84200
+ try {
84201
+ const [tpl, row] = await Promise.all([
84202
+ api().templates.get(template),
84203
+ api().templates.versions.get(template, version ?? "latest")
84204
+ ]);
84205
+ return text3({
84206
+ template: {
84207
+ id: tpl.id,
84208
+ slug: tpl.slug,
84209
+ name: tpl.name,
84210
+ kind: tpl.kind,
84211
+ engine: tpl.engine,
84212
+ published_version: tpl.published_version
84213
+ },
84214
+ version: {
84215
+ number: row.number,
84216
+ status: row.status,
84217
+ checksum: row.checksum,
84218
+ change_note: row.change_note
84219
+ },
84220
+ html: row.html ?? "",
84221
+ css: row.css ?? "",
84222
+ head: row.head ?? "",
84223
+ settings: row.settings ?? {},
84224
+ sample_data: row.sample_data ?? {},
84225
+ data_sets: row.data_sets ?? {},
84226
+ data_schema: row.data_schema ?? null,
84227
+ i18n: row.i18n ?? null,
84228
+ partials: row.partials ?? {}
84229
+ });
84230
+ } catch (e) {
84231
+ return failure(e);
84232
+ }
84233
+ }
84234
+ );
84106
84235
  server.registerTool(
84107
84236
  "validate_template",
84108
84237
  {
84109
84238
  title: "Validate a template with data",
84110
- description: "Checks a template and its data without rendering: free, no units, nothing stored, no document produced. Call it before render when you assembled the data yourself, or after writing ad-hoc html. With template, the API checks the published version (else the latest) as a render would: syntax, header and footer, errors only this data triggers, and the data against the stored JSON Schema (a wrong type or a missing required field is an error). With html and engine it compiles offline and reports syntax errors, unknown filters and variables missing from data. Returns ok and diagnostics (severity, code, message, plus path, line and column, or part and paragraph for Word and PowerPoint). Warnings leave ok true: settle each before rendering.",
84239
+ description: "Checks a template and its data without rendering: free, no units, nothing stored, no document produced. Call it before render when you assembled the data yourself, or after writing ad-hoc html. With template, the API checks the published version (else the latest, or the version you name) as a render would: syntax, header and footer, errors only this data triggers, and the data against the stored JSON Schema (a wrong type or a missing required field is an error). With html and engine it compiles offline and reports syntax errors, unknown filters and variables missing from data. Returns ok and diagnostics (severity, code, message, plus path, line and column, or part and paragraph for Word and PowerPoint). Warnings leave ok true: settle each before rendering.",
84111
84240
  inputSchema: {
84112
84241
  template: z3.string().optional().describe("Slug or tpl_ id of a stored template; pass this or html"),
84242
+ version: z3.union([z3.string().min(1).max(32), z3.number().int().positive()]).optional().describe(
84243
+ "With template: the version to check, such as latest for the newest draft or a number; left out, the published one, else the latest"
84244
+ ),
84113
84245
  html: z3.string().optional().describe(
84114
84246
  "Template source to check offline, when there is no stored template; pass this or template"
84115
84247
  ),
@@ -84125,28 +84257,21 @@ function createFormfeedServer(options) {
84125
84257
  },
84126
84258
  annotations: READS
84127
84259
  },
84128
- async ({ template, html, engine, data }) => {
84260
+ async ({ template, version, html, engine, data }) => {
84129
84261
  try {
84130
84262
  if (template) {
84131
- const check = (version) => api().templates.validate(template, {
84132
- version,
84263
+ const check = (which) => api().templates.validate(template, {
84264
+ version: which,
84133
84265
  ...data ? { data } : {}
84134
84266
  });
84135
- const result = await check("published").catch((e) => {
84267
+ const result = version !== void 0 ? await check(String(version)) : await check("published").catch((e) => {
84136
84268
  if (e.status === 404) return check("latest");
84137
84269
  throw e;
84138
84270
  });
84139
84271
  return text3({
84140
84272
  ok: result.ok,
84141
84273
  template: result.template,
84142
- diagnostics: result.diagnostics.map((d) => ({
84143
- severity: d.severity,
84144
- code: d.code,
84145
- message: d.message,
84146
- ...d.path ? { path: d.path } : {},
84147
- ...d.location ? { line: d.location.line, column: d.location.column } : {},
84148
- ...d.part ? { part: d.part, paragraph: d.paragraph } : {}
84149
- }))
84274
+ diagnostics: result.diagnostics.map(diagnosticOf)
84150
84275
  });
84151
84276
  }
84152
84277
  if (!html || !engine)
@@ -84184,6 +84309,166 @@ function createFormfeedServer(options) {
84184
84309
  }
84185
84310
  }
84186
84311
  );
84312
+ server.registerTool(
84313
+ "save_template",
84314
+ {
84315
+ title: "Save a template as a draft",
84316
+ description: "Saves a template as a draft, never published: renders keep using the published version until a person publishes the draft in Formfeed. With template and base_checksum it saves a new version of that template: read it with get_template_source first, change what the user asked for, and pass only the parts you changed (the others are kept from the version you read). With new_template and html it creates a template whose version 1 is the draft. change_note says what changed; the person reads it before publishing. Returns the saved version, the check of the draft with its sample data (fix every error and save again) and review_url, the page where the person reviews and publishes it: hand that to the user. Needs an API key with template:write, or, for a signed-in agent, the person's permission under Account \u2192 Connected agents in Formfeed. Word and PowerPoint templates cannot be saved here. Free; a new template counts towards the plan's template limit.",
84317
+ inputSchema: {
84318
+ template: z3.string().optional().describe(
84319
+ "Slug or tpl_ id of the template to save a new draft of; pass this or new_template"
84320
+ ),
84321
+ base_checksum: z3.string().max(80).optional().describe(
84322
+ "With template: the checksum get_template_source returned. When someone saved a newer version since, nothing is saved: read the source again and redo the change on it"
84323
+ ),
84324
+ new_template: z3.object({
84325
+ slug: slugSchema,
84326
+ name: z3.string().min(1).max(200),
84327
+ kind: z3.enum(["pdf", "image"]).describe("pdf for documents, image for pictures"),
84328
+ engine: engineSchema.describe("The template language of html"),
84329
+ description: z3.string().max(2e3).optional(),
84330
+ tags: z3.array(z3.string().max(40)).max(20).optional().describe("Lowercase with dashes, e.g. billing")
84331
+ }).optional().describe(
84332
+ "Creates a template whose version 1 is this draft; pass this and html, or template"
84333
+ ),
84334
+ ...sourceParts,
84335
+ change_note: z3.string().min(1).max(500).describe(
84336
+ "What the draft changes, in one sentence, shown in the version history"
84337
+ )
84338
+ },
84339
+ outputSchema: {
84340
+ template: z3.object({
84341
+ id: z3.string().describe("tpl_ id"),
84342
+ slug: z3.string(),
84343
+ name: z3.string(),
84344
+ published_version: z3.number().nullable().describe(
84345
+ "The version renders still use; null when nothing is published yet"
84346
+ )
84347
+ }),
84348
+ version: z3.object({
84349
+ number: z3.number(),
84350
+ status: z3.string().describe("draft"),
84351
+ checksum: z3.string().describe("base_checksum for a further change to this draft")
84352
+ }),
84353
+ ok: z3.boolean().nullable().describe(
84354
+ "false when the check of the draft found an error; null when the check could not run"
84355
+ ),
84356
+ diagnostics: z3.array(diagnosticOutput),
84357
+ review_url: z3.string().optional().describe(
84358
+ "The page in Formfeed where the person reviews and publishes the draft"
84359
+ ),
84360
+ next: z3.string()
84361
+ },
84362
+ annotations: CREATES
84363
+ },
84364
+ async (args2) => {
84365
+ const { template, base_checksum, new_template, change_note } = args2;
84366
+ try {
84367
+ if (!template === !new_template)
84368
+ return failure(
84369
+ new Error(
84370
+ "Pass template with base_checksum to save a new draft of a template, or new_template with html to create one"
84371
+ )
84372
+ );
84373
+ let saved;
84374
+ let slug;
84375
+ if (template) {
84376
+ if (!base_checksum)
84377
+ return failure(
84378
+ new Error(
84379
+ "Pass base_checksum: read the template with get_template_source, change it, and save it with the checksum you read"
84380
+ )
84381
+ );
84382
+ const [tpl2, base] = await Promise.all([
84383
+ api().templates.get(template),
84384
+ api().templates.versions.get(template, "latest")
84385
+ ]);
84386
+ if (tpl2.kind === "docx" || tpl2.kind === "pptx")
84387
+ return failure(
84388
+ new Error(
84389
+ `${tpl2.slug} is a ${tpl2.kind === "docx" ? "Word" : "PowerPoint"} template: its document is changed in ${tpl2.kind === "docx" ? "Word" : "PowerPoint"} and uploaded in Formfeed, not saved here`
84390
+ )
84391
+ );
84392
+ if (base.checksum !== base_checksum)
84393
+ return failure(
84394
+ new Error(
84395
+ "conflict: someone saved a newer version of this template since you read it. Read it again with get_template_source and make the change on that version"
84396
+ )
84397
+ );
84398
+ saved = await api().templates.versions.create(template, {
84399
+ html: args2.html ?? base.html ?? "",
84400
+ css: args2.css ?? base.css,
84401
+ head: args2.head ?? base.head,
84402
+ settings: args2.settings ?? base.settings,
84403
+ sample_data: args2.sample_data ?? base.sample_data,
84404
+ data_sets: args2.data_sets ?? base.data_sets,
84405
+ data_schema: args2.data_schema !== void 0 ? args2.data_schema : base.data_schema,
84406
+ i18n: args2.i18n !== void 0 ? args2.i18n : base.i18n,
84407
+ partials: args2.partials ?? base.partials,
84408
+ change_note,
84409
+ base_checksum,
84410
+ publish: false
84411
+ });
84412
+ slug = tpl2.slug;
84413
+ } else {
84414
+ if (!new_template || args2.html === void 0)
84415
+ return failure(new Error("html is required for a new template"));
84416
+ const created = await api().templates.create({
84417
+ ...new_template,
84418
+ html: args2.html,
84419
+ css: args2.css,
84420
+ head: args2.head,
84421
+ settings: args2.settings,
84422
+ sample_data: args2.sample_data,
84423
+ data_sets: args2.data_sets,
84424
+ data_schema: args2.data_schema,
84425
+ i18n: args2.i18n,
84426
+ partials: args2.partials,
84427
+ change_note,
84428
+ publish: false
84429
+ });
84430
+ slug = created.slug;
84431
+ saved = await api().templates.versions.get(slug, "latest");
84432
+ }
84433
+ const origin = appOrigin(options.baseUrl);
84434
+ const [tpl, check, account] = await Promise.all([
84435
+ api().templates.get(slug),
84436
+ // the check of the draft itself, with its sample data; a check that cannot run leaves the draft saved
84437
+ api().templates.validate(slug, { version: String(saved.number) }).then(
84438
+ (result) => ({ result, error: null }),
84439
+ (e) => ({
84440
+ result: null,
84441
+ error: e instanceof Error ? e.message : String(e)
84442
+ })
84443
+ ),
84444
+ origin ? api().account.get().catch(() => null) : Promise.resolve(null)
84445
+ ]);
84446
+ const org = slugIn(account, "organization");
84447
+ const workspace = slugIn(account, "workspace");
84448
+ return text3({
84449
+ template: {
84450
+ id: tpl.id,
84451
+ slug: tpl.slug,
84452
+ name: tpl.name,
84453
+ published_version: tpl.published_version
84454
+ },
84455
+ version: {
84456
+ number: saved.number,
84457
+ status: saved.status,
84458
+ checksum: saved.checksum
84459
+ },
84460
+ ok: check.result ? check.result.ok : null,
84461
+ diagnostics: check.result ? check.result.diagnostics.map(diagnosticOf) : [],
84462
+ ...origin && org && workspace ? {
84463
+ review_url: `${origin}/o/${org}/w/${workspace}/templates/${tpl.slug}`
84464
+ } : {},
84465
+ next: check.result ? `Saved as draft version ${saved.number}; nothing is published. Ask the person to review it in Formfeed and publish it there.` : `Saved as draft version ${saved.number}; nothing is published. The check could not run (${check.error}): call validate_template with version ${saved.number} before asking the person to publish it.`
84466
+ });
84467
+ } catch (e) {
84468
+ return failure(e);
84469
+ }
84470
+ }
84471
+ );
84187
84472
  server.registerTool(
84188
84473
  "check_invoice",
84189
84474
  {
@@ -84734,6 +85019,12 @@ function mcpFetchHandler(options = {}) {
84734
85019
  challenge(metadata, "the access token expired")
84735
85020
  );
84736
85021
  }
85022
+ if (request.method !== "POST")
85023
+ return json(
85024
+ { error: "This endpoint takes POST only; it opens no stream" },
85025
+ 405,
85026
+ { allow: "POST" }
85027
+ );
84737
85028
  const mcp = createFormfeedServer({
84738
85029
  apiKey: credential,
84739
85030
  ...workspaceId ? { workspaceId } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/mcp",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "mcpName": "io.github.formfeed-dev/formfeed",
5
5
  "description": "Model Context Protocol server for Formfeed: list templates, read data schemas, validate and render documents from AI agents.",
6
6
  "license": "MIT",