@dypai-ai/mcp 1.5.22 → 1.5.24
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 +1 -1
- package/src/index.js +8 -6
- package/src/tools/sync/codec.js +2 -0
- package/src/tools/sync/pull.js +13 -1
- package/src/tools/sync/push.js +1 -0
- package/src/tools/sync/validate.js +114 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1057,7 +1057,7 @@ Endpoint naming is strict: the YAML \`name\` is the public API slug and must exa
|
|
|
1057
1057
|
|
|
1058
1058
|
Mental translations: "edge function" → workflow with one code node; "cron" → \`trigger.schedule\` in the YAML; "webhook receiver" → \`trigger.webhook\`; "internal API" → \`trigger.http_api auth_mode:jwt\`.
|
|
1059
1059
|
|
|
1060
|
-
→ Full workflow patterns + YAML shape: \`search_docs("workflow patterns")\` and \`search_docs("trigger model")\`. Node catalog (full input/output schemas): read \`dypai/node-catalog.json\`.
|
|
1060
|
+
→ Full workflow patterns + YAML shape: \`search_docs("workflow patterns")\` and \`search_docs("trigger model")\`. Public HTTP response vs internal node output: \`search_docs("placeholder cheatsheet")\`. Node catalog (full input/output schemas): read \`dypai/node-catalog.json\`.
|
|
1061
1061
|
|
|
1062
1062
|
## What the engine handles for you (don't reinvent)
|
|
1063
1063
|
|
|
@@ -1078,9 +1078,11 @@ Mental translations: "edge function" → workflow with one code node; "cron" →
|
|
|
1078
1078
|
3. **Treating \`dypai_push\` as a deploy** — it's "save as draft", not publish. Live traffic is untouched until \`manage_drafts(publish, confirm:true)\`. Push freely, only ask the user before publish.
|
|
1079
1079
|
4. **\`public\` auth_mode with \`\${current_user_id}\`** — no JWT → placeholder empty → SQL fails or returns wrong data. Use \`jwt\` if you need the user.
|
|
1080
1080
|
5. **Missing \`return: true\`** — endpoint returns \`null\`. Every path that should produce an HTTP response needs one node with \`return: true\`.
|
|
1081
|
-
6.
|
|
1082
|
-
7.
|
|
1083
|
-
8.
|
|
1081
|
+
6. **SQL return + \`output.type: object\` without \`response_cardinality\` or \`set_fields\`** — runtime body is \`[{...}]\` but the frontend expects \`{...}\`. Add top-level \`response_cardinality: single\` for direct SQL returns, or compose the public object with \`set_fields operation: compose\`. \`dypai_validate\` errors with \`response_cardinality_required\`. Do not unwrap arrays in frontend code.
|
|
1082
|
+
7. **\`set_fields\` without \`operation\`** — runtime throws \`Unknown Set Fields operation: undefined\`. Wrapper responses need \`operation: compose\`; adding fields to upstream data uses \`operation: set\`. \`dypai_validate\` / \`backend_validate\` error with \`set_fields_operation_missing\`.
|
|
1083
|
+
8. **\`tool_ids\` in YAML instead of \`tools\`** — write \`tools: [name1, name2]\`. \`tool_ids\` bypasses the codec and fails silently in prod.
|
|
1084
|
+
9. **Putting workflow placeholders inside \`javascript_code.code\`** — code is raw JavaScript, so JS template literals like \`\${where.join(" AND ")}\` are safe and not rendered by DYPAI. Pass workflow values via \`input_data\`, \`ctx.nodes\`, \`ctx.user\`, or \`ctx.env\`; do not write \`\${input.email}\` inside code or set \`code\` from another node output.
|
|
1085
|
+
10. **Human endpoint names** — \`name: Listar videos\` in \`list-videos.yaml\` creates a draft the frontend cannot call as \`list-videos\`. \`dypai_validate\` and \`dypai_push\` reject this; fix the slug instead of testing around it.
|
|
1084
1086
|
|
|
1085
1087
|
→ Longer list of common pitfalls + fixes: \`search_docs("troubleshooting")\`.
|
|
1086
1088
|
|
|
@@ -1101,7 +1103,7 @@ Mental translations: "edge function" → workflow with one code node; "cron" →
|
|
|
1101
1103
|
|
|
1102
1104
|
SDK is pre-configured at \`src/lib/dypai.ts\` (or \`src/dypai.ts\`). Import \`dypai\` from there. Every method returns \`{ data, error }\` — never throws.
|
|
1103
1105
|
|
|
1104
|
-
- **API**: \`dypai.api.get(name)\`, \`.post(name, body)\`, \`.put()\`, \`.delete()\`, \`.upload(name, file)\`, \`.stream(name, body)\`.
|
|
1106
|
+
- **API**: \`dypai.api.get(name)\`, \`.post(name, body)\`, \`.put()\`, \`.delete()\`, \`.upload(name, file)\`, \`.stream(name, body)\`. \`response.data\` matches the endpoint \`output\` schema — if SQL returns row arrays, declare \`response_cardinality: single\` or use \`set_fields operation: compose\`; never unwrap in the UI.
|
|
1105
1107
|
- **Auth**: \`dypai.auth.signInWithPassword()\`, \`.signUp()\`, \`.signOut()\`, \`.getSession()\`. **Never** create login/signup workflows — auth is built-in.
|
|
1106
1108
|
- **Hooks**: \`useAuth\`, \`useEndpoint\`, \`useAction\`, \`useUpload\`, \`useRealtime\`, \`<ProtectedRoute>\`.
|
|
1107
1109
|
- **Rule**: NEVER \`fetch()\` directly — always through the SDK.
|
|
@@ -1142,7 +1144,7 @@ async function handleRequest(msg) {
|
|
|
1142
1144
|
return makeResponse(id, {
|
|
1143
1145
|
protocolVersion: "2024-11-05",
|
|
1144
1146
|
capabilities: { tools: {} },
|
|
1145
|
-
serverInfo: { name: "dypai", version: "1.5.
|
|
1147
|
+
serverInfo: { name: "dypai", version: "1.5.24" },
|
|
1146
1148
|
instructions: SERVER_INSTRUCTIONS,
|
|
1147
1149
|
})
|
|
1148
1150
|
}
|
package/src/tools/sync/codec.js
CHANGED
|
@@ -205,6 +205,7 @@ export function serializeEndpoint(row, mapsCtx) {
|
|
|
205
205
|
}
|
|
206
206
|
if (row.input) doc.input = row.input
|
|
207
207
|
if (row.output) doc.output = row.output
|
|
208
|
+
if (row.response_cardinality) doc.response_cardinality = row.response_cardinality
|
|
208
209
|
doc.trigger = triggersToYaml(wf.execution_config?.triggers)
|
|
209
210
|
|
|
210
211
|
const workflow = { nodes }
|
|
@@ -354,6 +355,7 @@ export function deserializeEndpoint(doc, mapsCtx) {
|
|
|
354
355
|
workflow_code,
|
|
355
356
|
input: doc.input || null,
|
|
356
357
|
output: doc.output || null,
|
|
358
|
+
response_cardinality: doc.response_cardinality || null,
|
|
357
359
|
allowed_roles: doc.allowed_roles || [],
|
|
358
360
|
// Accept both `tool: true` (canonical) and `is_tool: true` (engine-style).
|
|
359
361
|
// Without this, an agent that wrote `is_tool` in YAML would silently get
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -194,6 +194,7 @@ input:
|
|
|
194
194
|
|
|
195
195
|
# ─── Output schema (optional but recommended) ───────────────────────────────
|
|
196
196
|
# Describes the response shape. Helps the validator + frontend type-checkers.
|
|
197
|
+
# This is the PUBLIC HTTP body (what dypai.api.*().data receives).
|
|
197
198
|
output:
|
|
198
199
|
type: object
|
|
199
200
|
properties:
|
|
@@ -201,6 +202,15 @@ output:
|
|
|
201
202
|
total: { type: number }
|
|
202
203
|
status: { type: string }
|
|
203
204
|
|
|
205
|
+
# ─── Public response cardinality (optional) ───────────────────────────────────
|
|
206
|
+
# Only needed when the return node is dypai_database (SQL row arrays) and
|
|
207
|
+
# output.type is object. The engine unwraps [{...}] -> {...} at the HTTP boundary.
|
|
208
|
+
# response_cardinality: single # one row / INSERT RETURNING *
|
|
209
|
+
# response_cardinality: many # public body is an array (output.type: array)
|
|
210
|
+
# response_cardinality: zero_or_one # lookup; [] -> null
|
|
211
|
+
# Prefer set_fields (see build_response below) when composing { items, total, ... }.
|
|
212
|
+
# Node-level output_cardinality is internal-only for \${nodes.<id>.field} placeholders.
|
|
213
|
+
|
|
204
214
|
# ─── Workflow ───────────────────────────────────────────────────────────────
|
|
205
215
|
# Nodes are the steps. Placeholders wire data flow between them:
|
|
206
216
|
#
|
|
@@ -277,6 +287,7 @@ workflow:
|
|
|
277
287
|
# \`return: true\` — it marks that node's output as the HTTP body.
|
|
278
288
|
- id: build_response
|
|
279
289
|
type: set_fields
|
|
290
|
+
operation: compose
|
|
280
291
|
return: true
|
|
281
292
|
fields:
|
|
282
293
|
order_id: \${nodes.insert_order.id}
|
|
@@ -287,6 +298,7 @@ workflow:
|
|
|
287
298
|
# Multiple return nodes are fine as long as only one runs per execution.
|
|
288
299
|
- id: out_of_stock
|
|
289
300
|
type: set_fields
|
|
301
|
+
operation: compose
|
|
290
302
|
return: true
|
|
291
303
|
fields:
|
|
292
304
|
error: out_of_stock
|
|
@@ -600,7 +612,7 @@ export const dypaiPullTool = {
|
|
|
600
612
|
const [endpoints, credentials, groups, schemaSql, nodeCatalogResult, realtimePolicies, draftsResult] = await Promise.all([
|
|
601
613
|
execSql(project_id, `
|
|
602
614
|
SELECT id, name, method, description, workflow_code, input, output,
|
|
603
|
-
allowed_roles, is_tool, tool_description, group_id, is_active, updated_at
|
|
615
|
+
response_cardinality, allowed_roles, is_tool, tool_description, group_id, is_active, updated_at
|
|
604
616
|
FROM system.endpoints
|
|
605
617
|
ORDER BY name
|
|
606
618
|
`),
|
package/src/tools/sync/push.js
CHANGED
|
@@ -29,6 +29,20 @@ import {
|
|
|
29
29
|
* Within the freshness window, we trust it without re-checking the remote.
|
|
30
30
|
* Outside the window, sql_table_not_found triggers a remote verification. */
|
|
31
31
|
const SCHEMA_FRESHNESS_MS = 5 * 60 * 1000 // 5 minutes
|
|
32
|
+
const VALID_RESPONSE_CARDINALITIES = new Set(["single", "many", "zero_or_one"])
|
|
33
|
+
const VALID_SET_FIELDS_OPERATIONS = new Set(["set", "compose", "rename", "remove", "keep", "merge"])
|
|
34
|
+
const DATABASE_ROW_ARRAY_OPS = new Set([
|
|
35
|
+
"query",
|
|
36
|
+
"custom_query",
|
|
37
|
+
"select",
|
|
38
|
+
"update",
|
|
39
|
+
"delete",
|
|
40
|
+
"insert",
|
|
41
|
+
"mutation",
|
|
42
|
+
"aggregate",
|
|
43
|
+
"upsert",
|
|
44
|
+
"copy_to",
|
|
45
|
+
])
|
|
32
46
|
|
|
33
47
|
/**
|
|
34
48
|
* Validate dypai/realtime.yaml against known schemas/tables. Rules:
|
|
@@ -425,6 +439,17 @@ function nodeParams(node) {
|
|
|
425
439
|
: null
|
|
426
440
|
}
|
|
427
441
|
|
|
442
|
+
function nodeReturnsRowArray(node) {
|
|
443
|
+
const nodeType = node?.type ?? node?.node_type
|
|
444
|
+
if (nodeType === "set_fields" || nodeType === "javascript-code" || nodeType === "agent" || nodeType === "managed_ai") {
|
|
445
|
+
return false
|
|
446
|
+
}
|
|
447
|
+
if (nodeType !== "dypai_database") return false
|
|
448
|
+
const params = nodeParams(node)
|
|
449
|
+
const op = nodeField(node, params, "operation")
|
|
450
|
+
return DATABASE_ROW_ARRAY_OPS.has(op)
|
|
451
|
+
}
|
|
452
|
+
|
|
428
453
|
function nodeField(node, params, key) {
|
|
429
454
|
return node?.[key] ?? params?.[key]
|
|
430
455
|
}
|
|
@@ -780,7 +805,8 @@ function buildNodeOutputContracts(nodes, fileMap, ctx) {
|
|
|
780
805
|
const params = nodeParams(node)
|
|
781
806
|
|
|
782
807
|
if (nodeType === "set_fields") {
|
|
783
|
-
const op = nodeField(node, params, "operation")
|
|
808
|
+
const op = nodeField(node, params, "operation")
|
|
809
|
+
if (!op) continue
|
|
784
810
|
const fields = nodeField(node, params, "fields")
|
|
785
811
|
if (fields && typeof fields === "object" && !Array.isArray(fields)) {
|
|
786
812
|
contracts.set(node.id, {
|
|
@@ -1001,9 +1027,50 @@ function validateEndpoint(entry, ctx) {
|
|
|
1001
1027
|
const nodeIds = new Set(workflowNodes.map(n => n.id))
|
|
1002
1028
|
const nodeTypes = buildNodeTypeMap(workflowNodes)
|
|
1003
1029
|
const outputContracts = buildNodeOutputContracts(workflowNodes, fileMap, ctx)
|
|
1030
|
+
const outputType = typeof doc.output?.type === "string" ? doc.output.type : undefined
|
|
1031
|
+
const responseCardinality =
|
|
1032
|
+
typeof doc.response_cardinality === "string" ? doc.response_cardinality.trim() : ""
|
|
1004
1033
|
|
|
1005
1034
|
const jwt = ruleUsesJwt(doc.trigger)
|
|
1006
1035
|
|
|
1036
|
+
if (responseCardinality === "one") {
|
|
1037
|
+
diagnostics.push({
|
|
1038
|
+
severity: "error",
|
|
1039
|
+
rule: "response_cardinality_legacy_one",
|
|
1040
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1041
|
+
message: "Endpoint uses response_cardinality: one, which is not supported.",
|
|
1042
|
+
fix_hint: "Use response_cardinality: single.",
|
|
1043
|
+
})
|
|
1044
|
+
} else if (responseCardinality && !VALID_RESPONSE_CARDINALITIES.has(responseCardinality)) {
|
|
1045
|
+
diagnostics.push({
|
|
1046
|
+
severity: "error",
|
|
1047
|
+
rule: "response_cardinality_invalid",
|
|
1048
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1049
|
+
message: `Invalid response_cardinality '${responseCardinality}'.`,
|
|
1050
|
+
fix_hint: "Use single, many, or zero_or_one.",
|
|
1051
|
+
})
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (responseCardinality === "single" && outputType === "array") {
|
|
1055
|
+
diagnostics.push({
|
|
1056
|
+
severity: "error",
|
|
1057
|
+
rule: "response_cardinality_output_mismatch",
|
|
1058
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1059
|
+
message: "response_cardinality: single conflicts with output.type: array.",
|
|
1060
|
+
fix_hint: "Use response_cardinality: many or change output.type to object.",
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (responseCardinality === "many" && outputType === "object") {
|
|
1065
|
+
diagnostics.push({
|
|
1066
|
+
severity: "error",
|
|
1067
|
+
rule: "response_cardinality_output_mismatch",
|
|
1068
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1069
|
+
message: "response_cardinality: many conflicts with output.type: object.",
|
|
1070
|
+
fix_hint: "Use response_cardinality: single/zero_or_one or change output.type to array.",
|
|
1071
|
+
})
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1007
1074
|
// Collect all template-rendered strings, including query_file and prompts.
|
|
1008
1075
|
// Raw code is intentionally skipped: javascript_code.code/code_file may
|
|
1009
1076
|
// contain normal JS template literals such as `${where.join(" AND ")}`.
|
|
@@ -1372,6 +1439,28 @@ function validateEndpoint(entry, ctx) {
|
|
|
1372
1439
|
}
|
|
1373
1440
|
}
|
|
1374
1441
|
}
|
|
1442
|
+
|
|
1443
|
+
if (nodeType === "set_fields") {
|
|
1444
|
+
const params = nodeParams(node)
|
|
1445
|
+
const op = nodeField(node, params, "operation")
|
|
1446
|
+
if (!op) {
|
|
1447
|
+
diagnostics.push({
|
|
1448
|
+
severity: "error",
|
|
1449
|
+
rule: "set_fields_operation_missing",
|
|
1450
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id || "?"}]`,
|
|
1451
|
+
message: `set_fields node '${node.id || "(no id)"}' is missing operation.`,
|
|
1452
|
+
fix_hint: "Add operation: compose when building a fresh response object, or operation: set when adding fields to upstream input.",
|
|
1453
|
+
})
|
|
1454
|
+
} else if (!VALID_SET_FIELDS_OPERATIONS.has(op)) {
|
|
1455
|
+
diagnostics.push({
|
|
1456
|
+
severity: "error",
|
|
1457
|
+
rule: "set_fields_operation_invalid",
|
|
1458
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id || "?"}].operation`,
|
|
1459
|
+
message: `set_fields node '${node.id || "(no id)"}' has unsupported operation '${op}'.`,
|
|
1460
|
+
fix_hint: `Use one of: ${[...VALID_SET_FIELDS_OPERATIONS].join(", ")}.`,
|
|
1461
|
+
})
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1375
1464
|
if (node.tool_ids !== undefined && node.tools === undefined) {
|
|
1376
1465
|
const toolIds = Array.isArray(node.tool_ids) ? node.tool_ids : []
|
|
1377
1466
|
const legacyUuidToolIds = toolIds.length === 0 || toolIds.every(isUuidString)
|
|
@@ -1391,6 +1480,15 @@ function validateEndpoint(entry, ctx) {
|
|
|
1391
1480
|
|
|
1392
1481
|
const declaredCardinality =
|
|
1393
1482
|
nodeField(node, nodeParams(node), "output_cardinality") || node.output_cardinality
|
|
1483
|
+
if (declaredCardinality === "one") {
|
|
1484
|
+
diagnostics.push({
|
|
1485
|
+
severity: "error",
|
|
1486
|
+
rule: "output_cardinality_legacy_one",
|
|
1487
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id}].output_cardinality`,
|
|
1488
|
+
message: `Node '${node.id}' uses output_cardinality: one, which is not supported.`,
|
|
1489
|
+
fix_hint: "Use output_cardinality: single.",
|
|
1490
|
+
})
|
|
1491
|
+
}
|
|
1394
1492
|
if (
|
|
1395
1493
|
nodeType === "dypai_database" &&
|
|
1396
1494
|
(nodeField(node, nodeParams(node), "operation") === "query" ||
|
|
@@ -1721,6 +1819,19 @@ function validateEndpoint(entry, ctx) {
|
|
|
1721
1819
|
const NEEDS_RESPONSE = new Set(["http_api", "webhook"])
|
|
1722
1820
|
const needsResponse = triggerKeys.some(k => NEEDS_RESPONSE.has(k))
|
|
1723
1821
|
const hasReturn = allNodes.some(n => n?.return === true || n?.is_return === true)
|
|
1822
|
+
const returnNodes = allNodes.filter(n => n?.return === true || n?.is_return === true)
|
|
1823
|
+
const returnNode = returnNodes.length === 1 ? returnNodes[0] : null
|
|
1824
|
+
|
|
1825
|
+
if (outputType === "object" && !responseCardinality && returnNode && nodeReturnsRowArray(returnNode)) {
|
|
1826
|
+
diagnostics.push({
|
|
1827
|
+
severity: "error",
|
|
1828
|
+
rule: "response_cardinality_required",
|
|
1829
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1830
|
+
message:
|
|
1831
|
+
`Endpoint '${name}' declares output.type: object but return node '${returnNode.id}' returns SQL row arrays.`,
|
|
1832
|
+
fix_hint: "Add response_cardinality: single, return an object with set_fields, or change output.type to array.",
|
|
1833
|
+
})
|
|
1834
|
+
}
|
|
1724
1835
|
|
|
1725
1836
|
if (needsResponse && allNodes.length > 0 && !hasReturn) {
|
|
1726
1837
|
diagnostics.push({
|
|
@@ -2105,7 +2216,9 @@ export const __testing = {
|
|
|
2105
2216
|
extractRuntimePaths,
|
|
2106
2217
|
inferSelectOutputColumns,
|
|
2107
2218
|
inferSqlCardinality,
|
|
2219
|
+
nodeReturnsRowArray,
|
|
2108
2220
|
unsupportedRuntimePlaceholder,
|
|
2221
|
+
validateEndpoint,
|
|
2109
2222
|
validateNodeOutputRef,
|
|
2110
2223
|
validateStripeNodePlaceholder,
|
|
2111
2224
|
}
|