@gavdi/cap-mcp 1.1.5 → 1.2.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.
@@ -125,13 +125,17 @@ function parseAnnotations(definition) {
125
125
  function constructResourceAnnotation(serviceName, target, annotations, definition, model) {
126
126
  if (!(0, utils_1.isValidResourceAnnotation)(annotations))
127
127
  return undefined;
128
+ const entityTarget = `${serviceName}.${target}`;
128
129
  const functionalities = (0, utils_1.determineResourceOptions)(annotations);
129
- const foreignKeys = new Map(Object.entries(model.definitions?.[`${serviceName}.${target}`].elements ?? {})
130
+ const foreignKeys = new Map(Object.entries(model.definitions?.[entityTarget].elements ?? {})
130
131
  .filter(([_, v]) => v["@odata.foreignKey4"] !== undefined)
131
132
  .map(([k, v]) => [k, v["@odata.foreignKey4"]]));
133
+ const computedFields = new Set(Object.entries(model.definitions?.[entityTarget].elements ?? {})
134
+ .filter(([_, v]) => new Map(Object.entries(v).map(([key, value]) => [key.toLowerCase(), value])).get("@core.computed"))
135
+ .map(([k, _]) => k));
132
136
  const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition, model);
133
137
  const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
134
- return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, annotations.wrap, restrictions);
138
+ return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, annotations.wrap, restrictions, computedFields);
135
139
  }
136
140
  /**
137
141
  * Constructs a tool annotation from parsed annotation data
@@ -84,6 +84,8 @@ class McpResourceAnnotation extends McpAnnotation {
84
84
  _wrap;
85
85
  /** Map of foreign keys property -> associated entity */
86
86
  _foreignKeys;
87
+ /** Set of computed field names */
88
+ _computedFields;
87
89
  /**
88
90
  * Creates a new MCP resource annotation
89
91
  * @param name - Unique identifier for this resource
@@ -96,14 +98,16 @@ class McpResourceAnnotation extends McpAnnotation {
96
98
  * @param foreignKeys - Map of foreign keys used by entity
97
99
  * @param wrap - Wrap usage
98
100
  * @param restrictions - Optional restrictions based on CDS roles
101
+ * @param computedFields - Optional set of fields that are computed and should be ignored in create scenarios
99
102
  */
100
- constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, wrap, restrictions) {
103
+ constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, wrap, restrictions, computedFields) {
101
104
  super(name, description, target, serviceName, restrictions ?? []);
102
105
  this._functionalities = functionalities;
103
106
  this._properties = properties;
104
107
  this._resourceKeys = resourceKeys;
105
108
  this._wrap = wrap;
106
109
  this._foreignKeys = foreignKeys;
110
+ this._computedFields = computedFields;
107
111
  }
108
112
  /**
109
113
  * Gets the set of enabled OData query functionalities
@@ -139,6 +143,12 @@ class McpResourceAnnotation extends McpAnnotation {
139
143
  get wrap() {
140
144
  return this._wrap;
141
145
  }
146
+ /**
147
+ * Gets the computed fields if any are available
148
+ */
149
+ get computedFields() {
150
+ return this._computedFields;
151
+ }
142
152
  }
143
153
  exports.McpResourceAnnotation = McpResourceAnnotation;
144
154
  /**
@@ -288,10 +288,12 @@ function parseCdsRestrictions(restrictions, requires) {
288
288
  });
289
289
  continue;
290
290
  }
291
- const mapped = el.to.map((to) => ({
292
- role: to,
293
- operations: ops,
294
- }));
291
+ const mapped = Array.isArray(el.to)
292
+ ? el.to.map((to) => ({
293
+ role: to,
294
+ operations: ops,
295
+ }))
296
+ : [{ role: el.to, operations: ops }];
295
297
  result.push(...mapped);
296
298
  }
297
299
  return result;
@@ -300,29 +302,30 @@ function parseCdsRestrictions(restrictions, requires) {
300
302
  * Maps the "grant" property from CdsRestriction to McpRestriction
301
303
  */
