mcp-scraper 0.3.0 → 0.3.2

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 (52) hide show
  1. package/README.md +4 -1
  2. package/dist/bin/api-server.cjs +2047 -831
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +3 -3
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +1 -1
  11. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +350 -5
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -4
  14. package/dist/bin/mcp-scraper-install.cjs +3 -3
  15. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  16. package/dist/bin/mcp-scraper-install.js +2 -2
  17. package/dist/bin/mcp-stdio-server.cjs +346 -1
  18. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  19. package/dist/bin/mcp-stdio-server.js +2 -2
  20. package/dist/bin/paa-harvest.cjs +3 -1
  21. package/dist/bin/paa-harvest.cjs.map +1 -1
  22. package/dist/bin/paa-harvest.js +1 -1
  23. package/dist/{chunk-SXTXMFEQ.js → chunk-AZ5PKU4F.js} +34 -1
  24. package/dist/chunk-AZ5PKU4F.js.map +1 -0
  25. package/dist/{chunk-Q4STSM63.js → chunk-C3FGVJWH.js} +3 -3
  26. package/dist/chunk-C3FGVJWH.js.map +1 -0
  27. package/dist/{chunk-DH4H3F6S.js → chunk-HRTCMLZB.js} +4 -4
  28. package/dist/chunk-HRTCMLZB.js.map +1 -0
  29. package/dist/{chunk-SIXJ6EVS.js → chunk-KO223PSJ.js} +347 -2
  30. package/dist/chunk-KO223PSJ.js.map +1 -0
  31. package/dist/{chunk-IPW4LFOT.js → chunk-UWSG3C5J.js} +4 -2
  32. package/dist/chunk-UWSG3C5J.js.map +1 -0
  33. package/dist/chunk-VB65G7PS.js +7 -0
  34. package/dist/chunk-VB65G7PS.js.map +1 -0
  35. package/dist/index.cjs +3 -1
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/{server-MOP7NIUK.js → server-N5LM3PEO.js} +997 -253
  39. package/dist/server-N5LM3PEO.js.map +1 -0
  40. package/dist/{worker-SLQ375UG.js → worker-56IXWOQU.js} +3 -3
  41. package/docs/mcp-tool-craft-lint.generated.md +5 -2
  42. package/docs/mcp-tool-manifest.generated.json +85 -9
  43. package/package.json +1 -1
  44. package/dist/chunk-DH4H3F6S.js.map +0 -1
  45. package/dist/chunk-IPW4LFOT.js.map +0 -1
  46. package/dist/chunk-PGWJ2EJ3.js +0 -7
  47. package/dist/chunk-PGWJ2EJ3.js.map +0 -1
  48. package/dist/chunk-Q4STSM63.js.map +0 -1
  49. package/dist/chunk-SIXJ6EVS.js.map +0 -1
  50. package/dist/chunk-SXTXMFEQ.js.map +0 -1
  51. package/dist/server-MOP7NIUK.js.map +0 -1
  52. /package/dist/{worker-SLQ375UG.js.map → worker-56IXWOQU.js.map} +0 -0
@@ -15,7 +15,7 @@ import {
15
15
  } from "../chunk-DUEW4EOO.js";
16
16
  import {
17
17
  PACKAGE_VERSION
18
- } from "../chunk-PGWJ2EJ3.js";
18
+ } from "../chunk-VB65G7PS.js";
19
19
 
20
20
  // src/cli/human-cli.ts
21
21
  import { Command } from "commander";
