@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 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 Summer 2025**
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 (v0.9.2) and approaching production readiness.**
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
- - **SDK Bug**: Dynamic resource queries require all query parameters due to `@modelcontextprotocol/sdk` RFC template string issue
541
- - **Authentication Inheritance**: MCP authentication is tightly coupled to CAP's authentication system
542
- - **Session Cleanup**: MCP sessions are cleaned up on HTTP connection close, not on explicit disconnect
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
- if (!k.includes(constants_1.MCP_ANNOTATION_KEY))
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
- return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, annotations.wrap);
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
- return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams);
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
  /**
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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: {
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });