mcp-scraper 0.66.2 → 0.66.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to MCP Scraper are documented here. The format is based on [
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.66.4] - 2026-08-25
8
+
9
+ ### Added
10
+
11
+ - Connected-service discovery now returns a tenant-owned `userLabel` plus verified provider account ID, email, name, and identity status so agents can select the intended account when one integration has multiple connections.
12
+ - Managed OAuth connections now retain non-secret provider identity metadata and support owner-scoped account names through the dashboard API.
13
+
14
+ ### Fixed
15
+
16
+ - Nango tenant tags are no longer presented as the external provider account, preventing the MCP Scraper login from being mistaken for the Google, Slack, GitHub, Zoom, or other authorized identity.
17
+
18
+ ## [0.66.3] - 2026-08-25
19
+
20
+ ### Changed
21
+
22
+ - Runtime MCP discovery now omits output schemas across hosted OAuth, API-key, npm stdio, and MCPB clients, while the complete generated developer contract continues to supply typed Node and Python SDK outputs.
23
+
24
+ ### Fixed
25
+
26
+ - Restored compatibility for strict MCP clients that could connect successfully but silently register zero tools when the complete output-schema catalog was advertised; tool calls retain both readable content and structured results.
27
+
7
28
  ## [0.66.2] - 2026-08-24
8
29
 
9
30
  ### Changed
@@ -1088,7 +1109,9 @@ All notable changes to MCP Scraper are documented here. The format is based on [
1088
1109
  - Write actions remain unavailable until the account owner explicitly enables them.
1089
1110
  - Provider-specific connection data is normalized into one agent-facing contract.
1090
1111
 
1091
- [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.2...HEAD
1112
+ [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.4...HEAD
1113
+ [0.66.4]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.3...v0.66.4
1114
+ [0.66.3]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.2...v0.66.3
1092
1115
  [0.66.2]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.1...v0.66.2
1093
1116
  [0.66.1]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.0...v0.66.1
1094
1117
  [0.66.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.65.1...v0.66.0
package/README.md CHANGED
@@ -159,7 +159,7 @@ Build the branded one-click bundle:
159
159
  npm run build:mcpb
160
160
  ```
161
161
 
162
- The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.66.2`, SHA-256 `0ed9a49cc80f3d8b2da41039e9a9baeeb77b3f626cd9f98a4e2c84e566e1a79d`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, API-key configuration field, and manually curated current-release message from the bundle manifest.
162
+ The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.66.4`, SHA-256 `53fa34844c0a030c9777cb0e62d547c446ebf6f553f0a850d33bd0aa4185f73c`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, API-key configuration field, and manually curated current-release message from the bundle manifest.
163
163
 
164
164
  The MCPB install exposes every tool — web-intelligence plus all `browser_*` tools — through the one `mcp-scraper` server.
165
165
 
@@ -315,7 +315,7 @@ For Google SERP and Maps tools, callers provide the query, two-letter country co
315
315
 
316
316
  The `mcp-scraper` server (and the MCPB bundle, which runs it) exposes both sections through one MCP server.
317
317
 
318
- All MCP tools expose output schemas and return `structuredContent` with the IDs, URLs, CSV paths, transcripts, browser session handles, replay paths, artifacts, recipe fields, or blueprint fields needed by the next step. Browser Agent tools keep a JSON text block for older clients, but structured data is the primary contract. All tools carry MCP annotations; file-writing tools such as replay downloads and annotations state their filesystem side effects.
318
+ All MCP tools return `structuredContent` with the IDs, URLs, CSV paths, transcripts, browser session handles, replay paths, artifacts, recipe fields, or blueprint fields needed by the next step, plus readable text content for compatibility. Runtime `tools/list` omits output schemas so strict clients can register the complete catalog; the generated developer manifest retains every canonical output schema for validation and typed SDK generation. All tools carry MCP annotations; file-writing tools such as replay downloads and annotations state their filesystem side effects.
319
319
 
320
320
  The canonical tool inventory is generated at `docs/mcp-tool-manifest.generated.json`. The unified server exposes 259 tools: 158 scraper, browser, workflow, billing, and connected-service tools plus 101 durable-memory tools. The scraper-side inventory includes rendered site-content similarity, governed Local Sourcebook tools, Transparent Commons entity, linkset, and publication tools, direct site-export text and image readers, the searchable News Publisher and Editorial Reading Room template workflow, and 24 X-Ray tools for pixel administration, reporting, attribution journeys, campaign links, exports, and scheduled reports. Successful evidence-compiled Local Sourcebook revisions publish automatically to their canonical `localsourcebook.com` category profile and review URLs; administrator controls handle exceptional rejection or unpublishing. Release verification compares the exact local and hosted tool-name sets, not only the count.
321
321
 
@@ -49679,7 +49679,7 @@ var PACKAGE_VERSION;
49679
49679
  var init_version = __esm({
49680
49680
  "src/version.ts"() {
49681
49681
  "use strict";
49682
- PACKAGE_VERSION = "0.66.2";
49682
+ PACKAGE_VERSION = "0.66.4";
49683
49683
  }
49684
49684
  });
49685
49685
 
@@ -49940,10 +49940,10 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
49940
49940
  artifact, call \`report_artifact_read\` with the returned artifactId and follow nextOffset until null. This
49941
49941
  works through the authenticated MCP connection even when the client cannot open the signed download URL;
49942
49942
  do not try curl or web_fetch. Use \`archive_read\` for ZIP archives.
49943
- - Before using a connected account, call \`list_service_connections\` and match the intended provider-side
49944
- identity from \`providerAccountEmail\` or \`providerAccountName\`, not the MCP Scraper login. If
49945
- \`providerIdentityStatus\` is \`unavailable\`, ask the person to refresh that connection before assuming
49946
- which external account it represents.
49943
+ - Before using a connected account, call \`list_service_connections\`. Match an explicit \`userLabel\` first,
49944
+ then verified provider-side \`providerAccountEmail\`, \`providerAccountName\`, or \`providerAccountId\`; never
49945
+ treat the MCP Scraper login as external-provider identity. If identity is pending or unavailable and no
49946
+ distinct user label exists, ask the person which connection they mean instead of assuming.
49947
49947
  - For Google Search Console, use \`export_connected_service_data\` with
49948
49948
  \`dataset:"search_console_performance"\` when the person asks for a fresh JSONL download. When they
49949
49949
  ask to schedule ingestion or repeatedly filter historical rows, create a \`connection_sync\` schedule,
@@ -50109,14 +50109,30 @@ var init_server_instructions = __esm({
50109
50109
  });
50110
50110
 
50111
50111
  // src/mcp/output-schema-registry.ts
50112
+ function resolveOutputSchemaMode(env = process.env) {
50113
+ const requested = env.MCP_SCRAPER_OUTPUT_SCHEMA_MODE?.trim().toLowerCase();
50114
+ if (requested) {
50115
+ if (requested === "contract" || requested === "all") return "contract";
50116
+ if (requested === "runtime" || requested === "none" || requested === "essential") return "runtime";
50117
+ throw new Error(`Unsupported MCP_SCRAPER_OUTPUT_SCHEMA_MODE: ${requested}`);
50118
+ }
50119
+ const legacyAdvertisement = env.MCP_SCRAPER_ADVERTISE_OUTPUT_SCHEMAS?.trim().toLowerCase();
50120
+ if (legacyAdvertisement === "true") return "contract";
50121
+ if (legacyAdvertisement === void 0 || legacyAdvertisement === "" || legacyAdvertisement === "false") {
50122
+ return "runtime";
50123
+ }
50124
+ throw new Error(`Unsupported MCP_SCRAPER_ADVERTISE_OUTPUT_SCHEMAS value: ${legacyAdvertisement}`);
50125
+ }
50112
50126
  function recordOutputSchema(name, schema) {
50113
50127
  OUTPUT_SCHEMAS[name] = schema;
50114
- return schema;
50128
+ return ADVERTISE_OUTPUT_SCHEMAS ? schema : void 0;
50115
50129
  }
50116
- var OUTPUT_SCHEMAS;
50130
+ var OUTPUT_SCHEMA_MODE, ADVERTISE_OUTPUT_SCHEMAS, OUTPUT_SCHEMAS;
50117
50131
  var init_output_schema_registry = __esm({
50118
50132
  "src/mcp/output-schema-registry.ts"() {
50119
50133
  "use strict";
50134
+ OUTPUT_SCHEMA_MODE = resolveOutputSchemaMode();
50135
+ ADVERTISE_OUTPUT_SCHEMAS = OUTPUT_SCHEMA_MODE === "contract";
50120
50136
  OUTPUT_SCHEMAS = {};
50121
50137
  }
50122
50138
  });
@@ -53837,7 +53853,12 @@ var init_mcp_tool_schemas = __esm({
53837
53853
  connectionId: import_zod45.z.string(),
53838
53854
  providerConfigKey: import_zod45.z.string(),
53839
53855
  provider: import_zod45.z.string().nullable().optional(),
53840
- label: import_zod45.z.string(),
53856
+ label: import_zod45.z.string().describe("Best available human-facing account label: a userLabel when set, otherwise verified provider identity, then a stable fallback."),
53857
+ userLabel: import_zod45.z.string().nullable().optional().describe("Optional tenant-owned name set in the dashboard to distinguish this connection. This is not asserted provider identity."),
53858
+ providerAccountId: import_zod45.z.string().nullable().optional().describe("Stable provider-side account, user, workspace, channel, or tenant identifier when the provider exposes one."),
53859
+ providerAccountEmail: import_zod45.z.string().email().nullable().optional().describe("Verified provider-side email when the provider exposes one. This is never inferred from the MCP Scraper login."),
53860
+ providerAccountName: import_zod45.z.string().nullable().optional().describe("Provider-side account, user, workspace, channel, or tenant name when the provider exposes one."),
53861
+ providerIdentityStatus: import_zod45.z.enum(["pending", "verified", "unavailable"]).optional().describe("Whether provider identity discovery is pending, returned at least one verified provider identifier, or completed without one."),
53841
53862
  status: import_zod45.z.string(),
53842
53863
  lifecycleStatus: import_zod45.z.enum(["pending", "connected", "needs_reauth", "disconnecting", "disconnected"]).optional().describe("Credential lifecycle. This is separate from current provider availability."),
53843
53864
  operationalStatus: import_zod45.z.enum(["unknown", "available", "degraded", "unavailable"]).optional().describe("Last observed provider transport availability. Unavailable does not imply reconnect is required."),
@@ -56341,7 +56362,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
56341
56362
  }, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
56342
56363
  server.registerTool("list_service_connections", {
56343
56364
  title: "List Connected Services",
56344
- description: "List every service connection this MCP Scraper account has authorized, including Resend, GitHub, Google Analytics, Google Search Console, YouTube, Facebook Pages, LinkedIn, X, Meta Marketing, Slack, Gmail, Calendar, Google Drive, Zoom, Xero, and others. Returns the tenant-scoped connectionId, credential transport, exact live readTools and gated actionTools, permission-aware toolCapabilities with missing OAuth-grant or provider-app-feature blockers, permanently blocked administrative tools, and schema-discovery metadata. Get a connectionId and exact tool name here before calling describe_service_connection_tool, read_service_connection, or call_service_connection_action. Managed OAuth and official remote MCP connections use the same provider-neutral bridges; mutations still require the account action switch and an exact allowed action. A scheduled Search Console connection_sync creates a typed tenant-owned performance table; after it runs, use the returned tableName with table-describe and table-query instead of repeatedly calling Google for historical filtering.",
56365
+ description: "List every service connection this MCP Scraper account has authorized, including Resend, GitHub, Google Analytics, Google Search Console, YouTube, Facebook Pages, LinkedIn, X, Meta Marketing, Slack, Gmail, Calendar, Google Drive, Zoom, Xero, and others. Returns the tenant-scoped connectionId; a tenant-owned userLabel; verified provider-side account ID, email, or name when available; credential transport; exact live readTools and gated actionTools; permission-aware toolCapabilities with missing OAuth-grant or provider-app-feature blockers; permanently blocked administrative tools; and schema-discovery metadata. Get a connectionId and exact tool name here before calling describe_service_connection_tool, read_service_connection, or call_service_connection_action. Managed OAuth and official remote MCP connections use the same provider-neutral bridges; mutations still require the account action switch and an exact allowed action. A scheduled Search Console connection_sync creates a typed tenant-owned performance table; after it runs, use the returned tableName with table-describe and table-query instead of repeatedly calling Google for historical filtering.",
56345
56366
  inputSchema: ListServiceConnectionsInputSchema,
56346
56367
  outputSchema: recordOutputSchema("list_service_connections", ListServiceConnectionsOutputSchema),
56347
56368
  annotations: { title: "List Connected Services", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
@@ -81153,6 +81174,12 @@ function ensureServiceConnectionsSchema() {
81153
81174
  transport TEXT NOT NULL,
81154
81175
  upstream_connection_id TEXT,
81155
81176
  label TEXT,
81177
+ user_label TEXT,
81178
+ provider_account_id TEXT,
81179
+ provider_account_email TEXT,
81180
+ provider_account_name TEXT,
81181
+ provider_identity_checked_at TEXT,
81182
+ provider_identity_source_updated_at TEXT,
81156
81183
  lifecycle_status TEXT NOT NULL DEFAULT 'pending',
81157
81184
  operational_status TEXT NOT NULL DEFAULT 'unknown',
81158
81185
  reconnect_required INTEGER NOT NULL DEFAULT 0,
@@ -81174,6 +81201,30 @@ function ensureServiceConnectionsSchema() {
81174
81201
  UNIQUE(user_id, provider_config_key, upstream_connection_id)
81175
81202
  )
81176
81203
  `);
81204
+ try {
81205
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN user_label TEXT`);
81206
+ } catch {
81207
+ }
81208
+ try {
81209
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_id TEXT`);
81210
+ } catch {
81211
+ }
81212
+ try {
81213
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_email TEXT`);
81214
+ } catch {
81215
+ }
81216
+ try {
81217
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_name TEXT`);
81218
+ } catch {
81219
+ }
81220
+ try {
81221
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_identity_checked_at TEXT`);
81222
+ } catch {
81223
+ }
81224
+ try {
81225
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_identity_source_updated_at TEXT`);
81226
+ } catch {
81227
+ }
81177
81228
  await db.execute(`CREATE INDEX IF NOT EXISTS service_connections_user_updated ON service_connections(user_id, updated_at DESC)`);
