@daeda/mcp-pro 0.1.30 → 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 +229 -37
  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}`;
@@ -1733,7 +1734,7 @@ async function openDatabaseConnection(portalId, _encryptionKey, options2) {
1733
1734
  connection,
1734
1735
  mode,
1735
1736
  sourcePath,
1736
- replicaFingerprint: mode === "read_only" ? getReplicaFingerprint(portalId) : null
1737
+ replicaFingerprint: null
1737
1738
  };
1738
1739
  }
1739
1740
  var OBJECT_CSV_COLUMNS = ["id", "properties", "last_synced"];
@@ -1798,13 +1799,24 @@ async function bulkAppend(connection, tableName, columns) {
1798
1799
  function cleanupDbArtifacts(filePath) {
1799
1800
  try {
1800
1801
  fs3.unlinkSync(filePath);
1801
- } catch {
1802
+ } catch (error) {
1803
+ logFsCleanupError("remove file", filePath, error);
1802
1804
  }
1803
1805
  try {
1804
1806
  fs3.unlinkSync(`${filePath}.wal`);
1805
- } catch {
1807
+ } catch (error) {
1808
+ logFsCleanupError("remove wal", `${filePath}.wal`, error);
1806
1809
  }
1807
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
+ }
1808
1820
  function nextReplicaFileName(seed = Date.now()) {
1809
1821
  return `${REPLICA_VERSION_PREFIX2}${seed}-${process.pid}-${Math.random().toString(36).slice(2, 8)}${REPLICA_VERSION_SUFFIX2}`;
1810
1822
  }
@@ -1816,7 +1828,8 @@ function writeReplicaPointer(portalId, fileName) {
1816
1828
  `, "utf-8");
1817
1829
  try {
1818
1830
  fs3.unlinkSync(pointerPath);
1819
- } catch {
1831
+ } catch (error) {
1832
+ logFsCleanupError("remove pointer", pointerPath, error);
1820
1833
  }
1821
1834
  fs3.renameSync(nextPointerPath, pointerPath);
1822
1835
  }
@@ -1832,8 +1845,51 @@ function cleanupStaleReplicaFiles(portalId, currentReplicaPath) {
1832
1845
  }
1833
1846
  cleanupDbArtifacts(path2.join(portalDir(portalId), entry));
1834
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
+ };
1835
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
+ };
1836
1889
  }
1890
+ return {
1891
+ value: String(error)
1892
+ };
1837
1893
  }
1838
1894
  async function publishReplica({
1839
1895
  masterLock,
@@ -1844,11 +1900,87 @@ async function publishReplica({
1844
1900
  const liveDbPath = dbPath(portalId);
1845
1901
  const nextReplicaName = nextReplicaFileName();
1846
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
+ });
1847
1913
  cleanupDbArtifacts(nextReplicaPath);
1848
- await handle.connection.run("CHECKPOINT");
1849
- fs3.copyFileSync(liveDbPath, nextReplicaPath);
1850
- writeReplicaPointer(portalId, nextReplicaName);
1851
- 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
+ }
1852
1984
  }
1853
1985
  async function ensureTable(connection, ddlStatements) {
1854
1986
  for (const statement of ddlStatements) {
@@ -2860,8 +2992,11 @@ var makeReplicaLive = (config = {}) => {
2860
2992
  dirty: true
2861
2993
  })),
