@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.
Files changed (40) hide show
  1. package/cache/documentCache.js +12 -5
  2. package/language-server-worker.js +6 -0
  3. package/lexer/mcp.js +109 -0
  4. package/lexer/policy.js +30 -0
  5. package/lexer/tokens.js +14 -0
  6. package/onCompletion/contentAssist.js +6 -0
  7. package/onCompletion/onCompletion.js +33 -3
  8. package/onCompletion/policyCompletion.js +339 -0
  9. package/onHover/PolicyMessageProvider.js +142 -0
  10. package/onHover/functions.md +39 -0
  11. package/onHover/onHover.js +2 -0
  12. package/package.json +3 -1
  13. package/parser/base_parser.js +2 -0
  14. package/parser/clauses/inputClause.js +13 -4
  15. package/parser/clauses/mcpMetadataClauses.js +252 -0
  16. package/parser/clauses/nakedStackFn.js +8 -0
  17. package/parser/clauses/register.js +16 -0
  18. package/parser/definitions/columnDefinition.js +1 -0
  19. package/parser/definitions/enumColumnDefinition.js +1 -0
  20. package/parser/definitions/objectColumnDefinition.js +1 -0
  21. package/parser/functions/mcp/elicitLint.js +100 -0
  22. package/parser/functions/mcp/mcpElicitFn.js +103 -0
  23. package/parser/functions/mcp/mcpProgressFn.js +34 -0
  24. package/parser/functions/mcp/register.js +11 -0
  25. package/parser/functions/mcpFn.js +19 -0
  26. package/parser/functions/register.js +4 -0
  27. package/parser/mcp_primitive_parser.js +343 -0
  28. package/parser/mcp_server_parser.js +53 -0
  29. package/parser/multidoc.js +45 -13
  30. package/parser/parser.js +58 -0
  31. package/parser/policy/catalogue.js +100 -0
  32. package/parser/policy/catalogueSnapshot.js +7075 -0
  33. package/parser/policy/policyDocs.js +76 -0
  34. package/parser/policy/policyLiteral.js +227 -0
  35. package/parser/policy/validatePolicyRule.js +323 -0
  36. package/parser/policy_parser.js +463 -0
  37. package/parser/register.js +21 -0
  38. package/parser/tool_parser.js +32 -0
  39. package/server.js +6 -0
  40. 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: () => {
@@ -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
- const SEPARATOR = "\n---\n";
9
- const SEPARATOR_LENGTH = 5;
10
- const SEPARATOR_NEWLINES = 2; // number of \n characters in SEPARATOR
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
- * Detects `\n---\n` separator. If absent, delegates to xanoscriptParser as-is.
170
- * If present, splits, parses each segment, offsets positions, and merges results.
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 (!text.includes(SEPARATOR)) {
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
+ }