81178
81229
  await db.execute(`CREATE INDEX IF NOT EXISTS service_connections_upstream ON service_connections(provider_config_key, upstream_connection_id)`);
81179
81230
  await db.execute(`
@@ -81234,6 +81285,12 @@ function rowToRecord(row) {
81234
81285
  transport: row.transport === "remote_mcp" ? "remote_mcp" : "nango",
81235
81286
  upstreamConnectionId: typeof row.upstream_connection_id === "string" ? row.upstream_connection_id : null,
81236
81287
  label: typeof row.label === "string" ? row.label : null,
81288
+ userLabel: typeof row.user_label === "string" ? row.user_label : null,
81289
+ providerAccountId: typeof row.provider_account_id === "string" ? row.provider_account_id : null,
81290
+ providerAccountEmail: typeof row.provider_account_email === "string" ? row.provider_account_email : null,
81291
+ providerAccountName: typeof row.provider_account_name === "string" ? row.provider_account_name : null,
81292
+ providerIdentityCheckedAt: typeof row.provider_identity_checked_at === "string" ? row.provider_identity_checked_at : null,
81293
+ providerIdentitySourceUpdatedAt: typeof row.provider_identity_source_updated_at === "string" ? row.provider_identity_source_updated_at : null,
81237
81294
  lifecycleStatus: String(row.lifecycle_status),
81238
81295
  operationalStatus: String(row.operational_status),
81239
81296
  reconnectRequired: Number(row.reconnect_required) === 1,
@@ -81319,6 +81376,34 @@ async function updateServiceConnectionTools(connectionId, readTools, actionTools
81319
81376
  args: [JSON.stringify(readTools), JSON.stringify(actionTools), toolRevision, connectionId]
81320
81377
  });
81321
81378
  }
81379
+ async function setServiceConnectionProviderIdentity(args) {
81380
+ await ensureServiceConnectionsSchema();
81381
+ await getDb().execute({
81382
+ sql: `
81383
+ UPDATE service_connections SET
81384
+ provider_account_id = ?, provider_account_email = ?, provider_account_name = ?,
81385
+ provider_identity_checked_at = datetime('now'), provider_identity_source_updated_at = ?,
81386
+ updated_at = datetime('now')
81387
+ WHERE id = ?
81388
+ `,
81389
+ args: [
81390
+ args.providerAccountId ?? null,
81391
+ args.providerAccountEmail?.toLowerCase() ?? null,
81392
+ args.providerAccountName ?? null,
81393
+ args.sourceUpdatedAt,
81394
+ args.connectionId
81395
+ ]
81396
+ });
81397
+ }
81398
+ async function setServiceConnectionUserLabel(identity, connectionId, userLabel) {
81399
+ const connection = await getOwnedServiceConnection(identity, connectionId);
81400
+ if (!connection || connection.lifecycleStatus === "disconnected") throw new Error("service_connection_not_found");
81401
+ await getDb().execute({
81402
+ sql: `UPDATE service_connections SET user_label = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`,
81403
+ args: [userLabel, connectionId, connection.userId]
81404
+ });
81405
+ return userLabel;
81406
+ }
81322
81407
  async function recordServiceConnectionHealth(args) {
81323
81408
  await ensureServiceConnectionsSchema();
81324
81409
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -84395,7 +84480,8 @@ async function nangoRequest(path6, options = {}) {
84395
84480
  headers: {
84396
84481
  authorization: `Bearer ${nangoSecret()}`,
84397
84482
  accept: "application/json",
84398
- ...options.body ? { "content-type": "application/json" } : {}
84483
+ ...options.body ? { "content-type": "application/json" } : {},
84484
+ ...options.headers
84399
84485
  },
84400
84486
  body: options.body ? JSON.stringify(options.body) : void 0,
84401
84487
  signal: AbortSignal.timeout(NANGO_TIMEOUT_MS)
@@ -84443,22 +84529,126 @@ function discoveredConnection(value) {
84443
84529
  const upstreamConnectionId = text(row?.connection_id);
84444
84530
  const providerConfigKey = text(row?.provider_config_key);
84445
84531
  if (!upstreamConnectionId || !providerConfigKey) return null;
84446
- const tags = asRecord2(row?.tags);
84447
- const endUser = asRecord2(row?.end_user);
84448
84532
  const errors = Array.isArray(row?.errors) ? row.errors : [];
84449
84533
  const authError = errors.some((error) => text(asRecord2(error)?.type) === "auth");
84450
84534
  return {
84451
84535
  upstreamConnectionId,
84452
84536
  providerConfigKey,
84453
84537
  provider: text(row?.provider) ?? providerConfigKey,
84454
- label: text(tags?.end_user_display_name) ?? text(endUser?.display_name) ?? text(tags?.end_user_email) ?? text(endUser?.email),
84538
+ // Nango's end-user fields identify the MCP Scraper tenant, not the
84539
+ // provider-side principal that authorized this connection.
84540
+ label: null,
84455
84541
  lifecycleStatus: authError ? "needs_reauth" : "connected",
84456
84542
  reconnectRequired: authError,
84457
84543
  createdAt: text(row?.created_at) ?? text(row?.created),
84458
84544
  updatedAt: text(row?.updated_at)
84459
84545
  };
84460
84546
  }
84461
- async function discoverOwnedNangoConnections(identity) {
84547
+ function scalarId(value) {
84548
+ const valueText = text(value);
84549
+ if (valueText) return valueText;
84550
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
84551
+ }
84552
+ function providerEmail(value) {
84553
+ const email = text(value)?.toLowerCase() ?? null;
84554
+ return email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
84555
+ }
84556
+ function compactProviderIdentity(identity) {
84557
+ return identity.id || identity.email || identity.name ? identity : null;
84558
+ }
84559
+ async function providerProxyJson(args) {
84560
+ return nangoRequest(`/proxy${args.path}`, {
84561
+ query: args.query,
84562
+ headers: {
84563
+ "connection-id": args.upstreamConnectionId,
84564
+ "provider-config-key": args.providerConfigKey,
84565
+ ...args.baseUrlOverride ? { "base-url-override": args.baseUrlOverride } : {}
84566
+ }
84567
+ });
84568
+ }
84569
+ async function googleProviderAccountIdentity(args) {
84570
+ const request = (path6, query, baseUrlOverride) => providerProxyJson({ ...args, path: path6, query, baseUrlOverride });
84571
+ try {
84572
+ if (args.providerConfigKey === "google-mail") {
84573
+ const payload = asRecord2(await request("/gmail/v1/users/me/profile", void 0, "https://gmail.googleapis.com"));
84574
+ const email = providerEmail(payload?.emailAddress);
84575
+ return compactProviderIdentity({ id: email, email, name: null });
84576
+ }
84577
+ if (args.providerConfigKey === "google-drive") {
84578
+ const payload = asRecord2(await request("/drive/v3/about", { fields: "user(permissionId,displayName,emailAddress)" }, "https://www.googleapis.com"));
84579
+ const user = asRecord2(payload?.user);
84580
+ return compactProviderIdentity({
84581
+ id: text(user?.permissionId),
84582
+ email: providerEmail(user?.emailAddress),
84583
+ name: text(user?.displayName)
84584
+ });
84585
+ }
84586
+ if (args.providerConfigKey === "google-calendar") {
84587
+ const payload = asRecord2(await request("/calendar/v3/calendars/primary", void 0, "https://www.googleapis.com"));
84588
+ const id = text(payload?.id);
84589
+ return compactProviderIdentity({ id, email: providerEmail(id), name: text(payload?.summary) });
84590
+ }
84591
+ if (args.providerConfigKey === "youtube") {
84592
+ const payload = asRecord2(await request("/youtube/v3/channels", { part: "snippet", mine: "true", maxResults: 1 }, "https://www.googleapis.com"));
84593
+ const first = Array.isArray(payload?.items) ? asRecord2(payload.items[0]) : null;
84594
+ return compactProviderIdentity({ id: text(first?.id), email: null, name: text(asRecord2(first?.snippet)?.title) });
84595
+ }
84596
+ } catch {
84597
+ }
84598
+ try {
84599
+ const payload = asRecord2(await request("/v1/userinfo", void 0, "https://openidconnect.googleapis.com"));
84600
+ return compactProviderIdentity({
84601
+ id: text(payload?.sub),
84602
+ email: payload?.email_verified === false ? null : providerEmail(payload?.email),
84603
+ name: text(payload?.name)
84604
+ });
84605
+ } catch {
84606
+ return null;
84607
+ }
84608
+ }
84609
+ async function getNangoProviderAccountIdentity(args) {
84610
+ if (!IDENTITY_SUPPORTED_PROVIDERS.has(args.providerConfigKey)) return null;
84611
+ if (args.providerConfigKey.startsWith("google-") || args.providerConfigKey === "youtube") {
84612
+ return googleProviderAccountIdentity(args);
84613
+ }
84614
+ try {
84615
+ if (args.providerConfigKey === "github-getting-started") {
84616
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/user" }));
84617
+ return compactProviderIdentity({ id: scalarId(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) ?? text(payload?.login) });
84618
+ }
84619
+ if (args.providerConfigKey === "slack") {
84620
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/api/auth.test" }));
84621
+ return compactProviderIdentity({ id: text(payload?.user_id), email: null, name: [text(payload?.user), text(payload?.team)].filter(Boolean).join(" \xB7 ") || null });
84622
+ }
84623
+ if (args.providerConfigKey === "zoom") {
84624
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/v2/users/me" }));
84625
+ const fullName = [text(payload?.first_name), text(payload?.last_name)].filter(Boolean).join(" ");
84626
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.display_name) ?? (fullName || null) });
84627
+ }
84628
+ if (args.providerConfigKey === "linkedin") {
84629
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/v2/userinfo" }));
84630
+ return compactProviderIdentity({ id: text(payload?.sub), email: payload?.email_verified === false ? null : providerEmail(payload?.email), name: text(payload?.name) });
84631
+ }
84632
+ if (args.providerConfigKey === "meta-marketing-api" || args.providerConfigKey === "facebook") {
84633
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/me", query: { fields: "id,name,email" } }));
84634
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) });
84635
+ }
84636
+ if (args.providerConfigKey === "twitter-v2") {
84637
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/2/users/me", query: { "user.fields": "id,name,username" } }));
84638
+ const data = asRecord2(payload?.data);
84639
+ return compactProviderIdentity({ id: text(data?.id), email: null, name: text(data?.username) ?? text(data?.name) });
84640
+ }
84641
+ if (args.providerConfigKey === "xero") {
84642
+ const payload = await providerProxyJson({ ...args, path: "/connections" });
84643
+ const first = Array.isArray(payload) ? asRecord2(payload[0]) : null;
84644
+ return compactProviderIdentity({ id: text(first?.tenantId), email: null, name: text(first?.tenantName) });
84645
+ }
84646
+ } catch {
84647
+ return null;
84648
+ }
84649
+ return null;
84650
+ }
84651
+ async function discoverOwnedNangoConnections(identity, options = {}) {
84462
84652
  const rows = [];
84463
84653
  for (let page = 0; page < NANGO_CONNECTION_MAX_PAGES; page += 1) {
84464
84654
  const payload = await nangoRequest("/connections", {
@@ -84469,6 +84659,35 @@ async function discoverOwnedNangoConnections(identity) {
84469
84659
  if (pageRows.length < NANGO_CONNECTION_PAGE_SIZE) break;
84470
84660
  }
84471
84661
  const discovered = rows.filter((row) => connectionIdentityMatches(row, identity)).map(discoveredConnection).filter((connection) => connection !== null);
84662
+ const reconciled = await reconcileDiscoveredNangoConnections(identity, discovered);
84663
+ if (options.resolveProviderIdentity === false) return reconciled;
84664
+ const remoteByKey = new Map(discovered.map((connection) => [
84665
+ `${connection.providerConfigKey}\0${connection.upstreamConnectionId}`,
84666
+ connection
84667
+ ]));
84668
+ await Promise.all(reconciled.map(async (connection) => {
84669
+ if (connection.lifecycleStatus !== "connected" || !connection.upstreamConnectionId) return;
84670
+ const remote = remoteByKey.get(`${connection.providerConfigKey}\0${connection.upstreamConnectionId}`);
84671
+ if (!remote) return;
84672
+ const sourceUpdatedAt = remote.updatedAt ?? remote.createdAt ?? connection.lifecycleStatus;
84673
+ const hasProviderIdentity = Boolean(
84674
+ connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName
84675
+ );
84676
+ const checkedAt = connection.providerIdentityCheckedAt ? Date.parse(`${connection.providerIdentityCheckedAt.replace(" ", "T")}Z`) : Number.NaN;
84677
+ const unavailableIdentityIsFresh = !hasProviderIdentity && Number.isFinite(checkedAt) && Date.now() - checkedAt < PROVIDER_IDENTITY_RETRY_MS;
84678
+ if (connection.providerIdentitySourceUpdatedAt === sourceUpdatedAt && (hasProviderIdentity || unavailableIdentityIsFresh)) return;
84679
+ const providerIdentity = await getNangoProviderAccountIdentity({
84680
+ upstreamConnectionId: connection.upstreamConnectionId,
84681
+ providerConfigKey: connection.providerConfigKey
84682
+ });
84683
+ await setServiceConnectionProviderIdentity({
84684
+ connectionId: connection.id,
84685
+ providerAccountId: providerIdentity?.id,
84686
+ providerAccountEmail: providerIdentity?.email,
84687
+ providerAccountName: providerIdentity?.name,
84688
+ sourceUpdatedAt
84689
+ });
84690
+ }));
84472
84691
  return reconcileDiscoveredNangoConnections(identity, discovered);
84473
84692
  }
84474
84693
  async function listNangoCatalogDirect() {
@@ -84731,7 +84950,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
84731
84950
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
84732
84951
  };
84733
84952
  }
84734
- var import_node_crypto68, import_client13, DEFAULT_NANGO_MCP_URL, NANGO_TIMEOUT_MS, NANGO_CONNECTION_PAGE_SIZE, NANGO_CONNECTION_MAX_PAGES, MAIN_INTEGRATION_CONTRACT_VERSION, MAIN_INTEGRATION_CONTRACT_HASH, MainNangoTransportError;
84953
+ var import_node_crypto68, import_client13, DEFAULT_NANGO_MCP_URL, NANGO_TIMEOUT_MS, NANGO_CONNECTION_PAGE_SIZE, NANGO_CONNECTION_MAX_PAGES, PROVIDER_IDENTITY_RETRY_MS, MAIN_INTEGRATION_CONTRACT_VERSION, MAIN_INTEGRATION_CONTRACT_HASH, MainNangoTransportError, IDENTITY_SUPPORTED_PROVIDERS;
84735
84954
  var init_main_nango_transport = __esm({
84736
84955
  "src/api/main-nango-transport.ts"() {
84737
84956
  "use strict";
@@ -84745,6 +84964,7 @@ var init_main_nango_transport = __esm({
84745
84964
  NANGO_TIMEOUT_MS = 2e4;
84746
84965
  NANGO_CONNECTION_PAGE_SIZE = 100;
84747
84966
  NANGO_CONNECTION_MAX_PAGES = 20;
84967
+ PROVIDER_IDENTITY_RETRY_MS = 6 * 60 * 60 * 1e3;
84748
84968
  MAIN_INTEGRATION_CONTRACT_VERSION = "2026-07-16.1";
84749
84969
  MAIN_INTEGRATION_CONTRACT_HASH = "f037859ec80c2ea2fac6e4a67c1a59f3200dd22d3642c97789b9bb3236723d3e";
84750
84970
  MainNangoTransportError = class extends Error {
@@ -84761,6 +84981,22 @@ var init_main_nango_transport = __esm({
84761
84981
  this.retryAfterMs = retryAfterMs;
84762
84982
  }
84763
84983
  };
84984
+ IDENTITY_SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set([
84985
+ "facebook",
84986
+ "github-getting-started",
84987
+ "google-analytics",
84988
+ "google-calendar",
84989
+ "google-drive",
84990
+ "google-mail",
84991
+ "google-search-console",
84992
+ "linkedin",
84993
+ "meta-marketing-api",
84994
+ "slack",
84995
+ "twitter-v2",
84996
+ "xero",
84997
+ "youtube",
84998
+ "zoom"
84999
+ ]);
84764
85000
  }
84765
85001
  });
84766
85002
 
@@ -85051,7 +85287,7 @@ async function getNangoConnections(identity, options = {}) {
85051
85287
  if (mainOwnsIntegrations()) {
85052
85288
  let connections;
85053
85289
  try {
85054
- connections = await discoverOwnedNangoConnections(identity);
85290
+ connections = await discoverOwnedNangoConnections(identity, { resolveProviderIdentity: !options.summaryOnly });
85055
85291
  } catch (error) {
85056
85292
  try {
85057
85293
  connections = await listServiceConnections(identity, "nango");
@@ -85072,11 +85308,12 @@ async function getNangoConnections(identity, options = {}) {
85072
85308
  connectionId: connection.id,
85073
85309
  providerConfigKey: connection.providerConfigKey,
85074
85310
  provider: connection.provider,
85075
- label: connection.label || connection.providerConfigKey,
85076
- providerAccountId: null,
85077
- providerAccountEmail: null,
85078
- providerAccountName: null,
85079
- providerIdentityStatus: "unavailable",
85311
+ label: connection.userLabel || connection.providerAccountEmail || connection.providerAccountName || (connection.label && connection.label.toLowerCase() !== identity.trim().toLowerCase() ? connection.label : null) || connection.providerConfigKey,
85312
+ userLabel: connection.userLabel,
85313
+ providerAccountId: connection.providerAccountId,
85314
+ providerAccountEmail: connection.providerAccountEmail,
85315
+ providerAccountName: connection.providerAccountName,
85316
+ providerIdentityStatus: connection.providerIdentityCheckedAt ? connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName ? "verified" : "unavailable" : "pending",
85080
85317
  status: connection.reconnectRequired ? "needs_reauth" : "connected",
85081
85318
  lifecycleStatus: connection.lifecycleStatus,
85082
85319
  operationalStatus: connection.operationalStatus,
@@ -85177,6 +85414,7 @@ async function getNangoConnections(identity, options = {}) {
85177
85414
  providerConfigKey,
85178
85415
  provider,
85179
85416
  label,
85417
+ userLabel: null,
85180
85418
  providerAccountId,
85181
85419
  providerAccountEmail,
85182
85420
  providerAccountName,
@@ -86952,6 +87190,7 @@ var init_server = __esm({
86952
87190
  init_scheduling_access();
86953
87191
  init_memory_db();
86954
87192
  init_nango_control();
87193
+ init_service_connections();
86955
87194
  init_resend_control();
86956
87195
  init_scheduler_integration_auth();
86957
87196
  init_connected_account_billing();
@@ -87543,6 +87782,26 @@ var init_server = __esm({
87543
87782
  return scheduleConnectionError(c, err, "Unable to load service connections.");
87544
87783
  }
87545
87784
  });
87785
+ app.patch("/schedule-connections/:id/label", auth4, async (c) => {
87786
+ const user = c.get("user");
87787
+ const body = await c.req.json().catch(() => ({}));
87788
+ if (body.label !== null && typeof body.label !== "string") {
87789
+ return c.json({ ok: false, error: "label must be a string or null." }, 400);
87790
+ }
87791
+ const label = typeof body.label === "string" ? body.label.normalize("NFKC").trim().replace(/\s+/g, " ") : null;
87792
+ if (label && label.length > 80) {
87793
+ return c.json({ ok: false, error: "label must contain at most 80 characters." }, 400);
87794
+ }
87795
+ try {
87796
+ const userLabel = await setServiceConnectionUserLabel(user.email, c.req.param("id"), label || null);
87797
+ return c.json({ ok: true, userLabel });
87798
+ } catch (err) {
87799
+ if (err instanceof Error && err.message === "service_connection_not_found") {
87800
+ return c.json({ ok: false, error: "Service connection not found." }, 404);
87801
+ }
87802
+ return scheduleConnectionError(c, err, "Unable to rename this service connection.");
87803
+ }
87804
+ });
87546
87805
  app.post("/schedule-connections/billing/reconcile", auth4, requireIntegrationsTier, async (c) => {
87547
87806
  const user = c.get("user");
87548
87807
  try {
@@ -88101,6 +88360,7 @@ var init_server = __esm({
88101
88360
  app.post("/integrations/:id/reconnect-session", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/reconnect-session`));
