@sassoftware/sas-score-mcp-serverjs 1.0.1-2 → 1.0.1-21
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 +149 -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 +32 -25
- 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
|
-
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
This project is licensed under the Apache License 2.0. See LICENSE.
|
|
202
|
+
|
|
203
|
+
The container image published from this repository also includes third-party software, each component under its own license:
|
|
204
|
+
|
|
205
|
+
The npm dependencies that ship with this project, along with their respective licenses, are listed in LICENSES.json.
|
|
206
|
+
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.
|
|
207
|
+
As with any container image, direct and indirect dependencies are governed by their own licenses. 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';
|
|
@@ -72,11 +68,6 @@ const args = parseArgs({
|
|
|
72
68
|
short: 'c',
|
|
73
69
|
description: 'Client ID for authentication'
|
|
74
70
|
},
|
|
75
|
-
clientsecret: {
|
|
76
|
-
type: 'string',
|
|
77
|
-
short: 's',
|
|
78
|
-
description: 'Client Secret for authentication'
|
|
79
|
-
},
|
|
80
71
|
profile: {
|
|
81
72
|
type: 'string',
|
|
82
73
|
description: 'SAS CLI profile name'
|
|
@@ -107,6 +98,17 @@ const args = parseArgs({
|
|
|
107
98
|
alias: 'mcpclient',
|
|
108
99
|
description: 'MCP client name (github, claude...). Defaults to \'github\''
|
|
109
100
|
},
|
|
101
|
+
folder: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
short: 'f',
|
|
104
|
+
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'
|
|
105
|
+
|
|
106
|
+
},
|
|
107
|
+
skills: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
short: 's',
|
|
110
|
+
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'
|
|
111
|
+
},
|
|
110
112
|
help: {
|
|
111
113
|
type: 'boolean',
|
|
112
114
|
short: 'h',
|
|
@@ -125,28 +127,28 @@ const args = parseArgs({
|
|
|
125
127
|
// Handle help flag
|
|
126
128
|
if (args.values.help) {
|
|
127
129
|
console.error(`
|
|
128
|
-
|
|
130
|
+
sas-score-mcp-serverjs - Version: ${JSON.parse(pkg).version}
|
|
129
131
|
|
|
130
132
|
Usage: npx @sassoftware/sas-score-mcp-serverjs@dev [options]
|
|
131
133
|
|
|
132
134
|
Options:
|
|
133
135
|
Minimal options:
|
|
134
136
|
-v, --viya <url> Viya server URL
|
|
135
|
-
-c, --clientid <id> Client ID for oauth authentication(pkce preferred. default: vscodemcp)
|
|
136
137
|
|
|
137
138
|
MCP server options:
|
|
138
139
|
-t, --mcptype <type> MCP server type: http or stdio (default: http)
|
|
139
140
|
-m, --mcphost <host> MCP server host - can be remote URL - (default: http://localhost:8080)
|
|
140
141
|
|
|
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
142
|
Authentication options:
|
|
143
|
+
-c, --clientid <id> Client ID for oauth authentication(pkce preferred. default: vscodemcp)
|
|
145
144
|
-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
145
|
--profile <name> SAS CLI profile name for sascli flow (default: Default)
|
|
148
146
|
--config <path> SAS CLI config directory for sascli flow (default: user home directory)
|
|
149
147
|
|
|
148
|
+
Agent/skills options:
|
|
149
|
+
-s, --skills <name> Copies the skills for .github and .claude
|
|
150
|
+
-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.
|
|
151
|
+
|
|
150
152
|
Other options:
|
|
151
153
|
-p, --port <port> Port to run the server on (default: 8080)
|
|
152
154
|
--https Use HTTPS for the server (default: false)
|
|
@@ -165,6 +167,13 @@ Environment Variables:
|
|
|
165
167
|
`);
|
|
166
168
|
process.exit(0);
|
|
167
169
|
}
|
|
170
|
+
if (args.values.skills) {
|
|
171
|
+
console.error(`[Note] Settings up skills for ${args.values.skills }`);
|
|
172
|
+
setupSkills(args.values.skills, args.values.folder);;
|
|
173
|
+
console.error(`[Note] Skills setup completed. `);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
168
177
|
console.error('Parsed command line arguments:', args.values);
|
|
169
178
|
// read env file and then override with command line arguments
|
|
170
179
|
if (args.values.env) {
|
|
@@ -194,7 +203,7 @@ process.env.AUTHFLOW = args.values.authflow || process.env.AUTHFLOW || 'oauth';
|
|
|
194
203
|
process.env.MCPCLIENT = args.values.client || process.env.MCPCLIENT || 'github';
|
|
195
204
|
process.env.VIYA_SERVER = args.values.viya || process.env.VIYA_SERVER;
|
|
196
205
|
process.env.CLIENTID = args.values.clientid || process.env.CLIENTID || 'vscodemcp';
|
|
197
|
-
process.env.CLIENTSECRET =
|
|
206
|
+
process.env.CLIENTSECRET = null;
|
|
198
207
|
process.env.SAS_CLI_PROFILE = args.values.profile || process.env.SAS_CLI_PROFILE || 'Default';
|
|
199
208
|
process.env.SAS_CLI_CONFIG = args.values.config || process.env.SAS_CLI_CONFIG || os.homedir(); // default to user home directory
|
|
200
209
|
process.env.CASSERVER = args.values.casserver || process.env.CASSERVER || 'cas-shared-default';
|
|
@@ -219,6 +228,7 @@ if (args.values.version) {
|
|
|
219
228
|
console.error(`[Note] MCP client set to: ${process.env.CLIENT}`);
|
|
220
229
|
|
|
221
230
|
|
|
231
|
+
|
|
222
232
|
/********************************* */
|
|
223
233
|
const BRAND = 'sas-score'
|
|
224
234
|
/********************************* */
|
|
@@ -373,10 +383,13 @@ if (appEnvBase.TOKENFILE != null) {
|
|
|
373
383
|
|
|
374
384
|
// setup skills based on client before mcp initialization
|
|
375
385
|
//
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
376
389
|
if (process.env.AGENT === 'TRUE') {
|
|
377
390
|
if (process.env.CLIENT !== 'none') {
|
|
378
391
|
console.error(`[Note] Setting up skills for client: ${process.env.CLIENT}...`);
|
|
379
|
-
setupSkills(process.env.CLIENT);
|
|
392
|
+
setupSkills(process.env.CLIENT, args.values.folder);
|
|
380
393
|
}
|
|
381
394
|
} else {
|
|
382
395
|
console.error(`[Note] Agent mode not enabled`);
|
|
@@ -453,14 +466,8 @@ if (mcpType === 'stdio') {
|
|
|
453
466
|
|
|
454
467
|
} else {
|
|
455
468
|
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
|
-
}
|
|
469
|
+
await expressMcpServer(mcpServer, sessionCache, appEnvBase);
|
|
470
|
+
console.error('[Note] MCP HTTP express server started on port ' + appEnvBase.PORT);
|
|
464
471
|
}
|
|
465
472
|
|
|
466
473
|
// 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-21",
|
|
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;
|