Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sap-ux/fiori-mcp-server": patch
---

fix: improve floorplan descriptions and make service/entityConfig optional for FF_SIMPLE (Basic template)
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function command(params: ExecuteFunctionalityInput): Promise<Execut
throw new Error('Please provide a valid path to the CAP project folder.');
}

if (generatorConfig?.service.capService.serviceCdsPath) {
if (generatorConfig?.service?.capService?.serviceCdsPath) {
Comment thread
heimwege marked this conversation as resolved.
generatorConfig.service.capService.serviceCdsPath =
generatorConfig?.service.capService.serviceCdsPath?.startsWith('/')
? generatorConfig?.service.capService.serviceCdsPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ export default async function (params: ExecuteFunctionalityInput): Promise<Execu

await checkIfGeneratorInstalled();

const metadataPath = generatorConfig.service.metadataFilePath ?? join(targetDir, 'metadata.xml');
const metadataPath = generatorConfig.service?.metadataFilePath ?? join(targetDir, 'metadata.xml');
Comment thread
heimwege marked this conversation as resolved.

try {
const metadata = await FSpromises.readFile(metadataPath, { encoding: 'utf8' });
generatorConfig.service.edmx = metadata;
if (generatorConfig.service) {
const metadata = await FSpromises.readFile(metadataPath, { encoding: 'utf8' });
generatorConfig.service.edmx = metadata;
}

const content = JSON.stringify(generatorConfig, null, 4);

Expand Down Expand Up @@ -71,7 +73,7 @@ export default async function (params: ExecuteFunctionalityInput): Promise<Execu
if (existsSync(configPath)) {
await FSpromises.unlink(configPath);
}
if (existsSync(metadataPath)) {
if (generatorConfig.service && existsSync(metadataPath)) {
await FSpromises.unlink(metadataPath);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ export const PREDEFINED_GENERATOR_VALUES = {
}
};

export const floorplan = z
.literal(['FE_FPM', 'FE_LROP', 'FE_OVP', 'FE_ALP', 'FE_FEOP', 'FE_WORKLIST', 'FF_SIMPLE'])
.describe('SAP Fiori Elements floor plan type.');
export const floorplan = z.union([
z.literal('FE_LROP').describe('List Report Object Page (OData V2/V4).'),
z.literal('FE_ALP').describe('Analytical List Page (OData V2/V4).'),
z.literal('FE_OVP').describe('Overview Page (OData V2/V4).'),
z.literal('FE_WORKLIST').describe('Worklist (OData V2/V4).'),
z.literal('FE_FEOP').describe('Form Entry Object Page (OData V4 only).'),
z.literal('FE_FPM').describe('Flexible Programming Model / Custom Page (OData V4 only).'),
z
.literal('FF_SIMPLE')
.describe('Basic (SAPUI5 Freestyle template) — data source is optional for this template, supports "None".')
]);

export const project = z.object({
name: z
Expand All @@ -33,7 +41,9 @@ export const project = z.object({
export const serviceOdata = z.object({
servicePath: z
.string()
.describe('The odata endpoint. If the parameter is not provided, the agent should ask the user for it.')
.describe(
'The odata endpoint. Required for all floorplans except FF_SIMPLE. If the parameter is not provided, the agent should ask the user for it.'
)
.meta({
examples: ['/sap/opu/odata/sap/<servicename>/', '/<servicename>/']
}),
Expand Down Expand Up @@ -85,7 +95,9 @@ export const entityConfig = z.object({
mainEntity: z.object({
entityName: z
.string()
.describe('The name of the main entity. EntitySet Name attribute in OData Metadata.')
.describe(
'The name of the main entity. EntitySet Name attribute in OData Metadata. Required for all floorplans except FF_SIMPLE.'
)
.meta({ examples: ["'SalesOrder'", "'PurchaseOrderHeader'", "'MyEntity'"] })
}),
generateFormAnnotations: z
Expand Down
8 changes: 5 additions & 3 deletions packages/fiori-mcp-server/src/tools/schemas/cap-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { convertToSchema } from '../../utils';
import { entityConfig, floorplan, project, serviceCap as service } from './appgen-config-schema-props';

export const generatorConfigCAP = z.object({
entityConfig,
entityConfig: entityConfig.optional(),
floorplan,
project,
service
service: service.optional()
}).describe(`The configuration that will be used for the Application UI generation.
The configuration **MUST** be a valid JSON object corresponding to the inputSchema of the functionality.`);
The configuration **MUST** be a valid JSON object corresponding to the inputSchema of the functionality.
For floorplan FF_SIMPLE (Basic/SAPUI5 Freestyle template), service and entityConfig are optional (data source may be "None").
For all other floorplans, service and entityConfig are required.`);

// Input type for functionality parameters
export type GeneratorConfigCAP = z.infer<typeof generatorConfigCAP>;
Expand Down
10 changes: 6 additions & 4 deletions packages/fiori-mcp-server/src/tools/schemas/odata-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import { convertToSchema } from '../../utils';
import { entityConfig, floorplan, project, serviceOdata as service } from './appgen-config-schema-props';

export const generatorConfigOData = z.object({
entityConfig,
entityConfig: entityConfig.optional(),
floorplan,
project,
service
service: service.optional()
}).describe(`The configuration that will be used for the Application UI generation.
The configuration **MUST** be a valid JSON object corresponding to the inputSchema of the functionality.`);
The configuration **MUST** be a valid JSON object corresponding to the inputSchema of the functionality.
For floorplan FF_SIMPLE (Basic/SAPUI5 Freestyle template), service and entityConfig are optional (data source may be "None").
For all other floorplans, service and entityConfig are required.`);

// Input type for functionality parameters
export type GeneratorConfigOData = z.infer<typeof generatorConfigOData>;
export type GeneratorConfigODataWithAPI = GeneratorConfigOData &
typeof PREDEFINED_GENERATOR_VALUES & { service: { edmx?: string } };
typeof PREDEFINED_GENERATOR_VALUES & { service?: { edmx?: string } };

// JSON schema for functionality description
export const generatorConfigODataJson = convertToSchema(generatorConfigOData);
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Object {
"parameters": Object {
"additionalProperties": false,
"description": "The configuration that will be used for the Application UI generation.
The configuration **MUST** be a valid JSON object corresponding to the inputSchema of the functionality.",
The configuration **MUST** be a valid JSON object corresponding to the inputSchema of the functionality.
For floorplan FF_SIMPLE (Basic/SAPUI5 Freestyle template), service and entityConfig are optional (data source may be \\"None\\").
For all other floorplans, service and entityConfig are required.",
"properties": Object {
"entityConfig": Object {
"additionalProperties": false,
Expand All @@ -32,7 +34,7 @@ Object {
"additionalProperties": false,
"properties": Object {
"entityName": Object {
"description": "The name of the main entity. EntitySet Name attribute in OData Metadata.",
"description": "The name of the main entity. EntitySet Name attribute in OData Metadata. Required for all floorplans except FF_SIMPLE.",
"examples": Array [
"'SalesOrder'",
"'PurchaseOrderHeader'",
Expand All @@ -55,17 +57,43 @@ Object {
"type": "object",
},
"floorplan": Object {
"description": "SAP Fiori Elements floor plan type.",
"enum": Array [
"FE_FPM",
"FE_LROP",
"FE_OVP",
"FE_ALP",
"FE_FEOP",
"FE_WORKLIST",
"FF_SIMPLE",
"anyOf": Array [
Object {
"const": "FE_LROP",
"description": "List Report Object Page (OData V2/V4).",
"type": "string",
},
Object {
"const": "FE_ALP",
"description": "Analytical List Page (OData V2/V4).",
"type": "string",
},
Object {
"const": "FE_OVP",
"description": "Overview Page (OData V2/V4).",
"type": "string",
},
Object {
"const": "FE_WORKLIST",
"description": "Worklist (OData V2/V4).",
"type": "string",
},
Object {
"const": "FE_FEOP",
"description": "Form Entry Object Page (OData V4 only).",
"type": "string",
},
Object {
"const": "FE_FPM",
"description": "Flexible Programming Model / Custom Page (OData V4 only).",
"type": "string",
},
Object {
"const": "FF_SIMPLE",
"description": "Basic (SAPUI5 Freestyle template) — data source is optional for this template, supports \\"None\\".",
"type": "string",
},
],
"type": "string",
},
"project": Object {
"additionalProperties": false,
Expand Down Expand Up @@ -157,10 +185,8 @@ Object {
},
},
"required": Array [
"entityConfig",
"floorplan",
"project",
"service",
],
"type": "object",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,36 @@ describe('executeFunctionality', () => {
expect(config.project.sapux).toEqual(false);
});

test('executeFunctionality - success with floorplan="FF_SIMPLE" without service (no data source)', async () => {
let generatedConfigContent: string;
mockExec.mockImplementation((_cmd, _opts, callback) => {
callback(null, 'mock stdout', 'mock stderr');
});
mockFileWrite((content) => {
generatedConfigContent = content;
});
const result = await generateFioriUIApplicationCapHandlers.executeFunctionality({
appPath: join(testOutputDir, 'app1'),
functionalityId: GENERATE_FIORI_UI_APPLICATION_CAP.functionalityId,
parameters: {
floorplan: 'FF_SIMPLE',
project: {
name: 'app1',
targetFolder: join(testOutputDir, 'app1'),
title: 'App 1',
description: 'Description for App 1',
ui5Version: '1.136.7',
sapux: true
}
}
});
expect(result.status).toBe('Success');
const config = JSON.parse(generatedConfigContent!);
expect(config.project.sapux).toEqual(false);
expect(config.service).toBeUndefined();
expect(config.entityConfig).toBeUndefined();
});

test('executeFunctionality - unsuccess', async () => {
mockExec.mockImplementation((cmd, opts, callback) => {
throw new Error('Dummy');
Expand Down Expand Up @@ -239,28 +269,83 @@ describe('executeFunctionality', () => {
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Missing required fields in parameters. [
{
\\"expected\\": \\"object\\",
\\"code\\": \\"invalid_type\\",
\\"path\\": [
\\"entityConfig\\"
],
\\"message\\": \\"Invalid input: expected object, received undefined\\"
},
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_FPM\\",
\\"FE_LROP\\",
\\"FE_OVP\\",
\\"FE_ALP\\",
\\"FE_FEOP\\",
\\"FE_WORKLIST\\",
\\"FF_SIMPLE\\"
\\"code\\": \\"invalid_union\\",
\\"errors\\": [
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_LROP\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FE_LROP\\\\\\"\\"
}
],
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_ALP\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FE_ALP\\\\\\"\\"
}
],
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_OVP\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FE_OVP\\\\\\"\\"
}
],
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_WORKLIST\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FE_WORKLIST\\\\\\"\\"
}
],
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_FEOP\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FE_FEOP\\\\\\"\\"
}
],
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FE_FPM\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FE_FPM\\\\\\"\\"
}
],
[
{
\\"code\\": \\"invalid_value\\",
\\"values\\": [
\\"FF_SIMPLE\\"
],
\\"path\\": [],
\\"message\\": \\"Invalid input: expected \\\\\\"FF_SIMPLE\\\\\\"\\"
}
]
],
\\"path\\": [
\\"floorplan\\"
],
\\"message\\": \\"Invalid option: expected one of \\\\\\"FE_FPM\\\\\\"|\\\\\\"FE_LROP\\\\\\"|\\\\\\"FE_OVP\\\\\\"|\\\\\\"FE_ALP\\\\\\"|\\\\\\"FE_FEOP\\\\\\"|\\\\\\"FE_WORKLIST\\\\\\"|\\\\\\"FF_SIMPLE\\\\\\"\\"
\\"message\\": \\"Invalid input\\"
},
{
\\"expected\\": \\"object\\",
Expand All @@ -269,14 +354,6 @@ describe('executeFunctionality', () => {
\\"project\\"
],
\\"message\\": \\"Invalid input: expected object, received undefined\\"
},
{
\\"expected\\": \\"object\\",
\\"code\\": \\"invalid_type\\",
\\"path\\": [
\\"service\\"
],
\\"message\\": \\"Invalid input: expected object, received undefined\\"
}
]"
`);
Expand Down
Loading
Loading