88102
88361
  app.post("/integrations/:id/enable-actions", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/enable-actions`));
88103
88362
  app.post("/integrations/:id/test", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/test`));
88363
+ app.patch("/integrations/:id/label", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/label`));
88104
88364
  app.delete("/integrations/:id", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}`));
88105
88365
  app.post("/integrations/actions/read", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/read"));
88106
88366
  app.post("/integrations/actions/describe", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/describe"));
@@ -17,7 +17,7 @@ loadDotEnv();
17
17
  async function main() {
18
18
  const [{ serve }, { app }, { startWorker }, { migrate }] = await Promise.all([
19
19
  import("@hono/node-server"),
20
- import("../server-Q7VYGSGQ.js"),
20
+ import("../server-YATQ3WHT.js"),
21
21
  import("../worker-UTBUWEXQ.js"),
22
22
  import("../db-4RVZ3NYZ.js")
23
23
  ]);
@@ -30,7 +30,7 @@ var import_promises5 = require("fs/promises");
30
30
  var import_node_path3 = require("path");
31
31
 
32
32
  // src/version.ts
33
- var PACKAGE_VERSION = "0.66.2";
33
+ var PACKAGE_VERSION = "0.66.4";
34
34
 
35
35
  // src/cli/agent-config.ts
36
36
  function apiKeyValue(options) {
@@ -13,7 +13,7 @@ import "../chunk-VXLU74YZ.js";
13
13
  import "../chunk-GGZEC22A.js";
14
14
  import {
15
15
  PACKAGE_VERSION
16
- } from "../chunk-ALEBMPOW.js";
16
+ } from "../chunk-EGNT3AY2.js";
17
17
 
18
18
  // src/cli/human-cli.ts
19
19
  import { Command } from "commander";
@@ -3,7 +3,7 @@
3
3
 
4
4
  // release-message.json
5
5
  var release_message_default = {
6
- message: "PAA work now starts immediately on the direct runtime, and every MCP Scraper install exposes the same complete typed tool contract."
6
+ message: "Strict MCP clients now discover all 259 tools through a compact runtime catalog, while generated SDKs retain complete output types."
7
7
  };
8
8
 
9
9
  // src/install-terminal.ts
@@ -112,7 +112,7 @@ function renderInstallTerminal(options) {
112
112
  }
113
113
 
114
114
  // src/version.ts
115
- var PACKAGE_VERSION = "0.66.2";
115
+ var PACKAGE_VERSION = "0.66.4";
116
116
 
117
117
  // bin/mcp-scraper-install.ts
118
118
  var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  renderInstallTerminal
4
- } from "../chunk-OZDCTNUV.js";
4
+ } from "../chunk-2XVDLF6H.js";
5
5
  import {
6
6
  PACKAGE_VERSION
7
- } from "../chunk-ALEBMPOW.js";
7
+ } from "../chunk-EGNT3AY2.js";
8
8
 
