@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.7.8",
3
+ "version": "1.7.10",
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",
@@ -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
- execSql(project_id, "SELECT name, type FROM system.credentials ORDER BY name"),
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 execSql(projectId, "SELECT id, name, type FROM system.credentials")
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 = {
@@ -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
- execSql(project_id, "SELECT id, name, type FROM system.credentials"),
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.
@@ -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 = localToCanonical(local.byName[item.name], remote.mapsCtx)
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 = localToCanonical(local.byName[item.name], remote.mapsCtx)
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 }