@xano/xanoscript-language-server 12.16.6 → 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/mcp.js +109 -0
- package/lexer/tokens.js +8 -0
- package/onHover/functions.md +39 -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/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,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
|
+
}
|
|
@@ -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;
|
package/parser/register.js
CHANGED
|
@@ -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($));
|