@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.
Files changed (72) hide show
  1. package/.skills/agents/sas-viya-scoring-expert.md +58 -0
  2. package/.skills/copilot-instructions.md +155 -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/sas-read-strategy/SKILL.md +156 -0
  7. package/.skills/skills/sas-request-classifier/SKILL.md +69 -0
  8. package/.skills/skills/sas-score-workflow/SKILL.md +314 -0
  9. package/cli.js +311 -70
  10. package/package.json +7 -7
  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 -0
  24. package/src/createMcpServer.js +16 -5
  25. package/src/expressMcpServer.js +350 -308
  26. package/src/handleGetDelete.js +6 -3
  27. package/src/hapiMcpServer.js +10 -18
  28. package/src/oauthHandlers/authorize.js +46 -0
  29. package/src/oauthHandlers/baseUrl.js +8 -0
  30. package/src/oauthHandlers/callback.js +96 -0
  31. package/src/oauthHandlers/getMetadata.js +27 -0
  32. package/src/oauthHandlers/index.js +7 -0
  33. package/src/oauthHandlers/token.js +37 -0
  34. package/src/processHeaders.js +88 -0
  35. package/src/setupSkills.js +46 -0
  36. package/src/toolHelpers/_jobSubmit.js +2 -0
  37. package/src/toolHelpers/_listLibrary.js +55 -39
  38. package/src/toolHelpers/getLogonPayload.js +7 -1
  39. package/src/toolHelpers/readCerts.js +4 -4
  40. package/src/toolHelpers/refreshToken.js +3 -2
  41. package/src/toolHelpers/refreshTokenOauth.js +3 -3
  42. package/src/toolSet/.claude/settings.local.json +13 -0
  43. package/src/toolSet/devaScore.js +61 -69
  44. package/src/toolSet/findJob.js +38 -71
  45. package/src/toolSet/findJobdef.js +28 -59
  46. package/src/toolSet/findLibrary.js +68 -100
  47. package/src/toolSet/findModel.js +35 -58
  48. package/src/toolSet/findTable.js +31 -60
  49. package/src/toolSet/getEnv.js +30 -45
  50. package/src/toolSet/listJobdefs.js +61 -96
  51. package/src/toolSet/listJobs.js +61 -110
  52. package/src/toolSet/listLibraries.js +78 -90
  53. package/src/toolSet/listModels.js +56 -83
  54. package/src/toolSet/listTables.js +66 -95
  55. package/src/toolSet/makeTools.js +1 -0
  56. package/src/toolSet/modelInfo.js +22 -54
  57. package/src/toolSet/modelScore.js +35 -77
  58. package/src/toolSet/readTable.js +63 -104
  59. package/src/toolSet/runCasProgram.js +32 -52
  60. package/src/toolSet/runJob.js +24 -24
  61. package/src/toolSet/runJobdef.js +26 -29
  62. package/src/toolSet/runMacro.js +82 -82
  63. package/src/toolSet/runProgram.js +32 -84
  64. package/src/toolSet/sasQuery.js +77 -126
  65. package/src/toolSet/sasQueryTemplate.js +4 -5
  66. package/src/toolSet/sasQueryTemplate2.js +4 -5
  67. package/src/toolSet/scrInfo.js +4 -7
  68. package/src/toolSet/scrScore.js +69 -70
  69. package/src/toolSet/searchAssets.js +5 -6
  70. package/src/toolSet/setContext.js +65 -92
  71. package/src/toolSet/superstat.js +61 -60
  72. package/src/toolSet/tableInfo.js +58 -102
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env python3
2
+ import re
3
+
4
+ # Read and replace listJobs
5
+ with open('src/toolSet/listJobs.js', 'r') as f:
6
+ content = f.read()
7
+
8
+ # Find and replace listJobs description - find the full description block
9
+ pattern = r'let description = `\n ## list-jobs.*? `;'
10
+ replacement = r'''let description = `
11
+ list-jobs — enumerate SAS Viya job assets.
12
+
13
+ USE when: list jobs, show jobs, browse jobs, list available jobs, next page of jobs
14
+ DO NOT USE for: find single job (use find-job), execute job (use run-job), run jobdef (use run-jobdef), sas code (use run-sas-program)
15
+
16
+ PARAMETERS
17
+ - limit: number (default: 10) — number of jobs per page
18
+ - start: number (default: 1) — 1-based page offset
19
+ - where: string (default: '') — optional filter expression
20
+
21
+ ROUTING RULES
22
+ - "list jobs" → { start: 1, limit: 10 }
23
+ - "show me 25 jobs" → { start: 1, limit: 25 }
24
+ - "list jobs limit 50" → { start: 1, limit: 50 }
25
+ - "next jobs" (after prior page) → { start: previousStart + previousLimit, limit: previousLimit }
26
+
27
+ EXAMPLES
28
+ - "list jobs" → { start: 1, limit: 10 }
29
+ - "list 25 jobs" → { start: 1, limit: 25 }
30
+ - "next jobs" → { start: 11, limit: 10 }
31
+
32
+ NEGATIVE EXAMPLES (do not route here)
33
+ - "find job abc" (use find-job)
34
+ - "run job abc" (use run-job)
35
+ - "list models" (use list-models)
36
+
37
+ PAGINATION
38
+ If returned length === limit, hint: next start = start + limit. Empty result with start > 1 means paged past end.
39
+
40
+ ERRORS
41
+ Surface backend error directly; never fabricate job names.
42
+ `;'''
43
+
44
+ if re.search(pattern, content, re.DOTALL):
45
+ content = re.sub(pattern, replacement, content, flags=re.DOTALL)
46
+ with open('src/toolSet/listJobs.js', 'w') as f:
47
+ f.write(content)
48
+ print("✓ Updated listJobs.js")
49
+ else:
50
+ print("✗ Pattern not found in listJobs.js")
51
+
52
+ # Read and replace listJobdefs
53
+ with open('src/toolSet/listJobdefs.js', 'r') as f:
54
+ content = f.read()
55
+
56
+ pattern = r'let description = `\n ## list-jobdefs.*? `;'
57
+ replacement = r'''let description = `
58
+ list-jobdefs — enumerate SAS Viya job definitions (jobdefs) assets.
59
+
60
+ USE when: list jobdefs, show jobdefs, browse jobdefs, list available jobdefs, next page
61
+ DO NOT USE for: find single jobdef (use find-jobdef), execute jobdef (use run-jobdef), find job (use find-job), sas code (use run-sas-program)
62
+
63
+ PARAMETERS
64
+ - limit: number (default: 10) — number of jobdefs per page
65
+ - start: number (default: 1) — 1-based page offset
66
+ - where: string (default: '') — optional filter expression
67
+
68
+ ROUTING RULES
69
+ - "list jobdefs" → { start: 1, limit: 10 }
70
+ - "show me 25 jobdefs" → { start: 1, limit: 25 }
71
+ - "next jobdefs" → { start: previousStart + previousLimit, limit: previousLimit }
72
+
73
+ EXAMPLES
74
+ - "list jobdefs" → { start: 1, limit: 10 }
75
+ - "list 25 jobdefs" → { start: 1, limit: 25 }
76
+ - "next jobdefs" → { start: 11, limit: 10 }
77
+
78
+ NEGATIVE EXAMPLES (do not route here)
79
+ - "find jobdef abc" (use find-jobdef)
80
+ - "list jobs" (use list-jobs)
81
+ - "run jobdef abc" (use run-jobdef)
82
+
83
+ PAGINATION
84
+ If returned length === limit, hint: next start = start + limit.
85
+
86
+ ERRORS
87
+ Surface backend error directly; never fabricate jobdef names.
88
+ `;'''
89
+
90
+ if re.search(pattern, content, re.DOTALL):
91
+ content = re.sub(pattern, replacement, content, flags=re.DOTALL)
92
+ with open('src/toolSet/listJobdefs.js', 'w') as f:
93
+ f.write(content)
94
+ print("✓ Updated listJobdefs.js")
95
+ else:
96
+ print("✗ Pattern not found in listJobdefs.js")
97
+
98
+ # Read and replace runCasProgram
99
+ with open('src/toolSet/runCasProgram.js', 'r') as f:
100
+ content = f.read()
101
+
102
+ pattern = r'let description = `\n## run-cas-program.*?Response\n`'
103
+ replacement = r'''let description = `
104
+ run-cas-program — execute a CAS program on SAS Viya server.
105
+
106
+ USE when: run cas program, execute cas, submit cas, run cas code, cas action
107
+ DO NOT USE for: macros (use run-macro), sas code (use run-sas-program), jobs (use run-job/find-job), jobdefs (use run-jobdef/find-jobdef), models (use find-model)
108
+
109
+ PARAMETERS
110
+ - src: string (required) — CAS program code to execute verbatim
111
+ - scenario: string | object (optional) — input parameters. Accepts: "x=1, y=2" or {x:1, y:2}
112
+
113
+ ROUTING RULES
114
+ - "run cas program 'action echo / code=\"xyz\"'" → { src: "action echo / code=\"xyz\"" }
115
+ - "submit cas action echo" → { src: "action echo" }
116
+ - "cas program with param1=10" → { src: "...", scenario: {param1: 10} }
117
+
118
+ EXAMPLES
119
+ - "run cas program 'action echo / code=\"hello\"'" → { src: "action echo / code=\"hello\"" }
120
+ - "execute cas action simple.summary" → { src: "action simple.summary" }
121
+
122
+ NEGATIVE EXAMPLES (do not route here)
123
+ - "run sas macro" (use run-macro)
124
+ - "submit sas code" (use run-sas-program)
125
+ - "run job X" (use run-job)
126
+
127
+ NOTES
128
+ Sends src verbatim without validation. For SAS macros use run-macro. For arbitrary SAS code use run-sas-program.
129
+
130
+ RESPONSE
131
+ Log output, listings, tables from CAS execution. Error if execution fails.
132
+ `'''
133
+
134
+ if re.search(pattern, content, re.DOTALL):
135
+ content = re.sub(pattern, replacement, content, flags=re.DOTALL)
136
+ with open('src/toolSet/runCasProgram.js', 'w') as f:
137
+ f.write(content)
138
+ print("✓ Updated runCasProgram.js")
139
+ else:
140
+ print("✗ Pattern not found in runCasProgram.js")
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ import re
3
+
4
+ # DevaScore optimization
5
+ deva_old = ''' let description = `
6
+ ## deva-score — compute a numeric score based on two input values
7
+
8
+ LLM Invocation Guidance (When to use)
9
+ Use THIS tool when:
10
+ - User wants to calculate the deva score: "Calculate deva score for 5 and 10"
11
+ - User provides two numbers for scoring: "Score these values: 3 and 7"
12
+ - User wants to compute a score in a series: "Calculate scores for [list of numbers]"
13
+
14
+ Do NOT use this tool for:
15
+ - Scoring models (use model-score)
16
+ - Statistical calculations beyond deva scoring
17
+ - Looking up data or metadata
18
+
19
+ Purpose
20
+ Compute a numeric deva score by applying the formula (a + b) * 42 to two input numbers. For scoring more than two numbers, call this tool multiple times using the previous result as the first input (left-to-right fold).
21
+
22
+ Parameters
23
+ - a (number, required): First numeric input value
24
+ - b (number, required): Second numeric input value
25
+
26
+ Response Contract
27
+ Returns a numeric result: (a + b) * 42
28
+ The result is always a number representing the computed deva score.
29
+
30
+ Disambiguation & Clarification
31
+ - If user provides more than two numbers without clear instructions: "Do you want to calculate the deva score by combining these numbers left-to-right?"
32
+ - If user provides non-numeric input: "Please provide numeric values"
33
+
34
+ Examples (→ mapped params)
35
+ - "Calculate deva score for 5 and 10" → { a: 5, b: 10 } returns 630
36
+ - "Score 1 and 2" → { a: 1, b: 2 } returns 126
37
+ - For multiple numbers, chain calls: devaScore(1,2)→126, then devaScore(126,3)→5418
38
+
39
+ Negative Examples (should NOT call deva-score)
40
+ - "Score this customer with the credit model" (use model-score instead)
41
+ - "Calculate the mean of these values" (use run-sas-program or sas-query instead)
42
+
43
+ Related Tools
44
+ - None directly related (this is a specialized scoring tool)
45
+
46
+ Notes
47
+ For sequences of numbers, use a left-to-right fold: call devaScore(first, second), then use that result as the first parameter for devaScore(result, third), and so on.
48
+ `;'''
49
+
50
+ deva_new = ''' let description = `
51
+ deva-score — compute a numeric score based on two input values.
52
+
53
+ USE when: calculate deva score, score these values, compute score for numbers
54
+ DO NOT USE for: model scoring (use model-score), statistical calculations, data lookup
55
+
56
+ PARAMETERS
57
+ - a: number (required) — first input value
58
+ - b: number (required) — second input value
59
+
60
+ FORMULA: (a + b) * 42
61
+
62
+ ROUTING RULES
63
+ - "calculate deva score for 5 and 10" → { a: 5, b: 10 }
64
+ - "score 1 and 2" → { a: 1, b: 2 }
65
+ - "deva score a=3, b=7" → { a: 3, b: 7 }
66
+ - Multiple numbers → chain calls left-to-right: call(first, second), then call(result, third)
67
+
68
+ EXAMPLES
69
+ - "Calculate deva score for 5 and 10" → { a: 5, b: 10 } returns 630
70
+ - "Score 1 and 2" → { a: 1, b: 2 } returns 126
71
+ - "Deva score 20 and 30" → { a: 20, b: 30 } returns 2100
72
+
73
+ NEGATIVE EXAMPLES (do not route here)
74
+ - "Score this customer with credit model" (use model-score)
75
+ - "Calculate the mean of these values" (use run-sas-program or sas-query)
76
+ - "Statistical analysis of numbers" (use sas-query)
77
+
78
+ RESPONSE
79
+ Returns { score: (a + b) * 42 }
80
+ `;'''
81
+
82
+ files = {
83
+ 'src/toolSet/devaScore.js': (deva_old, deva_new),
84
+ }
85
+
86
+ for filepath, (old, new) in files.items():
87
+ try:
88
+ with open(filepath, 'r', encoding='utf-8') as f:
89
+ content = f.read()
90
+
91
+ if old in content:
92
+ content = content.replace(old, new)
93
+ with open(filepath, 'w', encoding='utf-8') as f:
94
+ f.write(content)
95
+ print(f"✓ Updated {filepath}")
96
+ else:
97
+ print(f"✗ Pattern not found in {filepath}")
98
+ except Exception as e:
99
+ print(f"✗ Error updating {filepath}: {e}")
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import os from 'os';
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ // Paths
8
+ let client = (process.env.CLIENTNAME == null) ? '.github' : `.${process.env.CLIENTNAME.toLowerCase()}`;
9
+ const source = path.join(__dirname, `../.skills`);
10
+ //const destination = path.join(process.cwd(), client);
11
+ const destination = path.join(os.homedir(), client)
12
+ console.error(`📁 Copying ${source} to ${destination}...`);
13
+ function copyFolderSync(from, to) {
14
+ if (!fs.existsSync(from)) return;
15
+ if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });
16
+ console.error(`📁 Copying folder: ${from} to ${to}`);
17
+ fs.readdirSync(from).forEach(element => {
18
+ const fromPath = path.join(from, element);
19
+ const toPath = path.join(to, element);
20
+ if (fs.lstatSync(fromPath).isFile()) {
21
+ fs.copyFileSync(fromPath, toPath);
22
+ } else if (fs.lstatSync(fromPath).isDirectory()) {
23
+ copyFolderSync(fromPath, toPath);
24
+ }
25
+ });
26
+ }
27
+
28
+ try {
29
+ copyFolderSync(source, destination);
30
+ console.error(`✅ Success: ${destination} folder is now in your project root.`);
31
+ } catch (err) {
32
+ console.error('❌ Error copying files:', err.message);
33
+ }
34
+ process.exit(0);
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ import re
3
+ import os
4
+
5
+ os.chdir('c:/dev/github/sas-score-mcp-serverjs')
6
+
7
+ # Update runJob.js
8
+ file_path = 'src/toolSet/runJob.js'
9
+ with open(file_path, 'r', encoding='utf-8') as f:
10
+ content = f.read()
11
+
12
+ new_desc = """run-job — execute a deployed SAS Viya job.
13
+
14
+ USE when: run job, execute job, run with parameters
15
+ DO NOT USE for: arbitrary SAS code (use run-sas-program), macros (use run-macro), list/find jobs
16
+
17
+ PARAMETERS
18
+ - name: string — job name (required)
19
+ - scenario: string | object — input parameters. Accepts: "x=1, y=2" or {x:1, y:2}
20
+
21
+ ROUTING RULES
22
+ - "run job xyz" → { name: "xyz" }
23
+ - "run job xyz with param1=10, param2=val2" → { name: "xyz", scenario: {param1:10, param2:"val2"} }
24
+
25
+ EXAMPLES
26
+ - "run job xyz" → { name: "xyz" }
27
+ - "run job monthly_etl with month=10, year=2025" → { name: "monthly_etl", scenario: {month:10, year:2025} }
28
+
29
+ NEGATIVE EXAMPLES (do not route here)
30
+ - "run SAS code" (use run-sas-program)
31
+ - "run macro X" (use run-macro)
32
+ - "list jobs" (use list-jobs)
33
+ - "find job X" (use find-job)
34
+
35
+ ERRORS
36
+ Returns log output, listings, tables from job. Error if job not found."""
37
+
38
+ # Use a more flexible pattern
39
+ pattern = r'let description = `\n## run-job.*?`;\n'
40
+ replacement = f'let description = `\n{new_desc}\n`;\n'
41
+ updated = re.sub(pattern, replacement, content, flags=re.DOTALL)
42
+
43
+ with open(file_path, 'w', encoding='utf-8') as f:
44
+ f.write(updated)
45
+
46
+ print(f"✓ Updated {file_path}")
@@ -0,0 +1,3 @@
1
+ kubectl cp $(kubectl get pod | grep "sas-consul-server-0" | awk -F" " '{print $1}'):security/ca.crt ./ca.crt
2
+ kubectl cp $(kubectl get pod | grep "sas-consul-server-0" | awk -F" " '{print $1}'):security/tls.crt ./tls.crt
3
+ kubectl cp $(kubectl get pod | grep "sas-consul-server-0" | awk -F" " '{print $1}'):security/tls.key ./tls.key
@@ -0,0 +1,219 @@
1
+ /**
2
+ * SAS Viya PKCE Authorization Flow
3
+ *
4
+ * Uses the Authorization Code flow with PKCE (RFC 7636).
5
+ * Starts a local HTTP server to capture the redirect callback.
6
+ *
7
+ * Usage:
8
+ * node sas-viya-pkce-auth.js
9
+ *
10
+ * Prerequisites:
11
+ * - A SAS Viya client registered with:
12
+ * grant_types: authorization_code
13
+ * redirect_uri: http://localhost:3000/callback
14
+ * PKCE enabled (no client_secret required)
15
+ */
16
+
17
+ import http from "http";
18
+ import https from "https";
19
+ import crypto from "crypto";
20
+ import url from "url";
21
+
22
+ let VIYA_SERVER=process.env.VIYA_SERVER;
23
+ let PORT=8080;
24
+ let CLIENTID='pkcemcp';
25
+ // ── Configuration ────────────────────────────────────────────────────────────
26
+
27
+ const CONFIG = {
28
+ authBaseUrl: `${VIYA_SERVER}/oauth/SASLogon`,
29
+ clientId: CLIENTID, // <-- replace with your client ID
30
+ redirectUri: `http://localhost:${PORT}/callback`,
31
+ scopes: "openid profile",
32
+ localPort: PORT,
33
+ };
34
+
35
+ main().catch((err) => {
36
+ console.error("Unexpected error:", err);
37
+ process.exit(1);
38
+ });
39
+
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ // Generate a cryptographically random code verifier (43–128 chars, base64url)
43
+ function generateCodeVerifier() {
44
+ return crypto.randomBytes(64).toString("base64url");
45
+ }
46
+
47
+ // Derive the code challenge: BASE64URL(SHA-256(verifier))
48
+ function generateCodeChallenge(verifier) {
49
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
50
+ }
51
+
52
+ // Build the SASLogon authorization URL
53
+ function buildAuthUrl(codeChallenge, state) {
54
+ const params = new URLSearchParams({
55
+ response_type: "code",
56
+ client_id: CONFIG.clientId,
57
+ redirect_uri: CONFIG.redirectUri,
58
+ scope: CONFIG.scopes,
59
+ state,
60
+ code_challenge: codeChallenge,
61
+ code_challenge_method: "S256",
62
+ });
63
+ return `${CONFIG.authBaseUrl}/authorize?${params}`;
64
+ }
65
+
66
+ // Exchange the authorization code for tokens
67
+ function exchangeCodeForTokens(code, codeVerifier) {
68
+ return new Promise((resolve, reject) => {
69
+ const body = new URLSearchParams({
70
+ grant_type: "authorization_code",
71
+ client_id: CONFIG.clientId,
72
+ redirect_uri: CONFIG.redirectUri,
73
+ code,
74
+ code_verifier: codeVerifier,
75
+ }).toString();
76
+
77
+ const tokenUrl = new URL(`${CONFIG.authBaseUrl}/token`);
78
+
79
+ const options = {
80
+ hostname: tokenUrl.hostname,
81
+ port: tokenUrl.port || 443,
82
+ path: tokenUrl.pathname,
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/x-www-form-urlencoded",
86
+ "Content-Length": Buffer.byteLength(body),
87
+ },
88
+ // Remove the line below if your Viya instance has a valid TLS cert
89
+ rejectUnauthorized: false,
90
+ };
91
+
92
+ const req = https.request(options, (res) => {
93
+ let data = "";
94
+ res.on("data", (chunk) => (data += chunk));
95
+ res.on("end", () => {
96
+ try {
97
+ const parsed = JSON.parse(data);
98
+ if (res.statusCode >= 400) {
99
+ reject(new Error(`Token error (${res.statusCode}): ${data}`));
100
+ } else {
101
+ resolve(parsed);
102
+ }
103
+ } catch {
104
+ reject(new Error(`Failed to parse token response: ${data}`));
105
+ }
106
+ });
107
+ });
108
+
109
+ req.on("error", reject);
110
+ req.write(body);
111
+ req.end();
112
+ });
113
+ }
114
+
115
+ // Wait for the browser redirect and extract code + state
116
+ function waitForCallback(expectedState) {
117
+ return new Promise((resolve, reject) => {
118
+ const server = http.createServer((req, res) => {
119
+ const parsed = url.parse(req.url, true);
120
+
121
+ if (parsed.pathname !== "/callback") {
122
+ res.writeHead(404);
123
+ res.end("Not found");
124
+ return;
125
+ }
126
+
127
+ const { code, state, error, error_description } = parsed.query;
128
+
129
+ res.writeHead(200, { "Content-Type": "text/html" });
130
+ res.end(`
131
+ <html><body>
132
+ <h2>${error ? "Authorization failed" : "Authorization successful!"}</h2>
133
+ <p>You may close this tab.</p>
134
+ </body></html>
135
+ `);
136
+
137
+ server.close();
138
+
139
+ if (error) {
140
+ reject(new Error(`OAuth error: ${error} — ${error_description}`));
141
+ return;
142
+ }
143
+ if (state !== expectedState) {
144
+ reject(new Error("State mismatch — possible CSRF attack"));
145
+ return;
146
+ }
147
+
148
+ resolve(code);
149
+ });
150
+
151
+ server.listen(CONFIG.localPort, () => {
152
+ console.error(`Listening on http://localhost:${CONFIG.localPort}/callback`);
153
+ });
154
+
155
+ server.on("error", reject);
156
+ });
157
+ }
158
+
159
+ // Main flow
160
+ async function main() {
161
+ const codeVerifier = generateCodeVerifier();
162
+ const codeChallenge = generateCodeChallenge(codeVerifier);
163
+ const state = crypto.randomBytes(16).toString("hex");
164
+
165
+ const authUrl = buildAuthUrl(codeChallenge, state);
166
+
167
+ console.error("\nOpen this URL in your browser to log in:\n");
168
+ console.error(authUrl);
169
+ console.error();
170
+
171
+ // Try to auto-open in the default browser (best-effort)
172
+ try {
173
+ const { exec } = require("child_process");
174
+ const cmd =
175
+ process.platform === "win32"
176
+ ? `start "" "${authUrl}"`
177
+ : process.platform === "darwin"
178
+ ? `open "${authUrl}"`
179
+ : `xdg-open "${authUrl}"`;
180
+ exec(cmd);
181
+ } catch {
182
+ // ignore — user can open manually
183
+ }
184
+
185
+ let code;
186
+ try {
187
+ code = await waitForCallback(state);
188
+ } catch (err) {
189
+ console.error("Callback error:", err.message);
190
+ return null;
191
+ }
192
+
193
+ console.error("Authorization code received. Exchanging for tokens...");
194
+
195
+ let tokens;
196
+ try {
197
+ tokens = await exchangeCodeForTokens(code, codeVerifier);
198
+ } catch (err) {
199
+ console.error("Token exchange error:", err.message);
200
+ return null;
201
+ }
202
+
203
+ console.error("\nTokens received:");
204
+ console.error(" access_token :", tokens.access_token?.slice(0, 40) + "...");
205
+ console.error(" token_type :", tokens.token_type);
206
+ console.error(" expires_in :", tokens.expires_in, "seconds");
207
+ if (tokens.refresh_token) {
208
+ console.error(" refresh_token:", tokens.refresh_token.slice(0, 20) + "...");
209
+ }
210
+ if (tokens.id_token) {
211
+ console.error(" id_token :", tokens.id_token.slice(0, 40) + "...");
212
+ }
213
+
214
+ // tokens.access_token is ready to use as a Bearer token for Viya REST APIs
215
+ return tokens.access_token
216
+ }
217
+
218
+
219
+
@@ -15,7 +15,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
15
  import makeTools from "./toolSet/makeTools.js";
