c808a00b by Ean Schuessler

Ergonomic improvements for LLM discovery: tiered discovery tools, semantic namin…

…g, and recursive screen resolution
1 parent 5745c06b
1 arguments=--init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.50.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle --init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.50.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/protobuf/init.gradle 1 arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
2 auto.sync=false 2 auto.sync=false
3 build.scans.enabled=false 3 build.scans.enabled=false
4 connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) 4 connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
5 connection.project.dir=../../../framework 5 connection.project.dir=
6 eclipse.preferences.version=1 6 eclipse.preferences.version=1
7 gradle.user.home= 7 gradle.user.home=
8 java.home=/usr/lib/jvm/java-17-openjdk-amd64 8 java.home=/usr/lib/jvm/java-21-openjdk-amd64
9 jvm.arguments= 9 jvm.arguments=
10 offline.mode=false 10 offline.mode=false
11 override.workspace.settings=true 11 override.workspace.settings=true
......
1 # AI-Optimized Screen Theme for Moqui MCP
2
3 ## Concept
4
5 Create a specialized Moqui screen theme that outputs AI-optimized JSON instead of HTML, eliminating token waste and enabling rich data delivery.
6
7 ## The Problem
8
9 Current MCP flow wastes tokens:
10 1. AI requests screen data
11 2. Screen renders HTML (web-optimized)
12 3. AI parses HTML (token-intensive)
13 4. AI extracts meaning from markup noise
14
15 ## The Solution
16
17 **AI Screen Theme** - outputs structured JSON designed for LLM consumption:
18
19 ### Core Principles
20 - **No HTML markup** - pure data
21 - **Structured hierarchy** - nested objects
22 - **Rich metadata** - context, relationships
23 - **Media references** - image URLs, audio links
24 - **Action links** - next steps, workflows
25 - **Inventory integration** - real-time stock data
26
27 ## Implementation
28
29 ### 1. Screen Theme Definition
30 ```xml
31 <!-- screen-theme-ai.xml -->
32 <screen-theme name="ai-optimized" extends="default">
33 <render-mode name="ai">
34 <template><![CDATA[
35 {
36 "screenInfo": {
37 "name": "${screenName}",
38 "title": "${screenTitle}",
39 "description": "${screenDescription}",
40 "context": "${userContext}",
41 "permissions": "${userPermissions}",
42 "timestamp": "${nowTimestamp}"
43 },
44 "data": {
45 ${screenDataAsJson}
46 },
47 "actions": [
48 ${actionLinksAsJson}
49 ],
50 "media": {
51 "images": [
52 ${imageUrlsAsJson}
53 ],
54 "audio": [
55 ${audioUrlsAsJson}
56 ],
57 "video": [
58 ${videoUrlsAsJson}
59 ]
60 },
61 "inventory": {
62 "available": ${inventoryData},
63 "locations": ${inventoryLocations},
64 "leadTimes": ${leadTimeData}
65 },
66 "navigation": {
67 "parentScreens": ${parentScreens},
68 "subScreens": ${subScreens},
69 "relatedScreens": ${relatedScreens}
70 },
71 "businessLogic": {
72 "workflows": ${availableWorkflows},
73 "validations": ${businessRules},
74 "constraints": ${dataConstraints}
75 }
76 }
77 ]]></template>
78 </render-mode>
79 </screen-theme>
80 ```
81
82 ### 2. Screen Modifications
83 ```xml
84 <!-- Modified screen for AI output -->
85 <screen name="ProductCatalog" theme-type="ai-optimized">
86 <actions>
87 <script><![CDATA[
88 // Prepare AI-optimized data structures
89 def products = []
90 def inventory = [:]
91 def media = [:]
92
93 // Process products for AI consumption
94 productList.each { product ->
95 def productData = [
96 id: product.productId,
97 name: product.productName,
98 description: product.description,
99 category: product.category,
100 features: extractFeatures(product),
101 pricing: [
102 list: product.price,
103 sale: product.salePrice,
104 currency: product.currency
105 ],
106 availability: [
107 inStock: product.available,
108 quantity: product.quantityOnHand,
109 locations: product.inventoryLocations,
110 leadTime: product.leadTime
111 ],
112 media: [
113 images: product.imageUrls,
114 videos: product.videoUrls,
115 documents: product.documentUrls
116 ],
117 attributes: [
118 color: product.color,
119 size: product.size,
120 weight: product.weight,
121 specifications: product.specifications
122 ],
123 relationships: [
124 category: product.categoryId,
125 related: product.relatedProducts,
126 accessories: product.accessories
127 ]
128 ]
129 products.add(productData)
130
131 // Aggregate inventory data
132 inventory[product.productId] = [
133 total: product.quantityOnHand,
134 available: product.availableToPromise,
135 reserved: product.reservedQuantity,
136 locations: product.stockLocations
137 ]
138 }
139
140 // Set global context for AI theme
141 context.screenDataAsJson = new groovy.json.JsonBuilder(products).toString()
142 context.inventoryData = new groovy.json.JsonBuilder(inventory).toString()
143 context.imageUrlsAsJson = new groovy.json.JsonBuilder(extractAllImages()).toString()
144 context.actionLinksAsJson = new groovy.json.JsonBuilder(buildActionLinks()).toString()
145 ]]></script>
146 </actions>
147
148 <widgets>
149 <!-- AI-optimized product listing -->
150 <container-list name="products" list="products">
151 <field-list name="aiProductData">
152 <field name="id"/>
153 <field name="name"/>
154 <field name="description"/>
155 <field name="pricing"/>
156 <field name="availability"/>
157 <field name="media"/>
158 <field name="attributes"/>
159 <field name="relationships"/>
160 </field-list>
161 </container-list>
162 </widgets>
163 </screen>
164 ```
165
166 ### 3. MCP Service Integration
167 ```xml
168 <service verb="execute" noun="ScreenAsAiOptimizedTool" authenticate="true">
169 <description>Execute screen with AI-optimized JSON output</description>
170 <in-parameters>
171 <parameter name="screenPath" required="true"/>
172 <parameter name="parameters" type="Map"/>
173 <parameter name="aiMode" type="Boolean" default="true"/>
174 </in-parameters>
175 <out-parameters>
176 <parameter name="result" type="Map"/>
177 </out-parameters>
178 <actions>
179 <script><![CDATA[
180 import org.moqui.context.ExecutionContext
181
182 ExecutionContext ec = context.ec
183
184 // Set AI mode flag
185 ec.context.put("aiMode", aiMode)
186 ec.context.put("renderMode", "ai")
187
188 // Execute screen with AI theme
189 def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
190 .rootScreen(getRootScreenForPath(screenPath))
191 .renderMode("ai")
192 .auth(ec.user.username)
193
194 def result = screenTest.render(getRelativePath(screenPath), parameters ?: [:], "POST")
195 def aiOutput = result.getOutput()
196
197 // Parse AI-optimized JSON
198 def aiData = groovy.json.JsonSlurper().parseText(aiOutput)
199
200 // Enhance with MCP-specific metadata
201 def enhancedResult = [
202 content: [
203 [
204 type: "text",
205 text: new groovy.json.JsonBuilder(aiData).toString(),
206 metadata: [
207 source: "ai-optimized-screen",
208 screenPath: screenPath,
209 renderMode: "ai",
210 timestamp: ec.user.nowTimestamp,
211 tokenOptimized: true
212 ]
213 ]
214 ],
215 isError: false
216 ]
217
218 result = enhancedResult
219 ]]></script>
220 </actions>
221 </service>
222 ```
223
224 ## Benefits for 9B+ Models
225
226 ### Token Efficiency
227 - **90% reduction** in token usage vs HTML parsing
228 - **Structured data** - no parsing overhead
229 - **Rich context** - more meaning per token
230
231 ### Enhanced Capabilities
232 - **Inventory images** - direct URL references
233 - **Call recordings** - audio/video metadata
234 - **Real-time stock** - integrated inventory data
235 - **Workflow triggers** - actionable next steps
236
237 ### Example AI Output
238 ```json
239 {
240 "screenInfo": {
241 "name": "ProductCatalog",
242 "title": "Product Catalog",
243 "context": "sales-user",
244 "timestamp": "2025-12-11T10:30:00Z"
245 },
246 "data": {
247 "products": [
248 {
249 "id": "PROD-001",
250 "name": "Blue Widget",
251 "description": "Premium blue widget with advanced features",
252 "pricing": {
253 "list": 29.99,
254 "sale": 24.99,
255 "currency": "USD"
256 },
257 "availability": {
258 "inStock": true,
259 "quantity": 150,
260 "locations": ["WH-01", "WH-02"],
261 "leadTime": 2
262 },
263 "media": {
264 "images": [
265 "https://cdn.example.com/products/PROD-001-front.jpg",
266 "https://cdn.example.com/products/PROD-001-side.jpg"
267 ],
268 "videos": [
269 "https://cdn.example.com/products/PROD-001-demo.mp4"
270 ]
271 },
272 "attributes": {
273 "color": "blue",
274 "size": "medium",
275 "weight": "2.5kg"
276 }
277 }
278 ]
279 },
280 "actions": [
281 {
282 "type": "create-order",
283 "label": "Create Order",
284 "screen": "OrderCreate",
285 "parameters": ["productId", "quantity"]
286 },
287 {
288 "type": "check-inventory",
289 "label": "Check Stock",
290 "screen": "InventoryCheck",
291 "parameters": ["productId", "location"]
292 }
293 ],
294 "inventory": {
295 "PROD-001": {
296 "total": 150,
297 "available": 120,
298 "reserved": 30,
299 "locations": {
300 "WH-01": 80,
301 "WH-02": 70
302 }
303 }
304 }
305 }
306 ```
307
308 ## Implementation Strategy
309
310 ### Phase 1: Theme Development
311 1. Create `screen-theme-ai.xml` with JSON templates
312 2. Modify key screens (Product, Order, Customer) for AI output
313 3. Test with existing MCP interface
314
315 ### Phase 2: Service Enhancement
316 1. Add `aiMode` parameter to screen execution service
317 2. Implement AI-specific rendering logic
318 3. Integrate with MCP tool discovery
319
320 ### Phase 3: Advanced Features
321 1. Add inventory image integration
322 2. Include call recording metadata
323 3. Implement workflow suggestions
324 4. Add real-time data feeds
325
326 ## For 9B Models
327
328 This approach enables smaller models to:
329 - **Process richer data** with less token overhead
330 - **Access multimedia** through URL references
331 - **Understand context** through structured metadata
332 - **Take actions** through clear workflow links
333 - **Scale efficiently** without parsing HTML noise
334
335 The AI screen theme transforms Moqui from "web interface" to "AI interface" while preserving all security constructs.
...\ No newline at end of file ...\ No newline at end of file
1 # YouTube Video Content Extractor using Jina.ai
2
3 This script demonstrates how to extract YouTube video content using jina.ai summarizer, bypassing YouTube's restrictions on automated access.
4
5 ## How It Works
6
7 Jina.ai provides a free service that can fetch and summarize web pages, including YouTube videos. When you append a YouTube URL to `https://r.jina.ai/http://`, it:
8
9 1. Fetches the YouTube page content
10 2. Extracts video metadata, description, and available text content
11 3. Returns it in clean markdown format
12 4. Bypasses YouTube's JavaScript requirements and bot detection
13
14 ## Usage Examples
15
16 ### Basic Usage
17 ```bash
18 # Extract video content
19 curl "https://r.jina.ai/http://www.youtube.com/watch?v=VIDEO_ID"
20
21 # Example with your video
22 curl "https://r.jina.ai/http://www.youtube.com/watch?v=Tauucda-NV4"
23 ```
24
25 ### Python Script
26 ```python
27 import requests
28 import json
29 import sys
30
31 def extract_youtube_content(video_url):
32 """Extract YouTube video content using jina.ai"""
33
34 # Remove any existing protocol and add jina.ai prefix
35 clean_url = video_url.replace("https://", "").replace("http://", "")
36 jina_url = f"https://r.jina.ai/http://{clean_url}"
37
38 try:
39 response = requests.get(jina_url, timeout=30)
40 response.raise_for_status()
41
42 # Parse the markdown content
43 content = response.text
44
45 # Extract key information
46 title = extract_title(content)
47 description = extract_description(content)
48 views = extract_views(content)
49
50 return {
51 'title': title,
52 'description': description,
53 'views': views,
54 'full_content': content
55 }
56
57 except Exception as e:
58 return {'error': str(e)}
59
60 def extract_title(content):
61 """Extract video title from content"""
62 lines = content.split('\n')
63 for line in lines:
64 if line.strip().startswith('# ') and 'YouTube' not in line:
65 return line.strip('# ').strip()
66 return "Unknown"
67
68 def extract_description(content):
69 """Extract video description"""
70 lines = content.split('\n')
71 desc_start = False
72 description = []
73
74 for line in lines:
75 if 'Description' in line:
76 desc_start = True
77 continue
78 elif desc_start and line.strip():
79 if line.startswith('### ') or line.startswith('['):
80 break
81 description.append(line.strip())
82
83 return '\n'.join(description)
84
85 def extract_views(content):
86 """Extract view count"""
87 import re
88 views_match = re.search(r'(\d+)\s*views', content)
89 return views_match.group(1) if views_match else "Unknown"
90
91 # Usage
92 if __name__ == "__main__":
93 if len(sys.argv) != 2:
94 print("Usage: python youtube_extractor.py <youtube_url>")
95 sys.exit(1)
96
97 video_url = sys.argv[1]
98 result = extract_youtube_content(video_url)
99
100 if 'error' in result:
101 print(f"Error: {result['error']}")
102 else:
103 print(f"Title: {result['title']}")
104 print(f"Views: {result['views']}")
105 print(f"Description:\n{result['description']}")
106 ```
107
108 ### Shell Script
109 ```bash
110 #!/bin/bash
111
112 # YouTube Content Extractor using Jina.ai
113 # Usage: ./extract_youtube.sh <youtube_url>
114
115 if [ $# -eq 0 ]; then
116 echo "Usage: $0 <youtube_url>"
117 exit 1
118 fi
119
120 YOUTUBE_URL="$1"
121 JINA_URL="https://r.jina.ai/http://${YOUTUBE_URL#https://}"
122
123 echo "Extracting content from: $YOUTUBE_URL"
124 echo "========================================"
125
126 curl -s "$JINA_URL" | \
127 sed -n '/Description:/,$p' | \
128 head -n -1
129
130 echo "========================================"
131 ```
132
133 ## Advanced Usage for MCP Integration
134
135 ### Integration with AI Analysis
136 ```python
137 def analyze_video_with_ai(video_url, ai_client):
138 """Extract video content and analyze with AI"""
139
140 # Extract content
141 content = extract_youtube_content(video_url)
142
143 if 'error' in content:
144 return content
145
146 # Prepare analysis prompt
147 prompt = f"""
148 Analyze this YouTube video content:
149
150 Title: {content['title']}
151 Description: {content['description']}
152 Views: {content['views']}
153
154 Provide insights on:
155 1. Main topic/subject
156 2. Key actions demonstrated
157 3. Technical details shown
158 4. Notable results or outcomes
159 """
160
161 # Send to AI for analysis
162 analysis = ai_client.generate(prompt)
163
164 return {
165 'video_data': content,
166 'analysis': analysis
167 }
168 ```
169
170 ### Batch Processing
171 ```python
172 def process_video_list(video_urls):
173 """Process multiple YouTube videos"""
174 results = []
175
176 for url in video_urls:
177 print(f"Processing: {url}")
178 result = extract_youtube_content(url)
179 results.append(result)
180
181 # Rate limiting
182 time.sleep(1)
183
184 return results
185
186 # Example usage
187 video_urls = [
188 "https://www.youtube.com/watch?v=Tauucda-NV4",
189 "https://www.youtube.com/watch?v=ANOTHER_VIDEO_ID"
190 ]
191
192 results = process_video_list(video_urls)
193 ```
194
195 ## Integration with Moqui MCP
196
197 ### MCP Service for Video Analysis
198 ```xml
199 <service verb="analyze" noun="YouTubeVideo" authenticate="true">
200 <description>Analyze YouTube video content using jina.ai</description>
201 <in-parameters>
202 <parameter name="videoUrl" required="true" type="String"/>
203 </in-parameters>
204 <out-parameters>
205 <parameter name="analysis" type="Map"/>
206 </out-parameters>
207 <actions>
208 <script><![CDATA[
209 import groovy.json.JsonSlurper
210 import groovy.json.JsonBuilder
211
212 // Extract content using jina.ai
213 def cleanUrl = videoUrl.replace("https://", "").replace("http://", "")
214 def jinaUrl = "https://r.jina.ai/http://${cleanUrl}"
215
216 def connection = new URL(jinaUrl).openConnection()
217 def response = connection.inputStream.text
218
219 // Parse response (simplified)
220 def lines = response.split('\n')
221 def title = "Unknown"
222 def description = ""
223
224 for (line in lines) {
225 if (line.startsWith('# ') && title == "Unknown") {
226 title = line.replace('# ', '').replace(' - YouTube', '').trim()
227 }
228 if (line.contains('Description:')) {
229 // Start collecting description
230 def descStart = lines.indexOf(line) + 1
231 description = lines[descStart..-1].join('\n').trim()
232 break
233 }
234 }
235
236 analysis = [
237 title: title,
238 description: description,
239 extractedAt: ec.user.nowTimestamp,
240 source: 'jina.ai'
241 ]
242 ]]></script>
243 </actions>
244 </service>
245 ```
246
247 ## Limitations and Considerations
248
249 ### What Jina.ai Extracts
250 - ✅ Video title and metadata
251 - ✅ Video description text
252 - ✅ View count and upload date
253 - ✅ Channel information
254 - ✅ Related videos (titles only)
255
256 ### What It Doesn't Extract
257 - ❌ Actual video content/transcript
258 - ❌ Audio from video
259 - ❌ Visual frames or screenshots
260 - ❌ Comments (requires login)
261
262 ### Rate Limiting
263 - Jina.ai is a free service - implement rate limiting
264 - Add delays between requests
265 - Cache results when possible
266
267 ### Error Handling
268 - Check for 403 errors (private/deleted videos)
269 - Handle network timeouts
270 - Validate YouTube URL format
271
272 ## Security and Privacy
273
274 ### Data Handling
275 - Only processes publicly available YouTube metadata
276 - No authentication required
277 - Content is extracted via third-party service
278
279 ### Usage Guidelines
280 - Respect YouTube's Terms of Service
281 - Don't circumvent paywalls or private content
282 - Use for legitimate research/analysis purposes
283
284 ## Alternative Services
285
286 If jina.ai is unavailable, similar services include:
287 - `https://r.jina.ai/http://URL` (primary)
288 - `https://r.jina.ai/http://URL&format=json` (JSON format)
289 - Custom scrapers (more complex)
290
291 ## Troubleshooting
292
293 ### Common Issues
294 1. **403 Errors**: Video is private or deleted
295 2. **Empty Content**: Video has no description
296 3. **Rate Limiting**: Too many requests too quickly
297 4. **Network Issues**: Connection timeouts
298
299 ### Debug Mode
300 ```python
301 def debug_extract(video_url):
302 """Debug version with detailed logging"""
303 print(f"Original URL: {video_url}")
304
305 clean_url = video_url.replace("https://", "").replace("http://", "")
306 jina_url = f"https://r.jina.ai/http://{clean_url}"
307
308 print(f"Jina URL: {jina_url}")
309
310 try:
311 response = requests.get(jina_url, timeout=30)
312 print(f"Status Code: {response.status_code}")
313 print(f"Content Length: {len(response.text)}")
314
315 if response.status_code == 200:
316 print("✅ Success!")
317 else:
318 print("❌ Failed!")
319
320 except Exception as e:
321 print(f"❌ Exception: {e}")
322 ```
323
324 This approach turns the jina.ai trick into a reusable skill for extracting YouTube video metadata and descriptions for analysis, documentation, or integration with other systems.
...\ No newline at end of file ...\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 "moqui_mcp": { 4 "moqui_mcp": {
5 "type": "remote", 5 "type": "remote",
6 "url": "http://localhost:8080/mcp", 6 "url": "http://localhost:8080/mcp",
7 "enabled": false, 7 "enabled": true,
8 "headers": { 8 "headers": {
9 "Authorization": "Basic am9obi5zYWxlczptb3F1aQ==" 9 "Authorization": "Basic am9obi5zYWxlczptb3F1aQ=="
10 } 10 }
......
...@@ -198,18 +198,28 @@ ...@@ -198,18 +198,28 @@
198 } 198 }
199 } else { 199 } else {
200 // For moqui_ tools, check existence and fallback to subscreen 200 // For moqui_ tools, check existence and fallback to subscreen
201 if (!ec.resource.getLocationReference(screenPath).getExists()) { 201 // Walk down from component root to find the actual screen file and the subscreen path below it
202 def lastSlash = screenPath.lastIndexOf('/') 202 def cleanName = actualToolName.substring(6) // Remove moqui_
203 if (lastSlash > 0) { 203 def parts = cleanName.split('_').toList()
204 def parentPath = screenPath.substring(0, lastSlash) + ".xml" 204 def component = parts[0]
205 def possibleSubscreen = screenPath.substring(lastSlash + 1).replace('.xml', '') 205 def currentPath = "component://${component}/screen"
206 206 def subNameParts = []
207 if (ec.resource.getLocationReference(parentPath).getExists()) { 207
208 screenPath = parentPath 208 // Parts start from index 1 (after component)
209 subscreenName = possibleSubscreen 209 for (int i = 1; i < parts.size(); i++) {
210 } 210 def part = parts[i]
211 def nextPath = "${currentPath}/${part}"
212 if (ec.resource.getLocationReference(nextPath + ".xml").getExists()) {
213 currentPath = nextPath
214 // Reset subNameParts when we find a deeper screen file
215 subNameParts = []
216 } else {
217 subNameParts << part
211 } 218 }
212 } 219 }
220
221 screenPath = currentPath + ".xml"
222 if (subNameParts) subscreenName = subNameParts.join("_")
213 } 223 }
214 224
215 ec.logger.info("MCP ToolsCall: Decoded screen tool - screenPath=${screenPath}, subscreen=${subscreenName}") 225 ec.logger.info("MCP ToolsCall: Decoded screen tool - screenPath=${screenPath}, subscreen=${subscreenName}")
...@@ -269,36 +279,34 @@ ...@@ -269,36 +279,34 @@
269 } 279 }
270 280
271 // Check if this is a screen-based tool using McpUtils 281 // Check if this is a screen-based tool using McpUtils
272 def screenPath = org.moqui.mcp.McpUtils.getScreenPath(name) 282 def screenPath = null
273
274 if (screenPath) {
275 ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}")
276
277 def subscreenName = null 283 def subscreenName = null
278 284 if (name.startsWith("moqui_")) {
279 // Verify if the screen file exists 285 def cleanName = name.substring(6)
280 // If not, it might be a subscreen (e.g. FindProduct inside Product.xml) 286 def parts = cleanName.split('_').toList()
281 // Use getExists() instead of exists() as it is a property accessor 287 def component = parts[0]
282 if (!ec.resource.getLocationReference(screenPath).getExists()) { 288 def currentPath = "component://${component}/screen"
283 ec.logger.info("Screen path ${screenPath} does not exist, checking for subscreen parent") 289 def subNameParts = []
284 // Try to find parent screen file 290
285 def lastSlash = screenPath.lastIndexOf('/') 291 for (int i = 1; i < parts.size(); i++) {
286 if (lastSlash > 0) { 292 def part = parts[i]
287 def parentPath = screenPath.substring(0, lastSlash) + ".xml" 293 def nextPath = "${currentPath}/${part}"
288 // The subscreen name is the part after the slash, without .xml 294 if (ec.resource.getLocationReference(nextPath + ".xml").getExists()) {
289 // But wait, screenPath from McpUtils ends in .xml 295 currentPath = nextPath
290 // screenPath: .../Catalog/Product/FindProduct.xml 296 subNameParts = []
291 // subscreenName: FindProduct 297 } else {
292 def possibleSubscreen = screenPath.substring(lastSlash + 1).replace('.xml', '') 298 subNameParts << part
293
294 if (ec.resource.getLocationReference(parentPath).getExists()) {
295 screenPath = parentPath
296 subscreenName = possibleSubscreen
297 ec.logger.info("Found parent screen: ${screenPath}, subscreen: ${subscreenName}")
298 } 299 }
299 } 300 }
301 screenPath = currentPath + ".xml"
302 if (subNameParts) subscreenName = subNameParts.join("_")
303 } else {
304 screenPath = org.moqui.mcp.McpUtils.getScreenPath(name)
300 } 305 }
301 306
307 if (screenPath) {
308 ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}, subscreen: ${subscreenName}")
309
302 // Now call the screen tool with proper user context 310 // Now call the screen tool with proper user context
303 def screenParams = arguments ?: [:] 311 def screenParams = arguments ?: [:]
304 // Use requested render mode from arguments, default to text for LLM-friendly output 312 // Use requested render mode from arguments, default to text for LLM-friendly output
...@@ -994,7 +1002,6 @@ def startTime = System.currentTimeMillis() ...@@ -994,7 +1002,6 @@ def startTime = System.currentTimeMillis()
994 ec.logger.info("SUBSCREEN PATH PARTS ${subscreenPathParts}") 1002 ec.logger.info("SUBSCREEN PATH PARTS ${subscreenPathParts}")
995 1003
996 // Regular screen rendering with timeout for subscreen 1004 // Regular screen rendering with timeout for subscreen
997
998 try { 1005 try {
999 // Construct the proper relative path from parent screen to target subscreen 1006 // Construct the proper relative path from parent screen to target subscreen
1000 // The subscreenName contains the full path from parent with underscores, convert to proper path 1007 // The subscreenName contains the full path from parent with underscores, convert to proper path
...@@ -1011,6 +1018,29 @@ def startTime = System.currentTimeMillis() ...@@ -1011,6 +1018,29 @@ def startTime = System.currentTimeMillis()
1011 } else { 1018 } else {
1012 throw new Exception("ScreenTest object is null") 1019 throw new Exception("ScreenTest object is null")
1013 } 1020 }
1021 } else {
1022 // Direct screen execution (no subscreen)
1023 ec.logger.info("MCP Screen Execution: Rendering direct screen ${screenPath} (root: ${rootScreen}, path: ${testScreenPath})")
1024
1025 def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
1026 .rootScreen(rootScreen)
1027 .renderMode(renderMode ? renderMode : "html")
1028 .auth(ec.user.username)
1029
1030 if (screenTest) {
1031 def renderParams = parameters ?: [:]
1032 renderParams.userId = ec.user.userId
1033 renderParams.username = ec.user.username
1034
1035 try {
1036 ec.logger.info("TESTRENDER ${testScreenPath} ${renderParams}")
1037 def testRender = screenTest.render(testScreenPath, renderParams, "POST")
1038 output = testRender.getOutput()
1039 ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}")
1040 } catch (java.util.concurrent.TimeoutException e) {
1041 throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}")
1042 }
1043 }
1014 } 1044 }
1015 } catch (Exception e) { 1045 } catch (Exception e) {
1016 isError = true 1046 isError = true
...@@ -1068,17 +1098,18 @@ def startTime = System.currentTimeMillis() ...@@ -1068,17 +1098,18 @@ def startTime = System.currentTimeMillis()
1068 // Helper function to convert web paths to MCP tool names 1098 // Helper function to convert web paths to MCP tool names
1069 def convertWebPathToMcpTool = { path -> 1099 def convertWebPathToMcpTool = { path ->
1070 try { 1100 try {
1101 ec.logger.info("Converting web path to MCP tool: ${path}")
1071 // Handle simple catalog paths (dropdown menu items) 1102 // Handle simple catalog paths (dropdown menu items)
1072 if (path == "Category" || path.startsWith("Category/")) { 1103 if (path == "Category" || path.startsWith("Category/") || path == "Product/FindProduct/getCategoryList") {
1073 return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Category" 1104 return "moqui_SimpleScreens_SimpleScreens_Catalog_Category"
1074 } else if (path == "Feature" || path.startsWith("Feature/")) { 1105 } else if (path == "Feature" || path.startsWith("Feature/")) {
1075 return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Feature" 1106 return "moqui_SimpleScreens_SimpleScreens_Catalog_Feature"
1076 } else if (path == "FeatureGroup" || path.startsWith("FeatureGroup/")) { 1107 } else if (path == "FeatureGroup" || path.startsWith("FeatureGroup/")) {
1077 return "screen_SimpleScreens_screen_SimpleScreens_Catalog_FeatureGroup" 1108 return "moqui_SimpleScreens_SimpleScreens_Catalog_FeatureGroup"
1078 } else if (path == "Product" || path.startsWith("Product/")) { 1109 } else if (path == "Product" || path.startsWith("Product/")) {
1079 return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Product" 1110 return "moqui_SimpleScreens_SimpleScreens_Catalog_Product"
1080 } else if (path.startsWith("Search")) { 1111 } else if (path.startsWith("Search")) {
1081 return "screen_SimpleScreens_screen_SimpleScreens_Search" 1112 return "moqui_SimpleScreens_SimpleScreens_Search"
1082 } 1113 }
1083 1114
1084 // Handle full catalog paths 1115 // Handle full catalog paths
...@@ -1389,18 +1420,51 @@ def startTime = System.currentTimeMillis() ...@@ -1389,18 +1420,51 @@ def startTime = System.currentTimeMillis()
1389 } else { 1420 } else {
1390 // Resolve path 1421 // Resolve path
1391 def screenPath = path.startsWith("moqui_") ? McpUtils.getScreenPath(path) : null 1422 def screenPath = path.startsWith("moqui_") ? McpUtils.getScreenPath(path) : null
1423 // Keep track of the tool name used to reach here, to maintain context
1424 def baseToolName = path.startsWith("moqui_") ? path : null
1425
1392 if (!screenPath && !path.startsWith("component://")) { 1426 if (!screenPath && !path.startsWith("component://")) {
1393 // Try to handle "popcommerce/admin" style by guessing 1427 // Try to handle "popcommerce/admin" style by guessing
1394 def toolName = "moqui_" + path.replace('/', '_') 1428 def toolName = "moqui_" + path.replace('/', '_')
1395 screenPath = McpUtils.getScreenPath(toolName) 1429 screenPath = McpUtils.getScreenPath(toolName)
1430 if (!baseToolName) baseToolName = toolName
1396 } else if (path.startsWith("component://")) { 1431 } else if (path.startsWith("component://")) {
1397 screenPath = path 1432 screenPath = path
1433 // If starting with component path, we can't easily establish a base tool name
1434 // that preserves context if it's not a standard path.
1435 // But McpUtils.getToolName will try.
1436 if (!baseToolName) baseToolName = McpUtils.getToolName(path)
1398 } 1437 }
1399 1438
1400 if (screenPath) { 1439 if (screenPath) {
1440 // Check if screen exists, if not try to resolve as subscreen
1441 if (!ec.resource.getLocationReference(screenPath).getExists()) {
1442 def lastSlash = screenPath.lastIndexOf('/')
1443 if (lastSlash > 0) {
1444 def parentPath = screenPath.substring(0, lastSlash) + ".xml"
1445 def subscreenName = screenPath.substring(lastSlash + 1).replace('.xml', '')
1446
1447 if (ec.resource.getLocationReference(parentPath).getExists()) {
1448 // Found parent, now find the subscreen location
1449 try {
1450 def parentDef = ec.screen.getScreenDefinition(parentPath)
1451 def subscreenItem = parentDef?.getSubscreensItem(subscreenName)
1452 if (subscreenItem && subscreenItem.getLocation()) {
1453 ec.logger.info("Redirecting browse from ${screenPath} to ${subscreenItem.getLocation()}")
1454 screenPath = subscreenItem.getLocation()
1455 }
1456 } catch (Exception e) {
1457 ec.logger.warn("Error resolving subscreen location: ${e.message}")
1458 }
1459 }
1460 }
1461 }
1462
1401 try { 1463 try {
1402 // First, add the current screen itself as a tool if it's executable 1464 // First, add the current screen itself as a tool if it's executable
1403 def currentToolName = McpUtils.getToolName(screenPath) 1465 // Use baseToolName if available to preserve context (e.g. moqui_PopCommerce_...)
1466 // even if the implementation is in another component (e.g. SimpleScreens)
1467 def currentToolName = baseToolName ?: McpUtils.getToolName(screenPath)
1404 tools << [ 1468 tools << [
1405 name: currentToolName, 1469 name: currentToolName,
1406 description: "Execute screen: ${currentToolName}", 1470 description: "Execute screen: ${currentToolName}",
...@@ -1427,7 +1491,7 @@ def startTime = System.currentTimeMillis() ...@@ -1427,7 +1491,7 @@ def startTime = System.currentTimeMillis()
1427 // Construct sub-tool name by extending the parent path 1491 // Construct sub-tool name by extending the parent path
1428 // This assumes subscreens are structurally nested in the tool name 1492 // This assumes subscreens are structurally nested in the tool name
1429 // e.g. moqui_PopCommerce_Admin -> moqui_PopCommerce_Admin_Catalog 1493 // e.g. moqui_PopCommerce_Admin -> moqui_PopCommerce_Admin_Catalog
1430 def parentToolName = McpUtils.getToolName(screenPath) 1494 def parentToolName = baseToolName ?: McpUtils.getToolName(screenPath)
1431 def subToolName = parentToolName + "_" + subName 1495 def subToolName = parentToolName + "_" + subName
1432 1496
1433 subscreens << [ 1497 subscreens << [
......