@formfeed/mcp 0.3.13 → 0.3.14

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,9 +3,9 @@
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 six tools: `list_templates`, `get_template_schema`,
6
+ Gives Claude, Cursor and other agents seven tools: `list_templates`, `get_template_schema`,
7
7
  `validate_template` (free: the data against the template's schema, ad-hoc HTML offline), `render`, `convert_to_pdf` (Word, Excel,
8
- PowerPoint and OpenDocument to PDF) and `get_render`.
8
+ PowerPoint and OpenDocument to PDF), `get_render` and `get_workspace` (which workspace the server works in, and the units left).
9
9
 
10
10
  ## stdio (Claude Desktop, Cursor, Claude Code)
11
11
 
@@ -26,9 +26,10 @@ A test key renders free with a watermark; a live key consumes units. `FORMFEED_R
26
26
 
27
27
  ## Hosted (Streamable HTTP)
28
28
 
29
- `https://mcp.formfeed.dev/mcp/<workspace id>` signs in with your Formfeed account (OAuth; the client
30
- opens the browser), `https://mcp.formfeed.dev/mcp` takes `Authorization: Bearer ff_…`. Self-host the
31
- same endpoint with `formfeed-mcp --http --port 8790`, plus `--auth-server <issuer>` for OAuth sign-in
32
- against your own Supabase Auth.
29
+ `https://mcp.formfeed.dev/mcp` takes `Authorization: Bearer ff_…`, or signs in with your Formfeed
30
+ account when no key is sent (OAuth; the client opens the browser, and you choose the workspace the
31
+ agent works in). `https://mcp.formfeed.dev/mcp/<workspace id>` pins another workspace for a signed-in
32
+ client. Self-host the same endpoint with `formfeed-mcp --http --port 8790`, plus `--auth-server <issuer>`
33
+ for OAuth sign-in against your own Supabase Auth.
33
34
 
34
35
  Documentation: <https://docs.formfeed.dev/integrations/mcp>
@@ -21408,7 +21408,9 @@ var require_liquid_node = __commonJS({
21408
21408
  });
21409
21409
 
21410
21410
  // packages/mcp/src/lib/transports.ts
21411
- import { createServer } from "node:http";
21411
+ import {
21412
+ createServer
21413
+ } from "node:http";
21412
21414
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21413
21415
 
21414
21416
  // packages/mcp/src/lib/server.ts
@@ -81976,7 +81978,8 @@ var Formfeed = class {
81976
81978
  `render ${target.id} has no output (${target.status})`,
81977
81979
  0
81978
81980
  );