@@ -219,6 +219,12 @@ var HttpMcpToolExecutor = class {
219
219
  facebookVideoTranscribe(input) {
220
220
  return this.call("/facebook/video-transcribe", input, this.httpTimeoutOverrideMs ?? 24e4);
221
221
  }
222
+ instagramProfileContent(input) {
223
+ return this.call("/instagram/profile-content", input, this.httpTimeoutOverrideMs ?? 24e4);
224
+ }
225
+ instagramMediaDownload(input) {
226
+ return this.call("/instagram/media-download", input, this.httpTimeoutOverrideMs ?? 3e5);
227
+ }
222
228
  mapsPlaceIntel(input) {
223
229
  return this.call("/maps/place", input);
224
230
  }
@@ -1673,7 +1679,7 @@ var LocalBrowserAgentService = class {
1673
1679
  };
1674
1680
 
1675
1681
  // src/version.ts
1676
- var PACKAGE_VERSION = "0.3.0";
1682
+ var PACKAGE_VERSION = "0.3.2";
1677
1683
 
1678
1684
  // src/mcp/browser-agent-tool-schemas.ts
1679
1685
  var import_zod = require("zod");
@@ -3140,10 +3146,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
3140
3146
  "browser_capture_fanout",
3141
3147
  {
3142
3148
  title: "Capture AI Search Fan-Out",
3143
- description: "Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO) analysis: the exact sub-queries the model issued, every researched URL split into cited vs browsed-only with citation frequency and snippet, each source tagged by site category, plus top sourced sites and citation order. This tool returns the raw, structured fan-out data; YOU then analyze it \u2014 classify each sub-query by funnel stage (Problem/Solution/Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, operator), identify which brands the model researched, and surface AEO insights. Requires MCP_SCRAPER_BROWSER_MODE=local with the user's own logged-in Chrome profile (or a logged-in hosted profile); it reads the network stream the page receives and never leaves the machine. Open the session with browser_open and browser_goto to chatgpt.com or claude.ai first. Pass prompt to send and capture a new query, or omit it to capture a prompt the user just ran. Fan-out is only captured as it streams, so run the prompt with the session open.",
3149
+ description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): the exact sub-queries the model issued, every researched URL split into cited vs browsed-only (with citation frequency and snippet), each source tagged by category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already done: import a logged-in session with browser_profile_import (local Chrome) or browser_profile_onboard (hosted Kernel), then browser_open and browser_goto to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
3144
3150
  inputSchema: BrowserCaptureFanoutInputSchema,
3145
3151
  outputSchema: BrowserCaptureFanoutOutputSchema,
3146
- annotations: annotations("Capture AI Search Fan-Out", true)
3152
+ annotations: annotations("Capture AI Search Fan-Out")
3147
3153
  },
