Skip to content

feat: add scheduled sessions, export, workflow, and repo management tools#42

Merged
jeremyeder merged 1 commit intoambient-code:mainfrom
jeremyeder:feat/scheduled-sessions-and-new-tools
Apr 1, 2026
Merged

feat: add scheduled sessions, export, workflow, and repo management tools#42
jeremyeder merged 1 commit intoambient-code:mainfrom
jeremyeder:feat/scheduled-sessions-and-new-tools

Conversation

@jeremyeder
Copy link
Copy Markdown
Contributor

@jeremyeder jeremyeder commented Apr 1, 2026

Summary

Add 15 new MCP tools expanding the server from 26 to 41 tools:

  • Scheduled sessions (9 tools): full CRUD lifecycle for cron-based scheduled AgenticSessions
  • Session export (1 tool): export session chat as markdown
  • Workflow management (2 tools): set active workflow, get workflow metadata
  • Repo management (3 tools): add repo, remove repo, get clone status

All 15 tools follow the established 4-step pattern: client method → formatter → tool definition + dispatch → unit tests.

Tool Capabilities (41 total)

New in this PR

# Tool Category Description
1 acp_list_scheduled_sessions Scheduled Sessions List all cron-based scheduled sessions
2 acp_get_scheduled_session Scheduled Sessions Get details of a scheduled session
3 acp_create_scheduled_session Scheduled Sessions Create a scheduled session with cron schedule
4 acp_update_scheduled_session Scheduled Sessions Update schedule, template, or suspend state
5 acp_delete_scheduled_session Scheduled Sessions Delete a scheduled session and its CronJob
6 acp_suspend_scheduled_session Scheduled Sessions Pause a schedule (stop creating sessions)
7 acp_resume_scheduled_session Scheduled Sessions Resume a paused schedule
8 acp_trigger_scheduled_session Scheduled Sessions Manually trigger an immediate run
9 acp_list_scheduled_session_runs Scheduled Sessions List past sessions spawned by a schedule
10 acp_export_session Session Export Export session chat as markdown
11 acp_set_workflow Workflow Mgmt Set active workflow on a session
12 acp_get_workflow_metadata Workflow Mgmt Get workflow steps and config
13 acp_add_repo Repo Mgmt Add a git repo to a running session
14 acp_remove_repo Repo Mgmt Remove a repo from a session
15 acp_get_repos_status Repo Mgmt Check repo clone status

Existing (on main)

# Tool Category Description
16 acp_list_sessions Session Lifecycle List all sessions in a project
17 acp_get_session Session Lifecycle Get details of a specific session
18 acp_create_session Session Lifecycle Create a new session
19 acp_create_session_from_template Session Lifecycle Create from template (triage, bugfix, feature, exploration)
20 acp_delete_session Session Lifecycle Delete a session
21 acp_restart_session Session Lifecycle Restart a session
22 acp_clone_session Session Lifecycle Clone an existing session
23 acp_update_session Session Lifecycle Update session properties
24 acp_get_session_logs Observability Get container logs
25 acp_get_session_transcript Observability Get conversation transcript
26 acp_get_session_metrics Observability Get resource usage metrics
27 acp_label_resource Labels Add labels to a session
28 acp_unlabel_resource Labels Remove labels from a session
29 acp_list_sessions_by_label Labels Find sessions matching label selectors
30 acp_bulk_label_resources Bulk Operations Label multiple sessions by name
31 acp_bulk_unlabel_resources Bulk Operations Unlabel multiple sessions by name
32 acp_bulk_delete_sessions Bulk Operations Delete multiple sessions by name (max 3)
33 acp_bulk_stop_sessions Bulk Operations Stop multiple sessions by name (max 3)
34 acp_bulk_restart_sessions Bulk Operations Restart multiple sessions by name (max 3)
35 acp_bulk_delete_sessions_by_label Bulk Operations Delete sessions matching labels (max 3)
36 acp_bulk_stop_sessions_by_label Bulk Operations Stop sessions matching labels (max 3)
37 acp_bulk_restart_sessions_by_label Bulk Operations Restart sessions matching labels (max 3)
38 acp_list_clusters Cluster & Auth List configured ACP clusters
39 acp_whoami Cluster & Auth Show current user and active cluster
40 acp_switch_cluster Cluster & Auth Switch active cluster
41 acp_login Cluster & Auth Authenticate to a cluster

