@xano/xanoscript-language-server 12.16.5 → 12.17.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.
@@ -0,0 +1,71 @@
1
+ import { IncrementToken } from "../../../lexer/db.js";
2
+ import { StringLiteral } from "../../../lexer/literal.js";
3
+ import { Identifier } from "../../../lexer/tokens.js";
4
+ import { getVarName } from "../../generic/utils.js";
5
+ import { captureFieldName } from "./captureFieldName.js";
6
+ import { addonSchema } from "./schema.js";
7
+
8
+ /**
9
+ * db.increment <table> { where, field_name, value, ... } as $var
10
+ *
11
+ * Atomically adds `value` (negative decrements) to the numeric column
12
+ * `field_name` on every row matching `where`. The key set must match the
13
+ * server-side kind schema exactly, or the editor reports false diagnostics.
14
+ *
15
+ * @param {import('../../base_parser.js').XanoBaseParser} $
16
+ */
17
+ export function dbIncrementFn($) {
18
+ return () => {
19
+ $.sectionStack.push("dbIncrementFn");
20
+ const fnToken = $.CONSUME(IncrementToken); // "increment"
21
+ const nameToken = $.OR({
22
+ DEF: [
23
+ { ALT: () => $.CONSUME(Identifier) },
24
+ { ALT: () => $.CONSUME1(StringLiteral) },
25
+ ],
26
+ ERR_MSG: "Expected a table name",
27
+ });
28
+ $.addReference("table", getVarName(nameToken), nameToken);
29
+
30
+ const captured = {};
31
+ $.SUBRULE($.schemaParseAttributeFn, {
32
+ ARGS: [
33
+ fnToken,
34
+ {
35
+ "description?": "[string]",
36
+ "disabled?": "[boolean]",
37
+ where: "[query]",
38
+ field_name: "[expression]",
39
+ value: "[expression]",
40
+ "return?": {
41
+ "type?": ["list", "count"],
42
+ },
43
+ "output?": ["[string]"],
44
+ "addon?": [addonSchema],
45
+ "mock?": { "![string]": "[expression]" },
46
+ },
47
+ captured,
48
+ ],
49
+ });
50
+
51
+ $.ACTION(() => {
52
+ captureFieldName($, captured);
53
+ // Extract addon name references (mirrors db.query)
54
+ if (captured.addon?.value && Array.isArray(captured.addon.value)) {
55
+ for (const item of captured.addon.value) {
56
+ const nameEntry = item.name;
57
+ if (nameEntry?.value) {
58
+ $.addReference(
59
+ "function",
60
+ getVarName(nameEntry.value),
61
+ nameEntry.value
62
+ );
63
+ }
64
+ }
65
+ }
66
+ });
67
+
68
+ $.SUBRULE($.asVariable, { ARGS: [fnToken] });
69
+ $.sectionStack.pop();
70
+ };
71
+ }
@@ -13,6 +13,7 @@ import { dbExternalDirectQueryFn } from "./dbExternalDirectQueryFn.js";
13
13
  import { dbExternalFn } from "./dbExternalFn.js";
14
14
  import { dbGetFn } from "./dbGetFn.js";
15
15
  import { dbHasFn } from "./dbHasFn.js";
16
+ import { dbIncrementFn } from "./dbIncrementFn.js";
16
17
  import { dbPatchFn } from "./dbPatchFn.js";
17
18
  import { dbQueryFn } from "./dbQueryFn.js";
18
19
  import { dbSchemaFn } from "./dbSchemaFn.js";
@@ -41,6 +42,7 @@ export const register = ($) => {
41
42
  $.dbExternalFn = $.RULE("dbExternalFn", dbExternalFn($));
42
43
  $.dbGetFn = $.RULE("dbGetFn", dbGetFn($));
43
44
  $.dbHasFn = $.RULE("dbHasnpmFn", dbHasFn($));
45
+ $.dbIncrementFn = $.RULE("dbIncrementFn", dbIncrementFn($));
44
46
  $.dbPatchFn = $.RULE("dbPatchFn", dbPatchFn($));
45
47
  $.dbQueryFn = $.RULE("dbQueryFn", dbQueryFn($));
46
48
  $.dbSchemaFn = $.RULE("dbSchemaFn", dbSchemaFn($));
@@ -23,6 +23,7 @@ export function dbFn($) {
23
23
  { ALT: () => $.SUBRULE($.dbTruncateFn) }, // "db.truncate"
24
24
  { ALT: () => $.SUBRULE($.dbTransactionFn) }, // "db.transaction"
25
25
  { ALT: () => $.SUBRULE($.dbPatchFn) }, // "db.patch"
26
+ { ALT: () => $.SUBRULE($.dbIncrementFn) }, // "db.increment"
26
27
  { ALT: () => $.SUBRULE($.dbBulkFn) }, // "db.bulk"
27
28
  { ALT: () => $.SUBRULE($.dbAddOrEditFn) }, // db.add_or_edit
28
29
  ]);
