@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassoftware/sas-score-mcp-serverjs",
3
- "version": "1.0.1-29",
3
+ "version": "1.0.1-30",
4
4
  "description": "A mcp server for SAS Viya",
5
5
  "author": "Deva Kumar <deva.kumar@sas.com>",
6
6
  "license": "Apache-2.0",
@@ -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
@@ -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(mcpServer, cache, baseAppEnvContext) {
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
- // Create ONE shared transport for all sessions/users
54
- const sharedTransport = new StreamableHTTPServerTransport({
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
- if (!sessionId && isInitializeRequest(req.body)) {
186
- // Use the shared transport for new initialization request
187
- console.error("[Note] Initializing new session with shared transport...");
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 shared transport for session ID:", sessionId);
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
- res.status(404).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
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" && sessionId != null) {
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
  }
@@ -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",