@gavdi/cap-mcp 0.9.7 → 0.9.9-alpha.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 +55 -27
- package/lib/.DS_Store +0 -0
- package/lib/annotations/constants.js +9 -1
- package/lib/annotations/parser.js +15 -3
- package/lib/annotations/structures.js +20 -6
- package/lib/annotations/utils.js +62 -0
- package/lib/annotations.js +257 -0
- package/lib/auth/adapter.js +2 -0
- package/lib/auth/handler.js +13 -1
- package/lib/auth/mock.js +2 -0
- package/lib/auth/types.js +2 -0
- package/lib/auth/utils.js +139 -51
- package/lib/config/loader.js +1 -0
- package/lib/mcp/customResourceTemplate.js +156 -0
- package/lib/mcp/entity-tools.js +86 -6
- package/lib/mcp/factory.js +9 -3
- package/lib/mcp.js +4 -2
- package/lib/types.js +2 -0
- package/lib/utils.js +136 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
|
|
6
|
+
> 🔧 **In active development - 1.0 release scheduled for September 2025**
|
|
7
7
|
|
|
8
8
|
# CAP-MCP Plugin
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ Transform your CAP OData services into AI-accessible resources, tools, and promp
|
|
|
15
15
|
The Model Context Protocol bridges the gap between your enterprise data and AI agents.
|
|
16
16
|
By integrating MCP with your CAP applications, you unlock:
|
|
17
17
|
|
|
18
|
-
- **AI-Native Data Access**: Your CAP services become directly accessible to AI agents like Claude, enabling natural language queries against your business data
|
|
18
|
+
- **AI-Native Data Access**: Your CAP services become directly accessible to MCP enabled AI agents like Claude, enabling natural language queries against your business data
|
|
19
19
|
- **Enterprise Integration**: Seamlessly connect AI tools to your SAP systems, databases, and business logic
|
|
20
20
|
- **Intelligent Automation**: Enable AI agents to perform complex business operations by combining multiple CAP service calls
|
|
21
21
|
- **Developer Productivity**: Allow AI assistants to help developers understand, query, and work with your CAP data models
|
|
@@ -23,19 +23,11 @@ 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
|
|
26
|
+
**This plugin is currently in active development and approaching production readiness.**
|
|
27
27
|
APIs and annotations may change in future releases. Authentication and security features are implemented and tested.
|
|
28
28
|
|
|
29
29
|
Version 1.0 of the plugin is scheduled for release in Summer 2025 after final stability testing and documentation completion.
|
|
30
30
|
|
|
31
|
-
## 📦 Installation
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npm install @gavdi/cap-mcp
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
The plugin follows CAP's standard plugin architecture and will automatically integrate with your CAP application.
|
|
38
|
-
|
|
39
31
|
## 🚀 Quick Setup
|
|
40
32
|
|
|
41
33
|
### Prerequisites
|
|
@@ -51,6 +43,8 @@ The plugin follows CAP's standard plugin architecture and will automatically int
|
|
|
51
43
|
npm install @gavdi/cap-mcp
|
|
52
44
|
```
|
|
53
45
|
|
|
46
|
+
The plugin follows CAP's standard plugin architecture and will automatically integrate with your CAP application upon installation.
|
|
47
|
+
|
|
54
48
|
### Step 2: Configure Your CAP Application
|
|
55
49
|
|
|
56
50
|
Add MCP configuration to your `package.json`:
|
|
@@ -62,7 +56,8 @@ Add MCP configuration to your `package.json`:
|
|
|
62
56
|
"name": "my-bookshop-mcp",
|
|
63
57
|
"auth": "inherit",
|
|
64
58
|
"wrap_entities_to_actions": false,
|
|
65
|
-
"wrap_entity_modes": ["query", "get"]
|
|
59
|
+
"wrap_entity_modes": ["query", "get"],
|
|
60
|
+
"instructions": "MCP server instructions for agents"
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
63
|
}
|
|
@@ -82,7 +77,7 @@ service CatalogService {
|
|
|
82
77
|
resource: ['filter', 'orderby', 'select', 'top', 'skip']
|
|
83
78
|
}
|
|
84
79
|
entity Books as projection on my.Books;
|
|
85
|
-
|
|
80
|
+
|
|
86
81
|
// Optionally expose Books as tools for LLMs (query/get enabled by default config)
|
|
87
82
|
annotate CatalogService.Books with @mcp.wrap: {
|
|
88
83
|
tools: true,
|
|
@@ -134,17 +129,6 @@ This plugin transforms your annotated CAP services into a fully functional MCP s
|
|
|
134
129
|
- Start demo app: `npm run mock`
|
|
135
130
|
- Inspector: `npx @modelcontextprotocol/inspector`
|
|
136
131
|
|
|
137
|
-
### New wrapper tools
|
|
138
|
-
|
|
139
|
-
When `wrap_entities_to_actions` is enabled (globally or via `@mcp.wrap.tools: true`), you will see tools named like:
|
|
140
|
-
|
|
141
|
-
- `CatalogService_Books_query`
|
|
142
|
-
- `CatalogService_Books_get`
|
|
143
|
-
- `CatalogService_Books_create` (if enabled)
|
|
144
|
-
- `CatalogService_Books_update` (if enabled)
|
|
145
|
-
|
|
146
|
-
Each tool includes a description with fields and OData notes to guide the model. You can add `@mcp.wrap.hint` per entity to enrich descriptions for LLMs.
|
|
147
|
-
|
|
148
132
|
### Bruno collection
|
|
149
133
|
|
|
150
134
|
The `bruno/` folder contains HTTP requests for the MCP endpoint (handy for local manual testing using Bruno or any HTTP client). You may add calls for `tools/list` and `tools/call` to exercise the new wrapper tools.
|
|
@@ -196,6 +180,33 @@ service CatalogService {
|
|
|
196
180
|
- **Dynamic Filtering**: Complex filter expressions using OData syntax
|
|
197
181
|
- **Flexible Selection**: Choose specific fields and sort orders
|
|
198
182
|
|
|
183
|
+
### Wrapper tools
|
|
184
|
+
|
|
185
|
+
When `wrap_entities_to_actions` is enabled (globally or via `@mcp.wrap.tools: true`), you will see tools named like:
|
|
186
|
+
|
|
187
|
+
- `CatalogService_Books_query`
|
|
188
|
+
- `CatalogService_Books_get`
|
|
189
|
+
- `CatalogService_Books_create` (if enabled)
|
|
190
|
+
- `CatalogService_Books_update` (if enabled)
|
|
191
|
+
|
|
192
|
+
Each tool includes a description with fields and OData notes to guide the model. You can add `@mcp.wrap.hint` per entity to enrich descriptions for LLMs.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
|
|
196
|
+
```cds
|
|
197
|
+
// Wrap Books entity as tools for query/get/create/update (demo)
|
|
198
|
+
annotate CatalogService.Books with @mcp.wrap: {
|
|
199
|
+
tools: true,
|
|
200
|
+
modes: [
|
|
201
|
+
'query',
|
|
202
|
+
'get',
|
|
203
|
+
'create',
|
|
204
|
+
'update'
|
|
205
|
+
],
|
|
206
|
+
hint : 'Use for read and write demo operations'
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
199
210
|
### Tool Annotations
|
|
200
211
|
|
|
201
212
|
Convert CAP functions and actions into executable AI tools:
|
|
@@ -251,6 +262,7 @@ Configure the MCP plugin through your CAP application's `package.json` or `.cdsr
|
|
|
251
262
|
"name": "my-mcp-server",
|
|
252
263
|
"version": "1.0.0",
|
|
253
264
|
"auth": "inherit",
|
|
265
|
+
"instructions": "mcp server instructions for agents",
|
|
254
266
|
"capabilities": {
|
|
255
267
|
"resources": {
|
|
256
268
|
"listChanged": true,
|
|
@@ -275,6 +287,7 @@ Configure the MCP plugin through your CAP application's `package.json` or `.cdsr
|
|
|
275
287
|
| `name` | string | package.json name | MCP server name |
|
|
276
288
|
| `version` | string | package.json version | MCP server version |
|
|
277
289
|
| `auth` | `"inherit"` \| `"none"` | `"inherit"` | Authentication mode |
|
|
290
|
+
| `instructions` | string | `null` | MCP server instructions for agents |
|
|
278
291
|
| `capabilities.resources.listChanged` | boolean | `true` | Enable resource list change notifications |
|
|
279
292
|
| `capabilities.resources.subscribe` | boolean | `false` | Enable resource subscriptions |
|
|
280
293
|
| `capabilities.tools.listChanged` | boolean | `true` | Enable tool list change notifications |
|
|
@@ -537,9 +550,24 @@ npm test -- --testPathPattern=integration
|
|
|
537
550
|
## 🚨 Performance & Limitations
|
|
538
551
|
|
|
539
552
|
### Known Limitations
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
553
|
+
|
|
554
|
+
#### No Interactive Authentication Support
|
|
555
|
+
**The plugin currently does NOT support interactive OAuth flows** that allow end-users to log in through MCP clients like Claude Desktop, Cursor, or other consumer MCP applications.
|
|
556
|
+
|
|
557
|
+
**What this means:**
|
|
558
|
+
- ✅ Works with custom MCP clients that can inject pre-obtained bearer tokens
|
|
559
|
+
- ✅ Works in development with `dummy` authentication
|
|
560
|
+
- ❌ **Does NOT work with Claude Desktop, Cursor, or similar clients expecting OAuth login flows**
|
|
561
|
+
- ❌ End-users cannot authenticate interactively when connecting
|
|
562
|
+
|
|
563
|
+
**Technical Context:** This limitation exists due to architectural constraints in the current Model Context Protocol SDK. The MCP community is actively working on a solution that would enable proper interactive authentication flows, but no timeline has been announced. This is expected to be resolved in the second half of 2025.
|
|
564
|
+
|
|
565
|
+
**Workarounds:**
|
|
566
|
+
- **For Development**: Use `"auth": { "kind": "dummy" }` in your CAP configuration
|
|
567
|
+
- **For Production**: Custom MCP clients must obtain valid bearer tokens through your CAP application's existing authentication flow and include them in requests as `Authorization: Bearer <token>`
|
|
568
|
+
|
|
569
|
+
#### SDK Bug
|
|
570
|
+
- **Dynamic Resource Queries**: Require all query parameters due to `@modelcontextprotocol/sdk` RFC template string issue
|
|
543
571
|
|
|
544
572
|
### Performance Considerations
|
|
545
573
|
- **Large Datasets**: Use `resource: ['top']` or similar constraints for entities with many records
|
package/lib/.DS_Store
ADDED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.MCP_ANNOTATION_PROPS = exports.MCP_ANNOTATION_KEY = void 0;
|
|
3
|
+
exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.CDS_AUTH_ANNOTATIONS = exports.MCP_ANNOTATION_PROPS = exports.MCP_ANNOTATION_KEY = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* MCP annotation constants and default configurations
|
|
6
6
|
* Defines the standard annotation keys and default values used throughout the plugin
|
|
@@ -28,6 +28,14 @@ exports.MCP_ANNOTATION_PROPS = {
|
|
|
28
28
|
/** Wrapper configuration for exposing entities as tools */
|
|
29
29
|
MCP_WRAP: "@mcp.wrap",
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Set of annotations used for CDS auth annotations
|
|
33
|
+
* Maps logical names to their actual annotation keys used in CDS files.
|
|
34
|
+
*/
|
|
35
|
+
exports.CDS_AUTH_ANNOTATIONS = {
|
|
36
|
+
REQUIRES: "@requires",
|
|
37
|
+
RESTRICT: "@restrict",
|
|
38
|
+
};
|
|
31
39
|
/**
|
|
32
40
|
* Default set of all available OData query options for MCP resources
|
|
33
41
|
* Used when @mcp.resource is set to `true` to enable all capabilities
|
|
@@ -70,8 +70,12 @@ function parseAnnotations(definition) {
|
|
|
70
70
|
definition: definition,
|
|
71
71
|
};
|
|
72
72
|
for (const [k, v] of Object.entries(definition)) {
|
|
73
|
-
|
|
73
|
+
// Process MCP annotations and CDS auth annotations
|
|
74
|
+
if (!k.includes(constants_1.MCP_ANNOTATION_KEY) &&
|
|
75
|
+
!k.startsWith("@requires") &&
|
|
76
|
+
!k.startsWith("@restrict")) {
|
|
74
77
|
continue;
|
|
78
|
+
}
|
|
75
79
|
logger_1.LOGGER.debug("Parsing: ", k, v);
|
|
76
80
|
switch (k) {
|
|
77
81
|
case constants_1.MCP_ANNOTATION_PROPS.MCP_NAME:
|
|
@@ -93,6 +97,12 @@ function parseAnnotations(definition) {
|
|
|
93
97
|
// Wrapper container to expose resources as tools
|
|
94
98
|
annotations.wrap = v;
|
|
95
99
|
continue;
|
|
100
|
+
case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
|
|
101
|
+
annotations.requires = v;
|
|
102
|
+
continue;
|
|
103
|
+
case constants_1.CDS_AUTH_ANNOTATIONS.RESTRICT:
|
|
104
|
+
annotations.restrict = v;
|
|
105
|
+
continue;
|
|
96
106
|
default:
|
|
97
107
|
continue;
|
|
98
108
|
}
|
|
@@ -112,7 +122,8 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
|
|
|
112
122
|
return undefined;
|
|
113
123
|
const functionalities = (0, utils_1.determineResourceOptions)(annotations);
|
|
114
124
|
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition);
|
|
115
|
-
|
|
125
|
+
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
126
|
+
return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, annotations.wrap, restrictions);
|
|
116
127
|
}
|
|
117
128
|
/**
|
|
118
129
|
* Constructs a tool annotation from parsed annotation data
|
|
@@ -127,7 +138,8 @@ function constructToolAnnotation(serviceName, target, annotations, entityKey, ke
|
|
|
127
138
|
if (!(0, utils_1.isValidToolAnnotation)(annotations))
|
|
128
139
|
return undefined;
|
|
129
140
|
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
|
|
130
|
-
|
|
141
|
+
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
142
|
+
return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions);
|
|
131
143
|
}
|
|
132
144
|
/**
|
|
133
145
|
* Constructs a prompt annotation from parsed annotation data
|
|
@@ -14,18 +14,22 @@ class McpAnnotation {
|
|
|
14
14
|
_target;
|
|
15
15
|
/** The name of the CAP service this annotation belongs to */
|
|
16
16
|
_serviceName;
|
|
17
|
+
/** Auth roles by providing CDS that is required for use */
|
|
18
|
+
_restrictions;
|
|
17
19
|
/**
|
|
18
20
|
* Creates a new MCP annotation instance
|
|
19
21
|
* @param name - Unique identifier for this annotation
|
|
20
22
|
* @param description - Human-readable description
|
|
21
23
|
* @param target - The target element this annotation applies to
|
|
22
24
|
* @param serviceName - Name of the associated CAP service
|
|
25
|
+
* @param restrictions - Roles required for the given annotation
|
|
23
26
|
*/
|
|
24
|
-
constructor(name, description, target, serviceName) {
|
|
27
|
+
constructor(name, description, target, serviceName, restrictions) {
|
|
25
28
|
this._name = name;
|
|
26
29
|
this._description = description;
|
|
27
30
|
this._target = target;
|
|
28
31
|
this._serviceName = serviceName;
|
|
32
|
+
this._restrictions = restrictions;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* Gets the unique name identifier for this annotation
|
|
@@ -55,6 +59,14 @@ class McpAnnotation {
|
|
|
55
59
|
get serviceName() {
|
|
56
60
|
return this._serviceName;
|
|
57
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Gets the list of roles required for access to the annotation.
|
|
64
|
+
* If the list is empty, then all can access.
|
|
65
|
+
* @returns List of required roles
|
|
66
|
+
*/
|
|
67
|
+
get restrictions() {
|
|
68
|
+
return this._restrictions;
|
|
69
|
+
}
|
|
58
70
|
}
|
|
59
71
|
exports.McpAnnotation = McpAnnotation;
|
|
60
72
|
/**
|
|
@@ -79,9 +91,10 @@ class McpResourceAnnotation extends McpAnnotation {
|
|
|
79
91
|
* @param functionalities - Set of enabled OData query options (filter, top, skip, etc.)
|
|
80
92
|
* @param properties - Map of entity properties to their CDS types
|
|
81
93
|
* @param resourceKeys - Map of key fields to their types
|
|
94
|
+
* @param restrictions - Optional restrictions based on CDS roles
|
|
82
95
|
*/
|
|
83
|
-
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, wrap) {
|
|
84
|
-
super(name, description, target, serviceName);
|
|
96
|
+
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, wrap, restrictions) {
|
|
97
|
+
super(name, description, target, serviceName, restrictions ?? []);
|
|
85
98
|
this._functionalities = functionalities;
|
|
86
99
|
this._properties = properties;
|
|
87
100
|
this._resourceKeys = resourceKeys;
|
|
@@ -139,9 +152,10 @@ class McpToolAnnotation extends McpAnnotation {
|
|
|
139
152
|
* @param entityKey - Optional entity key field for bound operations
|
|
140
153
|
* @param operationKind - Optional operation type ('function' or 'action')
|
|
141
154
|
* @param keyTypeMap - Optional map of key fields to types for bound operations
|
|
155
|
+
* @param restrictions - Optional restrictions based on CDS roles
|
|
142
156
|
*/
|
|
143
|
-
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
|
144
|
-
super(name, description, operation, serviceName);
|
|
157
|
+
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions) {
|
|
158
|
+
super(name, description, operation, serviceName, restrictions ?? []);
|
|
145
159
|
this._parameters = parameters;
|
|
146
160
|
this._entityKey = entityKey;
|
|
147
161
|
this._operationKind = operationKind;
|
|
@@ -192,7 +206,7 @@ class McpPromptAnnotation extends McpAnnotation {
|
|
|
192
206
|
* @param prompts - Array of prompt template definitions
|
|
193
207
|
*/
|
|
194
208
|
constructor(name, description, serviceName, prompts) {
|
|
195
|
-
super(name, description, serviceName, serviceName);
|
|
209
|
+
super(name, description, serviceName, serviceName, []);
|
|
196
210
|
this._prompts = prompts;
|
|
197
211
|
}
|
|
198
212
|
/**
|
package/lib/annotations/utils.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.determineResourceOptions = determineResourceOptions;
|
|
|
10
10
|
exports.parseResourceElements = parseResourceElements;
|
|
11
11
|
exports.parseOperationElements = parseOperationElements;
|
|
12
12
|
exports.parseEntityKeys = parseEntityKeys;
|
|
13
|
+
exports.parseCdsRestrictions = parseCdsRestrictions;
|
|
13
14
|
const constants_1 = require("./constants");
|
|
14
15
|
const logger_1 = require("../logger");
|
|
15
16
|
/**
|
|
@@ -191,3 +192,64 @@ function parseEntityKeys(definition) {
|
|
|
191
192
|
}
|
|
192
193
|
return result;
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Parses the CDS role restrictions to be used for MCP
|
|
197
|
+
*/
|
|
198
|
+
function parseCdsRestrictions(restrictions, requires) {
|
|
199
|
+
if (!restrictions && !requires)
|
|
200
|
+
return [];
|
|
201
|
+
const result = [];
|
|
202
|
+
if (requires) {
|
|
203
|
+
result.push({
|
|
204
|
+
role: requires,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (!restrictions || restrictions.length <= 0)
|
|
208
|
+
return result;
|
|
209
|
+
for (const el of restrictions) {
|
|
210
|
+
const ops = mapOperationRestriction(el.grant);
|
|
211
|
+
if (!el.to) {
|
|
212
|
+
result.push({
|
|
213
|
+
role: "authenticated-user",
|
|
214
|
+
operations: ops,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const mapped = el.to.map((to) => ({
|
|
219
|
+
role: to,
|
|
220
|
+
operations: ops,
|
|
221
|
+
}));
|
|
222
|
+
result.push(...mapped);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Maps the "grant" property from CdsRestriction to McpRestriction
|
|
228
|
+
*/
|
|
229
|
+
function mapOperationRestriction(cdsRestrictions) {
|
|
230
|
+
const result = [];
|
|
231
|
+
if (!cdsRestrictions || cdsRestrictions.length <= 0) {
|
|
232
|
+
result.push("CREATE");
|
|
233
|
+
result.push("READ");
|
|
234
|
+
result.push("UPDATE");
|
|
235
|
+
result.push("DELETE");
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
for (const el of cdsRestrictions) {
|
|
239
|
+
switch (el) {
|
|
240
|
+
case "CHANGE":
|
|
241
|
+
result.push("UPDATE");
|
|
242
|
+
continue;
|
|
243
|
+
case "*":
|
|
244
|
+
result.push("CREATE");
|
|
245
|
+
result.push("READ");
|
|
246
|
+
result.push("UPDATE");
|
|
247
|
+
result.push("DELETE");
|
|
248
|
+
continue;
|
|
249
|
+
default:
|
|
250
|
+
result.push(el);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpPromptAnnotation = exports.McpToolAnnotation = exports.McpResourceAnnotation = exports.McpAnnotation = exports.McpAnnotations = exports.McpAnnotationKey = void 0;
|
|
4
|
+
exports.parseAnnotations = parseAnnotations;
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
const DEFAULT_ALL_RESOURCE_OPTIONS = new Set([
|
|
7
|
+
"filter",
|
|
8
|
+
"sort",
|
|
9
|
+
"top",
|
|
10
|
+
"skip",
|
|
11
|
+
"select",
|
|
12
|
+
]);
|
|
13
|
+
exports.McpAnnotationKey = "@mcp";
|
|
14
|
+
exports.McpAnnotations = {
|
|
15
|
+
// Resource annotations for MCP
|
|
16
|
+
MCP_RESOURCE: "@mcp.resource",
|
|
17
|
+
// Tool annotations for MCP
|
|
18
|
+
MCP_TOOL_NAME: "@mcp.tool.name",
|
|
19
|
+
MCP_TOOL_DESCRIPTION: "@mcp.tool.description",
|
|
20
|
+
// Prompt annotations for MCP
|
|
21
|
+
MCP_PROMPT: "@mcp.prompt",
|
|
22
|
+
};
|
|
23
|
+
class McpAnnotation {
|
|
24
|
+
_target;
|
|
25
|
+
_serviceName;
|
|
26
|
+
constructor(target, serviceName) {
|
|
27
|
+
this._target = target;
|
|
28
|
+
this._serviceName = serviceName;
|
|
29
|
+
}
|
|
30
|
+
get target() {
|
|
31
|
+
return this._target;
|
|
32
|
+
}
|
|
33
|
+
get serviceName() {
|
|
34
|
+
return this._serviceName;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.McpAnnotation = McpAnnotation;
|
|
38
|
+
class McpResourceAnnotation extends McpAnnotation {
|
|
39
|
+
_includeAll;
|
|
40
|
+
_functionalities;
|
|
41
|
+
_properties;
|
|
42
|
+
constructor(target, serviceName, includeAll, functionalities, properties) {
|
|
43
|
+
super(target, serviceName);
|
|
44
|
+
this._includeAll = includeAll;
|
|
45
|
+
this._functionalities = functionalities;
|
|
46
|
+
this._properties = properties;
|
|
47
|
+
}
|
|
48
|
+
get includeAll() {
|
|
49
|
+
return this._includeAll;
|
|
50
|
+
}
|
|
51
|
+
get functionalities() {
|
|
52
|
+
return this._functionalities;
|
|
53
|
+
}
|
|
54
|
+
get properties() {
|
|
55
|
+
return this._properties;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.McpResourceAnnotation = McpResourceAnnotation;
|
|
59
|
+
class McpToolAnnotation extends McpAnnotation {
|
|
60
|
+
_name;
|
|
61
|
+
_description;
|
|
62
|
+
_parameters;
|
|
63
|
+
_entityKey;
|
|
64
|
+
_operationKind;
|
|
65
|
+
_keyTypeMap;
|
|
66
|
+
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
|
67
|
+
super(operation, serviceName);
|
|
68
|
+
this._name = name;
|
|
69
|
+
this._description = description;
|
|
70
|
+
this._parameters = parameters;
|
|
71
|
+
this._entityKey = entityKey;
|
|
72
|
+
this._operationKind = operationKind;
|
|
73
|
+
this._keyTypeMap = keyTypeMap;
|
|
74
|
+
}
|
|
75
|
+
get name() {
|
|
76
|
+
return this._name;
|
|
77
|
+
}
|
|
78
|
+
get description() {
|
|
79
|
+
return this._description;
|
|
80
|
+
}
|
|
81
|
+
get parameters() {
|
|
82
|
+
return this._parameters;
|
|
83
|
+
}
|
|
84
|
+
get entityKey() {
|
|
85
|
+
return this._entityKey;
|
|
86
|
+
}
|
|
87
|
+
get operationKind() {
|
|
88
|
+
return this._operationKind;
|
|
89
|
+
}
|
|
90
|
+
get keyTypeMap() {
|
|
91
|
+
return this._keyTypeMap;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.McpToolAnnotation = McpToolAnnotation;
|
|
95
|
+
class McpPromptAnnotation extends McpAnnotation {
|
|
96
|
+
_name;
|
|
97
|
+
_template;
|
|
98
|
+
constructor(target, serviceName, name, template) {
|
|
99
|
+
super(target, serviceName);
|
|
100
|
+
this._name = name;
|
|
101
|
+
this._template = template;
|
|
102
|
+
}
|
|
103
|
+
get name() {
|
|
104
|
+
return this._name;
|
|
105
|
+
}
|
|
106
|
+
get template() {
|
|
107
|
+
return this._template;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.McpPromptAnnotation = McpPromptAnnotation;
|
|
111
|
+
function parseAnnotations(services) {
|
|
112
|
+
const annotations = [];
|
|
113
|
+
for (const serviceName of Object.keys(services)) {
|
|
114
|
+
const srv = services[serviceName];
|
|
115
|
+
if (srv.name === "CatalogService") {
|
|
116
|
+
utils_1.LOGGER.debug("SERVICE: ", srv.model.definitions);
|
|
117
|
+
}
|
|
118
|
+
const entities = srv.entities;
|
|
119
|
+
const operations = srv.operations; // Refers to action and function imports
|
|
120
|
+
// Find entities
|
|
121
|
+
for (const entityName of Object.keys(entities)) {
|
|
122
|
+
const target = entities[entityName];
|
|
123
|
+
const res = findEntityAnnotations(target, entityName, srv);
|
|
124
|
+
if (target.actions) {
|
|
125
|
+
const bound = parseBoundOperations(target.actions, entityName, target, srv);
|
|
126
|
+
if (bound && bound.length > 0) {
|
|
127
|
+
annotations.push(...bound);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!res)
|
|
131
|
+
continue;
|
|
132
|
+
annotations.push(res);
|
|
133
|
+
}
|
|
134
|
+
// Find operations
|
|
135
|
+
for (const operationName of Object.keys(operations)) {
|
|
136
|
+
const op = operations[operationName];
|
|
137
|
+
const res = findOperationAnnotations(op, operationName, srv);
|
|
138
|
+
if (!res)
|
|
139
|
+
continue;
|
|
140
|
+
annotations.push(res);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const result = formatAnnotations(annotations);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
function formatAnnotations(annotationList) {
|
|
147
|
+
const result = new Map();
|
|
148
|
+
for (const annotation of annotationList) {
|
|
149
|
+
if (annotation.operation) {
|
|
150
|
+
if (!annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME] ||
|
|
151
|
+
!annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION]) {
|
|
152
|
+
utils_1.LOGGER.error(`Invalid annotation found for operation`, annotation);
|
|
153
|
+
throw new Error(`Invalid annotations for operation '${annotation.operation}'`);
|
|
154
|
+
}
|
|
155
|
+
else if (typeof annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME] !==
|
|
156
|
+
"string" ||
|
|
157
|
+
typeof annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION] !==
|
|
158
|
+
"string") {
|
|
159
|
+
utils_1.LOGGER.error("Invalid data for annotations", annotation);
|
|
160
|
+
throw new Error(`Invalid annotation data for operation '${annotation.operation}'`);
|
|
161
|
+
}
|
|
162
|
+
const entry = new McpToolAnnotation(annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME], annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION], annotation.operation, annotation.serviceName, mapOperationInput(annotation.context), // TODO: Parse the parameters from the context and place them in the class
|
|
163
|
+
annotation.entityKey, annotation.operationKind, annotation.keyTypeMap);
|
|
164
|
+
result.set(entry.target, entry);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!annotation.entityKey) {
|
|
168
|
+
utils_1.LOGGER.error("Invalid entry", annotation);
|
|
169
|
+
throw new Error(`Invalid annotated entry found with no target`);
|
|
170
|
+
}
|
|
171
|
+
if (!annotation.annotations[exports.McpAnnotations.MCP_RESOURCE]) {
|
|
172
|
+
utils_1.LOGGER.error("No valid annotations found for entry", annotation);
|
|
173
|
+
throw new Error(`Invalid annotations for entry target: '${annotation.entityKey}'`);
|
|
174
|
+
}
|
|
175
|
+
const includeAll = annotation.annotations[exports.McpAnnotations.MCP_RESOURCE] === true;
|
|
176
|
+
const functionalities = Array.isArray(annotation.annotations[exports.McpAnnotations.MCP_RESOURCE])
|
|
177
|
+
? new Set(annotation.annotations[exports.McpAnnotations.MCP_RESOURCE])
|
|
178
|
+
: DEFAULT_ALL_RESOURCE_OPTIONS;
|
|
179
|
+
const entry = new McpResourceAnnotation(annotation.entityKey, annotation.serviceName, includeAll, functionalities, (0, utils_1.parseEntityElements)(annotation.context));
|
|
180
|
+
result.set(entry.target, entry);
|
|
181
|
+
}
|
|
182
|
+
utils_1.LOGGER.debug("Formatted annotations", result);
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
function findEntityAnnotations(entry, entityKey, service) {
|
|
186
|
+
const annotations = findAnnotations(entry);
|
|
187
|
+
return Object.keys(annotations).length > 0
|
|
188
|
+
? {
|
|
189
|
+
serviceName: service.name,
|
|
190
|
+
annotations: annotations,
|
|
191
|
+
entityKey: entityKey,
|
|
192
|
+
context: entry,
|
|
193
|
+
}
|
|
194
|
+
: undefined;
|
|
195
|
+
}
|
|
196
|
+
function findOperationAnnotations(operation, operationName, service) {
|
|
197
|
+
const annotations = findAnnotations(operation);
|
|
198
|
+
return Object.keys(annotations).length > 0
|
|
199
|
+
? {
|
|
200
|
+
serviceName: service.name,
|
|
201
|
+
annotations: annotations,
|
|
202
|
+
operation: operationName,
|
|
203
|
+
operationKind: operation.kind,
|
|
204
|
+
context: operation,
|
|
205
|
+
}
|
|
206
|
+
: undefined;
|
|
207
|
+
}
|
|
208
|
+
function parseBoundOperations(operations, entityKey, entity, service) {
|
|
209
|
+
const res = new Array();
|
|
210
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
211
|
+
const annotation = findBoundOperationAnnotations(operation, operationName, entityKey, service);
|
|
212
|
+
if (!annotation)
|
|
213
|
+
continue;
|
|
214
|
+
annotation.keyTypeMap = new Map();
|
|
215
|
+
for (const [k, v] of Object.entries(entity.keys)) {
|
|
216
|
+
if (!v.type) {
|
|
217
|
+
utils_1.LOGGER.error("Invalid key type", k);
|
|
218
|
+
throw new Error("Invalid key type found for bound operation");
|
|
219
|
+
}
|
|
220
|
+
annotation.keyTypeMap.set(k, v.type.replace("cds.", ""));
|
|
221
|
+
}
|
|
222
|
+
res.push(annotation);
|
|
223
|
+
}
|
|
224
|
+
return res;
|
|
225
|
+
}
|
|
226
|
+
function findBoundOperationAnnotations(operation, operationName, entityKey, service) {
|
|
227
|
+
const annotations = findAnnotations(operation);
|
|
228
|
+
return Object.keys(annotations).length > 0
|
|
229
|
+
? {
|
|
230
|
+
serviceName: service.name,
|
|
231
|
+
annotations: annotations,
|
|
232
|
+
operation: operationName,
|
|
233
|
+
operationKind: operation.kind,
|
|
234
|
+
entityKey: entityKey,
|
|
235
|
+
context: operation,
|
|
236
|
+
}
|
|
237
|
+
: undefined;
|
|
238
|
+
}
|
|
239
|
+
function findAnnotations(entry) {
|
|
240
|
+
const annotations = {};
|
|
241
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
242
|
+
if (!k.includes(exports.McpAnnotationKey))
|
|
243
|
+
continue;
|
|
244
|
+
annotations[k] = v;
|
|
245
|
+
}
|
|
246
|
+
return annotations;
|
|
247
|
+
}
|
|
248
|
+
function mapOperationInput(ctx) {
|
|
249
|
+
const params = ctx["params"];
|
|
250
|
+
if (!params)
|
|
251
|
+
return undefined;
|
|
252
|
+
const result = new Map();
|
|
253
|
+
for (const [k, v] of Object.entries(params)) {
|
|
254
|
+
result.set(k, v.type.replace("cds.", ""));
|
|
255
|
+
}
|
|
256
|
+
return result.size > 0 ? result : undefined;
|
|
257
|
+
}
|
package/lib/auth/handler.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.authHandlerFactory = authHandlerFactory;
|
|
4
4
|
exports.errorHandlerFactory = errorHandlerFactory;
|
|
5
|
+
const logger_1 = require("../logger");
|
|
5
6
|
/** JSON-RPC 2.0 error code for unauthorized requests */
|
|
6
7
|
const RPC_UNAUTHORIZED = 10;
|
|
8
|
+
/** HTTP Authenticate header **/
|
|
9
|
+
const WWW_AUTHENTICATE = "WWW-Authenticate";
|
|
7
10
|
/* @ts-ignore */
|
|
8
11
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
9
12
|
/**
|
|
@@ -31,9 +34,15 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
|
|
|
31
34
|
* @throws {500} When CAP context is not properly loaded
|
|
32
35
|
*/
|
|
33
36
|
function authHandlerFactory() {
|
|
34
|
-
const authKind = cds.env.requires.auth.kind;
|
|
35
37
|
return (req, res, next) => {
|
|
38
|
+
const auth = cds.env.requires.auth;
|
|
39
|
+
const authKind = auth.kind;
|
|
40
|
+
const credentials = auth.credentials;
|
|
36
41
|
if (!req.headers.authorization && authKind !== "dummy") {
|
|
42
|
+
logger_1.LOGGER.warn("No valid authorization header provided");
|
|
43
|
+
// We need to return a WWW-Authenticate response header here with .well-known metadata
|
|
44
|
+
// Otherwise the MCP client will not be able to figure out the auth flow
|
|
45
|
+
res.setHeader(WWW_AUTHENTICATE, `Bearer error='"invalid_token", resource_metadata="${credentials?.url}/.well-known/oauth-protected-resource"`);
|
|
37
46
|
res.status(401).json({
|
|
38
47
|
jsonrpc: "2.0",
|
|
39
48
|
error: {
|
|
@@ -58,6 +67,9 @@ function authHandlerFactory() {
|
|
|
58
67
|
}
|
|
59
68
|
const user = ctx.user;
|
|
60
69
|
if (!user || user === cds.User.anonymous) {
|
|
70
|
+
// We need to return a WWW-Authenticate response header here with .well-known metadata
|
|
71
|
+
// Otherwise the MCP client will not be able to figure out the auth flow
|
|
72
|
+
res.setHeader(WWW_AUTHENTICATE, `Bearer error='"invalid_token", resource_metadata="${credentials?.url}/.well-known/oauth-protected-resource"`);
|
|
61
73
|
res.status(401).json({
|
|
62
74
|
jsonrpc: "2.0",
|
|
63
75
|
error: {
|
package/lib/auth/mock.js
ADDED