@sassoftware/sas-score-mcp-serverjs 0.4.1-7 → 1.0.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.
Files changed (46) hide show
  1. package/.skills/agents/sas-viya-scoring-expert.md +58 -0
  2. package/.skills/copilot-instructions.md +147 -0
  3. package/.skills/skills/sas-find-library-smart/SKILL.md +154 -0
  4. package/.skills/skills/sas-list-tables-smart/SKILL.md +127 -0
  5. package/.skills/skills/sas-read-and-score/SKILL.md +111 -0
  6. package/{skills → .skills/skills}/sas-read-strategy/SKILL.md +43 -30
  7. package/.skills/skills/sas-request-classifier/SKILL.md +69 -0
  8. package/{skills → .skills/skills}/sas-score-workflow/SKILL.md +49 -35
  9. package/cli.js +222 -140
  10. package/package.json +5 -4
  11. package/scripts/docs/SCORE_SKILL_REFERENCE.md +142 -0
  12. package/scripts/docs/TOOL_DESCRIPTION_TEMPLATE.md +157 -0
  13. package/scripts/docs/TOOL_UPDATES_SUMMARY.md +208 -0
  14. package/scripts/docs/mcp-localhost-config-guide.md +184 -0
  15. package/scripts/docs/oauth-http-transport.md +96 -0
  16. package/scripts/docs/sas-mcp-tools-reference.md +600 -0
  17. package/scripts/getViyaca.sh +1 -0
  18. package/scripts/optimize_final.py +140 -0
  19. package/scripts/optimize_tools.py +99 -0
  20. package/scripts/setup-skills.js +34 -0
  21. package/scripts/update_descriptions.py +46 -0
  22. package/scripts/viyatls.sh +3 -0
  23. package/src/authpkce.js +219 -220
  24. package/src/createMcpServer.js +10 -5
  25. package/src/expressMcpServer.js +54 -186
  26. package/src/oauthHandlers/authorize.js +46 -0
  27. package/src/oauthHandlers/baseUrl.js +8 -0
  28. package/src/oauthHandlers/callback.js +96 -0
  29. package/src/oauthHandlers/getMetadata.js +27 -0
  30. package/src/oauthHandlers/index.js +7 -0
  31. package/src/oauthHandlers/token.js +37 -0
  32. package/src/processHeaders.js +88 -0
  33. package/src/setupSkills.js +37 -0
  34. package/src/toolHelpers/_listLibrary.js +0 -1
  35. package/src/toolHelpers/getLogonPayload.js +5 -1
  36. package/src/toolHelpers/refreshToken.js +3 -2
  37. package/src/toolHelpers/refreshTokenOauth.js +3 -3
  38. package/src/toolSet/.claude/settings.local.json +2 -1
  39. package/src/toolSet/findModel.js +1 -1
  40. package/src/toolSet/findTable.js +3 -3
  41. package/src/toolSet/modelScore.js +2 -2
  42. package/src/toolSet/runJob.js +81 -81
  43. package/src/toolSet/runJobdef.js +82 -82
  44. package/skills/mcp-tool-description-optimizer/SKILL.md +0 -129
  45. package/skills/mcp-tool-description-optimizer/references/examples.md +0 -123
  46. package/skills/sas-read-and-score/SKILL.md +0 -91
@@ -11,21 +11,22 @@ 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, randomBytes, createHash } from "node:crypto";
14
+ import { randomUUID } from "node:crypto";
15
15
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16
- import { Agent, fetch as undiciFetch } from "undici";
16
+
17
17
  import tlogon from "./toolHelpers/tlogon.js";
18
18
 
19
+ import { getMetadata, authorize, callback, token, baseUrl } from "./oauthHandlers/index.js";
20
+ import processHeaders from "./processHeaders.js";
19
21
 
20
22
  // setup express server
21
23
 
22
24
  async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
23
25
  // setup for change to persistence session
24
- cache.set("headerCache", {});
25
-
26
+ cache.del("headerCache");
26
27
  const app = express();
27
28
  let appStatus = false;
28
-
29
+ app.use(express.urlencoded({ extended: true })); // MUST be before your routes
29
30
  app.use(express.json({ limit: "50mb" }));
30
31
  app.use(
31
32
  cors({
@@ -49,150 +50,69 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
49
50
  const pkceStore = new Map(); // ourState -> { codeVerifier, clientRedirectUri, clientState }
50
51
  const codeStore = new Map(); // ourCode -> { access_token, refresh_token, expires_in }
51
52
 
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
- }
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
+ });
58
63
 
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
- };
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);
69
68
  return res.status(200).json(metadata);