302
304
  function mapOperationRestriction(cdsRestrictions) {
303
- const result = [];
304
305
  if (!cdsRestrictions || cdsRestrictions.length <= 0) {
305
- result.push("CREATE");
306
- result.push("READ");
307
- result.push("UPDATE");
308
- result.push("DELETE");
309
- return result;
306
+ return ["CREATE", "READ", "UPDATE", "DELETE"];
310
307
  }
308
+ if (!Array.isArray(cdsRestrictions)) {
309
+ return translateOperationRestriction(cdsRestrictions);
310
+ }
311
+ const result = [];
311
312
  for (const el of cdsRestrictions) {
312
- switch (el) {
313
- case "CHANGE":
314
- result.push("UPDATE");
315
- continue;
316
- case "*":
317
- result.push("CREATE");
318
- result.push("READ");
319
- result.push("UPDATE");
320
- result.push("DELETE");
321
- continue;
322
- default:
323
- result.push(el);
324
- continue;
325
- }
313
+ const translated = translateOperationRestriction(el);
314
+ if (!translated || translated.length <= 0)
315
+ continue;
316
+ result.push(...translated);
326
317
  }
327
318
  return result;
328
319
  }
320
+ function translateOperationRestriction(restrictionType) {
321
+ switch (restrictionType) {
322
+ case "CHANGE":
323
+ return ["UPDATE"];
324
+ case "WRITE":
325
+ return ["CREATE", "UPDATE", "DELETE"];
326
+ case "*":
327
+ return ["CREATE", "READ", "UPDATE", "DELETE"];
328
+ default:
329
+ return [restrictionType];
330
+ }
331
+ }
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.authHandlerFactory = authHandlerFactory;
4
4
  exports.errorHandlerFactory = errorHandlerFactory;
5
5
  const xsuaa_service_1 = require("./xsuaa-service");
6
+ const utils_1 = require("./utils");
7
+ const logger_1 = require("../logger");
6
8
  /** JSON-RPC 2.0 error code for unauthorized requests */
7
9
  const RPC_UNAUTHORIZED = 10;
8
10
  /* @ts-ignore */
@@ -33,7 +35,8 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
33
35
  */
