@sassoftware/sas-score-mcp-serverjs 0.4.1-1 → 0.4.1-17
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 +210 -108
- package/package.json +6 -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 +78 -0
- package/scripts/update_descriptions.py +46 -0
- package/scripts/viyatls.sh +3 -0
- package/skills/sas-find-library-smart/SKILL.md +154 -0
- package/skills/sas-list-tables-smart/SKILL.md +127 -0
- package/skills/sas-read-and-score/SKILL.md +71 -51
- package/skills/sas-read-strategy/SKILL.md +43 -30
- package/skills/sas-score-workflow/SKILL.md +65 -34
- 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 +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/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 +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));
|
|
@@ -44,15 +46,40 @@ const args = parseArgs({
|
|
|
44
46
|
short: 'm',
|
|
45
47
|
description: 'MCP server type (http or stdio)'
|
|
46
48
|
},
|
|
49
|
+
https: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
description: 'Use HTTPS for the server (default: FALSE)'
|
|
52
|
+
},
|
|
53
|
+
'skills-folder': {
|
|
54
|
+
type: 'string',
|
|
55
|
+
short: 'f',
|
|
56
|
+
description: 'Skills folder name'
|
|
57
|
+
},
|
|
58
|
+
|
|
47
59
|
viya: {
|
|
48
60
|
type: 'string',
|
|
49
61
|
short: 'v',
|
|
50
62
|
description: 'Viya server URL'
|
|
51
63
|
},
|
|
64
|
+
mcphost: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
short: 'm',
|
|
67
|
+
description: 'MCP server host (default: http://localhost:8080)'
|
|
68
|
+
},
|
|
52
69
|
authflow: {
|
|
53
70
|
type: 'string',
|
|
54
71
|
short: 'a',
|
|
55
|
-
description: 'Authentication flow (sascli, code, token)'
|
|
72
|
+
description: 'Authentication flow (sascli, code, token, oauth, oauth,oauthclient)'
|
|
73
|
+
},
|
|
74
|
+
clientid: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
short: 'c',
|
|
77
|
+
description: 'Client ID for authentication'
|
|
78
|
+
},
|
|
79
|
+
clientsecret: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
short: 's',
|
|
82
|
+
description: 'Client Secret for authentication'
|
|
56
83
|
},
|
|
57
84
|
profile: {
|
|
58
85
|
type: 'string',
|
|
@@ -62,10 +89,22 @@ const args = parseArgs({
|
|
|
62
89
|
type: 'string',
|
|
63
90
|
description: 'SAS CLI config directory'
|
|
64
91
|
},
|
|
65
|
-
|
|
92
|
+
casserver: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'CAS server name (default: cas-shared-default)'
|
|
95
|
+
},
|
|
96
|
+
computecontext: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Compute session name or context (default: SAS Job Execution compute context)'
|
|
99
|
+
},
|
|
100
|
+
env: {
|
|
66
101
|
type: 'string',
|
|
67
102
|
short: 'e',
|
|
68
|
-
description: 'Environment file path'
|
|
103
|
+
description: 'Environment file path (default: .env in current working directory)'
|
|
104
|
+
},
|
|
105
|
+
client: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'MCP client name (github, claude...). Defaults to \'github\''
|
|
69
108
|
},
|
|
70
109
|
help: {
|
|
71
110
|
type: 'boolean',
|
|
@@ -74,9 +113,9 @@ const args = parseArgs({
|
|
|
74
113
|
},
|
|
75
114
|
version: {
|
|
76
115
|
type: 'boolean',
|
|
77
|
-
short: 'v',
|
|
78
116
|
description: 'Show version'
|
|
79
|
-
}
|
|
117
|
+
},
|
|
118
|
+
|
|
80
119
|
},
|
|
81
120
|
strict: false,
|
|
82
121
|
allowPositionals: false
|
|
@@ -84,89 +123,131 @@ const args = parseArgs({
|
|
|
84
123
|
|
|
85
124
|
// Handle help flag
|
|
86
125
|
if (args.values.help) {
|
|
87
|
-
console.
|
|
126
|
+
console.error(`
|
|
88
127
|
Usage: sas-score-mcp-serverjs [options]
|
|
89
128
|
|
|
90
129
|
Options:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
Minimal options:
|
|
131
|
+
-v, --viya <url> Viya server URL
|
|
132
|
+
-c, --clientid <id> Client ID for oauth authentication(pkce preferred)
|
|
133
|
+
|
|
134
|
+
MCP server options:
|
|
135
|
+
-t, --mcptype <type> MCP server type: http or stdio (default: http)
|
|
136
|
+
-m, --mcphost <host> MCP server host - can be remote URL - (default: http://localhost:8080)
|
|
137
|
+
--client <name> MCP client name (github, claude...). Defaults to 'github'.Use to install skills
|
|
138
|
+
Authentication options:
|
|
139
|
+
-a, --authflow <flow> Authentication flow: oauth, oauthclient, sascli, code, token(default oauth)
|
|
140
|
+
-s, --clientsecret <secret> Client Secret for authentication(if necessary). See clientid option as well.
|
|
141
|
+
--profile <name> SAS CLI profile name for sascli flow (default: Default)
|
|
142
|
+
--config <path> SAS CLI config directory for sascli flow (default: user home directory)
|
|
143
|
+
|
|
144
|
+
Other options:
|
|
145
|
+
-p, --port <port> Port to run the server on (default: 8080)
|
|
146
|
+
--https Use HTTPS for the server (default: false)
|
|
147
|
+
--casserver <name> CAS server name (default: cas-shared-default)
|
|
148
|
+
--computecontext <name> Compute session name or context (default: SAS Job Execution compute context)
|
|
149
|
+
|
|
150
|
+
-e, --envfile <path> Environment file path
|
|
151
|
+
-h, --help Show this help message
|
|
152
|
+
--version Show version
|
|
153
|
+
|
|
100
154
|
|
|
101
155
|
Environment Variables:
|
|
102
156
|
Use .env file or set environment variables for configuration.
|
|
157
|
+
A alternative to cmd line arguments, and in some cases required for sensitive information like client secrets.
|
|
103
158
|
See README.md for more information.
|
|
104
159
|
`);
|
|
105
160
|
process.exit(0);
|
|
106
161
|
}
|
|
107
162
|
|
|
108
|
-
//
|
|
109
|
-
if (args.values.
|
|
110
|
-
let pkgJson = JSON.parse(pkg);
|
|
111
|
-
console.log(pkgJson.version);
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (process.env.ENVFILE === 'FALSE') {
|
|
116
|
-
//use this when using remote mcp server and no .env file is desired
|
|
117
|
-
console.error('[Note]: Skipping .env file as ENVFILE is set to FALSE...');
|
|
118
|
-
} else {
|
|
163
|
+
// read env file and then override with command line arguments
|
|
164
|
+
if (args.values.env) {
|
|
119
165
|
console.error('Working Directory', process.cwd());
|
|
120
|
-
let envf = process.
|
|
166
|
+
let envf = process.cwd() + '\\' + args.values.env;
|
|
121
167
|
//__dirname + '\\.env';
|
|
122
|
-
console.error('Env file:', envf);
|
|
168
|
+
console.error('Env file:', envf);
|
|
123
169
|
if (fs.existsSync(envf)) {
|
|
124
170
|
console.error(`Loading environment variables from ${envf}...`);
|
|
125
|
-
let e = iconfig(envf); // avoid dotenv since it writes to console.
|
|
171
|
+
let e = iconfig(envf); // avoid dotenv since it writes to console.error
|
|
126
172
|
console.error('[Note]: Environment variables loaded from .env file...');
|
|
127
173
|
console.error('Loaded env variables:', e);
|
|
128
174
|
// dotenvExpand.expand(e);
|
|
129
175
|
} else {
|
|
130
176
|
console.error(
|
|
131
|
-
'[Note]: No
|
|
177
|
+
'[Note]: No env file found, Using default environment variables...'
|
|
132
178
|
);
|
|
133
179
|
}
|
|
134
180
|
}
|
|
135
|
-
|
|
136
181
|
// Apply command line arguments to override environment variables
|
|
137
|
-
if (args.values.port) {
|
|
138
|
-
process.env.PORT = args.values.port;
|
|
139
|
-
console.error(`[Note] PORT set from command line: ${args.values.port}`);
|
|
140
|
-
}
|
|
141
182
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
183
|
+
process.env.PORT = process.env.PORT || '8080';
|
|
184
|
+
process.env.HTTPS = (args.values.https) ? 'TRUE' : 'FALSE';
|
|
185
|
+
process.env.MCPTYPE = args.values.mcptype || process.env.MCPTYPE || 'http';
|
|
186
|
+
process.env.MCPHOST = args.values.mcphost || process.env.MCPHOST || 'http://localhost:8080';
|
|
187
|
+
process.env.AUTHFLOW = args.values.authflow || process.env.AUTHFLOW || 'oauth';
|
|
188
|
+
process.env.MCPCLIENT = args.values.client || process.env.MCPCLIENT || 'github';
|
|
189
|
+
process.env.VIYA_SERVER = args.values.viya || process.env.VIYA_SERVER || null;
|
|
190
|
+
process.env.CLIENTID = args.values.clientid || process.env.CLIENTID || 'vscodemcp';
|
|
191
|
+
process.env.CLIENTSECRET = args.values.clientsecret || process.env.CLIENTSECRET || null;
|
|
192
|
+
process.env.SAS_CLI_PROFILE = args.values.profile || process.env.SAS_CLI_PROFILE || 'Default';
|
|
193
|
+
process.env.SAS_CLI_CONFIG = args.values.config || process.env.SAS_CLI_CONFIG || process.env.HOME; // default to user home directory
|
|
194
|
+
process.env.CASSERVER = args.values.casserver || process.env.CASSERVER || 'cas-shared-default';
|
|
195
|
+
process.env.COMPUTECONTEXT = args.values.computecontext || process.env.COMPUTECONTEXT || 'SAS Job Execution compute context';
|
|
196
|
+
process.env.APPHOST = 'localhost';
|
|
197
|
+
process.env.CLIENT = args.values.client || process.env.CLIENT || 'github';
|
|
146
198
|
|
|
147
|
-
if (args.values.viya) {
|
|
148
|
-
process.env.VIYA_SERVER = args.values.viya;
|
|
149
|
-
console.error(`[Note] VIYA_SERVER set from command line: ${args.values.viya}`);
|
|
150
|
-
}
|
|
151
199
|
|
|
152
|
-
if (args.values.authflow) {
|
|
153
|
-
process.env.AUTHFLOW = args.values.authflow;
|
|
154
|
-
console.error(`[Note] AUTHFLOW set from command line: ${args.values.authflow}`);
|
|
155
|
-
}
|
|
156
200
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
201
|
+
process.env.SAMESITE = 'Lax,secure';
|
|
202
|
+
process.env.APPHOST = '0.0.0.0';
|
|
203
|
+
process.env.APPNAME = 'sas-score-mcp-serverjs';
|
|
204
|
+
|
|
205
|
+
// Handle version flag
|
|
206
|
+
if (args.values.version) {
|
|
207
|
+
let pkgJson = JSON.parse(pkg);
|
|
208
|
+
console.error(pkgJson.version);
|
|
209
|
+
process.exit(0);
|
|
160
210
|
}
|
|
161
211
|
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
212
|
+
// copy the skills to directory based on the client name, so that different MCP clients can have different sets of skills if needed
|
|
213
|
+
// the -client indicates the current mcp client
|
|
214
|
+
console.error(`[Note] MCP client set to: ${process.env.CLIENT}`);
|
|
215
|
+
|
|
216
|
+
let client = process.env.CLIENT;
|
|
217
|
+
if (client != null) {
|
|
218
|
+
let destdir = '.' + client;
|
|
219
|
+
let skillsDest = join(os.homedir(), destdir,'skills');
|
|
220
|
+
const skillsSrc = join(__dirname, 'skills');
|
|
221
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
222
|
+
console.error('No skills directory found in this package.');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
226
|
+
|
|
227
|
+
const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
|
|
228
|
+
.filter(d => d.isDirectory())
|
|
229
|
+
.map(d => d.name);
|
|
230
|
+
|
|
231
|
+
if (skills.length === 0) {
|
|
232
|
+
console.error('[Note]No skills found to install.');
|
|
233
|
+
} else {
|
|
234
|
+
console.error(`Installing ${skills.length} skill(s) to ${skillsDest}...`);
|
|
235
|
+
for (const skill of skills) {
|
|
236
|
+
const src = join(skillsSrc, skill);
|
|
237
|
+
const dest = join(skillsDest, skill);
|
|
238
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
239
|
+
console.error(` installed: ${skill}`);
|
|
240
|
+
}
|
|
241
|
+
console.error(`\n installed in cli. ${client}`);
|
|
242
|
+
console.error(`\n${skills.length} skill(s) installed to ${skillsDest}`);
|
|
243
|
+
console.error('[Note] Skills are ready for use.');
|
|
244
|
+
|
|
245
|
+
}
|
|
165
246
|
}
|
|
166
247
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
170
251
|
/********************************* */
|
|
171
252
|
const BRAND = 'sas-score'
|
|
172
253
|
/********************************* */
|
|
@@ -175,16 +256,16 @@ let version = pkgJson.version;
|
|
|
175
256
|
let mcpType = process.env.MCPTYPE || 'http';
|
|
176
257
|
console.error(
|
|
177
258
|
`\nStarting MCP ServerJS - Version: ${pkgJson.version} - ${new Date().toISOString()}\n
|
|
178
|
-
brand: ${process.env.BRAND || BRAND}
|
|
179
|
-
mcpType: ${mcpType}
|
|
180
|
-
viyaServer: ${process.env.VIYA_SERVER}
|
|
259
|
+
brand: ${process.env.BRAND || BRAND}
|
|
260
|
+
mcpType: ${mcpType}
|
|
261
|
+
viyaServer: ${process.env.VIYA_SERVER}`
|
|
181
262
|
);
|
|
182
263
|
// session sessionCache
|
|
183
264
|
// For more robust caching consider products like Redis
|
|
184
265
|
// and storage provided by cloud providers
|
|
185
|
-
|
|
266
|
+
|
|
186
267
|
debugger;
|
|
187
|
-
let sessionCache = new NodeCache({ stdTTL: 24 *60*60, checkperiod: 2 * 60, useClones: false });
|
|
268
|
+
let sessionCache = new NodeCache({ stdTTL: 24 * 60 * 60, checkperiod: 2 * 60, useClones: false });
|
|
188
269
|
|
|
189
270
|
//
|
|
190
271
|
// Load environment variables from .env file if present
|
|
@@ -192,35 +273,25 @@ let sessionCache = new NodeCache({ stdTTL: 24 *60*60, checkperiod: 2 * 60, useCl
|
|
|
192
273
|
// stdio: set the env in the mcp config
|
|
193
274
|
// http: use dotenv-cli to load env before starting the mcp server
|
|
194
275
|
|
|
195
|
-
|
|
196
|
-
// need to tell core what transport to use(http or stdio)
|
|
197
|
-
|
|
198
|
-
// subclasses for sasQuery tool (special use case)
|
|
199
|
-
// to be replaced by the planned adding external tool definition capability
|
|
200
|
-
|
|
201
|
-
let subclassJson = [];
|
|
202
|
-
if (process.env.SUBCLASS != null) {
|
|
203
|
-
console.error(`Using subclass: ${process.env.SUBCLASS}`);
|
|
204
|
-
let subclass = process.env.SUBCLASS;
|
|
205
|
-
if (fs.existsSync(subclass)) {
|
|
206
|
-
console.error(`Loading subclass information from ${subclass}...`);
|
|
207
|
-
let s = fs.readFileSync(subclass, 'utf8');
|
|
208
|
-
subclassJson = JSON.parse(s);
|
|
209
|
-
console.error(`Loaded subclass: ${JSON.stringify(subclassJson, null, 2)}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
276
|
// setup base appEnv
|
|
213
277
|
// for stdio this is the _appContext
|
|
214
278
|
// for http each session a copy of this as appEnvTemplate is created in corehttp
|
|
215
279
|
|
|
216
|
-
// backward compability variables
|
|
217
|
-
let clientID = process.env.CLIENTID || process.env.CLIENTIDPW || null;
|
|
218
|
-
let clientSecret = process.env.CLIENTSECRET || process.env.CLIENTSECRETPW || null;
|
|
219
280
|
let https = process.env.HTTPS != null ? process.env.HTTPS.toUpperCase() : "FALSE";
|
|
281
|
+
let authExternal = false;
|
|
282
|
+
let authFlow = process.env.AUTHFLOW;
|
|
283
|
+
let mcpHost = process.env.MCPHOST;
|
|
284
|
+
|
|
285
|
+
if (authFlow === 'oauth' || authFlow === 'oauthclient') {
|
|
286
|
+
authFlow = 'bearer';
|
|
287
|
+
authExternal = (authFlow === 'oauthclient') ? true : false;
|
|
288
|
+
}
|
|
220
289
|
let autoLogon = process.env.AUTOLOGON != null ? process.env.AUTOLOGON.toUpperCase() : "FALSE";
|
|
221
290
|
const appEnvBase = {
|
|
222
291
|
version: version,
|
|
223
|
-
mcpType: mcpType,
|
|
292
|
+
mcpType: mcpType,
|
|
293
|
+
mcpClient: process.env.MCPCLIENT || 'github',
|
|
294
|
+
mcpHost: (process.env.MCPHOST == null) ? 'http://localhost:8080' : process.env.MCPHOST,
|
|
224
295
|
brand: (process.env.BRAND == null) ? BRAND : process.env.BRAND,
|
|
225
296
|
HTTPS: https,
|
|
226
297
|
SAS_CLI_PROFILE: process.env.SAS_CLI_PROFILE || 'Default',
|
|
@@ -228,27 +299,25 @@ const appEnvBase = {
|
|
|
228
299
|
SSLCERT: process.env.SSLCERT || null,
|
|
229
300
|
VIYACERT: process.env.VIYACERT || null,
|
|
230
301
|
|
|
231
|
-
AUTHFLOW:
|
|
302
|
+
AUTHFLOW: authFlow,
|
|
303
|
+
AUTHEXTERNAL: authExternal,
|
|
304
|
+
BEARERTOKEN: null,
|
|
232
305
|
AUTOLOGON: autoLogon,
|
|
233
306
|
VIYA_SERVER: process.env.VIYA_SERVER,
|
|
234
307
|
PORT: process.env.PORT || 8080,
|
|
235
308
|
USERNAME: process.env.USERNAME || null,
|
|
236
309
|
PASSWORD: process.env.PASSWORD || null,
|
|
237
|
-
CLIENTID:
|
|
238
|
-
CLIENTSECRET:
|
|
310
|
+
CLIENTID: process.env.CLIENTID || null,
|
|
311
|
+
CLIENTSECRET: process.env.CLIENTSECRET || null,
|
|
239
312
|
PKCE: process.env.PKCE || null,
|
|
240
313
|
|
|
241
314
|
TOKEN: process.env.TOKEN || null,
|
|
242
315
|
REFRESH_TOKEN: process.env.REFRESH_TOKEN || null,
|
|
243
316
|
TOKENFILE: process.env.TOKENFILE || null,
|
|
244
317
|
TLS_CREATE: process.env.TLS_CREATE || null,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
toolsets:
|
|
249
|
-
process.env.TOOLSETS != null
|
|
250
|
-
? process.env.TOOLSETS.split(',')
|
|
251
|
-
: ['default'],
|
|
318
|
+
CASSERVER: process.env.CASSERVER,
|
|
319
|
+
COMPUTECONTEXT: process.env.COMPUTECONTEXT,
|
|
320
|
+
|
|
252
321
|
// command line arguments
|
|
253
322
|
cliArgs: args.values,
|
|
254
323
|
// user defined tools
|
|
@@ -268,7 +337,8 @@ const appEnvBase = {
|
|
|
268
337
|
tlsOpts: null,
|
|
269
338
|
oauthInfo: null,
|
|
270
339
|
contexts: {
|
|
271
|
-
AUTHFLOW:
|
|
340
|
+
AUTHFLOW: authFlow,
|
|
341
|
+
AUTHEXTERNAL: authExternal,
|
|
272
342
|
host: process.env.VIYA_SERVER,
|
|
273
343
|
APPHOST: process.env.APPHOST || 'localhost',
|
|
274
344
|
APPNAME: process.env.APPNAME || 'sas-score-mcp-serverjs',
|
|
@@ -277,8 +347,8 @@ const appEnvBase = {
|
|
|
277
347
|
store: null, /* for restaf users */
|
|
278
348
|
storeConfig: {},
|
|
279
349
|
oauthInfo: null,
|
|
280
|
-
CLIENTID:
|
|
281
|
-
CLIENTSECRET:
|
|
350
|
+
CLIENTID: process.env.CLIENTID || null,
|
|
351
|
+
CLIENTSECRET: process.env.CLIENTSECRET || null,
|
|
282
352
|
pkce: process.env.PKCE || null,
|
|
283
353
|
casSession: null, /* restaf cas session object */
|
|
284
354
|
computeSession: null, /* restaf compute session object */
|
|
@@ -293,15 +363,14 @@ const appEnvBase = {
|
|
|
293
363
|
}
|
|
294
364
|
};
|
|
295
365
|
|
|
296
|
-
process.env.APPPORT=appEnvBase.PORT;
|
|
366
|
+
process.env.APPPORT = appEnvBase.PORT;
|
|
367
|
+
let useHapi = process.env.USEHAPI === 'TRUE' ? true : false;
|
|
368
|
+
appEnvBase.useHapi = useHapi;
|
|
297
369
|
|
|
298
370
|
// setup TLS options for viya calls
|
|
299
|
-
console.error('[Note]Viya SSL dir set to: ' + appEnvBase.VIYACERT);
|
|
300
371
|
appEnvBase.contexts.viyaCert = readCerts(appEnvBase.VIYACERT); /* appEnvBase.contexts.viyaCert is set here */
|
|
301
372
|
|
|
302
373
|
// setup TLS options for app server (expressMcpServer or hapiMcpServer)
|
|
303
|
-
|
|
304
|
-
console.error('[Note]App SSL dir set to: ' + appEnvBase.SSLCERT);
|
|
305
374
|
appEnvBase.tlsOpts = readCerts(appEnvBase.SSLCERT);
|
|
306
375
|
appEnvBase.contexts.appCert = appEnvBase.tlsOpts; /* just for completeness */
|
|
307
376
|
|
|
@@ -315,7 +384,7 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
315
384
|
console.error(`[Note]Loading token from file: ${appEnvBase.TOKENFILE}...`);
|
|
316
385
|
appEnvBase.TOKEN = fs.readFileSync(appEnvBase.TOKENFILE, { encoding: 'utf8' });
|
|
317
386
|
appEnvBase.AUTHFLOW = 'token';
|
|
318
|
-
appEnvBase.
|
|
387
|
+
appEnvBase.contexts.logonPayload = {
|
|
319
388
|
host: appEnvBase.VIYA_SERVER,
|
|
320
389
|
authType: 'server',
|
|
321
390
|
token: appEnvBase.TOKEN,
|
|
@@ -328,10 +397,6 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
328
397
|
}
|
|
329
398
|
|
|
330
399
|
|
|
331
|
-
|
|
332
|
-
// if authflow is cli or code, postpone getting logonPayload until needed
|
|
333
|
-
|
|
334
|
-
|
|
335
400
|
// setup mcpServer (both http and stdio use this)
|
|
336
401
|
// this is singleton - best practices recommend this
|
|
337
402
|
|
|
@@ -342,18 +407,55 @@ let appEnvTemplate = Object.assign({}, appEnvBase);
|
|
|
342
407
|
|
|
343
408
|
sessionCache.set('appEnvTemplate', appEnvTemplate);
|
|
344
409
|
|
|
410
|
+
// prime transport cache
|
|
345
411
|
let transports = {
|
|
346
412
|
"dummy": null
|
|
347
413
|
};
|
|
348
414
|
sessionCache.set('transports', transports);
|
|
415
|
+
let tokenlist = {
|
|
416
|
+
dummy: null
|
|
417
|
+
}
|
|
418
|
+
sessionCache.set('tokenlist', tokenlist);
|
|
349
419
|
|
|
350
420
|
// set this for stdio transport use
|
|
351
421
|
// dummy sessionId for use in the tools
|
|
352
|
-
|
|
353
|
-
console.error('[Note] appEnvBase is', JSON.stringify(appEnvBase, null,2));
|
|
422
|
+
|
|
354
423
|
// 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
424
|
let sessionId = randomUUID();
|
|
356
425
|
sessionCache.set(sessionId, appEnvBase);
|
|
426
|
+
sessionCache.set('currentId', sessionId);
|
|
427
|
+
|
|
428
|
+
console.error('===================================================================');
|
|
429
|
+
console.error(`MCP ServerJS - Version: ${pkgJson.version} - ${new Date().toISOString()}`);
|
|
430
|
+
console.error(`
|
|
431
|
+
Usage: sas-score-mcp-serverjs [options]
|
|
432
|
+
|
|
433
|
+
Options:
|
|
434
|
+
Minimal options:
|
|
435
|
+
VIYA_SERVER ${appEnvBase.VIYA_SERVER}
|
|
436
|
+
CLIENTID ${appEnvBase.CLIENTID}
|
|
437
|
+
|
|
438
|
+
MCP server options:
|
|
439
|
+
MCPTYPE ${appEnvBase.mcpType}
|
|
440
|
+
MCPHOST ${appEnvBase.mcpHost}
|
|
441
|
+
PORT ${appEnvBase.PORT}
|
|
442
|
+
HTTPS ${appEnvBase.contexts.HTTPS}
|
|
443
|
+
CLIENT ${appEnvBase.mcpClient}
|
|
444
|
+
|
|
445
|
+
Authentication options:
|
|
446
|
+
AUTHFLOW ${process.env.AUTHFLOW}
|
|
447
|
+
CLIENTSECRET ${appEnvBase.CLIENTSECRET}
|
|
448
|
+
PROFILE ${appEnvBase.SAS_CLI_PROFILE}
|
|
449
|
+
CONFIG ${appEnvBase.SAS_CLI_CONFIG}
|
|
450
|
+
|
|
451
|
+
Other options:
|
|
452
|
+
CASSERVER ${appEnvBase.CASSERVER}
|
|
453
|
+
COMPUTECONTEXT ${appEnvBase.COMPUTECONTEXT}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
`);
|
|
457
|
+
console
|
|
458
|
+
debugger;
|
|
357
459
|
if (mcpType === 'stdio') {
|
|
358
460
|
console.error('[Note] Setting up stdio transport with sessionId:', sessionId);
|
|
359
461
|
console.error('[Note] Used in setting up tools and some persistence(not all).');
|
|
@@ -390,7 +492,7 @@ function iconfig(envFile) {
|
|
|
390
492
|
});
|
|
391
493
|
return envData;
|
|
392
494
|
} catch (err) {
|
|
393
|
-
console.
|
|
495
|
+
console.error(err);
|
|
394
496
|
process.exit(0);
|
|
395
497
|
}
|
|
396
498
|
}
|
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-17",
|
|
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": [
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
"cli.js",
|
|
41
43
|
"openApi.json",
|
|
42
44
|
"openApi.yaml",
|
|
43
|
-
"skills"
|
|
45
|
+
"skills",
|
|
46
|
+
"scripts"
|
|
44
47
|
],
|
|
45
48
|
"dependencies": {
|
|
46
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -71,7 +74,6 @@
|
|
|
71
74
|
"@babel/preset-env": "^7.28.5",
|
|
72
75
|
"@types/debug": "^4.1.12",
|
|
73
76
|
"@types/node": "^25.0.3",
|
|
74
|
-
"npm-check-updates": "^19.2.0",
|
|
75
77
|
"rimraf": "^6.1.2",
|
|
76
78
|
"typescript": "^5.9.3"
|
|
77
79
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Score Skill Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The `score` skill is a generic scoring interface that automatically routes scoring requests to the appropriate tool based on the model type specified in the request.
|
|
5
|
+
|
|
6
|
+
## Syntax
|
|
7
|
+
```
|
|
8
|
+
score with model <name>.<type> [scenario =<key=value pairs>]
|
|
9
|
+
score <name>.<type> [scenario =<key=value pairs>]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Supported Types
|
|
13
|
+
- **job** — Route to `run-job` for job-based scoring
|
|
14
|
+
- **jobdef** — Route to `run-jobdef` for job definition-based scoring
|
|
15
|
+
- **mas** — Route to `model-score` (Model Aggregation Service)
|
|
16
|
+
- **scr** — Route to `scr-score` (Score Code Runtime container)
|
|
17
|
+
- **sas** — Route to `run-sas-program` (arbitrary SAS/SQL scoring)
|
|
18
|
+
|
|
19
|
+
## Usage Examples
|
|
20
|
+
|
|
21
|
+
### MAS Model Scoring
|
|
22
|
+
```
|
|
23
|
+
score with model churn.mas where scenario =age=45,income=60000
|
|
24
|
+
score mymodel.mas using age=45, income=60000
|
|
25
|
+
```
|
|
26
|
+
Routes to: `model-score` with model name and scenario parameters
|
|
27
|
+
|
|
28
|
+
### Job-Based Scoring
|
|
29
|
+
```
|
|
30
|
+
score with model monthly_scorer.job scenario =month=10,year=2025
|
|
31
|
+
score mymodel.job with month=10, year=2025
|
|
32
|
+
```
|
|
33
|
+
Routes to: `run-job` with job name and parameters
|
|
34
|
+
|
|
35
|
+
### Job Definition Scoring
|
|
36
|
+
```
|
|
37
|
+
score fraud_detector.jobdef using amount=500,merchant=online
|
|
38
|
+
score predictions.jobdef where scenario =x=1,y=2
|
|
39
|
+
```
|
|
40
|
+
Routes to: `run-jobdef` with jobdef name and parameters
|
|
41
|
+
|
|
42
|
+
### SCR (Score Code Runtime) Scoring
|
|
43
|
+
```
|
|
44
|
+
score https://scr-host/models/loan.scr using age=45,credit_score=700
|
|
45
|
+
score mymodel.scr where scenario =age=45,income=60000
|
|
46
|
+
```
|
|
47
|
+
Routes to: `scr-score` with SCR URL and scenario
|
|
48
|
+
|
|
49
|
+
### SAS Program Scoring
|
|
50
|
+
```
|
|
51
|
+
score predictions.sas where scenario =x=1,y=2
|
|
52
|
+
score my_scoring_code.sas using month=10,year=2025
|
|
53
|
+
```
|
|
54
|
+
Routes to: `run-sas-program` with scenario parameters
|
|
55
|
+
|
|
56
|
+
## Parameter Details
|
|
57
|
+
|
|
58
|
+
| Parameter | Required | Type | Description |
|
|
59
|
+
|-----------|----------|------|-------------|
|
|
60
|
+
| `model` | Yes | string | Model name with type suffix (e.g., `mymodel.mas`) |
|
|
61
|
+
| `scenario` | No | string\|object\|array | Input data as comma-separated key=value pairs |
|
|
62
|
+
| `type` | No | string | Type override (inferred from model name if not specified) |
|
|
63
|
+
| `prompt` | No | string | Full prompt for context |
|
|
64
|
+
| `context_data` | No | object | Contextual variables for fallback |
|
|
65
|
+
|
|
66
|
+
## Scenario Format
|
|
67
|
+
The scenario parameter accepts multiple formats:
|
|
68
|
+
|
|
69
|
+
**String format** (comma-separated):
|
|
70
|
+
```
|
|
71
|
+
age=45,income=60000,credit=700
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Object format**:
|
|
75
|
+
```javascript
|
|
76
|
+
{age: 45, income: 60000, credit: 700}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Array format** (batch scoring):
|
|
80
|
+
```javascript
|
|
81
|
+
[
|
|
82
|
+
{age: 45, income: 60000},
|
|
83
|
+
{age: 50, income: 75000},
|
|
84
|
+
{age: 35, income: 55000}
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Type Inference
|
|
89
|
+
The skill automatically infers the type from the model name:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
mymodel.mas → type = "mas"
|
|
93
|
+
scorer.job → type = "job"
|
|
94
|
+
detector.jobdef → type = "jobdef"
|
|
95
|
+
risk.scr → type = "scr"
|
|
96
|
+
predict.sas → type = "sas"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If the type is not specified in the model name or as a parameter, the skill will ask for clarification:
|
|
100
|
+
```
|
|
101
|
+
"Is this a mas, scr, job, jobdef, or sas model?"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Context-Based Scoring
|
|
105
|
+
If no scenario is provided, the skill can extract relevant variables from the conversation context:
|
|
106
|
+
|
|
107
|
+
1. Extracts available variables from context
|
|
108
|
+
2. Asks user for confirmation: *"I found these variables: [list]. Should I use them for scoring?"*
|
|
109
|
+
3. Proceeds with confirmed variables
|
|
110
|
+
|
|
111
|
+
## Return Values
|
|
112
|
+
The scoring response varies by type:
|
|
113
|
+
|
|
114
|
+
| Type | Returns |
|
|
115
|
+
|------|---------|
|
|
116
|
+
| **job** | Log output, tables created by job |
|
|
117
|
+
| **jobdef** | Log output, tables created by jobdef |
|
|
118
|
+
| **mas** | Predictions, probabilities, scores |
|
|
119
|
+
| **scr** | Predictions and metadata from SCR endpoint |
|
|
120
|
+
| **sas** | SAS execution output with results |
|
|
121
|
+
|
|
122
|
+
All responses include metadata indicating which tool was invoked.
|
|
123
|
+
|
|
124
|
+
## Error Handling
|
|
125
|
+
|
|
126
|
+
| Error | Message |
|
|
127
|
+
|-------|---------|
|
|
128
|
+
| Invalid type | "Unknown model type. Use: job, jobdef, mas, scr, or sas" |
|
|
129
|
+
| Missing model | "Please provide model name (e.g., score with model mymodel.mas)" |
|
|
130
|
+
| Invalid scenario | "Scenario must be key=value pairs separated by commas" |
|
|
131
|
+
| Routing failure | Backend error from invoked tool |
|
|
132
|
+
|
|
133
|
+
## Implementation Details
|
|
134
|
+
|
|
135
|
+
The skill is defined in `src/toolSet/scoreSkill.js` and:
|
|
136
|
+
- Parses model name to extract type
|
|
137
|
+
- Normalizes type names (e.g., `jobs` → `job`)
|
|
138
|
+
- Routes to appropriate tool handler
|
|
139
|
+
- Attaches scoring metadata to response
|
|
140
|
+
- Handles errors from backend tools
|
|
141
|
+
|
|
142
|
+
The skill is automatically registered in `makeTools.js` and available alongside other MCP tools.
|