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,145 @@
# MXCP Jira Python Endpoints Example
This example demonstrates how to use MXCP with Jira data using Python endpoints. This approach uses Python functions directly as MCP tools.
## Overview
This example provides Python MCP endpoints that allow you to:
- Execute JQL queries to search issues
- Get detailed information for specific issues
- Get user information
- List projects and their details
- Get project metadata
## Implementation Approach
This example uses Python functions that are exposed as MCP tools:
- Python functions handle the Jira API interactions
- Tool definitions map to these Python functions
- Results are returned as JSON data
## Configuration
### 1. Creating an Atlassian API Token
**Important:** This plugin currently only supports API tokens **without scopes**. While Atlassian has introduced scoped API tokens, there are known compatibility issues when using scoped tokens with basic authentication that this plugin relies on.
To create an API token without scopes:
1. **Log in to your Atlassian account** at [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
2. **Verify your identity** (if prompted):
- Atlassian may ask you to verify your identity before creating API tokens
- Check your email for a one-time passcode and enter it when prompted
3. **Create the API token**:
- Click **"Create API token"** (not "Create API token with scopes")
- Enter a descriptive name for your token (e.g., "MXCP Jira Python Integration")
- Select an expiration date (tokens can last from 1 day to 1 year)
- Click **"Create"**
4. **Copy and save your token**:
- Click **"Copy to clipboard"** to copy the token
- **Important:** Save this token securely (like in a password manager) as you won't be able to view it again
- This token will be used as your "password" in the configuration below
### 2. User Configuration
Add the following to your MXCP user config (`~/.mxcp/config.yml`):
```yaml
mxcp: 1
projects:
jira-demo:
profiles:
default:
secrets:
- name: "jira"
type: "python"
parameters:
url: "https://your-domain.atlassian.net"
username: "your-email@example.com"
password: "your-api-token" # Use the API token you created above
```
### 3. Site Configuration
Create an `mxcp-site.yml` file:
```yaml
mxcp: 1
project: jira-demo
profile: default
secrets:
- jira
```
## Available Tools
### JQL Query
Execute JQL queries:
```bash
mxcp run tool jql_query --param query="project = TEST" --param limit=10
```
### Get Issue
Get detailed information for a specific issue by its key:
```bash
mxcp run tool get_issue --param issue_key="RD-123"
```
### Get User
Get a specific user by their account ID:
```bash
mxcp run tool get_user --param account_id="557058:ab168c94-8485-405c-88e6-6458375eb30b"
```
### Search Users
Search for users by name, email, or other criteria:
```bash
mxcp run tool search_user --param query="john.doe@example.com"
```
### List Projects
List all projects:
```bash
mxcp run tool list_projects
```
### Get Project
Get project details:
```bash
mxcp run tool get_project --param project_key="TEST"
```
### Get Project Roles
Get all roles available in a project:
```bash
mxcp run tool get_project_roles --param project_key="TEST"
```
### Get Project Role Users
Get users and groups for a specific role in a project:
```bash
mxcp run tool get_project_role_users --param project_key="TEST" --param role_name="Developers"
```
## Project Structure
```
jira-python/
├── mxcp-site.yml # Site configuration
├── python/ # Python implementations
│ └── jira_endpoints.py # All JIRA endpoint functions
├── tools/ # Tool definitions
│ ├── jql_query.yml
│ ├── get_issue.yml
│ ├── get_user.yml
│ ├── search_user.yml
│ ├── list_projects.yml
│ ├── get_project.yml
│ ├── get_project_roles.yml
│ └── get_project_role_users.yml
└── README.md
```

View File

@@ -0,0 +1,17 @@
mxcp: 1
# Sample configuration file for JIRA Python endpoints example
# Copy this to ~/.mxcp/config.yml and update with your JIRA details
projects:
jira-demo:
profiles:
default:
secrets:
- name: "jira"
type: "python"
parameters:
url: "${JIRA_URL}"
username: "${JIRA_USERNAME}"
password: "${JIRA_API_TOKEN}"

View File

@@ -0,0 +1,5 @@
mxcp: 1
project: jira-demo
profile: default
secrets:
- jira

View File

@@ -0,0 +1,569 @@
"""
JIRA Python Endpoints
This module provides direct Python MCP endpoints for querying Atlassian JIRA.
This is a simpler alternative to the plugin-based approach.
"""
from typing import Dict, Any, List, Optional, Callable
import logging
from atlassian import Jira
from mxcp.runtime import config, on_init, on_shutdown
import threading
import functools
import time
logger = logging.getLogger(__name__)
# Global JIRA client for reuse across all function calls
jira_client: Optional[Jira] = None
# Thread lock to protect client initialization
_client_lock = threading.Lock()
@on_init
def setup_jira_client() -> None:
"""Initialize JIRA client when server starts.
Thread-safe: multiple threads can safely call this simultaneously.
"""
global jira_client
with _client_lock:
logger.info("Initializing JIRA client...")
jira_config = config.get_secret("jira")
if not jira_config:
raise ValueError(
"JIRA configuration not found. Please configure JIRA secrets in your user config."
)
required_keys = ["url", "username", "password"]
missing_keys = [key for key in required_keys if not jira_config.get(key)]
if missing_keys:
raise ValueError(f"Missing JIRA configuration keys: {', '.join(missing_keys)}")
jira_client = Jira(
url=jira_config["url"],
username=jira_config["username"],
password=jira_config["password"],
cloud=True,
)
logger.info("JIRA client initialized successfully")
@on_shutdown
def cleanup_jira_client() -> None:
"""Clean up JIRA client when server stops."""
global jira_client
if jira_client:
# JIRA client doesn't need explicit cleanup, but we'll clear the reference
jira_client = None
logger.info("JIRA client cleaned up")
def retry_on_session_expiration(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator that automatically retries functions on JIRA session expiration.
This only retries on HTTP 401 Unauthorized errors, not other authentication failures.
Retries up to 2 times on session expiration (3 total attempts).
Thread-safe: setup_jira_client() handles concurrent access internally.
Usage:
@retry_on_session_expiration
def my_jira_function():
# Function that might fail due to session expiration
pass
"""
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
max_retries = 2 # Hardcoded: 2 retries = 3 total attempts
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
# Check if this is a 401 Unauthorized error (session expired)
if _is_session_expired(e):
if attempt < max_retries:
logger.warning(
f"Session expired on attempt {attempt + 1} in {func.__name__}: {e}"
)
logger.info(
f"Retrying after re-initializing client (attempt {attempt + 2}/{max_retries + 1})"
)
try:
setup_jira_client() # Thread-safe internally
time.sleep(0.1) # Small delay to avoid immediate retry
except Exception as setup_error:
logger.error(f"Failed to re-initialize JIRA client: {setup_error}")
raise setup_error # Raise the setup error, not the original session error
else:
# Last attempt failed, re-raise the session expiration error
raise e
else:
# Not a session expiration error, re-raise immediately
raise e
return wrapper
def _is_session_expired(exception: Exception) -> bool:
"""Check if the exception indicates a JIRA session has expired."""
error_msg = str(exception).lower()
# Check for HTTP 401 Unauthorized
if "401" in error_msg or "unauthorized" in error_msg:
return True
# Check for common session expiration messages
if any(
phrase in error_msg
for phrase in [
"session expired",
"session invalid",
"authentication failed",
"invalid session",
"session timeout",
]
):
return True
return False
def _get_jira_client() -> Jira:
"""Get the global JIRA client."""
if jira_client is None:
raise RuntimeError("JIRA client not initialized. Make sure the server is started properly.")
return jira_client
@retry_on_session_expiration
def jql_query(
query: str, start: Optional[int] = None, limit: Optional[int] = None
) -> List[Dict[str, Any]]:
"""Execute a JQL query against Jira.
Args:
query: The JQL query string
start: Starting index for pagination (default: None, which becomes 0)
limit: Maximum number of results to return (default: None, meaning no limit)
Returns:
List of Jira issues matching the query
"""
logger.info("Executing JQL query: %s with start=%s, limit=%s", query, start, limit)
jira = _get_jira_client()
raw = jira.jql(
jql=query,
start=start if start is not None else 0,
limit=limit,
fields=(
"key,summary,status,resolution,resolutiondate,"
"assignee,reporter,issuetype,priority,"
"created,updated,labels,fixVersions,parent"
),
)
if not raw:
raise ValueError("JIRA JQL query returned empty result")
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]] = []
jira_url = jira.url
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"{jira_url}/browse/{issue['key']}", # web UI URL
}
)
return cleaned
@retry_on_session_expiration
def get_issue(issue_key: str) -> Dict[str, Any]:
"""Get detailed information for a specific JIRA issue by its key.
Args:
issue_key: The issue key (e.g., 'RD-123', 'TEST-456')
Returns:
Dictionary containing comprehensive issue information
Raises:
ValueError: If issue is not found or access is denied
"""
logger.info("Getting issue details for key: %s", issue_key)
jira = _get_jira_client()
# Get issue by key - this method handles the REST API call
issue = jira.issue(issue_key)
# Extract and clean up the most important fields for easier consumption
fields = issue.get("fields", {})
jira_url = jira.url
def _safe_get(obj: Any, key: str, default: Any = None) -> Any:
"""Safely get a value from a dict/object that might be None."""
if obj is None:
return default
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
cleaned_issue = {
"key": issue.get("key"),
"id": issue.get("id"),
"summary": fields.get("summary"),
"description": fields.get("description"),
"status": _safe_get(fields.get("status"), "name"),
"assignee": _safe_get(fields.get("assignee"), "displayName"),
"assignee_account_id": _safe_get(fields.get("assignee"), "accountId"),
"reporter": _safe_get(fields.get("reporter"), "displayName"),
"reporter_account_id": _safe_get(fields.get("reporter"), "accountId"),
"issue_type": _safe_get(fields.get("issuetype"), "name"),
"priority": _safe_get(fields.get("priority"), "name"),
"resolution": _safe_get(fields.get("resolution"), "name"),
"resolution_date": fields.get("resolutiondate"),
"created": fields.get("created"),
"updated": fields.get("updated"),
"due_date": fields.get("duedate"),
"labels": fields.get("labels", []) or [],
"components": (
[comp.get("name") for comp in fields.get("components", []) if comp and comp.get("name")]
if fields.get("components")
else []
),
"fix_versions": (
[ver.get("name") for ver in fields.get("fixVersions", []) if ver and ver.get("name")]
if fields.get("fixVersions")
else []
),
"project": {
"key": _safe_get(fields.get("project"), "key"),
"name": _safe_get(fields.get("project"), "name"),
},
"parent": _safe_get(fields.get("parent"), "key"),
"url": f"{jira_url}/browse/{issue.get('key')}",
}
return cleaned_issue
@retry_on_session_expiration
def get_user(account_id: str) -> Dict[str, Any]:
"""Get a specific user by their unique account ID.
Args:
account_id: The unique Atlassian account ID for the user.
Example: "557058:ab168c94-8485-405c-88e6-6458375eb30b"
Returns:
Dictionary containing filtered user details
Raises:
ValueError: If user is not found or account ID is invalid
"""
logger.info("Getting user details for account ID: %s", account_id)
jira = _get_jira_client()
# Get user by account ID - pass as account_id parameter for Jira Cloud
user = jira.user(account_id=account_id)
# Return only the requested fields
return {
"accountId": user.get("accountId"),
"displayName": user.get("displayName"),
"emailAddress": user.get("emailAddress"),
"active": user.get("active"),
"timeZone": user.get("timeZone"),
}
@retry_on_session_expiration
def search_user(query: str) -> List[Dict[str, Any]]:
"""Search for users by query string (username, email, or display name).
Args:
query: Search term - can be username, email, display name, or partial matches.
Examples: "ben@raw-labs.com", "Benjamin Gaidioz", "ben", "benjamin", "gaidioz"
Returns:
List of matching users with filtered fields. Empty list if no matches found.
"""
logger.info("Searching for users with query: %s", query)
jira = _get_jira_client()
# user_find_by_user_string returns a list of users matching the query
users = jira.user_find_by_user_string(query=query)
if not users:
return []
# Filter users to only include relevant fields
filtered_users = []
for user in users:
filtered_users.append(
{
"accountId": user.get("accountId"),
"displayName": user.get("displayName"),
"emailAddress": user.get("emailAddress"),
"active": user.get("active"),
"timeZone": user.get("timeZone"),
}
)
return filtered_users
@retry_on_session_expiration
def list_projects() -> List[Dict[str, Any]]:
"""Return a concise list of Jira projects.
Returns:
List of dictionaries containing project information
"""
logger.info("Listing all projects")
jira = _get_jira_client()
raw_projects: List[Dict[str, Any]] = jira.projects(expand="lead")
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]] = []
jira_url = jira.url
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"{jira_url}/projects/{p.get('key')}", # web UI URL
}
)
return concise
@retry_on_session_expiration
def get_project(project_key: str) -> Dict[str, Any]:
"""Get details for a specific project by its key.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
Returns:
Dictionary containing the project details
Raises:
ValueError: If project is not found or access is denied
"""
logger.info("Getting project details for key: %s", project_key)
jira = _get_jira_client()
try:
info = jira.project(project_key)
except Exception as e:
# Handle various possible errors from the JIRA API
error_msg = str(e).lower()
if "404" in error_msg or "not found" in error_msg:
raise ValueError(f"Project '{project_key}' not found in JIRA")
elif "403" in error_msg or "forbidden" in error_msg:
raise ValueError(f"Access denied to project '{project_key}' in JIRA")
else:
# Re-raise other errors with context
raise ValueError(f"Error retrieving project '{project_key}': {e}") from e
# Filter to essential fields only to avoid response size issues
cleaned_info = {
"key": info.get("key"),
"name": info.get("name"),
"description": info.get("description"),
"projectTypeKey": info.get("projectTypeKey"),
"simplified": info.get("simplified"),
"style": info.get("style"),
"isPrivate": info.get("isPrivate"),
"archived": info.get("archived"),
}
# Add lead info if present
if "lead" in info and info["lead"]:
cleaned_info["lead"] = {
"displayName": info["lead"].get("displayName"),
"emailAddress": info["lead"].get("emailAddress"),
"accountId": info["lead"].get("accountId"),
"active": info["lead"].get("active"),
}
cleaned_info["url"] = f"{jira.url}/projects/{project_key}"
return cleaned_info
@retry_on_session_expiration
def get_project_roles(project_key: str) -> List[Dict[str, Any]]:
"""Get all roles available in a project.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
Returns:
List of roles available in the project
Raises:
ValueError: If project is not found or access is denied
"""
logger.info("Getting project roles for key: %s", project_key)
jira = _get_jira_client()
try:
# Get all project roles using the correct method
project_roles = jira.get_project_roles(project_key)
result = []
for role_name, role_url in project_roles.items():
# Extract role ID from URL (e.g., "https://domain.atlassian.net/rest/api/3/project/10000/role/10002")
role_id = role_url.split("/")[-1]
result.append({"name": role_name, "id": role_id})
return result
except Exception as e:
# Handle various possible errors from the JIRA API
error_msg = str(e).lower()
if "404" in error_msg or "not found" in error_msg:
raise ValueError(f"Project '{project_key}' not found in JIRA")
elif "403" in error_msg or "forbidden" in error_msg:
raise ValueError(f"Access denied to project '{project_key}' in JIRA")
else:
# Re-raise other errors with context
raise ValueError(f"Error retrieving project roles for '{project_key}': {e}") from e
@retry_on_session_expiration
def get_project_role_users(project_key: str, role_name: str) -> Dict[str, Any]:
"""Get users and groups for a specific role in a project.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
role_name: The name of the role to get users for
Returns:
Dictionary containing users and groups for the specified role
Raises:
ValueError: If project or role is not found, or access is denied
"""
logger.info("Getting users for role '%s' in project '%s'", role_name, project_key)
jira = _get_jira_client()
try:
# First get all project roles to find the role ID
project_roles = jira.get_project_roles(project_key)
if role_name not in project_roles:
available_roles = list(project_roles.keys())
raise ValueError(
f"Role '{role_name}' not found in project '{project_key}'. Available roles: {available_roles}"
)
# Extract role ID from URL
role_url = project_roles[role_name]
role_id = role_url.split("/")[-1]
# Get role details including actors (users and groups)
role_details = jira.get_project_actors_for_role_project(project_key, role_id)
result = {
"project_key": project_key,
"role_name": role_name,
"role_id": role_id,
"users": [],
"groups": [],
}
# Process actors (role_details is a list of actors)
if isinstance(role_details, list):
for actor in role_details:
if isinstance(actor, dict):
actor_type = actor.get("type", "")
if actor_type == "atlassian-user-role-actor":
# Individual user
user_info = {
"accountId": actor.get("actorUser", {}).get("accountId"),
"displayName": actor.get("displayName"),
}
result["users"].append(user_info)
elif actor_type == "atlassian-group-role-actor":
# Group
group_info = {
"name": actor.get("displayName"),
"groupId": actor.get("actorGroup", {}).get("groupId"),
}
result["groups"].append(group_info)
else:
# Handle other actor types or simple user entries
display_name = actor.get("displayName") or actor.get("name")
if display_name:
user_info = {
"accountId": actor.get("accountId"),
"displayName": display_name,
}
result["users"].append(user_info)
return result
except ValueError:
# Re-raise ValueError as-is (these are our custom error messages)
raise
except Exception as e:
# Handle various possible errors from the JIRA API
error_msg = str(e).lower()
# Don't handle 401 errors here - let the retry decorator handle them
if "401" in error_msg or "unauthorized" in error_msg:
raise e # Let the retry decorator catch this
elif "404" in error_msg or "not found" in error_msg:
raise ValueError(f"Project '{project_key}' not found in JIRA")
elif "403" in error_msg or "forbidden" in error_msg:
raise ValueError(f"Access denied to project '{project_key}' in JIRA")
else:
# Re-raise other errors with context
raise ValueError(
f"Error retrieving users for role '{role_name}' in project '{project_key}': {e}"
) from e

View File

@@ -0,0 +1,114 @@
mxcp: 1
tool:
name: get_issue
description: |
Get detailed information for a specific JIRA issue by its key.
Returns comprehensive issue information including all fields, assignee, reporter, etc.
type: tool
annotations:
title: Get Issue
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: issue_key
type: string
description: |
The issue key (e.g., 'RD-123', 'TEST-456').
This is the unique identifier for the issue visible in the Jira UI.
examples: [
"RD-123",
"TEST-456",
"PROJ-789"
]
return:
type: object
properties:
key:
type: string
description: The issue key
id:
type: string
description: The issue ID
summary:
type: string
description: The issue summary
description:
type: string
description: The issue description
status:
type: string
description: The current status
assignee:
type: string
description: The assignee display name
assignee_account_id:
type: string
description: The assignee account ID
reporter:
type: string
description: The reporter display name
reporter_account_id:
type: string
description: The reporter account ID
issue_type:
type: string
description: The issue type
priority:
type: string
description: The priority level
resolution:
type: string
description: The resolution
resolution_date:
type: string
description: The resolution date
created:
type: string
description: The creation date
updated:
type: string
description: The last update date
due_date:
type: string
description: The due date
labels:
type: array
items:
type: string
description: The issue labels
components:
type: array
items:
type: string
description: The issue components
fix_versions:
type: array
items:
type: string
description: The fix versions
project:
type: object
properties:
key:
type: string
name:
type: string
description: The project information
parent:
type: string
description: The parent issue key
url:
type: string
description: The issue URL
tests:
- name: "Get issue by key"
description: "Verify issue retrieval returns expected structure"
arguments:
- key: issue_key
value: "RD-15333"

View File

@@ -0,0 +1,76 @@
mxcp: 1
tool:
name: get_project
description: |
Get details for a specific project by its key.
Returns comprehensive project information including description, settings, and lead.
type: tool
annotations:
title: Get Project
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: project_key
type: string
description: |
The project key (e.g., 'TEST' for project TEST).
This is the short identifier for the project.
examples: [
"TEST",
"PROJ",
"DEV"
]
return:
type: object
properties:
key:
type: string
description: The project key
name:
type: string
description: The project name
description:
type: string
description: The project description
projectTypeKey:
type: string
description: The project type key
simplified:
type: boolean
description: Whether the project is simplified
style:
type: string
description: The project style
isPrivate:
type: boolean
description: Whether the project is private
archived:
type: boolean
description: Whether the project is archived
lead:
type: object
properties:
displayName:
type: string
emailAddress:
type: string
accountId:
type: string
active:
type: boolean
description: The project lead information
url:
type: string
description: The project URL
tests:
- name: "Get project by key"
description: "Verify project retrieval returns expected structure"
arguments:
- key: project_key
value: "RD"

View File

@@ -0,0 +1,78 @@
mxcp: 1
tool:
name: get_project_role_users
description: |
Get users and groups for a specific role in a project.
Returns detailed information about users and groups assigned to the role.
type: tool
annotations:
title: Get Project Role Users
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: project_key
type: string
description: |
The project key (e.g., 'TEST' for project TEST).
This is the short identifier for the project.
examples: [
"TEST",
"PROJ",
"DEV"
]
- name: role_name
type: string
description: |
The name of the role to get users for.
Common roles include 'Administrators', 'Developers', 'Users'.
examples: [
"Administrators",
"Developers",
"Users"
]
return:
type: object
properties:
project_key:
type: string
description: The project key
role_name:
type: string
description: The role name
role_id:
type: string
description: The role ID
users:
type: array
items:
type: object
properties:
accountId:
type: string
displayName:
type: string
description: List of users in the role
groups:
type: array
items:
type: object
properties:
name:
type: string
groupId:
type: string
description: List of groups in the role
tests:
- name: "Get role users"
description: "Verify role users returns expected structure"
arguments:
- key: project_key
value: "RD"
- key: role_name
value: "Administrators"

View File

@@ -0,0 +1,45 @@
mxcp: 1
tool:
name: get_project_roles
description: |
Get all roles available in a project.
Returns a list of roles with their IDs and URLs.
type: tool
annotations:
title: Get Project Roles
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: project_key
type: string
description: |
The project key (e.g., 'TEST' for project TEST).
This is the short identifier for the project.
examples: [
"TEST",
"PROJ",
"DEV"
]
return:
type: array
items:
type: object
properties:
name:
type: string
description: The role name
id:
type: string
description: The role ID
tests:
- name: "Get project roles"
description: "Verify project roles returns array of roles"
arguments:
- key: project_key
value: "RD"

View File

@@ -0,0 +1,53 @@
mxcp: 1
tool:
name: get_user
description: |
Get a specific user by their unique account ID.
Returns detailed user information including display name, email, and account status.
type: tool
annotations:
title: Get User
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: account_id
type: string
description: |
The unique Atlassian account ID for the user.
This is typically in the format: "557058:ab168c94-8485-405c-88e6-6458375eb30b"
You can get account IDs from other API calls like get_issue or search_user.
examples: [
"557058:ab168c94-8485-405c-88e6-6458375eb30b",
"5b10ac8d82e05b22cc7d4ef5",
"712020:0e99e8b3-7b3a-4b7c-9a1f-9e5d8c7b4a3e"
]
return:
type: object
properties:
accountId:
type: string
description: The account ID
displayName:
type: string
description: The display name
emailAddress:
type: string
description: The email address
active:
type: boolean
description: Whether the user is active
timeZone:
type: string
description: The user's time zone
tests:
- name: "Get user by account ID"
description: "Just run the tool"
arguments:
- key: account_id
value: "557058:ab168c94-8485-405c-88e6-6458375eb30b"

View File

@@ -0,0 +1,84 @@
mxcp: 1
tool:
name: jql_query
description: |
Execute a JQL (Jira Query Language) query to search for issues in your Jira instance.
Returns a list of issues with their details.
Use the start and limit parameters to paginate through large result sets.
type: tool
annotations:
title: JQL Query
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
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.
default: 0
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]
default: null
return:
type: array
items:
type: object
properties:
key:
type: string
summary:
type: string
status:
type: string
assignee:
type: string
reporter:
type: string
created:
type: string
updated:
type: string
url:
type: string
tests:
- name: "Basic project query"
description: "Verify JQL query returns array of issues"
arguments:
- key: query
value: "project = RD"
- key: limit
value: 1
- name: "Status filter query"
description: "Verify JQL query with status filter"
arguments:
- key: query
value: "status = 'In Progress'"
- key: limit
value: 1

View File

@@ -0,0 +1,42 @@
mxcp: 1
tool:
name: list_projects
description: |
Return a concise list of Jira projects.
Returns basic project information including key, name, type, and lead.
type: tool
annotations:
title: List Projects
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters: []
return:
type: array
items:
type: object
properties:
key:
type: string
description: The project key
name:
type: string
description: The project name
type:
type: string
description: The project type
lead:
type: string
description: The project lead
url:
type: string
description: The project URL
tests:
- name: "List all projects"
description: "Verify projects list returns array of projects"
arguments: []

View File

@@ -0,0 +1,56 @@
mxcp: 1
tool:
name: search_user
description: |
Search for users by query string (username, email, or display name).
Returns a list of matching users with their details.
type: tool
annotations:
title: Search User
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: query
type: string
description: |
Search term - can be username, email, display name, or partial matches.
The search is case-insensitive and supports partial matching.
examples: [
"ben@raw-labs.com",
"Benjamin Gaidioz",
"ben",
"benjamin",
"gaidioz"
]
return:
type: array
items:
type: object
properties:
accountId:
type: string
description: The account ID
displayName:
type: string
description: The display name
emailAddress:
type: string
description: The email address
active:
type: boolean
description: Whether the user is active
timeZone:
type: string
description: The user's time zone
tests:
- name: "Search by name"
description: "Verify user search by name returns results"
arguments:
- key: query
value: "Ben"