Test plan

  • uv run pytest tests/ — 232 tests pass
  • uv run ruff check . — no lint issues
  • uv run ruff format . — properly formatted
  • Pre-commit hooks pass
  • Manual validation against staging cluster

Closes #28

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Walkthrough

Adds scheduled sessions (cron) management, session export, workflow and repository management across MCP client/server, formatters, and tests; updates documentation tool count from 26 to 41. No API surface removals or runtime schema/dispatch removals beyond added tools.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md
Updated reported MCP server tool count from 26 to 41.
Client Layer
src/mcp_acp/client.py
Added 14 async methods: scheduled sessions (list, get, create, update, delete, suspend, resume, trigger, list runs), session export, workflow management (set, get metadata), and repo management (add, remove, get status). Methods validate inputs, call REST endpoints, support dry-run for lifecycle ops, and return structured dict results.
Formatters
src/mcp_acp/formatters.py
Added format_scheduled_sessions_list, format_scheduled_session_created, and format_scheduled_session_runs for rendering scheduled-session lists, creation (including dry-run/failure), and run histories.
Server Layer
src/mcp_acp/server.py
Registered and dispatched 15 new MCP tools: scheduled-session CRUD/control/list-runs, acp_export_session, workflow tools, and repo management tools. Added shared JSON schema fragments for inputs and wired new formatters for output rendering.
Client Tests
tests/test_client.py
Added async tests covering scheduled-session CRUD (incl. dry-run), suspend/resume/trigger, run listing, session export, workflow set/get, and repo add/remove/status. HTTP client calls are mocked and responses asserted for expected shapes/fields.
Formatter Tests
tests/test_formatters.py
Added tests for the three new formatter functions including success/failure and dry-run cases and empty-list handling.
Server Tests
tests/test_server.py, tests/test_server_e2e.py
Updated tool-count expectations from 26→41 and expanded expected tool names. Added dispatch unit tests for scheduled-session and export/workflow/repo tools and updated E2E registration assertions.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant MCP_Server
participant ACPClient
participant BackendAPI
User->>MCP_Server: invoke acp_create_scheduled_session tool
MCP_Server->>ACPClient: call create_scheduled_session(...)
ACPClient->>BackendAPI: POST /v1/scheduled-sessions (payload)
BackendAPI-->>ACPClient: 201 Created / simulated dry-run payload
ACPClient-->>MCP_Server: return dict result
MCP_Server->>MCP_Server: format result via formatter
MCP_Server-->>User: tool result text

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR implements acp_export_session tool as required by #28, consuming a backend export endpoint. However, the PR also adds 14 additional tools (scheduled sessions, workflow, repo management) beyond the single tool scope of issue #28. Verify whether the 14 additional tools beyond acp_export_session are within the scope of issue #28 or represent scope creep. Clarify if these were added as part of the same feature request or as a separate effort.
Out of Scope Changes check ❓ Inconclusive The PR adds 15 new tools, but issue #28 specifically requests only acp_export_session. The other 14 tools (scheduled sessions, workflow, repo management) appear to be outside the stated scope of the linked issue. Confirm whether the 14 additional tools were intentionally included as part of this PR or should be separated into distinct pull requests linked to separate issues.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the primary changes: adding scheduled sessions, export, workflow, and repo management tools.
Description check ✅ Passed The description thoroughly documents the 15 new tools added, organized by category, with a complete tool inventory and test plan status.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 84.18367% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/mcp_acp/server.py 62.50% 18 Missing ⚠️
src/mcp_acp/client.py 88.07% 13 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp_acp/client.py`:
- Around line 681-720: The update_scheduled_session method builds a sparse
payload for a partial update but currently uses PUT; change the HTTP verb in the
self._request call inside update_scheduled_session from "PUT" to "PATCH"
(keeping the same path and payload) so semantics match the docstring and other
partial-update endpoints, and update related tests to assert that the client
issues a PATCH request (add an assertion checking the HTTP method) to prevent
regressions.
- Around line 1064-1078: The remove_repo() method is incorrectly validating
repo_name with the DNS-1123 validator used for session names; update remove_repo
to only check repo_name is non-empty (use a simple non-empty string check
instead of self._validate_input(..., "repo_name")), and when building the DELETE
request path call self._request use a URL-encoded path segment for repo_name
(e.g., encode repo_name before interpolating into
f"/v1/sessions/{session}/repos/{repo_name}") so names like "foo_bar" or
"foo.bar" can be removed; keep existing validations for project and session and
continue to return the same dict response after the await self._request call.

In `@src/mcp_acp/server.py`:
- Around line 73-87: The _SESSION_TEMPLATE currently requires "task", but
acp_update_scheduled_session reuses it which prevents partial updates; change
the schema used by the update tool to a non-required version (e.g., create a new
_SESSION_UPDATE_TEMPLATE or clone _SESSION_TEMPLATE and remove the "required"
key / any nested required fields) and update acp_update_scheduled_session to
reference the new schema instead of _SESSION_TEMPLATE; make sure
ACPClient.update_scheduled_session remains accepting partial session_template
values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9265dc19-2eb0-4998-add7-b40cab931d20

📥 Commits

Reviewing files that changed from the base of the PR and between 1605b9c and 488edb4.

📒 Files selected for processing (8)
  • CLAUDE.md
  • src/mcp_acp/client.py
  • src/mcp_acp/formatters.py
  • src/mcp_acp/server.py
  • tests/test_client.py
  • tests/test_formatters.py
  • tests/test_server.py
  • tests/test_server_e2e.py

Comment on lines +681 to +720
async def update_scheduled_session(
self,
project: str,
name: str,
schedule: str | None = None,
display_name: str | None = None,
session_template: dict[str, Any] | None = None,
suspend: bool | None = None,
dry_run: bool = False,
) -> dict[str, Any]:
"""Update a scheduled session (partial update)."""
self._validate_input(project, "project")
self._validate_input(name, "name")

payload: dict[str, Any] = {}
if schedule is not None:
payload["schedule"] = schedule
if display_name is not None:
payload["displayName"] = display_name
if session_template is not None:
payload["sessionTemplate"] = session_template
if suspend is not None:
payload["suspend"] = suspend

if not payload:
raise ValueError("No fields to update. Provide schedule, display_name, session_template, or suspend.")

if dry_run:
return {
"dry_run": True,
"success": True,
"message": f"Would update scheduled session '{name}'",
"patch": payload,
}

try:
await self._request("PUT", f"/v1/scheduled-sessions/{name}", project, json_data=payload)
return {"updated": True, "message": f"Successfully updated scheduled session '{name}'"}
except ValueError as e:
return {"updated": False, "message": f"Failed to update: {str(e)}"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, read the exact code at the specified lines to confirm the HTTP method
sed -n '681,720p' src/mcp_acp/client.py

Repository: ambient-code/mcp

Length of output: 1599


🏁 Script executed:

# Search for any API documentation or contract definitions for scheduled-sessions
rg "scheduled-session|scheduled_session" -A 2 -B 2 --type py | head -100

Repository: ambient-code/mcp

Length of output: 6199


🏁 Script executed:

# Look for tests related to scheduled-session updates
fd test -type f -name "*.py" | xargs rg "update_scheduled_session|scheduled.session" 2>/dev/null | head -50

Repository: ambient-code/mcp

Length of output: 4342


🏁 Script executed:

# Search for any backend API specification or documentation files
fd -e md -e txt -e json | xargs rg "scheduled.session.*PUT|scheduled.session.*PATCH" 2>/dev/null

Repository: ambient-code/mcp

Length of output: 44


🏁 Script executed:

# Check how _request method is defined and what verbs it supports
rg "def _request\(" -A 10 src/mcp_acp/client.py

Repository: ambient-code/mcp

Length of output: 462


🏁 Script executed:

# Find and read the test_update_scheduled_session test
rg "test_update_scheduled_session" -A 20 tests/test_client.py

Repository: ambient-code/mcp

Length of output: 953


🏁 Script executed:

# Search for PATCH usage in the client
rg "PATCH" src/mcp_acp/client.py

Repository: ambient-code/mcp

Length of output: 596


🏁 Script executed:

# Check the full _request method implementation
sed -n '100,200p' src/mcp_acp/client.py | head -100

Repository: ambient-code/mcp

Length of output: 3718


🏁 Script executed:

# Search for any API documentation or backend contract info
find . -name "*.md" -o -name "*.txt" -o -name "*.rst" | xargs rg "scheduled.session.*PUT\|scheduled.session.*PATCH\|scheduled-sessions.*verb" 2>/dev/null || echo "No docs found with those patterns"

Repository: ambient-code/mcp

Length of output: 93


🏁 Script executed:

# Check if there are other partial update methods in the client that use PUT or PATCH
rg "partial update\|sparse.*payload" -B 5 src/mcp_acp/client.py

Repository: ambient-code/mcp

Length of output: 42


Use PATCH instead of PUT for this partial update.

The method docstring states "partial update" and builds a sparse payload, but line 717 sends it with PUT. This creates a semantic mismatch: PUT typically means full replacement (omitted fields could be cleared), while PATCH means partial update. The codebase already uses PATCH for other partial updates on sessions. Either switch to PATCH or fetch and resend the full resource document. Additionally, the existing test does not validate the HTTP method, so add an assertion to prevent regression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/client.py` around lines 681 - 720, The update_scheduled_session
method builds a sparse payload for a partial update but currently uses PUT;
change the HTTP verb in the self._request call inside update_scheduled_session
from "PUT" to "PATCH" (keeping the same path and payload) so semantics match the
docstring and other partial-update endpoints, and update related tests to assert
that the client issues a PATCH request (add an assertion checking the HTTP
method) to prevent regressions.

