@sassoftware/sas-score-mcp-serverjs 0.3.19 → 0.4.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/cli.js +19 -22
- package/package.json +2 -2
- package/src/expressMcpServer.js +22 -8
- package/src/hapiMcpServer.js +38 -0
- package/src/openAPIJson.js +175 -175
- package/src/toolHelpers/getLogonPayload.js +6 -6
- package/src/toolHelpers/getStoreOpts.js +1 -2
- package/src/toolHelpers/getToken.js +0 -1
- package/src/toolHelpers/refreshToken.js +48 -46
- package/src/toolHelpers/refreshTokenOauth.js +2 -2
- package/src/toolHelpers/tlogon.js +9 -0
- package/src/toolSet/devaScore.js +4 -4
- package/src/toolHelpers/getOpts.js +0 -51
- package/src/toolHelpers/getOptsViya.js +0 -44
package/cli.js
CHANGED
|
@@ -16,14 +16,15 @@ import createMcpServer from './src/createMcpServer.js';
|
|
|
16
16
|
import fs from 'fs';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
|
|
19
|
-
import refreshToken from './src/toolHelpers/refreshToken.js';
|
|
20
|
-
import getOptsViya from './src/toolHelpers/getOptsViya.js';
|
|
19
|
+
//import refreshToken from './src/toolHelpers/refreshToken.js';
|
|
20
|
+
//import getOptsViya from './src/toolHelpers/getOptsViya.js';
|
|
21
|
+
import readCerts from './src/toolHelpers/readCerts.js';
|
|
21
22
|
|
|
22
23
|
import { fileURLToPath } from 'url';
|
|
23
24
|
import { dirname } from 'path';
|
|
24
25
|
|
|
25
26
|
import NodeCache from 'node-cache';
|
|
26
|
-
import getOpts from './src/toolHelpers/getOpts.js';
|
|
27
|
+
//import getOpts from './src/toolHelpers/getOpts.js';
|
|
27
28
|
|
|
28
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
|
|
@@ -70,7 +71,7 @@ console.error(
|
|
|
70
71
|
// and storage provided by cloud providers
|
|
71
72
|
console.error(process.env.COMPUTECONTEXT);
|
|
72
73
|
debugger;
|
|
73
|
-
let sessionCache = new NodeCache({ stdTTL:
|
|
74
|
+
let sessionCache = new NodeCache({ stdTTL: 60*60, checkperiod: 2 * 60, useClones: false });
|
|
74
75
|
|
|
75
76
|
//
|
|
76
77
|
// Load environment variables from .env file if present
|
|
@@ -167,6 +168,7 @@ const appEnvBase = {
|
|
|
167
168
|
casSession: null, /* restaf cas session object */
|
|
168
169
|
computeSession: null, /* restaf compute session object */
|
|
169
170
|
viyaCert: null, /* ssl/tsl certificates to connect to viya */
|
|
171
|
+
appCert: null,
|
|
170
172
|
logonPayload: null, /* viya logon payload to connect to viya */
|
|
171
173
|
casServerId: null,
|
|
172
174
|
computeSessonId: null,
|
|
@@ -179,10 +181,15 @@ const appEnvBase = {
|
|
|
179
181
|
process.env.APPPORT=appEnvBase.PORT;
|
|
180
182
|
|
|
181
183
|
// setup TLS options for viya calls
|
|
182
|
-
|
|
183
184
|
console.error('[Note]Viya SSL dir set to: ' + appEnvBase.VIYACERT);
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
appEnvBase.contexts.viyaCert = readCerts(appEnvBase.VIYACERT); /* appEnvBase.contexts.viyaCert is set here */
|
|
186
|
+
|
|
187
|
+
// setup TLS options for app server (expressMcpServer or hapiMcpServer)
|
|
188
|
+
|
|
189
|
+
console.error('[Note]App SSL dir set to: ' + appEnvBase.SSLCERT);
|
|
190
|
+
appEnvBase.tlsOpts = readCerts(appEnvBase.SSLCERT);
|
|
191
|
+
appEnvBase.contexts.appCert = appEnvBase.tlsOpts; /* just for completeness */
|
|
192
|
+
|
|
186
193
|
appEnvBase.contexts.storeConfig = {
|
|
187
194
|
casProxy: true,
|
|
188
195
|
options: { ns: null, proxyServer: null, httpOptions: appEnvBase.contexts.viyaCert }
|
|
@@ -205,19 +212,7 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
|
|
209
|
-
// use this for testing only.
|
|
210
|
-
if (appEnvBase.REFRESH_TOKEN != null) {
|
|
211
|
-
appEnvBase.refreshToken = appEnvBase.REFRESH_TOKEN;
|
|
212
|
-
appEnvBase.AUTHFLOW = 'refresh';
|
|
213
|
-
let t = await refreshToken(appEnvBase, { token: appEnvBase.REFRESH_TOKEN, host: appEnvBase.VIYA_SERVER });
|
|
214
|
-
appEnvBase.contexts.logonPayload = {
|
|
215
|
-
host: appEnvBase.VIYA_SERVER,
|
|
216
|
-
authType: 'server',
|
|
217
|
-
token: t,
|
|
218
|
-
tokenType: 'Bearer'
|
|
219
|
-
}
|
|
220
|
-
}
|
|
215
|
+
|
|
221
216
|
|
|
222
217
|
// if authflow is cli or code, postpone getting logonPayload until needed
|
|
223
218
|
|
|
@@ -232,12 +227,13 @@ let appEnvTemplate = Object.assign({}, appEnvBase);
|
|
|
232
227
|
|
|
233
228
|
sessionCache.set('appEnvTemplate', appEnvTemplate);
|
|
234
229
|
|
|
235
|
-
let transports = {};
|
|
230
|
+
let transports = {'dummy': null};
|
|
236
231
|
sessionCache.set('transports', transports);
|
|
237
232
|
|
|
238
233
|
// set this for stdio transport use
|
|
239
234
|
// dummy sessionId for use in the tools
|
|
240
|
-
let useHapi = process.env.
|
|
235
|
+
let useHapi = (process.env.USEHAPI === 'TRUE') ? true : false;
|
|
236
|
+
console.error('[Note] appEnvBase is', JSON.stringify(appEnvBase, null,2));
|
|
241
237
|
if (mcpType === 'stdio') {
|
|
242
238
|
let sessionId = randomUUID();
|
|
243
239
|
sessionCache.set('currentId', sessionId);
|
|
@@ -249,6 +245,7 @@ if (mcpType === 'stdio') {
|
|
|
249
245
|
} else {
|
|
250
246
|
console.error('[Note] Starting HTTP MCP server...');
|
|
251
247
|
if (useHapi === true) {
|
|
248
|
+
process.env.AUTHFLOW = null;;
|
|
252
249
|
await hapiMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
253
250
|
console.error('[Note] Using HAPI HTTP server...')
|
|
254
251
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassoftware/sas-score-mcp-serverjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"start": "node cli.js",
|
|
13
|
+
"start": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 node cli.js",
|
|
14
14
|
"testi": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 npx @modelcontextprotocol/inspector",
|
|
15
15
|
"test": "cd test && node",
|
|
16
16
|
"debug": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 node --inspect-brk cli.js",
|
package/src/expressMcpServer.js
CHANGED
|
@@ -6,17 +6,14 @@ import express from "express";
|
|
|
6
6
|
|
|
7
7
|
import https from "https";
|
|
8
8
|
import cors from "cors";
|
|
9
|
-
//import rateLimit from "express-rate-limit";
|
|
10
|
-
//import helmet from "helmet";
|
|
11
9
|
import bodyParser from "body-parser";
|
|
12
|
-
|
|
13
10
|
import selfsigned from "selfsigned";
|
|
14
11
|
import openAPIJson from "./openAPIJson.js";
|
|
15
|
-
import fs from "fs";
|
|
16
12
|
|
|
17
13
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
18
14
|
import { randomUUID } from "node:crypto";
|
|
19
15
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import tlogon from "./toolHelpers/tlogon.js";
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
// setup express server
|
|
@@ -115,14 +112,16 @@ function requireBearer(req, res, next) {
|
|
|
115
112
|
if (hdr != null) {
|
|
116
113
|
headerCache.bearerToken = hdr.slice(7);
|
|
117
114
|
headerCache.AUTHFLOW = "bearer";
|
|
115
|
+
console.error("[Note] Using user supplied bearer token for authorization");
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
// faking out api key since Viya does not support
|
|
121
119
|
// not ideal for production
|
|
122
120
|
const hdr2 = req.header("X-REFRESH-TOKEN");
|
|
123
121
|
if (hdr2 != null) {
|
|
124
|
-
headerCache.
|
|
122
|
+
headerCache.REFRESH_TOKEN = hdr2;
|
|
125
123
|
headerCache.AUTHFLOW = "refresh";
|
|
124
|
+
console.error("[Note] Using user supplied refresh token for authorization");
|
|
126
125
|
}
|
|
127
126
|
cache.set("headerCache", headerCache);
|
|
128
127
|
next();
|
|
@@ -132,6 +131,10 @@ function requireBearer(req, res, next) {
|
|
|
132
131
|
const handleRequest = async (req, res) => {
|
|
133
132
|
let transport;
|
|
134
133
|
let transports = cache.get("transports");
|
|
134
|
+
if (transports == null) {
|
|
135
|
+
console.error("[Error] No transports found in cache. Initializing empty transports object.");
|
|
136
|
+
transports = {'dummy': null };
|
|
137
|
+
}
|
|
135
138
|
console.error("current transports in cache:", Object.keys(transports));
|
|
136
139
|
try {
|
|
137
140
|
|
|
@@ -221,12 +224,15 @@ const handleRequest = async (req, res) => {
|
|
|
221
224
|
};
|
|
222
225
|
const handleGetDelete = async (req, res) => {
|
|
223
226
|
console.error(req.method, "/mcp called");
|
|
227
|
+
|
|
224
228
|
const sessionId = req.headers["mcp-session-id"];
|
|
229
|
+
console.error("Headers:", sessionId);
|
|
225
230
|
console.error("Handling GET/DELETE for session ID:", sessionId);
|
|
226
231
|
let transports = cache.get("transports");
|
|
227
232
|
let transport = (sessionId == null) ? null : transports[sessionId];
|
|
233
|
+
console.error("Found transport:", transport != null);
|
|
228
234
|
if (!sessionId || transport == null) {
|
|
229
|
-
res.status(
|
|
235
|
+
res.status(200).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
|
|
230
236
|
return;
|
|
231
237
|
}
|
|
232
238
|
await transport.handleRequest(req, res);
|
|
@@ -248,7 +254,15 @@ app.get("/startup", (_req, res) => {
|
|
|
248
254
|
}
|
|
249
255
|
return res.status(200).json({ status: "started" });
|
|
250
256
|
});
|
|
251
|
-
|
|
257
|
+
app.get("/tlogon", async (_req, res) => {
|
|
258
|
+
console.error(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Testing logon");
|
|
259
|
+
if (appStatus === false) {
|
|
260
|
+
return res.status(500).json({ status: "not ready" });
|
|
261
|
+
}
|
|
262
|
+
let r = await tlogon(baseAppEnvContext);
|
|
263
|
+
console.error(r);
|
|
264
|
+
return res.status(200).json(r);
|
|
265
|
+
});
|
|
252
266
|
app.get("/status", (_req, res) => {
|
|
253
267
|
console.error("Received request for status endpoint. Current app status:", appStatus);
|
|
254
268
|
if (appStatus === false) {
|
|
@@ -274,12 +288,12 @@ let appServer;
|
|
|
274
288
|
|
|
275
289
|
// get TLS options
|
|
276
290
|
if (appEnvBase.HTTPS === 'TRUE') {
|
|
277
|
-
//appEnvBase.tlsOpts = getOpts(appEnvBase);
|
|
278
291
|
if (appEnvBase.tlsOpts == null) {
|
|
279
292
|
appEnvBase.tlsOpts = await getTls(appEnvBase);
|
|
280
293
|
console.error(Object.keys(appEnvBase.tlsOpts));
|
|
281
294
|
appEnvBase.tlsOpts.requestCert = false;
|
|
282
295
|
appEnvBase.tlsOpts.rejectUnauthorized = false;
|
|
296
|
+
appEnvBase.contexts.appCert = appEnvBase.tlsOpts; /* just for completeness */
|
|
283
297
|
}
|
|
284
298
|
|
|
285
299
|
cache.set("appEnvBase", appEnvBase);
|
package/src/hapiMcpServer.js
CHANGED
|
@@ -126,6 +126,44 @@ async function hapiMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
126
126
|
tags: ["mcp"],
|
|
127
127
|
}
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
method: ["GET"],
|
|
131
|
+
path: "/startz",
|
|
132
|
+
options: {
|
|
133
|
+
handler: async (req, h) => {
|
|
134
|
+
let status = { status: (process.env.HEALTH === 'true') ? 'ready' : 'starting' };
|
|
135
|
+
console.error("startz check requested, returning:", status);
|
|
136
|
+
if (process.env.HEALTH !== 'true') {
|
|
137
|
+
return h.response(status).code(200).type('application/json');
|
|
138
|
+
} else {
|
|
139
|
+
return h.response(status).code(503).type('application/json');
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
auth: false,
|
|
143
|
+
description: "Help",
|
|
144
|
+
notes: "Help",
|
|
145
|
+
tags: ["mcp"],
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
method: ["GET"],
|
|
150
|
+
path: "/readyz",
|
|
151
|
+
options: {
|
|
152
|
+
handler: async (req, h) => {
|
|
153
|
+
let status = { status: (process.env.HEALTH === 'true') ? 'ready' : 'starting' };
|
|
154
|
+
console.error("readyz check requested, returning:", status);
|
|
155
|
+
if (process.env.HEALTH !== 'true') {
|
|
156
|
+
return h.response(status).code(200).type('application/json');
|
|
157
|
+
} else {
|
|
158
|
+
return h.response(status).code(503).type('application/json');
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
auth: false,
|
|
162
|
+
description: "Help",
|
|
163
|
+
notes: "Help",
|
|
164
|
+
tags: ["mcp"],
|
|
165
|
+
}
|
|
166
|
+
},
|
|
129
167
|
{
|
|
130
168
|
method: ["GET"],
|
|
131
169
|
path: "/apiMeta",
|
package/src/openAPIJson.js
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
function openAPIJson(version) {
|
|
6
|
-
let spec = {
|
|
7
|
-
"swagger": "2.0",
|
|
8
|
-
"info": {
|
|
9
|
-
"title": "sas-score-mcp-serverjs API",
|
|
10
|
-
"version": "1.0.0",
|
|
11
|
-
"description": "sas-score-mcp-serverjs is a mcp server for SAS Viya"
|
|
12
|
-
},
|
|
13
|
-
"host": "localhost:8080",
|
|
14
|
-
"basePath": "/",
|
|
15
|
-
"schemes": ["http", "https"],
|
|
16
|
-
"consumes": ["application/json"],
|
|
17
|
-
"produces": ["application/json"],
|
|
18
|
-
"paths": {
|
|
19
|
-
"/health": {
|
|
20
|
-
"get": {
|
|
21
|
-
"summary": "Health check",
|
|
22
|
-
"operationId": "getHealth",
|
|
23
|
-
"description": "Returns health and version information.",
|
|
24
|
-
"responses": {
|
|
25
|
-
"200": {
|
|
26
|
-
"description": "Health information",
|
|
27
|
-
"schema": {
|
|
28
|
-
"type": "object",
|
|
29
|
-
"properties": {
|
|
30
|
-
"name": { "type": "string" },
|
|
31
|
-
"version": { "type": "string" },
|
|
32
|
-
"description": { "type": "string" },
|
|
33
|
-
"endpoints": { "type": "object" },
|
|
34
|
-
"usage": { "type": "string" }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
"/apiMeta": {
|
|
42
|
-
"get": {
|
|
43
|
-
"summary": "API metadata using apiMeta",
|
|
44
|
-
"operationId": "GetApiMeta",
|
|
45
|
-
"responses": {
|
|
46
|
-
"200": {
|
|
47
|
-
"description": "OpenAPI document",
|
|
48
|
-
"schema": { "type": "object" }
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
"/openapi.json": {
|
|
54
|
-
"get": {
|
|
55
|
-
"summary": "API metadata using openapi.json",
|
|
56
|
-
"operationId": "GetOpenApiJson",
|
|
57
|
-
"description": "Returns the OpenAPI specification for this server.",
|
|
58
|
-
"responses": {
|
|
59
|
-
"200": {
|
|
60
|
-
"description": "OpenAPI document",
|
|
61
|
-
"schema": { "type": "object" }
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
"/mcp": {
|
|
67
|
-
"options": {
|
|
68
|
-
"summary": "CORS preflight",
|
|
69
|
-
"operationId": "OptionsMcp",
|
|
70
|
-
"description": "CORS preflight endpoint.",
|
|
71
|
-
"responses": {
|
|
72
|
-
"204": { "description": "No Content" }
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
"post": {
|
|
76
|
-
"summary": "MCP request",
|
|
77
|
-
"operationId": "PostMcp",
|
|
78
|
-
"parameters": [
|
|
79
|
-
{
|
|
80
|
-
"name": "body",
|
|
81
|
-
"in": "body",
|
|
82
|
-
"required": true,
|
|
83
|
-
"schema": { "type": "object" }
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"name": "Authorization",
|
|
87
|
-
"in": "header",
|
|
88
|
-
"required": false,
|
|
89
|
-
"type": "string",
|
|
90
|
-
"description": "Bearer token for authentication"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"name": "X-VIYA-SERVER",
|
|
94
|
-
"in": "header",
|
|
95
|
-
"required": false,
|
|
96
|
-
"type": "string",
|
|
97
|
-
"description": "Override VIYA server"
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"name": "X-REFRESH-TOKEN",
|
|
101
|
-
"in": "header",
|
|
102
|
-
"required": false,
|
|
103
|
-
"type": "string",
|
|
104
|
-
"description": "Refresh token for authentication"
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"name": "mcp-session-id",
|
|
108
|
-
"in": "header",
|
|
109
|
-
"required": false,
|
|
110
|
-
"type": "string",
|
|
111
|
-
"description": "Session ID"
|
|
112
|
-
}
|
|
113
|
-
],
|
|
114
|
-
"responses": {
|
|
115
|
-
"200": {
|
|
116
|
-
"description": "MCP response",
|
|
117
|
-
"schema": { "type": "object" }
|
|
118
|
-
},
|
|
119
|
-
"500": {
|
|
120
|
-
"description": "Server error",
|
|
121
|
-
"schema": { "type": "object" }
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
"get": {
|
|
126
|
-
"summary": "get MCP session",
|
|
127
|
-
"operationId": "GetMcp",
|
|
128
|
-
"description": "Retrieves information for an MCP session.",
|
|
129
|
-
"parameters": [
|
|
130
|
-
{
|
|
131
|
-
"name": "mcp-session-id",
|
|
132
|
-
"in": "header",
|
|
133
|
-
"required": true,
|
|
134
|
-
"type": "string",
|
|
135
|
-
"description": "Session ID"
|
|
136
|
-
}
|
|
137
|
-
],
|
|
138
|
-
"responses": {
|
|
139
|
-
"200": {
|
|
140
|
-
"description": "Session information",
|
|
141
|
-
"schema": { "type": "object" }
|
|
142
|
-
},
|
|
143
|
-
"400": {
|
|
144
|
-
"description": "Invalid or missing session ID"
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
"delete": {
|
|
149
|
-
"summary": "Delete MCP session",
|
|
150
|
-
"operationId": "DeleteMcp",
|
|
151
|
-
"parameters": [
|
|
152
|
-
{
|
|
153
|
-
"name": "mcp-session-id",
|
|
154
|
-
"in": "header",
|
|
155
|
-
"required": true,
|
|
156
|
-
"type": "string",
|
|
157
|
-
"description": "Session ID"
|
|
158
|
-
}
|
|
159
|
-
],
|
|
160
|
-
"responses": {
|
|
161
|
-
"200": {
|
|
162
|
-
"description": "Session deleted",
|
|
163
|
-
"schema": { "type": "object" }
|
|
164
|
-
},
|
|
165
|
-
"400": {
|
|
166
|
-
"description": "Invalid or missing session ID"
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
spec.info.version = version;
|
|
174
|
-
return spec;
|
|
175
|
-
};
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
function openAPIJson(version) {
|
|
6
|
+
let spec = {
|
|
7
|
+
"swagger": "2.0",
|
|
8
|
+
"info": {
|
|
9
|
+
"title": "sas-score-mcp-serverjs API",
|
|
10
|
+
"version": "1.0.0",
|
|
11
|
+
"description": "sas-score-mcp-serverjs is a mcp server for SAS Viya"
|
|
12
|
+
},
|
|
13
|
+
"host": "localhost:8080",
|
|
14
|
+
"basePath": "/",
|
|
15
|
+
"schemes": ["http", "https"],
|
|
16
|
+
"consumes": ["application/json"],
|
|
17
|
+
"produces": ["application/json"],
|
|
18
|
+
"paths": {
|
|
19
|
+
"/health": {
|
|
20
|
+
"get": {
|
|
21
|
+
"summary": "Health check",
|
|
22
|
+
"operationId": "getHealth",
|
|
23
|
+
"description": "Returns health and version information.",
|
|
24
|
+
"responses": {
|
|
25
|
+
"200": {
|
|
26
|
+
"description": "Health information",
|
|
27
|
+
"schema": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"name": { "type": "string" },
|
|
31
|
+
"version": { "type": "string" },
|
|
32
|
+
"description": { "type": "string" },
|
|
33
|
+
"endpoints": { "type": "object" },
|
|
34
|
+
"usage": { "type": "string" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"/apiMeta": {
|
|
42
|
+
"get": {
|
|
43
|
+
"summary": "API metadata using apiMeta",
|
|
44
|
+
"operationId": "GetApiMeta",
|
|
45
|
+
"responses": {
|
|
46
|
+
"200": {
|
|
47
|
+
"description": "OpenAPI document",
|
|
48
|
+
"schema": { "type": "object" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"/openapi.json": {
|
|
54
|
+
"get": {
|
|
55
|
+
"summary": "API metadata using openapi.json",
|
|
56
|
+
"operationId": "GetOpenApiJson",
|
|
57
|
+
"description": "Returns the OpenAPI specification for this server.",
|
|
58
|
+
"responses": {
|
|
59
|
+
"200": {
|
|
60
|
+
"description": "OpenAPI document",
|
|
61
|
+
"schema": { "type": "object" }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"/mcp": {
|
|
67
|
+
"options": {
|
|
68
|
+
"summary": "CORS preflight",
|
|
69
|
+
"operationId": "OptionsMcp",
|
|
70
|
+
"description": "CORS preflight endpoint.",
|
|
71
|
+
"responses": {
|
|
72
|
+
"204": { "description": "No Content" }
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"post": {
|
|
76
|
+
"summary": "MCP request",
|
|
77
|
+
"operationId": "PostMcp",
|
|
78
|
+
"parameters": [
|
|
79
|
+
{
|
|
80
|
+
"name": "body",
|
|
81
|
+
"in": "body",
|
|
82
|
+
"required": true,
|
|
83
|
+
"schema": { "type": "object" }
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "Authorization",
|
|
87
|
+
"in": "header",
|
|
88
|
+
"required": false,
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Bearer token for authentication"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "X-VIYA-SERVER",
|
|
94
|
+
"in": "header",
|
|
95
|
+
"required": false,
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Override VIYA server"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "X-REFRESH-TOKEN",
|
|
101
|
+
"in": "header",
|
|
102
|
+
"required": false,
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "Refresh token for authentication"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "mcp-session-id",
|
|
108
|
+
"in": "header",
|
|
109
|
+
"required": false,
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Session ID"
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"responses": {
|
|
115
|
+
"200": {
|
|
116
|
+
"description": "MCP response",
|
|
117
|
+
"schema": { "type": "object" }
|
|
118
|
+
},
|
|
119
|
+
"500": {
|
|
120
|
+
"description": "Server error",
|
|
121
|
+
"schema": { "type": "object" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"get": {
|
|
126
|
+
"summary": "get MCP session",
|
|
127
|
+
"operationId": "GetMcp",
|
|
128
|
+
"description": "Retrieves information for an MCP session.",
|
|
129
|
+
"parameters": [
|
|
130
|
+
{
|
|
131
|
+
"name": "mcp-session-id",
|
|
132
|
+
"in": "header",
|
|
133
|
+
"required": true,
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "Session ID"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"responses": {
|
|
139
|
+
"200": {
|
|
140
|
+
"description": "Session information",
|
|
141
|
+
"schema": { "type": "object" }
|
|
142
|
+
},
|
|
143
|
+
"400": {
|
|
144
|
+
"description": "Invalid or missing session ID"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"delete": {
|
|
149
|
+
"summary": "Delete MCP session",
|
|
150
|
+
"operationId": "DeleteMcp",
|
|
151
|
+
"parameters": [
|
|
152
|
+
{
|
|
153
|
+
"name": "mcp-session-id",
|
|
154
|
+
"in": "header",
|
|
155
|
+
"required": true,
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "Session ID"
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"responses": {
|
|
161
|
+
"200": {
|
|
162
|
+
"description": "Session deleted",
|
|
163
|
+
"schema": { "type": "object" }
|
|
164
|
+
},
|
|
165
|
+
"400": {
|
|
166
|
+
"description": "Invalid or missing session ID"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
spec.info.version = version;
|
|
174
|
+
return spec;
|
|
175
|
+
};
|
|
176
176
|
export default openAPIJson;
|
|
@@ -13,7 +13,7 @@ async function getLogonPayload(_appContext) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async function igetLogonPayload(_appContext) {
|
|
16
|
-
|
|
16
|
+
console.error('[Info] Getting logon payload...',_appContext.AUTHFLOW);
|
|
17
17
|
// Use cached logonPayload if available
|
|
18
18
|
// This will cause timeouts if the token expires
|
|
19
19
|
if (_appContext.contexts.logonPayload != null && _appContext.tokenRefresh !== true) {
|
|
@@ -57,12 +57,12 @@ async function igetLogonPayload(_appContext) {
|
|
|
57
57
|
|
|
58
58
|
// Use user supplied refresh token-
|
|
59
59
|
if (_appContext.AUTHFLOW === "refresh") {
|
|
60
|
-
console.error("[Note] Using user supplied refresh token");
|
|
61
|
-
let token = await refreshToken(_appContext,{token: _appContext.
|
|
60
|
+
console.error("[Note] Using user supplied refresh token", _appContext.REFRESH_TOKEN);
|
|
61
|
+
let token = await refreshToken(_appContext,{token: _appContext.REFRESH_TOKEN, host: _appContext.VIYA_SERVER});
|
|
62
62
|
let logonPayload = {
|
|
63
63
|
host: _appContext.VIYA_SERVER,
|
|
64
64
|
authType: "server",
|
|
65
|
-
token: token,
|
|
65
|
+
token: token,
|
|
66
66
|
tokenType: "Bearer",
|
|
67
67
|
};
|
|
68
68
|
|
|
@@ -97,8 +97,8 @@ async function igetLogonPayload(_appContext) {
|
|
|
97
97
|
authType: "password",
|
|
98
98
|
user: _appContext.USERNAME,
|
|
99
99
|
password: _appContext.PASSWORD,
|
|
100
|
-
clientID: _appContext.
|
|
101
|
-
clientSecret: _appContext.
|
|
100
|
+
clientID: _appContext.CLIENTID,
|
|
101
|
+
clientSecret: _appContext.CLIENTSECRET,
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
return logonPayload;
|
|
@@ -2,11 +2,10 @@
|
|
|
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 getOptsViya from './getOptsViya.js';
|
|
6
5
|
|
|
7
6
|
function getStoreOpts(_appContext) {
|
|
8
7
|
|
|
9
|
-
let opts =
|
|
8
|
+
let opts = _appContext.contexts.viyaCert;
|
|
10
9
|
let storeOpts = {
|
|
11
10
|
casProxy: true,
|
|
12
11
|
httpOptions: { ...opts, rejectUnauthorized: true }
|
|
@@ -15,7 +15,6 @@ async function getToken(_appContext) {
|
|
|
15
15
|
let sep = (os.platform() === 'win32') ? '\\' : '/';
|
|
16
16
|
let credentials = homedir + sep + '.sas' + sep + 'credentials.json';
|
|
17
17
|
let url = homedir + sep + '.sas' + sep + 'config.json';
|
|
18
|
-
console.error('[Note] Using credentials file: ' + credentials);
|
|
19
18
|
console.error('[Note] Using config file: ' + url);
|
|
20
19
|
let profile = (_appContext.SAS_CLI_PROFILE == null || _appContext.SAS_CLI_PROFILE.toLowerCase() === 'default')
|
|
21
20
|
? 'Default' : _appContext.SAS_CLI_PROFILE;
|
|
@@ -1,49 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let {host, token} = params;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
|
|
7
|
+
async function refreshToken(_appContext, params) {
|
|
8
|
+
let { host, token } = params;
|
|
9
|
+
let url = `${host}/SASLogon/oauth/token`;
|
|
10
|
+
|
|
11
|
+
let aconnect = {
|
|
12
|
+
ca: _appContext.contexts.viyaCert.ca, // trust this CA
|
|
13
|
+
rejectUnauthorized: false // or false, if you really want to bypass checks
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const agent = new Agent(aconnect);
|
|
17
|
+
|
|
18
|
+
console.error('[Info] Refreshing token...', token);
|
|
19
|
+
const ibody = {
|
|
20
|
+
grant_type: 'refresh_token',
|
|
21
|
+
refresh_token: token,
|
|
22
|
+
client_id: 'sas.cli'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let body = new URLSearchParams(ibody);
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Accept': 'application/json',
|
|
31
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
32
|
+
dispatcher: agent
|
|
33
|
+
},
|
|
34
|
+
body: body.toString()
|
|
21
35
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
console.error('[Info] Token refreshed successfully: ', data.access_token);
|
|
42
|
-
return data.access_token;
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.error('[Error] Failed to refresh token: ', err);
|
|
45
|
-
throw err;
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = await response.text();
|
|
39
|
+
console.error('[Error] Failed to refresh token: ', error);
|
|
40
|
+
throw new Error(error);
|
|
46
41
|
}
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
return data.access_token;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
47
|
+
throw err;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default refreshToken;
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { Agent, fetch } from 'undici';
|
|
6
|
-
import getOpts from './getOpts.js';
|
|
6
|
+
//import getOpts from './getOpts.js';
|
|
7
7
|
async function refreshTokenOauth(_appContext, oauthInfo ){
|
|
8
8
|
|
|
9
9
|
const url = `${process.env.VIYA_SERVER}/SASLogon/oauth/token`;
|
|
10
|
-
let opts =
|
|
10
|
+
let opts = _appContext.contexts.appCert;
|
|
11
11
|
|
|
12
12
|
const agent = new Agent({
|
|
13
13
|
connect: opts
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import getLogonPayload from "./getLogonPayload.js";
|
|
2
|
+
|
|
3
|
+
async function tlogon(_appContext) {
|
|
4
|
+
console.error('[Info] Starting tlogon...');
|
|
5
|
+
let r = await getLogonPayload(_appContext);
|
|
6
|
+
console.error('[Info] tlogon completed');
|
|
7
|
+
return r;
|
|
8
|
+
}
|
|
9
|
+
export default tlogon;
|
package/src/toolSet/devaScore.js
CHANGED
|
@@ -38,7 +38,7 @@ Disambiguation & Clarification
|
|
|
38
38
|
Examples (→ mapped params)
|
|
39
39
|
- "Calculate deva score for 5 and 10" → { a: 5, b: 10 } returns 630
|
|
40
40
|
- "Score 1 and 2" → { a: 1, b: 2 } returns 126
|
|
41
|
-
- For multiple numbers, chain calls:
|
|
41
|
+
- For multiple numbers, chain calls: devaScore(1,2)→126, then devaScore(126,3)→5418
|
|
42
42
|
|
|
43
43
|
Negative Examples (should NOT call deva-score)
|
|
44
44
|
- "Score this customer with the credit model" (use model-score instead)
|
|
@@ -58,11 +58,11 @@ For sequences of numbers, use a left-to-right fold: call devaScore(first, second
|
|
|
58
58
|
a: z.number(),
|
|
59
59
|
b: z.number()
|
|
60
60
|
},
|
|
61
|
-
handler: async ({ a, b
|
|
61
|
+
handler: async ({ a, b }) => {
|
|
62
62
|
console.error( a, b);
|
|
63
|
-
return {
|
|
64
|
-
text: String((a + b) * 42) }] }
|
|
63
|
+
return `MagicScore ${ (a + b) * 42 }`;
|
|
65
64
|
}
|
|
65
|
+
|
|
66
66
|
}
|
|
67
67
|
return spec;
|
|
68
68
|
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Helper function to get TLS options(for the app server) from specified directory
|
|
7
|
-
* signed certificates
|
|
8
|
-
* for testing you can use mkcert
|
|
9
|
-
* if this function return a null, coreehttp will create unsigned certs
|
|
10
|
-
* @param {Object} _appContext - Application context containing SSLCERT property
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import readCerts from './readCerts.js';
|
|
14
|
-
function getOpts(_appContext) {
|
|
15
|
-
|
|
16
|
-
if (_appContext.tlsOpts != null) {
|
|
17
|
-
return _appContext.tlsOpts;
|
|
18
|
-
}
|
|
19
|
-
let r = readCerts(_appContext.SSLCERT);
|
|
20
|
-
_appContext.tlsOpts = r;
|
|
21
|
-
return r;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/*
|
|
25
|
-
let tlsdir = _appContext.SSLCERT;
|
|
26
|
-
if (tlsdir == null || tlsdir === 'NONE') {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
console.error("[Note] Using TLS dir: " + tlsdir);
|
|
31
|
-
if (fs.existsSync(tlsdir) === false) {
|
|
32
|
-
console.error("[Warning] Specified TLS dir does not exist: " + tlsdir);
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let listOfFiles = fs.readdirSync(tlsdir);
|
|
37
|
-
console.error("[Note] TLS/SSL files found: " + listOfFiles);
|
|
38
|
-
let options = {};
|
|
39
|
-
for(let i=0; i < listOfFiles.length; i++) {
|
|
40
|
-
let fname = listOfFiles[i];
|
|
41
|
-
let name = tlsdir + '/' + listOfFiles[i];
|
|
42
|
-
let key = fname.split('.')[0];
|
|
43
|
-
options[key] = fs.readFileSync(name, { encoding: 'utf8' });
|
|
44
|
-
}
|
|
45
|
-
console.error('TLS FILES', Object.keys(options));
|
|
46
|
-
_appContext.tlsOpts = options;
|
|
47
|
-
return options;
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
export default getOpts;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
import readCerts from './readCerts.js';
|
|
6
|
-
function getOptsViya(_appContext) {
|
|
7
|
-
|
|
8
|
-
if (_appContext.contexts.viyaCert != null) {
|
|
9
|
-
console.error('[Note] Using cached viyaOpts');
|
|
10
|
-
return _appContext.contexts.viyaCert;
|
|
11
|
-
}
|
|
12
|
-
let r = readCerts(_appContext.VIYACERT);
|
|
13
|
-
_appContext.contexts.viyaCert = r;
|
|
14
|
-
return r;
|
|
15
|
-
|
|
16
|
-
/*
|
|
17
|
-
let tlsdir = _appContext.VIYACERT;
|
|
18
|
-
if (tlsdir == null || tlsdir === 'NONE') {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
console.error(`[Note] Using VIYACERT dir: ` + tlsdir);
|
|
23
|
-
if (fs.existsSync(tlsdir) === false) {
|
|
24
|
-
console.error("[Warning] Specified VIYACERT dir does not exist: " + tlsdir);
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let listOfFiles = fs.readdirSync(tlsdir);
|
|
29
|
-
console.error("[Note] TLS/SSL files found: " + listOfFiles);
|
|
30
|
-
let options = {};
|
|
31
|
-
for(let i=0; i < listOfFiles.length; i++) {
|
|
32
|
-
let fname = listOfFiles[i];
|
|
33
|
-
let name = tlsdir + '/' + listOfFiles[i];
|
|
34
|
-
let key = fname.split('.')[0];
|
|
35
|
-
console.error('Reading TLS file: ' + name + ' as key: ' + key);
|
|
36
|
-
options[key] = fs.readFileSync(name, { encoding: 'utf8' });
|
|
37
|
-
}
|
|
38
|
-
console.error('VIYACERT FILES', Object.keys(options));
|
|
39
|
-
_appContext.contexts.viyaCert = options;
|
|
40
|
-
return options;
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
export default getOptsViya;
|