16
16
  import getLogonPayload from "./toolHelpers/getLogonPayload.js";
17
17
 
18
+ function getServerBaseUrl(appContext) {
19
+ const protocol = appContext.HTTPS === "TRUE" ? "https" : "http";
20
+ const host = appContext.contexts?.APPHOST || "localhost";
21
+ return `${protocol}://${host}:${appContext.PORT}`;
22
+ }
23
+
18
24
  async function createMcpServer(cache, _appContext) {
25
+ const serverBaseUrl = getServerBaseUrl(_appContext);
19
26
 
20
27
  let mcpServer = new McpServer(
21
28
  {
@@ -39,13 +46,12 @@ async function createMcpServer(cache, _appContext) {
39
46
  let currentId = cache.get('currentId');
40
47
  let _appContext = cache.get(currentId);
41
48
  let params;
42
- // get Viya token
43
49
 
44
50
  let errorStatus = cache.get('errorStatus');
45
51
  if (errorStatus) {
46
52
  return { isError: true, content: [{ type: 'text', text: errorStatus }] }
47
53
  };
48
- if (_appContext.AUTHFLOW === 'code' && _appContext.contexts.oauthInfo == null) {
54
+ if (/*_appContext.AUTHFLOW === 'code'*/ _appContext.useHapi === true && _appContext.contexts.oauthInfo == null) {
49
55
  return { isError: true, content: [{ type: 'text', text: 'Please visit https://localhost:8080/mcpserver to connect to Viya. Then try again.' }] }
50
56
  }
51
57
  console.error("Getting logon payload for tool with session ID:", currentId);
@@ -64,6 +70,7 @@ async function createMcpServer(cache, _appContext) {
64
70
 
65
71
  // call the actual tool handler
66
72
  debugger;
73
+ console.error("Calling tool handler with enhanced params");
67
74
  let r = await builtin(params);
68
75
  return r;
69
76
  }
@@ -73,10 +80,14 @@ async function createMcpServer(cache, _appContext) {
73
80
  let toolNames = [];
74
81
  toolSet.forEach((tool, i) => {
75
82
  let toolName = _appContext.brand + '-' + tool.name;
76
- // console.error(`\n[Note] Registering tool ${i + 1} : ${toolName}`);
83
+ //tool.inputSchema.additionalProperties = false; // disallow extra properties
84
+ let config = {
85
+ description: tool.description,
86
+ inputSchema: tool.inputSchema
87
+ }
77
88
  let toolHandler = wrapf(cache, tool.handler);
78
-
79
- mcpServer.tool(toolName, tool.description, tool.schema, toolHandler);
89
+ // console.error(`[Note] Registering tool ${toolName} with config: ${JSON.stringify(config)}`);
90
+ let r = mcpServer.registerTool(toolName, config, toolHandler);
80
91
  toolNames.push(toolName);
81
92
  });
82
93
  console.error(`[Note] Registered ${toolSet.length} tools: ${toolNames}`);