@sassoftware/sas-score-mcp-serverjs 0.4.1-5 → 0.4.1-7

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.
@@ -11,8 +11,9 @@ import selfsigned from "selfsigned";
11
11
  import openAPIJson from "./openAPIJson.js";
12
12
 
13
13
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
- import { randomUUID } from "node:crypto";
14
+ import { randomUUID, randomBytes, createHash } from "node:crypto";
15
15
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16
+ import { Agent, fetch as undiciFetch } from "undici";
16
17
  import tlogon from "./toolHelpers/tlogon.js";
17
18
 
18
19
 
@@ -44,6 +45,153 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
44
45
  // app.use(helmet());
45
46
  app.use(bodyParser.json({ limit: process.env.JSON_LIMIT ?? "50mb" }));
46
47
 
48
+ // In-memory stores for the OAuth PKCE proxy flow (cleared on server restart)
49
+ const pkceStore = new Map(); // ourState -> { codeVerifier, clientRedirectUri, clientState }
50
+ const codeStore = new Map(); // ourCode -> { access_token, refresh_token, expires_in }
51
+
52
+ // Helper: build this server's base URL from appEnvBase
53
+ function serverBaseUrl() {
54
+ const appEnvBase = cache.get("appEnvBase");
55
+ const protocol = appEnvBase.HTTPS === "TRUE" ? "https" : "http";
56
+ return `${protocol}://localhost:${appEnvBase.PORT}`;
57
+ }
58
+
59
+ app.get("/.well-known/oauth-authorization-server", (req, res) => {
60
+ const base = serverBaseUrl();
61
+ let metadata = {
62
+ "issuer": base,
63
+ "authorization_endpoint": `${base}/oauth/authorize`,
64
+ "token_endpoint": `${base}/oauth/token`,
65
+ "response_types_supported": ["code"],
66
+ "grant_types_supported": ["authorization_code", "refresh_token"],
67
+ "code_challenge_methods_supported": ["S256"]
68
+ };
69
+ return res.status(200).json(metadata);
70
+ });
71
+
72
+ // OAuth authorize — generates PKCE params, stores state, redirects to SAS Viya
73
+ app.get("/oauth/authorize", (req, res) => {
74
+ const { response_type, redirect_uri, state, scope } = req.query;
75
+
76
+ if (response_type !== "code") {
77
+ return res.status(400).json({ error: "unsupported_response_type" });
78
+ }
79
+ if (!redirect_uri) {
80
+ return res.status(400).json({ error: "invalid_request", error_description: "redirect_uri is required" });
81
+ }
82
+
83
+ const codeVerifier = randomBytes(64).toString("base64url");
84
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
85
+ const ourState = randomBytes(16).toString("hex");
86
+
87
+ pkceStore.set(ourState, { codeVerifier, clientRedirectUri: redirect_uri, clientState: state });
88
+
89
+ const callbackUri = `${serverBaseUrl()}/callback`;
90
+ const params = new URLSearchParams({
91
+ response_type: "code",
92
+ client_id: baseAppEnvContext.CLIENTID,
93
+ redirect_uri: callbackUri,
94
+ scope: scope || "openid",
95
+ state: ourState,
96
+ code_challenge: codeChallenge,
97
+ code_challenge_method: "S256",
98
+ });
99
+
100
+ console.error("[Note] OAuth authorize: redirecting to SAS Viya");
101
+ return res.redirect(`${baseAppEnvContext.VIYA_SERVER}/SASLogon/oauth/authorize?${params}`);
102
+ });
103
+
104
+ // OAuth callback — receives code from SAS Viya, exchanges for tokens, redirects to MCP client
105
+ app.get("/callback", async (req, res) => {
106
+ const { code, state, error, error_description } = req.query;
107
+
108
+ if (error) {
109
+ console.error("[Error] OAuth callback error:", error, error_description);
110
+ return res.status(400).send(`Authorization failed: ${error} — ${error_description ?? ""}`);
111
+ }
112
+
113
+ const pending = pkceStore.get(state);
114
+ if (!pending) {
115
+ return res.status(400).send("Invalid or expired state parameter");
116
+ }
117
+ pkceStore.delete(state);
118
+
119
+ const callbackUri = `${serverBaseUrl()}/callback`;
120
+
121
+ try {
122
+ const body = new URLSearchParams({
123
+ grant_type: "authorization_code",
124
+ client_id: baseAppEnvContext.CLIENTID,
125
+ redirect_uri: callbackUri,
126
+ code,
127
+ code_verifier: pending.codeVerifier,
128
+ });
129
+
130
+ const connectOpts = baseAppEnvContext.contexts?.appCert
131
+ ? baseAppEnvContext.contexts.appCert
132
+ : { rejectUnauthorized: false };
133
+ const agent = new Agent({ connect: connectOpts });
134
+
135
+ const response = await undiciFetch(`${baseAppEnvContext.VIYA_SERVER}/SASLogon/oauth/token`, {
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/x-www-form-urlencoded",
139
+ "Accept": "application/json",
140
+ },
141
+ body: body.toString(),
142
+ dispatcher: agent,
143
+ });
144
+
145
+ if (!response.ok) {
146
+ const errText = await response.text();
147
+ console.error("[Error] SAS Viya token exchange failed:", errText);
148
+ return res.status(502).send("Token exchange with SAS Viya failed");
149
+ }
150
+
151
+ const tokens = await response.json();
152
+ const ourCode = randomUUID();
153
+ codeStore.set(ourCode, {
154
+ access_token: tokens.access_token,
155
+ refresh_token: tokens.refresh_token,
156
+ expires_in: tokens.expires_in,
157
+ });
158
+
159
+ const redirectParams = new URLSearchParams({ code: ourCode });
160
+ if (pending.clientState) {
161
+ redirectParams.set("state", pending.clientState);
162
+ }
163
+
164
+ console.error("[Note] OAuth callback complete, redirecting to MCP client");
165
+ return res.redirect(`${pending.clientRedirectUri}?${redirectParams}`);
166
+ } catch (err) {
167
+ console.error("[Error] OAuth callback handler error:", err);
168
+ return res.status(500).send("Internal server error during token exchange");
169
+ }
170
+ });
171
+
172
+ // OAuth token endpoint — MCP client exchanges intermediate code for access token
173
+ app.post("/oauth/token", express.urlencoded({ extended: false }), (req, res) => {
174
+ const { grant_type, code } = req.body;
175
+
176
+ if (grant_type !== "authorization_code") {
177
+ return res.status(400).json({ error: "unsupported_grant_type" });
178
+ }
179
+
180
+ const tokenData = codeStore.get(code);
181
+ if (!tokenData) {
182
+ return res.status(400).json({ error: "invalid_grant", error_description: "Invalid or expired authorization code" });
183
+ }
184
+ codeStore.delete(code);
185
+
186
+ console.error("[Note] OAuth token issued via code exchange");
187
+ return res.json({
188
+ access_token: tokenData.access_token,
189
+ token_type: "Bearer",
190
+ expires_in: tokenData.expires_in,
191
+ ...(tokenData.refresh_token && { refresh_token: tokenData.refresh_token }),
192
+ });
193
+ });
194
+
47
195
  // setup routes
