@daeda/mcp-pro 0.1.53 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +313 -214
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -84095,36 +84095,6 @@ async function deleteRowsForObjectTypes(connection, tableName, objectTypes) {
84095
84095
  );
84096
84096
  }
84097
84097
  }
84098
- async function readPropertyDefinitionStatesForTypes(connection, objectTypes) {
84099
- if (objectTypes.length === 0) {
84100
- return /* @__PURE__ */ new Map();
84101
- }
84102
- const states = /* @__PURE__ */ new Map();
84103
- for (const batch of chunk([...objectTypes], DELETE_BATCH_SIZE)) {
84104
- const reader = await connection.runAndReadAll(
84105
- `SELECT object_type, object_type_id, fingerprint, property_count, group_count, last_checked_at, last_changed_at, status, error
84106
- FROM property_definition_object_state
84107
- WHERE object_type IN (${buildSqlPlaceholders(batch.length)})`,
84108
- batch
84109
- );
84110
- const rows = reader.getRowObjects();
84111
- for (const row of rows) {
84112
- const parsed = PROPERTY_DEFINITION_OBJECT_STATE_ROW_SCHEMA.parse({
84113
- objectType: row.object_type,
84114
- objectTypeId: row.object_type_id,
84115
- fingerprint: row.fingerprint,
84116
- propertyCount: typeof row.property_count === "number" ? row.property_count : Number(row.property_count ?? 0),
84117
- groupCount: typeof row.group_count === "number" ? row.group_count : Number(row.group_count ?? 0),
84118
- lastCheckedAt: row.last_checked_at,
84119
- lastChangedAt: row.last_changed_at,
84120
- status: row.status,
84121
- error: row.error ?? null
84122
- });
84123
- states.set(parsed.objectType, parsed);
84124
- }
84125
- }
84126
- return states;
84127
- }
84128
84098
  async function replacePropertyDefinitionSlices(connection, objectTypes, propertyDefinitions, propertyGroups, objectStates) {
84129
84099
  await deleteRowsForObjectTypes(connection, "property_definitions", objectTypes);
84130
84100
  await deleteRowsForObjectTypes(connection, "property_groups", objectTypes);
@@ -84254,108 +84224,6 @@ async function syncAssociationSchemaSnapshot({
84254
84224
  });
84255
84225
  return { portalId };
84256
84226
  }
84257
- async function syncPropertyDefinitionsSnapshot({
84258
- masterLock,
84259
- portalId,
84260
- snapshot
84261
- }) {
84262
- assertWritable(masterLock);
84263
- const handle = await getDatabaseConnection(portalId, null);
84264
- const parsedSnapshot = PROPERTY_DEFINITIONS_SNAPSHOT_SCHEMA.parse(snapshot);
84265
- await initializeDb(handle.connection);
84266
- await ensureDataPluginSchema(handle.connection, "property-definitions");
84267
- await withTransaction(handle.connection, async () => {
84268
- await ensureTable(handle.connection, PROPERTY_DEFINITIONS_TABLE_STATEMENTS);
84269
- const skippedObjectTypes = new Set(
84270
- parsedSnapshot.partial?.skippedObjects.map((item) => item.objectType) ?? []
84271
- );
84272
- if (skippedObjectTypes.size === 0) {
84273
- await replacePluginTable(
84274
- handle.connection,
84275
- /* @__PURE__ */ new Set([
84276
- "property_definitions",
84277
- "property_groups",
84278
- "property_definition_object_state"
84279
- ]),
84280
- "property_definitions",
84281
- propertyDefinitionRowsToColumns(parsedSnapshot.propertyDefinitions)
84282
- );
84283
- await replacePluginTable(
84284
- handle.connection,
84285
- /* @__PURE__ */ new Set([
84286
- "property_definitions",
84287
- "property_groups",
84288
- "property_definition_object_state"
84289
- ]),
84290
- "property_groups",
84291
- propertyGroupRowsToColumns(parsedSnapshot.propertyGroups)
84292
- );
84293
- await replacePluginTable(
84294
- handle.connection,
84295
- /* @__PURE__ */ new Set([
84296
- "property_definitions",
84297
- "property_groups",
84298
- "property_definition_object_state"
84299
- ]),
84300
- "property_definition_object_state",
84301
- propertyDefinitionObjectStateRowsToColumns(parsedSnapshot.objectStates)
84302
- );
84303
- return;
84304
- }
84305
- const includedObjectTypes = [
84306
- ...new Set(parsedSnapshot.objectStates.map((row) => row.objectType))
84307
- ].filter((objectType) => !skippedObjectTypes.has(objectType));
84308
- const existingSkippedStates = await readPropertyDefinitionStatesForTypes(
84309
- handle.connection,
84310
- parsedSnapshot.partial?.skippedObjects.map((item) => item.objectType) ?? []
84311
- );
84312
- const mergedObjectStates = parsedSnapshot.objectStates.map((row) => {
84313
- if (!skippedObjectTypes.has(row.objectType)) {
84314
- return row;
84315
- }
84316
- const existing = existingSkippedStates.get(row.objectType);
84317
- if (!existing) {
84318
- return row;
84319
- }
84320
- return {
84321
- ...existing,
84322
- objectTypeId: row.objectTypeId,
84323
- lastCheckedAt: row.lastCheckedAt,
84324
- status: row.status,
84325
- error: row.error
84326
- };
84327
- });
84328
- await replacePropertyDefinitionSlices(
84329
- handle.connection,
84330
- includedObjectTypes,
84331
- parsedSnapshot.propertyDefinitions.filter(
84332
- (row) => !skippedObjectTypes.has(row.objectType)
84333
- ),
84334
- parsedSnapshot.propertyGroups.filter(
84335
- (row) => !skippedObjectTypes.has(row.objectType)
84336
- ),
84337
- mergedObjectStates.filter(
84338
- (row) => !skippedObjectTypes.has(row.objectType)
84339
- )
84340
- );
84341
- const skippedStates = mergedObjectStates.filter(
84342
- (row) => skippedObjectTypes.has(row.objectType)
84343
- );
84344
- if (skippedStates.length > 0) {
84345
- await deleteRowsForObjectTypes(
84346
- handle.connection,
84347
- "property_definition_object_state",
84348
- skippedStates.map((row) => row.objectType)
84349
- );
84350
- await bulkAppend(
84351
- handle.connection,
84352
- "property_definition_object_state",
84353
- propertyDefinitionObjectStateRowsToColumns(skippedStates)
84354
- );
84355
- }
84356
- });
84357
- return { portalId };
84358
- }
84359
84227
  async function applyWorkflowDelta({
84360
84228
  masterLock,
84361
84229
  portalId,
@@ -84656,6 +84524,20 @@ async function evictDatabaseConnections(portalId) {
84656
84524
  }
84657
84525
  }
84658
84526
  }
