@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 CHANGED
@@ -1,19 +1,14 @@
1
-
2
- PORT=8080
3
- HTTPS=TRUE
4
- MCPTYPE=stdio
5
-
6
- # Sample values shown below
7
- VIYA_SERVER=<viya usl>
8
- AUTHFLOW=password
9
- USERNAME=sastest1
10
- PASSWORD=<some_password>
11
- CLIENTID=mcppw
12
- CLIENTSECRET=xxxxx
13
- SAS_CLI_PROFILE=00m
14
- SAS_CLI_CONFIG=c:\Users\kumar
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=http
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
- # * none * No aut tokens are created - useful if you want to control authentication
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
- This is under development.
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 -key-file key.pem -cert-file crt.pem localhost 127:0.0.1 ::1
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 corehttp from './src/corehttp.js';
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 === 'NONE') {
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 NONE...');
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
- // dotenvExpand.expand(e);
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: process.env.CLIENTID|| null,
112
- CLIENTSECRET: process.env.CLIENTSECRET || null,
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.contexts.logonPayload = {
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
- await corehttp(mcpServer, sessionCache, appEnvBase);
223
- console.error('[Note] MCP HTTP server started on port ' + appEnvBase.PORT);
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
- try {
229
- let data = fs.readFileSync(envFile, 'utf8');
230
- let d = data.split(/\r?\n/);
258
+ try {
259
+ let data = fs.readFileSync(envFile, 'utf8');
260
+ let d = data.split(/\r?\n/);
231
261
  let envData = {};
232
- d.forEach(l => {
233
- if (l.length > 0 && l.indexOf('#') === -1) {
234
- let la = l.split('=');
235
- let envName = la[0];
236
- if (la.length === 2 && la[1].length > 0) {
237
- let t = la[1].trim();
238
- process.env[envName] = t;
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
- } catch (err) {
245
- console.log(err);
246
- process.exit(0);
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,8 @@
1
+ {
2
+ "servers": {
3
+ "sasmcp": {
4
+ "type": "http",
5
+ "url": "http://localhost:8080/mcp"
6
+ }
7
+ }
8
+ }
@@ -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
+
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "sasmcp": {
4
+ "type": "http",
5
+ "url": "http://localhost:8080/mcp"
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+
2
+ MCPTYPE=stdio
3
+ AUTHFLOW=sascli
4
+ SAS_CLI_PROFILE=xf1
5
+ SAS_CLI_CONFIG=c:\Users\kumar
6
+ SSLCERT=c:\Users\kumar\.tls
7
+ VIYACERT=c:\Users\kumar\viyaCert\xf1\viya
8
+ CAS_SERVER=cas-shared-default
9
+ COMPUTECONTEXT=SAS Job Execution compute context
@@ -2,18 +2,20 @@
2
2
  "servers": {
3
3
  "sasmcp": {
4
4
  "type": "stdio",
5
- "command": "npx",
5
+ "command": "node",
6
6
  "args": [
7
- "@sassoftware/sas-score-mcp-serverjs@alpha"
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": "cis",
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
- "CAS_SERVER": "cas-shared-default"
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\\gitlab\\mcp-serverjs\\cli.js"
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.1.1",
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
@@ -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
- // create enhanced appContext for tool
49
- if (args == null) {
50
- params = {_appContext: _appContext.contexts};
51
- } else {
52
- params = Object.assign({}, args, {_appContext: _appContext.contexts});
53
- }
54
-
55
- // call the actual tool handler
56
- let r = await builtin(params);
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
- // console.error(`\n[Note] Registering tool ${i + 1} : ${toolName}`);
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 corehttp(mcpServer, cache, currentAppEnvContext) {
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 === true) {
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 corehttp;
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 suplied bearer token ");
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.CLIENTID,
78
- clientSecret: _appContext.CLIENTSECRET,
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] got refresh token from getToken() for host ", host);
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
- import fs from 'fs';
12
+
13
+ import readCerts from './readCerts.js';
13
14
  function getOpts(_appContext) {
14
-
15
- if (_appContext.tlsOpts != null) {
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 fs from 'fs';
5
+ import readCerts from './readCerts.js';
6
6
  function getOptsViya(_appContext) {
7
-
8
- if (_appContext.contexts.viyaCert != null) {
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;
@@ -7,8 +7,6 @@ import getOptsViya from './getOptsViya.js';
7
7
  function getStoreOpts(_appContext) {
8
8
 
9
9
  let opts = getOptsViya(_appContext);
10
-
11
-
12
10
  let storeOpts = {
13
11
  casProxy: true,
14
12
  httpOptions: { ...opts, rejectUnauthorized: true }
@@ -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
- console.error(os.platform());
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
- let profile = (_appContext.SAS_CLI_PROFILE == null || _appContext.SAS_CLI_PROFILE.toLowerCase() === 'default')
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;
@@ -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