@sassoftware/sas-score-mcp-serverjs 0.4.0 → 0.4.1-15

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.
Files changed (53) hide show
  1. package/cli.js +112 -33
  2. package/package.json +5 -5
  3. package/skills/sas-list-tables-smart/SKILL.md +123 -0
  4. package/skills/sas-read-and-score/SKILL.md +54 -53
  5. package/skills/sas-read-strategy/SKILL.md +10 -10
  6. package/skills/sas-score-workflow/SKILL.md +19 -1
  7. package/skills/sas-spec-migration/SKILL.md +303 -0
  8. package/src/authpkce.js +219 -0
  9. package/src/createMcpServer.js +16 -6
  10. package/src/expressMcpServer.js +354 -338
  11. package/src/handleGetDelete.js +1 -1
  12. package/src/oauthHandlers/authorize.js +46 -0
  13. package/src/oauthHandlers/baseUrl.js +8 -0
  14. package/src/oauthHandlers/callback.js +93 -0
  15. package/src/oauthHandlers/getMetadata.js +27 -0
  16. package/src/oauthHandlers/index.js +7 -0
  17. package/src/oauthHandlers/token.js +37 -0
  18. package/src/processHeaders.js +88 -0
  19. package/src/toolHelpers/_listLibrary.js +0 -1
  20. package/src/toolHelpers/getLogonPayload.js +5 -1
  21. package/src/toolHelpers/readCerts.js +4 -4
  22. package/src/toolHelpers/refreshTokenOauth.js +3 -3
  23. package/src/toolSet/.claude/settings.local.json +13 -0
  24. package/src/toolSet/devaScore.js +61 -61
  25. package/src/toolSet/findJob.js +9 -16
  26. package/src/toolSet/findJobdef.js +4 -5
  27. package/src/toolSet/findLibrary.js +68 -68
  28. package/src/toolSet/findModel.js +4 -5
  29. package/src/toolSet/findTable.js +6 -6
  30. package/src/toolSet/getEnv.js +10 -7
  31. package/src/toolSet/listJobdefs.js +61 -62
  32. package/src/toolSet/listJobs.js +61 -62
  33. package/src/toolSet/listLibraries.js +78 -80
  34. package/src/toolSet/listModels.js +56 -56
  35. package/src/toolSet/listTables.js +66 -66
  36. package/src/toolSet/makeTools.js +1 -3
  37. package/src/toolSet/modelInfo.js +4 -5
  38. package/src/toolSet/modelScore.js +7 -7
  39. package/src/toolSet/readTable.js +63 -67
  40. package/src/toolSet/runCasProgram.js +9 -9
  41. package/src/toolSet/runJob.js +81 -82
  42. package/src/toolSet/runJobdef.js +82 -83
  43. package/src/toolSet/runMacro.js +82 -82
  44. package/src/toolSet/runProgram.js +8 -13
  45. package/src/toolSet/sasQuery.js +77 -79
  46. package/src/toolSet/sasQueryTemplate.js +4 -5
  47. package/src/toolSet/sasQueryTemplate2.js +4 -5
  48. package/src/toolSet/scrInfo.js +3 -5
  49. package/src/toolSet/scrScore.js +69 -70
  50. package/src/toolSet/searchAssets.js +5 -6
  51. package/src/toolSet/setContext.js +65 -66
  52. package/src/toolSet/superstat.js +61 -60
  53. package/src/toolSet/tableInfo.js +58 -59
@@ -13,18 +13,20 @@ import openAPIJson from "./openAPIJson.js";
13
13
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
14
  import { randomUUID } from "node:crypto";
15
15
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16
+
16
17
  import tlogon from "./toolHelpers/tlogon.js";
17
18
 
19
+ import { getMetadata, authorize, callback, token, baseUrl } from "./oauthHandlers/index.js";
20
+ import processHeaders from "./processHeaders.js";
18
21
 
19
22
  // setup express server
20
23
 
21
24
  async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
22
25
  // setup for change to persistence session
23
- cache.set("headerCache", {});
24
-
26
+ cache.del("headerCache");
25
27
  const app = express();
26
28
  let appStatus = false;
