Building Custom MCP Agents for Jira and GitHub Integration

Meta Description: Learn how to build custom MCP agents that bridge Jira and GitHub, comparing local vs remote MCP servers, navigating Atlassian auth quirks, and understanding the GitHub PAT vs OAuth security shift. A deep-dive tutorial for AI agent developers.


Target Keywords: Model Context Protocol, MCP server, Jira GitHub integration, AI Agent, custom MCP client, GitHub PAT vs OAuth, Atlassian MCP, fine-grained tokens, langchain-mcp-adapters, Dynamic Client Registration, Docker MCP

Reading Time: 15 minutes
Difficulty: Intermediate to Advanced


Introduction: The Context-Switching Tax We All Pay

Picture this: You’re deep in flow, refactoring a critical authentication module. Your IDE is humming, your coffee is at the perfect temperature, and your brain is finally holding the entire system in working memory. Then—ping—a Slack notification. “Hey, can you check PROJ-1847? The stakeholder wants an update.”

You alt-tab to Jira. You read the ticket. You alt-tab to GitHub. You check the linked PR. You alt-tab back to Jira to update the status. By the time you return to your IDE, that beautiful mental model has evaporated like morning dew on a highway.

This is the context-switching tax, and every developer pays it daily.

But what if your tools could talk to each other—not through brittle webhooks or clunky integrations, but through a standardized protocol that lets AI agents orchestrate across platforms? Enter the Model Context Protocol (MCP), and the custom agents that can finally bridge the gap between Jira and GitHub.

Let’s peel this onion together.


Layer 1: Building a Custom MCP Agent Client from Scratch

Before we bridge worlds, we need to understand the foundation. MCP is a protocol that allows AI models (and agents) to interact with external tools through a standardized interface. Think of it as the USB-C of AI integrations—one connector, many possibilities.

The MCP Architecture at a Glance

The Easy Path: langchain-mcp-adapters

Building an MCP client from scratch is educational, but for production use, langchain-mcp-adapters does the heavy lifting. It’s the official LangChain integration that turns any MCP server into LangChain-compatible tools with minimal code.

# That's it. Seriously.
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

async with MultiServerMCPClient(
    {
        "atlassian": {
            "command": "npx",
            "args": ["mcp-remote", "https://mcp.atlassian.com/v1/mcp"],
            "transport": "stdio",
        },
        "github": {
            "url": "https://api.githubcopilot.com/mcp/",
            "transport": "sse",
            "headers": {"Authorization": f"Bearer {GITHUB_TOKEN}"},
        },
    }
) as client:
    # All tools from both servers, ready to use
    tools = client.get_tools()

    # Create a ReAct agent with one line
    agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools)

    result = await agent.ainvoke({"messages": "Sync PROJ-123 with GitHub"})

Why langchain-mcp-adapters?

FeatureRaw MCP SDKlangchain-mcp-adapters
Multi-server supportDIY✅ Built-in
LangChain/LangGraph integrationManual wrapping✅ Native
Tool discoveryManual loop✅ Automatic
Error handlingDIY✅ Standardized
Lines of code100+~15

Key Takeaway: Don’t reinvent the wheel. Use langchain-mcp-adapters for LangChain/LangGraph projects, and reserve raw MCP SDK work for custom edge cases.


Layer 2: The Great Debate — Local MCP Servers vs. Remote MCP Servers

Now that you have a client, where should your servers live? This decision has real implications for security, performance, and developer experience.

Architecture Comparison

The Comparison Table

FactorLocal MCP (stdio)Remote MCP (HTTP/SSE)
Latency⚡ Microseconds (IPC)🐢 50-500ms (network round-trip)
Privacy✅ Data never leaves machine⚠️ Data traverses third-party servers
Setup Complexity🔧 Requires local runtime (Node, Python)🎉 Just an HTTP endpoint
Deployment📦 Must bundle with your app☁️ Already deployed, just authenticate
Offline Support✅ Works without internet❌ Requires connectivity
Scaling🏠 Single-machine bound♾️ Horizontal scaling possible
Auth Flow🔑 API keys, local secrets🔐 OAuth, JIT auth, browser redirects
Rate Limits📈 You control the throttle🚦 Subject to provider limits
Updates🔄 Manual version management✨ Always latest version
Debugging🐛 Full access to logs/state🔍 Limited observability

