@sassoftware/sas-score-mcp-serverjs 0.3.29-0 → 0.4.1-1

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.
Files changed (45) hide show
  1. package/cli.js +118 -3
  2. package/openApi.yaml +121 -121
  3. package/package.json +4 -5
  4. package/skills/mcp-tool-description-optimizer/SKILL.md +129 -0
  5. package/skills/mcp-tool-description-optimizer/references/examples.md +123 -0
  6. package/skills/sas-read-and-score/SKILL.md +91 -0
  7. package/skills/sas-read-strategy/SKILL.md +143 -0
  8. package/skills/sas-score-workflow/SKILL.md +283 -0
  9. package/src/createMcpServer.js +11 -4
  10. package/src/expressMcpServer.js +1 -1
  11. package/src/handleGetDelete.js +1 -1
  12. package/src/openApi.yaml +121 -121
  13. package/src/toolHelpers/_jobSubmit.js +2 -0
  14. package/src/toolHelpers/_listLibrary.js +56 -39
  15. package/src/toolHelpers/readCerts.js +4 -4
  16. package/src/toolSet/devaScore.js +31 -44
  17. package/src/toolSet/findJob.js +37 -70
  18. package/src/toolSet/findJobdef.js +27 -58
  19. package/src/toolSet/findLibrary.js +29 -62
  20. package/src/toolSet/findModel.js +34 -57
  21. package/src/toolSet/findTable.js +29 -59
  22. package/src/toolSet/getEnv.js +26 -45
  23. package/src/toolSet/listJobdefs.js +30 -65
  24. package/src/toolSet/listJobs.js +30 -79
  25. package/src/toolSet/listLibraries.js +43 -55
  26. package/src/toolSet/listModels.js +25 -52
  27. package/src/toolSet/listTables.js +36 -66
  28. package/src/toolSet/makeTools.js +1 -0
  29. package/src/toolSet/modelInfo.js +21 -53
  30. package/src/toolSet/modelScore.js +30 -73
  31. package/src/toolSet/readTable.js +33 -72
  32. package/src/toolSet/runCasProgram.js +30 -51
  33. package/src/toolSet/runJob.js +24 -24
  34. package/src/toolSet/runJobdef.js +26 -29
  35. package/src/toolSet/runMacro.js +23 -24
  36. package/src/toolSet/runProgram.js +32 -80
  37. package/src/toolSet/sasQuery.js +31 -79
  38. package/src/toolSet/sasQueryTemplate.js +4 -5
  39. package/src/toolSet/sasQueryTemplate2.js +4 -5
  40. package/src/toolSet/scrInfo.js +4 -7
  41. package/src/toolSet/scrScore.js +2 -4
  42. package/src/toolSet/searchAssets.js +5 -6
  43. package/src/toolSet/setContext.js +30 -57
  44. package/src/toolSet/superstat.js +2 -3
  45. package/src/toolSet/tableInfo.js +32 -77
