@formfeed/mcp 0.3.2 → 0.3.4

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
@@ -23,7 +23,9 @@ A test key renders free with a watermark; a live key consumes units. `FORMFEED_R
23
23
 
24
24
  ## Hosted (Streamable HTTP)
25
25
 
26
- `https://mcp.formfeed.dev/mcp` with `Authorization: Bearer ff_…`. Self-host the same endpoint with
27
- `formfeed-mcp --http --port 8790`.
26
+ `https://mcp.formfeed.dev/mcp/<workspace id>` signs in with your Formfeed account (OAuth; the client
27
+ opens the browser), `https://mcp.formfeed.dev/mcp` takes `Authorization: Bearer ff_…`. Self-host the
28
+ same endpoint with `formfeed-mcp --http --port 8790`, plus `--auth-server <issuer>` for OAuth sign-in
29
+ against your own Supabase Auth.
28
30
 
29
31
  Documentation: <https://docs.formfeed.dev/integrations/mcp>
@@ -18058,7 +18058,7 @@ var require_liquid_node = __commonJS({
18058
18058
  return false;
18059
18059
  return isFalsy(value, this.context) ? defaultValue : value;
18060
18060
  }
18061
- function json(value, space = 0) {
18061
+ function json2(value, space = 0) {
18062
18062
  const memoryLimit = this.context.memoryLimit;
18063
18063
  return JSON.stringify(value, (_key, val2) => {
18064
18064
  chargeJsonReplacerValue(memoryLimit, val2);
@@ -18094,9 +18094,9 @@ var require_liquid_node = __commonJS({
18094
18094
  var misc = {
18095
18095
  default: defaultFilter,
18096
18096
  raw,
18097
- jsonify: json,
18097
+ jsonify: json2,
18098
18098
  to_integer,
18099
- json,
18099
+ json: json2,
18100
18100
  inspect
18101
18101
  };
18102
18102
  var escapeMap = {
@@ -21410,7 +21410,6 @@ var require_liquid_node = __commonJS({
21410
21410
  // packages/mcp/src/lib/transports.ts
21411
21411
  import { createServer } from "node:http";
21412
21412
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21413
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21414
21413
 
21415
21414
  // packages/mcp/src/lib/server.ts
21416
21415
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -77504,6 +77503,7 @@ function toDate2(value, tz) {
77504
77503
  function escapeHtml(value) {
77505
77504
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
77506
77505
  }
77506
+ var isNamedArgs = (v2) => !!v2 && typeof v2 === "object" && !Array.isArray(v2) && !(v2 instanceof Date);
77507
77507
  var str = (v2) => v2 === void 0 || v2 === null ? "" : String(v2);
77508
77508
  var opt = (v2, fallback) => v2 === void 0 || v2 === null || v2 === "" ? fallback : v2;
77509
77509
  var wordsByLang = {
@@ -77538,7 +77538,7 @@ var formatHelpers = [
77538
77538
  name: "number",
77539
77539
  doc: {
77540
77540
  signature: "number(value, decimals?, locale?)",
77541
- description: "Formats a number with grouping and a fixed number of decimals.",
77541
+ 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
77542
  example: "{{ qty | number(2) }} \u2192 1.234,00",
77543
77543
  category: "format"
77544
77544
  },
@@ -77632,15 +77632,26 @@ var formatHelpers = [
77632
77632
  {
77633
77633
  name: "round",
77634
77634
  doc: {
77635
- signature: "round(value, decimals = 0)",
77636
- description: "Rounds half away from zero.",
77637
- example: "{{ 2.345 | round(2) }} gives 2.35",
77635
+ signature: "round(value, decimals = 0, method = 'common')",
77636
+ description: "Rounds to the given decimals, as Jinja2's round does: 'common' rounds half away from zero, 'floor' always down and 'ceil' always up.",
77637
+ example: "{{ 2.345 | round(2) }} gives 2.35, {{ 2.349 | round(2, 'floor') }} gives 2.34",
77638
77638
  category: "format"
77639
77639
  },
77640
- fn: (_ctx, value, decimals) => {
77640
+ // `round(2, method='floor')` (Jinja2) and `round: 2, method: 'floor'` (Liquid) arrive as a
77641
+ // trailing object, as does a Handlebars hash
77642
+ fn: (_ctx, value, ...args2) => {
77641
77643
  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);
77644
+ if (Number.isNaN(n)) return "";
77645
+ const named = args2.find(isNamedArgs) ?? {};
77646
+ const [decimals, method] = args2.filter((a) => !isNamedArgs(a));
77647
+ const places = Number(opt(decimals, opt(named["precision"], opt(named["decimals"], 0))));
77648
+ const how = String(opt(method, opt(named["method"], "common")));
77649
+ const f2 = 10 ** places;
77650
+ const scaled = Number((Math.abs(n) * f2).toPrecision(15));
77651
+ if (how === "floor") return (n < 0 ? -Math.ceil(scaled) : Math.floor(scaled)) / f2;
77652
+ if (how === "ceil") return (n < 0 ? -Math.floor(scaled) : Math.ceil(scaled)) / f2;
77653
+ if (how !== "common") throw new Error(`round: method must be 'common', 'floor' or 'ceil', not '${how}'`);
77654
+ return Math.round(scaled + Number.EPSILON) / f2 * Math.sign(n);
77644
77655
  }
77645
77656
  },
77646
77657
  {
@@ -82395,6 +82406,7 @@ var Formfeed = class {
82395
82406
  fetchImpl;
82396
82407
  maxRetries;
82397
82408
  timeoutMs;
82409
+ extraHeaders;
82398
82410
  constructor(options) {
82399
82411
  if (!options.apiKey) throw new FormfeedError("invalid_request", "apiKey is required", 0);
82400
82412
  this.apiKey = options.apiKey;
@@ -82402,6 +82414,7 @@ var Formfeed = class {
82402
82414
  this.fetchImpl = options.fetch ?? globalThis.fetch;
82403
82415
  this.maxRetries = options.maxRetries ?? 3;
82404
82416
  this.timeoutMs = options.timeoutMs ?? 12e4;
82417
+ this.extraHeaders = Object.fromEntries(Object.entries(options.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
82405
82418
  if (!this.fetchImpl) throw new FormfeedError("invalid_request", "fetch is not available; pass options.fetch", 0);
82406
82419
  }
82407
82420
  renders = {
@@ -82669,11 +82682,20 @@ var Formfeed = class {
82669
82682
  );
82670
82683
  }
82671
82684
  },
82672
- schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options),
82685
+ /**
82686
+ * The JSON Schema of the template's data: stored, else inferred from the sample data. Of the
82687
+ * published (else the latest) version, or of `version`: `published`, `latest`, a number or a
82688
+ * channel name.
82689
+ */
82690
+ schema: (idOrSlug, options = {}) => {
82691
+ const { version, ...rest } = options;
82692
+ const query = version === void 0 ? "" : `?version=${encodeURIComponent(String(version))}`;
82693
+ return this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema${query}`, void 0, rest);
82694
+ },
82673
82695
  /**
82674
82696
  * Checks a version with data without rendering: syntax, header and footer, missing partials,
82675
82697
  * runtime errors and the data against the stored schema. Free. Without `data`, the version's
82676
- * sample data is checked.
82698
+ * sample data is checked. `version` takes a channel name too.
82677
82699
  */
82678
82700
  validate: (idOrSlug, input = {}, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/validate`, input, options)
82679
82701
  };
@@ -82712,7 +82734,8 @@ var Formfeed = class {
82712
82734
  const headers = {
82713
82735
  authorization: `Bearer ${this.apiKey}`,
82714
82736
  accept: as === "bytes" ? "*/*" : "application/json",
82715
- "user-agent": "formfeed-sdk-ts/0.3"
82737
+ "user-agent": "formfeed-sdk-ts/0.3",
82738
+ ...this.extraHeaders
82716
82739
  };
82717
82740
  const isForm = typeof FormData !== "undefined" && body instanceof FormData;
82718
82741
  if (body !== void 0 && !isForm) headers["content-type"] = "application/json";
@@ -82748,14 +82771,14 @@ var Formfeed = class {
82748
82771
  if (res.status === 204) return { status: 204, body: void 0 };
82749
82772
  if (as === "bytes" && res.ok) return { status: res.status, body: new Uint8Array(await res.arrayBuffer()) };
82750
82773
  const text2 = await res.text();
82751
- let json = null;
82774
+ let json2 = null;
82752
82775
  try {
82753
- json = text2 ? JSON.parse(text2) : null;
82776
+ json2 = text2 ? JSON.parse(text2) : null;
82754
82777
  } catch {
82755
82778
  if (res.ok) throw new FormfeedError("invalid_response", `the API answered HTTP ${res.status} with a body that is not JSON`, res.status);
82756
82779
  }
82757
82780
  if (!res.ok) {
82758
- const problem = json ?? {};
82781
+ const problem = json2 ?? {};
82759
82782
  throw new FormfeedError(
82760
82783
  problem.code ?? `http_${res.status}`,
82761
82784
  problem.detail ?? problem.title ?? `HTTP ${res.status}`,
@@ -82764,7 +82787,7 @@ var Formfeed = class {
82764
82787
  res.headers.get("x-request-id")
82765
82788
  );
82766
82789
  }
82767
- return { status: res.status, body: json };
82790
+ return { status: res.status, body: json2 };
82768
82791
  }
82769
82792
  }
82770
82793
  backoff(attempt, retryAfter) {
@@ -82780,7 +82803,7 @@ var encoder2 = new TextEncoder();
82780
82803
  // packages/mcp/src/lib/server.ts
82781
82804
  import { z as z2 } from "zod";
82782
82805
  var CONVERT_LIMIT_BYTES = 20 * 1024 * 1024;
82783
- var SERVER_INFO = { name: "formfeed", version: "0.3.2" };
82806
+ var SERVER_INFO = { name: "formfeed", version: "0.3.4" };
82784
82807
  var engineSchema = z2.enum(["jinja2", "liquid", "handlebars"]);
82785
82808
  var outputSchema = z2.enum(outputFormats);
82786
82809
  var kindSchema = z2.enum(["pdf", "image", "docx", "pptx"]);
@@ -82816,6 +82839,7 @@ function failure(e) {
82816
82839
  function createFormfeedServer(options) {
82817
82840
  const client = new Formfeed({
82818
82841
  apiKey: options.apiKey,
82842
+ headers: { "user-agent": `formfeed-mcp/${SERVER_INFO.version}`, ...options.workspaceId ? { "x-formfeed-workspace": options.workspaceId } : {} },
82819
82843
  ...options.region ? { region: options.region } : {},
82820
82844
  ...options.baseUrl ? { baseUrl: options.baseUrl } : {},
82821
82845
  ...options.fetch ? { fetch: options.fetch } : {}
@@ -83044,6 +83068,102 @@ function createFormfeedServer(options) {
83044
83068
  return server;
83045
83069
  }
83046
83070
 
83071
+ // packages/mcp/src/lib/web.ts
83072
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
83073
+ var WORKSPACE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
83074
+ var API_KEY = /^ff_(live|test)_/;
83075
+ var JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
83076
+ var MCP_PATH = "/mcp";
83077
+ var RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
83078
+ function resourceMetadata(origin, workspaceId, authorizationServer2) {
83079
+ return {
83080
+ resource: `${origin}${MCP_PATH}/${workspaceId}`,
83081
+ authorization_servers: [authorizationServer2.replace(/\/$/, "")],
83082
+ bearer_methods_supported: ["header"],
83083
+ // Supabase Auth accepts the OIDC identity scopes only; what the token may do is the gateway's decision
83084
+ scopes_supported: ["openid", "email", "profile"],
83085
+ resource_name: "Formfeed",
83086
+ resource_documentation: "https://docs.formfeed.dev/integrations/mcp"
83087
+ };
83088
+ }
83089
+ function tokenExpired(token, now = Date.now()) {
83090
+ const part = token.split(".")[1] ?? "";
83091
+ try {
83092
+ const json2 = atob(part.replace(/-/g, "+").replace(/_/g, "/"));
83093
+ const exp = JSON.parse(json2).exp;
83094
+ return typeof exp === "number" && exp * 1e3 <= now;
83095
+ } catch {
83096
+ return false;
83097
+ }
83098
+ }
83099
+ function json(body, status, headers = {}) {
83100
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } });
83101
+ }
83102
+ function bearer(request) {
83103
+ const auth = request.headers.get("authorization") ?? "";
83104
+ return /^Bearer\s+/i.test(auth) ? auth.replace(/^Bearer\s+/i, "").trim() : "";
83105
+ }
83106
+ function mcpFetchHandler(options = {}) {
83107
+ const authorizationServer2 = options.authorizationServer?.replace(/\/$/, "");
83108
+ return async (request) => {
83109
+ const url = new URL(request.url);
83110
+ const origin = (options.publicOrigin ?? url.origin).replace(/\/$/, "");
83111
+ const path = url.pathname.replace(/\/+$/, "") || "/";
83112
+ if (path === "/healthz") return json({ ok: true }, 200);
83113
+ if (path === RESOURCE_METADATA_PATH || path.startsWith(`${RESOURCE_METADATA_PATH}${MCP_PATH}`)) {
83114
+ if (!authorizationServer2) return json({ error: "This endpoint takes API keys only; no authorization server is configured" }, 404);
83115
+ const workspaceId2 = path.slice(`${RESOURCE_METADATA_PATH}${MCP_PATH}/`.length);
83116
+ if (path === RESOURCE_METADATA_PATH || path === `${RESOURCE_METADATA_PATH}${MCP_PATH}`)
83117
+ return json(
83118
+ {
83119
+ resource: `${origin}${MCP_PATH}`,
83120
+ authorization_servers: [authorizationServer2],
83121
+ bearer_methods_supported: ["header"],
83122
+ resource_name: "Formfeed",
83123
+ resource_documentation: "https://docs.formfeed.dev/integrations/mcp",
83124
+ // the OAuth resource is one workspace: add its id to the path
83125
+ formfeed_workspace_path: `${origin}${MCP_PATH}/{workspace_id}`
83126
+ },
83127
+ 200
83128
+ );
83129
+ if (!WORKSPACE_ID.test(workspaceId2)) return json({ error: "The workspace id is not a UUID" }, 404);
83130
+ return json(resourceMetadata(origin, workspaceId2, authorizationServer2), 200);
83131
+ }
83132
+ if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return new Response(null, { status: 404 });
83133
+ const workspaceId = path === MCP_PATH ? null : path.slice(MCP_PATH.length + 1);
83134
+ if (workspaceId !== null && !WORKSPACE_ID.test(workspaceId))
83135
+ return json({ error: `The path is /mcp for an API key or /mcp/<workspace id> for a signed-in member` }, 404);
83136
+ const credential = bearer(request);
83137
+ if (workspaceId === null) {
83138
+ 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"' });
83139
+ if (!API_KEY.test(credential) && JWT.test(credential))
83140
+ return json({ error: "An access token needs its workspace: connect to /mcp/<workspace id>" }, 401, { "www-authenticate": 'Bearer realm="formfeed"' });
83141
+ } else {
83142
+ if (!authorizationServer2) return json({ error: "This endpoint takes API keys only; connect to /mcp with a key" }, 404);
83143
+ const metadata = `${origin}${RESOURCE_METADATA_PATH}${MCP_PATH}/${workspaceId}`;
83144
+ const challenge = (description) => ({
83145
+ "www-authenticate": `Bearer realm="formfeed", resource_metadata="${metadata}", error="invalid_token", error_description="${description}"`
83146
+ });
83147
+ if (!credential || !JWT.test(credential)) return json({ error: "Sign in: this endpoint takes an OAuth access token" }, 401, challenge("an access token is required"));
83148
+ if (tokenExpired(credential)) return json({ error: "The access token expired" }, 401, challenge("the access token expired"));
83149
+ }
83150
+ const mcp = createFormfeedServer({
83151
+ apiKey: credential,
83152
+ ...workspaceId ? { workspaceId } : {},
83153
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {},
83154
+ ...options.region ? { region: options.region } : {},
83155
+ ...options.fetch ? { fetch: options.fetch } : {}
83156
+ });
83157
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
83158
+ await mcp.connect(transport);
83159
+ try {
83160
+ return await transport.handleRequest(request);
83161
+ } finally {
83162
+ void mcp.close().catch(() => void 0);
83163
+ }
83164
+ };
83165
+ }
83166
+
83047
83167
  // packages/mcp/src/lib/transports.ts
83048
83168
  async function startStdio(options) {
83049
83169
  const server = createFormfeedServer({ localFiles: true, ...options });
@@ -83051,52 +83171,58 @@ async function startStdio(options) {
83051
83171
  }
83052
83172
  async function startHttp(options = {}) {
83053
83173
  const host = options.host ?? "127.0.0.1";
83054
- const path = options.path ?? "/mcp";
83174
+ const { port: _port, host: _host, ...handlerOptions } = options;
83175
+ void _port;
83176
+ void _host;
83177
+ const handler = mcpFetchHandler(handlerOptions);
83055
83178
  const server = createServer((req, res) => {
83056
- void handle(req, res).catch((e) => {
83179
+ void toRequest(req, host).then(handler).then((response) => send(response, res)).catch((e) => {
83057
83180
  if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
83058
83181
  res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
83059
83182
  });
83060
83183
  });
83061
- async function handle(req, res) {
83062
- const url = new URL(req.url ?? "/", `http://${host}`);
83063
- if (url.pathname === "/healthz") {
83064
- res.writeHead(200, { "content-type": "application/json" });
83065
- res.end(JSON.stringify({ ok: true }));
83066
- return;
83067
- }
83068
- if (url.pathname !== path) {
83069
- res.writeHead(404);
83070
- res.end();
83071
- return;
83072
- }
83073
- const auth = req.headers.authorization ?? "";
83074
- const apiKey = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
83075
- if (!apiKey) {
83076
- res.writeHead(401, { "content-type": "application/json", "www-authenticate": 'Bearer realm="formfeed"' });
83077
- res.end(JSON.stringify({ error: "Authorization: Bearer <formfeed api key> required" }));
83078
- return;
83079
- }
83080
- const mcp = createFormfeedServer({ apiKey, baseUrl: options.baseUrl, region: options.region, fetch: options.fetch });
83081
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
83082
- res.on("close", () => {
83083
- void transport.close();
83084
- void mcp.close();
83085
- });
83086
- await mcp.connect(transport);
83087
- await transport.handleRequest(req, res);
83088
- }
83089
83184
  await new Promise((resolve2, reject) => {
83090
83185
  server.once("error", reject);
83091
83186
  server.listen(options.port ?? 8790, host, () => resolve2());
83092
83187
  });
83093
83188
  const port = server.address().port;
83094
83189
  return {
83095
- url: `http://${host}:${port}${path}`,
83190
+ url: `http://${host}:${port}${MCP_PATH}`,
83096
83191
  port,
83097
83192
  close: () => new Promise((resolve2) => server.close(() => resolve2()))
83098
83193
  };
83099
83194
  }
83195
+ async function toRequest(req, host) {
83196
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? host}`);
83197
+ const headers = new Headers();
83198
+ for (const [name, value] of Object.entries(req.headers)) {
83199
+ if (Array.isArray(value)) for (const v2 of value) headers.append(name, v2);
83200
+ else if (value !== void 0) headers.set(name, value);
83201
+ }
83202
+ const chunks = [];
83203
+ for await (const chunk of req) chunks.push(chunk);
83204
+ const method = req.method ?? "GET";
83205
+ const body = method === "GET" || method === "HEAD" ? void 0 : new Uint8Array(Buffer.concat(chunks));
83206
+ return new Request(url, { method, headers, ...body ? { body } : {} });
83207
+ }
83208
+ async function send(response, res) {
83209
+ const headers = {};
83210
+ response.headers.forEach((value, name) => {
83211
+ headers[name] = name === "set-cookie" ? [...headers[name] ?? [], value] : value;
83212
+ });
83213
+ res.writeHead(response.status, headers);
83214
+ if (!response.body) {
83215
+ res.end();
83216
+ return;
83217
+ }
83218
+ const reader = response.body.getReader();
83219
+ for (; ; ) {
83220
+ const { done, value } = await reader.read();
83221
+ if (done) break;
83222
+ res.write(value);
83223
+ }
83224
+ res.end();
83225
+ }
83100
83226
 
83101
83227
  // packages/mcp/src/main.ts
83102
83228
  var args = process.argv.slice(2);
@@ -83106,12 +83232,14 @@ var flag = (name) => {
83106
83232
  };
83107
83233
  var baseUrl = process.env["FORMFEED_BASE_URL"];
83108
83234
  var region = process.env["FORMFEED_REGION"];
83235
+ var authorizationServer = flag("--auth-server") ?? process.env["FORMFEED_AUTH_SERVER"];
83236
+ var publicOrigin = flag("--public-origin") ?? process.env["FORMFEED_PUBLIC_ORIGIN"];
83109
83237
  if (args.includes("--help") || args.includes("-h")) {
83110
83238
  process.stdout.write(
83111
- "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"
83239
+ "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"
83112
83240
  );
83113
83241
  } else if (args.includes("--http")) {
83114
- startHttp({ port: Number(flag("--port") ?? 8790), host: flag("--host") ?? "127.0.0.1", baseUrl, region }).then(
83242
+ startHttp({ port: Number(flag("--port") ?? 8790), host: flag("--host") ?? "127.0.0.1", baseUrl, region, authorizationServer, publicOrigin }).then(
83115
83243
  (h) => process.stderr.write(`formfeed-mcp listening on ${h.url}
83116
83244
  `)
83117
83245
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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": {