Comment on lines +836 to +842
async def export_session(self, project: str, session: str) -> dict[str, Any]:
"""Export session chat as markdown."""
self._validate_input(project, "project")
self._validate_input(session, "session")

text = await self._request_text("GET", f"/v1/sessions/{session}/export", project)
return {"export": text, "session": session}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the _request_text() method implementation
cd src/mcp_acp && grep -n "_request_text" client.py | head -20

Repository: ambient-code/mcp

Length of output: 297


🏁 Script executed:

# Search for _request_text method definition
rg -n "def _request_text" src/mcp_acp/client.py -A 15

Repository: ambient-code/mcp

Length of output: 661


🏁 Script executed:

# Check for any Accept header specifications in the codebase
rg -n "Accept" src/mcp_acp/ -B 2 -A 2

Repository: ambient-code/mcp

Length of output: 602


🏁 Script executed:

# Look for export endpoint tests or documentation
fd -type f \( -name "*.py" -o -name "*.md" \) | xargs rg -l "export.*session\|export_session" 2>/dev/null

Repository: ambient-code/mcp

Length of output: 232


🏁 Script executed:

# Search for API specification files
fd -type f \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) | head -20

Repository: ambient-code/mcp

Length of output: 230


🏁 Script executed:

# Get the complete _request_text() method
sed -n '205,240p' src/mcp_acp/client.py

