46e8d9ed by Ean Schuessler

Fix action execution - resolve screen path and traverse default subscreens

Critical fixes:
- Set resolvedScreenDef after successful literal path resolution (was falling back to webroot)
- Traverse default subscreens when looking up transitions (matching action discovery logic)
- Build full path including subscreens for framework transition execution

New features:
- moqui_batch_operations tool for multi-step workflows
- Enhanced validation error handling with field-level details
- ARIA mode now includes value, ref, description, describedby attributes

Documentation:
- Added render modes table and examples to AGENTS.md
- Updated README with MARIA format explanation
- Added MCP_SERVICE_DOCS wiki space with service parameter docs
1 parent 7c09f005
1 # Moqui MCP: AI in the Corporate Cockpit 1 # Moqui MCP: AI Agents in the Enterprise
2 2
3 **⚠️ WARNING: THIS DOG MAY EAT YOUR HOMEWORK! ⚠️** 3 **Give AI agents real jobs in real business systems.**
4 4
5 This system can give AI agents god-mode access to an entire ERP system - that's not a feature, it's a loaded weapon requiring serious security consideration. 5 [![Moqui MCP Demo](https://img.youtube.com/vi/Tauucda-NV4/0.jpg)](https://www.youtube.com/watch?v=Tauucda-NV4)
6 6
7 ## 🎥 **SEE IT WORK** 7 ## What This Is
8 8
9 [![Moqui MCP Demo](https://img.youtube.com/vi/Tauucda-NV4/0.jpg)](https://www.youtube.com/watch?v=Tauucda-NV4) 9 Moqui MCP connects AI agents to [Moqui Framework](https://www.moqui.org/), an open-source ERP platform. Through MCP (Model Context Protocol), agents can browse screens, fill forms, execute transactions, and query data - the same operations humans perform through the web interface.
10
11 **This isn't a chatbot bolted onto an ERP. It's AI with direct access to business operations.**
10 12
11 **AI agents running real business operations.** 13 ## What Agents Can Do
12 14
13 --- 15 - **Browse** the complete application hierarchy - catalog, orders, parties, accounting
16 - **Search** products, customers, inventory with full query capabilities
17 - **Create** orders, invoices, shipments, parties, products
18 - **Update** prices, quantities, statuses, relationships
19 - **Execute** workflows spanning multiple screens and services
14 20
15 ## 🚀 **THE POSSIBILITIES** 21 All operations respect Moqui's security model. Agents see only what their user account permits.
22
23 ## Why Moqui?
24
25 ERP systems are the operational backbone of business. They contain:
26
27 - **Real data**: Actual inventory levels, customer records, financial transactions
28 - **Real processes**: Order-to-cash, procure-to-pay, hire-to-retire workflows
29 - **Real constraints**: Business rules, approval chains, compliance requirements
30
31 Moqui provides all of this as open-source software with a uniquely AI-friendly architecture:
16 32
17 ### **Autonomous Business Operations** 33 - **Declarative screens**: XML definitions with rich semantic metadata
18 - **AI Purchasing Agents**: Negotiate with suppliers using real inventory data 34 - **Service-oriented**: Clean separation between UI and business logic
19 - **Dynamic Pricing**: Adjust prices based on live demand and supply constraints 35 - **Artifact security**: Fine-grained permissions on every screen, service, and entity
20 - **Workforce Intelligence**: Optimize scheduling with real financial modeling 36 - **Extensible**: Add AI-specific screens and services without forking
21 - **Supply Chain Orchestration**: Coordinate global logistics automatically 37
38 ## The MARIA Format
22 39
23 ### **Real-World Intelligence** 40 Enterprise screens are built for humans with visual context. AI agents need structured semantics. We solved this with **MARIA (MCP Accessible Rich Internet Applications)** - a JSON format based on W3C accessibility standards.
24 - **Market Analysis**: AI sees actual sales data, not just trends
25 - **Financial Forecasting**: Ground predictions in real business metrics
26 - **Risk Management**: Monitor operations for anomalies and opportunities
27 - **Compliance Automation**: Enforce business rules across all processes
28 41
29 ### **The Agentic Economy** 42 MARIA transforms Moqui screens into accessibility trees that LLMs naturally understand:
30 - **Multi-Agent Systems**: Sales, purchasing, operations AI working together
31 - **ECA/SECA Integration**: Event-driven autonomous decision making
32 - **Cross-Company Coordination**: AI agents negotiating with other AI agents
33 - **Economic Simulation**: Test strategies in real business environment
34 43
35 ERP systems naturally create the perfect **agentic theater** - casting distinct roles around HR, Sales, Production, Accounting, and more. This organizational structure provides a powerful grounding framework where AI agents can inhabit authentic business personas with clear responsibilities, workflows, and relationships. The departmental boundaries and process flows that govern human collaboration become the stage directions for autonomous agents, turning corporate hierarchy into a narrative engine for AI coordination. 44 ```json
45 {
46 "role": "document",
47 "name": "FindParty",
48 "children": [
49 {
50 "role": "form",
51 "name": "CreatePersonForm",
52 "children": [
53 {"role": "textbox", "name": "First Name", "required": true},
54 {"role": "textbox", "name": "Last Name", "required": true},
55 {"role": "combobox", "name": "Role", "options": 140},
56 {"role": "button", "name": "createPerson"}
57 ]
58 },
59 {
60 "role": "grid",
61 "name": "PartyListForm",
62 "rowcount": 47,
63 "columns": ["ID", "Name", "Username", "Role"],
64 "children": [
65 {"role": "row", "name": "John Sales"},
66 {"role": "row", "name": "Jane Accountant"}
67 ],
68 "moreRows": 45
69 }
70 ]
71 }
72 ```
36 73
37 **Every product you touch passed through an inventory system. Now AI touches back.** 74 The insight: **AI agents are a new kind of accessibility-challenged user.** They can't see pixels or interpret visual layout - they need structured semantics. This is exactly the problem ARIA solved for screen readers decades ago. But ARIA has no JSON serialization, and screen readers access accessibility trees via local OS APIs, not network protocols. MARIA fills this gap: the ARIA vocabulary, serialized as JSON, transported over MCP.
38 75
39 --- 76 Because humans and agents interact through the same semantic model, they can explain actions to each other in the same terms. "I selected 'Shipped' from the Order Status dropdown" means the same thing whether a human or an agent did it - no translation layer needed.
40 77
41 ### 💬 **From the Maintainer** 78 ### Why Integrated MARIA Beats Browser Automation
42 79
43 > *"About 50% of this is slop. Ideas for JobSandbox integration?"* 80 Tools like Playwright MCP let agents control browsers by capturing screenshots and accessibility snapshots. This works, but it's the wrong abstraction for enterprise systems:
44 81
45 **Your input shapes the roadmap.** 82 | Aspect | Playwright/Browser | Integrated MARIA |
83 |--------|-------------------|------------------|
84 | **Latency** | Screenshot → Vision model → Action | Direct JSON-RPC round-trip |
85 | **Token cost** | Images + DOM snapshots burn tokens | Semantic-only payload |
86 | **State access** | Limited to visible DOM | Full server-side context |
87 | **Security** | Browser session = full UI access | Fine-grained artifact authorization |
88 | **Reliability** | CSS changes break selectors | Stable semantic contracts |
89 | **Batch operations** | One click at a time | Bulk actions in single call |
46 90
47 --- 91 MARIA delivers the accessibility tree directly from the source - no browser rendering, no vision model, no DOM scraping. The agent gets exactly the semantic structure it needs with none of the overhead.
48 92
93 ### Render Modes
49 94
95 | Mode | Output | Use Case |
96 |------|--------|----------|
97 | `aria` | MARIA accessibility tree | Structured agent interaction |
98 | `compact` | Condensed JSON summary | Quick screen overview |
99 | `mcp` | Full semantic state | Complete metadata access |
100 | `text` | Plain text | Simple queries |
101 | `html` | Standard HTML | Debugging, human review |
50 102
51 ## Overview 103 ## Example Session
52 104
53 This implementation provides the **foundational bridge** between AI assistants and real-world business operations through Moqui ERP. It exposes the complete corporate operating system - screens, services, entities, workflows, and business rules - as MCP tools with **recursive discovery** to arbitrary depth. 105 ```
106 Agent: moqui_browse_screens(path="PopCommerce/PopCommerceAdmin/Catalog/Product/FindProduct")
54 107
55 **Think of this as giving AI agents actual jobs in real companies, with real responsibilities, real consequences, and real accountability.** 108 Server: {
109 "summary": "20 products. Forms: NewProductForm. Actions: createProduct",
110 "grids": {"ProductsForm": {"rowCount": 20, "columns": ["ID", "Name", "Type"]}},
111 "actions": {"createProduct": {"service": "create#mantle.product.Product"}}
112 }
113
114 Agent: I need to create a new product with variants.
115 moqui_browse_screens(
116 path="PopCommerce/PopCommerceAdmin/Catalog/Product/FindProduct",
117 action="createProduct",
118 parameters={"productName": "Widget Pro", "productTypeEnumId": "PtVirtual"}
119 )
120
121 Server: {
122 "result": {"status": "executed", "productId": "100042"},
123 "summary": "Product created. Navigate to EditProduct to add features."
124 }
125 ```
126
127 ## Getting Started
128
129 ```bash
130 # Clone with submodules
131 git clone --recursive https://github.com/moqui/moqui-mcp
132
133 # Build and load demo data
134 ./gradlew load
135
136 # Start server
137 ./gradlew run
138
139 # MCP endpoint: http://localhost:8080/mcp
140 ```
141
142 ### MCP Tools
143
144 | Tool | Purpose |
145 |------|---------|
146 | `moqui_browse_screens` | Navigate screens, execute actions, render content |
147 | `moqui_search_screens` | Find screens by name |
148 | `moqui_get_screen_details` | Get field metadata, dropdown options |
56 149
57 ## Architecture 150 ## Architecture
58 151
59 The implementation consists of: 152 ```
153 ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
154 │ AI Agent │────▶│ MCP Servlet │────▶│ Moqui Screen │
155 │ │◀────│ (JSON-RPC 2.0) │◀────│ Framework │
156 └─────────────────┘ └──────────────────┘ └─────────────────┘
157
158 ┌───────┴───────┐
159 ▼ ▼
160 ┌──────────────┐ ┌──────────────┐
161 │ MARIA │ │ Wiki Docs │
162 │ Transform │ │ (Guidance) │
163 └──────────────┘ └──────────────┘
164 ```
60 165
61 - **EnhancedMcpServlet** - Main MCP servlet handling JSON-RPC 2.0 protocol 166 - **MCP Servlet**: JSON-RPC 2.0 protocol, session management, authentication
62 - **McpServices** - Core services for initialization, tool discovery, and execution 167 - **Screen Framework**: Moqui's rendering engine with MCP output mode
63 - **Screen Discovery** - Recursive screen traversal with XML parsing 168 - **MARIA Transform**: Converts semantic state to accessibility tree format
64 - **Security Integration** - Moqui artifact authorization system 169 - **Wiki Docs**: Screen-specific instructions with path inheritance
65 - **Test Suite** - Java/Groovy tests (help please!)
66 170
171 ## Use Cases
67 172
173 ### Autonomous Operations
174 - Purchasing agents negotiating with supplier catalogs
175 - Inventory agents reordering based on demand forecasts
176 - Pricing agents adjusting margins in real-time
68 177
69 ## License 178 ### Assisted Workflows
179 - Customer service agents with full order history access
180 - Sales agents generating quotes from live pricing
181 - Warehouse agents coordinating picks and shipments
182
183 ### Analysis & Reporting
184 - Financial agents querying actuals vs. budgets
185 - Operations agents identifying bottlenecks
186 - Compliance agents auditing transaction trails
187
188 ## Security
70 189
71 This project is in the public domain under CC0 1.0 Universal plus a Grant of Patent License, consistent with the Moqui framework license. 190 Production deployments should:
72 191
192 - Create dedicated service accounts for AI agents
193 - Use Moqui artifact authorization to limit permissions
194 - Enable comprehensive audit logging
195 - Consider human-in-the-loop for sensitive operations
196 - Start with read-only access, expand incrementally
73 197
198 ## Status
74 199
75 ## Related Projects 200 This is an active proof-of-concept. Working:
76 201
77 - **Moqui Framework** - https://github.com/moqui/moqui-framework 202 - Screen browsing and discovery
78 - **PopCommerce** - https://github.com/moqui/PopCommerce 203 - Form submission and action execution
79 - **MCP Specification** - https://modelcontextprotocol.io/ 204 - MARIA/compact/MCP render modes
205 - Wiki documentation with inheritance
206 - Artifact security integration
80 207
208 Roadmap:
81 209
210 - Entity-level queries (beyond screen context)
211 - Service direct invocation
212 - Real-time notifications (ARIA live regions)
213 - Multi-agent coordination patterns
214
215 ## Contributing
216
217 Contributions welcome:
218
219 - Test coverage and edge cases
220 - Additional ARIA role mappings
221 - Performance optimization
222 - Documentation and examples
223 - Integration patterns for other MCP clients
224
225 ## License
82 226
83 ## Support 227 Public domain under CC0 1.0 Universal plus Grant of Patent License, consistent with Moqui Framework.
84 228
85 For issues and questions: 229 ## Links
86 230
87 1. [Moqui Forum Discussion](https://forum.moqui.org/t/poc-mcp-server-as-a-moqui-component) 231 - [Moqui Framework](https://github.com/moqui/moqui-framework)
88 2. Review test examples in `test/` 232 - [MCP Specification](https://modelcontextprotocol.io/)
89 3. Consult Moqui documentation 233 - [W3C ARIA](https://www.w3.org/WAI/ARIA/apg/)
90 4. Check server logs for detailed error information 234 - [Forum Discussion](https://forum.moqui.org/t/poc-mcp-server-as-a-moqui-component)
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
24 <!-- WikiSpace Root Page --> 24 <!-- WikiSpace Root Page -->
25 <moqui.resource.DbResource 25 <moqui.resource.DbResource
26 resourceId="WIKI_MCP_SCREEN_DOCS" 26 resourceId="WIKI_MCP_SCREEN_DOCS"
27 parentResourceId="" 27 parentResourceId="WIKI_SPACE_ROOT"
28 filename="MCP_SCREEN_DOCS.md" 28 filename="MCP_SCREEN_DOCS.md"
29 isFile="Y"/> 29 isFile="Y"/>
30 <moqui.resource.DbResourceFile 30 <moqui.resource.DbResourceFile
...@@ -653,23 +653,46 @@ parameters: {productFeatureId: "ColorBlue"} ...@@ -653,23 +653,46 @@ parameters: {productFeatureId: "ColorBlue"}
653 653
654 ## Creating Products 654 ## Creating Products
655 655
656 **IMPORTANT: productId vs pseudoId**
657
658 When creating products, Moqui uses TWO different identifiers:
659 - **pseudoId**: User-visible ID (e.g., `DEMO_VAR_GR_SM`) - used for display and searches
660 - **productId**: Internal database ID (e.g., `100000`) - auto-generated, used by all services
661
662 After calling `createProduct`, the response includes `productId` in the result. **You must use this internal productId** (not the pseudoId) for all subsequent operations like:
663 - Adding features (`createProductFeature`)
664 - Creating associations (`createProductAssoc`)
665 - Setting prices (`createProductPrice`)
666
667 Example response after createProduct:
668 ```
669 actionResult: {
670 status: "executed",
671 result: { productId: "100000" } // Use THIS ID
672 }
673 ```
674
656 ### Standard Product 675 ### Standard Product
657 ``` 676 ```
658 action: createProduct 677 action: createProduct
659 parameters: { 678 parameters: {
679 pseudoId: "MY_WIDGET_001", // Optional user-visible ID
660 productName: "New Widget", 680 productName: "New Widget",
661 productTypeEnumId: "PtAsset", 681 productTypeEnumId: "PtAsset",
662 assetTypeEnumId: "AstTpInventory" 682 assetTypeEnumId: "AstTpInventory"
663 } 683 }
684 // Response includes: productId: "100XXX" - use this for subsequent calls
664 ``` 685 ```
665 686
666 ### Virtual Product (for variants) 687 ### Virtual Product (for variants)
667 ``` 688 ```
668 action: createProduct 689 action: createProduct
669 parameters: { 690 parameters: {
691 pseudoId: "MY_VIRTUAL_001", // Optional user-visible ID
670 productName: "Widget with Options", 692 productName: "Widget with Options",
671 productTypeEnumId: "PtVirtual" 693 productTypeEnumId: "PtVirtual"
672 } 694 }
695 // Response includes: productId: "100XXX" - use this for subsequent calls
673 ``` 696 ```
674 697
675 ## Product Types 698 ## Product Types
...@@ -723,7 +746,9 @@ Manage product details, features, prices, categories, and associations. ...@@ -723,7 +746,9 @@ Manage product details, features, prices, categories, and associations.
723 746
724 ## Required Parameter 747 ## Required Parameter
725 748
726 - **productId**: Product to edit (e.g., `DEMO_1`, `100000`) 749 - **productId**: Internal product ID to edit (e.g., `DEMO_1`, `100000`)
750
751 **Note:** Always use the internal `productId` (returned from createProduct), NOT the user-visible `pseudoId`. Using pseudoId will cause referential integrity errors.
727 752
728 ## Key Subscreens 753 ## Key Subscreens
729 754
...@@ -735,9 +760,11 @@ Manage product details, features, prices, categories, and associations. ...@@ -735,9 +760,11 @@ Manage product details, features, prices, categories, and associations.
735 760
736 ## Managing Features 761 ## Managing Features
737 762
763 Features are managed directly on the EditProduct screen (not a separate EditFeatures subscreen).
764
738 ### Add Selectable Feature (Virtual Products) 765 ### Add Selectable Feature (Virtual Products)
739 ``` 766 ```
740 path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditProduct/EditFeatures 767 path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditProduct
741 action: createProductFeature 768 action: createProductFeature
742 parameters: { 769 parameters: {
743 productId: "DEMO_VAR", 770 productId: "DEMO_VAR",
...@@ -748,6 +775,7 @@ parameters: { ...@@ -748,6 +775,7 @@ parameters: {
748 775
749 ### Add Distinguishing Feature (Variant Products) 776 ### Add Distinguishing Feature (Variant Products)
750 ``` 777 ```
778 path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditProduct
751 action: createProductFeature 779 action: createProductFeature
752 parameters: { 780 parameters: {
753 productId: "DEMO_VAR_RED", 781 productId: "DEMO_VAR_RED",
...@@ -765,9 +793,11 @@ parameters: { ...@@ -765,9 +793,11 @@ parameters: {
765 793
766 ## Managing Associations 794 ## Managing Associations
767 795
796 EditAssocs is a sibling screen to EditProduct under Product (not a child of EditProduct).
797
768 ### Link Variant to Parent 798 ### Link Variant to Parent
769 ``` 799 ```
770 path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditProduct/EditAssocs 800 path: PopCommerce/PopCommerceAdmin/Catalog/Product/EditAssocs
771 action: createProductAssoc 801 action: createProductAssoc
772 parameters: { 802 parameters: {
773 productId: "DEMO_VAR", 803 productId: "DEMO_VAR",
...@@ -807,4 +837,400 @@ parameters: { ...@@ -807,4 +837,400 @@ parameters: {
807 userId="EX_JOHN_DOE" 837 userId="EX_JOHN_DOE"
808 changeDateTime="2025-01-19 00:00:00.000"/> 838 changeDateTime="2025-01-19 00:00:00.000"/>
809 839
840 <!-- ============================================== -->
841 <!-- MCP Service Documentation Wiki Space -->
842 <!-- Referenced by describedby in ARIA responses -->
843 <!-- ============================================== -->
844
845 <moqui.resource.wiki.WikiSpace wikiSpaceId="MCP_SERVICE_DOCS"
846 description="MCP Service Documentation - Parameter and usage docs for common services"
847 restrictView="N"
848 restrictUpdate="Y"
849 rootPageLocation="dbresource://WikiSpace/MCP_SERVICE_DOCS.md"/>
850
851 <!-- Service Docs Root -->
852 <moqui.resource.DbResource
853 resourceId="WIKI_MCP_SERVICE_DOCS"
854 parentResourceId="WIKI_SPACE_ROOT"
855 filename="MCP_SERVICE_DOCS.md"
856 isFile="Y"/>
857 <moqui.resource.DbResourceFile
858 resourceId="WIKI_MCP_SERVICE_DOCS"
859 mimeType="text/markdown"
860 versionName="v1">
861 <fileData><![CDATA[# MCP Service Documentation
862
863 Documentation for common Moqui services used via MCP actions.
864
865 Services are referenced by `describedby` in ARIA mode responses.
866
867 ## Service Naming Convention
868
869 - `create#Entity` - Create new record
870 - `update#Entity` - Update existing record
871 - `delete#Entity` - Delete record
872 - `store#Entity` - Create or update (upsert)
873 - `Namespace.ServiceName.verb#Noun` - Custom service]]></fileData>
874 </moqui.resource.DbResourceFile>
875
876 <!-- ==================== -->
877 <!-- Product Services -->
878 <!-- ==================== -->
879
880 <!-- Product Entity -->
881 <moqui.resource.DbResource
882 resourceId="WIKI_SVC_PRODUCT"
883 parentResourceId="WIKI_MCP_SERVICE_DOCS"
884 filename="Product.md"
885 isFile="Y"/>
886 <moqui.resource.DbResourceFile
887 resourceId="WIKI_SVC_PRODUCT"
888 mimeType="text/markdown"
889 versionName="v1">
890 <fileData><![CDATA[# Product Service
891
892 Create and update products.
893
894 ## create#mantle.product.Product
895
896 **Required:**
897 - `productName` (string): Display name
898
899 **Optional:**
900 - `pseudoId` (string): User-visible ID (auto-generated if omitted)
901 - `productTypeEnumId`: PtAsset (default), PtVirtual, PtService, PtDigital
902 - `ownerPartyId`: Organization that owns this product
903 - `description`: Long description
904 - `assetTypeEnumId`: For physical products - AstTpInventory, AstTpFixed
905 - `assetClassEnumId`: AstClsInventoryFin (standard inventory)
906
907 **Returns:**
908 - `productId`: Internal ID (use this for subsequent operations)
909
910 **Example:**
911 ```
912 action: createProduct
913 parameters: {
914 productName: "Blue Widget",
915 productTypeEnumId: "PtAsset",
916 ownerPartyId: "ORG_ZIZI_RETAIL"
917 }
918 ```
919
920 ## update#mantle.product.Product
921
922 **Required:**
923 - `productId`: Internal product ID to update
924
925 **Optional:** Any Product field (productName, description, etc.)]]></fileData>
926 </moqui.resource.DbResourceFile>
927
928 <moqui.resource.wiki.WikiPage
929 wikiPageId="MCP_SERVICE_DOCS/Product"
930 wikiSpaceId="MCP_SERVICE_DOCS"
931 pagePath="Product"
932 publishedVersionName="v1"
933 restrictView="N"/>
934
935 <!-- ProductFeature Service -->
936 <moqui.resource.DbResource
937 resourceId="WIKI_SVC_PRODUCT_FEATURE"
938 parentResourceId="WIKI_MCP_SERVICE_DOCS"
939 filename="ProductFeature.md"
940 isFile="Y"/>
941 <moqui.resource.DbResourceFile
942 resourceId="WIKI_SVC_PRODUCT_FEATURE"
943 mimeType="text/markdown"
944 versionName="v1">
945 <fileData><![CDATA[# ProductFeature Service
946
947 Apply features (like color, size) to products.
948
949 ## mantle.product.ProductServices.create#ProductFeature
950
951 **Required:**
952 - `productId`: Product to add feature to
953 - `productFeatureId`: Feature ID (e.g., ColorRed, SizeMedium)
954 - `applTypeEnumId`: How feature applies to product
955
956 **Application Types (applTypeEnumId):**
957 - `PfatSelectable`: Customer can choose (use on virtual parent)
958 - `PfatDistinguishing`: Identifies specific variant (use on variants)
959 - `PfatStandard`: Always included
960 - `PfatRequired`: Must be selected at purchase
961
962 **Optional:**
963 - `fromDate`: When feature becomes active (default: now)
964 - `sequenceNum`: Display order
965
966 **Example - Virtual Parent:**
967 ```
968 action: createProductFeature
969 parameters: {
970 productId: "DEMO_VAR",
971 productFeatureId: "ColorGreen",
972 applTypeEnumId: "PfatSelectable"
973 }
974 ```
975
976 **Example - Variant:**
977 ```
978 action: createProductFeature
979 parameters: {
980 productId: "DEMO_VAR_GR_SM",
981 productFeatureId: "ColorGreen",
982 applTypeEnumId: "PfatDistinguishing"
983 }
984 ```
985
986 ## Common Feature IDs
987
988 Colors: ColorRed, ColorBlue, ColorGreen, ColorBlack, ColorWhite
989 Sizes: SizeSmall, SizeMedium, SizeLarge, SizeXL]]></fileData>
990 </moqui.resource.DbResourceFile>
991
992 <moqui.resource.wiki.WikiPage
993 wikiPageId="MCP_SERVICE_DOCS/ProductFeature"
994 wikiSpaceId="MCP_SERVICE_DOCS"
995 pagePath="ProductFeature"
996 publishedVersionName="v1"
997 restrictView="N"/>
998
999 <!-- ProductAssoc Service -->
1000 <moqui.resource.DbResource
1001 resourceId="WIKI_SVC_PRODUCT_ASSOC"
1002 parentResourceId="WIKI_MCP_SERVICE_DOCS"
1003 filename="ProductAssoc.md"
1004 isFile="Y"/>
1005 <moqui.resource.DbResourceFile
1006 resourceId="WIKI_SVC_PRODUCT_ASSOC"
1007 mimeType="text/markdown"
1008 versionName="v1">
1009 <fileData><![CDATA[# ProductAssoc Service
1010
1011 Link products together (variants, accessories, etc.)
1012
1013 ## create#mantle.product.ProductAssoc
1014
1015 **Required:**
1016 - `productId`: Parent/source product
1017 - `toProductId`: Related product
1018 - `productAssocTypeEnumId`: Type of relationship
1019
1020 **Association Types:**
1021 - `PatVariant`: Links virtual parent to variant product
1022 - `PatAccessory`: Suggested accessory
1023 - `PatComplement`: Complementary product
1024 - `PatSubstitute`: Equivalent alternative
1025 - `PatUpSell`: Higher-tier recommendation
1026 - `PatCrossSell`: Related product suggestion
1027
1028 **Optional:**
1029 - `fromDate`: When association starts (default: now)
1030 - `quantity`: For BOM/kits
1031 - `sequenceNum`: Display order
1032
1033 **Example - Link Variant:**
1034 ```
1035 action: createProductAssoc
1036 parameters: {
1037 productId: "DEMO_VAR",
1038 toProductId: "DEMO_VAR_GR_SM",
1039 productAssocTypeEnumId: "PatVariant"
1040 }
1041 ```
1042
1043 **Note:** Use internal `productId` values, not pseudoId.]]></fileData>
1044 </moqui.resource.DbResourceFile>
1045
1046 <moqui.resource.wiki.WikiPage
1047 wikiPageId="MCP_SERVICE_DOCS/ProductAssoc"
1048 wikiSpaceId="MCP_SERVICE_DOCS"
1049 pagePath="ProductAssoc"
1050 publishedVersionName="v1"
1051 restrictView="N"/>
1052
1053 <!-- ProductPrice Service -->
1054 <moqui.resource.DbResource
1055 resourceId="WIKI_SVC_PRODUCT_PRICE"
1056 parentResourceId="WIKI_MCP_SERVICE_DOCS"
1057 filename="ProductPrice.md"
1058 isFile="Y"/>
1059 <moqui.resource.DbResourceFile
1060 resourceId="WIKI_SVC_PRODUCT_PRICE"
1061 mimeType="text/markdown"
1062 versionName="v1">
1063 <fileData><![CDATA[# ProductPrice Service
1064
1065 Manage product pricing.
1066
1067 ## create#mantle.product.ProductPrice
1068
1069 **Required:**
1070 - `productId`: Product to price
1071 - `price`: Amount
1072 - `priceTypeEnumId`: Type of price
1073 - `pricePurposeEnumId`: Purpose/context
1074
1075 **Price Types (priceTypeEnumId):**
1076 - `PptList`: Standard list price
1077 - `PptCurrent`: Current selling price
1078 - `PptMin`: Minimum allowed price
1079 - `PptMax`: Maximum allowed price
1080 - `PptCompetitive`: Competitor price
1081 - `PptPromotional`: Sale/promo price
1082
1083 **Price Purposes (pricePurposeEnumId):**
1084 - `PppPurchase`: Buying price (from supplier)
1085 - `PppSales`: Selling price (to customer)
1086
1087 **Optional:**
1088 - `priceUomId`: Currency (default: USD)
1089 - `minQuantity`: Quantity break (for tiered pricing)
1090 - `fromDate`: When price becomes active
1091 - `thruDate`: When price expires
1092 - `productStoreId`: Store-specific pricing
1093
1094 **Example - Basic Price:**
1095 ```
1096 action: createProductPrice
1097 parameters: {
1098 productId: "DEMO_VAR_GR_SM",
1099 price: 19.99,
1100 priceTypeEnumId: "PptList",
1101 pricePurposeEnumId: "PppSales",
1102 priceUomId: "USD"
1103 }
1104 ```
1105
1106 **Example - Quantity Break:**
1107 ```
1108 action: createProductPrice
1109 parameters: {
1110 productId: "DEMO_VAR_GR_SM",
1111 price: 17.99,
1112 priceTypeEnumId: "PptList",
1113 pricePurposeEnumId: "PppSales",
1114 minQuantity: 10
1115 }
1116 ```]]></fileData>
1117 </moqui.resource.DbResourceFile>
1118
1119 <moqui.resource.wiki.WikiPage
1120 wikiPageId="MCP_SERVICE_DOCS/ProductPrice"
1121 wikiSpaceId="MCP_SERVICE_DOCS"
1122 pagePath="ProductPrice"
1123 publishedVersionName="v1"
1124 restrictView="N"/>
1125
1126 <!-- ==================== -->
1127 <!-- Party Services -->
1128 <!-- ==================== -->
1129
1130 <!-- Person Service -->
1131 <moqui.resource.DbResource
1132 resourceId="WIKI_SVC_PERSON"
1133 parentResourceId="WIKI_MCP_SERVICE_DOCS"
1134 filename="Person.md"
1135 isFile="Y"/>
1136 <moqui.resource.DbResourceFile
1137 resourceId="WIKI_SVC_PERSON"
1138 mimeType="text/markdown"
1139 versionName="v1">
1140 <fileData><![CDATA[# Person Service
1141
1142 Create and manage people (parties).
1143
1144 ## mantle.party.PartyServices.create#Person
1145
1146 **Required:**
1147 - `firstName`: First name
1148 - `lastName`: Last name
1149
1150 **Optional:**
1151 - `middleName`: Middle name
1152 - `emailAddress`: Primary email
1153 - `roleTypeId`: Role (Customer, Employee, etc.)
1154 - `ownerPartyId`: Organization that owns this party record
1155
1156 **Returns:**
1157 - `partyId`: Internal party ID
1158
1159 **Example:**
1160 ```
1161 action: createPerson
1162 parameters: {
1163 firstName: "Jane",
1164 lastName: "Smith",
1165 emailAddress: "jane@example.com"
1166 }
1167 ```
1168
1169 ## mantle.party.PartyServices.update#PartyDetail
1170
1171 **Required:**
1172 - `partyId`: Party to update
1173
1174 **Optional:** firstName, lastName, middleName, comments, etc.]]></fileData>
1175 </moqui.resource.DbResourceFile>
1176
1177 <moqui.resource.wiki.WikiPage
1178 wikiPageId="MCP_SERVICE_DOCS/Person"
1179 wikiSpaceId="MCP_SERVICE_DOCS"
1180 pagePath="Person"
1181 publishedVersionName="v1"
1182 restrictView="N"/>
1183
1184 <!-- CommunicationEvent (Message) Service -->
1185 <moqui.resource.DbResource
1186 resourceId="WIKI_SVC_MESSAGE"
1187 parentResourceId="WIKI_MCP_SERVICE_DOCS"
1188 filename="Message.md"
1189 isFile="Y"/>
1190 <moqui.resource.DbResourceFile
1191 resourceId="WIKI_SVC_MESSAGE"
1192 mimeType="text/markdown"
1193 versionName="v1">
1194 <fileData><![CDATA[# Message Service
1195
1196 Send messages between parties.
1197
1198 ## mantle.party.CommunicationServices.create#Message
1199
1200 **Required:**
1201 - `toPartyId`: Recipient party ID
1202 - `subject`: Message subject
1203 - `body`: Message content
1204
1205 **Optional:**
1206 - `fromPartyId`: Sender (defaults to current user)
1207 - `purposeEnumId`: Message purpose
1208 - `communicationEventTypeId`: Message type
1209
1210 **Purpose Types (purposeEnumId):**
1211 - `CpComment`: General comment
1212 - `CpSupport`: Support request
1213 - `CpActivityRequest`: Activity/task request
1214 - `CpBilling`: Billing inquiry
1215
1216 **Example:**
1217 ```
1218 action: createMessage
1219 parameters: {
1220 toPartyId: "EX_JOHN_DOE",
1221 subject: "Green variants ready",
1222 body: "The green product variants have been created and are ready for review."
1223 }
1224 ```
1225
1226 **Screen Path:** Party/PartyMessages]]></fileData>
1227 </moqui.resource.DbResourceFile>
1228
1229 <moqui.resource.wiki.WikiPage
1230 wikiPageId="MCP_SERVICE_DOCS/Message"
1231 wikiSpaceId="MCP_SERVICE_DOCS"
1232 pagePath="Message"
1233 publishedVersionName="v1"
1234 restrictView="N"/>
1235
810 </entity-facade-xml> 1236 </entity-facade-xml>
......
...@@ -59,4 +59,10 @@ ...@@ -59,4 +59,10 @@
59 <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_JD" fromDate="2025-01-01 00:00:00.000"/> 59 <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_JD" fromDate="2025-01-01 00:00:00.000"/>
60 <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_BD" fromDate="2025-01-01 00:00:00.000"/> 60 <moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="ORG_ZIZI_BD" fromDate="2025-01-01 00:00:00.000"/>
61 --> 61 -->
62
63 <!-- Add EX_JOHN_DOE to PopcAdminSales so JohnSales can see/message John Doe in searches -->
64 <moqui.security.UserGroupMember userGroupId="PopcAdminSales" userId="EX_JOHN_DOE" fromDate="2025-01-01 00:00:00.000"/>
65
66 <!-- Set EX_JOHN_DOE's ownerPartyId to ORG_ZIZI_RETAIL so they appear in org-filtered searches -->
67 <mantle.party.Party partyId="EX_JOHN_DOE" ownerPartyId="ORG_ZIZI_RETAIL"/>
62 </entity-facade-xml> 68 </entity-facade-xml>
......
...@@ -128,6 +128,11 @@ ...@@ -128,6 +128,11 @@
128 <#assign formName = (.node["@name"]!"")?string> 128 <#assign formName = (.node["@name"]!"")?string>
129 <#assign fieldMetaList = []> 129 <#assign fieldMetaList = []>
130 <#assign dummy = ec.resource.expression("if (mcpSemanticData.formMetadata == null) mcpSemanticData.formMetadata = [:]; mcpSemanticData.formMetadata.put('" + formName?js_string + "', [name: '" + formName?js_string + "', map: '" + (mapName!"")?js_string + "'])", "")!> 130 <#assign dummy = ec.resource.expression("if (mcpSemanticData.formMetadata == null) mcpSemanticData.formMetadata = [:]; mcpSemanticData.formMetadata.put('" + formName?js_string + "', [name: '" + formName?js_string + "', map: '" + (mapName!"")?js_string + "'])", "")!>
131 <#-- Store the actual form data (current entity values) in semanticData -->
132 <#if formMap?has_content>
133 <#assign dummy = ec.context.put("tempFormMapData", formMap)!>
134 <#assign dummy = ec.resource.expression("mcpSemanticData.put('" + formName?js_string + "_data', tempFormMapData)", "")!>
135 </#if>
131 </#if> 136 </#if>
132 <#t>${sri.pushSingleFormMapContext(mapName)} 137 <#t>${sri.pushSingleFormMapContext(mapName)}
133 <#list formNode["field"] as fieldNode> 138 <#list formNode["field"] as fieldNode>
......
...@@ -195,7 +195,9 @@ ...@@ -195,7 +195,9 @@
195 // Handle internal discovery/utility tools 195 // Handle internal discovery/utility tools
196 def internalToolMappings = [ 196 def internalToolMappings = [
197 "moqui_search_screens": "McpServices.mcp#SearchScreens", 197 "moqui_search_screens": "McpServices.mcp#SearchScreens",
198 "moqui_get_screen_details": "McpServices.mcp#GetScreenDetails" 198 "moqui_get_screen_details": "McpServices.mcp#GetScreenDetails",
199 "moqui_get_help": "McpServices.mcp#GetHelp",
200 "moqui_batch_operations": "McpServices.mcp#BatchOperations"
199 ] 201 ]
200 202
201 def targetServiceName = internalToolMappings[name] 203 def targetServiceName = internalToolMappings[name]
...@@ -759,6 +761,7 @@ serializeMoquiObject = { obj, depth = 0 -> ...@@ -759,6 +761,7 @@ serializeMoquiObject = { obj, depth = 0 ->
759 761
760 // Convert MCP semantic state to ARIA accessibility tree format 762 // Convert MCP semantic state to ARIA accessibility tree format
761 // This produces a compact, standard representation matching W3C ARIA roles 763 // This produces a compact, standard representation matching W3C ARIA roles
764 // Following ARIA naming guidelines: name (short label), description (brief explanation), details (extended help)
762 def convertToAriaTree = { semanticState, targetScreenPath -> 765 def convertToAriaTree = { semanticState, targetScreenPath ->
763 if (!semanticState) return null 766 if (!semanticState) return null
764 767
...@@ -781,55 +784,91 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -781,55 +784,91 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
781 case "number": return "spinbutton" 784 case "number": return "spinbutton"
782 case "password": return "textbox" 785 case "password": return "textbox"
783 case "hidden": return null // skip hidden fields 786 case "hidden": return null // skip hidden fields
787 case "display": return "text" // read-only display
788 case "link": return "link"
784 default: return "textbox" 789 default: return "textbox"
785 } 790 }
786 } 791 }
787 792
788 // Convert a field to ARIA node 793 // Convert a field to ARIA node with full attributes
789 def fieldToAriaNode = { field -> 794 def fieldToAriaNode = { field, formDataMap ->
790 def role = fieldToAriaRole(field) 795 def role = fieldToAriaRole(field)
791 if (!role) return null 796 if (!role) return null
792 797
793 def node = [ 798 def node = [
794 role: role, 799 role: role,
795 name: field.title ?: field.name 800 name: field.title ?: field.name,
801 ref: field.name // Reference for interaction
796 ] 802 ]
797 803
804 // Add current value if available from form data
805 if (formDataMap && formDataMap[field.name] != null) {
806 node.value = formDataMap[field.name]?.toString()
807 }
808
798 // Add required attribute 809 // Add required attribute
799 if (field.required) node.required = true 810 if (field.required) node.required = true
800 811
801 // For dropdowns, show option count instead of all options 812 // For dropdowns, show option count and examples
802 if (field.type == "dropdown") { 813 if (field.type == "dropdown") {
803 def optionCount = field.options?.size() ?: 0 814 def optionCount = field.options?.size() ?: 0
804 if (field.totalOptions) optionCount = field.totalOptions 815 if (field.totalOptions) optionCount = field.totalOptions
805 if (optionCount > 0) { 816 if (optionCount > 0) {
806 node.options = optionCount 817 node.options = optionCount
818 // Show first few example values
819 if (field.options instanceof List && field.options.size() > 0) {
820 node.examples = field.options.take(3).collect { opt ->
821 opt instanceof Map ? (opt.value ?: opt.label) : opt.toString()
822 }
823 }
807 if (field.optionsTruncated) { 824 if (field.optionsTruncated) {
808 node.fetchHint = field.fetchHint 825 node.description = "Use moqui_get_screen_details for all ${optionCount} options"
809 } 826 }
810 } 827 }
811 // Check for dynamic options 828 // Check for dynamic options
812 if (field.dynamicOptions) { 829 if (field.dynamicOptions) {
813 node.autocomplete = true 830 node.autocomplete = true
831 node.description = "Type to search, options load dynamically"
814 } 832 }
815 } 833 }
816 834
817 return node 835 return node
818 } 836 }
819 837
838 // Generate description for an action based on its name and service
839 def actionDescription = { actionName, serviceName ->
840 def verb = actionName.replaceAll(/([A-Z])/, ' $1').trim().toLowerCase()
841 if (serviceName) {
842 // Extract entity/operation from service name like "create#mantle.product.Product"
843 def parts = serviceName.split('#')
844 if (parts.length == 2) {
845 def operation = parts[0]
846 def entity = parts[1].split('\\.').last()
847 return "${operation} ${entity}"
848 }
849 return serviceName.split('\\.').last()
850 }
851 return verb
852 }
853
820 // Process forms (form-single types become form landmarks) 854 // Process forms (form-single types become form landmarks)
821 formMetadata.each { formName, formData -> 855 formMetadata.each { formName, formData ->
822 if (formData.type == "form-list") return // Handle separately 856 if (formData.type == "form-list") return // Handle separately
823 857
858 // Look for form data (current values)
859 def formDataKey = "${formName}_data"
860 def formDataMap = data[formDataKey] ?: [:]
861
824 def formNode = [ 862 def formNode = [
825 role: "form", 863 role: "form",
826 name: formData.name ?: formName, 864 name: formData.name ?: formName,
865 ref: formName,
827 children: [] 866 children: []
828 ] 867 ]
829 868
830 // Add fields 869 // Add fields with values
831 formData.fields?.each { field -> 870 formData.fields?.each { field ->
832 def fieldNode = fieldToAriaNode(field) 871 def fieldNode = fieldToAriaNode(field, formDataMap)
833 if (fieldNode) formNode.children << fieldNode 872 if (fieldNode) formNode.children << fieldNode
834 } 873 }
835 874
...@@ -841,7 +880,15 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -841,7 +880,15 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
841 a.name == expectedActionName || a.name?.equalsIgnoreCase(expectedActionName) 880 a.name == expectedActionName || a.name?.equalsIgnoreCase(expectedActionName)
842 } 881 }
843 if (submitAction) { 882 if (submitAction) {
844 formNode.children << [role: "button", name: submitAction.name] 883 def btnNode = [
884 role: "button",
885 name: submitAction.name,
886 ref: submitAction.name
887 ]
888 if (submitAction.service) {
889 btnNode.description = actionDescription(submitAction.name, submitAction.service)
890 }
891 formNode.children << btnNode
845 } 892 }
846 893
847 if (formNode.children) children << formNode 894 if (formNode.children) children << formNode
...@@ -860,6 +907,7 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -860,6 +907,7 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
860 def gridNode = [ 907 def gridNode = [
861 role: "grid", 908 role: "grid",
862 name: formData.name ?: formName, 909 name: formData.name ?: formName,
910 ref: formName,
863 rowcount: itemCount 911 rowcount: itemCount
864 ] 912 ]
865 913
...@@ -867,14 +915,17 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -867,14 +915,17 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
867 def columns = formData.fields?.collect { it.title ?: it.name } 915 def columns = formData.fields?.collect { it.title ?: it.name }
868 if (columns) gridNode.columns = columns 916 if (columns) gridNode.columns = columns
869 917
870 // Add sample rows (first 3) 918 // Add sample rows with more detail
871 if (listData instanceof List && listData.size() > 0) { 919 if (listData instanceof List && listData.size() > 0) {
872 gridNode.children = [] 920 gridNode.children = []
873 listData.take(3).each { row -> 921 listData.take(3).each { row ->
874 def rowNode = [role: "row"] 922 def rowNode = [role: "row"]
875 // Extract key identifying info 923 // Extract key identifying info
876 def name = row.combinedName ?: row.name ?: row.productName ?: row.pseudoId ?: row.id 924 def id = row.pseudoId ?: row.partyId ?: row.productId ?: row.id
925 def name = row.combinedName ?: row.name ?: row.productName
926 if (id) rowNode.ref = id
877 if (name) rowNode.name = name 927 if (name) rowNode.name = name
928 if (id && name && id != name) rowNode.description = id
878 gridNode.children << rowNode 929 gridNode.children << rowNode
879 } 930 }
880 if (listData.size() > 3) { 931 if (listData.size() > 3) {
...@@ -892,7 +943,8 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -892,7 +943,8 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
892 role: "navigation", 943 role: "navigation",
893 name: "Links", 944 name: "Links",
894 children: navLinks.take(10).collect { link -> 945 children: navLinks.take(10).collect { link ->
895 [role: "link", name: link.text, path: link.path] 946 def linkNode = [role: "link", name: link.text, ref: link.path]
947 linkNode
896 } 948 }
897 ] 949 ]
898 if (navLinks.size() > 10) { 950 if (navLinks.size() > 10) {
...@@ -901,27 +953,59 @@ def convertToAriaTree = { semanticState, targetScreenPath -> ...@@ -901,27 +953,59 @@ def convertToAriaTree = { semanticState, targetScreenPath ->
901 children << navNode 953 children << navNode
902 } 954 }
903 955
904 // Process actions as a toolbar 956 // Process ALL actions as buttons (unified - no separate transitions/actions)
905 def serviceActions = semanticState.actions?.findAll { it.type == "service-action" } 957 def allActions = semanticState.actions ?: []
906 if (serviceActions && serviceActions.size() > 0) { 958 if (allActions && allActions.size() > 0) {
907 def toolbarNode = [ 959 def toolbarNode = [
908 role: "toolbar", 960 role: "toolbar",
909 name: "Actions", 961 name: "Actions",
910 children: serviceActions.take(10).collect { action -> 962 description: "Available operations on this screen",
911 [role: "button", name: action.name] 963 children: allActions.take(15).collect { action ->
964 def btnNode = [
965 role: "button",
966 name: action.name,
967 ref: action.name
968 ]
969 // Add description based on service or action type
970 if (action.service) {
971 btnNode.description = actionDescription(action.name, action.service)
972 btnNode.service = action.service
973 // Add describedby for service documentation
974 // e.g., "mantle.product.ProductServices.create#ProductFeature" -> "wiki:service:ProductServices.create#ProductFeature"
975 def serviceParts = action.service.split('\\.')
976 if (serviceParts.length > 0) {
977 btnNode.describedby = "wiki:service:${serviceParts[-1]}"
978 }
979 } else if (action.type == "screen-transition") {
980 btnNode.description = "Navigate"
981 } else if (action.type == "form-action") {
982 btnNode.description = "Form operation"
983 }
984 btnNode
912 } 985 }
913 ] 986 ]
914 if (serviceActions.size() > 10) { 987 if (allActions.size() > 15) {
915 toolbarNode.moreActions = serviceActions.size() - 10 988 toolbarNode.moreActions = allActions.size() - 15
916 } 989 }
917 children << toolbarNode 990 children << toolbarNode
918 } 991 }
919 992
920 return [ 993 // Build the main node
994 def mainNode = [
921 role: "main", 995 role: "main",
922 name: targetScreenPath?.split('/')?.last() ?: "Screen", 996 name: targetScreenPath?.split('/')?.last() ?: "Screen",
997 description: "Use ref values with action parameter to interact",
923 children: children 998 children: children
924 ] 999 ]
1000
1001 // Add describedby reference if wiki instructions exist for this screen
1002 // This follows ARIA pattern: describedby points to extended documentation
1003 // Use full screen path since that's how WikiPage.pagePath is stored
1004 if (targetScreenPath) {
1005 mainNode.describedby = "wiki:screen:${targetScreenPath}"
1006 }
1007
1008 return mainNode
925 } 1009 }
926 1010
927 // Convert MCP semantic state to compact actionable format 1011 // Convert MCP semantic state to compact actionable format
...@@ -1049,16 +1133,60 @@ def convertToCompactFormat = { semanticState, targetScreenPath -> ...@@ -1049,16 +1133,60 @@ def convertToCompactFormat = { semanticState, targetScreenPath ->
1049 // Rows with key data and links 1133 // Rows with key data and links
1050 if (listData instanceof List && listData.size() > 0) { 1134 if (listData instanceof List && listData.size() > 0) {
1051 gridInfo.rowCount = listData.size() 1135 gridInfo.rowCount = listData.size()
1136
1137 // Get field names from form definition for determining key fields
1138 def fieldNames = formData.fields?.collect { it.name } ?: []
1139
1052 gridInfo.rows = listData.take(10).collect { row -> 1140 gridInfo.rows = listData.take(10).collect { row ->
1053 def rowInfo = [:] 1141 def rowInfo = [:]
1054 1142
1055 // Get identifying info 1143 // Get identifying info
1056 def id = row.pseudoId ?: row.partyId ?: row.productId ?: row.orderId ?: row.id 1144 def id = row.pseudoId ?: row.partyId ?: row.productId ?: row.orderId ?: row.communicationEventId ?: row.id
1057 def name = row.combinedName ?: row.productName ?: row.name 1145 def name = row.combinedName ?: row.productName ?: row.organizationName ?: row.subject ?: row.name
1058 1146
1059 if (id) rowInfo.id = id 1147 if (id) rowInfo.id = id
1060 if (name && name != id) rowInfo.name = name 1148 if (name && name != id) rowInfo.name = name
1061 1149
1150 // Add key display values (2-3 additional fields beyond id/name)
1151 // Priority: status, type, date, amount, role, class fields
1152 def keyFieldPriority = [
1153 'statusId', 'status', 'productTypeEnumId', 'productAssocTypeEnumId',
1154 'communicationEventTypeId', 'roleTypeId', 'partyClassificationId',
1155 'orderPartStatusId', 'placedDate', 'entryDate', 'fromDate', 'thruDate',
1156 'grandTotal', 'quantity', 'price', 'amount',
1157 'username', 'emailAddress', 'fromPartyId', 'toPartyId'
1158 ]
1159
1160 def extraFields = [:]
1161 def extraCount = 0
1162 for (fieldName in keyFieldPriority) {
1163 if (extraCount >= 3) break
1164 def value = row[fieldName]
1165 if (value != null && value != '' && value != id && value != name) {
1166 // Simplify enumId suffixes for display
1167 def displayKey = fieldName.replaceAll(/EnumId$/, '').replaceAll(/Id$/, '')
1168 extraFields[displayKey] = value.toString()
1169 extraCount++
1170 }
1171 }
1172
1173 // If no priority fields found, add first 2-3 non-empty visible fields
1174 if (extraCount == 0) {
1175 for (fieldDef in formData.fields?.take(8)) {
1176 if (extraCount >= 3) break
1177 def fieldName = fieldDef.name
1178 if (fieldDef.type == 'hidden') continue
1179 if (fieldName in ['id', 'pseudoId', 'name', 'productId', 'partyId', 'submitButton']) continue
1180 def value = row[fieldName]
1181 if (value != null && value != '' && value != id && value != name) {
1182 extraFields[fieldName] = value.toString()
1183 extraCount++
1184 }
1185 }
1186 }
1187
1188 if (extraFields) rowInfo.data = extraFields
1189
1062 // Find link for this row 1190 // Find link for this row
1063 def rowLinks = data.links?.findAll { link -> 1191 def rowLinks = data.links?.findAll { link ->
1064 link.path?.contains(id?.toString()) && link.type == "navigation" 1192 link.path?.contains(id?.toString()) && link.type == "navigation"
...@@ -1178,6 +1306,12 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1178,6 +1306,12 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1178 } 1306 }
1179 } 1307 }
1180 1308
1309 // Set resolvedScreenDef if we successfully traversed the path
1310 if (reachedIndex >= 0 && currentSd && reachedIndex == (screenPathList.size() - 1)) {
1311 resolvedScreenDef = currentSd
1312 ec.logger.info("MCP Path Resolution: Resolved to screen '${resolvedScreenDef?.getScreenName()}' via literal path")
1313 }
1314
1181 // 2. If literal resolution failed, try Component-based resolution 1315 // 2. If literal resolution failed, try Component-based resolution
1182 if (reachedIndex == -1 && pathSegments.size() >= 2) { 1316 if (reachedIndex == -1 && pathSegments.size() >= 2) {
1183 def componentName = pathSegments[0] 1317 def componentName = pathSegments[0]
...@@ -1261,20 +1395,62 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1261,20 +1395,62 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1261 def actionResult = [:] 1395 def actionResult = [:]
1262 1396
1263 if (action && resolvedScreenDef) { 1397 if (action && resolvedScreenDef) {
1264 // Verify the transition exists 1398 // Find the transition - must traverse default subscreens like discovery does
1265 def transition = resolvedScreenDef.getAllTransitions().find { it.getName() == action } 1399 // because transitions may be defined on default subscreens, not the parent screen
1400 def transition = null
1401 def transitionScreenDef = null
1402 def subscreenPath = "" // Track the path through default subscreens
1403
1404 def currentScreenDef = resolvedScreenDef
1405 def depth = 0
1406 while (currentScreenDef && depth < 5 && !transition) {
1407 // Check this screen for the transition
1408 transition = currentScreenDef.getAllTransitions().find { it.getName() == action }
1266 if (transition) { 1409 if (transition) {
1267 // Append transition to path - framework will execute it via recursiveRunTransition 1410 transitionScreenDef = currentScreenDef
1268 relativePath = "${testScreenPath}/${action}" 1411 ec.logger.info("MCP: Found transition '${action}' on screen '${currentScreenDef.getScreenName()}' at depth ${depth}")
1412 break
1413 }
1414
1415 // Check for default subscreen to continue traversal
1416 def defaultSubscreenName = currentScreenDef.getDefaultSubscreensItem()
1417 if (defaultSubscreenName) {
1418 def subscreenItem = currentScreenDef.getSubscreensItem(defaultSubscreenName)
1419 if (subscreenItem?.location) {
1420 try {
1421 def subscreenDef = currentScreenDef.sfi.getScreenDefinition(subscreenItem.location)
1422 if (subscreenDef) {
1423 subscreenPath += "/" + defaultSubscreenName
1424 currentScreenDef = subscreenDef
1425 depth++
1426 } else {
1427 currentScreenDef = null
1428 }
1429 } catch (Exception e) {
1430 ec.logger.warn("MCP: Could not load subscreen for transition lookup: ${e.message}")
1431 currentScreenDef = null
1432 }
1433 } else {
1434 currentScreenDef = null
1435 }
1436 } else {
1437 currentScreenDef = null
1438 }
1439 }
1440
1441 if (transition) {
1442 // Append subscreen path and transition to the render path
1443 // The framework needs the full path including default subscreens to find the transition
1444 relativePath = testScreenPath + subscreenPath + "/" + action
1269 def serviceName = transition.getSingleServiceName() 1445 def serviceName = transition.getSingleServiceName()
1270 actionResult = [ 1446 actionResult = [
1271 action: action, 1447 action: action,
1272 service: serviceName, 1448 service: serviceName,
1273 status: "pending" // Will be updated after render based on errors 1449 status: "pending" // Will be updated after render based on errors
1274 ] 1450 ]
1275 ec.logger.info("MCP Screen Execution: Will execute transition '${action}' via framework (service: ${serviceName ?: 'none'})") 1451 ec.logger.info("MCP Screen Execution: Will execute transition '${action}' via framework path '${relativePath}' (service: ${serviceName ?: 'none'})")
1276 } else { 1452 } else {
1277 ec.logger.warn("MCP Screen Execution: Action '${action}' not found in screen transitions") 1453 ec.logger.warn("MCP Screen Execution: Action '${action}' not found in screen transitions (checked ${depth + 1} screens in hierarchy)")
1278 actionResult = [ 1454 actionResult = [
1279 action: action, 1455 action: action,
1280 status: "error", 1456 status: "error",
...@@ -1300,9 +1476,52 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1300,9 +1476,52 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1300 def errorMessages = testRender.getErrorMessages() 1476 def errorMessages = testRender.getErrorMessages()
1301 def hasError = errorMessages && errorMessages.size() > 0 1477 def hasError = errorMessages && errorMessages.size() > 0
1302 1478
1479 // Also check JSON response for validation errors (more structured)
1480 def jsonResponse = testRender.getJsonObject()
1481 def validationErrors = []
1482 def jsonErrors = []
1483 if (jsonResponse instanceof Map) {
1484 // Extract structured validation errors from JSON response
1485 if (jsonResponse.validationErrors) {
1486 validationErrors = jsonResponse.validationErrors.collect { ve ->
1487 // ValidationError.getMap() returns: form, field, serviceName, message
1488 [
1489 field: ve.field ?: ve.fieldPretty,
1490 form: ve.form,
1491 service: ve.serviceName,
1492 message: ve.message ?: ve.messageWithFieldPretty ?: ve.toString()
1493 ]
1494 }
1495 hasError = true
1496 }
1497 // Also capture general errors from JSON
1498 if (jsonResponse.errors) {
1499 jsonErrors = jsonResponse.errors
1500 hasError = true
1501 }
1502 }
1503
1303 if (hasError) { 1504 if (hasError) {
1304 actionResult.status = "error" 1505 actionResult.status = "error"
1305 actionResult.message = errorMessages.join("; ") 1506
1507 // Build comprehensive error message
1508 def allMessages = []
1509 if (errorMessages) allMessages.addAll(errorMessages)
1510 if (jsonErrors) allMessages.addAll(jsonErrors)
1511 actionResult.message = allMessages.join("; ") ?: "Validation failed"
1512
1513 // Add structured validation errors for field-level feedback
1514 if (validationErrors) {
1515 actionResult.validationErrors = validationErrors
1516 // Also add field-specific summary
1517 def fieldSummary = validationErrors.collect { ve ->
1518 ve.field ? "${ve.field}: ${ve.message}" : ve.message
1519 }.join("; ")
1520 if (!actionResult.message || actionResult.message == "Validation failed") {
1521 actionResult.message = fieldSummary
1522 }
1523 }
1524
1306 ec.logger.error("MCP Screen Execution: Transition '${action}' completed with errors: ${actionResult.message}") 1525 ec.logger.error("MCP Screen Execution: Transition '${action}' completed with errors: ${actionResult.message}")
1307 } else { 1526 } else {
1308 actionResult.status = "executed" 1527 actionResult.status = "executed"
...@@ -1329,6 +1548,24 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1329,6 +1548,24 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1329 } 1548 }
1330 } 1549 }
1331 } 1550 }
1551
1552 // Also check JSON response for result data
1553 if (jsonResponse instanceof Map) {
1554 // Copy any IDs from JSON response
1555 jsonResponse.each { key, value ->
1556 if (key.toString().endsWith('Id') && value && !key.toString().startsWith('_')) {
1557 def keyStr = key.toString()
1558 if (!['userId', 'username', 'sessionId', 'requestId'].contains(keyStr)) {
1559 result[keyStr] = value
1560 }
1561 }
1562 }
1563 // Copy messages if present
1564 if (jsonResponse.messages) {
1565 actionResult.messages = jsonResponse.messages
1566 }
1567 }
1568
1332 if (result) { 1569 if (result) {
1333 actionResult.result = result 1570 actionResult.result = result
1334 } 1571 }
...@@ -1339,8 +1576,22 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1339,8 +1576,22 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1339 // --- Semantic State Extraction --- 1576 // --- Semantic State Extraction ---
1340 def semanticState = [:] 1577 def semanticState = [:]
1341 1578
1342 // Get final screen definition using resolved screen location 1579 // Get final screen definition - prefer the actual rendered screen from ScreenRender
1580 // This ensures we get the deepest screen in the path, not just the root
1343 def finalScreenDef = resolvedScreenDef 1581 def finalScreenDef = resolvedScreenDef
1582 try {
1583 def screenRender = testRender.getScreenRender()
1584 if (screenRender?.screenUrlInfo?.screenPathDefList) {
1585 def pathDefList = screenRender.screenUrlInfo.screenPathDefList
1586 if (pathDefList.size() > 0) {
1587 // Get the last (deepest) screen in the path
1588 finalScreenDef = pathDefList.get(pathDefList.size() - 1)
1589 ec.logger.info("MCP: Using actual rendered screen '${finalScreenDef?.getScreenName()}' from screenPathDefList (${pathDefList.size()} screens in path)")
1590 }
1591 }
1592 } catch (Exception e) {
1593 ec.logger.warn("MCP: Could not get screen from ScreenRender, using resolved: ${e.message}")
1594 }
1344 1595
1345 if (finalScreenDef && postContext) { 1596 if (finalScreenDef && postContext) {
1346 semanticState.screenPath = inputScreenPath 1597 semanticState.screenPath = inputScreenPath
...@@ -1355,9 +1606,50 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1355,9 +1606,50 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1355 } 1606 }
1356 1607
1357 // Extract transitions (Actions) with type classification and metadata 1608 // Extract transitions (Actions) with type classification and metadata
1609 // Collect from current screen AND active subscreens (iterative traversal)
1358 semanticState.actions = [] 1610 semanticState.actions = []
1359 finalScreenDef.getAllTransitions().each { trans -> 1611 def collectedTransitionNames = [] as Set
1612
1613 // Build list of screens to process (current + default subscreens chain)
1614 def screensToProcess = []
1615 def currentScreenDef = finalScreenDef
1616 def depth = 0
1617 while (currentScreenDef && depth < 5) {
1618 screensToProcess << currentScreenDef
1619
1620 // Check for default subscreen
1621 def defaultSubscreenName = currentScreenDef.getDefaultSubscreensItem()
1622 if (defaultSubscreenName) {
1623 def subscreenItem = currentScreenDef.getSubscreensItem(defaultSubscreenName)
1624 if (subscreenItem?.location) {
1625 try {
1626 def subscreenDef = currentScreenDef.sfi.getScreenDefinition(subscreenItem.location)
1627 if (subscreenDef) {
1628 ec.logger.info("MCP: Adding default subscreen '${defaultSubscreenName}' at ${subscreenItem.location} to traversal")
1629 currentScreenDef = subscreenDef
1630 depth++
1631 } else {
1632 currentScreenDef = null
1633 }
1634 } catch (Exception e) {
1635 ec.logger.warn("MCP: Could not load subscreen ${subscreenItem.location}: ${e.message}")
1636 currentScreenDef = null
1637 }
1638 } else {
1639 currentScreenDef = null
1640 }
1641 } else {
1642 currentScreenDef = null
1643 }
1644 }
1645
1646 // Now collect transitions from all screens
1647 screensToProcess.each { screenDef ->
1648 screenDef.getAllTransitions().each { trans ->
1360 def transName = trans.getName() 1649 def transName = trans.getName()
1650 if (collectedTransitionNames.contains(transName)) return // skip duplicates
1651 collectedTransitionNames.add(transName)
1652
1361 def service = trans.getSingleServiceName() 1653 def service = trans.getSingleServiceName()
1362 1654
1363 // Classify action type 1655 // Classify action type
...@@ -1380,6 +1672,7 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1380,6 +1672,7 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1380 1672
1381 semanticState.actions << actionInfo 1673 semanticState.actions << actionInfo
1382 } 1674 }
1675 }
1383 1676
1384 // 3. Extract parameters with metadata 1677 // 3. Extract parameters with metadata
1385 semanticState.parameters = [:] 1678 semanticState.parameters = [:]
...@@ -2236,7 +2529,7 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -2236,7 +2529,7 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
2236 </service> 2529 </service>
2237 2530
2238 <service verb="mcp" noun="SearchScreens" authenticate="false" allow-remote="true" transaction-timeout="60"> 2531 <service verb="mcp" noun="SearchScreens" authenticate="false" allow-remote="true" transaction-timeout="60">
2239 <description>Search for screens by name or path.</description> 2532 <description>Search for screens by name, path, or description. Builds an index by walking the screen tree.</description>
2240 <in-parameters> 2533 <in-parameters>
2241 <parameter name="query" required="true"/> 2534 <parameter name="query" required="true"/>
2242 <parameter name="sessionId"/> 2535 <parameter name="sessionId"/>
...@@ -2250,39 +2543,178 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -2250,39 +2543,178 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
2250 2543
2251 ExecutionContext ec = context.ec 2544 ExecutionContext ec = context.ec
2252 def matches = [] 2545 def matches = []
2546 def queryLower = query.toLowerCase()
2253 2547
2254 // Helper to convert full component path to simple forward-slash path 2548 // Screen index cache key - use Moqui's cache API properly
2255 def convertToSimplePath = { fullPath -> 2549 def cacheKey = "MCP_SCREEN_INDEX"
2256 if (!fullPath) return null 2550 def mcpCache = ec.cache.getCache("mcp.screen.index")
2257 String cleanPath = fullPath 2551 def screenIndex = mcpCache.get(cacheKey)
2258 if (cleanPath.startsWith("component://")) cleanPath = cleanPath.substring(12) 2552
2259 if (cleanPath.endsWith(".xml")) cleanPath = cleanPath.substring(0, cleanPath.length() - 4) 2553 if (!screenIndex) {
2260 List<String> parts = cleanPath.split('/').toList() 2554 ec.logger.info("SearchScreens: Building screen index...")
2261 if (parts.size() > 1 && parts[1] == "screen") parts.remove(1) 2555 screenIndex = []
2262 return parts.join('/') 2556
2557 // Helper to load wiki description for a path
2558 def loadWikiDescription = { simplePath ->
2559 try {
2560 def wikiPage = ec.entity.find("moqui.resource.wiki.WikiPage")
2561 .condition("wikiSpaceId", "MCP_SCREEN_DOCS")
2562 .condition("pagePath", simplePath)
2563 .useCache(true)
2564 .one()
2565
2566 if (wikiPage) {
2567 def dbResource = ec.entity.find("moqui.resource.DbResource")
2568 .condition("parentResourceId", "WIKI_MCP_SCREEN_DOCS")
2569 .condition("filename", wikiPage.pagePath + ".md")
2570 .one()
2571
2572 if (dbResource) {
2573 def dbResourceFile = ec.entity.find("moqui.resource.DbResourceFile")
2574 .condition("resourceId", dbResource.resourceId)
2575 .one()
2576
2577 if (dbResourceFile && dbResourceFile.fileData) {
2578 def content = new String(dbResourceFile.fileData.getBytes(1L, dbResourceFile.fileData.length() as int), "UTF-8")
2579 // Extract first line or sentence as description
2580 def lines = content.split('\n')
2581 for (line in lines) {
2582 line = line.trim()
2583 if (line && !line.startsWith('#')) {
2584 return line.length() > 150 ? line.substring(0, 147) + "..." : line
2585 }
2586 }
2587 }
2588 }
2589 }
2590 } catch (Exception e) {
2591 ec.logger.debug("SearchScreens: Error loading wiki for ${simplePath}: ${e.message}")
2592 }
2593 return null
2263 } 2594 }
2264 2595
2265 // Search all screens known to the system 2596 // Recursive function to walk screen tree
2266 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView") 2597 def walkScreenTree
2267 .condition("artifactTypeEnumId", "AT_XML_SCREEN") 2598 walkScreenTree = { screenDef, basePath, depth ->
2268 .condition("artifactName", "like", "%${query}%") 2599 if (depth > 6 || !screenDef) return // Limit depth to prevent infinite loops
2269 .selectField("artifactName") 2600
2270 .distinct(true) 2601 try {
2271 .disableAuthz() 2602 screenDef.getSubscreensItemsSorted().each { subItem ->
2272 .limit(20) 2603 def subName = subItem.getName()
2273 .list() 2604 def subPath = basePath ? "${basePath}/${subName}" : subName
2605 def subLocation = subItem.getLocation()
2606
2607 if (subLocation) {
2608 // Get wiki description or generate from name
2609 def description = loadWikiDescription(subPath)
2610 if (!description) {
2611 // Generate description from screen name
2612 def humanName = subName.replaceAll(/([A-Z])/, ' $1').trim()
2613 description = "Screen: ${humanName}"
2614 }
2615
2616 // Extract screen name for search
2617 def screenName = subName
2618
2619 screenIndex << [
2620 path: subPath,
2621 name: screenName,
2622 description: description,
2623 depth: depth
2624 ]
2625
2626 // Recurse into subscreen
2627 try {
2628 def subScreenDef = ec.screen.getScreenDefinition(subLocation)
2629 if (subScreenDef) {
2630 walkScreenTree(subScreenDef, subPath, depth + 1)
2631 }
2632 } catch (Exception e) {
2633 ec.logger.debug("SearchScreens: Could not load subscreen ${subPath}: ${e.message}")
2634 }
2635 }
2636 }
2637 } catch (Exception e) {
2638 ec.logger.debug("SearchScreens: Error walking ${basePath}: ${e.message}")
2639 }
2640 }
2641
2642 // Start from webroot
2643 def webrootSd = ec.screen.getScreenDefinition("component://webroot/screen/webroot.xml")
2644 if (webrootSd) {
2645 walkScreenTree(webrootSd, "", 0)
2646 }
2647
2648 // Cache the index
2649 mcpCache.put(cacheKey, screenIndex)
2650 ec.logger.info("SearchScreens: Built index with ${screenIndex.size()} screens")
2651 }
2652
2653 // Search the index
2654 def scored = []
2655 for (screen in screenIndex) {
2656 def score = 0
2657 def nameLower = screen.name?.toLowerCase() ?: ""
2658 def pathLower = screen.path?.toLowerCase() ?: ""
2659 def descLower = screen.description?.toLowerCase() ?: ""
2660
2661 // Exact name match (highest priority)
2662 if (nameLower == queryLower) {
2663 score += 100
2664 }
2665 // Name starts with query
2666 else if (nameLower.startsWith(queryLower)) {
2667 score += 50
2668 }
2669 // Name contains query
2670 else if (nameLower.contains(queryLower)) {
2671 score += 30
2672 }
2673
2674 // Path contains query
2675 if (pathLower.contains(queryLower)) {
2676 score += 20
2677 }
2274 2678
2275 for (hit in aacvList) { 2679 // Description contains query
2276 def simplePath = convertToSimplePath(hit.artifactName) 2680 if (descLower.contains(queryLower)) {
2277 if (simplePath) { 2681 score += 10
2682 }
2683
2684 // Prefer shallower screens (more likely to be entry points)
2685 if (score > 0) {
2686 score -= (screen.depth ?: 0) * 2
2687 scored << [screen: screen, score: score]
2688 }
2689 }
2690
2691 // Sort by score descending, take top 15
2692 scored.sort { -it.score }
2693 def topMatches = scored.take(15)
2694
2695 for (item in topMatches) {
2278 matches << [ 2696 matches << [
2279 path: simplePath, 2697 path: item.screen.path,
2280 description: "Screen: ${hit.artifactName}" 2698 name: item.screen.name,
2699 description: item.screen.description
2281 ] 2700 ]
2282 } 2701 }
2702
2703 // Add hint if no matches
2704 def hint = null
2705 if (matches.isEmpty()) {
2706 hint = "No screens found matching '${query}'. Try broader terms like 'product', 'order', 'party', or use moqui_browse_screens to explore."
2707 } else if (matches.size() >= 15) {
2708 hint = "Showing top 15 results. Refine your search for more specific matches."
2283 } 2709 }
2284 2710
2285 result = [matches: matches] 2711 def resultMap = hint ? [matches: matches, hint: hint] : [matches: matches]
2712
2713 // Return in MCP format - content array with JSON text
2714 result = [
2715 content: [[type: "text", text: new groovy.json.JsonBuilder(resultMap).toString()]],
2716 isError: false
2717 ]
2286 ]]></script> 2718 ]]></script>
2287 </actions> 2719 </actions>
2288 </service> 2720 </service>
...@@ -2344,6 +2776,45 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -2344,6 +2776,45 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
2344 ] 2776 ]
2345 ], 2777 ],
2346 [ 2778 [
2779 name: "moqui_get_help",
2780 title: "Get Help",
2781 description: "Fetch extended documentation for a screen or service. Use URIs from 'describedby' fields in ARIA responses.",
2782 inputSchema: [
2783 type: "object",
2784 properties: [
2785 "uri": [type: "string", description: "Help URI (e.g., 'wiki:screen:EditProduct' or 'wiki:service:ProductFeature')"]
2786 ],
2787 required: ["uri"]
2788 ]
2789 ],
2790 [
2791 name: "moqui_batch_operations",
2792 title: "Batch Operations",
2793 description: "Execute multiple screen operations in sequence. Stops on first error. Returns results for each operation.",
2794 inputSchema: [
2795 type: "object",
2796 properties: [
2797 "operations": [
2798 type: "array",
2799 description: "Array of operations to execute in sequence",
2800 items: [
2801 type: "object",
2802 properties: [
2803 "id": [type: "string", description: "Optional operation identifier for result tracking"],
2804 "path": [type: "string", description: "Screen path"],
2805 "action": [type: "string", description: "Action/transition to execute"],
2806 "parameters": [type: "object", description: "Parameters for the action"]
2807 ],
2808 required: ["path", "action"]
2809 ]
2810 ],
2811 "stopOnError": [type: "boolean", description: "Stop execution on first error (default: true)"],
2812 "returnLastOnly": [type: "boolean", description: "Return only last operation result (default: false)"]
2813 ],
2814 required: ["operations"]
2815 ]
2816 ],
2817 [
2347 name: "prompts_list", 2818 name: "prompts_list",
2348 title: "List Prompts", 2819 title: "List Prompts",
2349 description: "List available MCP prompt templates.", 2820 description: "List available MCP prompt templates.",
...@@ -2402,6 +2873,270 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -2402,6 +2873,270 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
2402 </actions> 2873 </actions>
2403 </service> 2874 </service>
2404 2875
2876 <service verb="mcp" noun="GetHelp" authenticate="true" allow-remote="true" transaction-timeout="30">
2877 <description>Fetch extended documentation for a screen or service. Use URIs from 'describedby' fields in ARIA responses.</description>
2878 <in-parameters>
2879 <parameter name="uri" required="true"><description>Help URI (e.g., 'wiki:screen:EditProduct' or 'wiki:service:ProductFeature')</description></parameter>
2880 </in-parameters>
2881 <out-parameters>
2882 <parameter name="result" type="Map"/>
2883 </out-parameters>
2884 <actions>
2885 <script><![CDATA[
2886 import org.moqui.context.ExecutionContext
2887 import groovy.json.JsonBuilder
2888
2889 ExecutionContext ec = context.ec
2890
2891 def content = null
2892 def metadata = [uri: uri]
2893
2894 // Parse URI format: wiki:type:name
2895 // e.g., wiki:screen:EditProduct, wiki:service:ProductFeature
2896 if (uri?.startsWith("wiki:")) {
2897 def parts = uri.split(":", 3)
2898 if (parts.length >= 3) {
2899 def wikiType = parts[1] // screen, service
2900 def pageName = parts[2]
2901
2902 metadata.type = wikiType
2903 metadata.name = pageName
2904
2905 // Determine wiki space based on type
2906 def wikiSpaceId = null
2907 def pagePath = null
2908
2909 switch (wikiType) {
2910 case "screen":
2911 wikiSpaceId = "MCP_SCREEN_DOCS"
2912 // Try to find by screen name in page path
2913 pagePath = pageName
2914 break
2915 case "service":
2916 wikiSpaceId = "MCP_SERVICE_DOCS"
2917 pagePath = pageName
2918 break
2919 default:
2920 wikiSpaceId = "MCP_SCREEN_DOCS"
2921 pagePath = pageName
2922 }
2923
2924 // Try to find wiki page by path
2925 def wikiPage = ec.entity.find("moqui.resource.wiki.WikiPage")
2926 .condition("wikiSpaceId", wikiSpaceId)
2927 .condition("pagePath", pagePath)
2928 .useCache(true)
2929 .one()
2930
2931 // If not found by exact path, try partial match
2932 if (!wikiPage) {
2933 def allPages = ec.entity.find("moqui.resource.wiki.WikiPage")
2934 .condition("wikiSpaceId", wikiSpaceId)
2935 .useCache(true)
2936 .list()
2937 wikiPage = allPages.find { it.pagePath?.endsWith(pagePath) || it.pagePath?.contains(pagePath) }
2938 }
2939
2940 if (wikiPage) {
2941 // Use direct DbResource/DbResourceFile lookup (like BrowseScreens does)
2942 def parentResourceId = (wikiType == "service") ? "WIKI_MCP_SERVICE_DOCS" : "WIKI_MCP_SCREEN_DOCS"
2943
2944 def dbResource = ec.entity.find("moqui.resource.DbResource")
2945 .condition("parentResourceId", parentResourceId)
2946 .condition("filename", wikiPage.pagePath + ".md")
2947 .one()
2948
2949
2950
2951 if (dbResource) {
2952 def dbResourceFile = ec.entity.find("moqui.resource.DbResourceFile")
2953 .condition("resourceId", dbResource.resourceId)
2954 .condition("versionName", wikiPage.publishedVersionName)
2955 .one()
2956
2957 if (!dbResourceFile) {
2958 dbResourceFile = ec.entity.find("moqui.resource.DbResourceFile")
2959 .condition("resourceId", dbResource.resourceId)
2960 .one()
2961 }
2962
2963 if (dbResourceFile && dbResourceFile.fileData) {
2964 content = new String(dbResourceFile.fileData.getBytes(new Long(1).longValue(), new Long(dbResourceFile.fileData.length()).intValue()), "UTF-8")
2965 metadata.found = true
2966 metadata.pagePath = wikiPage.pagePath
2967 metadata.resourceId = dbResource.resourceId
2968 }
2969 }
2970 }
2971
2972 if (!content) {
2973 metadata.found = false
2974 content = "No documentation found for ${wikiType}: ${pageName}. Available documentation can be found in the MCP_SCREEN_DOCS and MCP_SERVICE_DOCS wiki spaces."
2975 }
2976 }
2977 } else {
2978 metadata.error = "Invalid URI format. Expected 'wiki:type:name' (e.g., 'wiki:service:ProductFeature')"
2979 content = metadata.error
2980 }
2981
2982 def resultData = [
2983 content: content,
2984 metadata: metadata
2985 ]
2986
2987 result = [
2988 content: [[type: "text", text: new JsonBuilder(resultData).toString()]],
2989 isError: false
2990 ]
2991 ]]></script>
2992 </actions>
2993 </service>
2994
2405 <!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling --> 2995 <!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling -->
2406 2996
2997 <service verb="mcp" noun="BatchOperations" authenticate="true" allow-remote="true" transaction-timeout="120">
2998 <description>Execute multiple screen operations in sequence. Stops on first error by default. Returns results for each operation.</description>
2999 <in-parameters>
3000 <parameter name="operations" type="List" required="true"><description>Array of operations to execute in sequence</description></parameter>
3001 <parameter name="stopOnError" type="Boolean" default="true"><description>Stop execution on first error (default: true)</description></parameter>
3002 <parameter name="returnLastOnly" type="Boolean" default="false"><description>Return only last operation result (default: false)</description></parameter>
3003 </in-parameters>
3004 <out-parameters>
3005 <parameter name="result" type="Map"/>
3006 </out-parameters>
3007 <actions>
3008 <script><![CDATA[
3009 import org.moqui.context.ExecutionContext
3010 import groovy.json.JsonBuilder
3011 import groovy.json.JsonSlurper
3012
3013 ExecutionContext ec = context.ec
3014
3015 def results = []
3016 def hasError = false
3017 def lastResult = null
3018 def executedCount = 0
3019
3020 ec.logger.info("BatchOperations: Starting batch with ${operations?.size()} operations, stopOnError=${stopOnError}")
3021
3022 for (int i = 0; i < operations.size(); i++) {
3023 def op = operations[i]
3024 def opId = op.id ?: "op_${i + 1}"
3025
3026 ec.logger.info("BatchOperations: Executing operation ${opId}: path=${op.path}, action=${op.action}")
3027
3028 try {
3029 // Call browse screens for each operation
3030 def browseResult = ec.service.sync().name("McpServices.mcp#BrowseScreens")
3031 .parameters([
3032 path: op.path,
3033 action: op.action,
3034 parameters: op.parameters ?: [:],
3035 renderMode: "compact" // Use compact mode for efficiency
3036 ])
3037 .call()
3038
3039 // Extract the result content
3040 def opResult = [
3041 id: opId,
3042 path: op.path,
3043 action: op.action,
3044 status: "success"
3045 ]
3046
3047 // Parse the result to check for errors
3048 if (browseResult?.result?.content) {
3049 def contentList = browseResult.result.content
3050 if (contentList && contentList.size() > 0) {
3051 def rawText = contentList[0].text
3052 if (rawText && rawText.startsWith("{")) {
3053 try {
3054 def parsed = new JsonSlurper().parseText(rawText)
3055
3056 // Check if there's an action result with error
3057 if (parsed.result?.status == "error" || parsed.actionResult?.status == "error") {
3058 opResult.status = "error"
3059 opResult.message = parsed.result?.message ?: parsed.actionResult?.message
3060 opResult.validationErrors = parsed.result?.validationErrors ?: parsed.actionResult?.validationErrors
3061 hasError = true
3062 } else {
3063 // Success - extract relevant data
3064 opResult.result = parsed.result ?: parsed.actionResult
3065 if (parsed.result?.result) {
3066 // Copy any IDs for use in subsequent operations
3067 opResult.outputIds = parsed.result.result
3068 }
3069 }
3070 } catch (e) {
3071 // JSON parse failed, treat as success but no parsed data
3072 opResult.rawOutput = rawText
3073 }
3074 }
3075 }
3076 }
3077
3078 executedCount++
3079 lastResult = opResult
3080 results << opResult
3081
3082 ec.logger.info("BatchOperations: Operation ${opId} completed with status=${opResult.status}")
3083
3084 // Stop on error if requested
3085 if (hasError && stopOnError) {
3086 ec.logger.info("BatchOperations: Stopping batch due to error in operation ${opId}")
3087 break
3088 }
3089
3090 } catch (Exception e) {
3091 def opResult = [
3092 id: opId,
3093 path: op.path,
3094 action: op.action,
3095 status: "error",
3096 message: "Exception: ${e.message}"
3097 ]
3098 results << opResult
3099 lastResult = opResult
3100 hasError = true
3101 executedCount++
3102
3103 ec.logger.error("BatchOperations: Exception in operation ${opId}: ${e.message}", e)
3104
3105 if (stopOnError) {
3106 break
3107 }
3108 }
3109 }
3110
3111 def summary = [
3112 totalOperations: operations.size(),
3113 executedOperations: executedCount,
3114 successCount: results.count { it.status == "success" },
3115 errorCount: results.count { it.status == "error" },
3116 hasError: hasError
3117 ]
3118
3119 def resultData
3120 if (returnLastOnly) {
3121 resultData = [
3122 summary: summary,
3123 result: lastResult
3124 ]
3125 } else {
3126 resultData = [
3127 summary: summary,
3128 results: results
3129 ]
3130 }
3131
3132 ec.logger.info("BatchOperations: Completed. Summary: ${summary}")
3133
3134 result = [
3135 content: [[type: "text", text: new JsonBuilder(resultData).toString()]],
3136 isError: hasError
3137 ]
3138 ]]></script>
3139 </actions>
3140 </service>
3141
2407 </services> 3142 </services>
...\ No newline at end of file ...\ No newline at end of file
......