package/src/openApi.yaml CHANGED
@@ -1,121 +1,121 @@
1
- swagger: "2.0"
2
- info:
3
- title: SAS Viya Sample MCP Server API
4
- version: "1.0.0"
5
- description: API for interacting with the SAS Viya Sample MCP Server.
6
- host: localhost:8080
7
- basePath: /
8
- schemes:
9
- - http
10
- - https
11
- consumes:
12
- - application/json
13
- produces:
14
- - application/json
15
- paths:
16
- /health:
17
- get:
18
- summary: Health check
19
- description: Returns health and version information.
20
- responses:
21
- 200:
22
- description: Health information
23
- schema:
24
- type: object
25
- properties:
26
- name:
27
- type: string
28
- version:
29
- type: string
30
- description:
31
- type: string
32
- endpoints:
33
- type: object
34
- usage:
35
- type: string
36
- /apiMeta:
37
- get:
38
- summary: API metadata
39
- description: Returns the OpenAPI specification for this server.
40
- responses:
41
- 200:
42
- description: OpenAPI document
43
- schema:
44
- type: object
45
- /mcp:
46
- options:
47
- summary: CORS preflight
48
- description: CORS preflight endpoint.
49
- responses:
50
- 204:
51
- description: No Content
52
- post:
53
- summary: MCP request
54
- description: Handles MCP JSON-RPC requests.
55
- parameters:
56
- - name: body
57
- in: body
58
- required: true
59
- schema:
60
- type: object
61
- - name: Authorization
62
- in: header
63
- required: false
64
- type: string
65
- description: Bearer token for authentication
66
- - name: X-VIYA-SERVER
67
- in: header
68
- required: false
69
- type: string
70
- description: Override VIYA server
71
- - name: X-REFRESH-TOKEN
72
- in: header
73
- required: false
74
- type: string
75
- description: Refresh token for authentication
76
- - name: mcp-session-id
77
- in: header
78
- required: false
79
- type: string
80
- description: Session ID
81
- responses:
82
- 200:
83
- description: MCP response
84
- schema:
85
- type: object
86
- 500:
87
- description: Server error
88
- schema:
89
- type: object
90
- get:
91
- summary: Get MCP session
92
- description: Retrieves information for an MCP session.
93
- parameters:
94
- - name: mcp-session-id
95
- in: header
96
- required: true
97
- type: string
98
- description: Session ID
99
- responses:
100
- 200:
101
- description: Session information
102
- schema:
103
- type: object
104
- 400:
105
- description: Invalid or missing session ID
106
- delete:
107
- summary: Delete MCP session
108
- description: Deletes an MCP session.
109
- parameters:
110
- - name: mcp-session-id
111
- in: header
112
- required: true
113
- type: string
114
- description: Session ID
115
- responses:
116
- 200:
117
- description: Session deleted
118
- schema:
119
- type: object
120
- 400:
121
- description: Invalid or missing session ID
1
+ swagger: "2.0"
2
+ info:
3
+ title: SAS Viya Sample MCP Server API
4
+ version: "1.0.0"
5
+ description: API for interacting with the SAS Viya Sample MCP Server.
6
+ host: localhost:8080
7
+ basePath: /
8
+ schemes:
9
+ - http
10
+ - https
11
+ consumes:
12
+ - application/json
13
+ produces:
14
+ - application/json
15
+ paths:
16
+ /health:
17
+ get:
18
+ summary: Health check
19
+ description: Returns health and version information.
20
+ responses:
21
+ 200:
22
+ description: Health information
23
+ schema:
24
+ type: object
25
+ properties:
26
+ name:
27
+ type: string
28
+ version:
29
+ type: string
30
+ description:
31
+ type: string
32
+ endpoints:
33
+ type: object
34
+ usage:
35
+ type: string
36
+ /apiMeta:
37
+ get:
38
+ summary: API metadata
39
+ description: Returns the OpenAPI specification for this server.
40
+ responses:
41
+ 200:
42
+ description: OpenAPI document
43
+ schema:
44
+ type: object
45
+ /mcp:
46
+ options:
47
+ summary: CORS preflight
48
+ description: CORS preflight endpoint.
49
+ responses:
50
+ 204:
51
+ description: No Content
52
+ post:
53
+ summary: MCP request
54
+ description: Handles MCP JSON-RPC requests.
55
+ parameters:
56
+ - name: body
57
+ in: body
58
+ required: true
59
+ schema:
60
+ type: object
61
+ - name: Authorization
62
+ in: header
63
+ required: false
64
+ type: string
65
+ description: Bearer token for authentication
66
+ - name: X-VIYA-SERVER
67
+ in: header
68
+ required: false
69
+ type: string
70
+ description: Override VIYA server
71
+ - name: X-REFRESH-TOKEN
72
+ in: header
73
+ required: false
74
+ type: string
75
+ description: Refresh token for authentication
76
+ - name: mcp-session-id
77
+ in: header
78
+ required: false
79
+ type: string
80
+ description: Session ID
81
+ responses:
82
+ 200:
83
+ description: MCP response
84
+ schema:
85
+ type: object
86
+ 500:
87
+ description: Server error
88
+ schema:
89
+ type: object
90
+ get:
91
+ summary: Get MCP session
92
+ description: Retrieves information for an MCP session.
93
+ parameters:
94
+ - name: mcp-session-id
95
+ in: header
96
+ required: true
97
+ type: string
98
+ description: Session ID
99
+ responses:
100
+ 200:
101
+ description: Session information
102
+ schema:
103
+ type: object
104
+ 400:
105
+ description: Invalid or missing session ID
106
+ delete:
107
+ summary: Delete MCP session
108
+ description: Deletes an MCP session.
109
+ parameters:
110
+ - name: mcp-session-id
111
+ in: header
112
+ required: true
113
+ type: string
114
+ description: Session ID
115
+ responses:
116
+ 200:
117
+ description: Session deleted
118
+ schema:
119
+ type: object
120
+ 400:
121
+ description: Invalid or missing session ID
@@ -12,6 +12,8 @@ async function _jobSubmit(params) {
12
12
  // setup
13
13
  if (name === 'program') {
14
14
  let src = `
15
+ cas mycas;
16
+ caslib _all_ assign;
15
17
  proc sql;
16
18
  create table work.query_results as
17
19
  ${scenario.sql};
@@ -6,53 +6,70 @@
6
6
  import restafedit from '@sassoftware/restafedit';
7
7
  import deleteSession from './deleteSession.js';
8
8
 
9
- async function _listLibrary(params ){
9
+ async function _listLibrary(params) {
10
10
 
11
11
  let { server, limit, start, name, _appContext } = params;
12
-
13
- let config = {
14
- casServerName: _appContext.cas,
15
- computeContext: _appContext.sas,
16
- source: (server === 'sas') ? 'compute' : server,
17
- table: null
18
- };
19
- let appControl;
20
- try {
21
- // setup request control
22
- appControl = await restafedit.setup(
23
- _appContext.logonPayload,
24
- config
25
- ,null,{},'user',{}, {}, _appContext.storeConfig
26
- );
27
-
28
- // query parameters
29
- let payload = {
30
- qs: {
31
- limit: (limit != null) ? limit : 10,
32
- start: start - 1
33
- }
12
+
13
+ const _ilistLibrary = async (params) => {
14
+ let { server, limit, start, name, _appContext } = params;
15
+ console.error(_appContext);
16
+ let config = {
17
+ casServerName: _appContext.cas,
18
+ computeContext: _appContext.sas,
19
+ source: (server === 'sas') ? 'compute' : server,
20
+ table: null
34
21
  };
22
+ let appControl;
23
+ try {
24
+ // setup request control
25
+ appControl = await restafedit.setup(
26
+ _appContext.logonPayload,
27
+ config
28
+ , null, {}, 'user', {}, {}, _appContext.storeConfig
29
+ );
30
+
31
+ // query parameters
32
+ let payload = {
33
+ qs: {
34
+ limit: (limit != null) ? limit : 10,
35
+ start: start - 1
36
+ }
37
+ };
35
38
 
36
- if (name != null) {
37
- payload.qs = {
38
- filter: `eq(name, '${name}')`
39
+ if (name != null) {
40
+ payload.qs = {
41
+ filter: `eq(name, '${name}')`
42
+ }
39
43
  }
40
- }
41
-
42
- let items = await restafedit.getLibraryList(appControl, payload);
43
- let response = {libraries: items};
44
- await deleteSession(appControl);
45
-
46
- return { content: [{ type: 'text', text: JSON.stringify(response) }],
47
- structuredContent: response
48
- };
49
- } catch (err) {
50
- console.error(JSON.stringify(err));
51
- if (appControl != null) {
44
+
45
+ let items = await restafedit.getLibraryList(appControl, payload);
46
+ let response = { libraries: items };
52
47
  await deleteSession(appControl);
48
+
49
+ return {
50
+ content: [{ type: 'text', text: JSON.stringify(response) }],
51
+ structuredContent: response
52
+ };
53
+ } catch (err) {
54
+ console.error(JSON.stringify(err));
55
+ if (appControl != null) {
56
+ await deleteSession(appControl);
57
+ }
58
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
53
59
  }
54
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
55
60
  }
61
+
62
+ let source = (server === 'all') ? ['sas', 'cas'] : [server];
63
+ let response = {};
64
+
65
+ for (let i = 0; i < source.length; i++) {
66
+ let liblist = await _ilistLibrary({ server: source[i], limit, start, name, _appContext });
67
+ response[source[i]] = liblist.structuredContent;
68
+ }
69
+ return {
70
+ content: [{ type: 'text', text: JSON.stringify(response) }],
71
+ structuredContent: response
72
+ };
56
73
  }
57
74
 
58
75
  export default _listLibrary;
@@ -9,23 +9,23 @@ function getCerts(tlsdir) {
9
9
  return null;
10
10
  }
11
11
 
12
- console.log(`[Note] Reading certs from directory: ` + tlsdir);
12
+ console.error(`[Note] Reading certs from directory: ` + tlsdir);
13
13
  if (fs.existsSync(tlsdir) === false) {
14
14
  console.error("[Warning] Specified cert dir does not exist: " + tlsdir);
15
15
  return null;
16
16
  }
17
17
 
18
18
  let listOfFiles = fs.readdirSync(tlsdir);
19
- console.log("[Note] TLS/SSL files found: " + listOfFiles);
19
+ console.error("[Note] TLS/SSL files found: " + listOfFiles);
20
20
  let options = {};
21
21
  for(let i=0; i < listOfFiles.length; i++) {
22
22
  let fname = listOfFiles[i];
23
23
  let name = tlsdir + '/' + listOfFiles[i];
24
24
  let key = fname.split('.')[0];
25
- console.log('Reading TLS file: ' + name + ' as key: ' + key);
25
+ console.error('Reading TLS file: ' + name + ' as key: ' + key);
26
26
  options[key] = fs.readFileSync(name, { encoding: 'utf8' });
27
27
  }
28
- console.log('cert files', Object.keys(options));
28
+ console.error('cert files', Object.keys(options));
29
29
 
30
30
  return options;
31
31
 
@@ -2,73 +2,60 @@
2
2
  * Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import {z} from 'zod';
5
+ import { z } from 'zod';
6
6
 
7
7
  function devaScore(_appContext) {
8
8
 
9
9
  let description = `
10
- ## deva-score — compute a numeric score based on two input values
10
+ deva-score — compute a numeric score based on two input values.
11
11
 
12
- LLM Invocation Guidance (When to use)
13
- Use THIS tool when:
14
- - User wants to calculate the deva score: "Calculate deva score for 5 and 10"
15
- - User provides two numbers for scoring: "Score these values: 3 and 7"
16
- - User wants to compute a score in a series: "Calculate scores for [list of numbers]"
12
+ USE when: calculate deva score, score these values, compute score for numbers
13
+ DO NOT USE for: model scoring (use model-score), statistical calculations, data lookup
17
14
 
18
- Do NOT use this tool for:
19
- - Scoring models (use model-score)
20
- - Statistical calculations beyond deva scoring
21
- - Looking up data or metadata
15
+ PARAMETERS
16
+ - a: number (required) — first input value
17
+ - b: number (required) second input value
22
18
 
23
- Purpose
24
- Compute a numeric deva score by applying the formula (a + b) * 42 to two input numbers. For scoring more than two numbers, call this tool multiple times using the previous result as the first input (left-to-right fold).
19
+ FORMULA: (a + b) * 42
25
20
 
26
- Parameters
27
- - a (number, required): First numeric input value
28
- - b (number, required): Second numeric input value
21
+ ROUTING RULES
22
+ - "calculate deva score for 5 and 10" → { a: 5, b: 10 }
23
+ - "score 1 and 2" → { a: 1, b: 2 }
24
+ - "deva score a=3, b=7" → { a: 3, b: 7 }
25
+ - Multiple numbers → chain calls left-to-right: call(first, second), then call(result, third)
29
26
 
30
- Response Contract
31
- Returns a numeric result: (a + b) * 42
32
- The result is always a number representing the computed deva score.
33
-
34
- Disambiguation & Clarification
35
- - If user provides more than two numbers without clear instructions: "Do you want to calculate the deva score by combining these numbers left-to-right?"
36
- - If user provides non-numeric input: "Please provide numeric values"
37
-
38
- Examples (→ mapped params)
27
+ EXAMPLES
39
28
  - "Calculate deva score for 5 and 10" → { a: 5, b: 10 } returns 630
40
29
  - "Score 1 and 2" → { a: 1, b: 2 } returns 126
41
- - For multiple numbers, chain calls: devaScore(1,2)→126, then devaScore(126,3)→5418
42
-
43
- Negative Examples (should NOT call deva-score)
44
- - "Score this customer with the credit model" (use model-score instead)
45
- - "Calculate the mean of these values" (use run-sas-program or sas-query instead)
30
+ - "Deva score 20 and 30" → { a: 20, b: 30 } returns 2100
46
31
 
47
- Related Tools
48
- - None directly related (this is a specialized scoring tool)
32
+ NEGATIVE EXAMPLES (do not route here)
33
+ - "Score this customer with credit model" (use model-score)
34
+ - "Calculate the mean of these values" (use run-sas-program or sas-query)
35
+ - "Statistical analysis of numbers" (use sas-query)
49
36
 
50
- Notes
51
- For sequences of numbers, use a left-to-right fold: call devaScore(first, second), then use that result as the first parameter for devaScore(result, third), and so on.
52
- `;
37
+ RESPONSE
38
+ Returns { score: (a + b) * 42 }
39
+ `;
53
40
  let spec = {
54
41
  name: 'deva-score',
55
- aliases: ['devaScore','deva score','deva_score'],
56
42
  description: description,
57
- schema: {
43
+ inputSchema: z.object({
58
44
  a: z.number(),
59
45
  b: z.number()
60
- },
46
+ }),
61
47
  handler: async ({ a, b }) => {
62
- console.error( a, b);
63
- let r = {score: (a + b) * 42};
48
+ console.error(a, b);
49
+ let r = { score: (a + b) * 42 };
64
50
  console.error('deva score result', r);
65
51
  return {
66
- content: [{type: 'text', text: 'deva score result: ' + JSON.stringify(r)}],
52
+ content: [{ type: 'text', text: 'deva score result: ' + JSON.stringify(r) }],
67
53
  structuredContent: r
68
- };
69
- }
54
+ };
70
55
  }
71
-
56
+ }
57
+
72
58
  return spec;
