@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.5.22",
3
+ "version": "1.5.24",
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",
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. **\`tool_ids\` in YAML instead of \`tools\`** — write \`tools: [name1, name2]\`. \`tool_ids\` bypasses the codec and fails silently in prod.
1082
- 7. **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.
1083
- 8. **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.
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.14" },
1147
+ serverInfo: { name: "dypai", version: "1.5.24" },
1146
1148
  instructions: SERVER_INSTRUCTIONS,
1147
1149
  })
1148
1150
  }
@@ -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
@@ -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
  `),
@@ -57,6 +57,7 @@ function endpointPayload(row) {
57
57
  if (row.group_id) p.group_id = row.group_id
58
58
  if (row.input) p.input = row.input
59
59
  if (row.output) p.output = row.output
60
+ if (row.response_cardinality) p.response_cardinality = row.response_cardinality
60
61
  return p
61
62
  }
62
63
 
@@ -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") || "set"
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
  }