@sassoftware/sas-score-mcp-serverjs 1.0.1-9 → 1.1.1
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/.claude-plugin/plugin.json +59 -0
- package/.skills/agents/sas-score-mcp-serverjs-agent.md +26 -0
- package/.skills/copilot-instructions.md +62 -0
- package/.skills/skills/README.md +204 -0
- package/.skills/skills/detail-strategy/SKILL.md +316 -0
- package/.skills/skills/find-library-server/SKILL.md +62 -0
- package/.skills/skills/find-resources/SKILL.md +66 -0
- package/.skills/skills/list-library/SKILL.md +30 -0
- package/.skills/skills/list-mas-job-jobdef/SKILL.md +31 -0
- package/.skills/skills/list-tables/SKILL.md +30 -0
- package/.skills/skills/read-strategy/SKILL.md +87 -0
- package/.skills/skills/request-routing/SKILL.md +112 -0
- package/.skills/skills/score-cas/SKILL.md +95 -0
- package/.skills/skills/score-job-jobdef/SKILL.md +58 -0
- package/.skills/skills/score-mas-scr/SKILL.md +58 -0
- package/.skills/skills/score-program/SKILL.md +59 -0
- package/.skills/skills/score-strategy/SKILL.md +39 -0
- package/README.md +96 -54
- package/cli.js +11 -13
- package/openApi.yaml +121 -121
- package/package.json +16 -14
- package/scripts/docs/SCORE_SKILL_REFERENCE.md +17 -16
- package/scripts/docs/TOOL_DESCRIPTION_TEMPLATE.md +3 -3
- package/scripts/docs/TOOL_UPDATES_SUMMARY.md +65 -63
- package/scripts/docs/oauth-http-transport.md +2 -2
- package/scripts/docs/sas-mcp-tools-reference.md +43 -32
- package/scripts/plot_msrp_usa.py +49 -0
- package/scripts/refreshtoken.js +58 -0
- package/scripts/runListScr.mjs +16 -0
- package/src/createMcpServer.js +4 -1
- package/src/expressMcpServer.js +47 -49
- package/src/oauthHandlers/authorize.js +4 -1
- package/src/oauthHandlers/baseUrl.js +4 -0
- package/src/oauthHandlers/callback.js +4 -0
- package/src/oauthHandlers/getMetadata.js +4 -0
- package/src/oauthHandlers/index.js +4 -0
- package/src/oauthHandlers/token.js +4 -0
- package/src/openApi.yaml +121 -121
- package/src/processHeaders.js +10 -7
- package/src/setupSkills.js +1 -18
- package/src/toolHelpers/_casScore.js +32 -0
- package/src/toolHelpers/_desc.js +14 -0
- package/src/toolHelpers/_findJob.js +12 -0
- package/src/toolHelpers/_findJobdef.js +10 -0
- package/src/toolHelpers/_findLibrary.js +11 -0
- package/src/toolHelpers/_findMas.js +13 -0
- package/src/toolHelpers/_findScr.js +36 -0
- package/src/toolHelpers/_findTable.js +11 -0
- package/src/toolHelpers/_listJobdefs.js +12 -2
- package/src/toolHelpers/_listJobs.js +19 -8
- package/src/toolHelpers/{_listModels.js → _listMas.js} +4 -4
- package/src/toolHelpers/_listScr.js +13 -0
- package/src/toolHelpers/{_scrInfo.js → _scrDescribe.js} +4 -4
- package/src/toolHelpers/_scrScore.js +2 -2
- package/src/toolHelpers/_submitCasl.js +19 -17
- package/src/toolHelpers/{_tableInfo.js → _tableDescribe.js} +2 -2
- package/src/toolHelpers/getLogonPayload.js +2 -2
- package/src/toolSet/casModelScore.js +93 -0
- package/src/toolSet/casProgramScore.js +105 -0
- package/src/toolSet/devaScore.js +11 -6
- package/src/toolSet/findJob.js +74 -59
- package/src/toolSet/findJobdef.js +67 -64
- package/src/toolSet/findLibrary.js +28 -23
- package/src/toolSet/findMas.js +72 -0
- package/src/toolSet/findScr.js +69 -0
- package/src/toolSet/findTable.js +34 -27
- package/src/toolSet/getEnv.js +57 -57
- package/src/toolSet/jobDescribe.js +65 -0
- package/src/toolSet/jobScore.js +90 -0
- package/src/toolSet/jobdefDescribe.js +67 -0
- package/src/toolSet/jobdefScore.js +85 -0
- package/src/toolSet/listJobdefs.js +17 -8
- package/src/toolSet/listJobs.js +15 -8
- package/src/toolSet/listLibraries.js +16 -10
- package/src/toolSet/listMas.js +71 -0
- package/src/toolSet/listScr.js +62 -0
- package/src/toolSet/listTables.js +78 -66
- package/src/toolSet/{runMacro.js → macroScore.js} +86 -82
- package/src/toolSet/makeTools.js +39 -25
- package/src/toolSet/masDescribe.js +67 -0
- package/src/toolSet/masScore.js +95 -0
- package/src/toolSet/{runProgram.js → programScore.js} +96 -93
- package/src/toolSet/readTable.js +43 -26
- package/src/toolSet/sasQuery.js +24 -18
- package/src/toolSet/scrDescribe.js +55 -0
- package/src/toolSet/scrScore.js +63 -70
- package/src/toolSet/searchAssets.js +1 -1
- package/src/toolSet/setContext.js +8 -3
- package/src/toolSet/superstat.js +61 -61
- package/src/toolSet/tableDescribe.js +65 -0
- package/.agents/sas-score-mcp-serverjs-agent.md +0 -58
- package/.instructions/copilot-instructions.md +0 -201
- package/.instructions/enforce-find-resource-strategy.md +0 -35
- package/.skills/sas-find-library-smart/SKILL.md +0 -155
- package/.skills/sas-find-resource-strategy/SKILL.md +0 -105
- package/.skills/sas-list-resource-strategy/SKILL.md +0 -124
- package/.skills/sas-list-tables-smart/SKILL.md +0 -128
- package/.skills/sas-read-and-score-strategy/SKILL.md +0 -113
- package/.skills/sas-read-strategy/SKILL.md +0 -154
- package/.skills/sas-request-classifier/SKILL.md +0 -74
- package/.skills/sas-score-workflow-strategy/SKILL.md +0 -314
- package/scripts/optimize_final.py +0 -140
- package/scripts/optimize_tools.py +0 -99
- package/scripts/setup-skills.js +0 -34
- package/scripts/update_descriptions.py +0 -46
- package/src/authpkce.js +0 -219
- package/src/handleGetDelete.js +0 -34
- package/src/handleRequest.js +0 -112
- package/src/hapiMcpServer.js +0 -241
- package/src/toolSet/findModel.js +0 -60
- package/src/toolSet/listModels.js +0 -56
- package/src/toolSet/modelInfo.js +0 -55
- package/src/toolSet/modelScore.js +0 -89
- package/src/toolSet/runCasProgram.js +0 -98
- package/src/toolSet/runJob.js +0 -81
- package/src/toolSet/runJobdef.js +0 -82
- package/src/toolSet/scrInfo.js +0 -52
- package/src/toolSet/tableInfo.js +0 -58
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { Agent, fetch } from 'undici';
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
|
|
8
|
+
refreshToken()
|
|
9
|
+
.then (token => {
|
|
10
|
+
console.log(token);
|
|
11
|
+
fs.writeFileSync('token.txt', `"AUTHORIZATION": "Bearer ${token}"`, 'utf8');
|
|
12
|
+
})
|
|
13
|
+
.catch (err => {
|
|
14
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
15
|
+
});
|
|
16
|
+
async function refreshToken(){
|
|
17
|
+
let host = process.env.VIYA_SERVER;
|
|
18
|
+
let token = process.env.REFRESH_TOKEN;
|
|
19
|
+
let url = `${host}/SASLogon/oauth/token`;
|
|
20
|
+
|
|
21
|
+
let aconnect = {
|
|
22
|
+
rejectUnauthorized: false // or false, if you really want to bypass checks
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const agent = new Agent(aconnect);
|
|
26
|
+
|
|
27
|
+
console.error('[Info] Refreshing token...', token);
|
|
28
|
+
const ibody = {
|
|
29
|
+
grant_type: 'refresh_token',
|
|
30
|
+
refresh_token: token,
|
|
31
|
+
client_id: 'sas.cli'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let body = new URLSearchParams(ibody);
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Accept': 'application/json',
|
|
40
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
41
|
+
dispatcher: agent
|
|
42
|
+
},
|
|
43
|
+
body: body.toString()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = await response.text();
|
|
48
|
+
console.error('[Error] Failed to refresh token: ', error);
|
|
49
|
+
throw new Error(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
return data.access_token;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import listScr from '../src/toolSet/listScr.js';
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
const appContext = { brand: 'sas-score' };
|
|
6
|
+
const spec = listScr(appContext);
|
|
7
|
+
try {
|
|
8
|
+
const res = await spec.handler({});
|
|
9
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.error('Error running listScr:', err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
run();
|
package/src/createMcpServer.js
CHANGED
|
@@ -78,6 +78,7 @@ async function createMcpServer(cache, _appContext) {
|
|
|
78
78
|
// Register the tools with brand prefix
|
|
79
79
|
console.error(`[Note] Brand: ${_appContext.brand}`);
|
|
80
80
|
let toolNames = [];
|
|
81
|
+
let totalDescription = 0;
|
|
81
82
|
toolSet.forEach((tool, i) => {
|
|
82
83
|
let toolName = _appContext.brand + '-' + tool.name;
|
|
83
84
|
//tool.inputSchema.additionalProperties = false; // disallow extra properties
|
|
@@ -85,12 +86,14 @@ async function createMcpServer(cache, _appContext) {
|
|
|
85
86
|
description: tool.description,
|
|
86
87
|
inputSchema: tool.inputSchema
|
|
87
88
|
}
|
|
89
|
+
totalDescription += tool.description.length;``
|
|
88
90
|
let toolHandler = wrapf(cache, tool.handler);
|
|
89
|
-
// console.error(`[Note] Registering tool ${toolName} with config: ${JSON.stringify(config)}`);
|
|
90
91
|
let r = mcpServer.registerTool(toolName, config, toolHandler);
|
|
91
92
|
toolNames.push(toolName);
|
|
92
93
|
});
|
|
94
|
+
console.error(`[Note] Agent Mode: ${_appContext.agent}`);
|
|
93
95
|
console.error(`[Note] Registered ${toolSet.length} tools: ${toolNames}`);
|
|
96
|
+
console.error(`[Note] Total description length: ${totalDescription} characters`);
|
|
94
97
|
cache.set("mcpServer", mcpServer);
|
|
95
98
|
return mcpServer;
|
|
96
99
|
}
|
package/src/expressMcpServer.js
CHANGED
|
@@ -9,6 +9,7 @@ import cors from "cors";
|
|
|
9
9
|
import bodyParser from "body-parser";
|
|
10
10
|
import selfsigned from "selfsigned";
|
|
11
11
|
import openAPIJson from "./openAPIJson.js";
|
|
12
|
+
import createMcpServer from "./createMcpServer.js";
|
|
12
13
|
|
|
13
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
15
|
import { randomUUID } from "node:crypto";
|
|
@@ -21,7 +22,7 @@ import processHeaders from "./processHeaders.js";
|
|
|
21
22
|
|
|
22
23
|
// setup express server
|
|
23
24
|
|
|
24
|
-
async function expressMcpServer(
|
|
25
|
+
async function expressMcpServer(_mcpServer, cache, baseAppEnvContext) {
|
|
25
26
|
// setup for change to persistence session
|
|
26
27
|
cache.del("headerCache");
|
|
27
28
|
const app = express();
|
|
@@ -50,6 +51,10 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
50
51
|
const pkceStore = new Map(); // ourState -> { codeVerifier, clientRedirectUri, clientState }
|
|
51
52
|
const codeStore = new Map(); // ourCode -> { access_token, refresh_token, expires_in }
|
|
52
53
|
|
|
54
|
+
// Per-session transports — each initialize creates its own transport
|
|
55
|
+
const transports = new Map(); // sessionId -> transport
|
|
56
|
+
cache.set("transports", transports);
|
|
57
|
+
|
|
53
58
|
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
|
54
59
|
let payload = {
|
|
55
60
|
resource: `${baseAppEnvContext.mcpHost}/mcp`,
|
|
@@ -152,67 +157,54 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
152
157
|
|
|
153
158
|
// process mcp endpoint requests
|
|
154
159
|
const handleRequest = async (req, res) => {
|
|
155
|
-
let transport = null;
|
|
156
|
-
let transports = cache.get("transports");
|
|
157
160
|
console.error("=========================================================");
|
|
158
161
|
console.error("Processing POST /mcp request");
|
|
159
|
-
|
|
160
|
-
console.error("[Error] ***** transports cache is null. This is an error");
|
|
161
|
-
transports = {};
|
|
162
|
-
cache.set("transports", transports);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
console.error("current transports in cache:", Object.keys(transports));
|
|
162
|
+
console.error("current active sessions:", transports.size);
|
|
166
163
|
try {
|
|
167
164
|
|
|
168
165
|
let sessionId = req.headers["mcp-session-id"];
|
|
169
166
|
console.error("[Note]Incoming session ID:", sessionId);
|
|
170
167
|
let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
|
|
171
168
|
console.error('[Note] Payload is ', body);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.error("[Note]
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
|
|
170
|
+
if (isInitializeRequest(req.body)) {
|
|
171
|
+
console.error("[Note]>>>>>>>>>>>>>>>>>>>>>>>>> Creating new MCP server");
|
|
172
|
+
let mcpServer = await createMcpServer(cache, baseAppEnvContext);
|
|
173
|
+
// New session — create a dedicated transport
|
|
174
|
+
console.error("[Note] Initializing new session with fresh transport...");
|
|
175
|
+
const isInitRequest = req.body?.method === 'initialize';
|
|
176
|
+
|
|
177
|
+
const transport = new StreamableHTTPServerTransport({
|
|
177
178
|
sessionIdGenerator: () => randomUUID(),
|
|
178
179
|
enableJsonResponse: true,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.error("[Note]
|
|
184
|
-
transports[sessionId] = transport;
|
|
180
|
+
onsessioninitialized: (newSessionId) => {
|
|
181
|
+
console.error("[Note] Session initialized with ID:", newSessionId);
|
|
182
|
+
transports.set(newSessionId, transport);
|
|
183
|
+
cache.set("transports", transports);
|
|
184
|
+
console.error("[Note] Total active sessions:", Object.keys(transports));
|
|
185
185
|
},
|
|
186
186
|
});
|
|
187
|
-
// Clean up transport when closed
|
|
188
187
|
transport.onclose = () => {
|
|
189
|
-
if (transport.sessionId
|
|
190
|
-
|
|
188
|
+
if (transport.sessionId) {
|
|
189
|
+
console.error("[Note] Session closed, removing transport:", transport.sessionId);
|
|
190
|
+
transports.delete(transport.sessionId);
|
|
191
|
+
cache.del(transport.sessionId);
|
|
191
192
|
}
|
|
192
193
|
};
|
|
193
|
-
console.error("[Note] Connecting mcpServer to new transport...");
|
|
194
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
195
|
console.error("=======================================================");
|
|
200
196
|
return await transport.handleRequest(req, res, req.body);
|
|
201
197
|
|
|
202
|
-
// cache transport
|
|
203
|
-
|
|
204
198
|
} else if (sessionId != null) {
|
|
205
|
-
|
|
206
|
-
transport
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
199
|
+
const transport = transports.get(sessionId);
|
|
200
|
+
if (!transport) {
|
|
201
|
+
console.error("[Note] Unknown session ID:", sessionId);
|
|
202
|
+
return res.status(404).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Please re-initialize." }, id: null });
|
|
213
203
|
}
|
|
204
|
+
console.error('[Note] Incoming session ID:', sessionId);
|
|
205
|
+
console.error("[Note] Using transport for session ID:", sessionId);
|
|
214
206
|
|
|
215
|
-
// post the
|
|
207
|
+
// post the current session - used to pass _appContext to tools
|
|
216
208
|
cache.set("currentId", sessionId);
|
|
217
209
|
|
|
218
210
|
// get app context for session
|
|
@@ -230,7 +222,6 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
230
222
|
_appContext = Object.assign(_appContext, headerCache);
|
|
231
223
|
cache.set(sessionId, _appContext);
|
|
232
224
|
}
|
|
233
|
-
console.error("[Note] Using existing transport for session ID:", sessionId);
|
|
234
225
|
console.error("==========================================================");
|
|
235
226
|
await transport.handleRequest(req, res, req.body);
|
|
236
227
|
return;
|
|
@@ -260,21 +251,28 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
260
251
|
const sessionId = req.headers["mcp-session-id"];
|
|
261
252
|
console.error("[Note] SessionId:", sessionId);
|
|
262
253
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (!sessionId || transport == null) {
|
|
267
|
-
res.status(404).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
|
|
268
|
-
return;
|
|
254
|
+
if (!sessionId) {
|
|
255
|
+
console.error("[Note] No session ID on /DELETE — rejecting");
|
|
256
|
+
return res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: Mcp-Session-Id header is required" }, id: null });
|
|
269
257
|
}
|
|
258
|
+
|
|
259
|
+
const transport = transports.get(sessionId);
|
|
260
|
+
if (!transport) {
|
|
261
|
+
console.error("[Note] Unknown session ID:", sessionId);
|
|
262
|
+
return res.status(404).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Please re-initialize." }, id: null });
|
|
263
|
+
}
|
|
264
|
+
|
|
270
265
|
if (req.method === "GET") {
|
|
266
|
+
console.error("[Note] calling transport.handleRequest for GET /mcp");
|
|
271
267
|
await transport.handleRequest(req, res);
|
|
272
268
|
return;
|
|
273
269
|
}
|
|
274
|
-
if (req.method === "DELETE"
|
|
270
|
+
if (req.method === "DELETE") {
|
|
275
271
|
console.error("[Note] Deleting transport and cache for session ID:", sessionId);
|
|
276
|
-
delete
|
|
272
|
+
transports.delete(sessionId);
|
|
277
273
|
cache.del(sessionId);
|
|
274
|
+
console.error("[Note] Deleted session ID:", sessionId);
|
|
275
|
+
console.error("[Note] Total active sessions:", Object.keys(transports));
|
|
278
276
|
res.status(201).send(`[Info] Deleted session ${sessionId}`);
|
|
279
277
|
}
|
|
280
278
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
1
5
|
function token(req, res, appContext, codeStore, cache) {
|
|
2
6
|
console.error("===============================================================");
|
|
3
7
|
console.error("[Note] at /token endpoint");
|
package/src/openApi.yaml
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
swagger: "2.0"
|
|
2
|
-
info:
|
|
3
|
-
title: SAS Viya Sample MCP Server API
|
|
4
|
-
version: "1.0.0"
|
|
5
|
-
description: API for interacting with the SAS Viya Sample MCP Server.
|
|
6
|
-
host: localhost:8080
|
|
7
|
-
basePath: /
|
|
8
|
-
schemes:
|
|
9
|
-
- http
|
|
10
|
-
- https
|
|
11
|
-
consumes:
|
|
12
|
-
- application/json
|
|
13
|
-
produces:
|
|
14
|
-
- application/json
|
|
15
|
-
paths:
|
|
16
|
-
/health:
|
|
17
|
-
get:
|
|
18
|
-
summary: Health check
|
|
19
|
-
description: Returns health and version information.
|
|
20
|
-
responses:
|
|
21
|
-
200:
|
|
22
|
-
description: Health information
|
|
23
|
-
schema:
|
|
24
|
-
type: object
|
|
25
|
-
properties:
|
|
26
|
-
name:
|
|
27
|
-
type: string
|
|
28
|
-
version:
|
|
29
|
-
type: string
|
|
30
|
-
description:
|
|
31
|
-
type: string
|
|
32
|
-
endpoints:
|
|
33
|
-
type: object
|
|
34
|
-
usage:
|
|
35
|
-
type: string
|
|
36
|
-
/apiMeta:
|
|
37
|
-
get:
|
|
38
|
-
summary: API metadata
|
|
39
|
-
description: Returns the OpenAPI specification for this server.
|
|
40
|
-
responses:
|
|
41
|
-
200:
|
|
42
|
-
description: OpenAPI document
|
|
43
|
-
schema:
|
|
44
|
-
type: object
|
|
45
|
-
/mcp:
|
|
46
|
-
options:
|
|
47
|
-
summary: CORS preflight
|
|
48
|
-
description: CORS preflight endpoint.
|
|
49
|
-
responses:
|
|
50
|
-
204:
|
|
51
|
-
description: No Content
|
|
52
|
-
post:
|
|
53
|
-
summary: MCP request
|
|
54
|
-
description: Handles MCP JSON-RPC requests.
|
|
55
|
-
parameters:
|
|
56
|
-
- name: body
|
|
57
|
-
in: body
|
|
58
|
-
required: true
|
|
59
|
-
schema:
|
|
60
|
-
type: object
|
|
61
|
-
- name: Authorization
|
|
62
|
-
in: header
|
|
63
|
-
required: false
|
|
64
|
-
type: string
|
|
65
|
-
description: Bearer token for authentication
|
|
66
|
-
- name: X-VIYA-SERVER
|
|
67
|
-
in: header
|
|
68
|
-
required: false
|
|
69
|
-
type: string
|
|
70
|
-
description: Override VIYA server
|
|
71
|
-
- name: X-REFRESH-TOKEN
|
|
72
|
-
in: header
|
|
73
|
-
required: false
|
|
74
|
-
type: string
|
|
75
|
-
description: Refresh token for authentication
|
|
76
|
-
- name: mcp-session-id
|
|
77
|
-
in: header
|
|
78
|
-
required: false
|
|
79
|
-
type: string
|
|
80
|
-
description: Session ID
|
|
81
|
-
responses:
|
|
82
|
-
200:
|
|
83
|
-
description: MCP response
|
|
84
|
-
schema:
|
|
85
|
-
type: object
|
|
86
|
-
500:
|
|
87
|
-
description: Server error
|
|
88
|
-
schema:
|
|
89
|
-
type: object
|
|
90
|
-
get:
|
|
91
|
-
summary: Get MCP session
|
|
92
|
-
description: Retrieves information for an MCP session.
|
|
93
|
-
parameters:
|
|
94
|
-
- name: mcp-session-id
|
|
95
|
-
in: header
|
|
96
|
-
required: true
|
|
97
|
-
type: string
|
|
98
|
-
description: Session ID
|
|
99
|
-
responses:
|
|
100
|
-
200:
|
|
101
|
-
description: Session information
|
|
102
|
-
schema:
|
|
103
|
-
type: object
|
|
104
|
-
400:
|
|
105
|
-
description: Invalid or missing session ID
|
|
106
|
-
delete:
|
|
107
|
-
summary: Delete MCP session
|
|
108
|
-
description: Deletes an MCP session.
|
|
109
|
-
parameters:
|
|
110
|
-
- name: mcp-session-id
|
|
111
|
-
in: header
|
|
112
|
-
required: true
|
|
113
|
-
type: string
|
|
114
|
-
description: Session ID
|
|
115
|
-
responses:
|
|
116
|
-
200:
|
|
117
|
-
description: Session deleted
|
|
118
|
-
schema:
|
|
119
|
-
type: object
|
|
120
|
-
400:
|
|
121
|
-
description: Invalid or missing session ID
|
|
1
|
+
swagger: "2.0"
|
|
2
|
+
info:
|
|
3
|
+
title: SAS Viya Sample MCP Server API
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: API for interacting with the SAS Viya Sample MCP Server.
|
|
6
|
+
host: localhost:8080
|
|
7
|
+
basePath: /
|
|
8
|
+
schemes:
|
|
9
|
+
- http
|
|
10
|
+
- https
|
|
11
|
+
consumes:
|
|
12
|
+
- application/json
|
|
13
|
+
produces:
|
|
14
|
+
- application/json
|
|
15
|
+
paths:
|
|
16
|
+
/health:
|
|
17
|
+
get:
|
|
18
|
+
summary: Health check
|
|
19
|
+
description: Returns health and version information.
|
|
20
|
+
responses:
|
|
21
|
+
200:
|
|
22
|
+
description: Health information
|
|
23
|
+
schema:
|
|
24
|
+
type: object
|
|
25
|
+
properties:
|
|
26
|
+
name:
|
|
27
|
+
type: string
|
|
28
|
+
version:
|
|
29
|
+
type: string
|
|
30
|
+
description:
|
|
31
|
+
type: string
|
|
32
|
+
endpoints:
|
|
33
|
+
type: object
|
|
34
|
+
usage:
|
|
35
|
+
type: string
|
|
36
|
+
/apiMeta:
|
|
37
|
+
get:
|
|
38
|
+
summary: API metadata
|
|
39
|
+
description: Returns the OpenAPI specification for this server.
|
|
40
|
+
responses:
|
|
41
|
+
200:
|
|
42
|
+
description: OpenAPI document
|
|
43
|
+
schema:
|
|
44
|
+
type: object
|
|
45
|
+
/mcp:
|
|
46
|
+
options:
|
|
47
|
+
summary: CORS preflight
|
|
48
|
+
description: CORS preflight endpoint.
|
|
49
|
+
responses:
|
|
50
|
+
204:
|
|
51
|
+
description: No Content
|
|
52
|
+
post:
|
|
53
|
+
summary: MCP request
|
|
54
|
+
description: Handles MCP JSON-RPC requests.
|
|
55
|
+
parameters:
|
|
56
|
+
- name: body
|
|
57
|
+
in: body
|
|
58
|
+
required: true
|
|
59
|
+
schema:
|
|
60
|
+
type: object
|
|
61
|
+
- name: Authorization
|
|
62
|
+
in: header
|
|
63
|
+
required: false
|
|
64
|
+
type: string
|
|
65
|
+
description: Bearer token for authentication
|
|
66
|
+
- name: X-VIYA-SERVER
|
|
67
|
+
in: header
|
|
68
|
+
required: false
|
|
69
|
+
type: string
|
|
70
|
+
description: Override VIYA server
|
|
71
|
+
- name: X-REFRESH-TOKEN
|
|
72
|
+
in: header
|
|
73
|
+
required: false
|
|
74
|
+
type: string
|
|
75
|
+
description: Refresh token for authentication
|
|
76
|
+
- name: mcp-session-id
|
|
77
|
+
in: header
|
|
78
|
+
required: false
|
|
79
|
+
type: string
|
|
80
|
+
description: Session ID
|
|
81
|
+
responses:
|
|
82
|
+
200:
|
|
83
|
+
description: MCP response
|
|
84
|
+
schema:
|
|
85
|
+
type: object
|
|
86
|
+
500:
|
|
87
|
+
description: Server error
|
|
88
|
+
schema:
|
|
89
|
+
type: object
|
|
90
|
+
get:
|
|
91
|
+
summary: Get MCP session
|
|
92
|
+
description: Retrieves information for an MCP session.
|
|
93
|
+
parameters:
|
|
94
|
+
- name: mcp-session-id
|
|
95
|
+
in: header
|
|
96
|
+
required: true
|
|
97
|
+
type: string
|
|
98
|
+
description: Session ID
|
|
99
|
+
responses:
|
|
100
|
+
200:
|
|
101
|
+
description: Session information
|
|
102
|
+
schema:
|
|
103
|
+
type: object
|
|
104
|
+
400:
|
|
105
|
+
description: Invalid or missing session ID
|
|
106
|
+
delete:
|
|
107
|
+
summary: Delete MCP session
|
|
108
|
+
description: Deletes an MCP session.
|
|
109
|
+
parameters:
|
|
110
|
+
- name: mcp-session-id
|
|
111
|
+
in: header
|
|
112
|
+
required: true
|
|
113
|
+
type: string
|
|
114
|
+
description: Session ID
|
|
115
|
+
responses:
|
|
116
|
+
200:
|
|
117
|
+
description: Session deleted
|
|
118
|
+
schema:
|
|
119
|
+
type: object
|
|
120
|
+
400:
|
|
121
|
+
description: Invalid or missing session ID
|
package/src/processHeaders.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { start } from "node:repl";
|
|
2
|
-
|
|
3
1
|
/*
|
|
4
2
|
* Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
5
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
@@ -32,20 +30,25 @@ function processHeaders(req, res, next, cache, appContext) {
|
|
|
32
30
|
const hdr = req.header("Authorization");
|
|
33
31
|
//for now, ignore Authorization if authflow is not bearer
|
|
34
32
|
let token = (hdr != null) ? hdr.slice(7) : null;
|
|
35
|
-
//console.error("[Note] Authorization token", token);
|
|
36
33
|
debugger;
|
|
37
|
-
console.error('
|
|
34
|
+
console.error('[Note} AUTHFLOW=', appContext.AUTHFLOW);
|
|
35
|
+
console.error("[Note] External authorization :", appContext.AUTHEXTERNAL);
|
|
38
36
|
if (appContext.AUTHFLOW === 'bearer') {
|
|
39
37
|
debugger;
|
|
40
38
|
let startAuth = false;
|
|
41
|
-
|
|
39
|
+
|
|
42
40
|
if (appContext.AUTHEXTERNAL === true) {
|
|
43
41
|
console.error("[Note] Expecting external authorization");
|
|
44
42
|
if (token != null) {
|
|
45
43
|
console.error("[Note] Using user supplied token for authorization");
|
|
46
44
|
headerCache.bearerToken = token;
|
|
47
45
|
} else {
|
|
48
|
-
|
|
46
|
+
console.error("[Note] No Authorization token provided in header for external authorization.");
|
|
47
|
+
console.error("[Note] Returning 404 since we are configured for external token and no token provided in header.");
|
|
48
|
+
return res.status(404).json({
|
|
49
|
+
error: "unauthorized",
|
|
50
|
+
error_description: "[Error] Missing token for external authorization."
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
} else if (token == null) {
|
|
51
54
|
console.error("[Note] No Authorization token provided in header.");
|
|
@@ -55,7 +58,7 @@ function processHeaders(req, res, next, cache, appContext) {
|
|
|
55
58
|
let tokenlist = cache.get("tokenlist");
|
|
56
59
|
let tokenData = tokenlist[token];
|
|
57
60
|
if (tokenData == null) {
|
|
58
|
-
return res.status(
|
|
61
|
+
return res.status(401).json({
|
|
59
62
|
error: "unauthorized",
|
|
60
63
|
error_description: "[Error] Expired token. Clear token and try again."
|
|
61
64
|
});
|
package/src/setupSkills.js
CHANGED
|
@@ -19,27 +19,10 @@ function setupSkills(clientName,agentFolder) {
|
|
|
19
19
|
if (agentFolder) {
|
|
20
20
|
destination = path.join(destination, agentFolder);
|
|
21
21
|
}
|
|
22
|
-
const source = path.join(__dirname, `../.skills`
|
|
22
|
+
const source = path.join(__dirname, `../.skills`);
|
|
23
23
|
console.error("==================================================================");
|
|
24
24
|
console.error(` Copying ${source} to ${destination}...`);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Copy agents folder if it exists
|
|
29
|
-
let agentsFromPath = path.join(__dirname, `../.agents`);
|
|
30
|
-
let agentsToPath = path.join(destination, 'agents');
|
|
31
|
-
copyFolderSync(agentsFromPath, agentsToPath);
|
|
32
|
-
|
|
33
|
-
// now copy the skills folder to the destination
|
|
34
|
-
let toPath = path.join(destination, '.skills');
|
|
35
|
-
let fromPath = path.join(__dirname, `../.skills`);
|
|
36
|
-
copyFolderSync(fromPath, toPath);
|
|
37
|
-
|
|
38
|
-
// Now copy instructions
|
|
39
|
-
let instructionsFromPath = path.join(__dirname, `../.instructions`);
|
|
40
|
-
let instructionsToPath = destination;
|
|
41
|
-
copyFolderSync(instructionsFromPath, instructionsToPath);
|
|
42
|
-
|
|
43
26
|
function copyFolderSync(from, to) {
|
|
44
27
|
if (!fs.existsSync(from)) return [];
|
|
45
28
|
if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });;
|