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
Table of Contents
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?
| Feature | Raw MCP SDK | langchain-mcp-adapters |
|---|---|---|
| Multi-server support | DIY | ✅ Built-in |
| LangChain/LangGraph integration | Manual wrapping | ✅ Native |
| Tool discovery | Manual loop | ✅ Automatic |
| Error handling | DIY | ✅ Standardized |
| Lines of code | 100+ | ~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
| Factor | Local 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:
- Detect the 401/auth response
- Present the URL to the user somehow
- Wait for them to complete auth
- 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-streamfor 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:
| Scenario | Limit | Consequence |
|---|---|---|
| JQL searches | ~100/min | 429 Too Many Requests |
| Issue updates | ~60/min | Throttled responses |
| Attachment uploads | ~10/min | Hard failures |
| Concurrent sessions | ~5 per user | Earlier sessions invalidated |
Mitigation Strategies:
- 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
- Batch operations where possible (update multiple fields in one call)
- 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
- Blast Radius: A leaked classic PAT with
reposcope gives attackers access to every repository you can access. A fine-grained token can be limited to a single repo. - Auditability: Classic PATs are ghosts. Fine-grained tokens and OAuth apps leave audit trails.
- 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
| Aspect | Classic PAT | Fine-Grained PAT | GitHub App/OAuth |
|---|---|---|---|
| Scope Granularity | Coarse (repo, org) | Per-repo, per-permission | Per-installation |
| Expiration | Optional (dangerous) | Required (max 1 year) | Token refresh flow |
| Audit Trail | None | Full | Full + webhooks |
| Rate Limits | 5,000/hr (user) | 5,000/hr (user) | 15,000+/hr (app) |
| Setup Complexity | 🟢 Easy | 🟡 Medium | 🔴 Complex |
| Best For | Local scripts, testing | Agents, CI/CD | Production 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:
- Reads a Jira ticket
- Finds related GitHub issues/PRs
- Syncs status between them
- 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
- Navigate to API Token Management:
- Go to: `https://id.atlassian.com/manage-profile/security/api-tokens`
- Or: Atlassian Account → Security → API tokens
- Create a New Token:
Click "Create API token"
↓
Label: "MCP Agent - Development"
↓
Copy the token immediately (you won't see it again!) - Store Securely:
# .env (never commit this!)
ATLASSIAN_API_TOKEN=ATATT3xFfGF0...
GitHub Fine-Grained Token Creation
- Navigate to Token Settings:
- Go to: `https://github.com/settings/tokens?type=beta`
- Or: Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
- 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) - 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-remoteproxy 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:
- Install Docker Desktop 4.40+ (MCP support added late 2025)
- Enable MCP in Docker Settings:
Docker Desktop → Settings → Features in Development → Enable MCP Toolkit - 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
| Layer | Insight |
|---|---|
| The Client | langchain-mcp-adapters > raw SDK for 90% of use cases |
| Local vs. Remote | Hybrid is king—sensitive data local, APIs remote |
| Atlassian Auth | Expect JIT quirks; mcp-remote and Docker MCP Toolkit are your friends |
| GitHub Security | Fine-grained tokens now, OAuth for production |
| The Practice | 30 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
- Model Context Protocol Specification — The official MCP documentation
- langchain-mcp-adapters — LangChain’s official MCP integration
- RFC 7591 – Dynamic Client Registration — The OAuth DCR standard
- Atlassian MCP Server — Official Atlassian remote MCP endpoint
- GitHub MCP Documentation — GitHub’s MCP integration docs
- Docker MCP Toolkit — Docker Desktop’s MCP gateway
- mcp-remote — The stdio-to-HTTP bridge for remote MCP servers
- GitHub Fine-Grained PATs — Token security best practices

If you are looking for a complete PoC, here is an example https://github.com/jobairkhan/custom-mcp-client