@sassoftware/sas-score-mcp-serverjs 0.3.2 → 0.3.3

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": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A mcp server for SAS Viya",
5
5
  "author": "Deva Kumar <deva.kumar@sas.com>",
6
6
  "license": "Apache-2.0",
@@ -11,7 +11,7 @@ import cors from "cors";
11
11
  import bodyParser from "body-parser";
12
12
 
13
13
  import selfsigned from "selfsigned";
14
- import getOpts from "./toolHelpers/getOpts.js";
14
+ import openAPIJson from "./openAPIJson.js";
15
15
  import fs from "fs";
16
16
 
17
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -21,7 +21,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
21
21
 
22
22
  // setup express server
23
23
 
24
- async function expressMcpServer(mcpServer, cache, currentAppEnvContext) {
24
+ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
25
25
  // setup for change to persistence session
26
26
  let headerCache = {};
27
27
 
@@ -49,289 +49,293 @@ async function expressMcpServer(mcpServer, cache, currentAppEnvContext) {
49
49
  // setup routes
50
50
  app.get("/health", (req, res) => {
51
51
  console.error("Received request for health endpoint");
52
-
53
- res.json({
52
+ let health = {
54
53
  name: "@sassoftware/mcp-server",
55
- version: "1.0.0",
54
+ version: baseAppEnvContext.version,
56
55
  description: "SAS Viya Sample MCP Server",
57
56
  endpoints: {
58
57
  mcp: "/mcp",
59
58
  health: "/health",
59
+ apiMeta: "/apiMeta"
60
60
  },
61
61
  usage:
62
62
  "Use with MCP Inspector or compatible MCP clients like vscode or your own MCP client",
63
- });
63
+ };
64
+ res.json(health);
64
65
  });
65
-
66
- // Root endpoint info
67
-
68
- app.get("/", (req, res) => {
69
- res.json({
70
- name: "SAS Viya Sample MCP Server",
71
- version: "1.0.0",
72
- description: "SAS Viya Sample MCP Server",
73
- endpoints: {
74
- mcp: "/mcp",
75
- health: "/health",
76
- },
77
- usage: "Use with MCP Inspector or compatible MCP clients",
78
- });
66
+
67
+ // Root endpoint info
68
+
69
+ app.get("/", (req, res) => {
70
+ res.json({
71
+ name: "SAS Viya Sample MCP Server",
72
+ version: "1.0.0",
73
+ description: "SAS Viya Sample MCP Server",
74
+ endpoints: {
75
+ mcp: "/mcp",
76
+ health: "/health",
77
+ apiMeta: "/apiMeta"
78
+ },
79
+ usage: "Use with MCP Inspector or compatible MCP clients",
79
80
  });
81
+ });
80
82
 
81
- // api metadata endpoint
82
- app.get("/apiMeta", (req, res) => {
83
- let spec = fs.readFileSync("./openApi.json", "utf8");
84
- let specJson = JSON.parse(spec);
85
- res.json(specJson);
86
- });
83
+ // api metadata endpoint
84
+ app.get("/apiMeta", (req, res) => {
85
+ let spec = openAPIJson();
86
+ res.json(spec);
87
+ });
87
88
 
88
- // handle processing of information in header.
89
- function requireBearer(req, res, next) {
90
-
89
+ // handle processing of information in header.
90
+ function requireBearer(req, res, next) {
91
91
 
92
- // process any new header information
93
92
 
94
- // Allow different VIYA server per sessionid(user)
95
- let headerCache = {};
96
- if (req.header("X-VIYA-SERVER") != null) {
97
- console.error("[Note] Using user supplied VIYA server");
98
- headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
99
- }
93
+ // process any new header information
100
94
 
101
- // used when doing autorization via mcp client
102
- // ideal for production use
103
- const hdr = req.header("Authorization");
104
- if (hdr != null) {
105
- headerCache.bearerToken = hdr.slice(7);
106
- headerCache.AUTHFLOW = "bearer";
107
- }
95
+ // Allow different VIYA server per sessionid(user)
96
+ let headerCache = {};
97
+ if (req.header("X-VIYA-SERVER") != null) {
98
+ console.error("[Note] Using user supplied VIYA server");
99
+ headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
100
+ }
108
101
 
109
- // faking out api key since Viya does not support
110
- // not ideal for production
111
- const hdr2 = req.header("X-REFRESH-TOKEN");
112
- if (hdr2 != null) {
113
- headerCache.refreshToken = hdr2;
114
- headerCache.AUTHFLOW = "refresh";
115
- }
116
-
117
- next();
102
+ // used when doing autorization via mcp client
103
+ // ideal for production use
104
+ const hdr = req.header("Authorization");
105
+ if (hdr != null) {
106
+ headerCache.bearerToken = hdr.slice(7);
107
+ headerCache.AUTHFLOW = "bearer";
118
108
  }
119
109
 
120
- // process mcp endpoint requests
121
- const handleRequest = async (req, res) => {
122
- let transport;
123
- let transports = cache.get("transports");
124
- try {
125
-
126
- let sessionId = req.headers["mcp-session-id"];
127
-
128
- // we have session id, get existing transport
129
-
130
- if (sessionId != null) {
131
- /* existing transport */
132
- transport = transports[sessionId];
133
- if (transport == null) {
134
- throw new Error(`No transport found for session ID: ${sessionId}`);
135
- }
110
+ // faking out api key since Viya does not support
111
+ // not ideal for production
112
+ const hdr2 = req.header("X-REFRESH-TOKEN");
113
+ if (hdr2 != null) {
114
+ headerCache.refreshToken = hdr2;
115
+ headerCache.AUTHFLOW = "refresh";
116
+ }
136
117
 
137
- // post the curren session - used to pass _appContext to tools
138
- cache.set("currentId", sessionId);
118
+ next();
119
+ }
139
120
 
140
- // get app context for session
141
- let _appContext = cache.get(sessionId);
121
+ // process mcp endpoint requests
122
+ const handleRequest = async (req, res) => {
123
+ let transport;
124
+ let transports = cache.get("transports");
125
+ try {
142
126
 
143
- //if first prompt on a sessionid, create app context
144
- if (_appContext == null) {
145
-
146
- let appEnvTemplate = cache.get("appEnvTemplate");
147
- _appContext = Object.assign({}, appEnvTemplate, headerCache);
148
- cache.set(sessionId, _appContext);
149
- }
150
- console.error("[Note] Using existing transport for session ID:", sessionId);
151
-
152
- await transport.handleRequest(req, res, req.body);
153
- }
127
+ let sessionId = req.headers["mcp-session-id"];
154
128
 
155
- // initialize request
156
- else if (!sessionId && isInitializeRequest(req.body)) {
157
- // create transport
158
-
159
- transport = new StreamableHTTPServerTransport({
160
- sessionIdGenerator: () => randomUUID(),
161
- enableJsonResponse: true,
162
- onsessioninitialized: (sessionId) => {
163
- // Store the transport by session ID
164
- transports[sessionId] = transport;
165
- },
166
- });
167
- // Clean up transport when closed
168
- transport.onclose = () => {
169
- if (transport.sessionId) {
170
- delete transports[transport.sessionId];
171
- }
172
- };
173
- console.error("[Note] Connecting mcpServer to new transport...");
174
- await mcpServer.connect(transport);
175
-
176
- // Save transport data and app context for use in tools
177
-
178
- await transport.handleRequest(req, res, req.body);
179
- // cache transport
180
- cache.set("transports", transports);
181
-
182
- }
129
+ // we have session id, get existing transport
130
+
131
+ if (sessionId != null) {
132
+ /* existing transport */
133
+ transport = transports[sessionId];
134
+ if (transport == null) {
135
+ throw new Error(`No transport found for session ID: ${sessionId}`);
183
136
  }
184
- catch (error) {
185
- console.error("Error handling MCP request:", error);
186
- if (!res.headersSent) {
187
- res.status(500).json({
188
- jsonrpc: "2.0",
189
- error: {
190
- code: -32603,
191
- message: JSON.stringify(error),
192
- },
193
- id: null,
194
- });
137
+
138
+ // post the curren session - used to pass _appContext to tools
139
+ cache.set("currentId", sessionId);
140
+
141
+ // get app context for session
142
+ let _appContext = cache.get(sessionId);
143
+
144
+ //if first prompt on a sessionid, create app context
145
+ if (_appContext == null) {
146
+
147
+ let appEnvTemplate = cache.get("appEnvTemplate");
148
+ _appContext = Object.assign({}, appEnvTemplate, headerCache);
149
+ cache.set(sessionId, _appContext);
195
150
  }
196
- return;
151
+ console.error("[Note] Using existing transport for session ID:", sessionId);
152
+
153
+ await transport.handleRequest(req, res, req.body);
197
154
  }
198
- };
199
- const handleGetDelete = async (req, res) => {
200
- console.error(req.method, "/mcp called");
201
- const sessionId = req.headers["mcp-session-id"];
202
- console.error("Handling GET/DELETE for session ID:", sessionId);
203
- let transports = cache.get("transports");
204
- let transport = transports[sessionId];
205
- if (!sessionId || transport == null) {
206
- res.status(400).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
207
- return;
155
+
156
+ // initialize request
157
+ else if (!sessionId && isInitializeRequest(req.body)) {
158
+ // create transport
159
+
160
+ transport = new StreamableHTTPServerTransport({
161
+ sessionIdGenerator: () => randomUUID(),
162
+ enableJsonResponse: true,
163
+ onsessioninitialized: (sessionId) => {
164
+ // Store the transport by session ID
165
+ transports[sessionId] = transport;
166
+ },
167
+ });
168
+ // Clean up transport when closed
169
+ transport.onclose = () => {
170
+ if (transport.sessionId) {
171
+ delete transports[transport.sessionId];
172
+ }
173
+ };
174
+ console.error("[Note] Connecting mcpServer to new transport...");
175
+ await mcpServer.connect(transport);
176
+
177
+ // Save transport data and app context for use in tools
178
+
179
+ await transport.handleRequest(req, res, req.body);
180
+ // cache transport
181
+ cache.set("transports", transports);
182
+
208
183
  }
209
- await transport.handleRequest(req, res);
210
- if (req.method === "DELETE") {
211
- console.error("Deleting transport and cache for session ID:", sessionId);
212
- delete transports[sessionId];
213
- cache.del(sessionId);
184
+ }
185
+ catch (error) {
186
+ console.error("Error handling MCP request:", error);
187
+ if (!res.headersSent) {
188
+ res.status(500).json({
189
+ jsonrpc: "2.0",
190
+ error: {
191
+ code: -32603,
192
+ message: JSON.stringify(error),
193
+ },
194
+ id: null,
195
+ });
214
196
  }
197
+ return;
198
+ }
199
+ };
200
+ const handleGetDelete = async (req, res) => {
201
+ console.error(req.method, "/mcp called");
202
+ const sessionId = req.headers["mcp-session-id"];
203
+ console.error("Handling GET/DELETE for session ID:", sessionId);
204
+ let transports = cache.get("transports");
205
+ let transport = transports[sessionId];
206
+ if (!sessionId || transport == null) {
207
+ res.status(400).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
208
+ return;
209
+ }
210
+ await transport.handleRequest(req, res);
211
+ if (req.method === "DELETE") {
212
+ console.error("Deleting transport and cache for session ID:", sessionId);
213
+ delete transports[sessionId];
214
+ cache.del(sessionId);
215
215
  }
216
+ }
216
217
 
217
- app.options("/mcp", (_, res) => res.sendStatus(204));
218
- app.post("/mcp", requireBearer, handleRequest);
219
- app.get("/mcp", handleGetDelete);
220
- app.delete("/mcp", handleGetDelete);
221
-
222
- // Start the server
223
- let appEnvBase = cache.get("appEnvBase");
224
-
225
- const PORT = appEnvBase.PORT;
226
-
227
- // get user specified TLS options
228
- let appServer;
229
-
230
- // get TLS options
231
- if (appEnvBase.HTTPS === 'TRUE') {
232
- //appEnvBase.tlsOpts = getOpts(appEnvBase);
233
- if (appEnvBase.tlsOpts == null) {
234
- appEnvBase.tlsOpts = await getTls(appEnvBase);
235
- console.error(Object.keys(appEnvBase.tlsOpts));
236
- appEnvBase.tlsOpts.requestCert = false;
237
- appEnvBase.tlsOpts.rejectUnauthorized = false;
238
- }
239
-
240
- cache.set("appEnvBase", appEnvBase);
241
-
242
- console.error(`[Note] MCP Server listening on port ${PORT}`);
243
- console.error(
244
- "[Note] Visit https://localhost:8080/health for health check"
245
- );
246
- console.error(
247
- "[Note] Configure your mcp host to use https://localhost:8080/mcp to interact with the MCP server"
248
- );
249
- console.error("[Note] Press Ctrl+C to stop the server");
250
-
251
- appServer = https.createServer(appEnvBase.tlsOpts, app);
252
- appServer.listen(PORT, "0.0.0.0", () => {});
253
- } else {
254
- console.error(`[Note] MCP Server listening on port ${PORT}`);
255
- console.error("[Note] Visit http://localhost:8080/health for health check");
256
- console.error(
257
- "[Note] Configure your mcp host to use http://localhost:8080/mcp to interact with the MCP server"
258
- );
259
- console.error("[Note] Press Ctrl+C to stop the server");
218
+ app.options("/mcp", (_, res) => res.sendStatus(204));
219
+ app.post("/mcp", requireBearer, handleRequest);
220
+ app.get("/mcp", handleGetDelete);
221
+ app.delete("/mcp", handleGetDelete);
260
222
 
223
+ // Start the server
224
+ let appEnvBase = cache.get("appEnvBase");
225
+
226
+ const PORT = appEnvBase.PORT;
227
+
228
+ // get user specified TLS options
229
+ let appServer;
230
+
231
+ // get TLS options
232
+ if (appEnvBase.HTTPS === 'TRUE') {
233
+ //appEnvBase.tlsOpts = getOpts(appEnvBase);
234
+ if (appEnvBase.tlsOpts == null) {
235
+ appEnvBase.tlsOpts = await getTls(appEnvBase);
236
+ console.error(Object.keys(appEnvBase.tlsOpts));
237
+ appEnvBase.tlsOpts.requestCert = false;
238
+ appEnvBase.tlsOpts.rejectUnauthorized = false;
239
+ }
240
+
241
+ cache.set("appEnvBase", appEnvBase);
242
+
243
+ console.error(`[Note] MCP Server listening on port ${PORT}`);
244
+ console.error(
245
+ "[Note] Visit https://localhost:8080/health for health check"
246
+ );
247
+ console.error(
248
+ "[Note] Configure your mcp host to use https://localhost:8080/mcp to interact with the MCP server"
249
+ );
250
+ console.error("[Note] Press Ctrl+C to stop the server");
251
+
252
+ appServer = https.createServer(appEnvBase.tlsOpts, app);
253
+ appServer.listen(PORT, "0.0.0.0", () => { });
254
+ } else {
255
+ console.error(`[Note] MCP Server listening on port ${PORT}`);
256
+ console.error("[Note] Visit http://localhost:8080/health for health check");
257
+ console.error(
258
+ "[Note] Configure your mcp host to use http://localhost:8080/mcp to interact with the MCP server"
259
+ );
260
+ console.error("[Note] Press Ctrl+C to stop the server");
261
+ try {
261
262
  appServer = app.listen(PORT, "0.0.0.0", () => {
262
263
  console.error(
263
264
  `[Note] Express server successfully bound to 0.0.0.0:${PORT}`
264
265
  );
265
-
266
+
266
267
  });
268
+ } catch (error) {
269
+ console.error("Error starting server:", error);
267
270
  }
268
- process.on("SIGTERM", () => {
269
- console.error("Server closed");
270
- if (appServer != null) {
271
- appServer.close(() => {});
272
- }
273
- process.exit(0);
274
- });
275
- process.on("SIGINT", () => {
276
- console.error("Server closed");
277
- if (appServer != null) {
278
- appServer.close(() => {});
279
- }
280
- process.exit(0);
271
+ }
272
+ process.on("SIGTERM", () => {
273
+ console.error("Server closed");
274
+ if (appServer != null) {
275
+ appServer.close(() => { });
276
+ }
277
+ process.exit(0);
278
+ });
279
+ process.on("SIGINT", () => {
280
+ console.error("Server closed");
281
+ if (appServer != null) {
282
+ appServer.close(() => { });
283
+ }
284
+ process.exit(0);
285
+ });
286
+
287
+ // create unsigned TLS cert
288
+ async function getTls(appEnv) {
289
+ let tlscreate =
290
+ appEnv.TLS_CREATE == null
291
+ ? "TLS_CREATE=C:US,ST:NC,L:Cary,O:SAS Institute,OU:STO,CN:localhost,ALT:na.sas.com"
292
+ : appEnv.TLS_CREATE;
293
+ let subjt = tlscreate.replaceAll('"', "").trim();
294
+ let subj = subjt.split(",");
295
+
296
+ let d = {};
297
+ subj.map((c) => {
298
+ let r = c.split(":");
299
+ d[r[0]] = r[1];
300
+ return { value: r[1] };
281
301
  });
282
302
 
283
- // create unsigned TLS cert
284
- async function getTls(appEnv) {
285
- let tlscreate =
286
- appEnv.TLS_CREATE == null
287
- ? "TLS_CREATE=C:US,ST:NC,L:Cary,O:SAS Institute,OU:STO,CN:localhost,ALT:na.sas.com"
288
- : appEnv.TLS_CREATE;
289
- let subjt = tlscreate.replaceAll('"', "").trim();
290
- let subj = subjt.split(",");
291
-
292
- let d = {};
293
- subj.map((c) => {
294
- let r = c.split(":");
295
- d[r[0]] = r[1];
296
- return { value: r[1] };
297
- });
298
-
299
- let attr = [
300
- {
301
- name: "commonName",
302
- value: d.CN,
303
- },
304
- {
305
- name: "countryName",
306
- value: d.C,
307
- },
308
- {
309
- shortName: "ST",
310
- value: d.ST,
311
- },
312
- {
313
- name: "localityName",
314
- value: d.L,
315
- },
316
- {
317
- name: "organizationName",
318
- value: d.O,
319
- },
320
- {
321
- shortName: "OU",
322
- value: d.OU,
323
- },
324
- ];
325
-
326
- let pems = selfsigned.generate(attr);
327
- // selfsigned generates a new keypair
328
- let tls = {
329
- cert: pems.cert,
330
- key: pems.private,
331
- };
332
- console.error("Generated self-signed TLS certificate");
333
- return tls;
334
- }
303
+ let attr = [
304
+ {
305
+ name: "commonName",
306
+ value: d.CN,
307
+ },
308
+ {
309
+ name: "countryName",
310
+ value: d.C,
311
+ },
312
+ {
313
+ shortName: "ST",
314
+ value: d.ST,
315
+ },
316
+ {
317
+ name: "localityName",
318
+ value: d.L,
319
+ },
320
+ {
321
+ name: "organizationName",
322
+ value: d.O,
323
+ },
324
+ {
325
+ shortName: "OU",
326
+ value: d.OU,
327
+ },
328
+ ];
329
+
330
+ let pems = selfsigned.generate(attr);
331
+ // selfsigned generates a new keypair
332
+ let tls = {
333
+ cert: pems.cert,
334
+ key: pems.private,
335
+ };
336
+ console.error("Generated self-signed TLS certificate");
337
+ return tls;
338
+ }
335
339
  }
336
340
 
337
341
  export default expressMcpServer;
@@ -112,6 +112,7 @@ async function hapiMcpServer(mcpServer, cache, baseAppEnvContext) {
112
112
  endpoints: {
113
113
  mcp: "/mcp",
114
114
  health: "/health",
115
+ apiMeta: "/apiMeta"
115
116
  },
116
117
  usage:
117
118
  "Use with MCP Inspector or compatible MCP clients like vscode or your own MCP client",
@@ -165,8 +166,8 @@ async function hapiMcpServer(mcpServer, cache, baseAppEnvContext) {
165
166
  },
166
167
 
167
168
  auth: {
168
- strategy: "session",
169
- mode: 'try'
169
+ strategy: "sas",
170
+ mode: 'required'
170
171
  },
171
172
  description: "The main route for MCP requests",
172
173
  notes: "Requires a valid session",
@@ -0,0 +1,117 @@
1
+ /*
2
+ * Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ function openAPIJson() {
6
+ let spec =
7
+ {
8
+ "swagger": "2.0",
9
+ "info": {
10
+ "title": "SAS Viya Sample MCP Server",
11
+ "version": "1.0.0",
12
+ "description": "OpenAPI 2.0 spec for the SAS Viya Sample MCP Server"
13
+ },
14
+ "host": "localhost:8080",
15
+ "schemes": ["http", "https"],
16
+ "basePath": "/",
17
+ "paths": {
18
+ "/health": {
19
+ "get": {
20
+ "summary": "Health check endpoint",
21
+ "description": "Returns server health and metadata",
22
+ "produces": ["application/json"],
23
+ "responses": {
24
+ "200": {
25
+ "description": "Health info",
26
+ "schema": { "type": "object" }
27
+ }
28
+ }
29
+ }
30
+ },
31
+ "/": {
32
+ "get": {
33
+ "summary": "Root endpoint",
34
+ "description": "Returns server info and usage",
35
+ "produces": ["application/json"],
36
+ "responses": {
37
+ "200": {
38
+ "description": "Server info",
39
+ "schema": { "type": "object" }
40
+ }
41
+ }
42
+ }
43
+ },
44
+ "/mcp": {
45
+ "get": {
46
+ "summary": "MCP endpoint (GET)",
47
+ "description": "Handles MCP protocol requests (GET)",
48
+ "produces": ["application/json"],
49
+ "parameters": [
50
+ {
51
+ "name": "Authorization",
52
+ "in": "header",
53
+ "type": "string",
54
+ "required": false
55
+ }
56
+ ],
57
+ "responses": {
58
+ "200": {
59
+ "description": "MCP response",
60
+ "schema": { "type": "object" }
61
+ }
62
+ }
63
+ },
64
+ "post": {
65
+ "summary": "MCP endpoint (POST)",
66
+ "description": "Handles MCP protocol requests (POST)",
67
+ "consumes": ["application/json"],
68
+ "produces": ["application/json"],
69
+ "parameters": [
70
+ {
71
+ "name": "Authorization",
72
+ "in": "header",
73
+ "type": "string",
74
+ "required": false
75
+ },
76
+ {
77
+ "name": "X-VIYA-SERVER",
78
+ "in": "header",
79
+ "type": "string",
80
+ "required": false
81
+ },
82
+ {
83
+ "name": "X-REFRESH-TOKEN",
84
+ "in": "header",
85
+ "type": "string",
86
+ "required": false
87
+ },
88
+ {
89
+ "name": "body",
90
+ "in": "body",
91
+ "schema": { "type": "object" }
92
+ }
93
+ ],
94
+ "responses": {
95
+ "200": {
96
+ "description": "MCP response",
97
+ "schema": { "type": "object" }
98
+ }
99
+ }
100
+ },
101
+ "options": {
102
+ "summary": "CORS preflight",
103
+ "description": "CORS preflight for MCP endpoint",
104
+ "responses": {
105
+ "204": {
106
+ "description": "No Content"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ };
113
+
114
+ return spec;
115
+ }
116
+
117
+ export default openAPIJson;