@daeda/mcp-pro 0.1.29 → 0.1.31

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 +346 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1549,6 +1549,7 @@ var assertWritable = (masterLock) => {
1549
1549
  }
1550
1550
  };
1551
1551
  var escapeSqlString = (value) => value.replace(/'/g, "''");
1552
+ var escapeSqlIdentifier = (value) => `"${value.replace(/"/g, '""')}"`;
1552
1553
  var buildSqlPlaceholders = (count, startAt = 1) => Array.from({ length: count }, (_, index) => `$${index + startAt}`).join(",");
1553
1554
  var buildDatabaseCacheKey = (portalId, mode, encryptionKey) => `${portalId}:${mode}:${encryptionKey ?? ""}`;
1554
1555
  var makeAssociationStagingTableName = (seed = Date.now()) => `_staging_assoc_sync_${seed}`;
@@ -1712,6 +1713,20 @@ async function openDatabaseConnection(portalId, _encryptionKey, options2) {
1712
1713
  if (mode === "read_only" && !fs3.existsSync(sourcePath)) {
1713
1714
  throw new Error(`Replica database not found for portal ${portalId}: ${sourcePath}`);
1714
1715
  }
1716
+ if (mode === "read_only") {
1717
+ const instance2 = await DuckDBInstance.create(":memory:");
1718
+ const connection2 = await instance2.connect();
1719
+ const escapedPath = escapeSqlString(sourcePath);
1720
+ await connection2.run(`ATTACH '${escapedPath}' AS portal_replica (READ_ONLY)`);
1721
+ await connection2.run("USE portal_replica");
1722
+ return {
1723
+ instance: instance2,
1724
+ connection: connection2,
1725
+ mode,
1726
+ sourcePath,
1727
+ replicaFingerprint: getReplicaFingerprint(portalId)
1728
+ };
1729
+ }
1715
1730
  const instance = await DuckDBInstance.create(sourcePath);
1716
1731
  const connection = await instance.connect();
1717
1732
  return {
@@ -1719,7 +1734,7 @@ async function openDatabaseConnection(portalId, _encryptionKey, options2) {
1719
1734
  connection,
1720
1735
  mode,
1721
1736
  sourcePath,
1722
- replicaFingerprint: mode === "read_only" ? getReplicaFingerprint(portalId) : null
1737
+ replicaFingerprint: null
1723
1738
  };
1724
1739
  }
1725
1740
  var OBJECT_CSV_COLUMNS = ["id", "properties", "last_synced"];
@@ -1784,13 +1799,24 @@ async function bulkAppend(connection, tableName, columns) {
1784
1799
  function cleanupDbArtifacts(filePath) {
1785
1800
  try {
1786
1801
  fs3.unlinkSync(filePath);
1787
- } catch {
1802
+ } catch (error) {
1803
+ logFsCleanupError("remove file", filePath, error);
1788
1804
  }
1789
1805
  try {
1790
1806
  fs3.unlinkSync(`${filePath}.wal`);
1791
- } catch {
1807
+ } catch (error) {
1808
+ logFsCleanupError("remove wal", `${filePath}.wal`, error);
1792
1809
  }
1793
1810
  }
1811
+ function logFsCleanupError(action, filePath, error) {
1812
+ const code = error?.code;
1813
+ if (code === "ENOENT") return;
1814
+ console.error("[replica] Cleanup step failed", {
1815
+ action,
1816
+ filePath,
1817
+ error: describeError(error)
1818
+ });
1819
+ }
1794
1820
  function nextReplicaFileName(seed = Date.now()) {
1795
1821
  return `${REPLICA_VERSION_PREFIX2}${seed}-${process.pid}-${Math.random().toString(36).slice(2, 8)}${REPLICA_VERSION_SUFFIX2}`;
1796
1822
  }
@@ -1802,7 +1828,8 @@ function writeReplicaPointer(portalId, fileName) {
1802
1828
  `, "utf-8");
1803
1829
  try {
1804
1830
  fs3.unlinkSync(pointerPath);
1805
- } catch {
1831
+ } catch (error) {
1832
+ logFsCleanupError("remove pointer", pointerPath, error);
1806
1833
  }
1807
1834
  fs3.renameSync(nextPointerPath, pointerPath);
1808
1835
  }
@@ -1818,8 +1845,51 @@ function cleanupStaleReplicaFiles(portalId, currentReplicaPath) {
1818
1845
  }
1819
1846
  cleanupDbArtifacts(path2.join(portalDir(portalId), entry));
1820
1847
  }
1848
+ } catch (error) {
1849
+ console.error("[replica] Failed stale replica cleanup", {
1850
+ portalId,
1851
+ currentReplicaPath,
1852
+ error: describeError(error)
1853
+ });
1854
+ }
1855
+ }
1856
+ function getSourceDatabaseName(filePath) {
1857
+ return path2.parse(filePath).name;
1858
+ }
1859
+ function getFileSnapshot(filePath) {
1860
+ try {
1861
+ const stats = fs3.statSync(filePath);
1862
+ return {
1863
+ exists: true,
1864
+ sizeBytes: stats.size,
1865
+ mtimeMs: stats.mtimeMs
1866
+ };
1821
1867
  } catch {
1868
+ return {
1869
+ exists: false,
1870
+ sizeBytes: null,
1871
+ mtimeMs: null
1872
+ };
1873
+ }
1874
+ }
1875
+ function describeError(error) {
1876
+ if (error instanceof Error) {
1877
+ const errnoError = error;
1878
+ return {
1879
+ name: error.name,
1880
+ message: error.message,
1881
+ stack: error.stack,
1882
+ code: errnoError.code,
1883
+ errno: errnoError.errno,
1884
+ syscall: errnoError.syscall,
1885
+ path: errnoError.path,
1886
+ dest: errnoError.dest,
1887
+ cause: errnoError.cause ? describeError(errnoError.cause) : void 0
1888
+ };
1822
1889
  }
1890
+ return {
1891
+ value: String(error)
1892
+ };
1823
1893
  }
1824
1894
  async function publishReplica({
1825
1895
  masterLock,
@@ -1830,11 +1900,87 @@ async function publishReplica({
1830
1900
  const liveDbPath = dbPath(portalId);
1831
1901
  const nextReplicaName = nextReplicaFileName();
1832
1902
  const nextReplicaPath = replicaVersionDbPath(portalId, nextReplicaName);
1903
+ const nextReplicaTempPath = `${nextReplicaPath}.next`;
1904
+ const sourceDatabaseName = getSourceDatabaseName(liveDbPath);
1905
+ const targetDatabaseName = "portal_replica_next";
1906
+ console.error("[replica] Publish starting", {
1907
+ portalId,
1908
+ liveDbPath,
1909
+ nextReplicaPath,
1910
+ nextReplicaTempPath,
1911
+ liveDb: getFileSnapshot(liveDbPath)
1912
+ });
1833
1913
  cleanupDbArtifacts(nextReplicaPath);
1834
- await handle.connection.run("CHECKPOINT");
1835
- fs3.copyFileSync(liveDbPath, nextReplicaPath);
1836
- writeReplicaPointer(portalId, nextReplicaName);
1837
- cleanupStaleReplicaFiles(portalId, nextReplicaPath);
1914
+ cleanupDbArtifacts(nextReplicaTempPath);
1915
+ try {
1916
+ console.error("[replica] Checkpointing live database", {
1917
+ portalId,
1918
+ liveDbPath
1919
+ });
1920
+ await handle.connection.run("CHECKPOINT");
1921
+ console.error("[replica] Building replica snapshot via DuckDB", {
1922
+ portalId,
1923
+ sourceDatabaseName,
1924
+ targetDatabaseName,
1925
+ nextReplicaTempPath
1926
+ });
1927
+ await handle.connection.run(
1928
+ `ATTACH '${escapeSqlString(nextReplicaTempPath)}' AS ${escapeSqlIdentifier(targetDatabaseName)}`
1929
+ );
1930
+ try {
1931
+ await handle.connection.run(
1932
+ `COPY FROM DATABASE ${escapeSqlIdentifier(sourceDatabaseName)} TO ${escapeSqlIdentifier(targetDatabaseName)}`
1933
+ );
1934
+ await handle.connection.run(`CHECKPOINT ${escapeSqlIdentifier(targetDatabaseName)}`);
1935
+ } finally {
1936
+ await handle.connection.run(`DETACH ${escapeSqlIdentifier(targetDatabaseName)}`);
1937
+ }
1938
+ console.error("[replica] Verifying replica snapshot", {
1939
+ portalId,
1940
+ nextReplicaTempPath,
1941
+ snapshot: getFileSnapshot(nextReplicaTempPath)
1942
+ });
1943
+ const verifyInstance = await DuckDBInstance.create(":memory:");
1944
+ const verifyConnection = await verifyInstance.connect();
1945
+ try {
1946
+ await verifyConnection.run(
1947
+ `ATTACH '${escapeSqlString(nextReplicaTempPath)}' AS portal_replica_verify (READ_ONLY)`
1948
+ );
1949
+ await verifyConnection.run("DETACH portal_replica_verify");
1950
+ } finally {
1951
+ try {
1952
+ verifyConnection.closeSync();
1953
+ } catch {
1954
+ }
1955
+ try {
1956
+ verifyInstance.closeSync();
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ fs3.renameSync(nextReplicaTempPath, nextReplicaPath);
1961
+ console.error("[replica] Writing replica pointer", {
1962
+ portalId,
1963
+ nextReplicaName,
1964
+ nextReplicaPath
1965
+ });
1966
+ writeReplicaPointer(portalId, nextReplicaName);
1967
+ cleanupStaleReplicaFiles(portalId, nextReplicaPath);
1968
+ console.error("[replica] Publish complete", {
1969
+ portalId,
1970
+ nextReplicaPath,
1971
+ replica: getFileSnapshot(nextReplicaPath)
1972
+ });
1973
+ } catch (error) {
1974
+ console.error("[replica] Publish failed", {
1975
+ portalId,
1976
+ liveDbPath,
1977
+ nextReplicaPath,
1978
+ nextReplicaTempPath,
1979
+ error: describeError(error)
1980
+ });
1981
+ cleanupDbArtifacts(nextReplicaTempPath);
1982
+ throw error;
1983
+ }
1838
1984
  }
1839
1985
  async function ensureTable(connection, ddlStatements) {
1840
1986
  for (const statement of ddlStatements) {
@@ -2846,8 +2992,11 @@ var makeReplicaLive = (config = {}) => {
2846
2992
  dirty: true
2847
2993
  })),
2848
2994
  Effect25.flatMap(
2849
- () => logStderr3(
2850
- `[replica] Failed to publish replica for portal ${portalId}: ${error.message}`
2995
+ () => Effect25.sync(
2996
+ () => console.error("[replica] Failed to publish replica", {
2997
+ portalId,
2998
+ error
2999
+ })
2851
3000
  )
2852
3001
  ),
2853
3002
  Effect25.flatMap(() => Effect25.fail(error))
@@ -3919,26 +4068,64 @@ var setSelectedPortal = (portalId) => {
3919
4068
  // src/effects/read-connection.ts
3920
4069
  import { Effect as Effect31, pipe as pipe19 } from "effect";
3921
4070
  import fs6 from "fs";
3922
- var getReadConnection = (portalId) => pipe19(
4071
+ var openLiveConnection = (portalId, cause) => Effect31.tryPromise({
4072
+ try: async () => {
4073
+ const handle = await getDatabaseConnection(portalId, null);
4074
+ return handle.connection;
4075
+ },
4076
+ catch: (error) => new DatabaseError({
4077
+ message: `Failed to open live fallback connection for portal ${portalId}`,
4078
+ cause: { replicaError: cause, fallbackError: error }
4079
+ })
4080
+ });
4081
+ var getReadConnection = (portalId, options2) => pipe19(
3923
4082
  Effect31.sync(() => replicaDbPath(portalId)),
3924
- Effect31.filterOrFail(
3925
- (dbFile) => fs6.existsSync(dbFile),
3926
- () => new DatabaseError({
3927
- message: `Read replica not found for portal ${portalId}. Has it been synced?`
3928
- })
3929
- ),
3930
- Effect31.flatMap(
3931
- () => Effect31.tryPromise({
4083
+ Effect31.flatMap((dbFile) => {
4084
+ if (!fs6.existsSync(dbFile)) {
4085
+ if (options2?.allowLiveFallback === true && fs6.existsSync(dbPath(portalId))) {
4086
+ console.error("[read-connection] Replica missing, falling back to live database", {
4087
+ portalId,
4088
+ replicaPath: dbFile,
4089
+ livePath: dbPath(portalId)
4090
+ });
4091
+ return openLiveConnection(
4092
+ portalId,
4093
+ new DatabaseError({
4094
+ message: `Read replica not found for portal ${portalId}. Has it been synced?`
4095
+ })
4096
+ );
4097
+ }
4098
+ return Effect31.fail(
4099
+ new DatabaseError({
4100
+ message: `Read replica not found for portal ${portalId}. Has it been synced?`
4101
+ })
4102
+ );
4103
+ }
4104
+ return Effect31.tryPromise({
3932
4105
  try: async () => {
3933
4106
  const handle = await getDatabaseConnection(portalId, null, { readOnly: true });
3934
4107
  return handle.connection;
3935
4108
  },
3936
- catch: (e) => new DatabaseError({
3937
- message: `Failed to open read connection for portal ${portalId}`,
3938
- cause: e
3939
- })
3940
- })
3941
- )
4109
+ catch: (error) => error
4110
+ });
4111
+ }),
4112
+ Effect31.catchAll((error) => {
4113
+ if (!(options2?.allowLiveFallback === true) || !fs6.existsSync(dbPath(portalId))) {
4114
+ return Effect31.fail(
4115
+ error instanceof DatabaseError ? error : new DatabaseError({
4116
+ message: `Failed to open read connection for portal ${portalId}`,
4117
+ cause: error
4118
+ })
4119
+ );
4120
+ }
4121
+ console.error("[read-connection] Read-only replica open failed, falling back to live database", {
4122
+ portalId,
4123
+ replicaPath: replicaDbPath(portalId),
4124
+ livePath: dbPath(portalId),
4125
+ error
4126
+ });
4127
+ return openLiveConnection(portalId, error);
4128
+ })
3942
4129
  );
3943
4130
  var closeReadConnection = (portalId) => Effect31.promise(() => evictDatabaseConnections(portalId));
3944
4131
 
@@ -5620,8 +5807,10 @@ var resolvePortalIds = (explicitPortalIds, selectedPortalId2, portals) => {
5620
5807
 
5621
5808
  // src/tools/query.ts
5622
5809
  var MAX_ROWS = 200;
5623
- var runQueryForPortal = async (portalId, finalSql, autoCapped) => {
5624
- const conn = await Effect50.runPromise(getReadConnection(portalId));
5810
+ var runQueryForPortal = async (portalId, finalSql, autoCapped, allowLiveFallback) => {
5811
+ const conn = await Effect50.runPromise(
5812
+ getReadConnection(portalId, { allowLiveFallback })
5813
+ );
5625
5814
  const start = performance.now();
5626
5815
  const reader = await conn.runAndReadAll(finalSql);
5627
5816
  const rows = reader.getRowObjects();
@@ -5697,7 +5886,8 @@ Example queries:
5697
5886
  const result = await runQueryForPortal(
5698
5887
  portalId,
5699
5888
  finalSql,
5700
- !hasExplicitLimit
5889
+ !hasExplicitLimit,
5890
+ deps.isMasterClient()
5701
5891
  );
5702
5892
  return {
5703
5893
  content: [{ type: "text", text: stringifyResult(result) }]
@@ -5718,7 +5908,8 @@ Example queries:
5718
5908
  keyedResults[String(portalId)] = await runQueryForPortal(
5719
5909
  portalId,
5720
5910
  finalSql,
5721
- !hasExplicitLimit
5911
+ !hasExplicitLimit,
5912
+ deps.isMasterClient()
5722
5913
  );
5723
5914
  } catch (e) {
5724
5915
  hadAnyErrors = true;
@@ -5938,8 +6129,10 @@ function openInBrowser(target) {
5938
6129
 
5939
6130
  // src/tools/chart.ts
5940
6131
  var MAX_ROWS2 = 200;
5941
- var runQueryForPortal2 = async (portalId, sql) => {
5942
- const conn = await Effect51.runPromise(getReadConnection(portalId));
6132
+ var runQueryForPortal2 = async (portalId, sql, allowLiveFallback) => {
6133
+ const conn = await Effect51.runPromise(
6134
+ getReadConnection(portalId, { allowLiveFallback })
6135
+ );
5943
6136
  const start = performance.now();
5944
6137
  const reader = await conn.runAndReadAll(sql);
5945
6138
  const rows = reader.getRowObjects();
@@ -6051,7 +6244,11 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
6051
6244
  const [portalId] = portalResolution.portalIds;
6052
6245
  try {
6053
6246
  await deps.ensureFresh(portalId);
6054
- const result = await runQueryForPortal2(portalId, finalSql);
6247
+ const result = await runQueryForPortal2(
6248
+ portalId,
6249
+ finalSql,
6250
+ deps.isMasterClient()
6251
+ );
6055
6252
  if (result.rows.length === 0) {
6056
6253
  return {
6057
6254
  content: [{ type: "text", text: "Query returned no rows - nothing to chart." }],
@@ -6105,7 +6302,11 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
6105
6302
  for (const portalId of portalResolution.portalIds) {
6106
6303
  try {
6107
6304
  await deps.ensureFresh(portalId);
6108
- const result = await runQueryForPortal2(portalId, finalSql);
6305
+ const result = await runQueryForPortal2(
6306
+ portalId,
6307
+ finalSql,
6308
+ deps.isMasterClient()
6309
+ );
6109
6310
  if (result.rows.length === 0) {
6110
6311
  hadErrors = true;
6111
6312
  keyedPortalResults[String(portalId)] = { error: "Query returned no rows - nothing to chart." };
@@ -6204,8 +6405,10 @@ async function buildConnectionSection(deps) {
6204
6405
  totalPortals: portals.length
6205
6406
  };
6206
6407
  }
6207
- async function buildSchemaSection(portalId) {
6208
- const conn = await Effect52.runPromise(getReadConnection(portalId));
6408
+ async function buildSchemaSection(portalId, deps) {
6409
+ const conn = await Effect52.runPromise(
6410
+ getReadConnection(portalId, { allowLiveFallback: deps.isMasterClient() })
6411
+ );
6209
6412
  const allPluginTableNames = new Set(getAllTableNames());
6210
6413
  const sectionResults = await Promise.all(
6211
6414
  getPlugins().map(async (plugin) => {
@@ -6276,7 +6479,7 @@ Use "all" to get everything at once.`,
6276
6479
  for (const portalId of portalResolution.portalIds) {
6277
6480
  try {
6278
6481
  await deps.ensureFresh(portalId);
6279
- schemaByPortal[String(portalId)] = await buildSchemaSection(portalId);
6482
+ schemaByPortal[String(portalId)] = await buildSchemaSection(portalId, deps);
6280
6483
  } catch (e) {
6281
6484
  explicitSchemaHadErrors = true;
6282
6485
  const message = e instanceof Error ? e.message : String(e);
@@ -6287,7 +6490,7 @@ Use "all" to get everything at once.`,
6287
6490
  } else {
6288
6491
  const [portalId] = portalResolution.portalIds;
6289
6492
  await deps.ensureFresh(portalId);
6290
- result.schema = await buildSchemaSection(portalId);
6493
+ result.schema = await buildSchemaSection(portalId, deps);
6291
6494
  }
6292
6495
  }
6293
6496
  return {
@@ -9303,7 +9506,7 @@ var BUILT_IN_ACTION_TYPES = {
9303
9506
  { name: "date", type: "object", required: true, description: "Date spec \u2014 MUST include type: { type: 'STATIC_VALUE', staticValue: '<unix ms>' } or { type: 'OBJECT_PROPERTY', propertyName: '<name>' }. Omitting type causes HTTP 500." },
9304
9507
  { name: "delta", type: "string", required: true, description: "Offset from the date (e.g. '0')" },
9305
9508
  { name: "time_unit", type: "string", required: true, description: "Unit for delta: DAYS, HOURS, MINUTES" },
9306
- { name: "time_of_day", type: "object", required: true, description: "Required time of day: { hour: number, minute: number }. HubSpot UI rejects delay-until-date actions without it." }
9509
+ { name: "time_of_day", type: "object", required: true, description: "Required time of day: { hour: number, minute: number }. HubSpot UI rejects delay-until-date actions without it. If the workflow continues after this delay, the next action should be a STATIC_BRANCH on hs_delay_status rather than a direct business-action connection." }
9307
9510
  ]
9308
9511
  },
9309
9512
  "0-63809083": {
@@ -9623,7 +9826,7 @@ var WORKFLOW_ACTION_CREATE_METADATA_BY_ID = {
9623
9826
  },
9624
9827
  "0-35": {
9625
9828
  resourceRequirements: [],
9626
- createGuidanceText: "When using STATIC_VALUE dates, send a future Unix-milliseconds timestamp. time_of_day is required for reliable creation."
9829
+ createGuidanceText: "When using STATIC_VALUE dates, send a future Unix-milliseconds timestamp. time_of_day is required for reliable creation. If the delay action continues to another step, route it into a STATIC_BRANCH that reads inputValue { actionId: '<delay_action_id>', dataKey: 'hs_delay_status', type: 'FIELD_DATA' } and continues from the DATE_MET_AS_PLANNED branch instead of connecting directly to the next business action."
9627
9830
  },
9628
9831
  "0-14": {
9629
9832
  resourceRequirements: [],
@@ -11774,12 +11977,12 @@ var PROPERTY_VALUE_CHANGED_PARAMETER_SCHEMA = [
11774
11977
  {
11775
11978
  name: "property_value_operator",
11776
11979
  required: false,
11777
- description: "Defaults to IS_KNOWN. Supported values: IS_KNOWN, IS_UNKNOWN, IS_EQUAL_TO, IS_NOT_EQUAL_TO."
11980
+ description: "Defaults to IS_KNOWN. Supported values: IS_KNOWN, IS_UNKNOWN, IS_EQUAL_TO, IS_NOT_EQUAL_TO. This only controls the event refinement filter on hs_value inside the 4-655002 property-change event branch."
11778
11981
  },
11779
11982
  {
11780
11983
  name: "property_value",
11781
11984
  required: false,
11782
- description: "Required when property_value_operator is IS_EQUAL_TO or IS_NOT_EQUAL_TO."
11985
+ description: "Required when property_value_operator is IS_EQUAL_TO or IS_NOT_EQUAL_TO. This does not add generic object-property filters like pipeline or dealstage to the event branch."
11783
11986
  }
11784
11987
  ];
11785
11988
  var WORKFLOW_REFERENCE_PARAMETER_SCHEMA = [
@@ -11797,7 +12000,7 @@ var PARAMETER_SCHEMA_BY_TEMPLATE = {
11797
12000
  var EVENT_TYPE_METADATA_BY_EVENT_TYPE_ID = {
11798
12001
  "4-655002": {
11799
12002
  requiresRefinementFilters: true,
11800
- refinementNote: "Requires event-specific refinement filters (e.g. hs_property_name, hs_property_value) embedded in the event filter branch to specify which property and value to trigger on. Standard PROPERTY filters are NOT sufficient. Prefer LIST_BASED enrollment with re-enrollment triggers for 'property changed to X' scenarios."
12003
+ refinementNote: "Requires event-specific refinement filters embedded in the event branch. For MCP these are hs_name and hs_value. Standard object-property filters like pipeline, dealstage, amount, or closedate are not valid inside the 4-655002 branch. Prefer LIST_BASED enrollment with re-enrollment triggers for 'property changed to X' scenarios that also need record eligibility filters."
11801
12004
  }
11802
12005
  };
11803
12006
  var normalizedRows = normalized_triggers_default;
@@ -12235,6 +12438,13 @@ var buildEnrollmentCriteriaFromTrigger = (trigger) => {
12235
12438
  };
12236
12439
 
12237
12440
  // ../shared/pure/workflow-operation-schema.ts
12441
+ var ENUMERATION_DISALLOWED_OPERATORS = /* @__PURE__ */ new Set([
12442
+ "IS_EQUAL_TO",
12443
+ "IS_NOT_EQUAL_TO"
12444
+ ]);
12445
+ var REFINEMENT_PROPERTIES_BY_EVENT_TYPE_ID = {
12446
+ "4-655002": /* @__PURE__ */ new Set(["hs_name", "hs_value"])
12447
+ };
12238
12448
  var WorkflowConnectionSchema2 = z62.object({
12239
12449
  edgeType: z62.string().min(1),
12240
12450
  nextActionId: z62.string().min(1)
@@ -12262,6 +12472,14 @@ var WorkflowFilterSchema = z62.object({
12262
12472
  message: "property filters require operation",
12263
12473
  path: ["operation"]
12264
12474
  });
12475
+ return;
12476
+ }
12477
+ if (value.operation.operationType === "ENUMERATION" && ENUMERATION_DISALLOWED_OPERATORS.has(value.operation.operator)) {
12478
+ ctx.addIssue({
12479
+ code: z62.ZodIssueCode.custom,
12480
+ message: `operator '${value.operation.operator}' is not valid for ENUMERATION workflow filters. Use operators like IS_ANY_OF, IS_NONE_OF, IS_EXACTLY, or IS_NOT_EXACTLY.`,
12481
+ path: ["operation", "operator"]
12482
+ });
12265
12483
  }
12266
12484
  });
12267
12485
  var WorkflowBranchSchema = z62.object({
@@ -12302,6 +12520,19 @@ var WorkflowEnrollmentBranchSchema = z62.object({
12302
12520
  path: ["eventTypeId"]
12303
12521
  });
12304
12522
  }
12523
+ const allowedRefinementProperties = REFINEMENT_PROPERTIES_BY_EVENT_TYPE_ID[value.eventTypeId];
12524
+ if (!allowedRefinementProperties) return;
12525
+ for (const [index, filter] of value.filters.entries()) {
12526
+ if (!filter || typeof filter !== "object") continue;
12527
+ if (filter.filterType !== "PROPERTY") continue;
12528
+ if (typeof filter.property !== "string") continue;
12529
+ if (allowedRefinementProperties.has(filter.property)) continue;
12530
+ ctx.addIssue({
12531
+ code: z62.ZodIssueCode.custom,
12532
+ message: `eventTypeId '${value.eventTypeId}' only supports refinement filters on ${Array.from(allowedRefinementProperties).join(", ")}. Move generic properties like '${filter.property}' into LIST_BASED criteria or use enrollment_trigger.`,
12533
+ path: ["filters", index, "property"]
12534
+ });
12535
+ }
12305
12536
  });
12306
12537
  var EventBasedEnrollmentCriteriaSchema = z62.object({
12307
12538
  type: z62.literal("EVENT_BASED"),
@@ -12432,7 +12663,7 @@ var fields44 = [
12432
12663
  name: "enrollment_criteria",
12433
12664
  type: "object",
12434
12665
  required: false,
12435
- description: "Enrollment trigger config. Set type to 'EVENT_BASED', 'LIST_BASED', or 'MANUAL'. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each eventFilterBranch needs a valid eventTypeId from the workflow_enrollment_triggers table (e.g. '4-655002' for property change, '4-1639801' for form submission). Do NOT use object type IDs like '0-3' as eventTypeId \u2014 those are CRM object types, not events. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, usage_guidance, preferred_for_json, and avoid_for_json. IMPORTANT: Some event types (notably '4-655002' property value changed) require event-specific refinement filters (e.g. hs_property_name, hs_property_value) embedded in the event filter branch \u2014 standard PROPERTY filters are NOT sufficient. For 'when property X changes to Y' scenarios, prefer LIST_BASED enrollment with re-enrollment triggers on that property instead. PROPERTY filters must include operation.operationType. For enum properties use 'ENUMERATION' not 'MULTISTRING'. Query synced workflows for structural examples.",
12666
+ description: "Enrollment trigger config. Set type to 'EVENT_BASED', 'LIST_BASED', or 'MANUAL'. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each eventFilterBranch needs a valid eventTypeId from the workflow_enrollment_triggers table (e.g. '4-655002' for property change, '4-1639801' for form submission). Do NOT use object type IDs like '0-3' as eventTypeId \u2014 those are CRM object types, not events. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, usage_guidance, preferred_for_json, and avoid_for_json. IMPORTANT: Some event types (notably '4-655002' property value changed) require event-specific refinement filters embedded in the event branch. For '4-655002', only event refinement properties like hs_name and hs_value belong in that branch; generic record properties like pipeline, dealstage, amount, and closedate do NOT belong there. For 'when property X changes to Y' scenarios that also need record eligibility checks, prefer LIST_BASED enrollment with re-enrollment triggers on that property instead. PROPERTY filters must include operation.operationType. For enum properties use 'ENUMERATION' not 'MULTISTRING'. For LIST_BASED property existence filters like IS_KNOWN / IS_UNKNOWN, prefer operationType 'ALL_PROPERTY' instead of 'TIME_POINT', even for date properties. ENUMERATION filters do not support operators like 'IS_EQUAL_TO' or 'IS_NOT_EQUAL_TO'; use enum operators like 'IS_ANY_OF', 'IS_NONE_OF', 'IS_EXACTLY', or 'IS_NOT_EXACTLY'. Query synced workflows for structural examples.",
12436
12667
  shape: "EVENT_BASED: { shouldReEnroll: boolean, eventFilterBranches: [{ filterBranchType: 'UNIFIED_EVENTS', filterBranchOperator?, eventTypeId: string (REQUIRED, from workflow_enrollment_triggers.event_type_id), operator?, filters: [{ filterType: 'PROPERTY', property: string, operation: { operationType: string, operator: string } }], filterBranches: [...] }], listMembershipFilterBranches?: [...] } | LIST_BASED: { shouldReEnroll: boolean, listFilterBranch: { filterBranchType, filters: [...], filterBranches: [...] }, reEnrollmentTriggersFilterBranches?: [...], unEnrollObjectsNotMeetingCriteria?: boolean } | MANUAL: { shouldReEnroll: boolean }"
12437
12668
  },
12438
12669
  { name: "suppression_list_ids", type: "array", required: false, description: "Array of list IDs to suppress from enrollment" },
@@ -12539,7 +12770,7 @@ var fields45 = [
12539
12770
  description: "Replacement single-trigger shortcut. Query workflow_enrollment_triggers first and only use rows where shortcut_kind = 'EVENT_TRIGGER_SHORTCUT'. Then use catalog_id plus parameter_schema_json, official_filter_template, official_filters_example_json, and required_object_type_ids_json.",
12540
12771
  shape: "{ catalog_id: string, should_re_enroll?: boolean, ...template-specific parameters from parameter_schema_json such as property_name/property_value_operator/property_value or target_workflow_id }"
12541
12772
  },
12542
- { name: "enrollment_criteria", type: "object", required: false, description: "Replacement enrollment criteria (replaces existing). Set type to EVENT_BASED, LIST_BASED, or MANUAL. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each UNIFIED_EVENTS branch requires a valid eventTypeId from workflow_enrollment_triggers.event_type_id (e.g. '4-655002' for property change). Do NOT use object type IDs like '0-3' as eventTypeId. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, and usage_guidance. IMPORTANT: Event types like '4-655002' (property changed) require event-specific refinement filters (e.g. hs_property_name) \u2014 standard PROPERTY filters are NOT sufficient. Prefer LIST_BASED enrollment with re-enrollment triggers for property-change scenarios." },
12773
+ { name: "enrollment_criteria", type: "object", required: false, description: "Replacement enrollment criteria (replaces existing). Set type to EVENT_BASED, LIST_BASED, or MANUAL. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each UNIFIED_EVENTS branch requires a valid eventTypeId from workflow_enrollment_triggers.event_type_id (e.g. '4-655002' for property change). Do NOT use object type IDs like '0-3' as eventTypeId. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, and usage_guidance. IMPORTANT: Event types like '4-655002' require event-specific refinement filters only. In MCP this means hs_name and hs_value inside the event branch; generic properties like pipeline, dealstage, amount, or closedate do not belong there. ENUMERATION filters also do not support operators like IS_EQUAL_TO or IS_NOT_EQUAL_TO; use IS_ANY_OF, IS_NONE_OF, IS_EXACTLY, or IS_NOT_EXACTLY instead. Prefer LIST_BASED enrollment with re-enrollment triggers for property-change scenarios that also need record eligibility filters." },
12543
12774
  { name: "suppression_list_ids", type: "array", required: false, description: "Replacement suppression list IDs" },
12544
12775
  { name: "time_windows", type: "array", required: false, description: "Replacement execution time windows" },
12545
12776
  { name: "blocked_dates", type: "array", required: false, description: "Replacement blocked date ranges" }
@@ -16949,6 +17180,10 @@ var OPERATION_TYPE_FOR_PROPERTY_TYPE2 = {
16949
17180
  date: "TIME_POINT",
16950
17181
  datetime: "TIME_POINT"
16951
17182
  };
17183
+ var ENUMERATION_INVALID_OPERATORS = /* @__PURE__ */ new Set([
17184
+ "IS_EQUAL_TO",
17185
+ "IS_NOT_EQUAL_TO"
17186
+ ]);
16952
17187
  var isCompatibleEnrollmentOperationType = (operationType, expectedOperationType, propertyType) => operationType === expectedOperationType || operationType === "ALL_PROPERTY" || operationType === "TIME_RANGED" && (propertyType === "date" || propertyType === "datetime");
16953
17188
  var collectPropertyFiltersFromBranches = (branches, parentObjectTypeId, parentEventTypeId) => {
16954
17189
  const refs = [];
@@ -16963,6 +17198,7 @@ var collectPropertyFiltersFromBranches = (branches, parentObjectTypeId, parentEv
16963
17198
  refs.push({
16964
17199
  property: filter.property,
16965
17200
  operationType: operation.operationType,
17201
+ operator: typeof operation.operator === "string" ? operation.operator : void 0,
16966
17202
  objectTypeId: contextObjectTypeId,
16967
17203
  eventTypeId: branchEventTypeId
16968
17204
  });
@@ -17029,6 +17265,15 @@ var validateWorkflowEnrollmentMetadata = (params) => {
17029
17265
  });
17030
17266
  continue;
17031
17267
  }
17268
+ if (filterRef.eventTypeId === "4-655002") {
17269
+ add({
17270
+ severity: "error",
17271
+ operation_index: operationIndex,
17272
+ code: "INVALID_ENROLLMENT_REFINEMENT_FILTER",
17273
+ message: `eventTypeId '4-655002' only supports event refinement filters like hs_name and hs_value. Property '${filterRef.property}' should not appear in that event branch; move it into LIST_BASED criteria or use enrollment_trigger.`
17274
+ });
17275
+ continue;
17276
+ }
17032
17277
  if (!enabledObjectTypeIds.has(filterRef.objectTypeId)) continue;
17033
17278
  const propertyDefinitions = propertyDefinitionsByObjectType.get(filterRef.objectTypeId);
17034
17279
  const propertyDefinition = propertyDefinitions?.get(filterRef.property);
@@ -17043,7 +17288,24 @@ var validateWorkflowEnrollmentMetadata = (params) => {
17043
17288
  }
17044
17289
  const expectedOperationType = OPERATION_TYPE_FOR_PROPERTY_TYPE2[propertyDefinition.type];
17045
17290
  if (!expectedOperationType) continue;
17291
+ if (filterRef.eventTypeId === void 0 && (filterRef.operator === "IS_KNOWN" || filterRef.operator === "IS_UNKNOWN") && filterRef.operationType === "TIME_POINT" && (propertyDefinition.type === "date" || propertyDefinition.type === "datetime")) {
17292
+ add({
17293
+ severity: "error",
17294
+ operation_index: operationIndex,
17295
+ code: "INVALID_ENROLLMENT_FILTER_OPERATION_TYPE",
17296
+ message: `Workflow enrollment filter property '${filterRef.property}' uses operator '${filterRef.operator}' with operationType 'TIME_POINT'. For LIST_BASED property existence checks, use operationType 'ALL_PROPERTY' instead.`
17297
+ });
17298
+ continue;
17299
+ }
17046
17300
  if (isCompatibleEnrollmentOperationType(filterRef.operationType, expectedOperationType, propertyDefinition.type)) {
17301
+ if (propertyDefinition.type === "enumeration" && filterRef.operator !== void 0 && ENUMERATION_INVALID_OPERATORS.has(filterRef.operator)) {
17302
+ add({
17303
+ severity: "error",
17304
+ operation_index: operationIndex,
17305
+ code: "INVALID_ENROLLMENT_FILTER_OPERATOR",
17306
+ message: `Workflow enrollment filter property '${filterRef.property}' has type 'enumeration' but uses operator '${filterRef.operator}'. Use enumeration operators like IS_ANY_OF, IS_NONE_OF, IS_EXACTLY, or IS_NOT_EXACTLY.`
17307
+ });
17308
+ }
17047
17309
  continue;
17048
17310
  }
17049
17311
  add({
@@ -17166,6 +17428,42 @@ var validateDelayUntilDateAction = (params) => {
17166
17428
  }
17167
17429
  return issues;
17168
17430
  };
17431
+ var validateDelayUntilDateBranching = (params) => {
17432
+ const { actions, operationIndex } = params;
17433
+ const issues = [];
17434
+ const actionsById = new Map(actions.flatMap(
17435
+ (action) => typeof action.actionId === "string" && action.actionId.length > 0 ? [[action.actionId, action]] : []
17436
+ ));
17437
+ for (const action of actions) {
17438
+ if (action.actionTypeId !== "0-35") continue;
17439
+ const delayActionId = action.actionId ?? "<missing>";
17440
+ const branchActionId = action.connection?.nextActionId;
17441
+ if (!branchActionId) continue;
17442
+ const branchAction = actionsById.get(branchActionId);
17443
+ if (!branchAction || branchAction.type !== "STATIC_BRANCH") {
17444
+ issues.push({
17445
+ severity: "error",
17446
+ operation_index: operationIndex,
17447
+ code: "WORKFLOW_DELAY_UNTIL_DATE_BRANCH_REQUIRED",
17448
+ message: `Action '${delayActionId}' (0-35 delay until date) must flow into a STATIC_BRANCH that reads hs_delay_status before continuing. Do not connect the delay action directly to the next business action.`
17449
+ });
17450
+ continue;
17451
+ }
17452
+ const inputValue = branchAction.inputValue;
17453
+ const hasExpectedInputValue = inputValue?.actionId === action.actionId && inputValue?.dataKey === "hs_delay_status" && inputValue?.type === "FIELD_DATA";
17454
+ const hasDateMetAsPlannedBranch = Array.isArray(branchAction.staticBranches) && branchAction.staticBranches.some(
17455
+ (branch) => isRecord(branch) && branch.branchValue === "DATE_MET_AS_PLANNED" && typeof getNestedNextActionId(branch) === "string"
17456
+ );
17457
+ if (hasExpectedInputValue && hasDateMetAsPlannedBranch) continue;
17458
+ issues.push({
17459
+ severity: "error",
17460
+ operation_index: operationIndex,
17461
+ code: "WORKFLOW_DELAY_UNTIL_DATE_BRANCH_REQUIRED",
17462
+ message: `Action '${delayActionId}' (0-35 delay until date) must be followed by a STATIC_BRANCH with inputValue { actionId: '${delayActionId}', dataKey: 'hs_delay_status', type: 'FIELD_DATA' } and a DATE_MET_AS_PLANNED branch connection.`
17463
+ });
17464
+ }
17465
+ return issues;
17466
+ };
17169
17467
  var validateRotateToOwnerAction = (params) => {
17170
17468
  const { action, actionId, operationIndex } = params;
17171
17469
  if (action.actionTypeId !== "0-11") return [];
@@ -17312,6 +17610,7 @@ var validateWorkflowActions = (params) => {
17312
17610
  issues.push(...validateRotateToOwnerAction({ action, actionId, operationIndex }));
17313
17611
  issues.push(...validateCreateTaskAssociations({ action, actionId, operationIndex, context }));
17314
17612
  }
17613
+ issues.push(...validateDelayUntilDateBranching({ actions, operationIndex }));
17315
17614
  for (const action of actions) {
17316
17615
  for (const nextActionId of collectActionNextActionIds(action)) {
17317
17616
  if (actionIds.has(nextActionId)) continue;
@@ -19884,17 +20183,20 @@ var mainProgram = Effect108.gen(function* () {
19884
20183
  };
19885
20184
  registerPortalSelection(server, { ws, config, handoffSelectedPortal });
19886
20185
  registerQueryTool(server, {
20186
+ isMasterClient,
19887
20187
  getSelectedPortalId: getPortalId,
19888
20188
  getPortals,
19889
20189
  ensureFresh
19890
20190
  });
19891
20191
  registerChartTool(server, {
20192
+ isMasterClient,
19892
20193
  getSelectedPortalId: getPortalId,
19893
20194
  getPortals,
19894
20195
  ensureFresh
19895
20196
  });
19896
20197
  registerStatusTool(server, {
19897
20198
  getConnectionState: () => Effect108.runSync(ws.getState()),
20199
+ isMasterClient,
19898
20200
  getPortals,
19899
20201
  getSelectedPortalId: getPortalId,
19900
20202
  ensureFresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daeda/mcp-pro",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "MCP server for HubSpot CRM — sync, query, and manage your portal data",
5
5
  "type": "module",
6
6
  "bin": {