Repository: ambient-code/mcp

Length of output: 1418


🏁 Script executed:

# Look for test files related to export
fd -name "*test*.py" -o -name "test_*.py" | xargs rg -l "export_session\|export" 2>/dev/null

Repository: ambient-code/mcp

Length of output: 292


🏁 Script executed:

# Search for API specs or documentation
find . -name "*.md" -o -name "*.txt" -o -name "README*" | xargs rg -l "export\|markdown" 2>/dev/null | head -10

Repository: ambient-code/mcp

Length of output: 42


🏁 Script executed:

# Check if there are any comments about markdown in the export context
rg -n "markdown\|export" src/mcp_acp/client.py -i -B 2 -A 2

Repository: ambient-code/mcp

Length of output: 42


🏁 Script executed:

# Check if there are other methods that override Accept header
rg -n "Accept.*:" src/mcp_acp/ -B 3 -A 1

Repository: ambient-code/mcp

Length of output: 668


🏁 Script executed:

# Look for backend or spec files
find . -type f -name "*.py" | xargs rg -l "export.*markdown\|markdown.*export" 2>/dev/null

Repository: ambient-code/mcp

Length of output: 44


🏁 Script executed:

# Check all usages of Accept header in the entire codebase
rg -r "Accept" . --type py 2>/dev/null | head -30