84527
+ async function evictReadOnlyDatabaseConnections(portalId) {
84528
+ const readOnlyCachePrefix = `${portalId}:read_only:`;
84529
+ const matchingEntries = [...databaseConnectionCache.entries()].filter(
84530
+ ([cacheKey]) => cacheKey.startsWith(readOnlyCachePrefix)
84531
+ );
84532
+ for (const [cacheKey, handlePromise] of matchingEntries) {
84533
+ databaseConnectionCache.delete(cacheKey);
84534
+ try {
84535
+ const handle = await handlePromise;
84536
+ await closeDatabaseHandle(handle);
84537
+ } catch {
84538
+ }
84539
+ }
84540
+ }
84659
84541
 
84660
84542
  // src/pure/query-sql.ts
84661
84543
  var isWordCharacter = (char) => char !== void 0 && /[A-Za-z0-9_]/.test(char);
@@ -84767,6 +84649,7 @@ var resolveMasterLockDependencies = (config, defaults) => ({
84767
84649
 
84768
84650
  // src/pure/master-lock-events.ts
84769
84651
  var promotionListeners = /* @__PURE__ */ new Set();
84652
+ var demotionListeners = /* @__PURE__ */ new Set();
84770
84653
  var onPromotedToMaster = (listener) => {
84771
84654
  promotionListeners.add(listener);
84772
84655
  return () => {
@@ -84782,6 +84665,21 @@ var emitPromotedToMaster = () => {
84782
84665
  }
84783
84666
  }
84784
84667
  };
84668
+ var onDemotedToReadOnly = (listener) => {
84669
+ demotionListeners.add(listener);
84670
+ return () => {
84671
+ demotionListeners.delete(listener);
84672
+ };
84673
+ };
84674
+ var emitDemotedToReadOnly = () => {
84675
+ for (const listener of demotionListeners) {
84676
+ try {
84677
+ listener();
84678
+ } catch (error) {
84679
+ console.error("[master-lock] Demotion listener failed:", error);
84680
+ }
84681
+ }
84682
+ };
84785
84683
 
84786
84684
  // src/layers/MasterLockLive.ts
84787
84685
  var PROMOTION_POLL_MS = 3e3;
@@ -84832,6 +84730,7 @@ var handleCompromisedLease = (runtime, error) => pipe19(
84832
84730
  `[master-lock] master lease compromised: ${error.message}`
84833
84731
  ),
84834
84732
  Effect30.flatMap(() => Ref.set(runtime.connectionTypeRef, "READ_ONLY")),
84733
+ Effect30.tap(() => Effect30.sync(() => emitDemotedToReadOnly())),
84835
84734
  Effect30.flatMap(() => startPromotionPolling(runtime))
84836
84735
  );
84837
84736
  })
@@ -85334,6 +85233,32 @@ var DuckDBInterfaceLiveBase = Layer4.effect(
85334
85233
  }))
85335
85234
  )
85336
85235
  );
85236
+ const setMetadataWithoutReplica = (portalId, key2, value) => withPortalWritePermit(
85237
+ portalId,
85238
+ "setMetadataWithoutReplica",
85239
+ Effect32.orDie(
85240
+ Effect32.tryPromise({
85241
+ try: () => setPortalMetadata({
85242
+ masterLock: { isMaster: true },
85243
+ portalId,
85244
+ key: key2,
85245
+ value
85246
+ }),
85247
+ catch: (error) => error
85248
+ })
85249
+ )
85250
+ );
85251
+ const publishReplica2 = (input) => withPortalWritePermit(
85252
+ input.portalId,
85253
+ "publishReplica",
85254
+ Effect32.orDie(
85255
+ replica.syncReplica({
85256
+ portalId: input.portalId,
85257
+ reason: input.reason,
85258
+ mode: input.mode
85259
+ })
85260
+ )
85261
+ );
85337
85262
  const syncObjectArtifact2 = (input) => withPortalWritePermit(
85338
85263
  input.portalId,
85339
85264
  "syncObjectArtifact",
@@ -85421,23 +85346,6 @@ var DuckDBInterfaceLiveBase = Layer4.effect(
85421
85346
  }))
