Why These Four Systems Matter
Salesforce is the CRM backbone for most enterprise sales organisations. Jira is where engineering and product work lives. Slack is where real-time communication happens. HubSpot is the marketing automation and inbound CRM for growth teams. Together they touch nearly every customer-facing workflow in a modern enterprise. When Claude can read and write across all four via MCP, you unlock a class of AI agents that genuinely improve how enterprise teams work โ not just how they draft emails.
Before building any of these, review the foundational architecture covered in our MCP Enterprise Guide and, if you need to understand the Python implementation fundamentals, our MCP Python Tutorial. This article assumes you understand MCP server basics and focuses on what's unique to each system.
Salesforce MCP Server
Salesforce MCP
OAuth 2.0 ยท REST API ยท SOQL queries ยท Object CRUD
Salesforce uses OAuth 2.0 โ either the Authorization Code flow for user-delegated access or the Client Credentials flow for server-to-server integration. For an MCP server, use a Connected App in Salesforce with the Client Credentials (JWT Bearer) flow. This gives you a service account-style integration that doesn't require a human user to authorise each session.
get_accountRetrieve account record with contacts, opportunities, and open cases
search_opportunitiesSearch opportunities by stage, close date, owner, or account name
get_pipeline_summaryAggregate pipeline by stage, forecast category, and owner
create_activityLog a call, email, or meeting against an account or contact
update_opportunity_stageMove an opportunity to a new pipeline stage with optional note
get_recent_casesRetrieve open support cases for an account with status and priority
Salesforce Authentication: Connected App Setup
In Salesforce Setup, create a Connected App with OAuth enabled. Enable the Client Credentials Flow and assign it a dedicated integration user with appropriate object-level and field-level security. The integration user should have read access to the objects Claude needs to query, and write access only to the specific fields it needs to update. Never give the integration user System Administrator permissions โ scope it precisely.
Store your client_id, client_secret, and Salesforce instance URL in environment variables. Your MCP server requests a token on startup using the client credentials grant and refreshes it 5 minutes before expiry. Use Salesforce's token introspection endpoint to verify token validity before each API call in high-reliability contexts.
import httpx
import time
import os
class SalesforceClient:
def __init__(self):
self.instance_url = os.getenv("SF_INSTANCE_URL") # e.g. https://myorg.salesforce.com
self.client_id = os.getenv("SF_CLIENT_ID")
self.client_secret = os.getenv("SF_CLIENT_SECRET")
self._access_token = None
self._token_expiry = 0
async def _get_token(self) -> str:
if self._access_token and time.time() < self._token_expiry - 300:
return self._access_token
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{self.instance_url}/services/oauth2/token",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
)
resp.raise_for_status()
data = resp.json()
self._access_token = data["access_token"]
self._token_expiry = time.time() + data.get("expires_in", 7200)
return self._access_token
async def query(self, soql: str) -> dict:
token = await self._get_token()
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{self.instance_url}/services/data/v59.0/query",
params={"q": soql},
headers={"Authorization": f"Bearer {token}"},
timeout=30.0
)
resp.raise_for_status()
return resp.json()
# Tool handler example
async def get_pipeline_summary(sf_client, args: dict):
owner_filter = ""
if args.get("owner_name"):
# Parameterised โ no string injection risk
safe_name = args["owner_name"].replace("'", "\\'")
owner_filter = f"AND Owner.Name = '{safe_name}'"
soql = f"""
SELECT StageName, COUNT(Id) OpportunityCount, SUM(Amount) TotalValue
FROM Opportunity
WHERE IsClosed = false {owner_filter}
GROUP BY StageName
ORDER BY SUM(Amount) DESC
"""
result = await sf_client.query(soql)
import json
from mcp.types import TextContent
return [TextContent(type="text", text=json.dumps(result["records"], indent=2))]
Security Note โ Salesforce
Never expose a SOQL execution tool that accepts raw SOQL from Claude. Always build named tools around specific business queries. Salesforce's SOQL can access any object the integration user has permissions to โ raw access is both a data governance risk and a potential for unpredictable queries.
Jira MCP Server
Jira MCP
API Token ยท REST v3 ยท JQL search ยท Issue lifecycle
Jira Cloud uses API tokens for server-to-server integration. Create a dedicated service account (not a real person's account) in your Jira organisation, grant it the permissions it needs, and generate an API token. The token authenticates via HTTP Basic Auth: username is the service account email, password is the API token. Jira Data Center (on-premise) uses the same pattern or PATs (Personal Access Tokens) depending on version.
get_issueRetrieve issue details including description, status, assignee, and comments
search_issuesExecute JQL search and return matching issues with key fields
create_issueCreate a new issue with project, type, summary, description, and priority
update_issue_statusTransition an issue to a new status via available workflow transitions
add_commentAdd a comment to an existing issue
get_sprint_summaryGet current sprint issues by status for a specified team or project
Jira JQL: Safe Query Construction
Jira Query Language (JQL) is how you search issues. Like SOQL for Salesforce, the right pattern is to build named query functions rather than accepting raw JQL from Claude. The search_issues tool should accept structured parameters โ project key, assignee, status, sprint, priority, label โ and your tool handler constructs the JQL internally.
import httpx
import os
import json
from mcp.types import TextContent
from base64 import b64encode
JIRA_BASE_URL = os.getenv("JIRA_BASE_URL") # e.g. https://yourorg.atlassian.net
JIRA_EMAIL = os.getenv("JIRA_EMAIL")
JIRA_API_TOKEN = os.getenv("JIRA_API_TOKEN")
def get_auth_header():
credentials = f"{JIRA_EMAIL}:{JIRA_API_TOKEN}"
encoded = b64encode(credentials.encode()).decode()
return {"Authorization": f"Basic {encoded}", "Content-Type": "application/json"}
async def search_issues(args: dict):
# Build JQL from structured parameters โ never accept raw JQL
jql_parts = []
if project := args.get("project"):
jql_parts.append(f'project = "{project}"')
if status := args.get("status"):
jql_parts.append(f'status = "{status}"')
if assignee := args.get("assignee"):
jql_parts.append(f'assignee = "{assignee}"')
if sprint_name := args.get("sprint"):
jql_parts.append(f'sprint = "{sprint_name}"')
if not jql_parts:
return [TextContent(type="text", text=json.dumps({
"error": "validation_error",
"message": "At least one filter parameter is required"
}))]
jql = " AND ".join(jql_parts) + " ORDER BY updated DESC"
max_results = min(args.get("limit", 20), 50) # Cap at 50
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{JIRA_BASE_URL}/rest/api/3/search",
params={
"jql": jql,
"maxResults": max_results,
"fields": "summary,status,assignee,priority,created,updated,labels"
},
headers=get_auth_header(),
timeout=30.0
)
resp.raise_for_status()
data = resp.json()
# Return only the fields Claude needs โ strip unnecessary API metadata
issues = [{
"key": issue["key"],
"summary": issue["fields"]["summary"],
"status": issue["fields"]["status"]["name"],
"assignee": issue["fields"]["assignee"]["displayName"] if issue["fields"]["assignee"] else "Unassigned",
"priority": issue["fields"]["priority"]["name"] if issue["fields"].get("priority") else None,
} for issue in data.get("issues", [])]
return [TextContent(type="text", text=json.dumps({"total": data["total"], "issues": issues}))]
Want These MCP Servers Pre-Built and Production-Ready?
Our team delivers fully tested, security-reviewed MCP servers for Salesforce, Jira, Slack, and HubSpot. Each server includes authentication setup, documented tool surface, deployment guide, and ongoing support. See our MCP Development service.
Get a Quote โSlack MCP Server
Slack MCP
Bot OAuth ยท Web API ยท Channels ยท Messages ยท Search
Slack's integration model requires a Slack App with a Bot token. Create a Slack App in your workspace (or organisation-wide in Enterprise Grid), request the OAuth scopes you need, install it to your workspace, and store the Bot User OAuth Token. Slack uses a simple Bearer token auth model โ no token refresh needed as bot tokens don't expire unless revoked.
send_messagePost a message to a channel or DM with optional thread_ts for threading
search_messagesFull-text search across Slack for messages matching a query in specified channels
get_channel_historyRetrieve recent messages from a channel with optional time range
get_thread_repliesGet all replies in a message thread by channel and thread timestamp
list_channelsList channels the bot has joined, with member count and topic
get_user_infoLook up a user's display name, email, and timezone by user ID
Slack OAuth Scopes: Request Only What You Need
Slack's permission model is granular. The scopes your bot needs depend entirely on which tools you're exposing. For read-only message access: channels:history, groups:history, search:read. For sending messages: chat:write. For user lookups: users:read, users:read.email. Request exactly these scopes โ not the broad admin scope that many tutorials recommend for convenience.
One important consideration: Slack's Enterprise Grid has additional controls. In a Grid organisation, bot tokens are scoped to specific workspaces. If your enterprise spans multiple workspaces, you'll need either per-workspace bot tokens or to use the org-level token with explicit workspace IDs on every API call. Test this before deploying at scale.
When Not to Send Slack Messages Autonomously
An MCP server that allows Claude to post to Slack without human approval can cause real operational problems. A misconfigured agent posting to #general at 2 AM is a bad experience. Design your Slack MCP server with a human-in-the-loop checkpoint for any send_message calls in production agentic workflows. Confirm before posting, or restrict automated posting to dedicated bot channels.
HubSpot MCP Server
HubSpot MCP
Private App Token ยท CRM v3 API ยท Contacts ยท Deals ยท Engagement
HubSpot's recommended authentication for server-to-server integrations is the Private App model. Create a Private App in your HubSpot portal, select the scopes you need, and use the generated access token. Private App tokens don't expire (unlike OAuth tokens) and are managed independently of any user account โ making them ideal for MCP server integrations. Store the token as an environment variable and pass it as a Bearer token on every API call.
search_contactsSearch contacts by email, name, or company with full contact record
get_contactRetrieve a contact by ID with all standard and custom properties
get_deals_by_companyGet all deals associated with a company, filtered by stage or owner
create_noteCreate a logged note associated with a contact, company, or deal
update_deal_stageMove a deal to a new pipeline stage
get_email_engagementGet email open and click history for a contact or campaign
HubSpot CRM API v3: Key Patterns
HubSpot's CRM API uses a consistent REST pattern. All objects โ contacts, companies, deals, tickets โ follow the same endpoint structure: /crm/v3/objects/{objectType}. Search endpoints accept a filter array, property list, and sort specification. The API returns only the properties you explicitly request โ build your tool handlers to request exactly the properties Claude needs, not the entire object.
For associations โ linking a contact to a deal, or a note to a contact โ use HubSpot's Associations API. This is separate from the object APIs and often trips up developers new to HubSpot. When a get_contact tool needs to return associated deals, fetch the associations explicitly and then batch-fetch the deal records in a second API call.
import httpx
import os
import json
from mcp.types import TextContent
HUBSPOT_TOKEN = os.getenv("HUBSPOT_PRIVATE_APP_TOKEN")
HUBSPOT_BASE = "https://api.hubapi.com"
CONTACT_PROPERTIES = [
"firstname", "lastname", "email", "company",
"jobtitle", "phone", "hs_lead_status",
"lifecyclestage", "createdate", "lastmodifieddate"
]
async def search_contacts(args: dict):
query = args.get("query", "")
if not query:
return [TextContent(type="text", text=json.dumps({
"error": "validation_error",
"message": "Search query is required"
}))]
payload = {
"filterGroups": [{
"filters": [
{"propertyName": "email", "operator": "CONTAINS_TOKEN", "value": query},
]
}],
"properties": CONTACT_PROPERTIES,
"limit": min(args.get("limit", 10), 25)
}
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts/search",
json=payload,
headers={
"Authorization": f"Bearer {HUBSPOT_TOKEN}",
"Content-Type": "application/json"
},
timeout=30.0
)
resp.raise_for_status()
data = resp.json()
contacts = [{
"id": r["id"],
**{k: v for k, v in r["properties"].items() if v}
} for r in data.get("results", [])]
return [TextContent(type="text", text=json.dumps({
"total": data.get("total", 0),
"contacts": contacts
}, indent=2))]
Connecting All Four in a Multi-System Agent
The real value of having MCP servers for all four systems isn't in using them individually โ it's in building AI agents that coordinate across all of them. Consider a sales intelligence workflow: a Claude agent receives a trigger (an inbound HubSpot form submission), looks up the contact in HubSpot, searches Salesforce for the company's existing account and opportunity history, searches Slack for any recent conversations mentioning the company, creates a Jira ticket for the account executive to follow up, and posts a briefing to a designated Slack channel โ all in a single automated workflow.
This kind of cross-system coordination previously required either a dedicated workflow automation platform (Zapier, Make, n8n) with multiple custom integrations, or a custom-built integration service. With Claude and MCP servers for each system, it's an agentic workflow that can be adjusted through natural language rather than reconfiguring automation rules.
The architectural key is that Claude maintains separate client sessions with each MCP server and decides at runtime which servers to call based on the task. You don't need to pre-wire "if X then call Salesforce, then call Jira." You describe the workflow goal, and Claude figures out the right tool calls. Our multi-agent systems guide covers how to orchestrate these workflows at scale.
If you're ready to deploy MCP infrastructure for these systems in your enterprise, book a free strategy call with our team. We'll scope what you actually need, identify the right tool surfaces for your workflows, and deliver production-ready servers rather than prototypes you'll need to rebuild.
Key Takeaways
- Salesforce: use Client Credentials flow via Connected App; never expose raw SOQL
- Jira: API token auth via dedicated service account; build structured JQL constructors
- Slack: Bot OAuth token with minimal scopes; add human approval gates for send_message in agentic flows
- HubSpot: Private App token for server-to-server; request only needed properties per call
- Multi-system workflows are where MCP compounds in value โ design your tool surfaces with cross-system coordination in mind from day one