mcp-scraper 0.66.3 → 0.66.5

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,23 @@ All notable changes to MCP Scraper are documented here. The format is based on [
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.66.5] - 2026-08-25
8
+
9
+ ### Fixed
10
+
11
+ - Local Sourcebook submissions now distinguish accepted processing from publication, tell MCP clients to keep checking until the run is terminal, and return a plain-language completion message with the exact live listing URL.
12
+
13
+ ## [0.66.4] - 2026-08-25
14
+
15
+ ### Added
16
+
17
+ - 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.
18
+ - Managed OAuth connections now retain non-secret provider identity metadata and support owner-scoped account names through the dashboard API.
19
+
20
+ ### Fixed
21
+
22
+ - 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.
23
+
7
24
  ## [0.66.3] - 2026-08-25
8
25
 
9
26
  ### Changed
@@ -1098,7 +1115,8 @@ All notable changes to MCP Scraper are documented here. The format is based on [
1098
1115
  - Write actions remain unavailable until the account owner explicitly enables them.
1099
1116
  - Provider-specific connection data is normalized into one agent-facing contract.
1100
1117
 
1101
- [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.3...HEAD
1118
+ [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.4...HEAD
1119
+ [0.66.4]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.3...v0.66.4
1102
1120
  [0.66.3]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.2...v0.66.3
1103
1121
  [0.66.2]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.1...v0.66.2
1104
1122
  [0.66.1]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.66.0...v0.66.1
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.3`, SHA-256 `353b034b878340971fc1e07ca2ccf11578d6fd2e2ff894c8da1fa7df2c2bbd2d`). 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.5`, SHA-256 `958313b3c6a41b73a725bac75770f5d429d6a4969966f3051c86d51087f76993`). 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
 
@@ -33148,12 +33148,39 @@ function localSourcebookPublicUrls(category, state, slug4) {
33148
33148
  };
33149
33149
  }
33150
33150
  function localSourcebookPublicationLinks(submission) {
33151
+ const isLive = submission.publishedRevision !== null && submission.status !== "rejected" && submission.status !== "unpublished";
33151
33152
  return {
33152
33153
  ...localSourcebookPublicUrls(submission.category, submission.state, submission.slug),
33153
33154
  publicationStatus: submission.status,
33154
- isLive: submission.status === "published" && submission.publishedRevision !== null
33155
+ isLive
33155
33156
  };
33156
33157
  }
33158
+ function localSourcebookPublicationMessage(submission) {
33159
+ const { profileUrl } = localSourcebookPublicUrls(submission.category, submission.state, submission.slug);
33160
+ if (submission.status === "published" && submission.publishedRevision !== null) {
33161
+ return `Your Local Sourcebook listing is live: ${profileUrl}`;
33162
+ }
33163
+ if (submission.status === "failed") {
33164
+ if (submission.publishedRevision !== null) {
33165
+ return `We couldn't refresh this Local Sourcebook listing, but the previous version is still live: ${profileUrl}`;
33166
+ }
33167
+ return "We couldn't publish this Local Sourcebook listing because evidence collection failed. Review the reported issue before trying again.";
33168
+ }
33169
+ if (submission.status === "rejected") {
33170
+ return "This Local Sourcebook listing was rejected and is not live.";
33171
+ }
33172
+ if (submission.status === "unpublished") {
33173
+ return "This Local Sourcebook listing has been unpublished and is not live.";
33174
+ }
33175
+ if (submission.publishedRevision !== null) {
33176
+ return `We are collecting and compiling refreshed evidence. The current Local Sourcebook listing remains live: ${profileUrl}`;
33177
+ }
33178
+ return "Submission received. We are collecting and compiling evidence, so this Local Sourcebook listing is not live yet. Check the submission status again in about 10 seconds.";
33179
+ }
33180
+ function localSourcebookStatusCheck(submission) {
33181
+ const required = submission.status === "queued" || submission.status === "enriching" || submission.status === "needs_review";
33182
+ return required ? { required: true, submissionId: submission.id, pollAfterSeconds: 10 } : { required: false, submissionId: submission.id };
33183
+ }
33157
33184
  var DEFAULT_ROOT_DOMAIN, CATEGORY_PUBLIC_SLUGS;
33158
33185
  var init_local_sourcebook_public_urls = __esm({
33159
33186
  "src/api/local-sourcebook-public-urls.ts"() {
@@ -45265,7 +45292,7 @@ var init_local_sourcebook_routes = __esm({
45265
45292
  persisted = true;
45266
45293
  await persistLocalSourcebookTags(submission.id, Number(user.id), validation.normalizedTags, parsed.data.tagDecisions);
45267
45294
  const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
45268
- return c.json({ ok: true, valid: true, submission, publicUrls: localSourcebookPublicationLinks(submission), normalizedTags: validation.normalizedTags, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(identity.websiteUrl), billing: publicListingWriteBilling(billing) }, 202);
45295
+ return c.json({ ok: true, valid: true, message: localSourcebookPublicationMessage(submission), statusCheck: localSourcebookStatusCheck(submission), submission, publicUrls: localSourcebookPublicationLinks(submission), normalizedTags: validation.normalizedTags, acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(identity.websiteUrl), billing: publicListingWriteBilling(billing) }, 202);
45269
45296
  } catch (error) {
45270
45297
  if (billing && !persisted) await refundListingWrite(billing, "Local Sourcebook creation failed before persistence.").catch(() => null);
45271
45298
  const message = error instanceof Error ? error.message : String(error);
@@ -45314,7 +45341,7 @@ var init_local_sourcebook_routes = __esm({
45314
45341
  });
45315
45342
  persisted = true;
45316
45343
  const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
45317
- return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(input.websiteUrl), billing: publicListingWriteBilling(billing) }, 202);
45344
+ return c.json({ ok: true, message: localSourcebookPublicationMessage(submission), statusCheck: localSourcebookStatusCheck(submission), submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(input.websiteUrl), billing: publicListingWriteBilling(billing) }, 202);
45318
45345
  } catch (error) {
45319
45346
  if (billing && !persisted) await refundListingWrite(billing, "Local Sourcebook creation failed before persistence.").catch(() => null);
45320
45347
  return c.json({ error: "submission_conflict", message: error instanceof Error ? error.message : String(error) }, 409);
@@ -45324,7 +45351,7 @@ var init_local_sourcebook_routes = __esm({
45324
45351
  const userId = Number(c.get("user").id);
45325
45352
  const submission = await getLocalSourcebookSubmission(c.req.param("id"), userId);
45326
45353
  if (!submission) return c.json({ error: "not_found" }, 404);
45327
- return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), draft: await getLocalSourcebookDraft(submission.id, userId) });
45354
+ return c.json({ ok: true, message: localSourcebookPublicationMessage(submission), statusCheck: localSourcebookStatusCheck(submission), submission, publicUrls: localSourcebookPublicationLinks(submission), draft: await getLocalSourcebookDraft(submission.id, userId) });
45328
45355
  });
45329
45356
  localSourcebookApp.on(["PATCH", "POST"], "/submissions/:id/draft", createApiKeyAuth(), (c) => c.json({
45330
45357
  error: "evidence_refresh_required",
@@ -45359,7 +45386,7 @@ var init_local_sourcebook_routes = __esm({
45359
45386
  return c.json({ error: "not_found" }, 404);
45360
45387
  }
45361
45388
  const dispatched = await dispatchLocalSourcebookAcquisition(submission.id).catch(() => false);
45362
- return c.json({ ok: true, submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(submission.websiteUrl), billing: publicListingWriteBilling(billing) }, 202);
45389
+ return c.json({ ok: true, message: localSourcebookPublicationMessage(submission), statusCheck: localSourcebookStatusCheck(submission), submission, publicUrls: localSourcebookPublicationLinks(submission), acquisition: { queued: true, dispatched }, acquisitionPlan: acquisitionPlan(submission.websiteUrl), billing: publicListingWriteBilling(billing) }, 202);
45363
45390
  } catch (error) {
45364
45391
  if (billing) await refundListingWrite(billing, "Local Sourcebook refresh failed before persistence.").catch(() => null);
45365
45392
  return c.json({ error: "moderated_listing", message: error instanceof Error ? error.message : String(error) }, 409);
@@ -49679,7 +49706,7 @@ var PACKAGE_VERSION;
49679
49706
  var init_version = __esm({
49680
49707
  "src/version.ts"() {
49681
49708
  "use strict";
49682
- PACKAGE_VERSION = "0.66.3";
49709
+ PACKAGE_VERSION = "0.66.5";
49683
49710
  }
49684
49711
  });
49685
49712
 
@@ -49910,11 +49937,16 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
49910
49937
  - A new capture costs 10 Credits total and includes broad website, exact Maps place, maximum-accessible review,
49911
49938
  service-area, staff, and genuine-media acquisition. When compilation succeeds, that exact system-authored evidence revision
49912
49939
  publishes automatically. Never supply owner-authored services, reviews, images, or other public claims.
49940
+ - An accepted capture is still processing, not complete. While its returned \`statusCheck.required\` is true,
49941
+ wait the suggested interval and call **local_sourcebook_submission_status** again before giving the user a
49942
+ final completion answer. When it publishes, say that the listing is live and include the exact clickable
49943
+ \`publicUrls.profileUrl\`; when it fails, report the returned failure message and cause.
49913
49944
  - Read progress and the compiled record with **local_sourcebook_submission_status**. Use
49914
49945
  **local_sourcebook_refresh** only when the owner intentionally wants to pay 2 Credits for a new acquisition pass.
49915
49946
  Contract, tag, preparation, validation, and status tools are free; idempotent retries are not charged twice.
49916
- - Capture and status responses return \`publicUrls.profileUrl\` and \`publicUrls.reviewsUrl\`. Always give those
49917
- exact links to the user; do not report a listing as live without including its clickable profile URL.
49947
+ - Capture and status responses return a plain-language \`message\`, a \`statusCheck\`, and exact public URLs. Do not
49948
+ describe a queued or enriching listing as complete, and do not report a listing as live without including its
49949
+ clickable \`publicUrls.profileUrl\`.
49918
49950
  - The last published revision remains public during refresh. Administrators can reject or unpublish an
49919
49951
  exceptional record, but routine subscriber publication does not require administrator review.
49920
49952
 
@@ -49940,10 +49972,10 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
49940
49972
  artifact, call \`report_artifact_read\` with the returned artifactId and follow nextOffset until null. This
49941
49973
  works through the authenticated MCP connection even when the client cannot open the signed download URL;
49942
49974
  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.
49975
+ - Before using a connected account, call \`list_service_connections\`. Match an explicit \`userLabel\` first,
49976
+ then verified provider-side \`providerAccountEmail\`, \`providerAccountName\`, or \`providerAccountId\`; never
49977
+ treat the MCP Scraper login as external-provider identity. If identity is pending or unavailable and no
49978
+ distinct user label exists, ask the person which connection they mean instead of assuming.
49947
49979
  - For Google Search Console, use \`export_connected_service_data\` with
49948
49980
  \`dataset:"search_console_performance"\` when the person asks for a fresh JSONL download. When they
49949
49981
  ask to schedule ingestion or repeatedly filter historical rows, create a \`connection_sync\` schedule,
@@ -53853,7 +53885,12 @@ var init_mcp_tool_schemas = __esm({
53853
53885
  connectionId: import_zod45.z.string(),
53854
53886
  providerConfigKey: import_zod45.z.string(),
53855
53887
  provider: import_zod45.z.string().nullable().optional(),
53856
- label: import_zod45.z.string(),
53888
+ label: import_zod45.z.string().describe("Best available human-facing account label: a userLabel when set, otherwise verified provider identity, then a stable fallback."),
53889
+ 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."),
53890
+ providerAccountId: import_zod45.z.string().nullable().optional().describe("Stable provider-side account, user, workspace, channel, or tenant identifier when the provider exposes one."),
53891
+ 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."),
53892
+ providerAccountName: import_zod45.z.string().nullable().optional().describe("Provider-side account, user, workspace, channel, or tenant name when the provider exposes one."),
53893
+ 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."),
53857
53894
  status: import_zod45.z.string(),
53858
53895
  lifecycleStatus: import_zod45.z.enum(["pending", "connected", "needs_reauth", "disconnecting", "disconnected"]).optional().describe("Credential lifecycle. This is separate from current provider availability."),
53859
53896
  operationalStatus: import_zod45.z.enum(["unknown", "available", "degraded", "unavailable"]).optional().describe("Last observed provider transport availability. Unavailable does not imply reconnect is required."),
@@ -56205,14 +56242,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
56205
56242
  }, async (input) => executor.validateLocalSourcebookWrite(input));
56206
56243
  server.registerTool("local-sourcebook-capture", {
56207
56244
  title: "Capture Governed Local Sourcebook Listing",
56208
- description: "Strict owner-scoped LocalBusiness write path after list, contract, prepare, and validate. Creating a listing costs 10 Credits total, including website, exact-place, review, service-area, staff, genuine-media, and structured-data compilation; preparation and validation are free, and idempotent retries are not charged twice. Capture registers canonical tags and the accepted Schema.org LocalBusiness subtype, returns the exact LocalSourcebook.com profile and reviews URLs, and queues acquisition. MCP Scraper generates the final JSON-LD from source-backed evidence. A successful system-compiled evidence revision publishes automatically; owner-authored public claims and arbitrary JSON-LD are not accepted.",
56245
+ description: "Strict owner-scoped LocalBusiness write path after list, contract, prepare, and validate. Creating a listing costs 10 Credits total, including website, exact-place, review, service-area, staff, genuine-media, and structured-data compilation; preparation and validation are free, and idempotent retries are not charged twice. Capture registers canonical tags and the accepted Schema.org LocalBusiness subtype, then queues acquisition; an accepted capture is not yet live. Poll local_sourcebook_submission_status using statusCheck until it is terminal. When published, tell the user the returned message and exact publicUrls.profileUrl. MCP Scraper generates the final JSON-LD from source-backed evidence and publishes a successful system-compiled revision automatically; owner-authored public claims and arbitrary JSON-LD are not accepted.",
56209
56246
  inputSchema: LocalSourcebookCaptureInputSchema,
56210
56247
  outputSchema: recordOutputSchema("local-sourcebook-capture", LocalSourcebookOutputSchema),
56211
56248
  annotations: liveWebToolAnnotations("Capture Governed Local Sourcebook Listing")
56212
56249
  }, async (input) => executor.localSourcebookCapture(input));
56213
56250
  server.registerTool("local_sourcebook_submission_status", {
56214
56251
  title: "Local Sourcebook Submission Status",
56215
- description: "Read the caller\u2019s listing draft, enrichment coverage, revision number, publication state, and live LocalSourcebook.com profile and reviews URLs. When status is failed, acquisitionError says why \u2014 read it before retrying, since a refresh repeats the same acquisition. Coverage counters are written only on success, so zeroed counters on a failed listing do not mean the crawl never ran.",
56252
+ description: "Read the caller\u2019s listing draft, enrichment coverage, revision number, publication state, and LocalSourcebook.com profile and reviews URLs. While statusCheck.required is true, wait the returned interval and check again before giving the user a final completion answer. When published, present the returned message and exact clickable publicUrls.profileUrl. When failed, present the failure message and read acquisitionError before retrying, since a refresh repeats the same acquisition. Coverage counters are written only on success, so zeroed counters on a failed listing do not mean the crawl never ran.",
56216
56253
  inputSchema: LocalSourcebookSubmissionStatusInputSchema,
56217
56254
  outputSchema: recordOutputSchema("local_sourcebook_submission_status", LocalSourcebookOutputSchema),
56218
56255
  annotations: localPlanningToolAnnotations("Local Sourcebook Submission Status")
@@ -56357,7 +56394,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
56357
56394
  }, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
56358
56395
  server.registerTool("list_service_connections", {
56359
56396
  title: "List Connected Services",
56360
- 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.",
56397
+ 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.",
56361
56398
  inputSchema: ListServiceConnectionsInputSchema,
56362
56399
  outputSchema: recordOutputSchema("list_service_connections", ListServiceConnectionsOutputSchema),
56363
56400
  annotations: { title: "List Connected Services", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
@@ -81169,6 +81206,12 @@ function ensureServiceConnectionsSchema() {
81169
81206
  transport TEXT NOT NULL,
81170
81207
  upstream_connection_id TEXT,
81171
81208
  label TEXT,
81209
+ user_label TEXT,
81210
+ provider_account_id TEXT,
81211
+ provider_account_email TEXT,
81212
+ provider_account_name TEXT,
81213
+ provider_identity_checked_at TEXT,
81214
+ provider_identity_source_updated_at TEXT,
81172
81215
  lifecycle_status TEXT NOT NULL DEFAULT 'pending',
81173
81216
  operational_status TEXT NOT NULL DEFAULT 'unknown',
81174
81217
  reconnect_required INTEGER NOT NULL DEFAULT 0,
@@ -81190,6 +81233,30 @@ function ensureServiceConnectionsSchema() {
81190
81233
  UNIQUE(user_id, provider_config_key, upstream_connection_id)
81191
81234
  )
81192
81235
  `);
81236
+ try {
81237
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN user_label TEXT`);
81238
+ } catch {
81239
+ }
81240
+ try {
81241
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_id TEXT`);
81242
+ } catch {
81243
+ }
81244
+ try {
81245
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_email TEXT`);
81246
+ } catch {
81247
+ }
81248
+ try {
81249
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_account_name TEXT`);
81250
+ } catch {
81251
+ }
81252
+ try {
81253
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_identity_checked_at TEXT`);
81254
+ } catch {
81255
+ }
81256
+ try {
81257
+ await db.execute(`ALTER TABLE service_connections ADD COLUMN provider_identity_source_updated_at TEXT`);
81258
+ } catch {
81259
+ }
81193
81260
  await db.execute(`CREATE INDEX IF NOT EXISTS service_connections_user_updated ON service_connections(user_id, updated_at DESC)`);
81194
81261
  await db.execute(`CREATE INDEX IF NOT EXISTS service_connections_upstream ON service_connections(provider_config_key, upstream_connection_id)`);
81195
81262
  await db.execute(`
@@ -81250,6 +81317,12 @@ function rowToRecord(row) {
81250
81317
  transport: row.transport === "remote_mcp" ? "remote_mcp" : "nango",
81251
81318
  upstreamConnectionId: typeof row.upstream_connection_id === "string" ? row.upstream_connection_id : null,
81252
81319
  label: typeof row.label === "string" ? row.label : null,
81320
+ userLabel: typeof row.user_label === "string" ? row.user_label : null,
81321
+ providerAccountId: typeof row.provider_account_id === "string" ? row.provider_account_id : null,
81322
+ providerAccountEmail: typeof row.provider_account_email === "string" ? row.provider_account_email : null,
81323
+ providerAccountName: typeof row.provider_account_name === "string" ? row.provider_account_name : null,
81324
+ providerIdentityCheckedAt: typeof row.provider_identity_checked_at === "string" ? row.provider_identity_checked_at : null,
81325
+ providerIdentitySourceUpdatedAt: typeof row.provider_identity_source_updated_at === "string" ? row.provider_identity_source_updated_at : null,
81253
81326
  lifecycleStatus: String(row.lifecycle_status),
81254
81327
  operationalStatus: String(row.operational_status),
81255
81328
  reconnectRequired: Number(row.reconnect_required) === 1,
@@ -81335,6 +81408,34 @@ async function updateServiceConnectionTools(connectionId, readTools, actionTools
81335
81408
  args: [JSON.stringify(readTools), JSON.stringify(actionTools), toolRevision, connectionId]
81336
81409
  });
81337
81410
  }
81411
+ async function setServiceConnectionProviderIdentity(args) {
81412
+ await ensureServiceConnectionsSchema();
81413
+ await getDb().execute({
81414
+ sql: `
81415
+ UPDATE service_connections SET
81416
+ provider_account_id = ?, provider_account_email = ?, provider_account_name = ?,
81417
+ provider_identity_checked_at = datetime('now'), provider_identity_source_updated_at = ?,
81418
+ updated_at = datetime('now')
81419
+ WHERE id = ?
81420
+ `,
81421
+ args: [
81422
+ args.providerAccountId ?? null,
81423
+ args.providerAccountEmail?.toLowerCase() ?? null,
81424
+ args.providerAccountName ?? null,
81425
+ args.sourceUpdatedAt,
81426
+ args.connectionId
81427
+ ]
81428
+ });
81429
+ }
81430
+ async function setServiceConnectionUserLabel(identity, connectionId, userLabel) {
81431
+ const connection = await getOwnedServiceConnection(identity, connectionId);
81432
+ if (!connection || connection.lifecycleStatus === "disconnected") throw new Error("service_connection_not_found");
81433
+ await getDb().execute({
81434
+ sql: `UPDATE service_connections SET user_label = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`,
81435
+ args: [userLabel, connectionId, connection.userId]
81436
+ });
81437
+ return userLabel;
81438
+ }
81338
81439
  async function recordServiceConnectionHealth(args) {
81339
81440
  await ensureServiceConnectionsSchema();
81340
81441
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -84411,7 +84512,8 @@ async function nangoRequest(path6, options = {}) {
84411
84512
  headers: {
84412
84513
  authorization: `Bearer ${nangoSecret()}`,
84413
84514
  accept: "application/json",
84414
- ...options.body ? { "content-type": "application/json" } : {}
84515
+ ...options.body ? { "content-type": "application/json" } : {},
84516
+ ...options.headers
84415
84517
  },
84416
84518
  body: options.body ? JSON.stringify(options.body) : void 0,
84417
84519
  signal: AbortSignal.timeout(NANGO_TIMEOUT_MS)
@@ -84459,22 +84561,126 @@ function discoveredConnection(value) {
84459
84561
  const upstreamConnectionId = text(row?.connection_id);
84460
84562
  const providerConfigKey = text(row?.provider_config_key);
84461
84563
  if (!upstreamConnectionId || !providerConfigKey) return null;
84462
- const tags = asRecord2(row?.tags);
84463
- const endUser = asRecord2(row?.end_user);
84464
84564
  const errors = Array.isArray(row?.errors) ? row.errors : [];
84465
84565
  const authError = errors.some((error) => text(asRecord2(error)?.type) === "auth");
84466
84566
  return {
84467
84567
  upstreamConnectionId,
84468
84568
  providerConfigKey,
84469
84569
  provider: text(row?.provider) ?? providerConfigKey,
84470
- label: text(tags?.end_user_display_name) ?? text(endUser?.display_name) ?? text(tags?.end_user_email) ?? text(endUser?.email),
84570
+ // Nango's end-user fields identify the MCP Scraper tenant, not the
84571
+ // provider-side principal that authorized this connection.
84572
+ label: null,
84471
84573
  lifecycleStatus: authError ? "needs_reauth" : "connected",
84472
84574
  reconnectRequired: authError,
84473
84575
  createdAt: text(row?.created_at) ?? text(row?.created),
84474
84576
  updatedAt: text(row?.updated_at)
84475
84577
  };
84476
84578
  }
84477
- async function discoverOwnedNangoConnections(identity) {
84579
+ function scalarId(value) {
84580
+ const valueText = text(value);
84581
+ if (valueText) return valueText;
84582
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
84583
+ }
84584
+ function providerEmail(value) {
84585
+ const email = text(value)?.toLowerCase() ?? null;
84586
+ return email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
84587
+ }
84588
+ function compactProviderIdentity(identity) {
84589
+ return identity.id || identity.email || identity.name ? identity : null;
84590
+ }
84591
+ async function providerProxyJson(args) {
84592
+ return nangoRequest(`/proxy${args.path}`, {
84593
+ query: args.query,
84594
+ headers: {
84595
+ "connection-id": args.upstreamConnectionId,
84596
+ "provider-config-key": args.providerConfigKey,
84597
+ ...args.baseUrlOverride ? { "base-url-override": args.baseUrlOverride } : {}
84598
+ }
84599
+ });
84600
+ }
84601
+ async function googleProviderAccountIdentity(args) {
84602
+ const request = (path6, query, baseUrlOverride) => providerProxyJson({ ...args, path: path6, query, baseUrlOverride });
84603
+ try {
84604
+ if (args.providerConfigKey === "google-mail") {
84605
+ const payload = asRecord2(await request("/gmail/v1/users/me/profile", void 0, "https://gmail.googleapis.com"));
84606
+ const email = providerEmail(payload?.emailAddress);
84607
+ return compactProviderIdentity({ id: email, email, name: null });
84608
+ }
84609
+ if (args.providerConfigKey === "google-drive") {
84610
+ const payload = asRecord2(await request("/drive/v3/about", { fields: "user(permissionId,displayName,emailAddress)" }, "https://www.googleapis.com"));
84611
+ const user = asRecord2(payload?.user);
84612
+ return compactProviderIdentity({
84613
+ id: text(user?.permissionId),
84614
+ email: providerEmail(user?.emailAddress),
84615
+ name: text(user?.displayName)
84616
+ });
84617
+ }
84618
+ if (args.providerConfigKey === "google-calendar") {
84619
+ const payload = asRecord2(await request("/calendar/v3/calendars/primary", void 0, "https://www.googleapis.com"));
84620
+ const id = text(payload?.id);
84621
+ return compactProviderIdentity({ id, email: providerEmail(id), name: text(payload?.summary) });
84622
+ }
84623
+ if (args.providerConfigKey === "youtube") {
84624
+ const payload = asRecord2(await request("/youtube/v3/channels", { part: "snippet", mine: "true", maxResults: 1 }, "https://www.googleapis.com"));
84625
+ const first = Array.isArray(payload?.items) ? asRecord2(payload.items[0]) : null;
84626
+ return compactProviderIdentity({ id: text(first?.id), email: null, name: text(asRecord2(first?.snippet)?.title) });
84627
+ }
84628
+ } catch {
84629
+ }
84630
+ try {
84631
+ const payload = asRecord2(await request("/v1/userinfo", void 0, "https://openidconnect.googleapis.com"));
84632
+ return compactProviderIdentity({
84633
+ id: text(payload?.sub),
84634
+ email: payload?.email_verified === false ? null : providerEmail(payload?.email),
84635
+ name: text(payload?.name)
84636
+ });
84637
+ } catch {
84638
+ return null;
84639
+ }
84640
+ }
84641
+ async function getNangoProviderAccountIdentity(args) {
84642
+ if (!IDENTITY_SUPPORTED_PROVIDERS.has(args.providerConfigKey)) return null;
84643
+ if (args.providerConfigKey.startsWith("google-") || args.providerConfigKey === "youtube") {
84644
+ return googleProviderAccountIdentity(args);
84645
+ }
84646
+ try {
84647
+ if (args.providerConfigKey === "github-getting-started") {
84648
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/user" }));
84649
+ return compactProviderIdentity({ id: scalarId(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) ?? text(payload?.login) });
84650
+ }
84651
+ if (args.providerConfigKey === "slack") {
84652
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/api/auth.test" }));
84653
+ return compactProviderIdentity({ id: text(payload?.user_id), email: null, name: [text(payload?.user), text(payload?.team)].filter(Boolean).join(" \xB7 ") || null });
84654
+ }
84655
+ if (args.providerConfigKey === "zoom") {
84656
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/v2/users/me" }));
84657
+ const fullName = [text(payload?.first_name), text(payload?.last_name)].filter(Boolean).join(" ");
84658
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.display_name) ?? (fullName || null) });
84659
+ }
84660
+ if (args.providerConfigKey === "linkedin") {
84661
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/v2/userinfo" }));
84662
+ return compactProviderIdentity({ id: text(payload?.sub), email: payload?.email_verified === false ? null : providerEmail(payload?.email), name: text(payload?.name) });
84663
+ }
84664
+ if (args.providerConfigKey === "meta-marketing-api" || args.providerConfigKey === "facebook") {
84665
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/me", query: { fields: "id,name,email" } }));
84666
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) });
84667
+ }
84668
+ if (args.providerConfigKey === "twitter-v2") {
84669
+ const payload = asRecord2(await providerProxyJson({ ...args, path: "/2/users/me", query: { "user.fields": "id,name,username" } }));
84670
+ const data = asRecord2(payload?.data);
84671
+ return compactProviderIdentity({ id: text(data?.id), email: null, name: text(data?.username) ?? text(data?.name) });
84672
+ }
84673
+ if (args.providerConfigKey === "xero") {
84674
+ const payload = await providerProxyJson({ ...args, path: "/connections" });
84675
+ const first = Array.isArray(payload) ? asRecord2(payload[0]) : null;
84676
+ return compactProviderIdentity({ id: text(first?.tenantId), email: null, name: text(first?.tenantName) });
84677
+ }
84678
+ } catch {
84679
+ return null;
84680
+ }
84681
+ return null;
84682
+ }
84683
+ async function discoverOwnedNangoConnections(identity, options = {}) {
84478
84684
  const rows = [];
84479
84685
  for (let page = 0; page < NANGO_CONNECTION_MAX_PAGES; page += 1) {
84480
84686
  const payload = await nangoRequest("/connections", {
@@ -84485,6 +84691,35 @@ async function discoverOwnedNangoConnections(identity) {
84485
84691
  if (pageRows.length < NANGO_CONNECTION_PAGE_SIZE) break;
84486
84692
  }
84487
84693
  const discovered = rows.filter((row) => connectionIdentityMatches(row, identity)).map(discoveredConnection).filter((connection) => connection !== null);
84694
+ const reconciled = await reconcileDiscoveredNangoConnections(identity, discovered);
84695
+ if (options.resolveProviderIdentity === false) return reconciled;
84696
+ const remoteByKey = new Map(discovered.map((connection) => [
84697
+ `${connection.providerConfigKey}\0${connection.upstreamConnectionId}`,
84698
+ connection
84699
+ ]));
84700
+ await Promise.all(reconciled.map(async (connection) => {
84701
+ if (connection.lifecycleStatus !== "connected" || !connection.upstreamConnectionId) return;
84702
+ const remote = remoteByKey.get(`${connection.providerConfigKey}\0${connection.upstreamConnectionId}`);
84703
+ if (!remote) return;
84704
+ const sourceUpdatedAt = remote.updatedAt ?? remote.createdAt ?? connection.lifecycleStatus;
84705
+ const hasProviderIdentity = Boolean(
84706
+ connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName
84707
+ );
84708
+ const checkedAt = connection.providerIdentityCheckedAt ? Date.parse(`${connection.providerIdentityCheckedAt.replace(" ", "T")}Z`) : Number.NaN;
84709
+ const unavailableIdentityIsFresh = !hasProviderIdentity && Number.isFinite(checkedAt) && Date.now() - checkedAt < PROVIDER_IDENTITY_RETRY_MS;
84710
+ if (connection.providerIdentitySourceUpdatedAt === sourceUpdatedAt && (hasProviderIdentity || unavailableIdentityIsFresh)) return;
84711
+ const providerIdentity = await getNangoProviderAccountIdentity({
84712
+ upstreamConnectionId: connection.upstreamConnectionId,
84713
+ providerConfigKey: connection.providerConfigKey
84714
+ });
84715
+ await setServiceConnectionProviderIdentity({
84716
+ connectionId: connection.id,
84717
+ providerAccountId: providerIdentity?.id,
84718
+ providerAccountEmail: providerIdentity?.email,
84719
+ providerAccountName: providerIdentity?.name,
84720
+ sourceUpdatedAt
84721
+ });
84722
+ }));
84488
84723
  return reconcileDiscoveredNangoConnections(identity, discovered);
84489
84724
  }
84490
84725
  async function listNangoCatalogDirect() {
@@ -84747,7 +84982,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
84747
84982
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
84748
84983
  };
84749
84984
  }
84750
- 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;
84985
+ 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;
84751
84986
  var init_main_nango_transport = __esm({
84752
84987
  "src/api/main-nango-transport.ts"() {
84753
84988
  "use strict";
@@ -84761,6 +84996,7 @@ var init_main_nango_transport = __esm({
84761
84996
  NANGO_TIMEOUT_MS = 2e4;
84762
84997
  NANGO_CONNECTION_PAGE_SIZE = 100;
84763
84998
  NANGO_CONNECTION_MAX_PAGES = 20;
84999
+ PROVIDER_IDENTITY_RETRY_MS = 6 * 60 * 60 * 1e3;
84764
85000
  MAIN_INTEGRATION_CONTRACT_VERSION = "2026-07-16.1";
84765
85001
  MAIN_INTEGRATION_CONTRACT_HASH = "f037859ec80c2ea2fac6e4a67c1a59f3200dd22d3642c97789b9bb3236723d3e";
84766
85002
  MainNangoTransportError = class extends Error {
@@ -84777,6 +85013,22 @@ var init_main_nango_transport = __esm({
84777
85013
  this.retryAfterMs = retryAfterMs;
84778
85014
  }
84779
85015
  };
85016
+ IDENTITY_SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set([
85017
+ "facebook",
85018
+ "github-getting-started",
85019
+ "google-analytics",
85020
+ "google-calendar",
85021
+ "google-drive",
85022
+ "google-mail",
85023
+ "google-search-console",
85024
+ "linkedin",
85025
+ "meta-marketing-api",
85026
+ "slack",
85027
+ "twitter-v2",
85028
+ "xero",
85029
+ "youtube",
85030
+ "zoom"
85031
+ ]);
84780
85032
  }
84781
85033
  });
84782
85034
 
@@ -85067,7 +85319,7 @@ async function getNangoConnections(identity, options = {}) {
85067
85319
  if (mainOwnsIntegrations()) {
85068
85320
  let connections;
85069
85321
  try {
85070
- connections = await discoverOwnedNangoConnections(identity);
85322
+ connections = await discoverOwnedNangoConnections(identity, { resolveProviderIdentity: !options.summaryOnly });
85071
85323
  } catch (error) {
85072
85324
  try {
85073
85325
  connections = await listServiceConnections(identity, "nango");
@@ -85088,11 +85340,12 @@ async function getNangoConnections(identity, options = {}) {
85088
85340
  connectionId: connection.id,
85089
85341
  providerConfigKey: connection.providerConfigKey,
85090
85342
  provider: connection.provider,
85091
- label: connection.label || connection.providerConfigKey,
85092
- providerAccountId: null,
85093
- providerAccountEmail: null,
85094
- providerAccountName: null,
85095
- providerIdentityStatus: "unavailable",
85343
+ label: connection.userLabel || connection.providerAccountEmail || connection.providerAccountName || (connection.label && connection.label.toLowerCase() !== identity.trim().toLowerCase() ? connection.label : null) || connection.providerConfigKey,
85344
+ userLabel: connection.userLabel,
85345
+ providerAccountId: connection.providerAccountId,
85346
+ providerAccountEmail: connection.providerAccountEmail,
85347
+ providerAccountName: connection.providerAccountName,
85348
+ providerIdentityStatus: connection.providerIdentityCheckedAt ? connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName ? "verified" : "unavailable" : "pending",
85096
85349
  status: connection.reconnectRequired ? "needs_reauth" : "connected",
85097
85350
  lifecycleStatus: connection.lifecycleStatus,
85098
85351
  operationalStatus: connection.operationalStatus,
@@ -85193,6 +85446,7 @@ async function getNangoConnections(identity, options = {}) {
85193
85446
  providerConfigKey,
85194
85447
  provider,
85195
85448
  label,
85449
+ userLabel: null,
85196
85450
  providerAccountId,
85197
85451
  providerAccountEmail,
85198
85452
  providerAccountName,
@@ -86968,6 +87222,7 @@ var init_server = __esm({
86968
87222
  init_scheduling_access();
86969
87223
  init_memory_db();
86970
87224
  init_nango_control();
87225
+ init_service_connections();
86971
87226
  init_resend_control();
86972
87227
  init_scheduler_integration_auth();
86973
87228
  init_connected_account_billing();
@@ -87559,6 +87814,26 @@ var init_server = __esm({
87559
87814
  return scheduleConnectionError(c, err, "Unable to load service connections.");
87560
87815
  }
87561
87816
  });
87817
+ app.patch("/schedule-connections/:id/label", auth4, async (c) => {
87818
+ const user = c.get("user");
87819
+ const body = await c.req.json().catch(() => ({}));
87820
+ if (body.label !== null && typeof body.label !== "string") {
87821
+ return c.json({ ok: false, error: "label must be a string or null." }, 400);
87822
+ }
87823
+ const label = typeof body.label === "string" ? body.label.normalize("NFKC").trim().replace(/\s+/g, " ") : null;
87824
+ if (label && label.length > 80) {
87825
+ return c.json({ ok: false, error: "label must contain at most 80 characters." }, 400);
87826
+ }
87827
+ try {
87828
+ const userLabel = await setServiceConnectionUserLabel(user.email, c.req.param("id"), label || null);
87829
+ return c.json({ ok: true, userLabel });
87830
+ } catch (err) {
87831
+ if (err instanceof Error && err.message === "service_connection_not_found") {
87832
+ return c.json({ ok: false, error: "Service connection not found." }, 404);
87833
+ }
87834
+ return scheduleConnectionError(c, err, "Unable to rename this service connection.");
87835
+ }
87836
+ });
87562
87837
  app.post("/schedule-connections/billing/reconcile", auth4, requireIntegrationsTier, async (c) => {
87563
87838
  const user = c.get("user");
87564
87839
  try {
@@ -88117,6 +88392,7 @@ var init_server = __esm({
88117
88392
  app.post("/integrations/:id/reconnect-session", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/reconnect-session`));
88118
88393
  app.post("/integrations/:id/enable-actions", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/enable-actions`));
88119
88394
  app.post("/integrations/:id/test", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/test`));
88395
+ app.patch("/integrations/:id/label", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}/label`));
88120
88396
  app.delete("/integrations/:id", (c) => forwardIntegrationAlias(c, `/schedule-connections/${encodeURIComponent(c.req.param("id"))}`));
88121
88397
  app.post("/integrations/actions/read", (c) => forwardIntegrationAlias(c, "/schedule-connections/actions/read"));
88122
88398
  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-D47PNEXR.js"),
20
+ import("../server-LESUYFDI.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.3";
33
+ var PACKAGE_VERSION = "0.66.5";
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-JBFLOJGM.js";
16
+ } from "../chunk-RSFMR7O7.js";
17
17
 
18
18
  // src/cli/human-cli.ts
19
19
  import { Command } from "commander";