When to Use Each

Choose Local MCP When:

  • You’re processing sensitive data (PII, secrets, proprietary code)
  • You need sub-millisecond tool execution
  • Your agent runs in air-gapped environments
  • You want full control over the tool implementation

Choose Remote MCP When:

  • You need access to third-party platforms (Jira, GitHub, Slack)
  • You’re building a distributed/serverless agent
  • You want zero maintenance on tool servers
  • OAuth-based auth is required (you can’t store user tokens)

The Hybrid Approach

Most production agents use both. Your architecture might look like:


Layer 3: The Atlassian Roadblock — When Remote MCP Gets Complicated

Let’s be real: Atlassian’s remote MCP server is powerful, but it comes with… personality. Here’s what you’ll encounter when integrating it into a custom agent.

The Authentication Labyrinth

Problem #1: The JIT (Just-In-Time) Auth Flow Mismatch

Atlassian’s MCP uses browser-based OAuth with JIT authentication. In plain terms: Atlassian’s MCP doesn’t follow a traditional static OAuth bearer token usage — it expects clients to engage in its browser-driven JIT OAuth flow to get the right token/context for tool calls.

The Catch: Headless agents (Lambda, CLI tools, cron jobs) don’t have browsers. You’re expected to:

  1. Detect the 401/auth response
  2. Present the URL to the user somehow
  3. Wait for them to complete auth
  4. Retry the original request

Workaround: Pre-authenticate users and cache tokens, or use mcp-remote as a local proxy that handles the OAuth dance:

# This bridges the auth gap
npx mcp-remote https://mcp.atlassian.com/v1/mcp

Problem #2: The 401 Error Parade

Generic HTTP clients often fail because Atlassian’s MCP expects:

  • Specific headers (Accept: text/event-stream for SSE)
  • Proper session handling
  • Cookies for auth state

A raw requests.post() will give you a 401 faster than you can say “CORS”.

Solution: Use the official MCP SDK’s SSE client, which handles:

# ❌ This will fail
import requests
response = requests.post(
    "https://mcp.atlassian.com/v1/mcp",
    json={"method": "tools/list"}
)

# ✅ This works
from mcp.client.sse import sse_client
async with sse_client(url, headers={"Authorization": f"Bearer {token}"}) as transport:
    # Proper SSE streaming handled

Problem #3: Rate Limits and Quotas

Atlassian imposes rate limits that aren’t always clearly documented:

ScenarioLimitConsequence
JQL searches~100/min429 Too Many Requests
Issue updates~60/minThrottled responses
Attachment uploads~10/minHard failures
Concurrent sessions~5 per userEarlier sessions invalidated

Mitigation Strategies:

  1. Implement exponential backoff:
import asyncio
from functools import wraps

def with_retry(max_attempts=3, base_delay=1.0):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return await func(*args, **kwargs)
                except RateLimitError:
                    if attempt == max_attempts - 1:
                        raise
                    delay = base_delay * (2 ** attempt)
                    await asyncio.sleep(delay)
        return wrapper
    return decorator
  1. Batch operations where possible (update multiple fields in one call)
  2. Cache reads aggressively (Jira ticket data doesn’t change every millisecond)

Problem #4: The cloudId Mystery

Every Atlassian MCP call requires a cloudId—the unique identifier for your Atlassian instance. But finding it isn’t obvious.

How to Get Your cloudId:

# Call this first to discover accessible resources
result = await client.call_tool("atl_getAccessibleAtlassianResources", {})
# Returns: [{"id": "46c04afa-a84d-4625-ba28-0cab62877aba", "name": "yoursite", ...}]

Or extract it from any Atlassian URL:

https://yoursite.atlassian.net/...
      ↓
Cloud ID is NOT in the URL! You must call the API.

Layer 4: The Security Shift — GitHub PAT vs. OAuth

GitHub has been on a mission: kill the Personal Access Token (PAT). Or at least, make you really think twice before creating one.

The Problem with Classic PATs

