@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 +4 -2
- package/bin/formfeed-mcp.mjs +180 -52
- 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";
|
|
@@ -77504,6 +77503,7 @@ function toDate2(value, tz) {
|
|
|
77504
77503
|
function escapeHtml(value) {
|
|
77505
77504
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
77643
|
-
|
|
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
|
-
|
|
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
|
|
82774
|
+
let json2 = null;
|
|
82752
82775
|
try {
|
|
82753
|
-
|
|
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 =
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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}${
|
|
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