@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
package/cli.js
CHANGED
|
@@ -21,10 +21,12 @@ import { randomUUID } from 'node:crypto';
|
|
|
21
21
|
import readCerts from './src/toolHelpers/readCerts.js';
|
|
22
22
|
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
|
-
import { dirname } from 'path';
|
|
24
|
+
import { dirname, join } from 'path';
|
|
25
|
+
import os from 'os';
|
|
25
26
|
import { parseArgs } from "node:util";
|
|
26
27
|
|
|
27
28
|
import NodeCache from 'node-cache';
|
|
29
|
+
import { be } from 'zod/locales';
|
|
28
30
|
//import getOpts from './src/toolHelpers/getOpts.js';
|
|
29
31
|
|
|
30
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -39,6 +41,10 @@ const args = parseArgs({
|
|
|
39
41
|
short: 'p',
|
|
40
42
|
description: 'Port to run the server on'
|
|
41
43
|
},
|
|
44
|
+
https: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Use HTTPS for the server (default: false)'
|
|
47
|
+
},
|
|
42
48
|
mcptype: {
|
|
43
49
|
type: 'string',
|
|
44
50
|
short: 'm',
|
|
@@ -52,7 +58,7 @@ const args = parseArgs({
|
|
|
52
58
|
authflow: {
|
|
53
59
|
type: 'string',
|
|
54
60
|
short: 'a',
|
|
55
|
-
description: 'Authentication flow (sascli, code, token)'
|
|
61
|
+
description: 'Authentication flow (sascli, code, token, bearer,auth)'
|
|
56
62
|
},
|
|
57
63
|
profile: {
|
|
58
64
|
type: 'string',
|
|
@@ -76,6 +82,11 @@ const args = parseArgs({
|
|
|
76
82
|
type: 'boolean',
|
|
77
83
|
short: 'v',
|
|
78
84
|
description: 'Show version'
|
|
85
|
+
},
|
|
86
|
+
'install-skills': {
|
|
87
|
+
type: 'string',
|
|
88
|
+
short: 's',
|
|
89
|
+
description: 'Install bundled skills/'
|
|
79
90
|
}
|
|
80
91
|
},
|
|
81
92
|
strict: false,
|
|
@@ -84,19 +95,22 @@ const args = parseArgs({
|
|
|
84
95
|
|
|
85
96
|
// Handle help flag
|
|
86
97
|
if (args.values.help) {
|
|
87
|
-
console.
|
|
98
|
+
console.error(`
|
|
88
99
|
Usage: sas-score-mcp-serverjs [options]
|
|
89
100
|
|
|
90
101
|
Options:
|
|
91
|
-
-p, --port <port>
|
|
92
|
-
-m, --mcptype <type>
|
|
93
|
-
-v, --viya <url>
|
|
94
|
-
-
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
--
|
|
102
|
+
-p, --port <port> Port to run the server on (default: 8080)
|
|
103
|
+
-m, --mcptype <type> MCP server type: http or stdio (default: http)
|
|
104
|
+
-v, --viya <url> Viya server URL
|
|
105
|
+
-c, --mcpclient <name> Name of the MCP client (for token management)
|
|
106
|
+
-a, --authflow <flow> Authentication flow: sascli, code, or token
|
|
107
|
+
--provider <name> Authentication by external provider, e.g. AZURE.
|
|
108
|
+
--profile <name> SAS CLI profile name
|
|
109
|
+
--config <path> SAS CLI config directory
|
|
110
|
+
-e, --envfile <path> Environment file path
|
|
111
|
+
-h, --help Show this help message
|
|
112
|
+
--version Show version
|
|
113
|
+
-s, --install-skills <client>Install bundled skills to ~/.claude/skills/
|
|
100
114
|
|
|
101
115
|
Environment Variables:
|
|
102
116
|
Use .env file or set environment variables for configuration.
|
|
@@ -108,10 +122,44 @@ Environment Variables:
|
|
|
108
122
|
// Handle version flag
|
|
109
123
|
if (args.values.version) {
|
|
110
124
|
let pkgJson = JSON.parse(pkg);
|
|
111
|
-
console.
|
|
125
|
+
console.error(pkgJson.version);
|
|
112
126
|
process.exit(0);
|
|
113
127
|
}
|
|
114
128
|
|
|
129
|
+
// Handle install-skills flag
|
|
130
|
+
if (args.values['install-skills']) {
|
|
131
|
+
let destdir = '.' + args.values['install-skills'];
|
|
132
|
+
const skillsSrc = join(__dirname, 'skills');
|
|
133
|
+
const skillsDest = join(os.homedir(), destdir, 'skills');
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
136
|
+
console.error('No skills directory found in this package.');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
141
|
+
|
|
142
|
+
const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
|
|
143
|
+
.filter(d => d.isDirectory())
|
|
144
|
+
.map(d => d.name);
|
|
145
|
+
|
|
146
|
+
if (skills.length === 0) {
|
|
147
|
+
console.error('No skills found to install.');
|
|
148
|
+
} else {
|
|
149
|
+
console.error(`Installing ${skills.length} skill(s) to ${skillsDest}...`);
|
|
150
|
+
for (const skill of skills) {
|
|
151
|
+
const src = join(skillsSrc, skill);
|
|
152
|
+
const dest = join(skillsDest, skill);
|
|
153
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
154
|
+
console.error(` installed: ${skill}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.error(`\n${skills.length} skill(s) installed to ${skillsDest}`);
|
|
158
|
+
console.error('Restart Client to activate the new skills.');
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
115
163
|
if (process.env.ENVFILE === 'FALSE') {
|
|
116
164
|
//use this when using remote mcp server and no .env file is desired
|
|
117
165
|
console.error('[Note]: Skipping .env file as ENVFILE is set to FALSE...');
|
|
@@ -119,10 +167,10 @@ if (process.env.ENVFILE === 'FALSE') {
|
|
|
119
167
|
console.error('Working Directory', process.cwd());
|
|
120
168
|
let envf = process.env.ENVFILE || (process.cwd() + '\\.env');
|
|
121
169
|
//__dirname + '\\.env';
|
|
122
|
-
console.error('Env file:', envf);
|
|
170
|
+
console.error('Env file:', envf);
|
|
123
171
|
if (fs.existsSync(envf)) {
|
|
124
172
|
console.error(`Loading environment variables from ${envf}...`);
|
|
125
|
-
let e = iconfig(envf); // avoid dotenv since it writes to console.
|
|
173
|
+
let e = iconfig(envf); // avoid dotenv since it writes to console.error
|
|
126
174
|
console.error('[Note]: Environment variables loaded from .env file...');
|
|
127
175
|
console.error('Loaded env variables:', e);
|
|
128
176
|
// dotenvExpand.expand(e);
|
|
@@ -143,12 +191,20 @@ if (args.values.mcptype) {
|
|
|
143
191
|
process.env.MCPTYPE = args.values.mcptype;
|
|
144
192
|
console.error(`[Note] MCPTYPE set from command line: ${args.values.mcptype}`);
|
|
145
193
|
}
|
|
146
|
-
|
|
194
|
+
if (args.values.mcpclient) {
|
|
195
|
+
process.env.MCPCLIENT = args.values.mcpclient;
|
|
196
|
+
console.error(`[Note] MCPCLIENT set from command line: ${args.values.mcpclient}`);
|
|
197
|
+
}
|
|
147
198
|
if (args.values.viya) {
|
|
148
199
|
process.env.VIYA_SERVER = args.values.viya;
|
|
149
200
|
console.error(`[Note] VIYA_SERVER set from command line: ${args.values.viya}`);
|
|
150
201
|
}
|
|
151
202
|
|
|
203
|
+
if (args.values.mcpserver) {
|
|
204
|
+
process.env.MCPSHOST = args.values.mcphost;
|
|
205
|
+
console.error(`[Note] MCPHOST set from command line: ${args.values.mcpserver}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
if (args.values.authflow) {
|
|
153
209
|
process.env.AUTHFLOW = args.values.authflow;
|
|
154
210
|
console.error(`[Note] AUTHFLOW set from command line: ${args.values.authflow}`);
|
|
@@ -166,7 +222,7 @@ if (args.values.config) {
|
|
|
166
222
|
|
|
167
223
|
if (process.env.APPHOST == null) {
|
|
168
224
|
process.env.APPHOST = 'localhost';
|
|
169
|
-
}
|
|
225
|
+
}
|
|
170
226
|
/********************************* */
|
|
171
227
|
const BRAND = 'sas-score'
|
|
172
228
|
/********************************* */
|
|
@@ -182,9 +238,9 @@ console.error(
|
|
|
182
238
|
// session sessionCache
|
|
183
239
|
// For more robust caching consider products like Redis
|
|
184
240
|
// and storage provided by cloud providers
|
|
185
|
-
|
|
241
|
+
|
|
186
242
|
debugger;
|
|
187
|
-
let sessionCache = new NodeCache({ stdTTL: 24 *60*60, checkperiod: 2 * 60, useClones: false });
|
|
243
|
+
let sessionCache = new NodeCache({ stdTTL: 24 * 60 * 60, checkperiod: 2 * 60, useClones: false });
|
|
188
244
|
|
|
189
245
|
//
|
|
190
246
|
// Load environment variables from .env file if present
|
|
@@ -217,10 +273,26 @@ if (process.env.SUBCLASS != null) {
|
|
|
217
273
|
let clientID = process.env.CLIENTID || process.env.CLIENTIDPW || null;
|
|
218
274
|
let clientSecret = process.env.CLIENTSECRET || process.env.CLIENTSECRETPW || null;
|
|
219
275
|
let https = process.env.HTTPS != null ? process.env.HTTPS.toUpperCase() : "FALSE";
|
|
276
|
+
let authExternal = false;
|
|
277
|
+
let authFlow = process.env.AUTHFLOW;
|
|
278
|
+
let mcpHost = process.env.MCPHOST;
|
|
279
|
+
|
|
280
|
+
if (authFlow === 'oauth') {
|
|
281
|
+
authFlow = 'bearer';
|
|
282
|
+
authExternal = true;
|
|
283
|
+
}
|
|
284
|
+
if (authFlow === 'oauthproxy') {
|
|
285
|
+
authFlow = 'bearer';
|
|
286
|
+
authExternal = false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.error(`[Note] Authentication flow: ${authFlow}, External provider: ${authExternal}`);
|
|
220
290
|
let autoLogon = process.env.AUTOLOGON != null ? process.env.AUTOLOGON.toUpperCase() : "FALSE";
|
|
221
291
|
const appEnvBase = {
|
|
222
292
|
version: version,
|
|
223
|
-
mcpType: mcpType,
|
|
293
|
+
mcpType: mcpType,
|
|
294
|
+
mcpClient: process.env.MCPCLIENT || 'vscode',
|
|
295
|
+
mcpHost: mcpHost,
|
|
224
296
|
brand: (process.env.BRAND == null) ? BRAND : process.env.BRAND,
|
|
225
297
|
HTTPS: https,
|
|
226
298
|
SAS_CLI_PROFILE: process.env.SAS_CLI_PROFILE || 'Default',
|
|
@@ -228,7 +300,9 @@ const appEnvBase = {
|
|
|
228
300
|
SSLCERT: process.env.SSLCERT || null,
|
|
229
301
|
VIYACERT: process.env.VIYACERT || null,
|
|
230
302
|
|
|
231
|
-
AUTHFLOW:
|
|
303
|
+
AUTHFLOW: authFlow,
|
|
304
|
+
AUTHEXTERNAL: authExternal,
|
|
305
|
+
BEARERTOKEN: null,
|
|
232
306
|
AUTOLOGON: autoLogon,
|
|
233
307
|
VIYA_SERVER: process.env.VIYA_SERVER,
|
|
234
308
|
PORT: process.env.PORT || 8080,
|
|
@@ -268,7 +342,8 @@ const appEnvBase = {
|
|
|
268
342
|
tlsOpts: null,
|
|
269
343
|
oauthInfo: null,
|
|
270
344
|
contexts: {
|
|
271
|
-
AUTHFLOW:
|
|
345
|
+
AUTHFLOW: authFlow,
|
|
346
|
+
AUTHEXTERNAL: authExternal,
|
|
272
347
|
host: process.env.VIYA_SERVER,
|
|
273
348
|
APPHOST: process.env.APPHOST || 'localhost',
|
|
274
349
|
APPNAME: process.env.APPNAME || 'sas-score-mcp-serverjs',
|
|
@@ -293,7 +368,9 @@ const appEnvBase = {
|
|
|
293
368
|
}
|
|
294
369
|
};
|
|
295
370
|
|
|
296
|
-
process.env.APPPORT=appEnvBase.PORT;
|
|
371
|
+
process.env.APPPORT = appEnvBase.PORT;
|
|
372
|
+
let useHapi = process.env.USEHAPI === 'TRUE' ? true : false;
|
|
373
|
+
appEnvBase.useHapi = useHapi;
|
|
297
374
|
|
|
298
375
|
// setup TLS options for viya calls
|
|
299
376
|
console.error('[Note]Viya SSL dir set to: ' + appEnvBase.VIYACERT);
|
|
@@ -315,7 +392,7 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
315
392
|
console.error(`[Note]Loading token from file: ${appEnvBase.TOKENFILE}...`);
|
|
316
393
|
appEnvBase.TOKEN = fs.readFileSync(appEnvBase.TOKENFILE, { encoding: 'utf8' });
|
|
317
394
|
appEnvBase.AUTHFLOW = 'token';
|
|
318
|
-
appEnvBase.
|
|
395
|
+
appEnvBase.contexts.logonPayload = {
|
|
319
396
|
host: appEnvBase.VIYA_SERVER,
|
|
320
397
|
authType: 'server',
|
|
321
398
|
token: appEnvBase.TOKEN,
|
|
@@ -328,10 +405,6 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
328
405
|
}
|
|
329
406
|
|
|
330
407
|
|
|
331
|
-
|
|
332
|
-
// if authflow is cli or code, postpone getting logonPayload until needed
|
|
333
|
-
|
|
334
|
-
|
|
335
408
|
// setup mcpServer (both http and stdio use this)
|
|
336
409
|
// this is singleton - best practices recommend this
|
|
337
410
|
|
|
@@ -342,18 +415,25 @@ let appEnvTemplate = Object.assign({}, appEnvBase);
|
|
|
342
415
|
|
|
343
416
|
sessionCache.set('appEnvTemplate', appEnvTemplate);
|
|
344
417
|
|
|
418
|
+
// prime transport cache
|
|
345
419
|
let transports = {
|
|
346
420
|
"dummy": null
|
|
347
421
|
};
|
|
348
422
|
sessionCache.set('transports', transports);
|
|
423
|
+
let tokenlist = {
|
|
424
|
+
dummy: null
|
|
425
|
+
}
|
|
426
|
+
sessionCache.set('tokenlist', tokenlist);
|
|
349
427
|
|
|
350
428
|
// set this for stdio transport use
|
|
351
429
|
// dummy sessionId for use in the tools
|
|
352
|
-
|
|
353
|
-
|
|
430
|
+
|
|
431
|
+
;
|
|
354
432
|
// creat a dummy sessionId for stdio since there is only one session and transport in that case, and tools need a sessionId to access the appEnvBase and contexts
|
|
355
433
|
let sessionId = randomUUID();
|
|
356
434
|
sessionCache.set(sessionId, appEnvBase);
|
|
435
|
+
sessionCache.set('currentId', sessionId);
|
|
436
|
+
debugger;
|
|
357
437
|
if (mcpType === 'stdio') {
|
|
358
438
|
console.error('[Note] Setting up stdio transport with sessionId:', sessionId);
|
|
359
439
|
console.error('[Note] Used in setting up tools and some persistence(not all).');
|
|
@@ -390,7 +470,7 @@ function iconfig(envFile) {
|
|
|
390
470
|
});
|
|
391
471
|
return envData;
|
|
392
472
|
} catch (err) {
|
|
393
|
-
console.
|
|
473
|
+
console.error(err);
|
|
394
474
|
process.exit(0);
|
|
395
475
|
}
|
|
396
476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassoftware/sas-score-mcp-serverjs",
|
|
3
|
-
"version": "0.4.1-
|
|
3
|
+
"version": "0.4.1-15",
|
|
4
4
|
"description": "A mcp server for SAS Viya",
|
|
5
5
|
"author": "Deva Kumar <deva.kumar@sas.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"deploy": "bash ./deploy.sh",
|
|
19
19
|
"push2acr": "cd docker && bash ./push2acr.sh",
|
|
20
20
|
"bump": "npm version prerelease",
|
|
21
|
-
"pub": "npm publish --tag dev --access public"
|
|
21
|
+
"pub": "npm publish --tag dev --access public",
|
|
22
|
+
"postinstall": "node scripts/setup-skills.js",
|
|
23
|
+
"setup-skills": "node scripts/setup-skills.js"
|
|
22
24
|
},
|
|
23
25
|
"repository": "https://github.com/sassoftware/sas-score-mcp-serverjs",
|
|
24
26
|
"keywords": [
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sas-list-tables-smart
|
|
3
|
+
description: >
|
|
4
|
+
List all tables in a SAS Viya library with intelligent server detection. When the server is not
|
|
5
|
+
specified, automatically checks CAS first, then SAS if not found. Informs the user if the library
|
|
6
|
+
does not exist in either server. Use this skill when the user wants to browse or explore available
|
|
7
|
+
tables. Trigger phrases include: "list tables in", "show tables in", "what tables are in",
|
|
8
|
+
"browse tables in", "tables in library", "enumerate tables", or any request to explore data sources.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Smart Data Access in SAS Library (List, Read, Query)
|
|
12
|
+
|
|
13
|
+
Intelligently enumerates tables in a SAS Viya library, automatically determining the correct server
|
|
14
|
+
when not explicitly specified.
|
|
15
|
+
|
|
16
|
+
**If the user specifies the server explicitly** (e.g., "list tables in Public in cas"):
|
|
17
|
+
- Use the specified server: `server: "cas"` or `server: "sas"`
|
|
18
|
+
- Proceed directly to listing tables
|
|
19
|
+
|
|
20
|
+
**If the server is NOT specified:**
|
|
21
|
+
1. **First attempt**: Check CAS (`server: "cas"`)
|
|
22
|
+
2. **If no tables found in CAS**: Check SAS (`server: "sas"`)
|
|
23
|
+
3. **If no tables found in either**:
|
|
24
|
+
- Inform user: *"The library '<lib>' was not found in CAS or SAS. Please verify the library name is correct."*
|
|
25
|
+
- Ask: *"Would you like to list available libraries?"* (suggest `list-libraries`)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Using list-tables
|
|
30
|
+
|
|
31
|
+
**When:**
|
|
32
|
+
- User wants to browse all tables in a library
|
|
33
|
+
- User wants to see what data is available
|
|
34
|
+
- User wants to explore library contents before querying
|
|
35
|
+
|
|
36
|
+
**How:**
|
|
37
|
+
```
|
|
38
|
+
list-tables({
|
|
39
|
+
lib: "libraryname", // required
|
|
40
|
+
server: "cas" or "sas", // required; determined by server check
|
|
41
|
+
limit: 10, // optional; default 10, adjust for pagination
|
|
42
|
+
start: 1 // optional; default 1, use for pagination
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Rules:**
|
|
47
|
+
- Always determine the correct server first (cas → sas → neither)
|
|
48
|
+
- **For SAS server: always uppercase the library name** (e.g., "maps" → "MAPS")
|
|
49
|
+
- If library name is missing, ask: *"Which library should I list tables from?"*
|
|
50
|
+
- Default page size is 10; adjust based on user request ("show me all", "25 tables", etc.)
|
|
51
|
+
- If returned table count equals the limit, suggest pagination: *"There may be more tables. Use `start: {next_offset}` to see more."*
|
|
52
|
+
- If no tables are found despite library existing, report: *"No tables found in {lib} on {server} server."*
|
|
53
|
+
- Return table names only; do not fetch table metadata unless explicitly requested
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Smart server detection logic
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
IF server specified by user
|
|
61
|
+
→ IF server is "sas"
|
|
62
|
+
→ uppercase lib
|
|
63
|
+
→ use that server
|
|
64
|
+
ELSE
|
|
65
|
+
→ TRY list-tables(lib, server="cas")
|
|
66
|
+
IF tables found
|
|
67
|
+
→ success, return tables
|
|
68
|
+
ELSE
|
|
69
|
+
→ uppercase lib
|
|
70
|
+
→ TRY list-tables(lib.toUpperCase(), server="sas")
|
|
71
|
+
IF tables found
|
|
72
|
+
→ success, return tables
|
|
73
|
+
ELSE
|
|
74
|
+
→ inform user library not found in either server
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Common patterns
|
|
80
|
+
|
|
81
|
+
**Pattern 1 — List tables, server unspecified**
|
|
82
|
+
> "List tables in Public"
|
|
83
|
+
|
|
84
|
+
1. Try CAS: `list-tables({ lib: "Public", server: "cas" })`
|
|
85
|
+
2. If empty, try SAS with uppercase: `list-tables({ lib: "PUBLIC", server: "sas" })`
|
|
86
|
+
3. If still empty → inform user
|
|
87
|
+
|
|
88
|
+
**Pattern 2 — List tables with explicit server (SAS)**
|
|
89
|
+
> "List tables in sashelp in sas"
|
|
90
|
+
|
|
91
|
+
1. Skip server detection
|
|
92
|
+
2. Call with uppercase lib: `list-tables({ lib: "SASHELP", server: "sas" })`
|
|
93
|
+
|
|
94
|
+
**Pattern 3 — List tables with explicit server (CAS)**
|
|
95
|
+
> "List tables in Public in cas"
|
|
96
|
+
|
|
97
|
+
1. No uppercase needed for CAS
|
|
98
|
+
2. Call: `list-tables({ lib: "Public", server: "cas" })`
|
|
99
|
+
|
|
100
|
+
**Pattern 4 — Pagination**
|
|
101
|
+
> "Show me 25 tables in Samples, then the next batch"
|
|
102
|
+
|
|
103
|
+
1. First call: `list-tables({ lib: "Samples", limit: 25, start: 1 })`
|
|
104
|
+
2. Next call: `list-tables({ lib: "Samples", limit: 25, start: 26 })`
|
|
105
|
+
|
|
106
|
+
**Pattern 5 — Library not found**
|
|
107
|
+
> "List tables in foo"
|
|
108
|
+
|
|
109
|
+
1. Try CAS: empty
|
|
110
|
+
2. Try SAS with uppercase: empty
|
|
111
|
+
3. Response: *"The library 'foo' was not found in CAS or SAS. Please verify the library name."*
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Error handling
|
|
116
|
+
|
|
117
|
+
| Scenario | Action |
|
|
118
|
+
|---|---|
|
|
119
|
+
| Library not found in either server | Inform user and ask to verify library name |
|
|
120
|
+
| Empty result on first server | Automatically check second server |
|
|
121
|
+
| User specifies invalid server | Return error; ask user to clarify: `"cas"` or `"sas"` |
|
|
122
|
+
| Missing library name | Ask: *"Which library should I list tables from?"* |
|
|
123
|
+
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sas-read-and-score
|
|
3
3
|
description: >
|
|
4
|
-
Guide the full read → score workflow in SAS Viya: reading records from a table
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"score
|
|
9
|
-
"
|
|
10
|
-
|
|
4
|
+
Guide the full read → score workflow in SAS Viya: reading records from a table and then scoring
|
|
5
|
+
them with a MAS model (using model-score). Use this skill whenever the user wants to score records
|
|
6
|
+
from a table, run a model against query results, predict outcomes for a set of rows, or any
|
|
7
|
+
combination of fetching data and scoring it. Trigger phrases include: "score these records",
|
|
8
|
+
"score results of my query", "run the model on this table", "predict for these customers",
|
|
9
|
+
"fetch and score", "read and score", "score rows from", "run model on table data", or any request
|
|
10
|
+
that combines reading/querying table data with model prediction.
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# SAS Read → Score Workflow
|
|
@@ -15,68 +15,68 @@ description: >
|
|
|
15
15
|
Orchestrates the full two-step pattern of reading records from a SAS/CAS table and scoring them
|
|
16
16
|
with a deployed MAS model.
|
|
17
17
|
|
|
18
|
-
This skill chains two sub-skills:
|
|
19
|
-
1. **sas-read-strategy** — Choose between `read-table` and `sas-query`
|
|
20
|
-
2. **sas-score-workflow** — Validate model, invoke scoring, present results
|
|
21
|
-
|
|
22
18
|
---
|
|
23
19
|
|
|
24
|
-
##
|
|
20
|
+
## Workflow overview
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
The typical flow involves:
|
|
23
|
+
1. **Fetch data** — Identify which table/query will provide input records
|
|
24
|
+
2. **Validate model** — Confirm the model exists and understand its input schema
|
|
25
|
+
3. **Score** — Invoke the model on the fetched records
|
|
26
|
+
4. **Present results** — Merge predictions with original data and display
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
- Yes → proceed to score
|
|
32
|
-
- No → pause and use `find-model` / `model-info`
|
|
28
|
+
---
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
## Scenario: User already has data
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
If the user provides scenario data directly (e.g., "Score age=45, income=60000 with model X"):
|
|
33
|
+
- Extract the scenario values
|
|
34
|
+
- Validate against model's input schema
|
|
35
|
+
- Invoke scoring
|
|
36
|
+
- Return prediction
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Scenario: User wants to score table rows
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
If the user specifies a table (e.g., "Score all customers in Public.customers with model X"):
|
|
43
|
+
- Fetch raw rows (possibly filtered: "where status='active'")
|
|
44
|
+
- Validate model compatibility with input columns
|
|
45
|
+
- Invoke scoring on each row
|
|
46
|
+
- Merge results with original data
|
|
47
|
+
- Display combined table
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
2. Apply `sas-score-workflow` → invoke `model-score` and present results
|
|
47
|
-
|
|
48
|
-
**Flow B — Score results of an analytical query**
|
|
49
|
-
> "Score high-value customers (spend > 5000) in mylib.sales with the fraud model"
|
|
49
|
+
---
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
2. Apply `sas-score-workflow` → invoke `model-score` and present results
|
|
51
|
+
## Scenario: User wants to score query results
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
If the user wants to score aggregated/filtered results (e.g., "Score high-value customers (spend > 5000) with model X"):
|
|
54
|
+
- Determine which records meet criteria (aggregation/filtering)
|
|
55
|
+
- Validate model expects these input columns
|
|
56
|
+
- Invoke scoring
|
|
57
|
+
- Merge predictions with summary data
|
|
58
|
+
- Display results
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
2. Apply `sas-score-workflow` → invoke `model-score` and present result
|
|
60
|
+
---
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
> "Score Public.applicants with the creditRisk2 model"
|
|
62
|
+
## Scenario: User unfamiliar with model
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
If the user specifies a model name that's new/unknown:
|
|
65
|
+
- Check if model exists
|
|
66
|
+
- Retrieve model schema (inputs, outputs)
|
|
67
|
+
- Show user what inputs the model expects
|
|
68
|
+
- Confirm before proceeding with scoring
|
|
66
69
|
|
|
67
70
|
---
|
|
68
71
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
- **Read strategy decisions?** See `sas-read-strategy` skill
|
|
72
|
-
- **Scoring validation and presentation?** See `sas-score-workflow` skill
|
|
73
|
-
|
|
74
|
-
**Flow D — Model unfamiliar**
|
|
75
|
-
> "Score Public.applicants with the creditRisk2 model"
|
|
72
|
+
## Rules
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
- Always validate table/library existence before attempting to read
|
|
75
|
+
- Always check model exists before invoking `model-score`
|
|
76
|
+
- Match table columns to model input variables; warn on mismatch
|
|
77
|
+
- If multiple records: score batch if possible; fall back to row-by-row
|
|
78
|
+
- Merge predictions with original data using row index or key column
|
|
79
|
+
- Present results as table with original columns + new prediction columns
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -85,7 +85,8 @@ This skill chains two sub-skills:
|
|
|
85
85
|
| Problem | Action |
|
|
86
86
|
|---|---|
|
|
87
87
|
| Table not found | Ask for correct lib.tablename |
|
|
88
|
-
| Model not found |
|
|
89
|
-
| Field
|
|
90
|
-
| Scoring error | Return structured error, suggest model
|
|
91
|
-
| Empty read result |
|
|
88
|
+
| Model not found | Inform user; suggest verifying model name |
|
|
89
|
+
| Field/column mismatch | Show mismatch, ask user to confirm or adjust query |
|
|
90
|
+
| Scoring error | Return structured error, suggest checking model inputs |
|
|
91
|
+
| Empty read result | Inform user, ask if they want to adjust the query/filter |
|
|
92
|
+
| Data type mismatch | Warn user about type conversion, proceed or ask for clarification |
|
|
@@ -11,9 +11,7 @@ description: >
|
|
|
11
11
|
# SAS Read Strategy
|
|
12
12
|
|
|
13
13
|
Guides the decision between `read-table` and `sas-query` based on the user's intent and the nature
|
|
14
|
-
of the data operation.
|
|
15
|
-
|
|
16
|
-
---
|
|
14
|
+
of the data operation. Determines which server contains the data and which retrieval tool is most appropriate.
|
|
17
15
|
|
|
18
16
|
## Determine the server location
|
|
19
17
|
|
|
@@ -125,10 +123,11 @@ sas-query({
|
|
|
125
123
|
|
|
126
124
|
| Problem | Action |
|
|
127
125
|
|---|---|
|
|
128
|
-
| Table not found in either server |
|
|
126
|
+
| Table not found in either server | Inform user and ask: *"Which library contains this table? (e.g., Public, Samples, mylib)"* |
|
|
129
127
|
| Table exists in both CAS and SAS | Ask: *"The table exists in both servers. Which would you prefer: CAS or SAS?"* |
|
|
130
128
|
| Table exists only in one server | Use that server automatically in your request |
|
|
131
|
-
| Library not found |
|
|
129
|
+
| Library not found | Inform user and ask to verify the library name |
|
|
130
|
+
| Table name missing entirely | Ask: *"Which table should I read from?"* |
|
|
132
131
|
| Ambiguous intent (raw vs aggregate) | Ask: *"Do you want individual rows or a summary by some field?"* |
|
|
133
132
|
| Empty result | Inform user, ask to adjust filter or query |
|
|
134
133
|
|
|
@@ -136,8 +135,9 @@ sas-query({
|
|
|
136
135
|
|
|
137
136
|
## Next steps
|
|
138
137
|
|
|
139
|
-
Once data is retrieved,
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
138
|
+
Once data is retrieved, typical follow-ups include:
|
|
139
|
+
- **Visualize** — present as table or chart
|
|
140
|
+
- **Export** — format and offer download
|
|
141
|
+
- **Analyze further** — ask clarifying questions
|
|
142
|
+
- **Score** — run predictions on the data
|
|
143
|
+
- **Combine** — join with other datasets
|
|
@@ -33,12 +33,27 @@ score <name>.<type> [scenario =<key=value pairs>]
|
|
|
33
33
|
|
|
34
34
|
If no type is specified (bare model name), assume `.mas` (MAS model).
|
|
35
35
|
|
|
36
|
-
|
|
37
36
|
---
|
|
38
37
|
|
|
39
38
|
## Type-Based Routing
|
|
40
39
|
|
|
41
|
-
Parse
|
|
40
|
+
### Parse and Strip Model Type
|
|
41
|
+
|
|
42
|
+
When a user provides a model name with a type suffix (e.g., `simplejon.job`, `churn.mas`):
|
|
43
|
+
|
|
44
|
+
1. **Extract the type:** Split on the last dot to identify the type suffix
|
|
45
|
+
- `simplejon.job` → type = `job`, base name = `simplejon`
|
|
46
|
+
- `churn.mas` → type = `mas`, base name = `churn`
|
|
47
|
+
- `fraud_detector.jobdef` → type = `jobdef`, base name = `fraud_detector`
|
|
48
|
+
|
|
49
|
+
2. **Validate the type:** Confirm it matches one of the supported types: `job`, `jobdef`, `mas`, `scr`, `sas`
|
|
50
|
+
- If type is unrecognized, assume `.mas` (default MAS model) and treat the entire input as the model name
|
|
51
|
+
|
|
52
|
+
3. **Strip the type suffix:** Remove the `.type` from the model name before passing to the routing tool
|
|
53
|
+
- **Critical:** Always pass the base name (without the dot and type) to the invoked tool
|
|
54
|
+
- `simplejon.job` → pass `simplejon` to `run-job`
|
|
55
|
+
- `churn.mas` → pass `churn` to `model-score`
|
|
56
|
+
- `fraud_detector.jobdef` → pass `fraud_detector` to `run-jobdef`
|
|
42
57
|
|
|
43
58
|
### Type: `.mas` (Model Aggregation Service)
|
|
44
59
|
- **Tool**: `model-score`
|
|
@@ -70,6 +85,8 @@ Parse the model name to extract the type suffix and route accordingly:
|
|
|
70
85
|
- **Example**: `score my_scoring_code.sas using x=1,y=2`
|
|
71
86
|
- **Invocation**: `run-sas-program({ folder: "my_scoring_code", scenario: {...} })`
|
|
72
87
|
|
|
88
|
+
|
|
89
|
+
|
|
73
90
|
---
|
|
74
91
|
|
|
75
92
|
## Scenario Parsing
|