85422
85347
  )
85423
85348
  );
85424
- const syncPropertyDefinitionsSnapshot2 = (input) => withPortalWritePermit(
85425
- input.portalId,
85426
- "syncPropertyDefinitionsSnapshot",
85427
- Effect32.orDie(
85428
- syncReplicaAfterWrite({
85429
- portalId: input.portalId,
85430
- reason: "syncPropertyDefinitionsSnapshot"
85431
- }, Effect32.tryPromise({
85432
- try: () => syncPropertyDefinitionsSnapshot({
85433
- masterLock: { isMaster: true },
85434
- portalId: input.portalId,
85435
- snapshot: input.snapshot
85436
- }),
85437
- catch: (error) => error
85438
- }))
85439
- )
85440
- );
85441
85349
  const applyWorkflowDelta2 = (input) => withPortalWritePermit(
85442
85350
  input.portalId,
85443
85351
  "applyWorkflowDelta",
@@ -85555,12 +85463,13 @@ var DuckDBInterfaceLiveBase = Layer4.effect(
85555
85463
  getPropertyDefinitionStateSummary: getPropertyDefinitionStateSummary2,
85556
85464
  getWorkflowStateSummary: getWorkflowStateSummary2,
85557
85465
  setMetadata: setMetadata2,
85466
+ setMetadataWithoutReplica,
85467
+ publishReplica: publishReplica2,
85558
85468
  syncObjectArtifact: syncObjectArtifact2,
85559
85469
  syncAssociationArtifact: syncAssociationArtifact2,
85560
85470
  syncPlugin: syncPlugin2,
85561
85471
  replaceWorkflowRows: replaceWorkflowRows2,
85562
85472
  syncAssociationSchemaSnapshot: syncAssociationSchemaSnapshot2,
85563
- syncPropertyDefinitionsSnapshot: syncPropertyDefinitionsSnapshot2,
85564
85473
  applyWorkflowDelta: applyWorkflowDelta2,
85565
85474
  applyPropertyDefinitionsDelta: applyPropertyDefinitionsDelta2,
85566
85475
  applyDiffBatch: applyDiffBatch2,
@@ -85570,9 +85479,13 @@ var DuckDBInterfaceLiveBase = Layer4.effect(
85570
85479
  })
85571
85480
  )
85572
85481
  );
85482
+ var SharedMasterLockLive = Layer4.orDie(MasterLockLive);
85483
+ var SharedReplicaLive = makeReplicaLive({
85484
+ masterLockLayer: SharedMasterLockLive
85485
+ });
85573
85486
  var DuckDBInterfaceLive = pipe21(
85574
85487
  DuckDBInterfaceLiveBase,
85575
- Layer4.provide(Layer4.mergeAll(Layer4.orDie(MasterLockLive), ReplicaLive))
85488
+ Layer4.provide(Layer4.mergeAll(SharedMasterLockLive, SharedReplicaLive))
85576
85489
  );
85577
85490
 
85578
85491
  // src/layers/FileDownloadLive.ts
@@ -86431,7 +86344,7 @@ var getReadConnection = (portalId, options8) => pipe25(
86431
86344
  return openLiveConnection(portalId, error);
86432
86345
  })
86433
86346
  );
86434
- var closeReadConnection = (portalId) => Effect37.promise(() => evictDatabaseConnections(portalId));
86347
+ var closeReadConnection = (portalId) => Effect37.promise(() => evictReadOnlyDatabaseConnections(portalId));
86435
86348
 
86436
86349
  // src/layers/PortalFileStateLive.ts
86437
86350
  import { Effect as Effect38, Layer as Layer6, pipe as pipe26 } from "effect";
@@ -87095,36 +87008,6 @@ var PortalDataLiveBase = Layer7.effect(
87095
87008
  )
87096
87009
  )
87097
87010
  );
