@dypai-ai/mcp 1.7.6 → 1.7.8

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.6",
3
+ "version": "1.7.8",
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",
@@ -9,6 +9,8 @@
9
9
  },
10
10
  "scripts": {
11
11
  "build": "node scripts/embed-prompts.mjs",
12
+ "test": "node --test test-*.js",
13
+ "test:integration": "RUN_DYPAI_INTEGRATION=1 node --test test-clinic-manager-smoke.js test-generate-types.js",
12
14
  "prepublishOnly": "npm run build"
13
15
  },
14
16
  "files": [
@@ -98,6 +98,16 @@ const BLOCKED = new Set([
98
98
 
99
99
  const ROOT_CONFIG_RE = /^(package\.json|package-lock\.json|pnpm-lock\.yaml|bun\.lockb|yarn\.lock|vite\.config|vitest\.config|tsconfig|tailwind\.config|postcss\.config|next\.config|vinext\.config|astro\.config|nuxt\.config|svelte\.config|remix\.config|gatsby-config|angular\.json|docusaurus\.config|wrangler\.toml|wrangler\.json|vercel\.json|netlify\.toml|turbo\.json|components\.json|uno\.config|eslint\.config|prettier\.config|jest\.config|playwright\.config|\.prettierrc|\.eslintrc|\.browserslistrc|\.nvmrc|\.node-version|index\.html)/
100
100
 
101
+ const ROOT_DYPAI_FILES = new Set([
102
+ "dypai/schema.sql",
103
+ "dypai/realtime.yaml",
104
+ "dypai/dypai.config.yaml",
105
+ "dypai/capability-catalog.json",
106
+ "dypai/capability-brief.md",
107
+ "dypai/node-catalog.json",
108
+ "dypai/README.md",
109
+ ])
110
+
101
111
  // ─── Helpers ────────────────────────────────────────────────────────────────
102
112
 
103
113
  function extOf(entry) {
@@ -203,12 +213,16 @@ function isAllowedRootDypaiSourcePath(path) {
203
213
  if (!path.startsWith("dypai/")) return true
204
214
  if (
205
215
  path.startsWith("dypai/flows/")
216
+ || path.startsWith("dypai/automations/")
206
217
  || path.startsWith("dypai/endpoints/")
207
218
  || path.startsWith("dypai/migrations/")
219
+ || path.startsWith("dypai/prompts/")
220
+ || path.startsWith("dypai/code/")
221
+ || path.startsWith("dypai/sql/")
208
222
  || path.startsWith("dypai/types/")
209
223
  || path.startsWith("dypai/lib/")
210
224
  ) return true
211
- return path === "dypai/schema.sql" || path === "dypai/realtime.yaml"
225
+ return ROOT_DYPAI_FILES.has(path)
212
226
  }
213
227
 
214
228
  function isSkippedRootDypaiDirectory(path) {
@@ -27,3 +27,20 @@ export function findEndpointNameCollisions(endpointsOrNames) {
27
27
  .filter((item) => item.endpoints.length > 1)
28
28
  .sort((a, b) => a.canonical_name.localeCompare(b.canonical_name))
29
29
  }
30
+
31
+ /**
32
+ * Return only the non-canonical aliases from underscore/hyphen collision pairs.
33
+ *
34
+ * A name is considered a legacy alias only when the exact canonical hyphen
35
+ * slug also exists. This deliberately avoids guessing when a project only has
36
+ * underscore names, or when two unusual aliases normalize to a slug that does
37
+ * not exist remotely.
38
+ */
39
+ export function findLegacyAliasEndpointNames(endpointsOrNames) {
40
+ return findEndpointNameCollisions(endpointsOrNames)
41
+ .filter((collision) => collision.endpoints.includes(collision.canonical_name))
42
+ .flatMap((collision) => collision.endpoints
43
+ .filter((name) => name !== collision.canonical_name)
44
+ .map((name) => ({ name, canonical_name: collision.canonical_name })))
45
+ .sort((a, b) => a.name.localeCompare(b.name))
46
+ }
@@ -12,7 +12,7 @@
12
12
  * - .dypai/state.json — per-endpoint updated_at for conflict detection on push
13
13
  */
14
14
 
15
- import { writeFile, mkdir, rm, access, readFile } from "fs/promises"
15
+ import { writeFile, mkdir, rm, access, readFile, readdir } from "fs/promises"
16
16
  import { existsSync } from "fs"
17
17
  import { join, resolve as resolvePath, dirname, isAbsolute, delimiter, sep } from "path"
18
18
  import { homedir } from "os"
@@ -21,7 +21,7 @@ import { proxyToolCall } from "../proxy.js"
21
21
  import { api } from "../../api.js"
22
22
  import { serializeEndpoint } from "./codec.js"
23
23
  import { dumpPublicSchema } from "./schema-dump.js"
24
- import { findEndpointNameCollisions } from "./endpoint-names.js"
24
+ import { findEndpointNameCollisions, findLegacyAliasEndpointNames } from "./endpoint-names.js"
25
25
  // Codegen was removed from v1 — the agent reads dypai/schema.sql + endpoint YAMLs
26
26
  // directly instead of relying on auto-generated TS types. The codegen.js file
27
27
  // still lives on disk in case we resurface it later with a leaner design.
@@ -602,27 +602,34 @@ async function fetchPersistedFlowSources(projectId) {
602
602
  }
603
603
 
604
604
  /** Remove endpoint YAML files that would shadow or confuse a Flow-authored endpoint. */
605
- async function removeEndpointYamlFiles(outDir, name, groupName = null) {
606
- const candidates = [
607
- join(outDir, "endpoints", `${name}.yaml`),
608
- join(outDir, "endpoints", `${name}.yml`),
609
- join(outDir, "endpoints", `${name}.yaml.disabled`),
610
- join(outDir, "endpoints", `${name}.yml.disabled`),
611
- ]
612
- if (groupName) {
613
- candidates.push(
614
- join(outDir, "endpoints", groupName, `${name}.yaml`),
615
- join(outDir, "endpoints", groupName, `${name}.yml`),
616
- join(outDir, "endpoints", groupName, `${name}.yaml.disabled`),
617
- join(outDir, "endpoints", groupName, `${name}.yml.disabled`),
618
- )
619
- }
605
+ async function removeEndpointYamlFiles(outDir, name) {
606
+ const targetNames = new Set([
607
+ `${name}.yaml`,
608
+ `${name}.yml`,
609
+ `${name}.yaml.disabled`,
610
+ `${name}.yml.disabled`,
611
+ ])
620
612
  const removed = []
621
- for (const filePath of candidates) {
622
- if (!existsSync(filePath)) continue
623
- await rm(filePath, { force: true })
624
- removed.push(filePath.slice(outDir.length + 1))
613
+
614
+ async function walk(dir) {
615
+ let entries
616
+ try {
617
+ entries = await readdir(dir, { withFileTypes: true })
618
+ } catch {
619
+ return
620
+ }
621
+ for (const entry of entries) {
622
+ const filePath = join(dir, entry.name)
623
+ if (entry.isDirectory()) {
624
+ await walk(filePath)
625
+ } else if (targetNames.has(entry.name)) {
626
+ await rm(filePath, { force: true })
627
+ removed.push(filePath.slice(outDir.length + 1))
628
+ }
629
+ }
625
630
  }
631
+
632
+ await walk(join(outDir, "endpoints"))
626
633
  return removed
627
634
  }
628
635
 
@@ -870,6 +877,7 @@ export const dypaiPullTool = {
870
877
  const filesWritten = []
871
878
  const errors = []
872
879
  const sourceRecoveryWarnings = []
880
+ const ignoredLegacyAliases = []
873
881
 
874
882
  const capabilityGroups = capabilityCatalogResult?.groups || []
875
883
  const capabilityDoc = {
@@ -897,11 +905,16 @@ export const dypaiPullTool = {
897
905
  endpointIdToName: Object.fromEntries(endpoints.map(e => [e.id, e.name])),
898
906
  }
899
907
 
900
- const flowAuthoredEndpoints = endpoints.filter(raw => {
908
+ const endpointNameCollisions = findEndpointNameCollisions(endpoints)
909
+ const legacyAliasByName = new Map(
910
+ findLegacyAliasEndpointNames(endpoints).map((item) => [item.name, item]),
911
+ )
912
+
913
+ const editableSourceEndpoints = endpoints.filter(raw => {
901
914
  const source = parseMaybeJson(raw.workflow_source)
902
- return isFlowTsSource(source)
915
+ return isEditableTsSource(source)
903
916
  })
904
- const persistedFlowSources = flowAuthoredEndpoints.length
917
+ const persistedFlowSources = editableSourceEndpoints.length
905
918
  ? await fetchPersistedFlowSources(project_id)
906
919
  : { byPath: new Map(), ref: null, error: null }
907
920
 
@@ -944,6 +957,24 @@ export const dypaiPullTool = {
944
957
  row.workflow = parseMaybeJson(row.workflow)
945
958
  row.workflow_source = parseMaybeJson(row.workflow_source)
946
959
  try {
960
+ const legacyAlias = legacyAliasByName.get(row.name)
961
+ if (legacyAlias && !live_authoritative) {
962
+ const removedYaml = await removeEndpointYamlFiles(outDir, row.name)
963
+ if (removedYaml.length) filesWritten.push(...removedYaml.map(p => `(removed ${p})`))
964
+ ignoredLegacyAliases.push({
965
+ endpoint: row.name,
966
+ canonical_endpoint: legacyAlias.canonical_name,
967
+ id: row.id,
968
+ revision: row.revision,
969
+ updated_at: row.updated_at,
970
+ })
971
+ // The live alias remains intentionally remote-only. Recording it in
972
+ // state.json keeps conflict metadata fresh without making it an
973
+ // editable local endpoint that a later push could overwrite.
974
+ successfullyPulled.add(row.id)
975
+ continue
976
+ }
977
+
947
978
  const workflowSource = row.workflow_source
948
979
  if (isEditableTsSource(workflowSource) && !live_authoritative) {
949
980
  const groupName = row.group_id ? (mapsCtx.groupIdToName[row.group_id] || null) : null
@@ -955,9 +986,10 @@ export const dypaiPullTool = {
955
986
  const removedYaml = await removeEndpointYamlFiles(outDir, row.name, groupName)
956
987
  if (removedYaml.length) filesWritten.push(...removedYaml.map(p => `(removed ${p})`))
957
988
 
989
+ const flowPath = join(outDir, relFlowPath)
990
+ const existingFlowContent = await readUtf8IfExists(flowPath)
991
+
958
992
  if (flowContent != null) {
959
- const flowPath = join(outDir, relFlowPath)
960
- const existingFlowContent = await readUtf8IfExists(flowPath)
961
993
  const writeDecision = classifyFlowSourceWrite({
962
994
  existingContent: existingFlowContent,
963
995
  remoteContent: flowContent,
@@ -991,6 +1023,26 @@ export const dypaiPullTool = {
991
1023
  continue
992
1024
  }
993
1025
 
1026
+ if (existingFlowContent != null) {
1027
+ keptLocalEndpoints.push({
1028
+ endpoint: row.name,
1029
+ id: row.id,
1030
+ remote_updated_at: row.updated_at,
1031
+ remote_revision: row.revision,
1032
+ file: relFlowPath,
1033
+ reason: "persisted_source_missing_local_source_preserved",
1034
+ })
1035
+ sourceRecoveryWarnings.push({
1036
+ endpoint: row.name,
1037
+ source_file: sourceFile,
1038
+ recovered_as: "existing_local_typescript",
1039
+ warning:
1040
+ `Endpoint is source-authored but ${sourceFile} was not returned by persisted project source. ` +
1041
+ "Kept the existing local TypeScript source and did not create a duplicate legacy YAML endpoint.",
1042
+ })
1043
+ continue
1044
+ }
1045
+
994
1046
  // The editable TS source has been lost, but live still contains the
995
1047
  // complete executable workflow. Recover it as legacy YAML so a live
996
1048
  // repair pull remains complete and its state baseline stays honest.
@@ -1129,8 +1181,6 @@ export const dypaiPullTool = {
1129
1181
  const inactiveEndpoints = (endpoints || [])
1130
1182
  .filter(e => e.is_active === false)
1131
1183
  .map(e => e.name)
1132
- const endpointNameCollisions = findEndpointNameCollisions(endpoints)
1133
-
1134
1184
  // `environment` is an internal flag we still surface in `overview.project`
1135
1185
  // for diagnostics / dashboard parity, but the agent-facing copy uses the
1136
1186
  // universal "drafts → publish" vocabulary regardless of the value.
@@ -1210,6 +1260,13 @@ export const dypaiPullTool = {
1210
1260
  }
1211
1261
 
1212
1262
  const safetyWarnings = [
1263
+ ...(ignoredLegacyAliases.length ? [{
1264
+ type: "legacy_endpoint_aliases_kept_remote_only",
1265
+ count: ignoredLegacyAliases.length,
1266
+ aliases: ignoredLegacyAliases.slice(0, 50),
1267
+ truncated: ignoredLegacyAliases.length > 50,
1268
+ hint: "Canonical hyphen endpoints are editable locally. Legacy underscore aliases remain live for compatibility but are not materialized as active YAML, so pull/validate/push cannot overwrite them accidentally.",
1269
+ }] : []),
1213
1270
  ...(endpointNameCollisions.length ? [{
1214
1271
  type: "canonical_endpoint_name_collisions",
1215
1272
  count: endpointNameCollisions.length,
@@ -1229,6 +1286,7 @@ export const dypaiPullTool = {
1229
1286
  files_kept_local: keptLocalEndpoints.length,
1230
1287
  kept_local_endpoints: keptLocalEndpoints.length ? keptLocalEndpoints : undefined,
1231
1288
  source_recovery_warnings: sourceRecoveryWarnings.length ? sourceRecoveryWarnings : undefined,
1289
+ ignored_legacy_aliases: ignoredLegacyAliases.length ? ignoredLegacyAliases : undefined,
1232
1290
  name_collisions: endpointNameCollisions.length,
1233
1291
  output_dir: outDir,
1234
1292
  out_dir_resolved_via: outDirSource,