@dypai-ai/mcp 1.7.8 → 1.7.10
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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { proxyToolCall } from "../proxy.js"
|
|
13
|
+
import { fetchRemoteCredentials } from "./remote-metadata.js"
|
|
13
14
|
|
|
14
15
|
async function execSql(projectId, sql) {
|
|
15
16
|
// Bypass remote execute_sql auto-LIMIT 20 so describe counts match reality.
|
|
@@ -59,7 +60,7 @@ export const dypaiDescribeTool = {
|
|
|
59
60
|
LEFT JOIN system.endpoints_group g ON g.id = e.group_id
|
|
60
61
|
ORDER BY e.name
|
|
61
62
|
`),
|
|
62
|
-
|
|
63
|
+
fetchRemoteCredentials(project_id).catch(() => null),
|
|
63
64
|
execSql(project_id, "SELECT name, description FROM system.roles ORDER BY name"),
|
|
64
65
|
execSql(project_id, `
|
|
65
66
|
SELECT id, name, public FROM storage.buckets ORDER BY name
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
runEffectiveWorkflows,
|
|
18
18
|
} from "../../lib/effective-workflows-runner.js"
|
|
19
19
|
import { findEndpointNameCollisions } from "./endpoint-names.js"
|
|
20
|
+
import { fetchRemoteCredentials } from "./remote-metadata.js"
|
|
20
21
|
|
|
21
22
|
const ENDPOINT_NAME_RE = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/
|
|
22
23
|
|
|
@@ -195,7 +196,7 @@ export async function fetchRemoteState(projectId) {
|
|
|
195
196
|
FROM system.endpoints
|
|
196
197
|
ORDER BY name
|
|
197
198
|
`)
|
|
198
|
-
const credentials = await
|
|
199
|
+
const credentials = await fetchRemoteCredentials(projectId)
|
|
199
200
|
const groups = await execSql(projectId, "SELECT id, name FROM system.endpoints_group")
|
|
200
201
|
|
|
201
202
|
const mapsCtx = {
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -22,6 +22,7 @@ import { api } from "../../api.js"
|
|
|
22
22
|
import { serializeEndpoint } from "./codec.js"
|
|
23
23
|
import { dumpPublicSchema } from "./schema-dump.js"
|
|
24
24
|
import { findEndpointNameCollisions, findLegacyAliasEndpointNames } from "./endpoint-names.js"
|
|
25
|
+
import { fetchRemoteCredentials } from "./remote-metadata.js"
|
|
25
26
|
// Codegen was removed from v1 — the agent reads dypai/schema.sql + endpoint YAMLs
|
|
26
27
|
// directly instead of relying on auto-generated TS types. The codegen.js file
|
|
27
28
|
// still lives on disk in case we resurface it later with a leaner design.
|
|
@@ -817,7 +818,7 @@ export const dypaiPullTool = {
|
|
|
817
818
|
FROM system.endpoints
|
|
818
819
|
ORDER BY name
|
|
819
820
|
`),
|
|
820
|
-
|
|
821
|
+
fetchRemoteCredentials(project_id),
|
|
821
822
|
execSql(project_id, "SELECT id, name FROM system.endpoints_group"),
|
|
822
823
|
dumpPublicSchema(project_id),
|
|
823
824
|
// Dump the global node catalog (central control plane) in one call.
|
package/src/tools/sync/push.js
CHANGED
|
@@ -223,6 +223,69 @@ async function refreshBackendSourceBaseline(projectId, rootDir, sourceCheckpoint
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
async function readComputePolicy(rootDir) {
|
|
227
|
+
try {
|
|
228
|
+
const raw = await readFile(join(rootDir, "compute.json"), "utf8")
|
|
229
|
+
const parsed = JSON.parse(raw)
|
|
230
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
231
|
+
throw new Error("dypai/compute.json must contain an object")
|
|
232
|
+
}
|
|
233
|
+
return parsed
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error?.code === "ENOENT") return null
|
|
236
|
+
if (error instanceof SyntaxError) {
|
|
237
|
+
throw new Error("dypai/compute.json is not valid JSON")
|
|
238
|
+
}
|
|
239
|
+
throw error
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function prepareEndpointPayloadsForCompute(projectId, rootDir, names, local, mapsCtx) {
|
|
244
|
+
if (!names.length) {
|
|
245
|
+
return { ok: true, payloadsByName: {}, compute: { artifacts: 0, uploaded: 0, registered: 0 } }
|
|
246
|
+
}
|
|
247
|
+
const payloads = names.map((name) => {
|
|
248
|
+
const entry = local.byName[name]
|
|
249
|
+
if (!entry) throw new Error(`Compute preparation is missing local endpoint '${name}'`)
|
|
250
|
+
return localToCanonical(entry, mapsCtx)
|
|
251
|
+
})
|
|
252
|
+
const computePolicy = await readComputePolicy(rootDir)
|
|
253
|
+
const result = await api.post(
|
|
254
|
+
`/api/engine/${projectId}/backend/snapshot/prepare-push-payloads`,
|
|
255
|
+
{
|
|
256
|
+
payloads,
|
|
257
|
+
...(computePolicy ? { compute_policy: computePolicy } : {}),
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
if (!result || result.ok !== true || !Array.isArray(result.payloads)) {
|
|
261
|
+
throw new Error("Core API returned an invalid Compute preparation response")
|
|
262
|
+
}
|
|
263
|
+
const payloadsByName = {}
|
|
264
|
+
for (const payload of result.payloads) {
|
|
265
|
+
const name = typeof payload?.name === "string" ? payload.name : ""
|
|
266
|
+
if (!name || payloadsByName[name]) {
|
|
267
|
+
throw new Error("Core API returned duplicate or unnamed Compute payloads")
|
|
268
|
+
}
|
|
269
|
+
payloadsByName[name] = payload
|
|
270
|
+
}
|
|
271
|
+
const missing = names.filter((name) => !payloadsByName[name])
|
|
272
|
+
if (missing.length) {
|
|
273
|
+
throw new Error(`Core API omitted Compute payloads: ${missing.slice(0, 5).join(", ")}`)
|
|
274
|
+
}
|
|
275
|
+
return { ok: true, payloadsByName, compute: result.compute || {} }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function canonicalPreparedEndpoint(name, local, preparedByName, mapsCtx) {
|
|
279
|
+
const prepared = preparedByName[name]
|
|
280
|
+
if (!prepared) return localToCanonical(local.byName[name], mapsCtx)
|
|
281
|
+
const entry = local.byName[name]
|
|
282
|
+
return localToCanonical({
|
|
283
|
+
source: "flow",
|
|
284
|
+
group: entry?.group || prepared.group_name || null,
|
|
285
|
+
pushPayload: prepared,
|
|
286
|
+
}, mapsCtx)
|
|
287
|
+
}
|
|
288
|
+
|
|
226
289
|
/**
|
|
227
290
|
* Run an async worker over an array with bounded concurrency. Workers never
|
|
228
291
|
* throw (they handle their own errors via the push errors[] accumulator),
|
|
@@ -947,6 +1010,33 @@ export const dypaiPushTool = {
|
|
|
947
1010
|
// pushing 50 endpoints doesn't take 50 * 200ms = 10s — drops to ~2s.
|
|
948
1011
|
// Group operations are sequential because they're fast (a handful of
|
|
949
1012
|
// groups typically) and we need mapsCtx mutations to be race-free.
|
|
1013
|
+
const endpointNamesToWrite = [...new Set([
|
|
1014
|
+
...plan.create.map((item) => item.name),
|
|
1015
|
+
...plan.update.map((item) => item.name),
|
|
1016
|
+
])]
|
|
1017
|
+
let computePreparation
|
|
1018
|
+
try {
|
|
1019
|
+
computePreparation = await prepareEndpointPayloadsForCompute(
|
|
1020
|
+
targetProjectId,
|
|
1021
|
+
rootDir,
|
|
1022
|
+
endpointNamesToWrite,
|
|
1023
|
+
local,
|
|
1024
|
+
remote.mapsCtx,
|
|
1025
|
+
)
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
return {
|
|
1028
|
+
success: false,
|
|
1029
|
+
applied: false,
|
|
1030
|
+
phase: "prepare_compute",
|
|
1031
|
+
error: error?.body?.detail?.message
|
|
1032
|
+
|| error?.detail?.message
|
|
1033
|
+
|| error?.detail
|
|
1034
|
+
|| error?.message
|
|
1035
|
+
|| String(error),
|
|
1036
|
+
hint: "No backend drafts were written. Update/retry after the Compute preparation path is healthy.",
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const preparedByName = computePreparation.payloadsByName
|
|
950
1040
|
const CONCURRENCY = PUSH_CONCURRENCY
|
|
951
1041
|
// `applied.*_drafts` tracks the per-op subset that the API staged as
|
|
952
1042
|
// drafts (default behavior). The lists in `created/updated/deleted`
|
|
@@ -974,7 +1064,7 @@ export const dypaiPushTool = {
|
|
|
974
1064
|
|
|
975
1065
|
await runWithConcurrency(plan.create, CONCURRENCY, async (item) => {
|
|
976
1066
|
try {
|
|
977
|
-
const canonical =
|
|
1067
|
+
const canonical = canonicalPreparedEndpoint(item.name, local, preparedByName, remote.mapsCtx)
|
|
978
1068
|
const res = await applyCreate(canonical, targetProjectId)
|
|
979
1069
|
applied.created.push(item.name)
|
|
980
1070
|
if (isDraftResponse(res)) applied.created_drafts.push(item.name)
|
|
@@ -989,7 +1079,7 @@ export const dypaiPushTool = {
|
|
|
989
1079
|
|
|
990
1080
|
await runWithConcurrency(plan.update, CONCURRENCY, async (item) => {
|
|
991
1081
|
try {
|
|
992
|
-
const canonical =
|
|
1082
|
+
const canonical = canonicalPreparedEndpoint(item.name, local, preparedByName, remote.mapsCtx)
|
|
993
1083
|
const endpointId = remote.byName[item.name]?.id
|
|
994
1084
|
const res = endpointId
|
|
995
1085
|
? await applyUpdate(canonical, endpointId, targetProjectId, remote.byName[item.name]?.revision)
|
|
@@ -1139,6 +1229,7 @@ export const dypaiPushTool = {
|
|
|
1139
1229
|
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
1140
1230
|
errors: errors.length ? errors : undefined,
|
|
1141
1231
|
types: typesGenerated,
|
|
1232
|
+
compute: computePreparation.compute,
|
|
1142
1233
|
automation_sync: automationSync,
|
|
1143
1234
|
source_checkpoint: sourceCheckpoint,
|
|
1144
1235
|
source_baseline: sourceBaseline,
|
|
@@ -1199,5 +1290,8 @@ export const __testing = {
|
|
|
1199
1290
|
normalizeEndpointSelection,
|
|
1200
1291
|
scopePlanToEndpoints,
|
|
1201
1292
|
backendSourcePathsForEndpointSelection,
|
|
1293
|
+
readComputePolicy,
|
|
1294
|
+
prepareEndpointPayloadsForCompute,
|
|
1295
|
+
canonicalPreparedEndpoint,
|
|
1202
1296
|
PUSH_CONCURRENCY,
|
|
1203
1297
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { proxyToolCall } from "../proxy.js"
|
|
2
|
+
|
|
3
|
+
const CREDENTIAL_PAGE_SIZE = 50
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read credential metadata through the dedicated, secret-safe MCP tool.
|
|
7
|
+
*
|
|
8
|
+
* `execute_sql` intentionally rejects system.credentials because that table
|
|
9
|
+
* also stores encrypted secret material. Sync only needs id/name/type, which
|
|
10
|
+
* get_app_credentials exposes without returning any secret values.
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchRemoteCredentials(
|
|
13
|
+
projectId,
|
|
14
|
+
{ call = proxyToolCall, pageSize = CREDENTIAL_PAGE_SIZE } = {},
|
|
15
|
+
) {
|
|
16
|
+
const credentials = []
|
|
17
|
+
let offset = 0
|
|
18
|
+
|
|
19
|
+
while (true) {
|
|
20
|
+
const args = { limit: pageSize, offset }
|
|
21
|
+
if (projectId) args.project_id = projectId
|
|
22
|
+
|
|
23
|
+
const result = await call("get_app_credentials", args)
|
|
24
|
+
const page = Array.isArray(result?.credentials) ? result.credentials : []
|
|
25
|
+
credentials.push(...page)
|
|
26
|
+
|
|
27
|
+
const total = Number(result?.total)
|
|
28
|
+
if (
|
|
29
|
+
page.length === 0
|
|
30
|
+
|| page.length < pageSize
|
|
31
|
+
|| (Number.isFinite(total) && credentials.length >= total)
|
|
32
|
+
) {
|
|
33
|
+
return credentials
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
offset += page.length
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const __testing = { CREDENTIAL_PAGE_SIZE }
|