@xano/xanoscript-language-server 12.17.0 → 12.19.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 +26 -1
- package/lexer/policy.js +30 -0
- package/lexer/tokens.js +6 -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 +43 -0
- package/onHover/onHover.js +2 -0
- package/package.json +3 -1
- package/parser/functions/mcp/mcpOauthFn.js +140 -0
- package/parser/functions/mcp/register.js +10 -0
- package/parser/functions/mcpFn.js +2 -1
- package/parser/mcp_server_parser.js +48 -1
- package/parser/multidoc.js +45 -13
- package/parser/parser.js +52 -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 +12 -0
- package/server.js +6 -0
- package/utils.js +4 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { CommentToken } from "../lexer/comment.js";
|
|
2
|
+
import {
|
|
3
|
+
ColonToken,
|
|
4
|
+
EqualToken,
|
|
5
|
+
LCurly,
|
|
6
|
+
LSquare,
|
|
7
|
+
RCurly,
|
|
8
|
+
RSquare,
|
|
9
|
+
} from "../lexer/control.js";
|
|
10
|
+
import { SingleQuotedStringLiteral, StringLiteral } from "../lexer/literal.js";
|
|
11
|
+
import { PolicyToken } from "../lexer/policy.js";
|
|
12
|
+
import { Identifier, NewlineToken } from "../lexer/tokens.js";
|
|
13
|
+
import { getPolicyCatalogue } from "../parser/policy/catalogue.js";
|
|
14
|
+
import { checkMarkdown, paramMarkdown } from "../parser/policy/policyDocs.js";
|
|
15
|
+
import { isA, own, policyStringValue } from "../parser/policy/policyLiteral.js";
|
|
16
|
+
import { HoverMessageProvider } from "./HoverMessageProvider.js";
|
|
17
|
+
|
|
18
|
+
const isString = (token) => isA(token, StringLiteral) || isA(token, SingleQuotedStringLiteral);
|
|
19
|
+
|
|
20
|
+
const unquote = policyStringValue;
|
|
21
|
+
|
|
22
|
+
/** The key a value at `k` belongs to: the word before the `=` or `:` just before it. */
|
|
23
|
+
function keyBefore(tokens, k) {
|
|
24
|
+
const sep = tokens[k - 1];
|
|
25
|
+
if ((isA(sep, EqualToken) || isA(sep, ColonToken)) && isA(tokens[k - 2], Identifier)) {
|
|
26
|
+
return tokens[k - 2].image;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The first word on the line holding token `k`. */
|
|
32
|
+
function lineHead(tokens, k) {
|
|
33
|
+
if (tokens[k - 1]?.image === "rule" && tokens[k - 2]?.image !== "policy") return "rule";
|
|
34
|
+
let j = k;
|
|
35
|
+
while (j > 0 && !isA(tokens[j - 1], NewlineToken)) j--;
|
|
36
|
+
return isA(tokens[j], Identifier) ? tokens[j].image : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The blocks open at `index`, outermost first, each with the check its rule names (read from the
|
|
41
|
+
* whole rule, so a `check` written after `params` still counts).
|
|
42
|
+
*/
|
|
43
|
+
function framesAt(tokens, index) {
|
|
44
|
+
const stack = [];
|
|
45
|
+
let at = null;
|
|
46
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
47
|
+
if (k === index) at = [...stack];
|
|
48
|
+
const token = tokens[k];
|
|
49
|
+
if (isA(token, LCurly) || isA(token, LSquare)) {
|
|
50
|
+
stack.push({
|
|
51
|
+
owner: keyBefore(tokens, k) ?? (isA(token, LCurly) ? lineHead(tokens, k) : null),
|
|
52
|
+
check: null,
|
|
53
|
+
});
|
|
54
|
+
} else if (isA(token, RCurly) || isA(token, RSquare)) {
|
|
55
|
+
stack.pop();
|
|
56
|
+
// Past the block that holds `index`: nothing later can change what it names.
|
|
57
|
+
if (at && stack.length < 2) break;
|
|
58
|
+
} else if (isString(token) && keyBefore(tokens, k) === "check" && stack.length) {
|
|
59
|
+
stack[stack.length - 1].check = unquote(token.image).trim();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return at ?? [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Markdown for a policy or rule field, from the catalogue's `document`. */
|
|
66
|
+
function fieldMarkdown(name, spec) {
|
|
67
|
+
const lines = [`**${name}** (${spec.type ?? "string"}${spec.required ? ", required" : ""})`];
|
|
68
|
+
if (spec.description) lines.push("", spec.description);
|
|
69
|
+
if (spec.values?.length) lines.push("", `One of: ${spec.values.map((v) => "`" + v + "`").join(", ")}.`);
|
|
70
|
+
if (spec.default !== undefined) lines.push("", `Default: \`${spec.default}\``);
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Hover inside a `policy` document: a policy or rule field shows what it is for; a rule's check id
|
|
76
|
+
* shows what the check does and its params; a param (or an object param's field) shows its type,
|
|
77
|
+
* values, default and description. All of it is rendered from the check catalogue the host loaded.
|
|
78
|
+
*/
|
|
79
|
+
export class PolicyMessageProvider extends HoverMessageProvider {
|
|
80
|
+
/**
|
|
81
|
+
* @returns {{kind: "field", name: string, spec: Object} | {kind: "check", check: Object}
|
|
82
|
+
* | {kind: "param", check: Object, path: string, spec: Object} | null}
|
|
83
|
+
*/
|
|
84
|
+
resolve(index, tokens) {
|
|
85
|
+
const head = tokens.find((t) => !isA(t, NewlineToken) && !isA(t, CommentToken));
|
|
86
|
+
if (!isA(head, PolicyToken)) return null;
|
|
87
|
+
const catalogue = getPolicyCatalogue();
|
|
88
|
+
if (!catalogue.source) return null;
|
|
89
|
+
|
|
90
|
+
const token = tokens[index];
|
|
91
|
+
const frames = framesAt(tokens, index);
|
|
92
|
+
const [policy, rule, params, nested] = frames;
|
|
93
|
+
if (policy?.owner !== "policy") return null;
|
|
94
|
+
|
|
95
|
+
// A field name: `name =` in the policy or in a rule.
|
|
96
|
+
if (isA(token, Identifier) && isA(tokens[index + 1], EqualToken)) {
|
|
97
|
+
const fields =
|
|
98
|
+
frames.length === 1
|
|
99
|
+
? catalogue.document?.fields
|
|
100
|
+
: frames.length === 2 && rule?.owner === "rule"
|
|
101
|
+
? catalogue.document?.rule
|
|
102
|
+
: null;
|
|
103
|
+
const spec = own(fields, token.image);
|
|
104
|
+
return spec ? { kind: "field", name: token.image, spec } : null;
|
|
105
|
+
}
|
|
106
|
+
if (rule?.owner !== "rule") return null;
|
|
107
|
+
|
|
108
|
+
if (frames.length === 2 && isString(token) && keyBefore(tokens, index) === "check") {
|
|
109
|
+
const check = catalogue.checks.get(unquote(token.image).trim());
|
|
110
|
+
return check ? { kind: "check", check } : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const check = catalogue.checks.get(rule.check ?? "");
|
|
114
|
+
if (!check || params?.owner !== "params" || !isA(token, Identifier) || !isA(tokens[index + 1], ColonToken)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (frames.length === 3) {
|
|
118
|
+
const spec = own(check.params, token.image);
|
|
119
|
+
return spec ? { kind: "param", check, path: token.image, spec } : null;
|
|
120
|
+
}
|
|
121
|
+
if (frames.length === 4) {
|
|
122
|
+
const spec = own(own(check.params, nested.owner)?.properties, token.image);
|
|
123
|
+
return spec
|
|
124
|
+
? { kind: "param", check, path: `${nested.owner}.${token.image}`, spec }
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isMatch(index, tokens) {
|
|
131
|
+
return this.resolve(index, tokens) !== null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render(index, tokens) {
|
|
135
|
+
const found = this.resolve(index, tokens);
|
|
136
|
+
if (!found) return "";
|
|
137
|
+
if (found.kind === "field") return fieldMarkdown(found.name, found.spec);
|
|
138
|
+
return found.kind === "check"
|
|
139
|
+
? checkMarkdown(found.check, getPolicyCatalogue())
|
|
140
|
+
: paramMarkdown(found.check.id, found.path, found.spec);
|
|
141
|
+
}
|
|
142
|
+
}
|
package/onHover/functions.md
CHANGED
|
@@ -512,6 +512,49 @@ Sends an MCP `notifications/progress` update to the client while a tool, prompt
|
|
|
512
512
|
- `total`: optional. The expected final value.
|
|
513
513
|
- `message`: optional. A short status line.
|
|
514
514
|
|
|
515
|
+
# mcp.oauth.complete
|
|
516
|
+
|
|
517
|
+
```xs
|
|
518
|
+
mcp.oauth.complete {
|
|
519
|
+
request = $input.mcp_request
|
|
520
|
+
decision = $input.decision
|
|
521
|
+
} as $continue_url
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Finishes a hosted-mode MCP sign-in from your login endpoint and returns the single-use URL your login page sends the browser to. Your login page is the consent screen: it shows the request (see `mcp.oauth.request`) and passes the user's answer here.
|
|
525
|
+
|
|
526
|
+
- `request`: required. The `mcp_request` value your login page received.
|
|
527
|
+
- `decision`: required. `approve` signs the user in; `deny` refuses the sign-in.
|
|
528
|
+
- `unsafe_user_id`: optional and DANGEROUS. Signs in that auth-table row directly, skipping the `$auth` and session-age checks. Use it only in a login flow whose own stack has already proven who the user is.
|
|
529
|
+
|
|
530
|
+
By default the user is the endpoint's `$auth`: a row of the server's auth table whose session token was issued moments ago, so mint a fresh token at login.
|
|
531
|
+
|
|
532
|
+
# mcp.oauth.request
|
|
533
|
+
|
|
534
|
+
```xs
|
|
535
|
+
mcp.oauth.request {
|
|
536
|
+
request = $input.mcp_request
|
|
537
|
+
} as $sign_in
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Returns the verified details of a pending hosted-mode MCP sign-in request (`client_name`, `client_logo`, `callback_host`, `client_verified`, `server_name`) for the consent screen on your login page. It reads the request without consuming it.
|
|
541
|
+
|
|
542
|
+
- `request`: required. The `mcp_request` value your login page received.
|
|
543
|
+
|
|
544
|
+
# mcp.oauth.revoke
|
|
545
|
+
|
|
546
|
+
```xs
|
|
547
|
+
mcp.oauth.revoke {
|
|
548
|
+
mcp_server = "my_server"
|
|
549
|
+
user_id = $input.user_id
|
|
550
|
+
} as $revoked
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Ends hosted-mode MCP sign-ins on an MCP server of this workspace and branch. Returns `{revoked: <count>}`.
|
|
554
|
+
|
|
555
|
+
- `mcp_server`: required. The MCP server's name.
|
|
556
|
+
- Then exactly one of `user_id` (every grant of that auth-table row), `grant_id` (one grant) or `all = true` (every grant of the server).
|
|
557
|
+
|
|
515
558
|
# api.lambda
|
|
516
559
|
|
|
517
560
|
```xs
|
package/onHover/onHover.js
CHANGED
|
@@ -7,10 +7,12 @@ import { InputFilterMessageProvider } from "./InputFilterMessageProvider.js";
|
|
|
7
7
|
import inputFiltersMd from "./inputFilters.md";
|
|
8
8
|
import { InputVariableMessageProvider } from "./InputVariableMessageProvider.js";
|
|
9
9
|
import { onHoverDocument } from "./onHoverDocument.js";
|
|
10
|
+
import { PolicyMessageProvider } from "./PolicyMessageProvider.js";
|
|
10
11
|
import { QueryFilterMessageProvider } from "./queryFilterMessageProvider.js";
|
|
11
12
|
import queryFiltersMd from "./queryFilters.md";
|
|
12
13
|
|
|
13
14
|
const hoverProviders = [
|
|
15
|
+
new PolicyMessageProvider(), // Check ids and params in a policy, from the check catalogue
|
|
14
16
|
new InputVariableMessageProvider(),
|
|
15
17
|
new FunctionMessageProvider(functionsMd),
|
|
16
18
|
new InputFilterMessageProvider(inputFiltersMd), // Input filters (columnDefinition context)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xano/xanoscript-language-server",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.19.0",
|
|
4
4
|
"description": "Language Server Protocol implementation for XanoScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"onCheckError/",
|
|
19
19
|
"onCompletion/contentAssist.js",
|
|
20
20
|
"onCompletion/onCompletion.js",
|
|
21
|
+
"onCompletion/policyCompletion.js",
|
|
21
22
|
"onDefinition/onDefinition.js",
|
|
22
23
|
"onDidChangeContent/",
|
|
23
24
|
"onHover/",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"lint": "eslint . --ext .js",
|
|
48
49
|
"lint:fix": "eslint . --ext .js --fix",
|
|
49
50
|
"tm": "node ./toTmLanguage.js",
|
|
51
|
+
"gen:policy-catalogue": "node scripts/gen-policy-catalogue.mjs",
|
|
50
52
|
"prepare": "husky"
|
|
51
53
|
},
|
|
52
54
|
"dependencies": {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { RequestToken } from "../../../lexer/api.js";
|
|
2
|
+
import { RCurly } from "../../../lexer/control.js";
|
|
3
|
+
import { StringLiteral } from "../../../lexer/literal.js";
|
|
4
|
+
import { CompleteToken, OauthToken, RevokeToken } from "../../../lexer/mcp.js";
|
|
5
|
+
import { DotToken, NewlineToken } from "../../../lexer/tokens.js";
|
|
6
|
+
import { getVarName } from "../../generic/utils.js";
|
|
7
|
+
|
|
8
|
+
// The values cloud-client's mvp:mcp_oauth_complete accepts for `decision`.
|
|
9
|
+
const DECISIONS = ["approve", "deny"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `decision` takes any expression (usually `$input.decision`), but a plain
|
|
13
|
+
* string literal is checked against the enum here so a typo is caught in the
|
|
14
|
+
* editor rather than refused at run time.
|
|
15
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
16
|
+
* @param {import('chevrotain').IToken} identifier
|
|
17
|
+
*/
|
|
18
|
+
function decisionValue($, identifier) {
|
|
19
|
+
const first = $.LA(1);
|
|
20
|
+
const next = $.LA(2);
|
|
21
|
+
if (
|
|
22
|
+
first.tokenType === StringLiteral &&
|
|
23
|
+
(next.tokenType === NewlineToken || next.tokenType === RCurly)
|
|
24
|
+
) {
|
|
25
|
+
const value = getVarName(first);
|
|
26
|
+
if (!DECISIONS.includes(value)) {
|
|
27
|
+
$.addInvalidValueError(
|
|
28
|
+
first,
|
|
29
|
+
`Invalid value "${value}". Must be one of: ${DECISIONS.map(
|
|
30
|
+
(v) => `"${v}"`
|
|
31
|
+
).join(", ")}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
$.SUBRULE5($.expressionFn, { ARGS: [identifier, {}] });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The `mcp.oauth.*` login hand-off statements (cloud-client
|
|
39
|
+
// mvp:mcp_oauth_{complete,request,revoke}, kind schemas
|
|
40
|
+
// statement/mcp.oauth.{complete,request,revoke}.yaml). Each takes an optional
|
|
41
|
+
// `as $var` binding.
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* mcp.oauth.complete, mcp.oauth.request, mcp.oauth.revoke
|
|
45
|
+
* (the leading "mcp." is consumed by mcpFn)
|
|
46
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
47
|
+
*/
|
|
48
|
+
export function mcpOauthFn($) {
|
|
49
|
+
return () => {
|
|
50
|
+
$.sectionStack.push("mcpOauthFn");
|
|
51
|
+
$.CONSUME(OauthToken); // "oauth"
|
|
52
|
+
$.CONSUME(DotToken); // "."
|
|
53
|
+
$.OR([
|
|
54
|
+
{ ALT: () => $.SUBRULE($.mcpOauthCompleteFn) }, // "complete"
|
|
55
|
+
{ ALT: () => $.SUBRULE($.mcpOauthRequestFn) }, // "request"
|
|
56
|
+
{ ALT: () => $.SUBRULE($.mcpOauthRevokeFn) }, // "revoke"
|
|
57
|
+
]);
|
|
58
|
+
$.sectionStack.pop();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// mcp.oauth.complete {
|
|
63
|
+
// request = $input.mcp_request // required: the opaque request handle
|
|
64
|
+
// decision = "approve" // required: approve | deny
|
|
65
|
+
// unsafe_user_id = $user.id // optional: sign in a row without $auth
|
|
66
|
+
// } as $continue_url
|
|
67
|
+
/**
|
|
68
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
69
|
+
*/
|
|
70
|
+
export function mcpOauthCompleteFn($) {
|
|
71
|
+
const requiredAttrs = ["request", "decision"];
|
|
72
|
+
const optionalAttrs = ["unsafe_user_id", "description", "disabled", "mock"];
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
$.sectionStack.push("mcpOauthCompleteFn");
|
|
76
|
+
const fnToken = $.CONSUME(CompleteToken); // "complete"
|
|
77
|
+
$.SUBRULE($.functionAttrReq, {
|
|
78
|
+
ARGS: [
|
|
79
|
+
fnToken,
|
|
80
|
+
requiredAttrs,
|
|
81
|
+
optionalAttrs,
|
|
82
|
+
{ types: { decision: decisionValue } },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
$.SUBRULE($.asVariable, { ARGS: [fnToken] });
|
|
86
|
+
$.sectionStack.pop();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// mcp.oauth.request {
|
|
91
|
+
// request = $input.mcp_request
|
|
92
|
+
// } as $sign_in
|
|
93
|
+
/**
|
|
94
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
95
|
+
*/
|
|
96
|
+
export function mcpOauthRequestFn($) {
|
|
97
|
+
const requiredAttrs = ["request"];
|
|
98
|
+
const optionalAttrs = ["description", "disabled", "mock"];
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
$.sectionStack.push("mcpOauthRequestFn");
|
|
102
|
+
const fnToken = $.CONSUME(RequestToken); // "request"
|
|
103
|
+
$.SUBRULE($.functionAttrReq, {
|
|
104
|
+
ARGS: [fnToken, requiredAttrs, optionalAttrs],
|
|
105
|
+
});
|
|
106
|
+
$.SUBRULE($.asVariable, { ARGS: [fnToken] });
|
|
107
|
+
$.sectionStack.pop();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// mcp.oauth.revoke {
|
|
112
|
+
// mcp_server = "example_server"
|
|
113
|
+
// user_id = $input.user_id // exactly ONE of user_id, grant_id or
|
|
114
|
+
// grant_id = $input.grant_id // all = true must be in effect; that is
|
|
115
|
+
// all = true // checked at run time on the values
|
|
116
|
+
// } as $revoked
|
|
117
|
+
/**
|
|
118
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
119
|
+
*/
|
|
120
|
+
export function mcpOauthRevokeFn($) {
|
|
121
|
+
const requiredAttrs = ["mcp_server"];
|
|
122
|
+
const optionalAttrs = [
|
|
123
|
+
"user_id",
|
|
124
|
+
"grant_id",
|
|
125
|
+
"all",
|
|
126
|
+
"description",
|
|
127
|
+
"disabled",
|
|
128
|
+
"mock",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
return () => {
|
|
132
|
+
$.sectionStack.push("mcpOauthRevokeFn");
|
|
133
|
+
const fnToken = $.CONSUME(RevokeToken); // "revoke"
|
|
134
|
+
$.SUBRULE($.functionAttrReq, {
|
|
135
|
+
ARGS: [fnToken, requiredAttrs, optionalAttrs],
|
|
136
|
+
});
|
|
137
|
+
$.SUBRULE($.asVariable, { ARGS: [fnToken] });
|
|
138
|
+
$.sectionStack.pop();
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { mcpElicitFn } from "./mcpElicitFn.js";
|
|
2
|
+
import {
|
|
3
|
+
mcpOauthCompleteFn,
|
|
4
|
+
mcpOauthFn,
|
|
5
|
+
mcpOauthRequestFn,
|
|
6
|
+
mcpOauthRevokeFn,
|
|
7
|
+
} from "./mcpOauthFn.js";
|
|
2
8
|
import { mcpProgressFn } from "./mcpProgressFn.js";
|
|
3
9
|
|
|
4
10
|
/**
|
|
@@ -8,4 +14,8 @@ import { mcpProgressFn } from "./mcpProgressFn.js";
|
|
|
8
14
|
export const register = ($) => {
|
|
9
15
|
$.mcpElicitFn = $.RULE("mcpElicitFn", mcpElicitFn($));
|
|
10
16
|
$.mcpProgressFn = $.RULE("mcpProgressFn", mcpProgressFn($));
|
|
17
|
+
$.mcpOauthCompleteFn = $.RULE("mcpOauthCompleteFn", mcpOauthCompleteFn($));
|
|
18
|
+
$.mcpOauthRequestFn = $.RULE("mcpOauthRequestFn", mcpOauthRequestFn($));
|
|
19
|
+
$.mcpOauthRevokeFn = $.RULE("mcpOauthRevokeFn", mcpOauthRevokeFn($));
|
|
20
|
+
$.mcpOauthFn = $.RULE("mcpOauthFn", mcpOauthFn($));
|
|
11
21
|
};
|
|
@@ -2,7 +2,7 @@ import { McpToken } from "../../lexer/ai.js";
|
|
|
2
2
|
import { DotToken } from "../../lexer/tokens.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* mcp.elicit, mcp.progress
|
|
5
|
+
* mcp.elicit, mcp.progress, mcp.oauth.{complete,request,revoke}
|
|
6
6
|
* @param {import('../base_parser.js').XanoBaseParser} $
|
|
7
7
|
*/
|
|
8
8
|
export function mcpFn($) {
|
|
@@ -13,6 +13,7 @@ export function mcpFn($) {
|
|
|
13
13
|
$.OR([
|
|
14
14
|
{ ALT: () => $.SUBRULE($.mcpElicitFn) }, // "mcp.elicit"
|
|
15
15
|
{ ALT: () => $.SUBRULE($.mcpProgressFn) }, // "mcp.progress"
|
|
16
|
+
{ ALT: () => $.SUBRULE($.mcpOauthFn) }, // "mcp.oauth.*"
|
|
16
17
|
]);
|
|
17
18
|
$.sectionStack.pop();
|
|
18
19
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
+
import { OauthToken, PromptsToken } from "../lexer/mcp.js";
|
|
4
4
|
import {
|
|
5
5
|
ActiveToken,
|
|
6
6
|
InstructionsToken,
|
|
@@ -19,6 +19,7 @@ export function mcpServerDeclaration($) {
|
|
|
19
19
|
let hasDocs = false;
|
|
20
20
|
let hasHistory = false;
|
|
21
21
|
let hasInstructions = false;
|
|
22
|
+
let hasOauth = false;
|
|
22
23
|
let hasPrompts = false;
|
|
23
24
|
let hasResources = false;
|
|
24
25
|
let hasTags = false;
|
|
@@ -96,6 +97,52 @@ export function mcpServerDeclaration($) {
|
|
|
96
97
|
});
|
|
97
98
|
},
|
|
98
99
|
},
|
|
100
|
+
{
|
|
101
|
+
// Per-server OAuth (cloud-client mcp_server.yaml `oauth`; values are
|
|
102
|
+
// validated server-side by helper/mvp/mcpoauth/OauthConfig.php).
|
|
103
|
+
// An assignment block like a channel's `publish`. mcp_server ONLY:
|
|
104
|
+
// deliberately absent from agent_parser.js. Mode-specific rules
|
|
105
|
+
// (hosted needs login_url, external needs issuer, ...) stay
|
|
106
|
+
// server-side; only the key set and the enums are checked here.
|
|
107
|
+
// There is no consent, branding or trusted_clients key: consent is
|
|
108
|
+
// the customer's own login page, which calls mcp.oauth.complete
|
|
109
|
+
// with a decision.
|
|
110
|
+
GATE: () => !hasOauth,
|
|
111
|
+
ALT: () => {
|
|
112
|
+
hasOauth = true;
|
|
113
|
+
|
|
114
|
+
const subParent = $.CONSUME(OauthToken); // "oauth"
|
|
115
|
+
$.CONSUME5(EqualToken); // "="
|
|
116
|
+
$.SUBRULE($.schemaParseObjectFn, {
|
|
117
|
+
ARGS: [
|
|
118
|
+
subParent,
|
|
119
|
+
{
|
|
120
|
+
"mode?": ["hosted", "external"],
|
|
121
|
+
"auth_table?": "[string]",
|
|
122
|
+
// hosted; may be "${env.NAME}"
|
|
123
|
+
"login_url?": "[string]",
|
|
124
|
+
// external; may be "${env.NAME}"
|
|
125
|
+
"issuer?": "[string]",
|
|
126
|
+
"audience?": ["[string]"],
|
|
127
|
+
"audience_ack?": "[boolean]",
|
|
128
|
+
"claim?": "[string]",
|
|
129
|
+
"claim_ack?": "[boolean]",
|
|
130
|
+
"column?": "[string]",
|
|
131
|
+
"preset?": [
|
|
132
|
+
"auth0",
|
|
133
|
+
"clerk",
|
|
134
|
+
"workos",
|
|
135
|
+
"entra",
|
|
136
|
+
"okta",
|
|
137
|
+
"cognito",
|
|
138
|
+
"generic",
|
|
139
|
+
],
|
|
140
|
+
"allowed_client_ids?": ["[string]"],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
},
|
|
99
146
|
{
|
|
100
147
|
// First-class prompts attached to the server (MCP 2026-07-28).
|
|
101
148
|
// mcp_server ONLY: deliberately absent from agent_parser.js.
|
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,31 @@ 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;
|
|
66
94
|
case "prompt":
|
|
67
95
|
parser.promptDeclaration();
|
|
68
96
|
return parser;
|
|
@@ -109,6 +137,30 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
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
|
+
|
|
112
164
|
export class XanoScriptParser extends XanoBaseParser {
|
|
113
165
|
constructor() {
|
|
114
166
|
super(allTokens);
|