@xano/xanoscript-language-server 12.18.0 → 12.20.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 +26 -1
- package/onHover/functions.md +45 -0
- package/package.json +1 -1
- package/parser/functions/api/apiCallFn.js +4 -0
- 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/policy/catalogue.js +1 -1
- package/parser/policy/catalogueSnapshot.js +3613 -2167
- package/parser/policy/policyDocs.js +29 -0
- package/parser/policy/validatePolicyRule.js +18 -0
package/lexer/mcp.js
CHANGED
|
@@ -3,7 +3,8 @@ import { createTokenByName } from "./utils.js";
|
|
|
3
3
|
|
|
4
4
|
// MCP 2026-07-28 authoring keywords: the `prompt` and `resource` object kinds,
|
|
5
5
|
// the tool/prompt/resource metadata keys, and the `mcp.elicit` / `mcp.progress`
|
|
6
|
-
// statements
|
|
6
|
+
// statements, plus the per-server `oauth` block and the `mcp.oauth.complete` /
|
|
7
|
+
// `mcp.oauth.request` / `mcp.oauth.revoke` statements. Grounded against cloud-client's schema DSL
|
|
7
8
|
// (`script/kind/schema/core/{prompt,resource,tool,mcp_server}.yaml` and
|
|
8
9
|
// `script/kind/schema/statement/mcp.{elicit,progress}.yaml`).
|
|
9
10
|
//
|
|
@@ -71,6 +72,24 @@ export const ProgressToken = createTokenByName("progress", {
|
|
|
71
72
|
categories: [Identifier],
|
|
72
73
|
});
|
|
73
74
|
|
|
75
|
+
// "oauth" (the mcp_server `oauth` block and the `mcp.oauth.*` statements)
|
|
76
|
+
export const OauthToken = createTokenByName("oauth", {
|
|
77
|
+
longer_alt: Identifier,
|
|
78
|
+
categories: [Identifier],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// "complete" (mcp.oauth.complete)
|
|
82
|
+
export const CompleteToken = createTokenByName("complete", {
|
|
83
|
+
longer_alt: Identifier,
|
|
84
|
+
categories: [Identifier],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// "revoke" (mcp.oauth.revoke)
|
|
88
|
+
export const RevokeToken = createTokenByName("revoke", {
|
|
89
|
+
longer_alt: Identifier,
|
|
90
|
+
categories: [Identifier],
|
|
91
|
+
});
|
|
92
|
+
|
|
74
93
|
export const McpObjectTokens = [
|
|
75
94
|
PromptsToken,
|
|
76
95
|
PromptToken,
|
|
@@ -82,6 +101,9 @@ export const McpObjectTokens = [
|
|
|
82
101
|
MimeTypeToken,
|
|
83
102
|
ElicitToken,
|
|
84
103
|
ProgressToken,
|
|
104
|
+
OauthToken,
|
|
105
|
+
CompleteToken,
|
|
106
|
+
RevokeToken,
|
|
85
107
|
];
|
|
86
108
|
|
|
87
109
|
/**
|
|
@@ -99,9 +121,12 @@ export function mapTokenToType(token) {
|
|
|
99
121
|
case IconsToken.name:
|
|
100
122
|
case UriToken.name:
|
|
101
123
|
case MimeTypeToken.name:
|
|
124
|
+
case OauthToken.name:
|
|
102
125
|
return "variable";
|
|
103
126
|
case ElicitToken.name:
|
|
104
127
|
case ProgressToken.name:
|
|
128
|
+
case CompleteToken.name:
|
|
129
|
+
case RevokeToken.name:
|
|
105
130
|
return "function";
|
|
106
131
|
default:
|
|
107
132
|
return null;
|
package/onHover/functions.md
CHANGED
|
@@ -446,6 +446,8 @@ api.call "account/{account_id}" verb=POST {
|
|
|
446
446
|
|
|
447
447
|
Calls an internal Xano API endpoint from within a `workflow_test` or `test` clause. This allows you to invoke internal APIs as part of your end-to-end workflow tests or unit tests within queries, functions, and tasks.
|
|
448
448
|
|
|
449
|
+
To call the endpoint as a logged-in user, pass a token with `auth = {token: "...", ignore_expiration: true}` (a literal token) or an `Authorization` header (`headers = ["Authorization: Bearer " ~ $token]`).
|
|
450
|
+
|
|
449
451
|
# trigger.call
|
|
450
452
|
|
|
451
453
|
```xs
|
|
@@ -512,6 +514,49 @@ Sends an MCP `notifications/progress` update to the client while a tool, prompt
|
|
|
512
514
|
- `total`: optional. The expected final value.
|
|
513
515
|
- `message`: optional. A short status line.
|
|
514
516
|
|
|
517
|
+
# mcp.oauth.complete
|
|
518
|
+
|
|
519
|
+
```xs
|
|
520
|
+
mcp.oauth.complete {
|
|
521
|
+
request = $input.mcp_request
|
|
522
|
+
decision = $input.decision
|
|
523
|
+
} as $continue_url
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
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.
|
|
527
|
+
|
|
528
|
+
- `request`: required. The `mcp_request` value your login page received.
|
|
529
|
+
- `decision`: required. `approve` signs the user in; `deny` refuses the sign-in.
|
|
530
|
+
- `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.
|
|
531
|
+
|
|
532
|
+
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.
|
|
533
|
+
|
|
534
|
+
# mcp.oauth.request
|
|
535
|
+
|
|
536
|
+
```xs
|
|
537
|
+
mcp.oauth.request {
|
|
538
|
+
request = $input.mcp_request
|
|
539
|
+
} as $sign_in
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
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.
|
|
543
|
+
|
|
544
|
+
- `request`: required. The `mcp_request` value your login page received.
|
|
545
|
+
|
|
546
|
+
# mcp.oauth.revoke
|
|
547
|
+
|
|
548
|
+
```xs
|
|
549
|
+
mcp.oauth.revoke {
|
|
550
|
+
mcp_server = "my_server"
|
|
551
|
+
user_id = $input.user_id
|
|
552
|
+
} as $revoked
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Ends hosted-mode MCP sign-ins on an MCP server of this workspace and branch. Returns `{revoked: <count>}`.
|
|
556
|
+
|
|
557
|
+
- `mcp_server`: required. The MCP server's name.
|
|
558
|
+
- 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).
|
|
559
|
+
|
|
515
560
|
# api.lambda
|
|
516
561
|
|
|
517
562
|
```xs
|
package/package.json
CHANGED
|
@@ -56,6 +56,10 @@ export function apiCallFn($) {
|
|
|
56
56
|
fnToken,
|
|
57
57
|
{
|
|
58
58
|
api_group: "[string]",
|
|
59
|
+
// The endpoint's credential, as the server's transform reads it:
|
|
60
|
+
// a literal token (the parser refuses a computed one; use an
|
|
61
|
+
// Authorization header for that), and ignore_expiration.
|
|
62
|
+
"auth?": { token: "[string]", "ignore_expiration?": "[boolean]" },
|
|
59
63
|
"description?": "[string]",
|
|
60
64
|
"disabled?": "[boolean]",
|
|
61
65
|
"headers?": "[expression]",
|
|
@@ -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.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The policy check catalogue the language server validates policies against.
|
|
3
3
|
*
|
|
4
4
|
* The platform owns the catalogue: `GET workspace/{id}/policy/check` (and `xano policy
|
|
5
|
-
* catalogue -o json`) answers `{items: [check...],
|
|
5
|
+
* catalogue -o json`) answers `{items: [check...], templates, document}`: each check with its id,
|
|
6
6
|
* label, description and param schemas, and `document`, the policy file's own fields (their types,
|
|
7
7
|
* value sets and defaults, retired fields, reserved blocks, the key pattern). Nothing here writes a
|
|
8
8
|
* check or a field down by hand. The registry starts EMPTY, and an empty registry means the
|