@xano/xanoscript-language-server 12.20.1 → 12.22.0

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.
Files changed (50) hide show
  1. package/README.md +84 -149
  2. package/embedded/README.md +1 -2
  3. package/lexer/db.js +8 -0
  4. package/lexer/mcp.js +2 -3
  5. package/lexer/realtime.js +8 -0
  6. package/lexer/realtime_server_trigger.js +1 -1
  7. package/onHover/functions.md +29 -1
  8. package/package.json +15 -5
  9. package/parser/agent_parser.js +3 -1
  10. package/parser/attributes/descriptionFieldAttribute.js +8 -1
  11. package/parser/attributes/docsFieldAttribute.js +8 -1
  12. package/parser/branch_parser.js +25 -9
  13. package/parser/channel_parser.js +9 -0
  14. package/parser/clauses/flexibleStringClause.js +8 -1
  15. package/parser/clauses/historyClause.js +1 -2
  16. package/parser/clauses/mcpMetadataClauses.js +12 -8
  17. package/parser/clauses/nakedStackFn.js +4 -0
  18. package/parser/clauses/viewClause.js +8 -4
  19. package/parser/definitions/columnDefinition.js +7 -2
  20. package/parser/definitions/enumColumnDefinition.js +7 -2
  21. package/parser/definitions/objectColumnDefinition.js +7 -2
  22. package/parser/error_trigger_parser.js +13 -1
  23. package/parser/functions/controls/switchCaseFn.js +3 -0
  24. package/parser/functions/controls/throwFn.js +28 -2
  25. package/parser/functions/db/dbAddFn.js +2 -1
  26. package/parser/functions/db/dbAddOrEditFn.js +2 -1
  27. package/parser/functions/db/dbEditFn.js +2 -1
  28. package/parser/functions/db/dbExternalFn.js +2 -0
  29. package/parser/functions/mcp/mcpElicitFn.js +1 -1
  30. package/parser/functions/mcp/mcpOauthFn.js +2 -4
  31. package/parser/functions/mcp/mcpProgressFn.js +2 -2
  32. package/parser/functions/realtime/realtimeKickFn.js +35 -0
  33. package/parser/functions/realtime/realtimePublishFn.js +2 -2
  34. package/parser/functions/realtime/register.js +2 -0
  35. package/parser/functions/realtimeFn.js +1 -0
  36. package/parser/functions/register.js +2 -0
  37. package/parser/functions/storage/storageCreateImageFn.js +3 -1
  38. package/parser/functions/workflowTestCallFn.js +47 -0
  39. package/parser/generic/stringArray.js +9 -2
  40. package/parser/mcp_primitive_parser.js +4 -4
  41. package/parser/mcp_server_parser.js +1 -2
  42. package/parser/mcp_server_trigger_parser.js +3 -1
  43. package/parser/message_parser.js +3 -3
  44. package/parser/microservice_parser.js +1 -2
  45. package/parser/parser.js +1 -1
  46. package/parser/policy/catalogue.js +2 -2
  47. package/parser/policy/policyLiteral.js +1 -1
  48. package/parser/policy/validatePolicyRule.js +10 -11
  49. package/parser/policy_parser.js +3 -3
  50. package/parser/realtime_channel_parser.js +1 -1
@@ -7,10 +7,8 @@ import { AnnotationsToken, IconsToken, TitleToken } from "../../lexer/mcp.js";
7
7
  import { MultiLineStringToken } from "../../lexer/multiline.js";
8
8
 
9
9
  // Shared MCP metadata blocks for `tool`, `prompt` and `resource` (MCP spec
10
- // 2026-07-28). The rules mirror cloud-client's decode checks so the editor
11
- // reports what a push would refuse:
12
- // - `helper/mvp/mcpserver/ToolMetadata.php` (icons, tool annotations)
13
- // - `helper/script/transform/Resource.php` (resource annotations)
10
+ // 2026-07-28). The rules mirror the Xano backend's decode checks (icons, tool annotations,
11
+ // resource annotations) so the editor reports what a push would refuse.
14
12
 
15
13
  export const ICON_MIME_TYPES = ["image/png", "image/jpeg", "image/webp"];
16
14
  export const ICON_URL_MAX_LENGTH = 2048;
@@ -19,7 +17,7 @@ export const RESOURCE_AUDIENCES = ["user", "assistant"];
19
17
 
