@sassoftware/sas-score-mcp-serverjs 0.4.1 → 1.0.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.skills/agents/sas-viya-scoring-expert.md +58 -0
  2. package/.skills/copilot-instructions.md +155 -0
  3. package/.skills/skills/sas-find-library-smart/SKILL.md +154 -0
  4. package/.skills/skills/sas-list-tables-smart/SKILL.md +127 -0
  5. package/.skills/skills/sas-read-and-score/SKILL.md +111 -0
  6. package/.skills/skills/sas-read-strategy/SKILL.md +156 -0
  7. package/.skills/skills/sas-request-classifier/SKILL.md +69 -0
  8. package/.skills/skills/sas-score-workflow/SKILL.md +314 -0
  9. package/cli.js +311 -70
  10. package/package.json +7 -7
  11. package/scripts/docs/SCORE_SKILL_REFERENCE.md +142 -0
  12. package/scripts/docs/TOOL_DESCRIPTION_TEMPLATE.md +157 -0
  13. package/scripts/docs/TOOL_UPDATES_SUMMARY.md +208 -0
  14. package/scripts/docs/mcp-localhost-config-guide.md +184 -0
  15. package/scripts/docs/oauth-http-transport.md +96 -0
  16. package/scripts/docs/sas-mcp-tools-reference.md +600 -0
  17. package/scripts/getViyaca.sh +1 -0
  18. package/scripts/optimize_final.py +140 -0
  19. package/scripts/optimize_tools.py +99 -0
  20. package/scripts/setup-skills.js +34 -0
  21. package/scripts/update_descriptions.py +46 -0
  22. package/scripts/viyatls.sh +3 -0
  23. package/src/authpkce.js +219 -0
  24. package/src/createMcpServer.js +16 -5
  25. package/src/expressMcpServer.js +350 -308
  26. package/src/handleGetDelete.js +6 -3
  27. package/src/hapiMcpServer.js +10 -18
  28. package/src/oauthHandlers/authorize.js +46 -0
  29. package/src/oauthHandlers/baseUrl.js +8 -0
  30. package/src/oauthHandlers/callback.js +96 -0
  31. package/src/oauthHandlers/getMetadata.js +27 -0
  32. package/src/oauthHandlers/index.js +7 -0
  33. package/src/oauthHandlers/token.js +37 -0
  34. package/src/processHeaders.js +88 -0
  35. package/src/setupSkills.js +46 -0
  36. package/src/toolHelpers/_jobSubmit.js +2 -0
  37. package/src/toolHelpers/_listLibrary.js +55 -39
  38. package/src/toolHelpers/getLogonPayload.js +7 -1
  39. package/src/toolHelpers/readCerts.js +4 -4
  40. package/src/toolHelpers/refreshToken.js +3 -2
  41. package/src/toolHelpers/refreshTokenOauth.js +3 -3
  42. package/src/toolSet/.claude/settings.local.json +13 -0
  43. package/src/toolSet/devaScore.js +61 -69
  44. package/src/toolSet/findJob.js +38 -71
  45. package/src/toolSet/findJobdef.js +28 -59
  46. package/src/toolSet/findLibrary.js +68 -100
  47. package/src/toolSet/findModel.js +35 -58
  48. package/src/toolSet/findTable.js +31 -60
  49. package/src/toolSet/getEnv.js +30 -45
  50. package/src/toolSet/listJobdefs.js +61 -96
  51. package/src/toolSet/listJobs.js +61 -110
  52. package/src/toolSet/listLibraries.js +78 -90
  53. package/src/toolSet/listModels.js +56 -83
  54. package/src/toolSet/listTables.js +66 -95
  55. package/src/toolSet/makeTools.js +1 -0
  56. package/src/toolSet/modelInfo.js +22 -54
  57. package/src/toolSet/modelScore.js +35 -77
  58. package/src/toolSet/readTable.js +63 -104
  59. package/src/toolSet/runCasProgram.js +32 -52
  60. package/src/toolSet/runJob.js +24 -24
  61. package/src/toolSet/runJobdef.js +26 -29
  62. package/src/toolSet/runMacro.js +82 -82
  63. package/src/toolSet/runProgram.js +32 -84
  64. package/src/toolSet/sasQuery.js +77 -126
  65. package/src/toolSet/sasQueryTemplate.js +4 -5
  66. package/src/toolSet/sasQueryTemplate2.js +4 -5
  67. package/src/toolSet/scrInfo.js +4 -7
  68. package/src/toolSet/scrScore.js +69 -70
  69. package/src/toolSet/searchAssets.js +5 -6
  70. package/src/toolSet/setContext.js +65 -92
  71. package/src/toolSet/superstat.js +61 -60
  72. package/src/toolSet/tableInfo.js +58 -102