3148
3154
  async (input) => {
3149
3155
  const emit = (result, exports2) => structuredResult({
@@ -4643,6 +4649,216 @@ ${chunkRows}` : "",
4643
4649
  }
4644
4650
  };
4645
4651
  }
4652
+ function structuredInstagramBrowser(raw) {
4653
+ const browser = raw && typeof raw === "object" ? raw : {};
4654
+ const mode = browser.mode === "local" ? "local" : "hosted";
4655
+ const requestedMode = browser.requestedMode === "local" || browser.requestedMode === "hosted" || browser.requestedMode === "auto" ? browser.requestedMode : "auto";
4656
+ const profileSource = browser.profileSource === "managed_profile" || browser.profileSource === "direct_profile_dir" ? browser.profileSource : "hosted";
4657
+ return {
4658
+ mode,
4659
+ requestedMode,
4660
+ profileName: typeof browser.profileName === "string" ? browser.profileName : null,
4661
+ profileSource,
4662
+ profileDirConfigured: browser.profileDirConfigured === true,
4663
+ executablePathConfigured: browser.executablePathConfigured === true
4664
+ };
4665
+ }
4666
+ function structuredInstagramPagination(raw, input) {
4667
+ const pagination = raw && typeof raw === "object" ? raw : {};
4668
+ const stages = Array.isArray(pagination.stages) ? pagination.stages : [];
4669
+ const stoppedReason = typeof pagination.stoppedReason === "string" && ["max_items", "reported_post_count", "stable_scrolls", "max_scrolls", "no_scrolls"].includes(pagination.stoppedReason) ? pagination.stoppedReason : "no_scrolls";
4670
+ return {
4671
+ maxItems: typeof pagination.maxItems === "number" ? pagination.maxItems : Number(input.maxItems ?? 50),
4672
+ maxScrolls: typeof pagination.maxScrolls === "number" ? pagination.maxScrolls : Number(input.maxScrolls ?? 10),
4673
+ attemptedScrolls: typeof pagination.attemptedScrolls === "number" ? pagination.attemptedScrolls : 0,
4674
+ stableScrolls: typeof pagination.stableScrolls === "number" ? pagination.stableScrolls : 0,
4675
+ stableScrollLimit: typeof pagination.stableScrollLimit === "number" ? pagination.stableScrollLimit : Number(input.stableScrollLimit ?? 4),
4676
+ scrollDelayMs: typeof pagination.scrollDelayMs === "number" ? pagination.scrollDelayMs : Number(input.scrollDelayMs ?? 1200),
4677
+ reachedMaxItems: pagination.reachedMaxItems === true,
4678
+ reachedReportedPostCount: pagination.reachedReportedPostCount === true,
4679
+ finalScrollHeight: typeof pagination.finalScrollHeight === "number" ? pagination.finalScrollHeight : null,
4680
+ stoppedReason,
4681
+ stages: stages.map((stage) => {
4682
+ const row = stage && typeof stage === "object" ? stage : {};
4683
+ return {
4684
+ stage: String(row.stage ?? ""),
4685
+ itemCount: typeof row.itemCount === "number" ? row.itemCount : 0,
4686
+ addedCount: typeof row.addedCount === "number" ? row.addedCount : 0,
4687
+ scrollY: typeof row.scrollY === "number" ? row.scrollY : null,
4688
+ scrollHeight: typeof row.scrollHeight === "number" ? row.scrollHeight : null
4689
+ };
4690
+ })
4691
+ };
4692
+ }
4693
+ function formatInstagramProfileContent(raw, input) {
4694
+ const parsed = parseData(raw);
4695
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
4696
+ const d = parsed.data;
4697
+ const items = Array.isArray(d.items) ? d.items : [];
4698
+ const typeCounts = d.typeCounts;
4699
+ const limitations = Array.isArray(d.limitations) ? d.limitations.map(String) : [];
4700
+ const browser = structuredInstagramBrowser(d.browser);
4701
+ const pagination = structuredInstagramPagination(d.pagination, input);
4702
+ const itemRows = items.slice(0, 100).map(
4703
+ (item, i) => `| ${i + 1} | ${item.type} | \`${item.shortcode}\` | ${item.url} | ${cell(item.firstSeenStage ?? "")} |`
4704
+ ).join("\n");
4705
+ const browserLabel = browser.mode === "local" ? `local profile ${browser.profileName ? `\`${browser.profileName}\`` : "(unnamed)"}` : "hosted browser";
4706
+ const full = [
4707
+ `# Instagram Profile Content: ${d.handle ?? input.handle ?? input.url ?? "profile"}`,
4708
+ `**Collected:** ${items.length} items \xB7 posts ${typeCounts?.post ?? 0} \xB7 reels ${typeCounts?.reel ?? 0} \xB7 tv ${typeCounts?.tv ?? 0}`,
4709
+ `**Browser:** ${browserLabel}`,
4710
+ `**Pagination:** ${pagination.attemptedScrolls} scrolls \xB7 stopped: ${pagination.stoppedReason}`,
4711
+ d.reportedPostCountText ? `**Profile count:** ${d.reportedPostCountText}` : "",
4712
+ d.followerCountText ? `**Followers:** ${d.followerCountText}` : "",
4713
+ `
4714
+ ## Content Links
4715
+ | # | Type | Shortcode | URL | First Seen |
4716
+ |---|------|-----------|-----|------------|
4717
+ ${itemRows || "| \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |"}`,
4718
+ limitations.length ? `
4719
+ ## Limits
4720
+ ${limitations.map((l) => `- ${l}`).join("\n")}` : "",
4721
+ `
4722
+ ---
4723
+ \u{1F4A1} Use \`instagram_media_download\` with one of these URLs to download text, image, reel tracks, and optional transcript.`
4724
+ ].filter(Boolean).join("\n");
4725
+ return {
4726
+ ...oneBlock(full),
4727
+ structuredContent: {
4728
+ handle: String(d.handle ?? input.handle ?? ""),
4729
+ profileUrl: String(d.profileUrl ?? input.url ?? ""),
4730
+ pageUrl: String(d.pageUrl ?? d.profileUrl ?? input.url ?? ""),
4731
+ browser,
4732
+ profileName: typeof d.profileName === "string" ? d.profileName : null,
4733
+ reportedPostCount: typeof d.reportedPostCount === "number" ? d.reportedPostCount : null,
4734
+ reportedPostCountText: typeof d.reportedPostCountText === "string" ? d.reportedPostCountText : null,
4735
+ followerCountText: typeof d.followerCountText === "string" ? d.followerCountText : null,
4736
+ followingCountText: typeof d.followingCountText === "string" ? d.followingCountText : null,
4737
+ collectedContentCount: items.length,
4738
+ typeCounts: {
4739
+ post: Number(typeCounts?.post ?? 0),
4740
+ reel: Number(typeCounts?.reel ?? 0),
4741
+ tv: Number(typeCounts?.tv ?? 0)
4742
+ },
4743
+ pagination,
4744
+ limited: d.limited === true,
4745
+ limitations,
4746
+ items: items.map((item) => ({
4747
+ url: String(item.url ?? ""),
4748
+ type: item.type,
4749
+ shortcode: String(item.shortcode ?? ""),
4750
+ anchorText: item.anchorText ?? null,
4751
+ firstSeenStage: String(item.firstSeenStage ?? "")
4752
+ }))
4753
+ }
4754
+ };
4755
+ }
4756
+ function structuredInstagramTrack(track) {
4757
+ if (!track) return null;
4758
+ return {
4759
+ url: String(track.url ?? ""),
4760
+ streamType: track.streamType ?? "unknown",
4761
+ bitrate: typeof track.bitrate === "number" ? track.bitrate : null,
4762
+ durationSec: typeof track.durationSec === "number" ? track.durationSec : null,
4763
+ vencodeTag: track.vencodeTag ?? null,
4764
+ width: typeof track.width === "number" ? track.width : null,
4765
+ height: typeof track.height === "number" ? track.height : null
4766
+ };
4767
+ }
4768
+ function formatInstagramMediaDownload(raw, input) {
4769
+ const parsed = parseData(raw);
4770
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
4771
+ const d = parsed.data;
4772
+ const tracks = Array.isArray(d.tracks) ? d.tracks : [];
4773
+ const downloads = Array.isArray(d.downloads) ? d.downloads : [];
4774
+ const warnings = Array.isArray(d.warnings) ? d.warnings.map(String) : [];
4775
+ const limitations = Array.isArray(d.limitations) ? d.limitations.map(String) : [];
4776
+ const transcript = d.transcript;
4777
+ const transcriptText = transcript?.text ?? "";
4778
+ const chunks = transcript?.chunks ?? [];
4779
+ const browser = structuredInstagramBrowser(d.browser);
4780
+ const browserLabel = browser.mode === "local" ? `local profile ${browser.profileName ? `\`${browser.profileName}\`` : "(unnamed)"}` : "hosted browser";
4781
+ const downloadRows = downloads.map((download, i) => {
4782
+ const status = download.error ? `error: ${cell(download.error)}` : `${download.sizeBytes ?? 0} bytes`;
4783
+ return `| ${i + 1} | ${download.kind} | ${download.savedPath ? `\`${download.savedPath}\`` : "\u2014"} | ${status} |`;
4784
+ }).join("\n");
4785
+ const trackRows = tracks.slice(0, 20).map(
4786
+ (track, i) => `| ${i + 1} | ${track.streamType} | ${track.bitrate ?? "\u2014"} | ${track.durationSec ?? "\u2014"} | ${cell(track.vencodeTag ?? "")} |`
4787
+ ).join("\n");
4788
+ const full = [
4789
+ `# Instagram Media Download`,
4790
+ `**URL:** ${d.pageUrl ?? input.url}`,
4791
+ `**Browser:** ${browserLabel}`,
4792
+ d.ownerName ? `**Owner:** ${d.ownerName}` : "",
4793
+ d.shortcode ? `**Shortcode:** \`${d.shortcode}\`` : "",
4794
+ d.caption ? `
4795
+ ## Caption
4796
+ ${truncate(String(d.caption), 1200)}` : "",
4797
+ d.imageUrl ? `
4798
+ ## Image
4799
+ ${d.imageUrl}` : "",
4800
+ tracks.length ? `
4801
+ ## Media Tracks
4802
+ | # | Type | Bitrate | Duration | Tag |
4803
+ |---|------|---------|----------|-----|
4804
+ ${trackRows}` : "\n## Media Tracks\n*None captured.*",
4805
+ downloads.length ? `
4806
+ ## Downloads
4807
+ | # | Kind | File | Status |
4808
+ |---|------|------|--------|
4809
+ ${downloadRows}` : "",
4810
+ d.outputDir ? `
4811
+ **Output directory:** \`${d.outputDir}\`` : "",
4812
+ transcript ? `
4813
+ ## Transcript
4814
+ **${wordCount(transcriptText)} words** \xB7 ${chunks.length} chunks
4815
+
4816
+ ${transcriptText}` : "",
4817
+ warnings.length ? `
4818
+ ## Warnings
4819
+ ${warnings.map((w) => `- ${w}`).join("\n")}` : "",
4820
+ limitations.length ? `
4821
+ ## Limits
4822
+ ${limitations.map((l) => `- ${l}`).join("\n")}` : "",
4823
+ `
4824
+ ---
4825
+ \u{1F4A1} Reels may expose separate video-only and audio-only MP4 tracks. Use the muxed file when present; otherwise use the selected video/audio track URLs or saved files.`
4826
+ ].filter(Boolean).join("\n");
4827
+ return {
4828
+ ...oneBlock(full),
4829
+ structuredContent: {
4830
+ sourceUrl: String(d.sourceUrl ?? input.url),
4831
+ pageUrl: String(d.pageUrl ?? input.url),
4832
+ browser,
4833
+ type: d.type === "post" || d.type === "reel" || d.type === "tv" ? d.type : null,
4834
+ shortcode: typeof d.shortcode === "string" ? d.shortcode : null,
4835
+ ownerName: typeof d.ownerName === "string" ? d.ownerName : null,
4836
+ caption: typeof d.caption === "string" ? d.caption : null,
4837
+ imageUrl: typeof d.imageUrl === "string" ? d.imageUrl : null,
4838
+ trackCount: tracks.length,
4839
+ selectedVideoTrack: structuredInstagramTrack(d.selectedVideoTrack),
4840
+ selectedAudioTrack: structuredInstagramTrack(d.selectedAudioTrack),
4841
+ downloads: downloads.map((download) => ({
4842
+ kind: download.kind,
4843
+ url: download.url ?? null,
4844
+ savedPath: download.savedPath ?? null,
4845
+ sizeBytes: typeof download.sizeBytes === "number" ? download.sizeBytes : null,
4846
+ mimeType: download.mimeType ?? null,
4847
+ error: download.error ?? null
4848
+ })),
4849
+ outputDir: typeof d.outputDir === "string" ? d.outputDir : null,
4850
+ warnings,
4851
+ limitations,
4852
+ transcript: transcript ? {
4853
+ wordCount: wordCount(transcriptText),
4854
+ chunkCount: chunks.length,
4855
+ durationMs: typeof transcript.durationMs === "number" ? transcript.durationMs : null,
4856
+ transcriptText,
4857
+ chunks: structuredTranscriptChunks(chunks)
4858
+ } : null
4859
+ }
4860
+ };
4861
+ }
4646
4862
 
