@formfeed/mcp 0.3.3 → 0.3.5
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 +4 -2
- package/bin/formfeed-mcp.mjs +161 -45
- package/package.json +1 -1
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
|
|
27
|
-
`formfeed
|
|
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>
|
package/bin/formfeed-mcp.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
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";
|
|
@@ -82407,6 +82406,7 @@ var Formfeed = class {
|
|
|
82407
82406
|
fetchImpl;
|
|
82408
82407
|
maxRetries;
|
|
82409
82408
|
timeoutMs;
|
|
82409
|
+
extraHeaders;
|
|
82410
82410
|
constructor(options) {
|
|
82411
82411
|
if (!options.apiKey) throw new FormfeedError("invalid_request", "apiKey is required", 0);
|
|
82412
82412
|
this.apiKey = options.apiKey;
|
|
@@ -82414,6 +82414,7 @@ var Formfeed = class {
|
|
|
82414
82414
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
82415
82415
|
this.maxRetries = options.maxRetries ?? 3;
|
|
82416
82416
|
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
82417
|
+
this.extraHeaders = Object.fromEntries(Object.entries(options.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
|
|
82417
82418
|
if (!this.fetchImpl) throw new FormfeedError("invalid_request", "fetch is not available; pass options.fetch", 0);
|
|
82418
82419
|
}
|
|
82419
82420
|
renders = {
|
|
@@ -82681,11 +82682,20 @@ var Formfeed = class {
|
|
|
82681
82682
|
);
|
|
82682
82683
|
}
|
|
82683
82684
|
},
|
|
82684
|
-
|
|
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
|
+
},
|
|
82685
82695
|
/**
|
|
82686
82696
|
* Checks a version with data without rendering: syntax, header and footer, missing partials,
|
|
82687
82697
|
* runtime errors and the data against the stored schema. Free. Without `data`, the version's
|
|
82688
|
-
* sample data is checked.
|
|
82698
|
+
* sample data is checked. `version` takes a channel name too.
|
|
82689
82699
|
*/
|
|
82690
82700
|
validate: (idOrSlug, input = {}, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/validate`, input, options)
|
|
82691
82701
|
};
|
|
@@ -82724,7 +82734,8 @@ var Formfeed = class {
|
|
|
82724
82734
|
const headers = {
|
|
82725
82735
|
authorization: `Bearer ${this.apiKey}`,
|
|
82726
82736
|
accept: as === "bytes" ? "*/*" : "application/json",
|
|
82727
|
-
"user-agent": "formfeed-sdk-ts/0.3"
|
|
82737
|
+
"user-agent": "formfeed-sdk-ts/0.3",
|
|
82738
|
+
...this.extraHeaders
|
|
82728
82739
|
};
|
|
82729
82740
|
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
82730
82741
|
if (body !== void 0 && !isForm) headers["content-type"] = "application/json";
|
|
@@ -82760,14 +82771,14 @@ var Formfeed = class {
|
|
|
82760
82771
|
if (res.status === 204) return { status: 204, body: void 0 };
|
|
82761
82772
|
if (as === "bytes" && res.ok) return { status: res.status, body: new Uint8Array(await res.arrayBuffer()) };
|
|
82762
82773
|
const text2 = await res.text();
|
|
82763
|
-
let
|
|
82774
|
+
let json2 = null;
|
|
82764
82775
|
try {
|
|
82765
|
-
|
|
82776
|
+
json2 = text2 ? JSON.parse(text2) : null;
|
|
82766
82777
|
} catch {
|
|
82767
82778
|
if (res.ok) throw new FormfeedError("invalid_response", `the API answered HTTP ${res.status} with a body that is not JSON`, res.status);
|
|
82768
82779
|
}
|
|
82769
82780
|
if (!res.ok) {
|
|
82770
|
-
const problem =
|
|
82781
|
+
const problem = json2 ?? {};
|
|
82771
82782
|
throw new FormfeedError(
|
|
82772
82783
|
problem.code ?? `http_${res.status}`,
|
|
82773
82784
|
problem.detail ?? problem.title ?? `HTTP ${res.status}`,
|
|
@@ -82776,7 +82787,7 @@ var Formfeed = class {
|
|
|
82776
82787
|
res.headers.get("x-request-id")
|
|
82777
82788
|
);
|
|
82778
82789
|
}
|
|
82779
|
-
return { status: res.status, body:
|
|
82790
|
+
return { status: res.status, body: json2 };
|
|
82780
82791
|
}
|
|
82781
82792
|
}
|
|
82782
82793
|
backoff(attempt, retryAfter) {
|
|
@@ -82792,7 +82803,7 @@ var encoder2 = new TextEncoder();
|
|
|
82792
82803
|
// packages/mcp/src/lib/server.ts
|
|
82793
82804
|
import { z as z2 } from "zod";
|
|
82794
82805
|
var CONVERT_LIMIT_BYTES = 20 * 1024 * 1024;
|
|
82795
|
-
var SERVER_INFO = { name: "formfeed", version: "0.3.
|
|
82806
|
+
var SERVER_INFO = { name: "formfeed", version: "0.3.5" };
|
|
82796
82807
|
var engineSchema = z2.enum(["jinja2", "liquid", "handlebars"]);
|
|
82797
82808
|
var outputSchema = z2.enum(outputFormats);
|
|
82798
82809
|
var kindSchema = z2.enum(["pdf", "image", "docx", "pptx"]);
|
|
@@ -82828,6 +82839,7 @@ function failure(e) {
|
|
|
82828
82839
|
function createFormfeedServer(options) {
|
|
82829
82840
|
const client = new Formfeed({
|
|
82830
82841
|
apiKey: options.apiKey,
|
|
82842
|
+
headers: { "user-agent": `formfeed-mcp/${SERVER_INFO.version}`, ...options.workspaceId ? { "x-formfeed-workspace": options.workspaceId } : {} },
|
|
82831
82843
|
...options.region ? { region: options.region } : {},
|
|
82832
82844
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {},
|
|
82833
82845
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
@@ -83056,6 +83068,102 @@ function createFormfeedServer(options) {
|
|
|
83056
83068
|
return server;
|
|
83057
83069
|
}
|
|
83058
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
|
+
|
|
83059
83167
|
// packages/mcp/src/lib/transports.ts
|
|
83060
83168
|
async function startStdio(options) {
|
|
83061
83169
|
const server = createFormfeedServer({ localFiles: true, ...options });
|
|
@@ -83063,52 +83171,58 @@ async function startStdio(options) {
|
|
|
83063
83171
|
}
|
|
83064
83172
|
async function startHttp(options = {}) {
|
|
83065
83173
|
const host = options.host ?? "127.0.0.1";
|
|
83066
|
-
const
|
|
83174
|
+
const { port: _port, host: _host, ...handlerOptions } = options;
|
|
83175
|
+
void _port;
|
|
83176
|
+
void _host;
|
|
83177
|
+
const handler = mcpFetchHandler(handlerOptions);
|
|
83067
83178
|
const server = createServer((req, res) => {
|
|
83068
|
-
void
|
|
83179
|
+
void toRequest(req, host).then(handler).then((response) => send(response, res)).catch((e) => {
|
|
83069
83180
|
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
|
|
83070
83181
|
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
83071
83182
|
});
|
|
83072
83183
|
});
|
|
83073
|
-
async function handle(req, res) {
|
|
83074
|
-
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
83075
|
-
if (url.pathname === "/healthz") {
|
|
83076
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
83077
|
-
res.end(JSON.stringify({ ok: true }));
|
|
83078
|
-
return;
|
|
83079
|
-
}
|
|
83080
|
-
if (url.pathname !== path) {
|
|
83081
|
-
res.writeHead(404);
|
|
83082
|
-
res.end();
|
|
83083
|
-
return;
|
|
83084
|
-
}
|
|
83085
|
-
const auth = req.headers.authorization ?? "";
|
|
83086
|
-
const apiKey = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
|
|
83087
|
-
if (!apiKey) {
|
|
83088
|
-
res.writeHead(401, { "content-type": "application/json", "www-authenticate": 'Bearer realm="formfeed"' });
|
|
83089
|
-
res.end(JSON.stringify({ error: "Authorization: Bearer <formfeed api key> required" }));
|
|
83090
|
-
return;
|
|
83091
|
-
}
|
|
83092
|
-
const mcp = createFormfeedServer({ apiKey, baseUrl: options.baseUrl, region: options.region, fetch: options.fetch });
|
|
83093
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
83094
|
-
res.on("close", () => {
|
|
83095
|
-
void transport.close();
|
|
83096
|
-
void mcp.close();
|
|
83097
|
-
});
|
|
83098
|
-
await mcp.connect(transport);
|
|
83099
|
-
await transport.handleRequest(req, res);
|
|
83100
|
-
}
|
|
83101
83184
|
await new Promise((resolve2, reject) => {
|
|
83102
83185
|
server.once("error", reject);
|
|
83103
83186
|
server.listen(options.port ?? 8790, host, () => resolve2());
|
|
83104
83187
|
});
|
|
83105
83188
|
const port = server.address().port;
|
|
83106
83189
|
return {
|
|
83107
|
-
url: `http://${host}:${port}${
|
|
83190
|
+
url: `http://${host}:${port}${MCP_PATH}`,
|
|
83108
83191
|
port,
|
|
83109
83192
|
close: () => new Promise((resolve2) => server.close(() => resolve2()))
|
|
83110
83193
|
};
|
|
83111
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
|
+
}
|
|
83112
83226
|
|
|
83113
83227
|
// packages/mcp/src/main.ts
|
|
83114
83228
|
var args = process.argv.slice(2);
|
|
@@ -83118,12 +83232,14 @@ var flag = (name) => {
|
|
|
83118
83232
|
};
|
|
83119
83233
|
var baseUrl = process.env["FORMFEED_BASE_URL"];
|
|
83120
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"];
|
|
83121
83237
|
if (args.includes("--help") || args.includes("-h")) {
|
|
83122
83238
|
process.stdout.write(
|
|
83123
|
-
"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"
|
|
83124
83240
|
);
|
|
83125
83241
|
} else if (args.includes("--http")) {
|
|
83126
|
-
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(
|
|
83127
83243
|
(h) => process.stderr.write(`formfeed-mcp listening on ${h.url}
|
|
83128
83244
|
`)
|
|
83129
83245
|
);
|
package/package.json
CHANGED