@sassoftware/sas-score-mcp-serverjs 0.4.1-7 → 1.0.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/.skills/agents/sas-viya-scoring-expert.md +58 -0
- package/.skills/copilot-instructions.md +147 -0
- package/.skills/skills/sas-find-library-smart/SKILL.md +154 -0
- package/.skills/skills/sas-list-tables-smart/SKILL.md +127 -0
- package/.skills/skills/sas-read-and-score/SKILL.md +111 -0
- package/{skills → .skills/skills}/sas-read-strategy/SKILL.md +43 -30
- package/.skills/skills/sas-request-classifier/SKILL.md +69 -0
- package/{skills → .skills/skills}/sas-score-workflow/SKILL.md +49 -35
- package/cli.js +222 -140
- package/package.json +5 -4
- package/scripts/docs/SCORE_SKILL_REFERENCE.md +142 -0
- package/scripts/docs/TOOL_DESCRIPTION_TEMPLATE.md +157 -0
- package/scripts/docs/TOOL_UPDATES_SUMMARY.md +208 -0
- package/scripts/docs/mcp-localhost-config-guide.md +184 -0
- package/scripts/docs/oauth-http-transport.md +96 -0
- package/scripts/docs/sas-mcp-tools-reference.md +600 -0
- package/scripts/getViyaca.sh +1 -0
- package/scripts/optimize_final.py +140 -0
- package/scripts/optimize_tools.py +99 -0
- package/scripts/setup-skills.js +34 -0
- package/scripts/update_descriptions.py +46 -0
- package/scripts/viyatls.sh +3 -0
- package/src/authpkce.js +219 -220
- package/src/createMcpServer.js +10 -5
- package/src/expressMcpServer.js +54 -186
- package/src/oauthHandlers/authorize.js +46 -0
- package/src/oauthHandlers/baseUrl.js +8 -0
- package/src/oauthHandlers/callback.js +96 -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/setupSkills.js +37 -0
- package/src/toolHelpers/_listLibrary.js +0 -1
- package/src/toolHelpers/getLogonPayload.js +5 -1
- package/src/toolHelpers/refreshToken.js +3 -2
- package/src/toolHelpers/refreshTokenOauth.js +3 -3
- package/src/toolSet/.claude/settings.local.json +2 -1
- package/src/toolSet/findModel.js +1 -1
- package/src/toolSet/findTable.js +3 -3
- package/src/toolSet/modelScore.js +2 -2
- package/src/toolSet/runJob.js +81 -81
- package/src/toolSet/runJobdef.js +82 -82
- package/skills/mcp-tool-description-optimizer/SKILL.md +0 -129
- package/skills/mcp-tool-description-optimizer/references/examples.md +0 -123
- package/skills/sas-read-and-score/SKILL.md +0 -91
package/src/authpkce.js
CHANGED
|
@@ -1,220 +1,219 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SAS Viya PKCE Authorization Flow
|
|
3
|
-
*
|
|
4
|
-
* Uses the Authorization Code flow with PKCE (RFC 7636).
|
|
5
|
-
* Starts a local HTTP server to capture the redirect callback.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* node sas-viya-pkce-auth.js
|
|
9
|
-
*
|
|
10
|
-
* Prerequisites:
|
|
11
|
-
* - A SAS Viya client registered with:
|
|
12
|
-
* grant_types: authorization_code
|
|
13
|
-
* redirect_uri: http://localhost:3000/callback
|
|
14
|
-
* PKCE enabled (no client_secret required)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import http from "http";
|
|
18
|
-
import https from "https";
|
|
19
|
-
import crypto from "crypto";
|
|
20
|
-
import url from "url";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"Content-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
res.on("
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
req.
|
|
110
|
-
req.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
res.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
res.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.error(
|
|
168
|
-
console.error();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.error("
|
|
204
|
-
console.error("
|
|
205
|
-
console.error("
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SAS Viya PKCE Authorization Flow
|
|
3
|
+
*
|
|
4
|
+
* Uses the Authorization Code flow with PKCE (RFC 7636).
|
|
5
|
+
* Starts a local HTTP server to capture the redirect callback.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node sas-viya-pkce-auth.js
|
|
9
|
+
*
|
|
10
|
+
* Prerequisites:
|
|
11
|
+
* - A SAS Viya client registered with:
|
|
12
|
+
* grant_types: authorization_code
|
|
13
|
+
* redirect_uri: http://localhost:3000/callback
|
|
14
|
+
* PKCE enabled (no client_secret required)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import http from "http";
|
|
18
|
+
import https from "https";
|
|
19
|
+
import crypto from "crypto";
|
|
20
|
+
import url from "url";
|
|
21
|
+
|
|
22
|
+
let VIYA_SERVER=process.env.VIYA_SERVER;
|
|
23
|
+
let PORT=8080;
|
|
24
|
+
let CLIENTID='pkcemcp';
|
|
25
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const CONFIG = {
|
|
28
|
+
authBaseUrl: `${VIYA_SERVER}/oauth/SASLogon`,
|
|
29
|
+
clientId: CLIENTID, // <-- replace with your client ID
|
|
30
|
+
redirectUri: `http://localhost:${PORT}/callback`,
|
|
31
|
+
scopes: "openid profile",
|
|
32
|
+
localPort: PORT,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
main().catch((err) => {
|
|
36
|
+
console.error("Unexpected error:", err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
// Generate a cryptographically random code verifier (43–128 chars, base64url)
|
|
43
|
+
function generateCodeVerifier() {
|
|
44
|
+
return crypto.randomBytes(64).toString("base64url");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Derive the code challenge: BASE64URL(SHA-256(verifier))
|
|
48
|
+
function generateCodeChallenge(verifier) {
|
|
49
|
+
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build the SASLogon authorization URL
|
|
53
|
+
function buildAuthUrl(codeChallenge, state) {
|
|
54
|
+
const params = new URLSearchParams({
|
|
55
|
+
response_type: "code",
|
|
56
|
+
client_id: CONFIG.clientId,
|
|
57
|
+
redirect_uri: CONFIG.redirectUri,
|
|
58
|
+
scope: CONFIG.scopes,
|
|
59
|
+
state,
|
|
60
|
+
code_challenge: codeChallenge,
|
|
61
|
+
code_challenge_method: "S256",
|
|
62
|
+
});
|
|
63
|
+
return `${CONFIG.authBaseUrl}/authorize?${params}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Exchange the authorization code for tokens
|
|
67
|
+
function exchangeCodeForTokens(code, codeVerifier) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const body = new URLSearchParams({
|
|
70
|
+
grant_type: "authorization_code",
|
|
71
|
+
client_id: CONFIG.clientId,
|
|
72
|
+
redirect_uri: CONFIG.redirectUri,
|
|
73
|
+
code,
|
|
74
|
+
code_verifier: codeVerifier,
|
|
75
|
+
}).toString();
|
|
76
|
+
|
|
77
|
+
const tokenUrl = new URL(`${CONFIG.authBaseUrl}/token`);
|
|
78
|
+
|
|
79
|
+
const options = {
|
|
80
|
+
hostname: tokenUrl.hostname,
|
|
81
|
+
port: tokenUrl.port || 443,
|
|
82
|
+
path: tokenUrl.pathname,
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
86
|
+
"Content-Length": Buffer.byteLength(body),
|
|
87
|
+
},
|
|
88
|
+
// Remove the line below if your Viya instance has a valid TLS cert
|
|
89
|
+
rejectUnauthorized: false,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const req = https.request(options, (res) => {
|
|
93
|
+
let data = "";
|
|
94
|
+
res.on("data", (chunk) => (data += chunk));
|
|
95
|
+
res.on("end", () => {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(data);
|
|
98
|
+
if (res.statusCode >= 400) {
|
|
99
|
+
reject(new Error(`Token error (${res.statusCode}): ${data}`));
|
|
100
|
+
} else {
|
|
101
|
+
resolve(parsed);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
reject(new Error(`Failed to parse token response: ${data}`));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
req.on("error", reject);
|
|
110
|
+
req.write(body);
|
|
111
|
+
req.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Wait for the browser redirect and extract code + state
|
|
116
|
+
function waitForCallback(expectedState) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const server = http.createServer((req, res) => {
|
|
119
|
+
const parsed = url.parse(req.url, true);
|
|
120
|
+
|
|
121
|
+
if (parsed.pathname !== "/callback") {
|
|
122
|
+
res.writeHead(404);
|
|
123
|
+
res.end("Not found");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { code, state, error, error_description } = parsed.query;
|
|
128
|
+
|
|
129
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
130
|
+
res.end(`
|
|
131
|
+
<html><body>
|
|
132
|
+
<h2>${error ? "Authorization failed" : "Authorization successful!"}</h2>
|
|
133
|
+
<p>You may close this tab.</p>
|
|
134
|
+
</body></html>
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
server.close();
|
|
138
|
+
|
|
139
|
+
if (error) {
|
|
140
|
+
reject(new Error(`OAuth error: ${error} — ${error_description}`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (state !== expectedState) {
|
|
144
|
+
reject(new Error("State mismatch — possible CSRF attack"));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
resolve(code);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
server.listen(CONFIG.localPort, () => {
|
|
152
|
+
console.error(`Listening on http://localhost:${CONFIG.localPort}/callback`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
server.on("error", reject);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Main flow
|
|
160
|
+
async function main() {
|
|
161
|
+
const codeVerifier = generateCodeVerifier();
|
|
162
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
163
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
164
|
+
|
|
165
|
+
const authUrl = buildAuthUrl(codeChallenge, state);
|
|
166
|
+
|
|
167
|
+
console.error("\nOpen this URL in your browser to log in:\n");
|
|
168
|
+
console.error(authUrl);
|
|
169
|
+
console.error();
|
|
170
|
+
|
|
171
|
+
// Try to auto-open in the default browser (best-effort)
|
|
172
|
+
try {
|
|
173
|
+
const { exec } = require("child_process");
|
|
174
|
+
const cmd =
|
|
175
|
+
process.platform === "win32"
|
|
176
|
+
? `start "" "${authUrl}"`
|
|
177
|
+
: process.platform === "darwin"
|
|
178
|
+
? `open "${authUrl}"`
|
|
179
|
+
: `xdg-open "${authUrl}"`;
|
|
180
|
+
exec(cmd);
|
|
181
|
+
} catch {
|
|
182
|
+
// ignore — user can open manually
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let code;
|
|
186
|
+
try {
|
|
187
|
+
code = await waitForCallback(state);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error("Callback error:", err.message);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.error("Authorization code received. Exchanging for tokens...");
|
|
194
|
+
|
|
195
|
+
let tokens;
|
|
196
|
+
try {
|
|
197
|
+
tokens = await exchangeCodeForTokens(code, codeVerifier);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error("Token exchange error:", err.message);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.error("\nTokens received:");
|
|
204
|
+
console.error(" access_token :", tokens.access_token?.slice(0, 40) + "...");
|
|
205
|
+
console.error(" token_type :", tokens.token_type);
|
|
206
|
+
console.error(" expires_in :", tokens.expires_in, "seconds");
|
|
207
|
+
if (tokens.refresh_token) {
|
|
208
|
+
console.error(" refresh_token:", tokens.refresh_token.slice(0, 20) + "...");
|
|
209
|
+
}
|
|
210
|
+
if (tokens.id_token) {
|
|
211
|
+
console.error(" id_token :", tokens.id_token.slice(0, 40) + "...");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// tokens.access_token is ready to use as a Bearer token for Viya REST APIs
|
|
215
|
+
return tokens.access_token
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
package/src/createMcpServer.js
CHANGED
|
@@ -15,7 +15,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
15
15
|
import makeTools from "./toolSet/makeTools.js";
|
|
16
16
|
import getLogonPayload from "./toolHelpers/getLogonPayload.js";
|
|
17
17
|
|
|
18
|
+
function getServerBaseUrl(appContext) {
|
|
19
|
+
const protocol = appContext.HTTPS === "TRUE" ? "https" : "http";
|
|
20
|
+
const host = appContext.contexts?.APPHOST || "localhost";
|
|
21
|
+
return `${protocol}://${host}:${appContext.PORT}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
async function createMcpServer(cache, _appContext) {
|
|
25
|
+
const serverBaseUrl = getServerBaseUrl(_appContext);
|
|
19
26
|
|
|
20
27
|
let mcpServer = new McpServer(
|
|
21
28
|
{
|
|
@@ -39,13 +46,12 @@ async function createMcpServer(cache, _appContext) {
|
|
|
39
46
|
let currentId = cache.get('currentId');
|
|
40
47
|
let _appContext = cache.get(currentId);
|
|
41
48
|
let params;
|
|
42
|
-
// get Viya token
|
|
43
49
|
|
|
44
50
|
let errorStatus = cache.get('errorStatus');
|
|
45
51
|
if (errorStatus) {
|
|
46
52
|
return { isError: true, content: [{ type: 'text', text: errorStatus }] }
|
|
47
53
|
};
|
|
48
|
-
if (_appContext.AUTHFLOW === 'code' && _appContext.contexts.oauthInfo == null) {
|
|
54
|
+
if (/*_appContext.AUTHFLOW === 'code'*/ _appContext.useHapi === true && _appContext.contexts.oauthInfo == null) {
|
|
49
55
|
return { isError: true, content: [{ type: 'text', text: 'Please visit https://localhost:8080/mcpserver to connect to Viya. Then try again.' }] }
|
|
50
56
|
}
|
|
51
57
|
console.error("Getting logon payload for tool with session ID:", currentId);
|
|
@@ -64,6 +70,7 @@ async function createMcpServer(cache, _appContext) {
|
|
|
64
70
|
|
|
65
71
|
// call the actual tool handler
|
|
66
72
|
debugger;
|
|
73
|
+
console.error("Calling tool handler with enhanced params");
|
|
67
74
|
let r = await builtin(params);
|
|
68
75
|
return r;
|
|
69
76
|
}
|
|
@@ -78,10 +85,8 @@ async function createMcpServer(cache, _appContext) {
|
|
|
78
85
|
description: tool.description,
|
|
79
86
|
inputSchema: tool.inputSchema
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
console.error(`\n[Note] Registering tool ${i + 1} : ${toolName}`);
|
|
83
88
|
let toolHandler = wrapf(cache, tool.handler);
|
|
84
|
-
|
|
89
|
+
// console.error(`[Note] Registering tool ${toolName} with config: ${JSON.stringify(config)}`);
|
|
85
90
|
let r = mcpServer.registerTool(toolName, config, toolHandler);
|
|
86
91
|
toolNames.push(toolName);
|
|
87
92
|
});
|