added cms search skills

This commit is contained in:
thrylokya 2026-03-18 01:36:05 +05:30 committed by GitHub
parent d3928eb44c
commit 2b0ce1a693
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1302 additions and 0 deletions

86
skills/SKILL.md Normal file
View File

@ -0,0 +1,86 @@
---
name: cms-media-options
description: REQUIRED entry point for ALL media and image searches. Every request to find, search, locate, retrieve, browse, get, look up, or use images, photos, pictures, media, icons, illustrations, hero images, banners, thumbnails, logos MUST go through this skill first. Checks which search sources are available, presents only those options, and delegates to the appropriate search skill based on the user's choice. Never search for media directly — always start here.
license: Apache-2.0
compatibility: Python 3.x or Python 2.7+ recommended, bash 3.2+ (Git Bash/WSL on Windows). Cross-platform: Mac, Windows (Git Bash/WSL), Linux, POSIX.
metadata:
author: afv-library
version: "1.0"
---
# CMS Media Search — Source Selection
**This is the mandatory entry point for ALL media searches.** When the user asks to **find**, **search for**, **locate**, **retrieve**, **browse**, **get**, **look up**, or **use** any kind of visual content — **always start here**. Do not assume a source or call a search tool directly. Check what is available, present only those options, and delegate to the matching search skill.
## When to Use This Skill
Activate whenever the user's request involves any visual content, including but not limited to:
- Images, photos, pictures, media, visuals, graphics
- Icons, illustrations, banners, thumbnails, logos
- Hero images, background images, feature images, cover images
- Any asset described as visual (e.g. "something for the carousel", "a picture for the header")
Example triggers:
- "Find a modern luxury apartment exterior and use it in the hero section"
- "I need a hero image for the landing page"
- "Search for family lifestyle photos for the carousel"
- "Get me a logo for the about page"
- "Look up some banner graphics"
- "Can you find product images?"
---
## Step 1: Check Source Availability
**Before presenting any options**, run the MCP availability check script:
```bash
python3 scripts/check_mcp.py
```
Or if Python is not available:
```bash
bash scripts/check_mcp.sh
```
The script returns JSON:
```json
{"cms_search": true, "data_cloud": false, "unsplash": true}
```
**Only present sources that are `true` in the output**, plus **Other** (always available).
### Example: All sources available
> I can help you find that image. Where would you like to search?
> 1. **Data Cloud AI Hybrid Search (Salesforce CMS + 3rd-party DAMs)**
> 2. **CMS Keyword Search (Salesforce CMS)**
> 3. **Unsplash**
> 4. **Other** (please specify)
### Example: Only CMS available
> I can help you find that image. Where would you like to search?
> 1. **CMS Keyword Search (Salesforce CMS)**
> 2. **Other** (please specify)
## Step 2: Delegate to Search Skill
Only after the user selects an option, follow the matching skill by **source name** (not number — numbers change based on availability):
| User selects | Action |
|---|---|
| **CMS Keyword Search (Salesforce CMS)** | Read and follow `../cms-keyword-search/SKILL.md` |
| **Data Cloud AI Hybrid Search (Salesforce CMS + 3rd-party DAMs)** | Read and follow `../cms-d360-search/SKILL.md` |
| **Unsplash** | Invoke the Unsplash MCP tools
| **Other** | Ask the user for the source URL or asset library details, then retrieve accordingly |
## Step 3: Present Results
After the delegated skill returns results:
- Display returned assets with preview thumbnails when available.
- Include asset title, source system, and relevance score or tags.
- Let the user confirm which asset to use before inserting it into the page or component.
- Do **not** automatically use the first result — user selection is required.

View File

