@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 +3 -1
- package/src/tools/deploy.js +15 -1
- package/src/tools/sync/endpoint-names.js +17 -0
- package/src/tools/sync/pull.js +86 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dypai-ai/mcp",
|
|
3
|
-
"version": "1.7.
|
|
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": [
|
package/src/tools/deploy.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -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
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
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
|
|
915
|
+
return isEditableTsSource(source)
|
|
903
916
|
})
|
|
904
|
-
const persistedFlowSources =
|
|
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,
|