9
9
  // bin/mcp-scraper-install.ts
10
10
  var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
@@ -1232,7 +1232,7 @@ render();
1232
1232
  }
1233
1233
 
1234
1234
  // src/version.ts
1235
- var PACKAGE_VERSION = "0.66.2";
1235
+ var PACKAGE_VERSION = "0.66.4";
1236
1236
 
1237
1237
  // src/mcp/input-field-descriptions.ts
1238
1238
  var import_zod = require("zod");
@@ -2237,10 +2237,26 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
2237
2237
  }
2238
2238
 
2239
2239
  // src/mcp/output-schema-registry.ts
2240
+ function resolveOutputSchemaMode(env = process.env) {
2241
+ const requested = env.MCP_SCRAPER_OUTPUT_SCHEMA_MODE?.trim().toLowerCase();
2242
+ if (requested) {
2243
+ if (requested === "contract" || requested === "all") return "contract";
2244
+ if (requested === "runtime" || requested === "none" || requested === "essential") return "runtime";
2245
+ throw new Error(`Unsupported MCP_SCRAPER_OUTPUT_SCHEMA_MODE: ${requested}`);
2246
+ }
2247
+ const legacyAdvertisement = env.MCP_SCRAPER_ADVERTISE_OUTPUT_SCHEMAS?.trim().toLowerCase();
2248
+ if (legacyAdvertisement === "true") return "contract";
2249
+ if (legacyAdvertisement === void 0 || legacyAdvertisement === "" || legacyAdvertisement === "false") {
2250
+ return "runtime";
2251
+ }
2252
+ throw new Error(`Unsupported MCP_SCRAPER_ADVERTISE_OUTPUT_SCHEMAS value: ${legacyAdvertisement}`);
2253
+ }
2254
+ var OUTPUT_SCHEMA_MODE = resolveOutputSchemaMode();
2255
+ var ADVERTISE_OUTPUT_SCHEMAS = OUTPUT_SCHEMA_MODE === "contract";
2240
2256
  var OUTPUT_SCHEMAS = {};