@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Build CMS Search Input JSON
Generates properly formatted input JSON for search_media_cms_channels MCP tool.
Usage:
python3 build_cms_search_input.py --keywords "car,automobile,vehicle" --taxonomies "Modern,Luxury" --locale "en_US"
# With empty keywords
python3 build_cms_search_input.py --keywords "" --taxonomies "Bright,Spacious" --locale "en_US"
# With empty taxonomies
python3 build_cms_search_input.py --keywords "logo,brand" --taxonomies "" --locale "en_US"
# With page parameters
python3 build_cms_search_input.py --keywords "car" --taxonomies "Luxury" --locale "en_US" --limit 10 --offset 0
"""
import argparse
import json
import sys
def build_search_keyword(keywords):
"""
Convert comma-separated keywords to OR-separated format.
Args:
keywords: Comma-separated string of keywords, or empty string
Returns:
OR-separated keyword string, or empty string
Examples:
"car,automobile,vehicle" -> "car OR automobile OR vehicle"
"" -> ""
"logo" -> "logo"
"""
if not keywords or keywords.strip() == "":
return ""
# Split by comma, strip whitespace, filter empty
keyword_list = [k.strip() for k in keywords.split(',') if k.strip()]
if not keyword_list:
return ""
# Join with OR
return " OR ".join(keyword_list)
def build_taxonomy_expression(taxonomies):
"""
Convert comma-separated taxonomies to JSON string format.
Args:
taxonomies: Comma-separated string of taxonomy labels, or empty string
Returns:
JSON string in format: {"OR": ["Label1", "Label2", "Label3"]}, or "{}"
Examples:
"Modern,Luxury,Premium" -> "{\"OR\": [\"Modern\", \"Luxury\", \"Premium\"]}"
"" -> "{}"
"Bright" -> "{\"OR\": [\"Bright\"]}"
"""
if not taxonomies or taxonomies.strip() == "":
return "{}"
# Split by comma, strip whitespace, filter empty
taxonomy_list = [t.strip() for t in taxonomies.split(',') if t.strip()]
if not taxonomy_list:
return "{}"
# Build JSON object and convert to string
taxonomy_obj = {"OR": taxonomy_list}
return json.dumps(taxonomy_obj)
def build_cms_search_input(keywords, taxonomies, locale="en_US", page_offset=0, page_limit=5):
"""
Build the complete input JSON for search_media_cms_channels.
Args:
keywords: Comma-separated string of keywords
taxonomies: Comma-separated string of taxonomy labels
locale: Language locale (e.g., "en_US", "es_MX", "fr_CA")
page_offset: Starting offset for pagination (default: 0)
page_limit: Number of results to return (default: 5)
Returns:
Dictionary with properly formatted input
"""
return {
"inputs": [{
"searchKeyword": build_search_keyword(keywords),
"taxonomyExpression": build_taxonomy_expression(taxonomies),
"searchLanguage": locale,
"channelIds": "",
"channelType": "PublicUnauthenticated",
"contentTypeFqns": "sfdc_cms__image",
"pageOffset": page_offset,
"pageLimit": page_limit
}]
}
def main():
parser = argparse.ArgumentParser(
description='Build properly formatted input JSON for CMS image search',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic search
python3 build_cms_search_input.py --keywords "car,automobile,vehicle" --taxonomies "Modern,Luxury" --locale "en_US"
# Search with only taxonomies (no keywords)
python3 build_cms_search_input.py --keywords "" --taxonomies "Bright,Spacious,Airy" --locale "en_US"
# Search with only keywords (no taxonomies)
python3 build_cms_search_input.py --keywords "logo,brand,corporate" --taxonomies "" --locale "en_US"
# With custom page limit
python3 build_cms_search_input.py --keywords "apartment" --taxonomies "Luxury" --locale "en_US" --limit 20
"""
)
parser.add_argument(
'--keywords',
required=True,
help='Comma-separated keywords (e.g., "car,automobile,vehicle"). Use empty string "" for no keywords.'
)
parser.add_argument(
'--taxonomies',
required=True,
help='Comma-separated taxonomy labels (e.g., "Modern,Luxury,Premium"). Use empty string "" for no taxonomies.'
)
parser.add_argument(
'--locale',
default='en_US',
help='Language locale (e.g., "en_US", "es_MX", "fr_CA"). Default: "en_US"'
)
parser.add_argument(
'--offset',
type=int,
default=0,
help='Page offset for pagination. Default: 0'
)
parser.add_argument(
'--limit',
type=int,
default=5,
help='Number of results to return. Default: 5'
)
args = parser.parse_args()
# Build the input
input_json = build_cms_search_input(
keywords=args.keywords,
taxonomies=args.taxonomies,
locale=args.locale,
page_offset=args.offset,
page_limit=args.limit
)
# Output JSON
print(json.dumps(input_json, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Build CMS Search Input JSON
#
# Generates properly formatted input JSON for search_media_cms_channels MCP tool.
# Cross-platform compatible: Mac, Windows (Git Bash/MSYS/Cygwin), Linux, POSIX
#
# Usage:
# bash build_cms_search_input.sh --keywords "car,automobile,vehicle" --taxonomies "Modern,Luxury" --locale "en_US"
# bash build_cms_search_input.sh --keywords "car" --taxonomies "Luxury"
#
# Examples:
# # With empty keywords
# bash build_cms_search_input.sh --keywords "" --taxonomies "Bright,Spacious" --locale "en_US"
#
# # With empty taxonomies
# bash build_cms_search_input.sh --keywords "logo,brand" --taxonomies "" --locale "en_US"
#
# # With page parameters
# bash build_cms_search_input.sh --keywords "car" --taxonomies "Luxury" --locale "en_US" --limit 10 --offset 0
set -e
# Default values
KEYWORDS=""
TAXONOMIES=""
LOCALE="en_US"
PAGE_OFFSET=0
PAGE_LIMIT=5
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--keywords)
KEYWORDS="$2"
shift 2
;;
--taxonomies)
TAXONOMIES="$2"
shift 2
;;
--locale)
LOCALE="$2"
shift 2
;;
--offset)
PAGE_OFFSET="$2"
shift 2
;;
--limit)
PAGE_LIMIT="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 --keywords KEYWORDS --taxonomies TAXONOMIES [--locale LOCALE] [--offset OFFSET] [--limit LIMIT]"
echo ""
echo "Required arguments:"
echo " --keywords KEYWORDS Comma-separated keywords (e.g., 'car,automobile,vehicle')"
echo " Use empty string '' for no keywords"
echo " --taxonomies TAXONOMIES Comma-separated taxonomy labels (e.g., 'Modern,Luxury')"
echo " Use empty string '' for no taxonomies"
echo ""
echo "Optional arguments:"
echo " --locale LOCALE Language locale (default: 'en_US')"
echo " --offset OFFSET Page offset for pagination (default: 0)"
echo " --limit LIMIT Number of results to return (default: 5)"
echo ""
echo "Examples:"
echo " $0 --keywords 'car,automobile,vehicle' --taxonomies 'Modern,Luxury' --locale 'en_US'"
echo " $0 --keywords '' --taxonomies 'Bright,Spacious' --locale 'en_US'"
echo " $0 --keywords 'logo,brand' --taxonomies '' --locale 'en_US' --limit 10"
exit 0
;;
*)
echo "Unknown argument: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Function to build search keyword (OR-separated)
build_search_keyword() {
local keywords="$1"
if [[ -z "$keywords" ]]; then
echo ""
return
fi
# Replace commas with " OR "
local result=$(echo "$keywords" | sed 's/,/ OR /g' | sed 's/ */ /g')
echo "$result"
}
# Function to build taxonomy expression (JSON string)
build_taxonomy_expression() {
local taxonomies="$1"
if [[ -z "$taxonomies" ]]; then
echo "{}"
return
fi
# Split by comma and build JSON array
IFS=',' read -ra taxonomy_array <<< "$taxonomies"
# Build JSON array string
local json_array="["
local first=true
for tax in "${taxonomy_array[@]}"; do
tax=$(echo "$tax" | xargs) # Trim whitespace
if [[ -n "$tax" ]]; then
if [[ "$first" == true ]]; then
first=false
else
json_array+=", "
fi
json_array+="\"$tax\""
fi
done
json_array+="]"
# Build complete JSON string
echo "{\"OR\": $json_array}"
}
# Build components
SEARCH_KEYWORD=$(build_search_keyword "$KEYWORDS")
TAXONOMY_EXPRESSION=$(build_taxonomy_expression "$TAXONOMIES")
# Output JSON
cat <<EOF
{
"inputs": [{
"searchKeyword": "$SEARCH_KEYWORD",
"taxonomyExpression": "$TAXONOMY_EXPRESSION",
"searchLanguage": "$LOCALE",
"channelIds": "",
"channelType": "PublicUnauthenticated",
"contentTypeFqns": "sfdc_cms__image",
"pageOffset": $PAGE_OFFSET,
"pageLimit": $PAGE_LIMIT
}]
}
EOF