73
59
  }
74
60
  export default devaScore;
61
+
@@ -5,88 +5,55 @@
5
5
  import { z } from 'zod';
6
6
  import _listJobs from '../toolHelpers/_listJobs.js';
7
7
  function findJob(_appContext) {
8
- let llmDescription= {
9
- "purpose": "Map natural language requests to find a job in SAS Viya and return structured results.",
10
- "param_mapping": {
11
- "name": "required - single name. If missing, ask 'Which job name would you like to find?'.",
12
- "_userPrompt": "the original user prompt that triggered this tool."
13
-
14
- },
15
- "response_schema": "{ jobs: Array<string|object> }",
16
- "behavior": "Return only JSON matching response_schema when invoked by an LLM. If no matches, return { jobs: [] }"
17
- };
8
+
18
9
  let description = `
19
- ## find-job — locate a specific SAS Viya job
20
-
21
- LLM Invocation Guidance
22
- Use THIS tool when the user intent is to check if ONE job exists or retrieve its metadata:
23
- - "find job cars_job_v4"
24
- - "does job sales_summary exist"
25
- - "is there a job named churnScorer"
26
- - "lookup job forecast_monthly"
27
- - "verify job ETL_Daily"
28
-
29
- Do NOT use this tool when the user asks for:
30
- - A list or browse of many jobs (use listJobs)
31
- - Do not use this tool if the user want to find lib, find table, find model and similar requests
32
- - Executing a job (use job)
33
- - Running a job definition (use jobdef)
34
- - Submitting arbitrary code (use program)
35
-
36
- Purpose
37
- Quickly determine whether a named job asset is present in the Viya environment and return its entry (or an empty result if not found).
38
-
39
- Parameters
40
- - name (string, required): Exact job name (case preserved). If multiple tokens/names supplied, take the first and ignore the rest; optionally ask for a single name.
41
-
42
- Behavior & Matching
43
- - Attempt exact match first (backend determines sensitivity).
44
- - Returns { jobs: [...] } where array length is 0 (not found) or 1+ (if backend returns multiple with the same display name).
45
- - No fuzzy guesses—never fabricate a job.
46
- - If no name provided: ask "Which job name would you like to find?".
47
-
48
- Response Contract
49
- - Always: { jobs: Array<string|object> }
50
- - On error: propagate structured server error (do not wrap in prose when invoked programmatically).
51
-
52
- Disambiguation Rules
53
- - Input only "find job" → ask for missing name.
54
- - Input contains verbs like "run" or "execute" → use run-job or run-jobdef instead.
55
- - Input requesting many (e.g., "find all jobs") → use list-jobs.
56
-
57
- Examples (→ mapped params)
58
- - "find job cars_job_v4" → { name: "cars_job_v4" }
59
- - "does job ETL exist" → { name: "ETL" }
60
- - "is there a job named metricsRefresh" → { name: "metricsRefresh" }
61
-
62
- Negative Examples (should NOT call find-job)
63
- - "list jobs" (list-jobs)
64
- - "run job cars_job_v4" (run-job)
65
- - "execute jobdef cars_job_v4" (run-jobdef)
66
-
67
- Clarifying Question Template
68
- - Missing name: "Which job name would you like to find?"
69
- - Multiple names: "Please provide just one job name (e.g. 'cars_job_v4')."
70
-
71
- Notes
72
- - For bulk existence checks loop over names and call find-job per name.
73
- - Combine with run-job tool if user wants to execute after confirming existence.
10
+ find-job — locate a specific SAS Viya job.
11
+
12
+ USE when: find job, does job exist, is there a job named, lookup job, verify job exists
13
+ DO NOT USE for: list jobs (use list-jobs), run job (use run-job), execute jobdef (use run-jobdef), find lib/table/model (use respective tools)
14
+
15
+ PARAMETERS
16
+ - name: string (required) job name to locate; if multiple supplied, use first
17
+
18
+ ROUTING RULES
19
+ - "find job <name>" → { name: "<name>" }
20
+ - "does job <name> exist" { name: "<name>" }
21
+ - "is there a job named <name>" { name: "<name>" }
22
+ - "lookup/verify job <name>" { name: "<name>" }
23
+ - "find job" with no name → ask "Which job name would you like to find?"
24
+ - "find all jobs / list jobs" → use list-jobs instead
25
+ - "run job <name>" use run-job instead
26
+
27
+ EXAMPLES
28
+ - "find job cars_job_v4" { name: "cars_job_v4" }
29
+ - "does job ETL exist" → { name: "ETL" }
30
+ - "is there a job named metricsRefresh" → { name: "metricsRefresh" }
31
+
32
+ NEGATIVE EXAMPLES (do not route here)
33
+ - "list jobs" (use list-jobs)
34
+ - "run job cars_job_v4" (use run-job)
35
+ - "execute jobdef cars_job_v4" (use run-jobdef)
36
+
37
+ ERRORS
38
+ Returns { jobs: [] } if not found; { jobs: [name, ...] } if found. Never hallucinate job names.
74
39
  `;
75
40
 
76
41
  let spec = {
77
42
  name: 'find-job',
78
- aliases: ['findJob','find job','find_job'],
79
43
  description: description,
80
- schema: {
44
+ inputSchema: z.object({
81
45
  name: z.string(),
82
- _userPrompt: z.string()
83
- },
84
- required: ['name'],
46
+ }),
85
47
  handler: async (params) => {
86
48
  let r = await _listJobs(params);
87
49
  return r;
88
50
  }
89
51
  }
52
+
53
+
54
+ /* correct spec for registerTool with inputSchema */
55
+
90
56
  return spec;
91
57
  }
92
58
  export default findJob;
59
+