81979
- const res = await this.fetchImpl(target.download_url, {
81981
+ const fetchImpl = this.fetchImpl;
81982
+ const res = await fetchImpl(target.download_url, {
81980
81983
  signal: options.signal
81981
81984
  });
81982
81985
  if (!res.ok)
@@ -82492,8 +82495,9 @@ var Formfeed = class {
82492
82495
  () => controller.abort(options.signal?.reason)
82493
82496
  );
82494
82497
  let res;
82498
+ const fetchImpl = this.fetchImpl;
82495
82499
  try {
82496
- res = await this.fetchImpl(url, {
82500
+ res = await fetchImpl(url, {
82497
82501
  method,
82498
82502
  headers,
82499
82503
  body: body === void 0 ? void 0 : isForm ? body : JSON.stringify(body),
@@ -82565,7 +82569,7 @@ var encoder2 = new TextEncoder();
82565
82569
  // packages/mcp/src/lib/server.ts
82566
82570
  import { z as z3 } from "zod";
82567
82571
  var CONVERT_LIMIT_BYTES = 20 * 1024 * 1024;
82568
- var SERVER_INFO = { name: "formfeed", version: "0.3.13" };
82572
+ var SERVER_INFO = { name: "formfeed", version: "0.3.14" };
82569
82573
  var READS = {
82570
82574
  readOnlyHint: true,
82571
82575
  destructiveHint: false,
@@ -82637,7 +82641,7 @@ function createFormfeedServer(options) {
82637
82641
  ...options.fetch ? { fetch: options.fetch } : {}
82638
82642
  });
82639
82643
  const server = new McpServer(SERVER_INFO, {
82640
- 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."
82644
+ 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. get_workspace says which workspace this server works in and what the period has left."
82641
82645
  });
82642
82646
  server.registerTool(
82643
82647
  "list_templates",
@@ -82961,6 +82965,30 @@ function createFormfeedServer(options) {
82961
82965
  }
82962
82966
  }
82963
82967
  );
82968
+ server.registerTool(
82969
+ "get_workspace",
82970
+ {
82971
+ title: "Which workspace this server works in",
82972
+ description: "Returns the workspace and organisation this server acts in, the plan, the environment (live or test) and the units of the current period: included, used and the balance of prepaid units. Call it when the user asks where a document will land or how many renders are left, or when list_templates returns nothing you expected. A signed-in agent works in the workspace chosen when it was connected, which the person changes in Formfeed under Account \u2192 Connected agents; an API key works in the workspace it was made for. Read-only and free.",
82973
+ inputSchema: {},
82974
+ annotations: READS
82975
+ },
82976
+ async () => {
82977
+ try {
82978
+ const { workspace, organization, plan, environment, period: period2, units } = await client.account.get();
82979
+ return text3({
82980
+ workspace,
82981
+ organization,
82982
+ plan,
82983
+ environment,
82984
+ period: period2,
82985
+ units
82986
+ });
82987
+ } catch (e) {
82988
+ return failure(e);
82989
+ }
82990
+ }
82991
+ );
82964
82992
  return server;
82965
82993
  }
82966
82994
 
@@ -82993,12 +83021,20 @@ function tokenExpired(token, now = Date.now()) {
82993
83021
  }
82994
83022
  }
82995
83023
  function json(body, status, headers = {}) {
82996
- return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } });
83024
+ return new Response(JSON.stringify(body), {
83025
+ status,
83026
+ headers: { "content-type": "application/json", ...headers }
83027
+ });
82997
83028
  }
82998
83029
  function bearer(request) {
82999
83030
  const auth = request.headers.get("authorization") ?? "";
83000
83031
  return /^Bearer\s+/i.test(auth) ? auth.replace(/^Bearer\s+/i, "").trim() : "";
83001
83032
  }
83033
+ function challenge(metadata, description) {
83034
+ return {
83035
+ "www-authenticate": description ? `Bearer realm="formfeed", resource_metadata="${metadata}", error="invalid_token", error_description="${description}"` : `Bearer realm="formfeed", resource_metadata="${metadata}"`
83036
+ };
83037
+ }
83002
83038
  function mcpFetchHandler(options = {}) {
83003
83039
  const authorizationServer2 = options.authorizationServer?.replace(/\/$/, "");
83004
83040
  return async (request) => {
@@ -83007,41 +83043,99 @@ function mcpFetchHandler(options = {}) {
83007
83043
  const path = url.pathname.replace(/\/+$/, "") || "/";
83008
83044
  if (path === "/healthz") return json({ ok: true }, 200);
83009
83045
  if (path === RESOURCE_METADATA_PATH || path.startsWith(`${RESOURCE_METADATA_PATH}${MCP_PATH}`)) {
83010
- if (!authorizationServer2) return json({ error: "This endpoint takes API keys only; no authorization server is configured" }, 404);
83011
- const workspaceId2 = path.slice(`${RESOURCE_METADATA_PATH}${MCP_PATH}/`.length);
83046
+ if (!authorizationServer2)
83047
+ return json(
83048
+ {
83049
+ error: "This endpoint takes API keys only; no authorization server is configured"
83050
+ },
83051
+ 404
83052
+ );
83053
+ const workspaceId2 = path.slice(
83054
+ `${RESOURCE_METADATA_PATH}${MCP_PATH}/`.length
83055
+ );
83012
83056
  if (path === RESOURCE_METADATA_PATH || path === `${RESOURCE_METADATA_PATH}${MCP_PATH}`)
83013
83057
  return json(
83014
83058
  {
83015
83059
  resource: `${origin}${MCP_PATH}`,
83016
83060
  authorization_servers: [authorizationServer2],
83017
83061
  bearer_methods_supported: ["header"],
83062
+ scopes_supported: ["openid", "email", "profile"],
83018
83063
  resource_name: "Formfeed",
83019
83064
  resource_documentation: "https://docs.formfeed.dev/integrations/mcp",
83020
- // the OAuth resource is one workspace: add its id to the path
83065
+ // a signed-in client works in the workspace chosen at consent; this path pins another one
83021
83066
  formfeed_workspace_path: `${origin}${MCP_PATH}/{workspace_id}`
83022
83067
  },
83023
83068
  200
83024
83069
  );
83025
- if (!WORKSPACE_ID.test(workspaceId2)) return json({ error: "The workspace id is not a UUID" }, 404);
83026
- return json(resourceMetadata(origin, workspaceId2, authorizationServer2), 200);
83070
+ if (!WORKSPACE_ID.test(workspaceId2))
83071
+ return json({ error: "The workspace id is not a UUID" }, 404);
83072
+ return json(
83073
+ resourceMetadata(origin, workspaceId2, authorizationServer2),
83074
+ 200
83075
+ );
83027
83076
  }
83028
- if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return new Response(null, { status: 404 });
83077
+ if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`))
83078
+ return new Response(null, { status: 404 });
83029
83079
  const workspaceId = path === MCP_PATH ? null : path.slice(MCP_PATH.length + 1);
83030
83080
  if (workspaceId !== null && !WORKSPACE_ID.test(workspaceId))
83031
- return json({ error: `The path is /mcp for an API key or /mcp/<workspace id> for a signed-in member` }, 404);
83081
+ return json(
83082
+ {
83083
+ error: `The path is /mcp for an API key or /mcp/<workspace id> for a signed-in member`
83084
+ },
83085
+ 404
83086
+ );
83032
83087
  const credential = bearer(request);
83033
83088
  if (workspaceId === null) {
83034
- if (!credential) return json({ error: "Authorization: Bearer <formfeed api key> required; a signed-in member connects to /mcp/<workspace id>" }, 401, { "www-authenticate": 'Bearer realm="formfeed"' });
83035
- if (!API_KEY.test(credential) && JWT.test(credential))
83036
- return json({ error: "An access token needs its workspace: connect to /mcp/<workspace id>" }, 401, { "www-authenticate": 'Bearer realm="formfeed"' });
83089
+ const metadata = `${origin}${RESOURCE_METADATA_PATH}${MCP_PATH}`;
83090
+ if (!credential)
83091
+ return authorizationServer2 ? json(
83092
+ {
83093
+ error: "Authorization: Bearer <formfeed api key> required, or sign in with your Formfeed account"
83094
+ },
83095
+ 401,
83096
+ challenge(metadata)
83097
+ ) : json(
83098
+ { error: "Authorization: Bearer <formfeed api key> required" },
83099
+ 401,
83100
+ { "www-authenticate": 'Bearer realm="formfeed"' }
83101
+ );
83102
+ if (!API_KEY.test(credential) && JWT.test(credential)) {
83103
+ if (!authorizationServer2)
83104
+ return json(
83105
+ {
83106
+ error: "This endpoint takes API keys only; no authorization server is configured for a sign-in"
83107
+ },
83108
+ 401,
83109
+ { "www-authenticate": 'Bearer realm="formfeed"' }
83110
+ );
83111
+ if (tokenExpired(credential))
83112
+ return json(
83113
+ { error: "The access token expired" },
83114
+ 401,
83115
+ challenge(metadata, "the access token expired")
83116
+ );
83117
+ }
83037
83118
  } else {
83038
- if (!authorizationServer2) return json({ error: "This endpoint takes API keys only; connect to /mcp with a key" }, 404);
83119
+ if (!authorizationServer2)
83120
+ return json(
83121
+ {
83122
+ error: "This endpoint takes API keys only; connect to /mcp with a key"
83123
+ },
83124
+ 404
83125
+ );
83039
83126
  const metadata = `${origin}${RESOURCE_METADATA_PATH}${MCP_PATH}/${workspaceId}`;
83040
- const challenge = (description) => ({
83041
- "www-authenticate": `Bearer realm="formfeed", resource_metadata="${metadata}", error="invalid_token", error_description="${description}"`
83042
- });
83043
- if (!credential || !JWT.test(credential)) return json({ error: "Sign in: this endpoint takes an OAuth access token" }, 401, challenge("an access token is required"));
83044
- if (tokenExpired(credential)) return json({ error: "The access token expired" }, 401, challenge("the access token expired"));
83127
+ if (!credential || !JWT.test(credential))
83128
+ return json(
83129
+ { error: "Sign in: this endpoint takes an OAuth access token" },
83130
+ 401,
83131
+ challenge(metadata, "an access token is required")
83132
+ );
83133
+ if (tokenExpired(credential))
83134
+ return json(
83135
+ { error: "The access token expired" },
83136
+ 401,
83137
+ challenge(metadata, "the access token expired")
83138
+ );
83045
83139
  }
83046
83140
  const mcp = createFormfeedServer({
83047
83141
  apiKey: credential,
@@ -83050,7 +83144,10 @@ function mcpFetchHandler(options = {}) {
83050
83144
  ...options.region ? { region: options.region } : {},
83051
83145
  ...options.fetch ? { fetch: options.fetch } : {}
83052
83146
  });
83053
- const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
83147
+ const transport = new WebStandardStreamableHTTPServerTransport({
83148
+ sessionIdGenerator: void 0,
83149
+ enableJsonResponse: true
83150
+ });
83054
83151
  await mcp.connect(transport);
83055
83152
  try {
83056
83153
  return await transport.handleRequest(request);
@@ -83073,8 +83170,11 @@ async function startHttp(options = {}) {
83073
83170
  const handler = mcpFetchHandler(handlerOptions);
83074
83171
  const server = createServer((req, res) => {
83075
83172
  void toRequest(req, host).then(handler).then((response) => send(response, res)).catch((e) => {
83076
- if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
83077
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
83173
+ if (!res.headersSent)
83174
+ res.writeHead(500, { "content-type": "application/json" });
83175
+ res.end(
83176
+ JSON.stringify({ error: e instanceof Error ? e.message : String(e) })
83177
+ );
83078
83178
  });
83079
83179
  });
83080
83180
  await new Promise((resolve2, reject) => {
@@ -83132,13 +83232,18 @@ var authorizationServer = flag("--auth-server") ?? process.env["FORMFEED_AUTH_SE
83132
83232
  var publicOrigin = flag("--public-origin") ?? process.env["FORMFEED_PUBLIC_ORIGIN"];
83133
83233
  if (args.includes("--help") || args.includes("-h")) {
83134
83234
  process.stdout.write(
83135
- "formfeed-mcp: Model Context Protocol server for Formfeed\n\n formfeed-mcp stdio; needs FORMFEED_API_KEY\n formfeed-mcp --http Streamable HTTP on --port (8790) and --host (127.0.0.1); the key comes from Authorization: Bearer\n --auth-server <issuer> (or FORMFEED_AUTH_SERVER) also opens /mcp/<workspace id> for OAuth access tokens;\n --public-origin <url> (or FORMFEED_PUBLIC_ORIGIN) is the origin clients see behind a proxy\n"
83235
+ "formfeed-mcp: Model Context Protocol server for Formfeed\n\n formfeed-mcp stdio; needs FORMFEED_API_KEY\n formfeed-mcp --http Streamable HTTP on --port (8790) and --host (127.0.0.1); the key comes from Authorization: Bearer\n --auth-server <issuer> (or FORMFEED_AUTH_SERVER) also takes OAuth access tokens on /mcp and /mcp/<workspace id>;\n --public-origin <url> (or FORMFEED_PUBLIC_ORIGIN) is the origin clients see behind a proxy\n"
83136
83236
  );
83137
83237
  } else if (args.includes("--http")) {
83138
- startHttp({ port: Number(flag("--port") ?? 8790), host: flag("--host") ?? "127.0.0.1", baseUrl, region, authorizationServer, publicOrigin }).then(
83139
- (h) => process.stderr.write(`formfeed-mcp listening on ${h.url}
83140
- `)
83141
- );
83238
+ startHttp({
83239
+ port: Number(flag("--port") ?? 8790),
83240
+ host: flag("--host") ?? "127.0.0.1",
83241
+ baseUrl,
83242
+ region,
83243
+ authorizationServer,
83244
+ publicOrigin
83245
+ }).then((h) => process.stderr.write(`formfeed-mcp listening on ${h.url}
83246
+ `));
83142
83247
  } else {
83143
83248
  const apiKey = process.env["FORMFEED_API_KEY"];
83144
83249
  if (!apiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/mcp",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
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",