87098
- const syncPropertyDefinitionsSnapshot2 = (input) => pipe27(
87099
- requireMasterConnection(input.portalId, "syncPropertyDefinitionsSnapshot"),
87100
- Effect39.flatMap(
87101
- () => pipe27(
87102
- duckDb.syncPropertyDefinitionsSnapshot({
87103
- portalId: input.portalId,
87104
- snapshot: input.snapshot
87105
- }),
87106
- Effect39.flatMap(
87107
- () => duckDb.setMetadata(
87108
- input.portalId,
87109
- "last_synced:property_definitions",
87110
- (/* @__PURE__ */ new Date()).toISOString()
87111
- )
87112
- ),
87113
- Effect39.flatMap(
87114
- () => swallowPortalFileStateError(portalFileState.touchSyncedAt(input.portalId))
87115
- ),
87116
- Effect39.map(() => ({
87117
- portalId: input.portalId
87118
- })),
87119
- Effect39.catchAllCause(
87120
- (cause) => toSyncFailure(
87121
- `Failed to sync property definitions snapshot for portal ${input.portalId}`,
87122
- cause
87123
- )
87124
- )
87125
- )
87126
- )
87127
- );
87128
87011
  const applyWorkflowDelta2 = (input) => pipe27(
87129
87012
  requireMasterConnection(input.portalId, "applyWorkflowDelta"),
87130
87013
  Effect39.flatMap(
@@ -87258,7 +87141,6 @@ var PortalDataLiveBase = Layer7.effect(
87258
87141
  syncPluginPayload,
87259
87142
  replaceWorkflowRows: replaceWorkflowRows2,
87260
87143
  syncAssociationSchemaSnapshot: syncAssociationSchemaSnapshot2,
87261
- syncPropertyDefinitionsSnapshot: syncPropertyDefinitionsSnapshot2,
87262
87144
  applyWorkflowDelta: applyWorkflowDelta2,
87263
87145
  applyPropertyDefinitionsDelta: applyPropertyDefinitionsDelta2,
87264
87146
  applyDiffBatch: applyDiffBatch2,
@@ -88883,7 +88765,9 @@ Optional: pass portalIds to run the same query across one or more specific porta
88883
88765
 
88884
88766
  Data freshness: enabled portals are automatically checked for freshness before querying. If portal sync is disabled, queries use stale local data when available and otherwise return an error.
88885
88767
 
88886
- Lightweight message-plugin metadata can still be stale even when the portal is otherwise synced. Use \`status\` to inspect plugin freshness and \`refresh_plugins\` to explicitly refresh lightweight plugin tables like lists or owners when you need the latest values.
88768
+ IMPORTANT: the automatic freshness check covers artifact-backed CRM data, but it does NOT make lightweight message-plugin tables current. Before querying plugin-backed tables such as lists, owners, pipelines, forms, inboxes, or sequences when current values matter, first inspect status(section="schema"), call refresh_plugins if needed, and wait for replica-verified completion via status(section="schema") before running the query.
88769
+
88770
+ If your task depends on both artifact-backed data and plugin-backed metadata, refresh the plugin data first, wait for completion, then run the query.
88887
88771
 
88888
88772
  IMPORTANT: Always use the 'status' tool with section "schema" first to discover available object types and property names before writing queries. Do not guess property names.
88889
88773
 
@@ -89246,7 +89130,7 @@ Optional: pass portalIds to run the same chart query across one or more specific
89246
89130
 
89247
89131
  Data freshness: enabled portals are automatically checked for freshness before charting. If portal sync is disabled, charting uses stale local data when available and otherwise returns an error.
89248
89132
 
89249
- Lightweight message-plugin metadata can still be stale even when the portal is otherwise synced. Use \`status\` to inspect plugin freshness and \`refresh_plugins\` to explicitly refresh lightweight plugin tables like lists or owners when you need the latest values.
89133
+ IMPORTANT: the automatic freshness check covers artifact-backed CRM data, but it does NOT automatically refresh lightweight message-plugin tables. Before charting plugin-backed tables such as lists, owners, pipelines, forms, inboxes, or sequences when current values matter, first inspect status(section="schema"), call refresh_plugins if needed, and wait for replica-verified completion via status(section="schema") before charting.
89250
89134
 
89251
89135
  The query results map to the chart as follows:
89252
89136
  - First column \u2192 X-axis labels (categories)
@@ -89422,6 +89306,7 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
89422
89306
  import { z as z25 } from "zod";
89423
89307
  import fs12 from "fs";
89424
89308
  import { Effect as Effect66 } from "effect";
89309
+ var PLUGIN_SYNC_METADATA_PREFIX = "last_synced:plugin:";
89425
89310
  function formatBytes(bytes) {
89426
89311
  if (bytes === 0) return "0 B";
89427
89312
  const units = ["B", "KB", "MB", "GB"];
@@ -89434,12 +89319,16 @@ function getDbFileSize(portalId) {
89434
89319
  if (!fs12.existsSync(db2)) return null;
89435
89320
  return fs12.statSync(db2).size;
89436
89321
  }
89437
- var buildLightweightPluginFreshnessSection = (portalState, now = Date.now()) => Object.fromEntries(
89322
+ var buildLightweightPluginFreshnessSection = (portalState, now = Date.now(), replicaPluginSyncMetadata = {}) => Object.fromEntries(
89438
89323
  getMessagePlugins().map((plugin) => {
89439
89324
  const pluginState = portalState?.plugins.find((entry) => entry.name === plugin.name) ?? null;
89440
- const freshness = buildPluginFreshness(pluginState?.lastSynced?.toISOString() ?? null, now);
89325
+ const replicaLastSynced = replicaPluginSyncMetadata[`${PLUGIN_SYNC_METADATA_PREFIX}${plugin.name}`] ?? null;
89326
+ const freshness = buildPluginFreshness(
89327
+ replicaLastSynced ?? pluginState?.lastSynced?.toISOString() ?? null,
89328
+ now
89329
+ );
89441
89330
  return [plugin.name, {
89442
- status: pluginState?.status ?? "NOT_STARTED",
89331
+ status: pluginState?.status ?? (replicaLastSynced === null ? "NOT_STARTED" : "SYNCED"),
89443
89332
  error: pluginState?.error ?? null,
89444
89333
  lastRefreshedAt: freshness.lastRefreshedAt,
89445
89334
  ageMs: freshness.ageMs,
@@ -89447,6 +89336,29 @@ var buildLightweightPluginFreshnessSection = (portalState, now = Date.now()) =>
89447
89336
  }];
89448
89337
  })
89449
89338
  );
89339
+ var isMissingSyncMetadataTableError2 = (error) => {
89340
+ const message = error instanceof Error ? error.message : String(error);
89341
+ return message.includes("sync_metadata") && message.includes("does not exist");
89342
+ };
89343
+ var loadReplicaPluginSyncMetadata = async (conn) => {
89344
+ try {
89345
+ const reader = await conn.runAndReadAll(
89346
+ `SELECT key, value FROM sync_metadata WHERE key LIKE '${PLUGIN_SYNC_METADATA_PREFIX}%'`
89347
+ );
89348
+ const rows = reader.getRowObjects();
89349
+ const result = {};
89350
+ for (const row of rows) {
89351
+ if (typeof row.key !== "string" || typeof row.value !== "string") continue;
89352
+ result[row.key] = row.value;
89353
+ }
89354
+ return result;
89355
+ } catch (error) {
89356
+ if (isMissingSyncMetadataTableError2(error)) {
89357
+ return {};
89358
+ }
89359
+ throw error;
89360
+ }
89361
+ };
89450
89362
  async function buildConnectionSection(deps) {
89451
89363
  const selectedPortalId2 = deps.getSelectedPortalId();
89452
89364
  const connectionState = deps.getConnectionState();
@@ -89489,7 +89401,12 @@ async function buildSchemaSection(portalId, deps) {
89489
89401
  );
89490
89402
  const allPluginTableNames = new Set(getAllTableNames());
89491
89403
  const portalState = await deps.getPortalState(portalId);
89492
- const lightweightPlugins = buildLightweightPluginFreshnessSection(portalState);
89404
+ const replicaPluginSyncMetadata = await loadReplicaPluginSyncMetadata(conn);
89405
+ const lightweightPlugins = buildLightweightPluginFreshnessSection(
89406
+ portalState,
89407
+ Date.now(),
89408
+ replicaPluginSyncMetadata
89409
+ );
89493
89410
  const sectionResults = await Promise.all(
89494
89411
  getPlugins().map(async (plugin) => {
89495
89412
  try {
@@ -89563,6 +89480,10 @@ The schema output also includes lightweight plugin freshness data so you can see
89563
89480
 
89564
89481
  Status output also includes active and recent lightweight plugin refresh jobs so callers can inspect queued, running, completed, or failed manual refresh requests without blocking on the initial tool call.
89565
89482
 
89483
+ Use status(section="schema") as the authoritative freshness gate for lightweight plugins. After calling refresh_plugins, poll status(section="schema") for the same portal until the matching refresh job is COMPLETED, none of the requested plugins are still QUEUED, RUNNING, or REPLICATING, and the requested plugins show updated replica-backed lastRefreshedAt values. COMPLETED means the executor verified the newly published read replica, not merely that remote execution finished. Do not proceed with plugin-dependent work before those conditions are met.
89484
+
89485
+ Artifact-backed CRM data and lightweight plugin metadata have different freshness rules. Artifact freshness is handled automatically for query/chart reads, but lightweight plugin freshness is not.
89486
+
89566
89487
  Optional: pass portalIds to fetch schema status for specific portals. When portalIds is provided, schema output is a JSON object keyed by portal ID.
89567
89488
 
89568
89489
  Data readiness: only portals with sync enabled are refreshed automatically. Disabled portals can still report schema from stale local data if a local DB already exists.
@@ -133956,6 +133877,8 @@ VIEWING THE PLAN:
133956
133877
 
133957
133878
  IMPORTANT: After building the plan, present it to the user and ask for confirmation before calling submit_plan.
133958
133879
 
133880
+ When building operations that depend on current lightweight metadata or IDs from local tables, such as owners, lists, pipelines, forms, inboxes, or sequences, first ensure those plugin tables are fresh. Use status(section="schema") to inspect freshness, refresh_plugins to refresh the required plugin set, and wait for replica-verified completion with status(section="schema") before resolving IDs or constructing operations that depend on them.
133881
+
133959
133882
  WORKFLOW:
133960
133883
  1. describe_operations - Discover available operation types and their required fields
133961
133884
  2. build_plan - Create draft with title, description, and operations
@@ -134614,11 +134537,17 @@ function registerRefreshPluginsTool(server2, deps) {
134614
134537
 
134615
134538
  Lightweight plugins are not kept fresh automatically in real time. Their data may become stale until you explicitly refresh them with this tool.
134616
134539
 
134617
- Use this when you need the latest lightweight metadata, such as lists, owners, pipelines, forms, inboxes, or other message-backed plugin data.
134540
+ Use this when you need current lightweight metadata, such as lists, owners, pipelines, forms, inboxes, sequences, or other message-backed plugin data.
134541
+
134542
+ Before calling this tool, first determine the full set of plugin-backed metadata you will need for the task. Prefer a single refresh_plugins call with the union of all needed pluginNames rather than separate calls per plugin.
134618
134543
 
134619
134544
  The requested pluginNames array is required. Optional portalIds lets you refresh the same plugins across multiple portals; otherwise the selected portal is used.
134620
134545
 
134621
- This tool returns immediately with a refresh job record instead of blocking until the refresh completes. Use \`status\` to inspect in-flight and recent refresh jobs.`,
134546
+ This tool starts an asynchronous refresh job and returns immediately. It does NOT wait for fresh data to be available.
134547
+
134548
+ After calling refresh_plugins, do not run any query, chart, or plan-building step that depends on those plugin tables until you confirm completion with status(section="schema"). Wait until the relevant refresh job is COMPLETED and the requested plugins show updated replica-backed freshness data. COMPLETED means the executor verified the newly published read replica, not merely that remote execution finished. If the job FAILED, surface the error instead of proceeding.
134549
+
134550
+ If the same plugin set is needed across multiple portals, prefer one call using portalIds. Use \`status\` to inspect in-flight and recent refresh jobs.`,
134622
134551
  inputSchema: {
134623
134552
  pluginNames: z97.array(z97.string()).min(1).describe("Required lightweight plugin names to refresh, for example ['lists', 'owners']."),
134624
134553
  portalIds: z97.array(z97.number()).optional().describe("Optional explicit portal IDs to refresh. If omitted, the selected portal is used.")
@@ -134793,7 +134722,7 @@ var createPluginRefreshJobStore = () => {
134793
134722
  error: payload.error ?? null,
134794
134723
  coalescedRequests: 0,
134795
134724
  plugins: buildPluginSnapshots(pluginNames),
134796
- active: payload.status === "QUEUED" || payload.status === "RUNNING"
134725
+ active: payload.status === "QUEUED" || payload.status === "RUNNING" || payload.status === "REPLICATING"
134797
134726
  };
134798
134727
  job.portalId = payload.target_portal;
134799
134728
  job.pluginNames = pluginNames;
@@ -134803,7 +134732,7 @@ var createPluginRefreshJobStore = () => {
134803
134732
  job.startedAt = payload.started_at ?? job.startedAt ?? null;
134804
134733
  job.finishedAt = payload.finished_at ?? (payload.status === "COMPLETED" || payload.status === "FAILED" ? (/* @__PURE__ */ new Date()).toISOString() : null);
134805
134734
  job.error = payload.error ?? null;
134806
- job.active = payload.status === "QUEUED" || payload.status === "RUNNING";
134735
+ job.active = payload.status === "QUEUED" || payload.status === "RUNNING" || payload.status === "REPLICATING";
134807
134736
  const pluginMap = new Map(job.plugins.map((plugin) => [plugin.pluginName, plugin]));
134808
134737
  for (const pluginName of pluginNames) {
134809
134738
  if (!pluginMap.has(pluginName)) {
@@ -134897,6 +134826,72 @@ var createPluginRefreshJobStore = () => {
134897
134826
  };
134898
134827
  };
134899
134828
 
134829
+ // src/pure/manual-plugin-refresh-job.ts
134830
+ var earliestStartedAt = (plugins2) => plugins2.map((plugin) => plugin.startedAt).filter((value) => value !== null).sort()[0] ?? null;
134831
+ var finalizeManualPluginRefreshJob = async (input) => {
134832
+ const startedAt = earliestStartedAt(input.plugins);
134833
+ try {
134834
+ await input.prepareReplicaVerification();
134835
+ } catch (error) {
134836
+ const message = input.formatError(error);
134837
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
134838
+ await input.updateJobProgress({
134839
+ jobId: input.jobId,
134840
+ portalId: input.portalId,
134841
+ pluginNames: input.pluginNames,
134842
+ createdAt: input.createdAt,
134843
+ status: "FAILED",
134844
+ plugins: input.plugins,
134845
+ startedAt,
134846
+ finishedAt,
134847
+ error: message,
134848
+ relayToServer: input.relayToServer
134849
+ });
134850
+ return;
134851
+ }
134852
+ await input.updateJobProgress({
134853
+ jobId: input.jobId,
134854
+ portalId: input.portalId,
134855
+ pluginNames: input.pluginNames,
134856
+ createdAt: input.createdAt,
134857
+ status: "REPLICATING",
134858
+ plugins: input.plugins,
134859
+ startedAt,
134860
+ relayToServer: input.relayToServer
134861
+ });
134862
+ try {
134863
+ await input.publishReplica();
134864
+ await input.verifyReplica();
134865
+ } catch (error) {
134866
+ const message = input.formatError(error);
134867
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
134868
+ await input.updateJobProgress({
134869
+ jobId: input.jobId,
134870
+ portalId: input.portalId,
134871
+ pluginNames: input.pluginNames,
134872
+ createdAt: input.createdAt,
134873
+ status: "FAILED",
134874
+ plugins: input.plugins,
134875
+ startedAt,
134876
+ finishedAt,
134877
+ error: message,
134878
+ relayToServer: input.relayToServer
134879
+ });
134880
+ return;
134881
+ }
134882
+ await input.updateJobProgress({
134883
+ jobId: input.jobId,
134884
+ portalId: input.portalId,
134885
+ pluginNames: input.pluginNames,
134886
+ createdAt: input.createdAt,
134887
+ status: "COMPLETED",
134888
+ plugins: input.plugins,
134889
+ startedAt,
134890
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
134891
+ relayToServer: input.relayToServer
134892
+ });
134893
+ };
134894
+
134900
134895
  // src/effects/ensure-fresh.ts
134901
134896
  import fs20 from "fs";
134902
134897
  var DEFAULT_QUEUE_DRAIN_TIMEOUT_MS = 12e4;
@@ -135293,6 +135288,7 @@ var STATUS_METADATA = {
135293
135288
  };
135294
135289
  var UPDATE_CHECK_TIMEOUT_MS = 2e3;
135295
135290
  var UPDATE_CHECK_CACHE_TTL_MS = 5 * 6e4;
135291
+ var PLUGIN_REFRESH_VERIFICATION_PREFIX = "plugin_refresh_verification:";
135296
135292
  var latestPackageVersionCache = null;
135297
135293
  var fetchLatestPackageVersion = async (packageName) => {
135298
135294
  const now = Date.now();
@@ -135368,6 +135364,26 @@ var mainProgram = Effect147.gen(function* () {
135368
135364
  const isMasterClient = () => getConnectionType() === "MASTER";
135369
135365
  const getSyncRole = () => isMasterClient() ? "master" : "read_only";
135370
135366
  const formatErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
135367
+ const buildPluginRefreshVerificationKey = (jobId) => `${PLUGIN_REFRESH_VERIFICATION_PREFIX}${jobId}`;
135368
+ const buildStalePluginRefreshExecutorProgress = (jobId, portalId, pluginNames) => {
135369
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
135370
+ const error = `Rejected plugin refresh execute for portal ${portalId} because this client is read-only locally.`;
135371
+ return {
135372
+ job_id: jobId,
135373
+ target_portal: portalId,
135374
+ plugin_names: [...pluginNames],
135375
+ status: "FAILED",
135376
+ plugins: pluginNames.map((pluginName) => ({
135377
+ plugin_name: pluginName,
135378
+ status: "FAILED",
135379
+ error,
135380
+ finished_at: finishedAt
135381
+ })),
135382
+ created_at: finishedAt,
135383
+ finished_at: finishedAt,
135384
+ error
135385
+ };
135386
+ };
135371
135387
  const logReadOnlySkip = (action, portalId) => {
135372
135388
  const portalPart = portalId === void 0 ? "" : ` for portal ${portalId}`;
135373
135389
  console.error(`[sync] Skipping ${action}${portalPart} on read-only client`);
@@ -135394,6 +135410,20 @@ var mainProgram = Effect147.gen(function* () {
135394
135410
  syncRole: getSyncRole()
135395
135411
  })
135396
135412
  );
135413
+ const refreshRegistrationForConnectionTypeChange = (reason) => {
135414
+ scheduleRuntimeReconcile();
135415
+ Effect147.runFork(
135416
+ pipe122(
135417
+ Effect147.promise(() => syncRegistrationContext()),
135418
+ Effect147.flatMap(() => ws6.refreshRegistration()),
135419
+ Effect147.catchAll(
135420
+ (error) => Effect147.sync(() => {
135421
+ console.error(`[ws] Failed to refresh registration after ${reason}:`, error);
135422
+ })
135423
+ )
135424
+ )
135425
+ );
135426
+ };
135397
135427
  const ensureFresh = makeEnsureFresh({
135398
135428
  getSelectedPortalId: getPortalId,
135399
135429
  isMasterClient,
@@ -135595,17 +135625,76 @@ var mainProgram = Effect147.gen(function* () {
135595
135625
  const allDone = plugins2.every(
135596
135626
  (plugin) => plugin.status === "SYNCED" || plugin.status === "PARTIAL" || plugin.status === "FAILED"
135597
135627
  );
135598
- await updateJobProgress({
135628
+ if (!allDone) {
135629
+ await updateJobProgress({
135630
+ jobId: input.jobId,
135631
+ portalId: input.portalId,
135632
+ pluginNames: input.pluginNames,
135633
+ createdAt: input.createdAt,
135634
+ status: "RUNNING",
135635
+ plugins: plugins2,
135636
+ startedAt: plugins2.map((plugin) => plugin.startedAt).filter((value) => value !== null).sort()[0] ?? null,
135637
+ relayToServer: input.relayToServer
135638
+ });
135639
+ return;
135640
+ }
135641
+ if (hasFailure) {
135642
+ await updateJobProgress({
135643
+ jobId: input.jobId,
135644
+ portalId: input.portalId,
135645
+ pluginNames: input.pluginNames,
135646
+ createdAt: input.createdAt,
135647
+ status: "FAILED",
135648
+ plugins: plugins2,
135649
+ startedAt: plugins2.map((plugin) => plugin.startedAt).filter((value) => value !== null).sort()[0] ?? null,
135650
+ finishedAt: now,
135651
+ error: plugins2.find((plugin) => plugin.status === "FAILED")?.error ?? null,
135652
+ relayToServer: input.relayToServer
135653
+ });
135654
+ return;
135655
+ }
135656
+ const verificationMarker = {
135657
+ key: buildPluginRefreshVerificationKey(input.jobId),
135658
+ value: (/* @__PURE__ */ new Date()).toISOString()
135659
+ };
135660
+ await finalizeManualPluginRefreshJob({
135599
135661
  jobId: input.jobId,
135600
135662
  portalId: input.portalId,
135601
135663
  pluginNames: input.pluginNames,
135602
135664
  createdAt: input.createdAt,
135603
- status: allDone ? hasFailure ? "FAILED" : "COMPLETED" : "RUNNING",
135604
135665
  plugins: plugins2,
135605
- startedAt: plugins2.map((plugin) => plugin.startedAt).filter((value) => value !== null).sort()[0] ?? null,
135606
- finishedAt: allDone ? now : null,
135607
- error: hasFailure ? plugins2.find((plugin) => plugin.status === "FAILED")?.error ?? null : null,
135608
- relayToServer: input.relayToServer
135666
+ relayToServer: input.relayToServer,
135667
+ prepareReplicaVerification: async () => {
135668
+ await Effect147.runPromise(
135669
+ duckDb.setMetadataWithoutReplica(
135670
+ input.portalId,
135671
+ verificationMarker.key,
135672
+ verificationMarker.value
135673
+ )
135674
+ );
135675
+ },
135676
+ updateJobProgress,
135677
+ publishReplica: () => Effect147.runPromise(
135678
+ duckDb.publishReplica({
135679
+ portalId: input.portalId,
135680
+ reason: `manualPluginRefresh:${input.jobId}`,
135681
+ mode: "immediate"
135682
+ })
135683
+ ),
135684
+ verifyReplica: async () => {
135685
+ await evictReadOnlyDatabaseConnections(input.portalId);
135686
+ const replicatedValue = await getPortalMetadata({
135687
+ portalId: input.portalId,
135688
+ key: verificationMarker.key,
135689
+ masterLock: { isMaster: false }
135690
+ });
135691
+ if (replicatedValue !== verificationMarker.value) {
135692
+ throw new Error(
135693
+ `Replica verification failed for portal ${input.portalId}: expected marker '${verificationMarker.key}' to equal '${verificationMarker.value}', got '${replicatedValue ?? "null"}'.`
135694
+ );
135695
+ }
135696
+ },
135697
+ formatError: formatErrorMessage2
135609
135698
  });
135610
135699
  }
135611
135700
  });
@@ -135796,11 +135885,8 @@ var mainProgram = Effect147.gen(function* () {
135796
135885
  const workflowsArtifactReady = portalState?.artifacts.some(
135797
135886
  (artifact) => artifact.object_type === "workflows" && isArtifactBaselineReady(artifact.status)
135798
135887
  ) ?? false;
135799
- const propertyDefinitionsArtifactReady = portalState?.artifacts.some(
135800
- (artifact) => artifact.object_type === "property_definitions" && isArtifactBaselineReady(artifact.status)
135801
- ) ?? false;
135802
135888
  const workflowBaselineReady = workflowStateResult.readable && (workflowLastSyncedAt !== null || workflowsArtifactReady);
135803
- const propertyDefinitionBaselineReady = propertyDefinitionStateResult.readable && (propertyDefinitionLastSyncedAt !== null || propertyDefinitionsArtifactReady);
135889
+ const propertyDefinitionBaselineReady = propertyDefinitionStateResult.readable && propertyDefinitionLastSyncedAt !== null;
135804
135890
  return {
135805
135891
  clientUuid: clientState.clientUuid,
135806
135892
  portalId,
@@ -136011,17 +136097,10 @@ var mainProgram = Effect147.gen(function* () {
136011
136097
  scheduleRuntimeReconcile = runtimeReconciler.scheduleReconcile;
136012
136098
  runtimeReconciler.startPolling();
136013
136099
  onPromotedToMaster(() => {
136014
- Effect147.runFork(
136015
- pipe122(
136016
- Effect147.promise(() => syncRegistrationContext()),
136017
- Effect147.flatMap(() => ws6.refreshRegistration()),
136018
- Effect147.catchAll(
136019
- (error) => Effect147.sync(() => {
136020
- console.error("[ws] Failed to refresh registration after promotion:", error);
136021
- })
136022
- )
136023
- )
136024
- );
136100
+ refreshRegistrationForConnectionTypeChange("promotion");
136101
+ });
136102
+ onDemotedToReadOnly(() => {
136103
+ refreshRegistrationForConnectionTypeChange("demotion");
136025
136104
  });
136026
136105
  runtimeReconciler.scheduleReconcile();
136027
136106
  configureArtifactQueue(
@@ -136299,6 +136378,26 @@ var mainProgram = Effect147.gen(function* () {
136299
136378
  onPluginRefreshExecute: (payload) => {
136300
136379
  if (!isMasterClient()) {
136301
136380
  logReadOnlySkip("plugin refresh execute", payload.target_portal);
136381
+ refreshRegistrationForConnectionTypeChange("demotion");
136382
+ Effect147.runFork(
136383
+ pipe122(
136384
+ ws6.sendPluginRefreshProgress(
136385
+ buildStalePluginRefreshExecutorProgress(
136386
+ payload.job_id,
136387
+ payload.target_portal,
136388
+ payload.plugin_names
136389
+ )
136390
+ ),
136391
+ Effect147.catchAll(
136392
+ (error) => Effect147.sync(() => {
136393
+ console.error(
136394
+ `[plugin-refresh] Failed to reject stale executor for portal ${payload.target_portal}:`,
136395
+ error
136396
+ );
136397
+ })
136398
+ )
136399
+ )
136400
+ );
136302
136401
  return;
136303
136402
  }
136304
136403
  const existing = refreshJobStore.getSnapshot(payload.job_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daeda/mcp-pro",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "MCP server for HubSpot CRM — sync, query, and manage your portal data",
5
5
  "type": "module",
6
6
  "bin": {