Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Install the project
run: uv sync --locked --all-extras --dev

- name: Lint with ruff
run: uv run ruff check .

- name: Format with ruff
run: uv run ruff format --check .

# - name: Type check with mypy
# run: uv run mypy .

- name: Test with pytest
run: uv run -m pytest
70 changes: 70 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Publish Package

on:
release:
types: [created]

jobs:
pypi-publish:
name: Upload release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/jules-mcp
permissions:
id-token: write # this is required for trusted publishing

steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Build package
run: uv build
- uses: actions/upload-artifact@v4
with:
name: artifact
path: dist/*
- name: Check package
run: uvx twine check dist/*
- name: Publish package
uses: pypa/gh-action-pypi-publish@v1.9.0


docker-publish:
name: Upload release to GitHub Container Registry
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Use an official Python runtime as a parent image
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim

# Set the working directory in the container
WORKDIR /app

# Copy the dependency files
COPY pyproject.toml uv.lock ./

# Install dependencies
RUN uv sync --no-dev

# Copy the rest of the application code
COPY jules_mcp/ ./jules_mcp

ENV JULES_API_KEY=""

# Set the entrypoint
ENTRYPOINT ["uv", "run", "--with", "fastmcp", "--with", "jules-agent-sdk", "--with", "requests", "fastmcp", "run", "jules_mcp/jules_mcp.py"]
1 change: 0 additions & 1 deletion jules_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Copyright (C) 2025 Yurii Serhiichuk
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
3 changes: 1 addition & 2 deletions jules_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Copyright (C) 2025 Yurii Serhiichuk
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -15,5 +14,5 @@

from jules_mcp.jules_mcp import start_mcp

if __name__ == '__main__':
if __name__ == "__main__":
start_mcp()
56 changes: 33 additions & 23 deletions jules_mcp/jules_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ def jules(api_key: str | None = None) -> JulesClient:
description="Get a single source by ID (e.g., sources/abc123 or abc123)",
tags={"sources"},
annotations=ToolAnnotations(
title="Get Jules source by ID", readOnlyHint=True, idempotentHint=True,
destructiveHint=False, openWorldHint=True,
)
title="Get Jules source by ID",
readOnlyHint=True,
idempotentHint=True,
destructiveHint=False,
openWorldHint=True,
),
)
def get_source(source_id: str) -> models.Source:
"""Get a single source by ID.
Expand All @@ -68,9 +71,9 @@ def get_source(source_id: str) -> models.Source:
tags={"sources"},
)
def list_sources(
filter_str: str | None = None,
page_size: int | None = None,
page_token: str | None = None,
filter_str: str | None = None,
page_size: int | None = None,
page_token: str | None = None,
) -> dict[str, Any]:
"""List sources (paginated).

Expand All @@ -84,7 +87,9 @@ def list_sources(
- sources: list[Source]
- nextPageToken: Optional[str]
"""
result = jules().sources.list(filter_str=filter_str, page_size=page_size, page_token=page_token)
result = jules().sources.list(
filter_str=filter_str, page_size=page_size, page_token=page_token
)
return result


Expand Down Expand Up @@ -115,11 +120,11 @@ def get_all_sources(filter_str: str | None = None) -> list[models.Source]:
tags={"sessions"},
)
def create_session(
prompt: str,
source: str,
starting_branch: str | None = None,
title: str | None = None,
require_plan_approval: bool = False,
prompt: str,
source: str,
starting_branch: str | None = None,
title: str | None = None,
require_plan_approval: bool = False,
) -> models.Session:
"""Create a new session.

Expand Down Expand Up @@ -161,7 +166,9 @@ def get_session(session_id: str) -> models.Session:
description="List sessions with pagination; returns items and nextPageToken.",
tags={"sessions"},
)
def list_sessions(page_size: int | None = None, page_token: str | None = None) -> dict[str, Any]:
def list_sessions(
page_size: int | None = None, page_token: str | None = None
) -> dict[str, Any]:
"""List sessions (paginated).

Args:
Expand Down Expand Up @@ -222,9 +229,9 @@ def send_session_message(session_id: str, prompt: str) -> dict[str, str]:
tags={"sessions"},
)
def wait_for_session_completion(
session_id: str,
poll_interval: int = 5,
timeout: int | None = 600,
session_id: str,
poll_interval: int = 5,
timeout: int | None = 600,
) -> models.Session:
"""Wait for a session to reach a terminal state.

Expand All @@ -233,8 +240,9 @@ def wait_for_session_completion(
poll_interval: Seconds between polling requests (default: 5).
timeout: Optional timeout in seconds (default: 600). Set None for no timeout.
"""
return jules().sessions.wait_for_completion(session_id, poll_interval=poll_interval,
timeout=timeout)
return jules().sessions.wait_for_completion(
session_id, poll_interval=poll_interval, timeout=timeout
)


# -------------------- Activities --------------------
Expand All @@ -261,9 +269,9 @@ def get_activity(session_id: str, activity_id: str) -> models.Activity:
tags={"activities"},
)
def list_activities(
session_id: str,
page_size: int | None = None,
page_token: str | None = None,
session_id: str,
page_size: int | None = None,
page_token: str | None = None,
) -> dict[str, Any]:
"""List activities for a session (paginated).

Expand All @@ -277,7 +285,9 @@ def list_activities(
- activities: list[Activity]
- nextPageToken: Optional[str]
"""
return jules().activities.list(session_id, page_size=page_size, page_token=page_token)
return jules().activities.list(
session_id, page_size=page_size, page_token=page_token
)


@mcp.tool(
Expand All @@ -295,5 +305,5 @@ def start_mcp() -> None:
mcp.run()


if __name__ == '__main__':
if __name__ == "__main__":
start_mcp()
Empty file added jules_mcp/py.typed
Empty file.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ dependencies = [

[scripts]

[tool.setuptools.packages.find]
where = ["."]

[dependency-groups]
dev = [
"pytest>=8.4.2",
"pytest-asyncio>=1.2.0",
"types-requests>=2.32.4.20250913",
"ruff>=0.5.5",
"mypy>=1.11.0",
]
7 changes: 7 additions & 0 deletions tests/test_jules_mcp/test_jules_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os

import pytest
from fastmcp import Client

from jules_mcp import mcp

requires_api_key = pytest.mark.skipif(
not os.environ.get("JULES_API_KEY"), reason="JULES_API_KEY is not set"
)


@pytest.mark.asyncio
@requires_api_key
async def test_tool_execution():
client: Client
async with Client(mcp) as client:
Expand Down
Loading