MCP Integrations

MCP Servers for Salesforce, Jira, Slack & HubSpot

These four systems โ€” Salesforce, Jira, Slack, and HubSpot โ€” account for over 60% of the MCP server requests we receive from enterprise clients. Every organisation has them. Every organisation wants Claude to work with them. This guide covers the specific authentication patterns, tool designs, and production considerations for each system, based on what we've actually deployed.

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_account

Retrieve account record with contacts, opportunities, and open cases

search_opportunities

Search opportunities by stage, close date, owner, or account name

get_pipeline_summary

Aggregate pipeline by stage, forecast category, and owner

create_activity

Log a call, email, or meeting against an account or contact

update_opportunity_stage

Move an opportunity to a new pipeline stage with optional note

get_recent_cases

Retrieve 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.

python salesforce_client.py โ€” authentication and SOQL queries
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_issue

Retrieve issue details including description, status, assignee, and comments

search_issues

Execute JQL search and return matching issues with key fields

create_issue

Create a new issue with project, type, summary, description, and priority

update_issue_status

Transition an issue to a new status via available workflow transitions

add_comment

Add a comment to an existing issue

get_sprint_summary

Get 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.

python jira_tools.py โ€” safe JQL construction from structured parameters
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_message

Post a message to a channel or DM with optional thread_ts for threading

search_messages

Full-text search across Slack for messages matching a query in specified channels

get_channel_history

Retrieve recent messages from a channel with optional time range

get_thread_replies

Get all replies in a message thread by channel and thread timestamp

list_channels

List channels the bot has joined, with member count and topic

get_user_info

Look 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_contacts

Search contacts by email, name, or company with full contact record

get_contact

Retrieve a contact by ID with all standard and custom properties

get_deals_by_company

Get all deals associated with a company, filtered by stage or owner

create_note

Create a logged note associated with a contact, company, or deal

update_deal_stage

Move a deal to a new pipeline stage

get_email_engagement

Get 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.

python hubspot_tools.py โ€” contact search with property filtering
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

Get All Four MCP Servers Built and Deployed

Our team builds, tests, and deploys production-grade MCP servers for Salesforce, Jira, Slack, and HubSpot. Each includes authentication setup, security review, tool documentation, and deployment to your infrastructure.

Book a Scoping Call โ†’ MCP Dev Service

Related Articles

๐Ÿ”Œ

ClaudeImplementation Team

Claude Certified Architects specialising in enterprise MCP infrastructure and agentic AI deployment. Learn about our team โ†’

Connect Claude to Your Enterprise Stack

Salesforce, Jira, Slack, HubSpot โ€” and whatever else you're running. We build the MCP infrastructure that makes Claude genuinely useful across your enterprise, not just in a sandbox.