@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.
- package/cli.js +43 -1
- package/package.json +1 -1
- package/skills/mcp-tool-description-optimizer/SKILL.md +129 -0
- package/skills/mcp-tool-description-optimizer/references/examples.md +123 -0
- package/skills/sas-read-and-score/SKILL.md +91 -0
- package/skills/sas-read-strategy/SKILL.md +143 -0
- package/skills/sas-score-workflow/SKILL.md +300 -0
- package/src/authpkce.js +220 -0
- package/src/expressMcpServer.js +484 -336
- package/src/toolSet/.claude/settings.local.json +12 -0
package/src/expressMcpServer.js
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
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
|
-
}
|
|
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
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
let
|
|
151
|
-
console.error("
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.error("
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
}
|
|
357
|
-
console.error(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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.
|
|
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] };
|
|
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
|
-
|
|
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
|
-
|
|
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;
|