@dypai-ai/mcp 1.7.4 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "DYPAI MCP Server — AI agent toolkit for building and deploying full-stack apps",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -662,6 +662,39 @@ function localToComparable(entry, mapsCtx) {
662
662
  return remoteToComparable(row, mapsCtx)
663
663
  }
664
664
 
665
+ export function flowSourceHashesMatch(entry, remoteRow) {
666
+ if (entry?.source !== "flow" || !entry.pushPayload || !remoteRow) return false
667
+ const localHash = entry.pushPayload.workflow_code?.metadata?.source_hash
668
+ const remoteHash = remoteRow.workflow_code?.metadata?.source_hash
669
+ return typeof localHash === "string"
670
+ && localHash.length > 0
671
+ && localHash === remoteHash
672
+ }
673
+
674
+ export function preserveMatchingRemoteFileRefs(rawLocalDoc, localComparable, remoteComparable) {
675
+ if (!rawLocalDoc || !localComparable || !remoteComparable) return localComparable
676
+ if (Array.isArray(rawLocalDoc)) {
677
+ if (!Array.isArray(localComparable) || !Array.isArray(remoteComparable)) return localComparable
678
+ rawLocalDoc.forEach((item, index) => {
679
+ preserveMatchingRemoteFileRefs(item, localComparable[index], remoteComparable[index])
680
+ })
681
+ return localComparable
682
+ }
683
+ if (typeof rawLocalDoc !== "object" || Array.isArray(localComparable) || Array.isArray(remoteComparable)) {
684
+ return localComparable
685
+ }
686
+
687
+ for (const [key, value] of Object.entries(rawLocalDoc)) {
688
+ if (key.endsWith("_file") && typeof value === "string" && remoteComparable[key] === value) {
689
+ delete localComparable[key.replace(/_file$/, "")]
690
+ localComparable[key] = value
691
+ continue
692
+ }
693
+ preserveMatchingRemoteFileRefs(value, localComparable[key], remoteComparable[key])
694
+ }
695
+ return localComparable
696
+ }
697
+
665
698
  function deepEqual(a, b) {
666
699
  if (a === b) return true
667
700
  if (typeof a !== typeof b) return false
@@ -769,6 +802,7 @@ export function detectConflicts(stateSnapshot, remoteByName) {
769
802
  export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, stateSnapshot = null, liveRemote = null } = {}) {
770
803
  const plan = { create: [], update: [], delete: [], unchanged: [] }
771
804
  const warnings = []
805
+ const compilerOnlyDrift = []
772
806
 
773
807
  const endpointNameCollisions = findEndpointNameCollisions(Object.keys(remote?.byName || {}))
774
808
  if (endpointNameCollisions.length) {
@@ -822,6 +856,19 @@ export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, sta
822
856
  }
823
857
 
824
858
  const remoteCanonical = remoteToComparable(remoteRow, mapsCtx)
859
+ if (entry.doc) {
860
+ preserveMatchingRemoteFileRefs(entry.doc, localCanonical, remoteCanonical)
861
+ }
862
+ if (
863
+ flowSourceHashesMatch(entry, remoteRow)
864
+ && !deepEqual(localCanonical.workflow, remoteCanonical.workflow)
865
+ ) {
866
+ // Same authored source, different compiler artifact (for example after
867
+ // a compiler upgrade). Keep comparing roles/contracts/description, but
868
+ // do not stage a mass endpoint update solely to rewrite generated IR.
869
+ localCanonical.workflow = remoteCanonical.workflow
870
+ compilerOnlyDrift.push(name)
871
+ }
825
872
  if (deepEqual(localCanonical, remoteCanonical)) {
826
873
  plan.unchanged.push(name)
827
874
  } else {
@@ -830,6 +877,16 @@ export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, sta
830
877
  }
831
878
  }
832
879
 
880
+ if (compilerOnlyDrift.length) {
881
+ warnings.push({
882
+ type: "compiler_artifact_drift_ignored",
883
+ count: compilerOnlyDrift.length,
884
+ examples: compilerOnlyDrift.slice(0, 25),
885
+ truncated: compilerOnlyDrift.length > 25,
886
+ hint: "Authored Flow source_hash matches live. Generated workflow differences from a compiler upgrade were not staged.",
887
+ })
888
+ }
889
+
833
890
  if (deleteOrphans) {
834
891
  for (const name of Object.keys(remote.byName)) {
835
892
  if (!local.byName[name]) plan.delete.push({ name })
@@ -761,10 +761,15 @@ export const dypaiPullTool = {
761
761
  description: "If true, removes existing endpoints/, sql/, prompts/ before writing and allows persisted Flow source to overwrite differing local .flow.ts files.",
762
762
  default: false,
763
763
  },
764
+ live_authoritative: {
765
+ type: "boolean",
766
+ description: "Recovery mode. Export every live workflow as legacy YAML instead of trusting persisted Flow/Automation source. Use only when live is intentionally authoritative and source has drifted.",
767
+ default: false,
768
+ },
764
769
  },
765
770
  },
766
771
 