27
-
29
+ app.use(express.urlencoded({ extended: true })); // MUST be before your routes
28
30
  app.use(express.json({ limit: "50mb" }));
29
31
  app.use(
30
32
  cors({
@@ -44,7 +46,73 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
44
46
  // app.use(helmet());
45
47
  app.use(bodyParser.json({ limit: process.env.JSON_LIMIT ?? "50mb" }));
46
48
 
49
+ // In-memory stores for the OAuth PKCE proxy flow (cleared on server restart)
50
+ const pkceStore = new Map(); // ourState -> { codeVerifier, clientRedirectUri, clientState }
51
+ const codeStore = new Map(); // ourCode -> { access_token, refresh_token, expires_in }
52
+
53
+ app.get('/.well-known/oauth-protected-resource', (req, res) => {
54
+ let payload = {
55
+ resource: `${baseAppEnvContext.mcpHost}/mcp`,
56
+ authorization_servers: [`${baseAppEnvContext.VIYA_SERVER}`],
57
+ scopes_supported: ['openid'],
58
+ bearer_methods_supported: ["header"]
59
+ }
60
+ console.error("[Note]>>>>>>>>>>>>>>>>>>>>>>>>>> protected resource metadata ", payload );
61
+ return res.json(payload);
62
+ });
63
+
64
+ app.get("/.well-known/oauth-authorization-server", async (req, res) => {
65
+ console.error("[Note] Received request for OAuth authorization server metadata");
66
+ let metadata = getMetadata(req, res, baseAppEnvContext);
67
+ console.error("[Note]>>>>>>>>>>>>>>>>>>>>>>>>> metadata ", metadata);
68
+ return res.status(200).json(metadata);
69
+ });
70
+
71
+ // OAuth authorize — generates PKCE params, stores state, redirects to SAS Viya
72
+ app.get("/oauth/authorize", async (req, res) => {
73
+ console.error("[Note] Received request for /oauth/authorize");
74
+ return authorize(req, res, baseAppEnvContext, pkceStore, codeStore);
75
+ });
76
+ app.get("/authorize", async (req, res) => {
77
+ console.error("[Note] Received request for /authorize");
78
+ return authorize(req, res, baseAppEnvContext, pkceStore, codeStore);
79
+ });
80
+
81
+ // OAuth callback — receives code from SAS Viya, exchanges for tokens
82
+ app.get("/callback", async (req, res) => {
83
+ console.error("[Note] Received request for /callback with query parameters:");
84
+ return await callback(req, res, pkceStore, codeStore, baseAppEnvContext);
85
+ });
86
+
87
+ // OAuth token endpoint — MCP client exchanges intermediate code for access token
88
+ app.post("/oauth/token", (req, res) => {
89
+ console.error("[Note] Received request for /oauth/token");
90
+ return token(req, res, baseAppEnvContext, codeStore, cache);
91
+ });
92
+
93
+ app.post("/token", (req, res) => {
94
+ console.error("[Note] Received request for /token");
95
+ return token(req, res, baseAppEnvContext, codeStore, cache);
96
+ });
47
97
  // setup routes
98
+
99
+ // Root endpoint info
100
+
101
+ app.get("/", (req, res) => {
102
+ res.json({
103
+ name: "SAS Viya Sample MCP Server",
104
+ version: baseAppEnvContext.version,
105
+ description: "SAS Viya Sample MCP Server",
106
+ endpoints: {
107
+ mcp: "/mcp",
108
+ health: "/health",
109
+ apiMeta: "/apiMeta"
110
+ },
111
+ usage: "Use with MCP Inspector or compatible MCP clients",
112
+ });
113
+ });
114
+
115
+ // health endpoint
48
116
  app.get("/health", (req, res) => {
49
117
  console.error("Received request for health endpoint");
50
118
  if (appStatus === false) {
@@ -64,366 +132,314 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
64
132
  };
65
133
  res.json(health);
66
134
  });
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",
135
+
136
+ // api metadata endpoint(for sas specs)
137
+ app.get("/apiMeta", (req, res) => {
138
+ let spec = openAPIJson(baseAppEnvContext.version);
139
+ res.json(spec);
81
140
  });
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
141
 
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
- }
142
+ // for azure container apps
143
+ app.get("/openapi.json", (req, res) => {
144
+ let spec = openAPIJson(baseAppEnvContext.version);
145
+ res.json(spec);
146
+ });
120
147
 
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");
148
+ // handle processing of information in header.
149
+ function requireBearer(req, res, next) {
150
+ return processHeaders(req, res, next, cache, baseAppEnvContext);
128
151
  }
129
- cache.set("headerCache", headerCache);
130
- next();
131
- console.error("Finished processing headers for /mcp request");
132
- console.log("=======================================================");
133
- }
134
152
 
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);
145
- }
153
+ // process mcp endpoint requests
154
+ const handleRequest = async (req, res) => {
155
+ let transport = null;
156
+ let transports = cache.get("transports");
157
+ console.error("=========================================================");
158
+ console.error("Processing POST /mcp request");
159
+ if (transports == null) {
160
+ console.error("[Error] ***** transports cache is null. This is an error");
161
+ transports = {};
162
+ cache.set("transports", transports);
163
+ }
146
164
 
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];
165
+ console.error("current transports in cache:", Object.keys(transports));
166
+ try {
167
+
168
+ let sessionId = req.headers["mcp-session-id"];
169
+ console.error("[Note]Incoming session ID:", sessionId);
170
+ let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
171
+ console.error('[Note] Payload is ', body);
172
+ if (/*!sessionId &&*/ isInitializeRequest(req.body)) {
173
+ // create transport
174
+ console.error("[Note] Initializing new transport for MCP session...");
175
+
176
+ transport = new StreamableHTTPServerTransport({
177
+ sessionIdGenerator: () => randomUUID(),
178
+ enableJsonResponse: true,
179
+ enableDnsRebindingProtection: true,
180
+ onsessioninitialized: (sessionId) => {
181
+ // Store the transport by session ID
182
+ console.error('Session initialized');
183
+ console.error("[Note] Transport initialized with ID:", sessionId);
184
+ transports[sessionId] = transport;
185
+ },
186
+ });
187
+ // Clean up transport when closed
188
+ transport.onclose = () => {
189
+ if (transport.sessionId && transports[transport.sessionId]) {
190
+ delete transports[transport.sessionId];
191
+ }
192
+ };
193
+ console.error("[Note] Connecting mcpServer to new transport...");
194
+ await mcpServer.connect(transport);
195
+
196
+ // Save transport data and app context for use in tools
197
+ console.error('[Note] Connected to mcpServer');
198
+ cache.set("transports", transports);
199
+ console.error("=======================================================");
200
+ return await transport.handleRequest(req, res, req.body);
201
+
202
+ // cache transport
203
+
204
+ } else if (sessionId != null) {
205
+ console.error('[Note] Incoming session ID:', sessionId);
206
+ transport = transports[sessionId];
207
+ console.error("[Note] Found transport:", transport != null);
208
+ if (transport == null) {
209
+ // this can happen if client is holding on to old session id
210
+ console.error("[Error] No transport found for session ID:", sessionId, "Returning a 404 error with instructions for the user");
211
+ 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.`);
212
+ return;
173
213
  }
174
- };
175
- console.error("[Note] Connecting mcpServer to new transport...");
176
- await mcpServer.connect(transport);
177
214
 
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.`);
215
+ // post the curren session - used to pass _appContext to tools
216
+ cache.set("currentId", sessionId);
217
+
218
+ // get app context for session
219
+ let _appContext = cache.get(sessionId);
220
+
221
+ //if first prompt on a sessionid, create app context
222
+ if (_appContext == null) {
223
+
224
+ let appEnvTemplate = cache.get("appEnvTemplate");
225
+ let headerCache = cache.get("headerCache");
226
+ _appContext = Object.assign({}, appEnvTemplate, headerCache);
227
+ cache.set(sessionId, _appContext);
228
+ } else {
229
+ let headerCache = cache.get("headerCache");
230
+ _appContext = Object.assign(_appContext, headerCache);
231
+ cache.set(sessionId, _appContext);
232
+ }
233
+ console.error("[Note] Using existing transport for session ID:", sessionId);
234
+ console.error("==========================================================");
235
+ await transport.handleRequest(req, res, req.body);
194
236
  return;
195
237
  }
196
238
 
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);
239
+ // initialize request
240
+
241
+ }
242
+ catch (error) {
243
+ console.error("Error handling MCP request:", error);
244
+ if (!res.headersSent) {
245
+ res.status(500).json({
246
+ jsonrpc: "2.0",
247
+ error: {
248
+ code: -32603,
249
+ message: JSON.stringify(error),
250
+ },
251
+ id: null,
252
+ });
216
253
  }
217
- console.error("[Note] Using existing transport for session ID:", sessionId);
218
- console.error("==========================================================");
219
- await transport.handleRequest(req, res, req.body);
220
254
  return;
221
255
  }
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
- });
256
+ };
257
+ const handleGetDelete = async (req, res) => {
258
+ console.error("=========================================================");
259
+ console.error(`[Note] ${req.method} /mcp called`);
260
+ const sessionId = req.headers["mcp-session-id"];
261
+ console.error("[Note] SessionId:", sessionId);
262
+
263
+ let transports = cache.get("transports");
264
+ let transport = (sessionId == null) ? null : transports[sessionId];
265
+ console.error("[Note] Transport found:", transport != null);
266
+ if (!sessionId || transport == null) {
267
+ res.status(404).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
268
+ return;
269
+ }
270
+ if (req.method === "GET") {
271
+ await transport.handleRequest(req, res);
272
+ return;
273
+ }
274
+ if (req.method === "DELETE" && sessionId != null) {
275
+ console.error("[Note] Deleting transport and cache for session ID:", sessionId);
276
+ delete transports[sessionId];
277
+ cache.del(sessionId);
278
+ res.status(201).send(`[Info] Deleted session ${sessionId}`);
237
279
  }
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
280
  }
