UNPKG

bc-webclient-mcp

Version:

Model Context Protocol (MCP) server for Microsoft Dynamics 365 Business Central via WebUI protocol. Enables AI assistants to interact with BC through the web client protocol, supporting Card, List, and Document pages with full line item support and server

380 lines (308 loc) 11.3 kB
/** * Prompt Registry * * Provides parameterized workflow templates for common BC operations. * Prompts guide AI assistants through multi-step BC workflows. */ /** * Renders a prompt template with provided arguments. * Replaces {{key}} placeholders with values from args. * * @param template - The template string with {{key}} placeholders * @param args - Record of key-value pairs for interpolation * @returns Rendered template string */ export function renderPrompt(template, args) { return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, key) => { const value = args[key]; return value !== undefined ? value : match; }); } /** * Starter prompts for common BC workflows. */ export const PROMPTS = [ { name: 'create_bc_customer', description: 'Guided workflow for creating a new Business Central customer record.', arguments: [ { name: 'customerName', description: 'Full customer name', required: true }, { name: 'email', description: 'Customer email', required: false }, { name: 'phone', description: 'Customer phone', required: false }, ], template: `# Create Business Central Customer Follow this workflow to create a new customer in BC: ## Step 1: Search for Customer Page Use \`search_pages\` with: - \`query\`: "customer" - \`pageType\`: "Card" (if supported) This will return the Customer Card page ID (typically "21"). ## Step 2: Open Customer Card Use \`get_page_metadata\` with: - \`pageId\` from step 1 This returns a \`pageContextId\` that you'll use in subsequent operations. ## Step 3: Create New Record Use \`execute_action\` with: - \`pageContextId\` from step 2 - \`actionName\`: "New" This creates a new blank customer record. ## Step 4: Set Customer Fields Use \`write_page_data\` to set the following fields: - \`pageContextId\` from step 2 - \`fields\`: - **Name**: {{customerName}} - **E-Mail**: {{email}} (if provided) - **Phone No.**: {{phone}} (if provided) You can set multiple fields in a single \`write_page_data\` call: \`\`\`json { "pageContextId": "<from step 2>", "fields": { "Name": "{{customerName}}", "E-Mail": "{{email}}", "Phone No.": "{{phone}}" } } \`\`\` ## Step 5: Save / Post the Record Use \`execute_action\` with: - \`pageContextId\` from step 2 - \`actionName\`: "Post" or "Save" (depending on page behavior) ## Step 6: Confirm & Handle Errors - If any tool returns validation errors, surface them to the user - For BC errors (e.g., "Field X is required"), explain the BC business rule - Verify the record was created using \`read_page_data\` if needed ## Example Validation Errors - **Missing required fields**: BC requires "No." or auto-generates it - **Duplicate records**: "No." must be unique - **Invalid email format**: BC validates email addresses ## Tips - Use exact BC field names (case-sensitive): "E-Mail", "Phone No.", etc. - Check \`get_page_metadata\` response for available fields and required status - The "No." field is often auto-generated by BC number series `, }, { name: 'update_bc_record', description: 'Safe workflow for updating an existing BC record with validation.', arguments: [ { name: 'pageId', description: 'BC page ID', required: true }, { name: 'recordFilter', description: 'Filter to find record (JSON, e.g. {"No.": "10000"})', required: true, }, { name: 'updates', description: 'Fields to update as JSON object (e.g. {"Name": "New Name"})', required: true, }, ], template: `# Update Business Central Record Follow this workflow to safely update an existing record: ## Step 1: Open the Page Use \`get_page_metadata\` with: - \`pageId\`: {{pageId}} This returns a \`pageContextId\` for subsequent operations. ## Step 2: Locate the Target Record Use \`read_page_data\` to find the record: - \`pageContextId\` from step 1 - \`filters\`: {{recordFilter}} **Example filter**: \`\`\`json { "pageContextId": "<from step 1>", "filters": {{recordFilter}} } \`\`\` Verify that: - Exactly one record is returned (if multiple, ask user to refine filter) - The record contains the expected data ## Step 3: Apply Updates Use \`write_page_data\` with: - \`pageContextId\` from step 1 - \`fields\`: {{updates}} **Example update**: \`\`\`json { "pageContextId": "<from step 1>", "fields": {{updates}} } \`\`\` ## Step 4: Save / Post Changes Use \`execute_action\` with: - \`pageContextId\` from step 1 - \`actionName\`: "Post" or "Save" ## Step 5: Verify Result Optionally, call \`read_page_data\` again with the same filter to confirm: - Updated values are persisted - No unexpected changes occurred ## Safety Considerations 1. **Always verify the record before updating** - Use \`read_page_data\` to confirm you're updating the right record - If filter returns multiple records, ask user for more specific criteria 2. **Validate updates** - Check that field names are correct (use \`get_page_metadata\` to see available fields) - Ensure required fields are not being cleared - Respect BC business rules (e.g., can't change posted document fields) 3. **Handle errors gracefully** - If \`write_page_data\` fails, surface the BC error message to user - For validation errors, explain what BC requires - Don't retry automatically - ask user how to proceed ## Common Update Scenarios ### Update customer contact info \`\`\`json { "pageId": "21", "recordFilter": {"No.": "10000"}, "updates": { "E-Mail": "newemail@example.com", "Phone No.": "555-1234" } } \`\`\` ### Update item pricing \`\`\`json { "pageId": "30", "recordFilter": {"No.": "1000"}, "updates": { "Unit Price": "99.99", "Last Direct Cost": "50.00" } } \`\`\` ## Field Name Tips - Use exact BC field names (case-sensitive) - Common fields: "No.", "Name", "E-Mail", "Phone No.", "City", "Country/Region Code" - Check page metadata for available fields if unsure `, }, { name: 'drill_down_to_record', description: 'Guided workflow for drilling down from a List page to a Card/Document detail page.', arguments: [ { name: 'entityType', description: 'Type of entity (e.g., "customer", "vendor", "sales order")', required: true }, { name: 'recordIdentifier', description: 'How to identify the record (e.g., No., Name, Document No.)', required: true }, { name: 'recordValue', description: 'Value to search for (e.g., "10000", "Acme Corp")', required: true }, { name: 'action', description: 'Action to perform: "Edit" or "View"', required: false }, ], template: `# Drill Down to Business Central Record Follow this workflow to open a specific record's Card/Document page: ## Step 1: Identify the List Page Based on the entity type "{{entityType}}", determine the appropriate List page: - Customer -> Customer List (Page 22) - Vendor -> Vendor List (Page 27) - Item -> Item List (Page 31) - Sales Order -> Sales Orders (Page 43) - Purchase Order -> Purchase Orders (Page 51) Use \`search_pages\` if you're unsure of the page ID: - \`query\`: "{{entityType}} list" - \`pageType\`: "List" (if supported) ## Step 2: Open the List Page Use \`get_page_metadata\` with: - \`pageId\`: from step 1 This returns a \`pageContextId\` for the List page. ## Step 3: Find the Target Record Use \`read_page_data\` to locate the specific record: - \`pageContextId\`: from step 2 - \`filters\`: { "{{recordIdentifier}}": "{{recordValue}}" } **Example filter:** \`\`\`json { "pageContextId": "<from step 2>", "filters": { "{{recordIdentifier}}": "{{recordValue}}" } } \`\`\` **Important checks:** - If NO records found: Inform the user and ask for alternative criteria - If MULTIPLE records found: Either ask the user to choose OR select the most likely match and explain your choice - Extract the \`bookmark\` from the chosen record ## Step 4: Drill Down to Card/Document Use \`select_and_drill_down\` with: - \`pageContextId\`: from step 2 (List page) - \`bookmark\`: from step 3 (the record's bookmark) - \`action\`: {{action}} (use "Edit" if user wants to modify, "View" for read-only) **Example:** \`\`\`json { "pageContextId": "<from step 2>", "bookmark": "<from step 3>", "action": "{{action}}" } \`\`\` This returns: - \`sourcePageContextId\`: The List page (can reuse for other records) - \`targetPageContextId\`: The opened Card/Document page ## Step 5: Work with the Detail Page Use the \`targetPageContextId\` for subsequent operations: **To view details:** \`\`\` Use \`read_page_data\` with: - \`pageContextId\`: targetPageContextId \`\`\` **To update fields:** \`\`\` Use \`write_page_data\` with: - \`pageContextId\`: targetPageContextId - \`fields\`: { "Field Name": "New Value", ... } \`\`\` ## Error Handling **If select_and_drill_down fails:** - Check if the action ("{{action}}") exists on the page - Try alternative actions (e.g., "View" instead of "Edit") - If no suitable action exists, fall back to working on the List page directly **If record not found:** - Verify the filter criteria ("{{recordIdentifier}}" = "{{recordValue}}") - Try alternative identifiers (e.g., search by Name instead of No.) - Ask user for more specific identification ## When to Use This Pattern Use drill-down when: - User says "open", "show", or "edit the card/document" - Fields needed are only available on the Card/Document page - User wants full detail view for a specific record Avoid drill-down when: - Just reading data available in List view - Performing batch operations on multiple records - User doesn't need the Card UI ## Tips - The \`bookmark\` uniquely identifies a row and is required for drill-down - HeaderActions (Edit, View) are discovered from page metadata automatically - Keep both \`sourcePageContextId\` and \`targetPageContextId\` for navigation - Navigation triggers user consent due to potential sensitive data exposure `, }, ]; /** * Lists all available prompts. * @returns Array of prompt templates */ export function listPrompts() { return PROMPTS; } /** * Gets a prompt by name. * @param name - The prompt name * @returns The prompt template or undefined if not found */ export function getPromptByName(name) { return PROMPTS.find((p) => p.name === name); } /** * Builds a GetPromptResult by rendering a template with arguments. * @param template - The prompt template * @param args - Arguments to interpolate * @returns GetPromptResult with rendered prompt */ export function buildPromptResult(template, args) { const rendered = renderPrompt(template.template, args); return { name: template.name, description: template.description, arguments: template.arguments, prompt: rendered, }; } //# sourceMappingURL=index.js.map