@cap-js/ord 1.4.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
 
@@ -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,64 @@
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 { ord, getMetadata } = require("./index");
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 { BuildError } = cds.build;
7
+ const os = require("os");
8
+ const { ord } = require("./index");
9
+ const { BUILD_DEFAULT_PATH, ORD_DOCUMENT_FILE_NAME } = require("./constants");
10
10
 
11
11
  module.exports = class OrdBuildPlugin extends cds.build.Plugin {
12
12
  static taskDefaults = { src: cds.env.folders.srv };
13
13
 
14
14
  init() {
15
- if (this.task.dest === undefined) {
16
- this.task.dest = path.join(cds.root, BUILD_DEFAULT_PATH);
15
+ this.task.dest = this.task.dest ?? path.join(cds.root, BUILD_DEFAULT_PATH);
16
+ }
17
+
18
+ async build() {
19
+ // @cap-js/graphql registers protocols and compile targets at runtime via cds-plugin.js,
20
+ // but cds build rebuilds cds.env afterward, losing both registrations.
21
+ // Re-apply so endpoints4() and cds.compile.to.graphql work during build.
22
+ if ("@cap-js/graphql" in cds.env.plugins && !cds.env.protocols?.graphql) {
23
+ cds.env.protocols.graphql = { path: "/graphql", impl: "@cap-js/graphql" };
24
+ require("@cap-js/graphql/lib/api").registerCompileTargets();
17
25
  }
26
+
27
+ return this.model()
28
+ .then((model) => ({ model, document: ord(model) }))
29
+ .then(({ model, document }) =>
30
+ Promise.all([
31
+ this.write(this._postProcess(document)).to(ORD_DOCUMENT_FILE_NAME),
32
+ this._generateResourcesFiles(model, [
33
+ ...(document.apiResources || []),
34
+ ...(document.eventResources || []),
35
+ ]),
36
+ ]),
37
+ )
38
+ .catch((error) => {
39
+ throw new cds.build.BuildError(`ORD build failed: ${error.message}`);
40
+ });
18
41
  }
19
42
 
20
- _createProgressBar(totalFiles) {
21
- const progressBar = new cliProgress.SingleBar({
43
+ _createProgressBar() {
44
+ return new cliProgress.SingleBar({
22
45
  format: "Processing resourcesFiles [{bar}] {percentage}% | {value}/{total} | ETA: {eta}s",
23
46
  barCompleteChar: "█",
24
47
  barIncompleteChar: "░",
25
48
  stopOnComplete: true,
26
49
  });
27
- progressBar.start(totalFiles, 0);
28
- return progressBar;
29
50
  }
30
51
 
31
- _countTotalFiles(resObj) {
32
- return resObj.reduce((total, resource) => {
33
- if (!resource.ordId.includes(ORD_SERVICE_NAME) && resource.resourceDefinitions) {
34
- return total + resource.resourceDefinitions.length;
35
- }
36
- return total;
37
- }, 0);
38
- }
52
+ _postProcess(document) {
53
+ const clone = _.cloneDeep(document);
39
54
 
40
- async _writeResourcesFiles(resObj, model, promises) {
41
- const totalFiles = this._countTotalFiles(resObj);
42
- const progressBar = this._createProgressBar(totalFiles);
43
- let completed = 0;
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
- }
55
+ [...(clone.apiResources || []), ...(clone.eventResources || [])]
56
+ .flatMap((resource) => resource.resourceDefinitions || [])
57
+ .forEach((resourceDefinition) => {
58
+ resourceDefinition.url = this._createRelativePath(resourceDefinition.url);
59
+ });
64
60
 
65
- postProcess(ordDocument) {
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;
61
+ return clone;
81
62
  }
82
63
 
83
64
  _createRelativePath(url) {
@@ -86,34 +67,42 @@ module.exports = class OrdBuildPlugin extends cds.build.Plugin {
86
67
  return path.join(...relative.replace(/:/g, "_").split("/"));
87
68
  }
88
69
 
89
- async build() {
90
- // @cap-js/graphql registers protocols and compile targets at runtime via cds-plugin.js,
91
- // but cds build rebuilds cds.env afterwards, losing both registrations.
92
- // Re-apply so endpoints4() and cds.compile.to.graphql work during build.
93
- if ("@cap-js/graphql" in cds.env.plugins && !cds.env.protocols?.graphql) {
94
- cds.env.protocols.graphql = { path: "/graphql", impl: "@cap-js/graphql" };
95
- require("@cap-js/graphql/lib/api").registerCompileTargets();
96
- }
97
-
98
- try {
99
- const model = await this.model();
100
- const ordDocument = ord(model);
101
- const postProcessedOrdDocument = this.postProcess(ordDocument);
70
+ _createWorkerPool(tasks, model) {
71
+ const size = cds.cli?.options?.taskOptions?.workers || os.availableParallelism();
72
+ const maxThreads = Math.ceil(Number(size) || os.availableParallelism() * Number(size.replace(/C$/i, "")));
102
73
 
103
- const promises = [];
104
- promises.push(this.write(postProcessedOrdDocument).to(ORD_DOCUMENT_FILE_NAME));
105
-
106
- if (ordDocument.apiResources?.length > 0) {
107
- await this._writeResourcesFiles(ordDocument.apiResources, model, promises);
108
- }
74
+ return new Piscina({
75
+ filename: path.join(__dirname, "threads", "compile.js"),
76
+ workerData: { model: JSON.parse(JSON.stringify(model)) },
77
+ minThreads: Math.min(tasks, maxThreads, Math.ceil(os.availableParallelism() * 1.5)),
78
+ maxThreads: Math.min(tasks, maxThreads, Math.ceil(os.availableParallelism() * 1.5)),
79
+ });
80
+ }
109
81
 
110
- if (ordDocument.eventResources?.length > 0) {
111
- await this._writeResourcesFiles(ordDocument.eventResources, model, promises);
112
- }
82
+ _extractCompileTasks(resources) {
83
+ return resources
84
+ .filter(({ resourceDefinitions }) => !!resourceDefinitions)
85
+ .flatMap(({ ordId, resourceDefinitions }) => resourceDefinitions.map(({ url }) => ({ url, ordId })));
86
+ }
113
87
 
114
- return Promise.all(promises);
115
- } catch (error) {
116
- throw new BuildError(`ORD build failed: ${error.message}`);
117
- }
88
+ _generateResourcesFiles(model, resources) {
89
+ const progressBar = this._createProgressBar();
90
+ const tasks = this._extractCompileTasks(resources);
91
+ const pool = !tasks.length ? null : this._createWorkerPool(tasks.length, model);
92
+
93
+ progressBar.start(tasks.length, 0);
94
+
95
+ return Promise.all(
96
+ tasks.map(({ url, ordId }) =>
97
+ pool.run({ url }).then((response) =>
98
+ this.write(response)
99
+ .to(path.join(ordId, url.split("/").pop()).replace(/:/g, "_"))
100
+ .then(() => progressBar.increment()),
101
+ ),
102
+ ),
103
+ ).finally(() => {
104
+ progressBar.stop();
105
+ return pool?.close({ force: true });
106
+ });
118
107
  }
119
108
  };
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 MCP_CUSTOM_TYPE = "sap:mcp-server-card:v0";
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
- MCP_CUSTOM_TYPE,
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]))
package/lib/interopCsn.js CHANGED
@@ -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) {
package/lib/metaData.js CHANGED
@@ -5,7 +5,6 @@ const { COMPILER_TYPES, OPENAPI_SERVERS_ANNOTATION } = require("./constants");
5
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");
9
8
 
10
9
  /**
11
10
  * Read @OpenAPI.servers annotation from service definition
@@ -77,18 +76,10 @@ const getMetadata = async (url, model = null) => {
77
76
  }
78
77
  break;
79
78
  case COMPILER_TYPES.mcp:
80
- if (!isMCPPluginReady()) {
81
- throw new Error("MCP plugin is not available or not ready for use");
82
- }
83
79
  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;
80
+ responseFile = await cds.compile(csn).to["mcp"]({ ...options, ...(compileOptions?.mcp || {}) });
90
81
  } catch (error) {
91
- Logger.error(`MCP server definition error - ${error.message}`);
82
+ Logger.error("MCP error:", error.message);
92
83
  throw error;
93
84
  }
94
85
  break;
@@ -1,6 +1,6 @@
1
1
  const cds = require("@sap/cds");
2
2
  const ord = require("./ord.js");
3
- const getMetadata = require("./metaData.js");
3
+ const compileMetadata = require("./metaData.js");
4
4
  const defaults = require("./defaults.js");
5
5
  const { createAuthConfig, createAuthMiddleware } = require("./auth/authentication.js");
6
6
  const Logger = require("./logger.js");
@@ -34,7 +34,7 @@ class OpenResourceDiscoveryService extends cds.ApplicationService {
34
34
  // Handler for metadata requests (oas3, edmx, csn, etc.)
35
35
  const metadataHandler = async (req, res) => {
36
36
  try {
37
- const { contentType, response } = await getMetadata(req.url);
37
+ const { contentType, response } = await compileMetadata(req.url);
38
38
  return res.status(200).contentType(contentType).send(response);
39
39
  } catch (error) {
40
40
  Logger.error(error, "Error while processing the resource definition document");
package/lib/ord.js CHANGED
@@ -11,14 +11,12 @@ const {
11
11
  createEntityTypeMappingsItemTemplate,
12
12
  createEventResourceTemplate,
13
13
  createGroupsTemplateForService,
14
- createMCPAPIResourceTemplate,
15
14
  _propagateORDVisibility,
16
15
  } = require("./templates");
17
16
  const { getIntegrationDependencies } = require("./integrationDependency");
18
17
  const { extendCustomORDContentIfExists } = require("./extendOrdWithCustom");
19
18
  const { getRFC3339Date } = require("./date");
20
19
  const { createAuthConfig } = require("./auth/authentication");
21
- const { isMCPPluginReady } = require("./mcpAdapter");
22
20
 
23
21
  const Logger = require("./logger");
24
22
  const _ = require("lodash");
@@ -26,24 +24,6 @@ const cds = require("@sap/cds");
26
24
  const defaults = require("./defaults");
27
25
  const path = require("path");
28
26
 
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
27
  const initializeAppConfig = (csn) => {
48
28
  const packageJson = _loadPackageJson();
49
29
  const packageName = packageJson.name;
@@ -261,9 +241,6 @@ const _getAPIResources = (csn, appConfig, packageIds, accessStrategies) => {
261
241
  ),
262
242
  );
263
243
 
264
- // Conditionally add MCP API resource if plugin is available
265
- _addMCPResourceIfAvailable(apiResources, appConfig, packageIds, accessStrategies);
266
-
267
244
  return apiResources;
268
245
  };
269
246
 
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,10 @@
1
+ const { workerData } = require("piscina");
2
+
3
+ const compileMetadata = require("../metaData");
4
+
5
+ module.exports = ({ url }) => {
6
+ // JSON round-trip: CDS compiler output may contain Generator objects
7
+ // that cannot be transferred via postMessage (structured clone algorithm).
8
+ return compileMetadata(url, workerData.model) //
9
+ .then(({ response }) => JSON.parse(JSON.stringify(response)));
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/ord",
3
- "version": "1.4.5",
3
+ "version": "1.5.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)",
@@ -45,7 +45,8 @@
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
52
  "node": ">=18 <23"
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
- };