project_utils.py•14.1 kB
# Agent-MCP/agent-mcp/utils/project_utils.py
import os
import json
import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any
from ..core import globals as g
from ..core.config import (
logger,
get_project_dir,
) # Import get_project_dir for MCP_VERSION if needed
# __version__ was in mcp_template/__init__.py.
# If MCP_VERSION is needed here, it should ideally be sourced from a single place.
# For now, let's assume it might come from the main package's __init__ or a dedicated version file.
# We can hardcode it temporarily or make it configurable.
try:
# Attempt to get version from the root __init__.py of agent-mcp
from agent_mcp import __version__ as MCP_VERSION
except ImportError:
logger.warning(
"Could not import __version__ from agent_mcp. Using default '0.1.0'."
)
MCP_VERSION = "0.1.0" # Fallback, matches original main.py:1041
# Original location: main.py lines 876-929 (init_agent_directory)
def init_agent_directory(project_dir_str: str) -> Optional[Path]:
"""
Initialize the .agent directory structure in the specified project directory.
If the directory structure already exists, it verifies it.
Original main.py: lines 876-929
"""
try:
project_path = Path(project_dir_str).resolve()
except Exception as e:
logger.error(f"Invalid project directory string '{project_dir_str}': {e}")
return None
# Validate that the project directory is not the MCP directory itself
# This logic needs to correctly identify the MCP codebase root.
# Assuming this file is at: Agent-MCP/agent-mcp/utils/project_utils.py
# Then, __file__.resolve() gives the path to this file.
# .parent -> .../utils
# .parent.parent -> .../mcp_server_src
# .parent.parent.parent -> .../agent-mcp (This is the root of the agent code package)
# .parent.parent.parent.parent -> .../Agent-MCP (This is the repository root)
# The original check was against `Path(__file__).resolve().parent.parent` from `main.py`
# which would be `agent-mcp`.
agent_mcp_codebase_root_for_check = (
Path(__file__).resolve().parent.parent.parent
) # This should point to agent-mcp
# Original main.py line 880-884
if (
project_path == agent_mcp_codebase_root_for_check
or project_path in agent_mcp_codebase_root_for_check.parents
):
# This warning matches the original behavior.
logger.warning(
f"WARNING: Initializing .agent in the MCP directory itself ({project_path}) or its parent is not recommended!"
)
logger.warning(
f"Please specify a project directory that is NOT the MCP codebase."
)
# Original code proceeded with a warning, so we do the same.
agent_dir = project_path / ".agent"
# Original main.py lines 887-899 (directory list)
directories_to_create = [
"", # Ensures .agent itself is created
"logs",
"diffs",
"notifications",
"notifications/pending",
"notifications/acknowledged",
]
try:
for directory_suffix in directories_to_create:
(agent_dir / directory_suffix).mkdir(parents=True, exist_ok=True)
except OSError as e:
logger.error(f"Failed to create .agent directory structure in {agent_dir}: {e}")
return None # Indicate failure
# Create initial config file if it doesn't exist
# Original main.py lines 902-914
config_path = agent_dir / "config.json"
if not config_path.exists():
# g.admin_token might not be initialized when this function is first called
# during server startup before admin token persistence logic.
# The original code in main.py:1040 used `admin_token` which was set earlier.
# We should pass the admin_token to this function if it's needed at this stage,
# or ensure g.admin_token is reliably set before this.
# For now, let's assume g.admin_token will be set by the time this is called in a meaningful way,
# or it will be None if called very early (e.g. initial setup).
current_admin_token = g.admin_token # Get current global admin token
config_data = {
"project_name": project_path.name,
"created_at": datetime.datetime.now().isoformat(),
"admin_token": current_admin_token, # Use the admin token available at call time
"mcp_version": MCP_VERSION,
}
try:
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config_data, f, indent=2)
except IOError as e:
logger.error(f"Failed to write initial config.json to {config_path}: {e}")
return None # Indicate failure
except Exception as e:
logger.error(
f"Unexpected error writing initial config.json: {e}", exc_info=True
)
return None
# Create initial daily logs file if it doesn't exist
# Original main.py lines 917-926
log_file_dir = agent_dir / "logs"
# log_file_dir.mkdir(parents=True, exist_ok=True) # Ensured by directories_to_create
log_file_path = log_file_dir / f"{datetime.date.today().isoformat()}.json"
if not log_file_path.exists():
log_entry = {
"timestamp": datetime.datetime.now().isoformat(),
"event": "agent_directory_initialized",
"details": "Initial setup of .agent directory",
}
try:
with open(log_file_path, "w", encoding="utf-8") as f:
json.dump(
[log_entry], f, indent=2
) # Original stored a list with one entry
except IOError as e:
logger.error(
f"Failed to write initial daily log file to {log_file_path}: {e}"
)
# Continue, as this is less critical than config.json, matching original behavior.
except Exception as e:
logger.error(
f"Unexpected error writing initial daily log file: {e}", exc_info=True
)
logger.info(f".agent directory structure initialized/verified in {agent_dir}")
return agent_dir
# Original location: main.py lines 1206-1239 (generate_system_prompt)
def generate_system_prompt(
agent_id: str, agent_token_for_prompt: str, admin_token_runtime: Optional[str]
) -> str:
"""
Generate a system prompt for an agent.
Original main.py: lines 1206-1239.
Uses g.agent_working_dirs.
"""
# Determine working directory for the prompt
# Fallback to CWD if agent_id not in g.agent_working_dirs, though it should be by the time this is called.
# (Original main.py line 1226: agent_working_dirs.get(agent_id, os.getcwd()))
working_dir = g.agent_working_dirs.get(agent_id, os.getcwd())
# Base prompt content from original main.py lines 1208-1224
base_prompt = f"""You are an AI agent running in Cursor, connected to a Multi-Agent Collaboration Protocol (MCP) server.
Your goal is to complete tasks efficiently and collaboratively using a shared, persistent knowledge base.
**Core Responsibilities & Tools:**
* **File Safety:** Before modifying any file, use `check_file_status` to see if another agent is using it. Use `update_file_status` to claim files ('editing', 'reading', 'reviewing') before you start and 'released' when done.
* **Task Management:** Use `view_tasks` to see your assigned tasks (filter by agent ID or status). Update progress with `update_task_status`. If a task is complex, use `request_assistance` or `create_self_task`.
* **Project Context (Key-Value):**
* Use `view_project_context` with `context_key` for specific values (e.g., API endpoints, configuration) or `search_query` to find relevant keys via keywords.
* (Admin) Use `update_project_context` to add/modify precise key-value context.
* **File Metadata:**
* Use `view_file_metadata` (with `filepath`) to understand a file's purpose, components, etc.
* (Admin) Use `update_file_metadata` to add/update structured information about specific files.
* **RAG Querying:** Use `ask_project_rag` with a natural language `query` to ask broader questions about the project. The system will search across documentation, context, and metadata to synthesize an answer. (Index updates automatically in the background).
* **Parallelization:** Analyze tasks for opportunities to work in parallel. Break down large tasks into smaller sub-tasks. Clearly define dependencies.
* **Auditability:** Log all significant actions for tracking and debugging.
Your working directory is: {working_dir}
"""
# Determine agent type for the prompt
# (Original main.py line 1227: agent_details, and line 1210 for admin_token check)
agent_type = "Worker"
# The original logic in create_agent_tool (main.py:1134) passed `token` (which was the calling admin_token)
# as the third argument to generate_system_prompt if the agent_id started with "admin".
# So, `admin_token_runtime` here corresponds to that third argument.
# An agent is "Admin" type in the prompt if its own token IS the admin token.
if (
agent_id.lower().startswith("admin")
and agent_token_for_prompt == admin_token_runtime
):
agent_type = "Admin"
# A simpler check might be if the agent_token_for_prompt itself is the known g.admin_token
# However, the original call structure was a bit specific.
# Let's refine: the prompt should reflect if this *specific agent instance* is an admin.
# This happens if its `agent_token_for_prompt` is the same as the system's `admin_token_runtime`.
# The `agent_id.lower().startswith("admin")` is a secondary check.
if (
agent_token_for_prompt == admin_token_runtime
): # Primary check: is this agent's token THE admin token?
agent_type = "Admin"
agent_details_str = f"""Agent ID: {agent_id}
Agent Type: {agent_type}
"""
# Connection code snippet for the agent to use
# (Original main.py lines 1229-1237 for connection code structure)
# The MCP_SERVER_URL should come from a config or be dynamically determined.
# The original used os.environ.get('PORT', '8080') which implies it's for the SSE server.
# The client connection example in the prompt should use the /messages/ endpoint for tool calls if that's the design.
# Let's assume the agent's env var MCP_SERVER_URL points to the correct base for /messages/
mcp_server_url_for_client = os.environ.get(
"MCP_SERVER_URL", f"http://localhost:{os.environ.get('PORT', '8080')}/messages/"
)
# The original connection code snippet in main.py was quite extensive and specific.
# For 1-to-1, we should replicate that structure.
# It assumed the agent would use 'requests' and provided a `call_mcp_tool` like function.
# The token used in HEADERS should be `agent_token_for_prompt`.
connection_code_lines = [
f'MCP_SERVER_URL = "{mcp_server_url_for_client}" # Adjust if your server\'s tool endpoint is different',
f'AGENT_TOKEN = "{agent_token_for_prompt}" # This is your unique agent token',
"",
"HEADERS = {",
' "Content-Type": "application/json",',
# The original prompt had a complex way of deciding which token to use in Authorization.
# It should always be the AGENT_TOKEN for the agent's own calls.
' "Authorization": f"Bearer {{AGENT_TOKEN}}"',
"}",
"",
"def call_mcp_tool(tool_name: str, arguments: dict) -> dict:",
" payload = {",
' "id": f"call_{{requests.compat.urlencode(arguments)[:10]}}", # Example unique ID',
' "type": "tool_call",',
' "tool": tool_name,',
' "arguments": arguments',
" }",
' print(f"Calling tool: {{tool_name}} with args: {{arguments}}") # Debug print',
" try:",
" response = requests.post(MCP_SERVER_URL, headers=HEADERS, json=payload, timeout=60)",
" response.raise_for_status() # Raise an HTTPError for bad responses (4XX or 5XX)",
" # The MCP server is expected to return a list of content blocks.",
" # We need to parse the 'text' from the first TextContent block.",
" response_data = response.json()",
" if isinstance(response_data, list) and len(response_data) > 0:",
" first_item = response_data[0]",
" if isinstance(first_item, dict) and first_item.get('type') == 'text':",
" return {{'text_response': first_item.get('text', '')}}",
" return {{'raw_response': response_data}} # Fallback",
" except requests.exceptions.Timeout:",
' print(f"Timeout calling MCP tool {{tool_name}}")',
" return {{'error': 'Timeout'}}"
" except requests.exceptions.RequestException as e:",
' print(f"Error calling MCP tool {{tool_name}}: {{e}}")',
" if e.response is not None:",
' print(f"Response content: {{e.response.text}}")',
" return {{'error': str(e), 'response_text': e.response.text}}",
" return {{'error': str(e)}}",
"",
"# Example usage:",
'# result = call_mcp_tool("view_tasks", {{"token": AGENT_TOKEN}})',
"# if result and 'error' not in result: print(json.dumps(result, indent=2))",
]
connection_code_str = "\n".join(connection_code_lines)
# Construct full prompt (Original main.py line 1238)
full_prompt = (
base_prompt
+ agent_details_str
+ "\nCopy-paste this Python code into your environment to connect and interact with the MCP server:\n"
+ "```python\nimport requests\nimport json\n\n"
+ connection_code_str
+ "\n```\n\n"
+ "Use the available tools (the server will list them or consult documentation) via `call_mcp_tool` to manage your work."
)
return full_prompt