@sassoftware/sas-score-mcp-serverjs 0.1.1 → 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 +13 -9
- package/CHANGES.md +6 -0
- package/Public/index.html +70 -0
- package/README.md +60 -2
- package/cli.js +52 -29
- 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 +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/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,19 +1,23 @@
|
|
|
1
1
|
|
|
2
2
|
PORT=8080
|
|
3
|
-
HTTPS=
|
|
4
|
-
MCPTYPE=
|
|
3
|
+
HTTPS=true
|
|
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
|
-
|
|
7
|
-
VIYA_SERVER=<viya usl>
|
|
8
|
-
AUTHFLOW=password
|
|
9
|
-
USERNAME=sastest1
|
|
10
|
-
PASSWORD=<some_password>
|
|
11
|
-
CLIENTID=mcppw
|
|
12
|
-
CLIENTSECRET=xxxxx
|
|
14
|
+
AUTHFLOW=sascli
|
|
13
15
|
SAS_CLI_PROFILE=00m
|
|
14
16
|
SAS_CLI_CONFIG=c:\Users\kumar
|
|
15
17
|
SSLCERT=c:\Users\kumar\.tls
|
|
16
18
|
VIYACERT=c:\Users\kumar\viyaCert
|
|
17
19
|
CAS_SERVER=cas-shared-default
|
|
18
20
|
COMPUTECONTEXT=SAS Job Execution compute context
|
|
21
|
+
SAMESITE=Lax,false
|
|
22
|
+
AUTOSTART=TRUE
|
|
19
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
|
@@ -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.
|
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
|
-
CLIENTID:
|
|
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 */
|
|
@@ -166,7 +182,7 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
166
182
|
console.error(`[Note]Loading token from file: ${appEnvBase.TOKENFILE}...`);
|
|
167
183
|
appEnvBase.TOKEN = fs.readFileSync(appEnvBase.TOKENFILE, { encoding: 'utf8' });
|
|
168
184
|
appEnvBase.AUTHFLOW = 'token';
|
|
169
|
-
appEnvBase.
|
|
185
|
+
appEnvBase.appContexts.logonPayload = {
|
|
170
186
|
host: appEnvBase.VIYA_SERVER,
|
|
171
187
|
authType: 'server',
|
|
172
188
|
token: appEnvBase.TOKEN,
|
|
@@ -192,6 +208,7 @@ if (appEnvBase.REFRESH_TOKEN != null) {
|
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
210
|
|
|
211
|
+
// if authflow is cli or code, postpone getting logonPayload until needed
|
|
195
212
|
|
|
196
213
|
|
|
197
214
|
// setup mcpServer (both http and stdio use this)
|
|
@@ -209,6 +226,7 @@ sessionCache.set('transports', transports);
|
|
|
209
226
|
|
|
210
227
|
// set this for stdio transport use
|
|
211
228
|
// dummy sessionId for use in the tools
|
|
229
|
+
let useHapi = process.env.AUTHFLOW === 'code' ? true : false;
|
|
212
230
|
if (mcpType === 'stdio') {
|
|
213
231
|
let sessionId = randomUUID();
|
|
214
232
|
sessionCache.set('currentId', sessionId);
|
|
@@ -219,31 +237,36 @@ if (mcpType === 'stdio') {
|
|
|
219
237
|
|
|
220
238
|
} else {
|
|
221
239
|
console.error('[Note] Starting HTTP MCP server...');
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
247
|
}
|
|
225
248
|
|
|
226
249
|
// custom reader for .env file to avoid dotenv logging to console
|
|
227
250
|
function iconfig(envFile) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
try {
|
|
252
|
+
let data = fs.readFileSync(envFile, 'utf8');
|
|
253
|
+
let d = data.split(/\r?\n/);
|
|
231
254
|
let envData = {};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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;
|
|
239
262
|
envData[envName] = t;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
243
266
|
return envData;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.log(err);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
248
271
|
}
|
|
249
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.
|
|
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",
|
|
@@ -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;
|
|
@@ -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;
|