Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:49:50 +08:00
commit adc4b2be25
147 changed files with 24716 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
# Connect Jira to MXCP with OAuth
This example shows how to connect JIRA to MXCP using secure OAuth authentication.
## What You Get
Once configured, you can query your Jira data directly from MXCP:
```sql
-- Find all issues assigned to you
SELECT jql_query_jira('assignee = currentUser()') AS my_issues;
-- Get recent bugs in a project
SELECT jql_query_jira('project = MYPROJECT AND type = Bug AND created >= -7d') AS recent_bugs;
-- List all your accessible projects
SELECT list_projects_jira() AS projects;
-- Get user information
SELECT get_user_jira('john.doe@company.com') AS user_info;
```
## Quick Setup Guide
### Step 1: Create Your OAuth App in Atlassian
1. Go to [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/)
2. Click **Create****OAuth 2.0 (3LO)**
3. Fill in your app details:
- **App name**: `MXCP Jira Integration` (or whatever you prefer)
- **Description**: `OAuth integration for MXCP`
4. Click **Create**
### Step 2: Configure OAuth Settings
After creating your app:
1. Click on your newly created app
2. Go to **Permissions****Add****Jira API**
3. Add these scopes:
- `read:me` (to read your own profile information)
- `read:jira-work` (to read issues and projects)
- `read:jira-user` (to read user information)
- `offline_access` (to refresh tokens)
4. Go to **Authorization****OAuth 2.0 (3LO)**
5. Add your callback URL based on your deployment:
- **For production**: `https://your-domain.com/atlassian/callback`
- **For local development**: `http://localhost:8000/atlassian/callback`
- **For ngrok testing**: `https://your-ngrok-url.ngrok.io/atlassian/callback`
6. **Important**: Save your **Client ID** and **Client Secret** - you'll need these next!
### Step 3: Set Up Environment Variables
Create a `.env` file or set these environment variables:
```bash
export ATLASSIAN_CLIENT_ID="your-client-id-here"
export ATLASSIAN_CLIENT_SECRET="your-client-secret-here"
```
### Step 4: Configure MXCP
This example includes a ready-to-use `config.yml` file that you can customize with your OAuth credentials. You can either:
- **Use the included file**: Edit the existing `config.yml` in this directory
- **Create your own**: Use the template below
Configuration template:
```yaml
mxcp: 1.0.0
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:
my-jira-project:
profiles:
dev:
# OAuth Configuration
auth:
provider: atlassian
clients:
- client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
name: "MXCP Jira Integration"
redirect_uris:
# For production, use your actual domain (must match base_url above)
- "https://your-domain.com/atlassian/callback"
# For local development, uncomment the line below:
# - "http://localhost:8000/atlassian/callback"
scopes:
- "mxcp:access"
atlassian:
client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
scope: "read:me read:jira-work read:jira-user offline_access"
callback_path: "/atlassian/callback"
auth_url: "https://auth.atlassian.com/authorize"
token_url: "https://auth.atlassian.com/oauth/token"
# Plugin Configuration (minimal setup required!)
plugin:
config:
jira_oauth: {} # Named 'jira_oauth' here, but UDFs use 'jira' suffix from mxcp-site.yml
```
### Step 5: Install and Run
1. **Install dependencies**:
```bash
pip install atlassian-python-api requests
```
2. **Start MXCP**:
```bash
# From the examples/jira-oauth directory:
MXCP_CONFIG=config.yml mxcp serve
```
3. **Authenticate**:
- Configure the MXCP server in your MCP client (e.g., Claude Desktop)
- When the client connects, you'll be redirected to Atlassian to authorize the app
- After authorization, you'll be redirected back to your MCP client
- You're now ready to query Jira!
## Available Functions
| Function | Description | Example |
|----------|-------------|---------|
| `jql_query_jira(query, start, limit)` | Execute JQL queries | `SELECT jql_query_jira('project = TEST')` |
| `list_projects_jira()` | List all your accessible projects | `SELECT list_projects_jira()` |
| `get_project_jira(key)` | Get details for a specific project | `SELECT get_project_jira('TEST')` |
| `get_user_jira(username)` | Get user information | `SELECT get_user_jira('john@company.com')` |
## Example Queries
```sql
-- Get your assigned issues
SELECT jql_query_jira('assignee = currentUser() AND status != Done', 0, 20) AS my_open_issues;
-- Find high priority bugs
SELECT jql_query_jira('priority = High AND type = Bug', 0, 10) AS high_priority_bugs;
-- Recent activity in a project
SELECT jql_query_jira('project = MYPROJECT AND updated >= -3d') AS recent_activity;
-- Get project information
SELECT
list_projects_jira() AS all_projects,
get_project_jira('MYPROJECT') AS project_details;
-- Find issues by reporter
SELECT jql_query_jira('reporter = "john.doe@company.com"') AS johns_issues;
```

