mcp-scraper 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/bin/api-server.cjs +1398 -552
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +314 -0
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -0
- package/dist/bin/browser-agent-stdio-server.d.cts +1 -0
- package/dist/bin/browser-agent-stdio-server.d.ts +1 -0
- package/dist/bin/browser-agent-stdio-server.js +313 -0
- package/dist/bin/browser-agent-stdio-server.js.map +1 -0
- package/dist/bin/mcp-stdio-server.cjs +351 -314
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -1
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/chunk-2BS7BUEE.js +7 -0
- package/dist/chunk-2BS7BUEE.js.map +1 -0
- package/dist/{chunk-RE6HCRYC.js → chunk-BMVQB3WN.js} +352 -315
- package/dist/chunk-BMVQB3WN.js.map +1 -0
- package/dist/{chunk-ZK456YXN.js → chunk-GXBT5CDU.js} +20 -3
- package/dist/chunk-GXBT5CDU.js.map +1 -0
- package/dist/{server-QXVVTKJP.js → server-ASCMKUQ5.js} +794 -29
- package/dist/server-ASCMKUQ5.js.map +1 -0
- package/dist/{worker-AUCXFHEL.js → worker-KJ4A7WIR.js} +2 -2
- package/docs/specs/api-forge-spec.md +234 -0
- package/docs/specs/deferred-work-spec.md +74 -0
- package/docs/specs/oauth-mcp-spec.md +213 -0
- package/package.json +3 -2
- package/dist/chunk-RE6HCRYC.js.map +0 -1
- package/dist/chunk-ZK456YXN.js.map +0 -1
- package/dist/server-QXVVTKJP.js.map +0 -1
- /package/dist/{worker-AUCXFHEL.js.map → worker-KJ4A7WIR.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -3530,8 +3530,8 @@ var init_url_utils = __esm({
|
|
|
3530
3530
|
async function fetchWithKernel(url) {
|
|
3531
3531
|
const apiKey = browserServiceApiKey();
|
|
3532
3532
|
if (!apiKey) throw new Error("Browser backend API key not set");
|
|
3533
|
-
const
|
|
3534
|
-
const kb = await
|
|
3533
|
+
const client2 = new import_sdk.default({ apiKey });
|
|
3534
|
+
const kb = await client2.browsers.create({ stealth: true, timeout_seconds: 60 });
|
|
3535
3535
|
const browser = await import_playwright.chromium.connectOverCDP(kb.cdp_ws_url);
|
|
3536
3536
|
try {
|
|
3537
3537
|
const context = browser.contexts()[0] ?? await browser.newContext({
|
|
@@ -3544,7 +3544,7 @@ async function fetchWithKernel(url) {
|
|
|
3544
3544
|
} finally {
|
|
3545
3545
|
await browser.close().catch(() => {
|
|
3546
3546
|
});
|
|
3547
|
-
await
|
|
3547
|
+
await client2.browsers.deleteByID(kb.session_id).catch(() => {
|
|
3548
3548
|
});
|
|
3549
3549
|
}
|
|
3550
3550
|
}
|
|
@@ -4343,8 +4343,8 @@ async function downloadAsset(url, destDir, filename) {
|
|
|
4343
4343
|
}
|
|
4344
4344
|
const writer = (0, import_node_fs.createWriteStream)(dest);
|
|
4345
4345
|
await (0, import_promises2.pipeline)(import_node_stream.Readable.fromWeb(res.body), writer);
|
|
4346
|
-
const { statSync } = await import("fs");
|
|
4347
|
-
const sizeBytes =
|
|
4346
|
+
const { statSync: statSync2 } = await import("fs");
|
|
4347
|
+
const sizeBytes = statSync2(dest).size;
|
|
4348
4348
|
return { savedPath: dest, sizeBytes, mimeType };
|
|
4349
4349
|
}
|
|
4350
4350
|
async function harvestPageMedia(html, pageUrl, options = {}) {
|
|
@@ -8292,6 +8292,9 @@ var init_site_audit_routes = __esm({
|
|
|
8292
8292
|
});
|
|
8293
8293
|
|
|
8294
8294
|
// src/api/rates.ts
|
|
8295
|
+
function browserActiveCostMc(activeMs) {
|
|
8296
|
+
return Math.round(activeMs * MC_PER_BROWSER_MS);
|
|
8297
|
+
}
|
|
8295
8298
|
function mcToCredits(mc) {
|
|
8296
8299
|
return mc / MC_PER_CREDIT;
|
|
8297
8300
|
}
|
|
@@ -8308,7 +8311,7 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
|
|
|
8308
8311
|
topup_url: topupUrl
|
|
8309
8312
|
};
|
|
8310
8313
|
}
|
|
8311
|
-
var MC_COSTS, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, FREE_SIGNUP_MC, FREE_MONTHLY_REFRESH_MC, BALANCE_PRICE_IDS, BALANCE_PACK_LABELS, LedgerOperation;
|
|
8314
|
+
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, FREE_SIGNUP_MC, FREE_MONTHLY_REFRESH_MC, BALANCE_PRICE_IDS, BALANCE_PACK_LABELS, LedgerOperation;
|
|
8312
8315
|
var init_rates = __esm({
|
|
8313
8316
|
"src/api/rates.ts"() {
|
|
8314
8317
|
"use strict";
|
|
@@ -8324,8 +8327,11 @@ var init_rates = __esm({
|
|
|
8324
8327
|
maps_place: 2e3,
|
|
8325
8328
|
maps_review: 50,
|
|
8326
8329
|
fb_search: 50,
|
|
8327
|
-
fb_transcribe: 50
|
|
8330
|
+
fb_transcribe: 50,
|
|
8331
|
+
browser_minute: 4e3
|
|
8328
8332
|
};
|
|
8333
|
+
MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
|
|
8334
|
+
BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
|
|
8329
8335
|
MC_PER_CREDIT = 1e3;
|
|
8330
8336
|
CREDIT_COST_CATALOG = [
|
|
8331
8337
|
{
|
|
@@ -8421,6 +8427,14 @@ var init_rates = __esm({
|
|
|
8421
8427
|
credits: mcToCredits(MC_COSTS.fb_transcribe),
|
|
8422
8428
|
unit: "per call",
|
|
8423
8429
|
notes: "Whisper transcription of Facebook ad video via fal.ai."
|
|
8430
|
+
},
|
|
8431
|
+
{
|
|
8432
|
+
key: "browser_minute",
|
|
8433
|
+
label: "Interactive browser session",
|
|
8434
|
+
aliases: ["browser_open", "browser agent", "browser_agent", "live browser", "browse", "browser control", "interactive browser"],
|
|
8435
|
+
credits: mcToCredits(MC_COSTS.browser_minute),
|
|
8436
|
+
unit: "per minute of active time",
|
|
8437
|
+
notes: "Metered per second of active browser work (navigation, clicks, typing, screenshots). Idle and standby time are free. Billed against your balance as you act; close the session to stop the meter."
|
|
8424
8438
|
}
|
|
8425
8439
|
];
|
|
8426
8440
|
CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
|
|
@@ -8466,7 +8480,8 @@ var init_rates = __esm({
|
|
|
8466
8480
|
FB_SEARCH: "fb_search",
|
|
8467
8481
|
FB_TRANSCRIBE: "fb_transcribe",
|
|
8468
8482
|
FB_SEARCH_REFUND: "fb_search_refund",
|
|
8469
|
-
FB_TRANSCRIBE_REFUND: "fb_transcribe_refund"
|
|
8483
|
+
FB_TRANSCRIBE_REFUND: "fb_transcribe_refund",
|
|
8484
|
+
BROWSER_SESSION: "browser_session"
|
|
8470
8485
|
};
|
|
8471
8486
|
}
|
|
8472
8487
|
});
|
|
@@ -9032,19 +9047,19 @@ var init_BrowserDriver = __esm({
|
|
|
9032
9047
|
if (this.browser) {
|
|
9033
9048
|
const b = this.browser;
|
|
9034
9049
|
const sessionId = this.kernelSessionId;
|
|
9035
|
-
const
|
|
9050
|
+
const client2 = this.kernelClient;
|
|
9036
9051
|
this.browser = null;
|
|
9037
9052
|
this.context = null;
|
|
9038
9053
|
this.page = null;
|
|
9039
9054
|
this.kernelSessionId = null;
|
|
9040
9055
|
this.kernelClient = null;
|
|
9041
|
-
if (
|
|
9056
|
+
if (client2 && sessionId) {
|
|
9042
9057
|
console.info(JSON.stringify({
|
|
9043
9058
|
event: "kernel_browser_delete_started",
|
|
9044
9059
|
kernel_session_id: sessionId
|
|
9045
9060
|
}));
|
|
9046
9061
|
const deleteSession = withTimeout(
|
|
9047
|
-
|
|
9062
|
+
client2.browsers.deleteByID(sessionId),
|
|
9048
9063
|
KERNEL_SESSION_DELETE_TIMEOUT_MS,
|
|
9049
9064
|
`Kernel session ${sessionId} delete`
|
|
9050
9065
|
);
|
|
@@ -10513,13 +10528,13 @@ var init_FacebookAdExtractor = __esm({
|
|
|
10513
10528
|
}
|
|
10514
10529
|
await page.waitForTimeout(1500);
|
|
10515
10530
|
let prevCount = 0;
|
|
10516
|
-
for (let
|
|
10531
|
+
for (let scroll2 = 0; scroll2 < 20; scroll2++) {
|
|
10517
10532
|
const count = await page.evaluate(() => {
|
|
10518
10533
|
const bt = document.body ? document.body.innerText ?? "" : "";
|
|
10519
10534
|
return [...bt.matchAll(/Library ID/g)].length;
|
|
10520
10535
|
});
|
|
10521
10536
|
if (count >= maxAds) break;
|
|
10522
|
-
if (count === prevCount &&
|
|
10537
|
+
if (count === prevCount && scroll2 > 0) break;
|
|
10523
10538
|
prevCount = count;
|
|
10524
10539
|
await page.evaluate(() => {
|
|
10525
10540
|
if (document.body) window.scrollTo(0, document.body.scrollHeight);
|
|
@@ -11663,7 +11678,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
11663
11678
|
return c.json(searchResult2);
|
|
11664
11679
|
}
|
|
11665
11680
|
await page.waitForTimeout(1500);
|
|
11666
|
-
for (let
|
|
11681
|
+
for (let scroll2 = 0; scroll2 < 3; scroll2++) {
|
|
11667
11682
|
await page.evaluate(() => {
|
|
11668
11683
|
if (document.body) window.scrollTo(0, document.body.scrollHeight);
|
|
11669
11684
|
});
|
|
@@ -15276,321 +15291,7 @@ var PACKAGE_VERSION;
|
|
|
15276
15291
|
var init_version = __esm({
|
|
15277
15292
|
"src/version.ts"() {
|
|
15278
15293
|
"use strict";
|
|
15279
|
-
PACKAGE_VERSION = "0.
|
|
15280
|
-
}
|
|
15281
|
-
});
|
|
15282
|
-
|
|
15283
|
-
// src/mcp/mcp-tool-schemas.ts
|
|
15284
|
-
var import_zod19, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, NullableString, MapsSearchOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, CreditsInfoInputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
|
|
15285
|
-
var init_mcp_tool_schemas = __esm({
|
|
15286
|
-
"src/mcp/mcp-tool-schemas.ts"() {
|
|
15287
|
-
"use strict";
|
|
15288
|
-
import_zod19 = require("zod");
|
|
15289
|
-
HarvestPaaInputSchema = {
|
|
15290
|
-
query: import_zod19.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
|
|
15291
|
-
location: import_zod19.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
|
|
15292
|
-
maxQuestions: import_zod19.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
|
|
15293
|
-
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
|
|
15294
|
-
hl: import_zod19.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
|
|
15295
|
-
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
15296
|
-
proxyMode: import_zod19.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default so city/state searches create or reuse a matching residential proxy. Use configured for the static configured proxy. Use none only for direct-network debugging."),
|
|
15297
|
-
proxyZip: import_zod19.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only when the user gives a specific ZIP or city-center proxy targeting needs to be forced."),
|
|
15298
|
-
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
|
|
15299
|
-
};
|
|
15300
|
-
ExtractUrlInputSchema = {
|
|
15301
|
-
url: import_zod19.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
|
|
15302
|
-
screenshot: import_zod19.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
|
|
15303
|
-
screenshotDevice: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
|
|
15304
|
-
extractBranding: import_zod19.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
|
|
15305
|
-
downloadMedia: import_zod19.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
|
|
15306
|
-
mediaTypes: import_zod19.z.array(import_zod19.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
|
|
15307
|
-
allowLocal: import_zod19.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
|
|
15308
|
-
};
|
|
15309
|
-
MapSiteUrlsInputSchema = {
|
|
15310
|
-
url: import_zod19.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
15311
|
-
maxUrls: import_zod19.z.number().int().min(1).max(500).optional().describe("Maximum URLs to discover. Use 100 for normal maps, higher when the user asks for a full inventory.")
|
|
15312
|
-
};
|
|
15313
|
-
ExtractSiteInputSchema = {
|
|
15314
|
-
url: import_zod19.z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
|
|
15315
|
-
maxPages: import_zod19.z.number().int().min(1).max(50).optional().describe("Maximum pages to extract. Use 50 when the user asks for full results or a complete crawl within MCP limits.")
|
|
15316
|
-
};
|
|
15317
|
-
YoutubeHarvestInputSchema = {
|
|
15318
|
-
mode: import_zod19.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
15319
|
-
query: import_zod19.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
|
|
15320
|
-
channelHandle: import_zod19.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
|
|
15321
|
-
maxVideos: import_zod19.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50. Increase when user asks for full channel/history.")
|
|
15322
|
-
};
|
|
15323
|
-
YoutubeTranscribeInputSchema = {
|
|
15324
|
-
videoId: import_zod19.z.string().min(1).describe("YouTube video ID, e.g. dQw4w9WgXcQ")
|
|
15325
|
-
};
|
|
15326
|
-
FacebookPageIntelInputSchema = {
|
|
15327
|
-
pageId: import_zod19.z.string().optional(),
|
|
15328
|
-
libraryId: import_zod19.z.string().optional(),
|
|
15329
|
-
query: import_zod19.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
|
|
15330
|
-
maxAds: import_zod19.z.number().int().min(1).max(200).default(50),
|
|
15331
|
-
country: import_zod19.z.string().length(2).default("US")
|
|
15332
|
-
};
|
|
15333
|
-
FacebookAdSearchInputSchema = {
|
|
15334
|
-
query: import_zod19.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
|
|
15335
|
-
country: import_zod19.z.string().length(2).default("US"),
|
|
15336
|
-
maxResults: import_zod19.z.number().int().min(1).max(20).default(10)
|
|
15337
|
-
};
|
|
15338
|
-
FacebookAdTranscribeInputSchema = {
|
|
15339
|
-
videoUrl: import_zod19.z.string().url().describe("Facebook CDN video URL from a facebook_page_intel result")
|
|
15340
|
-
};
|
|
15341
|
-
MapsPlaceIntelInputSchema = {
|
|
15342
|
-
businessName: import_zod19.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
|
|
15343
|
-
location: import_zod19.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.'),
|
|
15344
|
-
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
15345
|
-
hl: import_zod19.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
15346
|
-
includeReviews: import_zod19.z.boolean().default(false).describe("Whether to fetch individual review cards"),
|
|
15347
|
-
maxReviews: import_zod19.z.number().int().min(1).max(500).default(50).describe("Max review cards to return (requires includeReviews: true)")
|
|
15348
|
-
};
|
|
15349
|
-
MapsSearchInputSchema = {
|
|
15350
|
-
query: import_zod19.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
|
|
15351
|
-
location: import_zod19.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
|
|
15352
|
-
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
15353
|
-
hl: import_zod19.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
15354
|
-
maxResults: import_zod19.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more.")
|
|
15355
|
-
};
|
|
15356
|
-
NullableString = import_zod19.z.string().nullable();
|
|
15357
|
-
MapsSearchOutputSchema = {
|
|
15358
|
-
query: import_zod19.z.string(),
|
|
15359
|
-
location: import_zod19.z.string().nullable(),
|
|
15360
|
-
searchQuery: import_zod19.z.string(),
|
|
15361
|
-
searchUrl: import_zod19.z.string().url(),
|
|
15362
|
-
extractedAt: import_zod19.z.string(),
|
|
15363
|
-
requestedMaxResults: import_zod19.z.number().int().min(1).max(50),
|
|
15364
|
-
resultCount: import_zod19.z.number().int().min(0).max(50),
|
|
15365
|
-
results: import_zod19.z.array(import_zod19.z.object({
|
|
15366
|
-
position: import_zod19.z.number().int().min(1),
|
|
15367
|
-
name: import_zod19.z.string(),
|
|
15368
|
-
placeUrl: import_zod19.z.string().url(),
|
|
15369
|
-
cid: NullableString,
|
|
15370
|
-
cidDecimal: NullableString,
|
|
15371
|
-
rating: NullableString,
|
|
15372
|
-
reviewCount: NullableString,
|
|
15373
|
-
category: NullableString,
|
|
15374
|
-
address: NullableString,
|
|
15375
|
-
websiteUrl: NullableString,
|
|
15376
|
-
directionsUrl: NullableString,
|
|
15377
|
-
metadata: import_zod19.z.array(import_zod19.z.string())
|
|
15378
|
-
})),
|
|
15379
|
-
durationMs: import_zod19.z.number().int().min(0)
|
|
15380
|
-
};
|
|
15381
|
-
OrganicResultOutput = import_zod19.z.object({
|
|
15382
|
-
position: import_zod19.z.number().int(),
|
|
15383
|
-
title: import_zod19.z.string(),
|
|
15384
|
-
url: import_zod19.z.string(),
|
|
15385
|
-
domain: import_zod19.z.string(),
|
|
15386
|
-
snippet: NullableString
|
|
15387
|
-
});
|
|
15388
|
-
AiOverviewOutput = import_zod19.z.object({
|
|
15389
|
-
detected: import_zod19.z.boolean(),
|
|
15390
|
-
text: NullableString
|
|
15391
|
-
}).nullable();
|
|
15392
|
-
EntityIdsOutput = import_zod19.z.object({
|
|
15393
|
-
kgIds: import_zod19.z.array(import_zod19.z.string()),
|
|
15394
|
-
cids: import_zod19.z.array(import_zod19.z.string()),
|
|
15395
|
-
gcids: import_zod19.z.array(import_zod19.z.string())
|
|
15396
|
-
}).nullable();
|
|
15397
|
-
HarvestPaaOutputSchema = {
|
|
15398
|
-
query: import_zod19.z.string(),
|
|
15399
|
-
location: NullableString,
|
|
15400
|
-
questionCount: import_zod19.z.number().int().min(0),
|
|
15401
|
-
completionStatus: NullableString,
|
|
15402
|
-
questions: import_zod19.z.array(import_zod19.z.object({
|
|
15403
|
-
question: import_zod19.z.string(),
|
|
15404
|
-
answer: NullableString,
|
|
15405
|
-
sourceTitle: NullableString,
|
|
15406
|
-
sourceSite: NullableString
|
|
15407
|
-
})),
|
|
15408
|
-
organicResults: import_zod19.z.array(OrganicResultOutput),
|
|
15409
|
-
aiOverview: AiOverviewOutput,
|
|
15410
|
-
entityIds: EntityIdsOutput,
|
|
15411
|
-
durationMs: import_zod19.z.number().min(0).nullable()
|
|
15412
|
-
};
|
|
15413
|
-
SearchSerpOutputSchema = {
|
|
15414
|
-
query: import_zod19.z.string(),
|
|
15415
|
-
location: NullableString,
|
|
15416
|
-
organicResults: import_zod19.z.array(OrganicResultOutput),
|
|
15417
|
-
localPack: import_zod19.z.array(import_zod19.z.object({
|
|
15418
|
-
position: import_zod19.z.number().int(),
|
|
15419
|
-
name: import_zod19.z.string(),
|
|
15420
|
-
rating: NullableString,
|
|
15421
|
-
reviewCount: NullableString,
|
|
15422
|
-
websiteUrl: NullableString
|
|
15423
|
-
})),
|
|
15424
|
-
aiOverview: AiOverviewOutput,
|
|
15425
|
-
entityIds: EntityIdsOutput
|
|
15426
|
-
};
|
|
15427
|
-
ExtractUrlOutputSchema = {
|
|
15428
|
-
url: import_zod19.z.string(),
|
|
15429
|
-
title: NullableString,
|
|
15430
|
-
headings: import_zod19.z.array(import_zod19.z.object({
|
|
15431
|
-
level: import_zod19.z.number().int(),
|
|
15432
|
-
text: import_zod19.z.string()
|
|
15433
|
-
})),
|
|
15434
|
-
schemaBlockCount: import_zod19.z.number().int().min(0),
|
|
15435
|
-
entityName: NullableString,
|
|
15436
|
-
entityTypes: import_zod19.z.array(import_zod19.z.string()),
|
|
15437
|
-
napScore: import_zod19.z.number().nullable(),
|
|
15438
|
-
missingSchemaFields: import_zod19.z.array(import_zod19.z.string()),
|
|
15439
|
-
screenshotSaved: NullableString
|
|
15440
|
-
};
|
|
15441
|
-
ExtractSiteOutputSchema = {
|
|
15442
|
-
url: import_zod19.z.string(),
|
|
15443
|
-
pageCount: import_zod19.z.number().int().min(0),
|
|
15444
|
-
pages: import_zod19.z.array(import_zod19.z.object({
|
|
15445
|
-
url: import_zod19.z.string(),
|
|
15446
|
-
title: NullableString,
|
|
15447
|
-
schemaTypes: import_zod19.z.array(import_zod19.z.string())
|
|
15448
|
-
})),
|
|
15449
|
-
durationMs: import_zod19.z.number().min(0)
|
|
15450
|
-
};
|
|
15451
|
-
MapsPlaceIntelOutputSchema = {
|
|
15452
|
-
name: import_zod19.z.string(),
|
|
15453
|
-
rating: NullableString,
|
|
15454
|
-
reviewCount: NullableString,
|
|
15455
|
-
category: NullableString,
|
|
15456
|
-
address: NullableString,
|
|
15457
|
-
phone: NullableString,
|
|
15458
|
-
website: NullableString,
|
|
15459
|
-
hoursSummary: NullableString,
|
|
15460
|
-
bookingUrl: NullableString,
|
|
15461
|
-
kgmid: NullableString,
|
|
15462
|
-
cidDecimal: NullableString,
|
|
15463
|
-
cidUrl: NullableString,
|
|
15464
|
-
lat: import_zod19.z.number().nullable(),
|
|
15465
|
-
lng: import_zod19.z.number().nullable(),
|
|
15466
|
-
reviewsStatus: import_zod19.z.string(),
|
|
15467
|
-
reviewsCollected: import_zod19.z.number().int().min(0),
|
|
15468
|
-
reviewTopics: import_zod19.z.array(import_zod19.z.object({
|
|
15469
|
-
label: import_zod19.z.string(),
|
|
15470
|
-
count: import_zod19.z.string()
|
|
15471
|
-
}))
|
|
15472
|
-
};
|
|
15473
|
-
CreditsInfoOutputSchema = {
|
|
15474
|
-
balanceCredits: import_zod19.z.number().nullable(),
|
|
15475
|
-
matchedCost: import_zod19.z.object({
|
|
15476
|
-
label: import_zod19.z.string(),
|
|
15477
|
-
credits: import_zod19.z.number(),
|
|
15478
|
-
unit: import_zod19.z.string(),
|
|
15479
|
-
notes: NullableString
|
|
15480
|
-
}).nullable(),
|
|
15481
|
-
costs: import_zod19.z.array(import_zod19.z.object({
|
|
15482
|
-
key: import_zod19.z.string(),
|
|
15483
|
-
label: import_zod19.z.string(),
|
|
15484
|
-
credits: import_zod19.z.number(),
|
|
15485
|
-
unit: import_zod19.z.string(),
|
|
15486
|
-
notes: NullableString
|
|
15487
|
-
})),
|
|
15488
|
-
ledger: import_zod19.z.array(import_zod19.z.object({
|
|
15489
|
-
createdAt: import_zod19.z.string(),
|
|
15490
|
-
operation: import_zod19.z.string(),
|
|
15491
|
-
credits: import_zod19.z.number(),
|
|
15492
|
-
description: NullableString
|
|
15493
|
-
}))
|
|
15494
|
-
};
|
|
15495
|
-
MapSiteUrlsOutputSchema = {
|
|
15496
|
-
startUrl: import_zod19.z.string(),
|
|
15497
|
-
totalFound: import_zod19.z.number().int().min(0),
|
|
15498
|
-
truncated: import_zod19.z.boolean(),
|
|
15499
|
-
okCount: import_zod19.z.number().int().min(0),
|
|
15500
|
-
redirectCount: import_zod19.z.number().int().min(0),
|
|
15501
|
-
brokenCount: import_zod19.z.number().int().min(0),
|
|
15502
|
-
urls: import_zod19.z.array(import_zod19.z.object({
|
|
15503
|
-
url: import_zod19.z.string(),
|
|
15504
|
-
status: import_zod19.z.number().int().nullable()
|
|
15505
|
-
})),
|
|
15506
|
-
durationMs: import_zod19.z.number().min(0)
|
|
15507
|
-
};
|
|
15508
|
-
YoutubeHarvestOutputSchema = {
|
|
15509
|
-
mode: import_zod19.z.string(),
|
|
15510
|
-
videoCount: import_zod19.z.number().int().min(0),
|
|
15511
|
-
channel: import_zod19.z.object({
|
|
15512
|
-
title: NullableString,
|
|
15513
|
-
subscriberCount: NullableString
|
|
15514
|
-
}).nullable(),
|
|
15515
|
-
videos: import_zod19.z.array(import_zod19.z.object({
|
|
15516
|
-
videoId: import_zod19.z.string(),
|
|
15517
|
-
title: import_zod19.z.string(),
|
|
15518
|
-
channelName: NullableString,
|
|
15519
|
-
views: NullableString,
|
|
15520
|
-
duration: NullableString,
|
|
15521
|
-
url: NullableString
|
|
15522
|
-
}))
|
|
15523
|
-
};
|
|
15524
|
-
FacebookAdSearchOutputSchema = {
|
|
15525
|
-
query: import_zod19.z.string(),
|
|
15526
|
-
advertiserCount: import_zod19.z.number().int().min(0),
|
|
15527
|
-
advertisers: import_zod19.z.array(import_zod19.z.object({
|
|
15528
|
-
name: NullableString,
|
|
15529
|
-
adCount: import_zod19.z.number().int().nullable(),
|
|
15530
|
-
libraryId: NullableString
|
|
15531
|
-
}))
|
|
15532
|
-
};
|
|
15533
|
-
FacebookPageIntelOutputSchema = {
|
|
15534
|
-
advertiserName: NullableString,
|
|
15535
|
-
totalAds: import_zod19.z.number().int().min(0),
|
|
15536
|
-
activeCount: import_zod19.z.number().int().min(0),
|
|
15537
|
-
videoCount: import_zod19.z.number().int().min(0),
|
|
15538
|
-
imageCount: import_zod19.z.number().int().min(0),
|
|
15539
|
-
ads: import_zod19.z.array(import_zod19.z.object({
|
|
15540
|
-
libraryId: NullableString,
|
|
15541
|
-
status: NullableString,
|
|
15542
|
-
creativeType: NullableString,
|
|
15543
|
-
headline: NullableString,
|
|
15544
|
-
cta: NullableString,
|
|
15545
|
-
startDate: NullableString,
|
|
15546
|
-
videoUrl: NullableString,
|
|
15547
|
-
variations: import_zod19.z.number().int().nullable()
|
|
15548
|
-
}))
|
|
15549
|
-
};
|
|
15550
|
-
CreditsInfoInputSchema = {
|
|
15551
|
-
item: import_zod19.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", or "YouTube transcription"'),
|
|
15552
|
-
includeLedger: import_zod19.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
|
|
15553
|
-
};
|
|
15554
|
-
SearchSerpInputSchema = {
|
|
15555
|
-
query: import_zod19.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
|
|
15556
|
-
location: import_zod19.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
|
|
15557
|
-
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
|
|
15558
|
-
hl: import_zod19.z.string().default("en").describe("Google interface/content language inferred from user request."),
|
|
15559
|
-
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
15560
|
-
proxyMode: import_zod19.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default so city/state searches create or reuse a matching residential proxy. Use configured for the static configured proxy. Use none only for direct-network debugging."),
|
|
15561
|
-
proxyZip: import_zod19.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only when the user gives a specific ZIP or city-center proxy targeting needs to be forced."),
|
|
15562
|
-
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
|
|
15563
|
-
pages: import_zod19.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
|
|
15564
|
-
};
|
|
15565
|
-
CaptureSerpSnapshotInputSchema = {
|
|
15566
|
-
query: import_zod19.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
|
|
15567
|
-
location: import_zod19.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
|
|
15568
|
-
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
|
|
15569
|
-
hl: import_zod19.z.string().default("en").describe("Google interface/content language inferred from the user request."),
|
|
15570
|
-
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
|
|
15571
|
-
proxyMode: import_zod19.z.enum(["location", "configured", "none"]).default("location").describe("Proxy behavior for capture. Use location for localized residential proxy targeting, configured for the static residential proxy, and none only for direct-network debugging."),
|
|
15572
|
-
proxyZip: import_zod19.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting when a precise city-center or ZIP proxy is needed."),
|
|
15573
|
-
pages: import_zod19.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
|
|
15574
|
-
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
|
|
15575
|
-
includePageSnapshots: import_zod19.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
|
|
15576
|
-
pageSnapshotLimit: import_zod19.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
|
|
15577
|
-
};
|
|
15578
|
-
ScreenshotInputSchema = {
|
|
15579
|
-
url: import_zod19.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
|
|
15580
|
-
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport profile. desktop = 1440\xD7900. mobile = 390\xD7844. Use desktop by default; use mobile when the user asks for a mobile view."),
|
|
15581
|
-
allowLocal: import_zod19.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
|
|
15582
|
-
};
|
|
15583
|
-
CaptureSerpPageSnapshotsInputSchema = {
|
|
15584
|
-
urls: import_zod19.z.array(import_zod19.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
|
|
15585
|
-
targets: import_zod19.z.array(import_zod19.z.object({
|
|
15586
|
-
url: import_zod19.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
|
|
15587
|
-
sourceKind: import_zod19.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
|
|
15588
|
-
sourcePosition: import_zod19.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
|
|
15589
|
-
}).strict()).min(1).max(25).optional().describe("Structured page snapshot targets. Use this instead of urls when source kind or position should be preserved."),
|
|
15590
|
-
maxConcurrency: import_zod19.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
|
|
15591
|
-
timeoutMs: import_zod19.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
|
|
15592
|
-
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
|
|
15593
|
-
};
|
|
15294
|
+
PACKAGE_VERSION = "0.2.0";
|
|
15594
15295
|
}
|
|
15595
15296
|
});
|
|
15596
15297
|
|
|
@@ -16461,200 +16162,552 @@ var init_mcp_response_formatter = __esm({
|
|
|
16461
16162
|
}
|
|
16462
16163
|
});
|
|
16463
16164
|
|
|
16464
|
-
// src/mcp/
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
readOnlyHint: true,
|
|
16469
|
-
destructiveHint: false,
|
|
16470
|
-
idempotentHint: false,
|
|
16471
|
-
openWorldHint: true
|
|
16472
|
-
};
|
|
16473
|
-
}
|
|
16474
|
-
function buildPaaExtractorMcpServer(executor, options = {}) {
|
|
16475
|
-
const savesReports = options.savesReportsLocally !== false;
|
|
16476
|
-
const reportNote = savesReports ? " Saves a full Markdown report locally." : " Reports are returned inline; no files are saved on this hosted endpoint.";
|
|
16477
|
-
const withReportNote = (description) => `${description}${reportNote}`;
|
|
16478
|
-
const server = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
16479
|
-
server.registerTool("harvest_paa", {
|
|
16480
|
-
title: "Google PAA + SERP Harvest",
|
|
16481
|
-
description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). Use maxQuestions 30 normally, 100-150 for "full", "deep", "all", or comprehensive research. Credits are charged by extracted question; unused request hold is refunded.'),
|
|
16482
|
-
inputSchema: HarvestPaaInputSchema,
|
|
16483
|
-
outputSchema: HarvestPaaOutputSchema,
|
|
16484
|
-
annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
|
|
16485
|
-
}, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
|
|
16486
|
-
server.registerTool("search_serp", {
|
|
16487
|
-
title: "Google SERP Lookup",
|
|
16488
|
-
description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request."),
|
|
16489
|
-
inputSchema: SearchSerpInputSchema,
|
|
16490
|
-
outputSchema: SearchSerpOutputSchema,
|
|
16491
|
-
annotations: liveWebToolAnnotations("Google SERP Lookup")
|
|
16492
|
-
}, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
|
|
16493
|
-
server.registerTool("extract_url", {
|
|
16494
|
-
title: "Single URL Extract",
|
|
16495
|
-
description: withReportNote("Extract structured data from one public URL: page content as Markdown, heading structure, JSON-LD schema, entity details, NAP score, metadata, and missing schema fields. Use when the user provides a single URL or asks to inspect/scrape one page."),
|
|
16496
|
-
inputSchema: ExtractUrlInputSchema,
|
|
16497
|
-
outputSchema: ExtractUrlOutputSchema,
|
|
16498
|
-
annotations: liveWebToolAnnotations("Single URL Extract")
|
|
16499
|
-
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
16500
|
-
server.registerTool("map_site_urls", {
|
|
16501
|
-
title: "Site URL Map",
|
|
16502
|
-
description: withReportNote("Map/crawl a public website to build a URL inventory with HTTP status codes, broken links, redirects, and site scope. Use before extract_site for audits or when the user asks for a sitemap/URL inventory."),
|
|
16503
|
-
inputSchema: MapSiteUrlsInputSchema,
|
|
16504
|
-
outputSchema: MapSiteUrlsOutputSchema,
|
|
16505
|
-
annotations: liveWebToolAnnotations("Site URL Map")
|
|
16506
|
-
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
16507
|
-
server.registerTool("extract_site", {
|
|
16508
|
-
title: "Multi-Page Site Extract",
|
|
16509
|
-
description: withReportNote("Run multi-page extraction across a public website. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use for website audits, competitor audits, and full-site extraction."),
|
|
16510
|
-
inputSchema: ExtractSiteInputSchema,
|
|
16511
|
-
outputSchema: ExtractSiteOutputSchema,
|
|
16512
|
-
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
16513
|
-
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
16514
|
-
server.registerTool("youtube_harvest", {
|
|
16515
|
-
title: "YouTube Video Harvest",
|
|
16516
|
-
description: withReportNote('Harvest YouTube video metadata by search query or channel handle/ID/URL. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, dates, durations, URLs, thumbnails, and videoIds for follow-up transcription.'),
|
|
16517
|
-
inputSchema: YoutubeHarvestInputSchema,
|
|
16518
|
-
outputSchema: YoutubeHarvestOutputSchema,
|
|
16519
|
-
annotations: liveWebToolAnnotations("YouTube Video Harvest")
|
|
16520
|
-
}, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
|
|
16521
|
-
server.registerTool("youtube_transcribe", {
|
|
16522
|
-
title: "YouTube Transcription",
|
|
16523
|
-
description: withReportNote("Fetch and transcribe captions from a YouTube video. Returns full transcript, timestamped chunks, and word count. Pass a videoId from youtube_harvest results or infer it from a YouTube URL if the user provided one."),
|
|
16524
|
-
inputSchema: YoutubeTranscribeInputSchema,
|
|
16525
|
-
annotations: liveWebToolAnnotations("YouTube Transcription")
|
|
16526
|
-
}, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
|
|
16527
|
-
server.registerTool("facebook_page_intel", {
|
|
16528
|
-
title: "Facebook Advertiser Ad Intel",
|
|
16529
|
-
description: withReportNote("Harvest ads from a Facebook advertiser. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and video URLs ready for transcription. Accepts pageId, libraryId, or a brand/advertiser name as query. Use after facebook_ad_search when possible."),
|
|
16530
|
-
inputSchema: FacebookPageIntelInputSchema,
|
|
16531
|
-
outputSchema: FacebookPageIntelOutputSchema,
|
|
16532
|
-
annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
|
|
16533
|
-
}, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
|
|
16534
|
-
server.registerTool("facebook_ad_search", {
|
|
16535
|
-
title: "Facebook Ad Library Search",
|
|
16536
|
-
description: withReportNote("Search Facebook Ad Library by brand, advertiser, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use to discover competitors, then pass libraryId to facebook_page_intel."),
|
|
16537
|
-
inputSchema: FacebookAdSearchInputSchema,
|
|
16538
|
-
outputSchema: FacebookAdSearchOutputSchema,
|
|
16539
|
-
annotations: liveWebToolAnnotations("Facebook Ad Library Search")
|
|
16540
|
-
}, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
|
|
16541
|
-
server.registerTool("facebook_ad_transcribe", {
|
|
16542
|
-
title: "Facebook Ad Transcription",
|
|
16543
|
-
description: "Transcribe audio from a Facebook ad video. Returns full transcript and timestamped chunks. Use the videoUrl value from facebook_page_intel results.",
|
|
16544
|
-
inputSchema: FacebookAdTranscribeInputSchema,
|
|
16545
|
-
annotations: liveWebToolAnnotations("Facebook Ad Transcription")
|
|
16546
|
-
}, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
|
|
16547
|
-
server.registerTool("maps_place_intel", {
|
|
16548
|
-
title: "Google Maps Business Profile Details",
|
|
16549
|
-
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.'),
|
|
16550
|
-
inputSchema: MapsPlaceIntelInputSchema,
|
|
16551
|
-
outputSchema: MapsPlaceIntelOutputSchema,
|
|
16552
|
-
annotations: liveWebToolAnnotations("Google Maps Business Profile Details")
|
|
16553
|
-
}, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
|
|
16554
|
-
server.registerTool("maps_search", {
|
|
16555
|
-
title: "Google Maps Business Search",
|
|
16556
|
-
description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, and profile metadata. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
|
|
16557
|
-
inputSchema: MapsSearchInputSchema,
|
|
16558
|
-
outputSchema: MapsSearchOutputSchema,
|
|
16559
|
-
annotations: liveWebToolAnnotations("Google Maps Business Search")
|
|
16560
|
-
}, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
|
|
16561
|
-
server.registerTool("credits_info", {
|
|
16562
|
-
title: "MCP Scraper Credits & Costs",
|
|
16563
|
-
description: "Answer questions about MCP Scraper credits: current credit balance, what a specific tool/action costs, the full cost table, and optionally recent credit ledger entries. Does not expose payment methods or credit card information.",
|
|
16564
|
-
inputSchema: CreditsInfoInputSchema,
|
|
16565
|
-
outputSchema: CreditsInfoOutputSchema,
|
|
16566
|
-
annotations: {
|
|
16567
|
-
title: "MCP Scraper Credits & Costs",
|
|
16568
|
-
readOnlyHint: true,
|
|
16569
|
-
destructiveHint: false,
|
|
16570
|
-
idempotentHint: true,
|
|
16571
|
-
openWorldHint: false
|
|
16572
|
-
}
|
|
16573
|
-
}, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
|
|
16574
|
-
return server;
|
|
16575
|
-
}
|
|
16576
|
-
var import_mcp;
|
|
16577
|
-
var init_paa_mcp_server = __esm({
|
|
16578
|
-
"src/mcp/paa-mcp-server.ts"() {
|
|
16579
|
-
"use strict";
|
|
16580
|
-
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16581
|
-
init_version();
|
|
16582
|
-
init_mcp_tool_schemas();
|
|
16583
|
-
init_mcp_response_formatter();
|
|
16584
|
-
}
|
|
16585
|
-
});
|
|
16586
|
-
|
|
16587
|
-
// src/mcp/http-mcp-tool-executor.ts
|
|
16588
|
-
var HttpMcpToolExecutor;
|
|
16589
|
-
var init_http_mcp_tool_executor = __esm({
|
|
16590
|
-
"src/mcp/http-mcp-tool-executor.ts"() {
|
|
16165
|
+
// src/mcp/mcp-tool-schemas.ts
|
|
16166
|
+
var import_zod19, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, NullableString, MapsSearchOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, CreditsInfoInputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
|
|
16167
|
+
var init_mcp_tool_schemas = __esm({
|
|
16168
|
+
"src/mcp/mcp-tool-schemas.ts"() {
|
|
16591
16169
|
"use strict";
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16170
|
+
import_zod19 = require("zod");
|
|
16171
|
+
HarvestPaaInputSchema = {
|
|
16172
|
+
query: import_zod19.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
|
|
16173
|
+
location: import_zod19.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
|
|
16174
|
+
maxQuestions: import_zod19.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
|
|
16175
|
+
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
|
|
16176
|
+
hl: import_zod19.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
|
|
16177
|
+
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
16178
|
+
proxyMode: import_zod19.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default so city/state searches create or reuse a matching residential proxy. Use configured for the static configured proxy. Use none only for direct-network debugging."),
|
|
16179
|
+
proxyZip: import_zod19.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only when the user gives a specific ZIP or city-center proxy targeting needs to be forced."),
|
|
16180
|
+
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
|
|
16181
|
+
};
|
|
16182
|
+
ExtractUrlInputSchema = {
|
|
16183
|
+
url: import_zod19.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
|
|
16184
|
+
screenshot: import_zod19.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
|
|
16185
|
+
screenshotDevice: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
|
|
16186
|
+
extractBranding: import_zod19.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
|
|
16187
|
+
downloadMedia: import_zod19.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
|
|
16188
|
+
mediaTypes: import_zod19.z.array(import_zod19.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
|
|
16189
|
+
allowLocal: import_zod19.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
|
|
16190
|
+
};
|
|
16191
|
+
MapSiteUrlsInputSchema = {
|
|
16192
|
+
url: import_zod19.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
16193
|
+
maxUrls: import_zod19.z.number().int().min(1).max(500).optional().describe("Maximum URLs to discover. Use 100 for normal maps, higher when the user asks for a full inventory.")
|
|
16194
|
+
};
|
|
16195
|
+
ExtractSiteInputSchema = {
|
|
16196
|
+
url: import_zod19.z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
|
|
16197
|
+
maxPages: import_zod19.z.number().int().min(1).max(50).optional().describe("Maximum pages to extract. Use 50 when the user asks for full results or a complete crawl within MCP limits.")
|
|
16198
|
+
};
|
|
16199
|
+
YoutubeHarvestInputSchema = {
|
|
16200
|
+
mode: import_zod19.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
16201
|
+
query: import_zod19.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
|
|
16202
|
+
channelHandle: import_zod19.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
|
|
16203
|
+
maxVideos: import_zod19.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50. Increase when user asks for full channel/history.")
|
|
16204
|
+
};
|
|
16205
|
+
YoutubeTranscribeInputSchema = {
|
|
16206
|
+
videoId: import_zod19.z.string().min(1).describe("YouTube video ID, e.g. dQw4w9WgXcQ")
|
|
16207
|
+
};
|
|
16208
|
+
FacebookPageIntelInputSchema = {
|
|
16209
|
+
pageId: import_zod19.z.string().optional(),
|
|
16210
|
+
libraryId: import_zod19.z.string().optional(),
|
|
16211
|
+
query: import_zod19.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
|
|
16212
|
+
maxAds: import_zod19.z.number().int().min(1).max(200).default(50),
|
|
16213
|
+
country: import_zod19.z.string().length(2).default("US")
|
|
16214
|
+
};
|
|
16215
|
+
FacebookAdSearchInputSchema = {
|
|
16216
|
+
query: import_zod19.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
|
|
16217
|
+
country: import_zod19.z.string().length(2).default("US"),
|
|
16218
|
+
maxResults: import_zod19.z.number().int().min(1).max(20).default(10)
|
|
16219
|
+
};
|
|
16220
|
+
FacebookAdTranscribeInputSchema = {
|
|
16221
|
+
videoUrl: import_zod19.z.string().url().describe("Facebook CDN video URL from a facebook_page_intel result")
|
|
16222
|
+
};
|
|
16223
|
+
MapsPlaceIntelInputSchema = {
|
|
16224
|
+
businessName: import_zod19.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
|
|
16225
|
+
location: import_zod19.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.'),
|
|
16226
|
+
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
16227
|
+
hl: import_zod19.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
16228
|
+
includeReviews: import_zod19.z.boolean().default(false).describe("Whether to fetch individual review cards"),
|
|
16229
|
+
maxReviews: import_zod19.z.number().int().min(1).max(500).default(50).describe("Max review cards to return (requires includeReviews: true)")
|
|
16230
|
+
};
|
|
16231
|
+
MapsSearchInputSchema = {
|
|
16232
|
+
query: import_zod19.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
|
|
16233
|
+
location: import_zod19.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
|
|
16234
|
+
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
16235
|
+
hl: import_zod19.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
16236
|
+
maxResults: import_zod19.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more.")
|
|
16237
|
+
};
|
|
16238
|
+
NullableString = import_zod19.z.string().nullable();
|
|
16239
|
+
MapsSearchOutputSchema = {
|
|
16240
|
+
query: import_zod19.z.string(),
|
|
16241
|
+
location: import_zod19.z.string().nullable(),
|
|
16242
|
+
searchQuery: import_zod19.z.string(),
|
|
16243
|
+
searchUrl: import_zod19.z.string().url(),
|
|
16244
|
+
extractedAt: import_zod19.z.string(),
|
|
16245
|
+
requestedMaxResults: import_zod19.z.number().int().min(1).max(50),
|
|
16246
|
+
resultCount: import_zod19.z.number().int().min(0).max(50),
|
|
16247
|
+
results: import_zod19.z.array(import_zod19.z.object({
|
|
16248
|
+
position: import_zod19.z.number().int().min(1),
|
|
16249
|
+
name: import_zod19.z.string(),
|
|
16250
|
+
placeUrl: import_zod19.z.string().url(),
|
|
16251
|
+
cid: NullableString,
|
|
16252
|
+
cidDecimal: NullableString,
|
|
16253
|
+
rating: NullableString,
|
|
16254
|
+
reviewCount: NullableString,
|
|
16255
|
+
category: NullableString,
|
|
16256
|
+
address: NullableString,
|
|
16257
|
+
websiteUrl: NullableString,
|
|
16258
|
+
directionsUrl: NullableString,
|
|
16259
|
+
metadata: import_zod19.z.array(import_zod19.z.string())
|
|
16260
|
+
})),
|
|
16261
|
+
durationMs: import_zod19.z.number().int().min(0)
|
|
16262
|
+
};
|
|
16263
|
+
OrganicResultOutput = import_zod19.z.object({
|
|
16264
|
+
position: import_zod19.z.number().int(),
|
|
16265
|
+
title: import_zod19.z.string(),
|
|
16266
|
+
url: import_zod19.z.string(),
|
|
16267
|
+
domain: import_zod19.z.string(),
|
|
16268
|
+
snippet: NullableString
|
|
16269
|
+
});
|
|
16270
|
+
AiOverviewOutput = import_zod19.z.object({
|
|
16271
|
+
detected: import_zod19.z.boolean(),
|
|
16272
|
+
text: NullableString
|
|
16273
|
+
}).nullable();
|
|
16274
|
+
EntityIdsOutput = import_zod19.z.object({
|
|
16275
|
+
kgIds: import_zod19.z.array(import_zod19.z.string()),
|
|
16276
|
+
cids: import_zod19.z.array(import_zod19.z.string()),
|
|
16277
|
+
gcids: import_zod19.z.array(import_zod19.z.string())
|
|
16278
|
+
}).nullable();
|
|
16279
|
+
HarvestPaaOutputSchema = {
|
|
16280
|
+
query: import_zod19.z.string(),
|
|
16281
|
+
location: NullableString,
|
|
16282
|
+
questionCount: import_zod19.z.number().int().min(0),
|
|
16283
|
+
completionStatus: NullableString,
|
|
16284
|
+
questions: import_zod19.z.array(import_zod19.z.object({
|
|
16285
|
+
question: import_zod19.z.string(),
|
|
16286
|
+
answer: NullableString,
|
|
16287
|
+
sourceTitle: NullableString,
|
|
16288
|
+
sourceSite: NullableString
|
|
16289
|
+
})),
|
|
16290
|
+
organicResults: import_zod19.z.array(OrganicResultOutput),
|
|
16291
|
+
aiOverview: AiOverviewOutput,
|
|
16292
|
+
entityIds: EntityIdsOutput,
|
|
16293
|
+
durationMs: import_zod19.z.number().min(0).nullable()
|
|
16294
|
+
};
|
|
16295
|
+
SearchSerpOutputSchema = {
|
|
16296
|
+
query: import_zod19.z.string(),
|
|
16297
|
+
location: NullableString,
|
|
16298
|
+
organicResults: import_zod19.z.array(OrganicResultOutput),
|
|
16299
|
+
localPack: import_zod19.z.array(import_zod19.z.object({
|
|
16300
|
+
position: import_zod19.z.number().int(),
|
|
16301
|
+
name: import_zod19.z.string(),
|
|
16302
|
+
rating: NullableString,
|
|
16303
|
+
reviewCount: NullableString,
|
|
16304
|
+
websiteUrl: NullableString
|
|
16305
|
+
})),
|
|
16306
|
+
aiOverview: AiOverviewOutput,
|
|
16307
|
+
entityIds: EntityIdsOutput
|
|
16308
|
+
};
|
|
16309
|
+
ExtractUrlOutputSchema = {
|
|
16310
|
+
url: import_zod19.z.string(),
|
|
16311
|
+
title: NullableString,
|
|
16312
|
+
headings: import_zod19.z.array(import_zod19.z.object({
|
|
16313
|
+
level: import_zod19.z.number().int(),
|
|
16314
|
+
text: import_zod19.z.string()
|
|
16315
|
+
})),
|
|
16316
|
+
schemaBlockCount: import_zod19.z.number().int().min(0),
|
|
16317
|
+
entityName: NullableString,
|
|
16318
|
+
entityTypes: import_zod19.z.array(import_zod19.z.string()),
|
|
16319
|
+
napScore: import_zod19.z.number().nullable(),
|
|
16320
|
+
missingSchemaFields: import_zod19.z.array(import_zod19.z.string()),
|
|
16321
|
+
screenshotSaved: NullableString
|
|
16322
|
+
};
|
|
16323
|
+
ExtractSiteOutputSchema = {
|
|
16324
|
+
url: import_zod19.z.string(),
|
|
16325
|
+
pageCount: import_zod19.z.number().int().min(0),
|
|
16326
|
+
pages: import_zod19.z.array(import_zod19.z.object({
|
|
16327
|
+
url: import_zod19.z.string(),
|
|
16328
|
+
title: NullableString,
|
|
16329
|
+
schemaTypes: import_zod19.z.array(import_zod19.z.string())
|
|
16330
|
+
})),
|
|
16331
|
+
durationMs: import_zod19.z.number().min(0)
|
|
16332
|
+
};
|
|
16333
|
+
MapsPlaceIntelOutputSchema = {
|
|
16334
|
+
name: import_zod19.z.string(),
|
|
16335
|
+
rating: NullableString,
|
|
16336
|
+
reviewCount: NullableString,
|
|
16337
|
+
category: NullableString,
|
|
16338
|
+
address: NullableString,
|
|
16339
|
+
phone: NullableString,
|
|
16340
|
+
website: NullableString,
|
|
16341
|
+
hoursSummary: NullableString,
|
|
16342
|
+
bookingUrl: NullableString,
|
|
16343
|
+
kgmid: NullableString,
|
|
16344
|
+
cidDecimal: NullableString,
|
|
16345
|
+
cidUrl: NullableString,
|
|
16346
|
+
lat: import_zod19.z.number().nullable(),
|
|
16347
|
+
lng: import_zod19.z.number().nullable(),
|
|
16348
|
+
reviewsStatus: import_zod19.z.string(),
|
|
16349
|
+
reviewsCollected: import_zod19.z.number().int().min(0),
|
|
16350
|
+
reviewTopics: import_zod19.z.array(import_zod19.z.object({
|
|
16351
|
+
label: import_zod19.z.string(),
|
|
16352
|
+
count: import_zod19.z.string()
|
|
16353
|
+
}))
|
|
16354
|
+
};
|
|
16355
|
+
CreditsInfoOutputSchema = {
|
|
16356
|
+
balanceCredits: import_zod19.z.number().nullable(),
|
|
16357
|
+
matchedCost: import_zod19.z.object({
|
|
16358
|
+
label: import_zod19.z.string(),
|
|
16359
|
+
credits: import_zod19.z.number(),
|
|
16360
|
+
unit: import_zod19.z.string(),
|
|
16361
|
+
notes: NullableString
|
|
16362
|
+
}).nullable(),
|
|
16363
|
+
costs: import_zod19.z.array(import_zod19.z.object({
|
|
16364
|
+
key: import_zod19.z.string(),
|
|
16365
|
+
label: import_zod19.z.string(),
|
|
16366
|
+
credits: import_zod19.z.number(),
|
|
16367
|
+
unit: import_zod19.z.string(),
|
|
16368
|
+
notes: NullableString
|
|
16369
|
+
})),
|
|
16370
|
+
ledger: import_zod19.z.array(import_zod19.z.object({
|
|
16371
|
+
createdAt: import_zod19.z.string(),
|
|
16372
|
+
operation: import_zod19.z.string(),
|
|
16373
|
+
credits: import_zod19.z.number(),
|
|
16374
|
+
description: NullableString
|
|
16375
|
+
}))
|
|
16376
|
+
};
|
|
16377
|
+
MapSiteUrlsOutputSchema = {
|
|
16378
|
+
startUrl: import_zod19.z.string(),
|
|
16379
|
+
totalFound: import_zod19.z.number().int().min(0),
|
|
16380
|
+
truncated: import_zod19.z.boolean(),
|
|
16381
|
+
okCount: import_zod19.z.number().int().min(0),
|
|
16382
|
+
redirectCount: import_zod19.z.number().int().min(0),
|
|
16383
|
+
brokenCount: import_zod19.z.number().int().min(0),
|
|
16384
|
+
urls: import_zod19.z.array(import_zod19.z.object({
|
|
16385
|
+
url: import_zod19.z.string(),
|
|
16386
|
+
status: import_zod19.z.number().int().nullable()
|
|
16387
|
+
})),
|
|
16388
|
+
durationMs: import_zod19.z.number().min(0)
|
|
16389
|
+
};
|
|
16390
|
+
YoutubeHarvestOutputSchema = {
|
|
16391
|
+
mode: import_zod19.z.string(),
|
|
16392
|
+
videoCount: import_zod19.z.number().int().min(0),
|
|
16393
|
+
channel: import_zod19.z.object({
|
|
16394
|
+
title: NullableString,
|
|
16395
|
+
subscriberCount: NullableString
|
|
16396
|
+
}).nullable(),
|
|
16397
|
+
videos: import_zod19.z.array(import_zod19.z.object({
|
|
16398
|
+
videoId: import_zod19.z.string(),
|
|
16399
|
+
title: import_zod19.z.string(),
|
|
16400
|
+
channelName: NullableString,
|
|
16401
|
+
views: NullableString,
|
|
16402
|
+
duration: NullableString,
|
|
16403
|
+
url: NullableString
|
|
16404
|
+
}))
|
|
16405
|
+
};
|
|
16406
|
+
FacebookAdSearchOutputSchema = {
|
|
16407
|
+
query: import_zod19.z.string(),
|
|
16408
|
+
advertiserCount: import_zod19.z.number().int().min(0),
|
|
16409
|
+
advertisers: import_zod19.z.array(import_zod19.z.object({
|
|
16410
|
+
name: NullableString,
|
|
16411
|
+
adCount: import_zod19.z.number().int().nullable(),
|
|
16412
|
+
libraryId: NullableString
|
|
16413
|
+
}))
|
|
16414
|
+
};
|
|
16415
|
+
FacebookPageIntelOutputSchema = {
|
|
16416
|
+
advertiserName: NullableString,
|
|
16417
|
+
totalAds: import_zod19.z.number().int().min(0),
|
|
16418
|
+
activeCount: import_zod19.z.number().int().min(0),
|
|
16419
|
+
videoCount: import_zod19.z.number().int().min(0),
|
|
16420
|
+
imageCount: import_zod19.z.number().int().min(0),
|
|
16421
|
+
ads: import_zod19.z.array(import_zod19.z.object({
|
|
16422
|
+
libraryId: NullableString,
|
|
16423
|
+
status: NullableString,
|
|
16424
|
+
creativeType: NullableString,
|
|
16425
|
+
headline: NullableString,
|
|
16426
|
+
cta: NullableString,
|
|
16427
|
+
startDate: NullableString,
|
|
16428
|
+
videoUrl: NullableString,
|
|
16429
|
+
variations: import_zod19.z.number().int().nullable()
|
|
16430
|
+
}))
|
|
16431
|
+
};
|
|
16432
|
+
CreditsInfoInputSchema = {
|
|
16433
|
+
item: import_zod19.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", or "YouTube transcription"'),
|
|
16434
|
+
includeLedger: import_zod19.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
|
|
16435
|
+
};
|
|
16436
|
+
SearchSerpInputSchema = {
|
|
16437
|
+
query: import_zod19.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
|
|
16438
|
+
location: import_zod19.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
|
|
16439
|
+
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
|
|
16440
|
+
hl: import_zod19.z.string().default("en").describe("Google interface/content language inferred from user request."),
|
|
16441
|
+
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
16442
|
+
proxyMode: import_zod19.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default so city/state searches create or reuse a matching residential proxy. Use configured for the static configured proxy. Use none only for direct-network debugging."),
|
|
16443
|
+
proxyZip: import_zod19.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only when the user gives a specific ZIP or city-center proxy targeting needs to be forced."),
|
|
16444
|
+
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
|
|
16445
|
+
pages: import_zod19.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
|
|
16446
|
+
};
|
|
16447
|
+
CaptureSerpSnapshotInputSchema = {
|
|
16448
|
+
query: import_zod19.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
|
|
16449
|
+
location: import_zod19.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
|
|
16450
|
+
gl: import_zod19.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
|
|
16451
|
+
hl: import_zod19.z.string().default("en").describe("Google interface/content language inferred from the user request."),
|
|
16452
|
+
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
|
|
16453
|
+
proxyMode: import_zod19.z.enum(["location", "configured", "none"]).default("location").describe("Proxy behavior for capture. Use location for localized residential proxy targeting, configured for the static residential proxy, and none only for direct-network debugging."),
|
|
16454
|
+
proxyZip: import_zod19.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting when a precise city-center or ZIP proxy is needed."),
|
|
16455
|
+
pages: import_zod19.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
|
|
16456
|
+
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
|
|
16457
|
+
includePageSnapshots: import_zod19.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
|
|
16458
|
+
pageSnapshotLimit: import_zod19.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
|
|
16459
|
+
};
|
|
16460
|
+
ScreenshotInputSchema = {
|
|
16461
|
+
url: import_zod19.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
|
|
16462
|
+
device: import_zod19.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport profile. desktop = 1440\xD7900. mobile = 390\xD7844. Use desktop by default; use mobile when the user asks for a mobile view."),
|
|
16463
|
+
allowLocal: import_zod19.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
|
|
16464
|
+
};
|
|
16465
|
+
CaptureSerpPageSnapshotsInputSchema = {
|
|
16466
|
+
urls: import_zod19.z.array(import_zod19.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
|
|
16467
|
+
targets: import_zod19.z.array(import_zod19.z.object({
|
|
16468
|
+
url: import_zod19.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
|
|
16469
|
+
sourceKind: import_zod19.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
|
|
16470
|
+
sourcePosition: import_zod19.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
|
|
16471
|
+
}).strict()).min(1).max(25).optional().describe("Structured page snapshot targets. Use this instead of urls when source kind or position should be preserved."),
|
|
16472
|
+
maxConcurrency: import_zod19.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
|
|
16473
|
+
timeoutMs: import_zod19.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
|
|
16474
|
+
debug: import_zod19.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
|
|
16475
|
+
};
|
|
16476
|
+
}
|
|
16477
|
+
});
|
|
16478
|
+
|
|
16479
|
+
// src/mcp/paa-mcp-server.ts
|
|
16480
|
+
function liveWebToolAnnotations(title) {
|
|
16481
|
+
return {
|
|
16482
|
+
title,
|
|
16483
|
+
readOnlyHint: true,
|
|
16484
|
+
destructiveHint: false,
|
|
16485
|
+
idempotentHint: false,
|
|
16486
|
+
openWorldHint: true
|
|
16487
|
+
};
|
|
16488
|
+
}
|
|
16489
|
+
function listSavedReports() {
|
|
16490
|
+
try {
|
|
16491
|
+
const dir = outputBaseDir();
|
|
16492
|
+
return (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs5.statSync)((0, import_node_path7.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
|
|
16493
|
+
} catch {
|
|
16494
|
+
return [];
|
|
16495
|
+
}
|
|
16496
|
+
}
|
|
16497
|
+
function registerSavedReportResources(server) {
|
|
16498
|
+
server.registerResource(
|
|
16499
|
+
"saved-report",
|
|
16500
|
+
new import_mcp.ResourceTemplate("report://{filename}", {
|
|
16501
|
+
list: () => ({
|
|
16502
|
+
resources: listSavedReports().map((r) => ({
|
|
16503
|
+
uri: `report://${encodeURIComponent(r.filename)}`,
|
|
16504
|
+
name: r.filename,
|
|
16505
|
+
mimeType: "text/markdown"
|
|
16506
|
+
}))
|
|
16507
|
+
})
|
|
16508
|
+
}),
|
|
16509
|
+
{
|
|
16510
|
+
title: "Saved MCP Scraper Reports",
|
|
16511
|
+
description: "Markdown research reports saved by previous MCP Scraper tool calls. Read a report to reuse prior research without re-scraping or spending credits.",
|
|
16512
|
+
mimeType: "text/markdown"
|
|
16513
|
+
},
|
|
16514
|
+
async (uri, variables) => {
|
|
16515
|
+
const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
|
|
16516
|
+
const filename = (0, import_node_path7.basename)(decodeURIComponent(String(requested ?? "")));
|
|
16517
|
+
if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
|
|
16518
|
+
const text = (0, import_node_fs5.readFileSync)((0, import_node_path7.join)(outputBaseDir(), filename), "utf8");
|
|
16519
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
|
|
16520
|
+
}
|
|
16521
|
+
);
|
|
16522
|
+
}
|
|
16523
|
+
function buildPaaExtractorMcpServer(executor, options = {}) {
|
|
16524
|
+
const savesReports = options.savesReportsLocally !== false;
|
|
16525
|
+
const reportNote = savesReports ? " Saves a full Markdown report locally." : " Reports are returned inline; no files are saved on this hosted endpoint.";
|
|
16526
|
+
const withReportNote = (description) => `${description}${reportNote}`;
|
|
16527
|
+
const server = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
16528
|
+
if (savesReports) registerSavedReportResources(server);
|
|
16529
|
+
server.registerTool("harvest_paa", {
|
|
16530
|
+
title: "Google PAA + SERP Harvest",
|
|
16531
|
+
description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). Use maxQuestions 30 normally, 100-200 for "full", "deep", "all", or comprehensive research. Deep harvests above 100 questions can run for several minutes with no interim progress \u2014 warn the user before starting one and keep maxQuestions at or below 100 unless they explicitly want a deep harvest. Credits are charged by extracted question; unused request hold is refunded.'),
|
|
16532
|
+
inputSchema: HarvestPaaInputSchema,
|
|
16533
|
+
outputSchema: HarvestPaaOutputSchema,
|
|
16534
|
+
annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
|
|
16535
|
+
}, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
|
|
16536
|
+
server.registerTool("search_serp", {
|
|
16537
|
+
title: "Google SERP Lookup",
|
|
16538
|
+
description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request."),
|
|
16539
|
+
inputSchema: SearchSerpInputSchema,
|
|
16540
|
+
outputSchema: SearchSerpOutputSchema,
|
|
16541
|
+
annotations: liveWebToolAnnotations("Google SERP Lookup")
|
|
16542
|
+
}, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
|
|
16543
|
+
server.registerTool("extract_url", {
|
|
16544
|
+
title: "Single URL Extract",
|
|
16545
|
+
description: withReportNote("Extract structured data from one public URL: page content as Markdown, heading structure, JSON-LD schema, entity details, NAP score, metadata, and missing schema fields. Use when the user provides a single URL or asks to inspect/scrape one page."),
|
|
16546
|
+
inputSchema: ExtractUrlInputSchema,
|
|
16547
|
+
outputSchema: ExtractUrlOutputSchema,
|
|
16548
|
+
annotations: liveWebToolAnnotations("Single URL Extract")
|
|
16549
|
+
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
16550
|
+
server.registerTool("map_site_urls", {
|
|
16551
|
+
title: "Site URL Map",
|
|
16552
|
+
description: withReportNote("Map/crawl a public website to build a URL inventory with HTTP status codes, broken links, redirects, and site scope. Use before extract_site for audits or when the user asks for a sitemap/URL inventory."),
|
|
16553
|
+
inputSchema: MapSiteUrlsInputSchema,
|
|
16554
|
+
outputSchema: MapSiteUrlsOutputSchema,
|
|
16555
|
+
annotations: liveWebToolAnnotations("Site URL Map")
|
|
16556
|
+
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
16557
|
+
server.registerTool("extract_site", {
|
|
16558
|
+
title: "Multi-Page Site Extract",
|
|
16559
|
+
description: withReportNote("Run multi-page extraction across a public website. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use for website audits, competitor audits, and full-site extraction."),
|
|
16560
|
+
inputSchema: ExtractSiteInputSchema,
|
|
16561
|
+
outputSchema: ExtractSiteOutputSchema,
|
|
16562
|
+
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
16563
|
+
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
16564
|
+
server.registerTool("youtube_harvest", {
|
|
16565
|
+
title: "YouTube Video Harvest",
|
|
16566
|
+
description: withReportNote('Harvest YouTube video metadata by search query or channel handle/ID/URL. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, dates, durations, URLs, thumbnails, and videoIds for follow-up transcription.'),
|
|
16567
|
+
inputSchema: YoutubeHarvestInputSchema,
|
|
16568
|
+
outputSchema: YoutubeHarvestOutputSchema,
|
|
16569
|
+
annotations: liveWebToolAnnotations("YouTube Video Harvest")
|
|
16570
|
+
}, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
|
|
16571
|
+
server.registerTool("youtube_transcribe", {
|
|
16572
|
+
title: "YouTube Transcription",
|
|
16573
|
+
description: withReportNote("Fetch and transcribe captions from a YouTube video. Returns full transcript, timestamped chunks, and word count. Pass a videoId from youtube_harvest results or infer it from a YouTube URL if the user provided one."),
|
|
16574
|
+
inputSchema: YoutubeTranscribeInputSchema,
|
|
16575
|
+
annotations: liveWebToolAnnotations("YouTube Transcription")
|
|
16576
|
+
}, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
|
|
16577
|
+
server.registerTool("facebook_page_intel", {
|
|
16578
|
+
title: "Facebook Advertiser Ad Intel",
|
|
16579
|
+
description: withReportNote("Harvest ads from a Facebook advertiser. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and video URLs ready for transcription. Accepts pageId, libraryId, or a brand/advertiser name as query. Use after facebook_ad_search when possible."),
|
|
16580
|
+
inputSchema: FacebookPageIntelInputSchema,
|
|
16581
|
+
outputSchema: FacebookPageIntelOutputSchema,
|
|
16582
|
+
annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
|
|
16583
|
+
}, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
|
|
16584
|
+
server.registerTool("facebook_ad_search", {
|
|
16585
|
+
title: "Facebook Ad Library Search",
|
|
16586
|
+
description: withReportNote("Search Facebook Ad Library by brand, advertiser, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use to discover competitors, then pass libraryId to facebook_page_intel."),
|
|
16587
|
+
inputSchema: FacebookAdSearchInputSchema,
|
|
16588
|
+
outputSchema: FacebookAdSearchOutputSchema,
|
|
16589
|
+
annotations: liveWebToolAnnotations("Facebook Ad Library Search")
|
|
16590
|
+
}, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
|
|
16591
|
+
server.registerTool("facebook_ad_transcribe", {
|
|
16592
|
+
title: "Facebook Ad Transcription",
|
|
16593
|
+
description: "Transcribe audio from a Facebook ad video. Returns full transcript and timestamped chunks. Use the videoUrl value from facebook_page_intel results.",
|
|
16594
|
+
inputSchema: FacebookAdTranscribeInputSchema,
|
|
16595
|
+
annotations: liveWebToolAnnotations("Facebook Ad Transcription")
|
|
16596
|
+
}, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
|
|
16597
|
+
server.registerTool("maps_place_intel", {
|
|
16598
|
+
title: "Google Maps Business Profile Details",
|
|
16599
|
+
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.'),
|
|
16600
|
+
inputSchema: MapsPlaceIntelInputSchema,
|
|
16601
|
+
outputSchema: MapsPlaceIntelOutputSchema,
|
|
16602
|
+
annotations: liveWebToolAnnotations("Google Maps Business Profile Details")
|
|
16603
|
+
}, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
|
|
16604
|
+
server.registerTool("maps_search", {
|
|
16605
|
+
title: "Google Maps Business Search",
|
|
16606
|
+
description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, and profile metadata. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
|
|
16607
|
+
inputSchema: MapsSearchInputSchema,
|
|
16608
|
+
outputSchema: MapsSearchOutputSchema,
|
|
16609
|
+
annotations: liveWebToolAnnotations("Google Maps Business Search")
|
|
16610
|
+
}, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
|
|
16611
|
+
server.registerTool("credits_info", {
|
|
16612
|
+
title: "MCP Scraper Credits & Costs",
|
|
16613
|
+
description: "Answer questions about MCP Scraper credits: current credit balance, what a specific tool/action costs, the full cost table, and optionally recent credit ledger entries. Does not expose payment methods or credit card information.",
|
|
16614
|
+
inputSchema: CreditsInfoInputSchema,
|
|
16615
|
+
outputSchema: CreditsInfoOutputSchema,
|
|
16616
|
+
annotations: {
|
|
16617
|
+
title: "MCP Scraper Credits & Costs",
|
|
16618
|
+
readOnlyHint: true,
|
|
16619
|
+
destructiveHint: false,
|
|
16620
|
+
idempotentHint: true,
|
|
16621
|
+
openWorldHint: false
|
|
16622
|
+
}
|
|
16623
|
+
}, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
|
|
16624
|
+
return server;
|
|
16625
|
+
}
|
|
16626
|
+
var import_mcp, import_node_fs5, import_node_path7;
|
|
16627
|
+
var init_paa_mcp_server = __esm({
|
|
16628
|
+
"src/mcp/paa-mcp-server.ts"() {
|
|
16629
|
+
"use strict";
|
|
16630
|
+
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
16631
|
+
import_node_fs5 = require("fs");
|
|
16632
|
+
import_node_path7 = require("path");
|
|
16633
|
+
init_version();
|
|
16634
|
+
init_mcp_response_formatter();
|
|
16635
|
+
init_mcp_tool_schemas();
|
|
16636
|
+
init_mcp_response_formatter();
|
|
16637
|
+
}
|
|
16638
|
+
});
|
|
16639
|
+
|
|
16640
|
+
// src/mcp/http-mcp-tool-executor.ts
|
|
16641
|
+
var HttpMcpToolExecutor;
|
|
16642
|
+
var init_http_mcp_tool_executor = __esm({
|
|
16643
|
+
"src/mcp/http-mcp-tool-executor.ts"() {
|
|
16644
|
+
"use strict";
|
|
16645
|
+
init_harvest_timeout();
|
|
16646
|
+
HttpMcpToolExecutor = class {
|
|
16647
|
+
baseUrl;
|
|
16648
|
+
apiKey;
|
|
16649
|
+
timeoutMs;
|
|
16650
|
+
httpTimeoutOverrideMs;
|
|
16651
|
+
serpIntelligenceTimeoutMs;
|
|
16652
|
+
constructor(baseUrl, apiKey) {
|
|
16653
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
16654
|
+
this.apiKey = apiKey;
|
|
16655
|
+
const rawOverride = process.env.MCP_SCRAPER_HTTP_TIMEOUT_MS;
|
|
16656
|
+
const parsedOverride = rawOverride === void 0 ? NaN : Number(rawOverride);
|
|
16657
|
+
this.httpTimeoutOverrideMs = Number.isFinite(parsedOverride) && parsedOverride > 0 ? parsedOverride : null;
|
|
16658
|
+
this.timeoutMs = this.httpTimeoutOverrideMs ?? 11e4;
|
|
16659
|
+
const configuredSerpIntelligenceTimeoutMs = Number(process.env.MCP_SCRAPER_SERP_INTELLIGENCE_HTTP_TIMEOUT_MS ?? this.timeoutMs);
|
|
16660
|
+
this.serpIntelligenceTimeoutMs = Number.isFinite(configuredSerpIntelligenceTimeoutMs) && configuredSerpIntelligenceTimeoutMs > 0 ? configuredSerpIntelligenceTimeoutMs : this.timeoutMs;
|
|
16661
|
+
}
|
|
16662
|
+
async call(path6, body, timeoutMs = this.timeoutMs) {
|
|
16663
|
+
try {
|
|
16664
|
+
const res = await fetch(`${this.baseUrl}${path6}`, {
|
|
16665
|
+
method: "POST",
|
|
16666
|
+
headers: {
|
|
16667
|
+
"Content-Type": "application/json",
|
|
16668
|
+
"x-api-key": this.apiKey
|
|
16669
|
+
},
|
|
16670
|
+
body: JSON.stringify(body),
|
|
16671
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
16672
|
+
});
|
|
16673
|
+
const data = await res.json();
|
|
16674
|
+
if (!res.ok) {
|
|
16675
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }], isError: true };
|
|
16676
|
+
}
|
|
16677
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
16678
|
+
} catch (err) {
|
|
16679
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16680
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
16681
|
+
return {
|
|
16682
|
+
content: [{
|
|
16683
|
+
type: "text",
|
|
16684
|
+
text: JSON.stringify({
|
|
16685
|
+
error: "mcp_request_timeout",
|
|
16686
|
+
error_type: "timeout",
|
|
16687
|
+
retryable: true,
|
|
16688
|
+
path: path6,
|
|
16689
|
+
timeoutMs,
|
|
16690
|
+
message: `MCP Scraper request exceeded ${Math.round(timeoutMs / 1e3)}s and was cancelled. Retry with fewer results or use the async API for deep harvests.`
|
|
16691
|
+
})
|
|
16692
|
+
}],
|
|
16693
|
+
isError: true
|
|
16694
|
+
};
|
|
16695
|
+
}
|
|
16696
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
16697
|
+
}
|
|
16698
|
+
}
|
|
16699
|
+
harvestPaa(input) {
|
|
16700
|
+
const timeoutMs = this.httpTimeoutOverrideMs ?? harvestTimeoutBudget(input.maxQuestions ?? 30).clientMs;
|
|
16701
|
+
return this.call("/harvest/sync", input, timeoutMs);
|
|
16702
|
+
}
|
|
16703
|
+
searchSerp(input) {
|
|
16704
|
+
const timeoutMs = this.httpTimeoutOverrideMs ?? harvestTimeoutBudget(0, true).clientMs;
|
|
16705
|
+
return this.call("/harvest/sync", { ...input, serpOnly: true }, timeoutMs);
|
|
16706
|
+
}
|
|
16707
|
+
extractUrl(input) {
|
|
16708
|
+
return this.call("/extract-url", input);
|
|
16709
|
+
}
|
|
16710
|
+
mapSiteUrls(input) {
|
|
16658
16711
|
return this.call("/map-urls", input);
|
|
16659
16712
|
}
|
|
16660
16713
|
extractSite(input) {
|
|
@@ -16706,7 +16759,10 @@ function mcpAuthError() {
|
|
|
16706
16759
|
});
|
|
16707
16760
|
return new Response(body, {
|
|
16708
16761
|
status: 401,
|
|
16709
|
-
headers: {
|
|
16762
|
+
headers: {
|
|
16763
|
+
"Content-Type": "application/json",
|
|
16764
|
+
"WWW-Authenticate": 'Bearer realm="mcp-scraper", error="invalid_token", error_description="Pass an MCP Scraper API key as x-api-key or Bearer token"'
|
|
16765
|
+
}
|
|
16710
16766
|
});
|
|
16711
16767
|
}
|
|
16712
16768
|
async function requireMcpCallerKey(c) {
|
|
@@ -16768,17 +16824,802 @@ var init_mcp_routes = __esm({
|
|
|
16768
16824
|
}
|
|
16769
16825
|
});
|
|
16770
16826
|
|
|
16827
|
+
// src/api/browser-agent-db.ts
|
|
16828
|
+
async function migrateBrowserAgent() {
|
|
16829
|
+
if (_ready) return;
|
|
16830
|
+
const db = getDb();
|
|
16831
|
+
await db.execute(`
|
|
16832
|
+
CREATE TABLE IF NOT EXISTS browser_agent_sessions (
|
|
16833
|
+
id TEXT PRIMARY KEY,
|
|
16834
|
+
runtime_session_id TEXT NOT NULL,
|
|
16835
|
+
live_view_url TEXT,
|
|
16836
|
+
cdp_ws_url TEXT NOT NULL,
|
|
16837
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
16838
|
+
label TEXT,
|
|
16839
|
+
user_id INTEGER,
|
|
16840
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16841
|
+
closed_at TEXT,
|
|
16842
|
+
last_action_at TEXT,
|
|
16843
|
+
active_ms INTEGER NOT NULL DEFAULT 0,
|
|
16844
|
+
billed_mc INTEGER NOT NULL DEFAULT 0
|
|
16845
|
+
)
|
|
16846
|
+
`);
|
|
16847
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_sessions_status ON browser_agent_sessions(status)`);
|
|
16848
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_sessions_user ON browser_agent_sessions(user_id)`);
|
|
16849
|
+
await db.execute(`
|
|
16850
|
+
CREATE TABLE IF NOT EXISTS browser_agent_actions (
|
|
16851
|
+
id TEXT PRIMARY KEY,
|
|
16852
|
+
session_id TEXT NOT NULL,
|
|
16853
|
+
type TEXT NOT NULL,
|
|
16854
|
+
params_json TEXT,
|
|
16855
|
+
ok INTEGER NOT NULL DEFAULT 1,
|
|
16856
|
+
error TEXT,
|
|
16857
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
16858
|
+
)
|
|
16859
|
+
`);
|
|
16860
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_actions_session ON browser_agent_actions(session_id)`);
|
|
16861
|
+
await db.execute(`
|
|
16862
|
+
CREATE TABLE IF NOT EXISTS browser_agent_replays (
|
|
16863
|
+
replay_id TEXT PRIMARY KEY,
|
|
16864
|
+
session_id TEXT NOT NULL,
|
|
16865
|
+
view_url TEXT,
|
|
16866
|
+
label TEXT,
|
|
16867
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16868
|
+
stopped_at TEXT
|
|
16869
|
+
)
|
|
16870
|
+
`);
|
|
16871
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
|
|
16872
|
+
_ready = true;
|
|
16873
|
+
}
|
|
16874
|
+
async function createSessionRow(input) {
|
|
16875
|
+
const db = getDb();
|
|
16876
|
+
const id = `bas_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
16877
|
+
await db.execute({
|
|
16878
|
+
sql: `INSERT INTO browser_agent_sessions (id, runtime_session_id, live_view_url, cdp_ws_url, status, label, user_id, last_action_at)
|
|
16879
|
+
VALUES (?, ?, ?, ?, 'open', ?, ?, datetime('now'))`,
|
|
16880
|
+
args: [id, input.runtimeSessionId, input.liveViewUrl, input.cdpWsUrl, input.label, input.userId]
|
|
16881
|
+
});
|
|
16882
|
+
const row = await getSessionRow(id);
|
|
16883
|
+
if (!row) throw new Error("session insert failed");
|
|
16884
|
+
return row;
|
|
16885
|
+
}
|
|
16886
|
+
async function getSessionRow(id) {
|
|
16887
|
+
const db = getDb();
|
|
16888
|
+
const res = await db.execute({ sql: `SELECT * FROM browser_agent_sessions WHERE id = ?`, args: [id] });
|
|
16889
|
+
return res.rows[0] ?? null;
|
|
16890
|
+
}
|
|
16891
|
+
async function listSessionRows(userId, includeClosed = false) {
|
|
16892
|
+
const db = getDb();
|
|
16893
|
+
const clauses = [];
|
|
16894
|
+
const args = [];
|
|
16895
|
+
if (userId != null) {
|
|
16896
|
+
clauses.push("(user_id = ? OR user_id IS NULL)");
|
|
16897
|
+
args.push(userId);
|
|
16898
|
+
}
|
|
16899
|
+
if (!includeClosed) clauses.push(`status = 'open'`);
|
|
16900
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
16901
|
+
const res = await db.execute({
|
|
16902
|
+
sql: `SELECT * FROM browser_agent_sessions ${where} ORDER BY created_at DESC LIMIT 100`,
|
|
16903
|
+
args
|
|
16904
|
+
});
|
|
16905
|
+
return res.rows;
|
|
16906
|
+
}
|
|
16907
|
+
async function addActiveMs(id, deltaMs) {
|
|
16908
|
+
const db = getDb();
|
|
16909
|
+
await db.execute({
|
|
16910
|
+
sql: `UPDATE browser_agent_sessions SET active_ms = active_ms + ?, last_action_at = datetime('now') WHERE id = ?`,
|
|
16911
|
+
args: [Math.max(0, Math.round(deltaMs)), id]
|
|
16912
|
+
});
|
|
16913
|
+
const res = await db.execute({ sql: `SELECT active_ms, billed_mc FROM browser_agent_sessions WHERE id = ?`, args: [id] });
|
|
16914
|
+
const row = res.rows[0];
|
|
16915
|
+
return { active_ms: Number(row?.active_ms ?? 0), billed_mc: Number(row?.billed_mc ?? 0) };
|
|
16916
|
+
}
|
|
16917
|
+
async function setBilledMc(id, billedMc) {
|
|
16918
|
+
const db = getDb();
|
|
16919
|
+
await db.execute({
|
|
16920
|
+
sql: `UPDATE browser_agent_sessions SET billed_mc = ? WHERE id = ?`,
|
|
16921
|
+
args: [Math.round(billedMc), id]
|
|
16922
|
+
});
|
|
16923
|
+
}
|
|
16924
|
+
async function markSessionClosed(id) {
|
|
16925
|
+
const db = getDb();
|
|
16926
|
+
await db.execute({
|
|
16927
|
+
sql: `UPDATE browser_agent_sessions SET status = 'closed', closed_at = datetime('now') WHERE id = ?`,
|
|
16928
|
+
args: [id]
|
|
16929
|
+
});
|
|
16930
|
+
}
|
|
16931
|
+
async function recordAction(input) {
|
|
16932
|
+
const db = getDb();
|
|
16933
|
+
await db.execute({
|
|
16934
|
+
sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
|
|
16935
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
16936
|
+
args: [
|
|
16937
|
+
`baa_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 20)}`,
|
|
16938
|
+
input.sessionId,
|
|
16939
|
+
input.type,
|
|
16940
|
+
input.params == null ? null : JSON.stringify(input.params),
|
|
16941
|
+
input.ok ? 1 : 0,
|
|
16942
|
+
input.error ?? null
|
|
16943
|
+
]
|
|
16944
|
+
});
|
|
16945
|
+
}
|
|
16946
|
+
async function recordReplayStart(input) {
|
|
16947
|
+
const db = getDb();
|
|
16948
|
+
await db.execute({
|
|
16949
|
+
sql: `INSERT INTO browser_agent_replays (replay_id, session_id, view_url, label)
|
|
16950
|
+
VALUES (?, ?, ?, ?)`,
|
|
16951
|
+
args: [input.replayId, input.sessionId, input.viewUrl, input.label]
|
|
16952
|
+
});
|
|
16953
|
+
}
|
|
16954
|
+
async function recordReplayStop(replayId, viewUrl) {
|
|
16955
|
+
const db = getDb();
|
|
16956
|
+
await db.execute({
|
|
16957
|
+
sql: `UPDATE browser_agent_replays SET stopped_at = datetime('now'), view_url = COALESCE(?, view_url) WHERE replay_id = ?`,
|
|
16958
|
+
args: [viewUrl, replayId]
|
|
16959
|
+
});
|
|
16960
|
+
}
|
|
16961
|
+
async function listReplayRows(sessionId) {
|
|
16962
|
+
const db = getDb();
|
|
16963
|
+
const res = await db.execute({
|
|
16964
|
+
sql: `SELECT * FROM browser_agent_replays WHERE session_id = ? ORDER BY started_at DESC`,
|
|
16965
|
+
args: [sessionId]
|
|
16966
|
+
});
|
|
16967
|
+
return res.rows;
|
|
16968
|
+
}
|
|
16969
|
+
var import_node_crypto3, _ready;
|
|
16970
|
+
var init_browser_agent_db = __esm({
|
|
16971
|
+
"src/api/browser-agent-db.ts"() {
|
|
16972
|
+
"use strict";
|
|
16973
|
+
import_node_crypto3 = require("crypto");
|
|
16974
|
+
init_db();
|
|
16975
|
+
_ready = false;
|
|
16976
|
+
}
|
|
16977
|
+
});
|
|
16978
|
+
|
|
16979
|
+
// src/services/browser-agent/browser-agent-service.ts
|
|
16980
|
+
function client() {
|
|
16981
|
+
const apiKey = browserServiceApiKey();
|
|
16982
|
+
if (!apiKey) throw new Error("Browser backend API key is required");
|
|
16983
|
+
return new import_sdk6.default({ apiKey });
|
|
16984
|
+
}
|
|
16985
|
+
async function createSession(opts = {}) {
|
|
16986
|
+
const k = client();
|
|
16987
|
+
const resolvedProxyId = opts.proxyId ?? browserServiceProxyId();
|
|
16988
|
+
const browser = await k.browsers.create({
|
|
16989
|
+
stealth: opts.stealth ?? true,
|
|
16990
|
+
timeout_seconds: opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
|
|
16991
|
+
...resolvedProxyId ? { proxy_id: resolvedProxyId } : {},
|
|
16992
|
+
...opts.profileName ? { profile: { name: opts.profileName } } : {}
|
|
16993
|
+
});
|
|
16994
|
+
const runtimeSessionId = browser.session_id;
|
|
16995
|
+
if (opts.disableDefaultProxy) {
|
|
16996
|
+
try {
|
|
16997
|
+
await k.browsers.update(runtimeSessionId, { disable_default_proxy: true });
|
|
16998
|
+
} catch {
|
|
16999
|
+
}
|
|
17000
|
+
}
|
|
17001
|
+
if (opts.viewport) {
|
|
17002
|
+
try {
|
|
17003
|
+
await k.browsers.update(runtimeSessionId, { viewport: opts.viewport });
|
|
17004
|
+
} catch {
|
|
17005
|
+
}
|
|
17006
|
+
}
|
|
17007
|
+
return {
|
|
17008
|
+
runtimeSessionId,
|
|
17009
|
+
liveViewUrl: browser.browser_live_view_url ?? null,
|
|
17010
|
+
cdpWsUrl: browser.cdp_ws_url
|
|
17011
|
+
};
|
|
17012
|
+
}
|
|
17013
|
+
async function closeSession(runtimeSessionId) {
|
|
17014
|
+
const k = client();
|
|
17015
|
+
await k.browsers.deleteByID(runtimeSessionId);
|
|
17016
|
+
}
|
|
17017
|
+
async function screenshot(runtimeSessionId) {
|
|
17018
|
+
const k = client();
|
|
17019
|
+
const res = await k.browsers.computer.captureScreenshot(runtimeSessionId);
|
|
17020
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
17021
|
+
return { base64: buf.toString("base64"), mimeType: "image/png" };
|
|
17022
|
+
}
|
|
17023
|
+
async function click(runtimeSessionId, x, y, opts = {}) {
|
|
17024
|
+
const k = client();
|
|
17025
|
+
await k.browsers.computer.clickMouse(runtimeSessionId, {
|
|
17026
|
+
x,
|
|
17027
|
+
y,
|
|
17028
|
+
button: opts.button ?? "left",
|
|
17029
|
+
...opts.numClicks ? { num_clicks: opts.numClicks } : {}
|
|
17030
|
+
});
|
|
17031
|
+
}
|
|
17032
|
+
async function typeText(runtimeSessionId, text, delayMs) {
|
|
17033
|
+
const k = client();
|
|
17034
|
+
await k.browsers.computer.typeText(runtimeSessionId, {
|
|
17035
|
+
text,
|
|
17036
|
+
...typeof delayMs === "number" ? { delay: delayMs } : {}
|
|
17037
|
+
});
|
|
17038
|
+
}
|
|
17039
|
+
async function scroll(runtimeSessionId, x, y, deltaX, deltaY) {
|
|
17040
|
+
const k = client();
|
|
17041
|
+
await k.browsers.computer.scroll(runtimeSessionId, { x, y, delta_x: deltaX, delta_y: deltaY });
|
|
17042
|
+
}
|
|
17043
|
+
async function pressKeys(runtimeSessionId, keys) {
|
|
17044
|
+
const k = client();
|
|
17045
|
+
await k.browsers.computer.pressKey(runtimeSessionId, { keys });
|
|
17046
|
+
}
|
|
17047
|
+
async function goto(cdpWsUrl, url) {
|
|
17048
|
+
const browser = await import_playwright4.chromium.connectOverCDP(cdpWsUrl);
|
|
17049
|
+
try {
|
|
17050
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
17051
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
17052
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
17053
|
+
return { url: page.url(), title: await page.title() };
|
|
17054
|
+
} finally {
|
|
17055
|
+
await browser.close().catch(() => {
|
|
17056
|
+
});
|
|
17057
|
+
}
|
|
17058
|
+
}
|
|
17059
|
+
async function readPage(cdpWsUrl) {
|
|
17060
|
+
const browser = await import_playwright4.chromium.connectOverCDP(cdpWsUrl);
|
|
17061
|
+
try {
|
|
17062
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
17063
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
17064
|
+
const url = page.url();
|
|
17065
|
+
const title = await page.title().catch(() => "");
|
|
17066
|
+
const data = await page.evaluate(() => {
|
|
17067
|
+
const SEL = 'a[href], button, input, textarea, select, [role="button"], [role="link"], [role="tab"], [onclick]';
|
|
17068
|
+
const out = [];
|
|
17069
|
+
const nodes = Array.from(document.querySelectorAll(SEL)).slice(0, 120);
|
|
17070
|
+
for (const el2 of nodes) {
|
|
17071
|
+
const r = el2.getBoundingClientRect();
|
|
17072
|
+
if (r.width < 1 || r.height < 1) continue;
|
|
17073
|
+
if (r.bottom < 0 || r.top > window.innerHeight) continue;
|
|
17074
|
+
const e = el2;
|
|
17075
|
+
const name = (e.getAttribute("aria-label") || e.placeholder || e.innerText || e.getAttribute("value") || e.getAttribute("title") || "").trim().replace(/\s+/g, " ").slice(0, 80);
|
|
17076
|
+
out.push({
|
|
17077
|
+
role: el2.getAttribute("role") || el2.tagName.toLowerCase(),
|
|
17078
|
+
name,
|
|
17079
|
+
x: Math.round(r.left + r.width / 2),
|
|
17080
|
+
y: Math.round(r.top + r.height / 2)
|
|
17081
|
+
});
|
|
17082
|
+
}
|
|
17083
|
+
const text = (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").trim().slice(0, 6e3);
|
|
17084
|
+
return { text, els: out };
|
|
17085
|
+
});
|
|
17086
|
+
return {
|
|
17087
|
+
url,
|
|
17088
|
+
title,
|
|
17089
|
+
text: data.text,
|
|
17090
|
+
elements: data.els.map((e, i) => ({ ref: i + 1, role: e.role, name: e.name, x: e.x, y: e.y }))
|
|
17091
|
+
};
|
|
17092
|
+
} finally {
|
|
17093
|
+
await browser.close().catch(() => {
|
|
17094
|
+
});
|
|
17095
|
+
}
|
|
17096
|
+
}
|
|
17097
|
+
async function replayStart(runtimeSessionId) {
|
|
17098
|
+
const k = client();
|
|
17099
|
+
const res = await k.browsers.replays.start(runtimeSessionId);
|
|
17100
|
+
return { replayId: res.replay_id, viewUrl: res.replay_view_url ?? null };
|
|
17101
|
+
}
|
|
17102
|
+
async function replayStop(runtimeSessionId, replayId) {
|
|
17103
|
+
const k = client();
|
|
17104
|
+
await k.browsers.replays.stop(replayId, { id: runtimeSessionId });
|
|
17105
|
+
}
|
|
17106
|
+
async function replayList(runtimeSessionId) {
|
|
17107
|
+
const k = client();
|
|
17108
|
+
const res = await k.browsers.replays.list(runtimeSessionId);
|
|
17109
|
+
return res.map((r) => ({
|
|
17110
|
+
replayId: r.replay_id,
|
|
17111
|
+
viewUrl: r.replay_view_url ?? null,
|
|
17112
|
+
startedAt: r.started_at ?? null,
|
|
17113
|
+
finishedAt: r.finished_at ?? null
|
|
17114
|
+
}));
|
|
17115
|
+
}
|
|
17116
|
+
var import_sdk6, import_playwright4, DEFAULT_TIMEOUT_SECONDS;
|
|
17117
|
+
var init_browser_agent_service = __esm({
|
|
17118
|
+
"src/services/browser-agent/browser-agent-service.ts"() {
|
|
17119
|
+
"use strict";
|
|
17120
|
+
import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
|
|
17121
|
+
import_playwright4 = require("playwright");
|
|
17122
|
+
init_browser_service_env();
|
|
17123
|
+
DEFAULT_TIMEOUT_SECONDS = 600;
|
|
17124
|
+
}
|
|
17125
|
+
});
|
|
17126
|
+
|
|
17127
|
+
// src/api/browser-agent-routes.ts
|
|
17128
|
+
async function charge(sessionId, userId, startedAtMs) {
|
|
17129
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
17130
|
+
const { active_ms, billed_mc } = await addActiveMs(sessionId, elapsedMs);
|
|
17131
|
+
const owed = browserActiveCostMc(active_ms);
|
|
17132
|
+
const delta = owed - billed_mc;
|
|
17133
|
+
if (delta > 0) {
|
|
17134
|
+
const res = await debitMc(userId, delta, LedgerOperation.BROWSER_SESSION, sessionId);
|
|
17135
|
+
if (res.ok) await setBilledMc(sessionId, owed);
|
|
17136
|
+
}
|
|
17137
|
+
}
|
|
17138
|
+
function publicSession(row) {
|
|
17139
|
+
return {
|
|
17140
|
+
session_id: row.id,
|
|
17141
|
+
status: row.status,
|
|
17142
|
+
label: row.label,
|
|
17143
|
+
created_at: row.created_at,
|
|
17144
|
+
last_action_at: row.last_action_at,
|
|
17145
|
+
closed_at: row.closed_at,
|
|
17146
|
+
active_seconds: Math.round((row.active_ms ?? 0) / 1e3),
|
|
17147
|
+
credits_used: Math.round((row.billed_mc ?? 0) / 10) / 100
|
|
17148
|
+
};
|
|
17149
|
+
}
|
|
17150
|
+
function failure(err) {
|
|
17151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17152
|
+
return { error: sanitizeVendorName(msg) };
|
|
17153
|
+
}
|
|
17154
|
+
async function loadOpenSession(id, userId) {
|
|
17155
|
+
const row = await getSessionRow(id);
|
|
17156
|
+
if (!row) return null;
|
|
17157
|
+
if (row.user_id != null && row.user_id !== userId) return null;
|
|
17158
|
+
return row;
|
|
17159
|
+
}
|
|
17160
|
+
function buildBrowserAgentRoutes() {
|
|
17161
|
+
const app2 = new import_hono8.Hono();
|
|
17162
|
+
app2.use("*", async (c, next) => {
|
|
17163
|
+
await migrateBrowserAgent();
|
|
17164
|
+
return next();
|
|
17165
|
+
});
|
|
17166
|
+
app2.use("*", auth);
|
|
17167
|
+
app2.post("/sessions", async (c) => {
|
|
17168
|
+
const user = c.get("user");
|
|
17169
|
+
if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
|
|
17170
|
+
return c.json(insufficientBalanceResponse(Number(user.balance_mc ?? 0), BROWSER_OPEN_MIN_BALANCE_MC), 402);
|
|
17171
|
+
}
|
|
17172
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17173
|
+
try {
|
|
17174
|
+
const created = await createSession({
|
|
17175
|
+
timeoutSeconds: typeof body.timeout_seconds === "number" ? body.timeout_seconds : void 0,
|
|
17176
|
+
proxyId: typeof body.proxy_id === "string" ? body.proxy_id : void 0,
|
|
17177
|
+
profileName: typeof body.profile === "string" ? body.profile : void 0,
|
|
17178
|
+
disableDefaultProxy: body.disable_default_proxy === true,
|
|
17179
|
+
viewport: body.viewport && typeof body.viewport === "object" ? body.viewport : void 0
|
|
17180
|
+
});
|
|
17181
|
+
const row = await createSessionRow({
|
|
17182
|
+
runtimeSessionId: created.runtimeSessionId,
|
|
17183
|
+
liveViewUrl: created.liveViewUrl,
|
|
17184
|
+
cdpWsUrl: created.cdpWsUrl,
|
|
17185
|
+
label: typeof body.label === "string" ? body.label : null,
|
|
17186
|
+
userId: user.id
|
|
17187
|
+
});
|
|
17188
|
+
return c.json({ ...publicSession(row), watch_url: `/console/${row.id}` });
|
|
17189
|
+
} catch (err) {
|
|
17190
|
+
return c.json(failure(err), 502);
|
|
17191
|
+
}
|
|
17192
|
+
});
|
|
17193
|
+
app2.get("/sessions", async (c) => {
|
|
17194
|
+
const user = c.get("user");
|
|
17195
|
+
const includeClosed = c.req.query("all") === "1";
|
|
17196
|
+
const rows = await listSessionRows(user.id, includeClosed);
|
|
17197
|
+
return c.json({ sessions: rows.map(publicSession) });
|
|
17198
|
+
});
|
|
17199
|
+
app2.get("/sessions/:id", async (c) => {
|
|
17200
|
+
const user = c.get("user");
|
|
17201
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17202
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17203
|
+
return c.json({ ...publicSession(row), watch_url: `/console/${row.id}` });
|
|
17204
|
+
});
|
|
17205
|
+
app2.get("/sessions/:id/live-view", async (c) => {
|
|
17206
|
+
const user = c.get("user");
|
|
17207
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17208
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17209
|
+
return c.json({ live_view_url: row.live_view_url });
|
|
17210
|
+
});
|
|
17211
|
+
app2.delete("/sessions/:id", async (c) => {
|
|
17212
|
+
const user = c.get("user");
|
|
17213
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17214
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17215
|
+
try {
|
|
17216
|
+
await closeSession(row.runtime_session_id);
|
|
17217
|
+
} catch {
|
|
17218
|
+
}
|
|
17219
|
+
await markSessionClosed(row.id);
|
|
17220
|
+
return c.json({ ok: true });
|
|
17221
|
+
});
|
|
17222
|
+
app2.post("/sessions/:id/goto", async (c) => {
|
|
17223
|
+
const user = c.get("user");
|
|
17224
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17225
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17226
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17227
|
+
const url = typeof body.url === "string" ? body.url : "";
|
|
17228
|
+
if (!url) return c.json({ error: "url is required" }, 400);
|
|
17229
|
+
const t0 = Date.now();
|
|
17230
|
+
try {
|
|
17231
|
+
const result = await goto(row.cdp_ws_url, url);
|
|
17232
|
+
await charge(row.id, user.id, t0);
|
|
17233
|
+
await recordAction({ sessionId: row.id, type: "goto", params: { url }, ok: true });
|
|
17234
|
+
return c.json(result);
|
|
17235
|
+
} catch (err) {
|
|
17236
|
+
await charge(row.id, user.id, t0);
|
|
17237
|
+
await recordAction({ sessionId: row.id, type: "goto", params: { url }, ok: false, error: String(err) });
|
|
17238
|
+
return c.json(failure(err), 502);
|
|
17239
|
+
}
|
|
17240
|
+
});
|
|
17241
|
+
app2.post("/sessions/:id/screenshot", async (c) => {
|
|
17242
|
+
const user = c.get("user");
|
|
17243
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17244
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17245
|
+
const t0 = Date.now();
|
|
17246
|
+
try {
|
|
17247
|
+
const shot = await screenshot(row.runtime_session_id);
|
|
17248
|
+
let page = null;
|
|
17249
|
+
try {
|
|
17250
|
+
page = await readPage(row.cdp_ws_url);
|
|
17251
|
+
} catch {
|
|
17252
|
+
page = null;
|
|
17253
|
+
}
|
|
17254
|
+
await charge(row.id, user.id, t0);
|
|
17255
|
+
await recordAction({ sessionId: row.id, type: "screenshot", params: null, ok: true });
|
|
17256
|
+
return c.json({
|
|
17257
|
+
image_base64: shot.base64,
|
|
17258
|
+
mime_type: shot.mimeType,
|
|
17259
|
+
url: page?.url ?? null,
|
|
17260
|
+
title: page?.title ?? null,
|
|
17261
|
+
elements: page?.elements ?? [],
|
|
17262
|
+
text: page?.text ?? null
|
|
17263
|
+
});
|
|
17264
|
+
} catch (err) {
|
|
17265
|
+
await charge(row.id, user.id, t0);
|
|
17266
|
+
await recordAction({ sessionId: row.id, type: "screenshot", params: null, ok: false, error: String(err) });
|
|
17267
|
+
return c.json(failure(err), 502);
|
|
17268
|
+
}
|
|
17269
|
+
});
|
|
17270
|
+
app2.post("/sessions/:id/read", async (c) => {
|
|
17271
|
+
const user = c.get("user");
|
|
17272
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17273
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17274
|
+
const t0 = Date.now();
|
|
17275
|
+
try {
|
|
17276
|
+
const page = await readPage(row.cdp_ws_url);
|
|
17277
|
+
await charge(row.id, user.id, t0);
|
|
17278
|
+
await recordAction({ sessionId: row.id, type: "read", params: null, ok: true });
|
|
17279
|
+
return c.json(page);
|
|
17280
|
+
} catch (err) {
|
|
17281
|
+
await charge(row.id, user.id, t0);
|
|
17282
|
+
return c.json(failure(err), 502);
|
|
17283
|
+
}
|
|
17284
|
+
});
|
|
17285
|
+
app2.post("/sessions/:id/click", async (c) => {
|
|
17286
|
+
const user = c.get("user");
|
|
17287
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17288
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17289
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17290
|
+
const x = Number(body.x);
|
|
17291
|
+
const y = Number(body.y);
|
|
17292
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return c.json({ error: "x and y are required" }, 400);
|
|
17293
|
+
const t0 = Date.now();
|
|
17294
|
+
try {
|
|
17295
|
+
await click(row.runtime_session_id, x, y, {
|
|
17296
|
+
button: body.button === "right" || body.button === "middle" ? body.button : "left",
|
|
17297
|
+
numClicks: typeof body.num_clicks === "number" ? body.num_clicks : void 0
|
|
17298
|
+
});
|
|
17299
|
+
await charge(row.id, user.id, t0);
|
|
17300
|
+
await recordAction({ sessionId: row.id, type: "click", params: { x, y }, ok: true });
|
|
17301
|
+
return c.json({ ok: true });
|
|
17302
|
+
} catch (err) {
|
|
17303
|
+
await charge(row.id, user.id, t0);
|
|
17304
|
+
await recordAction({ sessionId: row.id, type: "click", params: { x, y }, ok: false, error: String(err) });
|
|
17305
|
+
return c.json(failure(err), 502);
|
|
17306
|
+
}
|
|
17307
|
+
});
|
|
17308
|
+
app2.post("/sessions/:id/type", async (c) => {
|
|
17309
|
+
const user = c.get("user");
|
|
17310
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17311
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17312
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17313
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
17314
|
+
if (!text) return c.json({ error: "text is required" }, 400);
|
|
17315
|
+
const t0 = Date.now();
|
|
17316
|
+
try {
|
|
17317
|
+
await typeText(row.runtime_session_id, text, typeof body.delay === "number" ? body.delay : void 0);
|
|
17318
|
+
await charge(row.id, user.id, t0);
|
|
17319
|
+
await recordAction({ sessionId: row.id, type: "type", params: { length: text.length }, ok: true });
|
|
17320
|
+
return c.json({ ok: true });
|
|
17321
|
+
} catch (err) {
|
|
17322
|
+
await charge(row.id, user.id, t0);
|
|
17323
|
+
return c.json(failure(err), 502);
|
|
17324
|
+
}
|
|
17325
|
+
});
|
|
17326
|
+
app2.post("/sessions/:id/scroll", async (c) => {
|
|
17327
|
+
const user = c.get("user");
|
|
17328
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17329
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17330
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17331
|
+
const x = typeof body.x === "number" ? body.x : 640;
|
|
17332
|
+
const y = typeof body.y === "number" ? body.y : 400;
|
|
17333
|
+
const deltaX = typeof body.delta_x === "number" ? body.delta_x : 0;
|
|
17334
|
+
const deltaY = typeof body.delta_y === "number" ? body.delta_y : 5;
|
|
17335
|
+
const t0 = Date.now();
|
|
17336
|
+
try {
|
|
17337
|
+
await scroll(row.runtime_session_id, x, y, deltaX, deltaY);
|
|
17338
|
+
await charge(row.id, user.id, t0);
|
|
17339
|
+
await recordAction({ sessionId: row.id, type: "scroll", params: { deltaX, deltaY }, ok: true });
|
|
17340
|
+
return c.json({ ok: true });
|
|
17341
|
+
} catch (err) {
|
|
17342
|
+
await charge(row.id, user.id, t0);
|
|
17343
|
+
return c.json(failure(err), 502);
|
|
17344
|
+
}
|
|
17345
|
+
});
|
|
17346
|
+
app2.post("/sessions/:id/press", async (c) => {
|
|
17347
|
+
const user = c.get("user");
|
|
17348
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17349
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17350
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17351
|
+
const keys = Array.isArray(body.keys) ? body.keys.map(String) : [];
|
|
17352
|
+
if (!keys.length) return c.json({ error: "keys is required" }, 400);
|
|
17353
|
+
const t0 = Date.now();
|
|
17354
|
+
try {
|
|
17355
|
+
await pressKeys(row.runtime_session_id, keys);
|
|
17356
|
+
await charge(row.id, user.id, t0);
|
|
17357
|
+
await recordAction({ sessionId: row.id, type: "press", params: { keys }, ok: true });
|
|
17358
|
+
return c.json({ ok: true });
|
|
17359
|
+
} catch (err) {
|
|
17360
|
+
await charge(row.id, user.id, t0);
|
|
17361
|
+
return c.json(failure(err), 502);
|
|
17362
|
+
}
|
|
17363
|
+
});
|
|
17364
|
+
app2.post("/sessions/:id/replay/start", async (c) => {
|
|
17365
|
+
const user = c.get("user");
|
|
17366
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17367
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17368
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17369
|
+
try {
|
|
17370
|
+
const started = await replayStart(row.runtime_session_id);
|
|
17371
|
+
await recordReplayStart({
|
|
17372
|
+
sessionId: row.id,
|
|
17373
|
+
replayId: started.replayId,
|
|
17374
|
+
viewUrl: started.viewUrl,
|
|
17375
|
+
label: typeof body.label === "string" ? body.label : null
|
|
17376
|
+
});
|
|
17377
|
+
return c.json({ replay_id: started.replayId });
|
|
17378
|
+
} catch (err) {
|
|
17379
|
+
return c.json(failure(err), 502);
|
|
17380
|
+
}
|
|
17381
|
+
});
|
|
17382
|
+
app2.post("/sessions/:id/replay/stop", async (c) => {
|
|
17383
|
+
const user = c.get("user");
|
|
17384
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17385
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17386
|
+
const body = await c.req.json().catch(() => ({}));
|
|
17387
|
+
const replayId = typeof body.replay_id === "string" ? body.replay_id : "";
|
|
17388
|
+
if (!replayId) return c.json({ error: "replay_id is required" }, 400);
|
|
17389
|
+
try {
|
|
17390
|
+
await replayStop(row.runtime_session_id, replayId);
|
|
17391
|
+
let viewUrl = null;
|
|
17392
|
+
try {
|
|
17393
|
+
const all = await replayList(row.runtime_session_id);
|
|
17394
|
+
viewUrl = all.find((r) => r.replayId === replayId)?.viewUrl ?? null;
|
|
17395
|
+
} catch {
|
|
17396
|
+
viewUrl = null;
|
|
17397
|
+
}
|
|
17398
|
+
await recordReplayStop(replayId, viewUrl);
|
|
17399
|
+
return c.json({ ok: true });
|
|
17400
|
+
} catch (err) {
|
|
17401
|
+
return c.json(failure(err), 502);
|
|
17402
|
+
}
|
|
17403
|
+
});
|
|
17404
|
+
app2.get("/sessions/:id/replays", async (c) => {
|
|
17405
|
+
const user = c.get("user");
|
|
17406
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
17407
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
17408
|
+
const rows = await listReplayRows(row.id);
|
|
17409
|
+
return c.json({
|
|
17410
|
+
replays: rows.map((r) => ({
|
|
17411
|
+
replay_id: r.replay_id,
|
|
17412
|
+
view_url: r.view_url,
|
|
17413
|
+
label: r.label,
|
|
17414
|
+
started_at: r.started_at,
|
|
17415
|
+
stopped_at: r.stopped_at
|
|
17416
|
+
}))
|
|
17417
|
+
});
|
|
17418
|
+
});
|
|
17419
|
+
return app2;
|
|
17420
|
+
}
|
|
17421
|
+
var import_hono8, auth;
|
|
17422
|
+
var init_browser_agent_routes = __esm({
|
|
17423
|
+
"src/api/browser-agent-routes.ts"() {
|
|
17424
|
+
"use strict";
|
|
17425
|
+
import_hono8 = require("hono");
|
|
17426
|
+
init_api_auth();
|
|
17427
|
+
init_errors();
|
|
17428
|
+
init_db();
|
|
17429
|
+
init_rates();
|
|
17430
|
+
init_browser_agent_db();
|
|
17431
|
+
init_browser_agent_service();
|
|
17432
|
+
auth = createApiKeyAuth();
|
|
17433
|
+
}
|
|
17434
|
+
});
|
|
17435
|
+
|
|
17436
|
+
// src/api/browser-agent-console.ts
|
|
17437
|
+
function renderConsoleHtml(initialSessionId) {
|
|
17438
|
+
const initial = JSON.stringify(initialSessionId ?? "");
|
|
17439
|
+
return `<!DOCTYPE html>
|
|
17440
|
+
<html lang="en">
|
|
17441
|
+
<head>
|
|
17442
|
+
<meta charset="UTF-8" />
|
|
17443
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
17444
|
+
<title>Browser Agent Console</title>
|
|
17445
|
+
<style>
|
|
17446
|
+
:root { color-scheme: dark; }
|
|
17447
|
+
:where(*) { box-sizing: border-box; }
|
|
17448
|
+
body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; background: #0b0e14; color: #d7dce5; }
|
|
17449
|
+
header { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid #1c2230; background: #0f131c; }
|
|
17450
|
+
header h1 { font-size: 15px; margin: 0; font-weight: 600; color: #fff; letter-spacing: .2px; }
|
|
17451
|
+
header .spacer { flex: 1; }
|
|
17452
|
+
input, button, select { font: inherit; }
|
|
17453
|
+
input[type=text], input[type=password], input[type=url] { background: #141925; border: 1px solid #232b3a; color: #e6eaf2; border-radius: 7px; padding: 7px 10px; }
|
|
17454
|
+
button { background: #2b6cff; border: 0; color: #fff; border-radius: 7px; padding: 7px 12px; cursor: pointer; font-weight: 500; }
|
|
17455
|
+
button.ghost { background: #1a2030; color: #cdd5e4; border: 1px solid #28303f; }
|
|
17456
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
17457
|
+
.layout { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 53px); }
|
|
17458
|
+
aside { border-right: 1px solid #1c2230; overflow-y: auto; padding: 12px; }
|
|
17459
|
+
aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #6b7689; margin: 4px 4px 10px; }
|
|
17460
|
+
.sess { padding: 9px 10px; border-radius: 8px; border: 1px solid #1c2230; margin-bottom: 8px; cursor: pointer; }
|
|
17461
|
+
.sess:hover { border-color: #2b6cff; }
|
|
17462
|
+
.sess.active { border-color: #2b6cff; background: #131b2e; }
|
|
17463
|
+
.sess .id { font-family: ui-monospace, monospace; font-size: 12px; color: #aeb8cc; }
|
|
17464
|
+
.sess .meta { font-size: 11px; color: #6b7689; margin-top: 3px; }
|
|
17465
|
+
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; }
|
|
17466
|
+
.dot.open { background: #36d399; } .dot.closed { background: #5a6677; }
|
|
17467
|
+
main { display: flex; flex-direction: column; overflow: hidden; }
|
|
17468
|
+
.toolbar { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid #1c2230; }
|
|
17469
|
+
.toolbar label { font-size: 13px; color: #aeb8cc; display: flex; align-items: center; gap: 6px; }
|
|
17470
|
+
.stage { flex: 1; position: relative; background: #05070b; overflow: auto; }
|
|
17471
|
+
.stage iframe { width: 100%; height: 100%; border: 0; display: block; }
|
|
17472
|
+
.empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #5a6677; flex-direction: column; gap: 10px; }
|
|
17473
|
+
.replays { border-top: 1px solid #1c2230; padding: 10px 16px; max-height: 200px; overflow-y: auto; }
|
|
17474
|
+
.replays h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #6b7689; margin: 0 0 8px; }
|
|
17475
|
+
.replay { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; }
|
|
17476
|
+
.replay a { color: #7aa2ff; }
|
|
17477
|
+
.gate { max-width: 380px; margin: 80px auto; padding: 24px; border: 1px solid #1c2230; border-radius: 12px; background: #0f131c; }
|
|
17478
|
+
.gate h2 { margin: 0 0 6px; font-size: 16px; color: #fff; }
|
|
17479
|
+
.gate p { color: #8893a7; margin: 0 0 16px; }
|
|
17480
|
+
.gate input { width: 100%; margin-bottom: 12px; }
|
|
17481
|
+
.gate button { width: 100%; }
|
|
17482
|
+
.muted { color: #6b7689; font-size: 12px; }
|
|
17483
|
+
</style>
|
|
17484
|
+
</head>
|
|
17485
|
+
<body>
|
|
17486
|
+
<div id="app"></div>
|
|
17487
|
+
<script>
|
|
17488
|
+
const INITIAL_SESSION = ${initial};
|
|
17489
|
+
const KEY_STORE = 'browser_agent_api_key';
|
|
17490
|
+
let state = { key: localStorage.getItem(KEY_STORE) || '', sessions: [], current: INITIAL_SESSION || null, readOnly: true, liveUrl: null, replays: [] };
|
|
17491
|
+
|
|
17492
|
+
function api(method, path, body) {
|
|
17493
|
+
return fetch('/agent' + path, {
|
|
17494
|
+
method,
|
|
17495
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': state.key },
|
|
17496
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
17497
|
+
}).then(async r => ({ ok: r.ok, data: await r.json().catch(() => ({})) }));
|
|
17498
|
+
}
|
|
17499
|
+
|
|
17500
|
+
async function refreshSessions() {
|
|
17501
|
+
const r = await api('GET', '/sessions?all=1');
|
|
17502
|
+
if (r.ok) { state.sessions = r.data.sessions || []; render(); }
|
|
17503
|
+
}
|
|
17504
|
+
|
|
17505
|
+
async function selectSession(id) {
|
|
17506
|
+
state.current = id; state.liveUrl = null; state.replays = [];
|
|
17507
|
+
history.replaceState(null, '', '/console/' + id);
|
|
17508
|
+
render();
|
|
17509
|
+
const live = await api('GET', '/sessions/' + id + '/live-view');
|
|
17510
|
+
state.liveUrl = live.ok ? live.data.live_view_url : null;
|
|
17511
|
+
const reps = await api('GET', '/sessions/' + id + '/replays');
|
|
17512
|
+
state.replays = reps.ok ? (reps.data.replays || []) : [];
|
|
17513
|
+
render();
|
|
17514
|
+
}
|
|
17515
|
+
|
|
17516
|
+
async function openSession() {
|
|
17517
|
+
const r = await api('POST', '/sessions', { label: 'console' });
|
|
17518
|
+
if (r.ok) { await refreshSessions(); selectSession(r.data.session_id); }
|
|
17519
|
+
else alert('Open failed: ' + JSON.stringify(r.data));
|
|
17520
|
+
}
|
|
17521
|
+
|
|
17522
|
+
async function closeCurrent() {
|
|
17523
|
+
if (!state.current) return;
|
|
17524
|
+
await api('DELETE', '/sessions/' + state.current);
|
|
17525
|
+
await refreshSessions();
|
|
17526
|
+
}
|
|
17527
|
+
|
|
17528
|
+
function frameSrc() {
|
|
17529
|
+
if (!state.liveUrl) return null;
|
|
17530
|
+
const sep = state.liveUrl.includes('?') ? '&' : '?';
|
|
17531
|
+
return state.readOnly ? state.liveUrl + sep + 'readOnly=true' : state.liveUrl;
|
|
17532
|
+
}
|
|
17533
|
+
|
|
17534
|
+
function saveKey(v) { state.key = v.trim(); localStorage.setItem(KEY_STORE, state.key); render(); if (state.key) { refreshSessions(); if (state.current) selectSession(state.current); } }
|
|
17535
|
+
|
|
17536
|
+
function h(html) { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstChild; }
|
|
17537
|
+
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
17538
|
+
|
|
17539
|
+
function render() {
|
|
17540
|
+
const app = document.getElementById('app');
|
|
17541
|
+
app.innerHTML = '';
|
|
17542
|
+
if (!state.key) {
|
|
17543
|
+
app.appendChild(h('<div class="gate"><h2>Browser Agent Console</h2><p>Paste your API key to watch and control browser sessions.</p><input id="k" type="password" placeholder="API key" /><button id="kb">Continue</button></div>'));
|
|
17544
|
+
document.getElementById('kb').onclick = () => saveKey(document.getElementById('k').value);
|
|
17545
|
+
document.getElementById('k').onkeydown = e => { if (e.key === 'Enter') saveKey(e.target.value); };
|
|
17546
|
+
return;
|
|
17547
|
+
}
|
|
17548
|
+
const header = h('<header><h1>Browser Agent</h1><span class="muted">live control + replays</span><span class="spacer"></span><button id="open">+ New Session</button><button class="ghost" id="logout">Forget key</button></header>');
|
|
17549
|
+
app.appendChild(header);
|
|
17550
|
+
document.getElementById('open').onclick = openSession;
|
|
17551
|
+
document.getElementById('logout').onclick = () => saveKey('');
|
|
17552
|
+
|
|
17553
|
+
const layout = h('<div class="layout"></div>');
|
|
17554
|
+
const aside = h('<aside><h2>Sessions</h2></aside>');
|
|
17555
|
+
if (!state.sessions.length) aside.appendChild(h('<div class="muted" style="padding:4px">No sessions yet.</div>'));
|
|
17556
|
+
for (const s of state.sessions) {
|
|
17557
|
+
const el = h('<div class="sess ' + (s.session_id === state.current ? 'active' : '') + '"><div class="id">' + esc(s.session_id) + '</div><div class="meta"><span class="dot ' + esc(s.status) + '"></span>' + esc(s.status) + (s.label ? ' \xB7 ' + esc(s.label) : '') + '</div></div>');
|
|
17558
|
+
el.onclick = () => selectSession(s.session_id);
|
|
17559
|
+
aside.appendChild(el);
|
|
17560
|
+
}
|
|
17561
|
+
layout.appendChild(aside);
|
|
17562
|
+
|
|
17563
|
+
const main = h('<main></main>');
|
|
17564
|
+
if (!state.current) {
|
|
17565
|
+
main.appendChild(h('<div class="empty"><div>Select or open a session to watch.</div></div>'));
|
|
17566
|
+
} else {
|
|
17567
|
+
const tb = h('<div class="toolbar"><label><input type="checkbox" id="ro" ' + (state.readOnly ? 'checked' : '') + ' /> Read-only (uncheck to take control)</label><span class="spacer"></span><button class="ghost" id="reload">Reload view</button><button class="ghost" id="close">Close session</button></div>');
|
|
17568
|
+
main.appendChild(tb);
|
|
17569
|
+
const stage = h('<div class="stage"></div>');
|
|
17570
|
+
const src = frameSrc();
|
|
17571
|
+
if (src) {
|
|
17572
|
+
const f = h('<iframe allow="autoplay; clipboard-read; clipboard-write" src="' + esc(src) + '"></iframe>');
|
|
17573
|
+
stage.appendChild(f);
|
|
17574
|
+
} else {
|
|
17575
|
+
stage.appendChild(h('<div class="empty"><div>Live view unavailable for this session.</div><div class="muted">It may be closed or still starting.</div></div>'));
|
|
17576
|
+
}
|
|
17577
|
+
main.appendChild(stage);
|
|
17578
|
+
|
|
17579
|
+
const rep = h('<div class="replays"><h3>Replays</h3></div>');
|
|
17580
|
+
if (!state.replays.length) rep.appendChild(h('<div class="muted">No replays recorded.</div>'));
|
|
17581
|
+
for (const r of state.replays) {
|
|
17582
|
+
const status = r.stopped_at ? 'ready' : 'recording\u2026';
|
|
17583
|
+
const link = r.view_url ? '<a href="' + esc(r.view_url) + '" target="_blank" rel="noopener">view mp4</a>' : '<span class="muted">' + status + '</span>';
|
|
17584
|
+
rep.appendChild(h('<div class="replay"><span class="muted">' + esc(r.started_at || '') + '</span><span class="spacer"></span>' + link + '</div>'));
|
|
17585
|
+
}
|
|
17586
|
+
main.appendChild(rep);
|
|
17587
|
+
|
|
17588
|
+
layout.appendChild(main);
|
|
17589
|
+
}
|
|
17590
|
+
app.appendChild(layout);
|
|
17591
|
+
|
|
17592
|
+
const ro = document.getElementById('ro');
|
|
17593
|
+
if (ro) ro.onchange = e => { state.readOnly = e.target.checked; render(); };
|
|
17594
|
+
const reload = document.getElementById('reload');
|
|
17595
|
+
if (reload) reload.onclick = () => selectSession(state.current);
|
|
17596
|
+
const close = document.getElementById('close');
|
|
17597
|
+
if (close) close.onclick = closeCurrent;
|
|
17598
|
+
}
|
|
17599
|
+
|
|
17600
|
+
render();
|
|
17601
|
+
if (state.key) { refreshSessions(); if (state.current) selectSession(state.current); }
|
|
17602
|
+
</script>
|
|
17603
|
+
</body>
|
|
17604
|
+
</html>`;
|
|
17605
|
+
}
|
|
17606
|
+
var init_browser_agent_console = __esm({
|
|
17607
|
+
"src/api/browser-agent-console.ts"() {
|
|
17608
|
+
"use strict";
|
|
17609
|
+
}
|
|
17610
|
+
});
|
|
17611
|
+
|
|
16771
17612
|
// src/api/stripe-routes.ts
|
|
16772
|
-
var import_stripe,
|
|
17613
|
+
var import_stripe, import_hono9, stripe, stripeApp;
|
|
16773
17614
|
var init_stripe_routes = __esm({
|
|
16774
17615
|
"src/api/stripe-routes.ts"() {
|
|
16775
17616
|
"use strict";
|
|
16776
17617
|
import_stripe = __toESM(require("stripe"), 1);
|
|
16777
|
-
|
|
17618
|
+
import_hono9 = require("hono");
|
|
16778
17619
|
init_db();
|
|
16779
17620
|
init_rates();
|
|
16780
17621
|
stripe = new import_stripe.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
16781
|
-
stripeApp = new
|
|
17622
|
+
stripeApp = new import_hono9.Hono();
|
|
16782
17623
|
stripeApp.post("/webhooks", async (c) => {
|
|
16783
17624
|
const sig = c.req.header("stripe-signature");
|
|
16784
17625
|
const body = await c.req.text();
|
|
@@ -17065,14 +17906,14 @@ function getSessionSecret() {
|
|
|
17065
17906
|
function safeEqualHex(a, b) {
|
|
17066
17907
|
if (a.length !== b.length) return false;
|
|
17067
17908
|
try {
|
|
17068
|
-
return (0,
|
|
17909
|
+
return (0, import_node_crypto4.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
17069
17910
|
} catch {
|
|
17070
17911
|
return false;
|
|
17071
17912
|
}
|
|
17072
17913
|
}
|
|
17073
17914
|
function signSession(userId) {
|
|
17074
17915
|
const payload = String(userId);
|
|
17075
|
-
const sig = (0,
|
|
17916
|
+
const sig = (0, import_node_crypto4.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
17076
17917
|
return `${payload}.${sig}`;
|
|
17077
17918
|
}
|
|
17078
17919
|
function verifySession(token) {
|
|
@@ -17080,16 +17921,16 @@ function verifySession(token) {
|
|
|
17080
17921
|
if (dot === -1) return null;
|
|
17081
17922
|
const payload = token.slice(0, dot);
|
|
17082
17923
|
const sig = token.slice(dot + 1);
|
|
17083
|
-
const expected = (0,
|
|
17924
|
+
const expected = (0, import_node_crypto4.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
17084
17925
|
if (!safeEqualHex(sig, expected)) return null;
|
|
17085
17926
|
const id = parseInt(payload);
|
|
17086
17927
|
return isNaN(id) ? null : id;
|
|
17087
17928
|
}
|
|
17088
|
-
var
|
|
17929
|
+
var import_node_crypto4, isProduction, secret;
|
|
17089
17930
|
var init_session = __esm({
|
|
17090
17931
|
"src/api/session.ts"() {
|
|
17091
17932
|
"use strict";
|
|
17092
|
-
|
|
17933
|
+
import_node_crypto4 = require("crypto");
|
|
17093
17934
|
isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
17094
17935
|
secret = () => getSessionSecret();
|
|
17095
17936
|
}
|
|
@@ -17305,7 +18146,7 @@ async function checkHarvestLimits(userId, email, extraSlots = 0) {
|
|
|
17305
18146
|
if (active >= limit) return { error: `You have ${active} job${active !== 1 ? "s" : ""} running. Your account allows ${limit} concurrent job${limit !== 1 ? "s" : ""}. Wait for one to finish or add a concurrency slot at mcpscraper.dev/billing.` };
|
|
17306
18147
|
return null;
|
|
17307
18148
|
}
|
|
17308
|
-
var import_resend,
|
|
18149
|
+
var import_resend, import_hono10, import_hono11, import_factory6, import_cookie, import_stripe2, secureCookies, isProduction2, sessionCookieOptions, requireAllowedOrigin, auth2, adminAuth, sessionAuth, app, STRIPE_API_VERSION, BYPASS_EMAILS, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
|
|
17309
18150
|
var init_server = __esm({
|
|
17310
18151
|
"src/api/server.ts"() {
|
|
17311
18152
|
"use strict";
|
|
@@ -17322,8 +18163,8 @@ var init_server = __esm({
|
|
|
17322
18163
|
init_media_extractor();
|
|
17323
18164
|
init_site_mapper();
|
|
17324
18165
|
init_site_extractor();
|
|
17325
|
-
|
|
17326
|
-
|
|
18166
|
+
import_hono10 = require("hono");
|
|
18167
|
+
import_hono11 = require("inngest/hono");
|
|
17327
18168
|
init_client();
|
|
17328
18169
|
init_site_audit();
|
|
17329
18170
|
init_site_audit_routes();
|
|
@@ -17333,6 +18174,8 @@ var init_server = __esm({
|
|
|
17333
18174
|
init_maps_routes();
|
|
17334
18175
|
init_serp_intelligence_routes();
|
|
17335
18176
|
init_mcp_routes();
|
|
18177
|
+
init_browser_agent_routes();
|
|
18178
|
+
init_browser_agent_console();
|
|
17336
18179
|
init_stripe_routes();
|
|
17337
18180
|
init_site_audit_worker();
|
|
17338
18181
|
import_factory6 = require("hono/factory");
|
|
@@ -17362,7 +18205,7 @@ var init_server = __esm({
|
|
|
17362
18205
|
if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
|
|
17363
18206
|
return next();
|
|
17364
18207
|
});
|
|
17365
|
-
|
|
18208
|
+
auth2 = (0, import_factory6.createMiddleware)(async (c, next) => {
|
|
17366
18209
|
const key = c.req.header("x-api-key");
|
|
17367
18210
|
if (!key) return c.json({ error: "Missing API key" }, 401);
|
|
17368
18211
|
const user = await getUserByApiKey(key);
|
|
@@ -17391,7 +18234,7 @@ var init_server = __esm({
|
|
|
17391
18234
|
c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
|
|
17392
18235
|
return next();
|
|
17393
18236
|
});
|
|
17394
|
-
app = new
|
|
18237
|
+
app = new import_hono10.Hono();
|
|
17395
18238
|
STRIPE_API_VERSION = "2026-02-25.clover";
|
|
17396
18239
|
app.use("*", async (c, next) => {
|
|
17397
18240
|
await next();
|
|
@@ -17551,7 +18394,7 @@ var init_server = __esm({
|
|
|
17551
18394
|
const parsed = raw === void 0 ? NaN : Number(raw);
|
|
17552
18395
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
17553
18396
|
})();
|
|
17554
|
-
app.post("/harvest",
|
|
18397
|
+
app.post("/harvest", auth2, async (c) => {
|
|
17555
18398
|
const user = c.get("user");
|
|
17556
18399
|
const raw = await c.req.json().catch(() => ({}));
|
|
17557
18400
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -17593,7 +18436,7 @@ var init_server = __esm({
|
|
|
17593
18436
|
}
|
|
17594
18437
|
return c.json({ job_id: jobId, status: "pending" }, 202);
|
|
17595
18438
|
});
|
|
17596
|
-
app.post("/harvest/sync",
|
|
18439
|
+
app.post("/harvest/sync", auth2, async (c) => {
|
|
17597
18440
|
const user = c.get("user");
|
|
17598
18441
|
const raw = await c.req.json().catch(() => ({}));
|
|
17599
18442
|
const bodyResult = HarvestBodySchema.safeParse(raw);
|
|
@@ -17658,17 +18501,17 @@ var init_server = __esm({
|
|
|
17658
18501
|
return c.json({ job_id: jobId, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
|
|
17659
18502
|
}
|
|
17660
18503
|
});
|
|
17661
|
-
app.get("/jobs/:id",
|
|
18504
|
+
app.get("/jobs/:id", auth2, async (c) => {
|
|
17662
18505
|
const job = await getJob(c.req.param("id"), c.get("user").id);
|
|
17663
18506
|
if (!job) return c.json({ error: "Job not found" }, 404);
|
|
17664
18507
|
const attempts = await listHarvestAttempts(job.id, c.get("user").id);
|
|
17665
18508
|
const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
|
|
17666
18509
|
return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
|
|
17667
18510
|
});
|
|
17668
|
-
app.get("/jobs",
|
|
18511
|
+
app.get("/jobs", auth2, async (c) => {
|
|
17669
18512
|
return c.json(await listJobs(c.get("user").id));
|
|
17670
18513
|
});
|
|
17671
|
-
app.get("/history",
|
|
18514
|
+
app.get("/history", auth2, async (c) => {
|
|
17672
18515
|
const userId = c.get("user").id;
|
|
17673
18516
|
const [jobs, events] = await Promise.all([
|
|
17674
18517
|
listJobs(userId),
|
|
@@ -17698,7 +18541,7 @@ var init_server = __esm({
|
|
|
17698
18541
|
const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
|
|
17699
18542
|
return c.json(rows.slice(0, 100));
|
|
17700
18543
|
});
|
|
17701
|
-
app.get("/ledger",
|
|
18544
|
+
app.get("/ledger", auth2, async (c) => {
|
|
17702
18545
|
return c.json(await getLedger(c.get("user").id, 100));
|
|
17703
18546
|
});
|
|
17704
18547
|
app.post("/admin/users", adminAuth, async (c) => {
|
|
@@ -17736,11 +18579,11 @@ var init_server = __esm({
|
|
|
17736
18579
|
}
|
|
17737
18580
|
return c.json({ processed, credited, skipped, users_credited });
|
|
17738
18581
|
});
|
|
17739
|
-
app.post("/extract-url",
|
|
18582
|
+
app.post("/extract-url", auth2, async (c) => {
|
|
17740
18583
|
const raw = await c.req.json().catch(() => ({}));
|
|
17741
18584
|
const bodyResult = ExtractUrlBodySchema.safeParse(raw);
|
|
17742
18585
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
17743
|
-
const { url, screenshot, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
18586
|
+
const { url, screenshot: screenshot2, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
17744
18587
|
if (!allowLocal) {
|
|
17745
18588
|
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
17746
18589
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
@@ -17766,7 +18609,7 @@ var init_server = __esm({
|
|
|
17766
18609
|
const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
|
|
17767
18610
|
const [result, pageData] = await Promise.all([
|
|
17768
18611
|
extractKpo({ url: canonicalUrl, kernelApiKey }),
|
|
17769
|
-
|
|
18612
|
+
screenshot2 || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding }).catch(() => null) : null
|
|
17770
18613
|
]);
|
|
17771
18614
|
const screenshotBuf = pageData?.screenshot ?? null;
|
|
17772
18615
|
const brandingData = pageData?.branding ?? null;
|
|
@@ -17784,7 +18627,7 @@ var init_server = __esm({
|
|
|
17784
18627
|
return c.json({ error: msg }, 500);
|
|
17785
18628
|
}
|
|
17786
18629
|
});
|
|
17787
|
-
app.post("/map-urls",
|
|
18630
|
+
app.post("/map-urls", auth2, async (c) => {
|
|
17788
18631
|
const raw = await c.req.json().catch(() => ({}));
|
|
17789
18632
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|
|
17790
18633
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -17824,7 +18667,7 @@ var init_server = __esm({
|
|
|
17824
18667
|
return c.json({ error: msg }, 500);
|
|
17825
18668
|
}
|
|
17826
18669
|
});
|
|
17827
|
-
app.post("/extract-site",
|
|
18670
|
+
app.post("/extract-site", auth2, async (c) => {
|
|
17828
18671
|
const raw = await c.req.json().catch(() => ({}));
|
|
17829
18672
|
const bodyResult = ExtractSiteBodySchema.safeParse(raw);
|
|
17830
18673
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
@@ -17946,7 +18789,7 @@ var init_server = __esm({
|
|
|
17946
18789
|
await setConcurrencySubId(user.id, null);
|
|
17947
18790
|
return c.json({ ok: true, concurrency_limit: user.extra_concurrency_slots });
|
|
17948
18791
|
});
|
|
17949
|
-
app.get("/billing/balance",
|
|
18792
|
+
app.get("/billing/balance", auth2, async (c) => {
|
|
17950
18793
|
const user = c.get("user");
|
|
17951
18794
|
const balanceMc = await reconcileBalanceMc(user.id);
|
|
17952
18795
|
const ledger = await getLedger(user.id, 20);
|
|
@@ -17958,7 +18801,7 @@ var init_server = __esm({
|
|
|
17958
18801
|
ledger
|
|
17959
18802
|
});
|
|
17960
18803
|
});
|
|
17961
|
-
app.post("/billing/credits",
|
|
18804
|
+
app.post("/billing/credits", auth2, async (c) => {
|
|
17962
18805
|
const user = c.get("user");
|
|
17963
18806
|
const balanceMc = await reconcileBalanceMc(user.id);
|
|
17964
18807
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -17995,7 +18838,7 @@ var init_server = __esm({
|
|
|
17995
18838
|
]);
|
|
17996
18839
|
return c.json({ drained: results.length, results, sweepResult });
|
|
17997
18840
|
});
|
|
17998
|
-
app.on(["GET", "POST", "PUT"], "/api/inngest", (0,
|
|
18841
|
+
app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono11.serve)({ client: inngest, functions: [siteAuditFn] }));
|
|
17999
18842
|
app.route("/api/internal/site-architecture-auditor", siteAuditApp);
|
|
18000
18843
|
app.route("/youtube", youtubeApp);
|
|
18001
18844
|
app.route("/screenshot", screenshotApp);
|
|
@@ -18003,6 +18846,9 @@ var init_server = __esm({
|
|
|
18003
18846
|
app.route("/maps", mapsApp);
|
|
18004
18847
|
app.route("/serp-intelligence", serpIntelligenceApp);
|
|
18005
18848
|
app.route("/mcp", mcpApp);
|
|
18849
|
+
app.route("/agent", buildBrowserAgentRoutes());
|
|
18850
|
+
app.get("/console", (c) => c.html(renderConsoleHtml()));
|
|
18851
|
+
app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
|
|
18006
18852
|
app.route("/stripe", stripeApp);
|
|
18007
18853
|
if (!process.env.INNGEST_EVENT_KEY) {
|
|
18008
18854
|
startSiteAuditWorker();
|
|
@@ -18110,10 +18956,10 @@ var init_server = __esm({
|
|
|
18110
18956
|
});
|
|
18111
18957
|
|
|
18112
18958
|
// bin/api-server.ts
|
|
18113
|
-
var
|
|
18959
|
+
var import_node_fs6 = require("fs");
|
|
18114
18960
|
function loadDotEnv() {
|
|
18115
18961
|
try {
|
|
18116
|
-
for (const line of (0,
|
|
18962
|
+
for (const line of (0, import_node_fs6.readFileSync)(".env", "utf8").split("\n")) {
|
|
18117
18963
|
const eq = line.indexOf("=");
|
|
18118
18964
|
if (eq < 1 || line.trimStart().startsWith("#")) continue;
|
|
18119
18965
|
const k = line.slice(0, eq).trim();
|