@gavdi/cap-mcp 1.5.1 → 1.7.0
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 +35 -1
- package/lib/annotations/utils.js +2 -1
- package/lib/auth/utils.js +98 -97
- package/lib/mcp/utils.js +9 -5
- package/lib/mcp.js +9 -9
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ By integrating MCP with your CAP applications, you unlock:
|
|
|
29
29
|
|
|
30
30
|
- **Node.js**: Version 18 or higher
|
|
31
31
|
- **SAP CAP**: Version 9 or higher
|
|
32
|
-
- **Express**: Version
|
|
32
|
+
- **Express**: Version 5 or higher
|
|
33
33
|
- **TypeScript**: Optional but recommended
|
|
34
34
|
|
|
35
35
|
### Step 1: Install the Plugin
|
|
@@ -294,6 +294,40 @@ extend projection Books with actions {
|
|
|
294
294
|
}
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
+
#### Required vs Optional Parameters
|
|
298
|
+
|
|
299
|
+
Parameters follow standard CDS nullability rules. A parameter declared `not null` is **required** in the MCP tool schema; a parameter without `not null` is **optional** and may be omitted by the AI agent.
|
|
300
|
+
|
|
301
|
+
```cds
|
|
302
|
+
@mcp: {
|
|
303
|
+
name : 'search-items',
|
|
304
|
+
description: 'Search for items by keyword with optional filters',
|
|
305
|
+
tool : true
|
|
306
|
+
}
|
|
307
|
+
function searchItems(
|
|
308
|
+
query : String not null, // required — must be provided
|
|
309
|
+
category : String, // optional — may be omitted
|
|
310
|
+
limit : Integer, // optional — may be omitted
|
|
311
|
+
format : String // optional — may be omitted
|
|
312
|
+
) returns array of String;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The generated JSON Schema will list only `not null` parameters in the `required` array:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"properties": {
|
|
320
|
+
"query": { "type": "string" },
|
|
321
|
+
"category": { "type": "string" },
|
|
322
|
+
"limit": { "type": "integer" },
|
|
323
|
+
"format": { "type": "string" }
|
|
324
|
+
},
|
|
325
|
+
"required": ["query"]
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
> **Note:** This behaviour applies to unbound functions and actions. Entity wrapper tools (`query`, `get`, `create`, `update`) derive nullability from the entity element definitions.
|
|
330
|
+
|
|
297
331
|
#### Tool Elicitation
|
|
298
332
|
|
|
299
333
|
Request user confirmation or input before tool execution using the `elicit` property:
|
package/lib/annotations/utils.js
CHANGED
package/lib/auth/utils.js
CHANGED
|
@@ -304,111 +304,112 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
|
|
|
304
304
|
// OAuth Dynamic Client Registration discovery endpoint (GET)
|
|
305
305
|
expressApp.get("/oauth/register", async (req, res) => {
|
|
306
306
|
const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
|
|
307
|
+
// XSUAA does not support DCR so we will respond with the pre-configured client_id
|
|
307
308
|
// IAS does not support DCR so we will respond with the pre-configured client_id
|
|
308
|
-
if (kind === "ias") {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
309
|
+
// if (kind === "ias") {
|
|
310
|
+
const enhancedResponse = {
|
|
311
|
+
client_id: credentials.clientid, // Add our CAP app's client ID
|
|
312
|
+
redirect_uris: req.body.redirect_uris || [`${baseUrl}/oauth/callback`],
|
|
313
|
+
};
|
|
314
|
+
logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
|
|
315
|
+
res.json(enhancedResponse);
|
|
316
|
+
return;
|
|
317
|
+
// }
|
|
319
318
|
// Keep original implementation for XSUAA
|
|
320
|
-
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
319
|
+
// try {
|
|
320
|
+
// // Simple proxy for discovery - no CSRF needed
|
|
321
|
+
// const response = await fetch(`${credentials.url}/oauth/register`, {
|
|
322
|
+
// method: "GET",
|
|
323
|
+
// headers: {
|
|
324
|
+
// Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
|
|
325
|
+
// Accept: "application/json",
|
|
326
|
+
// },
|
|
327
|
+
// });
|
|
328
|
+
// const xsuaaData = await response.json();
|
|
329
|
+
// // Add missing required fields that MCP client expects
|
|
330
|
+
// const enhancedResponse = {
|
|
331
|
+
// ...xsuaaData, // Keep all XSUAA fields
|
|
332
|
+
// client_id: credentials.clientid, // Add our CAP app's client ID
|
|
333
|
+
// redirect_uris: [`${baseUrl}/oauth/callback`], // Add our callback URL for discovery
|
|
334
|
+
// };
|
|
335
|
+
// res.status(response.status).json(enhancedResponse);
|
|
336
|
+
// } catch (error) {
|
|
337
|
+
// LOGGER.error("OAuth registration discovery error:", error);
|
|
338
|
+
// res.status(500).json({
|
|
339
|
+
// error: "server_error",
|
|
340
|
+
// error_description:
|
|
341
|
+
// error instanceof Error ? error.message : "Unknown error",
|
|
342
|
+
// });
|
|
343
|
+
// }
|
|
345
344
|
});
|
|
346
345
|
// OAuth Dynamic Client Registration endpoint (POST) with CSRF handling
|
|
347
346
|
expressApp.post("/oauth/register", async (req, res) => {
|
|
348
347
|
const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
|
|
348
|
+
// XSUAA does not support DCR so we will respond with the pre-configured client_id
|
|
349
349
|
// IAS does not support DCR so we will respond with the pre-configured client_id
|
|
350
|
-
if (kind === "ias") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
350
|
+
// if (kind === "ias") {
|
|
351
|
+
const enhancedResponse = {
|
|
352
|
+
client_id: credentials.clientid, // Add our CAP app's client ID
|
|
353
|
+
redirect_uris: req.body.redirect_uris || [`${baseUrl}/oauth/callback`],
|
|
354
|
+
};
|
|
355
|
+
logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
|
|
356
|
+
res.json(enhancedResponse);
|
|
357
|
+
return;
|
|
358
|
+
// }
|
|
361
359
|
// Keep original implementation for XSUAA
|
|
362
|
-
try {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
360
|
+
// try {
|
|
361
|
+
// Step 1: Fetch CSRF token from XSUAA
|
|
362
|
+
// const csrfResponse = await fetch(`${credentials.url}/oauth/register`, {
|
|
363
|
+
// method: "GET",
|
|
364
|
+
// headers: {
|
|
365
|
+
// "X-CSRF-Token": "Fetch",
|
|
366
|
+
// Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
|
|
367
|
+
// Accept: "application/json",
|
|
368
|
+
// },
|
|
369
|
+
// });
|
|
370
|
+
// if (!csrfResponse.ok) {
|
|
371
|
+
// throw new Error(`CSRF fetch failed: ${csrfResponse.status}`);
|
|
372
|
+
// }
|
|
373
|
+
// Step 2: Extract CSRF token and session cookie
|
|
374
|
+
// const setCookieHeader = csrfResponse.headers.get("set-cookie") || "";
|
|
375
|
+
// const csrfToken = extractCsrfFromCookie(setCookieHeader);
|
|
376
|
+
// if (!csrfToken) {
|
|
377
|
+
// throw new Error("Could not extract CSRF token from XSUAA response");
|
|
378
|
+
// }
|
|
379
|
+
// Step 3: Make actual registration POST with CSRF token
|
|
380
|
+
// const registrationResponse = await fetch(
|
|
381
|
+
// `${credentials.url}/oauth/register`,
|
|
382
|
+
// {
|
|
383
|
+
// method: "POST",
|
|
384
|
+
// headers: {
|
|
385
|
+
// "Content-Type": "application/json",
|
|
386
|
+
// "X-CSRF-Token": csrfToken,
|
|
387
|
+
// Cookie: setCookieHeader,
|
|
388
|
+
// Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
|
|
389
|
+
// Accept: "application/json",
|
|
390
|
+
// },
|
|
391
|
+
// body: JSON.stringify(req.body),
|
|
392
|
+
// },
|
|
393
|
+
// );
|
|
394
|
+
// const xsuaaData = await registrationResponse.json();
|
|
395
|
+
// Add missing required fields that MCP client expects
|
|
396
|
+
// const enhancedResponse = {
|
|
397
|
+
// ...xsuaaData, // Keep all XSUAA fields
|
|
398
|
+
// client_id: credentials.clientid, // Add our CAP app's client ID
|
|
399
|
+
// redirect_uris: req.body.redirect_uris || [
|
|
400
|
+
// `${baseUrl}/oauth/callback`,
|
|
401
|
+
// ],
|
|
402
|
+
// };
|
|
403
|
+
// LOGGER.debug("[AUTH] Register POST response", enhancedResponse);
|
|
404
|
+
// res.status(registrationResponse.status).json(enhancedResponse);
|
|
405
|
+
// } catch (error) {
|
|
406
|
+
// LOGGER.error("OAuth registration error:", error);
|
|
407
|
+
// res.status(500).json({
|
|
408
|
+
// error: "server_error",
|
|
409
|
+
// error_description:
|
|
410
|
+
// error instanceof Error ? error.message : "Unknown error",
|
|
411
|
+
// });
|
|
412
|
+
// }
|
|
412
413
|
});
|
|
413
414
|
logger_1.LOGGER.debug("OAuth endpoints registered for XSUAA integration");
|
|
414
415
|
}
|
package/lib/mcp/utils.js
CHANGED
|
@@ -17,6 +17,10 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
|
|
|
17
17
|
* @returns Zod schema instance for the given type
|
|
18
18
|
*/
|
|
19
19
|
function determineMcpParameterType(cdsType, key, target) {
|
|
20
|
+
if (cdsType?.endsWith("Optional")) {
|
|
21
|
+
const baseType = cdsType.slice(0, -"Optional".length);
|
|
22
|
+
return determineMcpParameterType(baseType, key, target).optional();
|
|
23
|
+
}
|
|
20
24
|
switch (cdsType) {
|
|
21
25
|
case "String":
|
|
22
26
|
return zod_1.z.string();
|
|
@@ -195,14 +199,14 @@ function buildDeepInsertZodType(targetEntityName) {
|
|
|
195
199
|
async function handleMcpSessionRequest(req, res, sessions) {
|
|
196
200
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
|
197
201
|
if (!sessionIdHeader || !sessions.has(sessionIdHeader)) {
|
|
198
|
-
res.status(
|
|
202
|
+
res.status(404).json({
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
error: { code: -32001, message: "Session not found" },
|
|
205
|
+
id: null,
|
|
206
|
+
});
|
|
199
207
|
return;
|
|
200
208
|
}
|
|
201
209
|
const session = sessions.get(sessionIdHeader);
|
|
202
|
-
if (!session) {
|
|
203
|
-
res.status(400).send("Invalid session");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
210
|
await session.transport.handleRequest(req, res);
|
|
207
211
|
}
|
|
208
212
|
/**
|
package/lib/mcp.js
CHANGED
|
@@ -104,13 +104,13 @@ class McpPlugin {
|
|
|
104
104
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
|
105
105
|
const sessions = this.sessionManager.getSessions();
|
|
106
106
|
if (!sessionIdHeader || !sessions.has(sessionIdHeader)) {
|
|
107
|
-
return res.status(
|
|
107
|
+
return res.status(404).json({
|
|
108
108
|
jsonrpc: "2.0",
|
|
109
109
|
error: {
|
|
110
|
-
code: -
|
|
111
|
-
message: "
|
|
112
|
-
id: null,
|
|
110
|
+
code: -32001,
|
|
111
|
+
message: "Session not found",
|
|
113
112
|
},
|
|
113
|
+
id: null,
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
const session = sessions.get(sessionIdHeader);
|
|
@@ -139,13 +139,13 @@ class McpPlugin {
|
|
|
139
139
|
: this.sessionManager.getSession(sessionIdHeader);
|
|
140
140
|
if (!session) {
|
|
141
141
|
logger_1.LOGGER.error("Invalid session ID", sessionIdHeader);
|
|
142
|
-
res.status(
|
|
142
|
+
res.status(404).json({
|
|
143
143
|
jsonrpc: "2.0",
|
|
144
144
|
error: {
|
|
145
|
-
code: -
|
|
146
|
-
message: "
|
|
147
|
-
id: null,
|
|
145
|
+
code: -32001,
|
|
146
|
+
message: "Session not found",
|
|
148
147
|
},
|
|
148
|
+
id: null,
|
|
149
149
|
});
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
@@ -163,8 +163,8 @@ class McpPlugin {
|
|
|
163
163
|
error: {
|
|
164
164
|
code: -32603,
|
|
165
165
|
message: "Internal Error: Transport failed",
|
|
166
|
-
id: null,
|
|
167
166
|
},
|
|
167
|
+
id: null,
|
|
168
168
|
});
|
|
169
169
|
return;
|
|
170
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavdi/cap-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP Plugin for CAP",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MCP",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@sap/cds": ">=9",
|
|
47
|
-
"express": "^
|
|
47
|
+
"express": "^5"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@types/cors": "^2.8.19",
|
|
61
61
|
"@types/express": "^5.0.3",
|
|
62
62
|
"@types/jest": "^30.0.0",
|
|
63
|
-
"@types/node": "^
|
|
63
|
+
"@types/node": "^25.3.0",
|
|
64
64
|
"@types/sinon": "^17.0.4",
|
|
65
65
|
"@types/supertest": "^6.0.2",
|
|
66
66
|
"docsify-cli": "^4.4.4",
|