4647
4863
  // src/mcp/mcp-tool-schemas.ts
4648
4864
  var import_zod3 = require("zod");
@@ -4803,6 +5019,26 @@ var FacebookVideoTranscribeInputSchema = {
4803
5019
  url: import_zod3.z.string().url().describe("Organic Facebook reel, video, watch, post, or share URL from facebook.com, m.facebook.com, or fb.watch. The tool renders the page, extracts the best matching public Facebook CDN MP4 URL, then transcribes it. Use this when the user pastes a normal Facebook video page URL and asks for the transcript or downloadable MP4."),
4804
5020
  quality: import_zod3.z.enum(["best", "hd", "sd"]).default("best").describe("Preferred progressive MP4 quality. Use best by default; hd prefers the highest HD progressive URL; sd forces the SD URL.")
4805
5021
  };
5022
+ var InstagramProfileContentInputSchema = {
5023
+ handle: import_zod3.z.string().min(1).optional().describe("Instagram handle, with or without @. Provide handle or url. Use only public profiles unless a configured browser profile is available server-side."),
5024
+ url: import_zod3.z.string().url().optional().describe("Instagram profile URL, e.g. https://www.instagram.com/nasaartemis/. Provide handle or url."),
5025
+ maxItems: import_zod3.z.number().int().min(1).max(500).default(50).describe("Maximum profile grid post/reel/tv URLs to collect. Default 50, maximum 500. Use higher values with an authenticated browser profile when the user asks for a fuller archive."),
5026
+ maxScrolls: import_zod3.z.number().int().min(0).max(100).default(10).describe("Maximum pagination scroll attempts. Default 10, maximum 100. Increase when using authenticated Chromium and the profile has a long history."),
5027
+ scrollDelayMs: import_zod3.z.number().int().min(250).max(5e3).default(1200).describe("Delay after each pagination scroll before collecting newly loaded links. Default 1200ms. Increase to 2000-3000ms when Instagram loads slowly."),
5028
+ stableScrollLimit: import_zod3.z.number().int().min(1).max(10).default(4).describe("Stop after this many consecutive scrolls with no new links or scroll progress. Default 4."),
5029
+ browserMode: import_zod3.z.enum(["auto", "hosted", "local"]).default("auto").describe("Browser backend. auto uses the hosted browser unless local mode is configured or browserProfile is passed. local uses an imported/authenticated Chrome profile from browser_profile_import/sync. hosted forces the remote browser path."),
5030
+ browserProfile: import_zod3.z.string().min(1).optional().describe("Managed local browser profile name created by browser_profile_import/sync. Passing this opts the Instagram tool into local authenticated Chromium for this call.")
5031
+ };
5032
+ var InstagramMediaDownloadInputSchema = {
5033
+ url: import_zod3.z.string().url().describe("Instagram post, reel, or tv URL, e.g. https://www.instagram.com/reel/SHORTCODE/. The tool renders the page, extracts text, image metadata, and Instagram CDN media tracks."),
5034
+ mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download when downloadMedia is true. Reels commonly expose separate video-only and audio-only MP4 tracks."),
5035
+ downloadMedia: import_zod3.z.boolean().default(true).describe("Download extracted text/media files to the MCP Scraper output directory when the API server can write files. Always returns extracted media URLs even when false."),
5036
+ downloadAllTracks: import_zod3.z.boolean().default(false).describe("Download every captured Instagram MP4 track instead of only the selected best video and audio tracks. Use false by default to avoid duplicate bitrates."),
5037
+ includeTranscript: import_zod3.z.boolean().default(false).describe("Transcribe the selected audio track when available. This adds transcription cost and may take longer."),
5038
+ mux: import_zod3.z.boolean().default(true).describe("When video and audio tracks are downloaded separately, try to mux them into a single MP4 if ffmpeg is available. Returns separate tracks when muxing is unavailable."),
5039
+ browserMode: import_zod3.z.enum(["auto", "hosted", "local"]).default("auto").describe("Browser backend. auto uses the hosted browser unless local mode is configured or browserProfile is passed. local uses an imported/authenticated Chrome profile from browser_profile_import/sync. hosted forces the remote browser path."),
5040
+ browserProfile: import_zod3.z.string().min(1).optional().describe("Managed local browser profile name created by browser_profile_import/sync. Passing this opts the Instagram tool into local authenticated Chromium for this call.")
5041
+ };
4806
5042
  var MapsPlaceIntelInputSchema = {
4807
5043
  businessName: import_zod3.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
4808
5044
  location: import_zod3.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO". Infer from the user request when possible.'),
@@ -5199,6 +5435,101 @@ var TranscriptChunkOutput = import_zod3.z.object({
5199
5435
  endSec: import_zod3.z.number(),
5200
5436
  text: import_zod3.z.string()
5201
5437
  });
5438
+ var InstagramBrowserOutput = import_zod3.z.object({
5439
+ mode: import_zod3.z.enum(["hosted", "local"]),
5440
+ requestedMode: import_zod3.z.enum(["auto", "hosted", "local"]),
5441
+ profileName: NullableString2,
5442
+ profileSource: import_zod3.z.enum(["hosted", "managed_profile", "direct_profile_dir"]),
5443
+ profileDirConfigured: import_zod3.z.boolean(),
5444
+ executablePathConfigured: import_zod3.z.boolean()
5445
+ });
5446
+ var InstagramPaginationOutput = import_zod3.z.object({
5447
+ maxItems: import_zod3.z.number().int().min(1).max(500),
5448
+ maxScrolls: import_zod3.z.number().int().min(0).max(100),
5449
+ attemptedScrolls: import_zod3.z.number().int().min(0),
5450
+ stableScrolls: import_zod3.z.number().int().min(0),
5451
+ stableScrollLimit: import_zod3.z.number().int().min(1).max(10),
5452
+ scrollDelayMs: import_zod3.z.number().int().min(250).max(5e3),
5453
+ reachedMaxItems: import_zod3.z.boolean(),
5454
+ reachedReportedPostCount: import_zod3.z.boolean(),
5455
+ finalScrollHeight: import_zod3.z.number().int().nullable(),
5456
+ stoppedReason: import_zod3.z.enum(["max_items", "reported_post_count", "stable_scrolls", "max_scrolls", "no_scrolls"]),
5457
+ stages: import_zod3.z.array(import_zod3.z.object({
5458
+ stage: import_zod3.z.string(),
5459
+ itemCount: import_zod3.z.number().int().min(0),
5460
+ addedCount: import_zod3.z.number().int().min(0),
5461
+ scrollY: import_zod3.z.number().nullable(),
5462
+ scrollHeight: import_zod3.z.number().nullable()
5463
+ }))
5464
+ });
5465
+ var InstagramProfileContentOutputSchema = {
5466
+ handle: import_zod3.z.string(),
5467
+ profileUrl: import_zod3.z.string().url(),
5468
+ pageUrl: import_zod3.z.string().url(),
5469
+ browser: InstagramBrowserOutput,
5470
+ profileName: NullableString2,
5471
+ reportedPostCount: import_zod3.z.number().int().nullable(),
5472
+ reportedPostCountText: NullableString2,
5473
+ followerCountText: NullableString2,
5474
+ followingCountText: NullableString2,
5475
+ collectedContentCount: import_zod3.z.number().int().min(0),
5476
+ typeCounts: import_zod3.z.object({
5477
+ post: import_zod3.z.number().int().min(0),
5478
+ reel: import_zod3.z.number().int().min(0),
5479
+ tv: import_zod3.z.number().int().min(0)
5480
+ }),
5481
+ pagination: InstagramPaginationOutput,
5482
+ limited: import_zod3.z.boolean(),
5483
+ limitations: import_zod3.z.array(import_zod3.z.string()),
5484
+ items: import_zod3.z.array(import_zod3.z.object({
5485
+ url: import_zod3.z.string().url(),
5486
+ type: import_zod3.z.enum(["post", "reel", "tv"]),
5487
+ shortcode: import_zod3.z.string(),
5488
+ anchorText: NullableString2,
5489
+ firstSeenStage: import_zod3.z.string()
5490
+ }))
5491
+ };
5492
+ var InstagramMediaTrackOutput = import_zod3.z.object({
5493
+ url: import_zod3.z.string().url(),
5494
+ streamType: import_zod3.z.enum(["video", "audio", "unknown"]),
5495
+ bitrate: import_zod3.z.number().int().nullable(),
5496
+ durationSec: import_zod3.z.number().nullable(),
5497
+ vencodeTag: NullableString2,
5498
+ width: import_zod3.z.number().int().nullable(),
5499
+ height: import_zod3.z.number().int().nullable()
5500
+ });
5501
+ var InstagramDownloadOutput = import_zod3.z.object({
5502
+ kind: import_zod3.z.enum(["text", "image", "video", "audio", "muxed_video"]),
5503
+ url: import_zod3.z.string().url().nullable(),
5504
+ savedPath: NullableString2,
5505
+ sizeBytes: import_zod3.z.number().int().nullable(),
5506
+ mimeType: NullableString2,
5507
+ error: NullableString2
5508
+ });
5509
+ var InstagramMediaDownloadOutputSchema = {
5510
+ sourceUrl: import_zod3.z.string().url(),
5511
+ pageUrl: import_zod3.z.string().url(),
5512
+ browser: InstagramBrowserOutput,
5513
+ type: import_zod3.z.enum(["post", "reel", "tv"]).nullable(),
5514
+ shortcode: NullableString2,
5515
+ ownerName: NullableString2,
5516
+ caption: NullableString2,
5517
+ imageUrl: import_zod3.z.string().url().nullable(),
5518
+ trackCount: import_zod3.z.number().int().min(0),
5519
+ selectedVideoTrack: InstagramMediaTrackOutput.nullable(),
5520
+ selectedAudioTrack: InstagramMediaTrackOutput.nullable(),
5521
+ downloads: import_zod3.z.array(InstagramDownloadOutput),
5522
+ outputDir: NullableString2,
5523
+ warnings: import_zod3.z.array(import_zod3.z.string()),
5524
+ limitations: import_zod3.z.array(import_zod3.z.string()),
5525
+ transcript: import_zod3.z.object({
5526
+ wordCount: import_zod3.z.number().int().min(0),
5527
+ chunkCount: import_zod3.z.number().int().min(0),
5528
+ durationMs: import_zod3.z.number().nullable(),
5529
+ transcriptText: import_zod3.z.string(),
5530
+ chunks: import_zod3.z.array(TranscriptChunkOutput)
5531
+ }).nullable()
5532
+ };
5202
5533
  var YoutubeTranscribeOutputSchema = {
5203
5534
  videoId: NullableString2,
5204
5535
  url: NullableString2,
@@ -5843,6 +6174,20 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
5843
6174
  outputSchema: FacebookVideoTranscribeOutputSchema,
5844
6175
  annotations: liveWebToolAnnotations("Facebook Organic Video Transcription")
5845
6176
  }, async (input) => formatFacebookVideoTranscribe(await executor.facebookVideoTranscribe(input), input));
