Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:49:50 +08:00
commit adc4b2be25
147 changed files with 24716 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
# MXCP Salesforce Python Endpoints Example
This example demonstrates how to use MXCP with Salesforce data using **Python endpoints**.
## Overview
This example provides Python MCP endpoints that allow you to:
- Execute SOQL queries to retrieve Salesforce data
- Execute SOSL searches across multiple objects
- List all available Salesforce objects
- Get detailed object descriptions
- Retrieve specific records by ID
- Perform simple text searches across common objects
## Configuration
### 1. Getting Salesforce Credentials
To use this example, you'll need:
1. **Salesforce Username**: Your Salesforce username (email address)
2. **Salesforce Password**: Your Salesforce password
3. **Security Token**: Your Salesforce security token (get from Setup → My Personal Information → Reset My Security Token)
4. **Instance URL**: Your Salesforce instance URL (e.g., https://your-domain.salesforce.com)
5. **Client ID**: A connected app client ID (you can use any valid client ID)
### 2. User Configuration
Add the following to your MXCP user config (`~/.mxcp/config.yml`):
```yaml
mxcp: 1
projects:
salesforce-demo:
profiles:
dev:
secrets:
salesforce:
instance_url: "https://your-instance.salesforce.com"
username: "your-username@example.com"
password: "your-password"
security_token: "your-security-token"
client_id: "your-client-id"
```
### 3. Site Configuration
Create an `mxcp-site.yml` file:
```yaml
mxcp: 1
project: salesforce-demo
profile: dev
secrets:
- salesforce
```
## Available Tools
### SOQL Query
Execute SOQL queries directly as Python function calls:
```bash
mxcp run tool soql --param query="SELECT Id, Name FROM Account LIMIT 10"
```
### SOSL Search
Execute SOSL searches across multiple objects:
```bash
mxcp run tool sosl --param query="FIND {Acme} IN ALL FIELDS RETURNING Account(Name, Phone)"
```
### Simple Search
Perform simple text searches across common objects:
```bash
mxcp run tool search --param search_term="Acme"
```
### List Objects
List all available Salesforce objects:
```bash
mxcp run tool list_sobjects
```
### Describe Object
Get detailed information about a specific object:
```bash
mxcp run tool describe_sobject --param sobject_name="Account"
```
### Get Object
Get a specific record by its ID:
```bash
mxcp run tool get_sobject --param sobject_name="Account" --param record_id="001xx000003DIloAAG"
```
## Project Structure
```
salesforce/
├── mxcp-site.yml # Site configuration
├── python/ # Python implementations
│ └── salesforce_endpoints.py # All Salesforce endpoint functions
├── tools/ # Tool definitions
│ ├── soql.yml
│ ├── sosl.yml
│ ├── search.yml
│ ├── list_sobjects.yml
│ ├── describe_sobject.yml
│ └── get_sobject.yml
└── README.md
```

View File

@@ -0,0 +1,15 @@
mxcp: 1
projects:
salesforce-demo:
profiles:
dev:
secrets:
- name: salesforce
type: python
parameters:
instance_url: "${SALESFORCE_URL}"
username: "${SALESFORCE_USERNAME}"
password: "${SALESFORCE_PASSWORD}"
security_token: "${SALESFORCE_TOKEN}"
client_id: "${SALESFORCE_CLIENT_ID}"

View File

@@ -0,0 +1,5 @@
mxcp: 1
project: salesforce-demo
profile: dev
secrets:
- salesforce

View File

@@ -0,0 +1,330 @@
"""
Salesforce Python Endpoints
This module provides direct Python MCP endpoints for querying Salesforce.
This is a simpler alternative to the plugin-based approach.
"""
from typing import Dict, Any, List, Optional, Callable
import logging
import time
import functools
import threading
from simple_salesforce import Salesforce # type: ignore[attr-defined]
from simple_salesforce.exceptions import SalesforceExpiredSession
from mxcp.runtime import config, on_init, on_shutdown
logger = logging.getLogger(__name__)
# Global Salesforce client for reuse across all function calls
sf_client: Optional[Salesforce] = None
# Thread lock to protect client initialization
_client_lock = threading.Lock()
@on_init
def setup_salesforce_client() -> None:
"""Initialize Salesforce client when server starts.
Thread-safe: multiple threads can safely call this simultaneously.
"""
global sf_client
with _client_lock:
logger.info("Initializing Salesforce client...")
sf_config = config.get_secret("salesforce")
if not sf_config:
raise ValueError(
"Salesforce configuration not found. Please configure Salesforce secrets in your user config."
)
required_keys = ["username", "password", "security_token", "instance_url", "client_id"]
missing_keys = [key for key in required_keys if not sf_config.get(key)]
if missing_keys:
raise ValueError(f"Missing Salesforce configuration keys: {', '.join(missing_keys)}")
sf_client = Salesforce(
username=sf_config["username"],
password=sf_config["password"],
security_token=sf_config["security_token"],
instance_url=sf_config["instance_url"],
client_id=sf_config["client_id"],
)
logger.info("Salesforce client initialized successfully")
@on_shutdown
def cleanup_salesforce_client() -> None:
"""Clean up Salesforce client when server stops."""
global sf_client
if sf_client:
# Salesforce client doesn't need explicit cleanup, but we'll clear the reference
sf_client = None
logger.info("Salesforce client cleaned up")
def retry_on_session_expiration(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator that automatically retries functions on session expiration.
This only retries on SalesforceExpiredSession, not SalesforceAuthenticationFailed.
Authentication failures (wrong credentials) should not be retried.
Retries up to 2 times on session expiration (3 total attempts).
Thread-safe: setup_salesforce_client() handles concurrent access internally.
Usage:
@retry_on_session_expiration
def my_salesforce_function():
# Function that might fail due to session expiration
pass
"""
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
max_retries = 2 # Hardcoded: 2 retries = 3 total attempts
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except SalesforceExpiredSession as e:
if attempt < max_retries:
logger.warning(
f"Session expired on attempt {attempt + 1} in {func.__name__}: {e}"
)
logger.info(
f"Retrying after re-initializing client (attempt {attempt + 2}/{max_retries + 1})"
)
try:
setup_salesforce_client() # Thread-safe internally
time.sleep(0.1) # Small delay to avoid immediate retry
except Exception as setup_error:
logger.error(f"Failed to re-initialize Salesforce client: {setup_error}")
raise setup_error # Raise the setup error, not the original session error
else:
# Last attempt failed, re-raise the session expiration error
raise e
return wrapper
def _get_salesforce_client() -> Salesforce:
"""Get the global Salesforce client."""
if sf_client is None:
raise RuntimeError(
"Salesforce client not initialized. Make sure the server is started properly."
)
return sf_client
@retry_on_session_expiration
def soql(query: str) -> List[Dict[str, Any]]:
"""Execute an SOQL query against Salesforce.
Args:
query: The SOQL query to execute
Returns:
List of records returned by the query, with 'attributes' field removed
Example:
>>> soql("SELECT Id, Name FROM Account")
"""
logger.info("Executing SOQL query: %s", query)
sf = _get_salesforce_client()
result = sf.query(query)
# Remove 'attributes' field from each record for cleaner output
if "records" not in result:
raise ValueError(f"Unexpected SOQL response format: missing 'records' field in {result}")
records = result["records"]
return [{k: v for k, v in record.items() if k != "attributes"} for record in records]
@retry_on_session_expiration
def sosl(query: str) -> List[Dict[str, Any]]:
"""Execute a SOSL query against Salesforce.
Args:
query: The SOSL query to execute
Returns:
List of search results from searchRecords
Example:
>>> sosl("FIND {Acme} IN ALL FIELDS RETURNING Account(Name), Contact(FirstName,LastName)")
"""
logger.info("Executing SOSL query: %s", query)
sf = _get_salesforce_client()
result = sf.search(query)
# Return the searchRecords directly as a list
if "searchRecords" not in result:
raise ValueError(
f"Unexpected SOSL response format: missing 'searchRecords' field in {result}"
)
search_records: List[Dict[str, Any]] = result["searchRecords"]
return search_records
@retry_on_session_expiration
def search(search_term: str) -> List[Dict[str, Any]]:
"""Search across all Salesforce objects using a simple search term.
Args:
search_term: The term to search for
Returns:
List of search results
Example:
>>> search("Acme") # Searches for "Acme" across all objects
"""
logger.info("Searching for term: %s", search_term)
# Build a SOSL query that searches across common objects
sosl_query = f"FIND {{{search_term}}} IN ALL FIELDS RETURNING Account(Name, Phone, BillingCity), Contact(FirstName, LastName, Email), Lead(FirstName, LastName, Company), Opportunity(Name, Amount, StageName)"
result: List[Dict[str, Any]] = sosl(sosl_query)
return result
@retry_on_session_expiration
def list_sobjects(filter: Optional[str] = None) -> List[str]:
"""List all available Salesforce objects (sObjects) in the org.
Args:
filter: Optional fuzzy filter to match object names (case-insensitive substring search).
Examples: "account", "__c" for custom objects, "contact", etc.
Returns:
list: List of Salesforce object names as strings
"""
sf = _get_salesforce_client()
describe_result = sf.describe()
if not describe_result:
raise ValueError("Salesforce describe() returned empty result")
if "sobjects" not in describe_result:
raise ValueError(
f"Unexpected describe response format: missing 'sobjects' field in {describe_result}"
)
sobjects = describe_result["sobjects"]
object_names = []
for obj in sobjects:
if not isinstance(obj, dict):
raise ValueError(f"Unexpected sobject format: expected dict, got {type(obj)}: {obj}")
if "name" not in obj:
raise ValueError(f"Sobject missing 'name' field: {obj}")
object_names.append(obj["name"])
if filter is not None and filter.strip():
filter_lower = filter.lower()
object_names = [name for name in object_names if filter_lower in name.lower()]
object_names.sort()
return object_names
@retry_on_session_expiration
def describe_sobject(sobject_name: str) -> Dict[str, Any]:
"""Get the description of a Salesforce object type.
Args:
sobject_name: The name of the Salesforce object type
Returns:
Dictionary containing the object's field descriptions
Example:
>>> describe_sobject("Account")
"""
logger.info("Describing Salesforce object: %s", sobject_name)
sf = _get_salesforce_client()
# Try to get the object - catch this specifically for "object doesn't exist"
try:
sobject = getattr(sf, sobject_name)
except AttributeError:
raise ValueError(f"Salesforce object '{sobject_name}' does not exist")
# Let API errors from describe() propagate naturally with their original messages
describe_result = sobject.describe()
if not describe_result:
raise ValueError(f"Salesforce object '{sobject_name}' describe() returned empty result")
if "fields" not in describe_result:
raise ValueError(
f"Unexpected describe response format for '{sobject_name}': missing 'fields' field in {describe_result}"
)
# Process fields into the required format
fields_info = {}
for field in describe_result["fields"]:
if not isinstance(field, dict):
raise ValueError(
f"Unexpected field format in '{sobject_name}': expected dict, got {type(field)}: {field}"
)
required_fields = ["name", "type", "label"]
for required_field in required_fields:
if required_field not in field:
raise ValueError(f"Field missing '{required_field}' in '{sobject_name}': {field}")
field_name = field["name"]
field_info = {"type": field["type"], "label": field["label"]}
# Add referenceTo information for reference fields
if field["type"] == "reference" and field.get("referenceTo"):
field_info["referenceTo"] = field["referenceTo"]
fields_info[field_name] = field_info
return fields_info
@retry_on_session_expiration
def get_sobject(sobject_name: str, record_id: str) -> Dict[str, Any]:
"""Get a specific Salesforce object by its ID.
Args:
sobject_name: The name of the Salesforce object type
record_id: The Salesforce ID of the object
Returns:
Dictionary containing the object's field values
Example:
>>> get_sobject("Account", "001xx000003DIloAAG")
"""
logger.info("Getting Salesforce object: %s with ID: %s", sobject_name, record_id)
sf = _get_salesforce_client()
# Try to get the object - catch this specifically for "object doesn't exist"
try:
sobject = getattr(sf, sobject_name)
except AttributeError:
raise ValueError(f"Salesforce object '{sobject_name}' does not exist")
result = sobject.get(record_id)
# Remove 'attributes' field for consistency with other functions
if isinstance(result, dict) and "attributes" in result:
cleaned_result: Dict[str, Any] = {k: v for k, v in result.items() if k != "attributes"}
return cleaned_result
return dict(result) if result else {}

View File

@@ -0,0 +1,27 @@
mxcp: 1
tool:
name: "describe_sobject"
description: "Use this tool to get detailed information about a specific Salesforce object's structure. This is crucial for understanding what fields are available, their types, and their properties. The tool returns comprehensive metadata including field types, picklist values, required fields, and field-level security settings. Use this before querying an object to ensure you're using valid field names and to understand the data types. For example, use this to discover all available fields on an Account or to find the valid values for a picklist field."
language: python
source:
file: ../python/salesforce_endpoints.py
parameters:
- name: sobject_name
type: string
description: "The API name of the Salesforce object to describe (e.g., 'Account', 'Contact', 'CustomObject__c'). The name is case-sensitive and must match exactly how it appears in Salesforce. You can use list_sobjects() to see all available object names."
examples:
- "Account"
- "Contact"
- "Opportunity"
- "CustomObject__c"
return:
type: object
annotations:
readOnlyHint: true
tests:
- name: "Describe Account object"
description: "Verify Account object description contains expected fields"
arguments:
- key: sobject_name
value: "Account"

View File

@@ -0,0 +1,34 @@
mxcp: 1
tool:
name: "get_sobject"
description: "Use this tool when you have a specific Salesforce record ID and need to retrieve all fields for that record. This is ideal for getting complete details about a known record, like when you have an Account ID from a previous query and need all its information. The tool requires both the object type (e.g., 'Account', 'Contact') and the record's unique ID. This is different from search or SOQL queries which find records based on field values - this tool is for direct record lookup by ID."
language: python
source:
file: ../python/salesforce_endpoints.py
parameters:
- name: sobject_name
type: string
description: "The API name of the Salesforce object (e.g., 'Account', 'Contact', 'CustomObject__c'). The name is case-sensitive and must match exactly how it appears in Salesforce. You can use list_sobjects() to see all available object names."
examples:
- "Account"
- "Contact"
- "Opportunity"
- name: record_id
type: string
description: "The unique identifier (ID) of the record to retrieve. This is the 15 or 18 character ID assigned by Salesforce when the record is created. You can get these IDs from other queries or from the Salesforce UI."
examples:
- "001xx000003DIloAAG"
- "003xx000004TmiAAE"
return:
type: object
annotations:
readOnlyHint: true
tests:
- name: "Get Account record structure"
description: "Verify Account record has expected fields like Id, Name"
arguments:
- key: sobject_name
value: "Account"
- key: record_id
value: "001Qy00000pxRDKIA2"

View File

@@ -0,0 +1,38 @@
mxcp: 1
tool:
name: "list_sobjects"
description: "Use this tool to get a list of all Salesforce object names available in your org. This is essential for exploring your Salesforce instance and understanding what data you can access. The tool returns a simple list of object names (e.g., ['Account', 'Contact', 'Opportunity']). Use this before using other tools to ensure you're using valid object names. For example, use this to find custom objects in your org or to verify the exact spelling of standard objects. If you need detailed information about a specific object's structure, use the describe_sobject tool instead."
language: python
source:
file: ../python/salesforce_endpoints.py
parameters:
- name: filter
type: string
description: "Optional fuzzy filter to match object names (case-insensitive substring search). Examples: 'account', '__c' for custom objects, 'contact', etc. If not provided, all objects are returned."
examples:
- "account"
- "__c"
- "contact"
default: null
return:
type: array
items:
type: string
annotations:
readOnlyHint: true
tests:
- name: "Contains standard objects"
description: "Verify standard Salesforce objects are present"
arguments: []
result_contains_all:
- "Account"
- "Contact"
- "Opportunity"
- name: "filter"
description: "Verify the filter is applied"
arguments:
- key: filter
value: "count"
result_contains_all:
- "Account"

View File

@@ -0,0 +1,28 @@
mxcp: 1
tool:
name: "search"
description: "Use this tool when you want to quickly search for records across multiple Salesforce objects without knowing the exact field names. It's perfect for finding records by company names, people names, or locations. The search is case-insensitive and will match partial words. For example, searching for 'Acme' will find 'Acme Corp', 'Acme Inc', etc. across Account, Contact, Lead, and Opportunity records. If you need more control over which fields to search or which objects to include, use the sosl tool instead."
language: python
source:
file: ../python/salesforce_endpoints.py
parameters:
- name: search_term
type: string
description: "The term to search for. This will be matched against all searchable fields in the specified objects. For example, searching for 'Acme' will find records containing 'Acme', 'acme', 'ACME', etc."
examples:
- "Acme"
- "John Smith"
- "New York"
return:
type: array
items:
type: object
annotations:
readOnlyHint: true
tests:
- name: "Basic search"
description: "Verify search returns array of results"
arguments:
- key: search_term
value: "Test"

View File

@@ -0,0 +1,43 @@
mxcp: 1
tool:
name: "soql"
description: "Use this tool when you need to query specific fields from a single Salesforce object, similar to SQL. It's ideal for getting structured data like 'all accounts in New York' or 'all contacts for a specific account'. SOQL is more precise than search or SOSL because you specify exactly which fields you want. For example, use this when you need to get a list of accounts with their phone numbers and addresses, or when you need to find all opportunities over a certain amount. If you need to search across multiple objects or don't know the exact field names, use the search or sosl tools instead."
language: python
source:
file: ../python/salesforce_endpoints.py
parameters:
- name: query
type: string
description: "The SOQL query to execute. The query should follow SOQL syntax: SELECT field1, field2 FROM object_type [WHERE conditions] [ORDER BY field] [LIMIT n]. For example: 'SELECT Id, Name, Phone FROM Account WHERE BillingCity = 'New York' LIMIT 10'"
examples:
- "SELECT Id, Name FROM Account"
- "SELECT Id, Name, Email FROM Contact WHERE AccountId = '001xx000003DIloAAG'"
- "SELECT Id, Name, Amount, StageName FROM Opportunity WHERE IsWon = true ORDER BY Amount DESC LIMIT 5"
return:
type: array
items:
type: object
annotations:
readOnlyHint: true
tests:
- name: "Basic Account query"
description: "Verify SOQL query returns array of Account records"
arguments:
- key: query
value: "SELECT Id, Name FROM Account LIMIT 1"
- name: "Query with LIMIT"
description: "Verify SOQL query respects LIMIT clause"
arguments:
- key: query
value: "SELECT Id FROM Account LIMIT 2"
- name: "Contact query structure"
description: "Verify Contact query returns expected fields"
arguments:
- key: query
value: "SELECT Id, FirstName, LastName FROM Contact LIMIT 1"
- name: "Results should not contain attributes"
description: "Verify attributes field is filtered out from results"
arguments:
- key: query
value: "SELECT Id, Name FROM Account LIMIT 1"

View File

@@ -0,0 +1,43 @@
mxcp: 1
tool:
name: "sosl"
description: "Use this tool when you need advanced search capabilities across multiple Salesforce objects. It's perfect for complex search scenarios like 'find all records containing this text in any field' or 'search only in name fields across accounts and contacts'. SOSL gives you fine-grained control over which objects to search and which fields to return. For example, use this when you need to find all records mentioning a specific product across accounts, contacts, and opportunities. If you just need a simple search, use the search tool instead. If you need to query specific fields from a single object, use the soql tool instead."
language: python
source:
file: ../python/salesforce_endpoints.py
parameters:
- name: query
type: string
description: "The SOSL query to execute. The query should follow SOSL syntax: FIND {search_term} IN ALL FIELDS RETURNING object_type(field1, field2, ...). For example: 'FIND {Acme} IN ALL FIELDS RETURNING Account(Name, Phone), Contact(FirstName, LastName)'"
examples:
- "FIND {Acme} IN ALL FIELDS RETURNING Account(Name, Phone)"
- "FIND {John} IN NAME FIELDS RETURNING Contact(FirstName, LastName, Email)"
- "FIND {New York} IN ALL FIELDS RETURNING Account(Name, BillingCity), Lead(Company, City)"
return:
type: array
items:
type: object
annotations:
readOnlyHint: true
tests:
- name: "Basic SOSL search"
description: "Verify SOSL search returns array of search results"
arguments:
- key: query
value: "FIND {Test} IN ALL FIELDS RETURNING Account(Name)"
- name: "Multi-object search"
description: "Verify SOSL can search across multiple objects"
arguments:
- key: query
value: "FIND {Test} IN ALL FIELDS RETURNING Account(Name), Contact(FirstName, LastName)"
- name: "Name field search"
description: "Verify SOSL can search in specific fields"
arguments:
- key: query
value: "FIND {Test} IN NAME FIELDS RETURNING Account(Name)"
- name: "Search with specific fields"
description: "Verify SOSL returns specified fields"
arguments:
- key: query
value: "FIND {Test} IN ALL FIELDS RETURNING Account(Name, Phone)"