@sassoftware/sas-score-mcp-serverjs 1.0.1-29 → 1.0.1-30
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/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
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 fs from "fs";
|
|
7
|
+
|
|
8
|
+
refreshToken()
|
|
9
|
+
.then (token => {
|
|
10
|
+
console.log(token);
|
|
11
|
+
fs.writeFileSync('token.txt', token, 'utf8');
|
|
12
|
+
})
|
|
13
|
+
.catch (err => {
|
|
14
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
15
|
+
});
|
|
16
|
+
async function refreshToken(){
|
|
17
|
+
let host = process.env.VIYA_SERVER;
|
|
18
|
+
let token = "8afd75c0df5141b1a1d27dfe9db06fa5-r";
|
|
19
|
+
let url = `${host}/SASLogon/oauth/token`;
|
|
20
|
+
|
|
21
|
+
let aconnect = {
|
|
22
|
+
rejectUnauthorized: false // or false, if you really want to bypass checks
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const agent = new Agent(aconnect);
|
|
26
|
+
|
|
27
|
+
console.error('[Info] Refreshing token...', token);
|
|
28
|
+
const ibody = {
|
|
29
|
+
grant_type: 'refresh_token',
|
|
30
|
+
refresh_token: token,
|
|
31
|
+
client_id: 'sas.cli'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let body = new URLSearchParams(ibody);
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Accept': 'application/json',
|
|
40
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
41
|
+
dispatcher: agent
|
|
42
|
+
},
|
|
43
|
+
body: body.toString()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = await response.text();
|
|
48
|
+
console.error('[Error] Failed to refresh token: ', error);
|
|
49
|
+
throw new Error(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
return data.access_token;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eyJqa3UiOiJodHRwczovL2xvY2FsaG9zdC9TQVNMb2dvbi90b2tlbl9rZXlzIiwia2lkIjoibGVnYWN5LXRva2VuLWtleSIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxNjQzMmUzNC1kNTFiLTQ0ODUtYmRhNS0xMTBkYWI2ODJkMjAiLCJ1c2VyX25hbWUiOiJkZXZhLmt1bWFyQHNhcy5jb20iLCJvcmlnaW4iOiJleHRlcm5hbF9vYXV0aCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3QvU0FTTG9nb24vb2F1dGgvdG9rZW4iLCJhdXRob3JpdGllcyI6WyJWU0NvZGVHZW5BSSIsIkRhdGFRdWFsaXR5LkRhdGFRdWFsaXR5TW9uaXRvcmluZ0FkbWluaXN0cmF0b3JzIiwiU2NoZWR1bGVTZXJ2aWNlQWNjb3VudFVzZXJzIiwiQXBwbGljYXRpb25BZG1pbmlzdHJhdG9ycyIsIkJhdGNoU2VydmljZUFjY291bnRVc2VycyIsIkxhdW5jaGVyU3VwZXJVc2VycyIsIkVzcmlVc2VycyIsIkRhdGFBZ2VudEFkbWluaXN0cmF0b3JzIiwiU0NJTSIsIkRhdGFBZ2VudFBvd2VyVXNlcnMiLCJTQVNTY29yZVVzZXJzIiwiU0FTQWRtaW5pc3RyYXRvcnMiLCJHbG9zc2FyeS5HbG9zc2FyeUFkbWluaXN0cmF0b3JzIiwiQ2F0YWxvZy5TdWJqZWN0TWF0dGVyRXhwZXJ0cyIsIkNvbXB1dGVTZXJ2aWNlQWNjb3VudFVzZXJzIiwiQ0FTSG9zdEFjY291bnRSZXF1aXJlZCJdLCJjbGllbnRfaWQiOiJzYXMuY2xpIiwiYXVkIjpbInVhYSIsInNhcy1jb21wdXRlIiwic2FzLXRyYW5zZmVyIiwic2FzLXdvcmtsb2FkLW9yY2hlc3RyYXRvciIsInNhcy1jb25uZWN0Iiwic2FzLWNhcy1tYW5hZ2VtZW50Iiwic2FzLXNjaGVkdWxlciIsImNsaWVudHMiLCJzYXMtcmVwb3J0LXBhY2thZ2VzIiwib3BlbmlkIiwic2FzLWZvbnRzIiwic2FzLWNyZWRlbnRpYWxzIiwic2FzLWNvbmZpZ3VyYXRpb24iLCJzYXMuY2xpIiwic2FzLWF1dGhvcml6YXRpb24iLCJzYXMtZGV2aWNlLW1hbmFnZW1lbnQiLCJzYXMtZm9sZGVycyIsInNhcy1qb2ItZXhlY3V0aW9uIiwic2FzLWF1ZGl0Iiwic2FzLWlkZW50aXRpZXMiLCJzYXMtYmF0Y2giLCJzY2ltIiwic2FzLWxhdW5jaGVyIl0sImV4dF9pZCI6IjAwdTJzcTg3Ynh0YTI2OEhaMnA3IiwicmVtb3RlX2lwIjoiMTQ5LjE3My40NS44OCIsInppZCI6InVhYSIsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJ1c2VyX2lkIjoiMTY0MzJlMzQtZDUxYi00NDg1LWJkYTUtMTEwZGFiNjgyZDIwIiwiYXpwIjoic2FzLmNsaSIsInNjb3BlIjpbInNhcy1jb21wdXRlLnVzZXJfaW1wZXJzb25hdGlvbiIsInNhcy1jYXMtbWFuYWdlbWVudC51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtaWRlbnRpdGllcy51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtbGF1bmNoZXIudXNlcl9pbXBlcnNvbmF0aW9uIiwic2FzLWNyZWRlbnRpYWxzLnVzZXJfaW1wZXJzb25hdGlvbiIsInNhcy1hdWRpdC51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtY29uZmlndXJhdGlvbi51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtc2NoZWR1bGVyLnVzZXJfaW1wZXJzb25hdGlvbiIsImNsaWVudHMucmVhZCIsInNhcy1iYXRjaC51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtam9iLWV4ZWN1dGlvbi51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtZGV2aWNlLW1hbmFnZW1lbnQudXNlcl9pbXBlcnNvbmF0aW9uIiwiY2xpZW50cy5zZWNyZXQiLCJvcGVuaWQiLCJzYXMtZm9udHMudXNlcl9pbXBlcnNvbmF0aW9uIiwic2FzLXdvcmtsb2FkLW9yY2hlc3RyYXRvci51c2VyX2ltcGVyc29uYXRpb24iLCJ1YWEuYWRtaW4iLCJjbGllbnRzLmFkbWluIiwic2FzLWF1dGhvcml6YXRpb24udXNlcl9pbXBlcnNvbmF0aW9uIiwic2NpbS5yZWFkIiwidWFhLnVzZXIiLCJzYXMtY29ubmVjdC51c2VyX2ltcGVyc29uYXRpb24iLCJzYXMtcmVwb3J0LXBhY2thZ2VzLnVzZXJfaW1wZXJzb25hdGlvbiIsInNhcy1mb2xkZXJzLnVzZXJfaW1wZXJzb25hdGlvbiIsIlNBU0FkbWluaXN0cmF0b3JzIiwiY2xpZW50cy53cml0ZSIsInNjaW0ud3JpdGUiLCJzYXMtdHJhbnNmZXIudXNlcl9pbXBlcnNvbmF0aW9uIl0sImF1dGhfdGltZSI6MTc3ODY4NjQ5NywiZXhwIjoxNzc4NzAxMTY1LCJpYXQiOjE3Nzg2OTc1NjUsImp0aSI6ImFjNTk0ZGQyMDA0NDQwYTBhYjAzZGJlNzlmMWIzMzI5IiwiZW1haWwiOiJkZXZhLmt1bWFyQHNhcy5jb20iLCJyZXZfc2lnIjoiMmVjZTJiZDAiLCJjbGllbnRfYXV0aF9tZXRob2QiOiJub25lIiwiY2lkIjoic2FzLmNsaSJ9.fBObEchePj1_u3xpdXaXuy3O_7fiZyYSY38fK37LEduGs45ktDaYi_F7hWCQnHEK45b8Ti_ROcyesCVjUcXPzzbw722PYEDT4909qnUM-n0DXEV1tCoOkSbMF7Gv-wbRif7cJP2Q3G-sO5dkw9blGJC-YbvzR04f-TqhcQqmaxQLh7E-G4qIvKtcKtB9j4pCwwc-Dz9LxqIcY5xpEF92wnqYdKtNGSGQsTHyR0cCNAkQBv-dO6snhplBMVZrSGbuo0mGZcA3736n1JJ1jKvPT1rPKn7vtUO30sLfNP3rL21SAEZ6wO3SOs_liJNMllC_cJq5jYytx0rfYuubUH0xZA
|
package/src/expressMcpServer.js
CHANGED
|
@@ -9,6 +9,7 @@ import cors from "cors";
|
|
|
9
9
|
import bodyParser from "body-parser";
|
|
10
10
|
import selfsigned from "selfsigned";
|
|
11
11
|
import openAPIJson from "./openAPIJson.js";
|
|
12
|
+
import createMcpServer from "./createMcpServer.js";
|
|
12
13
|
|
|
13
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
15
|
import { randomUUID } from "node:crypto";
|
|
@@ -21,7 +22,7 @@ import processHeaders from "./processHeaders.js";
|
|
|
21
22
|
|
|
22
23
|
// setup express server
|
|
23
24
|
|
|
24
|
-
async function expressMcpServer(
|
|
25
|
+
async function expressMcpServer(_mcpServer, cache, baseAppEnvContext) {
|
|
25
26
|
// setup for change to persistence session
|
|
26
27
|
cache.del("headerCache");
|
|
27
28
|
const app = express();
|
|
@@ -50,23 +51,8 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
50
51
|
const pkceStore = new Map(); // ourState -> { codeVerifier, clientRedirectUri, clientState }
|
|
51
52
|
const codeStore = new Map(); // ourCode -> { access_token, refresh_token, expires_in }
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
sessionIdGenerator: () => randomUUID(),
|
|
56
|
-
enableJsonResponse: true,
|
|
57
|
-
enableDnsRebindingProtection: true,
|
|
58
|
-
onsessioninitialized: (sessionId) => {
|
|
59
|
-
console.error("[Note] Session initialized with ID:", sessionId);
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Connect mcpServer to the shared transport ONCE
|
|
64
|
-
await mcpServer.connect(sharedTransport);
|
|
65
|
-
console.error("[Note] MCP Server connected to shared transport");
|
|
66
|
-
|
|
67
|
-
// Store the shared transport for use in request handlers
|
|
68
|
-
cache.set("sharedTransport", sharedTransport);
|
|
69
|
-
const transports = new Map(); // Track active session transports for cleanup
|
|
54
|
+
// Per-session transports — each initialize creates its own transport
|
|
55
|
+
const transports = new Map(); // sessionId -> transport
|
|
70
56
|
cache.set("transports", transports);
|
|
71
57
|
|
|
72
58
|
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
|
@@ -171,27 +157,53 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
171
157
|
|
|
172
158
|
// process mcp endpoint requests
|
|
173
159
|
const handleRequest = async (req, res) => {
|
|
174
|
-
let transport = cache.get("sharedTransport");
|
|
175
160
|
console.error("=========================================================");
|
|
176
161
|
console.error("Processing POST /mcp request");
|
|
177
|
-
|
|
178
|
-
console.error("current active sessions:", cache.get("transports").size);
|
|
162
|
+
console.error("current active sessions:", transports.size);
|
|
179
163
|
try {
|
|
180
164
|
|
|
181
165
|
let sessionId = req.headers["mcp-session-id"];
|
|
182
166
|
console.error("[Note]Incoming session ID:", sessionId);
|
|
183
167
|
let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
|
|
184
168
|
console.error('[Note] Payload is ', body);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
console.error("[Note]
|
|
169
|
+
|
|
170
|
+
if (isInitializeRequest(req.body)) {
|
|
171
|
+
console.error("[Note]>>>>>>>>>>>>>>>>>>>>>>>>> Creating new MCP server");
|
|
172
|
+
let mcpServer = await createMcpServer(cache, baseAppEnvContext);
|
|
173
|
+
// New session — create a dedicated transport
|
|
174
|
+
console.error("[Note] Initializing new session with fresh transport...");
|
|
175
|
+
const isInitRequest = req.body?.method === 'initialize';
|
|
176
|
+
|
|
177
|
+
const transport = new StreamableHTTPServerTransport({
|
|
178
|
+
sessionIdGenerator: () => randomUUID(),
|
|
179
|
+
enableJsonResponse: true,
|
|
180
|
+
onsessioninitialized: (newSessionId) => {
|
|
181
|
+
console.error("[Note] Session initialized with ID:", newSessionId);
|
|
182
|
+
transports.set(newSessionId, transport);
|
|
183
|
+
cache.set("transports", transports);
|
|
184
|
+
console.error("[Note] Total active sessions:", Object.keys(transports));
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
transport.onclose = () => {
|
|
188
|
+
if (transport.sessionId) {
|
|
189
|
+
console.error("[Note] Session closed, removing transport:", transport.sessionId);
|
|
190
|
+
transports.delete(transport.sessionId);
|
|
191
|
+
cache.del(transport.sessionId);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
await mcpServer.connect(transport);
|
|
188
195
|
console.error("=======================================================");
|
|
189
196
|
return await transport.handleRequest(req, res, req.body);
|
|
190
197
|
|
|
191
198
|
} else if (sessionId != null) {
|
|
199
|
+
const transport = transports.get(sessionId);
|
|
200
|
+
if (!transport) {
|
|
201
|
+
console.error("[Note] Unknown session ID:", sessionId);
|
|
202
|
+
return res.status(404).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Please re-initialize." }, id: null });
|
|
203
|
+
}
|
|
192
204
|
console.error('[Note] Incoming session ID:', sessionId);
|
|
193
|
-
console.error("[Note] Using
|
|
194
|
-
|
|
205
|
+
console.error("[Note] Using transport for session ID:", sessionId);
|
|
206
|
+
|
|
195
207
|
// post the current session - used to pass _appContext to tools
|
|
196
208
|
cache.set("currentId", sessionId);
|
|
197
209
|
|
|
@@ -239,21 +251,28 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
239
251
|
const sessionId = req.headers["mcp-session-id"];
|
|
240
252
|
console.error("[Note] SessionId:", sessionId);
|
|
241
253
|
|
|
242
|
-
let transport = cache.get("sharedTransport");
|
|
243
|
-
console.error("[Note] Using shared transport");
|
|
244
|
-
/*
|
|
245
254
|
if (!sessionId) {
|
|
246
|
-
|
|
247
|
-
return;
|
|
255
|
+
console.error("[Note] No session ID on /DELETE — rejecting");
|
|
256
|
+
return res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: Mcp-Session-Id header is required" }, id: null });
|
|
248
257
|
}
|
|
249
|
-
|
|
258
|
+
|
|
259
|
+
const transport = transports.get(sessionId);
|
|
260
|
+
if (!transport) {
|
|
261
|
+
console.error("[Note] Unknown session ID:", sessionId);
|
|
262
|
+
return res.status(404).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Please re-initialize." }, id: null });
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
if (req.method === "GET") {
|
|
266
|
+
console.error("[Note] calling transport.handleRequest for GET /mcp");
|
|
251
267
|
await transport.handleRequest(req, res);
|
|
252
268
|
return;
|
|
253
269
|
}
|
|
254
|
-
if (req.method === "DELETE"
|
|
255
|
-
console.error("[Note] Deleting cache for session ID:", sessionId);
|
|
270
|
+
if (req.method === "DELETE") {
|
|
271
|
+
console.error("[Note] Deleting transport and cache for session ID:", sessionId);
|
|
272
|
+
transports.delete(sessionId);
|
|
256
273
|
cache.del(sessionId);
|
|
274
|
+
console.error("[Note] Deleted session ID:", sessionId);
|
|
275
|
+
console.error("[Note] Total active sessions:", Object.keys(transports));
|
|
257
276
|
res.status(201).send(`[Info] Deleted session ${sessionId}`);
|
|
258
277
|
}
|
|
259
278
|
}
|
package/src/processHeaders.js
CHANGED
|
@@ -30,7 +30,6 @@ function processHeaders(req, res, next, cache, appContext) {
|
|
|
30
30
|
const hdr = req.header("Authorization");
|
|
31
31
|
//for now, ignore Authorization if authflow is not bearer
|
|
32
32
|
let token = (hdr != null) ? hdr.slice(7) : null;
|
|
33
|
-
//console.error("[Note] Authorization token", token);
|
|
34
33
|
debugger;
|
|
35
34
|
console.error('[Note} AUTHFLOW=', appContext.AUTHFLOW);
|
|
36
35
|
console.error("[Note] External authorization :", appContext.AUTHEXTERNAL);
|
|
@@ -42,7 +41,6 @@ function processHeaders(req, res, next, cache, appContext) {
|
|
|
42
41
|
console.error("[Note] Expecting external authorization");
|
|
43
42
|
if (token != null) {
|
|
44
43
|
console.error("[Note] Using user supplied token for authorization");
|
|
45
|
-
console.error("[Note] Incoming token:", token);
|
|
46
44
|
headerCache.bearerToken = token;
|
|
47
45
|
} else {
|
|
48
46
|
console.error("[Note] No Authorization token provided in header for external authorization.");
|
|
@@ -47,7 +47,6 @@ async function igetLogonPayload(_appContext) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
console.error("[Note] Using user supplied bearer token ");
|
|
50
|
-
console.error("[Note] Ensure this token is valid and not expired: ", _appContext.bearerToken.substring(0,20) + "...");
|
|
51
50
|
let logonPayload = {
|
|
52
51
|
host: _appContext.VIYA_SERVER,
|
|
53
52
|
authType: "server",
|
|
@@ -72,8 +71,7 @@ async function igetLogonPayload(_appContext) {
|
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
if (_appContext.AUTHFLOW === "token") {
|
|
75
|
-
console.error("[Note] Using token supplied by user");
|
|
76
|
-
console.error("[Note] Ensure this token is valid and not expired: ", _appContext.TOKEN.substring(0,20) + "...");
|
|
74
|
+
console.error("[Note] Using token supplied by user");
|
|
77
75
|
let logonPayload = {
|
|
78
76
|
host: _appContext.VIYA_SERVER,
|
|
79
77
|
authType: "server",
|