View File

@@ -0,0 +1,36 @@
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:
jira-oauth-demo:
profiles:
dev:
# OAuth Authentication Configuration
auth:
provider: atlassian
clients:
- client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
name: "MXCP Jira OAuth Integration"
redirect_uris:
# For production, use your actual domain (must match base_url above)
- "http://localhost:8000/atlassian/callback"
scopes:
- "mxcp:access"
atlassian:
client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
scope: "read:me read:jira-work read:jira-user offline_access"
callback_path: "/atlassian/callback"
auth_url: "https://auth.atlassian.com/authorize"
token_url: "https://auth.atlassian.com/oauth/token"
# Plugin Configuration (minimal configuration - uses OAuth context!)
plugin:
config:
jira_oauth: {} # Named 'jira_oauth' here, but UDFs use 'jira' suffix from mxcp-site.yml

View File

@@ -0,0 +1,8 @@
mxcp: 1
project: jira-oauth-demo
profile: dev
plugin:
- name: jira
module: mxcp_plugin_jira_oauth
config: jira_oauth

View File

@@ -0,0 +1,10 @@
"""
MXCP Jira OAuth Plugin
This plugin provides UDFs for querying Atlassian Jira using OAuth authentication.
Unlike the API token version, this plugin uses OAuth tokens from authenticated users.
"""
from .plugin import MXCPPlugin
__all__ = ["MXCPPlugin"]

View File

