@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.
- package/lexer/db.js +8 -0
- package/lexer/mcp.js +109 -0
- package/lexer/tokens.js +8 -0
- package/onCompletion/onCompletion.js +1 -1
- package/onDefinition/onDefinition.js +1 -0
- package/onHover/functions.md +53 -0
- package/package.json +1 -1
- package/parser/base_parser.js +2 -0
- package/parser/clauses/inputClause.js +13 -4
- package/parser/clauses/mcpMetadataClauses.js +252 -0
- package/parser/clauses/nakedStackFn.js +8 -0
- package/parser/clauses/register.js +16 -0
- package/parser/definitions/columnDefinition.js +1 -0
- package/parser/definitions/enumColumnDefinition.js +1 -0
- package/parser/definitions/objectColumnDefinition.js +1 -0
- package/parser/functions/db/dbIncrementFn.js +71 -0
- package/parser/functions/db/register.js +2 -0
- package/parser/functions/dbFn.js +1 -0
- package/parser/functions/mcp/elicitLint.js +100 -0
- package/parser/functions/mcp/mcpElicitFn.js +103 -0
- package/parser/functions/mcp/mcpProgressFn.js +34 -0
- package/parser/functions/mcp/register.js +11 -0
- package/parser/functions/mcpFn.js +19 -0
- package/parser/functions/register.js +4 -0
- package/parser/mcp_primitive_parser.js +343 -0
- package/parser/mcp_server_parser.js +53 -0
- package/parser/parser.js +6 -0
- package/parser/register.js +9 -0
- package/parser/tool_parser.js +32 -0
- package/utils.js +2 -0
|
@@ -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($));
|
package/parser/functions/dbFn.js
CHANGED
|
@@ -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($);
|