264
- }
265
281
 
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
- }
282
+ app.options("/mcp", (_, res) => res.sendStatus(204));
283
+ app.post("/mcp", requireBearer, handleRequest);
284
+ app.get("/mcp", handleGetDelete);
285
+ app.delete("/mcp", handleGetDelete);
286
+ app.get("/StartUp", (_req, res) => {
287
+ console.error("===================================================================")
288
+ console.error("Received request for Startup endpoint. Current app status:", appStatus);
289
+ console.error("===================================================================");
290
+ if (appStatus === false) {
291
+ return res.status(503).json({ status: "starting" });
292
+ }
293
+ return res.status(200).json({ status: "started" });
294
+ });
295
+ app.get("/tlogon", async (_req, res) => {
296
+ console.error(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Testing logon");
297
+ if (appStatus === false) {
298
+ return res.status(503).json({ status: "not ready" });
299
+ }
300
+ let r = await tlogon(baseAppEnvContext);
301
+ console.error(r);
302
+ return res.status(200).json(r);
303
+ });
304
+ app.get("/status", (_req, res) => {
305
+ console.error("===================================================================")
306
+ console.error("Received request for status endpoint. Current app status:", appStatus);
307
+ console.error("===================================================================");
308
+ if (appStatus === false) {
309
+ return res.status(503).json({ status: "not ready" });
310
+ }
311
+ return res.status(200).json({ status: "ready" });
312
+ });
324
313
 
325
- cache.set("appEnvBase", appEnvBase);
314
+ app.get("/ready", (_req, res) => {
315
+ console.error("===================================================================")
316
+ console.error("Received request for ready endpoint. Current app status:", appStatus);
317
+ console.error("===================================================================")
318
+ if (appStatus === false) {
319
+ return res.status(503).json({ status: "not ready" });
320
+ }
321
+ return res.status(200).json({ status: "ready" });
322
+ });
323
+ // Start the server
324
+ let appEnvBase = cache.get("appEnvBase");
325
+
326
+ const PORT = appEnvBase.PORT;
327
+
328
+ // get user specified TLS options
329
+ let appServer;
330
+
331
+ // get TLS options
332
+ if (appEnvBase.HTTPS === 'TRUE') {
333
+ if (appEnvBase.tlsOpts == null) {
334
+ appEnvBase.tlsOpts = await getTls(appEnvBase);
335
+ console.error(Object.keys(appEnvBase.tlsOpts));
336
+ appEnvBase.tlsOpts.requestCert = false;
337
+ appEnvBase.tlsOpts.rejectUnauthorized = false;
338
+ appEnvBase.contexts.appCert = appEnvBase.tlsOpts; /* just for completeness */
339
+ }
326
340
 
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");
341
+ cache.set("appEnvBase", appEnvBase);
335
342
 
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
- );
343
+ console.error(`[Note] MCP Server listening on port ${PORT}`);
344
+ console.error(
345
+ "[Note] Visit https://localhost:8080/health for health check"
346
+ );
347
+ console.error(
348
+ "[Note] Configure your mcp host to use https://localhost:8080/mcp to interact with the MCP server"
349
+ );
350
+ console.error("[Note] Press Ctrl+C to stop the server");
354
351
 
352
+ appServer = https.createServer(appEnvBase.tlsOpts, app);
353
+ appServer.listen(PORT, "0.0.0.0", () => {
354
+ console.error(`[Note] Express server successfully bound to 0.0.0.0:${PORT}`);
355
+ appStatus = true;
355
356
  });
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(() => { });
357
+ } else {
358
+ console.error(`[Note] MCP Server listening on port ${PORT}`);
359
+ console.error("[Note] Visit http://localhost:8080/health for health check");
360
+ console.error(
361
+ "[Note] Configure your mcp host to use http://localhost:8080/mcp to interact with the MCP server"
362
+ );
363
+ console.error("[Note] Press Ctrl+C to stop the server");
364
+ try {
365
+ appServer = app.listen(PORT, "0.0.0.0", () => {
366
+ appStatus = true;
367
+ console.error(
368
+ `[Note] Express server successfully bound to 0.0.0.0:${PORT}`
369
+ );
370
+
371
+ });
372
+ } catch (error) {
373
+ console.error("Error starting server:", error);
374
+ }
371
375
  }
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] };
376
+ process.on("SIGTERM", () => {
377
+ console.error("Server closed");
378
+ if (appServer != null) {
379
+ appServer.close(() => { });
380
+ }
381
+ process.exit(0);
382
+ });
383
+ process.on("SIGINT", () => {
384
+ console.error("Server closed");
385
+ if (appServer != null) {
386
+ appServer.close(() => { });
387
+ }
388
+ process.exit(0);
389
389
  });
