mcp-scraper 0.4.1 → 0.4.3

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.
Files changed (45) hide show
  1. package/dist/bin/api-server.cjs +2976 -2023
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +2 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +3 -3
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +158 -22
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +3 -3
  13. package/dist/bin/paa-harvest.cjs +1 -0
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +2 -2
  16. package/dist/{chunk-6TDUY7CA.js → chunk-4MFYMUIF.js} +25 -3
  17. package/dist/chunk-4MFYMUIF.js.map +1 -0
  18. package/dist/{chunk-QUOZ3LZG.js → chunk-BAMVP57P.js} +157 -22
  19. package/dist/chunk-BAMVP57P.js.map +1 -0
  20. package/dist/{chunk-NNEIXK5L.js → chunk-FUWZWKGO.js} +2 -2
  21. package/dist/{chunk-HBC4C75S.js → chunk-NJK5BTUK.js} +2 -2
  22. package/dist/{chunk-JOEZJL4I.js → chunk-NVUBFCJK.js} +2 -2
  23. package/dist/chunk-SRMGSCC3.js +7 -0
  24. package/dist/chunk-SRMGSCC3.js.map +1 -0
  25. package/dist/{chunk-BP27CZ5Q.js → chunk-XGIPATLV.js} +2 -1
  26. package/dist/chunk-XGIPATLV.js.map +1 -0
  27. package/dist/index.cjs +1 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/{server-U7V6DUSY.js → server-LBEYRKM2.js} +1134 -400
  31. package/dist/server-LBEYRKM2.js.map +1 -0
  32. package/dist/{site-extract-repository-2MQBAOX5.js → site-extract-repository-4NVO5KQA.js} +3 -3
  33. package/dist/{worker-RVZXIALX.js → worker-7XRZNLKB.js} +4 -4
  34. package/package.json +2 -1
  35. package/dist/chunk-2ULBZRQQ.js +0 -7
  36. package/dist/chunk-2ULBZRQQ.js.map +0 -1
  37. package/dist/chunk-6TDUY7CA.js.map +0 -1
  38. package/dist/chunk-BP27CZ5Q.js.map +0 -1
  39. package/dist/chunk-QUOZ3LZG.js.map +0 -1
  40. package/dist/server-U7V6DUSY.js.map +0 -1
  41. /package/dist/{chunk-NNEIXK5L.js.map → chunk-FUWZWKGO.js.map} +0 -0
  42. /package/dist/{chunk-HBC4C75S.js.map → chunk-NJK5BTUK.js.map} +0 -0
  43. /package/dist/{chunk-JOEZJL4I.js.map → chunk-NVUBFCJK.js.map} +0 -0
  44. /package/dist/{site-extract-repository-2MQBAOX5.js.map → site-extract-repository-4NVO5KQA.js.map} +0 -0
  45. /package/dist/{worker-RVZXIALX.js.map → worker-7XRZNLKB.js.map} +0 -0
@@ -6,12 +6,12 @@ import {
6
6
  openWorkflowReport,
7
7
  runWorkflow,
8
8
  workflowOutputBaseDir
9
- } from "../chunk-NNEIXK5L.js";
9
+ } from "../chunk-FUWZWKGO.js";
10
10
  import "../chunk-K443GQY5.js";
11
- import "../chunk-BP27CZ5Q.js";
11
+ import "../chunk-XGIPATLV.js";
12
12
  import {
13
13
  PACKAGE_VERSION
14
- } from "../chunk-2ULBZRQQ.js";
14
+ } from "../chunk-SRMGSCC3.js";
15
15
 
16
16
  // src/cli/human-cli.ts
17
17
  import { Command } from "commander";
@@ -102,7 +102,7 @@ function renderInstallTerminal(options) {
102
102
  }
103
103
 
104
104
  // src/version.ts
105
- var PACKAGE_VERSION = "0.4.1";
105
+ var PACKAGE_VERSION = "0.4.3";
106
106
 
107
107
  // bin/mcp-scraper-install.ts