2241
2257
  function recordOutputSchema(name, schema) {
2242
2258
  OUTPUT_SCHEMAS[name] = schema;
2243
- return schema;
2259
+ return ADVERTISE_OUTPUT_SCHEMAS ? schema : void 0;
2244
2260
  }
2245
2261
 
2246
2262
  // src/mcp/browser-agent-mcp-server.ts
@@ -8392,10 +8408,10 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
8392
8408
  artifact, call \`report_artifact_read\` with the returned artifactId and follow nextOffset until null. This
8393
8409
  works through the authenticated MCP connection even when the client cannot open the signed download URL;
8394
8410
  do not try curl or web_fetch. Use \`archive_read\` for ZIP archives.
8395
- - Before using a connected account, call \`list_service_connections\` and match the intended provider-side
8396
- identity from \`providerAccountEmail\` or \`providerAccountName\`, not the MCP Scraper login. If
8397
- \`providerIdentityStatus\` is \`unavailable\`, ask the person to refresh that connection before assuming
8398
- which external account it represents.
8411
+ - Before using a connected account, call \`list_service_connections\`. Match an explicit \`userLabel\` first,
8412
+ then verified provider-side \`providerAccountEmail\`, \`providerAccountName\`, or \`providerAccountId\`; never
8413
+ treat the MCP Scraper login as external-provider identity. If identity is pending or unavailable and no
8414
+ distinct user label exists, ask the person which connection they mean instead of assuming.
8399
8415
  - For Google Search Console, use \`export_connected_service_data\` with
8400
8416
  \`dataset:"search_console_performance"\` when the person asks for a fresh JSONL download. When they
8401
8417
  ask to schedule ingestion or repeatedly filter historical rows, create a \`connection_sync\` schedule,
@@ -12048,7 +12064,12 @@ var ListServiceConnectionsOutputSchema = {
12048
12064
  connectionId: import_zod8.z.string(),
12049
12065
  providerConfigKey: import_zod8.z.string(),
12050
12066
  provider: import_zod8.z.string().nullable().optional(),
12051
- label: import_zod8.z.string(),
12067
+ label: import_zod8.z.string().describe("Best available human-facing account label: a userLabel when set, otherwise verified provider identity, then a stable fallback."),
12068
+ userLabel: import_zod8.z.string().nullable().optional().describe("Optional tenant-owned name set in the dashboard to distinguish this connection. This is not asserted provider identity."),
12069
+ providerAccountId: import_zod8.z.string().nullable().optional().describe("Stable provider-side account, user, workspace, channel, or tenant identifier when the provider exposes one."),
12070
+ providerAccountEmail: import_zod8.z.string().email().nullable().optional().describe("Verified provider-side email when the provider exposes one. This is never inferred from the MCP Scraper login."),
12071
+ providerAccountName: import_zod8.z.string().nullable().optional().describe("Provider-side account, user, workspace, channel, or tenant name when the provider exposes one."),
12072
+ providerIdentityStatus: import_zod8.z.enum(["pending", "verified", "unavailable"]).optional().describe("Whether provider identity discovery is pending, returned at least one verified provider identifier, or completed without one."),
12052
12073
  status: import_zod8.z.string(),
12053
12074
  lifecycleStatus: import_zod8.z.enum(["pending", "connected", "needs_reauth", "disconnecting", "disconnected"]).optional().describe("Credential lifecycle. This is separate from current provider availability."),
12054
12075
  operationalStatus: import_zod8.z.enum(["unknown", "available", "degraded", "unavailable"]).optional().describe("Last observed provider transport availability. Unavailable does not imply reconnect is required."),
@@ -14362,7 +14383,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
14362
14383
  }, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
14363
14384
  server.registerTool("list_service_connections", {
14364
14385
  title: "List Connected Services",
14365
- description: "List every service connection this MCP Scraper account has authorized, including Resend, GitHub, Google Analytics, Google Search Console, YouTube, Facebook Pages, LinkedIn, X, Meta Marketing, Slack, Gmail, Calendar, Google Drive, Zoom, Xero, and others. Returns the tenant-scoped connectionId, credential transport, exact live readTools and gated actionTools, permission-aware toolCapabilities with missing OAuth-grant or provider-app-feature blockers, permanently blocked administrative tools, and schema-discovery metadata. Get a connectionId and exact tool name here before calling describe_service_connection_tool, read_service_connection, or call_service_connection_action. Managed OAuth and official remote MCP connections use the same provider-neutral bridges; mutations still require the account action switch and an exact allowed action. A scheduled Search Console connection_sync creates a typed tenant-owned performance table; after it runs, use the returned tableName with table-describe and table-query instead of repeatedly calling Google for historical filtering.",
14386
+ description: "List every service connection this MCP Scraper account has authorized, including Resend, GitHub, Google Analytics, Google Search Console, YouTube, Facebook Pages, LinkedIn, X, Meta Marketing, Slack, Gmail, Calendar, Google Drive, Zoom, Xero, and others. Returns the tenant-scoped connectionId; a tenant-owned userLabel; verified provider-side account ID, email, or name when available; credential transport; exact live readTools and gated actionTools; permission-aware toolCapabilities with missing OAuth-grant or provider-app-feature blockers; permanently blocked administrative tools; and schema-discovery metadata. Get a connectionId and exact tool name here before calling describe_service_connection_tool, read_service_connection, or call_service_connection_action. Managed OAuth and official remote MCP connections use the same provider-neutral bridges; mutations still require the account action switch and an exact allowed action. A scheduled Search Console connection_sync creates a typed tenant-owned performance table; after it runs, use the returned tableName with table-describe and table-query instead of repeatedly calling Google for historical filtering.",
14366
14387
  inputSchema: ListServiceConnectionsInputSchema,
14367
14388
  outputSchema: recordOutputSchema("list_service_connections", ListServiceConnectionsOutputSchema),
14368
14389
  annotations: { title: "List Connected Services", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
@@ -18639,7 +18660,7 @@ var HostedMemoryMcpToolExecutor = class {
18639
18660
 
18640
18661
  // release-message.json
18641
18662
  var release_message_default = {
18642
- message: "PAA work now starts immediately on the direct runtime, and every MCP Scraper install exposes the same complete typed tool contract."
18663
+ message: "Strict MCP clients now discover all 259 tools through a compact runtime catalog, while generated SDKs retain complete output types."
18643
18664
  };
18644
18665
 
18645
18666
  // src/install-terminal.ts
@@ -13,18 +13,18 @@ import {
13
13
  registerScheduledResultsMcpTools,
14
14
  registerSerpIntelligenceCaptureTools,
15
15
  resolveDeploymentProfile
16
- } from "../chunk-YKL3O7IU.js";
16
+ } from "../chunk-UPN4W4SV.js";
17
17
  import "../chunk-PGJQDMC2.js";
18
18
  import "../chunk-3EYALCYK.js";
19
19
  import "../chunk-3TKKJQB4.js";
20
20
  import {
21
21
  renderInstallTerminal
22
- } from "../chunk-OZDCTNUV.js";
22
+ } from "../chunk-2XVDLF6H.js";
23
23
  import "../chunk-VXLU74YZ.js";
24
24
  import "../chunk-GGZEC22A.js";
25
25
  import {
26
26
  PACKAGE_VERSION
27
- } from "../chunk-ALEBMPOW.js";
27
+ } from "../chunk-EGNT3AY2.js";
28
28
  import "../chunk-DNM65UCK.js";
29
29
  import "../chunk-AYJHI2FK.js";
30
30
  import "../chunk-55B3T5L2.js";
@@ -1,6 +1,6 @@
1
1
  // release-message.json
2
2
  var release_message_default = {
3
- message: "PAA work now starts immediately on the direct runtime, and every MCP Scraper install exposes the same complete typed tool contract."
3
+ message: "Strict MCP clients now discover all 259 tools through a compact runtime catalog, while generated SDKs retain complete output types."
4
4
  };
5
5
 
6
6
  // src/install-terminal.ts
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var PACKAGE_VERSION = "0.66.2";
2
+ var PACKAGE_VERSION = "0.66.4";
3
3
 
4
4
  export {
5
5
  PACKAGE_VERSION
@@ -25,7 +25,7 @@ import {
25
25
  } from "./chunk-VXLU74YZ.js";
26
26
  import {
27
27
  PACKAGE_VERSION
28
- } from "./chunk-ALEBMPOW.js";
28
+ } from "./chunk-EGNT3AY2.js";
29
29
  import {
30
30
  createPrivateArtifact,
31
31
  privateArtifactOwnerId,
@@ -580,10 +580,10 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
580
580
  artifact, call \`report_artifact_read\` with the returned artifactId and follow nextOffset until null. This
581
581
  works through the authenticated MCP connection even when the client cannot open the signed download URL;
582
582
  do not try curl or web_fetch. Use \`archive_read\` for ZIP archives.
583
- - Before using a connected account, call \`list_service_connections\` and match the intended provider-side
584
- identity from \`providerAccountEmail\` or \`providerAccountName\`, not the MCP Scraper login. If
585
- \`providerIdentityStatus\` is \`unavailable\`, ask the person to refresh that connection before assuming
586
- which external account it represents.
583
+ - Before using a connected account, call \`list_service_connections\`. Match an explicit \`userLabel\` first,
584
+ then verified provider-side \`providerAccountEmail\`, \`providerAccountName\`, or \`providerAccountId\`; never
585
+ treat the MCP Scraper login as external-provider identity. If identity is pending or unavailable and no
586
+ distinct user label exists, ask the person which connection they mean instead of assuming.
587
587
  - For Google Search Console, use \`export_connected_service_data\` with
588
588
  \`dataset:"search_console_performance"\` when the person asks for a fresh JSONL download. When they
589
589
  ask to schedule ingestion or repeatedly filter historical rows, create a \`connection_sync\` schedule,
@@ -5006,10 +5006,26 @@ ${rows}` : ""
5006
5006
  }
