@sassoftware/sas-score-mcp-serverjs 0.1.0 → 0.2.1-0
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 +11 -2
- package/CHANGES.md +6 -0
- package/Public/index.html +70 -0
- package/README.md +67 -9
- package/SECURITY.md +3 -6
- package/cli.js +51 -35
- package/mcpConfigurations/stdio.json +2 -2
- package/mcpConfigurations/stdiodev.json +2 -1
- package/package.json +4 -1
- package/src/createMcpServer.js +23 -12
- package/src/{corehttp.js → expressMcpServer.js} +2 -2
- package/src/handleGetDelete.js +31 -0
- package/src/handleRequest.js +110 -0
- package/src/hapiMcpServer.js +89 -0
- package/src/toolHelpers/getLogonPayload.js +23 -4
- package/src/toolHelpers/getOpts.js +12 -4
- package/src/toolHelpers/getOptsViya.js +10 -4
- package/src/toolHelpers/getStoreOpts.js +0 -2
- 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 +12 -0
package/.env
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
PORT=8080
|
|
3
|
-
HTTPS=
|
|
3
|
+
HTTPS=true
|
|
4
4
|
MCPTYPE=http
|
|
5
|
+
USELOGON=FALSE
|
|
6
|
+
USETOKEN=TRUE
|
|
7
|
+
APPNAME=mcpserver
|
|
8
|
+
APPHOST=localhost
|
|
9
|
+
APPPORT=8080
|
|
10
|
+
|
|
11
|
+
CLIENTID=mcpserver
|
|
12
|
+
CLIENTSECRET=jellico
|
|
5
13
|
|
|
6
|
-
# Sample values shown below
|
|
7
14
|
AUTHFLOW=sascli
|
|
8
15
|
SAS_CLI_PROFILE=00m
|
|
9
16
|
SAS_CLI_CONFIG=c:\Users\kumar
|
|
@@ -11,4 +18,6 @@ SSLCERT=c:\Users\kumar\.tls
|
|
|
11
18
|
VIYACERT=c:\Users\kumar\viyaCert
|
|
12
19
|
CAS_SERVER=cas-shared-default
|
|
13
20
|
COMPUTECONTEXT=SAS Job Execution compute context
|
|
21
|
+
SAMESITE=Lax,false
|
|
22
|
+
AUTOSTART=TRUE
|
|
14
23
|
|
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
|
@@ -19,10 +19,10 @@ This MCP server was developed for two types of SAS users.
|
|
|
19
19
|
|
|
20
20
|
### SAS users
|
|
21
21
|
SAS users who want to use natural language("chat") to execute prebuilt SAS code and models.
|
|
22
|
-
See this [quick reference](
|
|
22
|
+
See this [quick reference](sas-mcp-tools-reference.md) for details.
|
|
23
23
|
|
|
24
24
|
### MCP tool developers
|
|
25
|
-
SAS developers who want to extend the capabilities of the server with their own tools. See the [guide](
|
|
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
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.
|
|
@@ -65,7 +65,7 @@ VIYA_SERVER= your Viya server url
|
|
|
65
65
|
TOKENFILE=
|
|
66
66
|
|
|
67
67
|
# if password flow specify these
|
|
68
|
-
|
|
68
|
+
CLIENTID=
|
|
69
69
|
CLIENTSECRET=
|
|
70
70
|
PASSWORD=
|
|
71
71
|
|
|
@@ -130,8 +130,66 @@ Set the env TOKENFILE to a file containing the token.
|
|
|
130
130
|
There seems to be a pattern of using a long-lived token.
|
|
131
131
|
If this is your use case, set the TOKENFILE to a file containing this token.
|
|
132
132
|
|
|
133
|
-
### Oauth
|
|
134
|
-
|
|
133
|
+
### Oauth - (experimental) Authentication handled by the mcp server
|
|
134
|
+
|
|
135
|
+
In this approach, the mcp client does not participate in the Oauth authentication process. It is handled by the mcp server at startup.
|
|
136
|
+
|
|
137
|
+
> This is marked as experimental since there can be timing issues between the mcp client and server. This needs to be investigated further.
|
|
138
|
+
|
|
139
|
+
#### SAS viya setup.
|
|
140
|
+
|
|
141
|
+
Create a Oauth client with the following properties
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
{
|
|
145
|
+
auth flow: authorization_code
|
|
146
|
+
clientid: <your client id>
|
|
147
|
+
clientsecret: <some client secret - pkce not supported at this time>
|
|
148
|
+
redirect: https://localhost:8080/mcpserver
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#### Use an .env file as follows(sample values shown)
|
|
152
|
+
|
|
153
|
+
```env
|
|
154
|
+
|
|
155
|
+
PORT=8080
|
|
156
|
+
HTTPS=true
|
|
157
|
+
MCPTYPE=http
|
|
158
|
+
USELOGON=FALSE
|
|
159
|
+
USETOKEN=TRUE
|
|
160
|
+
APPNAME=mcpserver
|
|
161
|
+
APPHOST=localhost
|
|
162
|
+
APPPORT=8080
|
|
163
|
+
|
|
164
|
+
CLIENTID=mcpserver
|
|
165
|
+
CLIENTSECRET=jellico
|
|
166
|
+
AUTHFLOW=code
|
|
167
|
+
SSLCERT=c:\Users\kumar\.tls
|
|
168
|
+
VIYACERT=c:\Users\kumar\viyaCert
|
|
169
|
+
CAS_SERVER=cas-shared-default
|
|
170
|
+
COMPUTECONTEXT=SAS Job Execution compute context
|
|
171
|
+
SAMESITE=Lax,false
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Usage
|
|
176
|
+
|
|
177
|
+
Start the server with this command:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npx @sassoftware/sas-score-mcp-serverjs@latest
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Then visit this site on your browser:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
https://localhost:8080/mcpserver
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
You will be prompted to logon to SAS Viya.
|
|
190
|
+
A dialog will be displayed if the logon was successful.
|
|
191
|
+
Icon this window and proceed to your mcp client
|
|
192
|
+
|
|
135
193
|
|
|
136
194
|
## Transport Methods
|
|
137
195
|
This server supports both stdio and http transport methods.
|
|
@@ -154,7 +212,7 @@ The env variables can be specified in two ways:
|
|
|
154
212
|
"command": "npx",
|
|
155
213
|
"args": [
|
|
156
214
|
"-y",
|
|
157
|
-
"@sassoftware/sas-
|
|
215
|
+
"@sassoftware/sas-score-mcp-serverjs@latest",
|
|
158
216
|
],
|
|
159
217
|
"env": {
|
|
160
218
|
"MCPTYPE": "stdio",
|
|
@@ -166,8 +224,8 @@ The env variables can be specified in two ways:
|
|
|
166
224
|
"VIYA_SERVER": "viya server if AUTHFLOW=password|token|refresh",
|
|
167
225
|
"PASSWORD": "password if AUTHFLOW is password",
|
|
168
226
|
"USERNAME": "username if AUTHFLOW is password",
|
|
169
|
-
"
|
|
170
|
-
"
|
|
227
|
+
"CLIENTID": "client password if AUTHFLOW is password",
|
|
228
|
+
"CLIENTSECRET": "client id if AUTHFLOW is password",
|
|
171
229
|
"TOKENFILE": "file if AUTHFLOW is token",
|
|
172
230
|
"COMPUTECONTEXT": "SAS Job Execution compute context",
|
|
173
231
|
"CASSERVER": "cas-shared-default",
|
|
@@ -222,7 +280,7 @@ But this step is necessary of using http transport.
|
|
|
222
280
|
|
|
223
281
|
|
|
224
282
|
```sh
|
|
225
|
-
npx @sassoftware/sas-
|
|
283
|
+
npx @sassoftware/sas-score-mcp-serverjs@latest
|
|
226
284
|
```
|
|
227
285
|
|
|
228
286
|
Make sure that the .env file is in the current working directory
|
package/SECURITY.md
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
A SECURITY.md outlines your project's security policy. It includes instructions on how to report a security vulnerability in your project.
|
|
3
|
-
If your project contains this file, link to it from the project's README.
|
|
4
|
-
-->
|
|
5
1
|
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
|
|
3
|
+
# sas-score-mcp-serverjs Security Policy
|
|
4
|
+
|
|
8
5
|
Project maintainers and community contributors take security issues seriously.
|
|
9
6
|
Efforts to disclose potential issues responsibly are appreciated, and viable contributions will be acknowledged.
|
|
10
7
|
To aid investigation of any reported vulnerabilities, please follow the [reporting guidelines](#reporting-guidelines) when submitting your findings.
|
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';
|
|
@@ -38,7 +40,7 @@ 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...'
|
|
@@ -90,14 +92,16 @@ if (process.env.SUBCLASS != null) {
|
|
|
90
92
|
// setup base appEnv
|
|
91
93
|
// for stdio this is the _appContext
|
|
92
94
|
// for http each session a copy of this as appEnvTemplate is created in corehttp
|
|
95
|
+
|
|
96
|
+
// backward compability variables
|
|
97
|
+
let clientID = process.env.CLIENTID || process.env.CLIENTIDPW || null;
|
|
98
|
+
let clientSecret = process.env.CLIENTSECRET || process.env.CLIENTSECRETPW || null;
|
|
99
|
+
let https = process.env.HTTPS != null ? process.env.HTTPS.toUpperCase() : "FALSE";
|
|
93
100
|
const appEnvBase = {
|
|
94
101
|
version: version,
|
|
95
|
-
mcpType: mcpType,
|
|
102
|
+
mcpType: mcpType,
|
|
96
103
|
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,
|
|
104
|
+
HTTPS: https,
|
|
101
105
|
SAS_CLI_PROFILE: process.env.SAS_CLI_PROFILE || 'Default',
|
|
102
106
|
SAS_CLI_CONFIG: process.env.SAS_CLI_CONFIG || process.env.HOME, // default to user home directory
|
|
103
107
|
SSLCERT: process.env.SSLCERT || null,
|
|
@@ -108,8 +112,10 @@ const appEnvBase = {
|
|
|
108
112
|
PORT: process.env.PORT || 8080,
|
|
109
113
|
USERNAME: process.env.USERNAME || null,
|
|
110
114
|
PASSWORD: process.env.PASSWORD || null,
|
|
111
|
-
|
|
112
|
-
CLIENTSECRET:
|
|
115
|
+
CLIENTID: clientID,
|
|
116
|
+
CLIENTSECRET: clientSecret,
|
|
117
|
+
PKCE: process.env.PKCE || null,
|
|
118
|
+
|
|
113
119
|
TOKEN: process.env.TOKEN || null,
|
|
114
120
|
REFRESH_TOKEN: process.env.REFRESH_TOKEN || null,
|
|
115
121
|
TOKENFILE: process.env.TOKENFILE || null,
|
|
@@ -136,9 +142,19 @@ const appEnvBase = {
|
|
|
136
142
|
logonPayload: null,
|
|
137
143
|
bearerToken: null,
|
|
138
144
|
tlsOpts: null,
|
|
145
|
+
oauthInfo: null,
|
|
139
146
|
contexts: {
|
|
147
|
+
host: process.env.VIYA_SERVER,
|
|
148
|
+
APPHOST: process.env.APPHOST || 'localhost',
|
|
149
|
+
APPNAME: process.env.APPNAME || 'mcpServer',
|
|
150
|
+
PORT: process.env.APPPORT || 8080,
|
|
151
|
+
HTTPS: https,
|
|
140
152
|
store: null, /* for restaf users */
|
|
141
153
|
storeConfig: {},
|
|
154
|
+
oauthInfo: null,
|
|
155
|
+
CLIENTID: clientID,
|
|
156
|
+
CLIENTSECRET: clientSecret,
|
|
157
|
+
pkce: process.env.PKCE || null,
|
|
142
158
|
casSession: null, /* restaf cas session object */
|
|
143
159
|
computeSession: null, /* restaf compute session object */
|
|
144
160
|
viyaCert: null, /* ssl/tsl certificates to connect to viya */
|
|
@@ -192,14 +208,8 @@ if (appEnvBase.REFRESH_TOKEN != null) {
|
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
210
|
|
|
195
|
-
// if authflow is cli, postpone getting logonPayload until needed
|
|
211
|
+
// if authflow is cli or code, postpone getting logonPayload until needed
|
|
196
212
|
|
|
197
|
-
/*
|
|
198
|
-
if(appEnvBase.AUTHFLOW ==='sascli') {
|
|
199
|
-
let logonPayload = await getLogonPayload(appEnvBase);
|
|
200
|
-
appEnvBase.logonPayload = logonPayload;
|
|
201
|
-
}
|
|
202
|
-
*/
|
|
203
213
|
|
|
204
214
|
// setup mcpServer (both http and stdio use this)
|
|
205
215
|
// this is singleton - best practices recommend this
|
|
@@ -216,6 +226,7 @@ sessionCache.set('transports', transports);
|
|
|
216
226
|
|
|
217
227
|
// set this for stdio transport use
|
|
218
228
|
// dummy sessionId for use in the tools
|
|
229
|
+
let useHapi = process.env.AUTHFLOW === 'code' ? true : false;
|
|
219
230
|
if (mcpType === 'stdio') {
|
|
220
231
|
let sessionId = randomUUID();
|
|
221
232
|
sessionCache.set('currentId', sessionId);
|
|
@@ -226,31 +237,36 @@ if (mcpType === 'stdio') {
|
|
|
226
237
|
|
|
227
238
|
} else {
|
|
228
239
|
console.error('[Note] Starting HTTP MCP server...');
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
if (useHapi === true) {
|
|
241
|
+
await hapiMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
242
|
+
console.error('[Note] Using HAPI HTTP server...')
|
|
243
|
+
} else {
|
|
244
|
+
await expressMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
245
|
+
console.error('[Note] MCP HTTP server started on port ' + appEnvBase.PORT);
|
|
246
|
+
}
|
|
231
247
|
}
|
|
232
248
|
|
|
233
249
|
// custom reader for .env file to avoid dotenv logging to console
|
|
234
250
|
function iconfig(envFile) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
try {
|
|
252
|
+
let data = fs.readFileSync(envFile, 'utf8');
|
|
253
|
+
let d = data.split(/\r?\n/);
|
|
238
254
|
let envData = {};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
255
|
+
d.forEach(l => {
|
|
256
|
+
if (l.length > 0 && l.indexOf('#') === -1) {
|
|
257
|
+
let la = l.split('=');
|
|
258
|
+
let envName = la[0];
|
|
259
|
+
if (la.length === 2 && la[1].length > 0) {
|
|
260
|
+
let t = la[1].trim();
|
|
261
|
+
process.env[envName] = t;
|
|
246
262
|
envData[envName] = t;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
250
266
|
return envData;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.log(err);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
255
271
|
}
|
|
256
272
|
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"type": "stdio",
|
|
5
5
|
"command": "npx",
|
|
6
6
|
"args": [
|
|
7
|
-
"@sassoftware/sas-score-mcp-serverjs@
|
|
7
|
+
"@sassoftware/sas-score-mcp-serverjs@latest"
|
|
8
8
|
],
|
|
9
9
|
"env": {
|
|
10
10
|
"MCPTYPE": "stdio",
|
|
11
|
-
"AUTHFLOW": "
|
|
11
|
+
"AUTHFLOW": "password",
|
|
12
12
|
"SAS_CLI_PROFILE": "cis",
|
|
13
13
|
"SAS_CLI_CONFIG": "c:\\Users\\kumar",
|
|
14
14
|
"SSLCERT": "c:\\Users\\kumar\\.tls",
|
|
@@ -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.1
|
|
3
|
+
"version": "0.2.1-0",
|
|
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.1-5",
|
|
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",
|
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
|
|
|
@@ -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,89 @@
|
|
|
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
|
+
//import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
10
|
+
|
|
11
|
+
async function hapiMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
12
|
+
|
|
13
|
+
console.error(appServer);
|
|
14
|
+
appServer(mcpHandlers, true, 'app', null);
|
|
15
|
+
if (process.env.AUTOSTART === 'TRUE') {
|
|
16
|
+
await urlOpen();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mcpHandlers() {
|
|
20
|
+
let routes = [
|
|
21
|
+
{
|
|
22
|
+
method: ["GET"],
|
|
23
|
+
path: "/health",
|
|
24
|
+
options: {
|
|
25
|
+
handler: async (req, h) => {
|
|
26
|
+
let health = {
|
|
27
|
+
name: "@sassoftware/mcp-server",
|
|
28
|
+
version: baseAppEnvContext.version,
|
|
29
|
+
description: "SAS Viya Sample MCP Server",
|
|
30
|
+
endpoints: {
|
|
31
|
+
mcp: "/mcp",
|
|
32
|
+
health: "/health",
|
|
33
|
+
},
|
|
34
|
+
usage:
|
|
35
|
+
"Use with MCP Inspector or compatible MCP clients like vscode or your own MCP client",
|
|
36
|
+
};
|
|
37
|
+
console.error("Health check requested, returning:", health);
|
|
38
|
+
return h.response(health).code(200).type('application/json');
|
|
39
|
+
},
|
|
40
|
+
auth: false,
|
|
41
|
+
description: "Help",
|
|
42
|
+
notes: "Help",
|
|
43
|
+
tags: ["app"],
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
method: ["POST"],
|
|
48
|
+
path: `/mcp`,
|
|
49
|
+
options: {
|
|
50
|
+
handler: async (req, h) => {
|
|
51
|
+
let precontext = req.pre.context;
|
|
52
|
+
let oauthInfo = (precontext != null) ? precontext.credentials : null;
|
|
53
|
+
await handleRequest(mcpServer, cache, req, h, oauthInfo);
|
|
54
|
+
return h.abandon;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
auth: {
|
|
58
|
+
strategy: "session",
|
|
59
|
+
mode: 'try'
|
|
60
|
+
},
|
|
61
|
+
description: "The main route for MCP requests",
|
|
62
|
+
notes: "Requires a valid session",
|
|
63
|
+
tags: ["mcp"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
method: ["GET", "DELETE"],
|
|
68
|
+
path: `/mcp`,
|
|
69
|
+
|
|
70
|
+
options: {
|
|
71
|
+
handler: async (req, h) => {
|
|
72
|
+
await handleGetDelete(mcpServer, cache, req, h);
|
|
73
|
+
return h.abandon;
|
|
74
|
+
},
|
|
75
|
+
auth: {
|
|
76
|
+
strategy: "session",
|
|
77
|
+
mode: 'try'
|
|
78
|
+
},
|
|
79
|
+
description: "Handle GET and DELETE requests",
|
|
80
|
+
notes: "Will fail if no valid session",
|
|
81
|
+
tags: ["mcp"],
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
];
|
|
86
|
+
return routes;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
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",
|
|
@@ -68,7 +87,7 @@ async function igetLogonPayload(_appContext) {
|
|
|
68
87
|
return logonPayload;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
|
-
if (_appContext.
|
|
90
|
+
if (_appContext.AUTHFLOW === "password") {
|
|
72
91
|
let logonPayload = {
|
|
73
92
|
host: _appContext.VIYA_SERVER,
|
|
74
93
|
authType: "password",
|
|
@@ -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;
|
|
@@ -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,12 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
|
|
3
|
+
async function urlOpen(contexts) {
|
|
4
|
+
let appHost = process.env.APPHOST || 'localhost';
|
|
5
|
+
let appPort = process.env.PORT || '8080';
|
|
6
|
+
let appName = process.env.APPNAME || 'mcpserver';
|
|
7
|
+
let protocol = (process.env.HTTPS != null && process.env.HTTPS.toUpperCase() === 'TRUE') ? 'https' : 'http';
|
|
8
|
+
let urlx = `${protocol}://${appHost}:${appPort}/${appName}`;
|
|
9
|
+
console.log(`Opening URL: ${urlx}`);
|
|
10
|
+
await open(urlx, {wait:true});
|
|
11
|
+
}
|
|
12
|
+
export default urlOpen;
|