@xano/xanoscript-language-server 12.16.6 → 12.18.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/cache/documentCache.js +12 -5
- package/language-server-worker.js +6 -0
- package/lexer/mcp.js +109 -0
- package/lexer/policy.js +30 -0
- package/lexer/tokens.js +14 -0
- package/onCompletion/contentAssist.js +6 -0
- package/onCompletion/onCompletion.js +33 -3
- package/onCompletion/policyCompletion.js +339 -0
- package/onHover/PolicyMessageProvider.js +142 -0
- package/onHover/functions.md +39 -0
- package/onHover/onHover.js +2 -0
- package/package.json +3 -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/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/multidoc.js +45 -13
- package/parser/parser.js +58 -0
- package/parser/policy/catalogue.js +100 -0
- package/parser/policy/catalogueSnapshot.js +7075 -0
- package/parser/policy/policyDocs.js +76 -0
- package/parser/policy/policyLiteral.js +227 -0
- package/parser/policy/validatePolicyRule.js +323 -0
- package/parser/policy_parser.js +463 -0
- package/parser/register.js +21 -0
- package/parser/tool_parser.js +32 -0
- package/server.js +6 -0
- package/utils.js +6 -2
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
|
|
2
2
|
import { StringLiteral } from "../lexer/literal.js";
|
|
3
|
+
import { PromptsToken } from "../lexer/mcp.js";
|
|
3
4
|
import {
|
|
4
5
|
ActiveToken,
|
|
5
6
|
InstructionsToken,
|
|
6
7
|
McpServerToken,
|
|
7
8
|
ToolsToken,
|
|
8
9
|
} from "../lexer/mcp_server.js";
|
|
10
|
+
import { ResourcesToken } from "../lexer/microservice.js";
|
|
9
11
|
import { Identifier, NewlineToken } from "../lexer/tokens.js";
|
|
10
12
|
|
|
11
13
|
export function mcpServerDeclaration($) {
|
|
@@ -17,6 +19,8 @@ export function mcpServerDeclaration($) {
|
|
|
17
19
|
let hasDocs = false;
|
|
18
20
|
let hasHistory = false;
|
|
19
21
|
let hasInstructions = false;
|
|
22
|
+
let hasPrompts = false;
|
|
23
|
+
let hasResources = false;
|
|
20
24
|
let hasTags = false;
|
|
21
25
|
let hasTools = false;
|
|
22
26
|
|
|
@@ -92,6 +96,55 @@ export function mcpServerDeclaration($) {
|
|
|
92
96
|
});
|
|
93
97
|
},
|
|
94
98
|
},
|
|
99
|
+
{
|
|
100
|
+
// First-class prompts attached to the server (MCP 2026-07-28).
|
|
101
|
+
// mcp_server ONLY: deliberately absent from agent_parser.js.
|
|
102
|
+
GATE: () => !hasPrompts,
|
|
103
|
+
ALT: () => {
|
|
104
|
+
hasPrompts = true;
|
|
105
|
+
|
|
106
|
+
const subParent = $.CONSUME(PromptsToken); // "prompts"
|
|
107
|
+
$.CONSUME3(EqualToken); // "="
|
|
108
|
+
|
|
109
|
+
// [{name: "prompt_name", active: true, auth: "user"}]
|
|
110
|
+
$.SUBRULE1($.schemaParseArrayFn, {
|
|
111
|
+
ARGS: [
|
|
112
|
+
subParent,
|
|
113
|
+
[
|
|
114
|
+
{
|
|
115
|
+
name: "[string]",
|
|
116
|
+
"active?": "[boolean]",
|
|
117
|
+
"auth?": "[string]",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
// First-class resources attached to the server (MCP 2026-07-28).
|
|
126
|
+
GATE: () => !hasResources,
|
|
127
|
+
ALT: () => {
|
|
128
|
+
hasResources = true;
|
|
129
|
+
|
|
130
|
+
const subParent = $.CONSUME(ResourcesToken); // "resources"
|
|
131
|
+
$.CONSUME4(EqualToken); // "="
|
|
132
|
+
|
|
133
|
+
// [{name: "resource_name", active: true, auth: "user"}]
|
|
134
|
+
$.SUBRULE2($.schemaParseArrayFn, {
|
|
135
|
+
ARGS: [
|
|
136
|
+
subParent,
|
|
137
|
+
[
|
|
138
|
+
{
|
|
139
|
+
name: "[string]",
|
|
140
|
+
"active?": "[boolean]",
|
|
141
|
+
"auth?": "[string]",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
},
|
|
95
148
|
{
|
|
96
149
|
GATE: () => !hasTags,
|
|
97
150
|
ALT: () => {
|
package/parser/multidoc.js
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import { lexDocument } from "../lexer/lexer.js";
|
|
2
|
+
import { MultiLineExpressionToken, MultiLineStringToken } from "../lexer/multiline.js";
|
|
2
3
|
import { crossFileValidate } from "../workspace/crossFileValidator.js";
|
|
3
4
|
import { WorkspaceIndex } from "../workspace/workspaceIndex.js";
|
|
4
5
|
import { scanDbReferences } from "./dbReferenceScanner.js";
|
|
5
6
|
import { xanoscriptParser } from "./parser.js";
|
|
6
7
|
import { validateVariables } from "./variableValidator.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Where a triple-quoted string could run, found with the lexer's own patterns for `"""`, `'''` and
|
|
11
|
+
* triple backticks rather than a full lex: in a lex, an ordinary string left open while typing
|
|
12
|
+
* pairs with a quote in a later document and hides every separator after it.
|
|
13
|
+
*/
|
|
14
|
+
const TRIPLE_QUOTED = new RegExp(
|
|
15
|
+
`${MultiLineStringToken.PATTERN.source}|${MultiLineExpressionToken.PATTERN.source}`,
|
|
16
|
+
"g"
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Separate documents at each `---` line, as the platform does, except one inside a triple-quoted
|
|
21
|
+
* string (a Markdown rule in a policy's narrative), which xanoscriptParser warns about: the
|
|
22
|
+
* platform's multidoc push splits there.
|
|
23
|
+
*/
|
|
24
|
+
export function splitMultidoc(text) {
|
|
25
|
+
const boundaries = [...text.matchAll(/\r?\n---[ \t]*\r?\n/g)];
|
|
26
|
+
if (!boundaries.length) return [{ text, globalOffset: 0, lineOffset: 0 }];
|
|
27
|
+
const quoted = /"""|'''|```/.test(text)
|
|
28
|
+
? [...text.matchAll(TRIPLE_QUOTED)].map((match) => [match.index, match.index + match[0].length])
|
|
29
|
+
: [];
|
|
30
|
+
const segments = [];
|
|
31
|
+
let quotedIndex = 0;
|
|
32
|
+
let globalOffset = 0;
|
|
33
|
+
let lineOffset = 0;
|
|
34
|
+
for (const boundary of boundaries) {
|
|
35
|
+
const dash = boundary.index + (boundary[0].startsWith("\r") ? 2 : 1);
|
|
36
|
+
while (quoted[quotedIndex]?.[1] <= dash) quotedIndex++;
|
|
37
|
+
const range = quoted[quotedIndex];
|
|
38
|
+
if (range && range[0] < dash && range[1] >= dash + 3) continue;
|
|
39
|
+
const segmentText = text.slice(globalOffset, boundary.index);
|
|
40
|
+
segments.push({ text: segmentText, globalOffset, lineOffset });
|
|
41
|
+
lineOffset += (segmentText.match(/\n/g) || []).length + 2;
|
|
42
|
+
globalOffset = boundary.index + boundary[0].length;
|
|
43
|
+
}
|
|
44
|
+
segments.push({ text: text.slice(globalOffset), globalOffset, lineOffset });
|
|
45
|
+
return segments;
|
|
46
|
+
}
|
|
11
47
|
|
|
12
48
|
/**
|
|
13
49
|
* Find the segment that contains the given global offset.
|
|
@@ -166,24 +202,22 @@ function offsetResult(snapshot, globalOffset, lineOffset) {
|
|
|
166
202
|
|
|
167
203
|
/**
|
|
168
204
|
* Parse a multidoc or single-doc XanoScript text.
|
|
169
|
-
*
|
|
170
|
-
*
|
|
205
|
+
* Splits with splitMultidoc (a `---` line outside a triple-quoted string). With one segment,
|
|
206
|
+
* delegates to xanoscriptParser as-is; otherwise parses each segment, offsets positions, and
|
|
207
|
+
* merges results.
|
|
171
208
|
* @param {string} text - The full document text
|
|
172
209
|
* @returns {Object} Merged parser result
|
|
173
210
|
*/
|
|
174
|
-
export function multidocParser(text) {
|
|
175
|
-
if (
|
|
211
|
+
export function multidocParser(text, segments = splitMultidoc(text)) {
|
|
212
|
+
if (segments.length === 1) {
|
|
176
213
|
// Single document — delegate as-is
|
|
177
214
|
const parser = xanoscriptParser(text);
|
|
178
215
|
return snapshotParser(parser);
|
|
179
216
|
}
|
|
180
217
|
|
|
181
|
-
const segments = text.split(SEPARATOR);
|
|
182
218
|
const results = [];
|
|
183
|
-
let globalOffset = 0;
|
|
184
|
-
let lineOffset = 0;
|
|
185
219
|
|
|
186
|
-
for (const segmentText of segments) {
|
|
220
|
+
for (const { text: segmentText, globalOffset, lineOffset } of segments) {
|
|
187
221
|
const parser = xanoscriptParser(segmentText);
|
|
188
222
|
const snapshot = snapshotParser(parser);
|
|
189
223
|
|
|
@@ -230,8 +264,6 @@ export function multidocParser(text) {
|
|
|
230
264
|
// Deep-copy the snapshot before offsetResult mutates it (needed for hover providers)
|
|
231
265
|
const localSnapshot = JSON.parse(JSON.stringify(snapshot));
|
|
232
266
|
results.push({ snapshot, localSnapshot, segmentText, segmentSymbolTable, globalOffset, lineOffset });
|
|
233
|
-
globalOffset += segmentText.length + SEPARATOR_LENGTH;
|
|
234
|
-
lineOffset += (segmentText.match(/\n/g) || []).length + SEPARATOR_NEWLINES;
|
|
235
267
|
}
|
|
236
268
|
|
|
237
269
|
// For now, merge without offsetting (next task)
|
package/parser/parser.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { lexDocument } from "../lexer/lexer.js";
|
|
2
|
+
import { MultiLineExpressionToken, MultiLineStringToken } from "../lexer/multiline.js";
|
|
2
3
|
import { allTokens } from "../lexer/tokens.js";
|
|
3
4
|
import { getSchemeFromContent } from "../utils.js";
|
|
4
5
|
import { XanoBaseParser } from "./base_parser.js";
|
|
6
|
+
import { explainPolicySyntaxErrors } from "./policy_parser.js";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Will parse the content of the file based on its scheme (db:/, task:/, api:/...)
|
|
@@ -17,6 +19,7 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
|
17
19
|
const lexResult = preTokenized || lexDocument(text);
|
|
18
20
|
parser.reset();
|
|
19
21
|
parser.input = lexResult.tokens;
|
|
22
|
+
warnSeparatorsInStrings(lexResult.tokens);
|
|
20
23
|
switch (scheme.toLowerCase()) {
|
|
21
24
|
case "addon":
|
|
22
25
|
parser.addonDeclaration();
|
|
@@ -63,6 +66,37 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
|
63
66
|
case "middleware":
|
|
64
67
|
parser.middlewareDeclaration();
|
|
65
68
|
return parser;
|
|
69
|
+
case "policy":
|
|
70
|
+
parser.policyDeclaration();
|
|
71
|
+
explainPolicySyntaxErrors(parser);
|
|
72
|
+
// Chevrotain skips an unrecognized character (`#`, `§`). A policy must never validate the
|
|
73
|
+
// text left after that recovery as if the character were absent: what the parse found at or
|
|
74
|
+
// after it is dropped, and the character is reported as the platform's syntax error.
|
|
75
|
+
if (lexResult.errors.length) {
|
|
76
|
+
const skipped = lexResult.errors[0].offset;
|
|
77
|
+
const before = (item) => !item.token || item.token.startOffset < skipped;
|
|
78
|
+
parser.errors = parser.errors.filter(before);
|
|
79
|
+
parser.warnings = parser.warnings.filter(before);
|
|
80
|
+
}
|
|
81
|
+
for (const error of lexResult.errors) {
|
|
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 `'\''`.
|
|
84
|
+
const first = String.fromCodePoint(text.codePointAt(error.offset)).replaceAll("'", "'\\''");
|
|
85
|
+
parser.addInvalidValueError({
|
|
86
|
+
image: text.slice(error.offset, error.offset + error.length),
|
|
87
|
+
startOffset: error.offset,
|
|
88
|
+
endOffset: error.offset + error.length - 1,
|
|
89
|
+
startLine: error.line,
|
|
90
|
+
startColumn: error.column,
|
|
91
|
+
}, `Syntax error: unexpected '${first}'`);
|
|
92
|
+
}
|
|
93
|
+
return parser;
|
|
94
|
+
case "prompt":
|
|
95
|
+
parser.promptDeclaration();
|
|
96
|
+
return parser;
|
|
97
|
+
case "resource":
|
|
98
|
+
parser.resourceDeclaration();
|
|
99
|
+
return parser;
|
|
66
100
|
case "realtime_server":
|
|
67
101
|
parser.realtimeServerDeclaration();
|
|
68
102
|
return parser;
|
|
@@ -103,6 +137,30 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
|
103
137
|
}
|
|
104
138
|
}
|
|
105
139
|
|
|
140
|
+
const SEPARATOR_IN_STRING =
|
|
141
|
+
'A line that is only "---" separates documents in a multidoc push (xano workspace push, release push), even inside a string. Indent it to keep it in the text.';
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A triple-quoted string keeps a `---` line here, as the platform's single-document parse does,
|
|
145
|
+
* but the platform's multidoc splitter ends a document at every unindented `---` line, strings
|
|
146
|
+
* included. Warn on each one, at the line a push would split.
|
|
147
|
+
*/
|
|
148
|
+
function warnSeparatorsInStrings(tokens) {
|
|
149
|
+
for (const token of tokens) {
|
|
150
|
+
if (token.tokenType !== MultiLineStringToken && token.tokenType !== MultiLineExpressionToken) continue;
|
|
151
|
+
for (const match of token.image.matchAll(/\n---[ \t]*(?=\r?\n)/g)) {
|
|
152
|
+
const at = match.index + 1;
|
|
153
|
+
parser.addWarning(SEPARATOR_IN_STRING, {
|
|
154
|
+
image: "---",
|
|
155
|
+
startOffset: token.startOffset + at,
|
|
156
|
+
endOffset: token.startOffset + at + 2,
|
|
157
|
+
startLine: token.startLine + (token.image.slice(0, at).match(/\n/g) || []).length,
|
|
158
|
+
startColumn: 1,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
106
164
|
export class XanoScriptParser extends XanoBaseParser {
|
|
107
165
|
constructor() {
|
|
108
166
|
super(allTokens);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The policy check catalogue the language server validates policies against.
|
|
3
|
+
*
|
|
4
|
+
* The platform owns the catalogue: `GET workspace/{id}/policy/check` (and `xano policy
|
|
5
|
+
* catalogue -o json`) answers `{items: [check...], goals, document}`: each check with its id,
|
|
6
|
+
* label, description and param schemas, and `document`, the policy file's own fields (their types,
|
|
7
|
+
* value sets and defaults, retired fields, reserved blocks, the key pattern). Nothing here writes a
|
|
8
|
+
* check or a field down by hand. The registry starts EMPTY, and an empty registry means the
|
|
9
|
+
* language server checks the policy's block shape only.
|
|
10
|
+
*
|
|
11
|
+
* A host fills it:
|
|
12
|
+
* - Studio sends its own live answer (`source: "live"`); a check-level problem is then an
|
|
13
|
+
* error, because the instance refuses the same save.
|
|
14
|
+
* - The Node server and offline tools load the generated snapshot (`catalogueSnapshot.js`,
|
|
15
|
+
* `source: "snapshot"`); a check-level problem is then a warning, because the snapshot can
|
|
16
|
+
* be older than the instance the file is pushed to.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const EMPTY = Object.freeze({
|
|
20
|
+
checks: new Map(),
|
|
21
|
+
ids: [],
|
|
22
|
+
scopeFields: new Set(),
|
|
23
|
+
document: null,
|
|
24
|
+
source: null,
|
|
25
|
+
generatedAt: null,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let state = EMPTY;
|
|
29
|
+
let version = 0;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace the catalogue.
|
|
33
|
+
* @param {Object|Object[]|null} catalogue `{items, document}` as the platform serves it, Studio's
|
|
34
|
+
* `{checks, document}`, or a bare list of checks; null clears it
|
|
35
|
+
* @param {{source?: "live"|"snapshot", generatedAt?: string|null}} [options]
|
|
36
|
+
*/
|
|
37
|
+
export function setPolicyCatalogue(catalogue, options = {}) {
|
|
38
|
+
const items = Array.isArray(catalogue)
|
|
39
|
+
? catalogue
|
|
40
|
+
: (catalogue?.items ?? catalogue?.checks ?? []);
|
|
41
|
+
const checks = new Map();
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
if (item && typeof item.id === "string" && item.id !== "") {
|
|
44
|
+
checks.set(item.id, item);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const document =
|
|
48
|
+
catalogue && !Array.isArray(catalogue) && catalogue.document && typeof catalogue.document === "object"
|
|
49
|
+
? catalogue.document
|
|
50
|
+
: null;
|
|
51
|
+
version++;
|
|
52
|
+
if (checks.size === 0 && !document) {
|
|
53
|
+
state = EMPTY;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// A scope field is any name the catalogue lists in some check's `scope_fields`: the shared
|
|
57
|
+
// params every check carries that choose which objects a rule selects.
|
|
58
|
+
const scopeFields = new Set();
|
|
59
|
+
for (const check of checks.values()) {
|
|
60
|
+
for (const field of check.scope_fields ?? []) {
|
|
61
|
+
if (field?.name) scopeFields.add(field.name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
state = Object.freeze({
|
|
65
|
+
checks,
|
|
66
|
+
ids: [...checks.keys()].sort(),
|
|
67
|
+
scopeFields,
|
|
68
|
+
document,
|
|
69
|
+
source: options.source === "snapshot" ? "snapshot" : "live",
|
|
70
|
+
generatedAt: options.generatedAt ?? catalogue?.generated_at ?? null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Forget the catalogue: structural checks only. */
|
|
75
|
+
export function clearPolicyCatalogue() {
|
|
76
|
+
setPolicyCatalogue(null);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @returns {{checks: Map<string, Object>, ids: string[], scopeFields: Set<string>,
|
|
81
|
+
* document: Object|null, source: "live"|"snapshot"|null, generatedAt: string|null}}
|
|
82
|
+
*/
|
|
83
|
+
export function getPolicyCatalogue() {
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether a catalogue is loaded. */
|
|
88
|
+
export function hasPolicyCatalogue() {
|
|
89
|
+
return state.source !== null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One check by id, or null. */
|
|
93
|
+
export function getPolicyCheck(id) {
|
|
94
|
+
return state.checks.get(id) ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Bumped on every change, so a cached parse made against an older catalogue is not reused. */
|
|
98
|
+
export function policyCatalogueVersion() {
|
|
99
|
+
return version;
|
|
100
|
+
}
|