6177
+ server2.registerTool("instagram_profile_content", {
6178
+ title: "Instagram Profile Content Discovery",
6179
+ description: withReportNote("Discover Instagram profile grid content links for a handle or profile URL. Use this when the user wants a person or brand account content inventory before selecting posts/reels to download. Returns profile stats, collected post/reel/tv URLs, shortcodes, type counts, browser mode, pagination attempts, stop reason, and limitations. For authenticated collection, first use browser_profile_import/sync, then pass browserProfile and raise maxItems/maxScrolls for longer profile pagination."),
6180
+ inputSchema: InstagramProfileContentInputSchema,
6181
+ outputSchema: InstagramProfileContentOutputSchema,
6182
+ annotations: liveWebToolAnnotations("Instagram Profile Content Discovery")
6183
+ }, async (input) => formatInstagramProfileContent(await executor.instagramProfileContent(input), input));
6184
+ server2.registerTool("instagram_media_download", {
6185
+ title: "Instagram Post/Reel Media Download",
6186
+ description: withReportNote("Extract and download media from one Instagram post, reel, or tv URL. Use after instagram_profile_content or when the user gives a specific Instagram URL and wants the image, caption/text, reel audio/video tracks, optional muxed MP4, or optional transcript. Reels commonly expose separate video-only and audio-only MP4 tracks; this tool selects the best video and audio tracks and attempts muxing when ffmpeg is available. Pass browserProfile from browser_profile_import/sync when the media is login-gated."),
6187
+ inputSchema: InstagramMediaDownloadInputSchema,
6188
+ outputSchema: InstagramMediaDownloadOutputSchema,
6189
+ annotations: liveWebToolAnnotations("Instagram Post/Reel Media Download")
6190
+ }, async (input) => formatInstagramMediaDownload(await executor.instagramMediaDownload(input), input));
5846
6191
  server2.registerTool("maps_place_intel", {
5847
6192
  title: "Google Maps Business Profile Details",
5848
6193
  description: withReportNote('Extract Google Maps business intelligence for one known/named business: rating, review count, category, address, phone, website, hours, booking URL, review histogram, review topics, about attributes, entity IDs, and optional review cards. Do not use this for category searches, local market prospect lists, or requests for multiple GMB/GBP profiles; use maps_search first for those. Split business name from location (e.g. "Elite Roofing Denver CO" => businessName "Elite Roofing", location "Denver, CO"). Pass includeReviews true when the user asks for reviews/customer pain.'),
@@ -5980,11 +6325,11 @@ function renderInstallTerminal(options) {
5980
6325
  "1/1 install surfaces ready",
5981
6326
  colorize("Newest: local Chrome profile mode for browser_open, browser_profile_import/sync, and one-server MCP installs.", "lime", color),
5982
6327
  "",
5983
- `${colorize("Tools", "cyan", color)} ${colorize("(43 MCP tools)", "muted", color)}`,
6328
+ `${colorize("Tools", "cyan", color)} ${colorize("(46 MCP tools)", "muted", color)}`,
5984
6329
  toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
5985
6330
  toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "directory_workflow"], color),
5986
6331
  toolRow("build", ["rank_tracker_blueprint", "cron plan", "database prompt"], color),
5987
- toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe"], color),
6332
+ toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe", "instagram_profile_content", "instagram_media_download"], color),
5988
6333
  toolRow("browser", ["browser_open", "browser_profile_import", "browser_profile_sync", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
5989
6334
  toolRow("account", ["credits_info", "local reports", "MCP resources"], color),
5990
6335
  `${colorize("Workflows", "cyan", color)} ${colorize("(MCP + CLI + API)", "muted", color)}`,