@cap-js/ord 1.4.5 → 1.6.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 +14 -0
- package/cds-plugin.js +4 -14
- package/lib/auth/mtls-endpoint-service.js +2 -2
- package/lib/build.js +73 -89
- package/lib/common/register-compile-targets.js +31 -0
- package/lib/constants.js +3 -2
- package/lib/{extendOrdWithCustom.js → extend-ord-with-custom.js} +2 -38
- package/lib/index.js +1 -1
- package/lib/{interopCsn.js → interop-csn.js} +13 -5
- package/lib/meta-data.js +90 -0
- package/lib/ord.js +12 -27
- package/lib/services/mtx-ord-provider-service.cds +21 -0
- package/lib/services/mtx-ord-provider-service.js +39 -0
- package/lib/{ord-service.js → services/ord-service.js} +16 -7
- package/lib/templates.js +5 -104
- package/lib/threads/compile.js +13 -0
- package/package.json +12 -6
- package/lib/mcpAdapter.js +0 -132
- package/lib/metaData.js +0 -117
- /package/lib/{integrationDependency.js → integration-dependency.js} +0 -0
- /package/lib/{ord-service.cds → services/ord-service.cds} +0 -0
package/README.md
CHANGED
|
@@ -231,6 +231,20 @@ const ord = cds.compile.to.ord(csn);
|
|
|
231
231
|
|
|
232
232
|
Build all ord related documents, including ordDocument and services resources files:
|
|
233
233
|
|
|
234
|
+
|
|
235
|
+
##### Supported Options
|
|
236
|
+
|
|
237
|
+
1. **workers**:
|
|
238
|
+
* **Description**: Max number of workers to use during execution
|
|
239
|
+
* **Supported Values**:
|
|
240
|
+
* _**workers=N**_ - max number of N (int) workers (e.g. 2 workers => _**workers=2**_)
|
|
241
|
+
* _**workers=NC**_ - max N (int/float) workers per CPU core (e.g. max 5 workers on a 2 core machine, max 10 workers on a 4 core machine => _**workers=2.5C**_)
|
|
242
|
+
* **Default Value**: max 1 worker per every 2 CPU cores => _**workers=0.5C**_
|
|
243
|
+
* **Usage Examples**:
|
|
244
|
+
* `cds build --for ord --opts 'workers=2'`
|
|
245
|
+
* `cds build --for ord --opts 'workers=2.5C'`
|
|
246
|
+
* `cds build --for ord --opts 'workers=0.5C'` (equivalent to `cds build --for ord`)
|
|
247
|
+
|
|
234
248
|
```sh
|
|
235
249
|
cds build --for ord
|
|
236
250
|
|
package/cds-plugin.js
CHANGED
|
@@ -4,17 +4,7 @@ if (cds.cli.command === "build") {
|
|
|
4
4
|
cds.build?.register?.("ord", require("./lib/build"));
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const registerORDCompileTarget = () => {
|
|
14
|
-
Object.defineProperty(cds.compile.to, "ord", {
|
|
15
|
-
get: _lazyRegisterCompileTarget,
|
|
16
|
-
configurable: true,
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
registerORDCompileTarget();
|
|
7
|
+
Object.defineProperty(cds.compile.to, "ord", {
|
|
8
|
+
configurable: true,
|
|
9
|
+
get: () => (model) => require("./lib/index").ord(model, []),
|
|
10
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* from external configuration endpoints and merge with static configuration.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/* global AbortController */
|
|
8
|
+
/* global AbortController */ // eslint-disable-line no-redeclare
|
|
9
9
|
|
|
10
10
|
const { HTTP_CONFIG } = require("../constants");
|
|
11
11
|
|
|
@@ -48,7 +48,7 @@ async function fetchMtlsCertInfo(endpoint, timeoutMs) {
|
|
|
48
48
|
} catch (error) {
|
|
49
49
|
clearTimeout(timeoutId);
|
|
50
50
|
if (error.name === "AbortError") {
|
|
51
|
-
throw new Error(`Request timeout after ${timeoutMs}ms
|
|
51
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`, { cause: error });
|
|
52
52
|
}
|
|
53
53
|
throw error;
|
|
54
54
|
}
|
package/lib/build.js
CHANGED
|
@@ -1,83 +1,59 @@
|
|
|
1
|
-
const cds = require("@sap/cds");
|
|
2
1
|
const path = require("path");
|
|
2
|
+
const cds = require("@sap/cds");
|
|
3
3
|
const _ = require("lodash");
|
|
4
|
-
const
|
|
4
|
+
const Piscina = require("piscina");
|
|
5
5
|
const cliProgress = require("cli-progress");
|
|
6
|
-
const { BUILD_DEFAULT_PATH, ORD_SERVICE_NAME, ORD_DOCUMENT_FILE_NAME } = require("./constants");
|
|
7
|
-
const { isMCPPluginInPackageJson } = require("./mcpAdapter");
|
|
8
6
|
|
|
9
|
-
const
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const { ord } = require("./index");
|
|
9
|
+
const registerCompileTargets = require("./common/register-compile-targets");
|
|
10
|
+
const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME } = require("./constants");
|
|
10
11
|
|
|
11
12
|
module.exports = class OrdBuildPlugin extends cds.build.Plugin {
|
|
12
13
|
static taskDefaults = { src: cds.env.folders.srv };
|
|
13
14
|
|
|
14
15
|
init() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
this.task.dest = this.task.dest ?? path.join(cds.root, BUILD_DEFAULT_PATH);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async build() {
|
|
20
|
+
registerCompileTargets();
|
|
21
|
+
|
|
22
|
+
return this.model()
|
|
23
|
+
.then((model) => ({ model, document: ord(model) }))
|
|
24
|
+
.then(({ model, document }) =>
|
|
25
|
+
Promise.all([
|
|
26
|
+
this.write(this._postProcess(document)).to(ORD_DOCUMENT_FILE_NAME),
|
|
27
|
+
this._generateResourcesFiles(model, [
|
|
28
|
+
...(document.apiResources || []),
|
|
29
|
+
...(document.eventResources || []),
|
|
30
|
+
]),
|
|
31
|
+
]),
|
|
32
|
+
)
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
throw new cds.build.BuildError(`ORD build failed: ${error.message}`);
|
|
35
|
+
});
|
|
18
36
|
}
|
|
19
37
|
|
|
20
|
-
_createProgressBar(
|
|
21
|
-
|
|
38
|
+
_createProgressBar() {
|
|
39
|
+
return new cliProgress.SingleBar({
|
|
22
40
|
format: "Processing resourcesFiles [{bar}] {percentage}% | {value}/{total} | ETA: {eta}s",
|
|
23
41
|
barCompleteChar: "█",
|
|
24
42
|
barIncompleteChar: "░",
|
|
25
43
|
stopOnComplete: true,
|
|
26
44
|
});
|
|
27
|
-
progressBar.start(totalFiles, 0);
|
|
28
|
-
return progressBar;
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!resource.ordId.includes(ORD_SERVICE_NAME) && resource.resourceDefinitions) {
|
|
34
|
-
return total + resource.resourceDefinitions.length;
|
|
35
|
-
}
|
|
36
|
-
return total;
|
|
37
|
-
}, 0);
|
|
38
|
-
}
|
|
47
|
+
_postProcess(document) {
|
|
48
|
+
const clone = _.cloneDeep(document);
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
for (const resource of resObj) {
|
|
47
|
-
const shouldGenerate = resource.resourceDefinitions || isMCPPluginInPackageJson();
|
|
48
|
-
if (!shouldGenerate) continue;
|
|
49
|
-
|
|
50
|
-
for (const resourceDefinition of resource.resourceDefinitions) {
|
|
51
|
-
const { _, response } = await getMetadata(resourceDefinition.url, model); // eslint-disable-line no-unused-vars
|
|
52
|
-
const fileName = path
|
|
53
|
-
.join(resource.ordId, resourceDefinition.url.split("/").pop())
|
|
54
|
-
.replace(/:/g, "_");
|
|
55
|
-
promises.push(this.write(response).to(fileName));
|
|
56
|
-
progressBar.update(++completed);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
await Promise.all(promises);
|
|
60
|
-
} finally {
|
|
61
|
-
progressBar.stop();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
50
|
+
[...(clone.apiResources || []), ...(clone.eventResources || [])]
|
|
51
|
+
.flatMap((resource) => resource.resourceDefinitions || [])
|
|
52
|
+
.forEach((resourceDefinition) => {
|
|
53
|
+
resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
|
|
54
|
+
});
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
const clonedOrdDocument = _.cloneDeep(ordDocument);
|
|
67
|
-
const _updateResourceUrls = (resources) => {
|
|
68
|
-
for (const resource of resources || []) {
|
|
69
|
-
if (resource.resourceDefinitions) {
|
|
70
|
-
for (const resourceDefinition of resource.resourceDefinitions) {
|
|
71
|
-
resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
_updateResourceUrls(clonedOrdDocument.apiResources);
|
|
78
|
-
_updateResourceUrls(clonedOrdDocument.eventResources);
|
|
79
|
-
|
|
80
|
-
return clonedOrdDocument;
|
|
56
|
+
return clone;
|
|
81
57
|
}
|
|
82
58
|
|
|
83
59
|
_createRelativePath(url) {
|
|
@@ -86,34 +62,42 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
|
|
|
86
62
|
return path.join(...relative.replace(/:/g, "_").split("/"));
|
|
87
63
|
}
|
|
88
64
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
65
|
+
_createWorkerPool(tasks, model) {
|
|
66
|
+
const size = cds.cli?.options?.taskOptions?.workers || os.availableParallelism();
|
|
67
|
+
const maxThreads = Math.ceil(Number(size) || os.availableParallelism() * Number(size.replace(/C$/i, "")));
|
|
68
|
+
|
|
69
|
+
return new Piscina({
|
|
70
|
+
filename: path.join(__dirname, "threads", "compile.js"),
|
|
71
|
+
workerData: { model: JSON.parse(JSON.stringify(model)) },
|
|
72
|
+
minThreads: Math.min(tasks, maxThreads, Math.ceil(os.availableParallelism() * 1.5)),
|
|
73
|
+
maxThreads: Math.min(tasks, maxThreads, Math.ceil(os.availableParallelism() * 1.5)),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_extractCompileTasks(resources) {
|
|
78
|
+
return resources
|
|
79
|
+
.filter(({ resourceDefinitions }) => !!resourceDefinitions)
|
|
80
|
+
.flatMap(({ ordId, resourceDefinitions }) => resourceDefinitions.map(({ url }) => ({ url, ordId })));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_generateResourcesFiles(model, resources) {
|
|
84
|
+
const progressBar = this._createProgressBar();
|
|
85
|
+
const tasks = this._extractCompileTasks(resources);
|
|
86
|
+
const pool = !tasks.length ? null : this._createWorkerPool(tasks.length, model);
|
|
87
|
+
|
|
88
|
+
progressBar.start(tasks.length, 0);
|
|
89
|
+
|
|
90
|
+
return Promise.all(
|
|
91
|
+
tasks.map(({ url, ordId }) =>
|
|
92
|
+
pool.run({ url }).then((response) =>
|
|
93
|
+
this.write(response)
|
|
94
|
+
.to(path.join(ordId, url.split("/").pop()).replace(/:/g, "_"))
|
|
95
|
+
.then(() => progressBar.increment()),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
).finally(() => {
|
|
99
|
+
progressBar.stop();
|
|
100
|
+
return pool?.close({ force: true });
|
|
101
|
+
});
|
|
118
102
|
}
|
|
119
103
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
const Logger = require("../logger");
|
|
3
|
+
|
|
4
|
+
const PROTOCOL_PROVIDERS = Object.freeze({
|
|
5
|
+
["@cap-js/mcp"]: "mcp",
|
|
6
|
+
["@cap-js/graphql"]: "graphql",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
module.exports = () => {
|
|
10
|
+
// Plugins like @cap-js/mcp and @cap-js/graphql register protocols and compile targets
|
|
11
|
+
// at runtime via cds-plugin.js, but cds build rebuilds cds.env afterward, losing both
|
|
12
|
+
// registrations. Re-apply so that compilation works accordingly during build and runtime.
|
|
13
|
+
|
|
14
|
+
Object.keys(cds.env.plugins || {})
|
|
15
|
+
.filter((plugin) => plugin !== "@cap-js/ord")
|
|
16
|
+
.forEach((plugin) => {
|
|
17
|
+
try {
|
|
18
|
+
const protocol = PROTOCOL_PROVIDERS[plugin];
|
|
19
|
+
|
|
20
|
+
require(`${plugin}/lib/api`)?.registerCompileTargets?.();
|
|
21
|
+
cds.env.protocols = {
|
|
22
|
+
...cds.env.protocols,
|
|
23
|
+
...(!protocol || cds.env.protocols?.[protocol]
|
|
24
|
+
? {}
|
|
25
|
+
: { [protocol]: { path: `/${protocol}`, impl: plugin } }),
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
Logger.warn(`Failed to register compile targets for ${plugin}: ${error}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
package/lib/constants.js
CHANGED
|
@@ -116,7 +116,7 @@ const SHORT_DESCRIPTION_PREFIX = "Short description of ";
|
|
|
116
116
|
const SEM_VERSION_REGEX =
|
|
117
117
|
/^(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-]+)*))?$/;
|
|
118
118
|
|
|
119
|
-
const
|
|
119
|
+
const MCP_RESOURCE_DEFINITION_TYPE = "sap:mcp-server-card:v0";
|
|
120
120
|
|
|
121
121
|
const EXTERNAL_DP_ORD_ID_ANNOTATION = "@cds.dp.ordId";
|
|
122
122
|
|
|
@@ -132,6 +132,7 @@ const ORD_API_PROTOCOL = Object.freeze({
|
|
|
132
132
|
GRAPHQL: "graphql",
|
|
133
133
|
SAP_INA: "sap-ina-api-v1",
|
|
134
134
|
SAP_DATA_SUBSCRIPTION: "sap.dp:data-subscription-api:v1",
|
|
135
|
+
MCP: "mcp",
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
// Mapping from CAP protocol kind to ORD apiProtocol
|
|
@@ -200,7 +201,7 @@ module.exports = {
|
|
|
200
201
|
EXTERNAL_SERVICE_ANNOTATION,
|
|
201
202
|
INTEGRATION_DEPENDENCY_RESOURCE_NAME,
|
|
202
203
|
LEVEL,
|
|
203
|
-
|
|
204
|
+
MCP_RESOURCE_DEFINITION_TYPE,
|
|
204
205
|
OPEN_RESOURCE_DISCOVERY_VERSION,
|
|
205
206
|
OPENAPI_SERVERS_ANNOTATION,
|
|
206
207
|
ORD_ACCESS_STRATEGY,
|
|
@@ -20,37 +20,7 @@ function patchGeneratedOrdResources(destinationObj, sourceObj) {
|
|
|
20
20
|
const destObj = _.keyBy(destinationObj, CONTENT_MERGE_KEY);
|
|
21
21
|
const srcObj = _.keyBy(sourceObj, CONTENT_MERGE_KEY);
|
|
22
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;
|
|
38
|
-
} else {
|
|
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
23
|
for (const ordId in srcObj) {
|
|
50
|
-
if (ordId.endsWith(":apiResource:mcp-server:v1")) {
|
|
51
|
-
mergeMCPResource(ordId, existingMCPKey);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
24
|
destObj[ordId] =
|
|
55
25
|
ordId in destObj
|
|
56
26
|
? _.assignWith(structuredClone(destObj[ordId]), structuredClone(srcObj[ordId]))
|
|
@@ -92,13 +62,7 @@ function getCustomORDContent(appConfig) {
|
|
|
92
62
|
return require(pathToCustomORDContent);
|
|
93
63
|
}
|
|
94
64
|
|
|
95
|
-
function extendCustomORDContentIfExists(appConfig, ordContent) {
|
|
96
|
-
const customORDContent = getCustomORDContent(appConfig);
|
|
97
|
-
return customORDContent
|
|
98
|
-
? compareAndHandleCustomORDContentWithExistingContent(ordContent, customORDContent)
|
|
99
|
-
: ordContent;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
65
|
module.exports = {
|
|
103
|
-
|
|
66
|
+
getCustomORDContent,
|
|
67
|
+
compareAndHandleCustomORDContentWithExistingContent,
|
|
104
68
|
};
|
package/lib/index.js
CHANGED
|
@@ -73,7 +73,7 @@ function remove_localized_assoc(csn) {
|
|
|
73
73
|
// - name is "localized"
|
|
74
74
|
// - type is "cds.Association"
|
|
75
75
|
// - target name is source name + ".texts"
|
|
76
|
-
// - there is an ON condition
|
|
76
|
+
// - there is an ON condition
|
|
77
77
|
for (let n1 in csn.definitions) {
|
|
78
78
|
let def = csn.definitions[n1];
|
|
79
79
|
if (def.kind === "entity")
|
|
@@ -132,10 +132,18 @@ function map_annotations(csn) {
|
|
|
132
132
|
function add_meta_info(csn) {
|
|
133
133
|
if (typeof csn != "object") return csn; // needed to make tests pass
|
|
134
134
|
csn["csnInteropEffective"] = "1.0";
|
|
135
|
-
csn.meta ??= {};
|
|
135
|
+
csn.meta ??= {}; // ensure there is a meta section
|
|
136
|
+
|
|
137
|
+
// Keep only the properties of csn.meta that are relevant for interop:
|
|
138
|
+
// "flavor", "document", "features", and private extension names (starting with "__")
|
|
139
|
+
// Remove everything else, including "creator" (leaks build details) and
|
|
140
|
+
// any unknown compiler-added properties (e.g. "compilerCsnFlavor").
|
|
141
|
+
for (let k in csn.meta) {
|
|
142
|
+
if (!["flavor", "document", "features"].includes(k) && !k.startsWith("__")) {
|
|
143
|
+
delete csn.meta[k];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
136
146
|
csn.meta.flavor = "effective";
|
|
137
|
-
// Remove compiler creator information – not relevant for ORD interop and leaks build details
|
|
138
|
-
if (csn.meta.creator) delete csn.meta.creator;
|
|
139
147
|
|
|
140
148
|
let services = Object.entries(csn.definitions).filter(([, def]) => def.kind === "service");
|
|
141
149
|
if (services.length === 1) {
|
|
@@ -157,5 +165,5 @@ function add_meta_info(csn) {
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
module.exports = {
|
|
160
|
-
interopCSN
|
|
168
|
+
interopCSN
|
|
161
169
|
};
|
package/lib/meta-data.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const cds = require("@sap/cds/lib");
|
|
3
|
+
const assert = require("node:assert");
|
|
4
|
+
const cdsc = require("@sap/cds-compiler/lib/main");
|
|
5
|
+
const { compile: openapi } = require("@cap-js/openapi");
|
|
6
|
+
const { compile: asyncapi } = require("@cap-js/asyncapi");
|
|
7
|
+
|
|
8
|
+
const Logger = require("./logger");
|
|
9
|
+
const { interopCSN } = require("./interop-csn.js");
|
|
10
|
+
const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
|
|
11
|
+
|
|
12
|
+
function extractServiceName(url) {
|
|
13
|
+
return path.basename(url, ".json").split(".").slice(0, -1).join(".");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractCompilerType(url) {
|
|
17
|
+
return path.basename(url, ".json").split(".").pop();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const compilers = Object.freeze({
|
|
21
|
+
[COMPILER_TYPES.csn]: function (csn, options) {
|
|
22
|
+
return {
|
|
23
|
+
contentType: "application/json",
|
|
24
|
+
response: interopCSN(
|
|
25
|
+
cdsc.for.effective(csn, { beta: { effectiveCsn: true }, effectiveServiceName: options.service }),
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
[COMPILER_TYPES.mcp]: async function (csn, options) {
|
|
30
|
+
return {
|
|
31
|
+
contentType: "application/json",
|
|
32
|
+
response: cds.compile(csn).to["mcp"]({ ...options, ...(cds.env.ord?.compileOptions?.mcp || {}) }),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
[COMPILER_TYPES.oas3]: function (csn, options) {
|
|
36
|
+
// Check for service-level @OpenAPI.servers annotation
|
|
37
|
+
const servers = csn?.definitions?.[options.service]?.[OPENAPI_SERVERS_ANNOTATION];
|
|
38
|
+
const openapiOptions = { ...options, ...(cds.env?.ord?.compileOptions?.openapi || {}) };
|
|
39
|
+
|
|
40
|
+
// Service-level annotation takes precedence over global config
|
|
41
|
+
if (Array.isArray(servers) && servers.length) {
|
|
42
|
+
openapiOptions["openapi:servers"] = JSON.stringify(servers);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
contentType: "application/json",
|
|
47
|
+
response: openapi(csn, openapiOptions),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
[COMPILER_TYPES.edmx]: function (csn, options) {
|
|
51
|
+
return {
|
|
52
|
+
contentType: "application/xml",
|
|
53
|
+
response: cds
|
|
54
|
+
.compile(csn)
|
|
55
|
+
.to["edmx"]({ ...options, ...(cds.env?.ord?.compileOptions?.edmx || {}) }),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
[COMPILER_TYPES.graphql]: function (csn, options) {
|
|
59
|
+
const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
|
|
60
|
+
const { printSchema, lexicographicSortSchema } = require("graphql");
|
|
61
|
+
const srv = new cds.ApplicationService(options.service, cds.linked(csn));
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
contentType: "text/plain",
|
|
65
|
+
response: printSchema(lexicographicSortSchema(generateSchema4({ [options.service]: srv }))),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
[COMPILER_TYPES.asyncapi2]: function (csn, options) {
|
|
69
|
+
return {
|
|
70
|
+
contentType: "application/json",
|
|
71
|
+
response: asyncapi(csn, { ...options, ...(cds.env?.ord?.compileOptions?.asyncapi || {}) }),
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
module.exports = async (url, model = null) => {
|
|
77
|
+
const name = extractServiceName(url);
|
|
78
|
+
const type = extractCompilerType(url);
|
|
79
|
+
const csn = model || cds.services[name]?.model;
|
|
80
|
+
const options = { service: name, as: "str", messages: [] };
|
|
81
|
+
|
|
82
|
+
assert(Object.hasOwn(compilers, type), `Unsupported format: ${type}`);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return await compilers[type](csn, options);
|
|
86
|
+
} catch(error) {
|
|
87
|
+
Logger.error(`Compilation failed for service ${name} (compiler: ${type}) - ${error.message}`);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
package/lib/ord.js
CHANGED
|
@@ -11,14 +11,15 @@ const {
|
|
|
11
11
|
createEntityTypeMappingsItemTemplate,
|
|
12
12
|
createEventResourceTemplate,
|
|
13
13
|
createGroupsTemplateForService,
|
|
14
|
-
createMCPAPIResourceTemplate,
|
|
15
14
|
_propagateORDVisibility,
|
|
16
15
|
} = require("./templates");
|
|
17
|
-
const { getIntegrationDependencies } = require("./
|
|
18
|
-
const {
|
|
16
|
+
const { getIntegrationDependencies } = require("./integration-dependency");
|
|
17
|
+
const {
|
|
18
|
+
getCustomORDContent,
|
|
19
|
+
compareAndHandleCustomORDContentWithExistingContent,
|
|
20
|
+
} = require("./extend-ord-with-custom");
|
|
19
21
|
const { getRFC3339Date } = require("./date");
|
|
20
22
|
const { createAuthConfig } = require("./auth/authentication");
|
|
21
|
-
const { isMCPPluginReady } = require("./mcpAdapter");
|
|
22
23
|
|
|
23
24
|
const Logger = require("./logger");
|
|
24
25
|
const _ = require("lodash");
|
|
@@ -26,24 +27,6 @@ const cds = require("@sap/cds");
|
|
|
26
27
|
const defaults = require("./defaults");
|
|
27
28
|
const path = require("path");
|
|
28
29
|
|
|
29
|
-
const _addMCPResourceIfAvailable = (apiResources, appConfig, packageIds, accessStrategies) => {
|
|
30
|
-
// Use comprehensive check for MCP plugin readiness
|
|
31
|
-
const shouldAddMCP = isMCPPluginReady();
|
|
32
|
-
if (shouldAddMCP) {
|
|
33
|
-
try {
|
|
34
|
-
const mcpResources = createMCPAPIResourceTemplate(appConfig, packageIds, accessStrategies);
|
|
35
|
-
// Handle both array and single object responses from MCP plugin
|
|
36
|
-
if (Array.isArray(mcpResources)) {
|
|
37
|
-
apiResources.push(...mcpResources);
|
|
38
|
-
} else if (mcpResources) {
|
|
39
|
-
apiResources.push(mcpResources);
|
|
40
|
-
}
|
|
41
|
-
} catch (error) {
|
|
42
|
-
Logger.warn("Failed to create MCP API resource:", error.message);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
30
|
const initializeAppConfig = (csn) => {
|
|
48
31
|
const packageJson = _loadPackageJson();
|
|
49
32
|
const packageName = packageJson.name;
|
|
@@ -261,9 +244,6 @@ const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
|
|
|
261
244
|
),
|
|
262
245
|
);
|
|
263
246
|
|
|
264
|
-
// Conditionally add MCP API resource if plugin is available
|
|
265
|
-
_addMCPResourceIfAvailable(apiResources, appConfig, packageIds, accessStrategies);
|
|
266
|
-
|
|
267
247
|
return apiResources;
|
|
268
248
|
};
|
|
269
249
|
|
|
@@ -344,7 +324,7 @@ function _filterUnusedPackages(ordDocument) {
|
|
|
344
324
|
return ordDocument.packages.filter((pkg) => usedPackageIds.has(pkg.ordId));
|
|
345
325
|
}
|
|
346
326
|
|
|
347
|
-
module.exports = (csn) => {
|
|
327
|
+
module.exports = (csn, extensions = []) => {
|
|
348
328
|
const linkedCsn = _propagateORDVisibility(cds.linked(csn));
|
|
349
329
|
const appConfig = initializeAppConfig(linkedCsn);
|
|
350
330
|
|
|
@@ -381,7 +361,12 @@ module.exports = (csn) => {
|
|
|
381
361
|
ordDocument.integrationDependencies = integrationDependencies;
|
|
382
362
|
}
|
|
383
363
|
|
|
384
|
-
|
|
364
|
+
[...(extensions || []), getCustomORDContent(appConfig)]
|
|
365
|
+
.filter((extension) => !!extension)
|
|
366
|
+
.forEach((extension) => {
|
|
367
|
+
ordDocument = compareAndHandleCustomORDContentWithExistingContent(ordDocument, extension);
|
|
368
|
+
});
|
|
369
|
+
|
|
385
370
|
ordDocument.packages = _filterUnusedPackages(ordDocument);
|
|
386
371
|
|
|
387
372
|
return ordDocument;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@protocol: 'rest'
|
|
2
|
+
@requires: [ 'internal-user' ]
|
|
3
|
+
@path: '/-/cds/ord-provider-service'
|
|
4
|
+
@impl:'@cap-js/ord/lib/services/mtx-ord-provider-service.js'
|
|
5
|
+
service cds.xt.ord.MtxOrdProviderService {
|
|
6
|
+
|
|
7
|
+
action getOrdDocument(
|
|
8
|
+
tenant : String,
|
|
9
|
+
@cds.validate: false
|
|
10
|
+
toggles : array of String,
|
|
11
|
+
for : String enum { nodejs; java; },
|
|
12
|
+
) returns {};
|
|
13
|
+
|
|
14
|
+
action getOrdResourceDefinition(
|
|
15
|
+
tenant : String,
|
|
16
|
+
@cds.validate: false
|
|
17
|
+
toggles : array of String,
|
|
18
|
+
for : String enum { nodejs; java; },
|
|
19
|
+
resource : String,
|
|
20
|
+
) returns LargeString;
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const cds = require("@sap/cds/lib");
|
|
2
|
+
const { ord, getMetadata } = require("@cap-js/ord/lib");
|
|
3
|
+
|
|
4
|
+
module.exports = class MtxOrdProviderService extends cds.ApplicationService {
|
|
5
|
+
init() {
|
|
6
|
+
this.on("getOrdDocument", async (req) => {
|
|
7
|
+
req._?.res?.set("Content-Type", "application/json");
|
|
8
|
+
|
|
9
|
+
return ord(
|
|
10
|
+
await (
|
|
11
|
+
await cds.connect.to("cds.xt.ModelProviderService")
|
|
12
|
+
).getCsn({
|
|
13
|
+
for: req.data.for,
|
|
14
|
+
tenant: req.data.tenant,
|
|
15
|
+
toggles: req.data.toggles,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
this.on("getOrdResourceDefinition", async (req) => {
|
|
21
|
+
const { response, contentType } = await getMetadata(
|
|
22
|
+
req.data.resource,
|
|
23
|
+
await (
|
|
24
|
+
await cds.connect.to("cds.xt.ModelProviderService")
|
|
25
|
+
).getCsn({
|
|
26
|
+
for: req.data.for,
|
|
27
|
+
tenant: req.data.tenant,
|
|
28
|
+
toggles: req.data.toggles,
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
req._?.res?.set("Content-Type", contentType);
|
|
33
|
+
|
|
34
|
+
return response;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return super.init();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
const cds = require("@sap/cds");
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
2
|
+
|
|
3
|
+
const ord = require("../ord.js");
|
|
4
|
+
const Logger = require("../logger.js");
|
|
5
|
+
const defaults = require("../defaults.js");
|
|
6
|
+
const compileMetadata = require("../meta-data.js");
|
|
7
|
+
const { createAuthConfig, createAuthMiddleware } = require("../auth/authentication.js");
|
|
7
8
|
|
|
8
9
|
class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
9
10
|
async init() {
|
|
11
|
+
this.extensions = {};
|
|
12
|
+
|
|
13
|
+
cds.on("ord.extension.publish", ({ id, data }) => {
|
|
14
|
+
Logger.info(`Registering extension with id ${id}...`);
|
|
15
|
+
|
|
16
|
+
this.extensions[id] = data;
|
|
17
|
+
});
|
|
18
|
+
|
|
10
19
|
// Initialize authentication configuration from .cdsrc.json or environment variables
|
|
11
20
|
// CF mTLS validator is lazily initialized on first mTLS request
|
|
12
21
|
const authConfig = createAuthConfig();
|
|
@@ -23,7 +32,7 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
|
23
32
|
|
|
24
33
|
cds.app.get(`/ord/v1/documents/ord-document`, authMiddleware, async (_, res) => {
|
|
25
34
|
const csn = cds.context?.model || cds.model;
|
|
26
|
-
const data = ord(csn);
|
|
35
|
+
const data = ord(csn, Array.from(Object.values(this.extensions)));
|
|
27
36
|
return res.status(200).send(data);
|
|
28
37
|
});
|
|
29
38
|
|
|
@@ -34,7 +43,7 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
|
|
|
34
43
|
// Handler for metadata requests (oas3, edmx, csn, etc.)
|
|
35
44
|
const metadataHandler = async (req, res) => {
|
|
36
45
|
try {
|
|
37
|
-
const { contentType, response } = await
|
|
46
|
+
const { contentType, response } = await compileMetadata(req.url);
|
|
38
47
|
return res.status(200).contentType(contentType).send(response);
|
|
39
48
|
} catch (error) {
|
|
40
49
|
Logger.error(error, "Error while processing the resource definition document");
|
package/lib/templates.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { hasSAPPolicyLevel } = require("./utils");
|
|
2
|
-
const { isMCPPluginReady } = require("./mcpAdapter");
|
|
3
2
|
const defaults = require("./defaults");
|
|
4
3
|
const _ = require("lodash");
|
|
5
4
|
const {
|
|
@@ -9,7 +8,6 @@ const {
|
|
|
9
8
|
DESCRIPTION_PREFIX,
|
|
10
9
|
ENTITY_RELATIONSHIP_ANNOTATION,
|
|
11
10
|
LEVEL,
|
|
12
|
-
MCP_CUSTOM_TYPE,
|
|
13
11
|
ORD_EXTENSIONS_PREFIX,
|
|
14
12
|
ORD_ODM_ENTITY_NAME_ANNOTATION,
|
|
15
13
|
ORD_RESOURCE_TYPE,
|
|
@@ -21,6 +19,7 @@ const {
|
|
|
21
19
|
CONTENT_MERGE_KEY,
|
|
22
20
|
CDS_ELEMENT_KIND,
|
|
23
21
|
ORD_API_PROTOCOL,
|
|
22
|
+
MCP_RESOURCE_DEFINITION_TYPE,
|
|
24
23
|
} = require("./constants");
|
|
25
24
|
const Logger = require("./logger");
|
|
26
25
|
const { ensureAccessStrategies } = require("./access-strategies");
|
|
@@ -348,6 +347,10 @@ const createAPIResourceTemplate = (serviceName, serviceDefinition, appConfig, pa
|
|
|
348
347
|
resourceDefinitions = [
|
|
349
348
|
_getResourceDefinition("openapi-v3", "json", ordId, serviceName, "oas3.json", accessStrategies),
|
|
350
349
|
];
|
|
350
|
+
} else if (apiProtocol === ORD_API_PROTOCOL.MCP) {
|
|
351
|
+
resourceDefinitions = [
|
|
352
|
+
_getResourceDefinition(MCP_RESOURCE_DEFINITION_TYPE, "json", ordId, serviceName, "mcp.json", accessStrategies),
|
|
353
|
+
];
|
|
351
354
|
} else if (apiProtocol === ORD_API_PROTOCOL.GRAPHQL) {
|
|
352
355
|
// GraphQL only has GraphQL SDL
|
|
353
356
|
resourceDefinitions = [
|
|
@@ -584,107 +587,6 @@ function _getPackageID(namespace, packageIds, resourceType, visibility = RESOURC
|
|
|
584
587
|
return packageIds.find((id) => id.includes(`-${resourceType}-`)) || packageIds.find((id) => id.includes(namespace));
|
|
585
588
|
}
|
|
586
589
|
|
|
587
|
-
/**
|
|
588
|
-
* Helper function to create a single MCP API resource.
|
|
589
|
-
*
|
|
590
|
-
* @param {object} metadata - The MCP metadata object.
|
|
591
|
-
* @param {object} appConfig - The application configuration.
|
|
592
|
-
* @param {Array} packageIds - The available package identifiers.
|
|
593
|
-
* @param {Array} accessStrategies - The array of accessStrategies objects.
|
|
594
|
-
* @returns {Object} An MCP API resource object.
|
|
595
|
-
*/
|
|
596
|
-
function createSingleMCPResource(metadata, appConfig, packageIds, accessStrategies) {
|
|
597
|
-
const visibility = metadata?.visibility || RESOURCE_VISIBILITY.public;
|
|
598
|
-
|
|
599
|
-
// Generate ordId based on visibility
|
|
600
|
-
let resourceId;
|
|
601
|
-
if (visibility === RESOURCE_VISIBILITY.public) {
|
|
602
|
-
resourceId = "mcp-server"; // Default public resource
|
|
603
|
-
} else {
|
|
604
|
-
resourceId = `mcp-server-${visibility}`; // mcp-server-internal, mcp-server-private
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const ordId = `${appConfig.ordNamespace}:apiResource:${resourceId}:v1`;
|
|
608
|
-
|
|
609
|
-
// Use metadata with proper fallbacks
|
|
610
|
-
const title = metadata?.title || `MCP Server for ${appConfig.appName}`;
|
|
611
|
-
const shortDescription =
|
|
612
|
-
metadata?.shortDescription || `This is the MCP server to interact with the ${appConfig.appName}`;
|
|
613
|
-
const description = metadata?.description || `This is the MCP server to interact with the ${appConfig.appName}`;
|
|
614
|
-
const version = metadata?.version || "1.0.0";
|
|
615
|
-
|
|
616
|
-
// Handle entryPoints: convert string to array, with fallback
|
|
617
|
-
const entryPoints = metadata?.entryPoints ? metadata.entryPoints : ["/rest/mcp/streaming"];
|
|
618
|
-
|
|
619
|
-
const packageId = _getPackageID(appConfig.ordNamespace, packageIds, ORD_RESOURCE_TYPE.api, visibility);
|
|
620
|
-
// Fallback: ensure partOfPackage never undefined to satisfy generic apiResources tests
|
|
621
|
-
const effectivePackageId = packageId || `${appConfig.ordNamespace}:package:${visibility}:v1`;
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
ordId,
|
|
625
|
-
title,
|
|
626
|
-
shortDescription,
|
|
627
|
-
description,
|
|
628
|
-
version,
|
|
629
|
-
lastUpdate: appConfig.lastUpdate,
|
|
630
|
-
visibility,
|
|
631
|
-
partOfPackage: effectivePackageId,
|
|
632
|
-
releaseStatus: "active",
|
|
633
|
-
apiProtocol: "mcp",
|
|
634
|
-
resourceDefinitions: [
|
|
635
|
-
{
|
|
636
|
-
type: "custom",
|
|
637
|
-
customType: MCP_CUSTOM_TYPE,
|
|
638
|
-
mediaType: "application/json",
|
|
639
|
-
url: `/ord/v1/${ordId}/mcp-server-definition.mcp.json`,
|
|
640
|
-
accessStrategies,
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
entryPoints,
|
|
644
|
-
extensible: { supported: "no" },
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* This is a template function to create MCP API Resource object(s).
|
|
650
|
-
*
|
|
651
|
-
* The generated MCP API resource(s) can be customized via the generic overwrite mechanism
|
|
652
|
-
* using custom.ord.json. Properties like visibility, title, version, entryPoints, etc.
|
|
653
|
-
* can be overridden by matching the ordId pattern.
|
|
654
|
-
*
|
|
655
|
-
* Metadata values are sourced from the MCP plugin's generateORDMetadata function when available,
|
|
656
|
-
* which reads from cds.env.mcp configuration. This allows users to customize MCP metadata via
|
|
657
|
-
* package.json or .cdsrc.json files.
|
|
658
|
-
*
|
|
659
|
-
* @param {object} appConfig - The application configuration.
|
|
660
|
-
* @param {Array} packageIds - The available package identifiers.
|
|
661
|
-
* @param {Array} accessStrategies The array of accessStrategies objects
|
|
662
|
-
* @returns {Object|Array} An MCP API resource object or array of objects.
|
|
663
|
-
*/
|
|
664
|
-
const createMCPAPIResourceTemplate = (appConfig, packageIds, accessStrategies) => {
|
|
665
|
-
// Get ORD metadata from MCP plugin if available
|
|
666
|
-
let ordMetadata = null;
|
|
667
|
-
// Use comprehensive check for MCP plugin readiness
|
|
668
|
-
const shouldAddMCP = isMCPPluginReady();
|
|
669
|
-
if (shouldAddMCP) {
|
|
670
|
-
try {
|
|
671
|
-
const { generateORDMetadata } = require("@btp-ai/mcp-plugin/lib/utils/metadata");
|
|
672
|
-
ordMetadata = generateORDMetadata();
|
|
673
|
-
} catch (error) {
|
|
674
|
-
Logger.warn("Failed to generate MCP ORD metadata:", error.message);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Handle both array and single object responses from MCP plugin
|
|
679
|
-
if (Array.isArray(ordMetadata)) {
|
|
680
|
-
return ordMetadata.map((metadata) =>
|
|
681
|
-
createSingleMCPResource(metadata, appConfig, packageIds, accessStrategies),
|
|
682
|
-
);
|
|
683
|
-
} else {
|
|
684
|
-
return createSingleMCPResource(ordMetadata || {}, appConfig, packageIds, accessStrategies);
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
|
|
688
590
|
function _propagateORDVisibility(model) {
|
|
689
591
|
for (const [name, def] of Object.entries(model.definitions)) {
|
|
690
592
|
if (def.kind === CDS_ELEMENT_KIND.service && def[ORD_EXTENSIONS_PREFIX + "visibility"]) {
|
|
@@ -710,7 +612,6 @@ module.exports = {
|
|
|
710
612
|
createGroupsTemplateForService,
|
|
711
613
|
createAPIResourceTemplate,
|
|
712
614
|
createEventResourceTemplate,
|
|
713
|
-
createMCPAPIResourceTemplate,
|
|
714
615
|
readORDExtensions,
|
|
715
616
|
_getPackageID,
|
|
716
617
|
_getEntityTypeMappings,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { workerData } = require("piscina");
|
|
2
|
+
|
|
3
|
+
const compileMetadata = require("../meta-data");
|
|
4
|
+
const registerCompileTargets = require("../common/register-compile-targets");
|
|
5
|
+
|
|
6
|
+
registerCompileTargets(); // Worker threads skip cds-plugin.js — re-register compile targets
|
|
7
|
+
|
|
8
|
+
module.exports = ({ url }) => {
|
|
9
|
+
// JSON round-trip: CDS compiler output may contain Generator objects
|
|
10
|
+
// that cannot be transferred via postMessage (structured clone algorithm).
|
|
11
|
+
return compileMetadata(url, workerData.model) //
|
|
12
|
+
.then(({ response }) => JSON.parse(JSON.stringify(response)));
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/ord",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "CAP Plugin for generating ORD document.",
|
|
5
5
|
"repository": "cap-js/ord",
|
|
6
6
|
"author": "SAP SE (https://www.sap.com)",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"eslint": "^10.0.0",
|
|
35
35
|
"express": "^4",
|
|
36
36
|
"jest": "^30.0.0",
|
|
37
|
-
"prettier": "3.8.
|
|
37
|
+
"prettier": "3.8.3",
|
|
38
38
|
"supertest": "^7.0.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -45,21 +45,27 @@
|
|
|
45
45
|
"@cap-js/openapi": "^1.2.1",
|
|
46
46
|
"bcryptjs": "3.0.3",
|
|
47
47
|
"cli-progress": "^3.12.0",
|
|
48
|
-
"lodash": "^4.17.21"
|
|
48
|
+
"lodash": "^4.17.21",
|
|
49
|
+
"piscina": "^5.1.4"
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
|
-
"node": ">=
|
|
52
|
+
"node": ">=20 <25"
|
|
52
53
|
},
|
|
53
54
|
"overrides": {
|
|
54
|
-
"@sap/cds-compiler": "6.
|
|
55
|
+
"@sap/cds-compiler": "6.9.0"
|
|
55
56
|
},
|
|
56
57
|
"cds": {
|
|
57
58
|
"requires": {
|
|
58
59
|
"SAP ORD Service": {
|
|
59
|
-
"model": "@cap-js/ord/lib/ord-service"
|
|
60
|
+
"model": "@cap-js/ord/lib/services/ord-service"
|
|
60
61
|
},
|
|
61
62
|
"[java]": {
|
|
62
63
|
"SAP ORD Service": false
|
|
64
|
+
},
|
|
65
|
+
"[mtx-sidecar]": {
|
|
66
|
+
"SAP ORD Service": {
|
|
67
|
+
"model": "@cap-js/ord/lib/services/mtx-ord-provider-service"
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
}
|
package/lib/mcpAdapter.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
const cds = require("@sap/cds/lib");
|
|
2
|
-
const { compile: openapi } = require("@cap-js/openapi");
|
|
3
|
-
const { compile: asyncapi } = require("@cap-js/asyncapi");
|
|
4
|
-
const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
|
|
5
|
-
const Logger = require("./logger");
|
|
6
|
-
const { interopCSN } = require("./interopCsn.js");
|
|
7
|
-
const cdsc = require("@sap/cds-compiler/lib/main");
|
|
8
|
-
const { isMCPPluginReady, buildMcpServerDefinition } = require("./mcpAdapter");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Read @OpenAPI.servers annotation from service definition
|
|
12
|
-
* @param {object} csn - The CSN model
|
|
13
|
-
* @param {string} serviceName - The service name
|
|
14
|
-
* @returns {string|undefined} - JSON string of servers array or undefined
|
|
15
|
-
*/
|
|
16
|
-
const _getServersFromAnnotation = (csn, serviceName) => {
|
|
17
|
-
const servers = csn?.definitions?.[serviceName]?.[OPENAPI_SERVERS_ANNOTATION];
|
|
18
|
-
const isValidServers = Array.isArray(servers) && servers.length > 0;
|
|
19
|
-
return isValidServers ? JSON.stringify(servers) : undefined;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const getMetadata = async (url, model = null) => {
|
|
23
|
-
const parts = url
|
|
24
|
-
?.split("/")
|
|
25
|
-
.pop()
|
|
26
|
-
.replace(/\.json$/, "")
|
|
27
|
-
.split(".");
|
|
28
|
-
const compilerType = parts.pop();
|
|
29
|
-
const serviceName = parts.join(".");
|
|
30
|
-
const csn = model || cds.services[serviceName]?.model;
|
|
31
|
-
const compileOptions = cds.env["ord"]?.compileOptions || {};
|
|
32
|
-
|
|
33
|
-
let responseFile;
|
|
34
|
-
const options = { service: serviceName, as: "str", messages: [] };
|
|
35
|
-
switch (compilerType) {
|
|
36
|
-
case COMPILER_TYPES.oas3:
|
|
37
|
-
try {
|
|
38
|
-
// Check for service-level @OpenAPI.servers annotation
|
|
39
|
-
const serversFromAnnotation = _getServersFromAnnotation(csn, serviceName);
|
|
40
|
-
const openapiOptions = { ...options, ...(compileOptions?.openapi || {}) };
|
|
41
|
-
|
|
42
|
-
// Service-level annotation takes precedence over global config
|
|
43
|
-
if (serversFromAnnotation) {
|
|
44
|
-
openapiOptions["openapi:servers"] = serversFromAnnotation;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
responseFile = openapi(csn, openapiOptions);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
Logger.error(`OpenApi error for service ${serviceName} - ${error.message}`);
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
break;
|
|
53
|
-
case COMPILER_TYPES.asyncapi2:
|
|
54
|
-
try {
|
|
55
|
-
responseFile = asyncapi(csn, { ...options, ...(compileOptions?.asyncapi || {}) });
|
|
56
|
-
} catch (error) {
|
|
57
|
-
Logger.error(`AsyncApi error for service ${serviceName} - ${error.message}`);
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
break;
|
|
61
|
-
case COMPILER_TYPES.csn:
|
|
62
|
-
try {
|
|
63
|
-
const opt_eff = { beta: { effectiveCsn: true }, effectiveServiceName: serviceName };
|
|
64
|
-
let effCsn = cdsc.for.effective(csn, opt_eff);
|
|
65
|
-
responseFile = interopCSN(effCsn);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
Logger.error(`Csn error for service ${serviceName} - ${error.message}`);
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
break;
|
|
71
|
-
case COMPILER_TYPES.edmx:
|
|
72
|
-
try {
|
|
73
|
-
responseFile = await cds.compile(csn).to["edmx"]({ ...options, ...(compileOptions?.edmx || {}) });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
Logger.error(`Edmx error for service ${serviceName} - ${error.message}`);
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
break;
|
|
79
|
-
case COMPILER_TYPES.mcp:
|
|
80
|
-
if (!isMCPPluginReady()) {
|
|
81
|
-
throw new Error("MCP plugin is not available or not ready for use");
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
// Get all available CDS services
|
|
85
|
-
const allServices = Object.values(cds.services);
|
|
86
|
-
// Generate metadata from runtime services using adapter
|
|
87
|
-
const mcpResult = await buildMcpServerDefinition(allServices);
|
|
88
|
-
// Extract only the MCP content, not the ORD metadata
|
|
89
|
-
responseFile = mcpResult.mcp;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
Logger.error(`MCP server definition error - ${error.message}`);
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
break;
|
|
95
|
-
case COMPILER_TYPES.graphql:
|
|
96
|
-
try {
|
|
97
|
-
const { generateSchema4 } = require("@cap-js/graphql/lib/schema");
|
|
98
|
-
const { lexicographicSortSchema, printSchema } = require("graphql");
|
|
99
|
-
const linked = cds.linked(csn);
|
|
100
|
-
const srv = new cds.ApplicationService(serviceName, linked);
|
|
101
|
-
let schema = generateSchema4({ [serviceName]: srv });
|
|
102
|
-
schema = lexicographicSortSchema(schema);
|
|
103
|
-
responseFile = printSchema(schema);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
Logger.error("GraphQL SDL error:", error.message);
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
contentType:
|
|
112
|
-
compilerType === "graphql" ? "text/plain" : `application/${compilerType === "edmx" ? "xml" : "json"}`,
|
|
113
|
-
response: responseFile,
|
|
114
|
-
};
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
module.exports = getMetadata;
|
|
File without changes
|
|
File without changes
|