@@ -0,0 +1,100 @@
1
+ import { PipeToken } from "../../../lexer/control.js";
2
+ import { StringLiteral } from "../../../lexer/literal.js";
3
+ import { getVarName } from "../../generic/utils.js";
4
+
5
+ // The write-before-elicit lint (plan KTD9). `mcp.elicit` asks the client for
6
+ // input by ending the request with `input_required`; the client then RETRIES
7
+ // the whole request with the answer, so every statement before the elicit runs
8
+ // once per round trip. A write placed before the first elicit repeats.
9
+ //
10
+ // The mutating set is deliberately conservative: the `db.*` writes, raw SQL
11
+ // (`db.direct_query`, `db.external.<driver>.direct_query`), an `api.request`
12
+ // whose method is not a plain literal GET/HEAD, `util.send_email`, and
13
+ // `function.run` (which may write anything).
14
+
15
+ const DB_WRITES = [
16
+ "add",
17
+ "add_or_edit",
18
+ "edit",
19
+ "patch",
20
+ "del",
21
+ "delete",
22
+ "truncate",
23
+ "increment",
24
+ "bulk",
25
+ // raw SQL may write
26
+ "direct_query",
27
+ "external",
28
+ ];
29
+
30
+ const READ_METHODS = ["GET", "HEAD"];
31
+
32
+ /**
33
+ * The name of the mutating statement spanning `input[start..end]`, or null.
34
+ * Reads tokens in place: this runs for every stack statement on every parse.
35
+ * @param {import('chevrotain').IToken[]} input the parser's token vector
36
+ * @param {number} start index of the statement's first token
37
+ * @param {number} end index of the statement's last token
38
+ * @returns {string|null}
39
+ */
40
+ export function mutatingStatementName(input, start, end) {
41
+ const ns = input[start]?.image;
42
+ const dot = input[start + 1]?.image;
43
+ const name = input[start + 2]?.image;
44
+ if (dot !== "." || !ns || !name) return null;
45
+
46
+ if (ns === "db" && DB_WRITES.includes(name)) return `db.${name}`;
47
+ if (ns === "util" && name === "send_email") return "util.send_email";
48
+ if (ns === "function" && name === "run") return "function.run";
49
+
50
+ if (ns === "api" && name === "request") {
51
+ for (let i = start + 3; i <= end - 2; i++) {
52
+ if (input[i].image !== "method" || input[i + 1].image !== "=") continue;
53
+ const value = input[i + 2];
54
+ // a filtered or compound method ("GET"|replace:...) is not a plain GET
55
+ const isPlain = input[i + 3]?.tokenType !== PipeToken;
56
+ if (
57
+ isPlain &&
58
+ value.tokenType === StringLiteral &&
59
+ READ_METHODS.includes(getVarName(value).toUpperCase())
60
+ ) {
61
+ return null;
62
+ }
63
+ return "api.request";
64
+ }
65
+ // no method: api.request defaults to GET
66
+ return null;
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Record a statement that just finished parsing (called from nakedStackFn).
74
+ * @param {import('../../base_parser.js').XanoBaseParser} $
75
+ * @param {number} startIdx lexer index of the statement's first token
76
+ */
77
+ export function recordStatement($, startIdx) {
78
+ const name = mutatingStatementName($.input, startIdx, $.exportLexerState());
79
+ if (name) {
80
+ $.__mcpElicitLint.mutations.push({ name, token: $.input[startIdx] });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Called when an `mcp.elicit` is parsed: warn when a write ran since the
86
+ * previous elicit (or since the start of the stack, for the first one).
87
+ * @param {import('../../base_parser.js').XanoBaseParser} $
88
+ * @param {import('chevrotain').IToken} elicitToken
89
+ */
90
+ export function checkElicit($, elicitToken) {
91
+ const lint = $.__mcpElicitLint;
92
+ const first = lint.mutations[lint.checked];
93
+ lint.checked = lint.mutations.length;
94
+ if (!first) return;
95
+
96
+ $.addWarning(
97
+ `\`${first.name}\` runs before this mcp.elicit. The client retries the whole request with each answer, so every statement before the elicit runs again on each round trip. Move writes after the last mcp.elicit.`,
98
+ elicitToken,
99
+ );
100
+ }
@@ -0,0 +1,103 @@
1
+ import { EqualToken, LCurly, RCurly } from "../../../lexer/control.js";
2
+ import { ElicitToken } from "../../../lexer/mcp.js";
3
+ import { Identifier, NewlineToken } from "../../../lexer/tokens.js";
4
+ import { getVarName } from "../../generic/utils.js";
5
+ import { checkElicit } from "./elicitLint.js";
6
+
7
+ // mcp.elicit {
8
+ // key = "confirm" // required: the answer's identity across retries
9
+ // message = "Delete " ~ $input.name ~ "?" // required
10
+ // input { // optional: the form the client shows
11
+ // bool proceed
12
+ // text reason?
13
+ // }
14
+ // } as $answer
15
+ //
16
+ // Asks the MCP client for input (cloud-client mvp:mcp_elicit). Input is only
17
+ // collected from 2026-07-28 clients with form elicitation; every other client
18
+ // (and a non-MCP run) resolves to {action: "cancel"}. The `input` block is the
19
+ // same grammar as a tool's `input` block.
20
+
21
+ const REQUIRED = ["key", "message"];
22
+ const OPTIONAL = ["description", "disabled"];
23
+
24
+ /**
25
+ * @param {import('../../base_parser.js').XanoBaseParser} $
26
+ */
27
+ export function mcpElicitFn($) {
28
+ return () => {
29
+ const defined = [];
30
+ let hasInput = false;
31
+ let hasMock = false;
32
+
33
+ $.sectionStack.push("mcpElicitFn");
34
+ const fnToken = $.CONSUME(ElicitToken); // "elicit"
35
+
36
+ $.CONSUME(LCurly); // "{"
37
+ $.MANY(() => {
38
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // one attribute per line
39
+ $.OR([
40
+ { ALT: () => $.SUBRULE($.commentBlockFn) },
41
+ {
42
+ // input { ... } -- the elicitation form. Its fields describe the
43
+ // ANSWER, not the object's own $input, so they must not register as
44
+ // document inputs.
45
+ GATE: () => !hasInput,
46
+ ALT: () => {
47
+ hasInput = true;
48
+ let savedInputs;
49
+ $.ACTION(() => {
50
+ savedInputs = { ...$.__symbolTable.input };
51
+ });
52
+ $.SUBRULE($.inputClause);
53
+ $.ACTION(() => {
54
+ $.__symbolTable.input = savedInputs;
55
+ });
56
+ },
57
+ },
58
+ {
59
+ GATE: () => !hasMock,
60
+ ALT: () => {
61
+ hasMock = true;
62
+ $.SUBRULE($.mockAttribute);
63
+ },
64
+ },
65
+ {
66
+ // key = ..., message = ..., description = "...", disabled = true
67
+ ALT: () => {
68
+ const identifier = $.CONSUME(Identifier);
69
+ $.CONSUME(EqualToken); // "="
70
+ $.SUBRULE($.expressionFn, { ARGS: [identifier] });
71
+
72
+ $.ACTION(() => {
73
+ const name = getVarName(identifier);
74
+ if (![...REQUIRED, ...OPTIONAL].includes(name)) {
75
+ $.addIllegalAttributeError(
76
+ identifier,
77
+ `Attribute \`${name}\` is not allowed in mcp.elicit (expected ${[...REQUIRED, ...OPTIONAL, "input"].join(", ")})`,
78
+ );
79
+ } else if (defined.includes(name)) {
80
+ $.addDuplicateAttributeError(identifier);
81
+ }
82
+ defined.push(name);
83
+ });
84
+ },
85
+ },
86
+ ]);
87
+ });
88
+ $.MANY1(() => $.CONSUME1(NewlineToken));
89
+ $.CONSUME(RCurly); // "}"
90
+
91
+ $.ACTION(() => {
92
+ const missing = REQUIRED.filter((name) => !defined.includes(name));
93
+ if (missing.length) {
94
+ $.addMissingAttributeError(fnToken, missing);
95
+ }
96
+ checkElicit($, fnToken);
97
+ });
98
+
99
+ // `as $answer` is optional: an elicit may be used only for its side effect.
100
+ $.SUBRULE($.asVariable);
101
+ $.sectionStack.pop();
102
+ };
103
+ }
@@ -0,0 +1,34 @@
1
+ import { ProgressToken } from "../../../lexer/mcp.js";
2
+
3
+ // mcp.progress {
4
+ // progress = $i
5
+ // total = 100 // optional
6
+ // message = "Working" // optional
7
+ // }
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
11
+ // dropped at run time. NO `as $var` binding.
12
+
13
+ /**
14
+ * @param {import('../../base_parser.js').XanoBaseParser} $
15
+ */
16
+ export function mcpProgressFn($) {
17
+ const requiredAttrs = ["progress"];
18
+ const optionalAttrs = [
19
+ "total",
20
+ "message",
21
+ "description",
22
+ "disabled",
23
+ "mock",
24
+ ];
25
+
26
+ return () => {
27
+ $.sectionStack.push("mcpProgressFn");
28
+ const fnToken = $.CONSUME(ProgressToken); // "progress"
29
+ $.SUBRULE($.functionAttrReq, {
30
+ ARGS: [fnToken, requiredAttrs, optionalAttrs],
31
+ });
32
+ $.sectionStack.pop();
33
+ };
34
+ }
@@ -0,0 +1,11 @@
1
+ import { mcpElicitFn } from "./mcpElicitFn.js";
2
+ import { mcpProgressFn } from "./mcpProgressFn.js";
3
+
4
+ /**
5
+ * Registers all the parsers in this folder
6
+ * @param {import('../../base_parser.js').XanoBaseParser} $
7
+ */
8
+ export const register = ($) => {
9
+ $.mcpElicitFn = $.RULE("mcpElicitFn", mcpElicitFn($));
10
+ $.mcpProgressFn = $.RULE("mcpProgressFn", mcpProgressFn($));
11
+ };
@@ -0,0 +1,19 @@
1
+ import { McpToken } from "../../lexer/ai.js";
2
+ import { DotToken } from "../../lexer/tokens.js";
3
+
4
+ /**
5
+ * mcp.elicit, mcp.progress
6
+ * @param {import('../base_parser.js').XanoBaseParser} $
7
+ */
8
+ export function mcpFn($) {
9
+ return () => {
10
+ $.sectionStack.push("mcp");
11
+ $.CONSUME(McpToken); // "mcp"
12
+ $.CONSUME(DotToken); // "."
13
+ $.OR([
14
+ { ALT: () => $.SUBRULE($.mcpElicitFn) }, // "mcp.elicit"
15
+ { ALT: () => $.SUBRULE($.mcpProgressFn) }, // "mcp.progress"
16
+ ]);
17
+ $.sectionStack.pop();
18
+ };
19
+ }
@@ -19,6 +19,8 @@ import { debugFn } from "./debugFn.js";
19
19
  import { register as register_expect } from "./expect/register.js";
20
20
  import { register as register_math } from "./math/register.js";
21
21
  import { mathFn } from "./mathFn.js";
22
+ import { register as register_mcp } from "./mcp/register.js";
23
+ import { mcpFn } from "./mcpFn.js";
22
24
  import { middlewareCallFn } from "./middlewareCallFn.js";
23
25
  import { register as register_object } from "./object/register.js";
24
26
  import { objectFn } from "./objectFn.js";
@@ -66,6 +68,7 @@ export const register = ($) => {
66
68
  $.mathFn = $.RULE("mathFn", mathFn($));
67
69
  $.objectFn = $.RULE("objectFn", objectFn($));
68
70
  $.realtimeFn = $.RULE("realtimeFn", realtimeFn($));
71
+ $.mcpFn = $.RULE("mcpFn", mcpFn($));
69
72
  $.redisFn = $.RULE("redisFn", redisFn($));
70
73
  $.securityFn = $.RULE("securityFn", securityFn($));
71
74
  $.storageFn = $.RULE("storageFn", storageFn($));
@@ -88,6 +91,7 @@ export const register = ($) => {
88
91
  register_db($);
89
92
  register_debug($);
90
93
  register_math($);
94
+ register_mcp($);
91
95
  register_object($);
92
96
  register_realtime($);
93
97
  register_redis($);