@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.5.22",
3
+ "version": "1.5.23",
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,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. **\`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\`. \`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.14" },
1146
+ serverInfo: { name: "dypai", version: "1.5.23" },
1146
1147
  instructions: SERVER_INSTRUCTIONS,
1147
1148
  })
1148
1149
  }
@@ -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
  #
@@ -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
  `),
@@ -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,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
  }