@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,343 @@
1
+ import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
2
+ import { StringLiteral } from "../lexer/literal.js";
3
+ import {
4
+ MimeTypeToken,
5
+ PromptToken,
6
+ ResourceToken,
7
+ UriToken,
8
+ } from "../lexer/mcp.js";
9
+ import { Identifier, NewlineToken } from "../lexer/tokens.js";
10
+ import { getVarName } from "./generic/utils.js";
11
+
12
+ // First-class MCP `prompt` and `resource` workspace objects (MCP 2026-07-28),
13
+ // attached to an `mcp_server` through its `prompts` / `resources` blocks.
14
+ // Grounded against cloud-client's `script/kind/schema/core/{prompt,resource}.yaml`.
15
+ //
16
+ // prompt code_review { resource regional_order {
17
+ // title = "Code review" uri = "orders://{region}/{order_id}"
18
+ // icons = [...] mime_type = "application/json"
19
+ // input { text code } annotations = {audience: ["user"]}
20
+ // stack { ... } input { enum region {...} int order_id }
21
+ // response = $messages stack { ... }
22
+ // } response = $order
23
+ // }
24
+
25
+ /** The input field types a resource uri variable can be bound to (ResourceUri::VARIABLE_TYPES). */
26
+ export const RESOURCE_URI_VARIABLE_TYPES = [
27
+ "text",
28
+ "int",
29
+ "decimal",
30
+ "bool",
31
+ "enum",
32
+ "email",
33
+ "uuid",
34
+ ];
35
+
36
+ const URI_OPERATORS = "+#./;?&=,!@|";
37
+ const MIME_TYPE =
38
+ /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*(\s*;.*)?$/;
39
+
40
+ /**
41
+ * Parse a resource uri: a literal URI, or an RFC 6570 LEVEL-1 template.
42
+ * Mirrors cloud-client's `ResourceUri::parse`.
43
+ * @param {string} uri
44
+ * @returns {{ problem: string|null, variables: string[] }}
45
+ */
46
+ export function parseResourceUri(uri) {
47
+ const fail = (problem) => ({ problem, variables: [] });
48
+
49
+ if (uri === "") return fail("a resource needs a uri");
50
+ if (/\s/.test(uri)) return fail(`resource uri "${uri}" must not contain whitespace`);
51
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(uri)) {
52
+ return fail(`resource uri "${uri}" must start with a scheme, e.g. docs://readme`);
53
+ }
54
+
55
+ const variables = [];
56
+ let i = 0;
57
+ let lastEnd = -1;
58
+ while (i < uri.length) {
59
+ const ch = uri[i];
60
+ if (ch === "}") return fail(`resource uri "${uri}" has a "}" with no matching "{"`);
61
+ if (ch !== "{") {
62
+ i++;
63
+ continue;
64
+ }
65
+ const close = uri.indexOf("}", i + 1);
66
+ const nextOpen = uri.indexOf("{", i + 1);
67
+ if (close === -1 || (nextOpen !== -1 && nextOpen < close)) {
68
+ return fail(`resource uri "${uri}" has an unclosed "{"`);
69
+ }
70
+ const expression = uri.slice(i + 1, close);
71
+ if (expression === "") return fail(`resource uri "${uri}" has an empty "{}"`);
72
+ if (URI_OPERATORS.includes(expression[0])) {
73
+ return fail(
74
+ `resource uri template "{${expression}}" uses an operator; only level-1 {name} variables are supported`,
75
+ );
76
+ }
77
+ if (expression.includes(",")) {
78
+ return fail(
79
+ `resource uri template "{${expression}}" lists several variables; use one {name} per variable`,
80
+ );
81
+ }
82
+ if (expression.includes("*") || expression.includes(":")) {
83
+ return fail(
84
+ `resource uri template "{${expression}}" uses a modifier; only level-1 {name} variables are supported`,
85
+ );
86
+ }
87
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(expression)) {
88
+ return fail(`resource uri variable "${expression}" must be letters, digits and _`);
89
+ }
90
+ if (variables.includes(expression)) {
91
+ return fail(`resource uri variable "${expression}" appears more than once`);
92
+ }
93
+ if (lastEnd === i - 1) {
94
+ return fail(`resource uri "${uri}" has two adjacent variables, which cannot be told apart`);
95
+ }
96
+ variables.push(expression);
97
+ lastEnd = close;
98
+ i = close + 1;
99
+ }
100
+
101
+ return { problem: null, variables };
102
+ }
103
+
104
+ /**
105
+ * Why a resource's uri variables and its input block disagree, or null.
106
+ * Mirrors cloud-client's `ResourceUri::inputProblem`.
107
+ * @param {string[]} variables
108
+ * @param {Object<string, {type: string, iterable: boolean}>} inputs
109
+ * @returns {string|null}
110
+ */
111
+ export function resourceInputProblem(variables, inputs) {
112
+ for (const variable of variables) {
113
+ const field = inputs[variable];
114
+ if (!field) {
115
+ return `resource uri variable "${variable}" must be declared in the input block`;
116
+ }
117
+ if (!RESOURCE_URI_VARIABLE_TYPES.includes(field.type)) {
118
+ return `resource uri variable "${variable}" must be one of ${RESOURCE_URI_VARIABLE_TYPES.join(", ")} — got "${field.type}"`;
119
+ }
120
+ if (field.iterable) {
121
+ return `resource uri variable "${variable}" must be a single value, not a list`;
122
+ }
123
+ }
124
+
125
+ for (const name of Object.keys(inputs)) {
126
+ if (!variables.includes(name)) {
127
+ return variables.length === 0
128
+ ? `input "${name}" cannot be set: a static resource has no inputs (use a {${name}} uri template)`
129
+ : `input "${name}" is not a variable of the resource uri`;
130
+ }
131
+ }
132
+
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * @param {import('./base_parser.js').XanoBaseParser} $
138
+ */
139
+ export function promptDeclaration($) {
140
+ return mcpPrimitiveDeclaration($, "prompt");
141
+ }
142
+
143
+ /**
144
+ * @param {import('./base_parser.js').XanoBaseParser} $
145
+ */
146
+ export function resourceDeclaration($) {
147
+ return mcpPrimitiveDeclaration($, "resource");
148
+ }
149
+
150
+ /**
151
+ * @param {import('./base_parser.js').XanoBaseParser} $
152
+ * @param {"prompt"|"resource"} kind
153
+ */
154
+ function mcpPrimitiveDeclaration($, kind) {
155
+ const isResource = kind === "resource";
156
+
157
+ return () => {
158
+ let hasAnnotations = false;
159
+ let hasDescription = false;
160
+ let hasDocs = false;
161
+ let hasGuid = false;
162
+ let hasHistory = false;
163
+ let hasIcons = false;
164
+ let hasInput = false;
165
+ let hasMiddleware = false;
166
+ let hasMimeType = false;
167
+ let hasResponse = false;
168
+ let hasStack = false;
169
+ let hasTags = false;
170
+ let hasTitle = false;
171
+ let uriToken = null;
172
+ const inputFields = {};
173
+
174
+ $.sectionStack.push(`${kind}Declaration`);
175
+ // Allow leading comments and newlines before the declaration
176
+ $.SUBRULE($.optionalCommentBlockFn);
177
+
178
+ const parent = $.OR([
179
+ { GATE: () => !isResource, ALT: () => $.CONSUME(PromptToken) },
180
+ { GATE: () => isResource, ALT: () => $.CONSUME(ResourceToken) },
181
+ ]);
182
+ const nameToken = $.OR1([
183
+ { ALT: () => $.CONSUME(StringLiteral) },
184
+ { ALT: () => $.CONSUME(Identifier) },
185
+ ]);
186
+ $.validateDeclarationName(nameToken, kind);
187
+ $.CONSUME(LCurly); // "{"
188
+ $.MANY(() => {
189
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
190
+ $.OR2([
191
+ { ALT: () => $.SUBRULE($.commentBlockFn) },
192
+ {
193
+ GATE: () => isResource && !hasAnnotations,
194
+ ALT: () => {
195
+ hasAnnotations = true;
196
+ $.SUBRULE($.mcpResourceAnnotationsClause);
197
+ },
198
+ },
199
+ {
200
+ GATE: () => !hasDescription,
201
+ ALT: () => {
202
+ hasDescription = true;
203
+ $.SUBRULE($.descriptionFieldAttribute);
204
+ },
205
+ },
206
+ {
207
+ GATE: () => !hasDocs,
208
+ ALT: () => {
209
+ hasDocs = true;
210
+ $.SUBRULE($.docsFieldAttribute);
211
+ },
212
+ },
213
+ {
214
+ GATE: () => !hasGuid,
215
+ ALT: () => {
216
+ hasGuid = true;
217
+ $.SUBRULE($.guidFieldAttribute);
218
+ },
219
+ },
220
+ {
221
+ GATE: () => !hasHistory,
222
+ ALT: () => {
223
+ hasHistory = true;
224
+ $.SUBRULE($.historyClause);
225
+ },
226
+ },
227
+ {
228
+ GATE: () => !hasIcons,
229
+ ALT: () => {
230
+ hasIcons = true;
231
+ $.SUBRULE($.mcpIconsClause);
232
+ },
233
+ },
234
+ {
235
+ GATE: () => !hasInput,
236
+ ALT: () => {
237
+ hasInput = true;
238
+ $.SUBRULE($.inputClause, { ARGS: [inputFields] });
239
+ },
240
+ },
241
+ {
242
+ GATE: () => !hasMiddleware,
243
+ ALT: () => {
244
+ hasMiddleware = true;
245
+ $.SUBRULE($.middlewareClause);
246
+ },
247
+ },
248
+ {
249
+ GATE: () => isResource && !hasMimeType,
250
+ ALT: () => {
251
+ hasMimeType = true;
252
+ $.CONSUME(MimeTypeToken); // "mime_type"
253
+ $.CONSUME(EqualToken); // "="
254
+ const value = $.CONSUME1(StringLiteral);
255
+ if (!MIME_TYPE.test(getVarName(value))) {
256
+ $.addInvalidValueError(
257
+ value,
258
+ `resource mime_type must be a MIME type such as text/markdown — got "${getVarName(value)}"`,
259
+ );
260
+ }
261
+ },
262
+ },
263
+ {
264
+ GATE: () => !hasResponse,
265
+ ALT: () => {
266
+ hasResponse = true;
267
+ $.SUBRULE($.responseClause);
268
+ },
269
+ },
270
+ {
271
+ GATE: () => !hasStack,
272
+ ALT: () => {
273
+ hasStack = true;
274
+ $.SUBRULE($.stackClause);
275
+ },
276
+ },
277
+ {
278
+ GATE: () => !hasTags,
279
+ ALT: () => {
280
+ hasTags = true;
281
+ $.SUBRULE($.tagsAttribute);
282
+ },
283
+ },
284
+ {
285
+ GATE: () => !hasTitle,
286
+ ALT: () => {
287
+ hasTitle = true;
288
+ $.SUBRULE($.mcpTitleClause);
289
+ },
290
+ },
291
+ {
292
+ GATE: () => isResource && !uriToken,
293
+ ALT: () => {
294
+ $.CONSUME(UriToken); // "uri"
295
+ $.CONSUME1(EqualToken); // "="
296
+ uriToken = $.CONSUME2(StringLiteral);
297
+ },
298
+ },
299
+ ]);
300
+ });
301
+
302
+ if (!isResource && !hasInput) {
303
+ $.addMissingError(
304
+ parent,
305
+ `{} ${nameToken.image} is missing an input clause`,
306
+ );
307
+ }
308
+
309
+ if (!hasStack) {
310
+ $.addMissingError(
311
+ parent,
312
+ `{} ${nameToken.image} is missing a stack clause`,
313
+ );
314
+ }
315
+
316
+ if (isResource) {
317
+ $.ACTION(() => {
318
+ if (!uriToken) {
319
+ $.addMissingError(parent, `{} ${nameToken.image} is missing a uri`);
320
+ return;
321
+ }
322
+ const { problem, variables } = parseResourceUri(getVarName(uriToken));
323
+ // the top-level fields only; the symbol table also holds nested
324
+ // object fields, and records an object input's type as "enum"
325
+ const inputs = {};
326
+ for (const [name, field] of Object.entries(inputFields)) {
327
+ inputs[name] = { type: field.typeToken.image, iterable: field.iterable };
328
+ }
329
+ const inputProblem = problem
330
+ ? problem
331
+ : resourceInputProblem(variables, inputs);
332
+ if (inputProblem) {
333
+ $.addInvalidValueError(uriToken, inputProblem);
334
+ }
335
+ });
336
+ }
337
+
338
+ $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
339
+ $.CONSUME(RCurly); // "}"
340
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // optional new line
341
+ $.sectionStack.pop();
342
+ };
343
+ }
@@ -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/parser.js CHANGED
@@ -63,6 +63,12 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
63
63
  case "middleware":
64
64
  parser.middlewareDeclaration();
65
65
  return parser;
66
+ case "prompt":
67
+ parser.promptDeclaration();
68
+ return parser;
69
+ case "resource":
70
+ parser.resourceDeclaration();
71
+ return parser;
66
72
  case "realtime_server":
67
73
  parser.realtimeServerDeclaration();
68
74
  return parser;
@@ -12,6 +12,10 @@ import { errorTriggerDeclaration } from "./error_trigger_parser.js";
12
12
  import { functionDeclaration } from "./function_parser.js";
13
13
  import { register as register_functions } from "./functions/register.js";
14
14
  import { register as register_generic } from "./generic/register.js";
15
+ import {
16
+ promptDeclaration,
17
+ resourceDeclaration,
18
+ } from "./mcp_primitive_parser.js";
15
19
  import { mcpServerDeclaration } from "./mcp_server_parser.js";
16
20
  import { mcpServerTriggerDeclaration } from "./mcp_server_trigger_parser.js";
17
21
  import { messageDeclaration } from "./message_parser.js";
@@ -63,7 +67,12 @@ export const register = ($) => {
63
67
  "agentTriggerDeclaration",
64
68
  agentTriggerDeclaration($)
65
69
  );
70
+ $.promptDeclaration = $.RULE("promptDeclaration", promptDeclaration($));
66
71
  $.queryDeclaration = $.RULE("queryDeclaration", queryDeclaration($));
72
+ $.resourceDeclaration = $.RULE(
73
+ "resourceDeclaration",
74
+ resourceDeclaration($)
75
+ );
67
76
  $.apiGroupDeclaration = $.RULE("apiGroupDeclaration", apiGroupDeclaration($));
68
77
  $.branchDeclaration = $.RULE("branchDeclaration", branchDeclaration($));
69
78
  $.channelDeclaration = $.RULE("channelDeclaration", channelDeclaration($));
@@ -5,16 +5,20 @@ import { InstructionsToken, ToolToken } from "../lexer/tool.js";
5
5
 
6
6
  export function toolDeclaration($) {
7
7
  return () => {
8
+ let hasAnnotations = false;
8
9
  let hasDescription = false;
9
10
  let hasGuid = false;
10
11
  let hasDocs = false;
11
12
  let hasHistory = false;
13
+ let hasIcons = false;
12
14
  let hasInput = false;
13
15
  let hasInstructions = false;
14
16
  let hasMiddleware = false;
17
+ let hasOutput = false;
15
18
  let hasResponse = false;
16
19
  let hasStack = false;
17
20
  let hasTags = false;
21
+ let hasTitle = false;
18
22
 
19
23
  $.sectionStack.push("toolDeclaration");
20
24
  // Allow leading comments and newlines before the tool declaration
@@ -31,6 +35,13 @@ export function toolDeclaration($) {
31
35
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
32
36
  $.OR2([
33
37
  { ALT: () => $.SUBRULE($.commentBlockFn) },
38
+ {
39
+ GATE: () => !hasAnnotations,
40
+ ALT: () => {
41
+ hasAnnotations = true;
42
+ $.SUBRULE($.mcpToolAnnotationsClause);
43
+ },
44
+ },
34
45
  {
35
46
  GATE: () => !hasDescription,
36
47
  ALT: () => {
@@ -59,6 +70,13 @@ export function toolDeclaration($) {
59
70
  $.SUBRULE($.historyClause);
60
71
  },
61
72
  },
73
+ {
74
+ GATE: () => !hasIcons,
75
+ ALT: () => {
76
+ hasIcons = true;
77
+ $.SUBRULE($.mcpIconsClause);
78
+ },
79
+ },
62
80
  {
63
81
  GATE: () => !hasInput,
64
82
  ALT: () => {
@@ -84,6 +102,13 @@ export function toolDeclaration($) {
84
102
  $.SUBRULE($.middlewareClause);
85
103
  },
86
104
  },
105
+ {
106
+ GATE: () => !hasOutput,
107
+ ALT: () => {
108
+ hasOutput = true;
109
+ $.SUBRULE($.outputClause);
110
+ },
111
+ },
87
112
  {
88
113
  GATE: () => !hasResponse,
89
114
  ALT: () => {
@@ -107,6 +132,13 @@ export function toolDeclaration($) {
107
132
  $.SUBRULE($.tagsAttribute);
108
133
  },
109
134
  },
135
+ {
136
+ GATE: () => !hasTitle,
137
+ ALT: () => {
138
+ hasTitle = true;
139
+ $.SUBRULE($.mcpTitleClause);
140
+ },
141
+ },
110
142
  ]);
111
143
  });
112
144
 
package/utils.js CHANGED
@@ -32,11 +32,13 @@ const schemeByFirstWord = {
32
32
  message: "message",
33
33
  microservice: "microservice",
34
34
  middleware: "middleware",
35
+ prompt: "prompt",
35
36
  query: "api",
36
37
  realtime_server: "realtime_server",
37
38
  realtime_server_trigger: "realtime_server_trigger",
38
39
  realtime_trigger: "realtime_trigger",
39
40
  realtime_channel: "realtime_channel",
41
+ resource: "resource",
40
42
  run: "run",
41
43
  table: "db",
42
44
  table_trigger: "table_trigger",