70
69
  });
71
70
 
72
71
  // 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}`);
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);
102
79
  });
103
80
 
104
- // OAuth callback — receives code from SAS Viya, exchanges for tokens, redirects to MCP client
81
+ // OAuth callback — receives code from SAS Viya, exchanges for tokens
105
82
  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
- }
83
+ console.error("[Note] Received request for /callback with query parameters:");
84
+ return await callback(req, res, pkceStore, codeStore, baseAppEnvContext);
170
85
  });
171
86
 
172
87
  // 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;
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
+ });
175
92
 
176
- if (grant_type !== "authorization_code") {
177
- return res.status(400).json({ error: "unsupported_grant_type" });
178
- }
93
+ app.post("/token", (req, res) => {
94
+ console.error("[Note] Received request for /token");
95
+ return token(req, res, baseAppEnvContext, codeStore, cache);
96
+ });
97
+ // setup routes
179
98
 
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 }),
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",
192
112
  });
193
113
  });
194
114
 
195
- // setup routes
115
+ // health endpoint
196
116
  app.get("/health", (req, res) => {
197
117
  console.error("Received request for health endpoint");
198
118
  if (appStatus === false) {
@@ -213,22 +133,6 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
213
133
  res.json(health);
214
134
  });
215
135
 
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
- });
230
- });
231
-
232
136
  // api metadata endpoint(for sas specs)
233
137
  app.get("/apiMeta", (req, res) => {
234
138
  let spec = openAPIJson(baseAppEnvContext.version);
@@ -243,41 +147,7 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
243
147
 
244
148
  // handle processing of information in header.
245
149
  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("=======================================================");
150
+ return processHeaders(req, res, next, cache, baseAppEnvContext);
281
151
  }
282
152
 
283
153
  // process mcp endpoint requests
@@ -357,9 +227,7 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
357
227
  cache.set(sessionId, _appContext);
358
228
  } else {
359
229
  let headerCache = cache.get("headerCache");
360
- console.error('compare tokens', headerCache.bearerToken === _appContext.bearerToken);
361
230
  _appContext = Object.assign(_appContext, headerCache);
362
- console.error('New bearerToken:', _appContext.bearerToken);
363
231
  cache.set(sessionId, _appContext);
364
232
  }
365
233
  console.error("[Note] Using existing transport for session ID:", sessionId);
