@cap-js/ord 1.3.14 → 1.4.1
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 +98 -13
- package/cds-plugin.js +0 -6
- package/lib/access-strategies.js +172 -0
- package/lib/auth/authentication.js +351 -0
- package/lib/auth/cf-mtls.js +516 -0
- package/lib/auth/mtls-endpoint-service.js +141 -0
- package/lib/build.js +7 -10
- package/lib/constants.js +45 -0
- package/lib/defaults.js +14 -10
- package/lib/extendOrdWithCustom.js +39 -7
- package/lib/index.js +8 -5
- package/lib/logger.js +3 -12
- package/lib/mcpAdapter.js +132 -0
- package/lib/metaData.js +26 -5
- package/lib/ord-service.js +20 -6
- package/lib/ord.js +35 -4
- package/lib/templates.js +128 -16
- package/package.json +11 -8
- package/lib/authentication.js +0 -153
package/lib/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const AUTHENTICATION_TYPE = Object.freeze({
|
|
2
2
|
Open: "open",
|
|
3
3
|
Basic: "basic",
|
|
4
|
+
CfMtls: "cf-mtls",
|
|
4
5
|
});
|
|
5
6
|
|
|
6
7
|
const BASIC_AUTH_HEADER_KEY = "authorization";
|
|
@@ -15,11 +16,24 @@ const BLOCKED_SERVICE_NAME = Object.freeze({
|
|
|
15
16
|
const ORD_ACCESS_STRATEGY = Object.freeze({
|
|
16
17
|
Open: "open",
|
|
17
18
|
Basic: "basic-auth",
|
|
19
|
+
CfMtls: "sap:cmp-mtls:v1",
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({
|
|
21
23
|
[AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open,
|
|
22
24
|
[AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic,
|
|
25
|
+
[AUTHENTICATION_TYPE.CfMtls]: ORD_ACCESS_STRATEGY.CfMtls,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// CF mTLS default header names
|
|
29
|
+
const CF_MTLS_HEADERS = Object.freeze({
|
|
30
|
+
ISSUER: "x-ssl-client-issuer-dn",
|
|
31
|
+
SUBJECT: "x-ssl-client-subject-dn",
|
|
32
|
+
ROOT_CA: "x-ssl-client-root-ca-dn",
|
|
33
|
+
// XFCC (X-Forwarded-Client-Cert) headers for proxy-verified mTLS
|
|
34
|
+
XFCC: "x-forwarded-client-cert",
|
|
35
|
+
CLIENT: "x-ssl-client",
|
|
36
|
+
CLIENT_VERIFY: "x-ssl-client-verify",
|
|
23
37
|
});
|
|
24
38
|
|
|
25
39
|
const CDS_ELEMENT_KIND = Object.freeze({
|
|
@@ -38,6 +52,7 @@ const COMPILER_TYPES = Object.freeze({
|
|
|
38
52
|
asyncapi2: "asyncapi2",
|
|
39
53
|
edmx: "edmx",
|
|
40
54
|
csn: "csn",
|
|
55
|
+
mcp: "mcp",
|
|
41
56
|
});
|
|
42
57
|
|
|
43
58
|
const CONTENT_MERGE_KEY = "ordId";
|
|
@@ -98,6 +113,31 @@ const SHORT_DESCRIPTION_PREFIX = "Short description of ";
|
|
|
98
113
|
const SEM_VERSION_REGEX =
|
|
99
114
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
100
115
|
|
|
116
|
+
const MCP_CUSTOM_TYPE = "sap:mcp-server-card:v0";
|
|
117
|
+
|
|
118
|
+
// CF mTLS Error Reasons
|
|
119
|
+
const CF_MTLS_ERROR_REASON = Object.freeze({
|
|
120
|
+
NO_HEADERS: "NO_HEADERS",
|
|
121
|
+
HEADER_MISSING: "HEADER_MISSING",
|
|
122
|
+
INVALID_ENCODING: "INVALID_ENCODING",
|
|
123
|
+
XFCC_VERIFICATION_FAILED: "XFCC_VERIFICATION_FAILED",
|
|
124
|
+
CERT_PAIR_MISMATCH: "CERT_PAIR_MISMATCH",
|
|
125
|
+
ROOT_CA_MISMATCH: "ROOT_CA_MISMATCH",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// HTTP Configuration Constants
|
|
129
|
+
const HTTP_CONFIG = Object.freeze({
|
|
130
|
+
METHOD_GET: "GET",
|
|
131
|
+
CONTENT_TYPE_JSON: "application/json",
|
|
132
|
+
MTLS_TIMEOUT_MS: 10000,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Authentication String Constants
|
|
136
|
+
const AUTH_STRINGS = Object.freeze({
|
|
137
|
+
BASIC_PREFIX: "Basic ",
|
|
138
|
+
WWW_AUTHENTICATE_REALM: 'Basic realm="401"',
|
|
139
|
+
});
|
|
140
|
+
|
|
101
141
|
module.exports = {
|
|
102
142
|
AUTHENTICATION_TYPE,
|
|
103
143
|
AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
|
|
@@ -105,6 +145,7 @@ module.exports = {
|
|
|
105
145
|
BUILD_DEFAULT_PATH,
|
|
106
146
|
BLOCKED_SERVICE_NAME,
|
|
107
147
|
CDS_ELEMENT_KIND,
|
|
148
|
+
CF_MTLS_HEADERS,
|
|
108
149
|
COMPILER_TYPES,
|
|
109
150
|
CONTENT_MERGE_KEY,
|
|
110
151
|
DATA_PRODUCT_ANNOTATION,
|
|
@@ -113,6 +154,7 @@ module.exports = {
|
|
|
113
154
|
DESCRIPTION_PREFIX,
|
|
114
155
|
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
115
156
|
LEVEL,
|
|
157
|
+
MCP_CUSTOM_TYPE,
|
|
116
158
|
OPEN_RESOURCE_DISCOVERY_VERSION,
|
|
117
159
|
ORD_ACCESS_STRATEGY,
|
|
118
160
|
ORD_DOCUMENT_FILE_NAME,
|
|
@@ -127,4 +169,7 @@ module.exports = {
|
|
|
127
169
|
SUPPORTED_IMPLEMENTATIONSTANDARD_VERSIONS,
|
|
128
170
|
SHORT_DESCRIPTION_PREFIX,
|
|
129
171
|
SEM_VERSION_REGEX,
|
|
172
|
+
CF_MTLS_ERROR_REASON,
|
|
173
|
+
HTTP_CONFIG,
|
|
174
|
+
AUTH_STRINGS,
|
|
130
175
|
};
|
package/lib/defaults.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { OPEN_RESOURCE_DISCOVERY_VERSION, SHORT_DESCRIPTION_PREFIX, RESOURCE_VISIBILITY } = require("./constants");
|
|
2
2
|
const { hasSAPPolicyLevel } = require("./utils");
|
|
3
|
-
const { getAuthConfig } = require("./authentication");
|
|
4
3
|
const _ = require("lodash");
|
|
5
4
|
|
|
6
5
|
const packageTypes = [
|
|
@@ -139,14 +138,19 @@ module.exports = {
|
|
|
139
138
|
apiResources: [],
|
|
140
139
|
eventResources: [],
|
|
141
140
|
entityTypes: [],
|
|
142
|
-
baseTemplate: {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
141
|
+
baseTemplate: (authConfig) => {
|
|
142
|
+
// Get access strategies from the provided authConfig
|
|
143
|
+
// If auth config is not available, fall back to empty array
|
|
144
|
+
const accessStrategies = authConfig?.accessStrategies || [];
|
|
145
|
+
return {
|
|
146
|
+
openResourceDiscoveryV1: {
|
|
147
|
+
documents: [
|
|
148
|
+
{
|
|
149
|
+
url: "/ord/v1/documents/ord-document",
|
|
150
|
+
accessStrategies,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
151
155
|
},
|
|
152
156
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { CONTENT_MERGE_KEY } = require("./constants");
|
|
2
2
|
const cds = require("@sap/cds");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const
|
|
4
|
+
const Logger = require("./logger");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const _ = require("lodash");
|
|
7
7
|
|
|
@@ -19,13 +19,44 @@ function cleanNullProperties(obj) {
|
|
|
19
19
|
function patchGeneratedOrdResources(destinationObj, sourceObj) {
|
|
20
20
|
const destObj = _.keyBy(destinationObj, CONTENT_MERGE_KEY);
|
|
21
21
|
const srcObj = _.keyBy(sourceObj, CONTENT_MERGE_KEY);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
// Helper: extract MCP merge logic for clarity & reuse
|
|
24
|
+
const mergeMCPResource = (ordId, existingKey) => {
|
|
25
|
+
const baseKey = ordId in destObj ? ordId : existingKey;
|
|
26
|
+
if (baseKey) {
|
|
27
|
+
const base = structuredClone(destObj[baseKey]);
|
|
28
|
+
const override = structuredClone(srcObj[ordId]);
|
|
29
|
+
const merged = {
|
|
30
|
+
...base, // preserve generated structural fields
|
|
31
|
+
...override, // overlay custom changes
|
|
32
|
+
apiProtocol: "mcp", // enforce protocol
|
|
33
|
+
partOfPackage: base.partOfPackage || override.partOfPackage,
|
|
34
|
+
resourceDefinitions: base.resourceDefinitions || override.resourceDefinitions,
|
|
35
|
+
};
|
|
36
|
+
if (baseKey !== ordId) delete destObj[baseKey];
|
|
37
|
+
destObj[ordId] = merged;
|
|
25
38
|
} else {
|
|
26
|
-
|
|
39
|
+
// MCP plugin not available: do not introduce brand-new MCP resource purely from custom file
|
|
40
|
+
// This preserves contract: MCP resource only exists when plugin is available.
|
|
41
|
+
// Silently skip or log (warn) to avoid test noise.
|
|
42
|
+
// Logger.warn could be added here if desired.
|
|
43
|
+
return; // skip adding
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const existingMCPKey = Object.keys(destObj).find((k) => destObj[k]?.apiProtocol === "mcp");
|
|
48
|
+
|
|
49
|
+
for (const ordId in srcObj) {
|
|
50
|
+
if (ordId.endsWith(":apiResource:mcp-server:v1")) {
|
|
51
|
+
mergeMCPResource(ordId, existingMCPKey);
|
|
52
|
+
continue;
|
|
27
53
|
}
|
|
54
|
+
destObj[ordId] =
|
|
55
|
+
ordId in destObj
|
|
56
|
+
? _.assignWith(structuredClone(destObj[ordId]), structuredClone(srcObj[ordId]))
|
|
57
|
+
: srcObj[ordId];
|
|
28
58
|
}
|
|
59
|
+
|
|
29
60
|
return cleanNullProperties(Object.values(destObj));
|
|
30
61
|
}
|
|
31
62
|
|
|
@@ -54,10 +85,11 @@ function compareAndHandleCustomORDContentWithExistingContent(ordContent, customO
|
|
|
54
85
|
function getCustomORDContent(appConfig) {
|
|
55
86
|
if (!appConfig.env?.customOrdContentFile) return;
|
|
56
87
|
const pathToCustomORDContent = path.join(cds.root, appConfig.env?.customOrdContentFile);
|
|
57
|
-
if (fs.existsSync(pathToCustomORDContent)) {
|
|
88
|
+
if (!fs.existsSync(pathToCustomORDContent)) {
|
|
58
89
|
Logger.error("Custom ORD content file not found at", pathToCustomORDContent);
|
|
59
|
-
return
|
|
90
|
+
return;
|
|
60
91
|
}
|
|
92
|
+
return require(pathToCustomORDContent);
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
function extendCustomORDContentIfExists(appConfig, ordContent) {
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API entry point for @cap-js/ord library
|
|
3
|
+
* Exposes core functionality for ORD document generation and metadata retrieval
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const { ord, getMetadata } = require('@cap-js/ord/lib');
|
|
7
|
+
*/
|
|
1
8
|
module.exports = {
|
|
2
|
-
defaults: require("./defaults.js"),
|
|
3
|
-
getMetadata: require("./metaData.js"),
|
|
4
9
|
ord: require("./ord.js"),
|
|
5
|
-
|
|
6
|
-
constants: require("./constants.js"),
|
|
7
|
-
authentication: require("./authentication.js"),
|
|
10
|
+
getMetadata: require("./metaData.js"),
|
|
8
11
|
};
|
package/lib/logger.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
const cds = require("@sap/cds");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function _getLogLevel(debugLevel, logLevels) {
|
|
6
|
-
return debugLevel ? logLevels.DEBUG : logLevels.WARN;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const level = _getLogLevel(_debugLevel, cds.log?.levels);
|
|
10
|
-
|
|
3
|
+
// Unified INFO-level logging - simple and consistent across all environments
|
|
11
4
|
const Logger = cds.log("ord-plugin", {
|
|
12
|
-
level,
|
|
5
|
+
level: cds.log?.levels?.INFO,
|
|
13
6
|
});
|
|
14
7
|
|
|
15
|
-
module.exports =
|
|
16
|
-
Logger,
|
|
17
|
-
};
|
|
8
|
+
module.exports = Logger;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const Logger = require("./logger");
|
|
2
|
+
const cds = require("@sap/cds");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP Plugin Adapter
|
|
7
|
+
* Provides an abstraction layer for interacting with @btp-ai/mcp-plugin
|
|
8
|
+
* This allows for easy mocking and testing without requiring the actual plugin
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load package.json from project root
|
|
13
|
+
* @returns {Object} Package.json content
|
|
14
|
+
* @throws {Error} If package.json is not found
|
|
15
|
+
*/
|
|
16
|
+
function _loadPackageJson() {
|
|
17
|
+
const packageJsonPath = path.join(cds.root, "package.json");
|
|
18
|
+
if (!cds.utils.exists(packageJsonPath)) {
|
|
19
|
+
throw new Error(`package.json not found in the project root directory`);
|
|
20
|
+
}
|
|
21
|
+
return require(packageJsonPath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if MCP plugin is listed in package.json dependencies
|
|
26
|
+
* @param {Function} loadPackageJsonFn - Optional function to load package.json (for testing)
|
|
27
|
+
* @returns {boolean} True if plugin is in dependencies or devDependencies
|
|
28
|
+
*/
|
|
29
|
+
function isMCPPluginInPackageJson(loadPackageJsonFn = _loadPackageJson) {
|
|
30
|
+
try {
|
|
31
|
+
const packageJson = loadPackageJsonFn();
|
|
32
|
+
const allDependencies = {
|
|
33
|
+
...(packageJson.dependencies || {}),
|
|
34
|
+
...(packageJson.devDependencies || {}),
|
|
35
|
+
};
|
|
36
|
+
const isInstalled = "@btp-ai/mcp-plugin" in allDependencies;
|
|
37
|
+
if (isInstalled) {
|
|
38
|
+
Logger.log("MCP plugin found in package.json dependencies");
|
|
39
|
+
}
|
|
40
|
+
return isInstalled;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
Logger.error("Could not check package.json for MCP plugin:", error.message);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if MCP plugin is available at runtime
|
|
49
|
+
* @param {Function} resolveFunction - Optional custom resolve function for testing
|
|
50
|
+
* @returns {boolean} True if plugin is available
|
|
51
|
+
*/
|
|
52
|
+
function isMCPPluginAvailable(resolveFunction = require.resolve) {
|
|
53
|
+
try {
|
|
54
|
+
resolveFunction("@btp-ai/mcp-plugin");
|
|
55
|
+
Logger.log("MCP plugin is available at runtime");
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get MCP plugin instance
|
|
64
|
+
* Returns null if plugin is not available
|
|
65
|
+
* @returns {Object|null} MCP plugin module or null
|
|
66
|
+
*/
|
|
67
|
+
function getMcpPlugin() {
|
|
68
|
+
if (!isMCPPluginAvailable()) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return require("@btp-ai/mcp-plugin");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
Logger.error("Failed to load MCP plugin:", error.message);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if MCP plugin is ready for use (both installed and available)
|
|
82
|
+
* This function combines the logic of both isMCPPluginInPackageJson and isMCPPluginAvailable
|
|
83
|
+
* to provide a comprehensive check for MCP plugin readiness.
|
|
84
|
+
* Maintains the original AND logic: both conditions must be true.
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} options - Options for checking
|
|
87
|
+
* @param {Function} options.resolveFunction - Custom resolve function for testing
|
|
88
|
+
* @param {Function} options.loadPackageJsonFn - Custom package.json loader for testing
|
|
89
|
+
* @returns {boolean} True if plugin is ready for use
|
|
90
|
+
*/
|
|
91
|
+
function isMCPPluginReady(options = {}) {
|
|
92
|
+
const { resolveFunction, loadPackageJsonFn } = options;
|
|
93
|
+
|
|
94
|
+
// Both conditions must be satisfied: declared in package.json AND available at runtime
|
|
95
|
+
return isMCPPluginInPackageJson(loadPackageJsonFn) && isMCPPluginAvailable(resolveFunction);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build MCP server definition
|
|
100
|
+
* @param {Array} services - Array of CDS services
|
|
101
|
+
* @returns {Promise<Object>} MCP server definition
|
|
102
|
+
* @throws {Error} If MCP plugin is not available
|
|
103
|
+
*/
|
|
104
|
+
async function buildMcpServerDefinition(services = []) {
|
|
105
|
+
const plugin = getMcpPlugin();
|
|
106
|
+
|
|
107
|
+
if (!plugin) {
|
|
108
|
+
throw new Error("MCP plugin not available");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Validate services parameter
|
|
112
|
+
if (!Array.isArray(services)) {
|
|
113
|
+
throw new Error("Services parameter must be an array");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const { exposeMcpServerDefinitionForOrd } = require("@btp-ai/mcp-plugin/lib/utils/metadata");
|
|
118
|
+
return await exposeMcpServerDefinitionForOrd(services);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
Logger.error("Failed to build MCP server definition:", error.message);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
isMCPPluginAvailable,
|
|
127
|
+
isMCPPluginInPackageJson,
|
|
128
|
+
isMCPPluginReady,
|
|
129
|
+
getMcpPlugin,
|
|
130
|
+
buildMcpServerDefinition,
|
|
131
|
+
_loadPackageJson, // Export for testing
|
|
132
|
+
};
|
package/lib/metaData.js
CHANGED
|
@@ -2,11 +2,12 @@ const cds = require("@sap/cds/lib");
|
|
|
2
2
|
const { compile: openapi } = require("@cap-js/openapi");
|
|
3
3
|
const { compile: asyncapi } = require("@cap-js/asyncapi");
|
|
4
4
|
const { COMPILER_TYPES } = require("./constants");
|
|
5
|
-
const
|
|
5
|
+
const Logger = require("./logger");
|
|
6
6
|
const { interopCSN } = require("./interopCsn.js");
|
|
7
7
|
const cdsc = require("@sap/cds-compiler/lib/main");
|
|
8
|
+
const { isMCPPluginReady, buildMcpServerDefinition } = require("./mcpAdapter");
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
const getMetadata = async (url, model = null) => {
|
|
10
11
|
const parts = url
|
|
11
12
|
?.split("/")
|
|
12
13
|
.pop()
|
|
@@ -15,13 +16,14 @@ module.exports = async (url, model = null) => {
|
|
|
15
16
|
const compilerType = parts.pop();
|
|
16
17
|
const serviceName = parts.join(".");
|
|
17
18
|
const csn = model || cds.services[serviceName]?.model;
|
|
19
|
+
const compileOptions = cds.env["ord"]?.compileOptions || {};
|
|
18
20
|
|
|
19
21
|
let responseFile;
|
|
20
22
|
const options = { service: serviceName, as: "str", messages: [] };
|
|
21
23
|
switch (compilerType) {
|
|
22
24
|
case COMPILER_TYPES.oas3:
|
|
23
25
|
try {
|
|
24
|
-
responseFile = openapi(csn, options);
|
|
26
|
+
responseFile = openapi(csn, { ...options, ...(compileOptions?.openapi || {}) });
|
|
25
27
|
} catch (error) {
|
|
26
28
|
Logger.error("OpenApi error:", error.message);
|
|
27
29
|
throw error;
|
|
@@ -29,7 +31,7 @@ module.exports = async (url, model = null) => {
|
|
|
29
31
|
break;
|
|
30
32
|
case COMPILER_TYPES.asyncapi2:
|
|
31
33
|
try {
|
|
32
|
-
responseFile = asyncapi(csn, options);
|
|
34
|
+
responseFile = asyncapi(csn, { ...options, ...(compileOptions?.asyncapi || {}) });
|
|
33
35
|
} catch (error) {
|
|
34
36
|
Logger.error("AsyncApi error:", error.message);
|
|
35
37
|
throw error;
|
|
@@ -47,14 +49,33 @@ module.exports = async (url, model = null) => {
|
|
|
47
49
|
break;
|
|
48
50
|
case COMPILER_TYPES.edmx:
|
|
49
51
|
try {
|
|
50
|
-
responseFile = await cds.compile(csn).to["edmx"](options);
|
|
52
|
+
responseFile = await cds.compile(csn).to["edmx"]({ ...options, ...(compileOptions?.edmx || {}) });
|
|
51
53
|
} catch (error) {
|
|
52
54
|
Logger.error("Edmx error:", error.message);
|
|
53
55
|
throw error;
|
|
54
56
|
}
|
|
57
|
+
break;
|
|
58
|
+
case COMPILER_TYPES.mcp:
|
|
59
|
+
if (!isMCPPluginReady()) {
|
|
60
|
+
throw new Error("MCP plugin is not available or not ready for use");
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
// Get all available CDS services
|
|
64
|
+
const allServices = Object.values(cds.services);
|
|
65
|
+
// Generate metadata from runtime services using adapter
|
|
66
|
+
const mcpResult = await buildMcpServerDefinition(allServices);
|
|
67
|
+
// Extract only the MCP content, not the ORD metadata
|
|
68
|
+
responseFile = mcpResult.mcp;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
Logger.error("MCP server definition error:", error.message);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
55
74
|
}
|
|
56
75
|
return {
|
|
57
76
|
contentType: `application/${compilerType === "edmx" ? "xml" : "json"}`,
|
|
58
77
|
response: responseFile,
|
|
59
78
|
};
|
|
60
79
|
};
|
|
80
|
+
|
|
81
|
+
module.exports = getMetadata;
|
package/lib/ord-service.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
const cds = require("@sap/cds");
|
|
2
|
-
const
|
|
2
|
+
const ord = require("./ord.js");
|
|
3
|
+
const getMetadata = require("./metaData.js");
|
|
4
|
+
const defaults = require("./defaults.js");
|
|
5
|
+
const { createAuthConfig, createAuthMiddleware } = require("./auth/authentication.js");
|
|
6
|
+
const Logger = require("./logger.js");
|
|
3
7
|
|
|
4
8
|
class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
5
|
-
init() {
|
|
9
|
+
async init() {
|
|
10
|
+
// Initialize authentication configuration from .cdsrc.json or environment variables
|
|
11
|
+
// CF mTLS validator is lazily initialized on first mTLS request
|
|
12
|
+
const authConfig = createAuthConfig();
|
|
13
|
+
if (authConfig.error) {
|
|
14
|
+
throw new Error(`Authentication initialization failed: ${authConfig.error}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Create authentication middleware
|
|
18
|
+
const authMiddleware = createAuthMiddleware(authConfig);
|
|
19
|
+
|
|
6
20
|
cds.app.get(`${this.path}`, (_, res) => {
|
|
7
|
-
return res.status(200).send(defaults.baseTemplate);
|
|
21
|
+
return res.status(200).send(defaults.baseTemplate(authConfig));
|
|
8
22
|
});
|
|
9
23
|
|
|
10
|
-
cds.app.get(`/ord/v1/documents/ord-document`,
|
|
24
|
+
cds.app.get(`/ord/v1/documents/ord-document`, authMiddleware, async (_, res) => {
|
|
11
25
|
const csn = cds.context?.model || cds.model;
|
|
12
26
|
const data = ord(csn);
|
|
13
27
|
return res.status(200).send(data);
|
|
14
28
|
});
|
|
15
29
|
|
|
16
|
-
cds.app.get(`/ord/v1/documents/:id`,
|
|
30
|
+
cds.app.get(`/ord/v1/documents/:id`, authMiddleware, async (_, res) => {
|
|
17
31
|
return res.status(404).send("404 Not Found");
|
|
18
32
|
});
|
|
19
33
|
|
|
20
|
-
cds.app.get(`/ord/v1/:ordId?/:service?`,
|
|
34
|
+
cds.app.get(`/ord/v1/:ordId?/:service?`, authMiddleware, async (req, res) => {
|
|
21
35
|
try {
|
|
22
36
|
const { contentType, response } = await getMetadata(req.url);
|
|
23
37
|
return res.status(200).contentType(contentType).send(response);
|
package/lib/ord.js
CHANGED
|
@@ -11,18 +11,38 @@ const {
|
|
|
11
11
|
createEntityTypeMappingsItemTemplate,
|
|
12
12
|
createEventResourceTemplate,
|
|
13
13
|
createGroupsTemplateForService,
|
|
14
|
+
createMCPAPIResourceTemplate,
|
|
14
15
|
_propagateORDVisibility,
|
|
15
16
|
} = require("./templates");
|
|
16
17
|
const { extendCustomORDContentIfExists } = require("./extendOrdWithCustom");
|
|
17
18
|
const { getRFC3339Date } = require("./date");
|
|
18
|
-
const {
|
|
19
|
+
const { createAuthConfig } = require("./auth/authentication");
|
|
20
|
+
const { isMCPPluginReady } = require("./mcpAdapter");
|
|
19
21
|
|
|
20
|
-
const
|
|
22
|
+
const Logger = require("./logger");
|
|
21
23
|
const _ = require("lodash");
|
|
22
24
|
const cds = require("@sap/cds");
|
|
23
25
|
const defaults = require("./defaults");
|
|
24
26
|
const path = require("path");
|
|
25
27
|
|
|
28
|
+
const _addMCPResourceIfAvailable = (apiResources, appConfig, packageIds, accessStrategies) => {
|
|
29
|
+
// Use comprehensive check for MCP plugin readiness
|
|
30
|
+
const shouldAddMCP = isMCPPluginReady();
|
|
31
|
+
if (shouldAddMCP) {
|
|
32
|
+
try {
|
|
33
|
+
const mcpResources = createMCPAPIResourceTemplate(appConfig, packageIds, accessStrategies);
|
|
34
|
+
// Handle both array and single object responses from MCP plugin
|
|
35
|
+
if (Array.isArray(mcpResources)) {
|
|
36
|
+
apiResources.push(...mcpResources);
|
|
37
|
+
} else if (mcpResources) {
|
|
38
|
+
apiResources.push(mcpResources);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
Logger.warn("Failed to create MCP API resource:", error.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
26
46
|
const initializeAppConfig = (csn) => {
|
|
27
47
|
const packageJson = _loadPackageJson();
|
|
28
48
|
const packageName = packageJson.name;
|
|
@@ -230,7 +250,7 @@ const _getEntityTypes = (appConfig, packageIds) => {
|
|
|
230
250
|
};
|
|
231
251
|
|
|
232
252
|
const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
233
|
-
|
|
253
|
+
const apiResources = appConfig.apiResourceNames.flatMap((apiResourceName) =>
|
|
234
254
|
createAPIResourceTemplate(
|
|
235
255
|
apiResourceName,
|
|
236
256
|
csn.definitions[apiResourceName],
|
|
@@ -239,6 +259,11 @@ const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
|
239
259
|
accessStrategies,
|
|
240
260
|
),
|
|
241
261
|
);
|
|
262
|
+
|
|
263
|
+
// Conditionally add MCP API resource if plugin is available
|
|
264
|
+
_addMCPResourceIfAvailable(apiResources, appConfig, packageIds, accessStrategies);
|
|
265
|
+
|
|
266
|
+
return apiResources;
|
|
242
267
|
};
|
|
243
268
|
|
|
244
269
|
const _getEventResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
@@ -322,7 +347,13 @@ function _filterUnusedPackages(ordDocument) {
|
|
|
322
347
|
module.exports = (csn) => {
|
|
323
348
|
const linkedCsn = _propagateORDVisibility(cds.linked(csn));
|
|
324
349
|
const appConfig = initializeAppConfig(linkedCsn);
|
|
325
|
-
|
|
350
|
+
|
|
351
|
+
// Create auth config and fail-closed on configuration errors
|
|
352
|
+
const authConfig = createAuthConfig();
|
|
353
|
+
if (authConfig.error) {
|
|
354
|
+
throw new Error(`Authentication configuration error: ${authConfig.error}`);
|
|
355
|
+
}
|
|
356
|
+
const accessStrategies = authConfig.accessStrategies;
|
|
326
357
|
|
|
327
358
|
let ordDocument = createDefaultORDDocument(linkedCsn, appConfig);
|
|
328
359
|
const packageIds = extractPackageIds(ordDocument);
|