@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
@@ -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($);
@@ -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
+ }