mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-04 22:33:31 +08:00
Delete skills/check_mcp.py
This commit is contained in:
parent
1db330b2f3
commit
59d7edde88
@ -1,444 +0,0 @@
|
||||
#!/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())
|
||||
Loading…
Reference in New Issue
Block a user