@sassoftware/sas-score-mcp-serverjs 0.1.1 → 0.2.1
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/.env +14 -19
- package/.envx +13 -0
- package/CHANGES.md +6 -0
- package/Public/index.html +70 -0
- package/README.md +79 -10
- package/cli.js +61 -31
- package/devTest/code/.env +12 -0
- package/devTest/code/http.mcp.json +8 -0
- package/devTest/http/.env +13 -0
- package/devTest/http/http.mcp.json +8 -0
- package/devTest/stdio/.env +9 -0
- package/{mcpConfigurations/stdio.json → devTest/stdio/stdio.mcp.json} +7 -5
- package/{mcpConfigurations → devTest}/stdiodev.json +2 -1
- package/package.json +4 -1
- package/scripts/viyatls.sh +3 -0
- package/src/createMcpServer.js +23 -12
- package/src/{corehttp.js → expressMcpServer.js} +3 -3
- package/src/handleGetDelete.js +31 -0
- package/src/handleRequest.js +110 -0
- package/src/hapiMcpServer.js +94 -0
- package/src/toolHelpers/getLogonPayload.js +24 -5
- package/src/toolHelpers/getOpts.js +12 -4
- package/src/toolHelpers/getOptsViya.js +10 -4
- package/src/toolHelpers/getStoreOpts.js +0 -2
- package/src/toolHelpers/getToken.js +5 -5
- package/src/toolHelpers/readCerts.js +33 -0
- package/src/toolHelpers/refreshTokenOauth.js +53 -0
- package/src/toolSet/findJob.js +3 -1
- package/src/urlOpen.js +13 -0
- /package/{mcpConfigurations → devTest}/README.md +0 -0
- /package/{mcpConfigurations → devTest}/http.json +0 -0
package/.env
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
VIYA_SERVER
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
SSLCERT=c:\Users\kumar\.tls
|
|
16
|
-
VIYACERT=c:\Users\kumar\viyaCert
|
|
17
|
-
CAS_SERVER=cas-shared-default
|
|
18
|
-
COMPUTECONTEXT=SAS Job Execution compute context
|
|
19
|
-
|
|
1
|
+
PORT=8080
|
|
2
|
+
HTTPS=TRUE
|
|
3
|
+
MCPTYPE=http
|
|
4
|
+
AUTHFLOW=sascli
|
|
5
|
+
CLIENTID=mcpserver
|
|
6
|
+
CLIENTSECRET=jellico
|
|
7
|
+
# VIYA_SERVER= set globally
|
|
8
|
+
SSLCERT=c:\Users\kumar\.tls
|
|
9
|
+
VIYACERT=c:\Users\kumar\viyaCert\xf1
|
|
10
|
+
CAS_SERVER=cas-shared-default
|
|
11
|
+
# APPNAME defaults to sas-score-mcp-serverjs but you can override it here
|
|
12
|
+
APPNAME=mcpserver
|
|
13
|
+
COMPUTECONTEXT=SAS Job Execution compute context
|
|
14
|
+
SAMESITE=Lax,secure
|
package/.envx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
PORT=8080
|
|
3
|
+
HTTPS=FALSE
|
|
4
|
+
MCPTYPE=http
|
|
5
|
+
|
|
6
|
+
AUTHFLOW=sascli
|
|
7
|
+
SAS_CLI_PROFILE=xf1
|
|
8
|
+
SAS_CLI_CONFIG=c:\Users\kumar
|
|
9
|
+
SSLCERT=c:\Users\kumar\.tls
|
|
10
|
+
VIYACERT=c:\Users\kumar\viyaCert\xf1\viya
|
|
11
|
+
CAS_SERVER=cas-shared-default
|
|
12
|
+
COMPUTECONTEXT=SAS Job Execution compute context
|
|
13
|
+
|
package/CHANGES.md
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
All notable changes to this project will be documented in this file in accordance with semantic versioning.
|
|
3
|
+
|
|
4
|
+
## V 0.2.0
|
|
5
|
+
|
|
6
|
+
1. Added support for authorization_code flow.(AUTHFLOW=code)
|
|
7
|
+
2. Logon dialog is auto started.
|
|
8
|
+
3. Marked as experimental until further testing is completed.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>sas-score-mcp-server</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: Arial, sans-serif;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dialog {
|
|
21
|
+
position: fixed;
|
|
22
|
+
left: 50%;
|
|
23
|
+
right: auto;
|
|
24
|
+
top: 0;
|
|
25
|
+
transform: translateX(-50%);
|
|
26
|
+
width: fit-content;
|
|
27
|
+
height: fit-content;
|
|
28
|
+
border: none;
|
|
29
|
+
border-radius: 4px;
|
|
30
|
+
padding: 16px;
|
|
31
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dialog::backdrop {
|
|
35
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dialog h2 {
|
|
39
|
+
font-size: 18px;
|
|
40
|
+
margin-bottom: 12px;
|
|
41
|
+
color: #000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dialog p {
|
|
45
|
+
font-family: 'Courier New', monospace;
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
width: fit-content;
|
|
48
|
+
line-height: 1.6;
|
|
49
|
+
color: #333;
|
|
50
|
+
word-wrap: break-word;
|
|
51
|
+
white-space: pre-wrap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Window styling to show it's 10px larger than dialog */
|
|
55
|
+
body::before {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<dialog open>
|
|
62
|
+
<h2>sas-score-mcp-server</h2>
|
|
63
|
+
<p>The mcp server is now ready for use. </p>
|
|
64
|
+
<p>You can close this window</p>
|
|
65
|
+
<p>For information on the tools see this documentation link:</p>
|
|
66
|
+
<a href="https://github.com/sassoftware/sas-score-mcp-serverjs/wiki " target="_blank">Summary of tools </a>
|
|
67
|
+
</p>
|
|
68
|
+
</dialog>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ See this [quick reference](sas-mcp-tools-reference.md) for details.
|
|
|
25
25
|
SAS developers who want to extend the capabilities of the server with their own tools. See the [guide](tool-developer-guide.md) for details.
|
|
26
26
|
|
|
27
27
|
## Configuration Variables
|
|
28
|
-
Typically these are set either in the .env file or as environment variables (or both). This is full list of the configuration variables used the mcp server.
|
|
28
|
+
Typically these are set either in the .env file or as environment variables (or both). This is full list of the configuration variables used the mcp server. You will need only a subset of these for the different [transport,authentication] schemes
|
|
29
29
|
|
|
30
30
|
```env
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ Typically these are set either in the .env file or as environment variables (or
|
|
|
33
33
|
# http is useful for remote mcp servers
|
|
34
34
|
# If running locally, recommend stdio
|
|
35
35
|
|
|
36
|
-
MCPTYPE
|
|
36
|
+
MCPTYPE=<stdio|http>
|
|
37
37
|
|
|
38
38
|
# Port for http transport(default is 8080)
|
|
39
39
|
|
|
@@ -42,8 +42,7 @@ PORT=8080
|
|
|
42
42
|
# If transport is http, optionally specify if the server
|
|
43
43
|
# is using http or https
|
|
44
44
|
|
|
45
|
-
HTTPS=FALSE
|
|
46
|
-
|
|
45
|
+
HTTPS=TRUE|FALSE
|
|
47
46
|
|
|
48
47
|
# Viya Authentication
|
|
49
48
|
# The mcp server support different ways to authenticate(see section on Authentication)
|
|
@@ -51,9 +50,10 @@ HTTPS=FALSE
|
|
|
51
50
|
# * sascli * will look for tokens created with sas-viya cli
|
|
52
51
|
# * token * a custom token
|
|
53
52
|
# * password * userid/password
|
|
54
|
-
# *
|
|
53
|
+
# * code * Oauth using authorization_code flow(pkce not supported in this release)
|
|
54
|
+
|
|
55
|
+
AUTHFLOW=sascli|token|password|code
|
|
55
56
|
|
|
56
|
-
AUTHFLOW=sascli
|
|
57
57
|
SAS_CLI_CONFIG=your-home-directory
|
|
58
58
|
SAS_CLI_PROFILE=your-sas-cli-profile
|
|
59
59
|
|
|
@@ -64,9 +64,11 @@ VIYA_SERVER= your Viya server url
|
|
|
64
64
|
# if AUTHFLOW=token, specify the file with the token
|
|
65
65
|
TOKENFILE=
|
|
66
66
|
|
|
67
|
-
# if password flow specify these
|
|
67
|
+
# if password flow or oauth flow specify these
|
|
68
68
|
CLIENTID=
|
|
69
69
|
CLIENTSECRET=
|
|
70
|
+
|
|
71
|
+
# specify this if password AUTHFLOW
|
|
70
72
|
PASSWORD=
|
|
71
73
|
|
|
72
74
|
# When HTTPS is TRUE, specify the folder with SSL certificates for the mcp server
|
|
@@ -130,8 +132,67 @@ Set the env TOKENFILE to a file containing the token.
|
|
|
130
132
|
There seems to be a pattern of using a long-lived token.
|
|
131
133
|
If this is your use case, set the TOKENFILE to a file containing this token.
|
|
132
134
|
|
|
133
|
-
### Oauth
|
|
134
|
-
|
|
135
|
+
### Oauth - (experimental) Authentication handled by the mcp server
|
|
136
|
+
|
|
137
|
+
In this approach, the mcp client does not participate in the Oauth authentication process. It is handled by the mcp server at startup.
|
|
138
|
+
|
|
139
|
+
> This is marked as experimental since the testing is not complete
|
|
140
|
+
|
|
141
|
+
#### SAS viya setup.
|
|
142
|
+
|
|
143
|
+
Create a Oauth client with the following properties
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
{
|
|
147
|
+
auth flow: authorization_code|password
|
|
148
|
+
clientid: <your client id>
|
|
149
|
+
clientsecret: <some client secret - pkce not supported at this time>
|
|
150
|
+
redirect: https://localhost:8080/mcpserver
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#### Use an .env file as follows(sample values shown)
|
|
154
|
+
|
|
155
|
+
```env
|
|
156
|
+
PORT=8080
|
|
157
|
+
AUTHFLOW=code
|
|
158
|
+
SSLCERT=c:\Users\kumar\.tls
|
|
159
|
+
VIYACERT=c:\Users\kumar\viyaCert
|
|
160
|
+
CAS_SERVER=cas-shared-default
|
|
161
|
+
COMPUTECONTEXT=SAS Job Execution compute context
|
|
162
|
+
|
|
163
|
+
PORT=8080
|
|
164
|
+
HTTPS=true
|
|
165
|
+
MCPTYPE=http
|
|
166
|
+
USELOGON=FALSE
|
|
167
|
+
USETOKEN=TRUE
|
|
168
|
+
APPNAME=sas-score-mcp-serverjs
|
|
169
|
+
|
|
170
|
+
CLIENTID=mcpserver
|
|
171
|
+
CLIENTSECRET=xxxxxx
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# SAMESITE=Lax,secure
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Usage
|
|
179
|
+
|
|
180
|
+
Start the server with this command:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
npx @sassoftware/sas-score-mcp-serverjs@latest
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Then visit this site on your browser:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
https://localhost:8080/mcpserver
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
You will be prompted to logon to SAS Viya.
|
|
193
|
+
A dialog will be displayed if the logon was successful.
|
|
194
|
+
Icon this window and proceed to your mcp client
|
|
195
|
+
|
|
135
196
|
|
|
136
197
|
## Transport Methods
|
|
137
198
|
This server supports both stdio and http transport methods.
|
|
@@ -238,6 +299,14 @@ The implication of this design choice is felt most when the tool needs is creati
|
|
|
238
299
|
## Other Useful Tips
|
|
239
300
|
|
|
240
301
|
### mkcert
|
|
302
|
+
|
|
303
|
+
### Install
|
|
304
|
+
|
|
305
|
+
1. Visit this [site](https://github.com/FiloSottile/mkcert/releases)
|
|
306
|
+
2. Download the proper version
|
|
307
|
+
- rename the file as mkcert (with proper exetension for your os)
|
|
308
|
+
- move it to a directory that is in the PATH value
|
|
309
|
+
|
|
241
310
|
To create a self-signed certificate for localhost:
|
|
242
311
|
|
|
243
312
|
```sh
|
|
@@ -251,7 +320,7 @@ Now go to the location where you want to store the certificates.
|
|
|
251
320
|
Then create the certificates:
|
|
252
321
|
|
|
253
322
|
```sh
|
|
254
|
-
mkcert
|
|
323
|
+
mkcert localhost 127:0.0.1 ::1
|
|
255
324
|
```
|
|
256
325
|
|
|
257
326
|
One last step for windows nodejs users.
|
package/cli.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
import coreSSE from './src/coreSSE.js';
|
|
11
|
-
import
|
|
11
|
+
import expressMcpServer from './src/expressMcpServer.js';
|
|
12
|
+
import hapiMcpServer from './src/hapiMcpServer.js';
|
|
13
|
+
|
|
12
14
|
import createMcpServer from './src/createMcpServer.js';
|
|
13
15
|
// import dotenvExpand from 'dotenv-expand';
|
|
14
16
|
import fs from 'fs';
|
|
@@ -27,9 +29,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
27
29
|
|
|
28
30
|
let pkg = fs.readFileSync(__dirname + '/package.json', 'utf8');
|
|
29
31
|
|
|
30
|
-
if (process.env.ENVFILE === '
|
|
32
|
+
if (process.env.ENVFILE === 'FALSE') {
|
|
31
33
|
//use this when using remote mcp server and no .env file is desired
|
|
32
|
-
console.error('[Note]: Skipping .env file as ENVFILE is set to
|
|
34
|
+
console.error('[Note]: Skipping .env file as ENVFILE is set to FALSE...');
|
|
33
35
|
} else {
|
|
34
36
|
let envf = __dirname + '\\.env';
|
|
35
37
|
console.error(envf);
|
|
@@ -38,13 +40,17 @@ if (process.env.ENVFILE === 'NONE') {
|
|
|
38
40
|
let e = iconfig(envf); // avoid dotenv since it writes to console.log
|
|
39
41
|
console.error('[Note]: Environment variables loaded from .env file...');
|
|
40
42
|
console.error('Loaded env variables:', e);
|
|
41
|
-
|
|
43
|
+
// dotenvExpand.expand(e);
|
|
42
44
|
} else {
|
|
43
45
|
console.error(
|
|
44
46
|
'[Note]: No .env file found, Using default environment variables...'
|
|
45
47
|
);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
if (process.env.APPHOST == null) {
|
|
52
|
+
process.env.APPHOST = 'localhost';
|
|
53
|
+
}
|
|
48
54
|
/********************************* */
|
|
49
55
|
const BRAND = 'sas-score'
|
|
50
56
|
/********************************* */
|
|
@@ -90,14 +96,16 @@ if (process.env.SUBCLASS != null) {
|
|
|
90
96
|
// setup base appEnv
|
|
91
97
|
// for stdio this is the _appContext
|
|
92
98
|
// for http each session a copy of this as appEnvTemplate is created in corehttp
|
|
99
|
+
|
|
100
|
+
// backward compability variables
|
|
101
|
+
let clientID = process.env.CLIENTID || process.env.CLIENTIDPW || null;
|
|
102
|
+
let clientSecret = process.env.CLIENTSECRET || process.env.CLIENTSECRETPW || null;
|
|
103
|
+
let https = process.env.HTTPS != null ? process.env.HTTPS.toUpperCase() : "FALSE";
|
|
93
104
|
const appEnvBase = {
|
|
94
105
|
version: version,
|
|
95
|
-
mcpType: mcpType,
|
|
106
|
+
mcpType: mcpType,
|
|
96
107
|
brand: (process.env.BRAND == null) ? BRAND : process.env.BRAND,
|
|
97
|
-
HTTPS:
|
|
98
|
-
process.env.HTTPS != null && process.env.HTTPS.toUpperCase() === 'TRUE'
|
|
99
|
-
? true
|
|
100
|
-
: false,
|
|
108
|
+
HTTPS: https,
|
|
101
109
|
SAS_CLI_PROFILE: process.env.SAS_CLI_PROFILE || 'Default',
|
|
102
110
|
SAS_CLI_CONFIG: process.env.SAS_CLI_CONFIG || process.env.HOME, // default to user home directory
|
|
103
111
|
SSLCERT: process.env.SSLCERT || null,
|
|
@@ -108,8 +116,10 @@ const appEnvBase = {
|
|
|
108
116
|
PORT: process.env.PORT || 8080,
|
|
109
117
|
USERNAME: process.env.USERNAME || null,
|
|
110
118
|
PASSWORD: process.env.PASSWORD || null,
|
|
111
|
-
CLIENTID:
|
|
112
|
-
CLIENTSECRET:
|
|
119
|
+
CLIENTID: clientID,
|
|
120
|
+
CLIENTSECRET: clientSecret,
|
|
121
|
+
PKCE: process.env.PKCE || null,
|
|
122
|
+
|
|
113
123
|
TOKEN: process.env.TOKEN || null,
|
|
114
124
|
REFRESH_TOKEN: process.env.REFRESH_TOKEN || null,
|
|
115
125
|
TOKENFILE: process.env.TOKENFILE || null,
|
|
@@ -136,9 +146,20 @@ const appEnvBase = {
|
|
|
136
146
|
logonPayload: null,
|
|
137
147
|
bearerToken: null,
|
|
138
148
|
tlsOpts: null,
|
|
149
|
+
oauthInfo: null,
|
|
139
150
|
contexts: {
|
|
151
|
+
AUTHFLOW: process.env.AUTHFLOW || 'sascli',
|
|
152
|
+
host: process.env.VIYA_SERVER,
|
|
153
|
+
APPHOST: process.env.APPHOST || 'localhost',
|
|
154
|
+
APPNAME: process.env.APPNAME || 'sas-score-mcp-serverjs',
|
|
155
|
+
PORT: process.env.PORT || 8080,
|
|
156
|
+
HTTPS: https,
|
|
140
157
|
store: null, /* for restaf users */
|
|
141
158
|
storeConfig: {},
|
|
159
|
+
oauthInfo: null,
|
|
160
|
+
CLIENTID: clientID,
|
|
161
|
+
CLIENTSECRET: clientSecret,
|
|
162
|
+
pkce: process.env.PKCE || null,
|
|
142
163
|
casSession: null, /* restaf cas session object */
|
|
143
164
|
computeSession: null, /* restaf compute session object */
|
|
144
165
|
viyaCert: null, /* ssl/tsl certificates to connect to viya */
|
|
@@ -151,6 +172,8 @@ const appEnvBase = {
|
|
|
151
172
|
}
|
|
152
173
|
};
|
|
153
174
|
|
|
175
|
+
process.env.APPPORT=appEnvBase.PORT;
|
|
176
|
+
|
|
154
177
|
// setup TLS options for viya calls
|
|
155
178
|
|
|
156
179
|
console.error('[Note]Viya SSL dir set to: ' + appEnvBase.VIYACERT);
|
|
@@ -166,7 +189,7 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
166
189
|
console.error(`[Note]Loading token from file: ${appEnvBase.TOKENFILE}...`);
|
|
167
190
|
appEnvBase.TOKEN = fs.readFileSync(appEnvBase.TOKENFILE, { encoding: 'utf8' });
|
|
168
191
|
appEnvBase.AUTHFLOW = 'token';
|
|
169
|
-
appEnvBase.
|
|
192
|
+
appEnvBase.appContexts.logonPayload = {
|
|
170
193
|
host: appEnvBase.VIYA_SERVER,
|
|
171
194
|
authType: 'server',
|
|
172
195
|
token: appEnvBase.TOKEN,
|
|
@@ -192,6 +215,7 @@ if (appEnvBase.REFRESH_TOKEN != null) {
|
|
|
192
215
|
}
|
|
193
216
|
}
|
|
194
217
|
|
|
218
|
+
// if authflow is cli or code, postpone getting logonPayload until needed
|
|
195
219
|
|
|
196
220
|
|
|
197
221
|
// setup mcpServer (both http and stdio use this)
|
|
@@ -209,6 +233,7 @@ sessionCache.set('transports', transports);
|
|
|
209
233
|
|
|
210
234
|
// set this for stdio transport use
|
|
211
235
|
// dummy sessionId for use in the tools
|
|
236
|
+
let useHapi = process.env.AUTHFLOW === 'code' ? true : false;
|
|
212
237
|
if (mcpType === 'stdio') {
|
|
213
238
|
let sessionId = randomUUID();
|
|
214
239
|
sessionCache.set('currentId', sessionId);
|
|
@@ -219,31 +244,36 @@ if (mcpType === 'stdio') {
|
|
|
219
244
|
|
|
220
245
|
} else {
|
|
221
246
|
console.error('[Note] Starting HTTP MCP server...');
|
|
222
|
-
|
|
223
|
-
|
|
247
|
+
if (useHapi === true) {
|
|
248
|
+
await hapiMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
249
|
+
console.error('[Note] Using HAPI HTTP server...')
|
|
250
|
+
} else {
|
|
251
|
+
await expressMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
252
|
+
console.error('[Note] MCP HTTP server started on port ' + appEnvBase.PORT);
|
|
253
|
+
}
|
|
224
254
|
}
|
|
225
255
|
|
|
226
256
|
// custom reader for .env file to avoid dotenv logging to console
|
|
227
257
|
function iconfig(envFile) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
258
|
+
try {
|
|
259
|
+
let data = fs.readFileSync(envFile, 'utf8');
|
|
260
|
+
let d = data.split(/\r?\n/);
|
|
231
261
|
let envData = {};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
d.forEach(l => {
|
|
263
|
+
if (l.length > 0 && l.indexOf('#') === -1) {
|
|
264
|
+
let la = l.split('=');
|
|
265
|
+
let envName = la[0];
|
|
266
|
+
if (la.length === 2 && la[1].length > 0) {
|
|
267
|
+
let t = la[1].trim();
|
|
268
|
+
process.env[envName] = t;
|
|
239
269
|
envData[envName] = t;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
243
273
|
return envData;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.log(err);
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
248
278
|
}
|
|
249
279
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
PORT=8080
|
|
3
|
+
HTTPS=FALSE
|
|
4
|
+
MCPTYPE=http
|
|
5
|
+
AUTHFLOW=sascli
|
|
6
|
+
SAS_CLI_PROFILE=xf1
|
|
7
|
+
SAS_CLI_CONFIG=c:\Users\kumar
|
|
8
|
+
SSLCERT=c:\Users\kumar\.tls
|
|
9
|
+
VIYACERT=c:\Users\kumar\viyaCert\xf1\viya
|
|
10
|
+
CAS_SERVER=cas-shared-default
|
|
11
|
+
COMPUTECONTEXT=SAS Job Execution compute context
|
|
12
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
PORT=8080
|
|
3
|
+
HTTPS=TRUE
|
|
4
|
+
MCPTYPE=http
|
|
5
|
+
AUTHFLOW=code
|
|
6
|
+
CLIENTID=mcpserver
|
|
7
|
+
CLIENTSECRET=jellico
|
|
8
|
+
# VIYA_SERVER= set globally
|
|
9
|
+
SSLCERT=c:\Users\kumar\.tls
|
|
10
|
+
VIYACERT=c:\Users\kumar\viyaCert\xf1\viya
|
|
11
|
+
CAS_SERVER=cas-shared-default
|
|
12
|
+
COMPUTECONTEXT=SAS Job Execution compute context
|
|
13
|
+
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
"servers": {
|
|
3
3
|
"sasmcp": {
|
|
4
4
|
"type": "stdio",
|
|
5
|
-
"command": "
|
|
5
|
+
"command": "node",
|
|
6
6
|
"args": [
|
|
7
|
-
"
|
|
7
|
+
"c:\\dev\\github\\sas-score-mcp-serverjs\\cli.js"
|
|
8
8
|
],
|
|
9
9
|
"env": {
|
|
10
10
|
"MCPTYPE": "stdio",
|
|
11
|
-
"AUTHFLOW": "sascli",
|
|
12
|
-
"SAS_CLI_PROFILE": "
|
|
11
|
+
"AUTHFLOW": "sascli",
|
|
12
|
+
"SAS_CLI_PROFILE": "xf1",
|
|
13
13
|
"SAS_CLI_CONFIG": "c:\\Users\\kumar",
|
|
14
14
|
"SSLCERT": "c:\\Users\\kumar\\.tls",
|
|
15
|
+
"VIYACERT": "c:\\Users\\kumar\\viyaCert\\xf1\\viya",
|
|
16
|
+
"CAS_SERVER": "cas-shared-default",
|
|
15
17
|
"COMPUTECONTEXT": "SAS Job Execution compute context",
|
|
16
|
-
"
|
|
18
|
+
"ENVFILE": "FALSE"
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "stdio",
|
|
5
5
|
"command": "node",
|
|
6
6
|
"args": [
|
|
7
|
-
"c:\\dev\\
|
|
7
|
+
"c:\\dev\\github\\mcp-serverjs\\cli.js"
|
|
8
8
|
],
|
|
9
9
|
"env": {
|
|
10
10
|
"MCPTYPE": "stdio",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"SAS_CLI_PROFILE": "00m",
|
|
13
13
|
"SAS_CLI_CONFIG": "c:\\Users\\kumar",
|
|
14
14
|
"SSLCERT": "c:\\Users\\kumar\\.tls",
|
|
15
|
+
"VIYACERT": "c:\\Users\\kumar\\.tls",
|
|
15
16
|
"COMPUTECONTEXT": "SAS Job Execution compute context",
|
|
16
17
|
"CAS_SERVER": "cas-shared-default"
|
|
17
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassoftware/sas-score-mcp-serverjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A mcp server for SAS Viya",
|
|
5
5
|
"author": "Deva Kumar <deva.kumar@sas.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@sassoftware/restaf": "^5.6.0",
|
|
41
41
|
"@sassoftware/restafedit": "^3.11.1-10",
|
|
42
42
|
"@sassoftware/restaflib": "^5.6.0",
|
|
43
|
+
"@sassoftware/viya-serverjs": "^0.6.3-0",
|
|
43
44
|
"axios": "^1.13.2",
|
|
44
45
|
"body-parser": "^2.2.1",
|
|
45
46
|
"cors": "^2.8.5",
|
|
@@ -51,6 +52,8 @@
|
|
|
51
52
|
"helmet": "^8.1.0",
|
|
52
53
|
"mcp-framework": "^0.2.16",
|
|
53
54
|
"node-cache": "^5.1.2",
|
|
55
|
+
"open": "^11.0.0",
|
|
56
|
+
"puppeteer": "^24.34.0",
|
|
54
57
|
"selfsigned": "^5.2.0",
|
|
55
58
|
"undici": "^7.16.0",
|
|
56
59
|
"uuid": "^13.0.0",
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
kubectl cp $(kubectl get pod | grep "sas-consul-server-0" | awk -F" " '{print $1}'):security/ca.crt ./ca.crt
|
|
2
|
+
kubectl cp $(kubectl get pod | grep "sas-consul-server-0" | awk -F" " '{print $1}'):security/tls.crt ./tls.crt
|
|
3
|
+
kubectl cp $(kubectl get pod | grep "sas-consul-server-0" | awk -F" " '{print $1}'):security/tls.key ./tls.key
|
package/src/createMcpServer.js
CHANGED
|
@@ -16,7 +16,7 @@ import makeTools from "./toolSet/makeTools.js";
|
|
|
16
16
|
import getLogonPayload from "./toolHelpers/getLogonPayload.js";
|
|
17
17
|
|
|
18
18
|
async function createMcpServer(cache, _appContext) {
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
let mcpServer = new McpServer(
|
|
21
21
|
{
|
|
22
22
|
name: "sasmcp",
|
|
@@ -40,20 +40,31 @@ async function createMcpServer(cache, _appContext) {
|
|
|
40
40
|
let _appContext = cache.get(currentId);
|
|
41
41
|
let params;
|
|
42
42
|
// get Viya token
|
|
43
|
+
|
|
44
|
+
let errorStatus = cache.get('errorStatus');
|
|
45
|
+
if (errorStatus) {
|
|
46
|
+
return { isError: true, content: [{ type: 'text', text: errorStatus }] }
|
|
47
|
+
};
|
|
48
|
+
if (_appContext.AUTHFLOW === 'code' && _appContext.contexts.oauthInfo == null) {
|
|
49
|
+
return { isError: true, content: [{ type: 'text', text: 'Please visit https://localhost:8080/mcpserver to connect to Viya. Then try again.' }] }
|
|
50
|
+
}
|
|
51
|
+
console.error("Getting logon payload for tool with session ID:", currentId);
|
|
43
52
|
_appContext.contexts.logonPayload = await getLogonPayload(_appContext);
|
|
44
53
|
if (_appContext.contexts.logonPayload == null) {
|
|
45
54
|
return { isError: true, content: [{ type: 'text', text: 'Unable to get authentication token for SAS Viya. Please check your configuration.' }] }
|
|
55
|
+
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
// create enhanced appContext for tool
|
|
59
|
+
if (args == null) {
|
|
60
|
+
params = { _appContext: _appContext.contexts };
|
|
61
|
+
} else {
|
|
62
|
+
params = Object.assign({}, args, { _appContext: _appContext.contexts });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// call the actual tool handler
|
|
66
|
+
debugger;
|
|
67
|
+
let r = await builtin(params);
|
|
57
68
|
return r;
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -62,9 +73,9 @@ async function createMcpServer(cache, _appContext) {
|
|
|
62
73
|
let toolNames = [];
|
|
63
74
|
toolSet.forEach((tool, i) => {
|
|
64
75
|
let toolName = _appContext.brand + '-' + tool.name;
|
|
65
|
-
|
|
76
|
+
// console.error(`\n[Note] Registering tool ${i + 1} : ${toolName}`);
|
|
66
77
|
let toolHandler = wrapf(cache, tool.handler);
|
|
67
|
-
|
|
78
|
+
|
|
68
79
|
mcpServer.tool(toolName, tool.description, tool.schema, toolHandler);
|
|
69
80
|
toolNames.push(toolName);
|
|
70
81
|
});
|
|
@@ -21,7 +21,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
21
21
|
|
|
22
22
|
// setup express server
|
|
23
23
|
|
|
24
|
-
async function
|
|
24
|
+
async function expressMcpServer(mcpServer, cache, currentAppEnvContext) {
|
|
25
25
|
// setup for change to persistence session
|
|
26
26
|
let headerCache = {};
|
|
27
27
|
|
|
@@ -228,7 +228,7 @@ async function corehttp(mcpServer, cache, currentAppEnvContext) {
|
|
|
228
228
|
let appServer;
|
|
229
229
|
|
|
230
230
|
// get TLS options
|
|
231
|
-
if (appEnvBase.HTTPS ===
|
|
231
|
+
if (appEnvBase.HTTPS === 'TRUE') {
|
|
232
232
|
//appEnvBase.tlsOpts = getOpts(appEnvBase);
|
|
233
233
|
if (appEnvBase.tlsOpts == null) {
|
|
234
234
|
appEnvBase.tlsOpts = await getTls(appEnvBase);
|
|
@@ -334,4 +334,4 @@ async function corehttp(mcpServer, cache, currentAppEnvContext) {
|
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
export default
|
|
337
|
+
export default expressMcpServer;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
async function handleGetDelete(mcpServer, cache, req, h) {
|
|
7
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
8
|
+
console.error("Handling GET/DELETE for session ID:", sessionId);
|
|
9
|
+
let transports = cache.get("transports");
|
|
10
|
+
let transport = transports[sessionId];
|
|
11
|
+
if (!sessionId || transport == null) {
|
|
12
|
+
console.error('[Note] Looks like a fresh start - no session id or transport found');
|
|
13
|
+
h.abandon;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (req.method === "GET") {
|
|
17
|
+
// You can customize the response as needed
|
|
18
|
+
await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
|
|
19
|
+
return h.abandon;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (req.method === "DELETE") {
|
|
23
|
+
console.error("Deleting transport and cache for session ID:", sessionId);
|
|
24
|
+
delete transports[sessionId];
|
|
25
|
+
cache.del(sessionId);
|
|
26
|
+
return h.response(`[Info] In DELETE: Session ID ${sessionId} deleted`).code(201);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
export default handleGetDelete;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
|
|
5
|
+
async function handleRequest(mcpServer, cache, req, h, credentials) {
|
|
6
|
+
let headerCache = {};
|
|
7
|
+
let transport;
|
|
8
|
+
let transports = cache.get("transports");
|
|
9
|
+
try {
|
|
10
|
+
|
|
11
|
+
headerCache = customHeaders(req, h);
|
|
12
|
+
let sessionId = req.headers["mcp-session-id"];
|
|
13
|
+
|
|
14
|
+
// we have session id, get existing transport
|
|
15
|
+
|
|
16
|
+
if (sessionId != null) {
|
|
17
|
+
/* existing transport */
|
|
18
|
+
transport = transports[sessionId];
|
|
19
|
+
if (transport == null) {
|
|
20
|
+
h.response({ isError: true, content: [{ type: 'text', text: 'Session not found. Please re-initialize the MCP client.' }] }).code(400).type('application/json');
|
|
21
|
+
return h.abandon;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (sessionId != null && transport != null) {
|
|
26
|
+
// post the curren session - used to pass _appContext to tools
|
|
27
|
+
cache.set("currentId", sessionId);
|
|
28
|
+
|
|
29
|
+
// get app context for session
|
|
30
|
+
let _appContext = cache.get(sessionId);
|
|
31
|
+
|
|
32
|
+
//if first prompt on a sessionid, create app context
|
|
33
|
+
|
|
34
|
+
if (_appContext == null) {
|
|
35
|
+
console.error("[Note] Creating new app context for session ID:", sessionId);
|
|
36
|
+
let appEnvTemplate = cache.get("appEnvTemplate");
|
|
37
|
+
_appContext = Object.assign({}, appEnvTemplate, headerCache);
|
|
38
|
+
_appContext.contexts.oauthInfo = credentials;
|
|
39
|
+
cache.set(sessionId, _appContext);
|
|
40
|
+
}
|
|
41
|
+
console.error("[Note] Using existing transport for session ID:", sessionId);
|
|
42
|
+
debugger;
|
|
43
|
+
console.error("calling transport.handleRequest");
|
|
44
|
+
return await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// initialize request
|
|
48
|
+
else if (!sessionId && isInitializeRequest(req.payload)) {
|
|
49
|
+
// create transport
|
|
50
|
+
console.error("[Note] Initializing new transport for MCP request...");
|
|
51
|
+
transport = new StreamableHTTPServerTransport({
|
|
52
|
+
sessionIdGenerator: () => randomUUID(),
|
|
53
|
+
enableJsonResponse: true,
|
|
54
|
+
onsessioninitialized: (sessionId) => {
|
|
55
|
+
// Store the transport by session ID
|
|
56
|
+
transports[sessionId] = transport;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
// Clean up transport when closed
|
|
60
|
+
transport.onclose = () => {
|
|
61
|
+
if (transport.sessionId) {
|
|
62
|
+
delete transports[transport.sessionId];
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
console.error("[Note] Connecting mcpServer to new transport...");
|
|
66
|
+
await mcpServer.connect(transport);
|
|
67
|
+
|
|
68
|
+
// Save transport data and app context for use in tools
|
|
69
|
+
cache.set("transports", transports);
|
|
70
|
+
return await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
|
|
71
|
+
// cache transport
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error("Error handling MCP request:", error);
|
|
78
|
+
let r = { isError: true, content: [{ type: 'text', text: 'Internal server error occurred while processing the request.' }] };
|
|
79
|
+
return h.response(r).code(500).type('application/json');
|
|
80
|
+
}
|
|
81
|
+
function customHeaders(req, h) {
|
|
82
|
+
|
|
83
|
+
// process any new header information
|
|
84
|
+
|
|
85
|
+
// Allow different VIYA server per sessionid(user)
|
|
86
|
+
let headerCache = {};
|
|
87
|
+
if (req.headers["X-VIYA-SERVER"] != null) {
|
|
88
|
+
console.error("[Note] Using user supplied VIYA server");
|
|
89
|
+
headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// used when doing autorization via mcp client
|
|
93
|
+
// ideal for production use
|
|
94
|
+
const hdr = req.headers["Authorization"];
|
|
95
|
+
if (hdr != null) {
|
|
96
|
+
headerCache.bearerToken = hdr.slice(7);
|
|
97
|
+
headerCache.AUTHFLOW = "bearer";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// faking out api key since Viya does not support
|
|
101
|
+
// not ideal for production
|
|
102
|
+
const hdr2 = req.headers["X-REFRESH-TOKEN"];
|
|
103
|
+
if (hdr2 != null) {
|
|
104
|
+
headerCache.refreshToken = hdr2;
|
|
105
|
+
headerCache.AUTHFLOW = "refresh";
|
|
106
|
+
}
|
|
107
|
+
return headerCache;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
export default handleRequest;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import appServer from "@sassoftware/viya-serverjs";
|
|
6
|
+
import handleRequest from "./handleRequest.js";
|
|
7
|
+
import handleGetDelete from "./handleGetDelete.js";
|
|
8
|
+
import urlOpen from "./urlOpen.js";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async function hapiMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
12
|
+
|
|
13
|
+
console.error('Starting Hapi MCP server...');
|
|
14
|
+
console.error("[Note]: Hapi MCP server started...", baseAppEnvContext.AUTHFLOW);
|
|
15
|
+
let r = await appServer.asyncCore(mcpHandlers, true, 'app', null);
|
|
16
|
+
console.error('Hapi server running result:', r);
|
|
17
|
+
if (baseAppEnvContext.AUTHFLOW === 'code'){
|
|
18
|
+
await urlOpen(r);
|
|
19
|
+
}
|
|
20
|
+
return r;
|
|
21
|
+
|
|
22
|
+
// add MCP handlers to the app server
|
|
23
|
+
|
|
24
|
+
function mcpHandlers() {
|
|
25
|
+
let routes = [
|
|
26
|
+
{
|
|
27
|
+
method: ["GET"],
|
|
28
|
+
path: "/health",
|
|
29
|
+
options: {
|
|
30
|
+
handler: async (req, h) => {
|
|
31
|
+
let health = {
|
|
32
|
+
name: "@sassoftware/mcp-server",
|
|
33
|
+
version: baseAppEnvContext.version,
|
|
34
|
+
description: "SAS Viya Sample MCP Server",
|
|
35
|
+
endpoints: {
|
|
36
|
+
mcp: "/mcp",
|
|
37
|
+
health: "/health",
|
|
38
|
+
},
|
|
39
|
+
usage:
|
|
40
|
+
"Use with MCP Inspector or compatible MCP clients like vscode or your own MCP client",
|
|
41
|
+
};
|
|
42
|
+
console.error("Health check requested, returning:", health);
|
|
43
|
+
return h.response(health).code(200).type('application/json');
|
|
44
|
+
},
|
|
45
|
+
auth: false,
|
|
46
|
+
description: "Help",
|
|
47
|
+
notes: "Help",
|
|
48
|
+
tags: ["app"],
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
method: ["POST"],
|
|
53
|
+
path: `/mcp`,
|
|
54
|
+
options: {
|
|
55
|
+
handler: async (req, h) => {
|
|
56
|
+
let precontext = req.pre.context;
|
|
57
|
+
let oauthInfo = (precontext != null) ? precontext.credentials : null;
|
|
58
|
+
await handleRequest(mcpServer, cache, req, h, oauthInfo);
|
|
59
|
+
return h.abandon;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
auth: {
|
|
63
|
+
strategy: "session",
|
|
64
|
+
mode: 'try'
|
|
65
|
+
},
|
|
66
|
+
description: "The main route for MCP requests",
|
|
67
|
+
notes: "Requires a valid session",
|
|
68
|
+
tags: ["mcp"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
method: ["GET", "DELETE"],
|
|
73
|
+
path: `/mcp`,
|
|
74
|
+
|
|
75
|
+
options: {
|
|
76
|
+
handler: async (req, h) => {
|
|
77
|
+
await handleGetDelete(mcpServer, cache, req, h);
|
|
78
|
+
return h.abandon;
|
|
79
|
+
},
|
|
80
|
+
auth: {
|
|
81
|
+
strategy: "session",
|
|
82
|
+
mode: 'try'
|
|
83
|
+
},
|
|
84
|
+
description: "Handle GET and DELETE requests",
|
|
85
|
+
notes: "Will fail if no valid session",
|
|
86
|
+
tags: ["mcp"],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
];
|
|
91
|
+
return routes;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export default hapiMcpServer;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import getToken from "./getToken.js";
|
|
7
7
|
import refreshToken from "./refreshToken.js";
|
|
8
|
+
import refreshTokenOauth from "./refreshTokenOauth.js";
|
|
8
9
|
|
|
9
10
|
async function getLogonPayload(_appContext) {
|
|
10
11
|
_appContext.contexts.logonPayload = await igetLogonPayload(_appContext);
|
|
@@ -15,14 +16,32 @@ async function igetLogonPayload(_appContext) {
|
|
|
15
16
|
|
|
16
17
|
// Use cached logonPayload if available
|
|
17
18
|
// This will cause timeouts if the token expires
|
|
18
|
-
if (_appContext.logonPayload != null && _appContext.tokenRefresh !== true) {
|
|
19
|
+
if (_appContext.contexts.logonPayload != null && _appContext.tokenRefresh !== true) {
|
|
19
20
|
console.error("[Note] Using cached logonPayload information");
|
|
20
21
|
return _appContext.contexts.logonPayload;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
if (_appContext.AUTHFLOW === 'code') {
|
|
25
|
+
let oauthInfo = _appContext.contexts.oauthInfo;
|
|
26
|
+
if (oauthInfo == null) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
_appContext.contexts.oauthInfo = await refreshTokenOauth(_appContext, oauthInfo);
|
|
30
|
+
if (_appContext.contexts.oauthInfo == null) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
let logonPayload = {
|
|
34
|
+
host: _appContext.VIYA_SERVER,
|
|
35
|
+
authType: "server",
|
|
36
|
+
token: _appContext.contexts.oauthInfo.accessToken,
|
|
37
|
+
tokenType: "Bearer",
|
|
38
|
+
}
|
|
39
|
+
return logonPayload;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
// Use user supplied bearer token
|
|
24
43
|
if (_appContext.AUTHFLOW === "bearer") {
|
|
25
|
-
console.error("[Note] Using user
|
|
44
|
+
console.error("[Note] Using user supplied bearer token ");
|
|
26
45
|
let logonPayload = {
|
|
27
46
|
host: _appContext.VIYA_SERVER,
|
|
28
47
|
authType: "server",
|
|
@@ -74,8 +93,8 @@ async function igetLogonPayload(_appContext) {
|
|
|
74
93
|
authType: "password",
|
|
75
94
|
user: _appContext.USERNAME,
|
|
76
95
|
password: _appContext.PASSWORD,
|
|
77
|
-
clientID: _appContext.
|
|
78
|
-
clientSecret: _appContext.
|
|
96
|
+
clientID: _appContext.CLIENTIDPW,
|
|
97
|
+
clientSecret: _appContext.CLIENTSECRETPW,
|
|
79
98
|
};
|
|
80
99
|
|
|
81
100
|
return logonPayload;
|
|
@@ -84,7 +103,7 @@ async function igetLogonPayload(_appContext) {
|
|
|
84
103
|
// sascli auth flow - create from credentials file
|
|
85
104
|
try {
|
|
86
105
|
let { host, token } = await getToken(_appContext)
|
|
87
|
-
console.error("[Note]
|
|
106
|
+
console.error("[Note] Token refreshed ", host);
|
|
88
107
|
let logonPayload = {
|
|
89
108
|
host: host,
|
|
90
109
|
authType: "server",
|
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
* if this function return a null, coreehttp will create unsigned certs
|
|
10
10
|
* @param {Object} _appContext - Application context containing SSLCERT property
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
import readCerts from './readCerts.js';
|
|
13
14
|
function getOpts(_appContext) {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
if (_appContext.tlsOpts != null) {
|
|
16
17
|
return _appContext.tlsOpts;
|
|
17
18
|
}
|
|
19
|
+
let r = readCerts(_appContext.SSLCERT);
|
|
20
|
+
_appContext.tlsOpts = r;
|
|
21
|
+
return r;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/*
|
|
18
25
|
let tlsdir = _appContext.SSLCERT;
|
|
19
26
|
if (tlsdir == null || tlsdir === 'NONE') {
|
|
20
27
|
return null;
|
|
@@ -38,6 +45,7 @@ function getOpts(_appContext) {
|
|
|
38
45
|
console.error('TLS FILES', Object.keys(options));
|
|
39
46
|
_appContext.tlsOpts = options;
|
|
40
47
|
return options;
|
|
41
|
-
|
|
48
|
+
*/
|
|
49
|
+
|
|
42
50
|
}
|
|
43
51
|
export default getOpts;
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import readCerts from './readCerts.js';
|
|
6
6
|
function getOptsViya(_appContext) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
|
|
8
|
+
if (_appContext.contexts.viyaCert != null) {
|
|
9
9
|
console.error('[Note] Using cached viyaOpts');
|
|
10
10
|
return _appContext.contexts.viyaCert;
|
|
11
11
|
}
|
|
12
|
+
let r = readCerts(_appContext.VIYACERT);
|
|
13
|
+
_appContext.contexts.viyaCert = r;
|
|
14
|
+
return r;
|
|
15
|
+
|
|
16
|
+
/*
|
|
12
17
|
let tlsdir = _appContext.VIYACERT;
|
|
13
18
|
if (tlsdir == null || tlsdir === 'NONE') {
|
|
14
19
|
return {};
|
|
@@ -33,6 +38,7 @@ function getOptsViya(_appContext) {
|
|
|
33
38
|
console.error('VIYACERT FILES', Object.keys(options));
|
|
34
39
|
_appContext.contexts.viyaCert = options;
|
|
35
40
|
return options;
|
|
36
|
-
|
|
41
|
+
*/
|
|
42
|
+
|
|
37
43
|
}
|
|
38
44
|
export default getOptsViya;
|
|
@@ -11,20 +11,20 @@ async function getToken(_appContext) {
|
|
|
11
11
|
if (_appContext.SAS_CLI_CONFIG != null) {
|
|
12
12
|
homedir = _appContext.SAS_CLI_CONFIG;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
let sep = (os.platform() === 'win32') ? '\\' : '/';
|
|
16
|
-
console.error('Using sep: ' + sep);
|
|
17
16
|
let credentials = homedir + sep + '.sas' + sep + 'credentials.json';
|
|
18
17
|
let url = homedir + sep + '.sas' + sep + 'config.json';
|
|
19
18
|
console.error('[Note] Using credentials file: ' + credentials);
|
|
20
19
|
console.error('[Note] Using config file: ' + url);
|
|
21
|
-
|
|
20
|
+
let profile = (_appContext.SAS_CLI_PROFILE == null || _appContext.SAS_CLI_PROFILE.toLowerCase() === 'default')
|
|
21
|
+
? 'Default' : _appContext.SAS_CLI_PROFILE;
|
|
22
|
+
console.error('[Note] Using SASCLI profile: ' + profile);
|
|
22
23
|
try {
|
|
23
24
|
|
|
24
25
|
let j = fs.readFileSync(credentials, 'utf8');
|
|
25
26
|
let js = JSON.parse(j);
|
|
26
|
-
|
|
27
|
-
? 'Default' : _appContext.SAS_CLI_PROFILE;
|
|
27
|
+
|
|
28
28
|
let refresh_token = js[profile]['refresh-token'];
|
|
29
29
|
j = fs.readFileSync(url, 'utf8');
|
|
30
30
|
js = JSON.parse(j);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
function getCerts(tlsdir) {
|
|
7
|
+
|
|
8
|
+
if (tlsdir == null || tlsdir === 'NONE') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(`[Note] Reading certs from directory: ` + tlsdir);
|
|
13
|
+
if (fs.existsSync(tlsdir) === false) {
|
|
14
|
+
console.error("[Warning] Specified cert dir does not exist: " + tlsdir);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let listOfFiles = fs.readdirSync(tlsdir);
|
|
19
|
+
console.log("[Note] TLS/SSL files found: " + listOfFiles);
|
|
20
|
+
let options = {};
|
|
21
|
+
for(let i=0; i < listOfFiles.length; i++) {
|
|
22
|
+
let fname = listOfFiles[i];
|
|
23
|
+
let name = tlsdir + '/' + listOfFiles[i];
|
|
24
|
+
let key = fname.split('.')[0];
|
|
25
|
+
console.log('Reading TLS file: ' + name + ' as key: ' + key);
|
|
26
|
+
options[key] = fs.readFileSync(name, { encoding: 'utf8' });
|
|
27
|
+
}
|
|
28
|
+
console.log('cert files', Object.keys(options));
|
|
29
|
+
|
|
30
|
+
return options;
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
export default getCerts;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { Agent, fetch } from 'undici';
|
|
6
|
+
import getOpts from './getOpts.js';
|
|
7
|
+
async function refreshTokenOauth(_appContext, oauthInfo ){
|
|
8
|
+
|
|
9
|
+
const url = `${process.env.VIYA_SERVER}/SASLogon/oauth/token`;
|
|
10
|
+
let opts = getOpts(_appContext);
|
|
11
|
+
|
|
12
|
+
const agent = new Agent({
|
|
13
|
+
connect: opts
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let bodyObject = {
|
|
17
|
+
grant_type: 'refresh_token',
|
|
18
|
+
refresh_token: oauthInfo.refreshToken,
|
|
19
|
+
client_id: _appContext.CLIENTID,
|
|
20
|
+
client_secret: _appContext.CLIENTSECRET
|
|
21
|
+
}
|
|
22
|
+
const body = new URLSearchParams(bodyObject);
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Accept': 'application/json',
|
|
28
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
29
|
+
dispatcher: agent
|
|
30
|
+
},
|
|
31
|
+
body: body.toString()
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const error = await response.text();
|
|
36
|
+
console.error('[Error] Failed to refresh token: ', error);
|
|
37
|
+
throw new Error(error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
let newauthInfo = {
|
|
42
|
+
accessToken: data.access_token,
|
|
43
|
+
refreshToken: data.refresh_token,
|
|
44
|
+
expiresIn: data.expires_in
|
|
45
|
+
}
|
|
46
|
+
return newauthInfo;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default refreshTokenOauth;
|
package/src/toolSet/findJob.js
CHANGED
|
@@ -9,6 +9,7 @@ function findJob(_appContext) {
|
|
|
9
9
|
"purpose": "Map natural language requests to find a job in SAS Viya and return structured results.",
|
|
10
10
|
"param_mapping": {
|
|
11
11
|
"name": "required - single name. If missing, ask 'Which job name would you like to find?'.",
|
|
12
|
+
"_userPrompt": "the original user prompt that triggered this tool."
|
|
12
13
|
|
|
13
14
|
},
|
|
14
15
|
"response_schema": "{ jobs: Array<string|object> }",
|
|
@@ -77,7 +78,8 @@ function findJob(_appContext) {
|
|
|
77
78
|
aliases: ['findJob','find job','find_job'],
|
|
78
79
|
description: description,
|
|
79
80
|
schema: {
|
|
80
|
-
name: z.string()
|
|
81
|
+
name: z.string(),
|
|
82
|
+
_userPrompt: z.string()
|
|
81
83
|
},
|
|
82
84
|
required: ['name'],
|
|
83
85
|
handler: async (params) => {
|
package/src/urlOpen.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import open from 'open';
|
|
6
|
+
|
|
7
|
+
async function urlOpen(url) {
|
|
8
|
+
|
|
9
|
+
console.error(`[Note]Opening URL: ${url} for user authentication`);
|
|
10
|
+
await open(url, {wait:true});
|
|
11
|
+
console.error(`[Note] User has closed the informational window after authentication.`);
|
|
12
|
+
}
|
|
13
|
+
export default urlOpen;
|
|
File without changes
|
|
File without changes
|