@gavdi/cap-mcp 0.9.4 → 0.9.8
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/README.md +38 -1
- package/cds-plugin.js +1 -1
- package/lib/annotations/constants.js +11 -1
- package/lib/annotations/parser.js +25 -7
- package/lib/annotations/structures.js +29 -6
- package/lib/annotations/utils.js +62 -0
- package/lib/auth/utils.js +60 -0
- package/lib/config/loader.js +8 -0
- package/lib/logger.js +48 -2
- package/lib/mcp/describe-model.js +103 -0
- package/lib/mcp/entity-tools.js +628 -0
- package/lib/mcp/factory.js +22 -2
- package/lib/mcp/resources.js +18 -3
- package/lib/mcp/session-manager.js +13 -2
- package/lib/mcp/tools.js +10 -18
- package/lib/mcp/utils.js +56 -0
- package/lib/mcp.js +24 -6
- package/package.json +11 -6
- package/lib/annotations.js +0 -257
- package/lib/auth/adapter.js +0 -2
- package/lib/auth/mock.js +0 -2
- package/lib/auth/types.js +0 -2
- package/lib/mcp/customResourceTemplate.js +0 -156
- package/lib/types.js +0 -2
- package/lib/utils.js +0 -136
package/lib/mcp/resources.js
CHANGED
|
@@ -6,9 +6,24 @@ const logger_1 = require("../logger");
|
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
7
|
const validation_1 = require("./validation");
|
|
8
8
|
const utils_2 = require("../auth/utils");
|
|
9
|
-
// import cds from "@sap/cds";
|
|
10
9
|
/* @ts-ignore */
|
|
11
10
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
11
|
+
async function resolveServiceInstance(serviceName) {
|
|
12
|
+
const CDS = global.cds || cds;
|
|
13
|
+
let svc = CDS.services?.[serviceName] || CDS.services?.[serviceName.toLowerCase()];
|
|
14
|
+
if (svc)
|
|
15
|
+
return svc;
|
|
16
|
+
const providers = (CDS.service && CDS.service.providers) ||
|
|
17
|
+
(CDS.services && CDS.services.providers) ||
|
|
18
|
+
[];
|
|
19
|
+
if (Array.isArray(providers)) {
|
|
20
|
+
const found = providers.find((p) => p?.definition?.name === serviceName || p?.name === serviceName);
|
|
21
|
+
if (found)
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
// do not connect; rely on served providers only to avoid duplicate cds contexts
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Registers a CAP entity as an MCP resource with optional OData query support
|
|
14
29
|
* Creates either static or dynamic resources based on configured functionalities
|
|
@@ -34,7 +49,7 @@ function assignResourceToServer(model, server, authEnabled) {
|
|
|
34
49
|
server.registerResource(model.name, template, // Type assertion to bypass strict type checking - necessary due to broken URI parser in the MCP SDK
|
|
35
50
|
{ title: model.target, description: detailedDescription }, async (uri, variables) => {
|
|
36
51
|
const queryParameters = variables;
|
|
37
|
-
const service =
|
|
52
|
+
const service = await resolveServiceInstance(model.serviceName);
|
|
38
53
|
if (!service) {
|
|
39
54
|
logger_1.LOGGER.error(`Invalid service found for service '${model.serviceName}'`);
|
|
40
55
|
throw new Error(`Invalid service found for service '${model.serviceName}'`);
|
|
@@ -56,7 +71,7 @@ function assignResourceToServer(model, server, authEnabled) {
|
|
|
56
71
|
case "filter":
|
|
57
72
|
// BUG: If filter value is e.g. "filter=1234" the value 1234 will go through
|
|
58
73
|
const validatedFilter = validator.validateFilter(v);
|
|
59
|
-
const expression = cds.parse.expr(validatedFilter);
|
|
74
|
+
const expression = (global.cds || cds).parse.expr(validatedFilter);
|
|
60
75
|
query.where(expression);
|
|
61
76
|
continue;
|
|
62
77
|
case "select":
|
|
@@ -63,18 +63,29 @@ class McpSessionManager {
|
|
|
63
63
|
* @returns Configured StreamableHTTPServerTransport instance
|
|
64
64
|
*/
|
|
65
65
|
createTransport(server) {
|
|
66
|
+
// Prefer JSON responses to avoid SSE client compatibility issues in dev/mock
|
|
67
|
+
const enableJson = (0, env_sanitizer_1.getSafeEnvVar)("MCP_ENABLE_JSON", "true") === "true" ||
|
|
68
|
+
(0, env_sanitizer_1.isTestEnvironment)();
|
|
66
69
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
67
70
|
sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
|
|
68
|
-
enableJsonResponse:
|
|
71
|
+
enableJsonResponse: enableJson,
|
|
69
72
|
onsessioninitialized: (sid) => {
|
|
70
73
|
logger_1.LOGGER.debug("Session initialized with ID: ", sid);
|
|
74
|
+
logger_1.LOGGER.debug("Transport mode", { enableJsonResponse: enableJson });
|
|
71
75
|
this.sessions.set(sid, {
|
|
72
76
|
server: server,
|
|
73
77
|
transport: transport,
|
|
74
78
|
});
|
|
75
79
|
},
|
|
76
80
|
});
|
|
77
|
-
|
|
81
|
+
// In JSON response mode, HTTP connections are short-lived per request.
|
|
82
|
+
// Closing the underlying connection does NOT mean the MCP session is over.
|
|
83
|
+
// Avoid deleting the session on close when enableJson is true.
|
|
84
|
+
transport.onclose = () => {
|
|
85
|
+
if (!enableJson) {
|
|
86
|
+
this.onCloseSession(transport);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
78
89
|
return transport;
|
|
79
90
|
}
|
|
80
91
|
/**
|
package/lib/mcp/tools.js
CHANGED
|
@@ -43,7 +43,10 @@ function assignBoundOperation(params, model, server, authEnabled) {
|
|
|
43
43
|
description: model.description,
|
|
44
44
|
inputSchema: inputSchema,
|
|
45
45
|
}, async (args) => {
|
|
46
|
-
|
|
46
|
+
// Resolve from current CAP context; prefer global to align with Jest mocks
|
|
47
|
+
const cdsMod = global.cds || cds;
|
|
48
|
+
const servicesMap = cdsMod.services || (cdsMod.services = {});
|
|
49
|
+
const service = servicesMap[model.serviceName];
|
|
47
50
|
if (!service) {
|
|
48
51
|
logger_1.LOGGER.error("Invalid CAP service - undefined");
|
|
49
52
|
return {
|
|
@@ -73,14 +76,7 @@ function assignBoundOperation(params, model, server, authEnabled) {
|
|
|
73
76
|
data: operationInput,
|
|
74
77
|
params: [operationKeys],
|
|
75
78
|
});
|
|
76
|
-
return
|
|
77
|
-
content: Array.isArray(response)
|
|
78
|
-
? response.map((el) => ({
|
|
79
|
-
type: "text",
|
|
80
|
-
text: formatResponseValue(el),
|
|
81
|
-
}))
|
|
82
|
-
: [{ type: "text", text: formatResponseValue(response) }],
|
|
83
|
-
};
|
|
79
|
+
return (0, utils_1.asMcpResult)(response);
|
|
84
80
|
});
|
|
85
81
|
}
|
|
86
82
|
/**
|
|
@@ -97,7 +93,10 @@ function assignUnboundOperation(params, model, server, authEnabled) {
|
|
|
97
93
|
description: model.description,
|
|
98
94
|
inputSchema: inputSchema,
|
|
99
95
|
}, async (args) => {
|
|
100
|
-
|
|
96
|
+
// Resolve from current CAP context; prefer global to align with Jest mocks
|
|
97
|
+
const cdsMod = global.cds || cds;
|
|
98
|
+
const servicesMap = cdsMod.services || (cdsMod.services = {});
|
|
99
|
+
const service = servicesMap[model.serviceName];
|
|
101
100
|
if (!service) {
|
|
102
101
|
logger_1.LOGGER.error("Invalid CAP service - undefined");
|
|
103
102
|
return {
|
|
@@ -114,14 +113,7 @@ function assignUnboundOperation(params, model, server, authEnabled) {
|
|
|
114
113
|
const response = await service
|
|
115
114
|
.tx({ user: accessRights })
|
|
116
115
|
.send(model.target, args);
|
|
117
|
-
return
|
|
118
|
-
content: Array.isArray(response)
|
|
119
|
-
? response.map((el) => ({
|
|
120
|
-
type: "text",
|
|
121
|
-
text: formatResponseValue(el),
|
|
122
|
-
}))
|
|
123
|
-
: [{ type: "text", text: formatResponseValue(response) }],
|
|
124
|
-
};
|
|
116
|
+
return (0, utils_1.asMcpResult)(response);
|
|
125
117
|
});
|
|
126
118
|
}
|
|
127
119
|
/**
|
package/lib/mcp/utils.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.determineMcpParameterType = determineMcpParameterType;
|
|
4
4
|
exports.handleMcpSessionRequest = handleMcpSessionRequest;
|
|
5
5
|
exports.writeODataDescriptionForResource = writeODataDescriptionForResource;
|
|
6
|
+
exports.toolError = toolError;
|
|
7
|
+
exports.asMcpResult = asMcpResult;
|
|
6
8
|
const constants_1 = require("./constants");
|
|
7
9
|
const zod_1 = require("zod");
|
|
8
10
|
/**
|
|
@@ -69,3 +71,57 @@ function writeODataDescriptionForResource(model) {
|
|
|
69
71
|
}
|
|
70
72
|
return description;
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Unified MCP tool error response helper
|
|
76
|
+
* Returns a consistent JSON error payload inside MCP content
|
|
77
|
+
*/
|
|
78
|
+
function toolError(code, message, extra) {
|
|
79
|
+
const payload = { error: code, message, ...(extra || {}) };
|
|
80
|
+
return {
|
|
81
|
+
isError: true,
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: JSON.stringify(payload),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Formats a payload as MCP result content with a single text part.
|
|
92
|
+
* This ensures compatibility with all MCP clients.
|
|
93
|
+
*/
|
|
94
|
+
function asMcpResult(payload) {
|
|
95
|
+
// Pretty-print for objects, stringify primitives, and split arrays into multiple parts
|
|
96
|
+
const toText = (value) => {
|
|
97
|
+
if (typeof value === "string")
|
|
98
|
+
return value;
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
return "undefined";
|
|
101
|
+
try {
|
|
102
|
+
if (value !== null && typeof value === "object") {
|
|
103
|
+
return JSON.stringify(value, null, 2);
|
|
104
|
+
}
|
|
105
|
+
return String(value);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Circular structures fall back to default string conversion
|
|
109
|
+
return String(value);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (Array.isArray(payload)) {
|
|
113
|
+
if (payload.length === 0)
|
|
114
|
+
return { content: [] };
|
|
115
|
+
return {
|
|
116
|
+
content: payload.map((item) => ({ type: "text", text: toText(item) })),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: toText(payload),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
package/lib/mcp.js
CHANGED
|
@@ -3,7 +3,6 @@ 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 cds_1 = __importDefault(require("@sap/cds"));
|
|
7
6
|
const logger_1 = require("./logger");
|
|
8
7
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
9
8
|
const express_1 = __importDefault(require("express"));
|
|
@@ -14,8 +13,7 @@ const loader_1 = require("./config/loader");
|
|
|
14
13
|
const session_manager_1 = require("./mcp/session-manager");
|
|
15
14
|
const utils_2 = require("./auth/utils");
|
|
16
15
|
/* @ts-ignore */
|
|
17
|
-
|
|
18
|
-
// TODO: Handle auth
|
|
16
|
+
const cds = global.cds; // Use hosting app's CDS instance exclusively
|
|
19
17
|
/**
|
|
20
18
|
* Main MCP plugin class that integrates CAP services with Model Context Protocol
|
|
21
19
|
* Manages server sessions, API endpoints, and annotation processing
|
|
@@ -41,7 +39,7 @@ class McpPlugin {
|
|
|
41
39
|
async onBootstrap(app) {
|
|
42
40
|
logger_1.LOGGER.debug("Event received for 'bootstrap'");
|
|
43
41
|
this.expressApp = app;
|
|
44
|
-
this.expressApp.use(express_1.default.json());
|
|
42
|
+
this.expressApp.use("/mcp", express_1.default.json());
|
|
45
43
|
if (this.config.auth === "inherit") {
|
|
46
44
|
(0, utils_2.registerAuthMiddleware)(this.expressApp);
|
|
47
45
|
}
|
|
@@ -85,7 +83,26 @@ class McpPlugin {
|
|
|
85
83
|
});
|
|
86
84
|
this.registerMcpSessionRoute();
|
|
87
85
|
this.expressApp?.get("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessionManager.getSessions()));
|
|
88
|
-
this.expressApp?.delete("/mcp", (req, res) =>
|
|
86
|
+
this.expressApp?.delete("/mcp", ((req, res) => {
|
|
87
|
+
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
|
88
|
+
const sessions = this.sessionManager.getSessions();
|
|
89
|
+
if (!sessionIdHeader || !sessions.has(sessionIdHeader)) {
|
|
90
|
+
return res.status(400).json({
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
error: {
|
|
93
|
+
code: -32000,
|
|
94
|
+
message: "Bad Request: No valid sessions ID provided",
|
|
95
|
+
id: null,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const session = sessions.get(sessionIdHeader);
|
|
100
|
+
// Fire-and-forget close operations
|
|
101
|
+
void session.transport.close();
|
|
102
|
+
void session.server.close();
|
|
103
|
+
sessions.delete(sessionIdHeader);
|
|
104
|
+
return res.status(200).json({ jsonrpc: "2.0", result: { closed: true } });
|
|
105
|
+
}));
|
|
89
106
|
}
|
|
90
107
|
/**
|
|
91
108
|
* Registers the main MCP POST endpoint for session creation and request handling
|
|
@@ -94,7 +111,6 @@ class McpPlugin {
|
|
|
94
111
|
registerMcpSessionRoute() {
|
|
95
112
|
logger_1.LOGGER.debug("Registering MCP entry point");
|
|
96
113
|
this.expressApp?.post("/mcp", async (req, res) => {
|
|
97
|
-
logger_1.LOGGER.debug("CONTEXT", cds_1.default.context); // TODO: Remove this line after testing
|
|
98
114
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
|
99
115
|
logger_1.LOGGER.debug("MCP request received", {
|
|
100
116
|
hasSessionId: !!sessionIdHeader,
|
|
@@ -117,7 +133,9 @@ class McpPlugin {
|
|
|
117
133
|
return;
|
|
118
134
|
}
|
|
119
135
|
try {
|
|
136
|
+
const t0 = Date.now();
|
|
120
137
|
await session.transport.handleRequest(req, res, req.body);
|
|
138
|
+
logger_1.LOGGER.debug("MCP request handled", { durationMs: Date.now() - t0 });
|
|
121
139
|
return;
|
|
122
140
|
}
|
|
123
141
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavdi/cap-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "MCP Pluging for CAP",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MCP",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"repository": "github:gavdilabs/cap-mcp-plugin",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"mock": "npm run start --workspace=test/demo",
|
|
25
|
+
"mock:watch": "npm run build && npm run watch --workspace=test/demo",
|
|
25
26
|
"test": "NODE_ENV=test jest --silent",
|
|
26
27
|
"test:unit": "NODE_ENV=test jest --silent test/unit",
|
|
27
28
|
"test:integration": "NODE_ENV=test jest --silent test/integration",
|
|
@@ -30,7 +31,10 @@
|
|
|
30
31
|
"prepare": "husky",
|
|
31
32
|
"lint": "eslint",
|
|
32
33
|
"lint-staged": "lint-staged",
|
|
33
|
-
"format": "prettier --check ./src"
|
|
34
|
+
"format": "prettier --check ./src",
|
|
35
|
+
"format:fix": "prettier --write ./src \"**/*.json\"",
|
|
36
|
+
"build:watch": "tsc --watch",
|
|
37
|
+
"release": "release-it"
|
|
34
38
|
},
|
|
35
39
|
"peerDependencies": {
|
|
36
40
|
"@sap/cds": "^9",
|
|
@@ -43,6 +47,7 @@
|
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
45
49
|
"@cap-js/cds-types": "^0.12.0",
|
|
50
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
46
51
|
"@types/express": "^5.0.3",
|
|
47
52
|
"@types/jest": "^30.0.0",
|
|
48
53
|
"@types/node": "^24.0.3",
|
|
@@ -53,7 +58,7 @@
|
|
|
53
58
|
"jest": "^29.7.0",
|
|
54
59
|
"lint-staged": "^16.1.2",
|
|
55
60
|
"prettier": "^3.5.3",
|
|
56
|
-
"release-it": "^19.0.
|
|
61
|
+
"release-it": "^19.0.4",
|
|
57
62
|
"sinon": "^21.0.0",
|
|
58
63
|
"supertest": "^7.0.0",
|
|
59
64
|
"ts-jest": "^29.4.0",
|
|
@@ -62,15 +67,15 @@
|
|
|
62
67
|
},
|
|
63
68
|
"lint-staged": {
|
|
64
69
|
"*.json": [
|
|
65
|
-
"prettier --
|
|
70
|
+
"prettier --write"
|
|
66
71
|
],
|
|
67
72
|
"*.ts": [
|
|
68
73
|
"eslint",
|
|
69
|
-
"prettier
|
|
74
|
+
"prettier --write"
|
|
70
75
|
],
|
|
71
76
|
"*.js": [
|
|
72
77
|
"eslint",
|
|
73
|
-
"prettier
|
|
78
|
+
"prettier --write"
|
|
74
79
|
]
|
|
75
80
|
},
|
|
76
81
|
"workspaces": [
|
package/lib/annotations.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.McpPromptAnnotation = exports.McpToolAnnotation = exports.McpResourceAnnotation = exports.McpAnnotation = exports.McpAnnotations = exports.McpAnnotationKey = void 0;
|
|
4
|
-
exports.parseAnnotations = parseAnnotations;
|
|
5
|
-
const utils_1 = require("./utils");
|
|
6
|
-
const DEFAULT_ALL_RESOURCE_OPTIONS = new Set([
|
|
7
|
-
"filter",
|
|
8
|
-
"sort",
|
|
9
|
-
"top",
|
|
10
|
-
"skip",
|
|
11
|
-
"select",
|
|
12
|
-
]);
|
|
13
|
-
exports.McpAnnotationKey = "@mcp";
|
|
14
|
-
exports.McpAnnotations = {
|
|
15
|
-
// Resource annotations for MCP
|
|
16
|
-
MCP_RESOURCE: "@mcp.resource",
|
|
17
|
-
// Tool annotations for MCP
|
|
18
|
-
MCP_TOOL_NAME: "@mcp.tool.name",
|
|
19
|
-
MCP_TOOL_DESCRIPTION: "@mcp.tool.description",
|
|
20
|
-
// Prompt annotations for MCP
|
|
21
|
-
MCP_PROMPT: "@mcp.prompt",
|
|
22
|
-
};
|
|
23
|
-
class McpAnnotation {
|
|
24
|
-
_target;
|
|
25
|
-
_serviceName;
|
|
26
|
-
constructor(target, serviceName) {
|
|
27
|
-
this._target = target;
|
|
28
|
-
this._serviceName = serviceName;
|
|
29
|
-
}
|
|
30
|
-
get target() {
|
|
31
|
-
return this._target;
|
|
32
|
-
}
|
|
33
|
-
get serviceName() {
|
|
34
|
-
return this._serviceName;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
exports.McpAnnotation = McpAnnotation;
|
|
38
|
-
class McpResourceAnnotation extends McpAnnotation {
|
|
39
|
-
_includeAll;
|
|
40
|
-
_functionalities;
|
|
41
|
-
_properties;
|
|
42
|
-
constructor(target, serviceName, includeAll, functionalities, properties) {
|
|
43
|
-
super(target, serviceName);
|
|
44
|
-
this._includeAll = includeAll;
|
|
45
|
-
this._functionalities = functionalities;
|
|
46
|
-
this._properties = properties;
|
|
47
|
-
}
|
|
48
|
-
get includeAll() {
|
|
49
|
-
return this._includeAll;
|
|
50
|
-
}
|
|
51
|
-
get functionalities() {
|
|
52
|
-
return this._functionalities;
|
|
53
|
-
}
|
|
54
|
-
get properties() {
|
|
55
|
-
return this._properties;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
exports.McpResourceAnnotation = McpResourceAnnotation;
|
|
59
|
-
class McpToolAnnotation extends McpAnnotation {
|
|
60
|
-
_name;
|
|
61
|
-
_description;
|
|
62
|
-
_parameters;
|
|
63
|
-
_entityKey;
|
|
64
|
-
_operationKind;
|
|
65
|
-
_keyTypeMap;
|
|
66
|
-
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
|
67
|
-
super(operation, serviceName);
|
|
68
|
-
this._name = name;
|
|
69
|
-
this._description = description;
|
|
70
|
-
this._parameters = parameters;
|
|
71
|
-
this._entityKey = entityKey;
|
|
72
|
-
this._operationKind = operationKind;
|
|
73
|
-
this._keyTypeMap = keyTypeMap;
|
|
74
|
-
}
|
|
75
|
-
get name() {
|
|
76
|
-
return this._name;
|
|
77
|
-
}
|
|
78
|
-
get description() {
|
|
79
|
-
return this._description;
|
|
80
|
-
}
|
|
81
|
-
get parameters() {
|
|
82
|
-
return this._parameters;
|
|
83
|
-
}
|
|
84
|
-
get entityKey() {
|
|
85
|
-
return this._entityKey;
|
|
86
|
-
}
|
|
87
|
-
get operationKind() {
|
|
88
|
-
return this._operationKind;
|
|
89
|
-
}
|
|
90
|
-
get keyTypeMap() {
|
|
91
|
-
return this._keyTypeMap;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
exports.McpToolAnnotation = McpToolAnnotation;
|
|
95
|
-
class McpPromptAnnotation extends McpAnnotation {
|
|
96
|
-
_name;
|
|
97
|
-
_template;
|
|
98
|
-
constructor(target, serviceName, name, template) {
|
|
99
|
-
super(target, serviceName);
|
|
100
|
-
this._name = name;
|
|
101
|
-
this._template = template;
|
|
102
|
-
}
|
|
103
|
-
get name() {
|
|
104
|
-
return this._name;
|
|
105
|
-
}
|
|
106
|
-
get template() {
|
|
107
|
-
return this._template;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
exports.McpPromptAnnotation = McpPromptAnnotation;
|
|
111
|
-
function parseAnnotations(services) {
|
|
112
|
-
const annotations = [];
|
|
113
|
-
for (const serviceName of Object.keys(services)) {
|
|
114
|
-
const srv = services[serviceName];
|
|
115
|
-
if (srv.name === "CatalogService") {
|
|
116
|
-
utils_1.LOGGER.debug("SERVICE: ", srv.model.definitions);
|
|
117
|
-
}
|
|
118
|
-
const entities = srv.entities;
|
|
119
|
-
const operations = srv.operations; // Refers to action and function imports
|
|
120
|
-
// Find entities
|
|
121
|
-
for (const entityName of Object.keys(entities)) {
|
|
122
|
-
const target = entities[entityName];
|
|
123
|
-
const res = findEntityAnnotations(target, entityName, srv);
|
|
124
|
-
if (target.actions) {
|
|
125
|
-
const bound = parseBoundOperations(target.actions, entityName, target, srv);
|
|
126
|
-
if (bound && bound.length > 0) {
|
|
127
|
-
annotations.push(...bound);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (!res)
|
|
131
|
-
continue;
|
|
132
|
-
annotations.push(res);
|
|
133
|
-
}
|
|
134
|
-
// Find operations
|
|
135
|
-
for (const operationName of Object.keys(operations)) {
|
|
136
|
-
const op = operations[operationName];
|
|
137
|
-
const res = findOperationAnnotations(op, operationName, srv);
|
|
138
|
-
if (!res)
|
|
139
|
-
continue;
|
|
140
|
-
annotations.push(res);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const result = formatAnnotations(annotations);
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
146
|
-
function formatAnnotations(annotationList) {
|
|
147
|
-
const result = new Map();
|
|
148
|
-
for (const annotation of annotationList) {
|
|
149
|
-
if (annotation.operation) {
|
|
150
|
-
if (!annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME] ||
|
|
151
|
-
!annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION]) {
|
|
152
|
-
utils_1.LOGGER.error(`Invalid annotation found for operation`, annotation);
|
|
153
|
-
throw new Error(`Invalid annotations for operation '${annotation.operation}'`);
|
|
154
|
-
}
|
|
155
|
-
else if (typeof annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME] !==
|
|
156
|
-
"string" ||
|
|
157
|
-
typeof annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION] !==
|
|
158
|
-
"string") {
|
|
159
|
-
utils_1.LOGGER.error("Invalid data for annotations", annotation);
|
|
160
|
-
throw new Error(`Invalid annotation data for operation '${annotation.operation}'`);
|
|
161
|
-
}
|
|
162
|
-
const entry = new McpToolAnnotation(annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME], annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION], annotation.operation, annotation.serviceName, mapOperationInput(annotation.context), // TODO: Parse the parameters from the context and place them in the class
|
|
163
|
-
annotation.entityKey, annotation.operationKind, annotation.keyTypeMap);
|
|
164
|
-
result.set(entry.target, entry);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (!annotation.entityKey) {
|
|
168
|
-
utils_1.LOGGER.error("Invalid entry", annotation);
|
|
169
|
-
throw new Error(`Invalid annotated entry found with no target`);
|
|
170
|
-
}
|
|
171
|
-
if (!annotation.annotations[exports.McpAnnotations.MCP_RESOURCE]) {
|
|
172
|
-
utils_1.LOGGER.error("No valid annotations found for entry", annotation);
|
|
173
|
-
throw new Error(`Invalid annotations for entry target: '${annotation.entityKey}'`);
|
|
174
|
-
}
|
|
175
|
-
const includeAll = annotation.annotations[exports.McpAnnotations.MCP_RESOURCE] === true;
|
|
176
|
-
const functionalities = Array.isArray(annotation.annotations[exports.McpAnnotations.MCP_RESOURCE])
|
|
177
|
-
? new Set(annotation.annotations[exports.McpAnnotations.MCP_RESOURCE])
|
|
178
|
-
: DEFAULT_ALL_RESOURCE_OPTIONS;
|
|
179
|
-
const entry = new McpResourceAnnotation(annotation.entityKey, annotation.serviceName, includeAll, functionalities, (0, utils_1.parseEntityElements)(annotation.context));
|
|
180
|
-
result.set(entry.target, entry);
|
|
181
|
-
}
|
|
182
|
-
utils_1.LOGGER.debug("Formatted annotations", result);
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
function findEntityAnnotations(entry, entityKey, service) {
|
|
186
|
-
const annotations = findAnnotations(entry);
|
|
187
|
-
return Object.keys(annotations).length > 0
|
|
188
|
-
? {
|
|
189
|
-
serviceName: service.name,
|
|
190
|
-
annotations: annotations,
|
|
191
|
-
entityKey: entityKey,
|
|
192
|
-
context: entry,
|
|
193
|
-
}
|
|
194
|
-
: undefined;
|
|
195
|
-
}
|
|
196
|
-
function findOperationAnnotations(operation, operationName, service) {
|
|
197
|
-
const annotations = findAnnotations(operation);
|
|
198
|
-
return Object.keys(annotations).length > 0
|
|
199
|
-
? {
|
|
200
|
-
serviceName: service.name,
|
|
201
|
-
annotations: annotations,
|
|
202
|
-
operation: operationName,
|
|
203
|
-
operationKind: operation.kind,
|
|
204
|
-
context: operation,
|
|
205
|
-
}
|
|
206
|
-
: undefined;
|
|
207
|
-
}
|
|
208
|
-
function parseBoundOperations(operations, entityKey, entity, service) {
|
|
209
|
-
const res = new Array();
|
|
210
|
-
for (const [operationName, operation] of Object.entries(operations)) {
|
|
211
|
-
const annotation = findBoundOperationAnnotations(operation, operationName, entityKey, service);
|
|
212
|
-
if (!annotation)
|
|
213
|
-
continue;
|
|
214
|
-
annotation.keyTypeMap = new Map();
|
|
215
|
-
for (const [k, v] of Object.entries(entity.keys)) {
|
|
216
|
-
if (!v.type) {
|
|
217
|
-
utils_1.LOGGER.error("Invalid key type", k);
|
|
218
|
-
throw new Error("Invalid key type found for bound operation");
|
|
219
|
-
}
|
|
220
|
-
annotation.keyTypeMap.set(k, v.type.replace("cds.", ""));
|
|
221
|
-
}
|
|
222
|
-
res.push(annotation);
|
|
223
|
-
}
|
|
224
|
-
return res;
|
|
225
|
-
}
|
|
226
|
-
function findBoundOperationAnnotations(operation, operationName, entityKey, service) {
|
|
227
|
-
const annotations = findAnnotations(operation);
|
|
228
|
-
return Object.keys(annotations).length > 0
|
|
229
|
-
? {
|
|
230
|
-
serviceName: service.name,
|
|
231
|
-
annotations: annotations,
|
|
232
|
-
operation: operationName,
|
|
233
|
-
operationKind: operation.kind,
|
|
234
|
-
entityKey: entityKey,
|
|
235
|
-
context: operation,
|
|
236
|
-
}
|
|
237
|
-
: undefined;
|
|
238
|
-
}
|
|
239
|
-
function findAnnotations(entry) {
|
|
240
|
-
const annotations = {};
|
|
241
|
-
for (const [k, v] of Object.entries(entry)) {
|
|
242
|
-
if (!k.includes(exports.McpAnnotationKey))
|
|
243
|
-
continue;
|
|
244
|
-
annotations[k] = v;
|
|
245
|
-
}
|
|
246
|
-
return annotations;
|
|
247
|
-
}
|
|
248
|
-
function mapOperationInput(ctx) {
|
|
249
|
-
const params = ctx["params"];
|
|
250
|
-
if (!params)
|
|
251
|
-
return undefined;
|
|
252
|
-
const result = new Map();
|
|
253
|
-
for (const [k, v] of Object.entries(params)) {
|
|
254
|
-
result.set(k, v.type.replace("cds.", ""));
|
|
255
|
-
}
|
|
256
|
-
return result.size > 0 ? result : undefined;
|
|
257
|
-
}
|
package/lib/auth/adapter.js
DELETED
package/lib/auth/mock.js
DELETED
package/lib/auth/types.js
DELETED