@sassoftware/sas-score-mcp-serverjs 0.4.1-19 → 0.4.1-20

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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sassoftware/sas-score-mcp-serverjs",
3
- "version": "0.4.1-19",
3
+ "version": "0.4.1-20",
4
4
  "description": "A mcp server for SAS Viya",
5
5
  "author": "Deva Kumar <deva.kumar@sas.com>",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,8 @@
43
43
  "openApi.json",
44
44
  "openApi.yaml",
45
45
  "skills",
46
- "scripts"
46
+ "scripts",
47
+ ".claude"
47
48
  ],
48
49
  "dependencies": {
49
50
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -69,15 +69,18 @@ sas-score-find-table({
69
69
  sas-score-read-table({
70
70
  table: "tablename",
71
71
  lib: "libraryname",
72
- server: "cas" or "sas", // determined from sas-score-find-table check
73
- limit: N, // default 10, adjust based on user request
72
+ server: "cas" or "sas", // REQUIRED: determined from sas-score-find-table check
73
+ start: 1, // REQUIRED: 1-based row offset (default: 1)
74
+ limit: N, // REQUIRED: max rows to retrieve (default: 10, max: 1000)
74
75
  where: "..." // optional SQL WHERE clause
75
76
  })
76
77
  ```
77
78
 
78
79
  **Rules:**
79
80
  - Always determine the server first using `sas-score-find-table` with smart detection (CAS → SAS)
80
- - Keep batch size 50 rows unless the user explicitly requests more
81
+ - **ALWAYS set `server` parameter** never omit it; use the result from `sas-score-find-table` to determine "cas" or "sas"
82
+ - **ALWAYS set `start` parameter** — use 1 for the first call, or the offset for pagination
83
+ - **ALWAYS set `limit` parameter** — keep batch size ≤ 50 rows unless the user explicitly requests more; cap at 1000
81
84
  - If table name is missing, ask: *"Which table should I read from? (format: lib.tablename)"*
82
85
  - If library is missing, ask: *"Which library contains the table?"*
83
86
  - If table exists in both servers, prefer CAS (already determined by smart detection)
@@ -124,12 +127,12 @@ sas-query({
124
127
  **Pattern A — Raw row retrieval**
125
128
  > "Show me the first 5 rows from Public.customers"
126
129
 
127
- → `sas-score-read-table({ table: "customers", lib: "Public", limit: 5 })`
130
+ → `sas-score-read-table({ table: "customers", lib: "Public", server: "cas", start: 1, limit: 5 })`
128
131
 
129
132
  **Pattern B — Filtered retrieval**
130
133
  > "Get all high-value orders (amount > 5000) from mylib.orders"
131
134
 
132
- → `sas-score-read-table({ table: "orders", lib: "mylib", where: "amount > 5000" })`
135
+ → `sas-score-read-table({ table: "orders", lib: "mylib", server: "sas", start: 1, limit: 50, where: "amount > 5000" })`
133
136
 
134
137
  **Pattern C — Aggregation**
135
138
  > "What is the average price by make in Public.cars?"