5007
5007
 
5008
5008
  // src/mcp/output-schema-registry.ts
5009
+ function resolveOutputSchemaMode(env = process.env) {
5010
+ const requested = env.MCP_SCRAPER_OUTPUT_SCHEMA_MODE?.trim().toLowerCase();
5011
+ if (requested) {
5012
+ if (requested === "contract" || requested === "all") return "contract";
5013
+ if (requested === "runtime" || requested === "none" || requested === "essential") return "runtime";
5014
+ throw new Error(`Unsupported MCP_SCRAPER_OUTPUT_SCHEMA_MODE: ${requested}`);
5015
+ }
5016
+ const legacyAdvertisement = env.MCP_SCRAPER_ADVERTISE_OUTPUT_SCHEMAS?.trim().toLowerCase();
5017
+ if (legacyAdvertisement === "true") return "contract";
5018
+ if (legacyAdvertisement === void 0 || legacyAdvertisement === "" || legacyAdvertisement === "false") {
5019
+ return "runtime";
5020
+ }
5021
+ throw new Error(`Unsupported MCP_SCRAPER_ADVERTISE_OUTPUT_SCHEMAS value: ${legacyAdvertisement}`);
5022
+ }
5023
+ var OUTPUT_SCHEMA_MODE = resolveOutputSchemaMode();
5024
+ var ADVERTISE_OUTPUT_SCHEMAS = OUTPUT_SCHEMA_MODE === "contract";
5009
5025
  var OUTPUT_SCHEMAS = {};
5010
5026
  function recordOutputSchema(name, schema) {
5011
5027
  OUTPUT_SCHEMAS[name] = schema;
5012
- return schema;
5028
+ return ADVERTISE_OUTPUT_SCHEMAS ? schema : void 0;
5013
5029
  }
5014
5030
 
5015
5031
  // src/mcp/meta-ad-creative-media.ts
@@ -9426,7 +9442,12 @@ var ListServiceConnectionsOutputSchema = {
9426
9442
  connectionId: z6.string(),
9427
9443
  providerConfigKey: z6.string(),
9428
9444
  provider: z6.string().nullable().optional(),
9429
- label: z6.string(),
9445
+ label: z6.string().describe("Best available human-facing account label: a userLabel when set, otherwise verified provider identity, then a stable fallback."),
9446
+ userLabel: z6.string().nullable().optional().describe("Optional tenant-owned name set in the dashboard to distinguish this connection. This is not asserted provider identity."),
9447
+ providerAccountId: z6.string().nullable().optional().describe("Stable provider-side account, user, workspace, channel, or tenant identifier when the provider exposes one."),
9448
+ providerAccountEmail: z6.string().email().nullable().optional().describe("Verified provider-side email when the provider exposes one. This is never inferred from the MCP Scraper login."),
9449
+ providerAccountName: z6.string().nullable().optional().describe("Provider-side account, user, workspace, channel, or tenant name when the provider exposes one."),
9450
+ providerIdentityStatus: z6.enum(["pending", "verified", "unavailable"]).optional().describe("Whether provider identity discovery is pending, returned at least one verified provider identifier, or completed without one."),
9430
9451
  status: z6.string(),
9431
9452
  lifecycleStatus: z6.enum(["pending", "connected", "needs_reauth", "disconnecting", "disconnected"]).optional().describe("Credential lifecycle. This is separate from current provider availability."),
9432
9453
  operationalStatus: z6.enum(["unknown", "available", "degraded", "unavailable"]).optional().describe("Last observed provider transport availability. Unavailable does not imply reconnect is required."),
@@ -11871,7 +11892,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
11871
11892
  }, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
11872
11893
  server.registerTool("list_service_connections", {
11873
11894
  title: "List Connected Services",
11874
- description: "List every service connection this MCP Scraper account has authorized, including Resend, GitHub, Google Analytics, Google Search Console, YouTube, Facebook Pages, LinkedIn, X, Meta Marketing, Slack, Gmail, Calendar, Google Drive, Zoom, Xero, and others. Returns the tenant-scoped connectionId, credential transport, exact live readTools and gated actionTools, permission-aware toolCapabilities with missing OAuth-grant or provider-app-feature blockers, permanently blocked administrative tools, and schema-discovery metadata. Get a connectionId and exact tool name here before calling describe_service_connection_tool, read_service_connection, or call_service_connection_action. Managed OAuth and official remote MCP connections use the same provider-neutral bridges; mutations still require the account action switch and an exact allowed action. A scheduled Search Console connection_sync creates a typed tenant-owned performance table; after it runs, use the returned tableName with table-describe and table-query instead of repeatedly calling Google for historical filtering.",
11895
+ description: "List every service connection this MCP Scraper account has authorized, including Resend, GitHub, Google Analytics, Google Search Console, YouTube, Facebook Pages, LinkedIn, X, Meta Marketing, Slack, Gmail, Calendar, Google Drive, Zoom, Xero, and others. Returns the tenant-scoped connectionId; a tenant-owned userLabel; verified provider-side account ID, email, or name when available; credential transport; exact live readTools and gated actionTools; permission-aware toolCapabilities with missing OAuth-grant or provider-app-feature blockers; permanently blocked administrative tools; and schema-discovery metadata. Get a connectionId and exact tool name here before calling describe_service_connection_tool, read_service_connection, or call_service_connection_action. Managed OAuth and official remote MCP connections use the same provider-neutral bridges; mutations still require the account action switch and an exact allowed action. A scheduled Search Console connection_sync creates a typed tenant-owned performance table; after it runs, use the returned tableName with table-describe and table-query instead of repeatedly calling Google for historical filtering.",
11875
11896
  inputSchema: ListServiceConnectionsInputSchema,
11876
11897
  outputSchema: recordOutputSchema("list_service_connections", ListServiceConnectionsOutputSchema),
11877
11898
  annotations: { title: "List Connected Services", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
@@ -199,7 +199,7 @@ import {
199
199
  resolveDeploymentProfile,
200
200
  resolveLocalSourcebookSchemaType,
201
201
  transcribeMediaUrl
202
- } from "./chunk-YKL3O7IU.js";
202
+ } from "./chunk-UPN4W4SV.js";
203
203
  import {
204
204
  auditImageUrls,
205
205
  auditImages,
@@ -256,7 +256,7 @@ import {
256
256
  } from "./chunk-GGZEC22A.js";
257
257
  import {
258
258
  PACKAGE_VERSION
259
- } from "./chunk-ALEBMPOW.js";
259
+ } from "./chunk-EGNT3AY2.js";
260
260
  import {
261
261
  SITE_EXTRACT_ARTIFACT_PREFIX,
262
262
  abandonExtractSettlement,
@@ -41109,6 +41109,12 @@ function ensureServiceConnectionsSchema() {
41109
41109
  transport TEXT NOT NULL,
41110
41110
  upstream_connection_id TEXT,
41111
41111
  label TEXT,
41112
+ user_label TEXT,
41113
+ provider_account_id TEXT,
41114
+ provider_account_email TEXT,
41115
+ provider_account_name TEXT,
41116
+ provider_identity_checked_at TEXT,
41117
+ provider_identity_source_updated_at TEXT,
41112
41118
  lifecycle_status TEXT NOT NULL DEFAULT 'pending',
41113
41119
  operational_status TEXT NOT NULL DEFAULT 'unknown',
41114
41120
  reconnect_required INTEGER NOT NULL DEFAULT 0,
@@ -41130,6 +41136,30 @@ function ensureServiceConnectionsSchema() {
41130
41136
  UNIQUE(user_id, provider_config_key, upstream_connection_id)
41131
41137
  )
41132
41138
  `);
41139
+ try {
41140
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN user_label TEXT`);
41141
+ } catch {
41142
+ }
41143
+ try {
41144
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_id TEXT`);
41145
+ } catch {
41146
+ }
41147
+ try {
41148
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_email TEXT`);
41149
+ } catch {
41150
+ }
41151
+ try {
41152
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_name TEXT`);
41153
+ } catch {
41154
+ }
41155
+ try {
41156
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_identity_checked_at TEXT`);
41157
+ } catch {
41158
+ }
41159
+ try {
41160
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_identity_source_updated_at TEXT`);
41161
+ } catch {
41162
+ }
41133
41163
  await db.execute(`CREATE INDEX IF NOT EXISTS service_connections_user_updated ON service_connections(user_id, updated_at DESC)`);
41134
41164
  await db.execute(`CREATE INDEX IF NOT EXISTS service_connections_upstream ON service_connections(provider_config_key, upstream_connection_id)`);
41135
41165
  await db.execute(`
@@ -41190,6 +41220,12 @@ function rowToRecord(row) {
41190
41220
  transport: row.transport === "remote_mcp" ? "remote_mcp" : "nango",
41191
41221
  upstreamConnectionId: typeof row.upstream_connection_id === "string" ? row.upstream_connection_id : null,
41192
41222
  label: typeof row.label === "string" ? row.label : null,
41223
+ userLabel: typeof row.user_label === "string" ? row.user_label : null,
41224
+ providerAccountId: typeof row.provider_account_id === "string" ? row.provider_account_id : null,
41225
+ providerAccountEmail: typeof row.provider_account_email === "string" ? row.provider_account_email : null,
41226
+ providerAccountName: typeof row.provider_account_name === "string" ? row.provider_account_name : null,
41227
+ providerIdentityCheckedAt: typeof row.provider_identity_checked_at === "string" ? row.provider_identity_checked_at : null,
41228
+ providerIdentitySourceUpdatedAt: typeof row.provider_identity_source_updated_at === "string" ? row.provider_identity_source_updated_at : null,
41193
41229
  lifecycleStatus: String(row.lifecycle_status),
41194
41230
  operationalStatus: String(row.operational_status),
41195
41231
  reconnectRequired: Number(row.reconnect_required) === 1,
@@ -41275,6 +41311,34 @@ async function updateServiceConnectionTools(connectionId, readTools, actionTools
41275
41311
  args: [JSON.stringify(readTools), JSON.stringify(actionTools), toolRevision, connectionId]
41276
41312
  });
41277
41313
  }
