@sassoftware/sas-score-mcp-serverjs 1.0.1-9 → 1.1.2
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 +66 -61
- package/src/toolSet/findJob.js +24 -9
- package/src/toolSet/findJobdef.js +22 -19
- package/src/toolSet/findLibrary.js +73 -68
- 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 +6 -6
- 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 +70 -61
- package/src/toolSet/listJobs.js +68 -61
- package/src/toolSet/listLibraries.js +84 -78
- 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} +21 -18
- package/src/toolSet/readTable.js +80 -63
- package/src/toolSet/sasQuery.js +83 -77
- 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 +70 -65
- 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
|
@@ -1,99 +0,0 @@
|
|
|
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}")
|
package/scripts/setup-skills.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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);
|
|
@@ -1,46 +0,0 @@
|
|
|
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}")
|
package/src/authpkce.js
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
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
|
-
|
package/src/handleGetDelete.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
async function handleGetDelete(mcpServer, cache, req, h) {
|
|
7
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
8
|
-
console.error("=======================================================");
|
|
9
|
-
console.error(`[Note] Handling ${req.method} for session ID:`, sessionId);
|
|
10
|
-
let transports = cache.get("transports");
|
|
11
|
-
let transport = transports[sessionId];
|
|
12
|
-
if (!sessionId || transport == null) {
|
|
13
|
-
console.error('[Note] Looks like a fresh start - no session id or transport found');
|
|
14
|
-
return h.abandon;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (req.method === "GET") {
|
|
18
|
-
// You can customize the response as needed
|
|
19
|
-
console.error("[Note] Payload:", req.payload);
|
|
20
|
-
console.error("======================================================");
|
|
21
|
-
await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
|
|
22
|
-
return h.abandon;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (req.method === "DELETE") {
|
|
26
|
-
console.error("[Note] Deleting transport and cache for session ID:", sessionId);
|
|
27
|
-
delete transports[sessionId];
|
|
28
|
-
cache.del(sessionId);
|
|
29
|
-
return h.response(`[Info] In DELETE: Session ID ${sessionId} deleted`).code(201);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
export default handleGetDelete;
|
package/src/handleRequest.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
|
|
5
|
-
async function handleRequest(mcpServer, cache, req, h, credentials) {
|
|
6
|
-
let headerCache = {};
|
|
7
|
-
let transport;
|
|
8
|
-
let transports = cache.get("transports");
|
|
9
|
-
try {
|
|
10
|
-
|
|
11
|
-
headerCache = customHeaders(req, h);
|
|
12
|
-
let sessionId = req.headers["mcp-session-id"];
|
|
13
|
-
|
|
14
|
-
// we have session id, get existing transport
|
|
15
|
-
|
|
16
|
-
if (sessionId != null) {
|
|
17
|
-
/* existing transport */
|
|
18
|
-
transport = transports[sessionId];
|
|
19
|
-
if (transport == null) {
|
|
20
|
-
h.response({ isError: true, content: [{ type: 'text', text: 'Session not found. Please re-initialize the MCP client.' }] }).code(400).type('application/json');
|
|
21
|
-
return h.abandon;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (sessionId != null && transport != null) {
|
|
26
|
-
// post the curren session - used to pass _appContext to tools
|
|
27
|
-
cache.set("currentId", sessionId);
|
|
28
|
-
|
|
29
|
-
// get app context for session
|
|
30
|
-
let _appContext = cache.get(sessionId);
|
|
31
|
-
|
|
32
|
-
//if first prompt on a sessionid, create app context
|
|
33
|
-
|
|
34
|
-
if (_appContext == null) {
|
|
35
|
-
console.error("[Note] Creating new app context for session ID:", sessionId);
|
|
36
|
-
let appEnvTemplate = cache.get("appEnvTemplate");
|
|
37
|
-
_appContext = Object.assign({}, appEnvTemplate, headerCache);
|
|
38
|
-
if (headerCache.AUTHFLOW === 'bearer') {
|
|
39
|
-
_appContext.contexts.AUTHFLOW = 'bearer';
|
|
40
|
-
_appContext.contexts.bearerToken = headerCache.bearerToken;
|
|
41
|
-
}
|
|
42
|
-
_appContext.contexts.oauthInfo = credentials;
|
|
43
|
-
cache.set(sessionId, _appContext);
|
|
44
|
-
}
|
|
45
|
-
console.error("[Note] Using existing transport for session ID:", sessionId);
|
|
46
|
-
return await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// initialize request
|
|
50
|
-
else if (!sessionId && isInitializeRequest(req.payload)) {
|
|
51
|
-
// create transport
|
|
52
|
-
console.error("[Note] Initializing new transport for MCP request...");
|
|
53
|
-
transport = new StreamableHTTPServerTransport({
|
|
54
|
-
sessionIdGenerator: () => randomUUID(),
|
|
55
|
-
enableJsonResponse: true,
|
|
56
|
-
onsessioninitialized: (sessionId) => {
|
|
57
|
-
// Store the transport by session ID
|
|
58
|
-
transports[sessionId] = transport;
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
// Clean up transport when closed
|
|
62
|
-
transport.onclose = () => {
|
|
63
|
-
if (transport.sessionId) {
|
|
64
|
-
delete transports[transport.sessionId];
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
console.error("[Note] Connecting mcpServer to new transport...");
|
|
68
|
-
await mcpServer.connect(transport);
|
|
69
|
-
|
|
70
|
-
// Save transport data and app context for use in tools
|
|
71
|
-
cache.set("transports", transports);
|
|
72
|
-
return await transport.handleRequest(req.raw.req, req.raw.res, req.payload);
|
|
73
|
-
// cache transport
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
console.error("Error handling MCP request:", error);
|
|
80
|
-
let r = { isError: true, content: [{ type: 'text', text: 'Internal server error occurred while processing the request.' }] };
|
|
81
|
-
return h.response(r).code(500).type('application/json');
|
|
82
|
-
}
|
|
83
|
-
function customHeaders(req, h) {
|
|
84
|
-
|
|
85
|
-
// process any new header information
|
|
86
|
-
|
|
87
|
-
// Allow different VIYA server per sessionid(user)
|
|
88
|
-
let headerCache = {};
|
|
89
|
-
if (req.headers["X-VIYA-SERVER"] != null) {
|
|
90
|
-
console.error("[Note] Using user supplied VIYA server");
|
|
91
|
-
headerCache.VIYA_SERVER = req.header("X-VIYA-SERVER");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// used when doing autorization via mcp client
|
|
95
|
-
// ideal for production use
|
|
96
|
-
const hdr = req.headers["Authorization"];
|
|
97
|
-
if (hdr != null) {
|
|
98
|
-
headerCache.bearerToken = hdr.slice(7);
|
|
99
|
-
headerCache.AUTHFLOW = "bearer";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// faking out api key since Viya does not support
|
|
103
|
-
// not ideal for production
|
|
104
|
-
const hdr2 = req.headers["X-REFRESH-TOKEN"];
|
|
105
|
-
if (hdr2 != null) {
|
|
106
|
-
headerCache.refreshToken = hdr2;
|
|
107
|
-
headerCache.AUTHFLOW = "refresh";
|
|
108
|
-
}
|
|
109
|
-
return headerCache;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
export default handleRequest;
|