@gavdi/cap-mcp 0.10.1 → 1.0.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 CHANGED
@@ -3,8 +3,6 @@
3
3
  > This implementation is based on the Model Context Protocol (MCP) put forward by Anthropic.
4
4
  > For more information on MCP, please have a look at their [official documentation.](https://modelcontextprotocol.io/introduction)
5
5
 
6
- > 🔧 **In active development - 1.0 release scheduled for September 2025**
7
-
8
6
  # CAP-MCP Plugin
9
7
 
10
8
  A CAP (Cloud Application Programming) plugin that automatically generates Model Context Protocol (MCP) servers from your CAP services using simple annotations.
@@ -21,13 +19,6 @@ By integrating MCP with your CAP applications, you unlock:
21
19
  - **Developer Productivity**: Allow AI assistants to help developers understand, query, and work with your CAP data models
22
20
  - **Business Intelligence**: Transform your structured business data into AI-queryable resources for insights and analysis
23
21
 
24
- ## ⚠️ Development Status
25
-
26
- **This plugin is currently in active development and approaching production readiness.**
27
- APIs and annotations may change in future releases. Authentication and security features are implemented and tested.
28
-
29
- Version 1.0 of the plugin is scheduled for release in Summer 2025 after final stability testing and documentation completion.
30
-
31
22
  ## 🚀 Quick Setup
32
23
 
33
24
  ### Prerequisites
@@ -379,9 +370,10 @@ Disables authentication completely:
379
370
 
380
371
  #### Authentication Flow
381
372
  1. MCP client connects to `/mcp` endpoint
382
- 2. CAP authentication middleware validates credentials (if `auth: "inherit"`)
383
- 3. MCP session established with authenticated user context
384
- 4. All MCP operations (resources, tools, prompts) inherit the authenticated user's permissions
373
+ 2. If the authentication style used is OAuth, the OAuth flow will be executed
374
+ 3. CAP authentication middleware validates credentials (if `auth: "inherit"`)
375
+ 4. MCP session established with authenticated user context
376
+ 5. All MCP operations (resources, tools, prompts) inherit the authenticated user's permissions
385
377
 
386
378
  ### Automatic Features
387
379
 
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.authHandlerFactory = authHandlerFactory;
4
4
  exports.errorHandlerFactory = errorHandlerFactory;
5
+ const xsuaa_service_1 = require("./xsuaa-service");
5
6
  /** JSON-RPC 2.0 error code for unauthorized requests */
6
7
  const RPC_UNAUTHORIZED = 10;
7
8
  /* @ts-ignore */
@@ -32,7 +33,8 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
32
33
  */
33
34
  function authHandlerFactory() {
34
35
  const authKind = cds.env.requires.auth.kind;
35
- return (req, res, next) => {
36
+ const xsuaaService = new xsuaa_service_1.XSUAAService();
37
+ return async (req, res, next) => {
36
38
  if (!req.headers.authorization && authKind !== "dummy") {
37
39
  res.status(401).json({
38
40
  jsonrpc: "2.0",
@@ -44,6 +46,25 @@ function authHandlerFactory() {
44
46
  });
45
47
  return;
46
48
  }
49
+ // For XSUAA/JWT auth types, use @sap/xssec for validation
50
+ if ((authKind === "jwt" || authKind === "xsuaa") &&
51
+ xsuaaService.isConfigured()) {
52
+ const securityContext = await xsuaaService.createSecurityContext(req);
53
+ if (!securityContext) {
54
+ res.status(401).json({
55
+ jsonrpc: "2.0",
56
+ error: {
57
+ code: RPC_UNAUTHORIZED,
58
+ message: "Invalid or expired token",
59
+ id: null,
60
+ },
61
+ });
62
+ return;
63
+ }
64
+ // Add security context to request for later use
65
+ req.securityContext = securityContext;
66
+ }
67
+ // Continue with existing CAP context validation
47
68
  const ctx = cds.context;
48
69
  if (!ctx) {
49
70
  res.status(500).json({
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleTokenRequest = handleTokenRequest;
4
+ const logger_1 = require("../logger");
5
+ /**
6
+ * Reusable OAuth token handler function
7
+ * Handles both GET and POST requests by extracting parameters from both query and body
8
+ * This unified approach follows lemaiwo's successful pattern and works around MCP SDK inconsistencies
9
+ */
10
+ async function handleTokenRequest(req, res, xsuaaService) {
11
+ try {
12
+ const params = { ...req.query, ...req.body };
13
+ const { grant_type, code, redirect_uri, refresh_token } = params;
14
+ if (grant_type === "authorization_code") {
15
+ if (!code || !redirect_uri) {
16
+ res.status(400).json({
17
+ error: "invalid_request",
18
+ error_description: "Missing code or redirect_uri",
19
+ });
20
+ return;
21
+ }
22
+ const tokenData = await xsuaaService.exchangeCodeForToken(code, redirect_uri);
23
+ logger_1.LOGGER.debug("[AUTH] Token exchange successful");
24
+ res.json(tokenData);
25
+ }
26
+ else if (grant_type === "refresh_token") {
27
+ if (!refresh_token) {
28
+ res.status(400).json({
29
+ error: "invalid_request",
30
+ error_description: "Missing refresh_token",
31
+ });
32
+ return;
33
+ }
34
+ const tokenData = await xsuaaService.refreshAccessToken(refresh_token);
35
+ logger_1.LOGGER.debug("[AUTH] Token refresh successful");
36
+ res.json(tokenData);
37
+ }
38
+ else {
39
+ res.status(400).json({
40
+ error: "unsupported_grant_type",
41
+ error_description: "Only authorization_code and refresh_token are supported",
42
+ });
43
+ }
44
+ }
45
+ catch (error) {
46
+ logger_1.LOGGER.error("OAuth token error:", error);
47
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
48
+ res.status(400).json({
49
+ error: "invalid_grant",
50
+ error_description: errorMessage,
51
+ });
52
+ }
53
+ }
package/lib/auth/utils.js CHANGED
@@ -1,13 +1,19 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.isAuthEnabled = isAuthEnabled;
4
7
  exports.getAccessRights = getAccessRights;
5
8
  exports.registerAuthMiddleware = registerAuthMiddleware;
6
9
  exports.hasToolOperationAccess = hasToolOperationAccess;
7
10
  exports.getWrapAccesses = getWrapAccesses;
8
- const handler_1 = require("./handler");
9
- const proxyProvider_js_1 = require("@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js");
10
- const router_js_1 = require("@modelcontextprotocol/sdk/server/auth/router.js");
11
+ const express_1 = __importDefault(require("express"));
12
+ const helmet_1 = __importDefault(require("helmet"));
13
+ const factory_1 = require("./factory");
14
+ const xsuaa_service_1 = require("./xsuaa-service");
15
+ const handlers_1 = require("./handlers");
16
+ const logger_1 = require("../logger");
11
17
  /**
12
18
  * @fileoverview Authentication utilities for MCP-CAP integration.
13
19
  *
@@ -119,7 +125,7 @@ function getAccessRights(authEnabled) {
119
125
  function registerAuthMiddleware(expressApp) {
120
126
  const middlewares = cds.middlewares.before; // No types exists for this part of the CDS library
121
127
  // Build array of auth middleware to apply
122
- const authMiddleware = [];
128
+ const authMiddleware = []; // Required any as a workaround for untyped cds middleware
123
129
  // Add CAP middleware
124
130
  middlewares.forEach((mw) => {
125
131
  const process = mw.factory();
@@ -128,12 +134,12 @@ function registerAuthMiddleware(expressApp) {
128
134
  }
129
135
  });
130
136
  // Add MCP auth middleware
131
- authMiddleware.push((0, handler_1.errorHandlerFactory)());
132
- authMiddleware.push((0, handler_1.authHandlerFactory)());
137
+ authMiddleware.push((0, factory_1.errorHandlerFactory)());
138
+ authMiddleware.push((0, factory_1.authHandlerFactory)());
139
+ // If we require OAuth then we should also apply for that
140
+ configureOAuthProxy(expressApp);
133
141
  // Apply auth middleware to all /mcp routes EXCEPT health
134
142
  expressApp?.use(/^\/mcp(?!\/health).*/, ...authMiddleware);
135
- // Then finally we add the oauth proxy to the xsuaa instance
136
- configureOAuthProxy(expressApp);
137
143
  }
138
144
  /**
139
145
  * Configures OAuth proxy middleware for enterprise authentication scenarios.
@@ -175,42 +181,214 @@ function configureOAuthProxy(expressApp) {
175
181
  const config = cds.env.requires.auth;
176
182
  const kind = config.kind;
177
183
  const credentials = config.credentials;
178
- // Safety guard - skip OAuth proxy for basic auth types
184
+ // PRESERVE existing logic - skip OAuth proxy for basic auth types
179
185
  if (kind === "dummy" || kind === "mocked" || kind === "basic")
180
186
  return;
181
- else if (!credentials ||
187
+ // PRESERVE existing validation
188
+ if (!credentials ||
182
189
  !credentials.clientid ||
183
190
  !credentials.clientsecret ||
184
191
  !credentials.url) {
185
192
  throw new Error("Invalid security credentials");
186
193
  }
187
- const proxyProvider = new proxyProvider_js_1.ProxyOAuthServerProvider({
188
- endpoints: {
189
- authorizationUrl: `${credentials.url}/oauth/authorize`,
190
- tokenUrl: `${credentials.url}/oauth/token`,
191
- revocationUrl: `${credentials.url}/oauth/revoke`,
194
+ registerOAuthEndpoints(expressApp, credentials);
195
+ }
196
+ /**
197
+ * Determines the correct protocol (HTTP/HTTPS) for URL construction.
198
+ * Accounts for reverse proxy headers and production environment defaults.
199
+ *
200
+ * @param req - Express request object
201
+ * @returns Protocol string ('http' or 'https')
202
+ */
203
+ function getProtocol(req) {
204
+ // Check for reverse proxy header first (most reliable)
205
+ if (req.headers["x-forwarded-proto"]) {
206
+ return req.headers["x-forwarded-proto"];
207
+ }
208
+ // Default to HTTPS in production environments
209
+ const isProduction = process.env.NODE_ENV === "production" || process.env.VCAP_APPLICATION;
210
+ return isProduction ? "https" : req.protocol;
211
+ }
212
+ /**
213
+ * Registers OAuth endpoints for XSUAA integration
214
+ * Only called for jwt/xsuaa/ias auth types with valid credentials
215
+ */
216
+ function registerOAuthEndpoints(expressApp, credentials) {
217
+ const xsuaaService = new xsuaa_service_1.XSUAAService();
218
+ // Add JSON and URL-encoded body parsing for OAuth endpoints
219
+ expressApp.use("/oauth", express_1.default.json());
220
+ expressApp.use("/oauth", express_1.default.urlencoded({ extended: true }));
221
+ // Apply helmet security middleware only to OAuth routes
222
+ expressApp.use("/oauth", (0, helmet_1.default)({
223
+ contentSecurityPolicy: {
224
+ directives: {
225
+ defaultSrc: ["'self'"],
226
+ styleSrc: ["'self'", "'unsafe-inline'"],
227
+ scriptSrc: ["'self'"],
228
+ imgSrc: ["'self'", "data:", "https:"],
229
+ },
192
230
  },
193
- verifyAccessToken: async (token) => {
194
- return {
195
- token,
196
- clientId: credentials.clientid,
197
- scopes: ["uaa.resource"],
231
+ }));
232
+ // OAuth Authorization endpoint - stateless redirect to XSUAA
233
+ expressApp.get("/oauth/authorize", (req, res) => {
234
+ const { state, redirect_uri } = req.query;
235
+ // Client validation and redirect URI validation is handled by XSUAA
236
+ // We delegate all client management to XSUAA's built-in OAuth server
237
+ const protocol = getProtocol(req);
238
+ const redirectUri = redirect_uri || `${protocol}://${req.get("host")}/oauth/callback`;
239
+ const authUrl = xsuaaService.getAuthorizationUrl(redirectUri, state);
240
+ res.redirect(authUrl);
241
+ });
242
+ // OAuth Callback endpoint - stateless token exchange
243
+ expressApp.get("/oauth/callback", async (req, res) => {
244
+ const { code, state, error, error_description, redirect_uri } = req.query;
245
+ logger_1.LOGGER.debug("[AUTH] Callback received", code, state);
246
+ if (error) {
247
+ res.status(400).json({
248
+ error: "authorization_failed",
249
+ error_description: error_description || error,
250
+ });
251
+ return;
252
+ }
253
+ if (!code) {
254
+ res.status(400).json({
255
+ error: "invalid_request",
256
+ error_description: "Missing authorization code",
257
+ });
258
+ return;
259
+ }
260
+ try {
261
+ const protocol = getProtocol(req);
262
+ const url = redirect_uri || `${protocol}://${req.get("host")}/oauth/callback`;
263
+ const tokenData = await xsuaaService.exchangeCodeForToken(code, url);
264
+ res.json(tokenData);
265
+ }
266
+ catch (error) {
267
+ logger_1.LOGGER.error("OAuth callback error:", error);
268
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
269
+ res.status(400).json({
270
+ error: "token_exchange_failed",
271
+ error_description: errorMessage,
272
+ });
273
+ }
274
+ });
275
+ // OAuth Token endpoint - POST (standard OAuth 2.0)
276
+ expressApp.post("/oauth/token", async (req, res) => {
277
+ await (0, handlers_1.handleTokenRequest)(req, res, xsuaaService);
278
+ });
279
+ // OAuth Discovery endpoint
280
+ expressApp.get("/.well-known/oauth-authorization-server", (req, res) => {
281
+ const protocol = getProtocol(req);
282
+ const baseUrl = `${protocol}://${req.get("host")}`;
283
+ res.json({
284
+ issuer: credentials.url,
285
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
286
+ token_endpoint: `${baseUrl}/oauth/token`,
287
+ registration_endpoint: `${baseUrl}/oauth/register`,
288
+ response_types_supported: ["code"],
289
+ grant_types_supported: ["authorization_code", "refresh_token"],
290
+ code_challenge_methods_supported: ["S256"],
291
+ // scopes_supported: ["uaa.resource"],
292
+ token_endpoint_auth_methods_supported: ["client_secret_post"],
293
+ registration_endpoint_auth_methods_supported: ["client_secret_basic"],
294
+ });
295
+ });
296
+ // OAuth Dynamic Client Registration discovery endpoint (GET)
297
+ expressApp.get("/oauth/register", async (req, res) => {
298
+ try {
299
+ // Simple proxy for discovery - no CSRF needed
300
+ const response = await fetch(`${credentials.url}/oauth/register`, {
301
+ method: "GET",
302
+ headers: {
303
+ Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
304
+ Accept: "application/json",
305
+ },
306
+ });
307
+ const xsuaaData = await response.json();
308
+ // Add missing required fields that MCP client expects
309
+ const protocol = getProtocol(req);
310
+ const enhancedResponse = {
311
+ ...xsuaaData, // Keep all XSUAA fields
312
+ client_id: credentials.clientid, // Add our CAP app's client ID
313
+ redirect_uris: [`${protocol}://${req.get("host")}/oauth/callback`], // Add our callback URL for discovery
198
314
  };
199
- },
200
- getClient: async (client_id) => {
201
- return {
202
- client_secret: credentials.clientsecret,
203
- client_id,
204
- redirect_uris: ["http://localhost:3000/callback"], // Temporary value for now
315
+ res.status(response.status).json(enhancedResponse);
316
+ }
317
+ catch (error) {
318
+ logger_1.LOGGER.error("OAuth registration discovery error:", error);
319
+ res.status(500).json({
320
+ error: "server_error",
321
+ error_description: error instanceof Error ? error.message : "Unknown error",
322
+ });
323
+ }
324
+ });
325
+ // OAuth Dynamic Client Registration endpoint (POST) with CSRF handling
326
+ expressApp.post("/oauth/register", async (req, res) => {
327
+ try {
328
+ // Step 1: Fetch CSRF token from XSUAA
329
+ const csrfResponse = await fetch(`${credentials.url}/oauth/register`, {
330
+ method: "GET",
331
+ headers: {
332
+ "X-CSRF-Token": "Fetch",
333
+ Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
334
+ Accept: "application/json",
335
+ },
336
+ });
337
+ if (!csrfResponse.ok) {
338
+ throw new Error(`CSRF fetch failed: ${csrfResponse.status}`);
339
+ }
340
+ // Step 2: Extract CSRF token and session cookie
341
+ const setCookieHeader = csrfResponse.headers.get("set-cookie") || "";
342
+ const csrfToken = extractCsrfFromCookie(setCookieHeader);
343
+ if (!csrfToken) {
344
+ throw new Error("Could not extract CSRF token from XSUAA response");
345
+ }
346
+ // Step 3: Make actual registration POST with CSRF token
347
+ const registrationResponse = await fetch(`${credentials.url}/oauth/register`, {
348
+ method: "POST",
349
+ headers: {
350
+ "Content-Type": "application/json",
351
+ "X-CSRF-Token": csrfToken,
352
+ Cookie: setCookieHeader,
353
+ Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
354
+ Accept: "application/json",
355
+ },
356
+ body: JSON.stringify(req.body),
357
+ });
358
+ const xsuaaData = await registrationResponse.json();
359
+ // Add missing required fields that MCP client expects
360
+ const protocol = getProtocol(req);
361
+ const enhancedResponse = {
362
+ ...xsuaaData, // Keep all XSUAA fields
363
+ client_id: credentials.clientid, // Add our CAP app's client ID
364
+ client_secret: credentials.clientsecret, // CAP app's client secret
365
+ redirect_uris: req.body.redirect_uris || [
366
+ `${protocol}://${req.get("host")}/oauth/callback`,
367
+ ], // Use client's redirect URIs
205
368
  };
206
- },
369
+ logger_1.LOGGER.debug("[AUTH] Register POST response", enhancedResponse);
370
+ res.status(registrationResponse.status).json(enhancedResponse);
371
+ }
372
+ catch (error) {
373
+ logger_1.LOGGER.error("OAuth registration error:", error);
374
+ res.status(500).json({
375
+ error: "server_error",
376
+ error_description: error instanceof Error ? error.message : "Unknown error",
377
+ });
378
+ }
207
379
  });
208
- expressApp.use((0, router_js_1.mcpAuthRouter)({
209
- provider: proxyProvider,
210
- issuerUrl: new URL(credentials.url),
211
- //baseUrl: new URL(""), // I have left this out for the time being due to the defaulting to issuer
212
- serviceDocumentationUrl: new URL("https://docs.cloudfoundry.org/api/uaa/version/77.34.0/index.html#authorization"),
213
- }));
380
+ logger_1.LOGGER.debug("OAuth endpoints registered for XSUAA integration");
381
+ }
382
+ /**
383
+ * Extracts CSRF token from XSUAA Set-Cookie header
384
+ * Looks for "X-Uaa-Csrf=<token>" pattern in the cookie string
385
+ */
386
+ function extractCsrfFromCookie(setCookieHeader) {
387
+ if (!setCookieHeader)
388
+ return null;
389
+ // Match X-Uaa-Csrf=<token> pattern
390
+ const csrfMatch = setCookieHeader.match(/X-Uaa-Csrf=([^;,]+)/i);
391
+ return csrfMatch ? csrfMatch[1] : null;
214
392
  }
215
393
  /**
216
394
  * Checks whether the requesting user's access matches that of the roles required
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.XSUAAService = void 0;
37
+ const xssec = __importStar(require("@sap/xssec"));
38
+ const logger_1 = require("../logger");
39
+ /* @ts-ignore */
40
+ const cds = global.cds || require("@sap/cds");
41
+ /**
42
+ * XSUAA service using official @sap/xssec library
43
+ * Leverages SAP's official authentication and validation mechanisms
44
+ */
45
+ class XSUAAService {
46
+ credentials;
47
+ xsuaaService;
48
+ constructor() {
49
+ this.credentials = cds.env.requires.auth.credentials;
50
+ // Initialize XSUAA service from @sap/xssec
51
+ this.xsuaaService = new xssec.XsuaaService(this.credentials);
52
+ }
53
+ isConfigured() {
54
+ return !!(this.credentials?.clientid &&
55
+ this.credentials?.clientsecret &&
56
+ this.credentials?.url);
57
+ }
58
+ /**
59
+ * Generates authorization URL using @sap/xssec
60
+ */
61
+ getAuthorizationUrl(redirectUri, state) {
62
+ const params = new URLSearchParams({
63
+ response_type: "code",
64
+ client_id: this.credentials.clientid,
65
+ redirect_uri: redirectUri,
66
+ // scope: "uaa.resource",
67
+ });
68
+ if (state) {
69
+ params.append("state", state);
70
+ }
71
+ return `${this.credentials.url}/oauth/authorize?${params.toString()}`;
72
+ }
73
+ /**
74
+ * Exchange authorization code for token using @sap/xssec
75
+ */
76
+ async exchangeCodeForToken(code, redirectUri) {
77
+ try {
78
+ const tokenOptions = {
79
+ grant_type: "authorization_code",
80
+ code,
81
+ redirect_uri: redirectUri,
82
+ };
83
+ // Use direct XSUAA token endpoint for authorization code exchange
84
+ const response = await fetch(`${this.credentials.url}/oauth/token`, {
85
+ method: "POST",
86
+ headers: {
87
+ "Content-Type": "application/x-www-form-urlencoded",
88
+ Authorization: `Basic ${Buffer.from(`${this.credentials.clientid}:${this.credentials.clientsecret}`).toString("base64")}`,
89
+ },
90
+ body: new URLSearchParams(tokenOptions),
91
+ });
92
+ if (!response.ok) {
93
+ const errorData = (await response.json());
94
+ throw new Error(`Token exchange failed: ${response.status} ${errorData.error_description || errorData.error}`);
95
+ }
96
+ return response.json();
97
+ }
98
+ catch (error) {
99
+ if (error instanceof Error) {
100
+ throw error;
101
+ }
102
+ throw new Error(`Token exchange failed: ${String(error)}`);
103
+ }
104
+ }
105
+ /**
106
+ * Refresh access token using @sap/xssec
107
+ */
108
+ async refreshAccessToken(refreshToken) {
109
+ try {
110
+ const response = await fetch(`${this.credentials.url}/oauth/token`, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/x-www-form-urlencoded",
114
+ Authorization: `Basic ${Buffer.from(`${this.credentials.clientid}:${this.credentials.clientsecret}`).toString("base64")}`,
115
+ },
116
+ body: new URLSearchParams({
117
+ grant_type: "refresh_token",
118
+ refresh_token: refreshToken,
119
+ }),
120
+ });
121
+ if (!response.ok) {
122
+ const errorData = (await response.json());
123
+ throw new Error(`Token refresh failed: ${response.status} ${errorData.error_description || errorData.error}`);
124
+ }
125
+ return response.json();
126
+ }
127
+ catch (error) {
128
+ if (error instanceof Error) {
129
+ throw error;
130
+ }
131
+ throw new Error(`Token refresh failed: ${String(error)}`);
132
+ }
133
+ }
134
+ /**
135
+ * Validate JWT token using @sap/xssec SecurityContext
136
+ * This is the proper way to validate tokens with XSUAA
137
+ */
138
+ async validateToken(accessToken, req) {
139
+ try {
140
+ // Create security context using @sap/xssec
141
+ const securityContext = await xssec.createSecurityContext(this.xsuaaService, {
142
+ req: req || { headers: { authorization: `Bearer ${accessToken}` } },
143
+ token: accessToken,
144
+ });
145
+ // If security context is created successfully, token is valid
146
+ return !!securityContext;
147
+ }
148
+ catch (error) {
149
+ // Log validation errors for debugging
150
+ if (error instanceof xssec.errors.TokenValidationError) {
151
+ logger_1.LOGGER.warn("Token validation failed:", error.message);
152
+ }
153
+ else if (error instanceof Error) {
154
+ logger_1.LOGGER.warn("Token validation failed:", error.message);
155
+ }
156
+ return false;
157
+ }
158
+ }
159
+ /**
160
+ * Create security context for authenticated requests
161
+ * Returns null if token is invalid
162
+ */
163
+ async createSecurityContext(req) {
164
+ try {
165
+ const authHeader = req.headers.authorization;
166
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
167
+ return null;
168
+ }
169
+ const token = authHeader.substring(7);
170
+ const securityContext = await xssec.createSecurityContext(this.xsuaaService, { req, token: token });
171
+ return securityContext;
172
+ }
173
+ catch (error) {
174
+ if (error instanceof xssec.errors.TokenValidationError) {
175
+ logger_1.LOGGER.warn("Security context creation failed:", error.message);
176
+ }
177
+ else if (error instanceof Error) {
178
+ logger_1.LOGGER.warn("Security context creation failed:", error.message);
179
+ }
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * Get XSUAA service instance for advanced operations
185
+ */
186
+ getXsuaaService() {
187
+ return this.xsuaaService;
188
+ }
189
+ }
190
+ exports.XSUAAService = XSUAAService;
package/lib/mcp.js CHANGED
@@ -12,6 +12,7 @@ const constants_1 = require("./mcp/constants");
12
12
  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
+ const helmet_1 = __importDefault(require("helmet"));
15
16
  /* @ts-ignore */
16
17
  const cds = global.cds; // Use hosting app's CDS instance exclusively
17
18
  /**
@@ -40,6 +41,17 @@ class McpPlugin {
40
41
  logger_1.LOGGER.debug("Event received for 'bootstrap'");
41
42
  this.expressApp = app;
42
43
  this.expressApp.use("/mcp", express_1.default.json());
44
+ // Apply helmet security middleware only to MCP routes
45
+ this.expressApp.use("/mcp", (0, helmet_1.default)({
46
+ contentSecurityPolicy: {
47
+ directives: {
48
+ defaultSrc: ["'self'"],
49
+ styleSrc: ["'self'", "'unsafe-inline'"],
50
+ scriptSrc: ["'self'"],
51
+ imgSrc: ["'self'", "data:", "https:"],
52
+ },
53
+ },
54
+ }));
43
55
  if (this.config.auth === "inherit") {
44
56
  (0, utils_2.registerAuthMiddleware)(this.expressApp);
45
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "0.10.1",
3
+ "version": "1.0.0",
4
4
  "description": "MCP Pluging for CAP",
5
5
  "keywords": [
6
6
  "MCP",
@@ -23,9 +23,9 @@
23
23
  "scripts": {
24
24
  "mock": "npm run start --workspace=test/demo",
25
25
  "mock:watch": "npm run build && npm run watch --workspace=test/demo",
26
- "test": "NODE_ENV=test jest --silent",
27
- "test:unit": "NODE_ENV=test jest --silent test/unit",
28
- "test:integration": "NODE_ENV=test jest --silent test/integration",
26
+ "test": "NODE_ENV=test jest --silent --testTimeout=30000",
27
+ "test:unit": "NODE_ENV=test jest --silent test/unit --testTimeout=30000",
28
+ "test:integration": "NODE_ENV=test jest --silent test/integration --testTimeout=30000",
29
29
  "build": "tsc",
30
30
  "inspect": "npx @modelcontextprotocol/inspector",
31
31
  "prepare": "husky",
@@ -42,6 +42,8 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.17.3",
45
+ "@sap/xssec": "^4.9.1",
46
+ "helmet": "^8.1.0",
45
47
  "zod": "^3.25.67",
46
48
  "zod-to-json-schema": "^3.24.5"
47
49
  },