@gavdi/cap-mcp 0.9.0 → 0.9.2
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/lib/annotations/constants.js +22 -4
- package/lib/annotations/parser.js +41 -3
- package/lib/annotations/structures.js +111 -0
- package/lib/annotations/utils.js +55 -0
- package/lib/config/env-sanitizer.js +78 -0
- package/lib/config/json-parser.js +165 -0
- package/lib/config/loader.js +20 -12
- package/lib/logger.js +8 -0
- package/lib/mcp/constants.js +16 -0
- package/lib/mcp/custom-resource-template.js +156 -0
- package/lib/mcp/customResourceTemplate.js +156 -0
- package/lib/mcp/factory.js +21 -17
- package/lib/mcp/prompts.js +12 -2
- package/lib/mcp/resources.js +89 -25
- package/lib/mcp/session-manager.js +92 -0
- package/lib/mcp/tools.js +82 -13
- package/lib/mcp/utils.js +12 -3
- package/lib/mcp/validation.js +318 -0
- package/lib/mcp.js +70 -42
- package/package.json +18 -6
package/lib/mcp.js
CHANGED
@@ -3,89 +3,102 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
7
6
|
const logger_1 = require("./logger");
|
8
|
-
const crypto_1 = require("crypto");
|
9
7
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
10
8
|
const express_1 = __importDefault(require("express"));
|
11
9
|
const parser_1 = require("./annotations/parser");
|
12
10
|
const utils_1 = require("./mcp/utils");
|
13
|
-
const factory_1 = require("./mcp/factory");
|
14
11
|
const constants_1 = require("./mcp/constants");
|
15
12
|
const loader_1 = require("./config/loader");
|
16
|
-
|
17
|
-
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
13
|
+
const session_manager_1 = require("./mcp/session-manager");
|
18
14
|
// TODO: Handle auth
|
15
|
+
/**
|
16
|
+
* Main MCP plugin class that integrates CAP services with Model Context Protocol
|
17
|
+
* Manages server sessions, API endpoints, and annotation processing
|
18
|
+
*/
|
19
19
|
class McpPlugin {
|
20
|
-
|
20
|
+
sessionManager;
|
21
21
|
config;
|
22
22
|
expressApp;
|
23
23
|
annotations;
|
24
|
+
/**
|
25
|
+
* Creates a new MCP plugin instance with configuration and session management
|
26
|
+
*/
|
24
27
|
constructor() {
|
25
28
|
logger_1.LOGGER.debug("Plugin instance created");
|
26
|
-
this.sessions = new Map();
|
27
29
|
this.config = (0, loader_1.loadConfiguration)();
|
30
|
+
this.sessionManager = new session_manager_1.McpSessionManager();
|
31
|
+
logger_1.LOGGER.debug("Running with configuration", this.config);
|
28
32
|
}
|
33
|
+
/**
|
34
|
+
* Handles the bootstrap event by setting up Express app and API endpoints
|
35
|
+
* @param app - Express application instance
|
36
|
+
*/
|
29
37
|
async onBootstrap(app) {
|
30
38
|
logger_1.LOGGER.debug("Event received for 'bootstrap'");
|
31
39
|
this.expressApp = app;
|
32
40
|
this.expressApp.use(express_1.default.json());
|
33
|
-
logger_1.LOGGER.debug("WHAT IS AUTH?", cds.env.requires.auth);
|
34
|
-
logger_1.LOGGER.debug("WHAT IS THIS?!", cds.middlewares.auth);
|
35
41
|
await this.registerApiEndpoints();
|
36
42
|
logger_1.LOGGER.debug("Bootstrap complete");
|
37
43
|
}
|
44
|
+
/**
|
45
|
+
* Handles the loaded event by parsing model definitions for MCP annotations
|
46
|
+
* @param model - CSN model containing definitions
|
47
|
+
*/
|
38
48
|
async onLoaded(model) {
|
39
49
|
logger_1.LOGGER.debug("Event received for 'loaded'");
|
40
50
|
this.annotations = (0, parser_1.parseDefinitions)(model);
|
41
51
|
logger_1.LOGGER.debug("Annotations have been loaded");
|
42
52
|
}
|
53
|
+
/**
|
54
|
+
* Handles the shutdown event by gracefully closing all MCP server sessions
|
55
|
+
*/
|
43
56
|
async onShutdown() {
|
44
57
|
logger_1.LOGGER.debug("Gracefully shutting down MCP server");
|
45
|
-
for (const session of this.
|
58
|
+
for (const session of this.sessionManager.getSessions().values()) {
|
59
|
+
await session.transport.close();
|
46
60
|
await session.server.close();
|
47
61
|
}
|
48
62
|
logger_1.LOGGER.debug("MCP server sessions has been shutdown");
|
49
63
|
}
|
64
|
+
/**
|
65
|
+
* Sets up HTTP endpoints for MCP communication and health checks
|
66
|
+
* Registers /mcp and /mcp/health routes with appropriate handlers
|
67
|
+
*/
|
50
68
|
async registerApiEndpoints() {
|
69
|
+
if (!this.expressApp) {
|
70
|
+
logger_1.LOGGER.warn("Cannot register MCP server as there is no available express layer");
|
71
|
+
return;
|
72
|
+
}
|
51
73
|
logger_1.LOGGER.debug("Registering health endpoint for MCP");
|
52
74
|
this.expressApp?.get("/mcp/health", (_, res) => {
|
53
75
|
res.json({
|
54
76
|
status: "UP",
|
55
77
|
});
|
56
78
|
});
|
79
|
+
logger_1.LOGGER.debug("TESTING - Annotations", this.annotations);
|
80
|
+
this.registerMcpSessionRoute();
|
81
|
+
this.expressApp?.get("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessionManager.getSessions()));
|
82
|
+
this.expressApp?.delete("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessionManager.getSessions()));
|
83
|
+
}
|
84
|
+
/**
|
85
|
+
* Registers the main MCP POST endpoint for session creation and request handling
|
86
|
+
* Handles session initialization and routes requests to appropriate sessions
|
87
|
+
*/
|
88
|
+
registerMcpSessionRoute() {
|
57
89
|
logger_1.LOGGER.debug("Registering MCP entry point");
|
58
90
|
this.expressApp?.post("/mcp", async (req, res) => {
|
59
91
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
}
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
onsessioninitialized: (sid) => {
|
71
|
-
this.sessions.set(sid, {
|
72
|
-
server: server,
|
73
|
-
transport: transport,
|
74
|
-
});
|
75
|
-
},
|
76
|
-
});
|
77
|
-
transport.onclose = () => {
|
78
|
-
if (!transport.sessionId || !this.sessions.has(transport.sessionId))
|
79
|
-
return;
|
80
|
-
this.sessions.delete(transport.sessionId);
|
81
|
-
};
|
82
|
-
await server.connect(transport);
|
83
|
-
sessionEntry = {
|
84
|
-
server: server,
|
85
|
-
transport: transport,
|
86
|
-
};
|
87
|
-
}
|
88
|
-
else {
|
92
|
+
logger_1.LOGGER.debug("MCP request received", {
|
93
|
+
hasSessionId: !!sessionIdHeader,
|
94
|
+
isInitialize: (0, types_js_1.isInitializeRequest)(req.body),
|
95
|
+
contentType: req.headers["content-type"],
|
96
|
+
});
|
97
|
+
const session = !sessionIdHeader && (0, types_js_1.isInitializeRequest)(req.body)
|
98
|
+
? await this.sessionManager.createSession(this.config, this.annotations)
|
99
|
+
: this.sessionManager.getSession(sessionIdHeader);
|
100
|
+
if (!session) {
|
101
|
+
logger_1.LOGGER.error("Invalid session ID", sessionIdHeader);
|
89
102
|
res.status(400).json({
|
90
103
|
jsonrpc: "2.0",
|
91
104
|
error: {
|
@@ -94,11 +107,26 @@ class McpPlugin {
|
|
94
107
|
id: null,
|
95
108
|
},
|
96
109
|
});
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
try {
|
113
|
+
await session.transport.handleRequest(req, res, req.body);
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
catch (e) {
|
117
|
+
if (res.headersSent)
|
118
|
+
return;
|
119
|
+
res.status(500).json({
|
120
|
+
jsonrpc: "2.0",
|
121
|
+
error: {
|
122
|
+
code: -32603,
|
123
|
+
message: "Internal Error: Transport failed",
|
124
|
+
id: null,
|
125
|
+
},
|
126
|
+
});
|
127
|
+
return;
|
97
128
|
}
|
98
|
-
await sessionEntry?.transport.handleRequest(req, res, req.body);
|
99
129
|
});
|
100
|
-
this.expressApp?.get("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessions));
|
101
|
-
this.expressApp?.delete("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessions));
|
102
130
|
}
|
103
131
|
}
|
104
132
|
exports.default = McpPlugin;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@gavdi/cap-mcp",
|
3
|
-
"version": "0.9.
|
3
|
+
"version": "0.9.2",
|
4
4
|
"description": "MCP Pluging for CAP",
|
5
5
|
"keywords": [
|
6
6
|
"MCP",
|
@@ -22,9 +22,15 @@
|
|
22
22
|
"repository": "github:gavdilabs/cap-mcp-plugin",
|
23
23
|
"scripts": {
|
24
24
|
"mock": "npm run start --workspace=test/demo",
|
25
|
-
"test": "jest",
|
25
|
+
"test": "NODE_ENV=test jest --silent",
|
26
|
+
"test:unit": "NODE_ENV=test jest --silent test/unit",
|
27
|
+
"test:integration": "NODE_ENV=test jest --silent test/integration",
|
26
28
|
"build": "tsc",
|
27
|
-
"inspect": "npx @modelcontextprotocol/inspector"
|
29
|
+
"inspect": "npx @modelcontextprotocol/inspector",
|
30
|
+
"prepare": "husky",
|
31
|
+
"lint": "eslint",
|
32
|
+
"lint-staged": "lint-staged",
|
33
|
+
"format": "prettier --check ./src"
|
28
34
|
},
|
29
35
|
"peerDependencies": {
|
30
36
|
"@sap/cds": "^9",
|
@@ -36,16 +42,22 @@
|
|
36
42
|
"zod-to-json-schema": "^3.24.5"
|
37
43
|
},
|
38
44
|
"devDependencies": {
|
39
|
-
"@cap-js/cds-types": "^0.
|
45
|
+
"@cap-js/cds-types": "^0.12.0",
|
40
46
|
"@types/express": "^5.0.3",
|
47
|
+
"@types/jest": "^30.0.0",
|
41
48
|
"@types/node": "^24.0.3",
|
49
|
+
"@types/sinon": "^17.0.4",
|
50
|
+
"@types/supertest": "^6.0.2",
|
42
51
|
"eslint": "^9.29.0",
|
43
52
|
"husky": "^9.1.7",
|
44
53
|
"jest": "^29.7.0",
|
45
54
|
"lint-staged": "^16.1.2",
|
46
55
|
"prettier": "^3.5.3",
|
47
56
|
"release-it": "^19.0.3",
|
57
|
+
"sinon": "^21.0.0",
|
58
|
+
"supertest": "^7.0.0",
|
48
59
|
"ts-jest": "^29.4.0",
|
60
|
+
"ts-node": "^10.9.2",
|
49
61
|
"typescript": "^5.8.3"
|
50
62
|
},
|
51
63
|
"lint-staged": {
|
@@ -54,11 +66,11 @@
|
|
54
66
|
],
|
55
67
|
"*.ts": [
|
56
68
|
"eslint",
|
57
|
-
"prettier --check
|
69
|
+
"prettier ./src --check"
|
58
70
|
],
|
59
71
|
"*.js": [
|
60
72
|
"eslint",
|
61
|
-
"prettier --check
|
73
|
+
"prettier ./src --check"
|
62
74
|
]
|
63
75
|
},
|
64
76
|
"workspaces": [
|