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