Repository: ambient-code/mcp

Length of output: 4475


🏁 Script executed:

# Look for the export endpoint handler or documentation in comments
rg -n "export" src/mcp_acp/client.py -B 3 -A 3

Repository: ambient-code/mcp

Length of output: 618


🏁 Script executed:

# Check if there's a different method that accepts custom headers
rg -n "def _request" src/mcp_acp/client.py -A 8

Repository: ambient-code/mcp

Length of output: 674


🏁 Script executed:

# Look for any markdown-related Accept headers or content types
rg -i "markdown\|text/markdown\|application/vnd" src/mcp_acp/

Repository: ambient-code/mcp

Length of output: 42


🏁 Script executed:

# Check if there are integration tests or docs that specify the expected format
find . -type f \( -name "*.md" -o -name "*.txt" \) -exec grep -l "export\|session" {} \;

Repository: ambient-code/mcp

Length of output: 175


🏁 Script executed:

# Check API_REFERENCE.md for export endpoint specification
grep -n -i "export\|/v1/sessions" ./API_REFERENCE.md -A 5 -B 2

Repository: ambient-code/mcp

Length of output: 3533


🏁 Script executed:

# Check README for export session information
grep -n -i "export\|markdown" ./README.md -A 3 -B 2

Repository: ambient-code/mcp

Length of output: 1178


🏁 Script executed:

# Check CLAUDE.md for any context about export format
grep -n -i "export\|markdown" ./CLAUDE.md -A 3 -B 2

Repository: ambient-code/mcp

Length of output: 155


🏁 Script executed:

# Look at the complete _request method to understand the pattern
sed -n '142,203p' src/mcp_acp/client.py

Repository: ambient-code/mcp

Length of output: 2454


Ensure export endpoint explicitly requests markdown format.

The export_session() method is documented to export as markdown, but _request_text() hardcodes Accept: text/plain. A strict backend requiring explicit markdown content negotiation could return 406 Not Acceptable. Either:

  1. Add an optional accept_header parameter to _request_text() and pass text/markdown from export_session(), or
  2. Follow the pattern used by get_session_transcript() (which accepts a format query parameter per API_REFERENCE.md) and add format negotiation to the export call.

Verify against the backend export endpoint spec to confirm expected content type or query parameter convention.

Comment on lines +1064 to +1078
async def add_repo(self, project: str, session: str, repo_url: str) -> dict[str, Any]:
"""Add a repository to a running session."""
self._validate_input(project, "project")
self._validate_input(session, "session")

return await self._request("POST", f"/v1/sessions/{session}/repos", project, json_data={"url": repo_url})

async def remove_repo(self, project: str, session: str, repo_name: str) -> dict[str, Any]:
"""Remove a repository from a session."""
self._validate_input(project, "project")
self._validate_input(session, "session")
self._validate_input(repo_name, "repo_name")

