@sassoftware/sas-score-mcp-serverjs 0.4.1-1 → 0.4.1-15
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/cli.js +111 -31
- package/package.json +4 -2
- package/skills/sas-list-tables-smart/SKILL.md +123 -0
- package/skills/sas-read-and-score/SKILL.md +54 -53
- package/skills/sas-read-strategy/SKILL.md +10 -10
- package/skills/sas-score-workflow/SKILL.md +19 -2
- package/skills/sas-spec-migration/SKILL.md +303 -0
- package/src/authpkce.js +219 -0
- package/src/createMcpServer.js +11 -8
- package/src/expressMcpServer.js +354 -338
- package/src/oauthHandlers/authorize.js +46 -0
- package/src/oauthHandlers/baseUrl.js +8 -0
- package/src/oauthHandlers/callback.js +93 -0
- package/src/oauthHandlers/getMetadata.js +27 -0
- package/src/oauthHandlers/index.js +7 -0
- package/src/oauthHandlers/token.js +37 -0
- package/src/processHeaders.js +88 -0
- package/src/toolHelpers/_listLibrary.js +0 -1
- package/src/toolHelpers/getLogonPayload.js +5 -1
- package/src/toolHelpers/refreshTokenOauth.js +3 -3
- package/src/toolSet/.claude/settings.local.json +13 -0
- package/src/toolSet/devaScore.js +61 -61
- package/src/toolSet/findJob.js +1 -1
- package/src/toolSet/findJobdef.js +2 -2
- package/src/toolSet/findLibrary.js +68 -67
- package/src/toolSet/findModel.js +2 -2
- package/src/toolSet/findTable.js +3 -2
- package/src/toolSet/getEnv.js +8 -4
- package/src/toolSet/listJobdefs.js +61 -61
- package/src/toolSet/listJobs.js +61 -61
- package/src/toolSet/listLibraries.js +78 -78
- package/src/toolSet/listModels.js +56 -56
- package/src/toolSet/listTables.js +66 -65
- package/src/toolSet/modelInfo.js +2 -2
- package/src/toolSet/modelScore.js +6 -5
- package/src/toolSet/readTable.js +63 -65
- package/src/toolSet/runCasProgram.js +7 -6
- package/src/toolSet/runJob.js +81 -81
- package/src/toolSet/runJobdef.js +82 -82
- package/src/toolSet/runMacro.js +81 -80
- package/src/toolSet/runProgram.js +4 -8
- package/src/toolSet/sasQuery.js +77 -78
- package/src/toolSet/scrInfo.js +1 -1
- package/src/toolSet/scrScore.js +69 -68
- package/src/toolSet/setContext.js +65 -65
- package/src/toolSet/superstat.js +61 -59
- package/src/toolSet/tableInfo.js +58 -57
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//authorize
|
|
2
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
+
import baseUrl from "./baseUrl.js";
|
|
4
|
+
|
|
5
|
+
function authorize(req, res, appContext, pkceStore, codeStore) {
|
|
6
|
+
const { response_type, redirect_uri, state, scope } = req.query;
|
|
7
|
+
console.error("===============================================================");
|
|
8
|
+
if (appContext.AUTHEXTERNAL === true) {
|
|
9
|
+
console.error('*************************************************************');
|
|
10
|
+
console.error("[Error] Received request for /authorize endpoint with external authorization expected");
|
|
11
|
+
console.error('*************************************************************');
|
|
12
|
+
}
|
|
13
|
+
console.error("[NOTE] query parameters:", { response_type, redirect_uri, state, scope });
|
|
14
|
+
if (response_type !== "code") {
|
|
15
|
+
return res.status(400).json({ error: "unsupported_response_type" });
|
|
16
|
+
}
|
|
17
|
+
if (!redirect_uri) {
|
|
18
|
+
return res.status(400).json({ error: "invalid_request", error_description: "redirect_uri is required" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const codeVerifier = randomBytes(64).toString("base64url");
|
|
22
|
+
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
23
|
+
const ourState = randomBytes(16).toString("hex");
|
|
24
|
+
|
|
25
|
+
pkceStore.set(ourState, { codeVerifier, clientRedirectUri: redirect_uri, clientState: state, codeChallenge });
|
|
26
|
+
|
|
27
|
+
const callbackUri = appContext.mcpHost + '/callback';
|
|
28
|
+
console.error("[Note] callbackUri:", callbackUri);
|
|
29
|
+
let urlConfig = {
|
|
30
|
+
response_type: "code",
|
|
31
|
+
client_id: appContext.CLIENTID,
|
|
32
|
+
redirect_uri: callbackUri,
|
|
33
|
+
scope: "openid",
|
|
34
|
+
state: ourState,
|
|
35
|
+
code_challenge: codeChallenge,
|
|
36
|
+
code_challenge_method: "S256"
|
|
37
|
+
}
|
|
38
|
+
if (appContext.PKCE !== "TRUE") {
|
|
39
|
+
urlConfig.client_secret = appContext.CLIENTSECRET;
|
|
40
|
+
}
|
|
41
|
+
console.error("[Note] Params for SAS Viya authorization request:", urlConfig);
|
|
42
|
+
const params = new URLSearchParams(urlConfig);
|
|
43
|
+
console.error("================================================================");
|
|
44
|
+
return res.redirect(`${appContext.VIYA_SERVER}/SASLogon/oauth/authorize?${params}`);
|
|
45
|
+
}
|
|
46
|
+
export default authorize;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import baseUrl from "./baseUrl.js";
|
|
2
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
async function callback(req, res, pkceStore, codeStore, appContext) {
|
|
5
|
+
console.error("===============================================================");
|
|
6
|
+
console.error("[Note] OAuth callback");
|
|
7
|
+
console.error(req.query);
|
|
8
|
+
const {code, state} = req.query;
|
|
9
|
+
|
|
10
|
+
console.error("[Note] query parameters:", code, state);
|
|
11
|
+
if (!code) {
|
|
12
|
+
return res.status(400).send("Missing code parameter");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pending = pkceStore.get(state);
|
|
16
|
+
if (!pending) {
|
|
17
|
+
return res.status(400).send("Invalid or expired state parameter");
|
|
18
|
+
}
|
|
19
|
+
pkceStore.delete(state);
|
|
20
|
+
|
|
21
|
+
// const callbackUri = `${baseUrl(appContext)}/callback`;
|
|
22
|
+
const callbackUri = appContext.mcpHost + '/callback';
|
|
23
|
+
console.error("[Note] callbackUri for token exchange:", callbackUri);
|
|
24
|
+
try {
|
|
25
|
+
const body = new URLSearchParams({
|
|
26
|
+
grant_type: "authorization_code",
|
|
27
|
+
client_id: appContext.CLIENTID,
|
|
28
|
+
code: code,
|
|
29
|
+
redirect_uri: callbackUri,
|
|
30
|
+
code_verifier: pending.codeVerifier
|
|
31
|
+
}).toString();
|
|
32
|
+
|
|
33
|
+
console.error("[Note] Token request body:", body.toString());
|
|
34
|
+
let connectOpts = {
|
|
35
|
+
ca: appContext.contexts.viyaCert.ca, // trust this CA
|
|
36
|
+
rejectUnauthorized: false // or false, if you really want to bypass checks
|
|
37
|
+
}
|
|
38
|
+
const agent = new Agent({ connect: connectOpts });
|
|
39
|
+
console.error("[Note] Initiating token exchange with SAS Viya");
|
|
40
|
+
let clientID= appContext.CLIENTID;
|
|
41
|
+
let h = buildBasicAuthFromClientId(clientID);
|
|
42
|
+
console.error("[Note] Authorization header for token request:", h);
|
|
43
|
+
const response = await undiciFetch(`${appContext.VIYA_SERVER}/SASLogon/oauth/token`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
47
|
+
"Accept": "application/json"
|
|
48
|
+
},
|
|
49
|
+
body: body,
|
|
50
|
+
dispatcher: agent,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const errText = await response.text();
|
|
55
|
+
console.error("[Error] SAS Viya token exchange failed:", errText);
|
|
56
|
+
return res.status(502).send("Token exchange with SAS Viya failed");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tokens = await response.json();
|
|
60
|
+
console.error("[Note] Received tokens from SAS Viya:", tokens);
|
|
61
|
+
const ourCode = randomUUID();
|
|
62
|
+
codeStore.set(ourCode, {
|
|
63
|
+
access_token: tokens.access_token,
|
|
64
|
+
refresh_token: tokens.refresh_token,
|
|
65
|
+
expires_in: tokens.expires_in,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const redirectParams = new URLSearchParams({ code: ourCode , state: pending.clientState});
|
|
69
|
+
//pending.clientState is the original state from the MCP client, which we should pass back to it for correlation
|
|
70
|
+
|
|
71
|
+
// pending.clientRedirectUri is the original redirect URI from the MCP client,
|
|
72
|
+
// which was part of the payload from the client to /oauth/authorize
|
|
73
|
+
// we trust since it was associated with the valid PKCE state
|
|
74
|
+
console.error("[Note] OAuth callback complete, redirecting to MCP client");
|
|
75
|
+
console.log(pending.clientRedirectUri.toString())
|
|
76
|
+
return res.redirect(`${pending.clientRedirectUri}?${redirectParams}`);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error("[Error] OAuth callback handler error:", err);
|
|
79
|
+
return res.status(500).send("Internal server error during token exchange");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildBasicAuthFromClientId(clientId) {
|
|
83
|
+
const raw = `${clientId}:`; // empty client_secret
|
|
84
|
+
const encoded =
|
|
85
|
+
typeof btoa === "function"
|
|
86
|
+
? btoa(raw)
|
|
87
|
+
: Buffer.from(raw).toString("base64");
|
|
88
|
+
|
|
89
|
+
return `Basic ${encoded}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
export default callback;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import baseUrl from "./baseUrl.js";
|
|
2
|
+
function getMetadata(req, res, appEnvContext) {
|
|
3
|
+
let base = '';
|
|
4
|
+
let host = '';
|
|
5
|
+
if (appEnvContext.AUTHEXTERNAL === true) {
|
|
6
|
+
base = appEnvContext.VIYA_SERVER + '/SASLogon';
|
|
7
|
+
host = appEnvContext.VIYA_SERVER;
|
|
8
|
+
} else {
|
|
9
|
+
base = appEnvContext.mcpHost;
|
|
10
|
+
host = appEnvContext.mcpHost;
|
|
11
|
+
}
|
|
12
|
+
console.error(`[Note] Base URL for Authorization Server Metadata: ${base}`);
|
|
13
|
+
let metadata = {
|
|
14
|
+
"issuer": host,
|
|
15
|
+
"authorization_endpoint": `${base}/oauth/authorize`,
|
|
16
|
+
"token_endpoint": `${base}/oauth/token`,
|
|
17
|
+
"response_types_supported": ["code"],
|
|
18
|
+
"grant_types_supported": ["authorization_code", "refresh_token"],
|
|
19
|
+
"code_challenge_methods_supported": ["S256"],
|
|
20
|
+
"scopes_supported": ["openid"]
|
|
21
|
+
};
|
|
22
|
+
console.error("===============================================================");
|
|
23
|
+
console.error("[Note] Authorization Server Metadata:", metadata);
|
|
24
|
+
console.error("===============================================================");
|
|
25
|
+
return metadata;
|
|
26
|
+
};
|
|
27
|
+
export default getMetadata;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function token(req, res, appContext, codeStore, cache) {
|
|
2
|
+
console.error("===============================================================");
|
|
3
|
+
console.error("[Note] at /token endpoint");
|
|
4
|
+
console.error("[Note] Request headers:", req.headers);
|
|
5
|
+
console.error("[Note] Handling token request with body:", req.body);
|
|
6
|
+
|
|
7
|
+
const { grant_type, code } = req.body;
|
|
8
|
+
console.error("[Note] OAuth token request received for code:", code);
|
|
9
|
+
console.error("[Note] Grant type:", grant_type);
|
|
10
|
+
|
|
11
|
+
const tokenData = codeStore.get(code);
|
|
12
|
+
if (!tokenData) {
|
|
13
|
+
return res.status(400).json({ error: "Invalid_code", error_description: "Invalid or expired authorization code" });
|
|
14
|
+
}
|
|
15
|
+
console.error("[Note] Retrieved token data for code:", Object.keys(tokenData));
|
|
16
|
+
|
|
17
|
+
let payload = {
|
|
18
|
+
access_token: code, // tokenData.access_token,
|
|
19
|
+
token_type: "Bearer",
|
|
20
|
+
expires_in: tokenData.expires_in,
|
|
21
|
+
scope: "openid" // You can include scopes if you have them, e.g., tokenData.scope
|
|
22
|
+
// scope: tokenData.scope,
|
|
23
|
+
};
|
|
24
|
+
let tokenlist = cache.get("tokenlist");
|
|
25
|
+
tokenData.refreshAt = Date.now() + tokenData.expires_in * 1000; // Set refresh time based on expires_in
|
|
26
|
+
tokenlist[code] = tokenData;
|
|
27
|
+
cache.set("tokenlist", tokenlist);
|
|
28
|
+
|
|
29
|
+
codeStore.delete(code); // Invalidate the code after use
|
|
30
|
+
console.error("[Note] Returning token to client");
|
|
31
|
+
console.error("[Note] Token sent to client:", JSON.stringify(payload, null, 2));
|
|
32
|
+
|
|
33
|
+
console.error("============================================================")
|
|
34
|
+
return res.status(200).json(payload);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default token;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { start } from "node:repl";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
import baseUrl from "./oauthHandlers/baseUrl.js";
|
|
8
|
+
function processHeaders(req, res, next, cache, appContext) {
|
|
9
|
+
|
|
10
|
+
// process any new header information
|
|
11
|
+
debugger;
|
|
12
|
+
console.error("=======================================================");
|
|
13
|
+
console.error("Processing headers for incoming request to /mcp endpoint");
|
|
14
|
+
// Allow different VIYA server per sessionid(user)
|
|
15
|
+
cache.del("headerCache");
|
|
16
|
+
let headerCache = {};
|
|
17
|
+
if (req.header("X-VIYA-SERVER") != null) {
|
|
18
|
+
console.error("[Note] Using user supplied VIYA server");
|
|
19
|
+
headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// fakeapi key since Viya does not support it
|
|
23
|
+
// not ideal for production, just for testing
|
|
24
|
+
const hdr2 = req.header("X-REFRESH-TOKEN");
|
|
25
|
+
if (hdr2 != null) {
|
|
26
|
+
headerCache.REFRESH_TOKEN = hdr2;
|
|
27
|
+
headerCache.AUTHFLOW = "refresh";
|
|
28
|
+
console.error("[Note] Using user supplied refresh token for authorization");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Oauth flow
|
|
32
|
+
const hdr = req.header("Authorization");
|
|
33
|
+
//for now, ignore Authorization if authflow is not bearer
|
|
34
|
+
let token = (hdr != null) ? hdr.slice(7) : null;
|
|
35
|
+
//console.error("[Note] Authorization token", token);
|
|
36
|
+
debugger;
|
|
37
|
+
console.log('>>>',appContext.AUTHFLOW);
|
|
38
|
+
if (appContext.AUTHFLOW === 'bearer') {
|
|
39
|
+
debugger;
|
|
40
|
+
let startAuth = false;
|
|
41
|
+
console.error("[Note] appContext.AUTHEXTERNAL:", appContext.AUTHEXTERNAL);
|
|
42
|
+
if (appContext.AUTHEXTERNAL === true) {
|
|
43
|
+
console.error("[Note] Expecting external authorization");
|
|
44
|
+
if (token != null) {
|
|
45
|
+
console.error("[Note] Using user supplied token for authorization");
|
|
46
|
+
headerCache.bearerToken = token;
|
|
47
|
+
} else {
|
|
48
|
+
startAuth = true;
|
|
49
|
+
}
|
|
50
|
+
} else if (token == null) {
|
|
51
|
+
console.error("[Note] No Authorization token provided in header.");
|
|
52
|
+
startAuth = true;
|
|
53
|
+
} else {
|
|
54
|
+
console.error("[Note] Checking token against cache", token);
|
|
55
|
+
let tokenlist = cache.get("tokenlist");
|
|
56
|
+
let tokenData = tokenlist[token];
|
|
57
|
+
if (tokenData == null) {
|
|
58
|
+
return res.status(403).json({
|
|
59
|
+
error: "unauthorized",
|
|
60
|
+
error_description: "[Error] Expired token. Clear token and try again."
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
token = tokenData.access_token;
|
|
64
|
+
headerCache.bearerToken = token;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (startAuth === true) {
|
|
68
|
+
// start oauth flow process
|
|
69
|
+
console.error(`[Note] No Authorization header provided.
|
|
70
|
+
Returning 401 to start the OAuth flow`);
|
|
71
|
+
const base = appContext.mcpHost;
|
|
72
|
+
res.set(
|
|
73
|
+
"WWW-Authenticate",
|
|
74
|
+
`Bearer resource_metadata="${base}/.well-known/oauth-authorization-server", scope="openid"`
|
|
75
|
+
);
|
|
76
|
+
return res.status(401).json({
|
|
77
|
+
error: "unauthorized",
|
|
78
|
+
error_description: "Bearer token required."
|
|
79
|
+
});
|
|
80
|
+
// start auth flow process since no token provided in header and we are not configured for external token
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// console.error("Header cache after processing:", headerCache);
|
|
84
|
+
cache.set("headerCache", headerCache);
|
|
85
|
+
next();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default processHeaders;
|
|
@@ -12,7 +12,6 @@ async function _listLibrary(params) {
|
|
|
12
12
|
|
|
13
13
|
const _ilistLibrary = async (params) => {
|
|
14
14
|
let { server, limit, start, name, _appContext } = params;
|
|
15
|
-
console.error(_appContext);
|
|
16
15
|
let config = {
|
|
17
16
|
casServerName: _appContext.cas,
|
|
18
17
|
computeContext: _appContext.sas,
|
|
@@ -53,7 +53,6 @@ async function igetLogonPayload(_appContext) {
|
|
|
53
53
|
token: _appContext.bearerToken,
|
|
54
54
|
tokenType: "Bearer",
|
|
55
55
|
};
|
|
56
|
-
console.error("[Note] Bearer token in logonPayload ", _appContext.bearerToken);
|
|
57
56
|
return logonPayload;
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -122,4 +121,9 @@ async function igetLogonPayload(_appContext) {
|
|
|
122
121
|
return null;
|
|
123
122
|
}
|
|
124
123
|
}
|
|
124
|
+
|
|
125
|
+
function isExpired(expiresAt, skewSeconds = 60) {
|
|
126
|
+
return Date.now() >= (expiresAt - skewSeconds * 1000);
|
|
127
|
+
}
|
|
128
|
+
|
|
125
129
|
export default getLogonPayload;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//import getOpts from './getOpts.js';
|
|
7
7
|
async function refreshTokenOauth(_appContext, oauthInfo ){
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const url = `${_appContext.VIYA_SERVER}/SASLogon/oauth/token`;
|
|
10
10
|
let opts = _appContext.contexts.appCert;
|
|
11
11
|
|
|
12
12
|
const agent = new Agent({
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
try {
|
|
24
24
|
const response = await fetch(url, {
|
|
25
25
|
method: 'POST',
|
|
26
|
+
dispatcher: agent,
|
|
26
27
|
headers: {
|
|
27
28
|
'Accept': 'application/json',
|
|
28
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
29
|
-
dispatcher: agent
|
|
29
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
30
30
|
},
|
|
31
31
|
body: body.toString()
|
|
32
32
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"mcp__sasmcp__sas-score-deva-score",
|
|
5
|
+
"mcp__sasmcp__sas-score-find-model",
|
|
6
|
+
"mcp__sasmcp__sas-score-model-info",
|
|
7
|
+
"Bash(cp \"C:/dev/github/7-skill-integration/.claude/skills/sas-read-and-score/SKILL.md\" \"C:/dev/github/7-skill-integration/skills/sas-read-and-score/SKILL.md\")",
|
|
8
|
+
"Bash(cp \"C:/dev/github/7-skill-integration/.claude/skills/sas-read-strategy/SKILL.md\" \"C:/dev/github/7-skill-integration/skills/sas-read-strategy/SKILL.md\")",
|
|
9
|
+
"Bash(cp \"C:/dev/github/7-skill-integration/.claude/skills/sas-score-workflow/SKILL.md\" \"C:/dev/github/7-skill-integration/skills/sas-score-workflow/SKILL.md\")",
|
|
10
|
+
"WebSearch"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/toolSet/devaScore.js
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
function devaScore(_appContext) {
|
|
8
|
-
|
|
9
|
-
let description = `
|
|
10
|
-
deva-score — compute a numeric score based on two input values.
|
|
11
|
-
|
|
12
|
-
USE when: calculate deva score, score these values, compute score for numbers
|
|
13
|
-
DO NOT USE for: model scoring (use model-score), statistical calculations, data lookup
|
|
14
|
-
|
|
15
|
-
PARAMETERS
|
|
16
|
-
- a: number (required) — first input value
|
|
17
|
-
- b: number (required) — second input value
|
|
18
|
-
|
|
19
|
-
FORMULA: (a + b) * 42
|
|
20
|
-
|
|
21
|
-
ROUTING RULES
|
|
22
|
-
- "calculate deva score for 5 and 10" → { a: 5, b: 10 }
|
|
23
|
-
- "score 1 and 2" → { a: 1, b: 2 }
|
|
24
|
-
- "deva score a=3, b=7" → { a: 3, b: 7 }
|
|
25
|
-
- Multiple numbers → chain calls left-to-right: call(first, second), then call(result, third)
|
|
26
|
-
|
|
27
|
-
EXAMPLES
|
|
28
|
-
- "Calculate deva score for 5 and 10" → { a: 5, b: 10 } returns 630
|
|
29
|
-
- "Score 1 and 2" → { a: 1, b: 2 } returns 126
|
|
30
|
-
- "Deva score 20 and 30" → { a: 20, b: 30 } returns 2100
|
|
31
|
-
|
|
32
|
-
NEGATIVE EXAMPLES (do not route here)
|
|
33
|
-
- "Score this customer with credit model" (use model-score)
|
|
34
|
-
- "Calculate the mean of these values" (use run-sas-program or sas-query)
|
|
35
|
-
- "Statistical analysis of numbers" (use sas-query)
|
|
36
|
-
|
|
37
|
-
RESPONSE
|
|
38
|
-
Returns { score: (a + b) * 42 }
|
|
39
|
-
`;
|
|
40
|
-
let spec = {
|
|
41
|
-
name: 'deva-score',
|
|
42
|
-
description: description,
|
|
43
|
-
inputSchema: z.object({
|
|
44
|
-
a: z.number(),
|
|
45
|
-
b: z.number()
|
|
46
|
-
}),
|
|
47
|
-
handler: async ({ a, b }) => {
|
|
48
|
-
console.error(a, b);
|
|
49
|
-
let r = { score: (a + b) * 42 };
|
|
50
|
-
console.error('deva score result', r);
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: 'text', text: 'deva score result: ' + JSON.stringify(r) }],
|
|
53
|
-
structuredContent: r
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return spec;
|
|
59
|
-
}
|
|
60
|
-
export default devaScore;
|
|
61
|
-
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
function devaScore(_appContext) {
|
|
8
|
+
|
|
9
|
+
let description = `
|
|
10
|
+
deva-score — compute a numeric score based on two input values.
|
|
11
|
+
|
|
12
|
+
USE when: calculate deva score, score these values, compute score for numbers
|
|
13
|
+
DO NOT USE for: model scoring (use model-score), statistical calculations, data lookup
|
|
14
|
+
|
|
15
|
+
PARAMETERS
|
|
16
|
+
- a: number (required) — first input value
|
|
17
|
+
- b: number (required) — second input value
|
|
18
|
+
|
|
19
|
+
FORMULA: (a + b) * 42
|
|
20
|
+
|
|
21
|
+
ROUTING RULES
|
|
22
|
+
- "calculate deva score for 5 and 10" → { a: 5, b: 10 }
|
|
23
|
+
- "score 1 and 2" → { a: 1, b: 2 }
|
|
24
|
+
- "deva score a=3, b=7" → { a: 3, b: 7 }
|
|
25
|
+
- Multiple numbers → chain calls left-to-right: call(first, second), then call(result, third)
|
|
26
|
+
|
|
27
|
+
EXAMPLES
|
|
28
|
+
- "Calculate deva score for 5 and 10" → { a: 5, b: 10 } returns 630
|
|
29
|
+
- "Score 1 and 2" → { a: 1, b: 2 } returns 126
|
|
30
|
+
- "Deva score 20 and 30" → { a: 20, b: 30 } returns 2100
|
|
31
|
+
|
|
32
|
+
NEGATIVE EXAMPLES (do not route here)
|
|
33
|
+
- "Score this customer with credit model" (use model-score)
|
|
34
|
+
- "Calculate the mean of these values" (use run-sas-program or sas-query)
|
|
35
|
+
- "Statistical analysis of numbers" (use sas-query)
|
|
36
|
+
|
|
37
|
+
RESPONSE
|
|
38
|
+
Returns { score: (a + b) * 42 }
|
|
39
|
+
`;
|
|
40
|
+
let spec = {
|
|
41
|
+
name: 'deva-score',
|
|
42
|
+
description: description,
|
|
43
|
+
inputSchema: z.object({
|
|
44
|
+
a: z.number(),
|
|
45
|
+
b: z.number(),
|
|
46
|
+
}),
|
|
47
|
+
handler: async ({ a, b }) => {
|
|
48
|
+
console.error(a, b);
|
|
49
|
+
let r = { score: (a + b) * 42 };
|
|
50
|
+
console.error('deva score result', r);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: 'deva score result: ' + JSON.stringify(r) }],
|
|
53
|
+
structuredContent: r
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return spec;
|
|
59
|
+
}
|
|
60
|
+
export default devaScore;
|
|
61
|
+
|
package/src/toolSet/findJob.js
CHANGED
|
@@ -42,7 +42,7 @@ Returns { jobs: [] } if not found; { jobs: [name, ...] } if found. Never halluci
|
|
|
42
42
|
name: 'find-job',
|
|
43
43
|
description: description,
|
|
44
44
|
inputSchema: z.object({
|
|
45
|
-
name: z.string()
|
|
45
|
+
name: z.string()
|
|
46
46
|
}),
|
|
47
47
|
handler: async (params) => {
|
|
48
48
|
let r = await _listJobs(params);
|
|
@@ -51,7 +51,7 @@ Returns { jobdefs: [] } if not found; { jobdefs: [name, ...] } if found. Never h
|
|
|
51
51
|
name: 'find-jobdef',
|
|
52
52
|
description: description,
|
|
53
53
|
inputSchema: z.object({
|
|
54
|
-
|
|
54
|
+
name: z.string()
|
|
55
55
|
}),
|
|
56
56
|
handler: async (params) => {
|
|
57
57
|
let r = await _listJobdefs(params);
|
|
@@ -61,4 +61,4 @@ Returns { jobdefs: [] } if not found; { jobdefs: [name, ...] } if found. Never h
|
|
|
61
61
|
return spec;
|
|
62
62
|
}
|
|
63
63
|
export default findJobdef;
|
|
64
|
-
|
|
64
|
+
|
|
@@ -1,67 +1,68 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import _listLibrary from '../toolHelpers/_listLibrary.js';
|
|
7
|
-
function findLibrary(_appContext) {
|
|
8
|
-
|
|
9
|
-
let description = `
|
|
10
|
-
find-library — locate a specific CAS or SAS library.
|
|
11
|
-
|
|
12
|
-
USE when: find library, find lib, does library exist, is library available, lookup library
|
|
13
|
-
DO NOT USE for: list libraries (use list-libraries), find table/job/jobdef/model (use respective tools), table structure (use table-info), create library (use run-sas-program)
|
|
14
|
-
|
|
15
|
-
PARAMETERS
|
|
16
|
-
- name: string (required) — library/caslib name; if multiple supplied, use first
|
|
17
|
-
- server: 'cas' | 'sas' (default: 'cas') — target environment
|
|
18
|
-
|
|
19
|
-
ROUTING RULES
|
|
20
|
-
- "find lib <name>" → { name: "<name>", server: "cas" }
|
|
21
|
-
- "find lib <name> in cas" → { name: "<name>", server: "cas" }
|
|
22
|
-
- "find library <name> in sas" → { name: "<name>", server: "sas" }
|
|
23
|
-
- "does library <name> exist" → { name: "<name>", server: "cas" }
|
|
24
|
-
- "find lib" with no name → ask "Which library name would you like to find?"
|
|
25
|
-
- "list libraries / list libs" → use list-libraries instead
|
|
26
|
-
- "tables in <lib>" → use list-tables instead
|
|
27
|
-
|
|
28
|
-
EXAMPLES
|
|
29
|
-
- "find lib Public" → { name: "Public", server: "cas" }
|
|
30
|
-
- "find library sasuser in sas" → { name: "sasuser", server: "sas" }
|
|
31
|
-
- "does library Formats exist" → { name: "Formats", server: "cas" }
|
|
32
|
-
|
|
33
|
-
NEGATIVE EXAMPLES (do not route here)
|
|
34
|
-
- "list libs" (use list-libraries)
|
|
35
|
-
- "show tables in Public" (use list-tables)
|
|
36
|
-
- "find table cars in sashelp" (use find-table)
|
|
37
|
-
- "find job cars_job" (use find-job)
|
|
38
|
-
|
|
39
|
-
ERRORS
|
|
40
|
-
Returns { libraries: [] } if not found; { libraries: [name, ...] } if found. Never hallucinate library names.
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
let spec = {
|
|
44
|
-
name: 'find-library',
|
|
45
|
-
description: description,
|
|
46
|
-
inputSchema: z.object({
|
|
47
|
-
name: z.string(),
|
|
48
|
-
server: z.string()
|
|
49
|
-
}),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
params.server
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import _listLibrary from '../toolHelpers/_listLibrary.js';
|
|
7
|
+
function findLibrary(_appContext) {
|
|
8
|
+
|
|
9
|
+
let description = `
|
|
10
|
+
find-library — locate a specific CAS or SAS library.
|
|
11
|
+
|
|
12
|
+
USE when: find library, find lib, does library exist, is library available, lookup library
|
|
13
|
+
DO NOT USE for: list libraries (use list-libraries), find table/job/jobdef/model (use respective tools), table structure (use table-info), create library (use run-sas-program)
|
|
14
|
+
|
|
15
|
+
PARAMETERS
|
|
16
|
+
- name: string (required) — library/caslib name; if multiple supplied, use first
|
|
17
|
+
- server: 'cas' | 'sas' (default: 'cas') — target environment
|
|
18
|
+
|
|
19
|
+
ROUTING RULES
|
|
20
|
+
- "find lib <name>" → { name: "<name>", server: "cas" }
|
|
21
|
+
- "find lib <name> in cas" → { name: "<name>", server: "cas" }
|
|
22
|
+
- "find library <name> in sas" → { name: "<name>", server: "sas" }
|
|
23
|
+
- "does library <name> exist" → { name: "<name>", server: "cas" }
|
|
24
|
+
- "find lib" with no name → ask "Which library name would you like to find?"
|
|
25
|
+
- "list libraries / list libs" → use list-libraries instead
|
|
26
|
+
- "tables in <lib>" → use list-tables instead
|
|
27
|
+
|
|
28
|
+
EXAMPLES
|
|
29
|
+
- "find lib Public" → { name: "Public", server: "cas" }
|
|
30
|
+
- "find library sasuser in sas" → { name: "sasuser", server: "sas" }
|
|
31
|
+
- "does library Formats exist" → { name: "Formats", server: "cas" }
|
|
32
|
+
|
|
33
|
+
NEGATIVE EXAMPLES (do not route here)
|
|
34
|
+
- "list libs" (use list-libraries)
|
|
35
|
+
- "show tables in Public" (use list-tables)
|
|
36
|
+
- "find table cars in sashelp" (use find-table)
|
|
37
|
+
- "find job cars_job" (use find-job)
|
|
38
|
+
|
|
39
|
+
ERRORS
|
|
40
|
+
Returns { libraries: [] } if not found; { libraries: [name, ...] } if found. Never hallucinate library names.
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
let spec = {
|
|
44
|
+
name: 'find-library',
|
|
45
|
+
description: description,
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
name: z.string(),
|
|
48
|
+
server: z.string().optional()
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
handler: async (params) => {
|
|
52
|
+
// normalize server to lowercase & default
|
|
53
|
+
if (!params.server) params.server = 'cas';
|
|
54
|
+
params.server = params.server.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// If multiple names passed (comma or space separated), take the first token (defensive)
|
|
57
|
+
if (params.name && /[,\s]+/.test(params.name.trim())) {
|
|
58
|
+
params.name = params.name.split(/[,\s]+/).filter(Boolean)[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let r = await _listLibrary(params);
|
|
62
|
+
return r;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return spec;
|
|
66
|
+
}
|
|
67
|
+
export default findLibrary;
|
|
68
|
+
|