48
196
  app.get("/health", (req, res) => {
49
197
  console.error("Received request for health endpoint");
@@ -64,366 +212,366 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
64
212
  };
65
213
  res.json(health);
66
214
  });
67
-
68
- // Root endpoint info
69
-
70
- app.get("/", (req, res) => {
71
- res.json({
72
- name: "SAS Viya Sample MCP Server",
73
- version: baseAppEnvContext.version,
74
- description: "SAS Viya Sample MCP Server",
75
- endpoints: {
76
- mcp: "/mcp",
77
- health: "/health",
78
- apiMeta: "/apiMeta"
79
- },
80
- usage: "Use with MCP Inspector or compatible MCP clients",
215
+
216
+ // Root endpoint info
217
+
218
+ app.get("/", (req, res) => {
219
+ res.json({
220
+ name: "SAS Viya Sample MCP Server",
221
+ version: baseAppEnvContext.version,
222
+ description: "SAS Viya Sample MCP Server",
223
+ endpoints: {
224
+ mcp: "/mcp",
225
+ health: "/health",
226
+ apiMeta: "/apiMeta"
227
+ },
228
+ usage: "Use with MCP Inspector or compatible MCP clients",
229
+ });
81
230
  });
82
- });
83
-
84
- // api metadata endpoint(for sas specs)
85
- app.get("/apiMeta", (req, res) => {
86
- let spec = openAPIJson(baseAppEnvContext.version);
87
- res.json(spec);
88
- });
89
-
90
- // for azure container apps
91
- app.get("/openapi.json", (req, res) => {
92
- let spec = openAPIJson(baseAppEnvContext.version);
93
- res.json(spec);
94
- });
95
-
96
- // handle processing of information in header.
97
- function requireBearer(req, res, next) {
98
- // process any new header information
99
- console.error("=======================================================");
100
- console.error("Processing headers for incoming request to /mcp endpoint");
101
- // Allow different VIYA server per sessionid(user)
102
- let headerCache = {};
103
- if (req.header("X-VIYA-SERVER") != null) {
104
- console.error("[Note] Using user supplied VIYA server");
105
- headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
106
- }
107
231
 
108
- // used when doing autorization via mcp client
109
- // ideal for production use
110
- const hdr = req.header("Authorization");
111
- if (hdr != null) {
112
- headerCache.bearerToken = hdr.slice(7);
113
- headerCache.AUTHFLOW = "bearer";
114
- console.error("[Note] Using user supplied bearer token for authorization");
115
- console.error("[Debug] Bearer token starts with:", headerCache.bearerToken);
116
- } else {
117
- console.error("[Note] No bearer token supplied in Authorization header");
118
- headerCache.bearerToken = null;
119
- }
232
+ // api metadata endpoint(for sas specs)
233
+ app.get("/apiMeta", (req, res) => {
234
+ let spec = openAPIJson(baseAppEnvContext.version);
235
+ res.json(spec);
236
+ });
120
237
 
121
- // faking out api key since Viya does not support
122
- // not ideal for production
123
- const hdr2 = req.header("X-REFRESH-TOKEN");
124
- if (hdr2 != null) {
125
- headerCache.REFRESH_TOKEN = hdr2;
126
- headerCache.AUTHFLOW = "refresh";
127
- console.error("[Note] Using user supplied refresh token for authorization");
128
- }
129
- cache.set("headerCache", headerCache);
130
- next();
131
- console.error("Finished processing headers for /mcp request");
132
- console.error("=======================================================");
133
- }
238
+ // for azure container apps
239
+ app.get("/openapi.json", (req, res) => {
240
+ let spec = openAPIJson(baseAppEnvContext.version);
241
+ res.json(spec);
242
+ });
134
243
 
135
- // process mcp endpoint requests
136
- const handleRequest = async (req, res) => {
137
- let transport = null;
138
- let transports = cache.get("transports");
139
- console.error("=========================================================");
140
- console.error("Processing POST /mcp request");
141
- if (transports == null) {
142
- console.error("[Error] ***** transports cache is null. This is an error");
143
- transports = {};
144
- cache.set("transports", transports);
244
+ // handle processing of information in header.
245
+ function requireBearer(req, res, next) {
246
+ // process any new header information
247
+ console.error("=======================================================");
248
+ console.error("Processing headers for incoming request to /mcp endpoint");
249
+ // Allow different VIYA server per sessionid(user)
250
+ let headerCache = {};
251
+ if (req.header("X-VIYA-SERVER") != null) {
252
+ console.error("[Note] Using user supplied VIYA server");
253
+ headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
254
+ }
255
+
256
+ // used when doing autorization via mcp client
257
+ // ideal for production use
258
+ const hdr = req.header("Authorization");
259
+ if (hdr != null) {
260
+ headerCache.bearerToken = hdr.slice(7);
261
+ headerCache.AUTHFLOW = "bearer";
262
+ console.error("[Note] Using user supplied bearer token for authorization");
263
+ console.error("[Debug] Bearer token starts with:", headerCache.bearerToken);
264
+ } else {
265
+ console.error("[Note] No bearer token supplied in Authorization header");
266
+ headerCache.bearerToken = null;
267
+ }
268
+
269
+ // faking out api key since Viya does not support
270
+ // not ideal for production
271
+ const hdr2 = req.header("X-REFRESH-TOKEN");
272
+ if (hdr2 != null) {
273
+ headerCache.REFRESH_TOKEN = hdr2;
274
+ headerCache.AUTHFLOW = "refresh";
275
+ console.error("[Note] Using user supplied refresh token for authorization");
276
+ }
277
+ cache.set("headerCache", headerCache);
278
+ next();
279
+ console.error("Finished processing headers for /mcp request");
280
+ console.error("=======================================================");
145
281
  }
146
282
 
147
- console.error("current transports in cache:", Object.keys(transports));
148
- try {
149
-
150
- let sessionId = req.headers["mcp-session-id"];
151
- console.error("[Note]Incoming session ID:", sessionId);
152
- let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
153
- console.error('[Note] Payload is ', body);
154
- if (/*!sessionId &&*/ isInitializeRequest(req.body)) {
155
- // create transport
156
- console.error("[Note] Initializing new transport for MCP session...");
157
-
158
- transport = new StreamableHTTPServerTransport({
159
- sessionIdGenerator: () => randomUUID(),
160
- enableJsonResponse: true,
161
- enableDnsRebindingProtection: true,
162
- onsessioninitialized: (sessionId) => {
163
- // Store the transport by session ID
164
- console.error('Session initialized');
165
- console.error("[Note] Transport initialized with ID:", sessionId);
166
- transports[sessionId] = transport;
167
- },
168
- });
169
- // Clean up transport when closed
170
- transport.onclose = () => {
171
- if (transport.sessionId && transports[transport.sessionId]) {
172
- delete transports[transport.sessionId];
283
+ // process mcp endpoint requests
284
+ const handleRequest = async (req, res) => {
285
+ let transport = null;
286
+ let transports = cache.get("transports");
287
+ console.error("=========================================================");
288
+ console.error("Processing POST /mcp request");
289
+ if (transports == null) {
290
+ console.error("[Error] ***** transports cache is null. This is an error");
291
+ transports = {};
292
+ cache.set("transports", transports);
293
+ }
294
+
295
+ console.error("current transports in cache:", Object.keys(transports));
296
+ try {
297
+
298
+ let sessionId = req.headers["mcp-session-id"];
299
+ console.error("[Note]Incoming session ID:", sessionId);
300
+ let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
301
+ console.error('[Note] Payload is ', body);
302
+ if (/*!sessionId &&*/ isInitializeRequest(req.body)) {
303
+ // create transport
304
+ console.error("[Note] Initializing new transport for MCP session...");
305
+
306
+ transport = new StreamableHTTPServerTransport({
307
+ sessionIdGenerator: () => randomUUID(),
308
+ enableJsonResponse: true,
309
+ enableDnsRebindingProtection: true,
310
+ onsessioninitialized: (sessionId) => {
311
+ // Store the transport by session ID
312
+ console.error('Session initialized');
313
+ console.error("[Note] Transport initialized with ID:", sessionId);
314
+ transports[sessionId] = transport;
315
+ },
316
+ });
317
+ // Clean up transport when closed
318
+ transport.onclose = () => {
319
+ if (transport.sessionId && transports[transport.sessionId]) {
320
+ delete transports[transport.sessionId];
321
+ }
322
+ };
323
+ console.error("[Note] Connecting mcpServer to new transport...");
324
+ await mcpServer.connect(transport);
325
+
326
+ // Save transport data and app context for use in tools
327
+ console.error('[Note] Connected to mcpServer');
328
+ cache.set("transports", transports);
329
+ console.error("=======================================================");
330
+ return await transport.handleRequest(req, res, req.body);
331
+
332
+ // cache transport
333
+
334
+ } else if (sessionId != null) {
335
+ console.error('[Note] Incoming session ID:', sessionId);
336
+ transport = transports[sessionId];
337
+ console.error("[Note] Found transport:", transport != null);
338
+ if (transport == null) {
339
+ // this can happen if client is holding on to old session id
340
+ console.error("[Error] No transport found for session ID:", sessionId, "Returning a 404 error with instructions for the user");
341
+ res.status(404).send(`Invalid or missing session ID ${sessionId}. Please ensure your MCP client is configured to use the correct session ID returned in the 'mcp-session-id' header of the response from the /mcp endpoint.`);
342
+ return;
173
343
  }
174
- };
175
- console.error("[Note] Connecting mcpServer to new transport...");
176
- await mcpServer.connect(transport);
177
344
 
178
- // Save transport data and app context for use in tools
179
- console.error('[Note] Connected to mcpServer');
180
- cache.set("transports", transports);
181
- console.error("=======================================================");
182
- return await transport.handleRequest(req, res, req.body);
183
-
184
- // cache transport
185
-
186
- } else if (sessionId != null) {
187
- console.error('[Note] Incoming session ID:', sessionId);
188
- transport = transports[sessionId];
189
- console.error("[Note] Found transport:", transport != null);
190
- if (transport == null) {
191
- // this can happen if client is holding on to old session id
192
- console.error("[Error] No transport found for session ID:", sessionId, "Returning a 404 error with instructions for the user");
193
- res.status(404).send(`Invalid or missing session ID ${sessionId}. Please ensure your MCP client is configured to use the correct session ID returned in the 'mcp-session-id' header of the response from the /mcp endpoint.`);
345
+ // post the curren session - used to pass _appContext to tools
346
+ cache.set("currentId", sessionId);
347
+
348
+ // get app context for session
349
+ let _appContext = cache.get(sessionId);
350
+
351
+ //if first prompt on a sessionid, create app context
352
+ if (_appContext == null) {
353
+
354
+ let appEnvTemplate = cache.get("appEnvTemplate");
355
+ let headerCache = cache.get("headerCache");
356
+ _appContext = Object.assign({}, appEnvTemplate, headerCache);
357
+ cache.set(sessionId, _appContext);
358
+ } else {
359
+ let headerCache = cache.get("headerCache");
360
+ console.error('compare tokens', headerCache.bearerToken === _appContext.bearerToken);
361
+ _appContext = Object.assign(_appContext, headerCache);
362
+ console.error('New bearerToken:', _appContext.bearerToken);
363
+ cache.set(sessionId, _appContext);
364
+ }
365
+ console.error("[Note] Using existing transport for session ID:", sessionId);
366
+ console.error("==========================================================");
367
+ await transport.handleRequest(req, res, req.body);
194
368
  return;
195
369
  }
196
370
 
197
- // post the curren session - used to pass _appContext to tools
198
- cache.set("currentId", sessionId);
199
-
200
- // get app context for session
201
- let _appContext = cache.get(sessionId);
202
-
203
- //if first prompt on a sessionid, create app context
204
- if (_appContext == null) {
205
-
206
- let appEnvTemplate = cache.get("appEnvTemplate");
207
- let headerCache = cache.get("headerCache");
208
- _appContext = Object.assign({}, appEnvTemplate, headerCache);
209
- cache.set(sessionId, _appContext);
210
- } else {
211
- let headerCache = cache.get("headerCache");
212
- console.error('compare tokens', headerCache.bearerToken === _appContext.bearerToken);
213
- _appContext = Object.assign(_appContext, headerCache);
214
- console.error('New bearerToken:', _appContext.bearerToken);
215
- cache.set(sessionId, _appContext);
371
+ // initialize request
372
+
373
+ }
374
+ catch (error) {
375
+ console.error("Error handling MCP request:", error);
376
+ if (!res.headersSent) {
377
+ res.status(500).json({
378
+ jsonrpc: "2.0",
379
+ error: {
380
+ code: -32603,
381
+ message: JSON.stringify(error),
382
+ },
383
+ id: null,
384
+ });
216
385
  }
217
- console.error("[Note] Using existing transport for session ID:", sessionId);
218
- console.error("==========================================================");
219
- await transport.handleRequest(req, res, req.body);
220
386
  return;
221
387
  }
222
-
223
- // initialize request
224
-
225
- }
226
- catch (error) {
227
- console.error("Error handling MCP request:", error);
228
- if (!res.headersSent) {
229
- res.status(500).json({
230
- jsonrpc: "2.0",
231
- error: {
232
- code: -32603,
233
- message: JSON.stringify(error),
234
- },
235
- id: null,
236
- });
388
+ };
389
+ const handleGetDelete = async (req, res) => {
390
+ console.error("=========================================================");
391
+ console.error(`[Note] ${req.method} /mcp called`);
392
+ const sessionId = req.headers["mcp-session-id"];
393
+ console.error("[Note] SessionId:", sessionId);
394
+
395
+ let transports = cache.get("transports");
396
+ let transport = (sessionId == null) ? null : transports[sessionId];
397
+ console.error("[Note] Transport found:", transport != null);
398
+ if (!sessionId || transport == null) {
399
+ res.status(404).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
400
+ return;
401
+ }
402
+ if (req.method === "GET") {
403
+ await transport.handleRequest(req, res);
404
+ return;
405
+ }
406
+ if (req.method === "DELETE" && sessionId != null) {
407
+ console.error("[Note] Deleting transport and cache for session ID:", sessionId);
408
+ delete transports[sessionId];
409
+ cache.del(sessionId);
410
+ res.status(201).send(`[Info] Deleted session ${sessionId}`);
237
411
  }
238
- return;
239
- }
240
- };
241
- const handleGetDelete = async (req, res) => {
242
- console.error("=========================================================");
243
- console.error(`[Note] ${req.method} /mcp called`);
244
- const sessionId = req.headers["mcp-session-id"];
245
- console.error("[Note] SessionId:", sessionId);
246
-
247
- let transports = cache.get("transports");
248
- let transport = (sessionId == null) ? null : transports[sessionId];
249
- console.error("[Note] Transport found:", transport != null);
250
- if (!sessionId || transport == null) {
251
- res.status(404).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
252
- return;
253
- }
254
- if (req.method === "GET") {
255
- await transport.handleRequest(req, res);
256
- return;
257
- }
258
- if (req.method === "DELETE" && sessionId != null) {
259
- console.error("[Note] Deleting transport and cache for session ID:", sessionId);
260
- delete transports[sessionId];
261
- cache.del(sessionId);
262
- res.status(201).send(`[Info] Deleted session ${sessionId}`);
263
412
  }
264
- }
265
413
 
266
- app.options("/mcp", (_, res) => res.sendStatus(204));
267
- app.post("/mcp", requireBearer, handleRequest);
268
- app.get("/mcp", handleGetDelete);
269
- app.delete("/mcp", handleGetDelete);
270
- app.get("/StartUp", (_req, res) => {
271
- console.error("===================================================================")
272
- console.error("Received request for Startup endpoint. Current app status:", appStatus);
273
- console.error("===================================================================");
274
- if (appStatus === false) {
275
- return res.status(503).json({ status: "starting" });
276
- }
277
- return res.status(200).json({ status: "started" });
278
- });
279
- app.get("/tlogon", async (_req, res) => {
280
- console.error(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Testing logon");
281
- if (appStatus === false) {
282
- return res.status(503).json({ status: "not ready" });
283
- }
284
- let r = await tlogon(baseAppEnvContext);
285
- console.error(r);
286
- return res.status(200).json(r);
287
- });
288
- app.get("/status", (_req, res) => {
289
- console.error("===================================================================")
290
- console.error("Received request for status endpoint. Current app status:", appStatus);
291
- console.error("===================================================================");
292
- if (appStatus === false) {
293
- return res.status(503).json({ status: "not ready" });
294
- }
295
- return res.status(200).json({ status: "ready" });
296
- });
297
-
298
- app.get("/ready", (_req, res) => {
299
- console.error("===================================================================")
300
- console.error("Received request for ready endpoint. Current app status:", appStatus);
301
- console.error("===================================================================")
302
- if (appStatus === false) {
303
- return res.status(503).json({ status: "not ready" });
304
- }
305
- return res.status(200).json({ status: "ready" });
306
- });
307
- // Start the server
308
- let appEnvBase = cache.get("appEnvBase");
309
-
310
- const PORT = appEnvBase.PORT;
311
-
312
- // get user specified TLS options
313
- let appServer;
314
-
315
- // get TLS options
316
- if (appEnvBase.HTTPS === 'TRUE') {
317
- if (appEnvBase.tlsOpts == null) {
318
- appEnvBase.tlsOpts = await getTls(appEnvBase);
319
- console.error(Object.keys(appEnvBase.tlsOpts));
320
- appEnvBase.tlsOpts.requestCert = false;
321
- appEnvBase.tlsOpts.rejectUnauthorized = false;
322
- appEnvBase.contexts.appCert = appEnvBase.tlsOpts; /* just for completeness */
323
- }
414
+ app.options("/mcp", (_, res) => res.sendStatus(204));
415
+ app.post("/mcp", requireBearer, handleRequest);
416
+ app.get("/mcp", handleGetDelete);
417
+ app.delete("/mcp", handleGetDelete);
418
+ app.get("/StartUp", (_req, res) => {
419
+ console.error("===================================================================")
420
+ console.error("Received request for Startup endpoint. Current app status:", appStatus);
421
+ console.error("===================================================================");
422
+ if (appStatus === false) {
423
+ return res.status(503).json({ status: "starting" });
424
+ }
425
+ return res.status(200).json({ status: "started" });
426
+ });
427
+ app.get("/tlogon", async (_req, res) => {
428
+ console.error(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Testing logon");
429
+ if (appStatus === false) {
430
+ return res.status(503).json({ status: "not ready" });
431
+ }
432
+ let r = await tlogon(baseAppEnvContext);
433
+ console.error(r);
434
+ return res.status(200).json(r);
435
+ });
436
+ app.get("/status", (_req, res) => {
437
+ console.error("===================================================================")
438
+ console.error("Received request for status endpoint. Current app status:", appStatus);
439
+ console.error("===================================================================");
440
+ if (appStatus === false) {
441
+ return res.status(503).json({ status: "not ready" });
442
+ }
443
+ return res.status(200).json({ status: "ready" });
444
+ });
324
445
 
325
- cache.set("appEnvBase", appEnvBase);
446
+ app.get("/ready", (_req, res) => {
447
+ console.error("===================================================================")
448
+ console.error("Received request for ready endpoint. Current app status:", appStatus);
449
+ console.error("===================================================================")
450
+ if (appStatus === false) {
451
+ return res.status(503).json({ status: "not ready" });
452
+ }
453
+ return res.status(200).json({ status: "ready" });
454
+ });
455
+ // Start the server
456
+ let appEnvBase = cache.get("appEnvBase");
457
+
458
+ const PORT = appEnvBase.PORT;
459
+
460
+ // get user specified TLS options
461
+ let appServer;
462
+
463
+ // get TLS options
464
+ if (appEnvBase.HTTPS === 'TRUE') {
465
+ if (appEnvBase.tlsOpts == null) {
466
+ appEnvBase.tlsOpts = await getTls(appEnvBase);
467
+ console.error(Object.keys(appEnvBase.tlsOpts));
468
+ appEnvBase.tlsOpts.requestCert = false;
469
+ appEnvBase.tlsOpts.rejectUnauthorized = false;
470
+ appEnvBase.contexts.appCert = appEnvBase.tlsOpts; /* just for completeness */
471
+ }
326
472
 
327
- console.error(`[Note] MCP Server listening on port ${PORT}`);
328
- console.error(
329
- "[Note] Visit https://localhost:8080/health for health check"
330
- );
331
- console.error(
332
- "[Note] Configure your mcp host to use https://localhost:8080/mcp to interact with the MCP server"
333
- );
334
- console.error("[Note] Press Ctrl+C to stop the server");
473
+ cache.set("appEnvBase", appEnvBase);
335
474
 
336
- appServer = https.createServer(appEnvBase.tlsOpts, app);
337
- appServer.listen(PORT, "0.0.0.0", () => {
338
- console.error( `[Note] Express server successfully bound to 0.0.0.0:${PORT}` );
339
- appStatus= true;
340
- });
341
- } else {
342
- console.error(`[Note] MCP Server listening on port ${PORT}`);
343
- console.error("[Note] Visit http://localhost:8080/health for health check");
344
- console.error(
345
- "[Note] Configure your mcp host to use http://localhost:8080/mcp to interact with the MCP server"
346
- );
347
- console.error("[Note] Press Ctrl+C to stop the server");
348
- try {
349
- appServer = app.listen(PORT, "0.0.0.0", () => {
350
- appStatus = true;
351
- console.error(
352
- `[Note] Express server successfully bound to 0.0.0.0:${PORT}`
353
- );
475
+ console.error(`[Note] MCP Server listening on port ${PORT}`);
476
+ console.error(
477
+ "[Note] Visit https://localhost:8080/health for health check"
478
+ );
479
+ console.error(
480
+ "[Note] Configure your mcp host to use https://localhost:8080/mcp to interact with the MCP server"
481
+ );
482
+ console.error("[Note] Press Ctrl+C to stop the server");
354
483
 
484
+ appServer = https.createServer(appEnvBase.tlsOpts, app);
485
+ appServer.listen(PORT, "0.0.0.0", () => {
486
+ console.error(`[Note] Express server successfully bound to 0.0.0.0:${PORT}`);
487
+ appStatus = true;
355
488
  });
356
- } catch (error) {
357
- console.error("Error starting server:", error);
358
- }
359
- }
360
- process.on("SIGTERM", () => {
361
- console.error("Server closed");
362
- if (appServer != null) {
363
- appServer.close(() => { });
364
- }
365
- process.exit(0);
366
- });
367
- process.on("SIGINT", () => {
368
- console.error("Server closed");
369
- if (appServer != null) {
370
- appServer.close(() => { });
489
+ } else {
490
+ console.error(`[Note] MCP Server listening on port ${PORT}`);
491
+ console.error("[Note] Visit http://localhost:8080/health for health check");
492
+ console.error(
493
+ "[Note] Configure your mcp host to use http://localhost:8080/mcp to interact with the MCP server"
494
+ );
495
+ console.error("[Note] Press Ctrl+C to stop the server");
496
+ try {
497
+ appServer = app.listen(PORT, "0.0.0.0", () => {
498
+ appStatus = true;
499
+ console.error(
500
+ `[Note] Express server successfully bound to 0.0.0.0:${PORT}`
501
+ );
502
+
503
+ });
504
+ } catch (error) {
505
+ console.error("Error starting server:", error);
506
+ }
371
507
  }
372
- process.exit(0);
373
- });
374
-
375
- // create unsigned TLS cert
376
- async function getTls(appEnv) {
377
- let tlscreate =
378
- appEnv.TLS_CREATE == null
379
- ? "TLS_CREATE=C:US,ST:NC,L:Cary,O:SAS Institute,OU:STO,CN:localhost,ALT:na.sas.com"
380
- : appEnv.TLS_CREATE;
381
- let subjt = tlscreate.replaceAll('"', "").trim();
382
- let subj = subjt.split(",");
383
-
384
- let d = {};
385
- subj.map((c) => {
386
- let r = c.split(":");
387
- d[r[0]] = r[1];
388
- return { value: r[1] };
508
+ process.on("SIGTERM", () => {
509
+ console.error("Server closed");
510
+ if (appServer != null) {
511
+ appServer.close(() => { });
512
+ }
513
+ process.exit(0);
514
+ });
515
+ process.on("SIGINT", () => {
516
+ console.error("Server closed");
517
+ if (appServer != null) {
518
+ appServer.close(() => { });
519
+ }
520
+ process.exit(0);
389
521
  });
390
522
 
391
- let attr = [
392
- {
393
- name: "commonName",
394
- value: d.CN,
395
- },
396
- {
397
- name: "countryName",
398
- value: d.C,
399
- },
400
- {
401
- shortName: "ST",
402
- value: d.ST,
403
- },
404
- {
405
- name: "localityName",
406
- value: d.L,
407
- },
408
- {
409
- name: "organizationName",
410
- value: d.O,
411
- },
412
- {
413
- shortName: "OU",
414
- value: d.OU,
415
- },
416
- ];
417
-
418
- let pems = selfsigned.generate(attr);
419
- // selfsigned generates a new keypair
420
- let tls = {
421
- cert: pems.cert,
422
- key: pems.private,
423
- };
424
- console.error("Generated self-signed TLS certificate");
425
- return tls;
426
- }
523
+ // create unsigned TLS cert
524
+ async function getTls(appEnv) {
525
+ let tlscreate =
526
+ appEnv.TLS_CREATE == null
527
+ ? "TLS_CREATE=C:US,ST:NC,L:Cary,O:SAS Institute,OU:STO,CN:localhost,ALT:na.sas.com"
528
+ : appEnv.TLS_CREATE;
529
+ let subjt = tlscreate.replaceAll('"', "").trim();
530
+ let subj = subjt.split(",");
531
+
532
+ let d = {};
533
+ subj.map((c) => {
534
+ let r = c.split(":");
535
+ d[r[0]] = r[1];
536
+ return { value: r[1] };
537
+ });
538
+
539
+ let attr = [
540
+ {
541
+ name: "commonName",
542
+ value: d.CN,
543
+ },
544
+ {
545
+ name: "countryName",
546
+ value: d.C,
547
+ },
548
+ {
549
+ shortName: "ST",
550
+ value: d.ST,
551
+ },
552
+ {
553
+ name: "localityName",
554
+ value: d.L,
555
+ },
556
+ {
557
+ name: "organizationName",
558
+ value: d.O,
559
+ },
560
+ {
561
+ shortName: "OU",
562
+ value: d.OU,
563
+ },
564
+ ];
565
+
566
+ let pems = selfsigned.generate(attr);
567
+ // selfsigned generates a new keypair
568
+ let tls = {
569
+ cert: pems.cert,
570
+ key: pems.private,
571
+ };
572
+ console.error("Generated self-signed TLS certificate");
573
+ return tls;
574
+ }
427
575
  }
428
576
 
429
577
  export default expressMcpServer;