@dypai-ai/mcp 1.5.22 → 1.5.23
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 +7 -6
- package/src/tools/sync/codec.js +2 -0
- package/src/tools/sync/pull.js +11 -1
- package/src/tools/sync/push.js +1 -0
- package/src/tools/sync/validate.js +89 -0
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,10 @@ 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\`. \`dypai_validate\` errors with \`response_cardinality_required\`. Do not unwrap arrays in frontend code.
|
|
1082
|
+
7. **\`tool_ids\` in YAML instead of \`tools\`** — write \`tools: [name1, name2]\`. \`tool_ids\` bypasses the codec and fails silently in prod.
|
|
1083
|
+
8. **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.
|
|
1084
|
+
9. **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
1085
|
|
|
1085
1086
|
→ Longer list of common pitfalls + fixes: \`search_docs("troubleshooting")\`.
|
|
1086
1087
|
|
|
@@ -1101,7 +1102,7 @@ Mental translations: "edge function" → workflow with one code node; "cron" →
|
|
|
1101
1102
|
|
|
1102
1103
|
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
1104
|
|
|
1104
|
-
- **API**: \`dypai.api.get(name)\`, \`.post(name, body)\`, \`.put()\`, \`.delete()\`, \`.upload(name, file)\`, \`.stream(name, body)\`.
|
|
1105
|
+
- **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\`; never unwrap in the UI.
|
|
1105
1106
|
- **Auth**: \`dypai.auth.signInWithPassword()\`, \`.signUp()\`, \`.signOut()\`, \`.getSession()\`. **Never** create login/signup workflows — auth is built-in.
|
|
1106
1107
|
- **Hooks**: \`useAuth\`, \`useEndpoint\`, \`useAction\`, \`useUpload\`, \`useRealtime\`, \`<ProtectedRoute>\`.
|
|
1107
1108
|
- **Rule**: NEVER \`fetch()\` directly — always through the SDK.
|
|
@@ -1142,7 +1143,7 @@ async function handleRequest(msg) {
|
|
|
1142
1143
|
return makeResponse(id, {
|
|
1143
1144
|
protocolVersion: "2024-11-05",
|
|
1144
1145
|
capabilities: { tools: {} },
|
|
1145
|
-
serverInfo: { name: "dypai", version: "1.5.
|
|
1146
|
+
serverInfo: { name: "dypai", version: "1.5.23" },
|
|
1146
1147
|
instructions: SERVER_INSTRUCTIONS,
|
|
1147
1148
|
})
|
|
1148
1149
|
}
|
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
|
#
|
|
@@ -600,7 +610,7 @@ export const dypaiPullTool = {
|
|
|
600
610
|
const [endpoints, credentials, groups, schemaSql, nodeCatalogResult, realtimePolicies, draftsResult] = await Promise.all([
|
|
601
611
|
execSql(project_id, `
|
|
602
612
|
SELECT id, name, method, description, workflow_code, input, output,
|
|
603
|
-
allowed_roles, is_tool, tool_description, group_id, is_active, updated_at
|
|
613
|
+
response_cardinality, allowed_roles, is_tool, tool_description, group_id, is_active, updated_at
|
|
604
614
|
FROM system.endpoints
|
|
605
615
|
ORDER BY name
|
|
606
616
|
`),
|
package/src/tools/sync/push.js
CHANGED
|
@@ -29,6 +29,19 @@ 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 DATABASE_ROW_ARRAY_OPS = new Set([
|
|
34
|
+
"query",
|
|
35
|
+
"custom_query",
|
|
36
|
+
"select",
|
|
37
|
+
"update",
|
|
38
|
+
"delete",
|
|
39
|
+
"insert",
|
|
40
|
+
"mutation",
|
|
41
|
+
"aggregate",
|
|
42
|
+
"upsert",
|
|
43
|
+
"copy_to",
|
|
44
|
+
])
|
|
32
45
|
|
|
33
46
|
/**
|
|
34
47
|
* Validate dypai/realtime.yaml against known schemas/tables. Rules:
|
|
@@ -425,6 +438,17 @@ function nodeParams(node) {
|
|
|
425
438
|
: null
|
|
426
439
|
}
|
|
427
440
|
|
|
441
|
+
function nodeReturnsRowArray(node) {
|
|
442
|
+
const nodeType = node?.type ?? node?.node_type
|
|
443
|
+
if (nodeType === "set_fields" || nodeType === "javascript-code" || nodeType === "agent" || nodeType === "managed_ai") {
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
if (nodeType !== "dypai_database") return false
|
|
447
|
+
const params = nodeParams(node)
|
|
448
|
+
const op = nodeField(node, params, "operation")
|
|
449
|
+
return DATABASE_ROW_ARRAY_OPS.has(op)
|
|
450
|
+
}
|
|
451
|
+
|
|
428
452
|
function nodeField(node, params, key) {
|
|
429
453
|
return node?.[key] ?? params?.[key]
|
|
430
454
|
}
|
|
@@ -1001,9 +1025,50 @@ function validateEndpoint(entry, ctx) {
|
|
|
1001
1025
|
const nodeIds = new Set(workflowNodes.map(n => n.id))
|
|
1002
1026
|
const nodeTypes = buildNodeTypeMap(workflowNodes)
|
|
1003
1027
|
const outputContracts = buildNodeOutputContracts(workflowNodes, fileMap, ctx)
|
|
1028
|
+
const outputType = typeof doc.output?.type === "string" ? doc.output.type : undefined
|
|
1029
|
+
const responseCardinality =
|
|
1030
|
+
typeof doc.response_cardinality === "string" ? doc.response_cardinality.trim() : ""
|
|
1004
1031
|
|
|
1005
1032
|
const jwt = ruleUsesJwt(doc.trigger)
|
|
1006
1033
|
|
|
1034
|
+
if (responseCardinality === "one") {
|
|
1035
|
+
diagnostics.push({
|
|
1036
|
+
severity: "error",
|
|
1037
|
+
rule: "response_cardinality_legacy_one",
|
|
1038
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1039
|
+
message: "Endpoint uses response_cardinality: one, which is not supported.",
|
|
1040
|
+
fix_hint: "Use response_cardinality: single.",
|
|
1041
|
+
})
|
|
1042
|
+
} else if (responseCardinality && !VALID_RESPONSE_CARDINALITIES.has(responseCardinality)) {
|
|
1043
|
+
diagnostics.push({
|
|
1044
|
+
severity: "error",
|
|
1045
|
+
rule: "response_cardinality_invalid",
|
|
1046
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1047
|
+
message: `Invalid response_cardinality '${responseCardinality}'.`,
|
|
1048
|
+
fix_hint: "Use single, many, or zero_or_one.",
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (responseCardinality === "single" && outputType === "array") {
|
|
1053
|
+
diagnostics.push({
|
|
1054
|
+
severity: "error",
|
|
1055
|
+
rule: "response_cardinality_output_mismatch",
|
|
1056
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1057
|
+
message: "response_cardinality: single conflicts with output.type: array.",
|
|
1058
|
+
fix_hint: "Use response_cardinality: many or change output.type to object.",
|
|
1059
|
+
})
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (responseCardinality === "many" && outputType === "object") {
|
|
1063
|
+
diagnostics.push({
|
|
1064
|
+
severity: "error",
|
|
1065
|
+
rule: "response_cardinality_output_mismatch",
|
|
1066
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1067
|
+
message: "response_cardinality: many conflicts with output.type: object.",
|
|
1068
|
+
fix_hint: "Use response_cardinality: single/zero_or_one or change output.type to array.",
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1007
1072
|
// Collect all template-rendered strings, including query_file and prompts.
|
|
1008
1073
|
// Raw code is intentionally skipped: javascript_code.code/code_file may
|
|
1009
1074
|
// contain normal JS template literals such as `${where.join(" AND ")}`.
|
|
@@ -1391,6 +1456,15 @@ function validateEndpoint(entry, ctx) {
|
|
|
1391
1456
|
|
|
1392
1457
|
const declaredCardinality =
|
|
1393
1458
|
nodeField(node, nodeParams(node), "output_cardinality") || node.output_cardinality
|
|
1459
|
+
if (declaredCardinality === "one") {
|
|
1460
|
+
diagnostics.push({
|
|
1461
|
+
severity: "error",
|
|
1462
|
+
rule: "output_cardinality_legacy_one",
|
|
1463
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id}].output_cardinality`,
|
|
1464
|
+
message: `Node '${node.id}' uses output_cardinality: one, which is not supported.`,
|
|
1465
|
+
fix_hint: "Use output_cardinality: single.",
|
|
1466
|
+
})
|
|
1467
|
+
}
|
|
1394
1468
|
if (
|
|
1395
1469
|
nodeType === "dypai_database" &&
|
|
1396
1470
|
(nodeField(node, nodeParams(node), "operation") === "query" ||
|
|
@@ -1721,6 +1795,19 @@ function validateEndpoint(entry, ctx) {
|
|
|
1721
1795
|
const NEEDS_RESPONSE = new Set(["http_api", "webhook"])
|
|
1722
1796
|
const needsResponse = triggerKeys.some(k => NEEDS_RESPONSE.has(k))
|
|
1723
1797
|
const hasReturn = allNodes.some(n => n?.return === true || n?.is_return === true)
|
|
1798
|
+
const returnNodes = allNodes.filter(n => n?.return === true || n?.is_return === true)
|
|
1799
|
+
const returnNode = returnNodes.length === 1 ? returnNodes[0] : null
|
|
1800
|
+
|
|
1801
|
+
if (outputType === "object" && !responseCardinality && returnNode && nodeReturnsRowArray(returnNode)) {
|
|
1802
|
+
diagnostics.push({
|
|
1803
|
+
severity: "error",
|
|
1804
|
+
rule: "response_cardinality_required",
|
|
1805
|
+
endpoint: name, file, loc: "response_cardinality",
|
|
1806
|
+
message:
|
|
1807
|
+
`Endpoint '${name}' declares output.type: object but return node '${returnNode.id}' returns SQL row arrays.`,
|
|
1808
|
+
fix_hint: "Add response_cardinality: single, return an object with set_fields, or change output.type to array.",
|
|
1809
|
+
})
|
|
1810
|
+
}
|
|
1724
1811
|
|
|
1725
1812
|
if (needsResponse && allNodes.length > 0 && !hasReturn) {
|
|
1726
1813
|
diagnostics.push({
|
|
@@ -2105,7 +2192,9 @@ export const __testing = {
|
|
|
2105
2192
|
extractRuntimePaths,
|
|
2106
2193
|
inferSelectOutputColumns,
|
|
2107
2194
|
inferSqlCardinality,
|
|
2195
|
+
nodeReturnsRowArray,
|
|
2108
2196
|
unsupportedRuntimePlaceholder,
|
|
2197
|
+
validateEndpoint,
|
|
2109
2198
|
validateNodeOutputRef,
|
|
2110
2199
|
validateStripeNodePlaceholder,
|
|
2111
2200
|
}
|