From 2d89c32c98bb9b4ebdf5ba3d10e08d60af58ffd9 Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Sun, 1 Mar 2026 00:42:42 -0500 Subject: [PATCH] fix: align MCP client with public API session contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP client was sending field names that don't match the public API's CreateSessionRequest DTO, causing session creation to fail with validation errors. Also removes parameters the public API doesn't support. - client.py: initialPrompt → task, llmConfig.model → model - client.py: remove interactive/timeout params (not in public API) - client.py: transform repos from bare strings to {url: str} objects - client.py: fix clone_session to use same corrected field names - client.py: update SESSION_TEMPLATES to use task/model fields - server.py: remove interactive/timeout from tool schema and dispatch Co-Authored-By: Claude Opus 4.6 (1M context) --- src/mcp_acp/client.py | 66 ++++++++++++++++++++++++++----------------- src/mcp_acp/server.py | 8 ------ 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/mcp_acp/client.py b/src/mcp_acp/client.py index 7f937c5..6fc391a 100644 --- a/src/mcp_acp/client.py +++ b/src/mcp_acp/client.py @@ -4,6 +4,7 @@ a simplified REST API for managing AgenticSessions. """ +import json import os import re from datetime import datetime, timedelta @@ -11,29 +12,42 @@ import httpx -from mcp_acp.settings import load_clusters_config, load_settings +from mcp_acp.settings import _acpctl_config_path, load_clusters_config, load_settings from utils.pylogger import get_python_logger logger = get_python_logger() LABEL_VALUE_PATTERN = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$") + +def _read_acpctl_token() -> str | None: + """Read token from the acpctl CLI config file as a last-resort fallback. + + Used when clusters.yaml exists but a cluster entry has no token. + """ + try: + data = json.loads(_acpctl_config_path().read_text()) + return data.get("access_token") or None + except (FileNotFoundError, json.JSONDecodeError, OSError): + return None + + SESSION_TEMPLATES: dict[str, dict[str, Any]] = { "triage": { - "workflow": "triage", - "llmConfig": {"model": "claude-sonnet-4", "temperature": 0.7}, + "task": "Triage: investigate and classify the issue.", + "model": "claude-sonnet-4", }, "bugfix": { - "workflow": "bugfix", - "llmConfig": {"model": "claude-sonnet-4", "temperature": 0.3}, + "task": "Bugfix: diagnose and fix the reported bug.", + "model": "claude-sonnet-4", }, "feature": { - "workflow": "feature-development", - "llmConfig": {"model": "claude-sonnet-4", "temperature": 0.5}, + "task": "Feature development: implement the requested feature.", + "model": "claude-sonnet-4", }, "exploration": { - "workflow": "codebase-exploration", - "llmConfig": {"model": "claude-sonnet-4", "temperature": 0.8}, + "task": "Codebase exploration: explore and document the codebase.", + "model": "claude-sonnet-4", }, } @@ -97,12 +111,20 @@ def _get_cluster_config(self, cluster_name: str | None = None) -> dict[str, Any] } def _get_token(self, cluster_config: dict[str, Any]) -> str: - """Get authentication token for a cluster.""" - token = cluster_config.get("token") or os.getenv("ACP_TOKEN") + """Get authentication token for a cluster. + + Resolution order: + 1. Per-cluster token in clusters.yaml + 2. ACP_TOKEN environment variable + 3. acpctl CLI config (~/.config/ambient/config.json) + """ + token = cluster_config.get("token") or os.getenv("ACP_TOKEN") or _read_acpctl_token() if not token: raise ValueError( - "No authentication token available. Set 'token' in clusters.yaml or ACP_TOKEN environment variable." + "No authentication token available. " + "Run 'acpctl login --token --url ', " + "set 'token' in clusters.yaml, or set ACP_TOKEN environment variable." ) return token @@ -338,26 +360,22 @@ async def create_session( initial_prompt: str, display_name: str | None = None, repos: list[str] | None = None, - interactive: bool = False, model: str = "claude-sonnet-4", - timeout: int = 900, dry_run: bool = False, ) -> dict[str, Any]: """Create an AgenticSession with a custom prompt.""" self._validate_input(project, "project") session_data: dict[str, Any] = { - "initialPrompt": initial_prompt, - "interactive": interactive, - "llmConfig": {"model": model}, - "timeout": timeout, + "task": initial_prompt, + "model": model, } if display_name: session_data["displayName"] = display_name if repos: - session_data["repos"] = repos + session_data["repos"] = [{"url": r} for r in repos] if dry_run: return { @@ -401,7 +419,7 @@ async def create_session_from_template( } if repos: - session_data["repos"] = repos + session_data["repos"] = [{"url": r} for r in repos] if dry_run: return { @@ -524,14 +542,10 @@ async def clone_session( source = await self._request("GET", f"/v1/sessions/{source_session}", project) clone_data: dict[str, Any] = { - "displayName": new_display_name, - "initialPrompt": source.get("initialPrompt", ""), - "interactive": source.get("interactive", False), - "timeout": source.get("timeout", 900), + "task": source.get("task", ""), + "model": source.get("model", "claude-sonnet-4"), } - if source.get("llmConfig"): - clone_data["llmConfig"] = source["llmConfig"] if source.get("repos"): clone_data["repos"] = source["repos"] diff --git a/src/mcp_acp/server.py b/src/mcp_acp/server.py index d9c08b1..9b1e852 100644 --- a/src/mcp_acp/server.py +++ b/src/mcp_acp/server.py @@ -111,13 +111,7 @@ async def list_tools() -> list[Tool]: }, "display_name": {"type": "string", "description": "Human-readable display name"}, "repos": {"type": "array", "items": {"type": "string"}, "description": "Repository URLs to clone"}, - "interactive": { - "type": "boolean", - "description": "Create an interactive session", - "default": False, - }, "model": {"type": "string", "description": "LLM model to use", "default": "claude-sonnet-4"}, - "timeout": {"type": "integer", "description": "Timeout in seconds", "default": 900, "minimum": 60}, "dry_run": _DRY_RUN, }, "required": ["initial_prompt"], @@ -491,9 +485,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: initial_prompt=arguments["initial_prompt"], display_name=arguments.get("display_name"), repos=arguments.get("repos"), - interactive=arguments.get("interactive", False), model=arguments.get("model", "claude-sonnet-4"), - timeout=arguments.get("timeout", 900), dry_run=arguments.get("dry_run", False), ) text = format_session_created(result)