mcp-scraper 0.3.1 → 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 (51) 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 +1 -1
  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 +348 -3
  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-LICHCMV6.js → chunk-HRTCMLZB.js} +2 -2
  28. package/dist/{chunk-55T4SRLJ.js → chunk-KO223PSJ.js} +347 -2
  29. package/dist/chunk-KO223PSJ.js.map +1 -0
  30. package/dist/{chunk-IPW4LFOT.js → chunk-UWSG3C5J.js} +4 -2
  31. package/dist/chunk-UWSG3C5J.js.map +1 -0
  32. package/dist/chunk-VB65G7PS.js +7 -0
  33. package/dist/chunk-VB65G7PS.js.map +1 -0
  34. package/dist/index.cjs +3 -1
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/{server-AXPNL2RV.js → server-N5LM3PEO.js} +997 -253
  38. package/dist/server-N5LM3PEO.js.map +1 -0
  39. package/dist/{worker-SLQ375UG.js → worker-56IXWOQU.js} +3 -3
  40. package/docs/mcp-tool-craft-lint.generated.md +5 -2
  41. package/docs/mcp-tool-manifest.generated.json +80 -5
  42. package/package.json +1 -1
  43. package/dist/chunk-55T4SRLJ.js.map +0 -1
  44. package/dist/chunk-D4JDGKOV.js +0 -7
  45. package/dist/chunk-D4JDGKOV.js.map +0 -1
  46. package/dist/chunk-IPW4LFOT.js.map +0 -1
  47. package/dist/chunk-Q4STSM63.js.map +0 -1
  48. package/dist/chunk-SXTXMFEQ.js.map +0 -1
  49. package/dist/server-AXPNL2RV.js.map +0 -1
  50. /package/dist/{chunk-LICHCMV6.js.map → chunk-HRTCMLZB.js.map} +0 -0
  51. /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-D4JDGKOV.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.1";
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");
@@ -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)}`,