@gavdi/cap-mcp 0.9.2 → 0.9.3
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 +264 -11
- package/lib/auth/adapter.js +2 -0
- package/lib/auth/handler.js +109 -0
- package/lib/auth/mock.js +2 -0
- package/lib/auth/utils.js +131 -0
- package/lib/config/json-parser.js +1 -0
- package/lib/config/loader.js +1 -0
- package/lib/mcp/factory.js +4 -3
- package/lib/mcp/resources.js +8 -5
- package/lib/mcp/tools.js +12 -7
- package/lib/mcp.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
@@ -23,15 +23,10 @@ By integrating MCP with your CAP applications, you unlock:
|
|
23
23
|
|
24
24
|
## ⚠️ Development Status
|
25
25
|
|
26
|
-
**This plugin is currently in active development (v0.9.
|
27
|
-
APIs and annotations may change in future releases.
|
26
|
+
**This plugin is currently in active development (v0.9.2) and approaching production readiness.**
|
27
|
+
APIs and annotations may change in future releases. Authentication and security features are implemented and tested.
|
28
28
|
|
29
|
-
Version 1.0 of the plugin is
|
30
|
-
|
31
|
-
### 👾 Known Bugs
|
32
|
-
|
33
|
-
> ❗Currently there is a bug in the `@modelcontextprotocol/sdk` package that breaks the RFC template strings. The issue has been reported.
|
34
|
-
> Until this issue is fixed, the resource reads for dynamic queries are only possible if all query options are provided.
|
29
|
+
Version 1.0 of the plugin is scheduled for release in Summer 2025 after final stability testing and documentation completion.
|
35
30
|
|
36
31
|
## 📦 Installation
|
37
32
|
|
@@ -41,6 +36,78 @@ npm install @gavdi/cap-mcp
|
|
41
36
|
|
42
37
|
The plugin follows CAP's standard plugin architecture and will automatically integrate with your CAP application.
|
43
38
|
|
39
|
+
## 🚀 Quick Setup
|
40
|
+
|
41
|
+
### Prerequisites
|
42
|
+
|
43
|
+
- **Node.js**: Version 18 or higher
|
44
|
+
- **SAP CAP**: Version 9 or higher
|
45
|
+
- **Express**: Version 4 or higher
|
46
|
+
- **TypeScript**: Optional but recommended
|
47
|
+
|
48
|
+
### Step 1: Install the Plugin
|
49
|
+
|
50
|
+
```bash
|
51
|
+
npm install @gavdi/cap-mcp
|
52
|
+
```
|
53
|
+
|
54
|
+
### Step 2: Configure Your CAP Application
|
55
|
+
|
56
|
+
Add MCP configuration to your `package.json`:
|
57
|
+
|
58
|
+
```json
|
59
|
+
{
|
60
|
+
"cds": {
|
61
|
+
"mcp": {
|
62
|
+
"name": "my-bookshop-mcp",
|
63
|
+
"auth": "inherit"
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
```
|
68
|
+
|
69
|
+
### Step 3: Add MCP Annotations
|
70
|
+
|
71
|
+
Annotate your CAP services with `@mcp` annotations:
|
72
|
+
|
73
|
+
```cds
|
74
|
+
// srv/catalog-service.cds
|
75
|
+
service CatalogService {
|
76
|
+
|
77
|
+
@mcp: {
|
78
|
+
name: 'books',
|
79
|
+
description: 'Book catalog with search and filtering',
|
80
|
+
resource: ['filter', 'orderby', 'select', 'top', 'skip']
|
81
|
+
}
|
82
|
+
entity Books as projection on my.Books;
|
83
|
+
|
84
|
+
@mcp: {
|
85
|
+
name: 'get-book-recommendations',
|
86
|
+
description: 'Get personalized book recommendations',
|
87
|
+
tool: true
|
88
|
+
}
|
89
|
+
function getRecommendations(genre: String, limit: Integer) returns array of String;
|
90
|
+
}
|
91
|
+
```
|
92
|
+
|
93
|
+
### Step 4: Start Your Application
|
94
|
+
|
95
|
+
```bash
|
96
|
+
cds serve
|
97
|
+
```
|
98
|
+
|
99
|
+
The MCP server will be available at:
|
100
|
+
- **MCP Endpoint**: `http://localhost:4004/mcp`
|
101
|
+
- **Health Check**: `http://localhost:4004/mcp/health`
|
102
|
+
|
103
|
+
### Step 5: Test with MCP Inspector
|
104
|
+
|
105
|
+
```bash
|
106
|
+
npx @modelcontextprotocol/inspector
|
107
|
+
```
|
108
|
+
|
109
|
+
Connect to `http://localhost:4004/mcp` to explore your generated MCP resources, tools, and prompts.
|
110
|
+
|
44
111
|
## 🎯 Features
|
45
112
|
|
46
113
|
This plugin transforms your annotated CAP services into a fully functional MCP server that can be consumed by any MCP-compatible AI client.
|
@@ -142,10 +209,97 @@ annotate CatalogService with @mcp.prompts: [{
|
|
142
209
|
|
143
210
|
## 🔧 Configuration
|
144
211
|
|
212
|
+
### Plugin Configuration
|
213
|
+
|
214
|
+
Configure the MCP plugin through your CAP application's `package.json` or `.cdsrc` file:
|
215
|
+
|
216
|
+
```json
|
217
|
+
{
|
218
|
+
"cds": {
|
219
|
+
"mcp": {
|
220
|
+
"name": "my-mcp-server",
|
221
|
+
"version": "1.0.0",
|
222
|
+
"auth": "inherit",
|
223
|
+
"capabilities": {
|
224
|
+
"resources": {
|
225
|
+
"listChanged": true,
|
226
|
+
"subscribe": false
|
227
|
+
},
|
228
|
+
"tools": {
|
229
|
+
"listChanged": true
|
230
|
+
},
|
231
|
+
"prompts": {
|
232
|
+
"listChanged": true
|
233
|
+
}
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
}
|
238
|
+
```
|
239
|
+
|
240
|
+
### Configuration Options
|
241
|
+
|
242
|
+
| Option | Type | Default | Description |
|
243
|
+
|--------|------|---------|-------------|
|
244
|
+
| `name` | string | package.json name | MCP server name |
|
245
|
+
| `version` | string | package.json version | MCP server version |
|
246
|
+
| `auth` | `"inherit"` \| `"none"` | `"inherit"` | Authentication mode |
|
247
|
+
| `capabilities.resources.listChanged` | boolean | `true` | Enable resource list change notifications |
|
248
|
+
| `capabilities.resources.subscribe` | boolean | `false` | Enable resource subscriptions |
|
249
|
+
| `capabilities.tools.listChanged` | boolean | `true` | Enable tool list change notifications |
|
250
|
+
| `capabilities.prompts.listChanged` | boolean | `true` | Enable prompt list change notifications |
|
251
|
+
|
252
|
+
### Authentication Configuration
|
253
|
+
|
254
|
+
The plugin supports two authentication modes:
|
255
|
+
|
256
|
+
#### `"inherit"` Mode (Default)
|
257
|
+
Uses your CAP application's existing authentication system:
|
258
|
+
|
259
|
+
```json
|
260
|
+
{
|
261
|
+
"cds": {
|
262
|
+
"mcp": {
|
263
|
+
"auth": "inherit"
|
264
|
+
},
|
265
|
+
"requires": {
|
266
|
+
"auth": {
|
267
|
+
"kind": "xsuaa"
|
268
|
+
}
|
269
|
+
}
|
270
|
+
}
|
271
|
+
}
|
272
|
+
```
|
273
|
+
|
274
|
+
#### `"none"` Mode (Development/Testing)
|
275
|
+
Disables authentication completely:
|
276
|
+
|
277
|
+
```json
|
278
|
+
{
|
279
|
+
"cds": {
|
280
|
+
"mcp": {
|
281
|
+
"auth": "none"
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
```
|
286
|
+
|
287
|
+
**⚠️ Security Warning**: Only use `"none"` mode in development environments. Never deploy to production without proper authentication.
|
288
|
+
|
289
|
+
#### Authentication Flow
|
290
|
+
1. MCP client connects to `/mcp` endpoint
|
291
|
+
2. CAP authentication middleware validates credentials (if `auth: "inherit"`)
|
292
|
+
3. MCP session established with authenticated user context
|
293
|
+
4. All MCP operations (resources, tools, prompts) inherit the authenticated user's permissions
|
294
|
+
|
295
|
+
### Automatic Features
|
296
|
+
|
145
297
|
The plugin automatically:
|
146
298
|
- Scans your CAP service definitions for `@mcp` annotations
|
147
299
|
- Generates appropriate MCP resources, tools, and prompts
|
148
300
|
- Creates ResourceTemplates with proper OData v4 query parameter support
|
301
|
+
- Sets up HTTP endpoints at `/mcp` and `/mcp/health`
|
302
|
+
- Manages MCP session lifecycle and cleanup
|
149
303
|
|
150
304
|
## 🌟 Example AI Interactions
|
151
305
|
|
@@ -233,11 +387,13 @@ Would you like me to help you review any of these in detail?"
|
|
233
387
|
- **Integration Ready**: Works with existing CAP-based workflow systems
|
234
388
|
- **Mobile Friendly**: Access approvals from any MCP-compatible AI client
|
235
389
|
|
236
|
-
## 🧰 Testing
|
390
|
+
## 🧰 Development & Testing
|
391
|
+
|
392
|
+
### Testing Your MCP Implementation
|
237
393
|
|
238
394
|
If you want to test the MCP implementation you have made on your CAP application locally, you have 2 options available (that does not involve direct integration with AI Agent).
|
239
395
|
|
240
|
-
|
396
|
+
#### Option #1 - MCP Inspector
|
241
397
|
|
242
398
|
You can inspect the MCP implementation by utilizing the official `@modelcontextprotocol/inspector`.
|
243
399
|
|
@@ -247,10 +403,28 @@ For plugin implementation implementation in your own project it is recommended t
|
|
247
403
|
|
248
404
|
For more information on the inspector, please [see the official documentation](https://github.com/modelcontextprotocol/inspector).
|
249
405
|
|
250
|
-
|
406
|
+
#### Option #2 - Bruno Collection
|
251
407
|
|
252
408
|
This repository comes with a Bruno collection available that includes some example queries you can use to verify your MCP implementation. These can be found in the `bruno` directory.
|
253
409
|
|
410
|
+
#### Option #3 - Automated Testing
|
411
|
+
|
412
|
+
Run the comprehensive test suite to validate your implementation:
|
413
|
+
|
414
|
+
```bash
|
415
|
+
# Test specific components
|
416
|
+
npm test -- --testPathPattern=annotations # Test annotation parsing
|
417
|
+
npm test -- --testPathPattern=mcp # Test MCP functionality
|
418
|
+
npm test -- --testPathPattern=security # Test security boundaries
|
419
|
+
npm test -- --testPathPattern=auth # Test authentication
|
420
|
+
|
421
|
+
# Run with detailed output
|
422
|
+
npm test -- --verbose
|
423
|
+
|
424
|
+
# Run in watch mode for development
|
425
|
+
npm test -- --watch
|
426
|
+
```
|
427
|
+
|
254
428
|
## 🤝 Contributing
|
255
429
|
|
256
430
|
Contributions are welcome! This is an open-source project aimed at bridging CAP applications with the AI ecosystem.
|
@@ -264,11 +438,90 @@ Contributions are welcome! This is an open-source project aimed at bridging CAP
|
|
264
438
|
|
265
439
|
This project is licensed under the Apache-2.0 License - see the [LICENSE.md](LICENSE.md) file for details.
|
266
440
|
|
441
|
+
## 🔧 Troubleshooting
|
442
|
+
|
443
|
+
### Common Issues
|
444
|
+
|
445
|
+
#### MCP Server Not Starting
|
446
|
+
- **Check Port Availability**: Ensure port 4004 is not in use by another process
|
447
|
+
- **Verify CAP Service**: Make sure your CAP application starts successfully with `cds serve`
|
448
|
+
- **Authentication Issues**: If using `auth: "inherit"`, ensure your CAP authentication is properly configured
|
449
|
+
|
450
|
+
#### MCP Client Connection Failures
|
451
|
+
```bash
|
452
|
+
# Check if MCP endpoint is accessible
|
453
|
+
curl http://localhost:4004/mcp/health
|
454
|
+
|
455
|
+
# Expected response:
|
456
|
+
# {"status": "healthy", "timestamp": "2025-01-XX..."}
|
457
|
+
```
|
458
|
+
|
459
|
+
#### Annotation Not Working
|
460
|
+
- **Syntax Check**: Verify your `@mcp` annotation syntax matches the examples
|
461
|
+
- **Service Deployment**: Ensure annotated entities/functions are properly deployed
|
462
|
+
- **Case Sensitivity**: Check that annotation properties use correct casing (`resource`, `tool`, `prompts`)
|
463
|
+
|
464
|
+
#### OData Query Issues
|
465
|
+
- **SDK Bug Workaround**: Due to the known `@modelcontextprotocol/sdk` bug, provide all query parameters when using dynamic queries
|
466
|
+
- **Parameter Validation**: Ensure query parameters match OData v4 syntax
|
467
|
+
|
468
|
+
#### Performance Issues
|
469
|
+
- **Resource Filtering**: Use specific `resource` arrays instead of `true` for large datasets
|
470
|
+
- **Query Optimization**: Implement proper database indexes for frequently queried fields
|
471
|
+
|
472
|
+
### Debugging
|
473
|
+
|
474
|
+
#### Enable Debug Logging
|
475
|
+
```json
|
476
|
+
{
|
477
|
+
"cds": {
|
478
|
+
"log": {
|
479
|
+
"levels": {
|
480
|
+
"mcp": "debug"
|
481
|
+
}
|
482
|
+
}
|
483
|
+
}
|
484
|
+
}
|
485
|
+
```
|
486
|
+
|
487
|
+
#### Test MCP Implementation
|
488
|
+
```bash
|
489
|
+
# Use MCP Inspector for interactive testing
|
490
|
+
npm run inspect
|
491
|
+
|
492
|
+
# Or run integration tests
|
493
|
+
npm test -- --testPathPattern=integration
|
494
|
+
```
|
495
|
+
|
496
|
+
### Getting Help
|
497
|
+
|
498
|
+
- **GitHub Issues**: Report bugs at [gavdilabs/cap-mcp-plugin](https://github.com/gavdilabs/cap-mcp-plugin/issues)
|
499
|
+
- **Documentation**: Check [MCP Specification](https://modelcontextprotocol.io) for protocol details
|
500
|
+
- **CAP Support**: Refer to [SAP CAP Documentation](https://cap.cloud.sap) for CAP-specific issues
|
501
|
+
|
502
|
+
## 🚨 Performance & Limitations
|
503
|
+
|
504
|
+
### Known Limitations
|
505
|
+
- **SDK Bug**: Dynamic resource queries require all query parameters due to `@modelcontextprotocol/sdk` RFC template string issue
|
506
|
+
- **Authentication Inheritance**: MCP authentication is tightly coupled to CAP's authentication system
|
507
|
+
- **Session Cleanup**: MCP sessions are cleaned up on HTTP connection close, not on explicit disconnect
|
508
|
+
|
509
|
+
### Performance Considerations
|
510
|
+
- **Large Datasets**: Use `resource: ['top']` or similar constraints for entities with many records
|
511
|
+
- **Complex Queries**: OData query parsing adds overhead - consider caching for frequently accessed data
|
512
|
+
- **Concurrent Sessions**: Each MCP client creates a separate session - monitor memory usage with many clients
|
513
|
+
|
514
|
+
### Scale Recommendations
|
515
|
+
- **Development**: No specific limits
|
516
|
+
- **Production**: Test with expected concurrent MCP client count
|
517
|
+
- **Enterprise**: Consider load balancing for high-availability scenarios
|
518
|
+
|
267
519
|
## 🔗 Resources
|
268
520
|
|
269
521
|
- [Model Context Protocol Specification](https://modelcontextprotocol.io)
|
270
522
|
- [SAP CAP Documentation](https://cap.cloud.sap)
|
271
523
|
- [OData v4 Specification](https://odata.org)
|
524
|
+
- [MCP Inspector Tool](https://github.com/modelcontextprotocol/inspector)
|
272
525
|
|
273
526
|
---
|
274
527
|
(c) Copyright by Gavdi Labs 2025 - All Rights Reserved
|
@@ -0,0 +1,109 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.authHandlerFactory = authHandlerFactory;
|
4
|
+
exports.errorHandlerFactory = errorHandlerFactory;
|
5
|
+
/** JSON-RPC 2.0 error code for unauthorized requests */
|
6
|
+
const RPC_UNAUTHORIZED = 10;
|
7
|
+
/* @ts-ignore */
|
8
|
+
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
9
|
+
/**
|
10
|
+
* Creates an Express middleware for MCP authentication validation.
|
11
|
+
*
|
12
|
+
* This handler validates that requests are properly authenticated based on the CAP authentication
|
13
|
+
* configuration. It checks for authorization headers (except for 'dummy' auth), validates the
|
14
|
+
* CAP context, and ensures a valid user is present.
|
15
|
+
*
|
16
|
+
* The middleware performs the following validations:
|
17
|
+
* 1. Checks for Authorization header (unless CAP auth is 'dummy')
|
18
|
+
* 2. Validates that CAP context is properly initialized
|
19
|
+
* 3. Ensures an authenticated user exists and is not anonymous
|
20
|
+
*
|
21
|
+
* @returns Express RequestHandler middleware function
|
22
|
+
*
|
23
|
+
* @example
|
24
|
+
* ```typescript
|
25
|
+
* const authMiddleware = authHandlerFactory();
|
26
|
+
* app.use('/mcp', authMiddleware);
|
27
|
+
* ```
|
28
|
+
*
|
29
|
+
* @throws {401} When authorization header is missing (non-dummy auth)
|
30
|
+
* @throws {401} When user is not authenticated or is anonymous
|
31
|
+
* @throws {500} When CAP context is not properly loaded
|
32
|
+
*/
|
33
|
+
function authHandlerFactory() {
|
34
|
+
const authKind = cds.env.requires.auth.kind;
|
35
|
+
return (req, res, next) => {
|
36
|
+
if (!req.headers.authorization && authKind !== "dummy") {
|
37
|
+
res.status(401).json({
|
38
|
+
jsonrpc: "2.0",
|
39
|
+
error: {
|
40
|
+
code: RPC_UNAUTHORIZED,
|
41
|
+
message: "Unauthorized",
|
42
|
+
id: null,
|
43
|
+
},
|
44
|
+
});
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
const ctx = cds.context;
|
48
|
+
if (!ctx) {
|
49
|
+
res.status(500).json({
|
50
|
+
jsonrpc: "2.0",
|
51
|
+
error: {
|
52
|
+
code: -32603,
|
53
|
+
message: "Internal Error: Context not correctly loaded",
|
54
|
+
id: null,
|
55
|
+
},
|
56
|
+
});
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
const user = ctx.user;
|
60
|
+
if (!user || user === cds.User.anonymous) {
|
61
|
+
res.status(401).json({
|
62
|
+
jsonrpc: "2.0",
|
63
|
+
error: {
|
64
|
+
code: RPC_UNAUTHORIZED,
|
65
|
+
message: "Unauthorized",
|
66
|
+
id: null,
|
67
|
+
},
|
68
|
+
});
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
return next();
|
72
|
+
};
|
73
|
+
}
|
74
|
+
/**
|
75
|
+
* Creates an Express error handling middleware for CAP authentication errors.
|
76
|
+
*
|
77
|
+
* This error handler catches authentication and authorization errors thrown by CAP
|
78
|
+
* middleware and converts them to JSON-RPC 2.0 compliant error responses. It handles
|
79
|
+
* both 401 (Unauthorized) and 403 (Forbidden) errors specifically.
|
80
|
+
*
|
81
|
+
* @returns Express ErrorRequestHandler middleware function
|
82
|
+
*
|
83
|
+
* @example
|
84
|
+
* ```typescript
|
85
|
+
* const errorHandler = errorHandlerFactory();
|
86
|
+
* app.use('/mcp', errorHandler);
|
87
|
+
* ```
|
88
|
+
*
|
89
|
+
* @param err - The error object, expected to be 401 or 403 for auth errors
|
90
|
+
* @param req - Express request object (unused, marked with underscore)
|
91
|
+
* @param res - Express response object for sending error responses
|
92
|
+
* @param next - Express next function for passing unhandled errors
|
93
|
+
*/
|
94
|
+
function errorHandlerFactory() {
|
95
|
+
return (err, _, res, next) => {
|
96
|
+
if (err === 401 || err === 403) {
|
97
|
+
res.status(err).json({
|
98
|
+
jsonrpc: "2.0",
|
99
|
+
error: {
|
100
|
+
code: RPC_UNAUTHORIZED,
|
101
|
+
message: err === 401 ? "Unauthorized" : "Forbidden",
|
102
|
+
id: null,
|
103
|
+
},
|
104
|
+
});
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
next(err);
|
108
|
+
};
|
109
|
+
}
|
package/lib/auth/mock.js
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.isAuthEnabled = isAuthEnabled;
|
4
|
+
exports.getAccessRights = getAccessRights;
|
5
|
+
exports.registerAuthMiddleware = registerAuthMiddleware;
|
6
|
+
const handler_1 = require("./handler");
|
7
|
+
/**
|
8
|
+
* @fileoverview Authentication utilities for MCP-CAP integration.
|
9
|
+
*
|
10
|
+
* This module provides utilities for integrating CAP authentication with MCP servers.
|
11
|
+
* It supports all standard CAP authentication types and provides functions for:
|
12
|
+
* - Determining authentication status
|
13
|
+
* - Managing user access rights
|
14
|
+
* - Registering authentication middleware
|
15
|
+
*
|
16
|
+
* Supported CAP authentication types:
|
17
|
+
* - 'dummy': No authentication (privileged access)
|
18
|
+
* - 'mocked': Mock users with predefined credentials
|
19
|
+
* - 'basic': HTTP Basic Authentication
|
20
|
+
* - 'jwt': Generic JWT token validation
|
21
|
+
* - 'xsuaa': SAP BTP XSUAA OAuth2/JWT authentication
|
22
|
+
* - 'ias': SAP Identity Authentication Service
|
23
|
+
* - Custom string types for user-defined authentication strategies
|
24
|
+
*
|
25
|
+
* Access CAP auth configuration via: cds.env.requires.auth.kind
|
26
|
+
*/
|
27
|
+
/* @ts-ignore */
|
28
|
+
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
29
|
+
/**
|
30
|
+
* Determines whether authentication is enabled for the MCP plugin.
|
31
|
+
*
|
32
|
+
* This function checks the plugin configuration to determine if authentication
|
33
|
+
* should be enforced. When authentication is disabled ('none'), the plugin
|
34
|
+
* operates with privileged access. For security reasons, this function defaults
|
35
|
+
* to enabling authentication unless explicitly disabled.
|
36
|
+
*
|
37
|
+
* @param configEnabled - The MCP authentication configuration type
|
38
|
+
* @returns true if authentication is enabled, false if disabled
|
39
|
+
*
|
40
|
+
* @example
|
41
|
+
* ```typescript
|
42
|
+
* const authEnabled = isAuthEnabled('inherit'); // true
|
43
|
+
* const noAuth = isAuthEnabled('none'); // false
|
44
|
+
* ```
|
45
|
+
*
|
46
|
+
* @since 1.0.0
|
47
|
+
*/
|
48
|
+
function isAuthEnabled(configEnabled) {
|
49
|
+
if (configEnabled === "none")
|
50
|
+
return false;
|
51
|
+
return true; // For now this will always default to true, as we do not want to falsely give access
|
52
|
+
}
|
53
|
+
/**
|
54
|
+
* Retrieves the appropriate user context for CAP service operations.
|
55
|
+
*
|
56
|
+
* This function returns the correct user context based on whether authentication
|
57
|
+
* is enabled. When authentication is enabled, it uses the current authenticated
|
58
|
+
* user from the CAP context. When disabled, it provides privileged access.
|
59
|
+
*
|
60
|
+
* The returned User object is used for:
|
61
|
+
* - Authorization checks in CAP services
|
62
|
+
* - Audit logging and traceability
|
63
|
+
* - Row-level security and data filtering
|
64
|
+
*
|
65
|
+
* @param authEnabled - Whether authentication is currently enabled
|
66
|
+
* @returns CAP User object with appropriate access rights
|
67
|
+
*
|
68
|
+
* @example
|
69
|
+
* ```typescript
|
70
|
+
* const user = getAccessRights(true); // Returns cds.context.user
|
71
|
+
* const admin = getAccessRights(false); // Returns cds.User.privileged
|
72
|
+
*
|
73
|
+
* // Use in CAP service calls
|
74
|
+
* const result = await service.tx({ user }).run(query);
|
75
|
+
* ```
|
76
|
+
*
|
77
|
+
* @throws {Error} When authentication is enabled but no user context exists
|
78
|
+
* @since 1.0.0
|
79
|
+
*/
|
80
|
+
function getAccessRights(authEnabled) {
|
81
|
+
return authEnabled ? cds.context.user : cds.User.privileged;
|
82
|
+
}
|
83
|
+
/**
|
84
|
+
* Registers comprehensive authentication middleware for MCP endpoints.
|
85
|
+
*
|
86
|
+
* This function sets up the complete authentication middleware chain for MCP endpoints.
|
87
|
+
* It integrates with CAP's authentication system by:
|
88
|
+
*
|
89
|
+
* 1. Applying all CAP 'before' middleware (including auth middleware)
|
90
|
+
* 2. Adding error handling for authentication failures
|
91
|
+
* 3. Adding MCP-specific authentication validation
|
92
|
+
*
|
93
|
+
* The middleware chain handles all CAP authentication types automatically and
|
94
|
+
* converts authentication errors to JSON-RPC 2.0 compliant responses.
|
95
|
+
*
|
96
|
+
* Middleware execution order:
|
97
|
+
* 1. CAP middleware chain (authentication, logging, etc.)
|
98
|
+
* 2. Authentication error handler
|
99
|
+
* 3. MCP authentication validator
|
100
|
+
*
|
101
|
+
* @param expressApp - Express application instance to register middleware on
|
102
|
+
*
|
103
|
+
* @example
|
104
|
+
* ```typescript
|
105
|
+
* const app = express();
|
106
|
+
* registerAuthMiddleware(app);
|
107
|
+
*
|
108
|
+
* // Now all /mcp routes are protected with CAP authentication
|
109
|
+
* app.post('/mcp', mcpHandler);
|
110
|
+
* ```
|
111
|
+
*
|
112
|
+
* @throws {Error} When CAP middleware chain is not properly initialized
|
113
|
+
* @since 1.0.0
|
114
|
+
*/
|
115
|
+
function registerAuthMiddleware(expressApp) {
|
116
|
+
const middlewares = cds.middlewares.before; // No types exists for this part of the CDS library
|
117
|
+
// Build array of auth middleware to apply
|
118
|
+
const authMiddleware = [];
|
119
|
+
// Add CAP middleware
|
120
|
+
middlewares.forEach((mw) => {
|
121
|
+
const process = mw.factory();
|
122
|
+
if (process && process.length > 0) {
|
123
|
+
authMiddleware.push(process);
|
124
|
+
}
|
125
|
+
});
|
126
|
+
// Add MCP auth middleware
|
127
|
+
authMiddleware.push((0, handler_1.errorHandlerFactory)());
|
128
|
+
authMiddleware.push((0, handler_1.authHandlerFactory)());
|
129
|
+
// Apply auth middleware to all /mcp routes EXCEPT health
|
130
|
+
expressApp?.use(/^\/mcp(?!\/health).*/, ...authMiddleware);
|
131
|
+
}
|
@@ -12,6 +12,7 @@ const logger_1 = require("../logger");
|
|
12
12
|
const CAPConfigurationSchema = zod_1.z.object({
|
13
13
|
name: zod_1.z.string(),
|
14
14
|
version: zod_1.z.string(),
|
15
|
+
auth: zod_1.z.custom(),
|
15
16
|
capabilities: zod_1.z.object({
|
16
17
|
tools: zod_1.z.object({
|
17
18
|
listChanged: zod_1.z.boolean().optional(),
|
package/lib/config/loader.js
CHANGED
@@ -22,6 +22,7 @@ function loadConfiguration() {
|
|
22
22
|
return {
|
23
23
|
name: cdsEnv?.name ?? packageInfo.name,
|
24
24
|
version: cdsEnv?.version ?? packageInfo.version,
|
25
|
+
auth: cdsEnv?.auth ?? "inherit",
|
25
26
|
capabilities: {
|
26
27
|
tools: cdsEnv?.capabilities?.tools ?? { listChanged: true },
|
27
28
|
resources: cdsEnv?.capabilities?.resources ?? { listChanged: true },
|
package/lib/mcp/factory.js
CHANGED
@@ -7,6 +7,7 @@ const structures_1 = require("../annotations/structures");
|
|
7
7
|
const tools_1 = require("./tools");
|
8
8
|
const resources_1 = require("./resources");
|
9
9
|
const prompts_1 = require("./prompts");
|
10
|
+
const utils_1 = require("../auth/utils");
|
10
11
|
/**
|
11
12
|
* Creates and configures an MCP server instance with the given configuration and annotations
|
12
13
|
* @param config - CAP configuration object
|
@@ -25,14 +26,14 @@ function createMcpServer(config, annotations) {
|
|
25
26
|
return server;
|
26
27
|
}
|
27
28
|
logger_1.LOGGER.debug("Annotations found for server: ", annotations);
|
28
|
-
|
29
|
+
const authEnabled = (0, utils_1.isAuthEnabled)(config.auth);
|
29
30
|
for (const entry of annotations.values()) {
|
30
31
|
if (entry instanceof structures_1.McpToolAnnotation) {
|
31
|
-
(0, tools_1.assignToolToServer)(entry, server);
|
32
|
+
(0, tools_1.assignToolToServer)(entry, server, authEnabled);
|
32
33
|
continue;
|
33
34
|
}
|
34
35
|
else if (entry instanceof structures_1.McpResourceAnnotation) {
|
35
|
-
(0, resources_1.assignResourceToServer)(entry, server);
|
36
|
+
(0, resources_1.assignResourceToServer)(entry, server, authEnabled);
|
36
37
|
continue;
|
37
38
|
}
|
38
39
|
else if (entry instanceof structures_1.McpPromptAnnotation) {
|
package/lib/mcp/resources.js
CHANGED
@@ -5,6 +5,7 @@ const custom_resource_template_1 = require("./custom-resource-template");
|
|
5
5
|
const logger_1 = require("../logger");
|
6
6
|
const utils_1 = require("./utils");
|
7
7
|
const validation_1 = require("./validation");
|
8
|
+
const utils_2 = require("../auth/utils");
|
8
9
|
// import cds from "@sap/cds";
|
9
10
|
/* @ts-ignore */
|
10
11
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
@@ -14,10 +15,10 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
|
|
14
15
|
* @param model - The resource annotation containing entity metadata and query options
|
15
16
|
* @param server - The MCP server instance to register the resource with
|
16
17
|
*/
|
17
|
-
function assignResourceToServer(model, server) {
|
18
|
+
function assignResourceToServer(model, server, authEnabled) {
|
18
19
|
logger_1.LOGGER.debug("Adding resource", model);
|
19
20
|
if (model.functionalities.size <= 0) {
|
20
|
-
registerStaticResource(model, server);
|
21
|
+
registerStaticResource(model, server, authEnabled);
|
21
22
|
return;
|
22
23
|
}
|
23
24
|
// Dynamic resource registration
|
@@ -83,7 +84,8 @@ function assignResourceToServer(model, server) {
|
|
83
84
|
};
|
84
85
|
}
|
85
86
|
try {
|
86
|
-
const
|
87
|
+
const accessRights = (0, utils_2.getAccessRights)(authEnabled);
|
88
|
+
const response = await service.tx({ user: accessRights }).run(query);
|
87
89
|
return {
|
88
90
|
contents: [
|
89
91
|
{
|
@@ -112,7 +114,7 @@ function assignResourceToServer(model, server) {
|
|
112
114
|
* @param model - The resource annotation with entity metadata
|
113
115
|
* @param server - The MCP server instance to register with
|
114
116
|
*/
|
115
|
-
function registerStaticResource(model, server) {
|
117
|
+
function registerStaticResource(model, server, authEnabled) {
|
116
118
|
server.registerResource(model.name, `odata://${model.serviceName}/${model.name}`, { title: model.target, description: model.description }, async (uri, extra) => {
|
117
119
|
const queryParameters = extra;
|
118
120
|
const service = cds.services[model.serviceName];
|
@@ -122,7 +124,8 @@ function registerStaticResource(model, server) {
|
|
122
124
|
const query = SELECT.from(model.target).limit(queryParameters.top
|
123
125
|
? validator.validateTop(queryParameters.top)
|
124
126
|
: 100);
|
125
|
-
const
|
127
|
+
const accessRights = (0, utils_2.getAccessRights)(authEnabled);
|
128
|
+
const response = await service.tx({ user: accessRights }).run(query);
|
126
129
|
return {
|
127
130
|
contents: [
|
128
131
|
{
|
package/lib/mcp/tools.js
CHANGED
@@ -5,6 +5,7 @@ const utils_1 = require("./utils");
|
|
5
5
|
const logger_1 = require("../logger");
|
6
6
|
const constants_1 = require("./constants");
|
7
7
|
const zod_1 = require("zod");
|
8
|
+
const utils_2 = require("../auth/utils");
|
8
9
|
/* @ts-ignore */
|
9
10
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
10
11
|
/**
|
@@ -13,15 +14,15 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
|
|
13
14
|
* @param model - The tool annotation containing operation metadata and parameters
|
14
15
|
* @param server - The MCP server instance to register the tool with
|
15
16
|
*/
|
16
|
-
function assignToolToServer(model, server) {
|
17
|
+
function assignToolToServer(model, server, authEnabled) {
|
17
18
|
logger_1.LOGGER.debug("Adding tool", model);
|
18
19
|
const parameters = buildToolParameters(model.parameters);
|
19
20
|
if (model.entityKey) {
|
20
21
|
// Assign tool as bound operation
|
21
|
-
assignBoundOperation(parameters, model, server);
|
22
|
+
assignBoundOperation(parameters, model, server, authEnabled);
|
22
23
|
return;
|
23
24
|
}
|
24
|
-
assignUnboundOperation(parameters, model, server);
|
25
|
+
assignUnboundOperation(parameters, model, server, authEnabled);
|
25
26
|
}
|
26
27
|
/**
|
27
28
|
* Registers a bound operation that operates on a specific entity instance
|
@@ -30,7 +31,7 @@ function assignToolToServer(model, server) {
|
|
30
31
|
* @param model - Tool annotation with bound operation metadata
|
31
32
|
* @param server - MCP server instance to register with
|
32
33
|
*/
|
33
|
-
function assignBoundOperation(params, model, server) {
|
34
|
+
function assignBoundOperation(params, model, server, authEnabled) {
|
34
35
|
if (!model.keyTypeMap || model.keyTypeMap.size <= 0) {
|
35
36
|
logger_1.LOGGER.error("Invalid tool assignment - missing key map for bound operation");
|
36
37
|
throw new Error("Bound operation cannot be assigned to tool list, missing keys");
|
@@ -65,7 +66,8 @@ function assignBoundOperation(params, model, server) {
|
|
65
66
|
continue;
|
66
67
|
operationInput[k] = v;
|
67
68
|
}
|
68
|
-
const
|
69
|
+
const accessRights = (0, utils_2.getAccessRights)(authEnabled);
|
70
|
+
const response = await service.tx({ user: accessRights }).send({
|
69
71
|
event: model.target,
|
70
72
|
entity: model.entityKey,
|
71
73
|
data: operationInput,
|
@@ -88,7 +90,7 @@ function assignBoundOperation(params, model, server) {
|
|
88
90
|
* @param model - Tool annotation with unbound operation metadata
|
89
91
|
* @param server - MCP server instance to register with
|
90
92
|
*/
|
91
|
-
function assignUnboundOperation(params, model, server) {
|
93
|
+
function assignUnboundOperation(params, model, server, authEnabled) {
|
92
94
|
const inputSchema = buildZodSchema(params);
|
93
95
|
server.registerTool(model.name, {
|
94
96
|
title: model.name,
|
@@ -108,7 +110,10 @@ function assignUnboundOperation(params, model, server) {
|
|
108
110
|
],
|
109
111
|
};
|
110
112
|
}
|
111
|
-
const
|
113
|
+
const accessRights = (0, utils_2.getAccessRights)(authEnabled);
|
114
|
+
const response = await service
|
115
|
+
.tx({ user: accessRights })
|
116
|
+
.send(model.target, args);
|
112
117
|
return {
|
113
118
|
content: Array.isArray(response)
|
114
119
|
? response.map((el) => ({
|
package/lib/mcp.js
CHANGED
@@ -3,6 +3,7 @@ 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"));
|
6
7
|
const logger_1 = require("./logger");
|
7
8
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
8
9
|
const express_1 = __importDefault(require("express"));
|
@@ -11,6 +12,9 @@ const utils_1 = require("./mcp/utils");
|
|
11
12
|
const constants_1 = require("./mcp/constants");
|
12
13
|
const loader_1 = require("./config/loader");
|
13
14
|
const session_manager_1 = require("./mcp/session-manager");
|
15
|
+
const utils_2 = require("./auth/utils");
|
16
|
+
/* @ts-ignore */
|
17
|
+
// const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
14
18
|
// TODO: Handle auth
|
15
19
|
/**
|
16
20
|
* Main MCP plugin class that integrates CAP services with Model Context Protocol
|
@@ -38,6 +42,9 @@ class McpPlugin {
|
|
38
42
|
logger_1.LOGGER.debug("Event received for 'bootstrap'");
|
39
43
|
this.expressApp = app;
|
40
44
|
this.expressApp.use(express_1.default.json());
|
45
|
+
if (this.config.auth === "inherit") {
|
46
|
+
(0, utils_2.registerAuthMiddleware)(this.expressApp);
|
47
|
+
}
|
41
48
|
await this.registerApiEndpoints();
|
42
49
|
logger_1.LOGGER.debug("Bootstrap complete");
|
43
50
|
}
|
@@ -76,7 +83,6 @@ class McpPlugin {
|
|
76
83
|
status: "UP",
|
77
84
|
});
|
78
85
|
});
|
79
|
-
logger_1.LOGGER.debug("TESTING - Annotations", this.annotations);
|
80
86
|
this.registerMcpSessionRoute();
|
81
87
|
this.expressApp?.get("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessionManager.getSessions()));
|
82
88
|
this.expressApp?.delete("/mcp", (req, res) => (0, utils_1.handleMcpSessionRequest)(req, res, this.sessionManager.getSessions()));
|
@@ -88,6 +94,7 @@ class McpPlugin {
|
|
88
94
|
registerMcpSessionRoute() {
|
89
95
|
logger_1.LOGGER.debug("Registering MCP entry point");
|
90
96
|
this.expressApp?.post("/mcp", async (req, res) => {
|
97
|
+
logger_1.LOGGER.debug("CONTEXT", cds_1.default.context); // TODO: Remove this line after testing
|
91
98
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
92
99
|
logger_1.LOGGER.debug("MCP request received", {
|
93
100
|
hasSessionId: !!sessionIdHeader,
|