@@ -5,22 +5,25 @@
5
5
 
6
6
  async function handleGetDelete(mcpServer, cache, req, h) {
7
7
  const sessionId = req.headers["mcp-session-id"];
8
- console.error(`Handling ${req.method} for session ID:`, sessionId);
8
+ console.error("=======================================================");
9
+ console.error(`[Note] Handling ${req.method} for session ID:`, sessionId);
9
10
  let transports = cache.get("transports");
10
11
  let transport = transports[sessionId];
11
12
  if (!sessionId || transport == null) {
12
13
  console.error('[Note] Looks like a fresh start - no session id or transport found');
13
- h.abandon;
14
+ return h.abandon;
14
15
  }
15
16
 
16
17
  if (req.method === "GET") {
17
18
  // You can customize the response as needed
19
+ console.error("[Note] Payload:", req.payload);
20
+ console.error("======================================================");
18
21
  await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
19
22
  return h.abandon;
20
23
  }
21
24
 
22
25
  if (req.method === "DELETE") {
23
- console.error("Deleting transport and cache for session ID:", sessionId);
26
+ console.error("[Note] Deleting transport and cache for session ID:", sessionId);
24
27
  delete transports[sessionId];
25
28
  cache.del(sessionId);
26
29
  return h.response(`[Info] In DELETE: Session ID ${sessionId} deleted`).code(201);
@@ -128,38 +128,30 @@ async function hapiMcpServer(mcpServer, cache, baseAppEnvContext) {
128
128
  },
129
129
  {
130
130
  method: ["GET"],
131
- path: "/startz",
131
+ path: "/ready",
132
132
  options: {
133
133
  handler: async (req, h) => {
134
- let status = { status: (process.env.HEALTH === 'true') ? 'ready' : 'starting' };
135
- console.error("startz check requested, returning:", status);
136
- if (process.env.HEALTH !== 'true') {
137
- return h.response(status).code(200).type('application/json');
138
- } else {
139
- return h.response(status).code(503).type('application/json');
140
- }
134
+ let status = {status: 1};
135
+ console.error("Ready check requested, returning:", status);
136
+ return h.response(status).code(200).type('application/json');
141
137
  },
142
138
  auth: false,
143
- description: "Help",
139
+ description: "probe readiness of the server",
144
140
  notes: "Help",
145
141
  tags: ["mcp"],
146
142
  }
147
143
  },
148
144
  {
149
145
  method: ["GET"],
150
- path: "/readyz",
146
+ path: "/StartUp",
151
147
  options: {
152
148
  handler: async (req, h) => {
153
- let status = { status: (process.env.HEALTH === 'true') ? 'ready' : 'starting' };
154
- console.error("readyz check requested, returning:", status);
155
- if (process.env.HEALTH !== 'true') {
156
- return h.response(status).code(200).type('application/json');
157
- } else {
158
- return h.response(status).code(503).type('application/json');
159
- }
149
+ let status = { status: 1 };
150
+ console.error("Startup check requested, returning:", status);
151
+ return h.response(status).code(200).type('application/json');
160
152
  },
161
153
  auth: false,
162
- description: "Help",
154
+ description: "probe startup of the server",
163
155
  notes: "Help",
164
156
  tags: ["mcp"],
165
157
  }
@@ -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,8 @@
1
+ function baseUrl(appContext) {
2
+ const protocol = appContext.HTTPS === "TRUE" ? "https" : "http";
3
+ //const host = "localhost";
4
+ const host = appContext.VIYA_SERVER;
5
+ return host;
6
+ return `${protocol}://${host}:${appContext.PORT}`;
7
+ }
8
+ export default baseUrl;
@@ -0,0 +1,96 @@
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
+
35
+ let connectOpts = {
36
+ rejectUnauthorized: false // or false, if you really want to bypass checks
37
+ }
38
+ if (appContext.contexts.viyaCert != null && appContext.contexts.viyaCert.ca != null) {
39
+ connectOpts.ca = appContext.contexts.viyaCert.ca;
40
+ }
41
+ const agent = new Agent({ connect: connectOpts });
42
+ console.error("[Note] Initiating token exchange with SAS Viya");
43
+ let clientID= appContext.CLIENTID;
44
+ let h = buildBasicAuthFromClientId(clientID);
45
+ // console.error("[Note] Authorization header for token request:", h);
46
+ const response = await undiciFetch(`${appContext.VIYA_SERVER}/SASLogon/oauth/token`, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/x-www-form-urlencoded",
50
+ "Accept": "application/json"
51
+ },
52
+ body: body,
53
+ dispatcher: agent,
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const errText = await response.text();
58
+ console.error("[Error] SAS Viya token exchange failed:", errText);
59
+ return res.status(502).send("Token exchange with SAS Viya failed");
60
+ }
61
+
62
+ const tokens = await response.json();
63
+ // console.error("[Note] Received tokens from SAS Viya:", tokens);
64
+ const ourCode = randomUUID();
65
+ codeStore.set(ourCode, {
66
+ access_token: tokens.access_token,
67
+ refresh_token: tokens.refresh_token,
68
+ expires_in: tokens.expires_in,
69
+ });
70
+
71
+ const redirectParams = new URLSearchParams({ code: ourCode , state: pending.clientState});
72
+ //pending.clientState is the original state from the MCP client, which we should pass back to it for correlation
73
+
74
+ // pending.clientRedirectUri is the original redirect URI from the MCP client,
75
+ // which was part of the payload from the client to /oauth/authorize
76
+ // we trust since it was associated with the valid PKCE state
77
+ console.error("[Note] OAuth callback complete, redirecting to MCP client");
78
+ console.log(pending.clientRedirectUri.toString())
79
+ return res.redirect(`${pending.clientRedirectUri}?${redirectParams}`);
80
+ } catch (err) {
81
+ console.error("[Error] OAuth callback handler error:", err);
82
+ return res.status(500).send("Internal server error during token exchange");
83
+ }
84
+
85
+ function buildBasicAuthFromClientId(clientId) {
86
+ const raw = `${clientId}:`; // empty client_secret
87
+ const encoded =
88
+ typeof btoa === "function"
89
+ ? btoa(raw)
90
+ : Buffer.from(raw).toString("base64");
91
+
92
+ return `Basic ${encoded}`;
93
+ }
94
+
95
+ }
96
+ 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,7 @@
1
+ import authorize from "./authorize.js";
2
+ import callback from "./callback.js";
3
+ import token from "./token.js";
4
+ import getMetadata from "./getMetadata.js";
5
+ import baseUrl from "./baseUrl.js";
6
+
7
+ export { authorize, callback, token, getMetadata, baseUrl };
@@ -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;
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ function setupSkills(clientName) {
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ // Paths
10
+ const source = path.join(__dirname, `../.skills`)
11
+ let client = (clientName == null) ? 'github' : `${clientName.toLowerCase()}`;
12
+ let destination;
13
+ // still hoping to put the definitions in the cache
14
+ // if it starts with ., then copy agent to where the command is run, otherwise copy to home directory
15
+ if (client.startsWith('.')) {
16
+ destination = path.join(process.cwd(), client);
17
+ } else {
18
+ client = '.' + client;
19
+ destination = path.join(os.homedir(), client);
20
+ }
21
+
22
+ console.error(`📁 Copying ${source} to ${destination}...`);
23
+ function copyFolderSync(from, to) {
24
+ if (!fs.existsSync(from)) return;
25
+ if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });
26
+ console.error(`📁 Copying folder: ${from} to ${to}`);
27
+ fs.readdirSync(from).forEach(element => {
28
+ console.error(`📁 Processing: ${element}`);
29
+ const fromPath = path.join(from, element);
30
+ const toPath = path.join(to, element);
31
+ if (fs.lstatSync(fromPath).isFile()) {
32
+ fs.copyFileSync(fromPath, toPath);
33
+ } else if (fs.lstatSync(fromPath).isDirectory()) {
34
+ copyFolderSync(fromPath, toPath);
35
+ }
36
+ });
37
+ }
38
+
39
+ try {
40
+ copyFolderSync(source, destination);
41
+ console.error(`✅ Success: ${destination} folder is now in your project root.`);
42
+ } catch (err) {
43
+ console.error('❌ Error copying files:', err.message);
44
+ }
45
+ }
46
+ export default setupSkills;
@@ -12,6 +12,8 @@ async function _jobSubmit(params) {
12
12
  // setup
13
13
  if (name === 'program') {
14
14
  let src = `
15
+ cas mycas;
16
+ caslib _all_ assign;
15
17
  proc sql;
16
18
  create table work.query_results as
17
19
  ${scenario.sql};
@@ -6,53 +6,69 @@
6
6
  import restafedit from '@sassoftware/restafedit';
7
7
  import deleteSession from './deleteSession.js';
8
8
 
9
- async function _listLibrary(params ){
9
+ async function _listLibrary(params) {
10
10
 
11
11
  let { server, limit, start, name, _appContext } = params;
12
-
13
- let config = {
14
- casServerName: _appContext.cas,
15
- computeContext: _appContext.sas,
16
- source: (server === 'sas') ? 'compute' : server,
17
- table: null
18
- };
19
- let appControl;
20
- try {
21
- // setup request control
22
- appControl = await restafedit.setup(
23
- _appContext.logonPayload,
24
- config
25
- ,null,{},'user',{}, {}, _appContext.storeConfig
26
- );
27
-
28
- // query parameters
29
- let payload = {
30
- qs: {
31
- limit: (limit != null) ? limit : 10,
32
- start: start - 1
33
- }
12
+
13
+ const _ilistLibrary = async (params) => {
14
+ let { server, limit, start, name, _appContext } = params;
15
+ let config = {
16
+ casServerName: _appContext.cas,
17
+ computeContext: _appContext.sas,
18
+ source: (server === 'sas') ? 'compute' : server,
19
+ table: null
34
20
  };
21
+ let appControl;
22
+ try {
23
+ // setup request control
24
+ appControl = await restafedit.setup(
25
+ _appContext.logonPayload,
26
+ config
27
+ , null, {}, 'user', {}, {}, _appContext.storeConfig
28
+ );
29
+
30
+ // query parameters
31
+ let payload = {
32
+ qs: {
33
+ limit: (limit != null) ? limit : 10,
34
+ start: start - 1
35
+ }
36
+ };
35
37
 
36
- if (name != null) {
37
- payload.qs = {
38
- filter: `eq(name, '${name}')`
38
+ if (name != null) {
39
+ payload.qs = {
40
+ filter: `eq(name, '${name}')`
41
+ }
39
42
  }
40
- }
41
-
42
- let items = await restafedit.getLibraryList(appControl, payload);
43
- let response = {libraries: items};
44
- await deleteSession(appControl);
45
-
46
- return { content: [{ type: 'text', text: JSON.stringify(response) }],
47
- structuredContent: response
48
- };
49
- } catch (err) {
50
- console.error(JSON.stringify(err));
51
- if (appControl != null) {
43
+
44
+ let items = await restafedit.getLibraryList(appControl, payload);
45
+ let response = { libraries: items };
52
46
  await deleteSession(appControl);
47
+
48
+ return {
49
+ content: [{ type: 'text', text: JSON.stringify(response) }],
50
+ structuredContent: response
51
+ };
52
+ } catch (err) {
53
+ console.error(JSON.stringify(err));
54
+ if (appControl != null) {
55
+ await deleteSession(appControl);
56
+ }
57
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
53
58
  }
54
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
55
59
  }
60
+
61
+ let source = (server === 'all') ? ['sas', 'cas'] : [server];
62
+ let response = {};
63
+
64
+ for (let i = 0; i < source.length; i++) {
65
+ let liblist = await _ilistLibrary({ server: source[i], limit, start, name, _appContext });
66
+ response[source[i]] = liblist.structuredContent;
67
+ }
68
+ return {
69
+ content: [{ type: 'text', text: JSON.stringify(response) }],
70
+ structuredContent: response
71
+ };
56
72
  }
57
73
 
58
74
  export default _listLibrary;
@@ -16,10 +16,11 @@ async function igetLogonPayload(_appContext) {
16
16
  console.error('[Info] Getting logon payload...',_appContext.AUTHFLOW);
17
17
  // Use cached logonPayload if available
18
18
  // This will cause timeouts if the token expires
19
- if (_appContext.contexts.logonPayload != null && _appContext.tokenRefresh !== true) {
19
+ /*if (_appContext.contexts.logonPayload != null && _appContext.tokenRefresh !== true) {
20
20
  console.error("[Note] Using cached logonPayload information");
21
21
  return _appContext.contexts.logonPayload;
22
22
  }
23
+ */
23
24
 
24
25
  if (_appContext.AUTHFLOW === 'code') {
25
26
  let oauthInfo = _appContext.contexts.oauthInfo;
@@ -120,4 +121,9 @@ async function igetLogonPayload(_appContext) {
120
121
  return null;
121
122
  }
122
123
  }
124
+
125
+ function isExpired(expiresAt, skewSeconds = 60) {
126
+ return Date.now() >= (expiresAt - skewSeconds * 1000);
127
+ }
128
+
123
129
  export default getLogonPayload;
@@ -9,23 +9,23 @@ function getCerts(tlsdir) {
9
9
  return null;
10
10
  }
11
11
 
12
- console.log(`[Note] Reading certs from directory: ` + tlsdir);
12
+ console.error(`[Note] Reading certs from directory: ` + tlsdir);
13
13
  if (fs.existsSync(tlsdir) === false) {
14
14
  console.error("[Warning] Specified cert dir does not exist: " + tlsdir);
15
15
  return null;
16
16
  }
17
17
 
18
18
  let listOfFiles = fs.readdirSync(tlsdir);
19
- console.log("[Note] TLS/SSL files found: " + listOfFiles);
19
+ console.error("[Note] TLS/SSL files found: " + listOfFiles);
20
20
  let options = {};
21
21
  for(let i=0; i < listOfFiles.length; i++) {
22
22
  let fname = listOfFiles[i];
23
23
  let name = tlsdir + '/' + listOfFiles[i];
24
24
  let key = fname.split('.')[0];
25
- console.log('Reading TLS file: ' + name + ' as key: ' + key);
25
+ console.error('Reading TLS file: ' + name + ' as key: ' + key);
26
26
  options[key] = fs.readFileSync(name, { encoding: 'utf8' });
27
27
  }
28
- console.log('cert files', Object.keys(options));
28
+ console.error('cert files', Object.keys(options));
29
29
 
30
30
  return options;
31
31
 
@@ -9,10 +9,11 @@ async function refreshToken(_appContext, params) {
9
9
  let url = `${host}/SASLogon/oauth/token`;
10
10
 
11
11
  let aconnect = {
12
- ca: _appContext.contexts.viyaCert.ca, // trust this CA
13
12
  rejectUnauthorized: false // or false, if you really want to bypass checks
14
13
  }
15
-
14
+ if (_appContext.contexts.viyaCert != null && _appContext.contexts.viyaCert.ca != null) {
15
+ aconnect.ca = _appContext.contexts.viyaCert.ca;
16
+ }
16
17
  const agent = new Agent(aconnect);
17
18
 
18
19
  console.error('[Info] Refreshing token...', token);
@@ -6,7 +6,7 @@
6
6
  //import getOpts from './getOpts.js';
7
7
  async function refreshTokenOauth(_appContext, oauthInfo ){
8
8
 
9
- const url = `${process.env.VIYA_SERVER}/SASLogon/oauth/token`;
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
+ }