diff --git a/.changeset/great-bees-drum.md b/.changeset/great-bees-drum.md new file mode 100644 index 00000000000..b852ec6b888 --- /dev/null +++ b/.changeset/great-bees-drum.md @@ -0,0 +1,5 @@ +--- +'@sap-ux/fiori-mcp-server': minor +--- + +Add new tool get_fiori_rules to include default rules diff --git a/packages/fiori-mcp-server/README.md b/packages/fiori-mcp-server/README.md index 2b863281c84..82bf673290f 100644 --- a/packages/fiori-mcp-server/README.md +++ b/packages/fiori-mcp-server/README.md @@ -59,23 +59,25 @@ The following rules help guide the LLM to use the server correctly: ```markdown ## Rules for creation or modification of SAP Fiori elements apps - -- When asked to create an SAP Fiori elements app check whether the user input can be interpreted as an application organized into one or more pages containing table data or forms, these can be translated into a SAP Fiori elements application, else ask the user for suitable input. -- The application typically starts with a List Report page showing the data of the base entity of the application in a table. Details of a specific table row are shown in the ObjectPage. This first Object Page is therefore based on the base entity of the application. -- An Object Page can contain one or more table sections based on to-many associations of its entity type. The details of a table section row can be shown in an another Object Page based on the associations target entity. -- The data model must be suitable for usage in a SAP Fiori elements frontend application. So there must be one main entity and one or more navigation properties to related entities. -- Each property of an entity must have a proper datatype. -- For all entities in the data model provide primary keys of type UUID. -- When creating sample data in CSV files, all primary keys and foreign keys MUST be in UUID format (e.g., `550e8400-e29b-41d4-a716-446655440001`). -- When generating or modifying the SAP Fiori elements application on top of the CAP service use the Fiori MCP server if available. -- When attempting to modify the SAP Fiori elements application like adding columns you must not use the screen personalization but instead modify the code of the project, before this first check whether an MCP server provides a suitable function. -- When previewing the SAP Fiori elements application use the most specific `npm run watch-*` script for the app in the `package.json`. +- You MUST use call the get_fiori_rules tool from fiori mcp before creating or modifying fiori applications. You Do NOT propose, suggest or make any changes without first checking. ``` Add these rules to your existing global or project-specific [`AGENTS.md`](https://agents.md/) (specifics may vary based on respective MCP client). ## [Available Tools](#available-tools) +#### `get_fiori_rules` +Returns the complete set of rules and best practices for creating or modifying SAP Fiori elements applications. + +Use this tool to understand constraints and guidelines for Fiori development, including: + +- Application structure requirements (List Report, Object Pages) +- Data model requirements (entities, navigation properties, UUIDs) +- Sample data format requirements +- Preview and testing guidelines + +This should be called when starting any Fiori application creation or modification task. + #### `search_docs` Searches SAP Fiori elements, Annotations, UI5, SAP Fiori tools documentation for the given query. diff --git a/packages/fiori-mcp-server/esbuild.config.js b/packages/fiori-mcp-server/esbuild.config.js new file mode 100644 index 00000000000..c24cb8571fc --- /dev/null +++ b/packages/fiori-mcp-server/esbuild.config.js @@ -0,0 +1,23 @@ +const { build } = require('esbuild'); + +const baseConfig = { + entryPoints: ['src/index.ts'], + bundle: true, + platform: 'node', + target: 'node20', + outdir: 'dist', + external: ['vscode', '@lancedb/lancedb', '@xenova/transformers', '@sap-ux/fiori-docs-embeddings'], + mainFields: ['module', 'main'], + loader: { + '.md': 'text' + } +}; + +const isDev = process.argv.includes('--dev'); +const isProd = process.argv.includes('--minify'); + +build({ + ...baseConfig, + sourcemap: isDev ? 'inline' : false, + minify: isProd +}).catch(() => process.exit(1)); diff --git a/packages/fiori-mcp-server/jest.config.js b/packages/fiori-mcp-server/jest.config.js index 81cfbdacdaf..03aa03db204 100644 --- a/packages/fiori-mcp-server/jest.config.js +++ b/packages/fiori-mcp-server/jest.config.js @@ -3,6 +3,10 @@ module.exports = { ...config, modulePathIgnorePatterns: [...config.modulePathIgnorePatterns, '/test/data/'], transformIgnorePatterns: ['/node_modules/(?!@xenova)'], + transform: { + ...config.transform, + '\\.md$': '/test/__mocks__/markdown-transform.js' + }, moduleNameMapper: { '^@lancedb/lancedb$': '/test/__mocks__/@lancedb/lancedb.js', '^@xenova/transformers$': '/test/__mocks__/@xenova/transformers.js' diff --git a/packages/fiori-mcp-server/package.json b/packages/fiori-mcp-server/package.json index e37ada5ee73..54fb55de65a 100644 --- a/packages/fiori-mcp-server/package.json +++ b/packages/fiori-mcp-server/package.json @@ -20,10 +20,10 @@ "license": "Apache-2.0", "main": "dist/index.js", "scripts": { - "build-esbuild-base": "esbuild --bundle --platform=node --target=node20 --outdir=dist --external:vscode --external:@lancedb/lancedb --external:@xenova/transformers --external:@sap-ux/fiori-docs-embeddings --main-fields=module,main src/index.ts", - "build-dev": "pnpm run build-esbuild-base --sourcemap=inline", + "build-esbuild-base": "node esbuild.config.js", + "build-dev": "node esbuild.config.js --dev", "build-compile": "tsc --noEmit", - "build-bundle": "pnpm run build-esbuild-base --minify", + "build-bundle": "node esbuild.config.js --minify", "build": "npm-run-all build-compile build-bundle", "watch": "tsc --watch", "bundle": "npm-run-all build tgz:package tgz:rename", diff --git a/packages/fiori-mcp-server/src/rules/fiori-rules.md b/packages/fiori-mcp-server/src/rules/fiori-rules.md new file mode 100644 index 00000000000..13001c7e8c7 --- /dev/null +++ b/packages/fiori-mcp-server/src/rules/fiori-rules.md @@ -0,0 +1,12 @@ +# Rules for creation or modification of SAP Fiori elements apps + +- When asked to create an SAP Fiori elements app check whether the user input can be interpreted as an application organized into one or more pages containing table data or forms, these can be translated into a SAP Fiori elements application, else ask the user for suitable input. +- The application typically starts with a List Report page showing the data of the base entity of the application in a table. Details of a specific table row are shown in the ObjectPage. This first Object Page is therefore based on the base entity of the application. +- An Object Page can contain one or more table sections based on to-many associations of its entity type. The details of a table section row can be shown in an another Object Page based on the associations target entity. +- The data model must be suitable for usage in a SAP Fiori elements frontend application. So there must be one main entity and one or more navigation properties to related entities. +- Each property of an entity must have a proper datatype. +- For all entities in the data model provide primary keys of type UUID. +- When creating sample data in CSV files, all primary keys and foreign keys MUST be in UUID format (e.g., `550e8400-e29b-41d4-a716-446655440001`). +- When generating or modifying the SAP Fiori elements application on top of the CAP service use the Fiori MCP server if available. +- When attempting to modify the SAP Fiori elements application like adding columns you must not use the screen personalization but instead modify the code of the project, before this first check whether an MCP server provides a suitable function. +- When previewing the SAP Fiori elements application use the most specific `npm run watch-*` script for the app in the `package.json`. diff --git a/packages/fiori-mcp-server/src/rules/get-fiori-rules.ts b/packages/fiori-mcp-server/src/rules/get-fiori-rules.ts new file mode 100644 index 00000000000..188d18e57e9 --- /dev/null +++ b/packages/fiori-mcp-server/src/rules/get-fiori-rules.ts @@ -0,0 +1,10 @@ +import fioriRulesContent from './fiori-rules.md'; + +/** + * Returns the Fiori rules content bundled at build time. + * + * @returns The Fiori rules content as a string + */ +export function getFioriRules(): string { + return fioriRulesContent; +} diff --git a/packages/fiori-mcp-server/src/server.ts b/packages/fiori-mcp-server/src/server.ts index 01cc20e710b..da0e63eb69a 100644 --- a/packages/fiori-mcp-server/src/server.ts +++ b/packages/fiori-mcp-server/src/server.ts @@ -2,7 +2,13 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, + type CallToolResult +} from '@modelcontextprotocol/sdk/types.js'; import packageJson from '../package.json'; import { docSearch, @@ -10,6 +16,7 @@ import { listFunctionalities, getFunctionalityDetails, executeFunctionality, + getFioriRules, tools } from './tools'; import { TelemetryHelper, unknownTool, type TelemetryData } from './telemetry'; @@ -48,12 +55,14 @@ export class FioriFunctionalityServer { }, { capabilities: { - tools: {} + tools: {}, + prompts: {} } } ); this.setupToolHandlers(); + this.setupPromptHandlers(); this.setupErrorHandling(); } @@ -76,6 +85,42 @@ export class FioriFunctionalityServer { await TelemetryHelper.initTelemetrySettings(); } + /** + * Sets up handlers for MCP prompts. + * Configures handlers for listing and getting prompts with Fiori rules. + */ + private setupPromptHandlers(): void { + this.server.setRequestHandler(ListPromptsRequestSchema, async () => { + return { + prompts: [ + { + name: 'fiori-rules', + description: + 'Complete set of rules and best practices for creating or modifying SAP Fiori elements applications' + } + ] + }; + }); + + this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { + if (request.params.name === 'fiori-rules') { + const rulesContent = getFioriRules(); + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: rulesContent + } + } + ] + }; + } + throw new Error(`Unknown prompt: ${request.params.name}`); + }); + } + /** * Sets up handlers for various MCP tools. * Configures handlers for listing tools, and calling specific Fiori functionality tools. @@ -114,10 +159,13 @@ export class FioriFunctionalityServer { case 'execute_functionality': result = await executeFunctionality(args as ExecuteFunctionalityInput); break; + case 'get_fiori_rules': + result = getFioriRules(); + break; default: await TelemetryHelper.sendTelemetry(unknownTool, telemetryProperties, (args as any)?.appPath); throw new Error( - `Unknown tool: ${name}. Try one of: list_fiori_apps, list_functionality, get_functionality_details, execute_functionality.` + `Unknown tool: ${name}. Try one of: list_fiori_apps, list_functionality, get_functionality_details, execute_functionality, get_fiori_rules.` ); } await TelemetryHelper.sendTelemetry(name, telemetryProperties, (args as any)?.appPath); diff --git a/packages/fiori-mcp-server/src/tools/index.ts b/packages/fiori-mcp-server/src/tools/index.ts index a8d4b8d75fc..24f05508af8 100644 --- a/packages/fiori-mcp-server/src/tools/index.ts +++ b/packages/fiori-mcp-server/src/tools/index.ts @@ -9,6 +9,7 @@ export { listFioriApps } from './list-fiori-apps'; export { listFunctionalities } from './list-functionalities'; export { getFunctionalityDetails } from './get-functionality-details'; export { executeFunctionality } from './execute-functionality'; +export { getFioriRules } from '../rules/get-fiori-rules'; export const tools = [ { @@ -71,5 +72,20 @@ export const tools = [ You MUST provide the exact parameter information obtained from get_functionality_details (Step 2).`, inputSchema: convertToSchema(Input.ExecuteFunctionalityInputSchema), outputSchema: convertToSchema(Output.ExecuteFunctionalityOutputSchema) + }, + { + name: 'get_fiori_rules', + description: `Returns the complete set of rules and best practices for creating or modifying SAP Fiori elements applications. + Use this tool to understand constraints and guidelines for Fiori development, including: + - Application structure requirements (List Report, Object Pages) + - Data model requirements (entities, navigation properties, UUIDs) + - Sample data format requirements + - Preview and testing guidelines + You MUST use this tool when starting any Fiori application creation or modification task.`, + inputSchema: { + type: 'object', + properties: {}, + required: [] + } } ] as Tool[]; diff --git a/packages/fiori-mcp-server/src/types/markdown.d.ts b/packages/fiori-mcp-server/src/types/markdown.d.ts new file mode 100644 index 00000000000..827adb7980c --- /dev/null +++ b/packages/fiori-mcp-server/src/types/markdown.d.ts @@ -0,0 +1,4 @@ +declare module '*.md' { + const content: string; + export default content; +} diff --git a/packages/fiori-mcp-server/test/__mocks__/markdown-transform.js b/packages/fiori-mcp-server/test/__mocks__/markdown-transform.js new file mode 100644 index 00000000000..a3e1c99114a --- /dev/null +++ b/packages/fiori-mcp-server/test/__mocks__/markdown-transform.js @@ -0,0 +1,13 @@ +const { readFileSync } = require('fs'); + +module.exports = { + process(sourceText, sourcePath) { + // Read the actual markdown file content + const content = readFileSync(sourcePath, 'utf-8'); + + // Return as a CommonJS module that exports the content as default + return { + code: `module.exports = ${JSON.stringify(content)};` + }; + } +}; diff --git a/packages/fiori-mcp-server/test/unit/server.test.ts b/packages/fiori-mcp-server/test/unit/server.test.ts index f2d12df6a2e..c7644d6f93d 100644 --- a/packages/fiori-mcp-server/test/unit/server.test.ts +++ b/packages/fiori-mcp-server/test/unit/server.test.ts @@ -38,9 +38,9 @@ describe('FioriFunctionalityServer', () => { // Check initialization expect(Server).toHaveBeenCalledWith( { name: 'fiori-mcp', version: expect.any(String) }, - { capabilities: { tools: {} } } + { capabilities: { tools: {}, prompts: {} } } ); - expect(setRequestHandlerMock).toHaveBeenCalledTimes(2); + expect(setRequestHandlerMock).toHaveBeenCalledTimes(4); }); test('setup tools', async () => { @@ -53,7 +53,8 @@ describe('FioriFunctionalityServer', () => { 'list_fiori_apps', 'list_functionality', 'get_functionality_details', - 'execute_functionality' + 'execute_functionality', + 'get_fiori_rules' ]); }); @@ -266,6 +267,31 @@ describe('FioriFunctionalityServer', () => { ); }); + test('get_fiori_rules', async () => { + const getFioriRulesSpy = jest.spyOn(tools, 'getFioriRules').mockReturnValue('# Rules for Fiori...'); + new FioriFunctionalityServer(); + const setRequestHandlerCall = setRequestHandlerMock.mock.calls[1]; + const onRequestCB = setRequestHandlerCall[1]; + const result = await onRequestCB({ + params: { + name: 'get_fiori_rules', + arguments: {} + } + }); + expect(getFioriRulesSpy).toHaveBeenCalledTimes(1); + expect(result.content).toEqual([ + { + text: '# Rules for Fiori...', + type: 'text' + } + ]); + expect(sendTelemetryMock).toHaveBeenLastCalledWith( + 'get_fiori_rules', + { tool: 'get_fiori_rules', functionalityId: undefined }, + undefined + ); + }); + test('Unknown tool', async () => { new FioriFunctionalityServer(); const setRequestHandlerCall = setRequestHandlerMock.mock.calls[1]; @@ -280,7 +306,7 @@ describe('FioriFunctionalityServer', () => { }); expect(result.content).toEqual([ { - text: 'Error: Unknown tool: unknown-tool-id. Try one of: list_fiori_apps, list_functionality, get_functionality_details, execute_functionality.', + text: 'Error: Unknown tool: unknown-tool-id. Try one of: list_fiori_apps, list_functionality, get_functionality_details, execute_functionality, get_fiori_rules.', type: 'text' } ]); @@ -295,6 +321,57 @@ describe('FioriFunctionalityServer', () => { }); }); + describe('Prompts', () => { + test('list_prompts', async () => { + new FioriFunctionalityServer(); + const setRequestHandlerCall = setRequestHandlerMock.mock.calls[2]; + const onRequestCB = setRequestHandlerCall[1]; + const result = await onRequestCB(); + expect(result.prompts).toEqual([ + { + name: 'fiori-rules', + description: + 'Complete set of rules and best practices for creating or modifying SAP Fiori elements applications' + } + ]); + }); + + test('get_prompt - fiori-rules', async () => { + const getFioriRulesSpy = jest.spyOn(tools, 'getFioriRules').mockReturnValue('# Fiori Rules Content...'); + new FioriFunctionalityServer(); + const setRequestHandlerCall = setRequestHandlerMock.mock.calls[3]; + const onRequestCB = setRequestHandlerCall[1]; + const result = await onRequestCB({ + params: { + name: 'fiori-rules' + } + }); + expect(getFioriRulesSpy).toHaveBeenCalledTimes(1); + expect(result.messages).toEqual([ + { + role: 'user', + content: { + type: 'text', + text: '# Fiori Rules Content...' + } + } + ]); + }); + + test('get_prompt - unknown prompt', async () => { + new FioriFunctionalityServer(); + const setRequestHandlerCall = setRequestHandlerMock.mock.calls[3]; + const onRequestCB = setRequestHandlerCall[1]; + await expect( + onRequestCB({ + params: { + name: 'unknown-prompt' + } + }) + ).rejects.toThrow('Unknown prompt: unknown-prompt'); + }); + }); + describe('Run', () => { test('execute_functionality', async () => { const server = new FioriFunctionalityServer(); diff --git a/packages/fiori-mcp-server/test/unit/tools/get-fiori-rules.test.ts b/packages/fiori-mcp-server/test/unit/tools/get-fiori-rules.test.ts new file mode 100644 index 00000000000..e8312888185 --- /dev/null +++ b/packages/fiori-mcp-server/test/unit/tools/get-fiori-rules.test.ts @@ -0,0 +1,53 @@ +import { getFioriRules } from '../../../src/rules/get-fiori-rules'; + +describe('getFioriRules', () => { + test('should return Fiori rules content as string', () => { + const rules = getFioriRules(); + + expect(typeof rules).toBe('string'); + expect(rules.length).toBeGreaterThan(0); + }); + + test('should contain expected rule sections', () => { + const rules = getFioriRules(); + + // Verify key sections exist + expect(rules).toContain('Rules for creation or modification of SAP Fiori elements apps'); + expect(rules).toContain('List Report'); + expect(rules).toContain('ObjectPage'); + }); + + test('should contain UUID requirements', () => { + const rules = getFioriRules(); + + expect(rules).toContain('UUID'); + expect(rules).toContain('primary keys'); + }); + + test('should contain data model requirements', () => { + const rules = getFioriRules(); + + expect(rules).toContain('data model'); + expect(rules).toContain('entity'); + expect(rules).toContain('navigation properties'); + }); + + test('should contain preview instructions', () => { + const rules = getFioriRules(); + + expect(rules).toContain('npm run watch'); + }); + + test('should contain MCP server references', () => { + const rules = getFioriRules(); + + expect(rules).toContain('Fiori MCP server'); + }); + + test('should contain sample data format requirements', () => { + const rules = getFioriRules(); + + expect(rules).toContain('CSV'); + expect(rules).toContain('sample data'); + }); +}); diff --git a/packages/fiori-mcp-server/test/unit/tools/index.test.ts b/packages/fiori-mcp-server/test/unit/tools/index.test.ts index 9e6b5dc7fec..8955fd19941 100644 --- a/packages/fiori-mcp-server/test/unit/tools/index.test.ts +++ b/packages/fiori-mcp-server/test/unit/tools/index.test.ts @@ -4,6 +4,7 @@ const listFioriApps = tools.find((tool) => tool.name === 'list_fiori_apps'); const listFunctionality = tools.find((tool) => tool.name === 'list_functionality'); const getFunctionalityDetails = tools.find((tool) => tool.name === 'get_functionality_details'); const executeFunctionaliy = tools.find((tool) => tool.name === 'execute_functionality'); +const getFioriRules = tools.find((tool) => tool.name === 'get_fiori_rules'); describe('Tools schemas', () => { test('list_fiori_apps', async () => { @@ -25,4 +26,14 @@ describe('Tools schemas', () => { expect(executeFunctionaliy?.inputSchema).toMatchSnapshot('Input schema for "execute_functionality"'); expect(executeFunctionaliy?.outputSchema).toMatchSnapshot('Output schema for "execute_functionality"'); }); + + test('get_fiori_rules', async () => { + expect(getFioriRules).toBeDefined(); + expect(getFioriRules?.name).toBe('get_fiori_rules'); + expect(getFioriRules?.description).toContain('rules and best practices'); + expect(getFioriRules?.inputSchema).toBeDefined(); + expect(getFioriRules?.inputSchema.type).toBe('object'); + expect(getFioriRules?.inputSchema.properties).toEqual({}); + expect(getFioriRules?.inputSchema.required).toEqual([]); + }); });