@sassoftware/sas-score-mcp-serverjs 1.0.1-8 → 1.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.
- package/.skills/.claude-plugin/plugin.json +59 -0
- package/.skills/agents/sas-score-mcp-serverjs-agent.md +26 -0
- package/.skills/copilot-instructions.md +62 -0
- package/.skills/skills/README.md +204 -0
- package/.skills/skills/detail-strategy/SKILL.md +316 -0
- package/.skills/skills/find-library-server/SKILL.md +62 -0
- package/.skills/skills/find-resources/SKILL.md +66 -0
- package/.skills/skills/list-library/SKILL.md +30 -0
- package/.skills/skills/list-mas-job-jobdef/SKILL.md +31 -0
- package/.skills/skills/list-tables/SKILL.md +30 -0
- package/.skills/skills/read-strategy/SKILL.md +87 -0
- package/.skills/skills/request-routing/SKILL.md +112 -0
- package/.skills/skills/score-cas/SKILL.md +95 -0
- package/.skills/skills/score-job-jobdef/SKILL.md +58 -0
- package/.skills/skills/score-mas-scr/SKILL.md +58 -0
- package/.skills/skills/score-program/SKILL.md +59 -0
- package/.skills/skills/score-strategy/SKILL.md +39 -0
- package/README.md +96 -54
- package/cli.js +29 -38
- package/openApi.yaml +121 -121
- package/package.json +15 -13
- package/scripts/docs/SCORE_SKILL_REFERENCE.md +17 -16
- package/scripts/docs/TOOL_DESCRIPTION_TEMPLATE.md +3 -3
- package/scripts/docs/TOOL_UPDATES_SUMMARY.md +65 -63
- package/scripts/docs/oauth-http-transport.md +2 -2
- package/scripts/docs/sas-mcp-tools-reference.md +43 -32
- package/scripts/plot_msrp_usa.py +49 -0
- package/scripts/refreshtoken.js +58 -0
- package/scripts/runListScr.mjs +16 -0
- package/src/createMcpServer.js +4 -1
- package/src/expressMcpServer.js +47 -49
- package/src/oauthHandlers/authorize.js +4 -1
- package/src/oauthHandlers/baseUrl.js +4 -0
- package/src/oauthHandlers/callback.js +4 -0
- package/src/oauthHandlers/getMetadata.js +4 -0
- package/src/oauthHandlers/index.js +4 -0
- package/src/oauthHandlers/token.js +4 -0
- package/src/openApi.yaml +121 -121
- package/src/processHeaders.js +10 -7
- package/src/setupSkills.js +6 -9
- package/src/toolHelpers/_casScore.js +32 -0
- package/src/toolHelpers/_desc.js +14 -0
- package/src/toolHelpers/_findJob.js +12 -0
- package/src/toolHelpers/_findJobdef.js +10 -0
- package/src/toolHelpers/_findLibrary.js +11 -0
- package/src/toolHelpers/_findMas.js +13 -0
- package/src/toolHelpers/_findScr.js +36 -0
- package/src/toolHelpers/_findTable.js +11 -0
- package/src/toolHelpers/_listJobdefs.js +12 -2
- package/src/toolHelpers/_listJobs.js +19 -8
- package/src/toolHelpers/{_listModels.js → _listMas.js} +4 -4
- package/src/toolHelpers/_listScr.js +13 -0
- package/src/toolHelpers/{_scrInfo.js → _scrDescribe.js} +4 -4
- package/src/toolHelpers/_scrScore.js +2 -2
- package/src/toolHelpers/_submitCasl.js +19 -17
- package/src/toolHelpers/{_tableInfo.js → _tableDescribe.js} +2 -2
- package/src/toolHelpers/getLogonPayload.js +2 -2
- package/src/toolSet/casModelScore.js +93 -0
- package/src/toolSet/casProgramScore.js +105 -0
- package/src/toolSet/devaScore.js +11 -6
- package/src/toolSet/findJob.js +74 -59
- package/src/toolSet/findJobdef.js +67 -64
- package/src/toolSet/findLibrary.js +28 -23
- package/src/toolSet/findMas.js +72 -0
- package/src/toolSet/findScr.js +69 -0
- package/src/toolSet/findTable.js +34 -27
- package/src/toolSet/getEnv.js +57 -57
- package/src/toolSet/jobDescribe.js +65 -0
- package/src/toolSet/jobScore.js +90 -0
- package/src/toolSet/jobdefDescribe.js +67 -0
- package/src/toolSet/jobdefScore.js +85 -0
- package/src/toolSet/listJobdefs.js +17 -8
- package/src/toolSet/listJobs.js +15 -8
- package/src/toolSet/listLibraries.js +16 -10
- package/src/toolSet/listMas.js +71 -0
- package/src/toolSet/listScr.js +62 -0
- package/src/toolSet/listTables.js +78 -66
- package/src/toolSet/{runMacro.js → macroScore.js} +86 -82
- package/src/toolSet/makeTools.js +39 -25
- package/src/toolSet/masDescribe.js +67 -0
- package/src/toolSet/masScore.js +95 -0
- package/src/toolSet/{runProgram.js → programScore.js} +96 -93
- package/src/toolSet/readTable.js +43 -26
- package/src/toolSet/sasQuery.js +24 -18
- package/src/toolSet/scrDescribe.js +55 -0
- package/src/toolSet/scrScore.js +63 -70
- package/src/toolSet/searchAssets.js +1 -1
- package/src/toolSet/setContext.js +8 -3
- package/src/toolSet/superstat.js +61 -61
- package/src/toolSet/tableDescribe.js +65 -0
- package/.skills/sas-find-library-smart/SKILL.md +0 -155
- package/.skills/sas-find-resource-strategy/SKILL.md +0 -105
- package/.skills/sas-list-resource-strategy/SKILL.md +0 -124
- package/.skills/sas-list-tables-smart/SKILL.md +0 -128
- package/.skills/sas-read-and-score-strategy/SKILL.md +0 -113
- package/.skills/sas-read-strategy/SKILL.md +0 -154
- package/.skills/sas-request-classifier/SKILL.md +0 -74
- package/.skills/sas-score-workflow-strategy/SKILL.md +0 -314
- package/.skills_claude/CLAUDE.md +0 -201
- package/.skills_claude/agents/sas-score-mcp-serverjs-agent.md +0 -58
- package/.skills_claude/enforce-find-resource-strategy.md +0 -35
- package/.skills_github/agents/sas-score-mcp-serverjs-agent.md +0 -58
- package/.skills_github/copilot-instructions.md +0 -201
- package/.skills_github/enforce-find-resource-strategy.md +0 -35
- package/scripts/optimize_final.py +0 -140
- package/scripts/optimize_tools.py +0 -99
- package/scripts/setup-skills.js +0 -34
- package/scripts/update_descriptions.py +0 -46
- package/src/authpkce.js +0 -219
- package/src/handleGetDelete.js +0 -34
- package/src/handleRequest.js +0 -112
- package/src/hapiMcpServer.js +0 -241
- package/src/toolSet/findModel.js +0 -60
- package/src/toolSet/listModels.js +0 -56
- package/src/toolSet/modelInfo.js +0 -55
- package/src/toolSet/modelScore.js +0 -89
- package/src/toolSet/runCasProgram.js +0 -98
- package/src/toolSet/runJob.js +0 -81
- package/src/toolSet/runJobdef.js +0 -82
- package/src/toolSet/scrInfo.js +0 -52
- package/src/toolSet/tableInfo.js +0 -58
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SAS MCP Server Tools Reference
|
|
1
|
+
# SAS MCP Server Tools Reference
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
@@ -11,8 +11,10 @@ All tools are registered as sas-score-<toolname>
|
|
|
11
11
|
- [Model Management & Scoring](#model-management--scoring)
|
|
12
12
|
- [Library Management](#library-management)
|
|
13
13
|
- [Table Operations](#table-operations)
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
14
|
+
- [Jobs Model](#jobs-model)
|
|
15
|
+
- [Jobdef Models](#jobdef-models)
|
|
16
|
+
- [Program Models](#program-models)
|
|
17
|
+
- [Macro Models](#macro-models)
|
|
16
18
|
- [Context & Configuration](#context--configuration)
|
|
17
19
|
- [Utility Tools](#utility-tools)
|
|
18
20
|
|
|
@@ -20,7 +22,7 @@ All tools are registered as sas-score-<toolname>
|
|
|
20
22
|
|
|
21
23
|
## Model Management & Scoring
|
|
22
24
|
|
|
23
|
-
### list-
|
|
25
|
+
### list-mas
|
|
24
26
|
|
|
25
27
|
Enumerate models published to MAS (Model Aggregation Service).
|
|
26
28
|
|
|
@@ -42,7 +44,7 @@ list 25 models
|
|
|
42
44
|
|
|
43
45
|
---
|
|
44
46
|
|
|
45
|
-
### find-
|
|
47
|
+
### find-mas
|
|
46
48
|
|
|
47
49
|
Locate a specific model deployed to MAS.
|
|
48
50
|
|
|
@@ -61,7 +63,7 @@ find model myModel
|
|
|
61
63
|
|
|
62
64
|
---
|
|
63
65
|
|
|
64
|
-
###
|
|
66
|
+
### mas-describe
|
|
65
67
|
|
|
66
68
|
Retrieve detailed metadata for a deployed model including input/output variables, data types, and constraints.
|
|
67
69
|
|
|
@@ -80,12 +82,12 @@ Retrieve detailed metadata for a deployed model including input/output variables
|
|
|
80
82
|
|
|
81
83
|
**Example:**
|
|
82
84
|
```
|
|
83
|
-
|
|
85
|
+
mas-describe model=churnRisk
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
---
|
|
87
89
|
|
|
88
|
-
###
|
|
90
|
+
### mas-score
|
|
89
91
|
|
|
90
92
|
Score user-supplied scenario data using a MAS-published model.
|
|
91
93
|
|
|
@@ -105,13 +107,13 @@ Score user-supplied scenario data using a MAS-published model.
|
|
|
105
107
|
|
|
106
108
|
**Example:**
|
|
107
109
|
```
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
mas-score model=mycoolmodel scenario={x:1,y:2}
|
|
111
|
+
mas-score model=cancer1 scenario="age=45, sex=M, tumor=stage2"
|
|
110
112
|
```
|
|
111
113
|
|
|
112
114
|
---
|
|
113
115
|
|
|
114
|
-
### scr-
|
|
116
|
+
### scr-describe
|
|
115
117
|
|
|
116
118
|
Return input/output schema and metadata for an SCR (Score Code Runtime) model.
|
|
117
119
|
|
|
@@ -124,7 +126,7 @@ Return input/output schema and metadata for an SCR (Score Code Runtime) model.
|
|
|
124
126
|
|
|
125
127
|
**Example:**
|
|
126
128
|
```
|
|
127
|
-
scr-
|
|
129
|
+
scr-describe name="https://scr-host/models/loan"
|
|
128
130
|
```
|
|
129
131
|
|
|
130
132
|
---
|
|
@@ -138,7 +140,7 @@ Score a scenario using an SCR container model.
|
|
|
138
140
|
- `scenario` (string | object | array, optional): Input values
|
|
139
141
|
|
|
140
142
|
**Usage:**
|
|
141
|
-
- Run
|
|
143
|
+
- Run scrDescribe first to inspect expected inputs
|
|
142
144
|
- Omit scenario to get model metadata
|
|
143
145
|
|
|
144
146
|
**Example:**
|
|
@@ -243,7 +245,7 @@ find table cars in sashelp in sas
|
|
|
243
245
|
|
|
244
246
|
---
|
|
245
247
|
|
|
246
|
-
### table-
|
|
248
|
+
### table-describe
|
|
247
249
|
|
|
248
250
|
Return metadata about a table including columns, types, and statistics.
|
|
249
251
|
|
|
@@ -262,7 +264,7 @@ Return metadata about a table including columns, types, and statistics.
|
|
|
262
264
|
|
|
263
265
|
**Example:**
|
|
264
266
|
```
|
|
265
|
-
table-
|
|
267
|
+
table-describe table=cars lib=Public
|
|
266
268
|
describe table air in lib sashelp on sas server
|
|
267
269
|
```
|
|
268
270
|
|
|
@@ -320,7 +322,7 @@ sasquery table=mylib.students query="How many students in each year as percentag
|
|
|
320
322
|
|
|
321
323
|
---
|
|
322
324
|
|
|
323
|
-
##
|
|
325
|
+
## Jobs Model
|
|
324
326
|
|
|
325
327
|
### list-jobs
|
|
326
328
|
|
|
@@ -363,7 +365,7 @@ find job cars_job_v4
|
|
|
363
365
|
|
|
364
366
|
---
|
|
365
367
|
|
|
366
|
-
###
|
|
368
|
+
### job-score
|
|
367
369
|
|
|
368
370
|
Execute a job on a SAS Viya server.
|
|
369
371
|
|
|
@@ -377,12 +379,14 @@ Execute a job on a SAS Viya server.
|
|
|
377
379
|
**Example:**
|
|
378
380
|
```
|
|
379
381
|
run job xyz param1=10,param2=val2
|
|
380
|
-
|
|
382
|
+
job-score myjob scenario a=10,b=20
|
|
381
383
|
job myjob scenario a=10,b=20
|
|
382
384
|
```
|
|
383
385
|
|
|
384
386
|
---
|
|
385
387
|
|
|
388
|
+
## Jobdef Models
|
|
389
|
+
|
|
386
390
|
### list-jobdefs
|
|
387
391
|
|
|
388
392
|
Enumerate SAS Viya job definition assets.
|
|
@@ -422,7 +426,7 @@ find jobdef metricsRefresh
|
|
|
422
426
|
|
|
423
427
|
---
|
|
424
428
|
|
|
425
|
-
###
|
|
429
|
+
### jobdef-score
|
|
426
430
|
|
|
427
431
|
Execute a job definition on a SAS Viya server.
|
|
428
432
|
|
|
@@ -435,15 +439,15 @@ Execute a job definition on a SAS Viya server.
|
|
|
435
439
|
|
|
436
440
|
**Example:**
|
|
437
441
|
```
|
|
438
|
-
|
|
442
|
+
jobdef-score xyz param1=10,param2=val2
|
|
439
443
|
jobdef myjobdef scenario a=10,b=20
|
|
440
444
|
```
|
|
441
445
|
|
|
442
446
|
---
|
|
443
447
|
|
|
444
|
-
## Program
|
|
448
|
+
## Program Models
|
|
445
449
|
|
|
446
|
-
###
|
|
450
|
+
### program-score
|
|
447
451
|
|
|
448
452
|
Execute arbitrary SAS code or stored programs on a SAS Viya server.
|
|
449
453
|
|
|
@@ -469,7 +473,9 @@ program sample folder=/Public/models scenario="name='John', age=45" output=a
|
|
|
469
473
|
|
|
470
474
|
---
|
|
471
475
|
|
|
472
|
-
|
|
476
|
+
## Macro Models
|
|
477
|
+
|
|
478
|
+
### macro-score
|
|
473
479
|
|
|
474
480
|
Submit and execute a SAS macro on a SAS Viya server.
|
|
475
481
|
|
|
@@ -478,8 +484,8 @@ Submit and execute a SAS macro on a SAS Viya server.
|
|
|
478
484
|
- `scenario` (string, optional): Parameters or SAS setup code
|
|
479
485
|
|
|
480
486
|
**Scenario formats:**
|
|
481
|
-
- Comma-separated: `"x=1, y=abc"`
|
|
482
|
-
- Raw SAS: `"%let x=1; %let y=abc;"`
|
|
487
|
+
- Comma-separated: `"x=1, y=abc"` → converted to %let statements
|
|
488
|
+
- Raw SAS: `"%let x=1; %let y=abc;"` → passed through unchanged
|
|
483
489
|
|
|
484
490
|
**Example:**
|
|
485
491
|
```
|
|
@@ -520,14 +526,14 @@ Use this to verify that the mcp server is up and running.
|
|
|
520
526
|
|
|
521
527
|
### deva-score
|
|
522
528
|
|
|
523
|
-
Compute a numeric score based on two input values using the formula: (a + b)
|
|
529
|
+
Compute a numeric score based on two input values using the formula: (a + b) × 42
|
|
524
530
|
|
|
525
531
|
**Parameters:**
|
|
526
532
|
- `a` (number, required): First numeric input
|
|
527
533
|
- `b` (number, required): Second numeric input
|
|
528
534
|
|
|
529
535
|
**Returns:**
|
|
530
|
-
- Numeric result: (a + b)
|
|
536
|
+
- Numeric result: (a + b) × 42
|
|
531
537
|
|
|
532
538
|
**Usage:**
|
|
533
539
|
- "Calculate deva score for 5 and 10"
|
|
@@ -545,11 +551,13 @@ deva-score a=1 b=2 // returns 126
|
|
|
545
551
|
|
|
546
552
|
| Category | Tool Count | Tools |
|
|
547
553
|
|----------|-----------|-------|
|
|
548
|
-
| **Model Management** | 6 | list-
|
|
554
|
+
| **Model Management** | 6 | list-mas, find-mas, mas-describe, mas-score, scr-describe, scr-score |
|
|
549
555
|
| **Library Management** | 2 | list-libraries, find-library |
|
|
550
|
-
| **Table Operations** | 5 | list-tables, find-table, table-
|
|
551
|
-
| **
|
|
552
|
-
| **
|
|
556
|
+
| **Table Operations** | 5 | list-tables, find-table, table-describe, read-table, sas-query |
|
|
557
|
+
| **Jobs Model** | 3 | list-jobs, find-job, job-score |
|
|
558
|
+
| **Jobdef Models** | 3 | list-jobdefs, find-jobdef, jobdef-score |
|
|
559
|
+
| **Program Models** | 1 | program-score |
|
|
560
|
+
| **Macro Models** | 1 | macro-score |
|
|
553
561
|
| **Context & Config** | 1 | set-context |
|
|
554
562
|
| **Utilities** | 1 | deva-score |
|
|
555
563
|
| **Total** | **24** | |
|
|
@@ -558,7 +566,7 @@ deva-score a=1 b=2 // returns 126
|
|
|
558
566
|
|
|
559
567
|
## Common Patterns
|
|
560
568
|
|
|
561
|
-
### Discovery
|
|
569
|
+
### Discovery → Inspection → Action
|
|
562
570
|
|
|
563
571
|
1. **List** tools to discover available resources
|
|
564
572
|
2. **Find** tools to locate specific items
|
|
@@ -598,3 +606,6 @@ Tools accepting scenarios support multiple formats:
|
|
|
598
606
|
|
|
599
607
|
*Document generated for @sassoftware/mcp-serverjs*
|
|
600
608
|
*Last updated: December 2024*
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import matplotlib
|
|
2
|
+
matplotlib.use('Agg')
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
data = [
|
|
7
|
+
("Cadillac", 50474.375),
|
|
8
|
+
("Hummer", 49995.0),
|
|
9
|
+
("Lincoln", 42875.555556),
|
|
10
|
+
("Buick", 30537.777778),
|
|
11
|
+
("GMC", 29560.5),
|
|
12
|
+
("Mercury", 27972.777778),
|
|
13
|
+
("Chrysler", 27252.0),
|
|
14
|
+
("Chevrolet", 26587.037037),
|
|
15
|
+
("Dodge", 26253.846154),
|
|
16
|
+
("Jeep", 24518.333333),
|
|
17
|
+
("Pontiac", 24156.363636),
|
|
18
|
+
("Ford", 24015.869565),
|
|
19
|
+
("Oldsmobile", 23763.333333),
|
|
20
|
+
("Saturn", 17234.375),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Sort by value descending
|
|
24
|
+
data.sort(key=lambda x: x[1], reverse=True)
|
|
25
|
+
makes = [d[0] for d in data]
|
|
26
|
+
values = [d[1] for d in data]
|
|
27
|
+
|
|
28
|
+
out_dir = Path('outputs')
|
|
29
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
out_file = out_dir / 'make_avg_msrp_usa.png'
|
|
31
|
+
|
|
32
|
+
plt.figure(figsize=(12, 6))
|
|
33
|
+
bars = plt.bar(makes, values, color='tab:blue')
|
|
34
|
+
plt.title('Average MSRP by Make (Origin = USA)')
|
|
35
|
+
plt.ylabel('Average MSRP')
|
|
36
|
+
plt.xticks(rotation=45, ha='right')
|
|
37
|
+
plt.tight_layout()
|
|
38
|
+
|
|
39
|
+
# Annotate bars
|
|
40
|
+
for bar in bars:
|
|
41
|
+
height = bar.get_height()
|
|
42
|
+
plt.annotate(f'{height:,.0f}',
|
|
43
|
+
xy=(bar.get_x() + bar.get_width() / 2, height),
|
|
44
|
+
xytext=(0, 3),
|
|
45
|
+
textcoords='offset points',
|
|
46
|
+
ha='center', va='bottom', fontsize=8)
|
|
47
|
+
|
|
48
|
+
plt.savefig(out_file)
|
|
49
|
+
print(f'Plot saved to {out_file}')
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { Agent, fetch } from 'undici';
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
|
|
8
|
+
refreshToken()
|
|
9
|
+
.then (token => {
|
|
10
|
+
console.log(token);
|
|
11
|
+
fs.writeFileSync('token.txt', `"AUTHORIZATION": "Bearer ${token}"`, 'utf8');
|
|
12
|
+
})
|
|
13
|
+
.catch (err => {
|
|
14
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
15
|
+
});
|
|
16
|
+
async function refreshToken(){
|
|
17
|
+
let host = process.env.VIYA_SERVER;
|
|
18
|
+
let token = process.env.REFRESH_TOKEN;
|
|
19
|
+
let url = `${host}/SASLogon/oauth/token`;
|
|
20
|
+
|
|
21
|
+
let aconnect = {
|
|
22
|
+
rejectUnauthorized: false // or false, if you really want to bypass checks
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const agent = new Agent(aconnect);
|
|
26
|
+
|
|
27
|
+
console.error('[Info] Refreshing token...', token);
|
|
28
|
+
const ibody = {
|
|
29
|
+
grant_type: 'refresh_token',
|
|
30
|
+
refresh_token: token,
|
|
31
|
+
client_id: 'sas.cli'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let body = new URLSearchParams(ibody);
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(url, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Accept': 'application/json',
|
|
40
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
41
|
+
dispatcher: agent
|
|
42
|
+
},
|
|
43
|
+
body: body.toString()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = await response.text();
|
|
48
|
+
console.error('[Error] Failed to refresh token: ', error);
|
|
49
|
+
throw new Error(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
return data.access_token;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('[Error] Failed to refresh token: ', err);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import listScr from '../src/toolSet/listScr.js';
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
const appContext = { brand: 'sas-score' };
|
|
6
|
+
const spec = listScr(appContext);
|
|
7
|
+
try {
|
|
8
|
+
const res = await spec.handler({});
|
|
9
|
+
console.log(JSON.stringify(res, null, 2));
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.error('Error running listScr:', err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
run();
|
package/src/createMcpServer.js
CHANGED
|
@@ -78,6 +78,7 @@ async function createMcpServer(cache, _appContext) {
|
|
|
78
78
|
// Register the tools with brand prefix
|
|
79
79
|
console.error(`[Note] Brand: ${_appContext.brand}`);
|
|
80
80
|
let toolNames = [];
|
|
81
|
+
let totalDescription = 0;
|
|
81
82
|
toolSet.forEach((tool, i) => {
|
|
82
83
|
let toolName = _appContext.brand + '-' + tool.name;
|
|
83
84
|
//tool.inputSchema.additionalProperties = false; // disallow extra properties
|
|
@@ -85,12 +86,14 @@ async function createMcpServer(cache, _appContext) {
|
|
|
85
86
|
description: tool.description,
|
|
86
87
|
inputSchema: tool.inputSchema
|
|
87
88
|
}
|
|
89
|
+
totalDescription += tool.description.length;``
|
|
88
90
|
let toolHandler = wrapf(cache, tool.handler);
|
|
89
|
-
// console.error(`[Note] Registering tool ${toolName} with config: ${JSON.stringify(config)}`);
|
|
90
91
|
let r = mcpServer.registerTool(toolName, config, toolHandler);
|
|
91
92
|
toolNames.push(toolName);
|
|
92
93
|
});
|
|
94
|
+
console.error(`[Note] Agent Mode: ${_appContext.agent}`);
|
|
93
95
|
console.error(`[Note] Registered ${toolSet.length} tools: ${toolNames}`);
|
|
96
|
+
console.error(`[Note] Total description length: ${totalDescription} characters`);
|
|
94
97
|
cache.set("mcpServer", mcpServer);
|
|
95
98
|
return mcpServer;
|
|
96
99
|
}
|
package/src/expressMcpServer.js
CHANGED
|
@@ -9,6 +9,7 @@ import cors from "cors";
|
|
|
9
9
|
import bodyParser from "body-parser";
|
|
10
10
|
import selfsigned from "selfsigned";
|
|
11
11
|
import openAPIJson from "./openAPIJson.js";
|
|
12
|
+
import createMcpServer from "./createMcpServer.js";
|
|
12
13
|
|
|
13
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
15
|
import { randomUUID } from "node:crypto";
|
|
@@ -21,7 +22,7 @@ import processHeaders from "./processHeaders.js";
|
|
|
21
22
|
|
|
22
23
|
// setup express server
|
|
23
24
|
|
|
24
|
-
async function expressMcpServer(
|
|
25
|
+
async function expressMcpServer(_mcpServer, cache, baseAppEnvContext) {
|
|
25
26
|
// setup for change to persistence session
|
|
26
27
|
cache.del("headerCache");
|
|
27
28
|
const app = express();
|
|
@@ -50,6 +51,10 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
50
51
|
const pkceStore = new Map(); // ourState -> { codeVerifier, clientRedirectUri, clientState }
|
|
51
52
|
const codeStore = new Map(); // ourCode -> { access_token, refresh_token, expires_in }
|
|
52
53
|
|
|
54
|
+
// Per-session transports — each initialize creates its own transport
|
|
55
|
+
const transports = new Map(); // sessionId -> transport
|
|
56
|
+
cache.set("transports", transports);
|
|
57
|
+
|
|
53
58
|
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
|
54
59
|
let payload = {
|
|
55
60
|
resource: `${baseAppEnvContext.mcpHost}/mcp`,
|
|
@@ -152,67 +157,54 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
152
157
|
|
|
153
158
|
// process mcp endpoint requests
|
|
154
159
|
const handleRequest = async (req, res) => {
|
|
155
|
-
let transport = null;
|
|
156
|
-
let transports = cache.get("transports");
|
|
157
160
|
console.error("=========================================================");
|
|
158
161
|
console.error("Processing POST /mcp request");
|
|
159
|
-
|
|
160
|
-
console.error("[Error] ***** transports cache is null. This is an error");
|
|
161
|
-
transports = {};
|
|
162
|
-
cache.set("transports", transports);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
console.error("current transports in cache:", Object.keys(transports));
|
|
162
|
+
console.error("current active sessions:", transports.size);
|
|
166
163
|
try {
|
|
167
164
|
|
|
168
165
|
let sessionId = req.headers["mcp-session-id"];
|
|
169
166
|
console.error("[Note]Incoming session ID:", sessionId);
|
|
170
167
|
let body = (req.body == null) ? 'no body' : JSON.stringify(req.body);
|
|
171
168
|
console.error('[Note] Payload is ', body);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.error("[Note]
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
|
|
170
|
+
if (isInitializeRequest(req.body)) {
|
|
171
|
+
console.error("[Note]>>>>>>>>>>>>>>>>>>>>>>>>> Creating new MCP server");
|
|
172
|
+
let mcpServer = await createMcpServer(cache, baseAppEnvContext);
|
|
173
|
+
// New session — create a dedicated transport
|
|
174
|
+
console.error("[Note] Initializing new session with fresh transport...");
|
|
175
|
+
const isInitRequest = req.body?.method === 'initialize';
|
|
176
|
+
|
|
177
|
+
const transport = new StreamableHTTPServerTransport({
|
|
177
178
|
sessionIdGenerator: () => randomUUID(),
|
|
178
179
|
enableJsonResponse: true,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
console.error("[Note]
|
|
184
|
-
transports[sessionId] = transport;
|
|
180
|
+
onsessioninitialized: (newSessionId) => {
|
|
181
|
+
console.error("[Note] Session initialized with ID:", newSessionId);
|
|
182
|
+
transports.set(newSessionId, transport);
|
|
183
|
+
cache.set("transports", transports);
|
|
184
|
+
console.error("[Note] Total active sessions:", Object.keys(transports));
|
|
185
185
|
},
|
|
186
186
|
});
|
|
187
|
-
// Clean up transport when closed
|
|
188
187
|
transport.onclose = () => {
|
|
189
|
-
if (transport.sessionId
|
|
190
|
-
|
|
188
|
+
if (transport.sessionId) {
|
|
189
|
+
console.error("[Note] Session closed, removing transport:", transport.sessionId);
|
|
190
|
+
transports.delete(transport.sessionId);
|
|
191
|
+
cache.del(transport.sessionId);
|
|
191
192
|
}
|
|
192
193
|
};
|
|
193
|
-
console.error("[Note] Connecting mcpServer to new transport...");
|
|
194
194
|
await mcpServer.connect(transport);
|
|
195
|
-
|
|
196
|
-
// Save transport data and app context for use in tools
|
|
197
|
-
console.error('[Note] Connected to mcpServer');
|
|
198
|
-
cache.set("transports", transports);
|
|
199
195
|
console.error("=======================================================");
|
|
200
196
|
return await transport.handleRequest(req, res, req.body);
|
|
201
197
|
|
|
202
|
-
// cache transport
|
|
203
|
-
|
|
204
198
|
} else if (sessionId != null) {
|
|
205
|
-
|
|
206
|
-
transport
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// this can happen if client is holding on to old session id
|
|
210
|
-
console.error("[Error] No transport found for session ID:", sessionId, "Returning a 404 error with instructions for the user");
|
|
211
|
-
res.status(404).send(`Invalid or missing session ID ${sessionId}. Please ensure your MCP client is configured to use the correct session ID returned in the 'mcp-session-id' header of the response from the /mcp endpoint.`);
|
|
212
|
-
return;
|
|
199
|
+
const transport = transports.get(sessionId);
|
|
200
|
+
if (!transport) {
|
|
201
|
+
console.error("[Note] Unknown session ID:", sessionId);
|
|
202
|
+
return res.status(404).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Please re-initialize." }, id: null });
|
|
213
203
|
}
|
|
204
|
+
console.error('[Note] Incoming session ID:', sessionId);
|
|
205
|
+
console.error("[Note] Using transport for session ID:", sessionId);
|
|
214
206
|
|
|
215
|
-
// post the
|
|
207
|
+
// post the current session - used to pass _appContext to tools
|
|
216
208
|
cache.set("currentId", sessionId);
|
|
217
209
|
|
|
218
210
|
// get app context for session
|
|
@@ -230,7 +222,6 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
230
222
|
_appContext = Object.assign(_appContext, headerCache);
|
|
231
223
|
cache.set(sessionId, _appContext);
|
|
232
224
|
}
|
|
233
|
-
console.error("[Note] Using existing transport for session ID:", sessionId);
|
|
234
225
|
console.error("==========================================================");
|
|
235
226
|
await transport.handleRequest(req, res, req.body);
|
|
236
227
|
return;
|
|
@@ -260,21 +251,28 @@ async function expressMcpServer(mcpServer, cache, baseAppEnvContext) {
|
|
|
260
251
|
const sessionId = req.headers["mcp-session-id"];
|
|
261
252
|
console.error("[Note] SessionId:", sessionId);
|
|
262
253
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (!sessionId || transport == null) {
|
|
267
|
-
res.status(404).send(`[Error] In ${req.method}: Invalid or missing session ID ${sessionId}`);
|
|
268
|
-
return;
|
|
254
|
+
if (!sessionId) {
|
|
255
|
+
console.error("[Note] No session ID on /DELETE — rejecting");
|
|
256
|
+
return res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: Mcp-Session-Id header is required" }, id: null });
|
|
269
257
|
}
|
|
258
|
+
|
|
259
|
+
const transport = transports.get(sessionId);
|
|
260
|
+
if (!transport) {
|
|
261
|
+
console.error("[Note] Unknown session ID:", sessionId);
|
|
262
|
+
return res.status(404).json({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Please re-initialize." }, id: null });
|
|
263
|
+
}
|
|
264
|
+
|
|
270
265
|
if (req.method === "GET") {
|
|
266
|
+
console.error("[Note] calling transport.handleRequest for GET /mcp");
|
|
271
267
|
await transport.handleRequest(req, res);
|
|
272
268
|
return;
|
|
273
269
|
}
|
|
274
|
-
if (req.method === "DELETE"
|
|
270
|
+
if (req.method === "DELETE") {
|
|
275
271
|
console.error("[Note] Deleting transport and cache for session ID:", sessionId);
|
|
276
|
-
delete
|
|
272
|
+
transports.delete(sessionId);
|
|
277
273
|
cache.del(sessionId);
|
|
274
|
+
console.error("[Note] Deleted session ID:", sessionId);
|
|
275
|
+
console.error("[Note] Total active sessions:", Object.keys(transports));
|
|
278
276
|
res.status(201).send(`[Info] Deleted session ${sessionId}`);
|
|
279
277
|
}
|
|
280
278
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2026, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
1
5
|
function token(req, res, appContext, codeStore, cache) {
|
|
2
6
|
console.error("===============================================================");
|
|
3
7
|
console.error("[Note] at /token endpoint");
|