2862
2994
  Effect25.flatMap(
2863
- () => logStderr3(
2864
- `[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
+ })
2865
3000
  )
2866
3001
  ),
2867
3002
  Effect25.flatMap(() => Effect25.fail(error))
@@ -3933,26 +4068,64 @@ var setSelectedPortal = (portalId) => {
3933
4068
  // src/effects/read-connection.ts
3934
4069
  import { Effect as Effect31, pipe as pipe19 } from "effect";
3935
4070
  import fs6 from "fs";
3936
- 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(
3937
4082
  Effect31.sync(() => replicaDbPath(portalId)),
3938
- Effect31.filterOrFail(
3939
- (dbFile) => fs6.existsSync(dbFile),
3940
- () => new DatabaseError({
3941
- message: `Read replica not found for portal ${portalId}. Has it been synced?`
3942
- })
3943
- ),
3944
- Effect31.flatMap(
3945
- () => 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({
3946
4105
  try: async () => {
3947
4106
  const handle = await getDatabaseConnection(portalId, null, { readOnly: true });
3948
4107
  return handle.connection;
3949
4108
  },
3950
- catch: (e) => new DatabaseError({
3951
- message: `Failed to open read connection for portal ${portalId}`,
3952
- cause: e
3953
- })
3954
- })
3955
- )
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
+ })
3956
4129
  );
3957
4130
  var closeReadConnection = (portalId) => Effect31.promise(() => evictDatabaseConnections(portalId));
3958
4131
 
@@ -5634,8 +5807,10 @@ var resolvePortalIds = (explicitPortalIds, selectedPortalId2, portals) => {
5634
5807
 
5635
5808
  // src/tools/query.ts
5636
5809
  var MAX_ROWS = 200;
5637
- var runQueryForPortal = async (portalId, finalSql, autoCapped) => {
5638
- 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
+ );
5639
5814
  const start = performance.now();
5640
5815
  const reader = await conn.runAndReadAll(finalSql);
5641
5816
  const rows = reader.getRowObjects();
@@ -5711,7 +5886,8 @@ Example queries:
5711
5886
  const result = await runQueryForPortal(
5712
5887
  portalId,
5713
5888
  finalSql,
5714
- !hasExplicitLimit
5889
+ !hasExplicitLimit,
5890
+ deps.isMasterClient()
5715
5891
  );
5716
5892
  return {
5717
5893
  content: [{ type: "text", text: stringifyResult(result) }]
@@ -5732,7 +5908,8 @@ Example queries:
5732
5908
  keyedResults[String(portalId)] = await runQueryForPortal(
5733
5909
  portalId,
5734
5910
  finalSql,
5735
- !hasExplicitLimit
5911
+ !hasExplicitLimit,
5912
+ deps.isMasterClient()
5736
5913
  );
5737
5914
  } catch (e) {
5738
5915
  hadAnyErrors = true;
@@ -5952,8 +6129,10 @@ function openInBrowser(target) {
5952
6129
 
5953
6130
  // src/tools/chart.ts
5954
6131
  var MAX_ROWS2 = 200;
5955
- var runQueryForPortal2 = async (portalId, sql) => {
5956
- 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
+ );
5957
6136
  const start = performance.now();
5958
6137
  const reader = await conn.runAndReadAll(sql);
5959
6138
  const rows = reader.getRowObjects();
@@ -6065,7 +6244,11 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
6065
6244
  const [portalId] = portalResolution.portalIds;
6066
6245
  try {
6067
6246
  await deps.ensureFresh(portalId);
6068
- const result = await runQueryForPortal2(portalId, finalSql);
6247
+ const result = await runQueryForPortal2(
6248
+ portalId,
6249
+ finalSql,
6250
+ deps.isMasterClient()
6251
+ );
6069
6252
  if (result.rows.length === 0) {
6070
6253
  return {
6071
6254
  content: [{ type: "text", text: "Query returned no rows - nothing to chart." }],
@@ -6119,7 +6302,11 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
6119
6302
  for (const portalId of portalResolution.portalIds) {
6120
6303
  try {
6121
6304
  await deps.ensureFresh(portalId);
6122
- const result = await runQueryForPortal2(portalId, finalSql);
6305
+ const result = await runQueryForPortal2(
6306
+ portalId,
6307
+ finalSql,
6308
+ deps.isMasterClient()
6309
+ );
6123
6310
  if (result.rows.length === 0) {
6124
6311
  hadErrors = true;
6125
6312
  keyedPortalResults[String(portalId)] = { error: "Query returned no rows - nothing to chart." };
@@ -6218,8 +6405,10 @@ async function buildConnectionSection(deps) {
6218
6405
  totalPortals: portals.length
6219
6406
  };
6220
6407
  }
6221
- async function buildSchemaSection(portalId) {
6222
- 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
+ );
6223
6412
  const allPluginTableNames = new Set(getAllTableNames());
6224
6413
  const sectionResults = await Promise.all(
6225
6414
  getPlugins().map(async (plugin) => {
@@ -6290,7 +6479,7 @@ Use "all" to get everything at once.`,
6290
6479
  for (const portalId of portalResolution.portalIds) {
6291
6480
  try {
6292
6481
  await deps.ensureFresh(portalId);
6293
- schemaByPortal[String(portalId)] = await buildSchemaSection(portalId);
6482
+ schemaByPortal[String(portalId)] = await buildSchemaSection(portalId, deps);
6294
6483
  } catch (e) {
6295
6484
  explicitSchemaHadErrors = true;
6296
6485
  const message = e instanceof Error ? e.message : String(e);
@@ -6301,7 +6490,7 @@ Use "all" to get everything at once.`,
6301
6490
  } else {
6302
6491
  const [portalId] = portalResolution.portalIds;
6303
6492
  await deps.ensureFresh(portalId);
6304
- result.schema = await buildSchemaSection(portalId);
6493
+ result.schema = await buildSchemaSection(portalId, deps);
6305
6494
  }
6306
6495
  }
6307
6496
  return {
@@ -19994,17 +20183,20 @@ var mainProgram = Effect108.gen(function* () {
19994
20183
  };
19995
20184
  registerPortalSelection(server, { ws, config, handoffSelectedPortal });
19996
20185
  registerQueryTool(server, {
20186
+ isMasterClient,
19997
20187
  getSelectedPortalId: getPortalId,
19998
20188
  getPortals,
19999
20189
  ensureFresh
20000
20190
  });
20001
20191
  registerChartTool(server, {
20192
+ isMasterClient,
20002
20193
  getSelectedPortalId: getPortalId,
20003
20194
  getPortals,
20004
20195
  ensureFresh
20005
20196
  });
20006
20197
  registerStatusTool(server, {
20007
20198
  getConnectionState: () => Effect108.runSync(ws.getState()),
20199
+ isMasterClient,
20008
20200
  getPortals,
20009
20201
  getSelectedPortalId: getPortalId,
20010
20202
  ensureFresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daeda/mcp-pro",
3
- "version": "0.1.30",
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": {