@sassoftware/sas-score-mcp-serverjs 0.4.1-1 → 0.4.1-15
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/cli.js +111 -31
- package/package.json +4 -2
- package/skills/sas-list-tables-smart/SKILL.md +123 -0
- package/skills/sas-read-and-score/SKILL.md +54 -53
- package/skills/sas-read-strategy/SKILL.md +10 -10
- package/skills/sas-score-workflow/SKILL.md +19 -2
- package/skills/sas-spec-migration/SKILL.md +303 -0
- package/src/authpkce.js +219 -0
- package/src/createMcpServer.js +11 -8
- package/src/expressMcpServer.js +354 -338
- package/src/oauthHandlers/authorize.js +46 -0
- package/src/oauthHandlers/baseUrl.js +8 -0
- package/src/oauthHandlers/callback.js +93 -0
- package/src/oauthHandlers/getMetadata.js +27 -0
- package/src/oauthHandlers/index.js +7 -0
- package/src/oauthHandlers/token.js +37 -0
- package/src/processHeaders.js +88 -0
- package/src/toolHelpers/_listLibrary.js +0 -1
- package/src/toolHelpers/getLogonPayload.js +5 -1
- package/src/toolHelpers/refreshTokenOauth.js +3 -3
- package/src/toolSet/.claude/settings.local.json +13 -0
- package/src/toolSet/devaScore.js +61 -61
- package/src/toolSet/findJob.js +1 -1
- package/src/toolSet/findJobdef.js +2 -2
- package/src/toolSet/findLibrary.js +68 -67
- package/src/toolSet/findModel.js +2 -2
- package/src/toolSet/findTable.js +3 -2
- package/src/toolSet/getEnv.js +8 -4
- package/src/toolSet/listJobdefs.js +61 -61
- package/src/toolSet/listJobs.js +61 -61
- package/src/toolSet/listLibraries.js +78 -78
- package/src/toolSet/listModels.js +56 -56
- package/src/toolSet/listTables.js +66 -65
- package/src/toolSet/modelInfo.js +2 -2
- package/src/toolSet/modelScore.js +6 -5
- package/src/toolSet/readTable.js +63 -65
- package/src/toolSet/runCasProgram.js +7 -6
- package/src/toolSet/runJob.js +81 -81
- package/src/toolSet/runJobdef.js +82 -82
- package/src/toolSet/runMacro.js +81 -80
- package/src/toolSet/runProgram.js +4 -8
- package/src/toolSet/sasQuery.js +77 -78
- package/src/toolSet/scrInfo.js +1 -1
- package/src/toolSet/scrScore.js +69 -68
- package/src/toolSet/setContext.js +65 -65
- package/src/toolSet/superstat.js +61 -59
- package/src/toolSet/tableInfo.js +58 -57
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sas-spec-migration
|
|
3
|
+
description: >
|
|
4
|
+
Migrate or clean up SAS MCP tool spec objects. Use this skill whenever the user asks
|
|
5
|
+
to migrate, convert, update, or clean up tool specs. Covers two patterns:
|
|
6
|
+
(1) Old Zod-based format (z.string(), z.number(), top-level schema/required) → new
|
|
7
|
+
JSON Schema inputSchema format.
|
|
8
|
+
(2) Existing JSON Schema specs that need cleanup: removing $schema, fixing required
|
|
9
|
+
arrays (optional fields like "where" should not be in required), typing untyped fields
|
|
10
|
+
like scenario. Also trigger on: "migrate my tools", "update my specs", "clean up my
|
|
11
|
+
specs", "remove $schema", "fix required", or any request to standardize tool specs.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# SAS Tool Spec Migration
|
|
15
|
+
|
|
16
|
+
Covers two migration scenarios:
|
|
17
|
+
|
|
18
|
+
- **Zod → JSON Schema**: Old format using `z.string()`, `z.number()` etc. with top-level `schema` and `required`
|
|
19
|
+
- **JSON Schema cleanup**: Existing `inputSchema` specs that need `$schema` removed, `required` fixed, or untyped fields corrected
|
|
20
|
+
|
|
21
|
+
Identify which pattern applies before proceeding.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Pattern 1 — JSON Schema cleanup
|
|
26
|
+
|
|
27
|
+
Use when the tool already has `inputSchema` but needs tidying. Apply all of these fixes:
|
|
28
|
+
|
|
29
|
+
### Remove `$schema`
|
|
30
|
+
|
|
31
|
+
Drop the `$schema` declaration entirely — the MCP SDK and LLMs ignore it at runtime and
|
|
32
|
+
it wastes tokens.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// Before
|
|
36
|
+
inputSchema: {
|
|
37
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
38
|
+
type: "object",
|
|
39
|
+
...
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// After
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
...
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Fix `required` arrays
|
|
50
|
+
|
|
51
|
+
Only include a field in `required` if it is truly mandatory. Common mistakes:
|
|
52
|
+
|
|
53
|
+
| Field | Rule |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `where` (filter expression) | Optional — remove from `required`, default to `""` in handler |
|
|
56
|
+
| `scenario` (job params) | Optional — not all jobs need parameters; remove from `required` |
|
|
57
|
+
| `name` | Required — always include |
|
|
58
|
+
| `limit`, `start` | Required for list tools — include |
|
|
59
|
+
|
|
60
|
+
### Type untyped fields
|
|
61
|
+
|
|
62
|
+
If a field has `{}` or no type, infer the correct type:
|
|
63
|
+
|
|
64
|
+
| Field | Type |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `scenario` | `{ type: "string" }` — handler already parses it as JSON |
|
|
67
|
+
| `where` | `{ type: "string" }` |
|
|
68
|
+
| Any flexible input | `{ type: "string" }` with note to parse in handler |
|
|
69
|
+
|
|
70
|
+
### Full cleanup example
|
|
71
|
+
|
|
72
|
+
**Before:**
|
|
73
|
+
```js
|
|
74
|
+
inputSchema: {
|
|
75
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
name: { type: "string" },
|
|
79
|
+
scenario: {}
|
|
80
|
+
},
|
|
81
|
+
required: ["name", "scenario"]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**After:**
|
|
86
|
+
```js
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
name: { type: "string" },
|
|
91
|
+
scenario: { type: "string" }
|
|
92
|
+
},
|
|
93
|
+
required: ["name"]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Pattern 2 — Zod → JSON Schema
|
|
100
|
+
|
|
101
|
+
## What changes
|
|
102
|
+
|
|
103
|
+
**Old format:**
|
|
104
|
+
```js
|
|
105
|
+
let spec = {
|
|
106
|
+
name: 'find-job',
|
|
107
|
+
aliases: [...],
|
|
108
|
+
description: description,
|
|
109
|
+
schema: {
|
|
110
|
+
name: z.string(),
|
|
111
|
+
limit: z.number().optional(),
|
|
112
|
+
},
|
|
113
|
+
required: ['name'],
|
|
114
|
+
handler: async (params) => { ... }
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**New format:**
|
|
119
|
+
```js
|
|
120
|
+
let spec = {
|
|
121
|
+
name: 'find-job',
|
|
122
|
+
aliases: [...],
|
|
123
|
+
description: description,
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
name: { type: "string", description: "Job name to locate" },
|
|
128
|
+
limit: { type: "number", description: "Max results to return" }
|
|
129
|
+
},
|
|
130
|
+
required: ['name']
|
|
131
|
+
},
|
|
132
|
+
handler: async (params) => { ... }
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Summary of changes:**
|
|
137
|
+
- `schema` → `inputSchema`
|
|
138
|
+
- `inputSchema` gains `type: "object"` and `properties: {}`
|
|
139
|
+
- Each Zod field moves inside `properties` as a JSON Schema object
|
|
140
|
+
- Top-level `required` array moves inside `inputSchema`
|
|
141
|
+
- Fields with `.optional()` are NOT included in `required`
|
|
142
|
+
- Fields without `.optional()` ARE included in `required` (unless already excluded)
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Zod → JSON Schema type mapping
|
|
147
|
+
|
|
148
|
+
| Zod | JSON Schema |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `z.string()` | `{ type: "string" }` |
|
|
151
|
+
| `z.number()` | `{ type: "number" }` |
|
|
152
|
+
| `z.boolean()` | `{ type: "boolean" }` |
|
|
153
|
+
| `z.array(z.string())` | `{ type: "array", items: { type: "string" } }` |
|
|
154
|
+
| `z.array(z.number())` | `{ type: "array", items: { type: "number" } }` |
|
|
155
|
+
| `z.object({...})` | `{ type: "object", properties: {...} }` |
|
|
156
|
+
| `z.enum(['a','b'])` | `{ type: "string", enum: ["a", "b"] }` |
|
|
157
|
+
| `.describe("text")` | Add `description: "text"` to the property |
|
|
158
|
+
| `.optional()` | Omit field from `required` array |
|
|
159
|
+
| `.optional().describe("text")` | Omit from `required`, add `description` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Step-by-step migration
|
|
164
|
+
|
|
165
|
+
### Step 1 — Identify the fields
|
|
166
|
+
|
|
167
|
+
Read the existing `schema` object. For each key, note:
|
|
168
|
+
- Its Zod type (string, number, boolean, array, enum, object)
|
|
169
|
+
- Whether it has `.optional()`
|
|
170
|
+
- Whether it has `.describe("...")` — extract the description text
|
|
171
|
+
- Whether it appears in the top-level `required` array
|
|
172
|
+
|
|
173
|
+
### Step 2 — Build `properties`
|
|
174
|
+
|
|
175
|
+
For each field in `schema`, create a JSON Schema property object:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
// Old
|
|
179
|
+
fieldName: z.string().describe("The name of the thing")
|
|
180
|
+
|
|
181
|
+
// New
|
|
182
|
+
fieldName: { type: "string", description: "The name of the thing" }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If no `.describe()` is present, infer a short description from the field name and tool context.
|
|
186
|
+
Keep descriptions concise — one sentence, imperative style ("Job name to locate", not "This is the name of the job").
|
|
187
|
+
|
|
188
|
+
### Step 3 — Build `required`
|
|
189
|
+
|
|
190
|
+
Include a field in `required` if:
|
|
191
|
+
- It appeared in the old top-level `required` array, OR
|
|
192
|
+
- It has no `.optional()` in its Zod definition
|
|
193
|
+
|
|
194
|
+
Exclude a field from `required` if:
|
|
195
|
+
- It has `.optional()` in its Zod definition
|
|
196
|
+
|
|
197
|
+
### Step 4 — Assemble `inputSchema`
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
// one entry per field from Step 2
|
|
204
|
+
},
|
|
205
|
+
required: [/* field names from Step 3 */]
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If `required` would be empty, omit it entirely rather than including `required: []`.
|
|
210
|
+
|
|
211
|
+
### Step 5 — Remove old fields
|
|
212
|
+
|
|
213
|
+
Remove `schema` and the top-level `required` from the spec object.
|
|
214
|
+
Leave everything else unchanged: `name`, `aliases`, `description`, `handler`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Edge cases
|
|
219
|
+
|
|
220
|
+
**Field in `required` but also `.optional()` in Zod:**
|
|
221
|
+
Trust `.optional()` — exclude from `required`. The old `required` array may be stale.
|
|
222
|
+
|
|
223
|
+
**Nested `z.object()`:**
|
|
224
|
+
```js
|
|
225
|
+
// Old
|
|
226
|
+
config: z.object({ host: z.string(), port: z.number() })
|
|
227
|
+
|
|
228
|
+
// New
|
|
229
|
+
config: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
host: { type: "string", description: "Host address" },
|
|
233
|
+
port: { type: "number", description: "Port number" }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**`z.union()` / `z.any()` / `z.unknown()`:**
|
|
239
|
+
Use `{}` (empty schema, accepts anything) or `{ type: "string" }` with a note that the
|
|
240
|
+
field accepts flexible input. Flag this to the user for review.
|
|
241
|
+
|
|
242
|
+
**No `schema` field at all:**
|
|
243
|
+
The tool takes no inputs. Set `inputSchema: { type: "object", properties: {} }` or omit
|
|
244
|
+
`inputSchema` entirely — both are valid. Omitting is cleaner for zero-input tools.
|
|
245
|
+
|
|
246
|
+
**`schema` is already a plain JSON object (not Zod):**
|
|
247
|
+
Check if values use `z.` prefix. If not, the schema may already be partially migrated —
|
|
248
|
+
wrap it in `{ type: "object", properties: { ... } }` and move `required` inside.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Output format
|
|
253
|
+
|
|
254
|
+
- Preserve the existing code style (quote style, indentation, trailing commas)
|
|
255
|
+
- Output the full updated spec object, not just the diff
|
|
256
|
+
- If migrating multiple specs at once, process them all and output each in full
|
|
257
|
+
- After outputting, note any fields that used `z.union()`, `z.any()`, or other ambiguous
|
|
258
|
+
types that the user should review manually
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Example — full migration
|
|
263
|
+
|
|
264
|
+
**Input:**
|
|
265
|
+
```js
|
|
266
|
+
let spec = {
|
|
267
|
+
name: 'find-job',
|
|
268
|
+
aliases: ['findJob', 'find job'],
|
|
269
|
+
description: description,
|
|
270
|
+
schema: {
|
|
271
|
+
name: z.string(),
|
|
272
|
+
caslib: z.string().optional().describe("CAS library name"),
|
|
273
|
+
limit: z.number().optional(),
|
|
274
|
+
},
|
|
275
|
+
required: ['name'],
|
|
276
|
+
handler: async (params) => {
|
|
277
|
+
let r = await _listJobs(params);
|
|
278
|
+
return r;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Output:**
|
|
284
|
+
```js
|
|
285
|
+
let spec = {
|
|
286
|
+
name: 'find-job',
|
|
287
|
+
aliases: ['findJob', 'find job'],
|
|
288
|
+
description: description,
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: "object",
|
|
291
|
+
properties: {
|
|
292
|
+
name: { type: "string", description: "Job name to locate" },
|
|
293
|
+
caslib: { type: "string", description: "CAS library name" },
|
|
294
|
+
limit: { type: "number", description: "Max results to return" }
|
|
295
|
+
},
|
|
296
|
+
required: ['name']
|
|
297
|
+
},
|
|
298
|
+
handler: async (params) => {
|
|
299
|
+
let r = await _listJobs(params);
|
|
300
|
+
return r;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
package/src/authpkce.js
ADDED
|
@@ -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
|
+
|
package/src/createMcpServer.js
CHANGED
|
@@ -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,18 +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
|
-
tool.inputSchema.additionalProperties = false; // disallow extra properties
|
|
83
|
+
//tool.inputSchema.additionalProperties = false; // disallow extra properties
|
|
77
84
|
let config = {
|
|
78
|
-
title: toolName,
|
|
79
85
|
description: tool.description,
|
|
80
86
|
inputSchema: tool.inputSchema
|
|
81
87
|
}
|
|
82
|
-
console.error(`[Note] Configuring tool ${toolName} with input schema: ${JSON.stringify(tool.inputSchema)}`);
|
|
83
|
-
console.error(`\n[Note] Registering tool ${i + 1} : ${toolName}`);
|
|
84
88
|
let toolHandler = wrapf(cache, tool.handler);
|
|
85
|
-
|
|
89
|
+
// console.error(`[Note] Registering tool ${toolName} with config: ${JSON.stringify(config)}`);
|
|
86
90
|
let r = mcpServer.registerTool(toolName, config, toolHandler);
|
|
87
|
-
console.error(`[Note] Tool ${toolName} registered with result: ${JSON.stringify(r)}`);
|
|
88
91
|
toolNames.push(toolName);
|
|
89
92
|
});
|
|
90
93
|
console.error(`[Note] Registered ${toolSet.length} tools: ${toolNames}`);
|