@dypai-ai/mcp 1.7.3 → 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.3",
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",
@@ -23,7 +23,10 @@ async function compileSnapshot(projectId, command, files, endpoint = undefined)
23
23
  files,
24
24
  }
25
25
  if (endpoint) args.endpoint = endpoint
26
- return proxyToolCall("backend_compile_snapshot", args)
26
+ // Compiler `ok:false` is a domain validation result, not a transport/tool
27
+ // failure. It intentionally carries valid payloads plus diagnostics so the
28
+ // push pipeline can skip only broken flows instead of dropping all source.
29
+ return proxyToolCall("backend_compile_snapshot", args, { allowFailureResult: true })
27
30
  }
28
31
 
29
32
  export async function buildSnapshotPayload(dypaiRootDir, projectRoot = null) {
@@ -209,7 +209,15 @@ async function ensureInitialized() {
209
209
  return initPromise
210
210
  }
211
211
 
212
- export async function proxyToolCall(toolName, args) {
212
+ function shouldPromoteRemoteFailure(parsed, allowFailureResult = false) {
213
+ return !allowFailureResult
214
+ && parsed
215
+ && typeof parsed === "object"
216
+ && !Array.isArray(parsed)
217
+ && (parsed.success === false || parsed.ok === false)
218
+ }
219
+
220
+ export async function proxyToolCall(toolName, args, { allowFailureResult = false } = {}) {
213
221
  async function callOnce() {
214
222
  await ensureInitialized()
215
223
 
@@ -242,11 +250,9 @@ export async function proxyToolCall(toolName, args) {
242
250
  // Other remote errors come as a JSON object with success:false but no isError
243
251
  // flag. The push pipeline was treating these as successes — promote them to
244
252
  // throws here so the caller's try/catch sees them.
245
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
246
- if (parsed.success === false || parsed.ok === false) {
247
- const msg = parsed.error || parsed.message || parsed.detail || JSON.stringify(parsed).slice(0, 300)
248
- throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg))
249
- }
253
+ if (shouldPromoteRemoteFailure(parsed, allowFailureResult)) {
254
+ const msg = parsed.error || parsed.message || parsed.detail || JSON.stringify(parsed).slice(0, 300)
255
+ throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg))
250
256
  }
251
257
  return parsed
252
258
  }
@@ -270,3 +276,7 @@ export async function proxyToolCall(toolName, args) {
270
276
  throw error
271
277
  }
272
278
  }
279
+
280
+ export const __testing = {
281
+ shouldPromoteRemoteFailure,
282
+ }
@@ -539,15 +539,6 @@ export async function readLocalEffectiveState(rootDir, projectId = null, deps =
539
539
  if (shadowedMeta) shadowed.push({ name: payload.name, shadowed: shadowedMeta })
540
540
  }
541
541
 
