@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.
- package/cli.js +112 -33
- package/package.json +5 -5
- package/skills/sas-list-tables-smart/SKILL.md +123 -0
- package/skills/sas-read-and-score/SKILL.md +54 -53
- package/skills/sas-read-strategy/SKILL.md +10 -10
- package/skills/sas-score-workflow/SKILL.md +19 -1
- package/skills/sas-spec-migration/SKILL.md +303 -0
- package/src/authpkce.js +219 -0
- package/src/createMcpServer.js +16 -6
- package/src/expressMcpServer.js +354 -338
- package/src/handleGetDelete.js +1 -1
- package/src/oauthHandlers/authorize.js +46 -0
- package/src/oauthHandlers/baseUrl.js +8 -0
- package/src/oauthHandlers/callback.js +93 -0
- package/src/oauthHandlers/getMetadata.js +27 -0
- package/src/oauthHandlers/index.js +7 -0
- package/src/oauthHandlers/token.js +37 -0
- package/src/processHeaders.js +88 -0
- package/src/toolHelpers/_listLibrary.js +0 -1
- package/src/toolHelpers/getLogonPayload.js +5 -1
- package/src/toolHelpers/readCerts.js +4 -4
- package/src/toolHelpers/refreshTokenOauth.js +3 -3
- package/src/toolSet/.claude/settings.local.json +13 -0
- package/src/toolSet/devaScore.js +61 -61
- package/src/toolSet/findJob.js +9 -16
- package/src/toolSet/findJobdef.js +4 -5
- package/src/toolSet/findLibrary.js +68 -68
- package/src/toolSet/findModel.js +4 -5
- package/src/toolSet/findTable.js +6 -6
- package/src/toolSet/getEnv.js +10 -7
- package/src/toolSet/listJobdefs.js +61 -62
- package/src/toolSet/listJobs.js +61 -62
- package/src/toolSet/listLibraries.js +78 -80
- package/src/toolSet/listModels.js +56 -56
- package/src/toolSet/listTables.js +66 -66
- package/src/toolSet/makeTools.js +1 -3
- package/src/toolSet/modelInfo.js +4 -5
- package/src/toolSet/modelScore.js +7 -7
- package/src/toolSet/readTable.js +63 -67
- package/src/toolSet/runCasProgram.js +9 -9
- package/src/toolSet/runJob.js +81 -82
- package/src/toolSet/runJobdef.js +82 -83
- package/src/toolSet/runMacro.js +82 -82
- package/src/toolSet/runProgram.js +8 -13
- package/src/toolSet/sasQuery.js +77 -79
- package/src/toolSet/sasQueryTemplate.js +4 -5
- package/src/toolSet/sasQueryTemplate2.js +4 -5
- package/src/toolSet/scrInfo.js +3 -5
- package/src/toolSet/scrScore.js +69 -70
- package/src/toolSet/searchAssets.js +5 -6
- package/src/toolSet/setContext.js +65 -66
- package/src/toolSet/superstat.js +61 -60
- package/src/toolSet/tableInfo.js +58 -59
package/src/expressMcpServer.js
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.error("
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
279
|
-
app.get("/tlogon", async (_req, res) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
});
|
|
288
|
-
app.get("/status", (_req, res) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
}
|
|
357
|
-
console.error(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
{
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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;
|