108
108
  var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/install-terminal.ts","../../src/version.ts","../../bin/mcp-scraper-install.ts"],"sourcesContent":["export interface InstallTerminalOptions {\n color?: boolean\n version: string\n apiKeyConfigured?: boolean\n}\n\ntype Tone = 'cyan' | 'lime' | 'amber' | 'red' | 'muted' | 'bold'\n\nconst CODES: Record<Tone | 'reset', string> = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n lime: '\\x1b[32m',\n amber: '\\x1b[33m',\n red: '\\x1b[31m',\n muted: '\\x1b[90m',\n bold: '\\x1b[1m',\n}\n\nfunction colorize(value: string, tone: Tone, enabled: boolean): string {\n if (!enabled) return value\n return `${CODES[tone]}${value}${CODES.reset}`\n}\n\nfunction toolRow(label: string, tools: string[], enabled: boolean): string {\n const padded = label.padEnd(9, ' ')\n return ` ${colorize(padded, 'muted', enabled)} ${tools.join(colorize(' . ', 'muted', enabled))}`\n}\n\nexport function renderInstallTerminal(options: InstallTerminalOptions): string {\n const color = options.color ?? true\n const apiKeyValue = options.apiKeyConfigured ? '$MCP_SCRAPER_API_KEY' : 'sk_live_your_key'\n const ascii = String.raw`\n __ __ ____ ____ \n| \\/ |/ ___| _ \\\n| |\\/| | | | |_) |\n| | | | |___| __/\n|_| |_|\\____|_|\n\n ____ ____ ____ _ ____ _____ ____\n/ ___| / ___| _ \\ / \\ | _ \\| ____| _ \\\n\\___ \\| | | |_) | / _ \\ | |_) | _| | |_) |\n ___) | |___| _ < / ___ \\| __/| |___| _ <\n|____/ \\____|_| \\_\\/_/ \\_\\_| |_____|_| \\_\\\n`\n\n const claudeCommand = [\n `MCP_SCRAPER_API_KEY=${apiKeyValue} npx -y -p mcp-scraper@latest \\\\`,\n ' mcp-scraper-cli agent install claude --apply',\n ].join('\\n')\n\n const codexConfig = [\n '[mcp_servers.mcp-scraper]',\n 'command = \"npx\"',\n 'args = [\"-y\", \"-p\", \"mcp-scraper@latest\", \"mcp-scraper\"]',\n `env = { MCP_SCRAPER_API_KEY = \"${apiKeyValue}\" }`,\n ].join('\\n')\n\n return [\n colorize(`mcp-scraper v${options.version}`, 'bold', color),\n colorize('> mcp-scraper-install', 'muted', color),\n colorize(ascii, 'amber', color),\n `${colorize('MCP Scraper Agent', 'cyan', color)} . v${options.version} . mcpscraper.dev`,\n '1/1 install surfaces ready',\n colorize('Newest: saved browser profiles that hold MANY site logins. Share the watch_url, user signs in, then verify with browser_profile_list. Browser sessions are direct/no-proxy by default.', 'lime', color),\n '',\n `${colorize('Tools', 'cyan', color)} ${colorize('(128 MCP tools)', 'muted', color)}`,\n toolRow('search', ['harvest_paa', 'search_serp', 'maps_search', 'maps_place_intel'], color),\n toolRow('extract', ['extract_url', 'map_site_urls', 'extract_site', 'audit_site', 'directory_workflow'], color),\n toolRow('build', ['rank_tracker_workflow', 'cron plan', 'database prompt'], color),\n toolRow('media', ['youtube_harvest', 'youtube_transcribe', 'facebook_ad_search', 'facebook_page_intel', 'facebook_ad_transcribe', 'facebook_video_transcribe', 'instagram_profile_content', 'instagram_media_download', 'reddit_thread'], color),\n toolRow('browser', ['browser_open', 'browser_profile_connect', 'browser_profile_list', 'browser_close', 'browser_screenshot', 'browser_read', 'browser_locate', 'browser_replay_mark', 'browser_replay_annotate'], color),\n toolRow('account', ['credits_info', 'reports', 'MCP resources'], color),\n toolRow('memory', ['memory-put', 'memory-get', 'memory-search', 'list-vaults', 'record-fact', 'list-scheduled-actions'], color),\n `${colorize('Workflows', 'cyan', color)} ${colorize('(MCP + CLI + API)', 'muted', color)}`,\n toolRow('route', ['workflow_list', 'workflow_suggest', 'workflow_run', 'workflow_step', 'workflow_status', 'workflow_artifact_read'], color),\n toolRow('seo', ['directory', 'agent-packet', 'competitive audit', 'map/serp comparison', 'PAA/AIO briefs', 'scheduled runs'], color),\n '',\n colorize('Usage tips:', 'amber', color),\n 'Run mcp-scraper-install for this visible card. Run mcp-scraper-cli for setup utilities and subcommands.',\n 'Run mcp-scraper in a human terminal to print this card; MCP clients get the same command as a silent stdio server.',\n 'Explicit card command: npx -y -p mcp-scraper@latest mcp-scraper-install',\n 'Customer auth setup: run browser_profile_connect, send the watch_url, let the user sign in, then call browser_profile_list until AUTHENTICATED.',\n 'Stack logins / reconnect: run browser_profile_connect again with the same profile name and another domain to add accounts or refresh a login.',\n 'Start with workflow_suggest for broad jobs like market analysis, ICP research, CRO audits, brand briefs, content gaps, and AI visibility.',\n 'For MCP clients, use mcp-scraper so one install can mix SERP, Maps, browser, reports, and saved MCP resources.',\n 'If you hit the concurrency limit, add an extra slot for $5/month with mcp-scraper-cli billing concurrency checkout.',\n '',\n `${colorize('Ready.', 'lime', color)} Install the combined MCP server with one command:`,\n '',\n colorize('Setup doctor', 'amber', color),\n 'npx -y -p mcp-scraper@latest mcp-scraper-cli doctor',\n '',\n colorize('Hosted profile setup', 'amber', color),\n 'In your MCP client, call browser_profile_connect with email=\"seo@example.com\" and domain=\"chatgpt.com\".',\n 'Give the returned watch_url to the user. After they sign in, call browser_profile_list, then browser_open with the returned profile. Add more logins by calling browser_profile_connect again with the same profile and a new domain.',\n '',\n colorize('Claude Code one-command setup', 'amber', color),\n claudeCommand,\n 'Then fully exit Claude Code and open a new Claude terminal. Check with: claude mcp list',\n '',\n colorize('Codex config', 'amber', color),\n codexConfig,\n '',\n colorize('Claude Desktop Extension', 'amber', color),\n 'Download: https://mcpscraper.dev/downloads/mcp-scraper.mcpb',\n '',\n colorize('Safety note:', 'muted', color),\n 'mcp-scraper prints this card only when stdin/stdout are an interactive TTY. In MCP clients it writes only JSON-RPC to stdout.',\n 'Use --stdio or MCP_SCRAPER_FORCE_STDIO=1 to force server mode from a terminal.',\n '',\n ].join('\\n')\n}\n","export const PACKAGE_VERSION = '0.4.1'\n","#!/usr/bin/env node\nimport { renderInstallTerminal } from '../src/install-terminal.js'\nimport { PACKAGE_VERSION } from '../src/version.js'\n\nconst noColor =\n process.argv.includes('--no-color') ||\n process.env.NO_COLOR !== undefined ||\n process.env.FORCE_COLOR === '0' ||\n !process.stdout.isTTY\n\nconst help = process.argv.includes('--help') || process.argv.includes('-h')\nif (help) {\n process.stdout.write([\n 'Usage: mcp-scraper-install [--no-color]',\n '',\n 'Prints the branded MCP Scraper terminal install card and copyable install commands.',\n 'mcp-scraper prints the same card in a human terminal and runs as the MCP stdio server in clients.',\n '',\n ].join('\\n'))\n process.exit(0)\n}\n\nprocess.stdout.write(renderInstallTerminal({\n version: PACKAGE_VERSION,\n color: !noColor,\n apiKeyConfigured: Boolean(process.env.MCP_SCRAPER_API_KEY?.trim()),\n}))\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/install-terminal.ts","../../src/version.ts","../../bin/mcp-scraper-install.ts"],"sourcesContent":["export interface InstallTerminalOptions {\n color?: boolean\n version: string\n apiKeyConfigured?: boolean\n}\n\ntype Tone = 'cyan' | 'lime' | 'amber' | 'red' | 'muted' | 'bold'\n\nconst CODES: Record<Tone | 'reset', string> = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n lime: '\\x1b[32m',\n amber: '\\x1b[33m',\n red: '\\x1b[31m',\n muted: '\\x1b[90m',\n bold: '\\x1b[1m',\n}\n\nfunction colorize(value: string, tone: Tone, enabled: boolean): string {\n if (!enabled) return value\n return `${CODES[tone]}${value}${CODES.reset}`\n}\n\nfunction toolRow(label: string, tools: string[], enabled: boolean): string {\n const padded = label.padEnd(9, ' ')\n return ` ${colorize(padded, 'muted', enabled)} ${tools.join(colorize(' . ', 'muted', enabled))}`\n}\n\nexport function renderInstallTerminal(options: InstallTerminalOptions): string {\n const color = options.color ?? true\n const apiKeyValue = options.apiKeyConfigured ? '$MCP_SCRAPER_API_KEY' : 'sk_live_your_key'\n const ascii = String.raw`\n __ __ ____ ____ \n| \\/ |/ ___| _ \\\n| |\\/| | | | |_) |\n| | | | |___| __/\n|_| |_|\\____|_|\n\n ____ ____ ____ _ ____ _____ ____\n/ ___| / ___| _ \\ / \\ | _ \\| ____| _ \\\n\\___ \\| | | |_) | / _ \\ | |_) | _| | |_) |\n ___) | |___| _ < / ___ \\| __/| |___| _ <\n|____/ \\____|_| \\_\\/_/ \\_\\_| |_____|_| \\_\\\n`\n\n const claudeCommand = [\n `MCP_SCRAPER_API_KEY=${apiKeyValue} npx -y -p mcp-scraper@latest \\\\`,\n ' mcp-scraper-cli agent install claude --apply',\n ].join('\\n')\n\n const codexConfig = [\n '[mcp_servers.mcp-scraper]',\n 'command = \"npx\"',\n 'args = [\"-y\", \"-p\", \"mcp-scraper@latest\", \"mcp-scraper\"]',\n `env = { MCP_SCRAPER_API_KEY = \"${apiKeyValue}\" }`,\n ].join('\\n')\n\n return [\n colorize(`mcp-scraper v${options.version}`, 'bold', color),\n colorize('> mcp-scraper-install', 'muted', color),\n colorize(ascii, 'amber', color),\n `${colorize('MCP Scraper Agent', 'cyan', color)} . v${options.version} . mcpscraper.dev`,\n '1/1 install surfaces ready',\n colorize('Newest: saved browser profiles that hold MANY site logins. Share the watch_url, user signs in, then verify with browser_profile_list. Browser sessions are direct/no-proxy by default.', 'lime', color),\n '',\n `${colorize('Tools', 'cyan', color)} ${colorize('(128 MCP tools)', 'muted', color)}`,\n toolRow('search', ['harvest_paa', 'search_serp', 'maps_search', 'maps_place_intel'], color),\n toolRow('extract', ['extract_url', 'map_site_urls', 'extract_site', 'audit_site', 'directory_workflow'], color),\n toolRow('build', ['rank_tracker_workflow', 'cron plan', 'database prompt'], color),\n toolRow('media', ['youtube_harvest', 'youtube_transcribe', 'facebook_ad_search', 'facebook_page_intel', 'facebook_ad_transcribe', 'facebook_video_transcribe', 'instagram_profile_content', 'instagram_media_download', 'reddit_thread'], color),\n toolRow('browser', ['browser_open', 'browser_profile_connect', 'browser_profile_list', 'browser_close', 'browser_screenshot', 'browser_read', 'browser_locate', 'browser_replay_mark', 'browser_replay_annotate'], color),\n toolRow('account', ['credits_info', 'reports', 'MCP resources'], color),\n toolRow('memory', ['memory-put', 'memory-get', 'memory-search', 'list-vaults', 'record-fact', 'list-scheduled-actions'], color),\n `${colorize('Workflows', 'cyan', color)} ${colorize('(MCP + CLI + API)', 'muted', color)}`,\n toolRow('route', ['workflow_list', 'workflow_suggest', 'workflow_run', 'workflow_step', 'workflow_status', 'workflow_artifact_read'], color),\n toolRow('seo', ['directory', 'agent-packet', 'competitive audit', 'map/serp comparison', 'PAA/AIO briefs', 'scheduled runs'], color),\n '',\n colorize('Usage tips:', 'amber', color),\n 'Run mcp-scraper-install for this visible card. Run mcp-scraper-cli for setup utilities and subcommands.',\n 'Run mcp-scraper in a human terminal to print this card; MCP clients get the same command as a silent stdio server.',\n 'Explicit card command: npx -y -p mcp-scraper@latest mcp-scraper-install',\n 'Customer auth setup: run browser_profile_connect, send the watch_url, let the user sign in, then call browser_profile_list until AUTHENTICATED.',\n 'Stack logins / reconnect: run browser_profile_connect again with the same profile name and another domain to add accounts or refresh a login.',\n 'Start with workflow_suggest for broad jobs like market analysis, ICP research, CRO audits, brand briefs, content gaps, and AI visibility.',\n 'For MCP clients, use mcp-scraper so one install can mix SERP, Maps, browser, reports, and saved MCP resources.',\n 'If you hit the concurrency limit, add an extra slot for $5/month with mcp-scraper-cli billing concurrency checkout.',\n '',\n `${colorize('Ready.', 'lime', color)} Install the combined MCP server with one command:`,\n '',\n colorize('Setup doctor', 'amber', color),\n 'npx -y -p mcp-scraper@latest mcp-scraper-cli doctor',\n '',\n colorize('Hosted profile setup', 'amber', color),\n 'In your MCP client, call browser_profile_connect with email=\"seo@example.com\" and domain=\"chatgpt.com\".',\n 'Give the returned watch_url to the user. After they sign in, call browser_profile_list, then browser_open with the returned profile. Add more logins by calling browser_profile_connect again with the same profile and a new domain.',\n '',\n colorize('Claude Code one-command setup', 'amber', color),\n claudeCommand,\n 'Then fully exit Claude Code and open a new Claude terminal. Check with: claude mcp list',\n '',\n colorize('Codex config', 'amber', color),\n codexConfig,\n '',\n colorize('Claude Desktop Extension', 'amber', color),\n 'Download: https://mcpscraper.dev/downloads/mcp-scraper.mcpb',\n '',\n colorize('Safety note:', 'muted', color),\n 'mcp-scraper prints this card only when stdin/stdout are an interactive TTY. In MCP clients it writes only JSON-RPC to stdout.',\n 'Use --stdio or MCP_SCRAPER_FORCE_STDIO=1 to force server mode from a terminal.',\n '',\n ].join('\\n')\n}\n","export const PACKAGE_VERSION = '0.4.3'\n","#!/usr/bin/env node\nimport { renderInstallTerminal } from '../src/install-terminal.js'\nimport { PACKAGE_VERSION } from '../src/version.js'\n\nconst noColor =\n process.argv.includes('--no-color') ||\n process.env.NO_COLOR !== undefined ||\n process.env.FORCE_COLOR === '0' ||\n !process.stdout.isTTY\n\nconst help = process.argv.includes('--help') || process.argv.includes('-h')\nif (help) {\n process.stdout.write([\n 'Usage: mcp-scraper-install [--no-color]',\n '',\n 'Prints the branded MCP Scraper terminal install card and copyable install commands.',\n 'mcp-scraper prints the same card in a human terminal and runs as the MCP stdio server in clients.',\n '',\n ].join('\\n'))\n process.exit(0)\n}\n\nprocess.stdout.write(renderInstallTerminal({\n version: PACKAGE_VERSION,\n color: !noColor,\n apiKeyConfigured: Boolean(process.env.MCP_SCRAPER_API_KEY?.trim()),\n}))\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "../chunk-5HNHYUJ5.js";