flowchart TB
    subgraph Classic["Classic PAT (Legacy)"]
        CP1["✓ Full repo access"]
        CP2["✓ Full org access"]
        CP3["✓ Never expires (optional)"]
        CP4["✗ No audit trail"]
        CP5["✗ All-or-nothing scopes"]
    end

    subgraph FineGrained["Fine-Grained PAT"]
        FG1["✓ Per-repository access"]
        FG2["✓ Granular permissions"]
        FG3["✓ Required expiration"]
        FG4["✓ Audit logging"]
        FG5["✓ Owner approval workflows"]
    end

    subgraph OAuth["GitHub App / OAuth"]
        OA1["✓ Org-level installation"]
        OA2["✓ Scoped to app identity"]
        OA3["✓ Revocable per-installation"]
        OA4["✓ Rate limit increases"]
        OA5["✓ Webhooks + API combined"]
    end

    Classic -->|"Migration Path"| FineGrained
    FineGrained -->|"For Serious Apps"| OAuth

Why GitHub Is Pushing This Change

  1. Blast Radius: A leaked classic PAT with repo scope gives attackers access to every repository you can access. A fine-grained token can be limited to a single repo.
  2. Auditability: Classic PATs are ghosts. Fine-grained tokens and OAuth apps leave audit trails.
  3. Expiration Enforcement: Classic PATs can live forever. That token you created in 2019? Still valid. Still dangerous.

The “Over-Permissioned Agent” Anti-Pattern

Here’s a horror story I see too often:

# ❌ The "just make it work" approach
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]  # Classic PAT with repo, admin:org, write:packages

# This agent can now:
# - Delete any repository
# - Add itself as an org admin
# - Publish malicious packages
# All because you wanted to read PR comments.

The Right Way:

# ✅ Fine-grained token, minimal permissions
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
# Scopes:
#   - Repository access: Only "my-project"
#   - Permissions: Pull requests (read), Issues (read/write)
#   - Expiration: 30 days

Comparison: PAT vs. Fine-Grained vs. OAuth

AspectClassic PATFine-Grained PATGitHub App/OAuth
Scope GranularityCoarse (repo, org)Per-repo, per-permissionPer-installation
ExpirationOptional (dangerous)Required (max 1 year)Token refresh flow
Audit TrailNoneFullFull + webhooks
Rate Limits5,000/hr (user)5,000/hr (user)15,000+/hr (app)
Setup Complexity🟢 Easy🟡 Medium🔴 Complex
Best ForLocal scripts, testingAgents, CI/CDProduction apps
GitHub’s Preference🚫 Deprecated soon✅ Recommended✅ Preferred

For MCP Agents: The Recommendation

If your agent is:

  • Personal/local: Fine-grained PAT with 30-day expiration, minimal repo access
  • Team/shared: GitHub App with installation tokens
  • Enterprise/SaaS: OAuth App with proper token refresh

Core Tutorial: Bridging Jira and GitHub with MCP

Now for the main event. Let’s build an agent that:

  1. Reads a Jira ticket
  2. Finds related GitHub issues/PRs
  3. Syncs status between them
  4. Updates both platforms

The Architecture

The Complete Agent (Using langchain-mcp-adapters)

With langchain-mcp-adapters, the entire bridge agent fits in one file:

# bridge_agent.py
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
import os

SYSTEM_PROMPT = """You are a bridge agent syncing Jira and GitHub.
Given a Jira key, fetch the issue, find related GitHub PRs/issues, and report.
Use cloudId: {cloud_id} for all Atlassian calls."""

async def bridge_jira_github(jira_key: str) -> str:
    async with MultiServerMCPClient({
        "atlassian": {
            "command": "npx",
            "args": ["mcp-remote", "https://mcp.atlassian.com/v1/mcp"],
            "transport": "stdio",
        },
        "github": {
            "url": "https://api.githubcopilot.com/mcp/",
            "transport": "sse",
            "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
        },
    }) as client:
        agent = create_react_agent(
            ChatOpenAI(model="gpt-4o"),
            client.get_tools(),
            prompt=SYSTEM_PROMPT.format(cloud_id=os.environ["ATLASSIAN_CLOUD_ID"])
        )
        result = await agent.ainvoke({"messages": f"Sync {jira_key} with GitHub"})
        return result["messages"][-1].content

if __name__ == "__main__":
    import sys
    print(asyncio.run(bridge_jira_github(sys.argv[1] if len(sys.argv) > 1 else "PROJ-123")))