34
36
  function authHandlerFactory() {
35
37
  const authKind = cds.env.requires.auth.kind;
36
- const xsuaaService = new xsuaa_service_1.XSUAAService();
38
+ const xsuaaService = !(0, utils_1.useMockAuth)(authKind) ? new xsuaa_service_1.XSUAAService() : undefined;
39
+ logger_1.LOGGER.debug("Authentication kind", authKind);
37
40
  return async (req, res, next) => {
38
41
  if (!req.headers.authorization && authKind !== "dummy") {
39
42
  res.status(401).json({
@@ -47,8 +50,8 @@ function authHandlerFactory() {
47
50
  return;
48
51
  }
49
52
  // For XSUAA/JWT auth types, use @sap/xssec for validation
50
- if ((authKind === "jwt" || authKind === "xsuaa") &&
51
- xsuaaService.isConfigured()) {
53
+ if ((authKind === "jwt" || authKind === "xsuaa" || authKind === "ias") &&
54
+ xsuaaService?.isConfigured()) {
52
55
  const securityContext = await xsuaaService.createSecurityContext(req);
53
56
  if (!securityContext) {
54
57
  res.status(401).json({
@@ -10,7 +10,7 @@ const logger_1 = require("../logger");
10
10
  async function handleTokenRequest(req, res, xsuaaService) {
11
11
  try {
12
12
  const params = { ...req.query, ...req.body };
13
- const { grant_type, code, redirect_uri, refresh_token } = params;
13
+ const { grant_type, code, redirect_uri, refresh_token, code_verifier } = params;
14
14
  if (grant_type === "authorization_code") {
15
15
  if (!code || !redirect_uri) {
16
16
  res.status(400).json({
@@ -19,9 +19,11 @@ async function handleTokenRequest(req, res, xsuaaService) {
19
19
  });
20
20
  return;
21
21
  }
22
- const tokenData = await xsuaaService.exchangeCodeForToken(code, redirect_uri);
22
+ const tokenData = await xsuaaService.exchangeCodeForToken(code, redirect_uri, code_verifier);
23
+ const scopedToken = await xsuaaService.getApplicationScopes(tokenData);
24
+ logger_1.LOGGER.debug("Scopes in token:", scopedToken.scope);
23
25
  logger_1.LOGGER.debug("[AUTH] Token exchange successful");
24
- res.json(tokenData);
26
+ res.json(scopedToken);
25
27
  }
26
28
  else if (grant_type === "refresh_token") {
27
29
  if (!refresh_token) {
package/lib/auth/utils.js CHANGED
@@ -8,6 +8,7 @@ exports.getAccessRights = getAccessRights;
8
8
  exports.registerAuthMiddleware = registerAuthMiddleware;
9
9
  exports.hasToolOperationAccess = hasToolOperationAccess;
10
10
  exports.getWrapAccesses = getWrapAccesses;
11
+ exports.useMockAuth = useMockAuth;
11
12
  const express_1 = __importDefault(require("express"));
12
13
  const helmet_1 = __importDefault(require("helmet"));
13
14
  const factory_1 = require("./factory");
@@ -191,7 +192,7 @@ function configureOAuthProxy(expressApp) {
191
192
  !credentials.url) {
192
193
  throw new Error("Invalid security credentials");
193
194
  }
194
- registerOAuthEndpoints(expressApp, credentials);
195
+ registerOAuthEndpoints(expressApp, credentials, kind);
195
196
  }
196
197
  /**
197
198
  * Determines the correct protocol (HTTP/HTTPS) for URL construction.
@@ -213,8 +214,10 @@ function getProtocol(req) {
213
214
  * Registers OAuth endpoints for XSUAA integration
214
215
  * Only called for jwt/xsuaa/ias auth types with valid credentials
215
216
  */
216
- function registerOAuthEndpoints(expressApp, credentials) {
217
+ function registerOAuthEndpoints(expressApp, credentials, kind) {
217
218
  const xsuaaService = new xsuaa_service_1.XSUAAService();
219
+ // Fetch endpoints from OIDC configuration
220
+ xsuaaService.discoverOAuthEndpoints();
218
221
  // Add JSON and URL-encoded body parsing for OAuth endpoints
219
222
  expressApp.use("/oauth", express_1.default.json());
220
223
  expressApp.use("/oauth", express_1.default.urlencoded({ extended: true }));
@@ -231,17 +234,17 @@ function registerOAuthEndpoints(expressApp, credentials) {
231
234
  }));
232
235
  // OAuth Authorization endpoint - stateless redirect to XSUAA
233
236
  expressApp.get("/oauth/authorize", (req, res) => {
234
- const { state, redirect_uri } = req.query;
237
+ const { state, redirect_uri, client_id, code_challenge, code_challenge_method, scope, } = req.query;
235
238
  // Client validation and redirect URI validation is handled by XSUAA
236
239
  // We delegate all client management to XSUAA's built-in OAuth server
237
240
  const protocol = getProtocol(req);
238
241
  const redirectUri = redirect_uri || `${protocol}://${req.get("host")}/oauth/callback`;
239
- const authUrl = xsuaaService.getAuthorizationUrl(redirectUri, state);
242
+ const authUrl = xsuaaService.getAuthorizationUrl(redirectUri, client_id ?? "", state, code_challenge, code_challenge_method, scope);
240
243
  res.redirect(authUrl);
241
244
  });
242
245
  // OAuth Callback endpoint - stateless token exchange
243
246
  expressApp.get("/oauth/callback", async (req, res) => {
244
- const { code, state, error, error_description, redirect_uri } = req.query;
247
+ const { code, state, error, error_description, redirect_uri, code_verifier, } = req.query;
245
248
  logger_1.LOGGER.debug("[AUTH] Callback received", code, state);
246
249
  if (error) {
247
250
  res.status(400).json({
@@ -260,8 +263,10 @@ function registerOAuthEndpoints(expressApp, credentials) {
260
263
  try {
261
264
  const protocol = getProtocol(req);
262
265
  const url = redirect_uri || `${protocol}://${req.get("host")}/oauth/callback`;
263
- const tokenData = await xsuaaService.exchangeCodeForToken(code, url);
264
- res.json(tokenData);
266
+ const tokenData = await xsuaaService.exchangeCodeForToken(code, url, code_verifier);
267
+ const scopedToken = await xsuaaService.getApplicationScopes(tokenData);
268
+ logger_1.LOGGER.debug("Scopes in token:", scopedToken.scope);
269
+ res.json(scopedToken);
265
270
  }
266
271
  catch (error) {
267
272
  logger_1.LOGGER.error("OAuth callback error:", error);
@@ -288,13 +293,27 @@ function registerOAuthEndpoints(expressApp, credentials) {
288
293
  response_types_supported: ["code"],
289
294
  grant_types_supported: ["authorization_code", "refresh_token"],
290
295
  code_challenge_methods_supported: ["S256"],
291
- // scopes_supported: ["uaa.resource"],
296
+ scopes_supported: ["openid"],
292
297
  token_endpoint_auth_methods_supported: ["client_secret_post"],
293
298
  registration_endpoint_auth_methods_supported: ["client_secret_basic"],
294
299
  });
295
300
  });
296
301
  // OAuth Dynamic Client Registration discovery endpoint (GET)
297
302
  expressApp.get("/oauth/register", async (req, res) => {
303
+ // IAS does not support DCR so we will respond with the pre-configured client_id
304
+ if (kind === "ias") {
305
+ const protocol = getProtocol(req);
306
+ const enhancedResponse = {
307
+ client_id: credentials.clientid, // Add our CAP app's client ID
308
+ redirect_uris: req.body.redirect_uris || [
309
+ `${protocol}://${req.get("host")}/oauth/callback`,
310
+ ],
311
+ };
312
+ logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
313
+ res.json(enhancedResponse);
314
+ return;
315
+ }
316
+ // Keep original implementation for XSUAA
298
317
  try {
299
318
  // Simple proxy for discovery - no CSRF needed
300
319
  const response = await fetch(`${credentials.url}/oauth/register`, {
@@ -324,6 +343,20 @@ function registerOAuthEndpoints(expressApp, credentials) {
324
343
  });
325
344
  // OAuth Dynamic Client Registration endpoint (POST) with CSRF handling
326
345
  expressApp.post("/oauth/register", async (req, res) => {
346
+ // IAS does not support DCR so we will respond with the pre-configured client_id
347
+ if (kind === "ias") {
348
+ const protocol = getProtocol(req);
349
+ const enhancedResponse = {
350
+ client_id: credentials.clientid, // Add our CAP app's client ID
351
+ redirect_uris: req.body.redirect_uris || [
352
+ `${protocol}://${req.get("host")}/oauth/callback`,
353
+ ],
354
+ };
355
+ logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
356
+ res.json(enhancedResponse);
357
+ return;
358
+ }
359
+ // Keep original implementation for XSUAA
327
360
  try {
328
361
  // Step 1: Fetch CSRF token from XSUAA
329
362
  const csrfResponse = await fetch(`${credentials.url}/oauth/register`, {
@@ -361,7 +394,7 @@ function registerOAuthEndpoints(expressApp, credentials) {
361
394
  const enhancedResponse = {
362
395
  ...xsuaaData, // Keep all XSUAA fields
363
396
  client_id: credentials.clientid, // Add our CAP app's client ID
364
- client_secret: credentials.clientsecret, // CAP app's client secret
397
+ // client_secret: credentials.clientsecret, // CAP app's client secret
365
398
  redirect_uris: req.body.redirect_uris || [
366
399
  `${protocol}://${req.get("host")}/oauth/callback`,
367
400
  ], // Use client's redirect URIs
@@ -448,3 +481,10 @@ function getWrapAccesses(user, restrictions) {
448
481
  }
449
482
  return access;
450
483
  }
484
+ /**
485
+ * Utility method for checking whether auth used is mocked and not live
486
+ * @returns boolean
487
+ */
488
+ function useMockAuth(authKind) {
489
+ return authKind !== "jwt" && authKind !== "ias" && authKind !== "xsuaa";
490
+ }
@@ -45,47 +45,104 @@ const cds = global.cds || require("@sap/cds");
45
45
  class XSUAAService {
46
46
  credentials;
47
47
  xsuaaService;
48
+ endpoints;
48
49
  constructor() {
49
- this.credentials = cds.env.requires.auth.credentials;
50
- // Initialize XSUAA service from @sap/xssec
51
- this.xsuaaService = new xssec.XsuaaService(this.credentials);
50
+ // In case of IAS, the final token conversion to get application scopes has to be done with XSUAA, so there will be 2 sets of credentials
51
+ this.credentials = {
52
+ authProvider: cds.env.requires.auth?.credentials,
53
+ xsuaa: cds.env.requires.xsuaa?.credentials ||
54
+ cds.env.requires.auth?.credentials,
55
+ };
56
+ // Set default endpoints in case OIDC discovery call fails
57
+ this.endpoints = {
58
+ authProvider: {
59
+ discovery_url: `${this.credentials.authProvider?.url}/.well-known/openid-configuration`,
60
+ authorization_endpoint: `${this.credentials.authProvider?.url}/oauth/authorize`,
61
+ token_endpoint: `${this.credentials.authProvider?.url}/oauth/token`,
62
+ },
63
+ xsuaa: {
64
+ discovery_url: `${this.credentials.xsuaa?.url}/.well-known/openid-configuration`,
65
+ authorization_endpoint: `${this.credentials.xsuaa?.url}/oauth/authorize`,
66
+ token_endpoint: `${this.credentials.xsuaa?.url}/oauth/token`,
67
+ },
68
+ };
69
+ this.xsuaaService = new xssec.XsuaaService(this.credentials.xsuaa);
52
70
  }
53
71
  isConfigured() {
54
- return !!(this.credentials?.clientid &&
55
- this.credentials?.clientsecret &&
56
- this.credentials?.url);
72
+ return !!(this.credentials.authProvider?.clientid &&
73
+ this.credentials.authProvider?.clientsecret &&
74
+ this.credentials.authProvider?.url);
75
+ }
76
+ /**
77
+ * Fetch oauth endpoints from the OIDC discovery endpoints from both XSUAA (and IAS).
78
+ * If none found than the default will be used.
79
+ */
80
+ async discoverOAuthEndpoints() {
81
+ // Do discovery for both 'authProvider' and 'xsuaa' endpoint sets
82
+ try {
83
+ const endpointKeys = Object.keys(this.endpoints);
84
+ for (let key of endpointKeys) {
85
+ const response = await fetch(this.endpoints[key].discovery_url, {
86
+ method: "GET",
87
+ headers: {
88
+ Accept: "application/json",
89
+ },
90
+ });
91
+ if (!response.ok) {
92
+ const errorData = await response.json();
93
+ logger_1.LOGGER.warn(`OAuth endpoints fetch failed: ${response.status} ${errorData.error_description || errorData.error}. Continuing with default configuration.`);
94
+ }
95
+ else {
96
+ const oidcConfig = await response.json();
97
+ this.endpoints[key].authorization_endpoint =
98
+ oidcConfig.authorization_endpoint;
99
+ this.endpoints[key].token_endpoint = oidcConfig.token_endpoint;
100
+ logger_1.LOGGER.debug(`OAuth endpoints for [${key}] set to:`, this.endpoints[key]);
101
+ }
102
+ }
103
+ }
104
+ catch (error) {
105
+ if (error instanceof Error) {
106
+ throw error;
107
+ }
108
+ throw new Error(`OAuth endpoints fetch failed: ${String(error)}`);
109
+ }
57
110
  }
58
111
  /**
59
112
  * Generates authorization URL using @sap/xssec
60
113
  */
61
- getAuthorizationUrl(redirectUri, state) {
114
+ getAuthorizationUrl(redirectUri, client_id, state, code_challenge, code_challenge_method, scope) {
62
115
  const params = new URLSearchParams({
63
116
  response_type: "code",
64
- client_id: this.credentials.clientid,
65
117
  redirect_uri: redirectUri,
66
118
  // scope: "uaa.resource",
119
+ client_id,
120
+ ...(!!code_challenge ? { code_challenge } : {}),
121
+ ...(!!code_challenge_method ? { code_challenge_method } : {}),
122
+ ...(!!scope ? { scope } : {}),
67
123
  });
68
124
  if (state) {
69
125
  params.append("state", state);
70
126
  }
71
- return `${this.credentials.url}/oauth/authorize?${params.toString()}`;
127
+ return `${this.endpoints.authProvider.authorization_endpoint}?${params.toString()}`;
72
128
  }
73
129
  /**
74
130
  * Exchange authorization code for token using @sap/xssec
75
131
  */
76
- async exchangeCodeForToken(code, redirectUri) {
132
+ async exchangeCodeForToken(code, redirectUri, code_verifier) {
77
133
  try {
78
134
  const tokenOptions = {
79
135
  grant_type: "authorization_code",
80
136
  code,
81
137
  redirect_uri: redirectUri,
138
+ ...(!!code_verifier ? { code_verifier } : {}),
82
139
  };
83
- // Use direct XSUAA token endpoint for authorization code exchange
84
- const response = await fetch(`${this.credentials.url}/oauth/token`, {
140
+ // Use direct XSUAA/IAS token endpoint for authorization code exchange
141
+ const response = await fetch(this.endpoints.authProvider.token_endpoint, {
85
142
  method: "POST",
86
143
  headers: {
87
144
  "Content-Type": "application/x-www-form-urlencoded",
88
- Authorization: `Basic ${Buffer.from(`${this.credentials.clientid}:${this.credentials.clientsecret}`).toString("base64")}`,
145
+ Authorization: `Basic ${Buffer.from(`${this.credentials.authProvider?.clientid}:${this.credentials.authProvider?.clientsecret}`).toString("base64")}`,
89
146
  },
90
147
  body: new URLSearchParams(tokenOptions),
91
148
  });
@@ -102,16 +159,47 @@ class XSUAAService {
102
159
  throw new Error(`Token exchange failed: ${String(error)}`);
103
160
  }
104
161
  }
162
+ /**
163
+ * Convert access_token from authorization code into access_token with application scopes
164
+ */
165
+ async getApplicationScopes(token) {
166
+ try {
167
+ const tokenOptions = {
168
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
169
+ reponse_type: "token+id_token",
170
+ assertion: token.access_token,
171
+ };
172
+ const response = await fetch(this.endpoints.xsuaa.token_endpoint, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/x-www-form-urlencoded",
176
+ Authorization: `Basic ${Buffer.from(`${this.credentials.xsuaa?.clientid}:${this.credentials.xsuaa?.clientsecret}`).toString("base64")}`,
177
+ },
178
+ body: new URLSearchParams(tokenOptions),
179
+ });
180
+ if (!response.ok) {
181
+ const errorData = (await response.json());
182
+ throw new Error(`Token exchange for scopes failed: ${response.status} ${errorData.error_description || errorData.error}`);
183
+ }
184
+ return response.json();
185
+ }
186
+ catch (error) {
187
+ if (error instanceof Error) {
188
+ throw error;
189
+ }
190
+ throw new Error(`Token exchange for scopes failed: ${String(error)}`);
191
+ }
192
+ }
105
193
  /**
106
194
  * Refresh access token using @sap/xssec
107
195
  */
108
196
  async refreshAccessToken(refreshToken) {
109
197
  try {
110
- const response = await fetch(`${this.credentials.url}/oauth/token`, {
198
+ const response = await fetch(this.endpoints.xsuaa.token_endpoint, {
111
199
  method: "POST",
112
200
  headers: {
113
201
  "Content-Type": "application/x-www-form-urlencoded",
114
- Authorization: `Basic ${Buffer.from(`${this.credentials.clientid}:${this.credentials.clientsecret}`).toString("base64")}`,
202
+ Authorization: `Basic ${Buffer.from(`${this.credentials.xsuaa?.clientid}:${this.credentials.xsuaa?.clientsecret}`).toString("base64")}`,
115
203
  },
116
204
  body: new URLSearchParams({
117
205
  grant_type: "refresh_token",
@@ -344,7 +344,8 @@ function registerCreateTool(resAnno, server, authEnabled) {
344
344
  const inputSchema = {};
345
345
  for (const [propName, cdsType] of resAnno.properties.entries()) {
346
346
  const isAssociation = String(cdsType).toLowerCase().includes("association");
347
- if (isAssociation) {
347
+ const isComputed = resAnno.computedFields?.has(propName);
348
+ if (isAssociation || isComputed) {
348
349
  // Association keys are supplied directly from model loading as of v1.1.2
349
350
  continue;
350
351
  }
@@ -431,8 +432,9 @@ function registerUpdateTool(resAnno, server, authEnabled) {
431
432
  for (const [propName, cdsType] of resAnno.properties.entries()) {
432
433
  if (resAnno.resourceKeys.has(propName))
433
434
  continue;
435
+ const isComputed = resAnno.computedFields?.has(propName);
434
436
  const isAssociation = String(cdsType).toLowerCase().includes("association");
435
- if (isAssociation) {
437
+ if (isAssociation || isComputed) {
436
438
  // Association keys are supplied directly from model loading as of v1.1.2
437
439
  continue;
438
440
  }
@@ -70,7 +70,7 @@ class McpSessionManager {
70
70
  sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
71
71
  enableJsonResponse: enableJson,
72
72
  onsessioninitialized: (sid) => {
73
- logger_1.LOGGER.debug("Session initialized with ID: ", sid);
73
+ logger_1.LOGGER.info("Session initialized with ID: ", sid);
74
74
  logger_1.LOGGER.debug("Transport mode", { enableJsonResponse: enableJson });
75
75
  this.sessions.set(sid, {
76
76
  server: server,
package/lib/mcp.js CHANGED
@@ -13,6 +13,7 @@ const loader_1 = require("./config/loader");
13
13
  const session_manager_1 = require("./mcp/session-manager");
14
14
  const utils_2 = require("./auth/utils");
15
15
  const helmet_1 = __importDefault(require("helmet"));
16
+ const cors_1 = __importDefault(require("cors"));
16
17
  /* @ts-ignore */
17
18
  const cds = global.cds; // Use hosting app's CDS instance exclusively
18
19
  /**
@@ -41,6 +42,8 @@ class McpPlugin {
41
42
  logger_1.LOGGER.debug("Event received for 'bootstrap'");
42
43
  this.expressApp = app;
43
44
  this.expressApp.use("/mcp", express_1.default.json());
45
+ // Only needed to use MCP Inspector in local browser:
46
+ this.expressApp.use(["/oauth", "/.well-known"], (0, cors_1.default)({ origin: "http://localhost:6274" }));
44
47
  // Apply helmet security middleware only to MCP routes
45
48
  this.expressApp.use("/mcp", (0, helmet_1.default)({
46
49
  contentSecurityPolicy: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
4
4
  "description": "MCP Pluging for CAP",
5
5
  "keywords": [
6
6
  "MCP",
@@ -47,6 +47,7 @@
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.19.1",
49
49
  "@sap/xssec": "^4.9.1",
50
+ "cors": "^2.8.5",
50
51
  "helmet": "^8.1.0",
51
52
  "zod": "^3.25.67",
52
53
  "zod-to-json-schema": "^3.24.5"
@@ -54,6 +55,7 @@
54
55
  "devDependencies": {
55
56
  "@cap-js/cds-types": "^0.13.0",
56
57
  "@release-it/conventional-changelog": "^10.0.1",
58
+ "@types/cors": "^2.8.19",
57
59
  "@types/express": "^5.0.3",
58
60
  "@types/jest": "^30.0.0",
59
61
  "@types/node": "^24.0.3",