iframer-toolkit 0.7.1 → 0.8.0

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.
@@ -98,23 +98,28 @@ function getLocalToken() {
98
98
 
99
99
  // src/mcp/local-server.ts
100
100
  var import_child_process = require("child_process");
101
+ var import_net = __toESM(require("net"));
101
102
  var import_fs2 = __toESM(require("fs"));
102
103
  var import_path3 = __toESM(require("path"));
103
104
  var __dirname = "/Users/eduardoverona/tools/iframer-toolkit/src/mcp";
104
- var PORT = parseInt(process.env.IFRAMER_LOCAL_PORT || "3022", 10);
105
+ var BASE_PORT = parseInt(process.env.IFRAMER_LOCAL_PORT || "3022", 10);
106
+ var PORT_SCAN_ATTEMPTS = 200;
105
107
  var STARTUP_TIMEOUT_MS = 15000;
106
108
  var HEALTH_POLL_MS = 300;
107
109
 
108
110
  class LocalServerManager {
109
111
  child = null;
110
112
  startingPromise = null;
111
- baseUrl;
113
+ port = null;
114
+ baseUrl = "";
112
115
  logPath;
113
116
  constructor() {
114
- this.baseUrl = `http://127.0.0.1:${PORT}`;
115
117
  this.logPath = import_path3.default.join(getDataDir(), "local-server.log");
116
118
  }
117
119
  getBaseUrl() {
120
+ if (!this.baseUrl) {
121
+ throw new Error("Local server not started yet — call ensureRunning() first.");
122
+ }
118
123
  return this.baseUrl;
119
124
  }
120
125
  async ensureRunning() {
@@ -128,16 +133,18 @@ class LocalServerManager {
128
133
  return this.startingPromise;
129
134
  }
130
135
  async doStart() {
131
- await this.killExisting();
136
+ this.port = await findFreePort(BASE_PORT, PORT_SCAN_ATTEMPTS);
137
+ this.baseUrl = `http://127.0.0.1:${this.port}`;
132
138
  const dataDir = getDataDir();
133
139
  import_fs2.default.mkdirSync(dataDir, { recursive: true });
134
140
  const { command, args } = this.resolveRuntime();
135
141
  const logFd = import_fs2.default.openSync(this.logPath, "a");
136
142
  const env = {
137
143
  ...process.env,
138
- PORT: String(PORT),
144
+ PORT: String(this.port),
139
145
  IFRAMER_MODE: "local",
140
- IFRAMER_DATA_DIR: dataDir
146
+ IFRAMER_DATA_DIR: dataDir,
147
+ IFRAMER_PARENT_PID: String(process.pid)
141
148
  };
142
149
  if (process.env.IFRAMER_SECRET) {
143
150
  env.IFRAMER_SECRET = process.env.IFRAMER_SECRET;
@@ -165,7 +172,7 @@ class LocalServerManager {
165
172
  }
166
173
  this.kill();
167
174
  const logTail = this.readLogTail();
168
- throw new Error(`Local iframer server failed to start on port ${PORT} within ${STARTUP_TIMEOUT_MS}ms.
175
+ throw new Error(`Local iframer server failed to start on port ${this.port} within ${STARTUP_TIMEOUT_MS}ms.
169
176
  ` + `Last log lines:
170
177
  ${logTail}`);
171
178
  }
@@ -207,23 +214,9 @@ ${logTail}`);
207
214
  }
208
215
  this.child = null;
209
216
  }
210
- async killExisting() {
211
- try {
212
- const res = await fetch(`${this.baseUrl}/health`, {
213
- signal: AbortSignal.timeout(2000)
214
- });
215
- if (res.ok) {
216
- try {
217
- await fetch(`${this.baseUrl}/shutdown`, {
218
- method: "POST",
219
- signal: AbortSignal.timeout(2000)
220
- });
221
- } catch {}
222
- await sleep(500);
223
- }
224
- } catch {}
225
- }
226
217
  async healthCheck() {
218
+ if (!this.baseUrl)
219
+ return false;
227
220
  try {
228
221
  const res = await fetch(`${this.baseUrl}/health`, {
229
222
  signal: AbortSignal.timeout(2000)
@@ -249,6 +242,26 @@ ${logTail}`);
249
242
  function sleep(ms) {
250
243
  return new Promise((r) => setTimeout(r, ms));
251
244
  }
245
+ function findFreePort(start, attempts) {
246
+ return new Promise((resolve, reject) => {
247
+ const tryPort = (p) => {
248
+ if (p >= start + attempts) {
249
+ reject(new Error(`No free port found in ${start}-${start + attempts}`));
250
+ return;
251
+ }
252
+ const srv = import_net.default.createServer();
253
+ srv.once("error", () => {
254
+ srv.close();
255
+ tryPort(p + 1);
256
+ });
257
+ srv.once("listening", () => {
258
+ srv.close(() => resolve(p));
259
+ });
260
+ srv.listen(p, "127.0.0.1");
261
+ };
262
+ tryPort(start);
263
+ });
264
+ }
252
265
 
253
266
  // src/mcp/helpers.ts
254
267
  var log = createLogger("mcp");
@@ -581,7 +594,8 @@ Returns: ok, completedSteps, results, obstacles, capturedApi, and on failure: er
581
594
  continueOnError: import_zod3.z.boolean().optional().describe("Continue past failing steps (default: false)"),
582
595
  captureApi: import_zod3.z.boolean().optional().describe("Record all API calls (XHR/fetch) the page makes."),
583
596
  mode: import_zod3.z.enum(["headless", "binary-headful", "docker-headful"]).optional().describe("DO NOT SET THIS unless user explicitly requests a mode. iframer auto-selects and auto-escalates."),
584
- autoEscalate: import_zod3.z.boolean().optional().describe("Auto-retry with a stronger mode if blocked (default: true)")
597
+ autoEscalate: import_zod3.z.boolean().optional().describe("Auto-retry with a stronger mode if blocked (default: true)"),
598
+ instanceId: import_zod3.z.string().optional().describe("Run in a named parallel browser within this session (default: 'default'). Use distinct ids to drive several browsers at once, e.g. one per account — each keeps its own login/session state.")
585
599
  }).optional()
586
600
  }, async (params) => {
587
601
  try {
@@ -759,150 +773,498 @@ ${api.domain} (${api.baseUrl})`);
759
773
  }
760
774
 
761
775
  // src/mcp/tools/session.ts
776
+ var import_zod5 = require("zod");
777
+ var import_path6 = __toESM(require("path"));
778
+ var import_fs4 = __toESM(require("fs"));
779
+
780
+ // src/mcp/tools/reverse-engineer.ts
762
781
  var import_zod4 = require("zod");
763
- function registerSessionTool(server) {
764
- server.tool("session", `Manage the browser session and lifecycle.
782
+ function registerReverseEngineerTool(server) {
783
+ server.tool("reverse-engineer", `Reverse-engineer a website's API. Navigates to a URL, performs the steps you specify, and captures every XHR/fetch request the page makes — including auth tokens, cookies, headers, request/response bodies, and ready-to-use curl commands.
765
784
 
766
- Actions:
767
- - **stop**: save cookies/localStorage to the store, then close the browser. Session data persists for the next run.
768
- - **clear**: wipe all stored session data (cookies/localStorage) from the database. Does NOT kill running browsers.
769
- - **restart**: kill all running browser instances (local + Docker) and reset state. The next \`execute\` call will launch a fresh browser automatically. Use this when the browser is frozen, crashed, or in a bad state. Credentials and knowledge cache are NOT affected.
785
+ Use this when the user asks to:
786
+ - "reverse engineer" or "map" a site's API
787
+ - "capture the endpoints" or "save the API"
788
+ - "figure out how this site works under the hood"
789
+ - "record the API calls" so they can be replayed later
770
790
 
771
- Sessions live in the single local SQLite database (~/.iframer/iframer.db), shared across every browser mode.`, {
772
- action: import_zod4.z.enum(["stop", "clear", "restart"]).describe("stop: save session state + close browser | clear: wipe stored session data | restart: kill all browsers, fresh start on next execute")
773
- }, async ({ action }) => {
791
+ How it works:
792
+ 1. You provide steps (same as execute) — navigate, click, fill, extract, etc.
793
+ 2. iframer runs them while recording all API calls the page makes
794
+ 3. Returns structured data per domain: shared auth + endpoints classified by protocol (rest, graphql, json-rpc, grpc-web, form-rpc, soap) with method, path, action, verb, headers, body, response, curl
795
+
796
+ IMPORTANT — one request ≠ one endpoint. Each endpoint has (protocol, action):
797
+ - REST: action = "METHOD /parameterized/path". Verb from HTTP method.
798
+ - GraphQL: action = operationName (or doc_id for persisted queries). Many ops share the single /graphql URL — EACH operation is its own endpoint.
799
+ - JSON-RPC: action = body.method (e.g. eth_getBalance, user.list).
800
+ - gRPC-web: action = request path.
801
+ - Form-RPC (FB-style urlencoded): action = fb_api_req_friendly_name / doc_id.
802
+ - SOAP: action = SOAPAction header.
803
+
804
+ Generate ONE function per (protocol, action). Do NOT merge different GraphQL operations into one function just because they share the URL.
805
+
806
+ Output layout — save as RUNNABLE CODE to <outputDir>/:
807
+ auth.{js,ts} — shared cookies, tokens, authorization
808
+ transport/
809
+ rest.{js,ts} — shared REST helper (only if any rest endpoints)
810
+ graphql.{js,ts} — shared GraphQL client: post(operationName|docId, variables)
811
+ jsonRpc.{js,ts} — shared JSON-RPC client (if any)
812
+ grpc.{js,ts} — shared gRPC-web client (if any)
813
+ <protocol>/<verb>/<functionName>.{js,ts}
814
+ e.g. graphql/queries/getTimelineFeed.ts
815
+ graphql/mutations/reactToPost.ts
816
+ rest/read/getChannelMessages.ts
817
+ rest/create/createMessage.ts
818
+ jsonRpc/ethGetBalance.ts
819
+ index.{js,ts} — re-exports all endpoint functions
820
+ types.ts — (typed mode only) inferred interfaces from responses
821
+ README.md — endpoints grouped by protocol + verb, dependency chain, auth expiry warning
822
+
823
+ Use capturedApi[i].endpoints[j].protocol, .action, .verb, .functionName directly — iframer already classified them. Put queries (verb=read|list) under queries/, mutations (verb=create|update|delete|action) under mutations/ for GraphQL. For REST, group by verb dir.
824
+
825
+ The outputDir defaults to ./<domain>/. Ask the user where to save if unclear.`, {
826
+ steps: import_zod4.z.array(stepSchema).describe("Pipeline steps to execute while capturing API calls"),
827
+ outputDir: import_zod4.z.string().optional().describe("Directory to save the captured API files. If not provided, ask the user or default to ./<domain>/"),
828
+ typed: import_zod4.z.boolean().optional().describe("Save as .ts with inferred types instead of .js. Set to true when the user asks for types, typescript, or type inference."),
829
+ options: import_zod4.z.object({
830
+ staleTimeoutMs: import_zod4.z.number().optional().describe("Override the 20s stale-state timeout per step"),
831
+ continueOnObstacle: import_zod4.z.boolean().optional().describe("Try to auto-resolve obstacles (default: true)"),
832
+ continueOnError: import_zod4.z.boolean().optional().describe("Continue past failing steps (default: false)"),
833
+ mode: import_zod4.z.enum(["headless", "binary-headful", "docker-headful"]).optional().describe("Browser mode override")
834
+ }).optional()
835
+ }, async (params) => {
774
836
  try {
775
- if (action === "stop") {
776
- const result = await localApiPost("/interactive/stop").catch(() => ({ ok: true, sessionSaved: false }));
777
- return { content: [{ type: "text", text: `Session stopped. State saved: ${result.sessionSaved ?? false}` }] };
837
+ const execParams = {
838
+ steps: params.steps,
839
+ options: { ...params.options, captureApi: true }
840
+ };
841
+ const mode = params.options?.mode;
842
+ const dockerRunning = await isDockerRunning();
843
+ const captureResult = mode === "docker-headful" && dockerRunning ? await apiPost("/execute", execParams) : await localApiPost("/execute", execParams);
844
+ const lines = [];
845
+ lines.push(`ok: ${captureResult.ok}`);
846
+ lines.push(`steps: ${captureResult.completedSteps}/${captureResult.totalSteps}`);
847
+ if (captureResult.durationMs)
848
+ lines.push(`duration: ${captureResult.durationMs}ms`);
849
+ if (captureResult.finalState) {
850
+ lines.push(`
851
+ Final page: ${captureResult.finalState.title}`);
852
+ lines.push(`URL: ${captureResult.finalState.url}`);
778
853
  }
779
- if (action === "clear") {
780
- await localApiDelete("/session").catch(() => {});
781
- return { content: [{ type: "text", text: "Session data cleared from database." }] };
854
+ if (captureResult.capturedApi && captureResult.capturedApi.length > 0) {
855
+ formatCapturedApi(lines, captureResult.capturedApi, params);
856
+ } else {
857
+ lines.push(`
858
+ No API calls were captured. The page may not have made any XHR/fetch requests during the steps, or the steps may not have triggered the expected behavior.`);
782
859
  }
783
- if (action === "restart") {
784
- const parts = [];
785
- try {
786
- await localApiPost("/browser/restart");
787
- parts.push("Local browser restarted.");
788
- } catch {
789
- try {
790
- await localServer.restart();
791
- parts.push("Local server respawned.");
792
- } catch (e) {
793
- parts.push(`Local restart failed: ${e instanceof Error ? e.message : String(e)}`);
794
- }
860
+ if (captureResult.error) {
861
+ lines.push(`
862
+ --- Failure ---`);
863
+ if (typeof captureResult.error === "string") {
864
+ lines.push(`Error: ${captureResult.error}`);
865
+ } else {
866
+ lines.push(`Failed at step ${captureResult.error.failedAtStep}: ${JSON.stringify(captureResult.error.failedStep)}`);
867
+ lines.push(`Error type: ${captureResult.error.errorType}`);
868
+ lines.push(`Message: ${captureResult.error.message}`);
869
+ if (captureResult.error.suggestion)
870
+ lines.push(`Suggestion: ${captureResult.error.suggestion}`);
871
+ }
872
+ }
873
+ const screenshotUrl = captureResult.error?.pageState?.screenshotUrl ?? captureResult.finalState?.screenshotUrl;
874
+ if (screenshotUrl) {
875
+ const filePath = await resolveScreenshotPath(screenshotUrl);
876
+ if (filePath) {
877
+ lines.push(`
878
+ Screenshot saved: ${filePath}`);
879
+ lines.push("Use the Read tool on the path above to view the screenshot.");
795
880
  }
881
+ }
882
+ if (captureResult.capturedApi && captureResult.capturedApi.length > 0) {
883
+ const mainDomain = captureResult.capturedApi[0]?.domain || "api";
884
+ const outDir = params.outputDir || `./${mainDomain}`;
796
885
  try {
797
- if (await isDockerRunning()) {
798
- await apiPost("/browser/restart");
799
- parts.push("Docker browser restarted.");
800
- }
801
- } catch {}
802
- parts.push("Credentials and knowledge cache are untouched. Next execute launches a fresh browser.");
803
- return { content: [{ type: "text", text: parts.join(" ") }] };
886
+ const fs4 = await import("fs");
887
+ const path6 = await import("path");
888
+ fs4.mkdirSync(outDir, { recursive: true });
889
+ const jsonPath = path6.join(outDir, "captured-api.json");
890
+ fs4.writeFileSync(jsonPath, JSON.stringify(captureResult.capturedApi, null, 2));
891
+ lines.push(`
892
+ Full captured data saved to: ${jsonPath}`);
893
+ lines.push("Read this file for complete curl commands, request/response bodies, and auth data.");
894
+ } catch (writeErr) {
895
+ lines.push(`
896
+ (Could not save captured JSON: ${writeErr instanceof Error ? writeErr.message : String(writeErr)})`);
897
+ }
804
898
  }
805
- return err("Unknown action");
899
+ let text = lines.join(`
900
+ `);
901
+ if (text.length > 80000) {
902
+ text = text.slice(0, 80000) + `
903
+
904
+ [... response truncated — read captured-api.json for full data]`;
905
+ }
906
+ const content = [{ type: "text", text }];
907
+ if (!captureResult.ok)
908
+ return { content, isError: true };
909
+ return { content };
806
910
  } catch (e) {
807
- return err(`Error: ${getErrorMessage(e)}`);
911
+ return err(`Connection error: ${getErrorMessage(e)}. Try \`session restart\` and retry.`);
808
912
  }
809
913
  });
810
914
  }
811
-
812
- // src/mcp/tools/credentials.ts
813
- var import_zod5 = require("zod");
814
- var import_fs5 = __toESM(require("fs"));
815
- var import_path7 = __toESM(require("path"));
816
-
817
- // src/lib/knowledge.ts
818
- var import_fs4 = __toESM(require("fs"));
819
- var import_path6 = __toESM(require("path"));
820
- var log3 = createLogger("knowledge");
821
- function getKnowledgeDir() {
822
- return import_path6.default.join(getDataDir(), "knowledge");
823
- }
824
- function getKnowledgePath(domain) {
825
- const safe = sanitizeDomain(domain);
826
- return import_path6.default.join(getKnowledgeDir(), `${safe}.md`);
827
- }
828
- function normalizeDomain(input) {
829
- let d = (input || "").trim().toLowerCase();
830
- if (!d)
831
- return "";
832
- try {
833
- if (d.includes("://")) {
834
- d = new URL(d).hostname;
835
- } else if (d.includes("/")) {
836
- d = new URL(`https://${d}`).hostname;
837
- }
838
- } catch {}
839
- d = d.replace(/:\d+$/, "");
840
- d = d.replace(/^www\./, "");
841
- return d;
842
- }
843
- function sanitizeDomain(input) {
844
- const normalized = normalizeDomain(input);
845
- return normalized.replace(/[^a-z0-9.-]/g, "_");
846
- }
847
- function readKnowledge(domain) {
848
- const p = getKnowledgePath(domain);
849
- try {
850
- return import_fs4.default.readFileSync(p, "utf8");
851
- } catch {
852
- return null;
853
- }
854
- }
855
- function listKnowledge() {
856
- const dir = getKnowledgeDir();
857
- let entries = [];
858
- try {
859
- entries = import_fs4.default.readdirSync(dir);
860
- } catch {
861
- return [];
862
- }
863
- const results = [];
864
- for (const file of entries) {
865
- if (!file.endsWith(".md"))
866
- continue;
867
- const full = import_path6.default.join(dir, file);
868
- try {
869
- const stat = import_fs4.default.statSync(full);
870
- const raw = import_fs4.default.readFileSync(full, "utf8");
871
- const parsed = parseMarkdown(raw);
872
- results.push({
873
- domain: parsed?.domain ?? file.replace(/\.md$/, ""),
874
- lastVerified: parsed?.lastVerified ?? new Date(stat.mtimeMs).toISOString(),
875
- lastMode: parsed?.lastMode ?? "unknown",
876
- sizeBytes: stat.size
877
- });
878
- } catch {}
879
- }
880
- results.sort((a, b) => a.lastVerified < b.lastVerified ? 1 : -1);
881
- return results;
882
- }
883
- function clearKnowledge(domain) {
884
- const dir = getKnowledgeDir();
885
- if (domain) {
886
- const p = getKnowledgePath(domain);
887
- try {
888
- import_fs4.default.unlinkSync(p);
889
- return { removed: 1 };
890
- } catch {
891
- return { removed: 0 };
892
- }
893
- }
894
- let removed = 0;
895
- try {
896
- const entries = import_fs4.default.readdirSync(dir);
897
- for (const f of entries) {
898
- if (f.endsWith(".md")) {
899
- try {
900
- import_fs4.default.unlinkSync(import_path6.default.join(dir, f));
901
- removed++;
902
- } catch {}
915
+ function formatCapturedApi(lines, capturedApi, params) {
916
+ for (const api of capturedApi) {
917
+ lines.push(`
918
+ ━━━ ${api.domain} (${api.baseUrl}) ━━━`);
919
+ const authParts = [];
920
+ if (api.auth?.authorization)
921
+ authParts.push(`Authorization: ${api.auth.authorization.slice(0, 30)}...`);
922
+ if (api.auth?.cookies && Object.keys(api.auth.cookies).length > 0)
923
+ authParts.push(`${Object.keys(api.auth.cookies).length} cookies`);
924
+ if (api.auth?.tokens && Object.keys(api.auth.tokens).length > 0) {
925
+ for (const [k, v] of Object.entries(api.auth.tokens)) {
926
+ authParts.push(`${k}: ${String(v).slice(0, 30)}...`);
903
927
  }
904
928
  }
905
- } catch {}
929
+ if (authParts.length > 0) {
930
+ lines.push(`
931
+ Auth:`);
932
+ for (const part of authParts)
933
+ lines.push(` ${part}`);
934
+ }
935
+ const byProtocol = new Map;
936
+ for (const ep of api.endpoints) {
937
+ const p = ep.protocol || "rest";
938
+ if (!byProtocol.has(p))
939
+ byProtocol.set(p, []);
940
+ byProtocol.get(p).push(ep);
941
+ }
942
+ const protocolSummary = Array.from(byProtocol.entries()).map(([p, eps]) => `${p}=${eps.length}`).join(", ");
943
+ lines.push(`
944
+ Endpoints (${api.endpoints.length}) [${protocolSummary}]:`);
945
+ for (const ep of api.endpoints) {
946
+ lines.push(`
947
+ [${ep.protocol}/${ep.verb}] ${ep.functionName}`);
948
+ lines.push(` Action: ${ep.action}`);
949
+ lines.push(` ${ep.method} ${ep.path} → ${ep.responseStatus}`);
950
+ if (ep.requestBody) {
951
+ const body = ep.requestBody;
952
+ const signalKeys = extractSignalKeys(body);
953
+ if (signalKeys)
954
+ lines.push(` Params: ${signalKeys}`);
955
+ }
956
+ }
957
+ }
958
+ const mainDomain = capturedApi[0]?.domain || "api";
959
+ const dir = params.outputDir || `./${mainDomain}`;
960
+ const ext = params.typed ? ".ts" : ".js";
961
+ lines.push(`
962
+ ━━━ Save instructions ━━━`);
963
+ lines.push(`Save to: ${dir}`);
964
+ lines.push(`Format: ${ext} files`);
965
+ const protocolsSeen = new Set;
966
+ for (const api of capturedApi)
967
+ for (const ep of api.endpoints)
968
+ protocolsSeen.add(ep.protocol);
969
+ lines.push(`
970
+ Layout (protocols present: ${Array.from(protocolsSeen).join(", ")}):`);
971
+ lines.push(` auth${ext}`);
972
+ if (protocolsSeen.has("rest"))
973
+ lines.push(` transport/rest${ext} — shared fetch wrapper with auth headers`);
974
+ if (protocolsSeen.has("graphql"))
975
+ lines.push(` transport/graphql${ext} — post(operationName|docId, variables) → shared GraphQL client`);
976
+ if (protocolsSeen.has("json-rpc"))
977
+ lines.push(` transport/jsonRpc${ext} — shared JSON-RPC client`);
978
+ if (protocolsSeen.has("grpc-web"))
979
+ lines.push(` transport/grpc${ext} — shared gRPC-web client`);
980
+ if (protocolsSeen.has("form-rpc"))
981
+ lines.push(` transport/formRpc${ext} — shared urlencoded RPC client`);
982
+ if (protocolsSeen.has("soap"))
983
+ lines.push(` transport/soap${ext} — shared SOAP client`);
984
+ lines.push(` <protocol>/<verb>/<functionName>${ext} — one file per endpoint, uses shared transport`);
985
+ lines.push(` e.g. graphql/queries/<fn>${ext}, graphql/mutations/<fn>${ext}, rest/read/<fn>${ext}, rest/create/<fn>${ext}`);
986
+ lines.push(` index${ext} — re-export all endpoint functions`);
987
+ if (params.typed)
988
+ lines.push(` types.ts — interfaces inferred from response bodies`);
989
+ lines.push(` README.md — endpoints grouped by protocol + verb, auth expiry warning`);
990
+ lines.push(`
991
+ Rules:`);
992
+ lines.push(` - One function per (protocol, action). Use the functionName field verbatim for the file + export name.`);
993
+ lines.push(` - verb=read|list → queries/ (GraphQL) or read/ (REST). verb=create|update|delete|action → mutations/ (GraphQL) or verb dir (REST).`);
994
+ lines.push(` - Each endpoint file is minimal: import transport + auth, call transport with the action id, pass variables, return typed result.`);
995
+ lines.push(` - GraphQL transport signature: post(opNameOrDocId: string, variables: object). Pick doc_id when present in captured body, else operationName.`);
996
+ lines.push(` - Do NOT inline auth headers per endpoint — they live in auth${ext} and the transport reads them.`);
997
+ lines.push(`
998
+ IMPORTANT: Auth data contains real tokens/cookies — they expire. Remind the user.`);
999
+ }
1000
+ function extractSignalKeys(body) {
1001
+ const NOISE = new Set([
1002
+ "__csr",
1003
+ "__hsdp",
1004
+ "__hblp",
1005
+ "__dyn",
1006
+ "__a",
1007
+ "__req",
1008
+ "__hs",
1009
+ "__comet_req",
1010
+ "__ccg",
1011
+ "__spin_r",
1012
+ "__spin_b",
1013
+ "__spin_t",
1014
+ "__jssesw",
1015
+ "lsd",
1016
+ "fb_dtsg",
1017
+ "fb_api_caller_class",
1018
+ "fb_api_req_friendly_name",
1019
+ "jazoest",
1020
+ "server_timestamps",
1021
+ "__s",
1022
+ "__user",
1023
+ "dpr",
1024
+ "__rev"
1025
+ ]);
1026
+ const signalEntries = [];
1027
+ for (const [k, v] of Object.entries(body)) {
1028
+ if (NOISE.has(k))
1029
+ continue;
1030
+ if (typeof v === "string" && v.length > 80) {
1031
+ signalEntries.push(`${k}=${v.slice(0, 40)}...`);
1032
+ } else if (typeof v === "object" && v !== null) {
1033
+ const keys = Object.keys(v).slice(0, 5).join(",");
1034
+ signalEntries.push(`${k}={${keys}${Object.keys(v).length > 5 ? ",..." : ""}}`);
1035
+ } else {
1036
+ signalEntries.push(`${k}=${String(v).slice(0, 40)}`);
1037
+ }
1038
+ }
1039
+ return signalEntries.length > 0 ? signalEntries.join(", ") : null;
1040
+ }
1041
+
1042
+ // src/mcp/tools/session.ts
1043
+ function registerSessionTool(server) {
1044
+ server.tool("session", `Manage the browser session and lifecycle.
1045
+
1046
+ Actions:
1047
+ - **stop**: save cookies/localStorage to the store, then close the browser. Session data persists for the next run.
1048
+ - **clear**: wipe all stored session data (cookies/localStorage) from the database. Does NOT kill running browsers.
1049
+ - **restart**: kill all running browser instances (local + Docker) and reset state. The next \`execute\` call will launch a fresh browser automatically. Use this when the browser is frozen, crashed, or in a bad state. Credentials and knowledge cache are NOT affected.
1050
+ - **capture-start**: attach a persistent XHR/fetch listener to the running browser. Accumulates ALL requests indefinitely — not tied to any pipeline. Use before triggering the action you want to capture (e.g. upload, delete, async mutation).
1051
+ - **capture-stop**: stop the persistent listener and return all captured endpoints + save to disk. Call this when you're satisfied you've seen the requests you need.
1052
+ - **get-cookies**: extract ALL cookies from the browser context via CDP (including HttpOnly/Secure — below the JS sandbox). Pass urls to scope. Returns name, value, domain, path, httpOnly, secure, expiry.
1053
+ - **get-auth**: extract cookies + localStorage + sessionStorage in one shot. Everything needed to replay authenticated requests from Node.js.
1054
+
1055
+ Sessions live in the single local SQLite database (~/.iframer/iframer.db), shared across every browser mode.`, {
1056
+ action: import_zod5.z.enum(["stop", "clear", "restart", "capture-start", "capture-stop", "get-cookies", "get-auth"]).describe("stop | clear | restart | capture-start | capture-stop | get-cookies | get-auth"),
1057
+ mode: import_zod5.z.enum(["headless", "binary-headful"]).optional().describe("Browser mode (default: binary-headful)"),
1058
+ urls: import_zod5.z.array(import_zod5.z.string()).optional().describe("URLs to scope cookie extraction (get-cookies/get-auth). Omit for all cookies."),
1059
+ instanceId: import_zod5.z.string().optional().describe("Target a named parallel browser within this session (default: 'default'). Match the instanceId used in execute."),
1060
+ outputDir: import_zod5.z.string().optional().describe("Where to save captured-api.json for capture-stop")
1061
+ }, async ({ action, mode, urls, instanceId, outputDir }) => {
1062
+ try {
1063
+ if (action === "stop") {
1064
+ const result = await localApiPost("/interactive/stop").catch(() => ({ ok: true, sessionSaved: false }));
1065
+ return { content: [{ type: "text", text: `Session stopped. State saved: ${result.sessionSaved ?? false}` }] };
1066
+ }
1067
+ if (action === "clear") {
1068
+ await localApiDelete("/session").catch(() => {});
1069
+ return { content: [{ type: "text", text: "Session data cleared from database." }] };
1070
+ }
1071
+ if (action === "restart") {
1072
+ const parts = [];
1073
+ try {
1074
+ await localApiPost("/browser/restart");
1075
+ parts.push("Local browser restarted.");
1076
+ } catch {
1077
+ try {
1078
+ await localServer.restart();
1079
+ parts.push("Local server respawned.");
1080
+ } catch (e) {
1081
+ parts.push(`Local restart failed: ${e instanceof Error ? e.message : String(e)}`);
1082
+ }
1083
+ }
1084
+ try {
1085
+ if (await isDockerRunning()) {
1086
+ await apiPost("/browser/restart");
1087
+ parts.push("Docker browser restarted.");
1088
+ }
1089
+ } catch {}
1090
+ parts.push("Credentials and knowledge cache are untouched. Next execute launches a fresh browser.");
1091
+ return { content: [{ type: "text", text: parts.join(" ") }] };
1092
+ }
1093
+ if (action === "capture-start") {
1094
+ const result = await localApiPost("/capture/start", { mode: mode ?? "binary-headful", instanceId });
1095
+ return { content: [{ type: "text", text: result.message }] };
1096
+ }
1097
+ if (action === "capture-stop") {
1098
+ const result = await localApiPost("/capture/stop", { mode: mode ?? "binary-headful", instanceId });
1099
+ const lines = [result.message];
1100
+ if (result.capturedApi && result.capturedApi.length > 0) {
1101
+ formatCapturedApi(lines, result.capturedApi, { outputDir, typed: false });
1102
+ const outDir = outputDir || import_path6.default.join(getDataDir(), "capture");
1103
+ try {
1104
+ import_fs4.default.mkdirSync(outDir, { recursive: true });
1105
+ const jsonPath = import_path6.default.join(outDir, "captured-api.json");
1106
+ import_fs4.default.writeFileSync(jsonPath, JSON.stringify(result.capturedApi, null, 2));
1107
+ lines.push(`
1108
+ Full data saved to: ${jsonPath}`);
1109
+ } catch (writeErr) {
1110
+ lines.push(`
1111
+ (Could not save: ${writeErr instanceof Error ? writeErr.message : String(writeErr)})`);
1112
+ }
1113
+ }
1114
+ let text = lines.join(`
1115
+ `);
1116
+ if (text.length > 80000)
1117
+ text = text.slice(0, 80000) + `
1118
+
1119
+ [truncated — read captured-api.json]`;
1120
+ return { content: [{ type: "text", text }] };
1121
+ }
1122
+ if (action === "get-cookies") {
1123
+ const result = await localApiPost("/auth/cookies", { mode: mode ?? "binary-headful", urls, instanceId });
1124
+ const lines = [result.message, ""];
1125
+ for (const c of result.cookies) {
1126
+ lines.push(`${c.name}=${c.value} [domain=${c.domain} path=${c.path} httpOnly=${c.httpOnly} secure=${c.secure}]`);
1127
+ }
1128
+ return { content: [{ type: "text", text: lines.join(`
1129
+ `) }] };
1130
+ }
1131
+ if (action === "get-auth") {
1132
+ const result = await localApiPost("/auth/full", { mode: mode ?? "binary-headful", urls, instanceId });
1133
+ const lines = [result.message, ""];
1134
+ lines.push("=== Cookies ===");
1135
+ for (const c of result.cookies) {
1136
+ lines.push(`${c.name}=${c.value} [domain=${c.domain} httpOnly=${c.httpOnly}]`);
1137
+ }
1138
+ if (Object.keys(result.localStorage || {}).length > 0) {
1139
+ lines.push(`
1140
+ === localStorage ===`);
1141
+ for (const [origin, store] of Object.entries(result.localStorage)) {
1142
+ lines.push(`[${origin}]`);
1143
+ for (const [k, v] of Object.entries(store)) {
1144
+ const val = String(v);
1145
+ lines.push(` ${k}: ${val.length > 120 ? val.slice(0, 120) + "…" : val}`);
1146
+ }
1147
+ }
1148
+ }
1149
+ if (Object.keys(result.sessionStorage || {}).length > 0) {
1150
+ lines.push(`
1151
+ === sessionStorage ===`);
1152
+ for (const [origin, store] of Object.entries(result.sessionStorage)) {
1153
+ lines.push(`[${origin}]`);
1154
+ for (const [k, v] of Object.entries(store)) {
1155
+ const val = String(v);
1156
+ lines.push(` ${k}: ${val.length > 120 ? val.slice(0, 120) + "…" : val}`);
1157
+ }
1158
+ }
1159
+ }
1160
+ let text = lines.join(`
1161
+ `);
1162
+ if (text.length > 80000)
1163
+ text = text.slice(0, 80000) + `
1164
+ [truncated]`;
1165
+ return { content: [{ type: "text", text }] };
1166
+ }
1167
+ return err("Unknown action");
1168
+ } catch (e) {
1169
+ return err(`Error: ${getErrorMessage(e)}`);
1170
+ }
1171
+ });
1172
+ }
1173
+
1174
+ // src/mcp/tools/credentials.ts
1175
+ var import_zod6 = require("zod");
1176
+ var import_fs6 = __toESM(require("fs"));
1177
+ var import_path8 = __toESM(require("path"));
1178
+
1179
+ // src/lib/knowledge.ts
1180
+ var import_fs5 = __toESM(require("fs"));
1181
+ var import_path7 = __toESM(require("path"));
1182
+ var log3 = createLogger("knowledge");
1183
+ function getKnowledgeDir() {
1184
+ return import_path7.default.join(getDataDir(), "knowledge");
1185
+ }
1186
+ function getKnowledgePath(domain) {
1187
+ const safe = sanitizeDomain(domain);
1188
+ return import_path7.default.join(getKnowledgeDir(), `${safe}.md`);
1189
+ }
1190
+ function normalizeDomain(input) {
1191
+ let d = (input || "").trim().toLowerCase();
1192
+ if (!d)
1193
+ return "";
1194
+ try {
1195
+ if (d.includes("://")) {
1196
+ d = new URL(d).hostname;
1197
+ } else if (d.includes("/")) {
1198
+ d = new URL(`https://${d}`).hostname;
1199
+ }
1200
+ } catch {}
1201
+ d = d.replace(/:\d+$/, "");
1202
+ d = d.replace(/^www\./, "");
1203
+ return d;
1204
+ }
1205
+ function sanitizeDomain(input) {
1206
+ const normalized = normalizeDomain(input);
1207
+ return normalized.replace(/[^a-z0-9.-]/g, "_");
1208
+ }
1209
+ function readKnowledge(domain) {
1210
+ const p = getKnowledgePath(domain);
1211
+ try {
1212
+ return import_fs5.default.readFileSync(p, "utf8");
1213
+ } catch {
1214
+ return null;
1215
+ }
1216
+ }
1217
+ function listKnowledge() {
1218
+ const dir = getKnowledgeDir();
1219
+ let entries = [];
1220
+ try {
1221
+ entries = import_fs5.default.readdirSync(dir);
1222
+ } catch {
1223
+ return [];
1224
+ }
1225
+ const results = [];
1226
+ for (const file of entries) {
1227
+ if (!file.endsWith(".md"))
1228
+ continue;
1229
+ const full = import_path7.default.join(dir, file);
1230
+ try {
1231
+ const stat = import_fs5.default.statSync(full);
1232
+ const raw = import_fs5.default.readFileSync(full, "utf8");
1233
+ const parsed = parseMarkdown(raw);
1234
+ results.push({
1235
+ domain: parsed?.domain ?? file.replace(/\.md$/, ""),
1236
+ lastVerified: parsed?.lastVerified ?? new Date(stat.mtimeMs).toISOString(),
1237
+ lastMode: parsed?.lastMode ?? "unknown",
1238
+ sizeBytes: stat.size
1239
+ });
1240
+ } catch {}
1241
+ }
1242
+ results.sort((a, b) => a.lastVerified < b.lastVerified ? 1 : -1);
1243
+ return results;
1244
+ }
1245
+ function clearKnowledge(domain) {
1246
+ const dir = getKnowledgeDir();
1247
+ if (domain) {
1248
+ const p = getKnowledgePath(domain);
1249
+ try {
1250
+ import_fs5.default.unlinkSync(p);
1251
+ return { removed: 1 };
1252
+ } catch {
1253
+ return { removed: 0 };
1254
+ }
1255
+ }
1256
+ let removed = 0;
1257
+ try {
1258
+ const entries = import_fs5.default.readdirSync(dir);
1259
+ for (const f of entries) {
1260
+ if (f.endsWith(".md")) {
1261
+ try {
1262
+ import_fs5.default.unlinkSync(import_path7.default.join(dir, f));
1263
+ removed++;
1264
+ } catch {}
1265
+ }
1266
+ }
1267
+ } catch {}
906
1268
  return { removed };
907
1269
  }
908
1270
  function parseMarkdown(raw) {
@@ -977,8 +1339,8 @@ var ELICIT_TIMEOUT_MS = 45000;
977
1339
  function mcpLog(event, data) {
978
1340
  try {
979
1341
  const dir = getDataDir();
980
- import_fs5.default.mkdirSync(dir, { recursive: true });
981
- import_fs5.default.appendFileSync(import_path7.default.join(dir, "mcp.log"), JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
1342
+ import_fs6.default.mkdirSync(dir, { recursive: true });
1343
+ import_fs6.default.appendFileSync(import_path8.default.join(dir, "mcp.log"), JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
982
1344
  `);
983
1345
  } catch {}
984
1346
  }
@@ -1009,9 +1371,9 @@ CRITICAL RULES
1009
1371
  5. **NEVER ask the user to paste their password in chat.**
1010
1372
 
1011
1373
  6. **\`force: true\` on store is ONLY for explicit password changes.**`, {
1012
- action: import_zod5.z.enum(["store", "list"]).describe("store: prompt for credentials | list: show stored domains"),
1013
- domain: import_zod5.z.string().optional().describe("Domain (required for store). Use the bare registrable domain."),
1014
- force: import_zod5.z.boolean().optional().describe("Overwrite existing. ONLY for explicit password changes.")
1374
+ action: import_zod6.z.enum(["store", "list"]).describe("store: prompt for credentials | list: show stored domains"),
1375
+ domain: import_zod6.z.string().optional().describe("Domain (required for store). Use the bare registrable domain."),
1376
+ force: import_zod6.z.boolean().optional().describe("Overwrite existing. ONLY for explicit password changes.")
1015
1377
  }, async ({ action, domain, force }) => {
1016
1378
  try {
1017
1379
  if (action === "list") {
@@ -1096,262 +1458,6 @@ ${domains.map((d) => ` - ${d}`).join(`
1096
1458
  });
1097
1459
  }
1098
1460
 
1099
- // src/mcp/tools/reverse-engineer.ts
1100
- var import_zod6 = require("zod");
1101
- function registerReverseEngineerTool(server) {
1102
- server.tool("reverse-engineer", `Reverse-engineer a website's API. Navigates to a URL, performs the steps you specify, and captures every XHR/fetch request the page makes — including auth tokens, cookies, headers, request/response bodies, and ready-to-use curl commands.
1103
-
1104
- Use this when the user asks to:
1105
- - "reverse engineer" or "map" a site's API
1106
- - "capture the endpoints" or "save the API"
1107
- - "figure out how this site works under the hood"
1108
- - "record the API calls" so they can be replayed later
1109
-
1110
- How it works:
1111
- 1. You provide steps (same as execute) — navigate, click, fill, extract, etc.
1112
- 2. iframer runs them while recording all API calls the page makes
1113
- 3. Returns structured data per domain: shared auth + endpoints classified by protocol (rest, graphql, json-rpc, grpc-web, form-rpc, soap) with method, path, action, verb, headers, body, response, curl
1114
-
1115
- IMPORTANT — one request ≠ one endpoint. Each endpoint has (protocol, action):
1116
- - REST: action = "METHOD /parameterized/path". Verb from HTTP method.
1117
- - GraphQL: action = operationName (or doc_id for persisted queries). Many ops share the single /graphql URL — EACH operation is its own endpoint.
1118
- - JSON-RPC: action = body.method (e.g. eth_getBalance, user.list).
1119
- - gRPC-web: action = request path.
1120
- - Form-RPC (FB-style urlencoded): action = fb_api_req_friendly_name / doc_id.
1121
- - SOAP: action = SOAPAction header.
1122
-
1123
- Generate ONE function per (protocol, action). Do NOT merge different GraphQL operations into one function just because they share the URL.
1124
-
1125
- Output layout — save as RUNNABLE CODE to <outputDir>/:
1126
- auth.{js,ts} — shared cookies, tokens, authorization
1127
- transport/
1128
- rest.{js,ts} — shared REST helper (only if any rest endpoints)
1129
- graphql.{js,ts} — shared GraphQL client: post(operationName|docId, variables)
1130
- jsonRpc.{js,ts} — shared JSON-RPC client (if any)
1131
- grpc.{js,ts} — shared gRPC-web client (if any)
1132
- <protocol>/<verb>/<functionName>.{js,ts}
1133
- e.g. graphql/queries/getTimelineFeed.ts
1134
- graphql/mutations/reactToPost.ts
1135
- rest/read/getChannelMessages.ts
1136
- rest/create/createMessage.ts
1137
- jsonRpc/ethGetBalance.ts
1138
- index.{js,ts} — re-exports all endpoint functions
1139
- types.ts — (typed mode only) inferred interfaces from responses
1140
- README.md — endpoints grouped by protocol + verb, dependency chain, auth expiry warning
1141
-
1142
- Use capturedApi[i].endpoints[j].protocol, .action, .verb, .functionName directly — iframer already classified them. Put queries (verb=read|list) under queries/, mutations (verb=create|update|delete|action) under mutations/ for GraphQL. For REST, group by verb dir.
1143
-
1144
- The outputDir defaults to ./<domain>/. Ask the user where to save if unclear.`, {
1145
- steps: import_zod6.z.array(stepSchema).describe("Pipeline steps to execute while capturing API calls"),
1146
- outputDir: import_zod6.z.string().optional().describe("Directory to save the captured API files. If not provided, ask the user or default to ./<domain>/"),
1147
- typed: import_zod6.z.boolean().optional().describe("Save as .ts with inferred types instead of .js. Set to true when the user asks for types, typescript, or type inference."),
1148
- options: import_zod6.z.object({
1149
- staleTimeoutMs: import_zod6.z.number().optional().describe("Override the 20s stale-state timeout per step"),
1150
- continueOnObstacle: import_zod6.z.boolean().optional().describe("Try to auto-resolve obstacles (default: true)"),
1151
- continueOnError: import_zod6.z.boolean().optional().describe("Continue past failing steps (default: false)"),
1152
- mode: import_zod6.z.enum(["headless", "binary-headful", "docker-headful"]).optional().describe("Browser mode override")
1153
- }).optional()
1154
- }, async (params) => {
1155
- try {
1156
- const execParams = {
1157
- steps: params.steps,
1158
- options: { ...params.options, captureApi: true }
1159
- };
1160
- const mode = params.options?.mode;
1161
- const dockerRunning = await isDockerRunning();
1162
- const captureResult = mode === "docker-headful" && dockerRunning ? await apiPost("/execute", execParams) : await localApiPost("/execute", execParams);
1163
- const lines = [];
1164
- lines.push(`ok: ${captureResult.ok}`);
1165
- lines.push(`steps: ${captureResult.completedSteps}/${captureResult.totalSteps}`);
1166
- if (captureResult.durationMs)
1167
- lines.push(`duration: ${captureResult.durationMs}ms`);
1168
- if (captureResult.finalState) {
1169
- lines.push(`
1170
- Final page: ${captureResult.finalState.title}`);
1171
- lines.push(`URL: ${captureResult.finalState.url}`);
1172
- }
1173
- if (captureResult.capturedApi && captureResult.capturedApi.length > 0) {
1174
- formatCapturedApi(lines, captureResult.capturedApi, params);
1175
- } else {
1176
- lines.push(`
1177
- No API calls were captured. The page may not have made any XHR/fetch requests during the steps, or the steps may not have triggered the expected behavior.`);
1178
- }
1179
- if (captureResult.error) {
1180
- lines.push(`
1181
- --- Failure ---`);
1182
- if (typeof captureResult.error === "string") {
1183
- lines.push(`Error: ${captureResult.error}`);
1184
- } else {
1185
- lines.push(`Failed at step ${captureResult.error.failedAtStep}: ${JSON.stringify(captureResult.error.failedStep)}`);
1186
- lines.push(`Error type: ${captureResult.error.errorType}`);
1187
- lines.push(`Message: ${captureResult.error.message}`);
1188
- if (captureResult.error.suggestion)
1189
- lines.push(`Suggestion: ${captureResult.error.suggestion}`);
1190
- }
1191
- }
1192
- const screenshotUrl = captureResult.error?.pageState?.screenshotUrl ?? captureResult.finalState?.screenshotUrl;
1193
- if (screenshotUrl) {
1194
- const filePath = await resolveScreenshotPath(screenshotUrl);
1195
- if (filePath) {
1196
- lines.push(`
1197
- Screenshot saved: ${filePath}`);
1198
- lines.push("Use the Read tool on the path above to view the screenshot.");
1199
- }
1200
- }
1201
- if (captureResult.capturedApi && captureResult.capturedApi.length > 0) {
1202
- const mainDomain = captureResult.capturedApi[0]?.domain || "api";
1203
- const outDir = params.outputDir || `./${mainDomain}`;
1204
- try {
1205
- const fs6 = await import("fs");
1206
- const path8 = await import("path");
1207
- fs6.mkdirSync(outDir, { recursive: true });
1208
- const jsonPath = path8.join(outDir, "captured-api.json");
1209
- fs6.writeFileSync(jsonPath, JSON.stringify(captureResult.capturedApi, null, 2));
1210
- lines.push(`
1211
- Full captured data saved to: ${jsonPath}`);
1212
- lines.push("Read this file for complete curl commands, request/response bodies, and auth data.");
1213
- } catch (writeErr) {
1214
- lines.push(`
1215
- (Could not save captured JSON: ${writeErr instanceof Error ? writeErr.message : String(writeErr)})`);
1216
- }
1217
- }
1218
- const content = [{ type: "text", text: lines.join(`
1219
- `) }];
1220
- if (!captureResult.ok)
1221
- return { content, isError: true };
1222
- return { content };
1223
- } catch (e) {
1224
- return err(`Connection error: ${getErrorMessage(e)}. Try \`session restart\` and retry.`);
1225
- }
1226
- });
1227
- }
1228
- function formatCapturedApi(lines, capturedApi, params) {
1229
- for (const api of capturedApi) {
1230
- lines.push(`
1231
- ━━━ ${api.domain} (${api.baseUrl}) ━━━`);
1232
- const authParts = [];
1233
- if (api.auth?.authorization)
1234
- authParts.push(`Authorization: ${api.auth.authorization.slice(0, 30)}...`);
1235
- if (api.auth?.cookies && Object.keys(api.auth.cookies).length > 0)
1236
- authParts.push(`${Object.keys(api.auth.cookies).length} cookies`);
1237
- if (api.auth?.tokens && Object.keys(api.auth.tokens).length > 0) {
1238
- for (const [k, v] of Object.entries(api.auth.tokens)) {
1239
- authParts.push(`${k}: ${String(v).slice(0, 30)}...`);
1240
- }
1241
- }
1242
- if (authParts.length > 0) {
1243
- lines.push(`
1244
- Auth:`);
1245
- for (const part of authParts)
1246
- lines.push(` ${part}`);
1247
- }
1248
- const byProtocol = new Map;
1249
- for (const ep of api.endpoints) {
1250
- const p = ep.protocol || "rest";
1251
- if (!byProtocol.has(p))
1252
- byProtocol.set(p, []);
1253
- byProtocol.get(p).push(ep);
1254
- }
1255
- const protocolSummary = Array.from(byProtocol.entries()).map(([p, eps]) => `${p}=${eps.length}`).join(", ");
1256
- lines.push(`
1257
- Endpoints (${api.endpoints.length}) [${protocolSummary}]:`);
1258
- for (const ep of api.endpoints) {
1259
- lines.push(`
1260
- [${ep.protocol}/${ep.verb}] ${ep.functionName}`);
1261
- lines.push(` Action: ${ep.action}`);
1262
- lines.push(` ${ep.method} ${ep.path} → ${ep.responseStatus}`);
1263
- if (ep.requestBody) {
1264
- const body = ep.requestBody;
1265
- const signalKeys = extractSignalKeys(body);
1266
- if (signalKeys)
1267
- lines.push(` Params: ${signalKeys}`);
1268
- }
1269
- }
1270
- }
1271
- const mainDomain = capturedApi[0]?.domain || "api";
1272
- const dir = params.outputDir || `./${mainDomain}`;
1273
- const ext = params.typed ? ".ts" : ".js";
1274
- lines.push(`
1275
- ━━━ Save instructions ━━━`);
1276
- lines.push(`Save to: ${dir}`);
1277
- lines.push(`Format: ${ext} files`);
1278
- const protocolsSeen = new Set;
1279
- for (const api of capturedApi)
1280
- for (const ep of api.endpoints)
1281
- protocolsSeen.add(ep.protocol);
1282
- lines.push(`
1283
- Layout (protocols present: ${Array.from(protocolsSeen).join(", ")}):`);
1284
- lines.push(` auth${ext}`);
1285
- if (protocolsSeen.has("rest"))
1286
- lines.push(` transport/rest${ext} — shared fetch wrapper with auth headers`);
1287
- if (protocolsSeen.has("graphql"))
1288
- lines.push(` transport/graphql${ext} — post(operationName|docId, variables) → shared GraphQL client`);
1289
- if (protocolsSeen.has("json-rpc"))
1290
- lines.push(` transport/jsonRpc${ext} — shared JSON-RPC client`);
1291
- if (protocolsSeen.has("grpc-web"))
1292
- lines.push(` transport/grpc${ext} — shared gRPC-web client`);
1293
- if (protocolsSeen.has("form-rpc"))
1294
- lines.push(` transport/formRpc${ext} — shared urlencoded RPC client`);
1295
- if (protocolsSeen.has("soap"))
1296
- lines.push(` transport/soap${ext} — shared SOAP client`);
1297
- lines.push(` <protocol>/<verb>/<functionName>${ext} — one file per endpoint, uses shared transport`);
1298
- lines.push(` e.g. graphql/queries/<fn>${ext}, graphql/mutations/<fn>${ext}, rest/read/<fn>${ext}, rest/create/<fn>${ext}`);
1299
- lines.push(` index${ext} — re-export all endpoint functions`);
1300
- if (params.typed)
1301
- lines.push(` types.ts — interfaces inferred from response bodies`);
1302
- lines.push(` README.md — endpoints grouped by protocol + verb, auth expiry warning`);
1303
- lines.push(`
1304
- Rules:`);
1305
- lines.push(` - One function per (protocol, action). Use the functionName field verbatim for the file + export name.`);
1306
- lines.push(` - verb=read|list → queries/ (GraphQL) or read/ (REST). verb=create|update|delete|action → mutations/ (GraphQL) or verb dir (REST).`);
1307
- lines.push(` - Each endpoint file is minimal: import transport + auth, call transport with the action id, pass variables, return typed result.`);
1308
- lines.push(` - GraphQL transport signature: post(opNameOrDocId: string, variables: object). Pick doc_id when present in captured body, else operationName.`);
1309
- lines.push(` - Do NOT inline auth headers per endpoint — they live in auth${ext} and the transport reads them.`);
1310
- lines.push(`
1311
- IMPORTANT: Auth data contains real tokens/cookies — they expire. Remind the user.`);
1312
- }
1313
- function extractSignalKeys(body) {
1314
- const NOISE = new Set([
1315
- "__csr",
1316
- "__hsdp",
1317
- "__hblp",
1318
- "__dyn",
1319
- "__a",
1320
- "__req",
1321
- "__hs",
1322
- "__comet_req",
1323
- "__ccg",
1324
- "__spin_r",
1325
- "__spin_b",
1326
- "__spin_t",
1327
- "__jssesw",
1328
- "lsd",
1329
- "fb_dtsg",
1330
- "fb_api_caller_class",
1331
- "fb_api_req_friendly_name",
1332
- "jazoest",
1333
- "server_timestamps",
1334
- "__s",
1335
- "__user",
1336
- "dpr",
1337
- "__rev"
1338
- ]);
1339
- const signalEntries = [];
1340
- for (const [k, v] of Object.entries(body)) {
1341
- if (NOISE.has(k))
1342
- continue;
1343
- if (typeof v === "string" && v.length > 80) {
1344
- signalEntries.push(`${k}=${v.slice(0, 40)}...`);
1345
- } else if (typeof v === "object" && v !== null) {
1346
- const keys = Object.keys(v).slice(0, 5).join(",");
1347
- signalEntries.push(`${k}={${keys}${Object.keys(v).length > 5 ? ",..." : ""}}`);
1348
- } else {
1349
- signalEntries.push(`${k}=${String(v).slice(0, 40)}`);
1350
- }
1351
- }
1352
- return signalEntries.length > 0 ? signalEntries.join(", ") : null;
1353
- }
1354
-
1355
1461
  // src/mcp/tools/knowledge.ts
1356
1462
  var import_zod7 = require("zod");
1357
1463
  function registerKnowledgeTool(server) {