@dypai-ai/mcp 1.5.18 → 1.5.22

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.
@@ -19,6 +19,11 @@ import { join, resolve as resolvePath } from "path"
19
19
  import { fetchRemoteState, readLocalState, readLocalConfig, readLocalRealtime } from "./planner.js"
20
20
  import { proxyToolCall } from "../proxy.js"
21
21
  import { dumpPublicSchema } from "./schema-dump.js"
22
+ import {
23
+ sqlProvesSingleRow,
24
+ outputSchemaPropertyNames,
25
+ workflowOutputKeys,
26
+ } from "../../lib/workflow-placeholder-contract.js"
22
27
 
23
28
  /** schema.sql is considered "fresh" if it was written less than this ago.
24
29
  * Within the freshness window, we trust it without re-checking the remote.
@@ -424,6 +429,10 @@ function nodeField(node, params, key) {
424
429
  return node?.[key] ?? params?.[key]
425
430
  }
426
431
 
432
+ function isUuidString(value) {
433
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
434
+ }
435
+
427
436
  function lookupFileMapContent(fileMap, ref) {
428
437
  if (!fileMap || typeof ref !== "string" || !ref.trim()) return ""
429
438
  const key = ref.trim()
@@ -740,6 +749,11 @@ function inferSqlCardinality(sqlText) {
740
749
  const sql = stripSqlForInference(sqlText)
741
750
  if (!sql) return null
742
751
  if (hasTopLevelLimitOne(sql) || hasTopLevelFetchOne(sql)) return "single"
752
+ // INSERT ... RETURNING is one row unless the caller uses bulk mode.
753
+ // UPDATE/DELETE ... RETURNING may affect many existing rows, so keep those as "many".
754
+ if (/\breturning\b/i.test(sql) && /^insert\b/i.test(sql.trim())) {
755
+ return "single"
756
+ }
743
757
  const startsLikeRead = /^(select|with)\b/i.test(sql)
744
758
  if (startsLikeRead && hasTopLevelSetOperation(sql)) return "many"
745
759
  const outerSelectList = findOuterSelectList(sql)
@@ -785,9 +799,14 @@ function buildNodeOutputContracts(nodes, fileMap, ctx) {
785
799
  if (op === "query" || op === "custom_query") {
786
800
  const query = nodeField(node, params, "query") || lookupFileMapContent(fileMap, nodeField(node, params, "query_file"))
787
801
  const inferred = inferSelectOutputColumns(query)
802
+ const declared = nodeField(node, params, "output_cardinality") || node.output_cardinality
803
+ const cardinality =
804
+ declared === "single" || declared === "many"
805
+ ? declared
806
+ : inferSqlCardinality(query)
788
807
  contracts.set(node.id, {
789
808
  type: "array",
790
- cardinality: inferSqlCardinality(query),
809
+ cardinality,
791
810
  properties: inferred.properties,
792
811
  strict: inferred.strict,
793
812
  })
@@ -895,6 +914,33 @@ function parseNodeOutputRef(path) {
895
914
  return { nodeId, indexed, prop }
896
915
  }
897
916
 
917
+ function buildNodeTypeMap(nodes) {
918
+ const map = new Map()
919
+ for (const node of nodes || []) {
920
+ if (!node || typeof node !== "object" || !node.id) continue
921
+ map.set(node.id, node.type ?? node.node_type)
922
+ }
923
+ return map
924
+ }
925
+
926
+ function validateStripeNodePlaceholder({ expr, path, nodeId, nodeTypes, endpoint, file, loc }) {
927
+ const nodeType = nodeTypes.get(nodeId)
928
+ if (nodeType !== "stripe") return null
929
+ const prefix = `nodes.${nodeId}.`
930
+ if (!path.startsWith(prefix)) return null
931
+ const rest = path.slice(prefix.length)
932
+ if (!rest || rest.startsWith("output.")) return null
933
+ return {
934
+ severity: "error",
935
+ rule: "stripe_output_wrong_path",
936
+ endpoint, file, loc,
937
+ message: `\${${expr}} is not the Stripe node output shape. Stripe returns { success, output: { metadata: { id, url, ... } } }.`,
938
+ fix_hint:
939
+ `Use \${nodes.${nodeId}.output.metadata.url} for checkout URL and \${nodes.${nodeId}.output.metadata.id} for session id. ` +
940
+ `See search_docs('stripe payments').`,
941
+ }
942
+ }
943
+
898
944
  function validateNodeOutputRef({ expr, path, contract, endpoint, file, loc }) {
899
945
  const ref = parseNodeOutputRef(path)
900
946
  if (!ref || !ref.prop || !contract) return null
@@ -919,7 +965,9 @@ function validateNodeOutputRef({ expr, path, contract, endpoint, file, loc }) {
919
965
  rule: "node_output_many_direct_property",
920
966
  endpoint, file, loc,
921
967
  message: `\${${expr}} reads property '${ref.prop}' directly from node '${ref.nodeId}', but that node is inferred to return many rows.`,
922
- fix_hint: `Use \${nodes.${ref.nodeId}} for the full array, or \${nodes.${ref.nodeId}[0].${ref.prop}} if you intentionally want the first row.`,
968
+ fix_hint:
969
+ `For multi-row query results, use \${nodes.${ref.nodeId}[0].${ref.prop}} for the first row, \${nodes.${ref.nodeId}} for the full array, or a set_fields / javascript_code node. ` +
970
+ `For INSERT ... RETURNING (single row), prefer \${nodes.${ref.nodeId}.${ref.prop}} without [0].`,
923
971
  }
924
972
  }
925
973
 
@@ -949,8 +997,10 @@ function validateEndpoint(entry, ctx) {
949
997
  const file = ctx.fileByName[name] || `endpoints/${name}.yaml`
950
998
 
951
999
  const inputProps = doc.input?.properties || {}
952
- const nodeIds = new Set((doc.workflow?.nodes || []).map(n => n.id))
953
- const outputContracts = buildNodeOutputContracts(doc.workflow?.nodes || [], fileMap, ctx)
1000
+ const workflowNodes = doc.workflow?.nodes || []
1001
+ const nodeIds = new Set(workflowNodes.map(n => n.id))
1002
+ const nodeTypes = buildNodeTypeMap(workflowNodes)
1003
+ const outputContracts = buildNodeOutputContracts(workflowNodes, fileMap, ctx)
954
1004
 
955
1005
  const jwt = ruleUsesJwt(doc.trigger)
956
1006
 
@@ -1038,6 +1088,17 @@ function validateEndpoint(entry, ctx) {
1038
1088
  missingNodeRefs.set(nodeId, { loc, expr })
1039
1089
  }
1040
1090
  } else {
1091
+ const stripeDiag = validateStripeNodePlaceholder({
1092
+ expr,
1093
+ path: e,
1094
+ nodeId,
1095
+ nodeTypes,
1096
+ endpoint: name,
1097
+ file,
1098
+ loc,
1099
+ })
1100
+ if (stripeDiag) diagnostics.push(stripeDiag)
1101
+
1041
1102
  const outputDiag = validateNodeOutputRef({
1042
1103
  expr,
1043
1104
  path: e,
@@ -1311,6 +1372,45 @@ function validateEndpoint(entry, ctx) {
1311
1372
  }
1312
1373
  }
1313
1374
  }
1375
+ if (node.tool_ids !== undefined && node.tools === undefined) {
1376
+ const toolIds = Array.isArray(node.tool_ids) ? node.tool_ids : []
1377
+ const legacyUuidToolIds = toolIds.length === 0 || toolIds.every(isUuidString)
1378
+ diagnostics.push({
1379
+ severity: legacyUuidToolIds ? "warn" : "error",
1380
+ rule: "tool_ids_used",
1381
+ endpoint: name, file, loc: `workflow.nodes[${node.id}]`,
1382
+ message: legacyUuidToolIds
1383
+ ? `Node '${node.id}' uses legacy 'tool_ids' in YAML. Prefer 'tools: [<endpoint-name>, ...]' for portable project files.`
1384
+ : `Node '${node.id}' uses 'tool_ids' with non-UUID values in YAML. The codec only resolves endpoint names via 'tools'.`,
1385
+ fix_hint: legacyUuidToolIds
1386
+ ? `This is allowed for compatibility with older/manual projects, but tools: is the canonical YAML field. See placeholder-cheatsheet.md.`
1387
+ : `Replace tool_ids with tools: [<endpoint-name>, ...]. See placeholder-cheatsheet.md.`,
1388
+ example_fix: "tools: [search-patients, book-appointment]",
1389
+ })
1390
+ }
1391
+
1392
+ const declaredCardinality =
1393
+ nodeField(node, nodeParams(node), "output_cardinality") || node.output_cardinality
1394
+ if (
1395
+ nodeType === "dypai_database" &&
1396
+ (nodeField(node, nodeParams(node), "operation") === "query" ||
1397
+ nodeField(node, nodeParams(node), "operation") === "custom_query") &&
1398
+ declaredCardinality === "single"
1399
+ ) {
1400
+ const query =
1401
+ nodeField(node, nodeParams(node), "query") ||
1402
+ lookupFileMapContent(ctx.fileMap || {}, nodeField(node, nodeParams(node), "query_file"))
1403
+ if (query && !sqlProvesSingleRow(query)) {
1404
+ diagnostics.push({
1405
+ severity: "warn",
1406
+ rule: "output_cardinality_unproven",
1407
+ endpoint: name, file, loc: `workflow.nodes[${node.id}]`,
1408
+ message: `Node '${node.id}' declares output_cardinality: single but SQL does not prove a single row (add LIMIT 1, aggregate without GROUP BY, or INSERT ... RETURNING).`,
1409
+ fix_hint: "Add LIMIT 1 to the query or remove output_cardinality if the query can return multiple rows.",
1410
+ })
1411
+ }
1412
+ }
1413
+
1314
1414
  // node_type exists in catalog?
1315
1415
  if (ctx.knownTypes.size && !ctx.knownTypes.has(nodeType)) {
1316
1416
  const suggestions = [...ctx.knownTypes].filter(t => levenshteinSmall(t, nodeType) <= 2).slice(0, 3)
@@ -1564,6 +1664,19 @@ function validateEndpoint(entry, ctx) {
1564
1664
  }
1565
1665
  }
1566
1666
 
1667
+ const webhookCred = doc.trigger?.webhook?.credential
1668
+ if (webhookCred && !ctx.remoteCredentials.has(webhookCred)) {
1669
+ diagnostics.push({
1670
+ severity: "error",
1671
+ rule: "credential_not_found",
1672
+ endpoint: name, file, loc: "trigger.webhook.credential",
1673
+ message: `credential '${webhookCred}' is not defined remotely.`,
1674
+ fix_hint: ctx.remoteCredentials.size
1675
+ ? `Available: ${[...ctx.remoteCredentials].join(", ")}. For Stripe webhooks use type stripe_webhook, recommended name stripe-webhook.`
1676
+ : "Create stripe-webhook (type stripe_webhook) in the dashboard. See search_docs('stripe payments').",
1677
+ })
1678
+ }
1679
+
1567
1680
  // ── Edge sanity: catch typos in workflow.edges before runtime ────────────
1568
1681
  // The engine silently skips edges whose `from`/`to` doesn't resolve to a
1569
1682
  // node id, which manifests as "node never ran" — extremely hard to debug.
@@ -1604,12 +1717,40 @@ function validateEndpoint(entry, ctx) {
1604
1717
  // that don't reconverge), "last to finish" is non-deterministic and
1605
1718
  // almost certainly not what the author intended.
1606
1719
  const allNodes = doc.workflow?.nodes || []
1720
+ const triggerKeys = Object.keys(doc.trigger || {})
1721
+ const NEEDS_RESPONSE = new Set(["http_api", "webhook"])
1722
+ const needsResponse = triggerKeys.some(k => NEEDS_RESPONSE.has(k))
1723
+ const hasReturn = allNodes.some(n => n?.return === true || n?.is_return === true)
1724
+
1725
+ if (needsResponse && allNodes.length > 0 && !hasReturn) {
1726
+ diagnostics.push({
1727
+ severity: "warn",
1728
+ rule: "endpoint_missing_return",
1729
+ endpoint: name, file, loc: "workflow.nodes",
1730
+ message:
1731
+ "HTTP/webhook endpoint has no node marked 'return: true'. The engine may return an unintended node output.",
1732
+ fix_hint: "Mark exactly one terminal node with 'return: true' (usually your set_fields or response node).",
1733
+ })
1734
+ }
1735
+
1736
+ const declaredOutputProps = outputSchemaPropertyNames(doc.output)
1737
+ const mappedOutputKeys = workflowOutputKeys(doc.workflow?.output)
1738
+ if (declaredOutputProps.size > 0 && mappedOutputKeys.size > 0) {
1739
+ const unknownKeys = [...mappedOutputKeys].filter(k => !declaredOutputProps.has(k))
1740
+ if (unknownKeys.length > 0) {
1741
+ diagnostics.push({
1742
+ severity: "warn",
1743
+ rule: "endpoint_output_mismatch",
1744
+ endpoint: name, file, loc: "workflow.output",
1745
+ message:
1746
+ `workflow.output declares keys not present in the endpoint output schema: ${unknownKeys.join(", ")}.`,
1747
+ fix_hint: `Add them to output.properties or rename workflow.output keys. Declared: ${[...declaredOutputProps].join(", ")}.`,
1748
+ })
1749
+ }
1750
+ }
1751
+
1607
1752
  if (allNodes.length > 1) {
1608
- const hasReturn = allNodes.some(n => n?.return === true || n?.is_return === true)
1609
1753
  if (!hasReturn) {
1610
- const triggerKeys = Object.keys(doc.trigger || {})
1611
- const NEEDS_RESPONSE = new Set(["http_api", "webhook"])
1612
- const needsResponse = triggerKeys.some(k => NEEDS_RESPONSE.has(k))
1613
1754
  if (needsResponse) {
1614
1755
  // Find terminal nodes (no outgoing edges).
1615
1756
  const edgeList = Array.isArray(doc.workflow?.edges) ? doc.workflow.edges : []
@@ -1960,11 +2101,13 @@ export async function runValidation(rootDir, projectId) {
1960
2101
 
1961
2102
  export const __testing = {
1962
2103
  buildNodeOutputContracts,
2104
+ buildNodeTypeMap,
1963
2105
  extractRuntimePaths,
1964
2106
  inferSelectOutputColumns,
1965
2107
  inferSqlCardinality,
1966
2108
  unsupportedRuntimePlaceholder,
1967
2109
  validateNodeOutputRef,
2110
+ validateStripeNodePlaceholder,
1968
2111
  }
1969
2112
 
1970
2113
  export const dypaiValidateTool = {