await self._request("DELETE", f"/v1/sessions/{session}/repos/{repo_name}", project)
return {"removed": True, "message": f"Repo '{repo_name}' removed from session '{session}'"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

repo_name is validated with the wrong contract.

Line 1075 reuses the DNS-1123 validator, but repository names derived from URLs are not guaranteed to fit that shape. add_repo() accepts arbitrary repo URLs, so a repo with a common name like foo_bar or foo.bar can be added and then become impossible to remove through this tool. Validate only that the value is non-empty and URL-encode the path segment instead of treating it like a session name.

Proposed fix
+from urllib.parse import quote
+
 async def remove_repo(self, project: str, session: str, repo_name: str) -> dict[str, Any]:
     """Remove a repository from a session."""
     self._validate_input(project, "project")
     self._validate_input(session, "session")
-    self._validate_input(repo_name, "repo_name")
+    if not repo_name:
+        raise ValueError("repo_name must not be empty")
 
-    await self._request("DELETE", f"/v1/sessions/{session}/repos/{repo_name}", project)
+    encoded_repo_name = quote(repo_name, safe="")
+    await self._request("DELETE", f"/v1/sessions/{session}/repos/{encoded_repo_name}", project)
     return {"removed": True, "message": f"Repo '{repo_name}' removed from session '{session}'"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/client.py` around lines 1064 - 1078, The remove_repo() method is
incorrectly validating repo_name with the DNS-1123 validator used for session
names; update remove_repo to only check repo_name is non-empty (use a simple
non-empty string check instead of self._validate_input(..., "repo_name")), and
when building the DELETE request path call self._request use a URL-encoded path
segment for repo_name (e.g., encode repo_name before interpolating into
f"/v1/sessions/{session}/repos/{repo_name}") so names like "foo_bar" or
"foo.bar" can be removed; keep existing validations for project and session and
continue to return the same dict response after the await self._request call.

Comment on lines +73 to +87
_SESSION_TEMPLATE = {
"type": "object",
"description": "Session template (same as create_session: task, model, repos, displayName, etc.)",
"properties": {
"task": {"type": "string", "description": "The prompt/instructions"},
"model": {"type": "string", "description": "LLM model", "default": "claude-sonnet-4"},
"repos": {
"type": "array",
"items": {"type": "object", "properties": {"url": {"type": "string"}}},
"description": "Repositories to clone",
},
"displayName": {"type": "string", "description": "Display name for created sessions"},
},
"required": ["task"],
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a different session_template schema for updates.

_SESSION_TEMPLATE makes task required, and acp_update_scheduled_session reuses it unchanged. That blocks valid partial updates like changing only model, repos, or suspend, even though ACPClient.update_scheduled_session() accepts a partial session_template. Split the create/update schemas, or drop the nested required fields for the update tool.

Also applies to: 470-485

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/server.py` around lines 73 - 87, The _SESSION_TEMPLATE currently
requires "task", but acp_update_scheduled_session reuses it which prevents
partial updates; change the schema used by the update tool to a non-required
version (e.g., create a new _SESSION_UPDATE_TEMPLATE or clone _SESSION_TEMPLATE
and remove the "required" key / any nested required fields) and update
acp_update_scheduled_session to reference the new schema instead of
_SESSION_TEMPLATE; make sure ACPClient.update_scheduled_session remains
accepting partial session_template values.

…ools

Add 15 new MCP tools expanding the server from 26 to 41 tools:

- Scheduled sessions (9): list, get, create, update, delete, suspend,
  resume, trigger, list_runs
- Session export (1): export_session
- Workflow management (2): set_workflow, get_workflow_metadata
- Repo management (3): add_repo, remove_repo, get_repos_status

All tools follow the 4-step pattern: client method, formatter,
tool definition + dispatch, and unit tests.

Closes ambient-code#28

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jeremyeder jeremyeder force-pushed the feat/scheduled-sessions-and-new-tools branch from 488edb4 to b7f56e9 Compare April 1, 2026 15:19
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
src/mcp_acp/client.py (3)

681-718: ⚠️ Potential issue | 🟠 Major

Use PATCH for scheduled-session partial updates.

This method builds a sparse payload, but Line 717 still sends it via PUT. On a full-replacement endpoint, omitted fields can be cleared, so a partial update can clobber existing config instead of patching it. Switch this call to PATCH; if the backend only supports PUT, fetch and resend the full document instead. Also add a verb assertion in the client test so this cannot regress.

Suggested fix
-            await self._request("PUT", f"/v1/scheduled-sessions/{name}", project, json_data=payload)
+            await self._request("PATCH", f"/v1/scheduled-sessions/{name}", project, json_data=payload)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/client.py` around lines 681 - 718, The update_scheduled_session
method currently builds a sparse payload but calls self._request with "PUT"
which risks clobbering omitted fields; change the HTTP verb to "PATCH" in the
call to self._request (in update_scheduled_session) so the server applies a
partial update, and if your backend does not support PATCH implement the
fallback: fetch the full scheduled-session via GET, merge the sparse payload
into the full document, then send the merged document with PUT using the same
_request helper. Also add/update a unit test to assert the client issues "PATCH"
for partial updates (or the merge+PUT behavior) to prevent regressions.

836-842: ⚠️ Potential issue | 🟠 Major

Request markdown explicitly when exporting a session.

export_session() promises markdown, but it still uses _request_text(), which hardcodes Accept: text/plain at Line 222. A strict backend can return the wrong representation or reject the request. Let this path override the accept header to the export endpoint’s markdown media type, or use whatever format parameter the export spec defines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/client.py` around lines 836 - 842, The export_session function
currently relies on _request_text which forces Accept: text/plain; change
export_session to request the markdown media type explicitly (e.g., Accept:
text/markdown or the API's defined export media type) by either calling a
lower-level request method that accepts custom headers or adding an optional
accept/header override to _request_text, then pass the markdown accept header
when calling export_session so the export endpoint returns markdown as promised;
reference export_session and _request_text to locate and update the call site
and/or signature.

1071-1078: ⚠️ Potential issue | 🟠 Major

repo_name needs path-safe handling, not DNS-1123 validation.

Line 1075 rejects valid repository names like foo_bar and foo.bar, and Line 1077 interpolates the raw value into the URL path. That makes some repos addable but impossible to remove. Validate only that repo_name is non-empty, then URL-encode the path segment before calling DELETE.

Suggested fix
+from urllib.parse import quote
+
     async def remove_repo(self, project: str, session: str, repo_name: str) -> dict[str, Any]:
         """Remove a repository from a session."""
         self._validate_input(project, "project")
         self._validate_input(session, "session")
-        self._validate_input(repo_name, "repo_name")
+        if not repo_name:
+            raise ValueError("repo_name must not be empty")
 
-        await self._request("DELETE", f"/v1/sessions/{session}/repos/{repo_name}", project)
+        encoded_repo_name = quote(repo_name, safe="")
+        await self._request("DELETE", f"/v1/sessions/{session}/repos/{encoded_repo_name}", project)
         return {"removed": True, "message": f"Repo '{repo_name}' removed from session '{session}'"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/client.py` around lines 1071 - 1078, The remove_repo method
currently applies DNS-1123 style validation and interpolates repo_name raw into
the URL, rejecting valid names like "foo_bar" and breaking deletes; change
validation in remove_repo to only ensure repo_name is non-empty (use the
existing _validate_input semantics or a simple truthy check instead of DNS-1123
rules) and URL-encode repo_name when constructing the path passed to
self._request (i.e., encode the path segment used in
f"/v1/sessions/{session}/repos/{repo_name}" before calling _request) so
arbitrary valid repo names can be removed safely.
src/mcp_acp/server.py (1)

73-87: ⚠️ Potential issue | 🟠 Major

Don’t reuse the create-only template schema for scheduled-session updates.

_SESSION_TEMPLATE requires task, and acp_update_scheduled_session reuses it unchanged. That blocks valid partial updates like changing only model or repos, even though ACPClient.update_scheduled_session() accepts a partial session_template. Define a separate update schema without required fields and use that for the update tool.

Suggested fix
 _SESSION_TEMPLATE = {
     "type": "object",
     "description": "Session template (same as create_session: task, model, repos, displayName, etc.)",
     "properties": {
         "task": {"type": "string", "description": "The prompt/instructions"},
         "model": {"type": "string", "description": "LLM model", "default": "claude-sonnet-4"},
         "repos": {
             "type": "array",
             "items": {"type": "object", "properties": {"url": {"type": "string"}}},
             "description": "Repositories to clone",
         },
         "displayName": {"type": "string", "description": "Display name for created sessions"},
     },
     "required": ["task"],
 }
+
+_SESSION_TEMPLATE_UPDATE = {
+    **_SESSION_TEMPLATE,
+    "required": [],
+}
@@
-                    "session_template": _SESSION_TEMPLATE,
+                    "session_template": _SESSION_TEMPLATE_UPDATE,

Also applies to: 470-485

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_acp/server.py` around lines 73 - 87, The _SESSION_TEMPLATE schema
currently requires "task" and is being reused for acp_update_scheduled_session,
preventing partial updates; create a new schema (e.g., _SESSION_UPDATE_TEMPLATE)
that mirrors _SESSION_TEMPLATE properties but has no "required" fields, then
update the acp_update_scheduled_session tool to use this new schema instead of
_SESSION_TEMPLATE so ACPClient.update_scheduled_session() can accept partial
session_template updates (refer to symbols _SESSION_TEMPLATE,
acp_update_scheduled_session, and ACPClient.update_scheduled_session).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/mcp_acp/client.py`:
- Around line 681-718: The update_scheduled_session method currently builds a
sparse payload but calls self._request with "PUT" which risks clobbering omitted
fields; change the HTTP verb to "PATCH" in the call to self._request (in
update_scheduled_session) so the server applies a partial update, and if your
backend does not support PATCH implement the fallback: fetch the full
scheduled-session via GET, merge the sparse payload into the full document, then
send the merged document with PUT using the same _request helper. Also
add/update a unit test to assert the client issues "PATCH" for partial updates
(or the merge+PUT behavior) to prevent regressions.
- Around line 836-842: The export_session function currently relies on
_request_text which forces Accept: text/plain; change export_session to request
the markdown media type explicitly (e.g., Accept: text/markdown or the API's
defined export media type) by either calling a lower-level request method that
accepts custom headers or adding an optional accept/header override to
_request_text, then pass the markdown accept header when calling export_session
so the export endpoint returns markdown as promised; reference export_session
and _request_text to locate and update the call site and/or signature.
- Around line 1071-1078: The remove_repo method currently applies DNS-1123 style
validation and interpolates repo_name raw into the URL, rejecting valid names
like "foo_bar" and breaking deletes; change validation in remove_repo to only
ensure repo_name is non-empty (use the existing _validate_input semantics or a
simple truthy check instead of DNS-1123 rules) and URL-encode repo_name when
constructing the path passed to self._request (i.e., encode the path segment
used in f"/v1/sessions/{session}/repos/{repo_name}" before calling _request) so
arbitrary valid repo names can be removed safely.

In `@src/mcp_acp/server.py`:
- Around line 73-87: The _SESSION_TEMPLATE schema currently requires "task" and
is being reused for acp_update_scheduled_session, preventing partial updates;
create a new schema (e.g., _SESSION_UPDATE_TEMPLATE) that mirrors
_SESSION_TEMPLATE properties but has no "required" fields, then update the
acp_update_scheduled_session tool to use this new schema instead of
_SESSION_TEMPLATE so ACPClient.update_scheduled_session() can accept partial
session_template updates (refer to symbols _SESSION_TEMPLATE,
acp_update_scheduled_session, and ACPClient.update_scheduled_session).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f06f40c6-d417-468b-94a4-f38c269b00c0

📥 Commits

Reviewing files that changed from the base of the PR and between 488edb4 and b7f56e9.

📒 Files selected for processing (8)
  • CLAUDE.md
  • src/mcp_acp/client.py
  • src/mcp_acp/formatters.py
  • src/mcp_acp/server.py
  • tests/test_client.py
  • tests/test_formatters.py
  • tests/test_server.py
  • tests/test_server_e2e.py

@jeremyeder
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jeremyeder jeremyeder merged commit 554fdac into ambient-code:main Apr 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement acp_export_session tool

1 participant