@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.
Files changed (47) hide show
  1. package/cli.js +111 -31
  2. package/package.json +4 -2
  3. package/skills/sas-list-tables-smart/SKILL.md +123 -0
  4. package/skills/sas-read-and-score/SKILL.md +54 -53
  5. package/skills/sas-read-strategy/SKILL.md +10 -10
  6. package/skills/sas-score-workflow/SKILL.md +19 -2
  7. package/skills/sas-spec-migration/SKILL.md +303 -0
  8. package/src/authpkce.js +219 -0
  9. package/src/createMcpServer.js +11 -8
  10. package/src/expressMcpServer.js +354 -338
  11. package/src/oauthHandlers/authorize.js +46 -0
  12. package/src/oauthHandlers/baseUrl.js +8 -0
  13. package/src/oauthHandlers/callback.js +93 -0
  14. package/src/oauthHandlers/getMetadata.js +27 -0
  15. package/src/oauthHandlers/index.js +7 -0
  16. package/src/oauthHandlers/token.js +37 -0
  17. package/src/processHeaders.js +88 -0
  18. package/src/toolHelpers/_listLibrary.js +0 -1
  19. package/src/toolHelpers/getLogonPayload.js +5 -1
  20. package/src/toolHelpers/refreshTokenOauth.js +3 -3
  21. package/src/toolSet/.claude/settings.local.json +13 -0
  22. package/src/toolSet/devaScore.js +61 -61
  23. package/src/toolSet/findJob.js +1 -1
  24. package/src/toolSet/findJobdef.js +2 -2
  25. package/src/toolSet/findLibrary.js +68 -67
  26. package/src/toolSet/findModel.js +2 -2
  27. package/src/toolSet/findTable.js +3 -2
  28. package/src/toolSet/getEnv.js +8 -4
  29. package/src/toolSet/listJobdefs.js +61 -61
  30. package/src/toolSet/listJobs.js +61 -61
  31. package/src/toolSet/listLibraries.js +78 -78
  32. package/src/toolSet/listModels.js +56 -56
  33. package/src/toolSet/listTables.js +66 -65
  34. package/src/toolSet/modelInfo.js +2 -2
  35. package/src/toolSet/modelScore.js +6 -5
  36. package/src/toolSet/readTable.js +63 -65
  37. package/src/toolSet/runCasProgram.js +7 -6
  38. package/src/toolSet/runJob.js +81 -81
  39. package/src/toolSet/runJobdef.js +82 -82
  40. package/src/toolSet/runMacro.js +81 -80
  41. package/src/toolSet/runProgram.js +4 -8
  42. package/src/toolSet/sasQuery.js +77 -78
  43. package/src/toolSet/scrInfo.js +1 -1
  44. package/src/toolSet/scrScore.js +69 -68
  45. package/src/toolSet/setContext.js +65 -65
  46. package/src/toolSet/superstat.js +61 -59
  47. 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.log(`
98
+ console.error(`
88
99
  Usage: sas-score-mcp-serverjs [options]
89
100
 
90
101
  Options:
91
- -p, --port <port> Port to run the server on (default: 8080)
92
- -m, --mcptype <type> MCP server type: http or stdio (default: http)
93
- -v, --viya <url> Viya server URL
94
- -a, --authflow <flow> Authentication flow: sascli, code, or token
95
- --profile <name> SAS CLI profile name
96
- --config <path> SAS CLI config directory
97
- -e, --envfile <path> Environment file path
98
- -h, --help Show this help message
99
- --version Show version
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.log(pkgJson.version);
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.log
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
- console.error(process.env.COMPUTECONTEXT);
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: process.env.AUTHFLOW || 'sascli',
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: process.env.AUTHFLOW || 'sascli',
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.appContexts.logonPayload = {
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
- let useHapi = process.env.AUTHFLOW === 'code' ? true : false;
353
- console.error('[Note] appEnvBase is', JSON.stringify(appEnvBase, null,2));
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.log(err);
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-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 '&lt;lib&gt;' 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 (using read-table
5
- or sas-query) and then scoring them with a MAS model (using model-score). Use this skill whenever
6
- the user wants to score records from a table, run a model against query results, predict outcomes
7
- for a set of rows, or any combination of fetching data and scoring it. Trigger phrases include:
8
- "score these records", "score results of my query", "run the model on this table",
9
- "predict for these customers", "fetch and score", "read and score", "score rows from",
10
- "run model on table data", or any request that combines reading table data with model prediction.
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
- ## Quick reference
20
+ ## Workflow overview
25
21
 
26
- 1. **Does the user already have data in hand?**
27
- - Yes skip to Step 2 (scoring)
28
- - No use `sas-read-strategy` to fetch data
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
- 2. **Is the model name familiar?**
31
- - Yes → proceed to score
32
- - No → pause and use `find-model` / `model-info`
28
+ ---
33
29
 
34
- 3. **Invoke model-score** with the fetched data
30
+ ## Scenario: User already has data
35
31
 
36
- 4. **Merge results** and present as a table
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
- ## Common flows
40
+ ## Scenario: User wants to score table rows
41
41
 
42
- **Flow A Score rows from a table directly**
43
- > "Score the first 10 customers in Public.customers with the churn model"
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
- 1. Apply `sas-read-strategy` → use `read-table` to fetch 10 rows
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
- 1. Apply `sas-read-strategy` use `sas-query` for aggregation
52
- 2. Apply `sas-score-workflow` → invoke `model-score` and present results
51
+ ## Scenario: User wants to score query results
53
52
 
54
- **Flow C User supplies scenario data directly**
55
- > "Score age=45, income=60000, region=South with the churn model"
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
- 1. Skip read strategy
58
- 2. Apply `sas-score-workflow` → invoke `model-score` and present result
60
+ ---
59
61
 
60
- **Flow D Model unfamiliar**
61
- > "Score Public.applicants with the creditRisk2 model"
62
+ ## Scenario: User unfamiliar with model
62
63
 
63
- 1. Model unfamiliar use `find-model` to verify existence
64
- 2. Apply `sas-read-strategy` to fetch data
65
- 3. Apply `sas-score-workflow` to score
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
- ## For detailed guidance
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
- 1. Pause "creditRisk2" is new
78
- 2. Suggest: `find-model` to confirm it exists, `model-info` to get input variables
79
- 3. Once confirmed `read-table` + `model-score`
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 | Suggest find-model |
89
- | Field name mismatch | Show mismatch, ask user to confirm mapping |
90
- | Scoring error | Return structured error, suggest model-info |
91
- | Empty read result | Tell user, ask if they want to adjust the query/filter |
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 | Ask: *"Which library contains the table? (e.g., Public, Samples, mylib)"* |
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 | Ask: *"Which library contains the table? (e.g., Public, Samples, mylib)"* |
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, decide the next action based on context:
140
- - If user wants to **score** the data move to `sas-score-workflow`
141
- - If user wants to **visualize** present as table or chart
142
- - If user wants to **export** format and offer download
143
- - If user wants to **analyze further** → ask clarifying questions
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 the model name to extract the type suffix and route accordingly:
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