@sassoftware/sas-score-mcp-serverjs 0.4.1 → 1.0.1-0
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/.skills/agents/sas-viya-scoring-expert.md +58 -0
- package/.skills/copilot-instructions.md +155 -0
- package/.skills/skills/sas-find-library-smart/SKILL.md +154 -0
- package/.skills/skills/sas-list-tables-smart/SKILL.md +127 -0
- package/.skills/skills/sas-read-and-score/SKILL.md +111 -0
- package/.skills/skills/sas-read-strategy/SKILL.md +156 -0
- package/.skills/skills/sas-request-classifier/SKILL.md +69 -0
- package/.skills/skills/sas-score-workflow/SKILL.md +314 -0
- package/cli.js +311 -70
- package/package.json +7 -7
- package/scripts/docs/SCORE_SKILL_REFERENCE.md +142 -0
- package/scripts/docs/TOOL_DESCRIPTION_TEMPLATE.md +157 -0
- package/scripts/docs/TOOL_UPDATES_SUMMARY.md +208 -0
- package/scripts/docs/mcp-localhost-config-guide.md +184 -0
- package/scripts/docs/oauth-http-transport.md +96 -0
- package/scripts/docs/sas-mcp-tools-reference.md +600 -0
- package/scripts/getViyaca.sh +1 -0
- package/scripts/optimize_final.py +140 -0
- package/scripts/optimize_tools.py +99 -0
- package/scripts/setup-skills.js +34 -0
- package/scripts/update_descriptions.py +46 -0
- package/scripts/viyatls.sh +3 -0
- package/src/authpkce.js +219 -0
- package/src/createMcpServer.js +16 -5
- package/src/expressMcpServer.js +350 -308
- package/src/handleGetDelete.js +6 -3
- package/src/hapiMcpServer.js +10 -18
- package/src/oauthHandlers/authorize.js +46 -0
- package/src/oauthHandlers/baseUrl.js +8 -0
- package/src/oauthHandlers/callback.js +96 -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/setupSkills.js +46 -0
- package/src/toolHelpers/_jobSubmit.js +2 -0
- package/src/toolHelpers/_listLibrary.js +55 -39
- package/src/toolHelpers/getLogonPayload.js +7 -1
- package/src/toolHelpers/readCerts.js +4 -4
- package/src/toolHelpers/refreshToken.js +3 -2
- package/src/toolHelpers/refreshTokenOauth.js +3 -3
- package/src/toolSet/.claude/settings.local.json +13 -0
- package/src/toolSet/devaScore.js +61 -69
- package/src/toolSet/findJob.js +38 -71
- package/src/toolSet/findJobdef.js +28 -59
- package/src/toolSet/findLibrary.js +68 -100
- package/src/toolSet/findModel.js +35 -58
- package/src/toolSet/findTable.js +31 -60
- package/src/toolSet/getEnv.js +30 -45
- package/src/toolSet/listJobdefs.js +61 -96
- package/src/toolSet/listJobs.js +61 -110
- package/src/toolSet/listLibraries.js +78 -90
- package/src/toolSet/listModels.js +56 -83
- package/src/toolSet/listTables.js +66 -95
- package/src/toolSet/makeTools.js +1 -0
- package/src/toolSet/modelInfo.js +22 -54
- package/src/toolSet/modelScore.js +35 -77
- package/src/toolSet/readTable.js +63 -104
- package/src/toolSet/runCasProgram.js +32 -52
- package/src/toolSet/runJob.js +24 -24
- package/src/toolSet/runJobdef.js +26 -29
- package/src/toolSet/runMacro.js +82 -82
- package/src/toolSet/runProgram.js +32 -84
- package/src/toolSet/sasQuery.js +77 -126
- package/src/toolSet/sasQueryTemplate.js +4 -5
- package/src/toolSet/sasQueryTemplate2.js +4 -5
- package/src/toolSet/scrInfo.js +4 -7
- package/src/toolSet/scrScore.js +69 -70
- package/src/toolSet/searchAssets.js +5 -6
- package/src/toolSet/setContext.js +65 -92
- package/src/toolSet/superstat.js +61 -60
- package/src/toolSet/tableInfo.js +58 -102
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,340 +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",
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
135
|
|
|
84
|
-
// api metadata endpoint(for sas specs)
|
|
85
|
-
app.get("/apiMeta", (req, res) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
136
|
+
// api metadata endpoint(for sas specs)
|
|
137
|
+
app.get("/apiMeta", (req, res) => {
|
|
138
|
+
let spec = openAPIJson(baseAppEnvContext.version);
|
|
139
|
+
res.json(spec);
|
|
140
|
+
});
|
|
89
141
|
|
|
90
|
-
// for azure container apps
|
|
91
|
-
app.get("/openapi.json", (req, res) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
142
|
+
// for azure container apps
|
|
143
|
+
app.get("/openapi.json", (req, res) => {
|
|
144
|
+
let spec = openAPIJson(baseAppEnvContext.version);
|
|
145
|
+
res.json(spec);
|
|
146
|
+
});
|
|
95
147
|
|
|
96
|
-
// handle processing of information in header.
|
|
97
|
-
function requireBearer(req, res, next) {
|
|
148
|
+
// handle processing of information in header.
|
|
149
|
+
function requireBearer(req, res, next) {
|
|
150
|
+
return processHeaders(req, res, next, cache, baseAppEnvContext);
|
|
151
|
+
}
|
|
98
152
|
|
|
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
|
+
}
|
|
99
164
|
|
|
100
|
-
|
|
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;
|
|
213
|
+
}
|
|
101
214
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (req.header("X-VIYA-SERVER") != null) {
|
|
105
|
-
console.error("[Note] Using user supplied VIYA server");
|
|
106
|
-
headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
|
|
107
|
-
}
|
|
215
|
+
// post the curren session - used to pass _appContext to tools
|
|
216
|
+
cache.set("currentId", sessionId);
|
|
108
217
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const hdr = req.header("Authorization");
|
|
112
|
-
if (hdr != null) {
|
|
113
|
-
headerCache.bearerToken = hdr.slice(7);
|
|
114
|
-
headerCache.AUTHFLOW = "bearer";
|
|
115
|
-
console.error("[Note] Using user supplied bearer token for authorization");
|
|
116
|
-
}
|
|
218
|
+
// get app context for session
|
|
219
|
+
let _appContext = cache.get(sessionId);
|
|
117
220
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const hdr2 = req.header("X-REFRESH-TOKEN");
|
|
121
|
-
if (hdr2 != null) {
|
|
122
|
-
headerCache.REFRESH_TOKEN = hdr2;
|
|
123
|
-
headerCache.AUTHFLOW = "refresh";
|
|
124
|
-
console.error("[Note] Using user supplied refresh token for authorization");
|
|
125
|
-
}
|
|
126
|
-
cache.set("headerCache", headerCache);
|
|
127
|
-
next();
|
|
128
|
-
}
|
|
221
|
+
//if first prompt on a sessionid, create app context
|
|
222
|
+
if (_appContext == null) {
|
|
129
223
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.error("current transports in cache:", Object.keys(transports));
|
|
139
|
-
try {
|
|
140
|
-
|
|
141
|
-
let sessionId = req.headers["mcp-session-id"];
|
|
142
|
-
console.error("========================================================");
|
|
143
|
-
console.error("post /mcp called with session ID:", sessionId);
|
|
144
|
-
let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
|
|
145
|
-
console.error('[Note] Payload is ', body);
|
|
146
|
-
if (/*!sessionId &&*/ isInitializeRequest(req.body)) {
|
|
147
|
-
// create transport
|
|
148
|
-
console.error("[Note] Initializing new transport for MCP session...");
|
|
149
|
-
|
|
150
|
-
transport = new StreamableHTTPServerTransport({
|
|
151
|
-
sessionIdGenerator: () => randomUUID(),
|
|
152
|
-
enableJsonResponse: true,
|
|
153
|
-
enableDnsRebindingProtection: true,
|
|
154
|
-
onsessioninitialized: (sessionId) => {
|
|
155
|
-
// Store the transport by session ID
|
|
156
|
-
console.error('Session initialized');
|
|
157
|
-
console.error("[Note] Transport initialized with ID:", sessionId);
|
|
158
|
-
transports[sessionId] = transport;
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
// Clean up transport when closed
|
|
162
|
-
transport.onclose = () => {
|
|
163
|
-
if (transport.sessionId) {
|
|
164
|
-
delete transports[transport.sessionId];
|
|
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);
|
|
165
232
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Save transport data and app context for use in tools
|
|
171
|
-
console.error('connected mcpServer');
|
|
172
|
-
cache.set("transports", transports);
|
|
173
|
-
return await transport.handleRequest(req, res, req.body);
|
|
174
|
-
// cache transport
|
|
175
|
-
|
|
176
|
-
} else if (sessionId != null) {
|
|
177
|
-
console.error('[Note] Incoming session ID:', sessionId);
|
|
178
|
-
transport = transports[sessionId];
|
|
179
|
-
console.error("[Note] Found transport:", transport != null);
|
|
180
|
-
if (transport == null) {
|
|
181
|
-
// this can happen if client is holding on to old session id
|
|
182
|
-
console.error("[Error] No transport found for session ID:", sessionId, "Returning a 400 error with instructions for the user");
|
|
183
|
-
res.status(400).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.`);
|
|
233
|
+
console.error("[Note] Using existing transport for session ID:", sessionId);
|
|
234
|
+
console.error("==========================================================");
|
|
235
|
+
await transport.handleRequest(req, res, req.body);
|
|
184
236
|
return;
|
|
185
237
|
}
|
|
186
238
|
|
|
187
|
-
//
|
|
188
|
-
cache.set("currentId", sessionId);
|
|
189
|
-
|
|
190
|
-
// get app context for session
|
|
191
|
-
let _appContext = cache.get(sessionId);
|
|
192
|
-
|
|
193
|
-
//if first prompt on a sessionid, create app context
|
|
194
|
-
if (_appContext == null) {
|
|
239
|
+
// initialize request
|
|
195
240
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
});
|
|
200
253
|
}
|
|
201
|
-
console.error("[Note] Using existing transport for session ID:", sessionId);
|
|
202
|
-
|
|
203
|
-
await transport.handleRequest(req, res, req.body);
|
|
204
254
|
return;
|
|
205
255
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.error("
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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}`);
|
|
221
279
|
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
const handleGetDelete = async (req, res) => {
|
|
226
|
-
console.error(req.method, "/mcp called");
|
|
227
|
-
|
|
228
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
229
|
-
console.error("Headers:", sessionId);
|
|
230
|
-
console.error("Handling GET/DELETE for session ID:", sessionId);
|
|
231
|
-
let transports = cache.get("transports");
|
|
232
|
-
let transport = (sessionId == null) ? null : transports[sessionId];
|
|
233
|
-
console.error("Found transport:", transport != null);
|
|
234
|
-
if (!sessionId || transport == null) {
|
|
235
|
-
res.status(200).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
await transport.handleRequest(req, res);
|
|
239
|
-
if (req.method === "DELETE" && sessionId != null) {
|
|
240
|
-
console.error("Deleting transport and cache for session ID:", sessionId);
|
|
241
|
-
delete transports[sessionId];
|
|
242
|
-
cache.del(sessionId);
|
|
243
280
|
}
|
|
244
|
-
}
|
|
245
281
|
|
|
246
|
-
app.options("/mcp", (_, res) => res.sendStatus(204));
|
|
247
|
-
app.post("/mcp", requireBearer, handleRequest);
|
|
248
|
-
app.get("/mcp", handleGetDelete);
|
|
249
|
-
app.delete("/mcp", handleGetDelete);
|
|
250
|
-
app.get("/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
});
|
|
273
313
|
|
|
274
|
-
app.get("/ready", (_req, res) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
}
|
|
298
340
|
|
|
299
|
-
|
|
341
|
+
cache.set("appEnvBase", appEnvBase);
|
|
300
342
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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");
|
|
309
351
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
appStatus= true;
|
|
314
|
-
});
|
|
315
|
-
} else {
|
|
316
|
-
console.error(`[Note] MCP Server listening on port ${PORT}`);
|
|
317
|
-
console.error("[Note] Visit http://localhost:8080/health for health check");
|
|
318
|
-
console.error(
|
|
319
|
-
"[Note] Configure your mcp host to use http://localhost:8080/mcp to interact with the MCP server"
|
|
320
|
-
);
|
|
321
|
-
console.error("[Note] Press Ctrl+C to stop the server");
|
|
322
|
-
try {
|
|
323
|
-
appServer = app.listen(PORT, "0.0.0.0", () => {
|
|
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}`);
|
|
324
355
|
appStatus = true;
|
|
325
|
-
console.error(
|
|
326
|
-
`[Note] Express server successfully bound to 0.0.0.0:${PORT}`
|
|
327
|
-
);
|
|
328
|
-
|
|
329
356
|
});
|
|
330
|
-
}
|
|
331
|
-
console.error(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
}
|
|
345
375
|
}
|
|
346
|
-
process.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
subj.map((c) => {
|
|
360
|
-
let r = c.split(":");
|
|
361
|
-
d[r[0]] = r[1];
|
|
362
|
-
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);
|
|
363
389
|
});
|
|
364
390
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
}
|
|
401
443
|
}
|
|
402
444
|
|
|
403
445
|
export default expressMcpServer;
|