@sassoftware/sas-score-mcp-serverjs 1.0.1-2 → 1.0.1-22
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/agents/sas-score-mcp-serverjs-agent.md +190 -0
- package/.skills/copilot-instructions.md +226 -0
- package/.skills/skills/README.md +125 -0
- package/.skills/skills/detail-strategy/SKILL.md +234 -0
- package/.skills/skills/find-resources/SKILL.md +152 -0
- package/.skills/skills/list-resource/SKILL.md +245 -0
- package/.skills/skills/read-strategy/SKILL.md +134 -0
- package/.skills/skills/request-routing/SKILL.md +95 -0
- package/.skills/skills/score-strategy/SKILL.md +210 -0
- package/README.md +9 -1
- package/cli.js +37 -27
- package/package.json +7 -8
- package/scripts/setup-skills.js +1 -1
- package/src/oauthHandlers/callback.js +1 -1
- package/src/processHeaders.js +1 -1
- package/src/setupSkills.js +12 -7
- package/src/setupSkills.v1.js +79 -0
- package/src/toolHelpers/_listLibrary.js +1 -1
- package/src/toolHelpers/_listTables.js +1 -1
- package/src/toolSet/findJobdef.js +5 -5
- package/src/toolSet/modelScore.js +23 -25
- package/src/toolSet/runCasProgram.js +21 -10
- package/src/toolSet/runJob.js +15 -19
- package/src/toolSet/runJobdef.js +15 -19
- package/src/toolSet/scrScore.js +60 -69
- package/src/toolSet/.claude/settings.local.json +0 -13
- /package/src/{handleGetDelete.js → handleGetDelete.txt} +0 -0
- /package/src/{handleRequest.js → handleRequest.txt} +0 -0
- /package/src/{hapiMcpServer.js → hapiMcpServer.txt} +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: read-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Strategy for reading data from CAS or SAS tables. Determines which read tool to use (raw reads vs analytical queries).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Read Table Strategy
|
|
8
|
+
|
|
9
|
+
Use this strategy when the user requests to read, fetch, or query data from a table.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Before reading:
|
|
14
|
+
1. Verify the table exists using FIND-RESOURCE strategy
|
|
15
|
+
2. Determine the server (CAS or SAS) from Step 1
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Two Types of Read Operations
|
|
20
|
+
|
|
21
|
+
### Type 1: Raw Row Read
|
|
22
|
+
|
|
23
|
+
**Trigger phrases**: "read rows from", "show first N records", "fetch records where", "get data from table"
|
|
24
|
+
|
|
25
|
+
**Tool**: `sas-score-read-table`
|
|
26
|
+
|
|
27
|
+
**When to use**:
|
|
28
|
+
- User wants raw records, not aggregations
|
|
29
|
+
- User wants to filter by WHERE clause
|
|
30
|
+
- User wants to browse data
|
|
31
|
+
|
|
32
|
+
**Parameters**:
|
|
33
|
+
```
|
|
34
|
+
lib: "<library>" # from FIND-RESOURCE verification
|
|
35
|
+
table: "<table>" # from FIND-RESOURCE verification
|
|
36
|
+
server: "cas" or "sas" # from FIND-RESOURCE verification
|
|
37
|
+
start: <row number> # default 1
|
|
38
|
+
limit: <max rows> # default 10, max 1000
|
|
39
|
+
where: "<SQL WHERE clause>" # optional filter
|
|
40
|
+
format: true # default: use formatted values
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Example**:
|
|
44
|
+
```
|
|
45
|
+
sas-score-read-table({
|
|
46
|
+
lib: "Public",
|
|
47
|
+
table: "customers",
|
|
48
|
+
server: "cas",
|
|
49
|
+
limit: 25,
|
|
50
|
+
where: "status='active'"
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### Type 2: Analytical Query
|
|
57
|
+
|
|
58
|
+
**Trigger phrases**: "how many", "count by", "average", "total", "sum", "group by", "aggregate", "distinct", "join"
|
|
59
|
+
|
|
60
|
+
**Tool**: `sas-score-sas-query`
|
|
61
|
+
|
|
62
|
+
**When to use**:
|
|
63
|
+
- User wants aggregations (SUM, AVG, COUNT, etc.)
|
|
64
|
+
- User wants GROUP BY or distinct counts
|
|
65
|
+
- User wants JOIN across tables
|
|
66
|
+
- User wants statistical summaries
|
|
67
|
+
|
|
68
|
+
**Parameters**:
|
|
69
|
+
```
|
|
70
|
+
table: "lib.table" # CAS: "Public.customers", SAS: "SASHELP.cars"
|
|
71
|
+
query: "<natural language question>"
|
|
72
|
+
sql: "<SELECT SQL>" # optional: pre-generated SQL
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Example**:
|
|
76
|
+
```
|
|
77
|
+
sas-score-sas-query({
|
|
78
|
+
table: "Public.customers",
|
|
79
|
+
query: "count of customers by region and status",
|
|
80
|
+
sql: "SELECT region, status, COUNT(*) as count FROM Public.customers GROUP BY region, status"
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Decision Tree
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
User requests data from table
|
|
90
|
+
↓
|
|
91
|
+
Is it an aggregation? (count, sum, avg, group by, distinct, etc.)
|
|
92
|
+
├─ YES → Use sas-score-sas-query
|
|
93
|
+
└─ NO → Use sas-score-read-table
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Table Name Format
|
|
99
|
+
|
|
100
|
+
- **CAS tables**: `Caslib.table` or `Public.customers` (lowercase caslib, mixed case table)
|
|
101
|
+
- **SAS tables**: `LIBREF.table` or `SASHELP.cars` (uppercase libref, case-insensitive table)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Error Handling
|
|
106
|
+
|
|
107
|
+
| Error | Action |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Table not found | Verify table exists with FIND-RESOURCE first |
|
|
110
|
+
| Server mismatch | Use server from FIND-RESOURCE verification |
|
|
111
|
+
| Empty result | Ask user to adjust WHERE clause or criteria |
|
|
112
|
+
| Column not found | Ask user to verify column name (case sensitivity) |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
### Example 1: Browse customer records
|
|
119
|
+
**Request**: "read first 20 customers from Public"
|
|
120
|
+
1. Find table customers in Public → CAS
|
|
121
|
+
2. Read: `sas-score-read-table({ lib: "Public", table: "customers", server: "cas", limit: 20 })`
|
|
122
|
+
3. Return 20 rows
|
|
123
|
+
|
|
124
|
+
### Example 2: Find active customers in a region
|
|
125
|
+
**Request**: "fetch customers from Public where status='active' and region='East'"
|
|
126
|
+
1. Find table customers in Public → CAS
|
|
127
|
+
2. Read: `sas-score-read-table({ lib: "Public", table: "customers", server: "cas", where: "status='active' and region='East'" })`
|
|
128
|
+
3. Return matching rows
|
|
129
|
+
|
|
130
|
+
### Example 3: Aggregate customers by region
|
|
131
|
+
**Request**: "how many customers by region in Public.customers"
|
|
132
|
+
1. Find table customers in Public → CAS
|
|
133
|
+
2. Query: `sas-score-sas-query({ table: "Public.customers", query: "count of customers by region", sql: "SELECT region, COUNT(*) FROM Public.customers GROUP BY region" })`
|
|
134
|
+
3. Return aggregated result
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: request-routing
|
|
3
|
+
description: >
|
|
4
|
+
Universal routing strategy for all SAS Viya requests. Every request follows the same three-step workflow.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Universal Request Routing Strategy
|
|
8
|
+
|
|
9
|
+
All SAS Viya requests follow this three-step workflow:
|
|
10
|
+
|
|
11
|
+
## Step 1: Verify Resources Exist
|
|
12
|
+
|
|
13
|
+
Before executing any action, verify that the target resources exist.
|
|
14
|
+
|
|
15
|
+
| Resource Type | Find Tool | Notes |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Library | `sas-score-find-library` | Specify server (cas or sas). For tables, determine server here. |
|
|
18
|
+
| Table | `sas-score-find-table` | Requires library name and server. |
|
|
19
|
+
| MAS Model | `sas-score-find-model` | No server selection. |
|
|
20
|
+
| Job | `sas-score-find-job` | No server selection. |
|
|
21
|
+
| JobDef | `sas-score-find-jobdef` | No server selection. |
|
|
22
|
+
| SCR Model | Skip verification | SCR models do not require pre-verification. |
|
|
23
|
+
|
|
24
|
+
**Rule**: Always verify before executing. Exception: SCR models can be scored directly.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Step 2: Execute the Request
|
|
29
|
+
|
|
30
|
+
Once resources are verified to exist, select the appropriate execution tool:
|
|
31
|
+
|
|
32
|
+
| Request Type | Tool | Input |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Read table rows | `sas-score-read-table` | lib, table, server (from Step 1) |
|
|
35
|
+
| Query table (aggregation) | `sas-score-sas-query` | lib.table, SQL query |
|
|
36
|
+
| Score with MAS model | `sas-score-mas-score` | model name, scenario data |
|
|
37
|
+
| Run Job | `sas-score-run-jobdef` | job name, scenario parameters |
|
|
38
|
+
| Run JobDef | `sas-score-run-jobdef` | jobdef name, scenario parameters |
|
|
39
|
+
| Score with SCR model | `sas-score-scr-score` | SCR URL, scenario data |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Step 3: Merge Results
|
|
44
|
+
|
|
45
|
+
Combine verification and execution results:
|
|
46
|
+
|
|
47
|
+
- For **read/query**: Return rows as-is.
|
|
48
|
+
- For **scoring**: Merge predictions with input scenario data.
|
|
49
|
+
- For **jobs/jobdefs**: Return execution results (tables, logs).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Special Case: Read + Score (Combined Workflow)
|
|
54
|
+
|
|
55
|
+
When the user requests scoring records from a table:
|
|
56
|
+
|
|
57
|
+
1. **Verify**: Find the table (determine server), find the model.
|
|
58
|
+
2. **Read**: Fetch rows from the table using `sas-score-read-table` or `sas-score-sas-query`.
|
|
59
|
+
3. **Map**: Check if table columns match model input variables. Ask user for mapping if needed.
|
|
60
|
+
4. **Score**: Score each row using `sas-score-mas-score` (for MAS) or `sas-score-scr-score` (for SCR).
|
|
61
|
+
5. **Merge**: Combine predictions with original rows.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Error Handling
|
|
66
|
+
|
|
67
|
+
| Error | Action |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Resource not found | Ask user to verify name and server (for tables). |
|
|
70
|
+
| Column/input mismatch | Ask user to map table columns to model inputs. |
|
|
71
|
+
| Empty result | Ask whether to adjust filter/criteria. |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Example 1: Read a table
|
|
78
|
+
**Request**: "read customers in Public"
|
|
79
|
+
1. Find library Public → Verified (CAS)
|
|
80
|
+
2. Read table customers in Public (CAS)
|
|
81
|
+
3. Return rows
|
|
82
|
+
|
|
83
|
+
### Example 2: Score with inline scenario
|
|
84
|
+
**Request**: "score a=1, b=2 with model simplejob.job"
|
|
85
|
+
1. Find job simplejob → Verified
|
|
86
|
+
2. Run simplejob with scenario {a: 1, b: 2}
|
|
87
|
+
3. Return result
|
|
88
|
+
|
|
89
|
+
### Example 3: Score records from table
|
|
90
|
+
**Request**: "score records from Public.customers with model risk_model.mas"
|
|
91
|
+
1. Find table customers in Public → Verified (CAS)
|
|
92
|
+
2. Find model risk_model → Verified (MAS)
|
|
93
|
+
3. Read rows from Public.customers
|
|
94
|
+
4. Score each row with risk_model
|
|
95
|
+
5. Return merged predictions + original data
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: score-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Unified scoring workflow. Handles MAS, Job, JobDef, SCR, and combined read+score scenarios.
|
|
5
|
+
Always verify resources before scoring.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Score Strategy
|
|
9
|
+
|
|
10
|
+
Use this strategy when the user requests model scoring, predictions, or running jobs/jobdefs.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
1. Verify the model/job exists using find-resources skill
|
|
15
|
+
2. If scoring table rows: Verify the table exists and determine server using find-resources skill
|
|
16
|
+
3. If scoring with inline scenario: Parse the scenario data
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 1: Parse the Score Request
|
|
21
|
+
|
|
22
|
+
Identify the scoring target (model type) and input source:
|
|
23
|
+
|
|
24
|
+
### Identify Model Type
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
score with model X.mas → MAS model
|
|
28
|
+
score with model X.job → Job
|
|
29
|
+
score with model X.jobdef → JobDef
|
|
30
|
+
score with model X.scr → SCR model
|
|
31
|
+
score with model X → Default to MAS
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Identify Input Source
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
score a=1, b=2 → Inline scenario
|
|
38
|
+
score with scenario {...} → Inline scenario
|
|
39
|
+
score records from table X → Table rows
|
|
40
|
+
score results of query... → Query results
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Step 2: Execute Scoring
|
|
46
|
+
|
|
47
|
+
### Option A: Score with Inline Scenario
|
|
48
|
+
|
|
49
|
+
**Trigger phrases**: "score a=1, b=2", "predict with values", "score scenario"
|
|
50
|
+
|
|
51
|
+
**Tools**:
|
|
52
|
+
- MAS: `sas-score-mas-score`
|
|
53
|
+
- Job: `sas-score-run-job`
|
|
54
|
+
- JobDef: `sas-score-run-jobdef`
|
|
55
|
+
- SCR: `sas-score-scr-score`
|
|
56
|
+
|
|
57
|
+
**Flow**:
|
|
58
|
+
1. Find model (find-resources)
|
|
59
|
+
2. Score with inline data
|
|
60
|
+
3. Return prediction + input data merged
|
|
61
|
+
|
|
62
|
+
**Parameters** (MAS):
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
sas-score-mas-score({
|
|
66
|
+
model: "<model name>",
|
|
67
|
+
scenario: { a: 1, b: 2 }
|
|
68
|
+
```
|
|
69
|
+
**Parameters** (job): ):
|
|
70
|
+
```
|
|
71
|
+
sas-score-run-job({
|
|
72
|
+
name: "<job name>",
|
|
73
|
+
scenario: { a: 1, b: 2 }
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Parameters** (jobdef):
|
|
78
|
+
```
|
|
79
|
+
sas-score-run-jobdef({
|
|
80
|
+
name: "<jobdef name>",
|
|
81
|
+
scenario: { a: 1, b: 2 }
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Parameters** (SCR):
|
|
86
|
+
```
|
|
87
|
+
sas-score-scr-score({
|
|
88
|
+
url: "<scr endpoint>",
|
|
89
|
+
scenario: { a: 1, b: 2 }
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Option B: Score Table Rows (Read + Score)
|
|
96
|
+
|
|
97
|
+
**Trigger phrases**: "score records from", "run model on table", "predict for customers in", "score rows from"
|
|
98
|
+
|
|
99
|
+
**Flow**:
|
|
100
|
+
1. Find model (find-resources)
|
|
101
|
+
2. Find table (find-resources) → get server
|
|
102
|
+
3. Read rows from table (read-strategy)
|
|
103
|
+
4. Score each row (or batch score)
|
|
104
|
+
5. Merge predictions with original rows
|
|
105
|
+
|
|
106
|
+
**Decision**: Read strategy first
|
|
107
|
+
- If user requests aggregation: Use `sas-score-sas-query`
|
|
108
|
+
- If user requests raw rows: Use `sas-score-read-table`
|
|
109
|
+
|
|
110
|
+
**Example workflow**:
|
|
111
|
+
```
|
|
112
|
+
Request: "score records from Public.customers with model risk_model"
|
|
113
|
+
|
|
114
|
+
1. Find table customers in Public → CAS
|
|
115
|
+
2. Find model risk_model → MAS
|
|
116
|
+
3. Read rows: sas-score-read-table({ lib: "Public", table: "customers", server: "cas", limit: 1000 })
|
|
117
|
+
4. Score each row: for each row, sas-score-mas-score({ model: "risk_model", scenario: {row} })
|
|
118
|
+
5. Merge: combine risk score with customer data
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Step 3: Result Formatting
|
|
124
|
+
|
|
125
|
+
### MAS Scoring Result
|
|
126
|
+
|
|
127
|
+
Return merged object:
|
|
128
|
+
```
|
|
129
|
+
Input data + Prediction fields
|
|
130
|
+
Example: { a: 1, b: 2, prediction: 0.85, probability_0: 0.15, probability_1: 0.85 }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Job/JobDef Result
|
|
134
|
+
|
|
135
|
+
Return execution output:
|
|
136
|
+
```
|
|
137
|
+
Tables, logs, listings as returned by job
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### SCR Result
|
|
141
|
+
|
|
142
|
+
Return merged object:
|
|
143
|
+
```
|
|
144
|
+
Input data + Prediction fields from SCR response
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Batch Scoring (Table Rows)
|
|
148
|
+
|
|
149
|
+
Return rows with predictions appended:
|
|
150
|
+
```
|
|
151
|
+
[
|
|
152
|
+
{ customer_id: 1, name: "Alice", ..., risk_score: 0.32 },
|
|
153
|
+
{ customer_id: 2, name: "Bob", ..., risk_score: 0.78 },
|
|
154
|
+
...
|
|
155
|
+
]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Column/Variable Mapping
|
|
161
|
+
|
|
162
|
+
If table columns don't match model input variable names:
|
|
163
|
+
|
|
164
|
+
1. Ask user: "Which table column maps to model input X?"
|
|
165
|
+
2. Wait for mapping: e.g., { customer_age: age, customer_income: income }
|
|
166
|
+
3. Transform row data using mapping
|
|
167
|
+
4. Score transformed data
|
|
168
|
+
5. Return with original column names
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Error Handling
|
|
173
|
+
|
|
174
|
+
| Error | Action |
|
|
175
|
+
|---|---|
|
|
176
|
+
| Model not found | Verify model name with user |
|
|
177
|
+
| Table not found | Verify table name and library |
|
|
178
|
+
| Scenario mismatch | Ask user to verify input variable names |
|
|
179
|
+
| Empty table | Ask whether to adjust filter or continue |
|
|
180
|
+
| Scoring failure | Return error message from scoring tool |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Examples
|
|
185
|
+
|
|
186
|
+
### Example 1: Score with inline scenario
|
|
187
|
+
**Request**: "score a=1, b=2 with model simplejob.job"
|
|
188
|
+
1. Find job simplejob using find-resources strategy
|
|
189
|
+
2. Run: `sas-score-run-jobdef({ name: "simplejob", scenario: { a: 1, b: 2 } })`
|
|
190
|
+
3. Return: `{ c: 3 }`
|
|
191
|
+
|
|
192
|
+
### Example 2: Score with MAS model
|
|
193
|
+
**Request**: "predict churn for age=45, income=60000 with model churn_predictor"
|
|
194
|
+
1. Find model churn_predictor.mas using find-resources strategy
|
|
195
|
+
2. Score: `sas-score-mas-score({ model: "churn_predictor", scenario: { age: 45, income: 60000 } })`
|
|
196
|
+
3. Return: `{ age: 45, income: 60000, churn_probability: 0.23, prediction: "no_churn" }`
|
|
197
|
+
|
|
198
|
+
### Example 3: Score table rows
|
|
199
|
+
**Request**: "score all active customers with model risk_model and table Public.customers"
|
|
200
|
+
1. Find model risk_model.mad using find-resources strategy
|
|
201
|
+
2. Find table Public.customers using find-resources strategy
|
|
202
|
+
3. Read: `sas-score-read-table({ lib: "Public", table: "customers", server: "cas", where: "status='active'" })`
|
|
203
|
+
4. Score each row with risk_model
|
|
204
|
+
5. Return: customers with risk_score appended
|
|
205
|
+
|
|
206
|
+
### Example 4: Score with SCR model
|
|
207
|
+
**Request**: "score age=50, income=75000 with model loan.scr"
|
|
208
|
+
1. Prepare: SCR URL for "loan"
|
|
209
|
+
2. Score: `sas-score-scr-score({ url: "loan", scenario: { age: 50, income: 75000 } })`
|
|
210
|
+
3. Return predictions from SCR endpoint
|
package/README.md
CHANGED
|
@@ -196,7 +196,15 @@ NODE_EXTRA_CA_CERTS=c:\Users\<your_username>\AppData\Local\mkcert\rootCA.pem
|
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
## License
|
|
199
|
-
This project is licensed under the
|
|
199
|
+
This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE).
|
|
200
|
+
|
|
201
|
+
The container image published from this repository also includes third-party software, each component under its own license:
|
|
202
|
+
|
|
203
|
+
- The npm dependencies that ship with this project, along with their respective licenses, are listed in [LICENSES.json](LICENSES.json).
|
|
204
|
+
- The container is built from the 25-alpine base image; license texts for its included software ship inside the image itself. License information for each Alpine package is available at [pkgs.alpinelinux.org](https://pkgs.alpinelinux.org/packages).
|
|
205
|
+
|
|
206
|
+
As with any container image, direct and indirect dependencies are governed by their own licenses.
|
|
207
|
+
Users of the published container image are responsible for ensuring that their use complies with all applicable licenses.
|
|
200
208
|
|
|
201
209
|
## Additional Resources
|
|
202
210
|
|
package/cli.js
CHANGED
|
@@ -9,15 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import coreSSE from './src/coreSSE.js';
|
|
11
11
|
import expressMcpServer from './src/expressMcpServer.js';
|
|
12
|
-
import hapiMcpServer from './src/hapiMcpServer.js';
|
|
13
|
-
|
|
14
12
|
import createMcpServer from './src/createMcpServer.js';
|
|
15
13
|
// import dotenvExpand from 'dotenv-expand';
|
|
16
14
|
import fs from 'fs';
|
|
17
15
|
import { randomUUID } from 'node:crypto';
|
|
18
16
|
|
|
19
|
-
//import refreshToken from './src/toolHelpers/refreshToken.js';
|
|
20
|
-
//import getOptsViya from './src/toolHelpers/getOptsViya.js';
|
|
21
17
|
import readCerts from './src/toolHelpers/readCerts.js';
|
|
22
18
|
|
|
23
19
|
import { fileURLToPath } from 'url';
|
|
@@ -27,7 +23,8 @@ import setupSkills from './src/setupSkills.js';
|
|
|
27
23
|
import { parseArgs } from "node:util";
|
|
28
24
|
|
|
29
25
|
import NodeCache from 'node-cache';
|
|
30
|
-
import { be } from 'zod/locales';
|
|
26
|
+
//import { be } from 'zod/locales';
|
|
27
|
+
//import { auth } from '@modelcontextprotocol/sdk/client/auth';
|
|
31
28
|
//import getOpts from './src/toolHelpers/getOpts.js';
|
|
32
29
|
|
|
33
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -72,11 +69,6 @@ const args = parseArgs({
|
|
|
72
69
|
short: 'c',
|
|
73
70
|
description: 'Client ID for authentication'
|
|
74
71
|
},
|
|
75
|
-
clientsecret: {
|
|
76
|
-
type: 'string',
|
|
77
|
-
short: 's',
|
|
78
|
-
description: 'Client Secret for authentication'
|
|
79
|
-
},
|
|
80
72
|
profile: {
|
|
81
73
|
type: 'string',
|
|
82
74
|
description: 'SAS CLI profile name'
|
|
@@ -107,6 +99,17 @@ const args = parseArgs({
|
|
|
107
99
|
alias: 'mcpclient',
|
|
108
100
|
description: 'MCP client name (github, claude...). Defaults to \'github\''
|
|
109
101
|
},
|
|
102
|
+
folder: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
short: 'f',
|
|
105
|
+
description: 'Subfolder under the client folder to copy the skills to, used to have different set of skills for different agents under the same client'
|
|
106
|
+
|
|
107
|
+
},
|
|
108
|
+
skills: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
short: 's',
|
|
111
|
+
description: 'Copies the skills for .github and .claude to the user home directory under .clientname (e.g. .github) or current directory if client name starts with dot (e.g. ./.github), used to have different set of skills for different clients'
|
|
112
|
+
},
|
|
110
113
|
help: {
|
|
111
114
|
type: 'boolean',
|
|
112
115
|
short: 'h',
|
|
@@ -125,28 +128,28 @@ const args = parseArgs({
|
|
|
125
128
|
// Handle help flag
|
|
126
129
|
if (args.values.help) {
|
|
127
130
|
console.error(`
|
|
128
|
-
|
|
131
|
+
sas-score-mcp-serverjs - Version: ${JSON.parse(pkg).version}
|
|
129
132
|
|
|
130
133
|
Usage: npx @sassoftware/sas-score-mcp-serverjs@dev [options]
|
|
131
134
|
|
|
132
135
|
Options:
|
|
133
136
|
Minimal options:
|
|
134
137
|
-v, --viya <url> Viya server URL
|
|
135
|
-
-c, --clientid <id> Client ID for oauth authentication(pkce preferred. default: vscodemcp)
|
|
136
138
|
|
|
137
139
|
MCP server options:
|
|
138
140
|
-t, --mcptype <type> MCP server type: http or stdio (default: http)
|
|
139
141
|
-m, --mcphost <host> MCP server host - can be remote URL - (default: http://localhost:8080)
|
|
140
142
|
|
|
141
|
-
Agent options:
|
|
142
|
-
--agent Enable agent mode with a pre-configured set of skills based on the client specified (default: false)
|
|
143
|
-
--client <name> MCP client name (github, claude...). Defaults to 'github'.Use to install skills
|
|
144
143
|
Authentication options:
|
|
144
|
+
-c, --clientid <id> Client ID for oauth authentication(pkce preferred. default: vscodemcp)
|
|
145
145
|
-a, --authflow <flow> Authentication flow: oauth, oauthclient, sascli, code, token(default oauth)
|
|
146
|
-
-s, --clientsecret <secret> Client Secret for oauth authentication (not needed for pkce)
|
|
147
146
|
--profile <name> SAS CLI profile name for sascli flow (default: Default)
|
|
148
147
|
--config <path> SAS CLI config directory for sascli flow (default: user home directory)
|
|
149
148
|
|
|
149
|
+
Agent/skills options:
|
|
150
|
+
-s, --skills <name> Copies the skills for .github and .claude
|
|
151
|
+
-f, --folder <folder> Subfolder to copy the skills to. ex: ./github/<folder>, used to have different set of skills for different agents under the same client.
|
|
152
|
+
|
|
150
153
|
Other options:
|
|
151
154
|
-p, --port <port> Port to run the server on (default: 8080)
|
|
152
155
|
--https Use HTTPS for the server (default: false)
|
|
@@ -165,6 +168,13 @@ Environment Variables:
|
|
|
165
168
|
`);
|
|
166
169
|
process.exit(0);
|
|
167
170
|
}
|
|
171
|
+
if (args.values.skills) {
|
|
172
|
+
console.error(`[Note] Settings up skills for ${args.values.skills }`);
|
|
173
|
+
setupSkills(args.values.skills, args.values.folder);;
|
|
174
|
+
console.error(`[Note] Skills setup completed. `);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
168
178
|
console.error('Parsed command line arguments:', args.values);
|
|
169
179
|
// read env file and then override with command line arguments
|
|
170
180
|
if (args.values.env) {
|
|
@@ -194,7 +204,7 @@ process.env.AUTHFLOW = args.values.authflow || process.env.AUTHFLOW || 'oauth';
|
|
|
194
204
|
process.env.MCPCLIENT = args.values.client || process.env.MCPCLIENT || 'github';
|
|
195
205
|
process.env.VIYA_SERVER = args.values.viya || process.env.VIYA_SERVER;
|
|
196
206
|
process.env.CLIENTID = args.values.clientid || process.env.CLIENTID || 'vscodemcp';
|
|
197
|
-
process.env.CLIENTSECRET =
|
|
207
|
+
process.env.CLIENTSECRET = null;
|
|
198
208
|
process.env.SAS_CLI_PROFILE = args.values.profile || process.env.SAS_CLI_PROFILE || 'Default';
|
|
199
209
|
process.env.SAS_CLI_CONFIG = args.values.config || process.env.SAS_CLI_CONFIG || os.homedir(); // default to user home directory
|
|
200
210
|
process.env.CASSERVER = args.values.casserver || process.env.CASSERVER || 'cas-shared-default';
|
|
@@ -219,6 +229,7 @@ if (args.values.version) {
|
|
|
219
229
|
console.error(`[Note] MCP client set to: ${process.env.CLIENT}`);
|
|
220
230
|
|
|
221
231
|
|
|
232
|
+
|
|
222
233
|
/********************************* */
|
|
223
234
|
const BRAND = 'sas-score'
|
|
224
235
|
/********************************* */
|
|
@@ -256,8 +267,10 @@ let mcpHost = process.env.MCPHOST;
|
|
|
256
267
|
if (authFlow === 'oauth' || authFlow === 'oauthclient') {
|
|
257
268
|
authFlow = 'bearer';
|
|
258
269
|
authExternal = (authFlow === 'oauthclient') ? true : false;
|
|
270
|
+
} else if (authFlow === 'bearer') {
|
|
271
|
+
authExternal = true; // in bearer token flow we assume the token is generated externally and passed in via env variable or token file, so we set authExternal to true to indicate that
|
|
259
272
|
}
|
|
260
|
-
let autoLogon = process.env.AUTOLOGON != null ? process.env.AUTOLOGON.toUpperCase() : "FALSE";
|
|
273
|
+
let autoLogon = process.env.AUTOLOGON != null ? process.env.AUTOLOGON.toUpperCase() : "FALSE";
|
|
261
274
|
const appEnvBase = {
|
|
262
275
|
version: version,
|
|
263
276
|
mcpType: mcpType,
|
|
@@ -373,10 +386,13 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
373
386
|
|
|
374
387
|
// setup skills based on client before mcp initialization
|
|
375
388
|
//
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
|
|
376
392
|
if (process.env.AGENT === 'TRUE') {
|
|
377
393
|
if (process.env.CLIENT !== 'none') {
|
|
378
394
|
console.error(`[Note] Setting up skills for client: ${process.env.CLIENT}...`);
|
|
379
|
-
setupSkills(process.env.CLIENT);
|
|
395
|
+
setupSkills(process.env.CLIENT, args.values.folder);
|
|
380
396
|
}
|
|
381
397
|
} else {
|
|
382
398
|
console.error(`[Note] Agent mode not enabled`);
|
|
@@ -453,14 +469,8 @@ if (mcpType === 'stdio') {
|
|
|
453
469
|
|
|
454
470
|
} else {
|
|
455
471
|
console.error('[Note] Starting HTTP MCP server...');
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
await hapiMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
459
|
-
console.error('[Note] Using HAPI HTTP server...')
|
|
460
|
-
} else {
|
|
461
|
-
await expressMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
462
|
-
console.error('[Note] MCP HTTP express server started on port ' + appEnvBase.PORT);
|
|
463
|
-
}
|
|
472
|
+
await expressMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
473
|
+
console.error('[Note] MCP HTTP express server started on port ' + appEnvBase.PORT);
|
|
464
474
|
}
|
|
465
475
|
|
|
466
476
|
// custom reader for .env file to avoid dotenv logging to console
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassoftware/sas-score-mcp-serverjs",
|
|
3
|
-
"version": "1.0.1-
|
|
3
|
+
"version": "1.0.1-22",
|
|
4
4
|
"description": "A mcp server for SAS Viya",
|
|
5
5
|
"author": "Deva Kumar <deva.kumar@sas.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -46,11 +46,10 @@
|
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"@sassoftware/restaf": "^5.
|
|
50
|
-
"@sassoftware/restafedit": "^3.
|
|
51
|
-
"@sassoftware/restaflib": "^5.
|
|
52
|
-
"
|
|
53
|
-
"axios": "^1.13.2",
|
|
49
|
+
"@sassoftware/restaf": "^5.7.2",
|
|
50
|
+
"@sassoftware/restafedit": "^3.10.5",
|
|
51
|
+
"@sassoftware/restaflib": "^5.7.2",
|
|
52
|
+
"axios": "^1.13.5",
|
|
54
53
|
"body-parser": "^2.2.1",
|
|
55
54
|
"cors": "^2.8.5",
|
|
56
55
|
"cross-env": "^10.1.0",
|
|
@@ -62,8 +61,8 @@
|
|
|
62
61
|
"node-cache": "^5.1.2",
|
|
63
62
|
"open": "^11.0.0",
|
|
64
63
|
"selfsigned": "^5.2.0",
|
|
65
|
-
"undici": "^7.
|
|
66
|
-
"uuid": "^
|
|
64
|
+
"undici": "^7.24.0",
|
|
65
|
+
"uuid": "^14.0.0",
|
|
67
66
|
"zod": "^4.2.1"
|
|
68
67
|
},
|
|
69
68
|
"devDependencies": {
|
package/scripts/setup-skills.js
CHANGED
|
@@ -6,7 +6,7 @@ import os from 'os';
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
// Paths
|
|
8
8
|
let client = (process.env.CLIENTNAME == null) ? '.github' : `.${process.env.CLIENTNAME.toLowerCase()}`;
|
|
9
|
-
const source = path.join(__dirname, `../.
|
|
9
|
+
const source = path.join(__dirname, `../.github`);
|
|
10
10
|
const destination = path.join(process.cwd(), client);
|
|
11
11
|
// const destination = path.join(os.homedir(), client)
|
|
12
12
|
console.error(`📁 Copying ${source} to ${destination}...`);
|
|
@@ -75,7 +75,7 @@ async function callback(req, res, pkceStore, codeStore, appContext) {
|
|
|
75
75
|
// which was part of the payload from the client to /oauth/authorize
|
|
76
76
|
// we trust since it was associated with the valid PKCE state
|
|
77
77
|
console.error("[Note] OAuth callback complete, redirecting to MCP client");
|
|
78
|
-
console.
|
|
78
|
+
console.error(pending.clientRedirectUri.toString())
|
|
79
79
|
return res.redirect(`${pending.clientRedirectUri}?${redirectParams}`);
|
|
80
80
|
} catch (err) {
|
|
81
81
|
console.error("[Error] OAuth callback handler error:", err);
|
package/src/processHeaders.js
CHANGED
|
@@ -34,7 +34,7 @@ function processHeaders(req, res, next, cache, appContext) {
|
|
|
34
34
|
let token = (hdr != null) ? hdr.slice(7) : null;
|
|
35
35
|
//console.error("[Note] Authorization token", token);
|
|
36
36
|
debugger;
|
|
37
|
-
console.
|
|
37
|
+
console.error('>>>',appContext.AUTHFLOW);
|
|
38
38
|
if (appContext.AUTHFLOW === 'bearer') {
|
|
39
39
|
debugger;
|
|
40
40
|
let startAuth = false;
|