41314
+ async function setServiceConnectionProviderIdentity(args) {
41315
+ await ensureServiceConnectionsSchema();
41316
+ await getDb().execute({
41317
+ sql: `
41318
+ UPDATE service_connections SET
41319
+ provider_account_id = ?, provider_account_email = ?, provider_account_name = ?,
41320
+ provider_identity_checked_at = datetime('now'), provider_identity_source_updated_at = ?,
41321
+ updated_at = datetime('now')
41322
+ WHERE id = ?
41323
+ `,
41324
+ args: [
41325
+ args.providerAccountId ?? null,
41326
+ args.providerAccountEmail?.toLowerCase() ?? null,
41327
+ args.providerAccountName ?? null,
41328
+ args.sourceUpdatedAt,
41329
+ args.connectionId
41330
+ ]
41331
+ });
41332
+ }
41333
+ async function setServiceConnectionUserLabel(identity, connectionId, userLabel) {
41334
+ const connection = await getOwnedServiceConnection(identity, connectionId);
41335
+ if (!connection || connection.lifecycleStatus === "disconnected") throw new Error("service_connection_not_found");
41336
+ await getDb().execute({
41337
+ sql: `UPDATE service_connections SET user_label = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`,
41338
+ args: [userLabel, connectionId, connection.userId]
41339
+ });
41340
+ return userLabel;
41341
+ }
41278
41342
  async function recordServiceConnectionHealth(args) {
41279
41343
  await ensureServiceConnectionsSchema();
41280
41344
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -44114,6 +44178,7 @@ var DEFAULT_NANGO_MCP_URL = "https://api.nango.dev/mcp";
44114
44178
  var NANGO_TIMEOUT_MS = 2e4;
44115
44179
  var NANGO_CONNECTION_PAGE_SIZE = 100;
44116
44180
  var NANGO_CONNECTION_MAX_PAGES = 20;
44181
+ var PROVIDER_IDENTITY_RETRY_MS = 6 * 60 * 60 * 1e3;
44117
44182
  var MAIN_INTEGRATION_CONTRACT_VERSION = "2026-07-16.1";
44118
44183
  var MAIN_INTEGRATION_CONTRACT_HASH = "f037859ec80c2ea2fac6e4a67c1a59f3200dd22d3642c97789b9bb3236723d3e";
44119
44184
  var MainNangoTransportError = class extends Error {
@@ -44217,7 +44282,8 @@ async function nangoRequest(path5, options = {}) {
44217
44282
  headers: {
44218
44283
  authorization: `Bearer ${nangoSecret()}`,
44219
44284
  accept: "application/json",
44220
- ...options.body ? { "content-type": "application/json" } : {}
44285
+ ...options.body ? { "content-type": "application/json" } : {},
44286
+ ...options.headers
44221
44287
  },
44222
44288
  body: options.body ? JSON.stringify(options.body) : void 0,
44223
44289
  signal: AbortSignal.timeout(NANGO_TIMEOUT_MS)
@@ -44265,22 +44331,142 @@ function discoveredConnection(value) {
44265
44331
  const upstreamConnectionId = text(row?.connection_id);
44266
44332
  const providerConfigKey = text(row?.provider_config_key);
44267
44333
  if (!upstreamConnectionId || !providerConfigKey) return null;
44268
- const tags = asRecord(row?.tags);
44269
- const endUser = asRecord(row?.end_user);
44270
44334
  const errors = Array.isArray(row?.errors) ? row.errors : [];
44271
44335
  const authError = errors.some((error) => text(asRecord(error)?.type) === "auth");
44272
44336
  return {
44273
44337
  upstreamConnectionId,
44274
44338
  providerConfigKey,
44275
44339
  provider: text(row?.provider) ?? providerConfigKey,
44276
- label: text(tags?.end_user_display_name) ?? text(endUser?.display_name) ?? text(tags?.end_user_email) ?? text(endUser?.email),
44340
+ // Nango's end-user fields identify the MCP Scraper tenant, not the
44341
+ // provider-side principal that authorized this connection.
44342
+ label: null,
44277
44343
  lifecycleStatus: authError ? "needs_reauth" : "connected",
44278
44344
  reconnectRequired: authError,
44279
44345
  createdAt: text(row?.created_at) ?? text(row?.created),
44280
44346
  updatedAt: text(row?.updated_at)
44281
44347
  };
44282
44348
  }
44283
- async function discoverOwnedNangoConnections(identity) {
44349
+ var IDENTITY_SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set([
44350
+ "facebook",
44351
+ "github-getting-started",
44352
+ "google-analytics",
44353
+ "google-calendar",
44354
+ "google-drive",
44355
+ "google-mail",
44356
+ "google-search-console",
44357
+ "linkedin",
44358
+ "meta-marketing-api",
44359
+ "slack",
44360
+ "twitter-v2",
44361
+ "xero",
44362
+ "youtube",
44363
+ "zoom"
44364
+ ]);
44365
+ function scalarId(value) {
44366
+ const valueText = text(value);
44367
+ if (valueText) return valueText;
44368
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
44369
+ }
44370
+ function providerEmail(value) {
44371
+ const email = text(value)?.toLowerCase() ?? null;
44372
+ return email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
44373
+ }
44374
+ function compactProviderIdentity(identity) {
44375
+ return identity.id || identity.email || identity.name ? identity : null;
44376
+ }
44377
+ async function providerProxyJson(args) {
44378
+ return nangoRequest(`/proxy${args.path}`, {
44379
+ query: args.query,
44380
+ headers: {
44381
+ "connection-id": args.upstreamConnectionId,
44382
+ "provider-config-key": args.providerConfigKey,
44383
+ ...args.baseUrlOverride ? { "base-url-override": args.baseUrlOverride } : {}
44384
+ }
44385
+ });
44386
+ }
44387
+ async function googleProviderAccountIdentity(args) {
44388
+ const request = (path5, query, baseUrlOverride) => providerProxyJson({ ...args, path: path5, query, baseUrlOverride });
44389
+ try {
44390
+ if (args.providerConfigKey === "google-mail") {
44391
+ const payload = asRecord(await request("/gmail/v1/users/me/profile", void 0, "https://gmail.googleapis.com"));
44392
+ const email = providerEmail(payload?.emailAddress);
44393
+ return compactProviderIdentity({ id: email, email, name: null });
44394
+ }
44395
+ if (args.providerConfigKey === "google-drive") {
44396
+ const payload = asRecord(await request("/drive/v3/about", { fields: "user(permissionId,displayName,emailAddress)" }, "https://www.googleapis.com"));
44397
+ const user = asRecord(payload?.user);
44398
+ return compactProviderIdentity({
44399
+ id: text(user?.permissionId),
44400
+ email: providerEmail(user?.emailAddress),
44401
+ name: text(user?.displayName)
44402
+ });
44403
+ }
44404
+ if (args.providerConfigKey === "google-calendar") {
44405
+ const payload = asRecord(await request("/calendar/v3/calendars/primary", void 0, "https://www.googleapis.com"));
44406
+ const id = text(payload?.id);
44407
+ return compactProviderIdentity({ id, email: providerEmail(id), name: text(payload?.summary) });
44408
+ }
44409
+ if (args.providerConfigKey === "youtube") {
44410
+ const payload = asRecord(await request("/youtube/v3/channels", { part: "snippet", mine: "true", maxResults: 1 }, "https://www.googleapis.com"));
44411
+ const first = Array.isArray(payload?.items) ? asRecord(payload.items[0]) : null;
44412
+ return compactProviderIdentity({ id: text(first?.id), email: null, name: text(asRecord(first?.snippet)?.title) });
44413
+ }
44414
+ } catch {
44415
+ }
44416
+ try {
44417
+ const payload = asRecord(await request("/v1/userinfo", void 0, "https://openidconnect.googleapis.com"));
44418
+ return compactProviderIdentity({
44419
+ id: text(payload?.sub),
44420
+ email: payload?.email_verified === false ? null : providerEmail(payload?.email),
44421
+ name: text(payload?.name)
44422
+ });
44423
+ } catch {
44424
+ return null;
44425
+ }
44426
+ }
44427
+ async function getNangoProviderAccountIdentity(args) {
44428
+ if (!IDENTITY_SUPPORTED_PROVIDERS.has(args.providerConfigKey)) return null;
44429
+ if (args.providerConfigKey.startsWith("google-") || args.providerConfigKey === "youtube") {
44430
+ return googleProviderAccountIdentity(args);
44431
+ }
44432
+ try {
44433
+ if (args.providerConfigKey === "github-getting-started") {
44434
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/user" }));
44435
+ return compactProviderIdentity({ id: scalarId(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) ?? text(payload?.login) });
44436
+ }
44437
+ if (args.providerConfigKey === "slack") {
44438
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/api/auth.test" }));
44439
+ return compactProviderIdentity({ id: text(payload?.user_id), email: null, name: [text(payload?.user), text(payload?.team)].filter(Boolean).join(" \xB7 ") || null });
44440
+ }
44441
+ if (args.providerConfigKey === "zoom") {
44442
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/v2/users/me" }));
44443
+ const fullName = [text(payload?.first_name), text(payload?.last_name)].filter(Boolean).join(" ");
44444
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.display_name) ?? (fullName || null) });
44445
+ }
44446
+ if (args.providerConfigKey === "linkedin") {
44447
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/v2/userinfo" }));
44448
+ return compactProviderIdentity({ id: text(payload?.sub), email: payload?.email_verified === false ? null : providerEmail(payload?.email), name: text(payload?.name) });
44449
+ }
44450
+ if (args.providerConfigKey === "meta-marketing-api" || args.providerConfigKey === "facebook") {
44451
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/me", query: { fields: "id,name,email" } }));
44452
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) });
44453
+ }
44454
+ if (args.providerConfigKey === "twitter-v2") {
44455
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/2/users/me", query: { "user.fields": "id,name,username" } }));
44456
+ const data = asRecord(payload?.data);
44457
+ return compactProviderIdentity({ id: text(data?.id), email: null, name: text(data?.username) ?? text(data?.name) });
44458
+ }
44459
+ if (args.providerConfigKey === "xero") {
44460
+ const payload = await providerProxyJson({ ...args, path: "/connections" });
44461
+ const first = Array.isArray(payload) ? asRecord(payload[0]) : null;
44462
+ return compactProviderIdentity({ id: text(first?.tenantId), email: null, name: text(first?.tenantName) });
44463
+ }
44464
+ } catch {
44465
+ return null;
44466
+ }
44467
+ return null;
44468
+ }
44469
+ async function discoverOwnedNangoConnections(identity, options = {}) {
44284
44470
  const rows = [];
44285
44471
  for (let page = 0; page < NANGO_CONNECTION_MAX_PAGES; page += 1) {
44286
44472
  const payload = await nangoRequest("/connections", {
@@ -44291,6 +44477,35 @@ async function discoverOwnedNangoConnections(identity) {
44291
44477
  if (pageRows.length < NANGO_CONNECTION_PAGE_SIZE) break;
44292
44478
  }
44293
44479
  const discovered = rows.filter((row) => connectionIdentityMatches(row, identity)).map(discoveredConnection).filter((connection) => connection !== null);
44480
+ const reconciled = await reconcileDiscoveredNangoConnections(identity, discovered);
44481
+ if (options.resolveProviderIdentity === false) return reconciled;
44482
+ const remoteByKey = new Map(discovered.map((connection) => [
44483
+ `${connection.providerConfigKey}\0${connection.upstreamConnectionId}`,
44484
+ connection
44485
+ ]));
44486
+ await Promise.all(reconciled.map(async (connection) => {
44487
+ if (connection.lifecycleStatus !== "connected" || !connection.upstreamConnectionId) return;
44488
+ const remote = remoteByKey.get(`${connection.providerConfigKey}\0${connection.upstreamConnectionId}`);
44489
+ if (!remote) return;
44490
+ const sourceUpdatedAt = remote.updatedAt ?? remote.createdAt ?? connection.lifecycleStatus;
44491
+ const hasProviderIdentity = Boolean(
44492
+ connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName
44493
+ );
44494
+ const checkedAt = connection.providerIdentityCheckedAt ? Date.parse(`${connection.providerIdentityCheckedAt.replace(" ", "T")}Z`) : Number.NaN;
44495
+ const unavailableIdentityIsFresh = !hasProviderIdentity && Number.isFinite(checkedAt) && Date.now() - checkedAt < PROVIDER_IDENTITY_RETRY_MS;
44496
+ if (connection.providerIdentitySourceUpdatedAt === sourceUpdatedAt && (hasProviderIdentity || unavailableIdentityIsFresh)) return;
44497
+ const providerIdentity = await getNangoProviderAccountIdentity({
44498
+ upstreamConnectionId: connection.upstreamConnectionId,
44499
+ providerConfigKey: connection.providerConfigKey
44500
+ });
44501
+ await setServiceConnectionProviderIdentity({
44502
+ connectionId: connection.id,
44503
+ providerAccountId: providerIdentity?.id,
44504
+ providerAccountEmail: providerIdentity?.email,
44505
+ providerAccountName: providerIdentity?.name,
44506
+ sourceUpdatedAt
44507
+ });
44508
+ }));
44294
44509
  return reconcileDiscoveredNangoConnections(identity, discovered);
44295
44510
  }
44296
44511
  async function listNangoCatalogDirect() {
@@ -44965,7 +45180,7 @@ async function getNangoConnections(identity, options = {}) {
44965
45180
  if (mainOwnsIntegrations()) {
44966
45181
  let connections;
44967
45182
  try {
44968
- connections = await discoverOwnedNangoConnections(identity);
45183
+ connections = await discoverOwnedNangoConnections(identity, { resolveProviderIdentity: !options.summaryOnly });
44969
45184
  } catch (error) {
44970
45185
  try {
44971
45186
  connections = await listServiceConnections(identity, "nango");
@@ -44986,11 +45201,12 @@ async function getNangoConnections(identity, options = {}) {
44986
45201
  connectionId: connection.id,
44987
45202
  providerConfigKey: connection.providerConfigKey,
44988
45203
  provider: connection.provider,
44989
- label: connection.label || connection.providerConfigKey,
44990
- providerAccountId: null,
44991
- providerAccountEmail: null,
44992
- providerAccountName: null,
44993
- providerIdentityStatus: "unavailable",
45204
+ label: connection.userLabel || connection.providerAccountEmail || connection.providerAccountName || (connection.label && connection.label.toLowerCase() !== identity.trim().toLowerCase() ? connection.label : null) || connection.providerConfigKey,
45205
+ userLabel: connection.userLabel,
45206
+ providerAccountId: connection.providerAccountId,
45207
+ providerAccountEmail: connection.providerAccountEmail,
45208
+ providerAccountName: connection.providerAccountName,
45209
+ providerIdentityStatus: connection.providerIdentityCheckedAt ? connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName ? "verified" : "unavailable" : "pending",
44994
45210
  status: connection.reconnectRequired ? "needs_reauth" : "connected",
44995
45211
  lifecycleStatus: connection.lifecycleStatus,
44996
45212
  operationalStatus: connection.operationalStatus,
@@ -45091,6 +45307,7 @@ async function getNangoConnections(identity, options = {}) {
45091
45307
  providerConfigKey,
45092
45308
  provider,
45093
45309
  label,
45310
+ userLabel: null,
45094
45311
  providerAccountId,
45095
45312
  providerAccountEmail,
45096
45313
  providerAccountName,
@@ -46932,6 +47149,26 @@ app.get("/schedule-connections", auth4, async (c) => {
46932
47149
  return scheduleConnectionError(c, err, "Unable to load service connections.");
46933
47150
  }
46934
47151
  });
47152
+ app.patch("/schedule-connections/:id/label", auth4, async (c) => {
47153
+ const user = c.get("user");
47154
+ const body = await c.req.json().catch(() => ({}));
47155
+ if (body.label !== null && typeof body.label !== "string") {
47156
+ return c.json({ ok: false, error: "label must be a string or null." }, 400);
47157
+ }
47158
+ const label = typeof body.label === "string" ? body.label.normalize("NFKC").trim().replace(/\s+/g, " ") : null;
47159
+ if (label && label.length > 80) {
47160
+ return c.json({ ok: false, error: "label must contain at most 80 characters." }, 400);
47161
+ }
47162
+ try {
47163
+ const userLabel = await setServiceConnectionUserLabel(user.email, c.req.param("id"), label || null);
47164
+ return c.json({ ok: true, userLabel });
47165
+ } catch (err) {
47166
+ if (err instanceof Error && err.message === "service_connection_not_found") {
47167
+ return c.json({ ok: false, error: "Service connection not found." }, 404);
47168
+ }
47169
+ return scheduleConnectionError(c, err, "Unable to rename this service connection.");
47170
+ }
47171
+ });
46935
47172
  app.post("/schedule-connections/billing/reconcile", auth4, requireIntegrationsTier, async (c) => {
46936
47173
  const user = c.get("user");
46937
47174
  try {
@@ -47518,6 +47755,7 @@ app.post("/integrations/billing/reconcile", (c) => forwardIntegrationAlias(c, "/
47518
47755
  app.post("/integrations/:id/reconnect-session", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/reconnect-session`));
47519
47756
  app.post("/integrations/:id/enable-actions", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/enable-actions`));
47520
47757
  app.post("/integrations/:id/test", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/test`));
47758
+ app.patch("/integrations/:id/label", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/label`));
47521
47759
  app.delete("/integrations/:id", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}`));
47522
47760
  app.post("/integrations/actions/read", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/read"));
47523
47761
  app.post("/integrations/actions/describe", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/describe"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-scraper",
3
- "version": "0.66.2",
3
+ "version": "0.66.4",
4
4
  "description": "MCP server for MCP Scraper web intelligence tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -37,6 +37,7 @@
37
37
  "build": "tsup",
38
38
  "build:md": "node scripts/build-md-mirror.mjs",
39
39
  "build:mcpb": "npm run build && node scripts/build-mcpb.mjs",
40
+ "build:mcpb:candidate": "npm run build && node scripts/build-mcpb.mjs --candidate",
40
41
  "build:rates": "node --import tsx scripts/gen-rates.ts",
41
42
  "build:geotargets": "node --import tsx scripts/gen-geotargets.ts",
42
43
  "build:blog": "node scripts/run-ts-sandbox-safe.mjs scripts/build-blog.ts",
@@ -79,7 +80,8 @@
79
80
  "db:migrate": "node --import tsx scripts/run-core-migration.ts",
80
81
  "analytics:migrate": "node --import tsx scripts/run-analytics-migration.ts",
81
82
  "analytics:rollups": "node --import tsx scripts/run-analytics-rollups.ts",
82
- "mcp:verify": "npm run build && npm run mcp:manifest && npm run build:rates && npm run mcp:measure-catalog && npm run mcp:lint-tools && npm run typecheck && npm test && npm run package:verify-entrypoints",
83
+ "test:mcp-output-contract": "vitest run --config vitest.mcp-output-contract.config.ts tests/unit/golden-payloads.test.ts tests/unit/mcp-formatter-structured.test.ts tests/unit/mcp-2026-stdio.test.ts",
84
+ "mcp:verify": "npm run build && npm run mcp:manifest && npm run build:rates && npm run mcp:measure-catalog && npm run mcp:lint-tools && npm run test:mcp-output-contract && npm run typecheck && npm test && npm run package:verify-entrypoints",
83
85
  "prepack": "npm run mcp:verify",
84
86
  "package:verify-entrypoints": "node scripts/verify-package-entrypoints.mjs"
85
87
  },