@@ -0,0 +1,46 @@
1
+ //authorize
2
+ import { randomBytes, createHash } from "node:crypto";
3
+ import baseUrl from "./baseUrl.js";
4
+
5
+ function authorize(req, res, appContext, pkceStore, codeStore) {
6
+ const { response_type, redirect_uri, state, scope } = req.query;
7
+ console.error("===============================================================");
8
+ if (appContext.AUTHEXTERNAL === true) {
9
+ console.error('*************************************************************');
10
+ console.error("[Error] Received request for /authorize endpoint with external authorization expected");
11
+ console.error('*************************************************************');
12
+ }
13
+ console.error("[NOTE] query parameters:", { response_type, redirect_uri, state, scope });
14
+ if (response_type !== "code") {
15
+ return res.status(400).json({ error: "unsupported_response_type" });
16
+ }
17
+ if (!redirect_uri) {
18
+ return res.status(400).json({ error: "invalid_request", error_description: "redirect_uri is required" });
19
+ }
20
+
21
+ const codeVerifier = randomBytes(64).toString("base64url");
22
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
23
+ const ourState = randomBytes(16).toString("hex");
24
+
25
+ pkceStore.set(ourState, { codeVerifier, clientRedirectUri: redirect_uri, clientState: state, codeChallenge });
26
+
27
+ const callbackUri = appContext.mcpHost + '/callback';
28
+ console.error("[Note] callbackUri:", callbackUri);
29
+ let urlConfig = {
30
+ response_type: "code",
31
+ client_id: appContext.CLIENTID,
32
+ redirect_uri: callbackUri,
33
+ scope: "openid",
34
+ state: ourState,
35
+ code_challenge: codeChallenge,
36
+ code_challenge_method: "S256"
37
+ }
38
+ if (appContext.PKCE !== "TRUE") {
39
+ urlConfig.client_secret = appContext.CLIENTSECRET;
40
+ }
41
+ console.error("[Note] Params for SAS Viya authorization request:", urlConfig);
42
+ const params = new URLSearchParams(urlConfig);
43
+ console.error("================================================================");
44
+ return res.redirect(`${appContext.VIYA_SERVER}/SASLogon/oauth/authorize?${params}`);
45
+ }
46
+ export default authorize;
@@ -0,0 +1,8 @@
1
+ function baseUrl(appContext) {
2
+ const protocol = appContext.HTTPS === "TRUE" ? "https" : "http";
3
+ //const host = "localhost";
4
+ const host = appContext.VIYA_SERVER;
5
+ return host;
6
+ return `${protocol}://${host}:${appContext.PORT}`;
7
+ }
8
+ export default baseUrl;
@@ -0,0 +1,96 @@
1
+ import baseUrl from "./baseUrl.js";
2
+ import { Agent, fetch as undiciFetch } from "undici";
3
+ import { randomUUID } from "node:crypto";
4
+ async function callback(req, res, pkceStore, codeStore, appContext) {
5
+ console.error("===============================================================");
6
+ console.error("[Note] OAuth callback");
7
+ console.error(req.query);
8
+ const {code, state} = req.query;
9
+
10
+ console.error("[Note] query parameters:", code, state);
11
+ if (!code) {
12
+ return res.status(400).send("Missing code parameter");
13
+ }
14
+
15
+ const pending = pkceStore.get(state);
16
+ if (!pending) {
17
+ return res.status(400).send("Invalid or expired state parameter");
18
+ }
19
+ pkceStore.delete(state);
20
+
21
+ // const callbackUri = `${baseUrl(appContext)}/callback`;
22
+ const callbackUri = appContext.mcpHost + '/callback';
23
+ console.error("[Note] callbackUri for token exchange:", callbackUri);
24
+ try {
25
+ const body = new URLSearchParams({
26
+ grant_type: "authorization_code",
27
+ client_id: appContext.CLIENTID,
28
+ code: code,
29
+ redirect_uri: callbackUri,
30
+ code_verifier: pending.codeVerifier
31
+ }).toString();
32
+
33
+ console.error("[Note] Token request body:", body.toString());
34
+
35
+ let connectOpts = {
36
+ rejectUnauthorized: false // or false, if you really want to bypass checks
37
+ }
38
+ if (appContext.contexts.viyaCert != null && appContext.contexts.viyaCert.ca != null) {
39
+ connectOpts.ca = appContext.contexts.viyaCert.ca;
40
+ }
41
+ const agent = new Agent({ connect: connectOpts });
42
+ console.error("[Note] Initiating token exchange with SAS Viya");
43
+ let clientID= appContext.CLIENTID;
44
+ let h = buildBasicAuthFromClientId(clientID);
45
+ // console.error("[Note] Authorization header for token request:", h);
46
+ const response = await undiciFetch(`${appContext.VIYA_SERVER}/SASLogon/oauth/token`, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/x-www-form-urlencoded",
50
+ "Accept": "application/json"
51
+ },
52
+ body: body,
53
+ dispatcher: agent,
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const errText = await response.text();
58
+ console.error("[Error] SAS Viya token exchange failed:", errText);
59
+ return res.status(502).send("Token exchange with SAS Viya failed");
60
+ }
61
+
62
+ const tokens = await response.json();
63
+ // console.error("[Note] Received tokens from SAS Viya:", tokens);
64
+ const ourCode = randomUUID();
65
+ codeStore.set(ourCode, {
66
+ access_token: tokens.access_token,
67
+ refresh_token: tokens.refresh_token,
68
+ expires_in: tokens.expires_in,
69
+ });
70
+
71
+ const redirectParams = new URLSearchParams({ code: ourCode , state: pending.clientState});
72
+ //pending.clientState is the original state from the MCP client, which we should pass back to it for correlation
73
+
74
+ // pending.clientRedirectUri is the original redirect URI from the MCP client,
75
+ // which was part of the payload from the client to /oauth/authorize
76
+ // we trust since it was associated with the valid PKCE state
77
+ console.error("[Note] OAuth callback complete, redirecting to MCP client");
78
+ console.log(pending.clientRedirectUri.toString())
79
+ return res.redirect(`${pending.clientRedirectUri}?${redirectParams}`);
80
+ } catch (err) {
81
+ console.error("[Error] OAuth callback handler error:", err);
82
+ return res.status(500).send("Internal server error during token exchange");
83
+ }
84
+
85
+ function buildBasicAuthFromClientId(clientId) {
86
+ const raw = `${clientId}:`; // empty client_secret
87
+ const encoded =
88
+ typeof btoa === "function"
89
+ ? btoa(raw)
90
+ : Buffer.from(raw).toString("base64");
91
+
92
+ return `Basic ${encoded}`;
93
+ }
94
+
95
+ }
96
+ export default callback;
@@ -0,0 +1,27 @@
1
+ import baseUrl from "./baseUrl.js";
2
+ function getMetadata(req, res, appEnvContext) {
3
+ let base = '';
4
+ let host = '';
5
+ if (appEnvContext.AUTHEXTERNAL === true) {
6
+ base = appEnvContext.VIYA_SERVER + '/SASLogon';
7
+ host = appEnvContext.VIYA_SERVER;
8
+ } else {
9
+ base = appEnvContext.mcpHost;
10
+ host = appEnvContext.mcpHost;
11
+ }
12
+ console.error(`[Note] Base URL for Authorization Server Metadata: ${base}`);
13
+ let metadata = {
14
+ "issuer": host,
15
+ "authorization_endpoint": `${base}/oauth/authorize`,
16
+ "token_endpoint": `${base}/oauth/token`,
17
+ "response_types_supported": ["code"],
18
+ "grant_types_supported": ["authorization_code", "refresh_token"],
19
+ "code_challenge_methods_supported": ["S256"],
20
+ "scopes_supported": ["openid"]
21
+ };
22
+ console.error("===============================================================");
23
+ console.error("[Note] Authorization Server Metadata:", metadata);
24
+ console.error("===============================================================");
25
+ return metadata;
26
+ };
27
+ export default getMetadata;
@@ -0,0 +1,7 @@
1
+ import authorize from "./authorize.js";
2
+ import callback from "./callback.js";
3
+ import token from "./token.js";
4
+ import getMetadata from "./getMetadata.js";
5
+ import baseUrl from "./baseUrl.js";
6
+
7
+ export { authorize, callback, token, getMetadata, baseUrl };
@@ -0,0 +1,37 @@
1
+ function token(req, res, appContext, codeStore, cache) {
2
+ console.error("===============================================================");
3
+ console.error("[Note] at /token endpoint");
4
+ console.error("[Note] Request headers:", req.headers);
5
+ console.error("[Note] Handling token request with body:", req.body);
6
+
7
+ const { grant_type, code } = req.body;
8
+ console.error("[Note] OAuth token request received for code:", code);
9
+ console.error("[Note] Grant type:", grant_type);
10
+
11
+ const tokenData = codeStore.get(code);
12
+ if (!tokenData) {
13
+ return res.status(400).json({ error: "Invalid_code", error_description: "Invalid or expired authorization code" });
14
+ }
15
+ console.error("[Note] Retrieved token data for code:", Object.keys(tokenData));
16
+
17
+ let payload = {
18
+ access_token: code, // tokenData.access_token,
19
+ token_type: "Bearer",
20
+ expires_in: tokenData.expires_in,
21
+ scope: "openid" // You can include scopes if you have them, e.g., tokenData.scope
22
+ // scope: tokenData.scope,
23
+ };
24
+ let tokenlist = cache.get("tokenlist");
25
+ tokenData.refreshAt = Date.now() + tokenData.expires_in * 1000; // Set refresh time based on expires_in
26
+ tokenlist[code] = tokenData;
27
+ cache.set("tokenlist", tokenlist);
28
+
29
+ codeStore.delete(code); // Invalidate the code after use
30
+ console.error("[Note] Returning token to client");
31
+ console.error("[Note] Token sent to client:", JSON.stringify(payload, null, 2));
32
+
33
+ console.error("============================================================")
34
+ return res.status(200).json(payload);
35
+ }
36
+
37
+ export default token;
@@ -0,0 +1,88 @@
1
+ import { start } from "node:repl";
2
+
3
+ /*
4
+ * Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
5
+ * SPDX-License-Identifier: Apache-2.0
6
+ */
7
+ import baseUrl from "./oauthHandlers/baseUrl.js";
8
+ function processHeaders(req, res, next, cache, appContext) {
9
+
10
+ // process any new header information
11
+ debugger;
12
+ console.error("=======================================================");
13
+ console.error("Processing headers for incoming request to /mcp endpoint");
14
+ // Allow different VIYA server per sessionid(user)
15
+ cache.del("headerCache");
16
+ let headerCache = {};
17
+ if (req.header("X-VIYA-SERVER") != null) {
18
+ console.error("[Note] Using user supplied VIYA server");
19
+ headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
20
+ }
21
+
22
+ // fakeapi key since Viya does not support it
23
+ // not ideal for production, just for testing
24
+ const hdr2 = req.header("X-REFRESH-TOKEN");
25
+ if (hdr2 != null) {
26
+ headerCache.REFRESH_TOKEN = hdr2;
27
+ headerCache.AUTHFLOW = "refresh";
28
+ console.error("[Note] Using user supplied refresh token for authorization");
29
+ }
30
+
31
+ // Oauth flow
32
+ const hdr = req.header("Authorization");
33
+ //for now, ignore Authorization if authflow is not bearer
34
+ let token = (hdr != null) ? hdr.slice(7) : null;
35
+ //console.error("[Note] Authorization token", token);
36
+ debugger;
37
+ console.log('>>>',appContext.AUTHFLOW);
38
+ if (appContext.AUTHFLOW === 'bearer') {
39
+ debugger;
40
+ let startAuth = false;
41
+ console.error("[Note] appContext.AUTHEXTERNAL:", appContext.AUTHEXTERNAL);
42
+ if (appContext.AUTHEXTERNAL === true) {
43
+ console.error("[Note] Expecting external authorization");
44
+ if (token != null) {
45
+ console.error("[Note] Using user supplied token for authorization");
46
+ headerCache.bearerToken = token;
47
+ } else {
48
+ startAuth = true;
49
+ }
50
+ } else if (token == null) {
51
+ console.error("[Note] No Authorization token provided in header.");
52
+ startAuth = true;
53
+ } else {
54
+ console.error("[Note] Checking token against cache", token);
55
+ let tokenlist = cache.get("tokenlist");
56
+ let tokenData = tokenlist[token];
57
+ if (tokenData == null) {
58
+ return res.status(403).json({
59
+ error: "unauthorized",
60
+ error_description: "[Error] Expired token. Clear token and try again."
61
+ });
62
+ } else {
63
+ token = tokenData.access_token;
64
+ headerCache.bearerToken = token;
65
+ }
66
+ }
67
+ if (startAuth === true) {
68
+ // start oauth flow process
69
+ console.error(`[Note] No Authorization header provided.
70
+ Returning 401 to start the OAuth flow`);
71
+ const base = appContext.mcpHost;
72
+ res.set(
73
+ "WWW-Authenticate",
74
+ `Bearer resource_metadata="${base}/.well-known/oauth-authorization-server", scope="openid"`
75
+ );
76
+ return res.status(401).json({
77
+ error: "unauthorized",
78
+ error_description: "Bearer token required."
79
+ });
80
+ // start auth flow process since no token provided in header and we are not configured for external token
81
+ }
82
+ }
83
+ // console.error("Header cache after processing:", headerCache);
84
+ cache.set("headerCache", headerCache);
85
+ next();
86
+ }
87
+
88
+ export default processHeaders;
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ function setupSkills(clientName) {
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ // Paths
10
+ let client = (clientName == null) ? '.github' : `.${clientName.toLowerCase()}`;
11
+ const source = path.join(__dirname, `../.skills`);
12
+ const destination = path.join(os.homedir(), client);
13
+ console.error(`📁 Copying ${source} to ${destination}...`);
14
+ function copyFolderSync(from, to) {
15
+ if (!fs.existsSync(from)) return;
16
+ if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });
17
+ console.error(`📁 Copying folder: ${from} to ${to}`);
18
+ fs.readdirSync(from).forEach(element => {
19
+ console.error(`📁 Processing: ${element}`);
20
+ const fromPath = path.join(from, element);
21
+ const toPath = path.join(to, element);
22
+ if (fs.lstatSync(fromPath).isFile()) {
23
+ fs.copyFileSync(fromPath, toPath);
24
+ } else if (fs.lstatSync(fromPath).isDirectory()) {
25
+ copyFolderSync(fromPath, toPath);
26
+ }
27
+ });
28
+ }
29
+
30
+ try {
31
+ copyFolderSync(source, destination);
32
+ console.error(`✅ Success: ${destination} folder is now in your project root.`);
33
+ } catch (err) {
34
+ console.error('❌ Error copying files:', err.message);
35
+ }
36
+ }
37
+ export default setupSkills;
@@ -12,7 +12,6 @@ async function _listLibrary(params) {
12
12
 
13
13
  const _ilistLibrary = async (params) => {
14
14
  let { server, limit, start, name, _appContext } = params;
15
- console.error(_appContext);
16
15
  let config = {
17
16
  casServerName: _appContext.cas,
18
17
  computeContext: _appContext.sas,