@@ -0,0 +1,250 @@
"""
Jira OAuth Plugin Implementation
This module provides UDFs for querying Atlassian Jira using JQL with OAuth 2.0 authentication.
"""
import json
import logging
from typing import Any, Dict, List, Optional
import requests
from atlassian import Jira
from mxcp.plugins import MXCPBasePlugin, udf
from mxcp.sdk.auth.context import get_user_context
logger = logging.getLogger(__name__)
class MXCPPlugin(MXCPBasePlugin):
"""Jira OAuth plugin that provides JQL query functionality using OAuth 2.0 Bearer tokens."""
def __init__(self, config: Dict[str, Any]):
"""Initialize the Jira OAuth plugin.
Args:
config: Plugin configuration containing optional settings
Optional keys:
- oauth_token: Fallback OAuth Bearer token (if not using user context)
"""
super().__init__(config)
self.fallback_oauth_token = config.get("oauth_token", "")
self.instance_url: Optional[str] = None
def _get_oauth_token(self) -> str:
"""Get OAuth token from user context or fallback configuration.
Returns:
OAuth Bearer token
Raises:
ValueError: If no OAuth token is available
"""
# First try to get token from user context (preferred)
user_context = get_user_context()
if user_context and user_context.external_token:
logger.debug("Using OAuth token from user context")
return user_context.external_token
# Fall back to configured token
if self.fallback_oauth_token:
logger.debug("Using fallback OAuth token from configuration")
return self.fallback_oauth_token
raise ValueError("No OAuth token available from user context or configuration")
def _get_cloud_id_and_url(self, oauth_token: str) -> tuple[str, str]:
"""Get the cloud ID and instance URL for the first accessible Jira instance using the OAuth token.
Args:
oauth_token: OAuth Bearer token
Returns:
Tuple of (cloud_id, instance_url) for the first accessible Jira instance
Raises:
ValueError: If cloud ID and URL cannot be retrieved
"""
try:
response = requests.get(
"https://api.atlassian.com/oauth/token/accessible-resources",
headers={"Authorization": f"Bearer {oauth_token}", "Accept": "application/json"},
)
response.raise_for_status()
resources = response.json()
logger.debug(f"Found {len(resources)} accessible resources")
# Use the first accessible resource
if resources:
cloud_id = resources[0].get("id")
instance_url = resources[0].get("url")
logger.info(f"Using cloud ID: {cloud_id} for instance: {instance_url}")
return cloud_id, instance_url
raise ValueError(f"No accessible resources found for OAuth token")
except requests.RequestException as e:
logger.error(f"Failed to get cloud ID and URL: {e}")
raise ValueError(f"Failed to retrieve cloud ID and URL: {e}")
def _create_jira_client(self) -> Jira:
"""Create a Jira client with OAuth authentication using the correct API gateway URL.
Returns:
Configured Jira client instance
"""
oauth_token = self._get_oauth_token()
# Get the cloud ID and instance URL for the first accessible Jira instance
cloud_id, instance_url = self._get_cloud_id_and_url(oauth_token)
# Store the instance URL for constructing web UI URLs
self.instance_url = instance_url
# Construct the API gateway URL for OAuth requests
api_gateway_url = f"https://api.atlassian.com/ex/jira/{cloud_id}"
logger.info("API Gateway URL: %s", api_gateway_url)
# Create a requests session with OAuth Bearer token
session = requests.Session()
session.headers["Authorization"] = f"Bearer {oauth_token}"
# Create and return Jira client with the OAuth session and API gateway URL
# Explicitly set cloud=True since we're using Jira Cloud with OAuth
return Jira(url=api_gateway_url, session=session, cloud=True)
@udf
def jql_query(self, query: str, start: Optional[int] = 0, limit: Optional[int] = None) -> str:
"""Execute a JQL query against Jira using OAuth authentication.
Args:
query: The JQL query string
start: Starting index for pagination (default: 0)
limit: Maximum number of results to return (default: None, meaning no limit)
Returns:
JSON string containing Jira issues matching the query
"""
logger.info(
"Executing JQL query with OAuth: %s with start=%s, limit=%s", query, start, limit
)
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
raw = jira.jql(
jql=query,
start=start,
limit=limit,
fields=(
"key,summary,status,resolution,resolutiondate,"
"assignee,reporter,issuetype,priority,"
"created,updated,labels,fixVersions,parent"
),
)
def _name(obj: Optional[Dict[str, Any]]) -> Optional[str]:
"""Return obj['name'] if present, else None."""
return obj.get("name") if obj else None
def _key(obj: Optional[Dict[str, Any]]) -> Optional[str]:
return obj.get("key") if obj else None
cleaned: List[Dict[str, Any]] = []
for issue in raw.get("issues", []):
f = issue["fields"]
cleaned.append(
{
"key": issue["key"],
"summary": f.get("summary"),
"status": _name(f.get("status")),
"resolution": _name(f.get("resolution")),
"resolution_date": f.get("resolutiondate"),
"assignee": _name(f.get("assignee")),
"reporter": _name(f.get("reporter")),
"type": _name(f.get("issuetype")),
"priority": _name(f.get("priority")),
"created": f.get("created"),
"updated": f.get("updated"),
"labels": f.get("labels") or [],
"fix_versions": [_name(v) for v in f.get("fixVersions", [])],
"parent": _key(f.get("parent")),
"url": f"{self.instance_url}/browse/{issue['key']}", # web UI URL
}
)
return json.dumps(cleaned)
@udf
def get_user(self, username: str) -> str:
"""Get details for a specific user by username using OAuth.
Args:
username: The username to search for
Returns:
JSON string containing the user details
"""
logger.info("Getting user details with OAuth for username: %s", username)
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
return json.dumps(jira.user_find_by_user_string(query=username))
@udf
def list_projects(self) -> str:
"""List all accessible Jira projects using OAuth authentication.
Returns:
JSON string containing an array of accessible Jira projects
"""
logger.info("Listing all projects with OAuth")
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
raw_projects: List[Dict[str, Any]] = jira.projects()
def safe_name(obj: Optional[Dict[str, Any]]) -> Optional[str]:
return obj.get("displayName") or obj.get("name") if obj else None
concise: List[Dict[str, Any]] = []
for p in raw_projects:
concise.append(
{
"key": p.get("key"),
"name": p.get("name"),
"type": p.get("projectTypeKey"), # e.g. software, business
"lead": safe_name(p.get("lead")),
"url": f"{self.instance_url}/projects/{p.get('key')}", # web UI URL
}
)
return json.dumps(concise)
@udf
def get_project(self, project_key: str) -> str:
"""Get details for a specific project by its key using OAuth.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
Returns:
JSON string containing the project details
"""
logger.info("Getting project details with OAuth for key: %s", project_key)
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
info = jira.project(project_key)
# remove the self key if it exists
if "self" in info:
info.pop("self")
# Add web UI URL
info["url"] = f"{self.instance_url}/projects/{project_key}"
return json.dumps(info)