444
skills/check_mcp.py Normal file
View File

@ -0,0 +1,444 @@
#!/usr/bin/env python3
"""
Check MCP Content Server Availability and Tools
Returns JSON with availability status for each media source:
{
"cms_search": true|false,
"data_cloud": true|false,
"unsplash": true|false
}
Invocation: Run with `python3 check_mcp.py` or, if Python is not available,
use the Bash equivalent `bash check_mcp.sh`. Both produce identical output.
"""
import json
import os
import sys
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
# Use only stdlib (no requests) so the script runs in minimal Vibes/CI environments
# Timeout for HTTP requests (seconds)
TIMEOUT = 5
# Relative path from IDE config root (Code/Cursor) to A4D MCP settings file
_A4D_MCP_SETTINGS_REL = Path('User/globalStorage/salesforce.salesforcedx-einstein-gpt/settings/a4d_mcp_settings.json')
def _get_ide_config_root():
"""
Return the IDE (Code/VS Code) config root directory for this platform.
Used to find globalStorage for the Salesforce Einstein GPT extension.
"""
if sys.platform == 'win32':
appdata = os.environ.get('APPDATA', '')
if not appdata:
appdata = Path.home() / 'AppData' / 'Roaming'
return Path(appdata) / 'Code'
if sys.platform == 'darwin':
return Path.home() / 'Library' / 'Application Support' / 'Code'
# Linux and other POSIX
xdg = os.environ.get('XDG_CONFIG_HOME') or (Path.home() / '.config')
return Path(xdg) / 'Code'
def get_a4d_mcp_settings_path():
"""Return the A4D MCP settings path for the current platform (Mac, Windows, Linux, etc.)."""
return _get_ide_config_root() / _A4D_MCP_SETTINGS_REL
# Path to A4D MCP settings (platform-agnostic)
SETTINGS_PATH = get_a4d_mcp_settings_path()
# URL patterns that identify the content MCP endpoint (server name is irrelevant)
CONTENT_ENDPOINT_PATTERN = 'platform/content'
CONTENT_READONLY_PATTERN = 'content-readonly'
# Args pattern that identifies the Unsplash MCP server (server name is irrelevant)
UNSPLASH_MCP_ARGS_PATTERN = 'unsplash-mcp-server'
def _http_post_json(url, headers, payload, timeout=TIMEOUT):
"""
POST JSON to url using stdlib only (no requests dependency).
Returns (status_code, response_headers, response_data_dict or None).
On failure returns (None, None, None).
"""
data = json.dumps(payload).encode('utf-8')
req_headers = {**headers, 'Content-Type': 'application/json'}
req = Request(url, data=data, headers=req_headers, method='POST')
try:
with urlopen(req, timeout=timeout) as resp:
body = resp.read().decode('utf-8')
result = json.loads(body) if body else None
return (resp.status, resp.headers, result)
except HTTPError as e:
try:
body = e.read().decode('utf-8')
result = json.loads(body) if body else None
except Exception:
result = None
return (e.code, e.headers, result)
except (URLError, OSError, ValueError, json.JSONDecodeError):
return (None, None, None)
def get_raw_settings():
"""
Read and parse the full A4D MCP settings file.
Returns the full settings dict or None if not found / invalid.
"""
try:
if not SETTINGS_PATH.exists():
return None
with open(SETTINGS_PATH, 'r') as f:
return json.load(f)
except Exception:
return None
def get_mcp_settings():
"""
Read and parse A4D MCP settings.
Returns the mcpServers dict or None if not found.
"""
settings = get_raw_settings()
if not settings:
return None
return settings.get('mcpServers', {})
def _args_contain_content_endpoint(args):
"""True if args list contains the content MCP endpoint URL (any server name)."""
if not args:
return False
for arg in args:
if isinstance(arg, str) and (
CONTENT_ENDPOINT_PATTERN in arg or CONTENT_READONLY_PATTERN in arg
):
return True
return False
def get_content_server_config():
"""
Get the MCP server config that uses the content endpoint URL.
Does not depend on server name; finds any server whose args contain
the content endpoint (e.g. .../platform/content or .../content-readonly).
Returns the config dict or empty dict if not found.
"""
mcp_servers = get_mcp_settings()
if not mcp_servers:
return {}
for config in mcp_servers.values():
if not isinstance(config, dict):
continue
args = config.get('args', [])
if _args_contain_content_endpoint(args):
return config
return {}
def get_contentmcp_url():
"""
Extract MCP content server URL from settings.
Returns the URL or None if not found, not configured, or stdio type.
"""
content_config = get_content_server_config()
if not content_config:
return None
if content_config.get('type') == 'stdio':
return None
args = content_config.get('args', [])
for arg in args:
if isinstance(arg, str) and (
CONTENT_ENDPOINT_PATTERN in arg or CONTENT_READONLY_PATTERN in arg
):
return arg
return None
def is_contentmcp_enabled():
"""
Check if content MCP server is enabled in settings.
Returns True if enabled (disabled: false) AND not stdio type, False otherwise.
Note: For stdio type servers, we cannot check availability via URL,
so we return False to skip URL-based checks.
"""
content_config = get_content_server_config()
if not content_config:
return False
# Check if disabled field is explicitly false (enabled)
disabled = content_config.get('disabled', False)
if disabled:
return False
# If it's stdio type, we can't do URL-based availability checks
# The MCP server is managed by the IDE/extension, not accessible via HTTP
if content_config.get('type') == 'stdio':
# For stdio, assume it's available if enabled
# (The IDE manages the connection)
return True
# For non-stdio (like mcp-remote), we can check via URL
return True
def _args_contain_unsplash_mcp(args):
"""True if args list contains the unsplash MCP server (any server name)."""
if not args:
return False
for arg in args:
if isinstance(arg, str) and UNSPLASH_MCP_ARGS_PATTERN in arg:
return True
return False
def get_unsplash_server_config():
"""
Get the Unsplash MCP server config from settings.
Checks (1) root-level "unsplash" key and (2) any server in mcpServers
whose args contain "unsplash-mcp-server" (name-agnostic).
Returns the config dict or empty dict if not found.
"""
settings = get_raw_settings()
if not settings:
return {}
# Root-level "unsplash" (some configs put it here)
root_unsplash = settings.get('unsplash')
if isinstance(root_unsplash, dict) and root_unsplash:
return root_unsplash
# Any server in mcpServers that runs unsplash-mcp-server
mcp_servers = settings.get('mcpServers', {})
for config in mcp_servers.values() if mcp_servers else ():
if isinstance(config, dict) and _args_contain_unsplash_mcp(config.get('args', [])):
return config
return {}
def is_unsplash_mcp_enabled():
"""
Check if Unsplash MCP server is configured and enabled.
Returns True if configured and enabled, False otherwise.
"""
unsplash_config = get_unsplash_server_config()
if not unsplash_config:
return False
disabled = unsplash_config.get('disabled', False)
return not disabled
def get_bearer_token():
"""
Extract bearer token from A4D MCP settings.
Returns the token string or None if not found.
"""
content_config = get_content_server_config()
if not content_config:
return None
# Extract Authorization header from args
args = content_config.get('args', [])
for i, arg in enumerate(args):
if arg == '--header' and i + 1 < len(args):
auth_header = args[i + 1]
# Format: "Authorization: Bearer <token>"
if auth_header.startswith('Authorization: Bearer '):
return auth_header.replace('Authorization: Bearer ', '')
return None
def get_base_headers():
"""Get base headers for MCP requests (without session ID)."""
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
}
token = get_bearer_token()
if token:
headers['Authorization'] = f'Bearer {token}'
return headers
def initialize_mcp_session(endpoint_url):
"""
Initialize MCP session and get session ID.
Args:
endpoint_url: The MCP server endpoint URL
Returns session ID or None if initialization fails.
"""
headers = get_base_headers()
# Step 1: Initialize
payload = {
"jsonrpc": "2.0",
"method": "initialize",
"id": "0",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "CheckMCPScript", "version": "1.0.0"}
}
}
try:
status, resp_headers, data = _http_post_json(endpoint_url, headers, payload, TIMEOUT)
if status == 200 and data is not None:
# Try to get from Mcp-Session-Id header
session_id = resp_headers.get('Mcp-Session-Id') if resp_headers else None
if session_id:
return session_id
# Try to get from response body
if 'result' in data and isinstance(data['result'], dict):
session_id = data['result'].get('sessionId') or data['result'].get('session_id')
if session_id:
return session_id
except Exception:
pass
return None
def send_initialized_notification(endpoint_url, session_id):
"""
Send initialized notification to MCP server.
Args:
endpoint_url: The MCP server endpoint URL
session_id: The session ID from initialization
Returns True if successful, False otherwise.
"""
headers = get_base_headers()
headers['Mcp-Session-Id'] = session_id
payload = {
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
try:
status, _, _ = _http_post_json(endpoint_url, headers, payload, TIMEOUT)
return status in [200, 202] # Accept both 200 and 202
except Exception:
return False
def list_available_tools():
"""
List all available tools from MCP server using JSON-RPC.
Follows proper MCP initialization flow.
Returns a list of tool names, or empty list if unavailable.
"""
# Get the endpoint URL
endpoint_url = get_contentmcp_url()
if not endpoint_url:
# stdio type or no URL configured
return []
# Step 1: Initialize and get session ID
session_id = initialize_mcp_session(endpoint_url)
if not session_id:
return []
# Step 2: Send initialized notification
if not send_initialized_notification(endpoint_url, session_id):
return []
# Step 3: List tools
headers = get_base_headers()
headers['Mcp-Session-Id'] = session_id
payload = {
"jsonrpc": "2.0",
"method": "tools/list",
"id": "1",
"params": {}
}
try:
status, _, data = _http_post_json(endpoint_url, headers, payload, TIMEOUT)
if status == 200 and data and 'result' in data and 'tools' in data['result']:
tools = data['result']['tools']
tool_names = [tool.get('name') for tool in tools if isinstance(tool, dict) and 'name' in tool]
return tool_names
except Exception:
pass
return []
def check_mcp_health():
"""Check if MCP server is reachable by attempting to list tools."""
tools = list_available_tools()
return len(tools) > 0
def check_tool_available(tool_name):
"""
Check if a specific tool is available via MCP server.
Uses JSON-RPC to list tools and searches for the tool name.
"""
available_tools = list_available_tools()
return tool_name in available_tools
def main():
"""Main execution."""
result = {
"cms_search": False,
"data_cloud": False,
"unsplash": False
}
# First check if contentmcp server is enabled in settings
if not is_contentmcp_enabled():
# contentmcp is disabled, return all false
print(json.dumps(result))
return 0
# Check if it's stdio type
content_config = get_content_server_config()
if content_config.get('type') == 'stdio':
# For stdio type, we can't check via URL
# Assume tools are available if the server is enabled
# The IDE/extension manages the connection
result["cms_search"] = True
result["data_cloud"] = True
else:
# For non-stdio (like mcp-remote), check via URL
if check_mcp_health():
# Check for CMS search tool
if check_tool_available("search_media_cms_channels"):
result["cms_search"] = True
# Check for Data Cloud search tool
if check_tool_available("search_electronic_media"):
result["data_cloud"] = True
# Check for Unsplash MCP server separately
if is_unsplash_mcp_enabled():
result["unsplash"] = True
# Output JSON result
print(json.dumps(result))
return 0
if __name__ == "__main__":
sys.exit(main())

448
skills/check_mcp.sh Normal file
View File

@ -0,0 +1,448 @@
#!/bin/bash
# Check MCP Content Server Availability and Tools
#
# Returns JSON with availability status for each media source:
# {
# "cms_search": true|false,
# "data_cloud": true|false,
# "unsplash": true|false
# }
set -e
TIMEOUT=5
# URL patterns that identify the content MCP endpoint (name-agnostic)
CONTENT_ENDPOINT_PATTERN="platform/content"
CONTENT_READONLY_PATTERN="content-readonly"
# Args pattern that identifies the Unsplash MCP server (name-agnostic)
UNSPLASH_MCP_ARGS_PATTERN="unsplash-mcp-server"
# ---------------------------------------------------------------------------
# Platform detection and settings path
# ---------------------------------------------------------------------------
get_ide_config_root() {
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" || "$OSTYPE" == "cygwin" ]]; then
local appdata="${APPDATA:-$HOME/AppData/Roaming}"
echo "$appdata/Code"
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "$HOME/Library/Application Support/Code"
else
local xdg="${XDG_CONFIG_HOME:-$HOME/.config}"
echo "$xdg/Code"
fi
}
SETTINGS_PATH="$(get_ide_config_root)/User/globalStorage/salesforce.salesforcedx-einstein-gpt/settings/a4d_mcp_settings.json"
# ---------------------------------------------------------------------------
# JSON helpers — jq preferred, grep/sed fallback
# ---------------------------------------------------------------------------
_has_jq() { command -v jq &> /dev/null; }
# Get the full raw settings as a string (only useful with jq)
_settings_exists() { [[ -f "$SETTINGS_PATH" ]]; }
# ---------------------------------------------------------------------------
# Name-agnostic content server detection
# Scans ALL mcpServers entries for args containing CONTENT_ENDPOINT_PATTERN
# or CONTENT_READONLY_PATTERN. Returns the server key (name) or empty.
# ---------------------------------------------------------------------------
get_content_server_key() {
if ! _settings_exists; then echo ""; return; fi
if _has_jq; then
local key
key=$(jq -r '
.mcpServers // {} | to_entries[] |
select(.value.args? // [] | map(select(type == "string")) |
any(contains("'"$CONTENT_ENDPOINT_PATTERN"'") or contains("'"$CONTENT_READONLY_PATTERN"'")))
| .key' "$SETTINGS_PATH" 2>/dev/null | head -n 1)
echo "$key"
else
# grep fallback: look for any args line containing the patterns
local key
key=$(grep -B 20 "$CONTENT_ENDPOINT_PATTERN\|$CONTENT_READONLY_PATTERN" "$SETTINGS_PATH" 2>/dev/null \
| grep -o '"[^"]*": *{' | tail -n 1 | sed 's/[": {]//g')
echo "$key"
fi
}
# ---------------------------------------------------------------------------
# Content server config queries (all name-agnostic via get_content_server_key)
# ---------------------------------------------------------------------------
is_content_server_disabled() {
local key
key=$(get_content_server_key)
[[ -z "$key" ]] && return 0 # no server found → treat as disabled
if _has_jq; then
local disabled
disabled=$(jq -r ".mcpServers[\"$key\"].disabled // false" "$SETTINGS_PATH" 2>/dev/null)
[[ "$disabled" == "true" ]] && return 0
return 1
else
if grep -A 5 "\"$key\"" "$SETTINGS_PATH" 2>/dev/null | grep -q '"disabled": *true'; then
return 0
fi
return 1
fi
}
is_contentmcp_enabled() {
if ! _settings_exists; then return 1; fi
local key
key=$(get_content_server_key)
[[ -z "$key" ]] && return 1
if is_content_server_disabled; then return 1; fi
return 0
}
is_stdio_type() {
if ! _settings_exists; then return 1; fi
local key
key=$(get_content_server_key)
[[ -z "$key" ]] && return 1
if _has_jq; then
local stype
stype=$(jq -r ".mcpServers[\"$key\"].type // empty" "$SETTINGS_PATH" 2>/dev/null)
[[ "$stype" == "stdio" ]] && return 0
else
if grep -A 10 "\"$key\"" "$SETTINGS_PATH" 2>/dev/null | grep -q '"type": *"stdio"'; then
return 0
fi
fi
return 1
}
# ---------------------------------------------------------------------------
# Extract the MCP content endpoint URL from settings args (name-agnostic).
# Returns empty if not found — no localhost or env var fallback.
# ---------------------------------------------------------------------------
get_contentmcp_url() {
local key
key=$(get_content_server_key)
[[ -z "$key" ]] && { echo ""; return; }
if _has_jq; then
local url
url=$(jq -r "
.mcpServers[\"$key\"].args // [] | map(select(type == \"string\")) |
map(select(contains(\"$CONTENT_ENDPOINT_PATTERN\") or contains(\"$CONTENT_READONLY_PATTERN\")))
| .[0] // empty" "$SETTINGS_PATH" 2>/dev/null)
if [[ -n "$url" ]]; then echo "$url"; return; fi
else
local url
url=$(grep -o 'http[s]*://[^"]*' "$SETTINGS_PATH" 2>/dev/null \
| grep -E "$CONTENT_ENDPOINT_PATTERN|$CONTENT_READONLY_PATTERN" | head -n 1)
if [[ -n "$url" ]]; then echo "$url"; return; fi
fi
echo ""
}
# ---------------------------------------------------------------------------
# Name-agnostic Unsplash detection.
# Checks (1) root-level "unsplash" key and (2) any mcpServers entry whose
# args contain "unsplash-mcp-server".
# ---------------------------------------------------------------------------
get_unsplash_server_key() {
if ! _settings_exists; then echo ""; return; fi
if _has_jq; then
# Check root-level "unsplash" key first
local root_unsplash
root_unsplash=$(jq -r '.unsplash // empty' "$SETTINGS_PATH" 2>/dev/null)
if [[ -n "$root_unsplash" && "$root_unsplash" != "null" ]]; then
echo "__root__"
return
fi
# Scan mcpServers for any server with unsplash-mcp-server in args
local key
key=$(jq -r '
.mcpServers // {} | to_entries[] |
select(.value.args? // [] | map(select(type == "string")) |
any(contains("'"$UNSPLASH_MCP_ARGS_PATTERN"'")))
| .key' "$SETTINGS_PATH" 2>/dev/null | head -n 1)
echo "$key"
else
# grep fallback: check root "unsplash" or args containing the pattern
if grep -q "\"unsplash\"" "$SETTINGS_PATH" 2>/dev/null; then
echo "unsplash"
return
fi
if grep -q "$UNSPLASH_MCP_ARGS_PATTERN" "$SETTINGS_PATH" 2>/dev/null; then
local key
key=$(grep -B 20 "$UNSPLASH_MCP_ARGS_PATTERN" "$SETTINGS_PATH" 2>/dev/null \
| grep -o '"[^"]*": *{' | tail -n 1 | sed 's/[": {]//g')
echo "$key"
fi
fi
}
is_unsplash_mcp_enabled() {
if ! _settings_exists; then return 1; fi
local key
key=$(get_unsplash_server_key)
[[ -z "$key" ]] && return 1
if [[ "$key" == "__root__" ]]; then
# Root-level unsplash: check disabled
if _has_jq; then
local disabled
disabled=$(jq -r '.unsplash.disabled // false' "$SETTINGS_PATH" 2>/dev/null)
[[ "$disabled" == "true" ]] && return 1
fi
return 0
fi
# mcpServers entry
if _has_jq; then
local disabled
disabled=$(jq -r ".mcpServers[\"$key\"].disabled // false" "$SETTINGS_PATH" 2>/dev/null)
[[ "$disabled" == "true" ]] && return 1
return 0
else
if grep -A 5 "\"$key\"" "$SETTINGS_PATH" 2>/dev/null | grep -q '"disabled": *true'; then
return 1
fi
return 0
fi
}
# ---------------------------------------------------------------------------
# Bearer token extraction (name-agnostic: from the matched content server)
# Looks for --header arg followed by "Authorization: Bearer <token>".
# ---------------------------------------------------------------------------
get_bearer_token() {
if ! _settings_exists; then echo ""; return; fi
local key
key=$(get_content_server_key)
[[ -z "$key" ]] && { echo ""; return; }
if _has_jq; then
# Find the arg after "--header" that starts with "Authorization: Bearer "
local token
token=$(jq -r "
.mcpServers[\"$key\"].args // [] | to_entries |
map(select(.value == \"--header\")) |
.[0].key as \$idx |
if \$idx then
.mcpServers[\"$key\"].args[\$idx + 1] // empty
else empty end
" "$SETTINGS_PATH" 2>/dev/null)
# jq path above is tricky; simpler approach: iterate pairs
token=$(jq -r "
[.mcpServers[\"$key\"].args // []] | .[0] | . as \$args |
[range(0; length - 1)] |
map(select(\$args[.] == \"--header\" and (\$args[. + 1] | startswith(\"Authorization: Bearer \")))) |
.[0] as \$i |
if \$i then \$args[\$i + 1] | sub(\"Authorization: Bearer \"; \"\") else empty end
" "$SETTINGS_PATH" 2>/dev/null)
echo "$token"
else
local token
token=$(grep -o 'Authorization: Bearer [^"]*' "$SETTINGS_PATH" 2>/dev/null \
| sed 's/Authorization: Bearer //' | head -n 1)
echo "$token"
fi
}
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
MCP_ENDPOINT=""
BEARER_TOKEN=""
_curl_post() {
local url="$1"
local payload="$2"
local include_headers="${3:-false}"
local -a curl_args=(-s -m "$TIMEOUT")
if [[ "$include_headers" == "true" ]]; then
curl_args+=(-i)
fi
if [[ -n "$BEARER_TOKEN" ]]; then
curl_args+=(-H "Authorization: Bearer $BEARER_TOKEN")
fi
curl_args+=(-H "Content-Type: application/json")
curl_args+=(-H "Accept: application/json, text/event-stream")
if [[ -n "${SESSION_ID:-}" ]]; then
curl_args+=(-H "Mcp-Session-Id: $SESSION_ID")
fi
curl_args+=(-X POST -d "$payload" "$url")
curl "${curl_args[@]}" 2>/dev/null
}
_curl_post_status() {
local url="$1"
local payload="$2"
local -a curl_args=(-s -o /dev/null -w "%{http_code}" -m "$TIMEOUT")
if [[ -n "$BEARER_TOKEN" ]]; then
curl_args+=(-H "Authorization: Bearer $BEARER_TOKEN")
fi
curl_args+=(-H "Content-Type: application/json")
curl_args+=(-H "Accept: application/json, text/event-stream")
if [[ -n "${SESSION_ID:-}" ]]; then
curl_args+=(-H "Mcp-Session-Id: $SESSION_ID")
fi
curl_args+=(-X POST -d "$payload" "$url")
curl "${curl_args[@]}" 2>/dev/null
}
# ---------------------------------------------------------------------------
# MCP JSON-RPC session flow
# ---------------------------------------------------------------------------
SESSION_ID=""
initialize_mcp_session() {
if ! command -v curl &> /dev/null; then echo ""; return 1; fi
local payload='{"jsonrpc":"2.0","method":"initialize","id":"0","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"CheckMCPScript","version":"1.0.0"}}}'
local response
response=$(_curl_post "$MCP_ENDPOINT" "$payload" "true")
# Try Mcp-Session-Id from response headers
local sid
sid=$(echo "$response" | grep -i "^Mcp-Session-Id:" | sed 's/^[^:]*: *//' | tr -d '\r\n ')
if [[ -n "$sid" ]]; then echo "$sid"; return 0; fi
# Try from response body: result.sessionId or result.session_id
local body
body=$(echo "$response" | sed -n '/^\r*$/,$p' | tail -n +2)
if [[ -n "$body" ]] && _has_jq; then
sid=$(echo "$body" | jq -r '.result.sessionId // .result.session_id // empty' 2>/dev/null)
if [[ -n "$sid" ]]; then echo "$sid"; return 0; fi
elif [[ -n "$body" ]]; then
sid=$(echo "$body" | grep -o '"sessionId":"[^"]*"' | sed 's/"sessionId":"//;s/"//' | head -n 1)
if [[ -z "$sid" ]]; then
sid=$(echo "$body" | grep -o '"session_id":"[^"]*"' | sed 's/"session_id":"//;s/"//' | head -n 1)
fi
if [[ -n "$sid" ]]; then echo "$sid"; return 0; fi
fi
echo ""
return 1
}
send_initialized_notification() {
local sid="$1"
if ! command -v curl &> /dev/null; then return 1; fi
SESSION_ID="$sid"
local payload='{"jsonrpc":"2.0","method":"notifications/initialized"}'
local status_code
status_code=$(_curl_post_status "$MCP_ENDPOINT" "$payload")
# Accept both 200 and 202 (matching Python)
if [[ "$status_code" == "200" || "$status_code" == "202" ]]; then
return 0
fi
return 1
}
list_available_tools() {
if ! command -v curl &> /dev/null; then echo ""; return 1; fi
local sid
sid=$(initialize_mcp_session)
if [[ -z "$sid" ]]; then echo ""; return 1; fi
if ! send_initialized_notification "$sid"; then echo ""; return 1; fi
SESSION_ID="$sid"
local payload='{"jsonrpc":"2.0","method":"tools/list","id":"1","params":{}}'
local response
response=$(_curl_post "$MCP_ENDPOINT" "$payload")
if [[ -n "$response" ]]; then
echo "$response"
return 0
fi
echo ""
return 1
}
check_mcp_health() {
local response
response=$(list_available_tools)
if [[ -n "$response" ]] && echo "$response" | grep -q '"tools"'; then
return 0
fi
return 1
}
check_tool_available() {
local tool_name="$1"
local tools_response
tools_response=$(list_available_tools)
if [[ -n "$tools_response" ]]; then
if echo "$tools_response" | grep -q "\"name\": *\"$tool_name\""; then
return 0
fi
if echo "$tools_response" | grep -q "\"name\":\"$tool_name\""; then
return 0
fi
fi
return 1
}
# ===========================================================================
# Main
# ===========================================================================
cms_search=false
data_cloud=false
unsplash=false
if ! is_contentmcp_enabled; then
echo '{"cms_search": false, "data_cloud": false, "unsplash": false}'
exit 0
fi
if is_stdio_type; then
cms_search=true
data_cloud=true
else
MCP_ENDPOINT=$(get_contentmcp_url)
BEARER_TOKEN=$(get_bearer_token)
if check_mcp_health; then
if check_tool_available "search_media_cms_channels"; then
cms_search=true
fi
if check_tool_available "search_electronic_media"; then
data_cloud=true
fi
fi
fi
if is_unsplash_mcp_enabled; then
unsplash=true
fi
echo "{\"cms_search\": $cms_search, \"data_cloud\": $data_cloud, \"unsplash\": $unsplash}"