Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6fed175
feat: Add manifest validation tool
dimovpetar Oct 21, 2025
278273f
refactor: Move manifest schema fetching to ui5Manifest.ts
dimovpetar Oct 21, 2025
dd54cd2
test(index.ts): Add tests
dimovpetar Oct 22, 2025
00de711
refactor(ui5Manifest.ts): Cache the schema
dimovpetar Oct 22, 2025
d6970ea
docs(README.md): List run_manifest_validation
dimovpetar Oct 27, 2025
a5ddede
refactor(ui5Manifest): Add cache
dimovpetar Oct 27, 2025
f08c45d
refactor: Improve error handling
dimovpetar Oct 27, 2025
a74700e
test(runValidation): Add tests
dimovpetar Oct 27, 2025
3c6b99c
refactor: Add comment containing link to AdaptiveCards issue
dimovpetar Oct 30, 2025
4c49e47
fix(package.json): List ajv as dependency
dimovpetar Oct 30, 2025
4c87335
fix(runValidation): Resolve meta schemas paths using import.meta.resolve
dimovpetar Oct 30, 2025
0f4d9df
fix(runValidation): Throw error if manifest path is not absolute
dimovpetar Oct 30, 2025
593f749
fix(run_manifest_validation): Normalize manifest path
dimovpetar Nov 5, 2025
7d71c5e
refactor: Fetch concrete manifest schema
dimovpetar Nov 10, 2025
05113b3
fix(runValidation): Properly release mutex
dimovpetar Nov 17, 2025
be513fb
fix(ui5Manifest): Throw errors for versions lt 1.68.0
dimovpetar Nov 17, 2025
a950b4b
fix(runValidation): Include ajv-formats
dimovpetar Nov 17, 2025
ae0b431
refactor(ui5Manifest): Enhance more errors with supported versions info
dimovpetar Nov 17, 2025
7d32b8f
test(runValidation): Increase coverage for external schemas
dimovpetar Nov 24, 2025
aefa858
refactor(runValidation): Add explanation why meta schemas are needed
dimovpetar Nov 24, 2025
005701b
refactor(runValidation): Add explanation for unicodeRegExp setting
dimovpetar Nov 24, 2025
53beef2
refactor(runValidation): Add coment why strict flag of ajv is off
dimovpetar Nov 24, 2025
95c3527
refactor(ui5Manifest): Add comments for lowest supported version 1.68.0
dimovpetar Nov 24, 2025
6c7eae5
fix(run_manifest_validation): Mark the tool as read-only
dimovpetar Nov 25, 2025
286fbe7
refactor(runValidation): Remove undefined properties from the result
dimovpetar Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The UI5 [Model Context Protocol](https://modelcontextprotocol.io/) server offers
- `run_ui5_linter`: Integrates with [`@ui5/linter`](https://github.com/UI5/linter) to analyze and report issues in UI5 code.
- `get_integration_cards_guidelines`: Provides access to UI Integration Cards development best practices.
- `create_integration_card`: Scaffolds a new UI Integration Card.
- `run_manifest_validation`: Validates the manifest against the UI5 Manifest schema.

## Requirements

Expand Down
19 changes: 19 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
"@ui5/linter": "^1.20.2",
"@ui5/logger": "^4.0.2",
"@ui5/project": "^4.0.8",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"async-mutex": "^0.5.0",
"ejs": "^3.1.10",
"execa": "^9.6.0",
Expand Down
1 change: 1 addition & 0 deletions resources/integration_cards_guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
## 2. Validation
- **ALWAYS** ensure that `manifest.json` file is valid JSON.
- **ALWAYS** ensure that in `manifest.json` file the property `sap.app/type` is set to `"card"`.
- **ALWAYS** validate the `manifest.json` against the UI5 Manifest schema. You must do it using the `run_manifest_validation` tool.
- **ALWAYS** avoid using deprecated properties in `manifest.json` and elsewhere.
- **NEVER** treat Integration Cards' project as UI5 project, except for cards of type "Component".

Expand Down
3 changes: 3 additions & 0 deletions src/registerTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import registerGetGuidelinesTool from "./tools/get_guidelines/index.js";
import registerGetVersionInfoTool from "./tools/get_version_info/index.js";
import registerGetIntegrationCardsGuidelinesTool from "./tools/get_integration_cards_guidelines/index.js";
import registerCreateIntegrationCardTool from "./tools/create_integration_card/index.js";
import registerRunManifestValidationTool from "./tools/run_manifest_validation/index.js";

interface Options {
useStructuredContentInResponse: boolean;
Expand Down Expand Up @@ -51,6 +52,8 @@ export default function (server: McpServer, context: Context, options: Options)
registerGetIntegrationCardsGuidelinesTool(registerTool, context);

registerCreateIntegrationCardTool(registerTool, context);

registerRunManifestValidationTool(registerTool, context);
}

export function _processResponse({content, structuredContent}: CallToolResult, options: Options) {
Expand Down
36 changes: 36 additions & 0 deletions src/tools/run_manifest_validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import runValidation from "./runValidation.js";
import {inputSchema, outputSchema} from "./schema.js";
import {getLogger} from "@ui5/logger";
import Context from "../../Context.js";
import {RegisterTool} from "../../registerTools.js";

const log = getLogger("tools:run_manifest_validation");

export default function registerTool(registerTool: RegisterTool, context: Context) {
registerTool("run_manifest_validation", {
title: "Manifest Validation",
description:
"Validates UI5 manifest file. " +
"After making changes, you should always run the validation again " +
"to verify that no new problems have been introduced.",
annotations: {
title: "Manifest Validation",
readOnlyHint: true,
},
inputSchema,
outputSchema,
}, async ({manifestPath}) => {
log.info(`Running manifest validation on ${manifestPath}...`);

const normalizedManifestPath = await context.normalizePath(manifestPath);
const result = await runValidation(normalizedManifestPath);

return {
content: [{
type: "text",
text: JSON.stringify(result),
}],
structuredContent: result,
};
});
}
155 changes: 155 additions & 0 deletions src/tools/run_manifest_validation/runValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {fetchCdn} from "../../utils/cdnHelper.js";
import {RunSchemaValidationResult} from "./schema.js";
import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import {readFile} from "fs/promises";
import {getLogger} from "@ui5/logger";
import {InvalidInputError} from "../../utils.js";
import {getManifestSchema, getManifestVersion} from "../../utils/ui5Manifest.js";
import {Mutex} from "async-mutex";
import {fileURLToPath} from "url";
import {isAbsolute} from "path";

const log = getLogger("tools:run_manifest_validation:runValidation");
const schemaCache = new Map<string, AnySchemaObject>();
const fetchSchemaMutex = new Mutex();

const AJV_SCHEMA_PATHS = {
draft06: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-06.json")),
draft07: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-07.json")),
} as const;

async function createUI5ManifestValidateFunction(ui5Schema: object) {
try {
const ajv = new Ajv2020.default({
// Collect all errors, not just the first one
allErrors: true,
// Allow additional properties that are not in schema such as "i18n",
// otherwise compilation fails
strict: false,
// Don't use Unicode-aware regular expressions,
// otherwise compilation fails with "Invalid escape" errors
unicodeRegExp: false,
loadSchema: async (uri) => {
const release = await fetchSchemaMutex.acquire();

try {
if (schemaCache.has(uri)) {
log.info(`Loading cached schema: ${uri}`);
return schemaCache.get(uri)!;
}

log.info(`Loading external schema: ${uri}`);
const schema = await fetchCdn(uri) as AnySchemaObject;

// Special handling for Adaptive Card schema to fix unsupported "id" property
// According to the JSON Schema spec Draft 06 (used by Adaptive Card schema),
// "$id" should be used instead of "id"
// See https://github.com/microsoft/AdaptiveCards/issues/9274
if (uri.includes("adaptive-card.json") && typeof schema.id === "string") {
schema.$id = schema.id;
delete schema.id;
}

schemaCache.set(uri, schema);

return schema;
} catch (error) {
log.warn(`Failed to load external schema ${uri}:` +
`${error instanceof Error ? error.message : String(error)}`);

throw error;
} finally {
release();
}
},
});

addFormats.default(ajv);

const draft06MetaSchema = JSON.parse(
await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8")
) as AnySchemaObject;
const draft07MetaSchema = JSON.parse(
await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8")
) as AnySchemaObject;

// Add meta-schemas for draft-06 and draft-07.
// These are required to support schemas that reference these drafts,
// for example the Adaptive Card schema and some sap.bpa.task properties.
ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#");
ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#");

const validate = await ajv.compileAsync(ui5Schema);

return validate;
} catch (error) {
throw new Error(`Failed to create UI5 manifest validate function: ` +
`${error instanceof Error ? error.message : String(error)}`);
}
}

async function readManifest(path: string) {
let content: string;
let json: object;

if (!isAbsolute(path)) {
throw new InvalidInputError(`The manifest path must be absolute: '${path}'`);
}

try {
content = await readFile(path, "utf-8");
} catch (error) {
throw new InvalidInputError(`Failed to read manifest file at ${path}: ` +
`${error instanceof Error ? error.message : String(error)}`);
}

try {
json = JSON.parse(content) as object;
} catch (error) {
throw new InvalidInputError(`Failed to parse manifest file at ${path} as JSON: ` +
`${error instanceof Error ? error.message : String(error)}`);
}

return json;
}

export default async function runValidation(manifestPath: string): Promise<RunSchemaValidationResult> {
log.info(`Starting manifest validation for file: ${manifestPath}`);

const manifest = await readManifest(manifestPath);
const manifestVersion = await getManifestVersion(manifest);
log.info(`Using manifest version: ${manifestVersion}`);
const ui5ManifestSchema = await getManifestSchema(manifestVersion);
const validate = await createUI5ManifestValidateFunction(ui5ManifestSchema);
const isValid = validate(manifest);

if (isValid) {
log.info("Manifest validation successful");

return {
isValid: true,
errors: [],
};
}

// Map AJV errors to our schema format
const validationErrors = validate.errors ?? [];
const errors = validationErrors.map((error): RunSchemaValidationResult["errors"][number] => {
return {
keyword: error.keyword ?? "",
instancePath: error.instancePath ?? "",
schemaPath: error.schemaPath ?? "",
params: error.params ?? {},
propertyName: error.propertyName,
message: error.message,
};
});

log.info(`Manifest validation failed with ${errors.length} error(s)`);

return {
isValid: false,
errors: errors,
};
}
31 changes: 31 additions & 0 deletions src/tools/run_manifest_validation/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {z} from "zod";

export const inputSchema = {
manifestPath: z.string()
.describe("Absolute path to the manifest file to validate."),
};

export const outputSchema = {
isValid: z.boolean()
.describe("Whether the manifest is valid according to the UI5 Manifest schema."),
errors: z.array(
z.object({
keyword: z.string()
.describe("Validation keyword."),
instancePath: z.string()
.describe("JSON Pointer to the location in the data instance (e.g., `/prop/1/subProp`)."),
schemaPath: z.string()
.describe("JSON Pointer to the location of the failing keyword in the schema."),
params: z.record(z.any())
.describe("An object with additional information about the error."),
propertyName: z.string()
.optional()
.describe("Set for errors in `propertyNames` keyword schema."),
message: z.string()
.optional()
.describe("The error message."),
})
).describe("Array of validation error objects as returned by Ajv."),
};
const _outputSchemaObject = z.object(outputSchema);
export type RunSchemaValidationResult = z.infer<typeof _outputSchemaObject>;
Loading