767
- async execute({ project_id, out_dir = "./dypai", clean = false } = {}) {
772
+ async execute({ project_id, out_dir = "./dypai", clean = false, live_authoritative = false } = {}) {
768
773
  const { path: outDir, source: outDirSource } = resolveOutDir(out_dir)
769
774
  const suspiciousWarning = suspiciousPathWarning(outDir, outDirSource)
770
775
 
@@ -864,6 +869,7 @@ export const dypaiPullTool = {
864
869
 
865
870
  const filesWritten = []
866
871
  const errors = []
872
+ const sourceRecoveryWarnings = []
867
873
 
868
874
  const capabilityGroups = capabilityCatalogResult?.groups || []
869
875
  const capabilityDoc = {
@@ -939,7 +945,7 @@ export const dypaiPullTool = {
939
945
  row.workflow_source = parseMaybeJson(row.workflow_source)
940
946
  try {
941
947
  const workflowSource = row.workflow_source
942
- if (isEditableTsSource(workflowSource)) {
948
+ if (isEditableTsSource(workflowSource) && !live_authoritative) {
943
949
  const groupName = row.group_id ? (mapsCtx.groupIdToName[row.group_id] || null) : null
944
950
  const sourceFile = normalizeEditableSourceFile(workflowSource, row.name)
945
951
  const relFlowPath = flowSourceRelPath(sourceFile)
@@ -985,18 +991,30 @@ export const dypaiPullTool = {
985
991
  continue
986
992
  }
987
993
 
988
- const relPath = groupName
989
- ? `endpoints/${groupName}/${row.name}.yaml.disabled`
990
- : `endpoints/${row.name}.yaml.disabled`
991
- await writeFileEnsured(join(outDir, relPath), renderMissingFlowStub(row, sourceFile, persistedFlowSources))
992
- filesWritten.push(relPath)
993
- errors.push({
994
+ // The editable TS source has been lost, but live still contains the
995
+ // complete executable workflow. Recover it as legacy YAML so a live
996
+ // repair pull remains complete and its state baseline stays honest.
997
+ // The warning makes the source loss visible without dropping the
998
+ // endpoint from disk or producing a false clean dry-run.
999
+ sourceRecoveryWarnings.push({
994
1000
  endpoint: row.name,
995
- error:
1001
+ source_file: sourceFile,
1002
+ recovered_as: "legacy_yaml",
1003
+ warning:
996
1004
  `Endpoint is source-authored but ${sourceFile} was not found in persisted project source. ` +
997
- `Restore/sync the editable TypeScript source, then rerun dypai_pull.`,
1005
+ "Recovered the live workflow as legacy YAML; restore editable TypeScript later if desired.",
998
1006
  })
999
- continue
1007
+ row.workflow_source = null
1008
+ }
1009
+
1010
+ if (isEditableTsSource(workflowSource) && live_authoritative) {
1011
+ sourceRecoveryWarnings.push({
1012
+ endpoint: row.name,
1013
+ source_file: normalizeEditableSourceFile(workflowSource, row.name),
1014
+ recovered_as: "legacy_yaml",
1015
+ warning: "Live-authoritative recovery exported the executable workflow as legacy YAML. The previous editable TypeScript source remains available in the backup/history.",
1016
+ })
1017
+ row.workflow_source = null
1000
1018
  }
1001
1019
 
1002
1020
  const { doc, extractedFiles } = serializeEndpoint(row, mapsCtx)
@@ -1191,12 +1209,26 @@ export const dypaiPullTool = {
1191
1209
  : ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/, Automations in dypai/automations/, or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push."],
1192
1210
  }
1193
1211
 
1212
+ const safetyWarnings = [
1213
+ ...(endpointNameCollisions.length ? [{
1214
+ type: "canonical_endpoint_name_collisions",
1215
+ count: endpointNameCollisions.length,
1216
+ examples: endpointNameCollisions.slice(0, 10),
1217
+ hint: "Legacy underscore and canonical hyphen slugs coexist. Treat them as separate live APIs; audit traffic and migrate/delete deliberately.",
1218
+ }] : []),
1219
+ ...sourceRecoveryWarnings.map((item) => ({
1220
+ type: "missing_editable_source_recovered",
1221
+ ...item,
1222
+ })),
1223
+ ]
1224
+
1194
1225
  return {
1195
1226
  success: errors.length === 0,
1196
1227
  endpoints: endpoints.length,
1197
1228
  files_written: filesWritten.length,
1198
1229
  files_kept_local: keptLocalEndpoints.length,
1199
1230
  kept_local_endpoints: keptLocalEndpoints.length ? keptLocalEndpoints : undefined,
1231
+ source_recovery_warnings: sourceRecoveryWarnings.length ? sourceRecoveryWarnings : undefined,
1200
1232
  name_collisions: endpointNameCollisions.length,
1201
1233
  output_dir: outDir,
1202
1234
  out_dir_resolved_via: outDirSource,
@@ -1206,12 +1238,7 @@ export const dypaiPullTool = {
1206
1238
  overview,
1207
1239
  errors: errors.length ? errors : undefined,
1208
1240
  warning: suspiciousWarning || undefined,
1209
- safety_warnings: endpointNameCollisions.length ? [{
1210
- type: "canonical_endpoint_name_collisions",
1211
- count: endpointNameCollisions.length,
1212
- examples: endpointNameCollisions.slice(0, 10),
1213
- hint: "Legacy underscore and canonical hyphen slugs coexist. Treat them as separate live APIs; audit traffic and migrate/delete deliberately.",
1214
- }] : undefined,
1241
+ safety_warnings: safetyWarnings.length ? safetyWarnings : undefined,
1215
1242
  hint: errors.length
1216
1243
  ? keptLocalEndpoints.length
1217
1244
  ? `${keptLocalEndpoints.length} local Flow file(s) differed and were kept. They were excluded from .dypai/state.json, so a later push will continue to block on remote drift. Review the files or rerun with clean:true only when remote is intentionally authoritative.`