@gavdi/cap-mcp 0.9.9-alpha.3 → 0.10.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 +48 -18
- package/lib/annotations/constants.js +2 -0
- package/lib/annotations/parser.js +18 -2
- package/lib/annotations/structures.js +12 -1
- package/lib/annotations/utils.js +16 -0
- package/lib/auth/handler.js +1 -13
- package/lib/auth/utils.js +51 -79
- package/lib/mcp/describe-model.js +6 -2
- package/lib/mcp/elicited-input.js +162 -0
- package/lib/mcp/entity-tools.js +98 -21
- package/lib/mcp/tools.js +20 -25
- package/lib/mcp/utils.js +2 -0
- package/lib/mcp.js +1 -3
- package/package.json +2 -2
- package/lib/.DS_Store +0 -0
- package/lib/annotations.js +0 -257
- package/lib/auth/adapter.js +0 -2
- package/lib/auth/mock.js +0 -2
- package/lib/auth/types.js +0 -2
- package/lib/mcp/customResourceTemplate.js +0 -156
- package/lib/types.js +0 -2
- package/lib/utils.js +0 -136
package/README.md
CHANGED
|
@@ -120,6 +120,7 @@ This plugin transforms your annotated CAP services into a fully functional MCP s
|
|
|
120
120
|
- **🔧 Tools**: Convert CAP functions and actions into executable MCP tools
|
|
121
121
|
- **🧩 Entity Wrappers (optional)**: Expose CAP entities as tools (`query`, `get`, and optionally `create`, `update`) for LLM tool use while keeping resources intact
|
|
122
122
|
- **💡 Prompts**: Define reusable prompt templates for AI interactions
|
|
123
|
+
- **⚡ Elicitation**: Request user confirmation or input parameters before tool execution
|
|
123
124
|
- **🔄 Auto-generation**: Automatically creates MCP server endpoints based on annotations
|
|
124
125
|
- **⚙️ Flexible Configuration**: Support for custom parameter sets and descriptions
|
|
125
126
|
|
|
@@ -231,6 +232,52 @@ extend projection Books with actions {
|
|
|
231
232
|
}
|
|
232
233
|
```
|
|
233
234
|
|
|
235
|
+
#### Tool Elicitation
|
|
236
|
+
|
|
237
|
+
Request user confirmation or input before tool execution using the `elicit` property:
|
|
238
|
+
|
|
239
|
+
```cds
|
|
240
|
+
// Request user confirmation before execution
|
|
241
|
+
@mcp: {
|
|
242
|
+
name : 'book-recommendation',
|
|
243
|
+
description: 'Get a random book recommendation',
|
|
244
|
+
tool : true,
|
|
245
|
+
elicit : ['confirm']
|
|
246
|
+
}
|
|
247
|
+
function getBookRecommendation() returns String;
|
|
248
|
+
|
|
249
|
+
// Request user input for parameters
|
|
250
|
+
@mcp: {
|
|
251
|
+
name : 'get-author',
|
|
252
|
+
description: 'Gets the desired author',
|
|
253
|
+
tool : true,
|
|
254
|
+
elicit : ['input']
|
|
255
|
+
}
|
|
256
|
+
function getAuthor(id: String) returns String;
|
|
257
|
+
|
|
258
|
+
// Request both input and confirmation
|
|
259
|
+
@mcp: {
|
|
260
|
+
name : 'books-by-author',
|
|
261
|
+
description: 'Gets a list of books made by the author',
|
|
262
|
+
tool : true,
|
|
263
|
+
elicit : ['input', 'confirm']
|
|
264
|
+
}
|
|
265
|
+
function getBooksByAuthor(authorName: String) returns array of String;
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
> NOTE: Elicitation is only available for direct tools at this moment. Wrapped entities are not covered by this.
|
|
269
|
+
|
|
270
|
+
**Elicit Types:**
|
|
271
|
+
- **`confirm`**: Requests user confirmation before executing the tool with a yes/no prompt
|
|
272
|
+
- **`input`**: Prompts the user to provide values for the tool's parameters
|
|
273
|
+
- **Combined**: Use both `['input', 'confirm']` to first collect parameters, then ask for confirmation
|
|
274
|
+
|
|
275
|
+
**User Experience:**
|
|
276
|
+
- **Confirmation**: "Please confirm that you want to perform action 'Get a random book recommendation'"
|
|
277
|
+
- **Input**: "Please fill out the required parameters" with a form for each parameter
|
|
278
|
+
- **User Actions**: Accept, decline, or cancel the elicitation request
|
|
279
|
+
- **Early Exit**: Tools return appropriate messages if declined or cancelled
|
|
280
|
+
|
|
234
281
|
### Prompt Templates
|
|
235
282
|
|
|
236
283
|
Define reusable AI prompt templates:
|
|
@@ -550,24 +597,7 @@ npm test -- --testPathPattern=integration
|
|
|
550
597
|
## 🚨 Performance & Limitations
|
|
551
598
|
|
|
552
599
|
### Known Limitations
|
|
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
|
|
600
|
+
- **SDK Bug**: Dynamic resource queries require all query parameters due to `@modelcontextprotocol/sdk` RFC template string issue
|
|
571
601
|
|
|
572
602
|
### Performance Considerations
|
|
573
603
|
- **Large Datasets**: Use `resource: ['top']` or similar constraints for entities with many records
|
|
@@ -27,6 +27,8 @@ exports.MCP_ANNOTATION_PROPS = {
|
|
|
27
27
|
MCP_PROMPT: "@mcp.prompts",
|
|
28
28
|
/** Wrapper configuration for exposing entities as tools */
|
|
29
29
|
MCP_WRAP: "@mcp.wrap",
|
|
30
|
+
/** Elicited user input annotation for tools in CAP services */
|
|
31
|
+
MCP_ELICIT: "@mcp.elicit",
|
|
30
32
|
};
|
|
31
33
|
/**
|
|
32
34
|
* Set of annotations used for CDS auth annotations
|
|
@@ -26,6 +26,13 @@ function parseDefinitions(model) {
|
|
|
26
26
|
if (!parsedAnnotations || !(0, utils_1.containsRequiredAnnotations)(parsedAnnotations)) {
|
|
27
27
|
continue; // This check must occur here, since we do want the bound operations even if the parent is not annotated
|
|
28
28
|
}
|
|
29
|
+
// Set the target in annotations for error reporting
|
|
30
|
+
if (parsedAnnotations) {
|
|
31
|
+
parsedAnnotations.target = key;
|
|
32
|
+
}
|
|
33
|
+
if (!(0, utils_1.containsRequiredElicitedParams)(parsedAnnotations)) {
|
|
34
|
+
continue; // Really doesn't do anything as the method will throw if the implementation is invalid
|
|
35
|
+
}
|
|
29
36
|
const verifiedAnnotations = parsedAnnotations;
|
|
30
37
|
switch (def.kind) {
|
|
31
38
|
case "entity":
|
|
@@ -97,6 +104,9 @@ function parseAnnotations(definition) {
|
|
|
97
104
|
// Wrapper container to expose resources as tools
|
|
98
105
|
annotations.wrap = v;
|
|
99
106
|
continue;
|
|
107
|
+
case constants_1.MCP_ANNOTATION_PROPS.MCP_ELICIT:
|
|
108
|
+
annotations.elicit = v;
|
|
109
|
+
continue;
|
|
100
110
|
case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
|
|
101
111
|
annotations.requires = v;
|
|
102
112
|
continue;
|
|
@@ -139,7 +149,7 @@ function constructToolAnnotation(serviceName, target, annotations, entityKey, ke
|
|
|
139
149
|
return undefined;
|
|
140
150
|
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
|
|
141
151
|
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);
|
|
152
|
+
return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions, annotations.elicit);
|
|
143
153
|
}
|
|
144
154
|
/**
|
|
145
155
|
* Constructs a prompt annotation from parsed annotation data
|
|
@@ -172,7 +182,13 @@ function parseBoundOperations(serviceName, entityKey, definition, resultRef) {
|
|
|
172
182
|
if (v.kind !== "function" && v.kind !== "action")
|
|
173
183
|
continue;
|
|
174
184
|
const parsedAnnotations = parseAnnotations(v);
|
|
175
|
-
|
|
185
|
+
// Set the target in annotations for error reporting
|
|
186
|
+
if (parsedAnnotations) {
|
|
187
|
+
parsedAnnotations.target = k;
|
|
188
|
+
}
|
|
189
|
+
if (!parsedAnnotations ||
|
|
190
|
+
!(0, utils_1.containsRequiredAnnotations)(parsedAnnotations) ||
|
|
191
|
+
!(0, utils_1.containsRequiredElicitedParams)(parsedAnnotations)) {
|
|
176
192
|
continue;
|
|
177
193
|
}
|
|
178
194
|
const verifiedAnnotations = parsedAnnotations;
|
|
@@ -142,6 +142,8 @@ class McpToolAnnotation extends McpAnnotation {
|
|
|
142
142
|
_operationKind;
|
|
143
143
|
/** Map of key field names to their types for bound operations */
|
|
144
144
|
_keyTypeMap;
|
|
145
|
+
/** Elicited user input object */
|
|
146
|
+
_elicits;
|
|
145
147
|
/**
|
|
146
148
|
* Creates a new MCP tool annotation
|
|
147
149
|
* @param name - Unique identifier for this tool
|
|
@@ -153,13 +155,15 @@ class McpToolAnnotation extends McpAnnotation {
|
|
|
153
155
|
* @param operationKind - Optional operation type ('function' or 'action')
|
|
154
156
|
* @param keyTypeMap - Optional map of key fields to types for bound operations
|
|
155
157
|
* @param restrictions - Optional restrictions based on CDS roles
|
|
158
|
+
* @param elicits - Optional elicited input requirement
|
|
156
159
|
*/
|
|
157
|
-
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions) {
|
|
160
|
+
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions, elicits) {
|
|
158
161
|
super(name, description, operation, serviceName, restrictions ?? []);
|
|
159
162
|
this._parameters = parameters;
|
|
160
163
|
this._entityKey = entityKey;
|
|
161
164
|
this._operationKind = operationKind;
|
|
162
165
|
this._keyTypeMap = keyTypeMap;
|
|
166
|
+
this._elicits = elicits;
|
|
163
167
|
}
|
|
164
168
|
/**
|
|
165
169
|
* Gets the map of function parameters to their CDS types
|
|
@@ -189,6 +193,13 @@ class McpToolAnnotation extends McpAnnotation {
|
|
|
189
193
|
get keyTypeMap() {
|
|
190
194
|
return this._keyTypeMap;
|
|
191
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Gets the elicited user input if any is required for the tool
|
|
198
|
+
* @returns Elicited user input object
|
|
199
|
+
*/
|
|
200
|
+
get elicits() {
|
|
201
|
+
return this._elicits;
|
|
202
|
+
}
|
|
192
203
|
}
|
|
193
204
|
exports.McpToolAnnotation = McpToolAnnotation;
|
|
194
205
|
/**
|
package/lib/annotations/utils.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.splitDefinitionName = splitDefinitionName;
|
|
4
4
|
exports.containsMcpAnnotation = containsMcpAnnotation;
|
|
5
5
|
exports.containsRequiredAnnotations = containsRequiredAnnotations;
|
|
6
|
+
exports.containsRequiredElicitedParams = containsRequiredElicitedParams;
|
|
6
7
|
exports.isValidResourceAnnotation = isValidResourceAnnotation;
|
|
7
8
|
exports.isValidToolAnnotation = isValidToolAnnotation;
|
|
8
9
|
exports.isValidPromptsAnnotation = isValidPromptsAnnotation;
|
|
@@ -55,6 +56,21 @@ function containsRequiredAnnotations(annotations) {
|
|
|
55
56
|
}
|
|
56
57
|
return true;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Validates that the required params for MCP elicited user input annotations are valid
|
|
61
|
+
* @param annotations - The annotation structure to validate
|
|
62
|
+
* @returns True if valid, throw error if invalid
|
|
63
|
+
* @throws Error if required annotations are missing
|
|
64
|
+
*/
|
|
65
|
+
function containsRequiredElicitedParams(annotations) {
|
|
66
|
+
if (!annotations.elicit)
|
|
67
|
+
return true;
|
|
68
|
+
const param = annotations.elicit;
|
|
69
|
+
if (!param || param?.length <= 0) {
|
|
70
|
+
throw new Error(`Invalid annotation '${annotations.target}' - Incomplete elicited user input`);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
58
74
|
/**
|
|
59
75
|
* Validates a resource annotation structure
|
|
60
76
|
* @param annotations - The annotation structure to validate
|
package/lib/auth/handler.js
CHANGED
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.authHandlerFactory = authHandlerFactory;
|
|
4
4
|
exports.errorHandlerFactory = errorHandlerFactory;
|
|
5
|
-
const logger_1 = require("../logger");
|
|
6
5
|
/** JSON-RPC 2.0 error code for unauthorized requests */
|
|
7
6
|
const RPC_UNAUTHORIZED = 10;
|
|
8
|
-
/** HTTP Authenticate header **/
|
|
9
|
-
const WWW_AUTHENTICATE = "WWW-Authenticate";
|
|
10
7
|
/* @ts-ignore */
|
|
11
8
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
12
9
|
/**
|
|
@@ -34,15 +31,9 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
|
|
|
34
31
|
* @throws {500} When CAP context is not properly loaded
|
|
35
32
|
*/
|
|
36
33
|
function authHandlerFactory() {
|
|
34
|
+
const authKind = cds.env.requires.auth.kind;
|
|
37
35
|
return (req, res, next) => {
|
|
38
|
-
const auth = cds.env.requires.auth;
|
|
39
|
-
const authKind = auth.kind;
|
|
40
|
-
const credentials = auth.credentials;
|
|
41
36
|
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"`);
|
|
46
37
|
res.status(401).json({
|
|
47
38
|
jsonrpc: "2.0",
|
|
48
39
|
error: {
|
|
@@ -67,9 +58,6 @@ function authHandlerFactory() {
|
|
|
67
58
|
}
|
|
68
59
|
const user = ctx.user;
|
|
69
60
|
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"`);
|
|
73
61
|
res.status(401).json({
|
|
74
62
|
jsonrpc: "2.0",
|
|
75
63
|
error: {
|
package/lib/auth/utils.js
CHANGED
|
@@ -6,10 +6,30 @@ exports.registerAuthMiddleware = registerAuthMiddleware;
|
|
|
6
6
|
exports.hasToolOperationAccess = hasToolOperationAccess;
|
|
7
7
|
exports.getWrapAccesses = getWrapAccesses;
|
|
8
8
|
const handler_1 = require("./handler");
|
|
9
|
+
const proxyProvider_js_1 = require("@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js");
|
|
9
10
|
const router_js_1 = require("@modelcontextprotocol/sdk/server/auth/router.js");
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @fileoverview Authentication utilities for MCP-CAP integration.
|
|
13
|
+
*
|
|
14
|
+
* This module provides utilities for integrating CAP authentication with MCP servers.
|
|
15
|
+
* It supports all standard CAP authentication types and provides functions for:
|
|
16
|
+
* - Determining authentication status
|
|
17
|
+
* - Managing user access rights
|
|
18
|
+
* - Registering authentication middleware
|
|
19
|
+
*
|
|
20
|
+
* Supported CAP authentication types:
|
|
21
|
+
* - 'dummy': No authentication (privileged access)
|
|
22
|
+
* - 'mocked': Mock users with predefined credentials
|
|
23
|
+
* - 'basic': HTTP Basic Authentication
|
|
24
|
+
* - 'jwt': Generic JWT token validation
|
|
25
|
+
* - 'xsuaa': SAP BTP XSUAA OAuth2/JWT authentication
|
|
26
|
+
* - 'ias': SAP Identity Authentication Service
|
|
27
|
+
* - Custom string types for user-defined authentication strategies
|
|
28
|
+
*
|
|
29
|
+
* Access CAP auth configuration via: cds.env.requires.auth.kind
|
|
30
|
+
*/
|
|
11
31
|
/* @ts-ignore */
|
|
12
|
-
const cds = global.cds || require("@sap/cds"); //
|
|
32
|
+
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
13
33
|
/**
|
|
14
34
|
* Determines whether authentication is enabled for the MCP plugin.
|
|
15
35
|
*
|
|
@@ -97,8 +117,7 @@ function getAccessRights(authEnabled) {
|
|
|
97
117
|
* @since 1.0.0
|
|
98
118
|
*/
|
|
99
119
|
function registerAuthMiddleware(expressApp) {
|
|
100
|
-
|
|
101
|
-
const middlewares = cds.middlewares?.before || []; // Handle missing middlewares gracefully
|
|
120
|
+
const middlewares = cds.middlewares.before; // No types exists for this part of the CDS library
|
|
102
121
|
// Build array of auth middleware to apply
|
|
103
122
|
const authMiddleware = [];
|
|
104
123
|
// Add CAP middleware
|
|
@@ -113,10 +132,8 @@ function registerAuthMiddleware(expressApp) {
|
|
|
113
132
|
authMiddleware.push((0, handler_1.authHandlerFactory)());
|
|
114
133
|
// Apply auth middleware to all /mcp routes EXCEPT health
|
|
115
134
|
expressApp?.use(/^\/mcp(?!\/health).*/, ...authMiddleware);
|
|
116
|
-
//
|
|
117
|
-
// Configure OAuth proxy for enterprise authentication scenarios
|
|
135
|
+
// Then finally we add the oauth proxy to the xsuaa instance
|
|
118
136
|
configureOAuthProxy(expressApp);
|
|
119
|
-
logger_1.LOGGER.debug("Auth middleware configured");
|
|
120
137
|
}
|
|
121
138
|
/**
|
|
122
139
|
* Configures OAuth proxy middleware for enterprise authentication scenarios.
|
|
@@ -129,7 +146,6 @@ function registerAuthMiddleware(expressApp) {
|
|
|
129
146
|
* - Access token verification and validation
|
|
130
147
|
* - Client credential management
|
|
131
148
|
* - Integration with CAP authentication configuration
|
|
132
|
-
* - Dynamic client registration for MCP clients
|
|
133
149
|
*
|
|
134
150
|
* The OAuth proxy is only configured for enterprise authentication types
|
|
135
151
|
* (jwt, xsuaa, ias) and skips configuration for basic auth types.
|
|
@@ -159,86 +175,42 @@ function configureOAuthProxy(expressApp) {
|
|
|
159
175
|
const config = cds.env.requires.auth;
|
|
160
176
|
const kind = config.kind;
|
|
161
177
|
const credentials = config.credentials;
|
|
162
|
-
logger_1.LOGGER.debug("Running auth with configuration kind", kind);
|
|
163
178
|
// Safety guard - skip OAuth proxy for basic auth types
|
|
164
|
-
if (kind === "dummy" || kind === "mocked" || kind === "basic")
|
|
165
|
-
logger_1.LOGGER.debug("Skipping OAuth proxy for auth type:", kind);
|
|
179
|
+
if (kind === "dummy" || kind === "mocked" || kind === "basic")
|
|
166
180
|
return;
|
|
167
|
-
|
|
168
|
-
if (!credentials ||
|
|
181
|
+
else if (!credentials ||
|
|
169
182
|
!credentials.clientid ||
|
|
170
183
|
!credentials.clientsecret ||
|
|
171
184
|
!credentials.url) {
|
|
172
|
-
|
|
173
|
-
return; // Don't throw error, just skip OAuth proxy
|
|
185
|
+
throw new Error("Invalid security credentials");
|
|
174
186
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// LOGGER.debug("OAuth proxy: verifyAccessToken called");
|
|
188
|
-
//
|
|
189
|
-
// // Use CAP's built-in JWT verification for XSUAA
|
|
190
|
-
// const decoded = await cds.auth.jwt.verify(token);
|
|
191
|
-
// LOGGER.debug(
|
|
192
|
-
// "Token decoded successfully for client:",
|
|
193
|
-
// decoded.client_id || decoded.azp,
|
|
194
|
-
// );
|
|
195
|
-
//
|
|
196
|
-
// return {
|
|
197
|
-
// token,
|
|
198
|
-
// clientId: decoded.client_id || decoded.azp,
|
|
199
|
-
// scopes: decoded.scope?.split(" ") || [],
|
|
200
|
-
// userId: decoded.sub,
|
|
201
|
-
// expiresAt: decoded.exp, // Unix timestamp, not Date object
|
|
202
|
-
// };
|
|
203
|
-
// } catch (error) {
|
|
204
|
-
// LOGGER.error("Token verification failed:", error);
|
|
205
|
-
// throw new Error("Invalid access token");
|
|
206
|
-
// }
|
|
207
|
-
// },
|
|
208
|
-
// getClient: async (client_id: string) => {
|
|
209
|
-
// LOGGER.debug("OAuth proxy: Dynamic client registration requested");
|
|
210
|
-
//
|
|
211
|
-
// return {
|
|
212
|
-
// client_secret: credentials.clientsecret as string,
|
|
213
|
-
// client_id,
|
|
214
|
-
// redirect_uris: [
|
|
215
|
-
// `${baseUrl}/oauth/callback`,
|
|
216
|
-
// `${baseUrl}/mcp/oauth/callback`,
|
|
217
|
-
// "http://localhost:3000/callback", // Claude Desktop default
|
|
218
|
-
// "http://localhost:3000/auth/callback", // Alternative format
|
|
219
|
-
// ],
|
|
220
|
-
// };
|
|
221
|
-
// },
|
|
222
|
-
// });
|
|
223
|
-
expressApp.use((0, router_js_1.mcpAuthMetadataRouter)({
|
|
224
|
-
oauthMetadata: {
|
|
225
|
-
issuer: credentials.url,
|
|
226
|
-
authorization_endpoint: `${credentials.url}/oauth/authorize`,
|
|
227
|
-
token_endpoint: `${credentials.url}/oauth/token`,
|
|
228
|
-
response_types_supported: ["code", "token"],
|
|
229
|
-
grant_types_supported: [
|
|
230
|
-
"authorization_code",
|
|
231
|
-
"client_credentials",
|
|
232
|
-
"urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
233
|
-
"refresh_token"
|
|
234
|
-
],
|
|
235
|
-
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
|
|
236
|
-
code_challenge_methods_supported: ["S256"]
|
|
187
|
+
const proxyProvider = new proxyProvider_js_1.ProxyOAuthServerProvider({
|
|
188
|
+
endpoints: {
|
|
189
|
+
authorizationUrl: `${credentials.url}/oauth/authorize`,
|
|
190
|
+
tokenUrl: `${credentials.url}/oauth/token`,
|
|
191
|
+
revocationUrl: `${credentials.url}/oauth/revoke`,
|
|
192
|
+
},
|
|
193
|
+
verifyAccessToken: async (token) => {
|
|
194
|
+
return {
|
|
195
|
+
token,
|
|
196
|
+
clientId: credentials.clientid,
|
|
197
|
+
scopes: ["uaa.resource"],
|
|
198
|
+
};
|
|
237
199
|
},
|
|
200
|
+
getClient: async (client_id) => {
|
|
201
|
+
return {
|
|
202
|
+
client_secret: credentials.clientsecret,
|
|
203
|
+
client_id,
|
|
204
|
+
redirect_uris: ["http://localhost:3000/callback"], // Temporary value for now
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
expressApp.use((0, router_js_1.mcpAuthRouter)({
|
|
209
|
+
provider: proxyProvider,
|
|
210
|
+
issuerUrl: new URL(credentials.url),
|
|
211
|
+
//baseUrl: new URL(""), // I have left this out for the time being due to the defaulting to issuer
|
|
238
212
|
serviceDocumentationUrl: new URL("https://docs.cloudfoundry.org/api/uaa/version/77.34.0/index.html#authorization"),
|
|
239
|
-
resourceServerUrl: new URL(baseUrl),
|
|
240
213
|
}));
|
|
241
|
-
logger_1.LOGGER.info("OAuth proxy configured successfully for XSUAA integration");
|
|
242
214
|
}
|
|
243
215
|
/**
|
|
244
216
|
* Checks whether the requesting user's access matches that of the roles required
|
|
@@ -61,7 +61,11 @@ function registerDescribeModelTool(server) {
|
|
|
61
61
|
}));
|
|
62
62
|
const keys = elements.filter((e) => e.key).map((e) => e.name);
|
|
63
63
|
const sampleTop = 5;
|
|
64
|
-
|
|
64
|
+
// Prefer scalar fields for sample selects; exclude associations
|
|
65
|
+
const scalarFields = elements
|
|
66
|
+
.filter((e) => String(e.type).toLowerCase() !== "cds.association")
|
|
67
|
+
.map((e) => e.name);
|
|
68
|
+
const shortFields = scalarFields.slice(0, 5);
|
|
65
69
|
// Match wrapper tool naming: Service_Entity_mode
|
|
66
70
|
const entName = String(ent?.name || "entity");
|
|
67
71
|
const svcPart = service || entName.split(".")[0] || "Service";
|
|
@@ -75,7 +79,7 @@ function registerDescribeModelTool(server) {
|
|
|
75
79
|
fields: elements,
|
|
76
80
|
usage: {
|
|
77
81
|
rationale: "Entity wrapper tools expose CRUD-like operations for LLMs. Prefer query/get globally; create/update must be explicitly enabled by the developer.",
|
|
78
|
-
guidance: "Use the *_query tool for retrieval with filters and projections
|
|
82
|
+
guidance: "Use the *_query tool for retrieval with filters and projections. All fields in select/where are consistent. For associations, use foreign key fields (e.g., author_ID not author). Use *_get with keys for a single record; use *_create/*_update only if enabled and necessary.",
|
|
79
83
|
},
|
|
80
84
|
examples: {
|
|
81
85
|
list_tool: listName,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isElicitInput = isElicitInput;
|
|
4
|
+
exports.constructElicitationFunctions = constructElicitationFunctions;
|
|
5
|
+
exports.handleElicitationRequests = handleElicitationRequests;
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
/**
|
|
8
|
+
* Message displayed to users when requesting input parameters
|
|
9
|
+
*/
|
|
10
|
+
const INPUT_MSG = "Please fill out the required parameters";
|
|
11
|
+
/**
|
|
12
|
+
* Checks if the elicited input array contains an 'input' requirement
|
|
13
|
+
* @param elicits - Array of elicit types or undefined
|
|
14
|
+
* @returns True if 'input' elicitation is required, false otherwise
|
|
15
|
+
*/
|
|
16
|
+
function isElicitInput(elicits) {
|
|
17
|
+
return elicits ? elicits.includes("input") : false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Constructs elicitation request parameters based on the tool annotation's elicit requirements
|
|
21
|
+
* @param model - MCP tool annotation containing elicit configuration
|
|
22
|
+
* @param params - Parameter definitions for the tool
|
|
23
|
+
* @returns Array of elicit request parameters for MCP server
|
|
24
|
+
* @throws Error if invalid elicitation type is encountered
|
|
25
|
+
*/
|
|
26
|
+
function constructElicitationFunctions(model, params) {
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const el of model.elicits ?? []) {
|
|
29
|
+
switch (el) {
|
|
30
|
+
case "input":
|
|
31
|
+
result.push(constructElicitInput(params));
|
|
32
|
+
continue;
|
|
33
|
+
case "confirm":
|
|
34
|
+
result.push(contructElicitConfirm(model));
|
|
35
|
+
continue;
|
|
36
|
+
default:
|
|
37
|
+
throw new Error("Invalid elicitation type");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Processes multiple elicitation requests sequentially and handles user responses
|
|
44
|
+
* @param requests - Array of elicit request parameters or undefined
|
|
45
|
+
* @param server - MCP server instance for making elicit calls
|
|
46
|
+
* @returns Promise resolving to elicitation response with early exit or data
|
|
47
|
+
*/
|
|
48
|
+
async function handleElicitationRequests(requests, server) {
|
|
49
|
+
if (!requests || requests.length <= 0) {
|
|
50
|
+
return { earlyResponse: undefined };
|
|
51
|
+
}
|
|
52
|
+
let data = undefined;
|
|
53
|
+
for (const req of requests) {
|
|
54
|
+
const res = await server.server.elicitInput(req);
|
|
55
|
+
const earlyResponse = handleElicitResponse(res);
|
|
56
|
+
if (earlyResponse) {
|
|
57
|
+
return { earlyResponse };
|
|
58
|
+
}
|
|
59
|
+
if (req.message === INPUT_MSG) {
|
|
60
|
+
data = res.content;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
earlyResponse: undefined,
|
|
65
|
+
data,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Converts elicit response action into appropriate MCP result
|
|
70
|
+
* @param elicitResponse - Result from MCP server elicit input call
|
|
71
|
+
* @returns MCP result for decline/cancel actions, undefined for accept
|
|
72
|
+
* @throws Error if invalid response action is received
|
|
73
|
+
*/
|
|
74
|
+
function handleElicitResponse(elicitResponse) {
|
|
75
|
+
switch (elicitResponse.action) {
|
|
76
|
+
case "accept":
|
|
77
|
+
return undefined;
|
|
78
|
+
case "decline":
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: "Action was declined.",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
case "cancel":
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: "Action was cancelled",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
default:
|
|
97
|
+
throw new Error("Invalid elicit response received");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Determines the schema type for elicit input based on Zod parameter type
|
|
102
|
+
* @param param - Zod schema parameter to analyze
|
|
103
|
+
* @returns Corresponding elicit schema type string
|
|
104
|
+
* @throws Error if parameter type is not supported for elicitation
|
|
105
|
+
*/
|
|
106
|
+
function determineSchemaType(param) {
|
|
107
|
+
if (param instanceof zod_1.z.ZodBoolean) {
|
|
108
|
+
return "boolean";
|
|
109
|
+
}
|
|
110
|
+
else if (param instanceof zod_1.z.ZodString) {
|
|
111
|
+
return "string";
|
|
112
|
+
}
|
|
113
|
+
else if (param instanceof zod_1.z.ZodNumber) {
|
|
114
|
+
return "number";
|
|
115
|
+
}
|
|
116
|
+
throw new Error("Unsupported elicitation input type");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Constructs confirmation elicit request for tool execution
|
|
120
|
+
* @param model - MCP annotation model containing tool description
|
|
121
|
+
* @returns Elicit request parameters for user confirmation
|
|
122
|
+
*/
|
|
123
|
+
function contructElicitConfirm(model) {
|
|
124
|
+
return {
|
|
125
|
+
message: `Please confirm that you want to perform action '${model.description}'`,
|
|
126
|
+
requestedSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
confirm: {
|
|
130
|
+
type: "boolean",
|
|
131
|
+
title: "Confirmation",
|
|
132
|
+
description: "Please confirm the action",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ["confirm"],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Constructs input elicit request for tool parameters
|
|
141
|
+
* @param params - Tool parameters definition with Zod schemas
|
|
142
|
+
* @returns Elicit request parameters for user input collection
|
|
143
|
+
*/
|
|
144
|
+
function constructElicitInput(params) {
|
|
145
|
+
const elicitSpec = {
|
|
146
|
+
message: INPUT_MSG,
|
|
147
|
+
requestedSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {},
|
|
150
|
+
required: [],
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
for (const [key, zodType] of Object.entries(params)) {
|
|
154
|
+
elicitSpec.requestedSchema.required?.push(key);
|
|
155
|
+
elicitSpec.requestedSchema.properties[key] = {
|
|
156
|
+
type: determineSchemaType(zodType),
|
|
157
|
+
title: key,
|
|
158
|
+
description: key,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return elicitSpec;
|
|
162
|
+
}
|