@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/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
- /* @ts-ignore */
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
- sessions;
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.sessions.values()) {
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
- let sessionEntry = undefined;
61
- if (sessionIdHeader && this.sessions.has(sessionIdHeader)) {
62
- logger_1.LOGGER.debug("Request received - Session ID", sessionIdHeader);
63
- sessionEntry = this.sessions.get(sessionIdHeader);
64
- }
65
- else if (!sessionIdHeader && (0, types_js_1.isInitializeRequest)(req.body)) {
66
- logger_1.LOGGER.debug("Initialize session request received");
67
- const server = (0, factory_1.createMcpServer)(this.config, this.annotations);
68
- const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
69
- sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
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.0",
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.11.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 ./src"
69
+ "prettier ./src --check"
58
70
  ],
59
71
  "*.js": [
60
72
  "eslint",
61
- "prettier --check ./src"
73
+ "prettier ./src --check"
62
74
  ]
63
75
  },
64
76
  "workspaces": [