That’s ~30 lines for a production-ready Jira-GitHub sync agent. The langchain-mcp-adapters library handles connection management, tool discovery, and LangGraph integration.

Example Output

🔌 Connecting to Atlassian MCP...
  ✓ Registered: atlassian__atl_searchJiraIssuesUsingJql
  ✓ Registered: atlassian__atl_getJiraIssue
  ✓ Registered: atlassian__atl_addCommentToJiraIssue
  ... (47 more tools)

🔌 Connecting to GitHub MCP...
  ✓ Registered: github__search_issues
  ✓ Registered: github__get_pull_request
  ✓ Registered: github__create_issue
  ... (23 more tools)

✅ Connected! 74 tools available.

==================================================
## Sync Report for PROJ-123

### Jira Issue
- **Title:** Implement OAuth2 refresh token flow
- **Status:** In Progress
- **Assignee:** jane.doe@company.com

### GitHub Findings
- **PR #456:** "feat: OAuth2 refresh token implementation" (MERGED)
- **Issue #789:** "Token refresh fails after 24h" (OPEN)

### Actions Taken
1. Updated Jira ticket status to "In Review" (PR merged)
2. Added comment linking to merged PR
3. Created cross-reference in GitHub issue #789

### Recommendation
Consider closing PROJ-123 as the feature PR has merged.

Layer 5: Reference Guide — Creating API Tokens and Local Setup