5
5
  import {
6
6
  PACKAGE_VERSION
7
- } from "../chunk-2ULBZRQQ.js";
7
+ } from "../chunk-SRMGSCC3.js";
8
8
 
9
9
  // bin/mcp-scraper-install.ts
10
10
  var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
@@ -297,6 +297,9 @@ var HttpMcpToolExecutor = class {
297
297
  mapsSearch(input) {
298
298
  return this.call("/maps/search", input);
299
299
  }
300
+ trustpilotReviews(input) {
301
+ return this.call("/trustpilot/reviews", input, this.httpTimeoutOverrideMs ?? 3e5);
302
+ }
300
303
  directoryWorkflow(input) {
301
304
  const cityCount = typeof input.maxCities === "number" ? input.maxCities : 25;
302
305
  const concurrency = typeof input.concurrency === "number" && input.concurrency > 0 ? input.concurrency : 5;
@@ -474,7 +477,7 @@ render();
474
477
  }
475
478
 
476
479
  // src/version.ts
477
- var PACKAGE_VERSION = "0.4.1";
480
+ var PACKAGE_VERSION = "0.4.3";
478
481
 
479
482
  // src/mcp/browser-agent-tool-schemas.ts
480
483
  var import_zod = require("zod");
@@ -696,7 +699,7 @@ var BrowserProfileLogin = import_zod.z.object({
696
699
  var BrowserProfileConnectOutputSchema = {
697
700
  ok: import_zod.z.boolean(),
698
701
  tool: import_zod.z.literal("browser_profile_connect"),
699
- session_id: NullableString,
702
+ session_id: NullableString.describe("The underlying live browser session id backing this sign-in."),
700
703
  auth_connection_id: import_zod.z.string(),
701
704
  watch_url: import_zod.z.string().describe("mcpscraper.dev sign-in link to give the user so they can complete this login."),
702
705
  live_view_url: NullableString.describe("Deprecated; always null. Open watch_url to view the sign-in."),
@@ -706,10 +709,10 @@ var BrowserProfileConnectOutputSchema = {
706
709
  account_email: NullableString,
707
710
  note: NullableString,
708
711
  status: import_zod.z.string(),
709
- flow_status: NullableString,
710
- flow_step: NullableString,
711
- flow_expires_at: NullableString,
712
- post_login_url: NullableString,
712
+ flow_status: NullableString.describe("Deprecated; always null."),
713
+ flow_step: NullableString.describe("Deprecated; always null."),
714
+ flow_expires_at: NullableString.describe("Deprecated; always null."),
715
+ post_login_url: NullableString.describe("Deprecated; always null."),
713
716
  connected_logins: import_zod.z.array(BrowserProfileLogin).describe("Every login currently saved in this profile, so you can see what else is connected."),
714
717
  next_steps: import_zod.z.array(import_zod.z.string()),
715
718
  raw: BrowserRawObject.optional()
@@ -1198,6 +1201,11 @@ function slugPart(value) {
1198
1201
  if (!trimmed) return null;
1199
1202
  return trimmed.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").replace(/[^a-z0-9._-]+/g, "-").replace(/[._-]+/g, "-").replace(/^-+|-+$/g, "");
1200
1203
  }
1204
+ function normalizeDomain(value) {
1205
+ const trimmed = value?.trim().toLowerCase();
1206
+ if (!trimmed) return null;
1207
+ return trimmed.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").replace(/[^a-z0-9.-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1208
+ }
1201
1209
  function savedProfileNameFromEmail(email) {
1202
1210
  const value = slugPart(email);
1203
1211
  if (!value) return null;
@@ -1307,13 +1315,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
1307
1315
  "browser_profile_connect",
1308
1316
  {
1309
1317
  title: "Save a Site Login to a Profile",
1310
- description: "Save a logged-in browser session so this server can reuse a site the user is signed into (ChatGPT, Claude, Reddit, any account-gated site). Returns a watch_url; the user signs in fresh (existing browser cookies are NOT imported) and cookies save to a named profile. ONE profile holds MANY logins \u2014 call again with the same profile and a different domain to stack another account. NOT for one-off scraping (use extract_url) or driving the browser (use browser_open). After sign-in, poll browser_profile_list until AUTHENTICATED, then browser_open with the profile.",
1318
+ description: 'Open a live hosted browser session so the user can sign into a site (ChatGPT, Claude, Reddit, any account-gated site) directly in their own browser tab, then save the login to a named profile. Returns a watch_url \u2014 give it to the user; they sign in fresh on the real site (existing browser cookies are NOT imported), then click "Done" on that page to save the session and close it. ONE profile holds MANY logins \u2014 call again with the same profile and a different domain to stack another account. NOT for one-off scraping (use extract_url) or driving the browser (use browser_open). Billed at the standard live-browser rate while the sign-in session stays open. After the user clicks Done, poll browser_profile_list until AUTHENTICATED, then browser_open with the profile.',
1311
1319
  inputSchema: BrowserProfileConnectInputSchema,
1312
1320
  outputSchema: recordOutputSchema("browser_profile_connect", BrowserProfileConnectOutputSchema),
1313
1321
  annotations: annotations("Save a Site Login to a Profile")
1314
1322
  },
1315
1323
  async (input) => {
1316
- const domain = slugPart(input.domain) || "chatgpt.com";
1324
+ const domain = normalizeDomain(input.domain) || "chatgpt.com";
1317
1325
  const setupUrl = input.login_url ?? input.url ?? (domain === "chatgpt.com" ? "https://chatgpt.com/" : `https://${domain}/`);
1318
1326
  const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
1319
1327
  if (!profile) {
@@ -1329,14 +1337,15 @@ function registerBrowserAgentMcpTools(server2, opts) {
1329
1337
  domain,
1330
1338
  login_url: setupUrl,
1331
1339
  ...accountEmail ? { account_email: accountEmail } : {},
1332
- ...note ? { note } : {}
1340
+ ...note ? { note } : {},
1341
+ ...typeof input.timeout_seconds === "number" ? { timeout_seconds: input.timeout_seconds } : {}
1333
1342
  });
1334
1343
  if (!open.ok) return errorResult("browser_profile_connect", open.data);
1335
1344
  const connectedLogins = await fetchConnectedLogins(profile).catch(() => []);
1336
1345
  return structuredResult({
1337
1346
  ok: true,
1338
1347
  tool: "browser_profile_connect",
1339
- session_id: null,
1348
+ session_id: open.data.browser_session_id ?? null,
1340
1349
  auth_connection_id: open.data.connection_id,
1341
1350
  watch_url: `${consoleBase}/console/auth/${open.data.connection_id}`,
1342
1351
  live_view_url: null,
@@ -1346,13 +1355,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
1346
1355
  account_email: accountEmail,
1347
1356
  note,
1348
1357
  status: open.data.status ?? null,
1349
- flow_status: open.data.flow_status ?? null,
1350
- flow_step: open.data.flow_step ?? null,
1351
- flow_expires_at: open.data.flow_expires_at ?? null,
1352
- post_login_url: open.data.post_login_url ?? null,
1358
+ flow_status: null,
1359
+ flow_step: null,
1360
+ flow_expires_at: null,
1361
+ post_login_url: null,
1353
1362
  connected_logins: connectedLogins,
1354
1363
  next_steps: [
1355
- `Give the user watch_url so they can sign in to ${domain}.`,
1364
+ `Give the user watch_url so they can sign in to ${domain} directly in that page, then click "Done \u2014 I'm signed in".`,
1356
1365
  "Poll browser_profile_list (with this profile) until the login reads AUTHENTICATED.",
1357
1366
  "To add another account to this same profile, call browser_profile_connect again with the same profile and a different domain.",
1358
1367
  "When authenticated, call browser_open with this profile to drive the logged-in session."
@@ -1377,7 +1386,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1377
1386
  error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
1378
1387
  });
1379
1388
  }
1380
- const domain = slugPart(input.domain) || void 0;
1389
+ const domain = normalizeDomain(input.domain) || void 0;
1381
1390
  const res = await req("POST", "/agent/profiles/list", {
1382
1391
  profile,
1383
1392
  ...domain ? { domain } : {},
@@ -1976,6 +1985,17 @@ var WORKFLOW_RECIPES = [
1976
1985
  produces: ["content gaps", "page comparison CSV", "PAA questions CSV", "section map", "writer brief"],
1977
1986
  runHint: "Use workflow_run with workflowId serp-comparison for competitor gaps or paa-expansion-brief when the user mainly wants a section map from PAA data."
1978
1987
  },
1988
+ {
1989
+ id: "recurring_tracking_and_memory",
1990
+ title: "Recurring Tracking & Memory",
1991
+ description: "Run a check on a daily, weekly, or custom cadence with no external cron needed, and save each result somewhere searchable so you can compare against past runs later - rankings, prices, competitor changes, anything you would otherwise check by hand and forget to log.",
1992
+ primaryWorkflowId: null,
1993
+ recommendedTools: ["create-scheduled-action", "search_serp", "table-create", "table-insert-rows", "table-query", "memory-put", "memory-search", "list-scheduled-actions"],
1994
+ requiredInputs: ["plain-language description of what to check and how often"],
1995
+ optionalInputs: ["vault to save into", "timeOfDay", "timezone"],
1996
+ produces: ["a live scheduled action that runs automatically on cadence", "a table row or note per run for structured history", "a searchable trend you can compare run-over-run"],
1997
+ runHint: "Use create-scheduled-action with a plain description and cadence; the run itself checks list-vaults, uses table-insert-rows for repeating structured data (e.g. a keyword rank check) or memory-put for a one-off note, and reads prior runs with table-query or memory-search before writing the new one. For a one-time blueprint to build your own external cron/DB instead of a live scheduled action, use rank_tracker_workflow."
1998
+ },
1979
1999
  {
1980
2000
  id: "ai_search_visibility_audit",
1981
2001
  title: "AI Search Visibility Audit",
@@ -4094,6 +4114,51 @@ ${rows}`,
4094
4114
  }
4095
4115
  };
4096
4116
  }
4117
+ function formatTrustpilotReviews(raw, input) {
4118
+ const parsed = parseData(raw);
4119
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
4120
+ const d = parsed.data;
4121
+ const domain = d.domain ?? input.domain;
4122
+ const reviewUrl = d.reviewUrl;
4123
+ const extractedAt = d.extractedAt;
4124
+ const requestedMaxPages = d.requestedMaxPages ?? input.maxPages ?? 5;
4125
+ const pagesFetched = d.pagesFetched;
4126
+ const reviews = d.reviews ?? [];
4127
+ const durationMs = d.durationMs;
4128
+ const rows = reviews.map((r) => {
4129
+ const stars = r.rating != null ? "\u2605".repeat(r.rating) + "\u2606".repeat(r.ratingScale - r.rating) : "\u2014";
4130
+ const answer = r.body[0]?.answer ?? "";
4131
+ return `| ${cell(r.reviewer.name ?? "Anonymous")} | ${stars} | ${cell(r.title)} | ${cell(r.date ? r.date.slice(0, 10) : null)} | ${r.flags.origin ?? "\u2014"} | ${r.flags.companyReplied ? "yes" : "no"} | ${cell(truncate(answer, 100))} |`;
4132
+ }).join("\n");
4133
+ const full = [
4134
+ `# Trustpilot Reviews: ${domain}`,
4135
+ `**Collected:** ${reviews.length} review${reviews.length === 1 ? "" : "s"} across ${pagesFetched ?? 0}/${requestedMaxPages} page${requestedMaxPages === 1 ? "" : "s"}`,
4136
+ reviewUrl ? `**Source:** ${reviewUrl}` : null,
4137
+ `
4138
+ ## Reviews
4139
+ | Reviewer | Rating | Title | Date | Origin | Company Replied | Excerpt |
4140
+ |----------|--------|-------|------|--------|------------------|---------|
4141
+ ${rows}`,
4142
+ `
4143
+ ---
4144
+ \u{1F4A1} **Note:** sampling tool, default 5 pages (~100 reviews), max 50. For full-corpus export use Trustpilot's official Business API.`,
4145
+ durationMs != null ? `
4146
+ *Extracted in ${(durationMs / 1e3).toFixed(1)}s*` : null
4147
+ ].filter(Boolean).join("\n");
4148
+ return {
4149
+ ...oneBlock(full),
4150
+ structuredContent: {
4151
+ domain,
4152
+ reviewUrl: reviewUrl ?? null,
4153
+ extractedAt: extractedAt ?? null,
4154
+ requestedMaxPages,
4155
+ pagesFetched: pagesFetched ?? 0,
4156
+ reviewCount: reviews.length,
4157
+ reviews,
4158
+ durationMs: durationMs ?? 0
4159
+ }
4160
+ };
4161
+ }
4097
4162
  async function formatDirectoryWorkflow(raw, input, ctx) {
4098
4163
  const parsed = parseData(raw);
4099
4164
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -4195,6 +4260,9 @@ function formatMapsPlaceIntel(raw, input) {
4195
4260
  const about = d.aboutAttributes ?? [];
4196
4261
  const reviews = d.reviews ?? [];
4197
4262
  const reviewsStatus = d.reviewsStatus ?? "not_requested";
4263
+ const services = d.services ?? [];
4264
+ const areasServed = d.areasServed ?? [];
4265
+ const servicesStatus = d.servicesStatus ?? "not_requested";
4198
4266
  const hoursTable = d.hoursTable ?? [];
4199
4267
  const ratingLine = [rating, reviewCount ? `(${reviewCount} reviews)` : null].filter(Boolean).join(" ");
4200
4268
  const basicLines = [
@@ -4249,6 +4317,20 @@ ${reviews.map((r, i) => {
4249
4317
  ${r.text ?? ""}`;
4250
4318
  }).join("\n\n")}`;
4251
4319
  })();
4320
+ const servicesSection = (() => {
4321
+ if (servicesStatus === "not_requested") return "";
4322
+ if (servicesStatus === "unavailable") return "\n## Services & Areas Served\n> Could not be retrieved this run \u2014 retry with `includeServices: true`.";
4323
+ if (servicesStatus === "none_exist") return "\n## Services & Areas Served\n*Not listed on this business's profile.*";
4324
+ const parts = [
4325
+ services.length ? `**Services**
4326
+ ${services.map((s) => `- ${s}`).join("\n")}` : null,
4327
+ areasServed.length ? `**Areas Served**
4328
+ ${areasServed.map((a) => `- ${a}`).join("\n")}` : null
4329
+ ].filter(Boolean);
4330
+ return parts.length ? `
4331
+ ## Services & Areas Served
4332
+ ${parts.join("\n\n")}` : "";
4333
+ })();
4252
4334
  const full = [
4253
4335
  `# ${name}`,
4254
4336
  category ? `*${category}*` : null,
@@ -4264,6 +4346,7 @@ ${basicLines}` : null,
4264
4346
  ## Entity IDs
4265
4347
  ${entitySection}` : null,
4266
4348
  reviewsSection,
4349
+ servicesSection,
4267
4350
  durationMs != null ? `
4268
4351
  ---
4269
4352
  *Extracted in ${(durationMs / 1e3).toFixed(1)}s*` : null
@@ -4287,7 +4370,10 @@ ${entitySection}` : null,
4287
4370
  lng: lng ?? null,
4288
4371
  reviewsStatus,
4289
4372
  reviewsCollected: reviews.length,
4290
- reviewTopics: topics.map((t) => ({ label: String(t.label ?? ""), count: String(t.count ?? "") }))
4373
+ reviewTopics: topics.map((t) => ({ label: String(t.label ?? ""), count: String(t.count ?? "") })),
4374
+ services,
4375
+ areasServed,
4376
+ servicesStatus
4291
4377
  }
4292
4378
  };
4293
4379
  }
@@ -4840,6 +4926,7 @@ var MapsPlaceOptionsSchema = import_zod2.z.object({
4840
4926
  hl: import_zod2.z.string().length(2).default("en"),
4841
4927
  includeReviews: import_zod2.z.boolean().default(false),
4842
4928
  maxReviews: import_zod2.z.number().int().min(1).max(500).default(50),
4929
+ includeServices: import_zod2.z.boolean().default(false),
4843
4930
  kernelApiKey: import_zod2.z.string().optional(),
4844
4931
  kernelProxyId: import_zod2.z.string().optional(),
4845
4932
  headless: import_zod2.z.boolean().default(true)
@@ -5028,8 +5115,37 @@ var MapsPlaceIntelInputSchema = {
5028
5115
  gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
5029
5116
  hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
5030
5117
  includeReviews: import_zod3.z.boolean().default(false).describe("Fetch individual review cards \u2014 for reviews, customer pain, complaints, or praise themes."),
5031
- maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards when includeReviews is true. Default 50, maximum 500.")
5032
- };
5118
+ maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards when includeReviews is true. Default 50, maximum 500."),
5119
+ includeServices: import_zod3.z.boolean().default(false).describe("Fetch the business's configured services list and areas-served list, when the profile has them. Adds one extra page visit; not present for every business.")
5120
+ };
5121
+ var TrustpilotReviewsInputSchema = {
5122
+ domain: import_zod3.z.string().min(1).describe(`The business's domain as it appears in its Trustpilot URL, e.g. "www.bhphotovideo.com" (include the www. if the site uses it \u2014 pass the domain as-is, do not guess).`),
5123
+ maxPages: import_zod3.z.number().int().min(1).max(50).default(5).describe("Review pages to fetch (~20 reviews per page). Default 5 (~100 reviews). Maximum 50 \u2014 large companies can have 1,000+ pages; this tool is for sampling, not full-corpus export.")
5124
+ };
5125
+ var ReviewCardSchema = import_zod3.z.object({
5126
+ source: import_zod3.z.enum(["trustpilot", "g2"]),
5127
+ sourceReviewId: import_zod3.z.string().nullable(),
5128
+ reviewUrl: import_zod3.z.string().nullable(),
5129
+ reviewer: import_zod3.z.object({
5130
+ name: import_zod3.z.string().nullable(),
5131
+ profileId: import_zod3.z.string().nullable(),
5132
+ title: import_zod3.z.string().nullable(),
5133
+ companySegment: import_zod3.z.string().nullable()
5134
+ }),
5135
+ rating: import_zod3.z.number().nullable(),
5136
+ ratingScale: import_zod3.z.number(),
5137
+ title: import_zod3.z.string().nullable(),
5138
+ date: import_zod3.z.string().nullable(),
5139
+ body: import_zod3.z.array(import_zod3.z.object({ question: import_zod3.z.string().nullable(), answer: import_zod3.z.string() })),
5140
+ truncated: import_zod3.z.boolean(),
5141
+ flags: import_zod3.z.object({
5142
+ origin: import_zod3.z.string().nullable(),
5143
+ incentivized: import_zod3.z.boolean().nullable(),
5144
+ validated: import_zod3.z.boolean().nullable(),
5145
+ currentUser: import_zod3.z.boolean().nullable(),
5146
+ companyReplied: import_zod3.z.boolean().nullable()
5147
+ })
5148
+ });
5033
5149
  var MapsSearchInputSchema = {
5034
5150
  query: import_zod3.z.string().min(1).describe('Business category, niche, or search term, e.g. "roofers". Do not include location here \u2014 use location instead.'),
5035
5151
  location: import_zod3.z.string().optional().describe('City, region, country, or service area, e.g. "Denver, CO".'),
@@ -5341,7 +5457,20 @@ var MapsPlaceIntelOutputSchema = {
5341
5457
  reviewTopics: import_zod3.z.array(import_zod3.z.object({
5342
5458
  label: import_zod3.z.string(),
5343
5459
  count: import_zod3.z.string()
5344
- }))
5460
+ })),
5461
+ services: import_zod3.z.array(import_zod3.z.string()),
5462
+ areasServed: import_zod3.z.array(import_zod3.z.string()),
5463
+ servicesStatus: import_zod3.z.string()
5464
+ };
5465
+ var TrustpilotReviewsOutputSchema = {
5466
+ domain: import_zod3.z.string(),
5467
+ reviewUrl: import_zod3.z.string(),
5468
+ extractedAt: import_zod3.z.string(),
5469
+ requestedMaxPages: import_zod3.z.number().int(),
5470
+ pagesFetched: import_zod3.z.number().int(),
5471
+ reviewCount: import_zod3.z.number().int(),
5472
+ reviews: import_zod3.z.array(ReviewCardSchema),
5473
+ durationMs: import_zod3.z.number()
5345
5474
  };
5346
5475
  var CreditsInfoOutputSchema = {
5347
5476
  balanceCredits: import_zod3.z.number().nullable(),
@@ -5844,7 +5973,7 @@ var ReportArtifactReadOutputSchema = {
5844
5973
 
5845
5974
  // src/mcp/rank-tracker-blueprint.ts
5846
5975
  var DEFAULT_MODES = ["maps", "organic", "ai_overview", "paa"];
5847
- function normalizeDomain(value) {
5976
+ function normalizeDomain2(value) {
5848
5977
  const trimmed = value?.trim();
5849
5978
  if (!trimmed) return null;
5850
5979
  try {
@@ -6106,7 +6235,7 @@ function buildPrompt(input, modes, tools, tables, jobs, metrics, expression, tar
6106
6235
  }
6107
6236
  function buildRankTrackerBlueprint(input) {
6108
6237
  const trackingModes = uniqueModes(input.trackingModes);
6109
- const targetDomain = normalizeDomain(input.targetDomain);
6238
+ const targetDomain = normalizeDomain2(input.targetDomain);
6110
6239
  const targetBusinessName = input.targetBusinessName?.trim() || null;
6111
6240
  const projectName = input.projectName?.trim() || "MCP Scraper Rank Tracker";
6112
6241
  const database = input.database || "postgres";
@@ -6394,6 +6523,13 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
6394
6523
  outputSchema: recordOutputSchema("maps_search", MapsSearchOutputSchema),
6395
6524
  annotations: liveWebToolAnnotations("Google Maps Business Search")
6396
6525
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
6526
+ server2.registerTool("trustpilot_reviews", {
6527
+ title: "Trustpilot Review Harvest",
6528
+ description: "Extract customer reviews for a business from Trustpilot \u2014 reviewer, rating, title, body, date, invited/organic origin, company-reply flag. Sampling tool, not a full-corpus export: default 5 pages (~100 reviews), max 50 pages. For bulk/complete extraction across thousands of pages, use Trustpilot's official Business API instead.",
6529
+ inputSchema: TrustpilotReviewsInputSchema,
6530
+ outputSchema: recordOutputSchema("trustpilot_reviews", TrustpilotReviewsOutputSchema),
6531
+ annotations: liveWebToolAnnotations("Trustpilot Review Harvest")
6532
+ }, async (input) => formatTrustpilotReviews(await executor.trustpilotReviews(input), input));
6397
6533
  server2.registerTool("directory_workflow", {
6398
6534
  title: "Directory Workflow: Markets + Maps",
6399
6535
  description: `Build directory/prospecting datasets: selects US city markets from Census population data, optionally joins configured ZIP groups, then runs Google Maps business searches per city in parallel. Use for "all cities over 100k population in a state" or market+Maps workflows. ${fileBehavior("Saves a CSV of results per city.", "Large results are stored as a retrievable artifact \u2014 you get an inline summary plus an artifactId for report_artifact_read.")}`,