-
{{or apiTitle name}}
+
{{or title name}}
{{version}}
MCP
diff --git a/portals/developer-portal/src/dto/apiDto.js b/portals/developer-portal/src/dto/apiDto.js
index 22e9805ac3..a022f8e3d4 100644
--- a/portals/developer-portal/src/dto/apiDto.js
+++ b/portals/developer-portal/src/dto/apiDto.js
@@ -46,7 +46,7 @@ class APIDTO {
class APIInfo {
constructor(apiInfo) {
this.name = apiInfo.name;
- this.apiTitle = apiInfo.metadata_search?.apiTitle || null;
+ this.title = apiInfo.metadata_search?.title || null;
this.remotes = apiInfo.metadata_search?.remotes || [];
this.version = apiInfo.version;
this.description = apiInfo.description;
diff --git a/portals/developer-portal/src/dto/mcpServerDto.js b/portals/developer-portal/src/dto/mcpServerDto.js
index 73d1b22347..049de8f074 100644
--- a/portals/developer-portal/src/dto/mcpServerDto.js
+++ b/portals/developer-portal/src/dto/mcpServerDto.js
@@ -42,7 +42,7 @@ class ServerResponseDTO {
this.server = {
$schema: 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json',
name: source.metadata_search?.proxyId || source.name,
- title: source.metadata_search?.apiTitle || undefined,
+ title: source.metadata_search?.title || undefined,
version: source.version,
description: source.description,
remotes
diff --git a/portals/developer-portal/src/pages/settings/page.hbs b/portals/developer-portal/src/pages/settings/page.hbs
index 3938c019cd..a9ebefdf4e 100644
--- a/portals/developer-portal/src/pages/settings/page.hbs
+++ b/portals/developer-portal/src/pages/settings/page.hbs
@@ -1604,7 +1604,7 @@
if (specStepLabel) specStepLabel.textContent = isMcp ? 'Tools Schema' : 'Spec';
var specDesc = document.querySelector('#cfg-wstep-1 .cfg-wstep-desc');
if (specDesc) specDesc.innerHTML = isMcp
- ? 'Upload the MCP tools schema that defines this server’s tools. * Required. Accepted: YAML or JSON (e.g. schemaDefinition.yaml).'
+ ? 'Upload the MCP tools schema that defines this server’s tools. * Required. Accepted: YAML or JSON (e.g. definition.yaml).'
: 'Upload the contract that defines this API. * Required. Accepted: OpenAPI (.json / .yaml), AsyncAPI, GraphQL SDL (.graphql), or WSDL (.wsdl / .xml).';
var specOnFileTxt = document.querySelector('#wz-spec-onfile span');
if (specOnFileTxt) specOnFileTxt.textContent = isMcp
@@ -1749,10 +1749,10 @@
};
var fd = new FormData();
- fd.append('apiMetadata', JSON.stringify(meta));
- /* For MCP servers the uploaded contract is the tools schema (stored as SCHEMA_DEFINITION),
- so it goes in the schemaDefinition field; every other type uses apiDefinition. */
- if (specFile) fd.append(meta.type === 'Mcp' ? 'schemaDefinition' : 'apiDefinition', specFile);
+ fd.append('metadata', JSON.stringify(meta));
+ /* The single `definition` field carries the contract for every type. For MCP servers it is
+ the tools schema (stored as SCHEMA_DEFINITION); for other types it is the API definition. */
+ if (specFile) fd.append('definition', specFile);
docFiles.forEach(function(f) { fd.append('docs', f); });
/* MCP servers are created/updated via /mcp-servers (which requires type MCP and rejects
@@ -1921,7 +1921,7 @@
subscriptionPlans: apiData.subscriptionPlans || [],
});
var fd = new FormData();
- fd.append('apiMetadata', JSON.stringify(meta));
+ fd.append('metadata', JSON.stringify(meta));
var putRes = await fetch(window.devportalApi.root(base+'/'+encodeURIComponent(apiId)), {
method: 'PUT',
headers: { 'X-CSRF-Token': window.devportalApi.csrfToken() },
@@ -2304,7 +2304,7 @@
}
try {
var fd = new FormData();
- fd.append('apiContent', contentZipFile);
+ fd.append('content', contentZipFile);
var res = await fetch(window.devportalApi.root('/apis/' + encodeURIComponent(editingId) + '/assets'), {
method: 'PUT',
headers: { 'X-CSRF-Token': window.devportalApi.csrfToken() },
diff --git a/portals/developer-portal/src/services/adminService.js b/portals/developer-portal/src/services/adminService.js
index 1fd9c7656e..be2293c6da 100644
--- a/portals/developer-portal/src/services/adminService.js
+++ b/portals/developer-portal/src/services/adminService.js
@@ -278,7 +278,7 @@ const updateOrganization = async (req, res) => {
const devportalMode = payload.configuration?.devportalMode;
if (devportalMode !== undefined && !Object.values(constants.DEVPORTAL_MODE).includes(devportalMode)) {
- return res.status(400).json({ error: `Invalid devportalMode '${devportalMode}'. Must be one of: ${Object.values(constants.DEVPORTAL_MODE).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid devportalMode '${devportalMode}'. Must be one of: ${Object.values(constants.DEVPORTAL_MODE).join(', ')}.`);
}
let updatedOrg;
diff --git a/portals/developer-portal/src/services/apiMetadataService.js b/portals/developer-portal/src/services/apiMetadataService.js
index 3077a74360..5a23503a2c 100644
--- a/portals/developer-portal/src/services/apiMetadataService.js
+++ b/portals/developer-portal/src/services/apiMetadataService.js
@@ -53,6 +53,8 @@ const createAPIMetadata = async (req, res) => {
let apiDefinitionFile, apiFileName = "";
let fullApiBundle;
const apiArtifactFile = req.files?.artifact?.[0];
+ // Single multipart field carrying the contract on a non-artifact upload; interpreted by type below.
+ const definitionUpload = req.files?.definition?.[0] || null;
try {
let artifactApiContent = [];
@@ -77,42 +79,41 @@ const createAPIMetadata = async (req, res) => {
file.key = filenameToKey[file.fileName];
}
});
- } else if (req.files?.api?.[0]) {
+ } else if (req.files?.metadata?.[0]) {
+ // `metadata` supplied as an uploaded YAML/JSON file (k8s-style artifact document).
apiMetadata = parseApiMetadataFromYamlRequest(req);
- if (req.files?.apiDefinition?.[0]) {
- const file = req.files.apiDefinition[0];
- const preparedDefinition = prepareApiDefinitionForStorage(file.originalname, file.buffer);
- apiDefinitionFile = preparedDefinition.apiDefinitionFile;
- apiFileName = preparedDefinition.apiDefinitionFileName;
- }
} else {
- apiMetadata = JSON.parse(req.body.apiMetadata);
+ if (!req.body.metadata) {
+ throw new Sequelize.ValidationError("Missing or invalid fields in the request payload");
+ }
+ apiMetadata = JSON.parse(req.body.metadata);
// Type is resolved centrally below, using this raw (possibly omitted) value —
// don't resolve here, or an omitted type on a /mcp-servers request can no
// longer be told apart from an explicit REST.
if (apiMetadata.id) {
apiMetadata.handle = apiMetadata.id;
}
- if (req.files?.apiDefinition?.[0]) {
- const file = req.files.apiDefinition[0];
- const preparedDefinition = prepareApiDefinitionForStorage(file.originalname, file.buffer);
- apiDefinitionFile = preparedDefinition.apiDefinitionFile;
- apiFileName = preparedDefinition.apiDefinitionFileName;
- }
}
apiMetadata.type = resolveTypeOrReject(apiMetadata.type, req.__forceApiType);
- // Validate input. An MCP server's contract IS its tools schema (schemaDefinition) —
- // it has no OpenAPI-style apiDefinition, so it requires a schemaDefinition and any
- // apiDefinition sent alongside is ignored (mirrors how sampleSeeder creates MCP servers
- // from api.yaml + schemaDefinition.yaml with no definition file). GraphQL may carry its
- // SDL as a schemaDefinition; every other type requires an apiDefinition.
+ // Validate input. A single `definition` multipart field carries the contract for a
+ // non-artifact upload. Its meaning depends on the resolved type: for REST/SOAP/etc. it
+ // is the OpenAPI/WSDL definition (prepared and stored as an apiDefinition here); for
+ // GraphQL it is the SDL and for MCP it is the tools schema — both stored by their
+ // type-specific branches below. An MCP server's contract IS its tools schema; it has no
+ // OpenAPI-style apiDefinition (mirrors how sampleSeeder creates MCP servers from api.yaml
+ // plus a definition file).
const isMcp = apiMetadata.type === constants.API_TYPE.MCP;
- const hasGraphQLSchema = apiMetadata.type === constants.API_TYPE.GRAPHQL &&
- req.files?.schemaDefinition?.[0];
+ const isGraphQL = apiMetadata.type === constants.API_TYPE.GRAPHQL;
+ if (definitionUpload && !fullApiBundle && !isMcp && !isGraphQL) {
+ const preparedDefinition = prepareApiDefinitionForStorage(definitionUpload.originalname, definitionUpload.buffer);
+ apiDefinitionFile = preparedDefinition.apiDefinitionFile;
+ apiFileName = preparedDefinition.apiDefinitionFileName;
+ }
+ const hasGraphQLSchema = isGraphQL && (definitionUpload || fullApiBundle?.apiDefinitionFile);
const hasMcpSchema = isMcp &&
- (req.files?.schemaDefinition?.[0] || fullApiBundle?.schemaDefinitionFile);
+ (definitionUpload || fullApiBundle?.schemaDefinitionFile);
const hasContract = isMcp ? hasMcpSchema : (apiDefinitionFile || hasGraphQLSchema);
if (!apiMetadata.name || !hasContract || !apiMetadata.endPoints) {
throw new Sequelize.ValidationError(
@@ -183,8 +184,8 @@ const createAPIMetadata = async (req, res) => {
}
await tagDao.createApiMapping(orgId, apiId, tags, userId, t);
}
- // store api definition file (skipped for GraphQL — schema stored below via schemaDefinition;
- // and for MCP, whose contract is its schemaDefinition — an MCP has no apiDefinition)
+ // store api definition file (skipped for GraphQL — schema stored below via the definition
+ // field; and for MCP, whose contract is its tools schema — an MCP has no apiDefinition)
if (apiDefinitionFile && !isMcp) {
await apiFileDao.store(apiDefinitionFile, apiFileName, apiId, constants.DOC_TYPES.API_DEFINITION, userId, t);
}
@@ -197,8 +198,8 @@ const createAPIMetadata = async (req, res) => {
// Save MCP tools as schema definition if the API type is MCP
if (constants.API_TYPE.MCP === apiMetadata.type) {
let schemaFile;
- if (req.files?.schemaDefinition?.[0]) {
- schemaFile = req.files.schemaDefinition[0];
+ if (definitionUpload) {
+ schemaFile = definitionUpload;
} else if (fullApiBundle?.schemaDefinitionFile) {
schemaFile = {
originalname: fullApiBundle.schemaDefinitionFileName,
@@ -221,8 +222,8 @@ const createAPIMetadata = async (req, res) => {
}
}
- if (constants.API_TYPE.GRAPHQL === apiMetadata.type && req.files?.schemaDefinition?.[0]) {
- const file = req.files.schemaDefinition[0];
+ if (constants.API_TYPE.GRAPHQL === apiMetadata.type && definitionUpload) {
+ const file = definitionUpload;
const schemaDefinitionFile = file.buffer;
logger.debug('GraphQL schema definition file received', {
apiId: apiId,
@@ -370,7 +371,7 @@ const getAPIMetadata = async (req, res) => {
// Create response object
res.status(200).send(retrievedAPI);
} else {
- res.status(404).send("API not found");
+ util.sendError(res, 404, 'API not found');
}
} catch (error) {
logger.error('API metadata retrieval failed', {
@@ -402,7 +403,7 @@ const getAllAPIMetadata = async (req, res) => {
try {
const orgId = req.orgId;
const searchTerm = req.query.query;
- const apiName = req.query.apiName;
+ const apiName = req.query.name;
const apiVersion = req.query.version;
const tags = req.query.tags;
const view = req.query.view;
@@ -414,7 +415,7 @@ const getAllAPIMetadata = async (req, res) => {
stack: error.stack,
orgId: req.orgId,
searchTerm: req.query.query,
- apiName: req.query.apiName,
+ apiName: req.query.name,
apiVersion: req.query.version,
tags: req.query.tags,
view: req.query.view
@@ -464,11 +465,13 @@ const updateAPIMetadata = async (req, res) => {
let apiDefinitionFile, apiFileName = "";
let fullApiBundle;
const apiArtifactFile = req.files?.artifact?.[0];
+ // Single multipart field carrying the contract on a non-artifact upload; interpreted by type below.
+ const definitionUpload = req.files?.definition?.[0] || null;
try {
apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
// `type` is immutable after creation — resolveTypeOrReject below only enforces the
// /apis-vs-/mcp-servers family boundary, not that the value matches this specific
@@ -503,28 +506,20 @@ const updateAPIMetadata = async (req, res) => {
file.key = filenameToKey[file.fileName];
}
});
- } else if (req.files?.api?.[0]) {
+ } else if (req.files?.metadata?.[0]) {
+ // `metadata` supplied as an uploaded YAML/JSON file (k8s-style artifact document).
apiMetadata = parseApiMetadataFromYamlRequest(req);
- if (req.files?.apiDefinition?.[0]) {
- const file = req.files.apiDefinition[0];
- const preparedDefinition = prepareApiDefinitionForStorage(file.originalname, file.buffer);
- apiDefinitionFile = preparedDefinition.apiDefinitionFile;
- apiFileName = preparedDefinition.apiDefinitionFileName;
- }
} else {
- apiMetadata = JSON.parse(req.body.apiMetadata);
+ if (!req.body.metadata) {
+ throw new Sequelize.ValidationError("Missing or invalid fields in the request payload");
+ }
+ apiMetadata = JSON.parse(req.body.metadata);
// Type is resolved centrally below, using this raw (possibly omitted) value —
// don't resolve here, or an omitted type on a /mcp-servers request can no
// longer be told apart from an explicit REST.
if (apiMetadata.id) {
apiMetadata.handle = apiMetadata.id;
}
- if (req.files?.apiDefinition?.[0]) {
- const file = req.files.apiDefinition[0];
- const preparedDefinition = prepareApiDefinitionForStorage(file.originalname, file.buffer);
- apiDefinitionFile = preparedDefinition.apiDefinitionFile;
- apiFileName = preparedDefinition.apiDefinitionFileName;
- }
}
apiMetadata.type = resolveTypeOrReject(apiMetadata.type, req.__forceApiType);
@@ -532,7 +527,15 @@ const updateAPIMetadata = async (req, res) => {
throw new CustomError(409, 'Conflict', `API type cannot be changed after creation (existing type is '${existingType}')`);
}
// An MCP server's contract is its tools schema (handled below); it has no apiDefinition.
+ // The single `definition` field is prepared as an apiDefinition only for the types that
+ // store one (not MCP, not GraphQL — those are handled by their branches below).
const isMcp = apiMetadata.type === constants.API_TYPE.MCP;
+ const isGraphQL = apiMetadata.type === constants.API_TYPE.GRAPHQL;
+ if (definitionUpload && !fullApiBundle && !isMcp && !isGraphQL) {
+ const preparedDefinition = prepareApiDefinitionForStorage(definitionUpload.originalname, definitionUpload.buffer);
+ apiDefinitionFile = preparedDefinition.apiDefinitionFile;
+ apiFileName = preparedDefinition.apiDefinitionFileName;
+ }
// Validate input — spec file is optional on update (already stored from create)
if (!apiMetadata.name || !apiMetadata.endPoints) {
@@ -661,7 +664,7 @@ const updateAPIMetadata = async (req, res) => {
}
}
// Update MCP tools schema definition if the API type is MCP
- const hasSchemaDefinitionFile = !!req.files?.schemaDefinition?.[0] || !!fullApiBundle?.schemaDefinitionFile;
+ const hasSchemaDefinitionFile = !!definitionUpload || !!fullApiBundle?.schemaDefinitionFile;
logger.debug('Processing MCP API schema definition', {
hasSchemaDefinition: hasSchemaDefinitionFile,
apiType: apiMetadata.type,
@@ -669,8 +672,8 @@ const updateAPIMetadata = async (req, res) => {
});
if (constants.API_TYPE.MCP === apiMetadata.type && hasSchemaDefinitionFile) {
let schemaFile;
- if (req.files?.schemaDefinition?.[0]) {
- schemaFile = req.files.schemaDefinition[0];
+ if (definitionUpload) {
+ schemaFile = definitionUpload;
} else if (fullApiBundle?.schemaDefinitionFile) {
schemaFile = {
originalname: fullApiBundle.schemaDefinitionFileName,
@@ -693,8 +696,8 @@ const updateAPIMetadata = async (req, res) => {
}
}
- if (constants.API_TYPE.GRAPHQL === apiMetadata.type && req.files?.schemaDefinition?.[0]) {
- const file = req.files.schemaDefinition[0];
+ if (constants.API_TYPE.GRAPHQL === apiMetadata.type && definitionUpload) {
+ const file = definitionUpload;
const schemaDefinitionFile = file.buffer;
const schemaFileName = constants.FILE_NAME.API_DEFINITION_GRAPHQL;
logger.debug('GraphQL schema definition file received for update', {
@@ -744,7 +747,7 @@ const deleteAPIMetadata = async (req, res) => {
try {
apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
const subApis = await subDao.listByApi(orgId, apiId);
if (subApis.length > 0) {
@@ -790,7 +793,7 @@ const createAPITemplate = async (req, res) => {
const { apiId: apiHandle } = req.params;
const apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
const userId = util.resolveActor(req);
const zipFilePath = req.file.path;
@@ -893,7 +896,7 @@ const createAPITemplate = async (req, res) => {
};
const createAPIContent = async (req, res) => {
- const uploadedFile = req.files?.apiContent?.[0] ?? req.file;
+ const uploadedFile = req.files?.content?.[0] ?? req.file;
logger.info('Creating API content...', {
orgId: req.orgId,
apiId: req.params.apiId,
@@ -904,7 +907,7 @@ const createAPIContent = async (req, res) => {
const { apiId: apiHandle } = req.params;
const apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
const userId = util.resolveActor(req);
let apiContent = await extractApiContentFromUploadedZip(uploadedFile, orgId, apiId, 'classic');
@@ -963,7 +966,7 @@ const updateAPITemplate = async (req, res) => {
const { apiId: apiHandle } = req.params;
const apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
const userId = util.resolveActor(req);
let imageMetadata;
@@ -1061,7 +1064,7 @@ const updateAPITemplate = async (req, res) => {
};
const updateAPIContent = async (req, res) => {
- const uploadedFile = req.files?.apiContent?.[0] ?? req.file;
+ const uploadedFile = req.files?.content?.[0] ?? req.file;
logger.info('Updating API content...', {
orgId: req.orgId,
apiId: req.params.apiId,
@@ -1072,7 +1075,7 @@ const updateAPIContent = async (req, res) => {
const { apiId: apiHandle } = req.params;
const apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
const userId = util.resolveActor(req);
let imageMetadata;
@@ -1131,12 +1134,12 @@ const getAPIFile = async (req, res) => {
try {
apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
const fileExtension = path.extname(apiFileName).toLowerCase();
apiFileResponse = await apiFileDao.get(apiFileName, type, orgId, apiId);
if (!apiFileResponse) {
- return res.status(404).send("API File not found");
+ return util.sendError(res, 404, 'API File not found');
}
apiFile = apiFileResponse.file_content;
//convert to text to check if link
@@ -1196,7 +1199,7 @@ const deleteAPIFile = async (req, res) => {
try {
const apiId = await resolveScopedApiId(req, orgId, apiHandle);
if (!apiId) {
- return res.status(404).send("API not found");
+ return util.sendError(res, 404, 'API not found');
}
let apiFileResponse;
if (apiFileName) {
@@ -1207,7 +1210,7 @@ const deleteAPIFile = async (req, res) => {
if (!apiFileResponse) {
res.status(204).send();
} else {
- res.status(404).send("API Content not found");
+ util.sendError(res, 404, 'API Content not found');
}
} catch (error) {
logger.error('API content deletion failed', {
@@ -1270,7 +1273,7 @@ const createSubscriptionPlan = async (req, res) => {
});
if (!subscriptionPlan || typeof subscriptionPlan !== "object") {
- return res.status(400).json({ message: "Request body is missing or invalid" });
+ return util.sendError(res, 400, "Request body is missing or invalid");
}
try {
@@ -1312,7 +1315,7 @@ const createSubscriptionPlans = async (req, res) => {
const userId = util.resolveActor(req);
if (!Array.isArray(subscriptionPlans) || subscriptionPlans.length === 0) {
- return res.status(400).json({ message: "Missing or invalid fields in the request payload" });
+ return util.sendError(res, 400, "Missing or invalid fields in the request payload");
}
const createdRecords = [];
@@ -1359,7 +1362,7 @@ const updateSubscriptionPlan = async (req, res) => {
const userId = util.resolveActor(req);
if (!subscriptionPlan || typeof subscriptionPlan !== "object") {
- return res.status(400).json({ message: "Request body is missing or invalid" });
+ return util.sendError(res, 400, "Request body is missing or invalid");
}
try {
@@ -1398,7 +1401,7 @@ const updateSubscriptionPlans = async (req, res) => {
const userId = util.resolveActor(req);
if (!Array.isArray(subscriptionPlans) || subscriptionPlans.length === 0) {
- return res.status(400).json({ message: "Missing or invalid fields in the request payload" });
+ return util.sendError(res, 400, "Missing or invalid fields in the request payload");
}
const updatedRecords = [];
@@ -1539,7 +1542,7 @@ const getLabel = async (req, res) => {
try {
const labelId = await labelDao.getIdByHandle(orgId, labelHandle);
if (!labelId) {
- return res.status(404).json({ code: '404', message: 'Not Found', description: 'Label not found' });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Label not found' }] });
}
const record = await labelDao.findById(orgId, labelId);
res.status(200).json(new LabelDTO(record));
@@ -1558,7 +1561,7 @@ const updateLabel = async (req, res) => {
try {
const labelId = await labelDao.getIdByHandle(orgId, labelHandle);
if (!labelId) {
- return res.status(404).json({ code: '404', message: 'Not Found', description: 'Label not found' });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Label not found' }] });
}
const record = await labelDao.updateById(orgId, labelId, label, userId);
res.status(200).json(new LabelDTO(record));
@@ -1575,7 +1578,7 @@ const deleteLabel = async (req, res) => {
try {
const labelId = await labelDao.getIdByHandle(orgId, labelHandle);
if (!labelId) {
- return res.status(404).json({ code: '404', message: 'Not Found', description: 'Label not found' });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Label not found' }] });
}
await labelDao.deleteById(orgId, labelId);
res.status(204).send();
@@ -1715,7 +1718,7 @@ const getView = async (req, res) => {
if (view) {
res.status(200).send(view);
} else {
- res.status(404).send(`View ${name} not found`);
+ util.sendError(res, 404, `View ${name} not found`);
}
} catch (error) {
logger.error('view retrieve error failed', {
@@ -1907,36 +1910,29 @@ async function extractFullApiBundleFromUploadedZip(zipFile, orgId, apiId) {
const apiMetadata = parseApiMetadataFromYamlFile(path.basename(metadataFilePath), apiMetadataBuffer);
const isMcp = apiMetadata.type === constants.API_TYPE.MCP;
- // The tools schema (MCP) lives in its own file, distinct from an apiDefinition.
- const schemaDefinitionFilePath = await util.findFileByNameRecursive(rootPath, [
- constants.FILE_NAME.SCHEMA_DEFINITION_FILE_NAME,
- constants.FILE_NAME.SCHEMA_DEFINITION_YAML_FILE_NAME,
- ]);
+ // The contract lives in a `definition` file. An MCP server's contract IS its tools
+ // schema (returned as schemaDefinitionFile so the MCP branch stores it under
+ // SCHEMA_DEFINITION); every other type carries an OpenAPI/WSDL/SDL definition
+ // (returned as apiDefinitionFile). Both use the same file name — the resolved
+ // API type decides how it is interpreted.
+ const definitionCandidates = [
+ constants.FILE_NAME.API_DEFINITION_YAML_FILE_NAME,
+ 'definition.yml',
+ constants.FILE_NAME.API_DEFINITION_FILE_NAME,
+ ];
let schemaDefinitionFile;
let schemaDefinitionFileName;
- if (schemaDefinitionFilePath) {
- schemaDefinitionFile = await fs.readFile(schemaDefinitionFilePath);
- schemaDefinitionFileName = path.basename(schemaDefinitionFilePath);
- }
-
- // An MCP server's contract IS its schemaDefinition (tools) — it has no apiDefinition,
- // so an MCP zip must carry a schemaDefinition and needs no definition file. Every other
- // API type requires a definition file and has no schemaDefinition here.
let apiDefinitionFile;
let apiDefinitionFileName;
if (isMcp) {
- if (!schemaDefinitionFile) {
- throw new Sequelize.ValidationError("Invalid MCP server zip: missing schemaDefinition file (schemaDefinition.yaml/json)");
+ const schemaDefinitionFilePath = await util.findFileByNameRecursive(rootPath, definitionCandidates);
+ if (!schemaDefinitionFilePath) {
+ throw new Sequelize.ValidationError("Invalid MCP server zip: missing definition file (definition.yaml/yml/json)");
}
+ schemaDefinitionFile = await fs.readFile(schemaDefinitionFilePath);
+ schemaDefinitionFileName = path.basename(schemaDefinitionFilePath);
} else {
- const definitionFilePath = await util.findFileByNameRecursive(rootPath, [
- 'definition.yaml',
- 'definition.yml',
- 'definition.json',
- 'apiDefinition.yaml',
- 'apiDefinition.yml',
- 'apiDefinition.json',
- ]);
+ const definitionFilePath = await util.findFileByNameRecursive(rootPath, definitionCandidates);
if (!definitionFilePath) {
throw new Sequelize.ValidationError("Invalid full API zip: missing definition file (definition.yaml/yml/json)");
}
@@ -2016,9 +2012,12 @@ function mapDevportalYamlToApiMetadata(parsedYaml) {
}
function parseApiMetadataFromYamlFile(fileName, fileBuffer) {
- const allowedMetadataFileNames = new Set(['api.yaml', 'mcp.yaml', 'devportal.yaml']);
+ const allowedMetadataFileNames = new Set([
+ 'metadata.yaml', 'metadata.yml', 'metadata.json',
+ 'api.yaml', 'mcp.yaml', 'devportal.yaml',
+ ]);
if (!allowedMetadataFileNames.has(String(fileName).toLowerCase())) {
- throw new Sequelize.ValidationError("Invalid metadata file name. Expected 'api.yaml', 'mcp.yaml' or 'devportal.yaml'");
+ throw new Sequelize.ValidationError("Invalid metadata file name. Expected 'metadata.yaml', 'metadata.yml', 'metadata.json', 'api.yaml', 'mcp.yaml' or 'devportal.yaml'");
}
let parsedYaml;
@@ -2032,14 +2031,14 @@ function parseApiMetadataFromYamlFile(fileName, fileBuffer) {
}
function parseApiMetadataFromYamlRequest(req) {
- const apiFile = req.files?.api?.[0];
- if (!apiFile?.buffer) {
+ const metadataFile = req.files?.metadata?.[0];
+ if (!metadataFile?.buffer) {
throw new Sequelize.ValidationError(
- "Missing required multipart file field: 'api'"
+ "Missing required multipart file field: 'metadata'"
);
}
- return parseApiMetadataFromYamlFile(apiFile.originalname, apiFile.buffer);
+ return parseApiMetadataFromYamlFile(metadataFile.originalname, metadataFile.buffer);
}
function legacyLimitsFromSpec(spec) {
diff --git a/portals/developer-portal/src/services/apiWorkflowService.js b/portals/developer-portal/src/services/apiWorkflowService.js
index 95a0993443..f7e226661d 100644
--- a/portals/developer-portal/src/services/apiWorkflowService.js
+++ b/portals/developer-portal/src/services/apiWorkflowService.js
@@ -186,23 +186,23 @@ const createAPIWorkflow = async (req, res) => {
resolvedHandle = `workflow-${suffix}`;
}
if (id && id.trim() && !HANDLE_PATTERN.test(resolvedHandle)) {
- return res.status(400).json({ message: "Invalid 'id'. Must contain only letters, numbers, underscores, and hyphens." });
+ return util.sendError(res, 400, "Invalid 'id'. Must contain only letters, numbers, underscores, and hyphens.");
}
const resolvedContentType = contentType || constants.API_WORKFLOW_CONTENT_TYPE.ARAZZO;
if (!Object.values(constants.API_WORKFLOW_CONTENT_TYPE).includes(resolvedContentType)) {
- return res.status(400).json({ message: `Invalid contentType. Must be one of: ${Object.values(constants.API_WORKFLOW_CONTENT_TYPE).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid contentType. Must be one of: ${Object.values(constants.API_WORKFLOW_CONTENT_TYPE).join(', ')}.`);
}
if (status && !Object.values(constants.API_WORKFLOW_STATUS).includes(status)) {
- return res.status(400).json({ message: `Invalid status. Must be one of: ${Object.values(constants.API_WORKFLOW_STATUS).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid status. Must be one of: ${Object.values(constants.API_WORKFLOW_STATUS).join(', ')}.`);
}
if (agentVisibility && !Object.values(constants.AGENT_VISIBILITY).includes(agentVisibility)) {
- return res.status(400).json({ message: `Invalid agentVisibility. Must be one of: ${Object.values(constants.AGENT_VISIBILITY).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid agentVisibility. Must be one of: ${Object.values(constants.AGENT_VISIBILITY).join(', ')}.`);
}
const resolvedContent = resolvedContentType === 'MD'
? (markdownContent || null)
: normalizeToJSON(apiWorkflowDefinition);
if (resolvedContentType !== 'MD' && resolvedContent === null) {
- return res.status(400).json({ message: 'Invalid API workflow definition: content could not be parsed as valid JSON or YAML.' });
+ return util.sendError(res, 400, 'Invalid API workflow definition: content could not be parsed as valid JSON or YAML.');
}
let t;
try {
@@ -235,13 +235,13 @@ const createAPIWorkflow = async (req, res) => {
} catch (error) {
if (t) await t.rollback();
if (error instanceof UniqueConstraintError) {
- return res.status(409).json({ message: 'An API workflow with this handle already exists. Please use a different handle.' });
+ return util.sendError(res, 409, 'An API workflow with this handle already exists. Please use a different handle.');
}
if (error instanceof CustomError) {
- return res.status(error.statusCode).json({ message: error.message });
+ return util.sendError(res, error.statusCode, error.message);
}
logger.error('Error creating API Workflow', { error: error.message, stack: error.stack });
- res.status(500).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_CREATE_ERROR });
+ util.sendError(res, 500, constants.ERROR_MESSAGE.API_WORKFLOW_CREATE_ERROR);
}
};
@@ -251,23 +251,23 @@ const updateAPIWorkflow = async (req, res) => {
const userId = util.resolveActor(req);
const { displayName, id, description, agentPrompt, status, agentVisibility, apiWorkflowDefinition, markdownContent, contentType } = req.body;
if (status !== undefined && !Object.values(constants.API_WORKFLOW_STATUS).includes(status)) {
- return res.status(400).json({ message: `Invalid status. Must be one of: ${Object.values(constants.API_WORKFLOW_STATUS).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid status. Must be one of: ${Object.values(constants.API_WORKFLOW_STATUS).join(', ')}.`);
}
if (agentVisibility !== undefined && !Object.values(constants.AGENT_VISIBILITY).includes(agentVisibility)) {
- return res.status(400).json({ message: `Invalid agentVisibility. Must be one of: ${Object.values(constants.AGENT_VISIBILITY).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid agentVisibility. Must be one of: ${Object.values(constants.AGENT_VISIBILITY).join(', ')}.`);
}
if (contentType !== undefined && !Object.values(constants.API_WORKFLOW_CONTENT_TYPE).includes(contentType)) {
- return res.status(400).json({ message: `Invalid contentType. Must be one of: ${Object.values(constants.API_WORKFLOW_CONTENT_TYPE).join(', ')}.` });
+ return util.sendError(res, 400, `Invalid contentType. Must be one of: ${Object.values(constants.API_WORKFLOW_CONTENT_TYPE).join(', ')}.`);
}
if (id !== undefined && !HANDLE_PATTERN.test(id)) {
- return res.status(400).json({ message: "Invalid 'id'. Must contain only letters, numbers, underscores, and hyphens." });
+ return util.sendError(res, 400, "Invalid 'id'. Must contain only letters, numbers, underscores, and hyphens.");
}
const resolvedContentType = contentType;
const resolvedContent = resolvedContentType === 'MD'
? (markdownContent !== undefined ? markdownContent : undefined)
: (apiWorkflowDefinition !== undefined ? normalizeToJSON(apiWorkflowDefinition) : undefined);
if (resolvedContentType !== 'MD' && apiWorkflowDefinition !== undefined && resolvedContent === null) {
- return res.status(400).json({ message: 'Invalid API workflow definition: content could not be parsed as valid JSON or YAML.' });
+ return util.sendError(res, 400, 'Invalid API workflow definition: content could not be parsed as valid JSON or YAML.');
}
const t = await sequelize.transaction();
try {
@@ -275,7 +275,7 @@ const updateAPIWorkflow = async (req, res) => {
const existing = await apiWorkflowDao.getByHandle(orgId, viewId, apiWorkflowHandle);
if (!existing) {
await t.rollback();
- return res.status(404).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND);
}
const [count] = await apiWorkflowDao.update(orgId, viewId, existing.uuid, {
displayName,
@@ -290,7 +290,7 @@ const updateAPIWorkflow = async (req, res) => {
if (count === 0) {
await t.rollback();
- return res.status(404).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND);
}
await t.commit();
@@ -300,13 +300,13 @@ const updateAPIWorkflow = async (req, res) => {
} catch (error) {
await t.rollback();
if (error instanceof UniqueConstraintError) {
- return res.status(409).json({ message: 'An API workflow with this handle already exists. Please use a different handle.' });
+ return util.sendError(res, 409, 'An API workflow with this handle already exists. Please use a different handle.');
}
if (error instanceof CustomError) {
- return res.status(error.statusCode).json({ message: error.message });
+ return util.sendError(res, error.statusCode, error.message);
}
logger.error('Error updating API Workflow', { error: error.message, stack: error.stack });
- res.status(500).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_UPDATE_ERROR });
+ util.sendError(res, 500, constants.ERROR_MESSAGE.API_WORKFLOW_UPDATE_ERROR);
}
};
@@ -319,12 +319,12 @@ const deleteAPIWorkflow = async (req, res) => {
const existing = await apiWorkflowDao.getByHandle(orgId, viewId, apiWorkflowHandle);
if (!existing) {
await t.rollback();
- return res.status(404).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND);
}
const count = await apiWorkflowDao.delete(orgId, viewId, existing.uuid, t);
if (count === 0) {
await t.rollback();
- return res.status(404).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND);
}
await t.commit();
logger.info('API Workflow deleted', { apiWorkflowId: existing.uuid, orgId, viewId });
@@ -333,10 +333,10 @@ const deleteAPIWorkflow = async (req, res) => {
} catch (error) {
await t.rollback();
if (error instanceof CustomError) {
- return res.status(error.statusCode).json({ message: error.message });
+ return util.sendError(res, error.statusCode, error.message);
}
logger.error('Error deleting API Workflow', { error: error.message, stack: error.stack });
- res.status(500).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_DELETE_ERROR });
+ util.sendError(res, 500, constants.ERROR_MESSAGE.API_WORKFLOW_DELETE_ERROR);
}
};
@@ -347,16 +347,16 @@ const getAPIWorkflow = async (req, res) => {
const viewId = await resolveViewId(orgId, viewHandle);
const apiWorkflow = await apiWorkflowDao.getByHandle(orgId, viewId, apiWorkflowHandle);
if (!apiWorkflow) {
- return res.status(404).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.API_WORKFLOW_NOT_FOUND);
}
const audit = await userIdpReferenceDao.buildSingleAuditFields(apiWorkflow);
res.status(200).json(toAPIWorkflowDTO(apiWorkflow, audit));
} catch (error) {
if (error instanceof CustomError) {
- return res.status(error.statusCode).json({ message: error.message });
+ return util.sendError(res, error.statusCode, error.message);
}
logger.error('Error fetching API Workflow', { error: error.message, stack: error.stack });
- res.status(500).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_RETRIEVE_ERROR });
+ util.sendError(res, 500, constants.ERROR_MESSAGE.API_WORKFLOW_RETRIEVE_ERROR);
}
};
@@ -369,10 +369,10 @@ const getAllAPIWorkflows = async (req, res) => {
res.status(200).json(util.toPaginatedList(await toAPIWorkflowListDTOs(apiWorkflows), req));
} catch (error) {
if (error instanceof CustomError) {
- return res.status(error.statusCode).json({ message: error.message });
+ return util.sendError(res, error.statusCode, error.message);
}
logger.error('Error fetching API Workflows', { error: error.message, stack: error.stack });
- res.status(500).json({ message: constants.ERROR_MESSAGE.API_WORKFLOW_RETRIEVE_ERROR });
+ util.sendError(res, 500, constants.ERROR_MESSAGE.API_WORKFLOW_RETRIEVE_ERROR);
}
};
@@ -384,7 +384,7 @@ const generatePrompt = async (req, res) => {
res.status(200).json({ agentPrompt: prompt });
} catch (error) {
logger.error('Error generating agent prompt', { error: error.message });
- res.status(500).json({ message: 'Error generating agent prompt' });
+ util.sendError(res, 500, 'Error generating agent prompt');
}
};
diff --git a/portals/developer-portal/src/services/devportalService.js b/portals/developer-portal/src/services/devportalService.js
index 83f9f180d6..2a39b79ab1 100644
--- a/portals/developer-portal/src/services/devportalService.js
+++ b/portals/developer-portal/src/services/devportalService.js
@@ -58,7 +58,7 @@ const getOrgContent = async (req, res) => {
res.set(constants.MIME_TYPES.CONYEMT_TYPE, contentType);
return res.status(200).send(Buffer.isBuffer(asset.file_content) ? asset.file_content : constants.CHARSET_UTF8);
} else {
- return res.status(404).send('Not Found');
+ return util.sendError(res, 404, 'Not Found');
}
} else if (req.params.fileType) {
const assets = await adminService.getOrgContent(req.orgId, req.params.viewId, req.params.fileType);
@@ -73,7 +73,7 @@ const getOrgContent = async (req, res) => {
}
return res.status(200).send(results);
} else {
- res.status(400).send('Invalid request');
+ util.sendError(res, 400, 'Invalid request');
}
} catch (error) {
logger.error('Error while fetching organization content', {
@@ -82,7 +82,7 @@ const getOrgContent = async (req, res) => {
orgId: req.orgId,
viewId: req.params.viewId
});
- res.status(404).send(error.message);
+ return util.sendError(res, 500, 'Internal Server Error');
}
};
diff --git a/portals/developer-portal/src/services/keyManagerService.js b/portals/developer-portal/src/services/keyManagerService.js
index f43d963524..01545314d9 100644
--- a/portals/developer-portal/src/services/keyManagerService.js
+++ b/portals/developer-portal/src/services/keyManagerService.js
@@ -139,12 +139,12 @@ const createKeyManager = async (req, res) => {
const validationError = _validateRequiredFields(payload);
if (validationError) {
- return res.status(400).json({ error: validationError });
+ return util.sendError(res, 400, validationError);
}
const hadExplicitHandle = !!(payload.handle && payload.handle.trim());
const resolvedHandle = hadExplicitHandle ? payload.handle.trim() : generateHandle(payload.displayName);
if (!resolvedHandle || !HANDLE_PATTERN.test(resolvedHandle)) {
- return res.status(400).json({ error: "Invalid 'id'. Must contain only letters, numbers, underscores, and hyphens." });
+ return util.sendError(res, 400, "Invalid 'id'. Must contain only letters, numbers, underscores, and hyphens.");
}
const userId = util.resolveActor(req);
@@ -164,15 +164,13 @@ const createKeyManager = async (req, res) => {
return res.status(201).json(dto);
} catch (error) {
if (error instanceof Sequelize.UniqueConstraintError) {
- return res.status(409).json({
- error: `A key manager with id "${req.body?.id}" already exists in this organization.`
- });
+ return util.sendError(res, 409, `A key manager with that id already exists in this organization.`);
}
if (error.name === 'YAMLException' || error.name === 'ValidationError') {
- return res.status(400).json({ error: error.message });
+ return util.sendError(res, 400, 'Invalid payload format or validation failed.');
}
logger.error(constants.ERROR_MESSAGE.KEY_MANAGER_CREATE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_CREATE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.KEY_MANAGER_CREATE_ERROR);
}
};
@@ -186,7 +184,7 @@ const updateKeyManager = async (req, res) => {
const kmId = await kmDao.getIdByHandle(orgId, kmHandle);
if (!kmId) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND);
}
const userId = util.resolveActor(req);
@@ -206,18 +204,16 @@ const updateKeyManager = async (req, res) => {
return res.status(200).json(dto);
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND);
}
if (error instanceof Sequelize.UniqueConstraintError) {
- return res.status(409).json({
- error: `A key manager with that id already exists in this organization.`
- });
+ return util.sendError(res, 409, `A key manager with that id already exists in this organization.`);
}
if (error.name === 'YAMLException' || error.name === 'ValidationError') {
- return res.status(400).json({ error: error.message });
+ return util.sendError(res, 400, 'Invalid payload format or validation failed.');
}
logger.error(constants.ERROR_MESSAGE.KEY_MANAGER_UPDATE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_UPDATE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.KEY_MANAGER_UPDATE_ERROR);
}
};
@@ -235,7 +231,7 @@ const getKeyManagers = async (req, res) => {
return res.status(200).json(util.toPaginatedList(dtos, req));
} catch (error) {
logger.error(constants.ERROR_MESSAGE.KEY_MANAGER_RETRIEVE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_RETRIEVE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.KEY_MANAGER_RETRIEVE_ERROR);
}
};
@@ -245,7 +241,7 @@ const getKeyManager = async (req, res) => {
const { kmId: kmHandle } = req.params;
const kmId = await kmDao.getIdByHandle(orgId, kmHandle);
if (!kmId) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND);
}
const record = await kmDao.get(kmId);
const audit = await userIdpReferenceDao.buildSingleAuditFields(record);
@@ -253,10 +249,10 @@ const getKeyManager = async (req, res) => {
return res.status(200).json(dto);
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND);
}
logger.error(constants.ERROR_MESSAGE.KEY_MANAGER_RETRIEVE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_RETRIEVE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.KEY_MANAGER_RETRIEVE_ERROR);
}
};
@@ -266,17 +262,17 @@ const deleteKeyManager = async (req, res) => {
const { kmId: kmHandle } = req.params;
const kmId = await kmDao.getIdByHandle(orgId, kmHandle);
if (!kmId) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND);
}
await kmDao.delete(kmId);
logUserAction('KEY_MANAGER_DELETED', req, { orgId, kmId, resourceUuid: kmId, resourceType: 'key_manager' });
return res.status(204).send();
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.KEY_MANAGER_NOT_FOUND);
}
logger.error(constants.ERROR_MESSAGE.KEY_MANAGER_DELETE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.KEY_MANAGER_DELETE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.KEY_MANAGER_DELETE_ERROR);
}
};
diff --git a/portals/developer-portal/src/services/mcpRegistryService.js b/portals/developer-portal/src/services/mcpRegistryService.js
index d6f9ac52f0..c8fd771ae8 100644
--- a/portals/developer-portal/src/services/mcpRegistryService.js
+++ b/portals/developer-portal/src/services/mcpRegistryService.js
@@ -35,7 +35,7 @@ const VERSION_RANGE_PATTERN = /^[\^~]|^>=?|^<=?|\*|(^|\.)x(\.|$)/i;
const DEFAULT_LIMIT = 30;
const MAX_LIMIT = 100;
// Canonical stored schema filename/shape across the whole platform: a flat, type-tagged
-// YAML array (schemaDefinition.yaml) — the same format the admin /mcp-servers API and the
+// YAML array (definition.yaml) — the same format the admin /mcp-servers API and the
// sample seeder write. Registry writes arrive grouped and are flattened to this on write
// (see toFlatSchema); parseSchema regroups on read.
const SCHEMA_FILE_NAME = constants.FILE_NAME.SCHEMA_DEFINITION_YAML_FILE_NAME;
@@ -199,7 +199,7 @@ function buildApiMetadataPayload(name, version, description, remotes, title, pub
referenceId: null,
name: name,
handle: apiHandle,
- apiTitle: title || null,
+ title: title || null,
description: description || `${name} MCP proxy`,
version: version,
type: constants.API_TYPE.MCP,
diff --git a/portals/developer-portal/src/services/mcpServerService.js b/portals/developer-portal/src/services/mcpServerService.js
index 541c952ba4..ee2039b262 100644
--- a/portals/developer-portal/src/services/mcpServerService.js
+++ b/portals/developer-portal/src/services/mcpServerService.js
@@ -71,7 +71,7 @@ const getAllMcpServersForOrganization = async (req, res) => {
try {
const orgId = req.orgId;
const searchTerm = req.query.query;
- const apiName = req.query.apiName;
+ const apiName = req.query.name;
const apiVersion = req.query.version;
const tags = req.query.tags;
const view = req.query.view;
diff --git a/portals/developer-portal/src/services/sampleSeederService.js b/portals/developer-portal/src/services/sampleSeederService.js
index 37fc639b90..8e6863b412 100644
--- a/portals/developer-portal/src/services/sampleSeederService.js
+++ b/portals/developer-portal/src/services/sampleSeederService.js
@@ -170,7 +170,7 @@ async function seedSampleAPIs(orgId) {
/**
* Deploy all sample MCP servers from samples/mcps/ into the given org.
- * Each subdirectory must contain api.yaml and optionally schemaDefinition.yaml and docs/.
+ * Each subdirectory must contain api.yaml and optionally definition.yaml and docs/.
* Returns an array of { name, status ('ok'|'exists'|'failed'), apiId?, error? }.
*/
async function seedSampleMCPs(orgId) {
diff --git a/portals/developer-portal/src/services/subscriptionService.js b/portals/developer-portal/src/services/subscriptionService.js
index 4d2f055cda..14150e1820 100644
--- a/portals/developer-portal/src/services/subscriptionService.js
+++ b/portals/developer-portal/src/services/subscriptionService.js
@@ -72,7 +72,7 @@ function formatSubscriptionResponse(sub, audit) {
subscriptionId: sub.uuid,
subscriptionToken: sub.token,
status: sub.status,
- apiId: api.handle || sub.api_uuid,
+ artifactId: api.handle || sub.api_uuid,
subscriptionPlanName: plan.display_name || null,
...audit,
};
@@ -80,46 +80,34 @@ function formatSubscriptionResponse(sub, audit) {
const createSubscription = async (req, res) => {
const orgId = req.orgId;
- const { apiId: apiHandle, subscriptionPlanId: reqPlanHandle } = req.body;
+ const { artifactId: apiHandle, subscriptionPlanId: reqPlanHandle } = req.body;
const createdBy = util.resolveActor(req);
let apiId;
if (!apiHandle || typeof apiHandle !== 'string' || !apiHandle.trim()) {
- return res.status(400).json({
- code: '400', message: 'Bad Request', description: 'apiId is required',
- });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: 'artifactId is required' }] });
}
try {
apiId = await apiDao.getId(orgId, apiHandle);
if (!apiId) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'API not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'API not found' }] });
}
const apiMetadataResponse = await apiDao.get(orgId, apiId);
if (!apiMetadataResponse || apiMetadataResponse.length === 0) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'API not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'API not found' }] });
}
const apiMetadata = apiMetadataResponse[0];
const plans = apiMetadata.dp_subscription_plans || [];
if (plans.length === 0) {
- return res.status(400).json({
- code: '400', message: 'Bad Request',
- description: 'This API does not support subscriptions',
- });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: 'This API does not support subscriptions' }] });
}
const matchedPlan = plans.find(p => p.handle === reqPlanHandle);
if (!matchedPlan) {
- return res.status(400).json({
- code: '400', message: 'Bad Request',
- description: 'Subscription plan not found for this API',
- });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: 'Subscription plan not found for this API' }] });
}
const planId = matchedPlan.uuid;
@@ -143,10 +131,7 @@ const createSubscription = async (req, res) => {
return res.status(201).json(formatSubscriptionResponse(created, audit));
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
- return res.status(409).json({
- code: '409', message: 'Conflict',
- description: 'A subscription for this API already exists',
- });
+ return util.sendError(res, 409, 'Conflict', { errors: [{ message: 'A subscription for this API already exists' }] });
}
logger.error('Error creating subscription', {
error: error.message, orgId, apiId,
@@ -157,16 +142,14 @@ const createSubscription = async (req, res) => {
const listSubscriptions = async (req, res) => {
const orgId = req.orgId;
- const apiHandle = req.query.apiId;
+ const apiHandle = req.query.artifactId;
let apiId;
try {
if (apiHandle) {
apiId = await apiDao.getId(orgId, apiHandle);
if (!apiId) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'API not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'API not found' }] });
}
}
@@ -188,9 +171,7 @@ const getSubscription = async (req, res) => {
try {
const sub = await subDao.get(orgId, subscriptionId, util.resolveActor(req));
if (!sub) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'Subscription not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
const audit = await userIdpReferenceDao.buildSingleAuditFields(sub);
return res.status(200).json(formatSubscriptionResponse(sub, audit));
@@ -207,16 +188,14 @@ const updateSubscription = async (req, res) => {
const subscriptionId = req.params.subId;
const { status } = req.body;
if (!Object.values(constants.SUBSCRIPTION_STATUS).includes(status)) {
- return res.status(400).json({ code: '400', message: 'Bad Request', description: `Invalid status. Must be one of: ${Object.values(constants.SUBSCRIPTION_STATUS).join(', ')}.` });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: `Invalid status. Must be one of: ${Object.values(constants.SUBSCRIPTION_STATUS).join(', ')}.` }] });
}
const actorId = util.resolveActor(req);
try {
const existing = await subDao.get(orgId, subscriptionId, actorId);
if (!existing) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'Subscription not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
let sub;
@@ -236,7 +215,7 @@ const updateSubscription = async (req, res) => {
return res.status(200).json(formatSubscriptionResponse(sub, audit));
} catch (error) {
if (error.status === 404) {
- return res.status(404).json({ code: '404', message: 'Not Found', description: 'Subscription not found' });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
logger.error('Error updating subscription', {
error: error.message, subscriptionId, status,
@@ -248,43 +227,33 @@ const updateSubscription = async (req, res) => {
const changePlan = async (req, res) => {
const orgId = req.orgId;
const subscriptionId = req.params.subId;
- const { apiId: reqApiHandle, planId: reqPlanHandle } = req.body;
+ const { artifactId: reqApiHandle, planId: reqPlanHandle } = req.body;
const actorId = util.resolveActor(req);
try {
const existing = await subDao.get(orgId, subscriptionId, actorId);
if (!existing) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'Subscription not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
const apiId = existing.api_uuid || (existing.dp_api_metadata ? existing.dp_api_metadata.uuid : null) || null;
if (!apiId) {
- return res.status(400).json({
- code: '400', message: 'Bad Request', description: 'API not found for this subscription',
- });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: 'API not found for this subscription' }] });
}
const apiHandle = existing.dp_api_metadata ? existing.dp_api_metadata.handle : null;
if (reqApiHandle && reqApiHandle !== apiHandle) {
- return res.status(400).json({
- code: '400', message: 'Bad Request', description: 'apiId does not match this subscription',
- });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: 'artifactId does not match this subscription' }] });
}
const apiMetadataResponse = await apiDao.get(orgId, apiId);
if (!apiMetadataResponse || apiMetadataResponse.length === 0) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'API not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'API not found' }] });
}
const apiMetadata = apiMetadataResponse[0];
const plans = apiMetadata.dp_subscription_plans || [];
const newPlan = plans.find(p => p.handle === reqPlanHandle);
if (!newPlan) {
- return res.status(400).json({
- code: '400', message: 'Bad Request', description: 'Subscription plan not found for this API',
- });
+ return util.sendError(res, 400, 'Bad Request', { errors: [{ message: 'Subscription plan not found for this API' }] });
}
const planId = newPlan.uuid;
@@ -315,7 +284,7 @@ const changePlan = async (req, res) => {
return res.status(200).json(formatSubscriptionResponse(updated, audit));
} catch (error) {
if (error.status === 404) {
- return res.status(404).json({ code: '404', message: 'Not Found', description: 'Subscription not found' });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
logger.error('Error changing subscription plan', { error: error.message, subscriptionId });
util.handleError(res, error);
@@ -330,9 +299,7 @@ const regenerateSubscriptionToken = async (req, res) => {
try {
const existing = await subDao.get(orgId, subscriptionId, actorId);
if (!existing) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'Subscription not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
const apiMetadata = existing.dp_api_metadata;
@@ -358,7 +325,7 @@ const regenerateSubscriptionToken = async (req, res) => {
return res.status(200).json(formatSubscriptionResponse(updated, audit));
} catch (error) {
if (error.status === 404) {
- return res.status(404).json({ code: '404', message: 'Not Found', description: 'Subscription not found' });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
logger.error('Error regenerating subscription token', { error: error.message, subscriptionId });
util.handleError(res, error);
@@ -373,9 +340,7 @@ const deleteSubscription = async (req, res) => {
try {
const existing = await subDao.get(orgId, subscriptionId, actorId);
if (!existing) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'Subscription not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
const apiMetadata = existing.dp_api_metadata;
@@ -396,9 +361,7 @@ const deleteSubscription = async (req, res) => {
return res.status(200).json({ message: 'Subscription deleted successfully' });
} catch (error) {
if (error.statusCode === 404) {
- return res.status(404).json({
- code: '404', message: 'Not Found', description: 'Subscription not found',
- });
+ return util.sendError(res, 404, 'Not Found', { errors: [{ message: 'Subscription not found' }] });
}
logger.error('Error deleting subscription', {
error: error.message, subscriptionId,
diff --git a/portals/developer-portal/src/services/webhookSubscriberService.js b/portals/developer-portal/src/services/webhookSubscriberService.js
index a745220753..6169d98a87 100644
--- a/portals/developer-portal/src/services/webhookSubscriberService.js
+++ b/portals/developer-portal/src/services/webhookSubscriberService.js
@@ -57,7 +57,7 @@ const createWebhookSubscriber = async (req, res) => {
const validationError = _validateRequiredFields(payload);
if (validationError) {
- return res.status(400).json({ error: validationError });
+ return util.sendError(res, 400, validationError);
}
const userId = util.resolveActor(req);
@@ -68,12 +68,10 @@ const createWebhookSubscriber = async (req, res) => {
return res.status(201).json(dto);
} catch (error) {
if (error instanceof Sequelize.UniqueConstraintError) {
- return res.status(409).json({
- error: _uniqueConstraintMessage(error, req.body)
- });
+ return util.sendError(res, 409, _uniqueConstraintMessage(error, req.body));
}
logger.error(constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_CREATE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_CREATE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_CREATE_ERROR);
}
};
@@ -94,15 +92,13 @@ const updateWebhookSubscriber = async (req, res) => {
return res.status(200).json(dto);
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND);
}
if (error instanceof Sequelize.UniqueConstraintError) {
- return res.status(409).json({
- error: _uniqueConstraintMessage(error, req.body)
- });
+ return util.sendError(res, 409, _uniqueConstraintMessage(error, req.body));
}
logger.error(constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_UPDATE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_UPDATE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_UPDATE_ERROR);
}
};
@@ -115,7 +111,7 @@ const getWebhookSubscribers = async (req, res) => {
return res.status(200).json(util.toPaginatedList(dtos, req));
} catch (error) {
logger.error(constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_RETRIEVE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_RETRIEVE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_RETRIEVE_ERROR);
}
};
@@ -129,10 +125,10 @@ const getWebhookSubscriber = async (req, res) => {
return res.status(200).json(dto);
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND);
}
logger.error(constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_RETRIEVE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_RETRIEVE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_RETRIEVE_ERROR);
}
};
@@ -156,13 +152,13 @@ const getWebhookSubscriberDeliveries = async (req, res) => {
const { subscriberId } = req.params;
const sub = await whDao.get(orgId, subscriberId);
const deliveries = await eventDao.listDeliveriesForSubscriber(orgId, sub.uuid, 20);
- return res.status(200).json({ list: deliveries.map(_formatDeliverySummary) });
+ return res.status(200).json(util.toPaginatedList(deliveries.map(_formatDeliverySummary), req));
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND);
}
logger.error('Error fetching webhook subscriber deliveries', { error });
- return res.status(500).json({ error: 'Failed to fetch webhook subscriber deliveries' });
+ return util.sendError(res, 500, 'Failed to fetch webhook subscriber deliveries');
}
};
@@ -176,10 +172,10 @@ const deleteWebhookSubscriber = async (req, res) => {
return res.status(204).send();
} catch (error) {
if (error instanceof Sequelize.EmptyResultError) {
- return res.status(404).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND });
+ return util.sendError(res, 404, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_NOT_FOUND);
}
logger.error(constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_DELETE_ERROR, { error });
- return res.status(500).json({ error: constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_DELETE_ERROR });
+ return util.sendError(res, 500, constants.ERROR_MESSAGE.WEBHOOK_SUBSCRIBER_DELETE_ERROR);
}
};
diff --git a/portals/developer-portal/src/utils/constants.js b/portals/developer-portal/src/utils/constants.js
index d526288d29..4e4ece3197 100644
--- a/portals/developer-portal/src/utils/constants.js
+++ b/portals/developer-portal/src/utils/constants.js
@@ -226,13 +226,13 @@ module.exports = {
API_DOC_HBS: 'api-doc.hbs',
API_CONTENT_PARTIAL_NAME: "api-content",
API_DOC_PARTIAL_NAME: "api-doc",
- API_DEFINITION_FILE_NAME: 'apiDefinition.json',
- API_DEFINITION_YAML_FILE_NAME: 'apiDefinition.yaml',
- SCHEMA_DEFINITION_FILE_NAME: 'schemaDefinition.json',
- SCHEMA_DEFINITION_YAML_FILE_NAME: 'schemaDefinition.yaml',
+ API_DEFINITION_FILE_NAME: 'definition.json',
+ API_DEFINITION_YAML_FILE_NAME: 'definition.yaml',
+ SCHEMA_DEFINITION_FILE_NAME: 'definition.json',
+ SCHEMA_DEFINITION_YAML_FILE_NAME: 'definition.yaml',
API_SPECIFICATION_PATH: 'specification',
- API_DEFINITION_GRAPHQL: 'apiDefinition.graphql',
- API_DEFINITION_XML: 'apiDefinition.xml',
+ API_DEFINITION_GRAPHQL: 'definition.graphql',
+ API_DEFINITION_XML: 'definition.xml',
LLMS_CONFIG: 'llms-config.json',
},
ARTIFACT_DIR: {
diff --git a/portals/developer-portal/src/utils/sampleApiLoader.js b/portals/developer-portal/src/utils/sampleApiLoader.js
index 3a6e206b3b..877a21d625 100644
--- a/portals/developer-portal/src/utils/sampleApiLoader.js
+++ b/portals/developer-portal/src/utils/sampleApiLoader.js
@@ -208,7 +208,7 @@ function getDefinition(apiHandle, samplesDir = './samples/apis/') {
}
/**
- * Load and parse the MCP schema definition file (schemaDefinition.yaml or .json).
+ * Load and parse the MCP schema definition file (definition.yaml or .json).
* Returns { tools, resources, prompts } or null if not found.
*/
function getMcpSchema(apiHandle, samplesDir = './samples/apis/') {
diff --git a/portals/developer-portal/src/utils/util.js b/portals/developer-portal/src/utils/util.js
index 31ac6ebc40..b2b518d7d6 100644
--- a/portals/developer-portal/src/utils/util.js
+++ b/portals/developer-portal/src/utils/util.js
@@ -280,12 +280,41 @@ function handleError(res, error) {
}
}
+/**
+ * Emit a standard error response body:
+ * { status: 'error', code, message, errors, [details], [trackingId] }
+ * `code` defaults to the HTTP-status catalog entry; pass opts.code to override
+ * with a resource-specific code. `errors` is always present (empty array when
+ * there are no field-level errors), matching handleError's shape.
+ */
+function sendError(res, statusCode, message, opts = {}) {
+ const body = {
+ status: 'error',
+ code: opts.code || HTTP_CODE_TO_CATALOG[statusCode] || 'INTERNAL_SERVER_ERROR',
+ message,
+ errors: opts.errors || [],
+ };
+ if (opts.details) body.details = opts.details;
+ if (opts.trackingId) body.trackingId = opts.trackingId;
+ return res.status(statusCode).json(body);
+}
+
+/**
+ * Wrap a fully-materialized list in the standard collection envelope:
+ * { count, list, pagination: { limit, offset, total } }
+ * `total` is the grand total across all pages; `count` is the number of items
+ * returned in this page. limit/offset are applied here (in-memory) so every
+ * list endpoint paginates consistently.
+ */
function toPaginatedList(list, req) {
- const limit = Math.min(parseInt((req.query && req.query.limit) || '20', 10) || 20, 100);
- const offset = parseInt((req.query && req.query.offset) || '0', 10) || 0;
+ const total = list.length;
+ const limit = Math.min(Math.max(parseInt((req.query && req.query.limit) || '20', 10) || 20, 0), 100);
+ const offset = Math.max(parseInt((req.query && req.query.offset) || '0', 10) || 0, 0);
+ const page = list.slice(offset, offset + limit);
return {
- list,
- pagination: { total: list.length, limit, offset },
+ count: page.length,
+ list: page,
+ pagination: { limit, offset, total },
};
}
@@ -1094,6 +1123,7 @@ module.exports = {
renderLlmsTxt,
renderGivenTemplate,
handleError,
+ sendError,
retrieveContentType,
getAPIFileContent,
getAPIImages,
diff --git a/tests/integration-e2e/steps_devportal_test.go b/tests/integration-e2e/steps_devportal_test.go
index 2e742a91d4..477c952201 100644
--- a/tests/integration-e2e/steps_devportal_test.go
+++ b/tests/integration-e2e/steps_devportal_test.go
@@ -264,10 +264,10 @@ paths:
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
- if err := addYAMLPart(mw, "api", "api.yaml", apiYAML); err != nil {
+ if err := addYAMLPart(mw, "metadata", "metadata.yaml", apiYAML); err != nil {
return "", nil, err
}
- if err := addYAMLPart(mw, "apiDefinition", "definition.yaml", defYAML); err != nil {
+ if err := addYAMLPart(mw, "definition", "definition.yaml", defYAML); err != nil {
return "", nil, err
}
if err := mw.Close(); err != nil {
@@ -295,7 +295,7 @@ func (w *world) subscribeInDevportal() error {
}
st, body, err = dpCall(http.MethodPost, "/subscriptions", map[string]any{
- "apiId": w.dpApiID, // the devportal API handle, not the referenceId
+ "artifactId": w.dpApiID, // the devportal API handle, not the referenceId
"subscriptionPlanId": w.planID,
})
if err != nil {