c808a00b by Ean Schuessler

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

…g, and recursive screen resolution
1 parent 5745c06b
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
arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
connection.project.dir=../../../framework
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/usr/lib/jvm/java-17-openjdk-amd64
java.home=/usr/lib/jvm/java-21-openjdk-amd64
jvm.arguments=
offline.mode=false
override.workspace.settings=true
......
# AI-Optimized Screen Theme for Moqui MCP
## Concept
Create a specialized Moqui screen theme that outputs AI-optimized JSON instead of HTML, eliminating token waste and enabling rich data delivery.
## The Problem
Current MCP flow wastes tokens:
1. AI requests screen data
2. Screen renders HTML (web-optimized)
3. AI parses HTML (token-intensive)
4. AI extracts meaning from markup noise
## The Solution
**AI Screen Theme** - outputs structured JSON designed for LLM consumption:
### Core Principles
- **No HTML markup** - pure data
- **Structured hierarchy** - nested objects
- **Rich metadata** - context, relationships
- **Media references** - image URLs, audio links
- **Action links** - next steps, workflows
- **Inventory integration** - real-time stock data
## Implementation
### 1. Screen Theme Definition
```xml
<!-- screen-theme-ai.xml -->
<screen-theme name="ai-optimized" extends="default">
<render-mode name="ai">
<template><![CDATA[
{
"screenInfo": {
"name": "${screenName}",
"title": "${screenTitle}",
"description": "${screenDescription}",
"context": "${userContext}",
"permissions": "${userPermissions}",
"timestamp": "${nowTimestamp}"
},
"data": {
${screenDataAsJson}
},
"actions": [
${actionLinksAsJson}
],
"media": {
"images": [
${imageUrlsAsJson}
],
"audio": [
${audioUrlsAsJson}
],
"video": [
${videoUrlsAsJson}
]
},
"inventory": {
"available": ${inventoryData},
"locations": ${inventoryLocations},
"leadTimes": ${leadTimeData}
},
"navigation": {
"parentScreens": ${parentScreens},
"subScreens": ${subScreens},
"relatedScreens": ${relatedScreens}
},
"businessLogic": {
"workflows": ${availableWorkflows},
"validations": ${businessRules},
"constraints": ${dataConstraints}
}
}
]]></template>
</render-mode>
</screen-theme>
```
### 2. Screen Modifications
```xml
<!-- Modified screen for AI output -->
<screen name="ProductCatalog" theme-type="ai-optimized">
<actions>
<script><![CDATA[
// Prepare AI-optimized data structures
def products = []
def inventory = [:]
def media = [:]
// Process products for AI consumption
productList.each { product ->
def productData = [
id: product.productId,
name: product.productName,
description: product.description,
category: product.category,
features: extractFeatures(product),
pricing: [
list: product.price,
sale: product.salePrice,
currency: product.currency
],
availability: [
inStock: product.available,
quantity: product.quantityOnHand,
locations: product.inventoryLocations,
leadTime: product.leadTime
],
media: [
images: product.imageUrls,
videos: product.videoUrls,
documents: product.documentUrls
],
attributes: [
color: product.color,
size: product.size,
weight: product.weight,
specifications: product.specifications
],
relationships: [
category: product.categoryId,
related: product.relatedProducts,
accessories: product.accessories
]
]
products.add(productData)
// Aggregate inventory data
inventory[product.productId] = [
total: product.quantityOnHand,
available: product.availableToPromise,
reserved: product.reservedQuantity,
locations: product.stockLocations
]
}
// Set global context for AI theme
context.screenDataAsJson = new groovy.json.JsonBuilder(products).toString()
context.inventoryData = new groovy.json.JsonBuilder(inventory).toString()
context.imageUrlsAsJson = new groovy.json.JsonBuilder(extractAllImages()).toString()
context.actionLinksAsJson = new groovy.json.JsonBuilder(buildActionLinks()).toString()
]]></script>
</actions>
<widgets>
<!-- AI-optimized product listing -->
<container-list name="products" list="products">
<field-list name="aiProductData">
<field name="id"/>
<field name="name"/>
<field name="description"/>
<field name="pricing"/>
<field name="availability"/>
<field name="media"/>
<field name="attributes"/>
<field name="relationships"/>
</field-list>
</container-list>
</widgets>
</screen>
```
### 3. MCP Service Integration
```xml
<service verb="execute" noun="ScreenAsAiOptimizedTool" authenticate="true">
<description>Execute screen with AI-optimized JSON output</description>
<in-parameters>
<parameter name="screenPath" required="true"/>
<parameter name="parameters" type="Map"/>
<parameter name="aiMode" type="Boolean" default="true"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
ExecutionContext ec = context.ec
// Set AI mode flag
ec.context.put("aiMode", aiMode)
ec.context.put("renderMode", "ai")
// Execute screen with AI theme
def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
.rootScreen(getRootScreenForPath(screenPath))
.renderMode("ai")
.auth(ec.user.username)
def result = screenTest.render(getRelativePath(screenPath), parameters ?: [:], "POST")
def aiOutput = result.getOutput()
// Parse AI-optimized JSON
def aiData = groovy.json.JsonSlurper().parseText(aiOutput)
// Enhance with MCP-specific metadata
def enhancedResult = [
content: [
[
type: "text",
text: new groovy.json.JsonBuilder(aiData).toString(),
metadata: [
source: "ai-optimized-screen",
screenPath: screenPath,
renderMode: "ai",
timestamp: ec.user.nowTimestamp,
tokenOptimized: true
]
]
],
isError: false
]
result = enhancedResult
]]></script>
</actions>
</service>
```
## Benefits for 9B+ Models
### Token Efficiency
- **90% reduction** in token usage vs HTML parsing
- **Structured data** - no parsing overhead
- **Rich context** - more meaning per token
### Enhanced Capabilities
- **Inventory images** - direct URL references
- **Call recordings** - audio/video metadata
- **Real-time stock** - integrated inventory data
- **Workflow triggers** - actionable next steps
### Example AI Output
```json
{
"screenInfo": {
"name": "ProductCatalog",
"title": "Product Catalog",
"context": "sales-user",
"timestamp": "2025-12-11T10:30:00Z"
},
"data": {
"products": [
{
"id": "PROD-001",
"name": "Blue Widget",
"description": "Premium blue widget with advanced features",
"pricing": {
"list": 29.99,
"sale": 24.99,
"currency": "USD"
},
"availability": {
"inStock": true,
"quantity": 150,
"locations": ["WH-01", "WH-02"],
"leadTime": 2
},
"media": {
"images": [
"https://cdn.example.com/products/PROD-001-front.jpg",
"https://cdn.example.com/products/PROD-001-side.jpg"
],
"videos": [
"https://cdn.example.com/products/PROD-001-demo.mp4"
]
},
"attributes": {
"color": "blue",
"size": "medium",
"weight": "2.5kg"
}
}
]
},
"actions": [
{
"type": "create-order",
"label": "Create Order",
"screen": "OrderCreate",
"parameters": ["productId", "quantity"]
},
{
"type": "check-inventory",
"label": "Check Stock",
"screen": "InventoryCheck",
"parameters": ["productId", "location"]
}
],
"inventory": {
"PROD-001": {
"total": 150,
"available": 120,
"reserved": 30,
"locations": {
"WH-01": 80,
"WH-02": 70
}
}
}
}
```
## Implementation Strategy
### Phase 1: Theme Development
1. Create `screen-theme-ai.xml` with JSON templates
2. Modify key screens (Product, Order, Customer) for AI output
3. Test with existing MCP interface
### Phase 2: Service Enhancement
1. Add `aiMode` parameter to screen execution service
2. Implement AI-specific rendering logic
3. Integrate with MCP tool discovery
### Phase 3: Advanced Features
1. Add inventory image integration
2. Include call recording metadata
3. Implement workflow suggestions
4. Add real-time data feeds
## For 9B Models
This approach enables smaller models to:
- **Process richer data** with less token overhead
- **Access multimedia** through URL references
- **Understand context** through structured metadata
- **Take actions** through clear workflow links
- **Scale efficiently** without parsing HTML noise
The AI screen theme transforms Moqui from "web interface" to "AI interface" while preserving all security constructs.
\ No newline at end of file
# YouTube Video Content Extractor using Jina.ai
This script demonstrates how to extract YouTube video content using jina.ai summarizer, bypassing YouTube's restrictions on automated access.
## How It Works
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:
1. Fetches the YouTube page content
2. Extracts video metadata, description, and available text content
3. Returns it in clean markdown format
4. Bypasses YouTube's JavaScript requirements and bot detection
## Usage Examples
### Basic Usage
```bash
# Extract video content
curl "https://r.jina.ai/http://www.youtube.com/watch?v=VIDEO_ID"
# Example with your video
curl "https://r.jina.ai/http://www.youtube.com/watch?v=Tauucda-NV4"
```
### Python Script
```python
import requests
import json
import sys
def extract_youtube_content(video_url):
"""Extract YouTube video content using jina.ai"""
# Remove any existing protocol and add jina.ai prefix
clean_url = video_url.replace("https://", "").replace("http://", "")
jina_url = f"https://r.jina.ai/http://{clean_url}"
try:
response = requests.get(jina_url, timeout=30)
response.raise_for_status()
# Parse the markdown content
content = response.text
# Extract key information
title = extract_title(content)
description = extract_description(content)
views = extract_views(content)
return {
'title': title,
'description': description,
'views': views,
'full_content': content
}
except Exception as e:
return {'error': str(e)}
def extract_title(content):
"""Extract video title from content"""
lines = content.split('\n')
for line in lines:
if line.strip().startswith('# ') and 'YouTube' not in line:
return line.strip('# ').strip()
return "Unknown"
def extract_description(content):
"""Extract video description"""
lines = content.split('\n')
desc_start = False
description = []
for line in lines:
if 'Description' in line:
desc_start = True
continue
elif desc_start and line.strip():
if line.startswith('### ') or line.startswith('['):
break
description.append(line.strip())
return '\n'.join(description)
def extract_views(content):
"""Extract view count"""
import re
views_match = re.search(r'(\d+)\s*views', content)
return views_match.group(1) if views_match else "Unknown"
# Usage
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python youtube_extractor.py <youtube_url>")
sys.exit(1)
video_url = sys.argv[1]
result = extract_youtube_content(video_url)
if 'error' in result:
print(f"Error: {result['error']}")
else:
print(f"Title: {result['title']}")
print(f"Views: {result['views']}")
print(f"Description:\n{result['description']}")
```
### Shell Script
```bash
#!/bin/bash
# YouTube Content Extractor using Jina.ai
# Usage: ./extract_youtube.sh <youtube_url>
if [ $# -eq 0 ]; then
echo "Usage: $0 <youtube_url>"
exit 1
fi
YOUTUBE_URL="$1"
JINA_URL="https://r.jina.ai/http://${YOUTUBE_URL#https://}"
echo "Extracting content from: $YOUTUBE_URL"
echo "========================================"
curl -s "$JINA_URL" | \
sed -n '/Description:/,$p' | \
head -n -1
echo "========================================"
```
## Advanced Usage for MCP Integration
### Integration with AI Analysis
```python
def analyze_video_with_ai(video_url, ai_client):
"""Extract video content and analyze with AI"""
# Extract content
content = extract_youtube_content(video_url)
if 'error' in content:
return content
# Prepare analysis prompt
prompt = f"""
Analyze this YouTube video content:
Title: {content['title']}
Description: {content['description']}
Views: {content['views']}
Provide insights on:
1. Main topic/subject
2. Key actions demonstrated
3. Technical details shown
4. Notable results or outcomes
"""
# Send to AI for analysis
analysis = ai_client.generate(prompt)
return {
'video_data': content,
'analysis': analysis
}
```
### Batch Processing
```python
def process_video_list(video_urls):
"""Process multiple YouTube videos"""
results = []
for url in video_urls:
print(f"Processing: {url}")
result = extract_youtube_content(url)
results.append(result)
# Rate limiting
time.sleep(1)
return results
# Example usage
video_urls = [
"https://www.youtube.com/watch?v=Tauucda-NV4",
"https://www.youtube.com/watch?v=ANOTHER_VIDEO_ID"
]
results = process_video_list(video_urls)
```
## Integration with Moqui MCP
### MCP Service for Video Analysis
```xml
<service verb="analyze" noun="YouTubeVideo" authenticate="true">
<description>Analyze YouTube video content using jina.ai</description>
<in-parameters>
<parameter name="videoUrl" required="true" type="String"/>
</in-parameters>
<out-parameters>
<parameter name="analysis" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import groovy.json.JsonSlurper
import groovy.json.JsonBuilder
// Extract content using jina.ai
def cleanUrl = videoUrl.replace("https://", "").replace("http://", "")
def jinaUrl = "https://r.jina.ai/http://${cleanUrl}"
def connection = new URL(jinaUrl).openConnection()
def response = connection.inputStream.text
// Parse response (simplified)
def lines = response.split('\n')
def title = "Unknown"
def description = ""
for (line in lines) {
if (line.startsWith('# ') && title == "Unknown") {
title = line.replace('# ', '').replace(' - YouTube', '').trim()
}
if (line.contains('Description:')) {
// Start collecting description
def descStart = lines.indexOf(line) + 1
description = lines[descStart..-1].join('\n').trim()
break
}
}
analysis = [
title: title,
description: description,
extractedAt: ec.user.nowTimestamp,
source: 'jina.ai'
]
]]></script>
</actions>
</service>
```
## Limitations and Considerations
### What Jina.ai Extracts
- ✅ Video title and metadata
- ✅ Video description text
- ✅ View count and upload date
- ✅ Channel information
- ✅ Related videos (titles only)
### What It Doesn't Extract
- ❌ Actual video content/transcript
- ❌ Audio from video
- ❌ Visual frames or screenshots
- ❌ Comments (requires login)
### Rate Limiting
- Jina.ai is a free service - implement rate limiting
- Add delays between requests
- Cache results when possible
### Error Handling
- Check for 403 errors (private/deleted videos)
- Handle network timeouts
- Validate YouTube URL format
## Security and Privacy
### Data Handling
- Only processes publicly available YouTube metadata
- No authentication required
- Content is extracted via third-party service
### Usage Guidelines
- Respect YouTube's Terms of Service
- Don't circumvent paywalls or private content
- Use for legitimate research/analysis purposes
## Alternative Services
If jina.ai is unavailable, similar services include:
- `https://r.jina.ai/http://URL` (primary)
- `https://r.jina.ai/http://URL&format=json` (JSON format)
- Custom scrapers (more complex)
## Troubleshooting
### Common Issues
1. **403 Errors**: Video is private or deleted
2. **Empty Content**: Video has no description
3. **Rate Limiting**: Too many requests too quickly
4. **Network Issues**: Connection timeouts
### Debug Mode
```python
def debug_extract(video_url):
"""Debug version with detailed logging"""
print(f"Original URL: {video_url}")
clean_url = video_url.replace("https://", "").replace("http://", "")
jina_url = f"https://r.jina.ai/http://{clean_url}"
print(f"Jina URL: {jina_url}")
try:
response = requests.get(jina_url, timeout=30)
print(f"Status Code: {response.status_code}")
print(f"Content Length: {len(response.text)}")
if response.status_code == 200:
print("✅ Success!")
else:
print("❌ Failed!")
except Exception as e:
print(f"❌ Exception: {e}")
```
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
......@@ -4,10 +4,10 @@
"moqui_mcp": {
"type": "remote",
"url": "http://localhost:8080/mcp",
"enabled": false,
"enabled": true,
"headers": {
"Authorization": "Basic am9obi5zYWxlczptb3F1aQ=="
}
}
}
}
\ No newline at end of file
}
......
......@@ -198,18 +198,28 @@
}
} else {
// For moqui_ tools, check existence and fallback to subscreen
if (!ec.resource.getLocationReference(screenPath).getExists()) {
def lastSlash = screenPath.lastIndexOf('/')
if (lastSlash > 0) {
def parentPath = screenPath.substring(0, lastSlash) + ".xml"
def possibleSubscreen = screenPath.substring(lastSlash + 1).replace('.xml', '')
if (ec.resource.getLocationReference(parentPath).getExists()) {
screenPath = parentPath
subscreenName = possibleSubscreen
}
// Walk down from component root to find the actual screen file and the subscreen path below it
def cleanName = actualToolName.substring(6) // Remove moqui_
def parts = cleanName.split('_').toList()
def component = parts[0]
def currentPath = "component://${component}/screen"
def subNameParts = []
// Parts start from index 1 (after component)
for (int i = 1; i < parts.size(); i++) {
def part = parts[i]
def nextPath = "${currentPath}/${part}"
if (ec.resource.getLocationReference(nextPath + ".xml").getExists()) {
currentPath = nextPath
// Reset subNameParts when we find a deeper screen file
subNameParts = []
} else {
subNameParts << part
}
}
screenPath = currentPath + ".xml"
if (subNameParts) subscreenName = subNameParts.join("_")
}
ec.logger.info("MCP ToolsCall: Decoded screen tool - screenPath=${screenPath}, subscreen=${subscreenName}")
......@@ -269,35 +279,33 @@
}
// Check if this is a screen-based tool using McpUtils
def screenPath = org.moqui.mcp.McpUtils.getScreenPath(name)
if (screenPath) {
ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}")
def subscreenName = null
def screenPath = null
def subscreenName = null
if (name.startsWith("moqui_")) {
def cleanName = name.substring(6)
def parts = cleanName.split('_').toList()
def component = parts[0]
def currentPath = "component://${component}/screen"
def subNameParts = []
// Verify if the screen file exists
// If not, it might be a subscreen (e.g. FindProduct inside Product.xml)
// Use getExists() instead of exists() as it is a property accessor
if (!ec.resource.getLocationReference(screenPath).getExists()) {
ec.logger.info("Screen path ${screenPath} does not exist, checking for subscreen parent")
// Try to find parent screen file
def lastSlash = screenPath.lastIndexOf('/')
if (lastSlash > 0) {
def parentPath = screenPath.substring(0, lastSlash) + ".xml"
// The subscreen name is the part after the slash, without .xml
// But wait, screenPath from McpUtils ends in .xml
// screenPath: .../Catalog/Product/FindProduct.xml
// subscreenName: FindProduct
def possibleSubscreen = screenPath.substring(lastSlash + 1).replace('.xml', '')
if (ec.resource.getLocationReference(parentPath).getExists()) {
screenPath = parentPath
subscreenName = possibleSubscreen
ec.logger.info("Found parent screen: ${screenPath}, subscreen: ${subscreenName}")
}
for (int i = 1; i < parts.size(); i++) {
def part = parts[i]
def nextPath = "${currentPath}/${part}"
if (ec.resource.getLocationReference(nextPath + ".xml").getExists()) {
currentPath = nextPath
subNameParts = []
} else {
subNameParts << part
}
}
screenPath = currentPath + ".xml"
if (subNameParts) subscreenName = subNameParts.join("_")
} else {
screenPath = org.moqui.mcp.McpUtils.getScreenPath(name)
}
if (screenPath) {
ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}, subscreen: ${subscreenName}")
// Now call the screen tool with proper user context
def screenParams = arguments ?: [:]
......@@ -993,24 +1001,46 @@ def startTime = System.currentTimeMillis()
ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}")
ec.logger.info("SUBSCREEN PATH PARTS ${subscreenPathParts}")
// Regular screen rendering with timeout for subscreen
try {
// Construct the proper relative path from parent screen to target subscreen
// The subscreenName contains the full path from parent with underscores, convert to proper path
def relativePath = subscreenName.replaceAll('_','/')
ec.logger.info("TESTRENDER ${relativePath} ${renderParams}")
// For subscreens, use the full relative path from parent screen to target subscreen
def testRender = screenTest.render(relativePath, renderParams, "POST")
output = testRender.getOutput()
def outputLength = output?.length() ?: 0
ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}")
} catch (java.util.concurrent.TimeoutException e) {
throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}")
// Regular screen rendering with timeout for subscreen
try {
// Construct the proper relative path from parent screen to target subscreen
// The subscreenName contains the full path from parent with underscores, convert to proper path
def relativePath = subscreenName.replaceAll('_','/')
ec.logger.info("TESTRENDER ${relativePath} ${renderParams}")
// For subscreens, use the full relative path from parent screen to target subscreen
def testRender = screenTest.render(relativePath, renderParams, "POST")
output = testRender.getOutput()
def outputLength = output?.length() ?: 0
ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}")
} catch (java.util.concurrent.TimeoutException e) {
throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}")
}
} else {
throw new Exception("ScreenTest object is null")
}
} else {
throw new Exception("ScreenTest object is null")
}
// Direct screen execution (no subscreen)
ec.logger.info("MCP Screen Execution: Rendering direct screen ${screenPath} (root: ${rootScreen}, path: ${testScreenPath})")
def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
.rootScreen(rootScreen)
.renderMode(renderMode ? renderMode : "html")
.auth(ec.user.username)
if (screenTest) {
def renderParams = parameters ?: [:]
renderParams.userId = ec.user.userId
renderParams.username = ec.user.username
try {
ec.logger.info("TESTRENDER ${testScreenPath} ${renderParams}")
def testRender = screenTest.render(testScreenPath, renderParams, "POST")
output = testRender.getOutput()
ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}")
} catch (java.util.concurrent.TimeoutException e) {
throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}")
}
}
}
} catch (Exception e) {
isError = true
......@@ -1068,17 +1098,18 @@ def startTime = System.currentTimeMillis()
// Helper function to convert web paths to MCP tool names
def convertWebPathToMcpTool = { path ->
try {
ec.logger.info("Converting web path to MCP tool: ${path}")
// Handle simple catalog paths (dropdown menu items)
if (path == "Category" || path.startsWith("Category/")) {
return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Category"
if (path == "Category" || path.startsWith("Category/") || path == "Product/FindProduct/getCategoryList") {
return "moqui_SimpleScreens_SimpleScreens_Catalog_Category"
} else if (path == "Feature" || path.startsWith("Feature/")) {
return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Feature"
return "moqui_SimpleScreens_SimpleScreens_Catalog_Feature"
} else if (path == "FeatureGroup" || path.startsWith("FeatureGroup/")) {
return "screen_SimpleScreens_screen_SimpleScreens_Catalog_FeatureGroup"
return "moqui_SimpleScreens_SimpleScreens_Catalog_FeatureGroup"
} else if (path == "Product" || path.startsWith("Product/")) {
return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Product"
return "moqui_SimpleScreens_SimpleScreens_Catalog_Product"
} else if (path.startsWith("Search")) {
return "screen_SimpleScreens_screen_SimpleScreens_Search"
return "moqui_SimpleScreens_SimpleScreens_Search"
}
// Handle full catalog paths
......@@ -1389,18 +1420,51 @@ def startTime = System.currentTimeMillis()
} else {
// Resolve path
def screenPath = path.startsWith("moqui_") ? McpUtils.getScreenPath(path) : null
// Keep track of the tool name used to reach here, to maintain context
def baseToolName = path.startsWith("moqui_") ? path : null
if (!screenPath && !path.startsWith("component://")) {
// Try to handle "popcommerce/admin" style by guessing
def toolName = "moqui_" + path.replace('/', '_')
screenPath = McpUtils.getScreenPath(toolName)
if (!baseToolName) baseToolName = toolName
} else if (path.startsWith("component://")) {
screenPath = path
// If starting with component path, we can't easily establish a base tool name
// that preserves context if it's not a standard path.
// But McpUtils.getToolName will try.
if (!baseToolName) baseToolName = McpUtils.getToolName(path)
}
if (screenPath) {
// Check if screen exists, if not try to resolve as subscreen
if (!ec.resource.getLocationReference(screenPath).getExists()) {
def lastSlash = screenPath.lastIndexOf('/')
if (lastSlash > 0) {
def parentPath = screenPath.substring(0, lastSlash) + ".xml"
def subscreenName = screenPath.substring(lastSlash + 1).replace('.xml', '')
if (ec.resource.getLocationReference(parentPath).getExists()) {
// Found parent, now find the subscreen location
try {
def parentDef = ec.screen.getScreenDefinition(parentPath)
def subscreenItem = parentDef?.getSubscreensItem(subscreenName)
if (subscreenItem && subscreenItem.getLocation()) {
ec.logger.info("Redirecting browse from ${screenPath} to ${subscreenItem.getLocation()}")
screenPath = subscreenItem.getLocation()
}
} catch (Exception e) {
ec.logger.warn("Error resolving subscreen location: ${e.message}")
}
}
}
}
try {
// First, add the current screen itself as a tool if it's executable
def currentToolName = McpUtils.getToolName(screenPath)
// Use baseToolName if available to preserve context (e.g. moqui_PopCommerce_...)
// even if the implementation is in another component (e.g. SimpleScreens)
def currentToolName = baseToolName ?: McpUtils.getToolName(screenPath)
tools << [
name: currentToolName,
description: "Execute screen: ${currentToolName}",
......@@ -1427,7 +1491,7 @@ def startTime = System.currentTimeMillis()
// Construct sub-tool name by extending the parent path
// This assumes subscreens are structurally nested in the tool name
// e.g. moqui_PopCommerce_Admin -> moqui_PopCommerce_Admin_Catalog
def parentToolName = McpUtils.getToolName(screenPath)
def parentToolName = baseToolName ?: McpUtils.getToolName(screenPath)
def subToolName = parentToolName + "_" + subName
subscreens << [
......