Skip to content

Commit 01fc0b4

Browse files
authored
feat: Add manifest validation tool (#93)
1 parent cb2871f commit 01fc0b4

File tree

17 files changed

+1407
-6
lines changed

17 files changed

+1407
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The UI5 [Model Context Protocol](https://modelcontextprotocol.io/) server offers
3232
- `get_typescript_conversion_guidelines`: Provides guidelines for converting UI5 applications and controls from JavaScript to TypeScript.
3333
- `get_integration_cards_guidelines`: Provides access to UI Integration Cards development best practices.
3434
- `create_integration_card`: Scaffolds a new UI Integration Card.
35+
- `run_manifest_validation`: Validates the manifest against the UI5 Manifest schema.
3536

3637
## Requirements
3738

docs/architecture.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ Tool Name | Description
7878
`get_typescript_conversion_guidelines` | Provides guidelines for converting UI5 applications and controls from JavaScript to TypeScript
7979
`get_version_info` | Retrieves version information for the UI5 framework
8080
`run_ui5_linter` | Integrates with `@ui5/linter` to analyze and report issues in UI5 code
81+
`run_manifest_validation` | Validates the manifest against the UI5 Manifest schema
82+
83+
### create_integration_card
84+
85+
The `create_integration_card` tool is designed to scaffold new UI Integration Cards using predefined templates. It automates the setup process, ensuring that developers can quickly start building integration cards without manually configuring the project structure.
86+
87+
Templates are stored in the `resources/` directory of the project. There is currently one template for every declarative card type (List Card, Table Card, etc.).
88+
89+
For rendering the templates with the provided data, the [EJS](https://github.com/mde/ejs) templating engine is used.
8190

8291
### create_ui5_app
8392

@@ -187,6 +196,10 @@ Drawbacks of the current approach using the semantic model:
187196

188197
The `get_guidelines` tool returns a single markdown resource containing best practices and guidelines for UI5 development, particularly targeted towards AI agents. The document can be found in the `resources/` directory of the project.
189198

199+
### get_integration_cards_guidelines
200+
201+
The `get_integration_cards_guidelines` tool returns a single markdown resource containing best practices and guidelines for UI Integration Cards development. The content is particularly targeted towards AI agents. This document can be found in the `resources/` directory of the project.
202+
190203
### get_project_info
191204

192205
The `get_project_info` tool extracts metadata and configuration from a UI5 project. It provides insights into the project's structure, dependencies, and configuration settings, helping developers understand the context of their application.
@@ -485,6 +498,107 @@ In the future, these guides should be moved into the UI5 linter project. See als
485498
}
486499
```
487500

501+
### run_manifest_validation
502+
503+
The `run_manifest_validation` tool provides comprehensive schema validation for UI5 manifest files (`manifest.json`). It ensures that manifest files conform to the official UI5 Manifest JSON Schema, helping developers catch configuration errors early in the development process.
504+
505+
#### Overview
506+
507+
This tool uses the [Ajv JSON schema validator](https://www.npmjs.com/package/ajv) (specifically Ajv 2020-12) to perform validation against the official manifest schema. The schema is dynamically fetched from the [UI5 Manifest repository](https://github.com/UI5/manifest) based on the `_version` property declared in the manifest file.
508+
509+
#### Schema Management
510+
511+
**Version Detection:**
512+
- The tool automatically detects the manifest version from the `_version` property
513+
- If the `_version` property is missing, malformed, or not a valid semantic version, validation fails with a helpful error message listing supported versions
514+
- The minimum supported manifest version is **1.68.0** (earlier versions use incompatible meta-schemas)
515+
516+
**Schema Retrieval:**
517+
- Schemas are fetched from: `https://raw.githubusercontent.com/UI5/manifest/v{version}/schema.json`
518+
- A version mapping is maintained at: `https://raw.githubusercontent.com/UI5/manifest/main/mapping.json`
519+
- Schemas are cached locally after first fetch to improve performance and reduce network requests
520+
- External schemas referenced by the UI5 manifest schema (e.g., Adaptive Card schema) are also fetched and cached as needed
521+
522+
#### Validation Process
523+
524+
- Reads the manifest file from the provided absolute path
525+
- Parses the JSON content and extracts the `_version` property
526+
- Fetches the corresponding schema based on the version
527+
- Uses Ajv to validate the manifest against the schema
528+
- Returns a detailed report of validation results, including specific error messages for any violations found
529+
530+
#### Performance Characteristics
531+
532+
**Caching Strategy:**
533+
- Schema files are cached in memory after first retrieval
534+
- Cache is shared across multiple validation calls in the same process
535+
- Mutex locks prevent duplicate concurrent downloads of the same schema
536+
- Network requests are only made once per schema version per process lifecycle
537+
538+
#### Error Handling
539+
540+
**Input Errors:**
541+
- Non-absolute paths: `InvalidInputError` with clear message
542+
- File not found: `InvalidInputError` indicating the file doesn't exist
543+
- Invalid JSON: `InvalidInputError` with JSON parsing error details
544+
- Missing `_version`: Detailed error with list of supported versions
545+
- Unsupported version: Error message with version requirements and supported versions list
546+
547+
**Network Errors:**
548+
- Schema fetch failures are caught and reported with helpful context
549+
- The tool provides fallback error messages even if the supported versions list cannot be fetched
550+
- Cached schemas allow continued operation even if the network is unavailable after initial setup
551+
552+
#### Example Input
553+
554+
```json
555+
{
556+
"manifestPath": "/absolute/path/to/project/webapp/manifest.json"
557+
}
558+
```
559+
560+
#### Example Output for Invalid Manifest
561+
562+
```json
563+
{
564+
"isValid": false,
565+
"errors": [
566+
{
567+
"keyword": "required",
568+
"instancePath": "",
569+
"schemaPath": "#/required",
570+
"params": {
571+
"missingProperty": "sap.ui"
572+
},
573+
"message": "must have required property 'sap.ui'"
574+
},
575+
{
576+
"keyword": "required",
577+
"instancePath": "/sap.app",
578+
"schemaPath": "#/properties/sap.app/required",
579+
"params": {
580+
"missingProperty": "title"
581+
},
582+
"message": "must have required property 'title'"
583+
}
584+
]
585+
}
586+
```
587+
588+
#### Example Output for Valid Manifest
589+
590+
```json
591+
{
592+
"isValid": true,
593+
"errors": []
594+
}
595+
```
596+
597+
#### Requirements
598+
599+
- **Network Access**: Initial schema fetch requires internet connectivity
600+
- **Manifest Versioning**: The manifest must declare a valid `_version` property
601+
488602
## Resources
489603

490604
### UI5 Documentation

npm-shrinkwrap.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
"@ui5/linter": "^1.20.7",
8080
"@ui5/logger": "^4.0.2",
8181
"@ui5/project": "^4.0.9",
82+
"ajv": "^8.17.1",
83+
"ajv-formats": "^3.0.1",
8284
"async-mutex": "^0.5.0",
8385
"ejs": "^3.1.10",
8486
"execa": "^9.6.1",

resources/integration_cards_guidelines.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
## 2. Validation
4343
- **ALWAYS** ensure that `manifest.json` file is valid JSON.
4444
- **ALWAYS** ensure that in `manifest.json` file the property `sap.app/type` is set to `"card"`.
45+
- **ALWAYS** validate the `manifest.json` against the UI5 Manifest schema. Use the `run_manifest_validation` tool to do this.
4546
- **ALWAYS** avoid using deprecated properties in `manifest.json` and elsewhere.
4647
- **NEVER** treat Integration Cards' project as UI5 project, except for cards of type "Component".
4748

src/registerTools.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import registerGetGuidelinesTool from "./tools/get_guidelines/index.js";
1111
import registerGetVersionInfoTool from "./tools/get_version_info/index.js";
1212
import registerGetIntegrationCardsGuidelinesTool from "./tools/get_integration_cards_guidelines/index.js";
1313
import registerCreateIntegrationCardTool from "./tools/create_integration_card/index.js";
14+
import registerRunManifestValidationTool from "./tools/run_manifest_validation/index.js";
1415
import registerGetTypescriptConversionGuidelinesTool from "./tools/get_typescript_conversion_guidelines/index.js";
1516

1617
interface Options {
@@ -56,6 +57,8 @@ export default function (server: McpServer, context: Context, options: Options)
5657

5758
registerCreateIntegrationCardTool(registerTool, context);
5859

60+
registerRunManifestValidationTool(registerTool, context);
61+
5962
registerGetTypescriptConversionGuidelinesTool(registerTool, context);
6063
}
6164

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import runValidation from "./runValidation.js";
2+
import {inputSchema, outputSchema} from "./schema.js";
3+
import {getLogger} from "@ui5/logger";
4+
import Context from "../../Context.js";
5+
import {RegisterTool} from "../../registerTools.js";
6+
7+
const log = getLogger("tools:run_manifest_validation");
8+
9+
export default function registerTool(registerTool: RegisterTool, context: Context) {
10+
registerTool("run_manifest_validation", {
11+
title: "Manifest Validation",
12+
description:
13+
"Validates UI5 manifest file. " +
14+
"After making changes, you should always run the validation again " +
15+
"to verify that no new problems have been introduced.",
16+
annotations: {
17+
title: "Manifest Validation",
18+
readOnlyHint: true,
19+
},
20+
inputSchema,
21+
outputSchema,
22+
}, async ({manifestPath}) => {
23+
log.info(`Running manifest validation on ${manifestPath}...`);
24+
25+
const normalizedManifestPath = await context.normalizePath(manifestPath);
26+
const result = await runValidation(normalizedManifestPath);
27+
28+
return {
29+
content: [{
30+
type: "text",
31+
text: JSON.stringify(result),
32+
}],
33+
structuredContent: result,
34+
};
35+
});
36+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {fetchCdn} from "../../utils/cdnHelper.js";
2+
import {RunSchemaValidationResult} from "./schema.js";
3+
import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js";
4+
import addFormats from "ajv-formats";
5+
import {readFile} from "fs/promises";
6+
import {getLogger} from "@ui5/logger";
7+
import {InvalidInputError} from "../../utils.js";
8+
import {getManifestSchema, getManifestVersion} from "../../utils/ui5Manifest.js";
9+
import {Mutex} from "async-mutex";
10+
import {fileURLToPath} from "url";
11+
import {isAbsolute} from "path";
12+
13+
const log = getLogger("tools:run_manifest_validation:runValidation");
14+
const schemaCache = new Map<string, AnySchemaObject>();
15+
const fetchSchemaMutex = new Mutex();
16+
17+
const AJV_SCHEMA_PATHS = {
18+
draft06: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-06.json")),
19+
draft07: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-07.json")),
20+
} as const;
21+
22+
async function createUI5ManifestValidateFunction(ui5Schema: object) {
23+
try {
24+
const ajv = new Ajv2020.default({
25+
// Collect all errors, not just the first one
26+
allErrors: true,
27+
// Allow additional properties that are not in schema such as "i18n",
28+
// otherwise compilation fails
29+
strict: false,
30+
// Don't use Unicode-aware regular expressions,
31+
// otherwise compilation fails with "Invalid escape" errors
32+
unicodeRegExp: false,
33+
loadSchema: async (uri) => {
34+
const release = await fetchSchemaMutex.acquire();
35+
36+
try {
37+
if (schemaCache.has(uri)) {
38+
log.info(`Loading cached schema: ${uri}`);
39+
return schemaCache.get(uri)!;
40+
}
41+
42+
log.info(`Loading external schema: ${uri}`);
43+
const schema = await fetchCdn(uri) as AnySchemaObject;
44+
45+
// Special handling for Adaptive Card schema to fix unsupported "id" property
46+
// According to the JSON Schema spec Draft 06 (used by Adaptive Card schema),
47+
// "$id" should be used instead of "id"
48+
// See https://github.com/microsoft/AdaptiveCards/issues/9274
49+
if (uri.includes("adaptive-card.json") && typeof schema.id === "string") {
50+
schema.$id = schema.id;
51+
delete schema.id;
52+
}
53+
54+
schemaCache.set(uri, schema);
55+
56+
return schema;
57+
} catch (error) {
58+
log.warn(`Failed to load external schema ${uri}:` +
59+
`${error instanceof Error ? error.message : String(error)}`);
60+
61+
throw error;
62+
} finally {
63+
release();
64+
}
65+
},
66+
});
67+
68+
addFormats.default(ajv);
69+
70+
const draft06MetaSchema = JSON.parse(
71+
await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8")
72+
) as AnySchemaObject;
73+
const draft07MetaSchema = JSON.parse(
74+
await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8")
75+
) as AnySchemaObject;
76+
77+
// Add meta-schemas for draft-06 and draft-07.
78+
// These are required to support schemas that reference these drafts,
79+
// for example the Adaptive Card schema and some sap.bpa.task properties.
80+
ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#");
81+
ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#");
82+
83+
const validate = await ajv.compileAsync(ui5Schema);
84+
85+
return validate;
86+
} catch (error) {
87+
throw new Error(`Failed to create UI5 manifest validate function: ` +
88+
`${error instanceof Error ? error.message : String(error)}`);
89+
}
90+
}
91+
92+
async function readManifest(path: string) {
93+
let content: string;
94+
let json: object;
95+
96+
if (!isAbsolute(path)) {
97+
throw new InvalidInputError(`The manifest path must be absolute: '${path}'`);
98+
}
99+
100+
try {
101+
content = await readFile(path, "utf-8");
102+
} catch (error) {
103+
throw new InvalidInputError(`Failed to read manifest file at ${path}: ` +
104+
`${error instanceof Error ? error.message : String(error)}`);
105+
}
106+
107+
try {
108+
json = JSON.parse(content) as object;
109+
} catch (error) {
110+
throw new InvalidInputError(`Failed to parse manifest file at ${path} as JSON: ` +
111+
`${error instanceof Error ? error.message : String(error)}`);
112+
}
113+
114+
return json;
115+
}
116+
117+
export default async function runValidation(manifestPath: string): Promise<RunSchemaValidationResult> {
118+
log.info(`Starting manifest validation for file: ${manifestPath}`);
119+
120+
const manifest = await readManifest(manifestPath);
121+
const manifestVersion = await getManifestVersion(manifest);
122+
log.info(`Using manifest version: ${manifestVersion}`);
123+
const ui5ManifestSchema = await getManifestSchema(manifestVersion);
124+
const validate = await createUI5ManifestValidateFunction(ui5ManifestSchema);
125+
const isValid = validate(manifest);
126+
127+
if (isValid) {
128+
log.info("Manifest validation successful");
129+
130+
return {
131+
isValid: true,
132+
errors: [],
133+
};
134+
}
135+
136+
// Map AJV errors to our schema format
137+
const validationErrors = validate.errors ?? [];
138+
const errors = validationErrors.map((error): RunSchemaValidationResult["errors"][number] => {
139+
return {
140+
keyword: error.keyword ?? "",
141+
instancePath: error.instancePath ?? "",
142+
schemaPath: error.schemaPath ?? "",
143+
params: error.params ?? {},
144+
propertyName: error.propertyName,
145+
message: error.message,
146+
};
147+
});
148+
149+
log.info(`Manifest validation failed with ${errors.length} error(s)`);
150+
151+
return {
152+
isValid: false,
153+
errors: errors,
154+
};
155+
}

0 commit comments

Comments
 (0)