@gavdi/cap-mcp 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # CAP MCP Plugin - AI With Ease
2
- ![NPM Version](https://img.shields.io/npm/v/%40gavdi%2Fcap-mcp) ![NPM License](https://img.shields.io/npm/l/%40gavdi%2Fcap-mcp) ![NPM Downloads](https://img.shields.io/npm/dm/%40gavdi%2Fcap-mcp) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/gavdilabs/cap-mcp-plugin/latest)
3
-
4
2
 
3
+ ![NPM Version](https://img.shields.io/npm/v/%40gavdi%2Fcap-mcp) ![NPM License](https://img.shields.io/npm/l/%40gavdi%2Fcap-mcp) ![NPM Downloads](https://img.shields.io/npm/dm/%40gavdi%2Fcap-mcp) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/gavdilabs/cap-mcp-plugin/latest)
5
4
 
6
5
  > This implementation is based on the Model Context Protocol (MCP) put forward by Anthropic.
7
6
  > For more information on MCP, please have a look at their [official documentation.](https://modelcontextprotocol.io/introduction)
@@ -99,6 +98,7 @@ cds serve
99
98
  ```
100
99
 
101
100
  The MCP server will be available at:
101
+
102
102
  - **MCP Endpoint**: `http://localhost:4004/mcp`
103
103
  - **Health Check**: `http://localhost:4004/mcp/health`
104
104
 
@@ -176,6 +176,7 @@ service CatalogService {
176
176
  ```
177
177
 
178
178
  **Generated MCP Resource Capabilities:**
179
+
179
180
  - **OData v4 Query Support**: `$filter`, `$orderby`, `$top`, `$skip`, `$select`, `$expand`
180
181
  - **Natural Language Queries**: "Find books by Stephen King with stock > 20"
181
182
  - **Dynamic Filtering**: Complex filter expressions using OData syntax
@@ -236,23 +237,27 @@ entity Users {
236
237
  ```
237
238
 
238
239
  **How It Works:**
240
+
239
241
  - Fields marked with `@mcp.omit` are automatically filtered from all MCP responses
240
242
  - Applies to:
241
243
  - **Resources**: Field will not appear in resource read operations
242
244
  - **Wrapped Entities**: Omission applies to all entity wrapper operations
243
245
 
244
246
  **Common Use Cases:**
247
+
245
248
  - **Security**: Hide information sensitive to functionality or business operations
246
249
  - **Privacy**: Protect personal identifiers
247
250
  - **Internal Data**: Exclude internal notes, audit logs, or system-only fields
248
251
  - **Compliance**: Ensure GDPR/CCPA compliance by hiding sensitive personal data
249
252
 
250
253
  **Important Notes:**
254
+
251
255
  - Omitted fields are **only excluded from outputs** - they can still be provided as inputs for create/update operations
252
256
  - The annotation works alongside the CAP standard annotation `@Core.Computed` for comprehensive field control
253
257
  - Omitted fields remain queryable in the CAP service - only MCP responses are filtered
254
258
 
255
259
  **Example with Multiple Annotations:**
260
+
256
261
  ```cds
257
262
  entity Products {
258
263
  key ID : Integer;
@@ -325,11 +330,13 @@ function getBooksByAuthor(authorName: String) returns array of String;
325
330
  > NOTE: Elicitation is only available for direct tools at this moment. Wrapped entities are not covered by this.
326
331
 
327
332
  **Elicit Types:**
333
+
328
334
  - **`confirm`**: Requests user confirmation before executing the tool with a yes/no prompt
329
335
  - **`input`**: Prompts the user to provide values for the tool's parameters
330
336
  - **Combined**: Use both `['input', 'confirm']` to first collect parameters, then ask for confirmation
331
337
 
332
338
  **User Experience:**
339
+
333
340
  - **Confirmation**: "Please confirm that you want to perform action 'Get a random book recommendation'"
334
341
  - **Input**: "Please fill out the required parameters" with a form for each parameter
335
342
  - **User Actions**: Accept, decline, or cancel the elicitation request
@@ -342,6 +349,7 @@ Provide contextual descriptions for individual properties and parameters using t
342
349
  #### Where to Use Hints
343
350
 
344
351
  **Resource Entity Properties**
352
+
345
353
  ```cds
346
354
  entity Books {
347
355
  key ID : Integer @mcp.hint: 'Must be a unique number not already in the system';
@@ -351,6 +359,7 @@ entity Books {
351
359
  ```
352
360
 
353
361
  **Array Elements**
362
+
354
363
  ```cds
355
364
  entity Authors {
356
365
  key ID : Integer;
@@ -360,6 +369,7 @@ entity Authors {
360
369
  ```
361
370
 
362
371
  **Function/Action Parameters**
372
+
363
373
  ```cds
364
374
  @mcp: {
365
375
  name : 'books-by-author',
@@ -372,6 +382,7 @@ function getBooksByAuthor(
372
382
  ```
373
383
 
374
384
  **Complex Type Fields**
385
+
375
386
  ```cds
376
387
  type TValidQuantities {
377
388
  positiveOnly : Integer @mcp.hint: 'Only takes in positive numbers, i.e. no negative values such as -1'
@@ -381,6 +392,7 @@ type TValidQuantities {
381
392
  #### How Hints Are Used
382
393
 
383
394
  Hints are automatically incorporated into:
395
+
384
396
  - **Resource Descriptions**: Field-level guidance in entity wrapper tools (query/get/create/update/delete)
385
397
  - **Tool Parameter Schemas**: Enhanced parameter descriptions visible to AI agents
386
398
  - **Input Validation**: Context for AI agents when constructing function calls
@@ -388,6 +400,7 @@ Hints are automatically incorporated into:
388
400
  #### Example: Enhanced Tool Experience
389
401
 
390
402
  Without `@mcp.hint`:
403
+
391
404
  ```json
392
405
  {
393
406
  "tool": "CatalogService_Books_create",
@@ -399,6 +412,7 @@ Without `@mcp.hint`:
399
412
  ```
400
413
 
401
414
  With `@mcp.hint`:
415
+
402
416
  ```json
403
417
  {
404
418
  "tool": "CatalogService_Books_create",
@@ -418,16 +432,20 @@ With `@mcp.hint`:
418
432
  #### Best Practices
419
433
 
420
434
  1. **Be Specific**: Provide concrete examples and constraints
435
+
421
436
  - ❌ Bad: `@mcp.hint: 'Author name'`
422
437
  - ✅ Good: `@mcp.hint: 'Full name of the author (e.g., "Ernest Hemingway")'`
423
438
 
424
439
  2. **Include Constraints**: Document validation rules and business logic
440
+
425
441
  - ✅ `@mcp.hint: 'Must be between 0 and 999, representing quantity in stock'`
426
442
 
427
443
  3. **Clarify Foreign Keys**: Help AI agents understand associations
444
+
428
445
  - ✅ `@mcp.hint: 'Foreign key reference to Authors.ID'`
429
446
 
430
447
  4. **Explain Business Context**: Add domain-specific information
448
+
431
449
  - ✅ `@mcp.hint: 'ISBN-13 format, used for unique book identification'`
432
450
 
433
451
  5. **Avoid Redundancy**: Don't repeat what's obvious from the field name and type
@@ -492,22 +510,24 @@ Configure the MCP plugin through your CAP application's `package.json` or `.cdsr
492
510
 
493
511
  ### Configuration Options
494
512
 
495
- | Option | Type | Default | Description |
496
- |--------|------|---------|-------------|
497
- | `name` | string | package.json name | MCP server name |
498
- | `version` | string | package.json version | MCP server version |
499
- | `auth` | `"inherit"` \| `"none"` | `"inherit"` | Authentication mode |
500
- | `instructions` | string | `null` | MCP server instructions for agents |
501
- | `capabilities.resources.listChanged` | boolean | `true` | Enable resource list change notifications |
502
- | `capabilities.resources.subscribe` | boolean | `false` | Enable resource subscriptions |
503
- | `capabilities.tools.listChanged` | boolean | `true` | Enable tool list change notifications |
504
- | `capabilities.prompts.listChanged` | boolean | `true` | Enable prompt list change notifications |
513
+ | Option | Type | Default | Description |
514
+ | ------------------------------------ | ----------------------- | -------------------- | --------------------------------------------------------------------------- |
515
+ | `name` | string | package.json name | MCP server name |
516
+ | `version` | string | package.json version | MCP server version |
517
+ | `auth` | `"inherit"` \| `"none"` | `"inherit"` | Authentication mode |
518
+ | `instructions` | string | `null` | MCP server instructions for agents |
519
+ | `enable_model_description` | boolean | `true` | Determines whether the MCP server should include the model description tool |
520
+ | `capabilities.resources.listChanged` | boolean | `true` | Enable resource list change notifications |
521
+ | `capabilities.resources.subscribe` | boolean | `false` | Enable resource subscriptions |
522
+ | `capabilities.tools.listChanged` | boolean | `true` | Enable tool list change notifications |
523
+ | `capabilities.prompts.listChanged` | boolean | `true` | Enable prompt list change notifications |
505
524
 
506
525
  ### Authentication Configuration
507
526
 
508
527
  The plugin supports two authentication modes:
509
528
 
510
529
  #### `"inherit"` Mode (Default)
530
+
511
531
  Uses your CAP application's existing authentication system:
512
532
 
513
533
  ```json
@@ -526,6 +546,7 @@ Uses your CAP application's existing authentication system:
526
546
  ```
527
547
 
528
548
  #### `"none"` Mode (Development/Testing)
549
+
529
550
  Disables authentication completely:
530
551
 
531
552
  ```json
@@ -541,6 +562,7 @@ Disables authentication completely:
541
562
  **⚠️ Security Warning**: Only use `"none"` mode in development environments. Never deploy to production without proper authentication.
542
563
 
543
564
  #### Authentication Flow
565
+
544
566
  1. MCP client connects to `/mcp` endpoint
545
567
  2. If the authentication style used is OAuth, the OAuth flow will be executed
546
568
  3. CAP authentication middleware validates credentials (if `auth: "inherit"`)
@@ -550,6 +572,7 @@ Disables authentication completely:
550
572
  ### Automatic Features
551
573
 
552
574
  The plugin automatically:
575
+
553
576
  - Scans your CAP service definitions for `@mcp` annotations
554
577
  - Generates appropriate MCP resources, tools, and prompts
555
578
  - Creates ResourceTemplates with proper OData v4 query parameter support
@@ -570,6 +593,7 @@ While this shows how this example CDS annotation works, the possibilities are en
570
593
  ## 📋 Business Case Example: Workflow Approval Management
571
594
 
572
595
  ### The Setup
596
+
573
597
  Your CAP service includes a workflow management system with MCP integration:
574
598
 
575
599
  ```cds
@@ -587,16 +611,19 @@ service WorkflowService {
587
611
  ### The Interaction Flow
588
612
 
589
613
  **1. User Query**
614
+
590
615
  ```
591
616
  User: "Hey <Agent>, do I have any workflows pending approval?"
592
617
  ```
593
618
 
594
619
  **2. AI Agent Processing**
620
+
595
621
  - Agent recognizes this as a request for pending approval information
596
622
  - Identifies the `get-my-pending-approval` tool as the appropriate method
597
623
  - Determines the user's ID from context (session, authentication, etc.)
598
624
 
599
625
  **3. MCP Tool Execution**
626
+
600
627
  ```javascript
601
628
  // Agent calls the MCP tool
602
629
  {
@@ -608,12 +635,14 @@ User: "Hey <Agent>, do I have any workflows pending approval?"
608
635
  ```
609
636
 
610
637
  **4. CAP Service Processing**
638
+
611
639
  - Your CAP service receives the tool call
612
640
  - Executes `getPendingApproval("john.doe@company.com")`
613
641
  - Queries your workflow database/system
614
642
  - Returns structured workflow data
615
643
 
616
644
  **5. AI Response**
645
+
617
646
  ```
618
647
  Agent: "You have 3 workflows pending your approval:
619
648
 
@@ -636,6 +665,7 @@ Would you like me to help you review any of these in detail?"
636
665
  ```
637
666
 
638
667
  ### Business Value
668
+
639
669
  - **Instant Access**: No need to log into workflow systems or navigate complex UIs
640
670
  - **Contextual Intelligence**: AI can prioritize based on urgency, amounts, or business rules
641
671
  - **Natural Interaction**: Users can ask follow-up questions in plain language
@@ -702,11 +732,13 @@ This project is licensed under the Apache-2.0 License - see the [LICENSE.md](LIC
702
732
  ### Common Issues
703
733
 
704
734
  #### MCP Server Not Starting
735
+
705
736
  - **Check Port Availability**: Ensure port 4004 is not in use by another process
706
737
  - **Verify CAP Service**: Make sure your CAP application starts successfully with `cds serve`
707
738
  - **Authentication Issues**: If using `auth: "inherit"`, ensure your CAP authentication is properly configured
708
739
 
709
740
  #### MCP Client Connection Failures
741
+
710
742
  ```bash
711
743
  # Check if MCP endpoint is accessible
712
744
  curl http://localhost:4004/mcp/health
@@ -716,21 +748,25 @@ curl http://localhost:4004/mcp/health
716
748
  ```
717
749
 
718
750
  #### Annotation Not Working
751
+
719
752
  - **Syntax Check**: Verify your `@mcp` annotation syntax matches the examples
720
753
  - **Service Deployment**: Ensure annotated entities/functions are properly deployed
721
754
  - **Case Sensitivity**: Check that annotation properties use correct casing (`resource`, `tool`, `prompts`)
722
755
 
723
756
  #### OData Query Issues
757
+
724
758
  - **SDK Bug Workaround**: Due to the known `@modelcontextprotocol/sdk` bug, provide all query parameters when using dynamic queries
725
759
  - **Parameter Validation**: Ensure query parameters match OData v4 syntax
726
760
 
727
761
  #### Performance Issues
762
+
728
763
  - **Resource Filtering**: Use specific `resource` arrays instead of `true` for large datasets
729
764
  - **Query Optimization**: Implement proper database indexes for frequently queried fields
730
765
 
731
766
  ### Debugging
732
767
 
733
768
  #### Enable Debug Logging
769
+
734
770
  ```json
735
771
  {
736
772
  "cds": {
@@ -744,6 +780,7 @@ curl http://localhost:4004/mcp/health
744
780
  ```
745
781
 
746
782
  #### Test MCP Implementation
783
+
747
784
  ```bash
748
785
  # Use MCP Inspector for interactive testing
749
786
  npm run inspect
@@ -761,14 +798,17 @@ npm test -- --testPathPattern=integration
761
798
  ## 🚨 Performance & Limitations
762
799
 
763
800
  ### Known Limitations
801
+
764
802
  - **SDK Bug**: Dynamic resource queries require all query parameters due to `@modelcontextprotocol/sdk` RFC template string issue
765
803
 
766
804
  ### Performance Considerations
805
+
767
806
  - **Large Datasets**: Use `resource: ['top']` or similar constraints for entities with many records
768
807
  - **Complex Queries**: OData query parsing adds overhead - consider caching for frequently accessed data
769
808
  - **Concurrent Sessions**: Each MCP client creates a separate session - monitor memory usage with many clients
770
809
 
771
810
  ### Scale Recommendations
811
+
772
812
  - **Development**: No specific limits
773
813
  - **Production**: Test with expected concurrent MCP client count
774
814
  - **Enterprise**: Consider load balancing for high-availability scenarios
@@ -781,6 +821,7 @@ npm test -- --testPathPattern=integration
781
821
  - [MCP Inspector Tool](https://github.com/modelcontextprotocol/inspector)
782
822
 
783
823
  ---
824
+
784
825
  (c) Copyright by Gavdi Labs 2025 - All Rights Reserved
785
826
 
786
827
  **Transform your CAP applications into AI-ready systems with the power of the Model Context Protocol.**
@@ -22,6 +22,7 @@ exports.MCP_ANNOTATION_MAPPING = new Map([
22
22
  ["@mcp.wrap", "wrap"],
23
23
  ["@mcp.wrap.tools", "wrap.tools"],
24
24
  ["@mcp.wrap.modes", "wrap.modes"],
25
+ ["@mcp.wrap.name", "wrap.name"],
25
26
  ["@mcp.wrap.hint", "wrap.hint"],
26
27
  ["@mcp.wrap.hint.get", "wrap.hint.get"],
27
28
  ["@mcp.wrap.hint.query", "wrap.hint.query"],
@@ -5,10 +5,32 @@ exports.errorHandlerFactory = errorHandlerFactory;
5
5
  const xsuaa_service_1 = require("./xsuaa-service");
6
6
  const utils_1 = require("./utils");
7
7
  const logger_1 = require("../logger");
8
+ const host_resolver_1 = require("./host-resolver");
8
9
  /** JSON-RPC 2.0 error code for unauthorized requests */
9
10
  const RPC_UNAUTHORIZED = 10;
10
11
  /* @ts-ignore */
11
12
  const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
13
+ /**
14
+ * Sends a 401 response with RFC 9728 compliant WWW-Authenticate header.
15
+ * The header includes `resource_metadata` pointing to the protected resource metadata endpoint.
16
+ *
17
+ * @param req - Express request object
18
+ * @param res - Express response object
19
+ * @param message - Error message for the JSON-RPC response
20
+ */
21
+ function send401WithMetadata(req, res, message) {
22
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
23
+ const metadataUrl = `${baseUrl}/.well-known/oauth-protected-resource`;
24
+ res.set("WWW-Authenticate", `Bearer resource_metadata="${metadataUrl}"`);
25
+ res.status(401).json({
26
+ jsonrpc: "2.0",
27
+ error: {
28
+ code: RPC_UNAUTHORIZED,
29
+ message,
30
+ id: null,
31
+ },
32
+ });
33
+ }
12
34
  /**
13
35
  * Creates an Express middleware for MCP authentication validation.
14
36
  *
@@ -39,14 +61,7 @@ function authHandlerFactory() {
39
61
  logger_1.LOGGER.debug("Authentication kind", authKind);
40
62
  return async (req, res, next) => {
41
63
  if (!req.headers.authorization && authKind !== "dummy") {
42
- res.status(401).json({
43
- jsonrpc: "2.0",
44
- error: {
45
- code: RPC_UNAUTHORIZED,
46
- message: "Unauthorized",
47
- id: null,
48
- },
49
- });
64
+ send401WithMetadata(req, res, "Unauthorized");
50
65
  return;
51
66
  }
52
67
  // For XSUAA/JWT auth types, use @sap/xssec for validation
@@ -54,14 +69,7 @@ function authHandlerFactory() {
54
69
  xsuaaService?.isConfigured()) {
55
70
  const securityContext = await xsuaaService.createSecurityContext(req);
56
71
  if (!securityContext) {
57
- res.status(401).json({
58
- jsonrpc: "2.0",
59
- error: {
60
- code: RPC_UNAUTHORIZED,
61
- message: "Invalid or expired token",
62
- id: null,
63
- },
64
- });
72
+ send401WithMetadata(req, res, "Invalid or expired token");
65
73
  return;
66
74
  }
67
75
  // Add security context to request for later use
@@ -82,14 +90,7 @@ function authHandlerFactory() {
82
90
  }
83
91
  const user = ctx.user;
84
92
  if (!user || user === cds.User.anonymous) {
85
- res.status(401).json({
86
- jsonrpc: "2.0",
87
- error: {
88
- code: RPC_UNAUTHORIZED,
89
- message: "Unauthorized",
90
- id: null,
91
- },
92
- });
93
+ send401WithMetadata(req, res, "Unauthorized");
93
94
  return;
94
95
  }
95
96
  return next();
@@ -116,13 +117,17 @@ function authHandlerFactory() {
116
117
  * @param next - Express next function for passing unhandled errors
117
118
  */
118
119
  function errorHandlerFactory() {
119
- return (err, _, res, next) => {
120
- if (err === 401 || err === 403) {
121
- res.status(err).json({
120
+ return (err, req, res, next) => {
121
+ if (err === 401) {
122
+ send401WithMetadata(req, res, "Unauthorized");
123
+ return;
124
+ }
125
+ if (err === 403) {
126
+ res.status(403).json({
122
127
  jsonrpc: "2.0",
123
128
  error: {
124
129
  code: RPC_UNAUTHORIZED,
125
- message: err === 401 ? "Unauthorized" : "Forbidden",
130
+ message: "Forbidden",
126
131
  id: null,
127
132
  },
128
133
  });
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ /**
3
+ * Host resolution for multi-tenant and reverse proxy scenarios.
4
+ *
5
+ * This module provides utilities to determine the correct public-facing host
6
+ * when the application runs behind SAP Approuter or other reverse proxies.
7
+ *
8
+ * Header Priority (based on SAP Approuter documentation):
9
+ * 1. x-forwarded-host - Standard header set by Approuter (setXForwardedHeaders=true, default)
10
+ * 2. x-custom-host - Only when EXTERNAL_REVERSE_PROXY=true
11
+ * 3. APP_DOMAIN env - Fallback for HTTP/2 bug where X-Forwarded headers may be missing
12
+ * 4. host header - Local development fallback
13
+ *
14
+ * @module host-resolver
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.getHostResolverEnv = getHostResolverEnv;
18
+ exports.isProductionEnv = isProductionEnv;
19
+ exports.extractSubdomain = extractSubdomain;
20
+ exports.normalizeHost = normalizeHost;
21
+ exports.resolveEffectiveHost = resolveEffectiveHost;
22
+ exports.getProtocol = getProtocol;
23
+ exports.buildPublicBaseUrl = buildPublicBaseUrl;
24
+ const logger_1 = require("../logger");
25
+ /**
26
+ * Reads environment configuration for host resolution.
27
+ * Separated for testability.
28
+ */
29
+ function getHostResolverEnv() {
30
+ return {
31
+ appDomain: process.env.APP_DOMAIN,
32
+ externalReverseProxy: process.env.EXTERNAL_REVERSE_PROXY === "true",
33
+ nodeEnv: process.env.NODE_ENV,
34
+ };
35
+ }
36
+ /**
37
+ * Determines if the environment is production.
38
+ * Production = NODE_ENV explicitly set to "production".
39
+ */
40
+ function isProductionEnv(env) {
41
+ return env.nodeEnv === "production";
42
+ }
43
+ /**
44
+ * Extracts subdomain from a hostname.
45
+ * For "tenant1.myapp.cloud", returns "tenant1".
46
+ * For "myapp.cloud" or "localhost", returns "".
47
+ */
48
+ function extractSubdomain(host, appDomain) {
49
+ if (!host || !appDomain)
50
+ return "";
51
+ // Normalize: remove port, lowercase
52
+ const cleanHost = host.split(":")[0].toLowerCase();
53
+ const cleanDomain = appDomain.toLowerCase();
54
+ if (cleanHost.endsWith(`.${cleanDomain}`)) {
55
+ return cleanHost.slice(0, -(cleanDomain.length + 1));
56
+ }
57
+ return "";
58
+ }
59
+ /**
60
+ * Safely gets the host header from request.
61
+ * Handles cases where req.get might not be available (e.g., in tests).
62
+ */
63
+ function getHostHeader(req) {
64
+ if (typeof req.get === "function") {
65
+ return req.get("host") || "";
66
+ }
67
+ return req.headers?.host || "";
68
+ }
69
+ /**
70
+ * Normalizes a host header value.
71
+ * - Takes first value if comma-separated (proxy chain)
72
+ * - Trims whitespace
73
+ */
74
+ function normalizeHost(headerValue) {
75
+ if (!headerValue)
76
+ return "";
77
+ // Take first host if comma-separated (multiple proxies)
78
+ return headerValue.split(",")[0].trim();
79
+ }
80
+ /**
81
+ * Resolves the effective public-facing host for the current request.
82
+ *
83
+ * @param req - Express request object
84
+ * @param env - Environment configuration (optional, uses process.env by default)
85
+ * @returns The resolved public host (without protocol)
86
+ */
87
+ function resolveEffectiveHost(req, env = getHostResolverEnv()) {
88
+ const isProduction = isProductionEnv(env);
89
+ // Priority 1: x-forwarded-host (SAP Approuter standard)
90
+ const xfh = normalizeHost(req.headers["x-forwarded-host"]);
91
+ if (xfh) {
92
+ logger_1.LOGGER.debug("[host-resolver] using x-forwarded-host", { host: xfh });
93
+ return xfh;
94
+ }
95
+ // Priority 2: x-custom-host (only with EXTERNAL_REVERSE_PROXY)
96
+ if (env.externalReverseProxy) {
97
+ const xch = normalizeHost(req.headers["x-custom-host"]);
98
+ if (xch) {
99
+ logger_1.LOGGER.debug("[host-resolver] using x-custom-host", { host: xch });
100
+ return xch;
101
+ }
102
+ }
103
+ // Priority 3: APP_DOMAIN fallback (production only)
104
+ if (env.appDomain && isProduction) {
105
+ logger_1.LOGGER.info("[host-resolver] using APP_DOMAIN fallback", {
106
+ host: env.appDomain,
107
+ });
108
+ return env.appDomain;
109
+ }
110
+ // Priority 4: Raw host header
111
+ const host = normalizeHost(getHostHeader(req)) || "localhost";
112
+ if (isProduction) {
113
+ logger_1.LOGGER.warn("[host-resolver] using raw host in production", { host });
114
+ }
115
+ return host;
116
+ }
117
+ /**
118
+ * Determines the protocol (http/https) for URL construction.
119
+ *
120
+ * @param req - Express request object
121
+ * @param env - Environment configuration (optional)
122
+ * @returns Protocol string ('http' or 'https')
123
+ */
124
+ function getProtocol(req, env = getHostResolverEnv()) {
125
+ // Check x-forwarded-proto first (most reliable behind proxy)
126
+ const forwardedProto = req.headers["x-forwarded-proto"];
127
+ if (forwardedProto) {
128
+ // Take first value if comma-separated
129
+ return forwardedProto.split(",")[0].trim();
130
+ }
131
+ // Default to HTTPS in production
132
+ return isProductionEnv(env) ? "https" : req.protocol;
133
+ }
134
+ /**
135
+ * Builds the complete public base URL for the current request.
136
+ * Combines protocol and resolved host.
137
+ *
138
+ * @param req - Express request object
139
+ * @param env - Environment configuration (optional)
140
+ * @returns Full base URL (e.g., "https://tenant1.myapp.cloud")
141
+ */
142
+ function buildPublicBaseUrl(req, env = getHostResolverEnv()) {
143
+ const protocol = getProtocol(req, env);
144
+ const host = resolveEffectiveHost(req, env);
145
+ return `${protocol}://${host}`;
146
+ }
package/lib/auth/utils.js CHANGED
@@ -15,6 +15,7 @@ const factory_1 = require("./factory");
15
15
  const xsuaa_service_1 = require("./xsuaa-service");
16
16
  const handlers_1 = require("./handlers");
17
17
  const logger_1 = require("../logger");
18
+ const host_resolver_1 = require("./host-resolver");
18
19
  /**
19
20
  * @fileoverview Authentication utilities for MCP-CAP integration.
20
21
  *
@@ -194,22 +195,6 @@ function configureOAuthProxy(expressApp) {
194
195
  }
195
196
  registerOAuthEndpoints(expressApp, credentials, kind);
196
197
  }
197
- /**
198
- * Determines the correct protocol (HTTP/HTTPS) for URL construction.
199
- * Accounts for reverse proxy headers and production environment defaults.
200
- *
201
- * @param req - Express request object
202
- * @returns Protocol string ('http' or 'https')
203
- */
204
- function getProtocol(req) {
205
- // Check for reverse proxy header first (most reliable)
206
- if (req.headers["x-forwarded-proto"]) {
207
- return req.headers["x-forwarded-proto"];
208
- }
209
- // Default to HTTPS in production environments
210
- const isProduction = process.env.NODE_ENV === "production" || process.env.VCAP_APPLICATION;
211
- return isProduction ? "https" : req.protocol;
212
- }
213
198
  /**
214
199
  * Registers OAuth endpoints for XSUAA integration
215
200
  * Only called for jwt/xsuaa/ias auth types with valid credentials
@@ -237,8 +222,8 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
237
222
  const { state, redirect_uri, client_id, code_challenge, code_challenge_method, scope, } = req.query;
238
223
  // Client validation and redirect URI validation is handled by XSUAA
239
224
  // We delegate all client management to XSUAA's built-in OAuth server
240
- const protocol = getProtocol(req);
241
- const redirectUri = redirect_uri || `${protocol}://${req.get("host")}/oauth/callback`;
225
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
226
+ const redirectUri = redirect_uri || `${baseUrl}/oauth/callback`;
242
227
  const authUrl = xsuaaService.getAuthorizationUrl(redirectUri, client_id ?? "", state, code_challenge, code_challenge_method, scope);
243
228
  res.redirect(authUrl);
244
229
  });
@@ -261,8 +246,8 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
261
246
  return;
262
247
  }
263
248
  try {
264
- const protocol = getProtocol(req);
265
- const url = redirect_uri || `${protocol}://${req.get("host")}/oauth/callback`;
249
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
250
+ const url = redirect_uri || `${baseUrl}/oauth/callback`;
266
251
  const tokenData = await xsuaaService.exchangeCodeForToken(code, url, code_verifier);
267
252
  const scopedToken = await xsuaaService.getApplicationScopes(tokenData);
268
253
  logger_1.LOGGER.debug("Scopes in token:", scopedToken.scope);
@@ -283,8 +268,7 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
283
268
  });
284
269
  // OAuth Discovery endpoint
285
270
  expressApp.get("/.well-known/oauth-authorization-server", (req, res) => {
286
- const protocol = getProtocol(req);
287
- const baseUrl = `${protocol}://${req.get("host")}`;
271
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
288
272
  res.json({
289
273
  issuer: credentials.url,
290
274
  authorization_endpoint: `${baseUrl}/oauth/authorize`,
@@ -298,15 +282,25 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
298
282
  registration_endpoint_auth_methods_supported: ["client_secret_basic"],
299
283
  });
300
284
  });
285
+ // RFC 9728: OAuth 2.0 Protected Resource Metadata endpoint
286
+ expressApp.get("/.well-known/oauth-protected-resource", (req, res) => {
287
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
288
+ res.json({
289
+ resource: baseUrl,
290
+ authorization_servers: [credentials.url],
291
+ bearer_methods_supported: ["header"],
292
+ resource_documentation: `${baseUrl}/mcp/health`,
293
+ });
294
+ });
301
295
  // OAuth Dynamic Client Registration discovery endpoint (GET)
302
296
  expressApp.get("/oauth/register", async (req, res) => {
297
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
303
298
  // IAS does not support DCR so we will respond with the pre-configured client_id
304
299
  if (kind === "ias") {
305
- const protocol = getProtocol(req);
306
300
  const enhancedResponse = {
307
301
  client_id: credentials.clientid, // Add our CAP app's client ID
308
302
  redirect_uris: req.body.redirect_uris || [
309
- `${protocol}://${req.get("host")}/oauth/callback`,
303
+ `${baseUrl}/oauth/callback`,
310
304
  ],
311
305
  };
312
306
  logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
@@ -325,11 +319,10 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
325
319
  });
326
320
  const xsuaaData = await response.json();
327
321
  // Add missing required fields that MCP client expects
328
- const protocol = getProtocol(req);
329
322
  const enhancedResponse = {
330
323
  ...xsuaaData, // Keep all XSUAA fields
331
324
  client_id: credentials.clientid, // Add our CAP app's client ID
332
- redirect_uris: [`${protocol}://${req.get("host")}/oauth/callback`], // Add our callback URL for discovery
325
+ redirect_uris: [`${baseUrl}/oauth/callback`], // Add our callback URL for discovery
333
326
  };
334
327
  res.status(response.status).json(enhancedResponse);
335
328
  }
@@ -343,13 +336,13 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
343
336
  });
344
337
  // OAuth Dynamic Client Registration endpoint (POST) with CSRF handling
345
338
  expressApp.post("/oauth/register", async (req, res) => {
339
+ const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
346
340
  // IAS does not support DCR so we will respond with the pre-configured client_id
347
341
  if (kind === "ias") {
348
- const protocol = getProtocol(req);
349
342
  const enhancedResponse = {
350
343
  client_id: credentials.clientid, // Add our CAP app's client ID
351
344
  redirect_uris: req.body.redirect_uris || [
352
- `${protocol}://${req.get("host")}/oauth/callback`,
345
+ `${baseUrl}/oauth/callback`,
353
346
  ],
354
347
  };
355
348
  logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
@@ -390,14 +383,12 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
390
383
  });
391
384
  const xsuaaData = await registrationResponse.json();
392
385
  // Add missing required fields that MCP client expects
393
- const protocol = getProtocol(req);
394
386
  const enhancedResponse = {
395
387
  ...xsuaaData, // Keep all XSUAA fields
396
388
  client_id: credentials.clientid, // Add our CAP app's client ID
397
- // client_secret: credentials.clientsecret, // CAP app's client secret
398
389
  redirect_uris: req.body.redirect_uris || [
399
- `${protocol}://${req.get("host")}/oauth/callback`,
400
- ], // Use client's redirect URIs
390
+ `${baseUrl}/oauth/callback`,
391
+ ],
401
392
  };
402
393
  logger_1.LOGGER.debug("[AUTH] Register POST response", enhancedResponse);
403
394
  res.status(registrationResponse.status).json(enhancedResponse);
@@ -36,6 +36,7 @@ function loadConfiguration() {
36
36
  "update",
37
37
  ],
38
38
  instructions: cdsEnv?.instructions,
39
+ enable_model_description: cdsEnv?.enable_model_description ?? true,
39
40
  };
40
41
  }
41
42
  /**
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.coerceKeyValue = coerceKeyValue;
3
4
  exports.registerEntityWrappers = registerEntityWrappers;
4
5
  const zod_1 = require("zod");
5
6
  const utils_1 = require("../auth/utils");
@@ -61,6 +62,90 @@ async function resolveServiceInstance(serviceName) {
61
62
  // NOTE: We use plain entity names (service projection) for queries.
62
63
  const MAX_TOP = 200;
63
64
  const TIMEOUT_MS = 10_000; // Standard timeout for tool calls (ms)
65
+ /**
66
+ * CDS integer types whose values can be safely represented as a JavaScript `Number`
67
+ * without risk of precision loss: `Integer`, `Int16`, `Int32`, `UInt8`.
68
+ *
69
+ * All values of these types are guaranteed to fit within `Number.MAX_SAFE_INTEGER`
70
+ * (2^53 - 1). When a key value arrives as a digit string (e.g. `"42"`),
71
+ * {@link coerceKeyValue} converts it to a `Number` so that CAP can resolve
72
+ * the entity lookup correctly.
73
+ *
74
+ * The following numeric CDS types are intentionally excluded:
75
+ *
76
+ * - `Int64` — values may exceed `Number.MAX_SAFE_INTEGER`, causing silent
77
+ * precision loss. Classified under {@link PRECISION_SENSITIVE_CDS_TYPES}.
78
+ * - `Decimal` — arbitrary-precision type; CAP preserves these as strings
79
+ * for exact arithmetic. Classified under {@link PRECISION_SENSITIVE_CDS_TYPES}.
80
+ * - `Double` — maps directly to IEEE 754 double-precision (identical to JS
81
+ * `number`), so there is no precision concern. Excluded here because it is
82
+ * a floating-point type, not an integer.
83
+ *
84
+ * @see {@link PRECISION_SENSITIVE_CDS_TYPES}
85
+ * @see {@link coerceKeyValue}
86
+ * @see https://tc39.es/ecma262/#sec-number.max_safe_integer — ECMAScript Number.MAX_SAFE_INTEGER
87
+ * @see https://cap.cloud.sap/docs/cds/types — CAP CDS Built-in Types
88
+ */
89
+ const SAFE_INTEGER_CDS_TYPES = new Set(["Integer", "Int16", "Int32", "UInt8"]);
90
+ /**
91
+ * CDS numeric types where values must be represented as strings to preserve
92
+ * precision: `Int64`, `Decimal`.
93
+ *
94
+ * JavaScript `Number` (IEEE 754 double-precision) cannot faithfully represent
95
+ * all values of these types. When a key value arrives as a `number` from the
96
+ * MCP tool input, {@link coerceKeyValue} normalizes it to a `string` so that
97
+ * CAP can handle it without silent truncation.
98
+ *
99
+ * CAP's `cds.features.ieee754compatible` setting controls whether OData
100
+ * responses serialize `Edm.Int64` and `Edm.Decimal` as JSON strings.
101
+ * Regardless of that setting, this set ensures keys are always passed as
102
+ * strings to the CAP runtime.
103
+ *
104
+ * `Double` is intentionally not included. It maps directly to IEEE 754
105
+ * double-precision — the same representation as a JavaScript `number` — so
106
+ * no precision is lost during coercion. The `ieee754compatible` flag does
107
+ * not affect `Double`.
108
+ *
109
+ * @see {@link SAFE_INTEGER_CDS_TYPES}
110
+ * @see {@link coerceKeyValue}
111
+ * @see https://www.rfc-editor.org/rfc/rfc8259#section-6 — RFC 8259, Section 6: Numbers
112
+ * @see https://cap.cloud.sap/docs/releases/jun24#ieee754compatible — CAP ieee754compatible
113
+ */
114
+ const PRECISION_SENSITIVE_CDS_TYPES = new Set(["Int64", "Decimal"]);
115
+ /**
116
+ * Coerces a key value between string and number representations based on
117
+ * the CDS type metadata of the target entity element.
118
+ *
119
+ * Coercion rules, applied in order:
120
+ *
121
+ * 1. Safe integer types ({@link SAFE_INTEGER_CDS_TYPES}) — digit-only strings
122
+ * (e.g. `"42"`) are converted to `Number`. `UInt8` only accepts non-negative
123
+ * digit strings since it is an unsigned type.
124
+ * 2. Precision-sensitive types ({@link PRECISION_SENSITIVE_CDS_TYPES}) — numeric
125
+ * values are converted to `String` to prevent silent precision loss beyond
126
+ * `Number.MAX_SAFE_INTEGER`.
127
+ * 3. All other types — returned unchanged.
128
+ *
129
+ * @param raw - The raw key value received from the MCP tool input.
130
+ * @param cdsType - The CDS type name of the key element (e.g. `"String"`, `"Integer"`).
131
+ * @returns The coerced value, or the original value if no coercion rule applies.
132
+ *
133
+ * @see {@link SAFE_INTEGER_CDS_TYPES}
134
+ * @see {@link PRECISION_SENSITIVE_CDS_TYPES}
135
+ */
136
+ function coerceKeyValue(raw, cdsType) {
137
+ if (typeof raw === "string" && SAFE_INTEGER_CDS_TYPES.has(cdsType)) {
138
+ // UInt8 is unsigned — only accept non-negative digit strings
139
+ const pattern = cdsType === "UInt8" ? /^\d+$/ : /^-?\d+$/;
140
+ if (pattern.test(raw)) {
141
+ return Number(raw);
142
+ }
143
+ }
144
+ if (typeof raw === "number" && PRECISION_SENSITIVE_CDS_TYPES.has(cdsType)) {
145
+ return String(raw);
146
+ }
147
+ return raw;
148
+ }
64
149
  // Map OData operators to CDS/SQL operators for better performance and readability
65
150
  const ODATA_TO_CDS_OPERATORS = new Map([
66
151
  ["eq", "="],
@@ -123,8 +208,13 @@ function registerEntityWrappers(resAnno, server, authEnabled, defaultModes, acce
123
208
  * Builds the visible tool name for a given operation mode.
124
209
  * We prefer a descriptive naming scheme that is easy for humans and LLMs:
125
210
  * Service_Entity_mode
211
+ * If customName is provided via @mcp.wrap.name, uses customName_mode instead.
126
212
  */
127
- function nameFor(service, entity, suffix) {
213
+ function nameFor(service, entity, suffix, customName) {
214
+ // Custom name takes priority - uses format: customName_suffix
215
+ if (customName) {
216
+ return `${customName}_${suffix}`;
217
+ }
128
218
  // Use explicit Service_Entity_suffix naming to match docs/tests
129
219
  const entityName = entity.split(".").pop(); // keep original case
130
220
  const serviceName = service.split(".").pop(); // keep original case
@@ -135,7 +225,7 @@ function nameFor(service, entity, suffix) {
135
225
  * Supports select/where/orderby/top/skip and simple text search (q).
136
226
  */
137
227
  function registerQueryTool(resAnno, server, authEnabled) {
138
- const toolName = nameFor(resAnno.serviceName, resAnno.target, "query");
228
+ const toolName = nameFor(resAnno.serviceName, resAnno.target, "query", resAnno.wrap?.name);
139
229
  // Structured input schema for queries with guard for empty property lists
140
230
  const allKeys = Array.from(resAnno.properties.keys());
141
231
  const scalarKeys = Array.from(resAnno.properties.entries())
@@ -275,7 +365,7 @@ function registerQueryTool(resAnno, server, authEnabled) {
275
365
  * Accepts keys either as an object or shorthand (single-key) value.
276
366
  */
277
367
  function registerGetTool(resAnno, server, authEnabled) {
278
- const toolName = nameFor(resAnno.serviceName, resAnno.target, "get");
368
+ const toolName = nameFor(resAnno.serviceName, resAnno.target, "get", resAnno.wrap?.name);
279
369
  const inputSchema = {};
280
370
  for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
281
371
  inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}. ${resAnno.propertyHints.get(k) ?? ""}`);
@@ -313,7 +403,7 @@ function registerGetTool(resAnno, server, authEnabled) {
313
403
  }
314
404
  }
315
405
  const keys = {};
316
- for (const [k] of resAnno.resourceKeys.entries()) {
406
+ for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
317
407
  let provided = normalizedArgs[k];
318
408
  if (provided === undefined) {
319
409
  const alt = Object.entries(normalizedArgs || {}).find(([kk]) => String(kk).toLowerCase() === String(k).toLowerCase());
@@ -324,9 +414,7 @@ function registerGetTool(resAnno, server, authEnabled) {
324
414
  logger_1.LOGGER.warn(`Get tool missing required key`, { key: k, toolName });
325
415
  return (0, utils_2.toolError)("MISSING_KEY", `Missing key '${k}'`);
326
416
  }
327
- const raw = provided;
328
- keys[k] =
329
- typeof raw === "string" && /^\d+$/.test(raw) ? Number(raw) : raw;
417
+ keys[k] = coerceKeyValue(provided, cdsType);
330
418
  }
331
419
  logger_1.LOGGER.debug(`Executing READ on ${resAnno.target} with keys`, keys);
332
420
  try {
@@ -348,7 +436,7 @@ function registerGetTool(resAnno, server, authEnabled) {
348
436
  * Associations are exposed via <assoc>_ID fields for simplicity.
349
437
  */
350
438
  function registerCreateTool(resAnno, server, authEnabled) {
351
- const toolName = nameFor(resAnno.serviceName, resAnno.target, "create");
439
+ const toolName = nameFor(resAnno.serviceName, resAnno.target, "create", resAnno.wrap?.name);
352
440
  const inputSchema = {};
353
441
  for (const [propName, cdsType] of resAnno.properties.entries()) {
354
442
  const isAssociation = String(cdsType).toLowerCase().includes("association");
@@ -447,7 +535,7 @@ function registerCreateTool(resAnno, server, authEnabled) {
447
535
  * Keys are required; non-key fields are optional. Associations via <assoc>_ID.
448
536
  */
449
537
  function registerUpdateTool(resAnno, server, authEnabled) {
450
- const toolName = nameFor(resAnno.serviceName, resAnno.target, "update");
538
+ const toolName = nameFor(resAnno.serviceName, resAnno.target, "update", resAnno.wrap?.name);
451
539
  const inputSchema = {};
452
540
  // Keys required
453
541
  for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
@@ -494,14 +582,11 @@ function registerUpdateTool(resAnno, server, authEnabled) {
494
582
  }
495
583
  // Extract keys and update fields
496
584
  const keys = {};
497
- for (const [k] of resAnno.resourceKeys.entries()) {
585
+ for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
498
586
  if (args[k] === undefined) {
499
- return {
500
- isError: true,
501
- content: [{ type: "text", text: `Missing key '${k}'` }],
502
- };
587
+ return (0, utils_2.toolError)("MISSING_KEY", `Missing key '${k}'`);
503
588
  }
504
- keys[k] = args[k];
589
+ keys[k] = coerceKeyValue(args[k], cdsType);
505
590
  }
506
591
  // Normalize updates: prefer *_ID for associations and coerce numeric strings
507
592
  const updates = {};
@@ -569,7 +654,7 @@ function registerUpdateTool(resAnno, server, authEnabled) {
569
654
  * Requires keys to identify the entity to delete.
570
655
  */
571
656
  function registerDeleteTool(resAnno, server, authEnabled) {
572
- const toolName = nameFor(resAnno.serviceName, resAnno.target, "delete");
657
+ const toolName = nameFor(resAnno.serviceName, resAnno.target, "delete", resAnno.wrap?.name);
573
658
  const inputSchema = {};
574
659
  // Keys required for deletion
575
660
  for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
@@ -589,7 +674,7 @@ function registerDeleteTool(resAnno, server, authEnabled) {
589
674
  }
590
675
  // Extract keys - similar to get/update handlers
591
676
  const keys = {};
592
- for (const [k] of resAnno.resourceKeys.entries()) {
677
+ for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
593
678
  let provided = args[k];
594
679
  if (provided === undefined) {
595
680
  // Case-insensitive key matching (like in get handler)
@@ -601,10 +686,7 @@ function registerDeleteTool(resAnno, server, authEnabled) {
601
686
  logger_1.LOGGER.warn(`Delete tool missing required key`, { key: k, toolName });
602
687
  return (0, utils_2.toolError)("MISSING_KEY", `Missing key '${k}'`);
603
688
  }
604
- // Coerce numeric strings (like in get handler)
605
- const raw = provided;
606
- keys[k] =
607
- typeof raw === "string" && /^\d+$/.test(raw) ? Number(raw) : raw;
689
+ keys[k] = coerceKeyValue(provided, cdsType);
608
690
  }
609
691
  logger_1.LOGGER.debug(`Executing DELETE on ${resAnno.target} with keys`, keys);
610
692
  const tx = svc.tx({ user: (0, utils_1.getAccessRights)(authEnabled) });
@@ -34,8 +34,10 @@ function createMcpServer(config, annotations) {
34
34
  }
35
35
  logger_1.LOGGER.debug("Annotations found for server: ", annotations);
36
36
  const authEnabled = (0, utils_1.isAuthEnabled)(config.auth);
37
- // Always register discovery tool for better model planning
38
- (0, describe_model_1.registerDescribeModelTool)(server);
37
+ // Always register discovery tool for better model planning unless specifically turned off in configuration
38
+ if (config.enable_model_description) {
39
+ (0, describe_model_1.registerDescribeModelTool)(server);
40
+ }
39
41
  const accessRights = (0, utils_1.getAccessRights)(authEnabled);
40
42
  for (const entry of annotations.values()) {
41
43
  if (entry instanceof structures_1.McpToolAnnotation) {
package/lib/mcp/utils.js CHANGED
@@ -31,17 +31,17 @@ function determineMcpParameterType(cdsType, key, target) {
31
31
  case "Timestamp":
32
32
  return zod_1.z.coerce.date();
33
33
  case "Integer":
34
- return zod_1.z.number();
34
+ return zod_1.z.number().int();
35
35
  case "Int16":
36
- return zod_1.z.number();
36
+ return zod_1.z.number().int();
37
37
  case "Int32":
38
- return zod_1.z.number();
38
+ return zod_1.z.number().int();
39
39
  case "Int64":
40
- return zod_1.z.number();
40
+ return zod_1.z.union([zod_1.z.string(), zod_1.z.number().int()]).transform(String);
41
41
  case "UInt8":
42
- return zod_1.z.number();
42
+ return zod_1.z.number().int().min(0);
43
43
  case "Decimal":
44
- return zod_1.z.number();
44
+ return zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).transform(String);
45
45
  case "Double":
46
46
  return zod_1.z.number();
47
47
  case "Boolean":
@@ -67,17 +67,17 @@ function determineMcpParameterType(cdsType, key, target) {
67
67
  case "UUIDArray":
68
68
  return zod_1.z.array(zod_1.z.string());
69
69
  case "IntegerArray":
70
- return zod_1.z.array(zod_1.z.number());
70
+ return zod_1.z.array(zod_1.z.number().int());
71
71
  case "Int16Array":
72
- return zod_1.z.array(zod_1.z.number());
72
+ return zod_1.z.array(zod_1.z.number().int());
73
73
  case "Int32Array":
74
- return zod_1.z.array(zod_1.z.number());
74
+ return zod_1.z.array(zod_1.z.number().int());
75
75
  case "Int64Array":
76
- return zod_1.z.array(zod_1.z.number());
76
+ return zod_1.z.array(zod_1.z.union([zod_1.z.string(), zod_1.z.number().int()]).transform(String));
77
77
  case "UInt8Array":
78
- return zod_1.z.array(zod_1.z.number());
78
+ return zod_1.z.array(zod_1.z.number().int().min(0));
79
79
  case "DecimalArray":
80
- return zod_1.z.array(zod_1.z.number());
80
+ return zod_1.z.array(zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).transform(String));
81
81
  case "BooleanArray":
82
82
  return zod_1.z.array(zod_1.z.boolean());
83
83
  case "DoubleArray":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "MCP Plugin for CAP",
5
5
  "keywords": [
6
6
  "MCP",