20
18
  const ICON_SCHEMA = {
21
19
  src: "[string]",
22
- // validated below: cloud-client compares it lowercased and allows ""
20
+ // validated below: the backend compares it lowercased and allows ""
23
21
  "mime_type?": "[string]",
24
22
  "sizes?": ["[string]"],
25
23
  "theme?": ["light", "dark"],
@@ -47,7 +45,7 @@ function stringValue(token) {
47
45
  }
48
46
 
49
47
  /**
50
- * Why an icon `src` is refused, or null. Same rules as ToolMetadata::srcProblem:
48
+ * Why an icon `src` is refused, or null. Same rules as the platform:
51
49
  * `https:` or a base64 `data:` URI of png/jpeg/webp. SVG and every other scheme
52
50
  * (`http:`, `javascript:`, `file:`, ...) are refused.
53
51
  * @param {string} src
@@ -105,8 +103,14 @@ export function mcpTitleClause($) {
105
103
  $.sectionStack.push("mcpTitleClause");
106
104
  $.CONSUME(TitleToken); // "title"
107
105
  $.CONSUME(EqualToken); // "="
108
- const value = $.CONSUME(StringLiteral); // "..."
109
- if (value.image === `""`) {
106
+ // The platform writes a title holding a double quote as '...' and one
107
+ // holding a newline as a """ block.
108
+ const value = $.OR([
109
+ { ALT: () => $.CONSUME(StringLiteral) }, // "..."
110
+ { ALT: () => $.CONSUME(SingleQuotedStringLiteral) }, // '...'
111
+ { ALT: () => $.CONSUME(MultiLineStringToken) }, // """..."""
112
+ ]);
113
+ if (stringValue(value) === "") {
110
114
  $.addMissingError(value, "title cannot be empty");
111
115
  }
112
116
  $.sectionStack.pop();
@@ -71,6 +71,10 @@ export function nakedStackFn($) {
71
71
  GATE: () => options.allowCallStatements,
72
72
  ALT: () => $.SUBRULE($.triggerCallFn),
73
73
  },
74
+ {
75
+ GATE: () => options.allowCallStatements,
76
+ ALT: () => $.SUBRULE($.workflowTestCallFn),
77
+ },
74
78
  { ALT: () => $.SUBRULE($.workflowExpectFn) },
75
79
  ]);
76
80
  // write-before-elicit lint (see functions/mcp/elicitLint.js)
@@ -22,10 +22,13 @@ export function viewClause($) {
22
22
  ARGS: [
23
23
  parent,
24
24
  {
25
+ // The platform writes each of alias/term/search/sort/hide only
26
+ // when the view has it.
25
27
  "[string]": {
28
+ "term?": "[string]",
26
29
  "search?": "[query]",
27
30
  "hide?": ["[string]"],
28
- sort: { "[string]": ["asc", "desc", "rand"] },
31
+ "sort?": { "[string]": ["asc", "desc", "rand"] },
29
32
  id: "[string]",
30
33
  "alias?": "[string]",
31
34
  },
@@ -34,14 +37,15 @@ export function viewClause($) {
34
37
  ],
35
38
  });
36
39
 
37
- // each key is the view name, ensure either search or hide is present
40
+ // each key is the view name, ensure it has some criteria
38
41
  for (const viewName in captured) {
42
+ const hasTerm = !!captured[viewName].term;
39
43
  const hasSearch = !!captured[viewName].search;
40
44
  const hasSort = !!captured[viewName].sort;
41
- if (!hasSearch && !hasSort && !captured[viewName].hide) {
45
+ if (!hasTerm && !hasSearch && !hasSort && !captured[viewName].hide) {
42
46
  $.addMissingError(
43
47
  parent,
44
- `view ${viewName} must have either a search, sort or hide criteria`,
48
+ `view ${viewName} must have either a term, search, sort or hide criteria`,
45
49
  );
46
50
  }
47
51
  }
@@ -24,6 +24,7 @@ import {
24
24
  } from "../../lexer/columns.js";
25
25
  import { Question } from "../../lexer/control.js";
26
26
  import { Identifier } from "../../lexer/identifier.js";
27
+ import { StringLiteral } from "../../lexer/literal.js";
27
28
  import { getVarName } from "../generic/utils.js";
28
29
 
29
30
  const DEFAULT_CONFIG = {
@@ -87,7 +88,11 @@ export function columnDefinition($) {
87
88
  });
88
89
 
89
90
  // Column name (e.g., "user_id", "optional_column", "required_column")
90
- const nameToken = $.CONSUME(Identifier);
91
+ // A name that is not an identifier (e.g. `created-at`) is written quoted.
92
+ const nameToken = $.OR9([
93
+ { ALT: () => $.CONSUME(Identifier) },
94
+ { ALT: () => $.CONSUME(StringLiteral) },
95
+ ]);
91
96
 
92
97
  // ? (is it an optional field)
93
98
  $.OPTION6(() => {
@@ -147,7 +152,7 @@ export function columnDefinition($) {
147
152
 
148
153
  // add the variable to the registry
149
154
  $.addInput(
150
- nameToken.image,
155
+ getVarName(nameToken),
151
156
  inputTypeToken.image,
152
157
  iterable,
153
158
  nullable,
@@ -1,6 +1,7 @@
1
1
  import { EnumToken } from "../../lexer/columns.js";
2
2
  import { Question } from "../../lexer/control.js";
3
3
  import { Identifier } from "../../lexer/identifier.js";
4
+ import { StringLiteral } from "../../lexer/literal.js";
4
5
  import { getVarName } from "../generic/utils.js";
5
6
 
6
7
  /**
@@ -30,7 +31,11 @@ export function enumColumnDefinition($) {
30
31
  }); // ? (optional field)
31
32
 
32
33
  // Column name (e.g., "user_id", "optional_column", "required_column")
33
- const name = $.CONSUME(Identifier);
34
+ // A name that is not an identifier (e.g. `created-at`) is written quoted.
35
+ const name = $.OR9([
36
+ { ALT: () => $.CONSUME(Identifier) },
37
+ { ALT: () => $.CONSUME(StringLiteral) },
38
+ ]);
34
39
 
35
40
  // add variable to schema
36
41
  schema[getVarName(name)] = {
@@ -49,7 +54,7 @@ export function enumColumnDefinition($) {
49
54
  $.SUBRULE($.enumColumnMetadataDefinition, { ARGS: [parent] }); // Enum metadata
50
55
 
51
56
  // add the variable to the registry
52
- $.addInput(name.image, "enum", iterable, nullable, optional);
57
+ $.addInput(getVarName(name), "enum", iterable, nullable, optional);
53
58
 
54
59
  $.sectionStack.pop();
55
60
  };
@@ -1,5 +1,6 @@
1
1
  import { ObjectToken } from "../../lexer/columns.js";
2
2
  import { Question } from "../../lexer/control.js";
3
+ import { StringLiteral } from "../../lexer/literal.js";
3
4
  import { Identifier } from "../../lexer/tokens.js";
4
5
  import { getVarName } from "../generic/utils.js";
5
6
 
@@ -35,7 +36,11 @@ export function objectColumnDefinition($) {
35
36
  });
36
37
 
37
38
  // Column name (e.g., "user_id", "optional_column", "required_column")
38
- const name = $.CONSUME(Identifier);
39
+ // A name that is not an identifier (e.g. `created-at`) is written quoted.
40
+ const name = $.OR9([
41
+ { ALT: () => $.CONSUME(Identifier) },
42
+ { ALT: () => $.CONSUME(StringLiteral) },
43
+ ]);
39
44
 
40
45
  // add variable to schema
41
46
  schema[getVarName(name)] = {
@@ -52,7 +57,7 @@ export function objectColumnDefinition($) {
52
57
 
53
58
  $.SUBRULE($.objectColumnMetadataDefinition); // Object metadata
54
59
 
55
- $.addInput(name.image, "enum", iterable, nullable, optional);
60
+ $.addInput(getVarName(name), "enum", iterable, nullable, optional);
56
61
 
57
62
  $.sectionStack.pop();
58
63
  };
@@ -1,10 +1,12 @@
1
- import { LCurly, RCurly } from "../lexer/control.js";
1
+ import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
2
2
  import { ErrorTriggerToken } from "../lexer/error_trigger.js";
3
3
  import { StringLiteral } from "../lexer/literal.js";
4
+ import { ActiveToken } from "../lexer/mcp_server_trigger.js";
4
5
  import { Identifier, NewlineToken } from "../lexer/tokens.js";
5
6
 
6
7
  export function errorTriggerDeclaration($) {
7
8
  return () => {
9
+ let hasActive = false;
8
10
  let hasDescription = false;
9
11
  let hasGuid = false;
10
12
  let hasHistory = false;
@@ -27,6 +29,16 @@ export function errorTriggerDeclaration($) {
27
29
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
28
30
  $.OR2([
29
31
  { ALT: () => $.SUBRULE($.commentBlockFn) },
32
+ {
33
+ // the platform writes `active = false` for a paused trigger
34
+ GATE: () => !hasActive,
35
+ ALT: () => {
36
+ hasActive = true;
37
+ $.CONSUME(ActiveToken);
38
+ $.CONSUME(EqualToken);
39
+ $.SUBRULE($.booleanValue);
40
+ },
41
+ },
30
42
  {
31
43
  GATE: () => !hasDescription,
32
44
  ALT: () => {
@@ -1,3 +1,4 @@
1
+ import { ExclamationToken } from "../../../lexer/cast.js";
1
2
  import {
2
3
  BreakToken,
3
4
  CaseToken,
@@ -12,6 +13,8 @@ export function switchCaseFn($) {
12
13
  return () => {
13
14
  $.sectionStack.push("switchCaseFn");
14
15
  $.SUBRULE($.optionalCommentBlockFn);
16
+ // a disabled case is written `!case`, like a disabled statement
17
+ $.OPTION2(() => $.CONSUME(ExclamationToken));
15
18
  $.CONSUME(CaseToken); // "case"
16
19
  $.CONSUME(LParent); // "("
17
20
  $.SUBRULE($.expressionFn);
@@ -1,4 +1,25 @@
1
1
  import { ThrowToken } from "../../../lexer/control.js";
2
+ import { IntegerLiteral } from "../../../lexer/literal.js";
3
+
4
+ /**
5
+ * `status` is the HTTP status an UNCAUGHT throw answers with
6
+ * (mvp:throw_error, `status?: static:int`).
7
+ * It is a static integer from 100 to 599, never an expression, so a variable
8
+ * or an out-of-range number is caught in the editor rather than at run time.
9
+ * @param {import('../../base_parser.js').XanoBaseParser} $
10
+ */
11
+ function statusValue($) {
12
+ const token = $.CONSUME(IntegerLiteral, {
13
+ ERR_MSG: "status must be a whole number from 100 to 599",
14
+ });
15
+ const value = Number(token.image);
16
+ if (!Number.isInteger(value) || value < 100 || value > 599) {
17
+ $.addInvalidValueError(
18
+ token,
19
+ `Invalid status ${token.image}. Must be a whole number from 100 to 599`,
20
+ );
21
+ }
22
+ }
2
23
 
3
24
  /**
4
25
  * represent $var.x or $x, the only format accepting an assigment
@@ -6,14 +27,19 @@ import { ThrowToken } from "../../../lexer/control.js";
6
27
  */
7
28
  export function throwFn($) {
8
29
  const requiredAttrs = ["value", "name"];
9
- const optionalAttrs = ["description", "disabled"];
30
+ const optionalAttrs = ["description", "disabled", "status"];
10
31
 
11
32
  return () => {
12
33
  $.sectionStack.push("throw");
13
34
  const tokenFn = $.CONSUME(ThrowToken); // "throw"
14
35
  // value is a required field
15
36
  $.SUBRULE($.functionAttrReq, {
16
- ARGS: [tokenFn, requiredAttrs, optionalAttrs],
37
+ ARGS: [
38
+ tokenFn,
39
+ requiredAttrs,
40
+ optionalAttrs,
41
+ { types: { status: statusValue } },
42
+ ],
17
43
  });
18
44
 
19
45
  $.sectionStack.pop();
@@ -24,7 +24,8 @@ export function dbAddFn($) {
24
24
  ARGS: [
25
25
  fnToken,
26
26
  {
27
- data: { "[string]": "[expression]" },
27
+ // The platform writes `data = {}` when no field is set.
28
+ data: { "[string]?": "[expression]" },
28
29
  "description?": "[string]",
29
30
  "disabled?": "[boolean]",
30
31
  "enforce_hidden_fields?": "[boolean]",
@@ -28,7 +28,8 @@ export function dbAddOrEditFn($) {
28
28
  {
29
29
  field_name: "[expression]",
30
30
  field_value: "[expression]",
31
- data: { "[string]": "[expression]" },
31
+ // The platform writes `data = {}` when no field is set.
32
+ data: { "[string]?": "[expression]" },
32
33
  "description?": "[string]",
33
34
  "disabled?": "[boolean]",
34
35
  "enforce_hidden_fields?": "[boolean]",
@@ -28,7 +28,8 @@ export function dbEditFn($) {
28
28
  {
29
29
  field_name: "[expression]",
30
30
  field_value: "[expression]",
31
- data: { "[string]": "[expression]" },
31
+ // The platform writes `data = {}` when no field is set.
32
+ data: { "[string]?": "[expression]" },
32
33
  "description?": "[string]",
33
34
  "disabled?": "[boolean]",
34
35
  "enforce_hidden_fields?": "[boolean]",
@@ -4,6 +4,7 @@ import {
4
4
  MysqlToken,
5
5
  OracleToken,
6
6
  PostgresToken,
7
+ SnowflakeToken,
7
8
  } from "../../../lexer/db.js";
8
9
  import { DotToken } from "../../../lexer/tokens.js";
9
10
 
@@ -20,6 +21,7 @@ export function dbExternalFn($) {
20
21
  { ALT: () => $.CONSUME(MysqlToken) }, // "external.mysql"
21
22
  { ALT: () => $.CONSUME(OracleToken) }, // "external.oracle"
22
23
  { ALT: () => $.CONSUME(PostgresToken) }, // "external.postgres"
24
+ { ALT: () => $.CONSUME(SnowflakeToken) }, // "external.snowflake"
23
25
  ]);
24
26
  $.CONSUME1(DotToken); // "."
25
27
  $.SUBRULE($.dbExternalDirectQueryFn); // "external.direct_query"
@@ -13,7 +13,7 @@ import { checkElicit } from "./elicitLint.js";
13
13
  // }
14
14
  // } as $answer
15
15
  //
16
- // Asks the MCP client for input (cloud-client mvp:mcp_elicit). Input is only
16
+ // Asks the MCP client for input. Input is only
17
17
  // collected from 2026-07-28 clients with form elicitation; every other client
18
18
  // (and a non-MCP run) resolves to {action: "cancel"}. The `input` block is the
19
19
  // same grammar as a tool's `input` block.
@@ -5,7 +5,7 @@ import { CompleteToken, OauthToken, RevokeToken } from "../../../lexer/mcp.js";
5
5
  import { DotToken, NewlineToken } from "../../../lexer/tokens.js";
6
6
  import { getVarName } from "../../generic/utils.js";
7
7
 
8
- // The values cloud-client's mvp:mcp_oauth_complete accepts for `decision`.
8
+ // The values `mcp.oauth.complete` accepts for `decision`.
9
9
  const DECISIONS = ["approve", "deny"];
10
10
 
11
11
  /**
@@ -35,9 +35,7 @@ function decisionValue($, identifier) {
35
35
  $.SUBRULE5($.expressionFn, { ARGS: [identifier, {}] });
36
36
  }
37
37
 
38
- // The `mcp.oauth.*` login hand-off statements (cloud-client
39
- // mvp:mcp_oauth_{complete,request,revoke}, kind schemas
40
- // statement/mcp.oauth.{complete,request,revoke}.yaml). Each takes an optional
38
+ // The `mcp.oauth.*` login hand-off statements. Each takes an optional
41
39
  // `as $var` binding.
42
40
 
43
41
  /**
@@ -6,8 +6,8 @@ import { ProgressToken } from "../../../lexer/mcp.js";
6
6
  // message = "Working" // optional
7
7
  // }
8
8
  //
9
- // Emits `notifications/progress` on the current MCP response stream
10
- // (cloud-client mvp:mcp_progress). A value that does not strictly increase is
9
+ // Emits `notifications/progress` on the current MCP response stream.
10
+ // A value that does not strictly increase is
11
11
  // dropped at run time. NO `as $var` binding.
12
12
 
13
13
  /**
@@ -0,0 +1,35 @@
1
+ import { KickToken } from "../../../lexer/realtime.js";
2
+
3
+ // realtime.kick {
4
+ // realtime_server = "main"
5
+ // channel = "rooms/42"
6
+ // auth_table = "user" // the identity to remove
7
+ // auth_id = $input.user_id
8
+ // reason = "removed" // optional — sent to the removed connections
9
+ // }
10
+ //
11
+ // Takes one identity's sockets out of one realtime v2 channel
12
+ // (mvp:realtime_kick). A channel's join
13
+ // gate runs once, so this is the step that removes a member whose access was
14
+ // revoked. Each of their sockets leaves the channel and is told it was kicked;
15
+ // it stays connected to its other channels.
16
+ // The identity is required (a kick names who to remove), so auth_table and
17
+ // auth_id are required here, though the kind schema lists them optional the way
18
+ // realtime.publish does. Like realtime.publish it has NO `as $var` binding.
19
+
20
+ /**
21
+ * @param {import('../../base_parser.js').XanoBaseParser} $
22
+ */
23
+ export function realtimeKickFn($) {
24
+ const requiredAttrs = ["realtime_server", "channel", "auth_table", "auth_id"];
25
+ const optionalAttrs = ["reason", "description", "disabled"];
26
+
27
+ return () => {
28
+ $.sectionStack.push("realtimeKickFn");
29
+ const fnToken = $.CONSUME(KickToken); // "kick"
30
+ $.SUBRULE($.functionAttrReq, {
31
+ ARGS: [fnToken, requiredAttrs, optionalAttrs],
32
+ });
33
+ $.sectionStack.pop();
34
+ };
35
+ }
@@ -9,8 +9,8 @@ import { PublishToken } from "../../../lexer/realtime.js";
9
9
  // auth_id = $auth.id // optional
10
10
  // }
11
11
  //
12
- // Injects a server-authored event onto a realtime v2 channel (cloud-client
13
- // mvp:realtime_publish). `realtime_server` + `channel` + `data` are required;
12
+ // Injects a server-authored event onto a realtime v2 channel.
13
+ // `realtime_server` + `channel` + `data` are required;
14
14
  // `message`/`auth_table`/`auth_id` are optional. Flat attrs (the PHP transform
15
15
  // maps auth_table/auth_id into the nested context.auth.{dbo_id,row_id} shape).
16
16
  // Unlike realtime.get_session it has NO `as $var` binding.
@@ -1,4 +1,5 @@
1
1
  import { realtimeGetSessionFn } from "./realtimeGetSessionFn.js";
2
+ import { realtimeKickFn } from "./realtimeKickFn.js";
2
3
  import { realtimePublishFn } from "./realtimePublishFn.js";
3
4
 
4
5
  /**
@@ -11,4 +12,5 @@ export const register = ($) => {
11
12
  realtimeGetSessionFn($),
12
13
  );
13
14
  $.realtimePublishFn = $.RULE("realtimePublishFn", realtimePublishFn($));
15
+ $.realtimeKickFn = $.RULE("realtimeKickFn", realtimeKickFn($));
14
16
  };
@@ -13,6 +13,7 @@ export function realtimeFn($) {
13
13
  $.OR([
14
14
  { ALT: () => $.SUBRULE($.realtimeGetSessionFn) }, // "realtime.get_session"
15
15
  { ALT: () => $.SUBRULE($.realtimePublishFn) }, // "realtime.publish"
16
+ { ALT: () => $.SUBRULE($.realtimeKickFn) }, // "realtime.kick"
16
17
  ]);
17
18
  $.sectionStack.pop();
18
19
  };
@@ -45,6 +45,7 @@ import { utilFn } from "./utilFn.js";
45
45
  import { varFn } from "./varFn.js";
46
46
  import { webflowFn } from "./webflowFn.js";
47
47
  import { workflowExpectFn } from "./workflowExpectFn.js";
48
+ import { workflowTestCallFn } from "./workflowTestCallFn.js";
48
49
  import { register as register_zip } from "./zip/register.js";
49
50
  import { zipFn } from "./zipFn.js";
50
51
 
@@ -81,6 +82,7 @@ export const register = ($) => {
81
82
  $.workflowExpectFn = $.RULE("workflowExpectFn", workflowExpectFn($));
82
83
  $.toolCallFn = $.RULE("toolCallFn", toolCallFn($));
83
84
  $.triggerCallFn = $.RULE("triggerCallFn", triggerCallFn($));
85
+ $.workflowTestCallFn = $.RULE("workflowTestCallFn", workflowTestCallFn($));
84
86
  register_ai($);
85
87
  register_schema($);
86
88
  register_api($);
@@ -13,7 +13,9 @@ export function storageCreateImageFn($) {
13
13
  {
14
14
  value: "[expression]",
15
15
  access: ["public", "private"],
16
- filename: "[expression]",
16
+ // Optional, as on create_audio/create_video: the platform omits it
17
+ // when unset.
18
+ "filename?": "[expression]",
17
19
  "description?": "[string]",
18
20
  "disabled?": "[boolean]",
19
21
  },
@@ -0,0 +1,47 @@
1
+ import { CallToken } from "../../lexer/function.js";
2
+ import { StringLiteral } from "../../lexer/literal.js";
3
+ import { DotToken, Identifier } from "../../lexer/tokens.js";
4
+ import { WorkflowTestToken } from "../../lexer/workflow_test.js";
5
+
6
+ /**
7
+ * workflow_test.call "other test" { datasource = "test" } as $result
8
+ *
9
+ * Runs another workflow test as a step. The platform resolves the target
10
+ * only inside a workflow test.
11
+ * @param {import('../base_parser.js').XanoBaseParser} $
12
+ */
13
+ export function workflowTestCallFn($) {
14
+ return () => {
15
+ $.sectionStack.push("workflowTestCallFn");
16
+
17
+ $.CONSUME(WorkflowTestToken); // "workflow_test"
18
+ $.CONSUME(DotToken); // "."
19
+ const fnToken = $.CONSUME(CallToken); // "call"
20
+
21
+ if (!$.sectionStack.includes("workflowTestDeclaration")) {
22
+ $.addInvalidValueError(
23
+ fnToken,
24
+ "workflow_test.call can only be used within workflow_test contexts",
25
+ );
26
+ }
27
+
28
+ $.OR([
29
+ { ALT: () => $.CONSUME(StringLiteral) }, // "other test"
30
+ { ALT: () => $.CONSUME(Identifier) }, // other_test
31
+ ]);
32
+
33
+ $.SUBRULE($.schemaParseAttributeFn, {
34
+ ARGS: [
35
+ fnToken,
36
+ {
37
+ "datasource?": "[string]",
38
+ "description?": "[string]",
39
+ "disabled?": "[boolean]",
40
+ "mock?": { "![string]": "[expression]" },
41
+ },
42
+ ],
43
+ });
44
+ $.SUBRULE($.asVariable, { ARGS: [fnToken] });
45
+ $.sectionStack.pop();
46
+ };
47
+ }
@@ -1,5 +1,8 @@
1
1
  import { CommaToken, LSquare, RSquare } from "../../lexer/control.js";
2
- import { StringLiteral } from "../../lexer/literal.js";
2
+ import {
3
+ SingleQuotedStringLiteral,
4
+ StringLiteral,
5
+ } from "../../lexer/literal.js";
3
6
  import { NewlineToken } from "../../lexer/tokens.js";
4
7
 
5
8
  /**
@@ -18,7 +21,11 @@ export const stringArray = ($) => () => {
18
21
  "Missing comma or newline between string values in the array"
19
22
  );
20
23
  }
21
- token = $.CONSUME(StringLiteral);
24
+ // '...' is how the platform writes a value holding a double quote
25
+ token = $.OR([
26
+ { ALT: () => $.CONSUME(StringLiteral) },
27
+ { ALT: () => $.CONSUME(SingleQuotedStringLiteral) },
28
+ ]);
22
29
  hasSeparator = false;
23
30
  $.OPTION(() => {
24
31
  $.CONSUME(CommaToken);
@@ -11,7 +11,7 @@ import { getVarName } from "./generic/utils.js";
11
11
 
12
12
  // First-class MCP `prompt` and `resource` workspace objects (MCP 2026-07-28),
13
13
  // attached to an `mcp_server` through its `prompts` / `resources` blocks.
14
- // Grounded against cloud-client's `script/kind/schema/core/{prompt,resource}.yaml`.
14
+ // Grounded against the Xano backend's XanoScript schema.
15
15
  //
16
16
  // prompt code_review { resource regional_order {
17
17
  // title = "Code review" uri = "orders://{region}/{order_id}"
@@ -22,7 +22,7 @@ import { getVarName } from "./generic/utils.js";
22
22
  // } response = $order
23
23
  // }
24
24
 
25
- /** The input field types a resource uri variable can be bound to (ResourceUri::VARIABLE_TYPES). */
25
+ /** The input field types a resource uri variable can be bound to. */
26
26
  export const RESOURCE_URI_VARIABLE_TYPES = [
27
27
  "text",
28
28
  "int",
@@ -39,7 +39,7 @@ const MIME_TYPE =
39
39
 
40
40
  /**
41
41
  * Parse a resource uri: a literal URI, or an RFC 6570 LEVEL-1 template.
42
- * Mirrors cloud-client's `ResourceUri::parse`.
42
+ * Mirrors the backend's resource URI parser.
43
43
  * @param {string} uri
44
44
  * @returns {{ problem: string|null, variables: string[] }}
45
45
  */
@@ -103,7 +103,7 @@ export function parseResourceUri(uri) {
103
103
 
104
104
  /**
105
105
  * Why a resource's uri variables and its input block disagree, or null.
106
- * Mirrors cloud-client's `ResourceUri::inputProblem`.
106
+ * Mirrors the backend's check that URI variables match `input`.
107
107
  * @param {string[]} variables
108
108
  * @param {Object<string, {type: string, iterable: boolean}>} inputs
109
109
  * @returns {string|null}
@@ -98,8 +98,7 @@ export function mcpServerDeclaration($) {
98
98
  },
99
99
  },
100
100
  {
101
- // Per-server OAuth (cloud-client mcp_server.yaml `oauth`; values are
102
- // validated server-side by helper/mvp/mcpoauth/OauthConfig.php).
101
+ // Per-server OAuth (values are validated server-side).
103
102
  // An assignment block like a channel's `publish`. mcp_server ONLY:
104
103
  // deliberately absent from agent_parser.js. Mode-specific rules
105
104
  // (hosted needs login_url, external needs issuer, ...) stay
@@ -49,7 +49,9 @@ export function mcpServerTriggerDeclaration($) {
49
49
  ARGS: [
50
50
  subParent,
51
51
  {
52
- connection: "[boolean]",
52
+ // Only the actions that are on are written, so a trigger
53
+ // with connection off is written as `actions = {}`.
54
+ "connection?": "[boolean]",
53
55
  },
54
56
  ],
55
57
  });
@@ -82,8 +82,8 @@ export function messageDeclaration($) {
82
82
  },
83
83
  },
84
84
  {
85
- // backend Message.php `encode()` emits `docs = "..."` for a
86
- // documented message; round-tripped by `decode()`. See DEV-7620.
85
+ // the backend emits `docs = "..."` for a
86
+ // documented message; round-tripped on push.
87
87
  GATE: () => !hasDocs,
88
88
  ALT: () => {
89
89
  hasDocs = true;
@@ -91,7 +91,7 @@ export function messageDeclaration($) {
91
91
  },
92
92
  },
93
93
  {
94
- // backend Message.php `encode()` emits `active = false` for a
94
+ // the backend emits `active = false` for a
95
95
  // disabled message; mirrors the sibling `channel` declaration.
96
96
  GATE: () => !hasActive,
97
97
  ALT: () => {
@@ -25,8 +25,7 @@ import { Identifier, NewlineToken, TypeToken } from "../lexer/tokens.js";
25
25
 
26
26
  // Grammar for the top-level `microservice <name> { ... }` entity.
27
27
  //
28
- // Grounded against cloud-client's authoritative XanoScript schema DSL
29
- // (extensions/MVP/includes/xano/script/kind/schema/{core,component}/*.yaml):
28
+ // Grounded against the Xano backend's XanoScript schema:
30
29
  //
31
30
  // microservice <name> {
32
31
  // description|guid|kind|tenant_deploy = "..." # static:text (optional)
package/parser/parser.js CHANGED
@@ -80,7 +80,7 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
80
80
  }
81
81
  for (const error of lexResult.errors) {
82
82
  // The platform names the first character it cannot read; the lexer skips a run of them.
83
- // Quoted as PHP's escapeshellarg quotes it (Unexpected.php): a `'` is written `'\''`.
83
+ // Quoted as PHP's escapeshellarg quotes it: a `'` is written `'\''`.
84
84
  const first = String.fromCodePoint(text.codePointAt(error.offset)).replaceAll("'", "'\\''");
85
85
  parser.addInvalidValueError({
86
86
  image: text.slice(error.offset, error.offset + error.length),