Atlassian API Token Creation

  1. Navigate to API Token Management:
    • Go to: `https://id.atlassian.com/manage-profile/security/api-tokens`
    • Or: Atlassian Account → Security → API tokens
  2. Create a New Token:
    Click "Create API token"

    Label: "MCP Agent - Development"

    Copy the token immediately (you won't see it again!)
  3. Store Securely:
    # .env (never commit this!)
    ATLASSIAN_API_TOKEN=ATATT3xFfGF0...

GitHub Fine-Grained Token Creation

  1. Navigate to Token Settings:
    • Go to: `https://github.com/settings/tokens?type=beta`
    • Or: Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
  2. Configure the Token:
    Token name: mcp-bridge-agent
    Expiration: 30 days (maximum recommended for agents)

    Resource owner: [Your org or personal account]
    Repository access: "Only select repositories"
    → Select: repo-1, repo-2

    Permissions:
    - Contents: Read
    - Issues: Read and write
    - Pull requests: Read
    - Metadata: Read (required)
  3. Generate and Store:
    # .env
    GITHUB_MCP_TOKEN=github_pat_11ABC...

Local Development with mcp-remote

The mcp-remote package bridges stdio and HTTP transports, handling OAuth flows:

# Install globally
npm install -g mcp-remote

# Or use npx (recommended)
npx mcp-remote https://mcp.atlassian.com/v1/mcp

How It Works:

Understanding DCR (Dynamic Client Registration)

DCR sounds scary, but it’s actually solving a real problem. Let me explain.

The Problem: Traditional OAuth requires you to pre-register your app with every service (Atlassian, GitHub, etc.) and get a client_id + client_secret. But what if you’re building a tool that connects to any user’s Atlassian instance? You can’t pre-register with every possible instance.

The Solution: DCR lets your app register itself dynamically at runtime:

Why This Matters for MCP:

  • Remote MCP servers like Atlassian use DCR behind the scenes
  • The mcp-remote proxy handles DCR for you automatically
  • You don’t need to manually register apps—the protocol does it

Reference: RFC 7591 – OAuth 2.0 Dynamic Client Registration


Docker MCP Toolkit: Authentication Made Easy 🐳

For Junior Devs: If you’ve ever struggled with OAuth browser redirects in headless environments (Docker, Lambda, CI/CD), Docker’s MCP Toolkit is your new best friend.

What It Does:
Docker Desktop now includes an MCP gateway that handles authentication for you. Instead of wrestling with browser redirects and callback URLs, Docker manages the OAuth flow through its desktop app.

Step-by-Step Setup:

  1. Install Docker Desktop 4.40+ (MCP support added late 2025)
  2. Enable MCP in Docker Settings:
    Docker Desktop → Settings → Features in Development → Enable MCP Toolkit
  3. Configure MCP Servers in ~/.docker/mcp.json:
{
	"servers": {
		"atlassian": {
			"type": "remote",
			"url": "https://mcp.atlassian.com/v1/mcp"
		},
		"github": {
			"type": "remote",
			"url": "https://api.githubcopilot.com/mcp/"
		}
	}
}

Run Your Agent via Docker MCP Proxy:

# Docker handles auth, your container gets stdio access
docker run --rm -it \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
my-mcp-agent

How It Works:

Why Devs May Love This:

  • ❌ No more “how do I open a browser from a container?”
  • ❌ No more hardcoded tokens in Dockerfiles
  • ❌ No more callback URL configuration
  • ✅ Docker Desktop handles the browser-based OAuth dance
  • ✅ Tokens persist across container restarts
  • ✅ Works identically in dev and CI/CD

Pro Tip: For CI/CD (no Docker Desktop), use pre-authenticated tokens stored in secrets managers. Docker MCP Toolkit is for development workflows.


Debugging with MCP Inspector

Intercept and inspect MCP traffic:

# Wrap your MCP server with the inspector
npx @anthropics/mcp-inspector npx mcp-remote https://mcp.atlassian.com/v1/mcp

This opens a web UI showing:

  • All tool calls and responses
  • Timing information
  • Error details

Conclusion: You’re Not Building a Bridge—You’re Building the Future

Let’s step back from the code for a moment.

In 2023, connecting Jira to GitHub meant Zapier, webhooks, or that one senior dev’s Python script that “just works” (until it doesn’t). In 2024, we got MCP—a protocol that finally treats AI agents as first-class citizens in the integration world.

But here’s what excites me most: you’re not just syncing tickets anymore.

The bridge you’ve built today can:

  • 🤖 Automate sprint planning by analyzing GitHub velocity and Jira backlogs
  • 🔍 Surface forgotten PRs that reference closed tickets
  • 📊 Generate standup reports by correlating commits with issue updates
  • 🚨 Alert on drift when GitHub reality doesn’t match Jira expectations

This is the difference between an integration and an intelligent agent. Integrations move data. Agents understand context.

What We’ve Learned

LayerInsight
The Clientlangchain-mcp-adapters > raw SDK for 90% of use cases
Local vs. RemoteHybrid is king—sensitive data local, APIs remote
Atlassian AuthExpect JIT quirks; mcp-remote and Docker MCP Toolkit are your friends
GitHub SecurityFine-grained tokens now, OAuth for production
The Practice30 lines of Python can replace 300 lines of webhook spaghetti

The Road Ahead

MCP adoption is accelerating. Anthropic, OpenAI, Google, and Microsoft are all backing the protocol. Every week, new MCP servers appear for services like Notion, Linear, Figma, and Datadog.

The agents you build today will compound in value as the ecosystem grows.

So here’s my challenge to you: Don’t just bridge Jira and GitHub. Pick another pair of tools that eat your productivity. Build the bridge. Share it with the community.

Because the developers who master MCP now won’t just be early adopters—they’ll be the architects of how AI and human tools work together for the next decade.

Now go abolish that context-switching tax. You’ve got the tools. 🚀


Quick Reference Card

# .env template for Jira-GitHub Bridge Agent

# Required: LLM
OPENAI_API_KEY=sk-...

# Required: Atlassian
ATLASSIAN_CLOUD_ID=your-cloud-uuid
ATLASSIAN_MCP_COMMAND=npx
ATLASSIAN_MCP_ARGS=["mcp-remote", "https://mcp.atlassian.com/v1/mcp"]

# Required: GitHub
GITHUB_ORG=your-org
GITHUB_MCP_URL=https://api.githubcopilot.com/mcp/
GITHUB_MCP_TOKEN=github_pat_...  # Fine-grained, 30-day expiry

# Optional: Observability
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=lsv2_...

Got questions? Found a bug in the bridge? Open an issue or reach out. The MCP community is small but mighty—let’s build it together.

Tags: #MCP #ModelContextProtocol #Jira #GitHub #AIAgents #DevTools #Integration #OAuth #Security #langchain-mcp-adapters #DCR #DockerMCP


Further Reading & References

1 thought on “Building Custom MCP Agents for Jira and GitHub Integration”

I would like to hear your thoughts