From 43e4ebaeadfc8e6fde91d7de5db230a35e00e67b Mon Sep 17 00:00:00 2001 From: Marian Paul Date: Tue, 27 Jan 2026 10:02:23 +0100 Subject: [PATCH 1/2] Fix: Fix expanding project from the ui by adding new features --- .claude/commands/expand-project.md | 18 +++ server/services/expand_chat_session.py | 153 ++++++++++++++++++------- 2 files changed, 127 insertions(+), 44 deletions(-) diff --git a/.claude/commands/expand-project.md b/.claude/commands/expand-project.md index e8005b28..3b10bc42 100644 --- a/.claude/commands/expand-project.md +++ b/.claude/commands/expand-project.md @@ -170,6 +170,24 @@ feature_create_bulk(features=[ - Each feature needs: category, name, description, steps (array of strings) - The tool will return the count of created features - verify it matches your expected count +**IMPORTANT - XML Fallback:** +If the `feature_create_bulk` tool is unavailable or fails, output features in this XML format as a backup: + +```xml + +[ + { + "category": "functional", + "name": "Feature name", + "description": "Description", + "steps": ["Step 1", "Step 2"] + } +] + +``` + +The system will parse this XML and create features automatically. + --- # FEATURE QUALITY STANDARDS diff --git a/server/services/expand_chat_session.py b/server/services/expand_chat_session.py index f582e7b0..a8e19f9c 100644 --- a/server/services/expand_chat_session.py +++ b/server/services/expand_chat_session.py @@ -12,6 +12,7 @@ import os import re import shutil +import sys import threading import uuid from datetime import datetime @@ -152,6 +153,12 @@ async def start(self) -> AsyncGenerator[dict, None]: "allow": [ "Read(./**)", "Glob(./**)", + # Auto-approve Feature MCP tools + "mcp__features__feature_create_bulk", + "mcp__features__feature_get_stats", + "mcp__features__feature_get_next", + "mcp__features__feature_add_dependency", + "mcp__features__feature_remove_dependency", ], }, } @@ -171,6 +178,18 @@ async def start(self) -> AsyncGenerator[dict, None]: # This allows using alternative APIs (e.g., GLM via z.ai) that may not support Claude model names model = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "claude-opus-4-5-20251101") + # Configure Feature MCP server for tool access + mcp_servers = { + "features": { + "command": sys.executable, + "args": ["-m", "mcp_server.feature_mcp"], + "env": { + "PROJECT_DIR": str(self.project_dir.resolve()), + "PYTHONPATH": str(Path(__file__).parent.parent.parent.resolve()), + }, + }, + } + # Create Claude SDK client try: self.client = ClaudeSDKClient( @@ -178,9 +197,16 @@ async def start(self) -> AsyncGenerator[dict, None]: model=model, cli_path=system_cli, system_prompt=system_prompt, + mcp_servers=mcp_servers, allowed_tools=[ "Read", "Glob", + # Feature MCP tools - creation and safe reads only + "mcp__features__feature_create_bulk", + "mcp__features__feature_get_stats", + "mcp__features__feature_get_next", + "mcp__features__feature_add_dependency", + "mcp__features__feature_remove_dependency", ], permission_mode="acceptEdits", max_turns=100, @@ -294,6 +320,9 @@ async def _query_claude( # Accumulate full response to detect feature blocks full_response = "" + # Track whether MCP tool succeeded (to skip XML parsing fallback) + mcp_tool_succeeded = False + # Stream the response async for msg in self.client.receive_response(): msg_type = type(msg).__name__ @@ -314,53 +343,89 @@ async def _query_claude( "timestamp": datetime.now().isoformat() }) - # Check for feature creation blocks in full response (handle multiple blocks) - features_matches = re.findall( - r'\s*(\[[\s\S]*?\])\s*', - full_response - ) - - if features_matches: - # Collect all features from all blocks, deduplicating by name - all_features: list[dict] = [] - seen_names: set[str] = set() - - for features_json in features_matches: - try: - features_data = json.loads(features_json) - - if features_data and isinstance(features_data, list): - for feature in features_data: - name = feature.get("name", "") - if name and name not in seen_names: - seen_names.add(name) - all_features.append(feature) - except json.JSONDecodeError as e: - logger.error(f"Failed to parse features JSON block: {e}") - # Continue processing other blocks - - if all_features: - try: - # Create all deduplicated features - created = await self._create_features_bulk(all_features) - - if created: - self.features_created += len(created) - self.created_feature_ids.extend([f["id"] for f in created]) + # Detect successful feature_create_bulk tool calls + elif block_type == "ToolResult": + tool_name = getattr(block, "tool_name", "") + if "feature_create_bulk" in tool_name: + mcp_tool_succeeded = True + logger.info("Detected successful feature_create_bulk MCP tool call") + + # Extract created features from tool result + tool_content = getattr(block, "content", []) + if tool_content: + for content_block in tool_content: + if hasattr(content_block, "text"): + try: + result_data = json.loads(content_block.text) + created_features = result_data.get("created_features", []) + + if created_features: + self.features_created += len(created_features) + self.created_feature_ids.extend([f["id"] for f in created_features]) + + yield { + "type": "features_created", + "count": len(created_features), + "features": created_features, + "source": "mcp" # Tag source for debugging + } + + logger.info(f"Created {len(created_features)} features for {self.project_name} (via MCP)") + except (json.JSONDecodeError, AttributeError) as e: + logger.warning(f"Failed to parse MCP tool result: {e}") + + # Only parse XML if MCP tool wasn't used (fallback mechanism) + if not mcp_tool_succeeded: + # Check for feature creation blocks in full response (handle multiple blocks) + features_matches = re.findall( + r'\s*(\[[\s\S]*?\])\s*', + full_response + ) + if features_matches: + # Collect all features from all blocks, deduplicating by name + all_features: list[dict] = [] + seen_names: set[str] = set() + + for features_json in features_matches: + try: + features_data = json.loads(features_json) + + if features_data and isinstance(features_data, list): + for feature in features_data: + name = feature.get("name", "") + if name and name not in seen_names: + seen_names.add(name) + all_features.append(feature) + except json.JSONDecodeError as e: + logger.error(f"Failed to parse features JSON block: {e}") + # Continue processing other blocks + + if all_features: + try: + # Create all deduplicated features + created = await self._create_features_bulk(all_features) + + if created: + self.features_created += len(created) + self.created_feature_ids.extend([f["id"] for f in created]) + + yield { + "type": "features_created", + "count": len(created), + "features": created, + "source": "xml_parsing" # Tag source for debugging + } + + logger.info(f"Created {len(created)} features for {self.project_name} (via XML parsing)") + except Exception: + logger.exception("Failed to create features") yield { - "type": "features_created", - "count": len(created), - "features": created + "type": "error", + "content": "Failed to create features" } - - logger.info(f"Created {len(created)} features for {self.project_name}") - except Exception: - logger.exception("Failed to create features") - yield { - "type": "error", - "content": "Failed to create features" - } + else: + logger.info(f"Skipping XML parsing for {self.project_name} (MCP tool succeeded)") async def _create_features_bulk(self, features: list[dict]) -> list[dict]: """ From fddd712912294e79f371e1766c1e75d03836b7fa Mon Sep 17 00:00:00 2001 From: Marian Paul Date: Tue, 27 Jan 2026 11:48:18 +0100 Subject: [PATCH 2/2] Review --- server/services/expand_chat_session.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/services/expand_chat_session.py b/server/services/expand_chat_session.py index a8e19f9c..982df286 100644 --- a/server/services/expand_chat_session.py +++ b/server/services/expand_chat_session.py @@ -360,18 +360,22 @@ async def _query_claude( created_features = result_data.get("created_features", []) if created_features: - self.features_created += len(created_features) - self.created_feature_ids.extend([f["id"] for f in created_features]) + # Safely extract valid IDs (filter features that have an "id" key) + valid_ids = [f["id"] for f in created_features if "id" in f] + + # Update counters based on features with valid IDs + self.features_created += len(valid_ids) + self.created_feature_ids.extend(valid_ids) yield { "type": "features_created", - "count": len(created_features), + "count": len(valid_ids), "features": created_features, "source": "mcp" # Tag source for debugging } - logger.info(f"Created {len(created_features)} features for {self.project_name} (via MCP)") - except (json.JSONDecodeError, AttributeError) as e: + logger.info(f"Created {len(valid_ids)} features for {self.project_name} (via MCP)") + except (json.JSONDecodeError, AttributeError, KeyError) as e: logger.warning(f"Failed to parse MCP tool result: {e}") # Only parse XML if MCP tool wasn't used (fallback mechanism)