View File

@@ -0,0 +1,2 @@
-- Get the username of the currently authenticated user
SELECT get_username() as authenticated_user;

View File

@@ -0,0 +1,2 @@
-- Get details for a specific Jira project using OAuth authentication
SELECT get_project_jira($project_key) as result;

View File

@@ -0,0 +1,2 @@
-- Get details for a specific Jira user using OAuth authentication
SELECT get_user_jira($username) as result;

View File

@@ -0,0 +1,2 @@
-- Example JQL query endpoint using OAuth authentication
SELECT jql_query_jira($query, $start, $limit) as result;

View File

@@ -0,0 +1,2 @@
-- List all projects in Jira using OAuth authentication
SELECT list_projects_jira() as result;

View File

@@ -0,0 +1,25 @@
mxcp: 1
tool:
name: get_current_user
description: |
Get the username of the currently authenticated user in MXCP.
This tool returns the username of the person who is authenticated via OAuth with Jira.
It's useful for understanding whose credentials are being used for Jira API calls,
and can help verify that the OAuth authentication flow completed successfully.
The username typically corresponds to the Atlassian account email address.
type: tool
annotations:
title: Get Current Authenticated User
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
return:
type: string
description: |
The username (typically email address) of the currently authenticated user.
Returns NULL if no user is authenticated.
language: "sql"
source:
file: "../sql/get_current_user.sql"

View File

@@ -0,0 +1,32 @@
mxcp: 1
tool:
name: get_project
description: |
Get details for a specific project in your Jira instance by its project key using OAuth authentication.
Returns a JSON string containing the project's details.
type: tool
annotations:
title: Get Project Details (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: project_key
type: string
description: |
The project key to search for. This is the short identifier for the project (e.g., 'TEST' for project TEST).
Project keys are typically uppercase and contain only letters and numbers.
examples: [
"TEST",
"PROJ",
"DEV"
]
return:
type: string
description: |
A JSON string containing the project's details.
language: "sql"
source:
file: "../sql/get_project.sql"

View File

@@ -0,0 +1,30 @@
mxcp: 1
tool:
name: get_user
description: |
Get details for a specific user in your Jira instance by their username using OAuth authentication.
Returns a JSON string containing the user's details.
type: tool
annotations:
title: Get User Details (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: username
type: string
description: |
The username to search for. This is typically the user's email address or username in Jira.
examples: [
"john.doe@example.com",
"jane.smith"
]
return:
type: string
description: |
A JSON string containing the user's details.
language: "sql"
source:
file: "../sql/get_user.sql"

View File

@@ -0,0 +1,50 @@
mxcp: 1
tool:
name: jql
description: |
Execute a JQL (Jira Query Language) query to search for issues in your Jira instance using OAuth authentication.
Returns a JSON string containing the matching issues with their details.
Use the start and limit parameters to paginate through large result sets.
type: tool
annotations:
title: JQL Query (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: query
type: string
description: |
The JQL query string to execute. Examples:
- "project = TEST" to find all issues in the TEST project
- "assignee = currentUser()" to find issues assigned to you
- "status = 'In Progress'" to find issues in progress
examples: [
"project = TEST",
"status = 'In Progress'",
"project = TEST AND status = 'Done'",
"created >= -30d ORDER BY created DESC"
]
- name: start
type: integer
description: |
The index of the first result to return (0-based).
Use this for pagination: start=0 for first page, start=50 for second page, etc.
Defaults to 0 if not specified.
examples: [0, 50, 100]
- name: limit
type: integer
description: |
Maximum number of results to return.
If not specified, returns all matching results.
Recommended to use with start parameter for pagination.
examples: [50, 100, 200]
return:
type: string
description: |
A JSON string containing an array of Jira issues.
language: "sql"
source:
file: "../sql/jql.sql"

View File

@@ -0,0 +1,21 @@
mxcp: 1
tool:
name: list_projects
description: |
List all projects in your Jira instance using OAuth authentication.
Returns a JSON string containing an array of projects with their details.
type: tool
annotations:
title: List Projects (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
return:
type: string
description: |
A JSON string containing an array of Jira projects.
language: "sql"
source:
file: "../sql/list_projects.sql"