"""MCP Server for running Gradle tasks."""
import os
from pathlib import Path
from fastmcp import Context, FastMCP
from pydantic import BaseModel
from gradle_mcp.gradle import GradleWrapper
# MCP Server instructions for AI agents
MCP_INSTRUCTIONS = """
# *gradle-mcp*
This MCP server provides tools to interact with Gradle projects without using command-line operations.
Use these tools instead of running `./gradlew` or `gradle` commands directly in the terminal.
## Usage Guidelines
- use `list_projects` to explore available projects
- use `list_project_tasks` to see tasks before running them
- Use `run_task` for building, testing, assembling, etc.
- Always use `clean` tool for cleaning operations, never `run_task` with clean tasks
- Use qualified task names (e.g., ':app:build') when targeting specific subprojects
- Pass '--info' or '--debug' in args for verbose output when troubleshooting
"""
# Initialize FastMCP server
mcp = FastMCP(name="gradle-mcp", instructions=MCP_INSTRUCTIONS)
class TaskResult(BaseModel):
"""Result of running a Gradle task."""
success: bool
error: str | None = None
class ProjectInfo(BaseModel):
"""Information about a Gradle project."""
name: str
path: str
description: str | None = None
class TaskInfo(BaseModel):
"""Information about a Gradle task."""
name: str
project: str
description: str | None = None
group: str | None = None
class TaskWithDescriptionInfo(BaseModel):
"""Task with its description."""
name: str
description: str
class GroupedTasksInfo(BaseModel):
"""Tasks grouped by their group name.
When include_descriptions is True, tasks contains TaskWithDescriptionInfo objects.
When include_descriptions is False, tasks contains task name strings.
"""
group: str
tasks: list[TaskWithDescriptionInfo] | list[str]
def _get_gradle_wrapper(ctx: Context | None = None) -> GradleWrapper:
"""Get a GradleWrapper instance, optionally using context to determine project root.
Respects the following environment variables:
- GRADLE_PROJECT_ROOT: Root directory of Gradle project (default: current directory)
- GRADLE_WRAPPER: Path to Gradle wrapper script (optional, auto-detected if not set)
Args:
ctx: MCP Context (optional).
Returns:
GradleWrapper instance.
Raises:
FileNotFoundError: If Gradle wrapper cannot be found.
"""
project_root = os.getenv("GRADLE_PROJECT_ROOT") or os.getcwd()
wrapper_path = os.getenv("GRADLE_WRAPPER")
gradle = GradleWrapper(project_root)
# If a custom wrapper path is specified, override the auto-detected one
if wrapper_path:
wrapper_path_obj = Path(wrapper_path)
if not wrapper_path_obj.exists():
raise FileNotFoundError(
f"Gradle wrapper not found at specified path: {wrapper_path}. "
"Please verify GRADLE_WRAPPER environment variable."
)
gradle.wrapper_script = wrapper_path_obj
return gradle
@mcp.tool()
async def list_projects(ctx: Context) -> list[ProjectInfo]:
"""List all Gradle projects in the workspace.
Returns:
List of Gradle projects.
"""
try:
await ctx.info("Listing all Gradle projects")
gradle = _get_gradle_wrapper(ctx)
projects = gradle.list_projects()
await ctx.info(f"Found {len(projects)} projects: {', '.join(p.name for p in projects)}")
return [
ProjectInfo(
name=p.name,
path=p.path,
description=p.description,
)
for p in projects
]
except Exception as e:
raise ValueError(f"Failed to list projects: {str(e)}") from e
@mcp.tool()
async def list_project_tasks(
project: str | None = None,
include_descriptions: bool = False,
group: str | None = None,
ctx: Context | None = None,
) -> list[GroupedTasksInfo]:
"""List all tasks available in a Gradle project.
Returns a nested structure grouped by task group. This is more compact
than a flat list and avoids repeating project/group information for each task.
Args:
project: Project path (e.g., ':app' or 'lib:module').
Use None, empty string, or ':' for root project.
include_descriptions: If True, include task descriptions in the response.
If False, return only task names for a more compact response.
group: Optional group name to filter tasks (e.g., 'Build', 'Verification').
Case-insensitive. If not provided, all groups are returned.
Returns:
List of grouped tasks. Each group contains a group name and list of tasks.
If include_descriptions is True, tasks include name and description.
If include_descriptions is False, tasks are just name strings.
"""
try:
if ctx:
await ctx.info(f"Listing tasks for project: {project or 'root'}")
gradle = _get_gradle_wrapper(ctx)
# Normalize root project: None, empty, or ":" all mean root
project_arg = project if project and project != "" else ":"
grouped_tasks = gradle.list_tasks(project_arg, include_descriptions, group)
if ctx:
total_tasks = sum(len(g.tasks) for g in grouped_tasks)
group_info = f" (filtered by group '{group}')" if group else ""
await ctx.info(
f"Found {total_tasks} tasks in {len(grouped_tasks)} groups for project {project_arg}{group_info}"
)
# Convert to response model
result = []
for group in grouped_tasks:
if include_descriptions:
# Tasks are TaskWithDescription objects
tasks = [
TaskWithDescriptionInfo(name=t.name, description=t.description)
for t in group.tasks
]
else:
# Tasks are already strings
tasks = group.tasks
result.append(GroupedTasksInfo(group=group.group, tasks=tasks))
return result
except Exception as e:
raise ValueError(f"Failed to list tasks: {str(e)}") from e
@mcp.tool()
async def run_task(
task: str | list[str],
args: list[str] | None = None,
ctx: Context | None = None,
) -> TaskResult:
"""Run one or more Gradle tasks.
Args:
task: Task(s) to run. Single task, space-separated tasks, or list of tasks.
Examples: 'build', ':app:build :core:build', [':core:build', ':app:assemble'].
args: Optional Gradle arguments (e.g., ['--info', '-x', 'test']).
Returns:
TaskResult with success status and error message if failed.
"""
try:
# Normalize to list - handle space-separated string or list
if isinstance(task, str):
tasks = task.split()
else:
tasks = task
task_str = ", ".join(tasks)
if ctx:
await ctx.info(f"Running task(s): {task_str}" + (f" with args: {args}" if args else ""))
gradle = _get_gradle_wrapper(ctx)
# run_task handles multiple tasks and progress reporting
result = await gradle.run_task(tasks, args, ctx)
# Log Gradle output
if ctx:
if result.get("stdout"):
await ctx.debug(f"Gradle stdout:\n{result['stdout']}")
if result.get("stderr"):
await ctx.debug(f"Gradle stderr:\n{result['stderr']}")
if result["success"]:
await ctx.info(f"Task(s) {task_str} completed successfully")
else:
await ctx.error(f"Task(s) {task_str} failed", extra={"error": result.get("error")})
return TaskResult(
success=result["success"],
error=result.get("error"),
)
except ValueError as e:
# Task is a cleaning task
raise ValueError(str(e)) from e
except Exception as e:
# Report error progress
if ctx:
await ctx.report_progress(progress=100, total=100)
return TaskResult(
success=False,
error=str(e),
)
@mcp.tool()
async def clean(
project: str | None = None,
ctx: Context | None = None,
) -> TaskResult:
"""Clean build artifacts for a Gradle project.
Args:
project: Project path (e.g., ':app').
Use None, empty string, or ':' for root project.
Returns:
TaskResult with success status and error message if failed.
"""
try:
if ctx:
await ctx.info(f"Cleaning project: {project or 'root'}")
gradle = _get_gradle_wrapper(ctx)
# clean now handles progress reporting internally by parsing Gradle output
result = await gradle.clean(project, ctx)
# Log Gradle output
if ctx:
if result.get("stdout"):
await ctx.debug(f"Gradle stdout:\n{result['stdout']}")
if result.get("stderr"):
await ctx.debug(f"Gradle stderr:\n{result['stderr']}")
if result["success"]:
await ctx.info(f"Clean completed successfully for project {project or 'root'}")
else:
await ctx.error(
f"Clean failed for project {project or 'root'}",
extra={"error": result.get("error")},
)
return TaskResult(
success=result["success"],
error=result.get("error"),
)
except Exception as e:
return TaskResult(
success=False,
error=str(e),
)
def main() -> None:
"""Run the MCP server."""
mcp.run()
if __name__ == "__main__":
main()