Initial commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# Salesforce OAuth Demo
|
||||
|
||||
This example demonstrates how to create MCP tools that interact with Salesforce using the MXCP OAuth authentication system with the `simple_salesforce` library.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### 1. MXCP OAuth Authentication
|
||||
- Project-wide Salesforce OAuth configuration
|
||||
- Automatic token management through MXCP authentication system
|
||||
- User authentication via standard OAuth 2.0 flow
|
||||
- Error handling for authentication failures
|
||||
|
||||
### 2. Salesforce API Integration
|
||||
- `list_sobjects` - Retrieve all available Salesforce objects (sObjects) from your org with optional filtering
|
||||
- `describe_sobject` - Get detailed metadata for a specific Salesforce object, including field information
|
||||
- `get_sobject` - Retrieve a specific Salesforce record by its ID
|
||||
- `search` - Search across all searchable Salesforce objects using simple search terms
|
||||
- `soql` - Execute SOQL (Salesforce Object Query Language) queries
|
||||
- `sosl` - Execute SOSL (Salesforce Object Search Language) queries for complex searches
|
||||
- `whoami` - Display information about the current authenticated Salesforce user
|
||||
- Token-based API access using authenticated user context
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Salesforce Org**: You need access to a Salesforce org (Developer Edition is fine)
|
||||
2. **Salesforce Connected App**: Create a Connected App in Salesforce with OAuth settings
|
||||
3. **Python Dependencies**: The `simple_salesforce` library (automatically managed by MXCP)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create Salesforce Connected App
|
||||
|
||||
1. Log into your Salesforce org
|
||||
2. Go to **Setup** → **App Manager** → **New Connected App**
|
||||
3. Fill in basic information:
|
||||
- **Connected App Name**: "MXCP Integration" (or your preferred name)
|
||||
- **API Name**: Will auto-populate
|
||||
- **Contact Email**: Your email
|
||||
4. Enable OAuth Settings:
|
||||
- **Enable OAuth Settings**: Check this box
|
||||
- **Callback URL**: This depends on your deployment:
|
||||
- **Local Development**: `http://localhost:8000/salesforce/callback`
|
||||
- **Remote/Production**: `https://your-domain.com/salesforce/callback` (replace with your actual server URL)
|
||||
- **Selected OAuth Scopes**: Add these scopes:
|
||||
- Access and manage your data (api)
|
||||
- Perform requests on your behalf at any time (refresh_token, offline_access)
|
||||
- Access your basic information (id, profile, email, address, phone)
|
||||
5. Save the Connected App
|
||||
6. Note down the **Consumer Key** (Client ID) and **Consumer Secret** (Client Secret)
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Set your Salesforce OAuth credentials:
|
||||
```bash
|
||||
export SALESFORCE_CLIENT_ID="your-consumer-key-from-connected-app"
|
||||
export SALESFORCE_CLIENT_SECRET="your-consumer-secret-from-connected-app"
|
||||
```
|
||||
|
||||
### 3. Configure Callback URL for Your Deployment
|
||||
|
||||
The callback URL configuration depends on where your MXCP server will run:
|
||||
|
||||
#### Local Development
|
||||
For local development, the default configuration in `config.yml` uses `http://localhost:8000/salesforce/callback`. This works when:
|
||||
- You're running MXCP locally on your development machine
|
||||
- Users authenticate from the same machine where MXCP is running
|
||||
|
||||
#### Remote/Production Deployment
|
||||
For remote servers or production deployments, you need to:
|
||||
|
||||
1. **Update config.yml**: Uncomment and modify the production callback URL:
|
||||
```yaml
|
||||
redirect_uris:
|
||||
- "http://localhost:8000/salesforce/callback" # Keep for local dev
|
||||
- "https://your-domain.com/salesforce/callback" # Add your actual URL
|
||||
```
|
||||
|
||||
2. **Update base_url**: Set the correct base URL in your config:
|
||||
```yaml
|
||||
transport:
|
||||
http:
|
||||
base_url: https://your-domain.com # Your actual server URL
|
||||
```
|
||||
|
||||
3. **Configure Connected App**: Add the production callback URL to your Salesforce Connected App's callback URLs
|
||||
|
||||
**Important**:
|
||||
- The callback URL must be accessible from the user's browser, not just from your server
|
||||
- For production deployments, Salesforce requires HTTPS for callback URLs
|
||||
- You can configure multiple callback URLs in your Connected App to support both local development and production
|
||||
|
||||
## Authenticate with Salesforce
|
||||
|
||||
When you first run MXCP, you'll need to authenticate with Salesforce:
|
||||
|
||||
```bash
|
||||
# Start the MXCP server with the config file - this will prompt for authentication
|
||||
MXCP_CONFIG=config.yml mxcp serve
|
||||
```
|
||||
|
||||
The authentication flow will:
|
||||
1. Open your browser to Salesforce login
|
||||
2. You'll log in with your Salesforce credentials
|
||||
3. Authorize the MXCP application
|
||||
4. Redirect back to complete authentication
|
||||
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
salesforce-oauth/
|
||||
├── mxcp-site.yml # Project metadata
|
||||
├── config.yml # Server and authentication configuration
|
||||
├── python/ # Python modules
|
||||
│ └── salesforce_client.py # Salesforce API implementations
|
||||
├── tools/ # Tool definitions
|
||||
│ ├── list_sobjects.yml # List all Salesforce objects
|
||||
│ ├── describe_sobject.yml # Get object metadata
|
||||
│ ├── get_sobject.yml # Get record by ID
|
||||
│ ├── search.yml # Search across objects
|
||||
│ ├── soql.yml # Execute SOQL queries
|
||||
│ ├── sosl.yml # Execute SOSL queries
|
||||
│ └── whoami.yml # Current user information
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
1. **MXCP OAuth Integration**: Uses MXCP's built-in Salesforce OAuth provider for secure authentication
|
||||
2. **User Context**: Access tokens are automatically managed and provided through `user_context()`
|
||||
3. **Token-based Authentication**: simple_salesforce is initialized with OAuth tokens instead of credentials
|
||||
4. **Project-wide Configuration**: Authentication is configured at the project level in `mxcp-site.yml`
|
||||
5. **Error Handling**: Comprehensive error handling for authentication and API failures
|
||||
6. **API Integration**: Demonstrates calling Salesforce REST API endpoints with proper OAuth tokens
|
||||
|
||||
## Example Output
|
||||
|
||||
When you run `list_sobjects`, you'll get a response like:
|
||||
|
||||
```json
|
||||
[
|
||||
"Account",
|
||||
"Contact",
|
||||
"Lead",
|
||||
"Opportunity",
|
||||
"Case",
|
||||
"Product2",
|
||||
"Task",
|
||||
"Event",
|
||||
"User",
|
||||
"CustomObject__c",
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Errors
|
||||
- **"No user context available"**: User needs to authenticate first by running `mxcp serve` and completing OAuth flow
|
||||
- **"No Salesforce access token found"**: Authentication was incomplete or token expired - re-authenticate
|
||||
- **Connected App Issues**: Verify your `SALESFORCE_CLIENT_ID` and `SALESFORCE_CLIENT_SECRET` are correct
|
||||
- **Callback URL Mismatch**: Ensure the callback URL in your Connected App matches where your MXCP server is accessible:
|
||||
- Local development: `http://localhost:8000/salesforce/callback`
|
||||
- Remote/production: `https://your-domain.com/salesforce/callback`
|
||||
- **OAuth Scopes**: Verify your Connected App has the required OAuth scopes (api, refresh_token, id, profile, email)
|
||||
|
||||
### API Errors
|
||||
- Verify you have the necessary permissions in Salesforce
|
||||
- Check that your org is accessible and not in maintenance mode
|
||||
- Ensure your Connected App is approved and not restricted by IP ranges
|
||||
|
||||
### Connected App Setup Issues
|
||||
- **App Not Found**: Make sure your Connected App is saved and the Consumer Key/Secret are copied correctly
|
||||
- **Callback URL**: The callback URL must exactly match your MXCP server's accessible address:
|
||||
- For local development: `http://localhost:8000/salesforce/callback`
|
||||
- For remote deployment: `https://your-domain.com/salesforce/callback`
|
||||
- **OAuth Scopes**: Missing scopes will cause authentication to fail - ensure all required scopes are selected
|
||||
|
||||
## Next Steps
|
||||
|
||||
This example demonstrates a comprehensive set of Salesforce integration tools. You could extend it with additional tools for data manipulation like:
|
||||
- `create_record` - Create new records in Salesforce objects
|
||||
- `update_record` - Update existing records
|
||||
- `delete_record` - Delete records
|
||||
- `bulk_operations` - Handle bulk data operations for large datasets
|
||||
@@ -0,0 +1,22 @@
|
||||
mxcp: 1
|
||||
transport:
|
||||
http:
|
||||
port: 8000
|
||||
host: 0.0.0.0
|
||||
# Set base_url to your server's public URL for production
|
||||
base_url: http://localhost:8000
|
||||
|
||||
projects:
|
||||
salesforce-oauth:
|
||||
profiles:
|
||||
default:
|
||||
# OAuth Authentication Configuration
|
||||
auth:
|
||||
provider: salesforce
|
||||
salesforce:
|
||||
client_id: "${SALESFORCE_CLIENT_ID}"
|
||||
client_secret: "${SALESFORCE_CLIENT_SECRET}"
|
||||
scope: "api refresh_token openid profile email"
|
||||
callback_path: "/salesforce/callback"
|
||||
auth_url: "https://login.salesforce.com/services/oauth2/authorize"
|
||||
token_url: "https://login.salesforce.com/services/oauth2/token"
|
||||
@@ -0,0 +1,3 @@
|
||||
mxcp: 1
|
||||
project: salesforce-oauth
|
||||
profile: default
|
||||
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
Salesforce MCP tools using simple_salesforce with MXCP OAuth authentication.
|
||||
"""
|
||||
|
||||
import threading
|
||||
from functools import wraps
|
||||
from typing import Dict, Any, List, Optional
|
||||
from mxcp.sdk.auth.context import get_user_context
|
||||
from simple_salesforce import Salesforce # type: ignore[attr-defined]
|
||||
from simple_salesforce.exceptions import SalesforceExpiredSession
|
||||
from mxcp.runtime import on_init, on_shutdown
|
||||
import logging
|
||||
|
||||
# Thread-safe cache for Salesforce clients
|
||||
_client_cache: Optional[Dict[str, Salesforce]] = None
|
||||
_cache_lock: Optional[threading.Lock] = None
|
||||
|
||||
|
||||
@on_init
|
||||
def init_client_cache() -> None:
|
||||
"""
|
||||
Initialize the Salesforce client cache.
|
||||
"""
|
||||
global _client_cache, _cache_lock
|
||||
_client_cache = {}
|
||||
_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
@on_shutdown
|
||||
def clear_client_cache() -> None:
|
||||
"""
|
||||
Clear the Salesforce client cache.
|
||||
"""
|
||||
global _client_cache, _cache_lock
|
||||
_client_cache = None
|
||||
_cache_lock = None
|
||||
|
||||
|
||||
def _get_cache_key(context: Any) -> Optional[str]:
|
||||
"""Generate a cache key based on user context."""
|
||||
if not context:
|
||||
return None
|
||||
|
||||
# Use user ID and instance URL as cache key
|
||||
user_id = getattr(context, "user_id", None) or getattr(context, "id", None)
|
||||
|
||||
# Extract instance URL
|
||||
instance_url = None
|
||||
if context.raw_profile and "urls" in context.raw_profile:
|
||||
urls = context.raw_profile["urls"]
|
||||
instance_url = urls.get("custom_domain")
|
||||
if not instance_url:
|
||||
for url_key in ["rest", "enterprise", "partner"]:
|
||||
if url_key in urls:
|
||||
service_url = urls[url_key]
|
||||
instance_url = service_url.split("/services/")[0]
|
||||
break
|
||||
|
||||
if user_id and instance_url:
|
||||
return f"{user_id}:{instance_url}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def with_session_retry(func: Any) -> Any:
|
||||
"""
|
||||
Decorator that automatically retries API calls with cache invalidation when sessions expire.
|
||||
|
||||
This handles the race condition where a session might expire between cache validation
|
||||
and the actual API call.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except SalesforceExpiredSession:
|
||||
logging.error("Salesforce session expired")
|
||||
# Session expired during the call - invalidate cache and retry once
|
||||
context = get_user_context()
|
||||
cache_key = _get_cache_key(context)
|
||||
if cache_key and _cache_lock and _client_cache:
|
||||
with _cache_lock:
|
||||
# Remove the expired client from cache
|
||||
_client_cache.pop(cache_key, None)
|
||||
|
||||
# Retry the function call - this will get a fresh client
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _escape_sosl_search_term(search_term: str) -> str:
|
||||
"""
|
||||
Escape special characters in SOSL search terms to prevent injection attacks.
|
||||
|
||||
SOSL special characters that need escaping: & | ! { } [ ] ( ) ^ ~ * ? : " ' + -
|
||||
"""
|
||||
# Escape backslashes first to avoid double-escaping
|
||||
escaped = search_term.replace("\\", "\\\\")
|
||||
|
||||
# Escape SOSL special characters
|
||||
special_chars = [
|
||||
"&",
|
||||
"|",
|
||||
"!",
|
||||
"{",
|
||||
"}",
|
||||
"[",
|
||||
"]",
|
||||
"(",
|
||||
")",
|
||||
"^",
|
||||
"~",
|
||||
"*",
|
||||
"?",
|
||||
":",
|
||||
'"',
|
||||
"'",
|
||||
"+",
|
||||
"-",
|
||||
]
|
||||
for char in special_chars:
|
||||
escaped = escaped.replace(char, f"\\{char}")
|
||||
|
||||
return escaped
|
||||
|
||||
|
||||
def _get_salesforce_client() -> Salesforce:
|
||||
"""
|
||||
Create and return an authenticated Salesforce client using OAuth tokens from user_context.
|
||||
|
||||
Uses caching to avoid recreating clients unnecessarily. Clients are cached per user
|
||||
and instance URL combination in a thread-safe manner.
|
||||
"""
|
||||
try:
|
||||
# Get the authenticated user's context
|
||||
context = get_user_context()
|
||||
|
||||
if not context:
|
||||
raise ValueError("No user context available. User must be authenticated.")
|
||||
|
||||
# Generate cache key
|
||||
cache_key = _get_cache_key(context)
|
||||
|
||||
# Try to get cached client first
|
||||
if cache_key and _cache_lock and _client_cache:
|
||||
with _cache_lock:
|
||||
if cache_key in _client_cache:
|
||||
logging.info("Using cached Salesforce client")
|
||||
# Return cached client - retry decorator will handle any session expiry
|
||||
return _client_cache[cache_key]
|
||||
|
||||
logging.info("No cached Salesforce client found, creating new one")
|
||||
# Extract Salesforce OAuth tokens from user context
|
||||
access_token = context.external_token
|
||||
|
||||
# Extract instance URL from user context (this is user/org-specific)
|
||||
instance_url = None
|
||||
if context.raw_profile and "urls" in context.raw_profile:
|
||||
urls = context.raw_profile["urls"]
|
||||
# Try custom_domain first (this is the full instance URL)
|
||||
instance_url = urls.get("custom_domain")
|
||||
if not instance_url:
|
||||
# Fallback: extract base URL from any service endpoint
|
||||
for url_key in ["rest", "enterprise", "partner"]:
|
||||
if url_key in urls:
|
||||
service_url = urls[url_key]
|
||||
instance_url = service_url.split("/services/")[0]
|
||||
break
|
||||
|
||||
if not access_token:
|
||||
raise ValueError(
|
||||
"No Salesforce access token found in user context. "
|
||||
"User must authenticate with Salesforce through MXCP."
|
||||
)
|
||||
|
||||
if not instance_url:
|
||||
raise ValueError(
|
||||
"No Salesforce instance URL found in user context. "
|
||||
"Authentication may be incomplete or profile missing URL information."
|
||||
)
|
||||
|
||||
# Initialize Salesforce client with OAuth token
|
||||
sf = Salesforce(session_id=access_token, instance_url=instance_url)
|
||||
|
||||
# Cache the client if we have a valid cache key
|
||||
if cache_key and _cache_lock and _client_cache:
|
||||
with _cache_lock:
|
||||
_client_cache[cache_key] = sf
|
||||
|
||||
return sf
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to authenticate with Salesforce: {str(e)}")
|
||||
|
||||
|
||||
@with_session_retry
|
||||
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
|
||||
"""
|
||||
try:
|
||||
sf = _get_salesforce_client()
|
||||
|
||||
# Get all sObjects metadata
|
||||
describe_result = sf.describe()
|
||||
|
||||
if not describe_result or "sobjects" not in describe_result:
|
||||
raise ValueError("Invalid describe response from Salesforce API")
|
||||
|
||||
# Extract just the object names
|
||||
sobjects = describe_result["sobjects"]
|
||||
object_names = []
|
||||
for obj in sobjects:
|
||||
if not isinstance(obj, dict) or "name" not in obj:
|
||||
raise ValueError(f"Invalid sobject format: {obj}")
|
||||
object_names.append(obj["name"])
|
||||
|
||||
# Apply fuzzy filter if provided
|
||||
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()]
|
||||
|
||||
# Sort alphabetically for consistent output
|
||||
object_names.sort()
|
||||
|
||||
return object_names
|
||||
|
||||
except Exception as e:
|
||||
# Return error in a format that can be handled by the caller
|
||||
raise Exception(f"Error listing Salesforce objects: {str(e)}")
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def describe_sobject(object_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get detailed field information for a specific Salesforce object (sObject).
|
||||
|
||||
Args:
|
||||
object_name: The API name of the Salesforce object to describe
|
||||
|
||||
Returns:
|
||||
dict: Dictionary where each key is a field name and each value contains field metadata
|
||||
"""
|
||||
sf = _get_salesforce_client()
|
||||
|
||||
# Try to get the object - catch this specifically for "object doesn't exist"
|
||||
try:
|
||||
sobject = getattr(sf, object_name)
|
||||
except AttributeError:
|
||||
raise Exception(f"Salesforce object '{object_name}' does not exist")
|
||||
|
||||
# Let API errors from describe() propagate naturally with their original messages
|
||||
describe_result = sobject.describe()
|
||||
|
||||
if not describe_result or "fields" not in describe_result:
|
||||
raise ValueError(f"Invalid describe response for object '{object_name}'")
|
||||
|
||||
# Process fields into the required format
|
||||
fields_info = {}
|
||||
for field in describe_result["fields"]:
|
||||
if not isinstance(field, dict):
|
||||
raise ValueError(f"Invalid field format in '{object_name}': {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 '{object_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
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def get_sobject(object_name: str, record_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve a specific Salesforce record by its object type and ID.
|
||||
|
||||
Args:
|
||||
object_name: The API name of the Salesforce object type
|
||||
record_id: The unique Salesforce ID of the record to retrieve
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing all fields and values for the specified record
|
||||
"""
|
||||
sf = _get_salesforce_client()
|
||||
|
||||
# Try to get the object - catch this specifically for "object doesn't exist"
|
||||
try:
|
||||
sobject = getattr(sf, object_name)
|
||||
except AttributeError:
|
||||
raise Exception(f"Salesforce object '{object_name}' does not exist")
|
||||
|
||||
# Let API errors from get() propagate naturally with their original messages
|
||||
record = sobject.get(record_id)
|
||||
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"Invalid record format returned for {object_name}:{record_id}")
|
||||
|
||||
# Remove 'attributes' field for consistency with other functions
|
||||
clean_record: Dict[str, Any] = {k: v for k, v in record.items() if k != "attributes"}
|
||||
return clean_record
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def soql(query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Execute an arbitrary SOQL (Salesforce Object Query Language) query.
|
||||
|
||||
Args:
|
||||
query: The SOQL query to execute
|
||||
|
||||
Returns:
|
||||
list: Array of records returned by the SOQL query
|
||||
"""
|
||||
sf = _get_salesforce_client()
|
||||
|
||||
# Execute the SOQL query
|
||||
result = sf.query(query)
|
||||
|
||||
if not result or "records" not in result:
|
||||
raise ValueError("Invalid SOQL query response from Salesforce API")
|
||||
|
||||
# Remove 'attributes' field from each record for cleaner output
|
||||
records = []
|
||||
for record in result["records"]:
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"Invalid record format in SOQL result: {record}")
|
||||
clean_record = {k: v for k, v in record.items() if k != "attributes"}
|
||||
records.append(clean_record)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def search(search_term: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search for records across all searchable Salesforce objects using a simple search term.
|
||||
Uses Salesforce's native search to automatically find matches across all objects.
|
||||
|
||||
Args:
|
||||
search_term: The term to search for across Salesforce objects
|
||||
|
||||
Returns:
|
||||
list: Array of matching records from various Salesforce objects
|
||||
"""
|
||||
sf = _get_salesforce_client()
|
||||
|
||||
# Escape the search term to prevent SOSL injection attacks
|
||||
escaped_search_term = _escape_sosl_search_term(search_term)
|
||||
|
||||
# Use simple SOSL syntax - Salesforce searches all searchable objects automatically
|
||||
sosl_query = f"FIND {{{escaped_search_term}}}"
|
||||
|
||||
# Execute the SOSL search
|
||||
search_results = sf.search(sosl_query)
|
||||
|
||||
if not search_results or "searchRecords" not in search_results:
|
||||
raise ValueError("Invalid SOSL search response from Salesforce API")
|
||||
|
||||
# Flatten results from all objects into a single array
|
||||
all_records = []
|
||||
for record in search_results["searchRecords"]:
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"Invalid record format in SOSL result: {record}")
|
||||
if "attributes" not in record or not isinstance(record["attributes"], dict):
|
||||
raise ValueError(f"Invalid record attributes in SOSL result: {record}")
|
||||
|
||||
# Remove 'attributes' field and add object type for context
|
||||
clean_record = {k: v for k, v in record.items() if k != "attributes"}
|
||||
clean_record["_ObjectType"] = record["attributes"]["type"]
|
||||
all_records.append(clean_record)
|
||||
|
||||
return all_records
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def sosl(query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Execute an arbitrary SOSL (Salesforce Object Search Language) query.
|
||||
|
||||
Args:
|
||||
query: The SOSL query to execute
|
||||
|
||||
Returns:
|
||||
list: Array of records returned by the SOSL search query
|
||||
"""
|
||||
sf = _get_salesforce_client()
|
||||
|
||||
# Execute the SOSL search
|
||||
search_results = sf.search(query)
|
||||
|
||||
if not search_results or "searchRecords" not in search_results:
|
||||
raise ValueError("Invalid SOSL query response from Salesforce API")
|
||||
|
||||
# Flatten results from all objects into a single array
|
||||
all_records = []
|
||||
for record in search_results["searchRecords"]:
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"Invalid record format in SOSL result: {record}")
|
||||
if "attributes" not in record or not isinstance(record["attributes"], dict):
|
||||
raise ValueError(f"Invalid record attributes in SOSL result: {record}")
|
||||
|
||||
# Remove 'attributes' field and add object type for context
|
||||
clean_record = {k: v for k, v in record.items() if k != "attributes"}
|
||||
clean_record["_ObjectType"] = record["attributes"]["type"]
|
||||
all_records.append(clean_record)
|
||||
|
||||
return all_records
|
||||
|
||||
|
||||
def whoami() -> Dict[str, Any]:
|
||||
"""
|
||||
Get basic information about the currently authenticated Salesforce user from the user context.
|
||||
|
||||
Returns essential user information from the MXCP authentication context without making API calls.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing essential current user information
|
||||
"""
|
||||
context = get_user_context()
|
||||
|
||||
if not context:
|
||||
raise ValueError("No user context available. User must be authenticated.")
|
||||
|
||||
# Extract instance URL from context
|
||||
instance_url = None
|
||||
if context.raw_profile and "urls" in context.raw_profile:
|
||||
urls = context.raw_profile["urls"]
|
||||
instance_url = urls.get("custom_domain")
|
||||
if not instance_url:
|
||||
# Fallback: extract base URL from any service endpoint
|
||||
for url_key in ["rest", "enterprise", "partner"]:
|
||||
if url_key in urls:
|
||||
service_url = urls[url_key]
|
||||
instance_url = service_url.split("/services/")[0]
|
||||
break
|
||||
|
||||
# Extract essential user information from raw profile
|
||||
raw_profile = context.raw_profile or {}
|
||||
|
||||
user_info = {
|
||||
"user_id": raw_profile.get("user_id"),
|
||||
"email": raw_profile.get("email"),
|
||||
"name": raw_profile.get("name"),
|
||||
"preferred_username": raw_profile.get("preferred_username"),
|
||||
"organization_id": raw_profile.get("organization_id"),
|
||||
"instanceUrl": instance_url,
|
||||
}
|
||||
|
||||
return user_info
|
||||
@@ -0,0 +1,34 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: describe_sobject
|
||||
description: |
|
||||
Get detailed metadata for a specific Salesforce object, including all field information.
|
||||
Returns field names, types, labels, and relationship details.
|
||||
tags: ["salesforce", "metadata", "schema"]
|
||||
annotations:
|
||||
title: "Describe Salesforce Object"
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: object_name
|
||||
type: string
|
||||
description: "Name of the Salesforce object to describe"
|
||||
examples: ["Account", "Contact", "Opportunity", "Lead", "Case"]
|
||||
return:
|
||||
type: object
|
||||
description: "Object metadata with field information"
|
||||
additionalProperties: true
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
tests:
|
||||
- name: "Describe Account object"
|
||||
description: "Test describing the standard Account object"
|
||||
arguments:
|
||||
- key: object_name
|
||||
value: "Account"
|
||||
result_contains:
|
||||
Name:
|
||||
type: "string"
|
||||
Id:
|
||||
type: "id"
|
||||
@@ -0,0 +1,37 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_sobject
|
||||
description: |
|
||||
Retrieve a specific Salesforce record by its ID.
|
||||
Returns the complete record data with all accessible fields.
|
||||
tags: ["salesforce", "data", "records"]
|
||||
annotations:
|
||||
title: "Get Salesforce Record"
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: object_name
|
||||
type: string
|
||||
description: "Name of the Salesforce object type"
|
||||
examples: ["Account", "Contact", "Opportunity", "Lead", "Case"]
|
||||
- name: record_id
|
||||
type: string
|
||||
description: "Salesforce record ID (15 or 18 character ID)"
|
||||
examples: ["001000000000001", "003000000000001AAA"]
|
||||
return:
|
||||
type: object
|
||||
description: "Complete record data"
|
||||
additionalProperties: true
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
tests:
|
||||
- name: "Get Account record"
|
||||
description: "Test retrieving an Account record by ID"
|
||||
arguments:
|
||||
- key: object_name
|
||||
value: "Account"
|
||||
- key: record_id
|
||||
value: "001000000000001"
|
||||
result_contains:
|
||||
Id: "001000000000001"
|
||||
@@ -0,0 +1,38 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: list_sobjects
|
||||
description: |
|
||||
List all available Salesforce objects (sObjects) in the organization.
|
||||
Optionally filter the list by providing a filter term for fuzzy matching on object names.
|
||||
tags: ["salesforce", "metadata", "objects"]
|
||||
annotations:
|
||||
title: "List Salesforce Objects"
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: filter
|
||||
type: string
|
||||
description: "Optional filter term to match against object names (case-insensitive fuzzy matching)"
|
||||
default: null
|
||||
examples: ["Account", "Contact", "Custom"]
|
||||
return:
|
||||
type: array
|
||||
description: "List of sObject names"
|
||||
items:
|
||||
type: string
|
||||
description: "Name of a Salesforce object"
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
tests:
|
||||
- name: "List all objects"
|
||||
description: "Test listing all available Salesforce objects"
|
||||
arguments: []
|
||||
result_contains_item: "Account"
|
||||
|
||||
- name: "Filter objects"
|
||||
description: "Test filtering objects by name"
|
||||
arguments:
|
||||
- key: filter
|
||||
value: "Account"
|
||||
result_contains_item: "Account"
|
||||
@@ -0,0 +1,38 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: search
|
||||
description: |
|
||||
Search across all searchable Salesforce objects using the native Salesforce search.
|
||||
This uses the simple SOSL syntax "FIND {search_term}" which automatically searches
|
||||
all searchable objects and fields.
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
parameters:
|
||||
- name: search_term
|
||||
type: string
|
||||
description: "Term to search for across all searchable objects"
|
||||
examples: ["John", "Acme", "555-1234", "example.com"]
|
||||
return:
|
||||
type: array
|
||||
description: "Search results from all matching objects"
|
||||
items:
|
||||
type: object
|
||||
description: "Search result record"
|
||||
additionalProperties: true
|
||||
tags:
|
||||
- salesforce
|
||||
- search
|
||||
- data
|
||||
annotations:
|
||||
title: "Search Salesforce Records"
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
tests:
|
||||
- name: "Basic search"
|
||||
description: "Test searching for a common term"
|
||||
arguments:
|
||||
- key: search_term
|
||||
value: "test"
|
||||
# Note: Using result type array since search results can be empty or contain records
|
||||
result: []
|
||||
@@ -0,0 +1,33 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: soql
|
||||
description: |
|
||||
Execute a SOQL (Salesforce Object Query Language) query. Returns query results as an array of records.
|
||||
For personalized queries (e.g., 'my tasks', 'my opportunities'), use the whoami tool first to get the current user's ID for filtering (e.g., WHERE OwnerId = 'user_id').
|
||||
tags: ["salesforce", "query", "data"]
|
||||
annotations:
|
||||
title: "Execute SOQL Query"
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: query
|
||||
type: string
|
||||
description: "SOQL query string to execute"
|
||||
examples: ["SELECT Id, Name FROM Account LIMIT 10", "SELECT Id, Email FROM Contact WHERE LastName = 'Smith'"]
|
||||
return:
|
||||
type: array
|
||||
description: "Query results"
|
||||
items:
|
||||
type: object
|
||||
description: "Record data"
|
||||
additionalProperties: true
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
tests:
|
||||
- name: "Simple Account query"
|
||||
description: "Test executing a basic SOQL query on Account object"
|
||||
arguments:
|
||||
- key: query
|
||||
value: "SELECT Id, Name FROM Account LIMIT 1"
|
||||
result_length: 1
|
||||
@@ -0,0 +1,36 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: sosl
|
||||
description: |
|
||||
Execute a raw SOSL (Salesforce Object Search Language) query.
|
||||
Allows complex search queries with specific object targeting and field selection.
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
parameters:
|
||||
- name: query
|
||||
type: string
|
||||
description: "SOSL query string to execute"
|
||||
examples: ["FIND {test} IN ALL FIELDS RETURNING Account(Id, Name)", "FIND {John} RETURNING Contact(Id, Name, Email)"]
|
||||
return:
|
||||
type: array
|
||||
description: "Search results"
|
||||
items:
|
||||
type: object
|
||||
description: "Search result record"
|
||||
additionalProperties: true
|
||||
tags:
|
||||
- salesforce
|
||||
- search
|
||||
- advanced
|
||||
annotations:
|
||||
title: "Execute SOSL Query"
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
tests:
|
||||
- name: "Simple SOSL query"
|
||||
description: "Test executing a basic SOSL search query"
|
||||
arguments:
|
||||
- key: query
|
||||
value: "FIND {test} IN ALL FIELDS RETURNING Account(Id, Name)"
|
||||
result: []
|
||||
@@ -0,0 +1,44 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: whoami
|
||||
title: Current User Information
|
||||
description: |
|
||||
Get the current authenticated user's information (user_id, email, name) from OAuth context.
|
||||
Use this tool before executing personalized SOQL queries to identify the user for filtering records by ownership or assignment.
|
||||
tags:
|
||||
- salesforce
|
||||
- user
|
||||
- auth
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters: []
|
||||
return:
|
||||
type: object
|
||||
description: Essential current user information from OAuth context
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
description: Salesforce user ID
|
||||
email:
|
||||
type: string
|
||||
description: User's email address
|
||||
name:
|
||||
type: string
|
||||
description: User's full name
|
||||
preferred_username:
|
||||
type: string
|
||||
description: User's preferred username
|
||||
organization_id:
|
||||
type: string
|
||||
description: Salesforce organization ID
|
||||
instanceUrl:
|
||||
type: string
|
||||
description: Salesforce instance URL for the authenticated user
|
||||
language: python
|
||||
source:
|
||||
file: ../python/salesforce_client.py
|
||||
tests:
|
||||
- name: whoami_basic
|
||||
description: Get current user information
|
||||
arguments: []
|
||||
Reference in New Issue
Block a user