542
- for (const diagnostic of diagnostics) {
543
- if (diagnostic?.severity !== "error") continue
544
- yamlState.errors.push({
545
- file: diagnostic.file || diagnostic.endpoint,
546
- error: diagnostic.message || "Failed to build flow push payload",
547
- rule: diagnostic.rule || "flow_push_payload_failed",
548
- })
549
- }
550
-
551
542
  for (const item of rejected) {
552
543
  if (!isBackendSourcePath(item?.path)) continue
553
544
  yamlState.errors.push({
@@ -562,7 +553,14 @@ export async function readLocalEffectiveState(rootDir, projectId = null, deps =
562
553
  byName[name] = { ...entry, source: "legacy-yaml" }
563
554
  }
564
555
 
565
- return { byName, errors: yamlState.errors, shadowed, runnerWarning: null, automations: bulkResult.data?.automations ?? null }
556
+ return {
557
+ byName,
558
+ errors: yamlState.errors,
559
+ shadowed,
560
+ runnerWarning: null,
561
+ compilerDiagnostics: diagnostics,
562
+ automations: bulkResult.data?.automations ?? null,
563
+ }
566
564
  }
567
565
 
568
566
  // ─── Normalization + diff ──────────────────────────────────────────────────
@@ -664,6 +662,39 @@ function localToComparable(entry, mapsCtx) {
664
662
  return remoteToComparable(row, mapsCtx)
665
663
  }
666
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
+
667
698
  function deepEqual(a, b) {
668
699
  if (a === b) return true
669
700
  if (typeof a !== typeof b) return false
@@ -771,6 +802,7 @@ export function detectConflicts(stateSnapshot, remoteByName) {
771
802
  export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, stateSnapshot = null, liveRemote = null } = {}) {
772
803
  const plan = { create: [], update: [], delete: [], unchanged: [] }
773
804
  const warnings = []
805
+ const compilerOnlyDrift = []
774
806
 
775
807
  const endpointNameCollisions = findEndpointNameCollisions(Object.keys(remote?.byName || {}))
776
808
  if (endpointNameCollisions.length) {
@@ -824,6 +856,19 @@ export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, sta
824
856
  }
825
857
 
826
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
+ }
827
872
  if (deepEqual(localCanonical, remoteCanonical)) {
828
873
  plan.unchanged.push(name)
829
874
  } else {
@@ -832,6 +877,16 @@ export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, sta
832
877
  }
833
878
  }
834
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
+
835
890
  if (deleteOrphans) {
836
891
  for (const name of Object.keys(remote.byName)) {
837
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.`
@@ -310,6 +310,41 @@ function dropErroringFromPlan(plan, erroringEndpoints, skippedDiagnostics) {
310
310
  : undefined
311
311
  }
312
312
 
313
+ function skippedEndpointsFromDiagnostics(diagnostics) {
314
+ const names = [...new Set(
315
+ (diagnostics || [])
316
+ .filter((item) => item?.severity === "error" && item.endpoint)
317
+ .map((item) => item.endpoint),
318
+ )]
319
+ return names.length
320
+ ? names.map((name) => ({
321
+ endpoint: name,
322
+ errors: diagnostics
323
+ .filter((item) => item.endpoint === name && item.severity === "error")
324
+ .slice(0, 5)
325
+ .map((item) => ({
326
+ rule: item.rule,
327
+ message: item.message,
328
+ file: item.file,
329
+ loc: item.loc,
330
+ })),
331
+ }))
332
+ : undefined
333
+ }
334
+
335
+ function mergeSkippedEndpointSummaries(...groups) {
336
+ const merged = new Map()
337
+ for (const group of groups) {
338
+ for (const item of group || []) {
339
+ const current = merged.get(item.endpoint)
340
+ merged.set(item.endpoint, current
341
+ ? { ...current, errors: [...(current.errors || []), ...(item.errors || [])].slice(0, 5) }
342
+ : item)
343
+ }
344
+ }
345
+ return merged.size ? [...merged.values()] : undefined
346
+ }
347
+
313
348
  function splitBlockingConflicts(plan, conflicts) {
314
349
  const touched = new Set([
315
350
  ...(plan.update || []).map((item) => item.name),
@@ -741,6 +776,20 @@ export const dypaiPushTool = {
741
776
  error: e.message,
742
777
  }
743
778
  }
779
+ const compilerValidation = partitionValidationErrors(local.compilerDiagnostics)
780
+ if (compilerValidation.projectLevel.length) {
781
+ return {
782
+ success: false,
783
+ applied: false,
784
+ reason: "compiler_validation_failed",
785
+ diagnostics: compilerValidation.projectLevel.slice(0, 20),
786
+ hint: "The compiler reported project-level errors that cannot be isolated to one endpoint.",
787
+ }
788
+ }
789
+ for (const endpoint of compilerValidation.erroringEndpoints) {
790
+ erroringEndpoints.add(endpoint)
791
+ }
792
+ skippedDiagnostics.push(...compilerValidation.errorDiags)
744
793
  try {
745
794
  remote = await fetchRemoteState(targetProjectId)
746
795
  } catch (e) {
@@ -817,7 +866,10 @@ export const dypaiPushTool = {
817
866
  // they aren't pushed (they keep their live version). Deletes stay — a broken
818
867
  // local flow shouldn't block removing endpoints. skip_validation leaves
819
868
  // erroringEndpoints empty, so nothing is dropped.
820
- const skipped_endpoints = dropErroringFromPlan(plan, erroringEndpoints, skippedDiagnostics)
869
+ const skipped_endpoints = mergeSkippedEndpointSummaries(
870
+ dropErroringFromPlan(plan, erroringEndpoints, skippedDiagnostics),
871
+ skippedEndpointsFromDiagnostics(compilerValidation.errorDiags),
872
+ )
821
873
 
822
874
  const groupChanges = (plan.groups?.create?.length || 0) + (plan.groups?.delete?.length || 0)
823
875
  const totalChanges = plan.create.length + plan.update.length + plan.delete.length + groupChanges
@@ -830,8 +882,13 @@ export const dypaiPushTool = {
830
882
  success: false,
831
883
  applied: false,
832
884
  reason: "conflicts_detected",
885
+ summary: summaryFromPlan(plan),
833
886
  conflicts: conflictSplit.blocking,
834
887
  non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
888
+ skipped_endpoints,
889
+ compiler_diagnostics: local.compilerDiagnostics?.length
890
+ ? local.compilerDiagnostics.slice(0, 50)
891
+ : undefined,
835
892
  hint: "Remote changes overlap endpoints this push would update/delete. Run dypai_pull to refresh local state, resolve the overlap, then push again. To override, pass force=true.",
836
893
  }
837
894
  }
@@ -861,6 +918,9 @@ export const dypaiPushTool = {
861
918
  : totalChanges === 0 ? "no_changes" : "dry_run",
862
919
  summary: summaryFromPlan(plan),
863
920
  skipped_endpoints,
921
+ compiler_diagnostics: local.compilerDiagnostics?.length
922
+ ? local.compilerDiagnostics.slice(0, 50)
923
+ : undefined,
864
924
  plan,
865
925
  non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
866
926
  types: typesGenerated,
@@ -1073,6 +1133,9 @@ export const dypaiPushTool = {
1073
1133
  details: applied,
1074
1134
  endpoint_results,
1075
1135
  skipped_endpoints,
1136
+ compiler_diagnostics: local.compilerDiagnostics?.length
1137
+ ? local.compilerDiagnostics.slice(0, 50)
1138
+ : undefined,
1076
1139
  non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
1077
1140
  errors: errors.length ? errors : undefined,
1078
1141
  types: typesGenerated,
@@ -1131,6 +1194,8 @@ export const __testing = {
1131
1194
  syncAutomationMetadata,
1132
1195
  partitionValidationErrors,
1133
1196
  dropErroringFromPlan,
1197
+ skippedEndpointsFromDiagnostics,
1198
+ mergeSkippedEndpointSummaries,
1134
1199
  normalizeEndpointSelection,
1135
1200
  scopePlanToEndpoints,
1136
1201
  backendSourcePathsForEndpointSelection,