390
390
 
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
- }
391
+ // create unsigned TLS cert
392
+ async function getTls(appEnv) {
393
+ let tlscreate =
394
+ appEnv.TLS_CREATE == null
395
+ ? "TLS_CREATE=C:US,ST:NC,L:Cary,O:SAS Institute,OU:STO,CN:localhost,ALT:na.sas.com"
396
+ : appEnv.TLS_CREATE;
397
+ let subjt = tlscreate.replaceAll('"', "").trim();
398
+ let subj = subjt.split(",");
399
+
400
+ let d = {};
401
+ subj.map((c) => {
402
+ let r = c.split(":");
403
+ d[r[0]] = r[1];
404
+ return { value: r[1] };
405
+ });
406
+
407
+ let attr = [
408
+ {
409
+ name: "commonName",
410
+ value: d.CN,
411
+ },
412
+ {
413
+ name: "countryName",
414
+ value: d.C,
415
+ },
416
+ {
417
+ shortName: "ST",
418
+ value: d.ST,
419
+ },
420
+ {
421
+ name: "localityName",
422
+ value: d.L,
423
+ },
424
+ {
425
+ name: "organizationName",
426
+ value: d.O,
427
+ },
428
+ {
429
+ shortName: "OU",
430
+ value: d.OU,
431
+ },
432
+ ];
433
+
434
+ let pems = selfsigned.generate(attr);
435
+ // selfsigned generates a new keypair
436
+ let tls = {
437
+ cert: pems.cert,
438
+ key: pems.private,
439
+ };
440
+ console.error("Generated self-signed TLS certificate");
441
+ return tls;
442
+ }
427
443
  }
428
444
 
429
445
  export default expressMcpServer;