Merge remote-tracking branch 'origin/codex/cleanup-files--newline-and-trailing-spaces'

This commit is contained in:
George Antonopoulos
2025-06-06 10:39:25 +01:00
13 changed files with 343 additions and 328 deletions

View File

@@ -37,11 +37,11 @@ logger = logging.getLogger('mcp_cli_server')
class MCPServer:
"""MCP server implementing the Model Context Protocol for Cursor."""
def __init__(self):
self.tools = self._get_available_tools()
logger.info("MCP CLI Server initialized")
def _get_available_tools(self) -> List[Dict[str, Any]]:
"""Get list of available tools for Basecamp."""
return [
@@ -58,7 +58,7 @@ class MCPServer:
"name": "get_project",
"description": "Get details for a specific project",
"inputSchema": {
"type": "object",
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "The project ID"}
},
@@ -150,34 +150,34 @@ class MCPServer:
"required": ["project_id", "question_id"]
}
]
def _get_basecamp_client(self) -> Optional[BasecampClient]:
"""Get authenticated Basecamp client."""
try:
token_data = token_storage.get_token()
logger.debug(f"Token data retrieved: {token_data}")
if not token_data or not token_data.get('access_token'):
logger.error("No OAuth token available")
return None
# Check if token is expired
if token_storage.is_token_expired():
logger.error("OAuth token has expired")
return None
# Get account_id from token data first, then fall back to env var
account_id = token_data.get('account_id') or os.getenv('BASECAMP_ACCOUNT_ID')
# Set a default user agent if none is provided
user_agent = os.getenv('USER_AGENT') or "Basecamp MCP Server (cursor@example.com)"
if not account_id:
logger.error(f"Missing account_id. Token data: {token_data}, Env BASECAMP_ACCOUNT_ID: {os.getenv('BASECAMP_ACCOUNT_ID')}")
return None
logger.debug(f"Creating Basecamp client with account_id: {account_id}, user_agent: {user_agent}")
return BasecampClient(
access_token=token_data['access_token'],
account_id=account_id,
@@ -187,7 +187,7 @@ class MCPServer:
except Exception as e:
logger.error(f"Error creating Basecamp client: {e}")
return None
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""Handle an MCP request."""
method = request.get("method")
@@ -195,9 +195,9 @@ class MCPServer:
method_lower = method.lower() if isinstance(method, str) else ''
params = request.get("params", {})
request_id = request.get("id")
logger.info(f"Handling request: {method}")
try:
if method_lower == "initialize":
return {
@@ -214,12 +214,12 @@ class MCPServer:
}
}
}
elif method_lower == "initialized":
# This is a notification, no response needed
logger.info("Received initialized notification")
return None
elif method_lower in ("tools/list", "listtools"):
return {
"jsonrpc": "2.0",
@@ -228,13 +228,13 @@ class MCPServer:
"tools": self.tools
}
}
elif method_lower in ("tools/call", "toolscall"):
tool_name = params.get("name")
arguments = params.get("arguments", {})
result = self._execute_tool(tool_name, arguments)
return {
"jsonrpc": "2.0",
"id": request_id,
@@ -247,7 +247,7 @@ class MCPServer:
]
}
}
elif method_lower in ("listofferings", "list_offerings", "loffering"):
# Respond to Cursor's ListOfferings UI request
offerings = []
@@ -264,7 +264,7 @@ class MCPServer:
"offerings": offerings
}
}
elif method_lower == "ping":
# Handle ping requests
return {
@@ -272,7 +272,7 @@ class MCPServer:
"id": request_id,
"result": {}
}
else:
return {
"jsonrpc": "2.0",
@@ -282,7 +282,7 @@ class MCPServer:
"message": f"Method not found: {method}"
}
}
except Exception as e:
logger.error(f"Error handling request: {e}")
return {
@@ -293,7 +293,7 @@ class MCPServer:
"message": f"Internal error: {str(e)}"
}
}
def _execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a tool and return the result."""
client = self._get_basecamp_client()
@@ -309,7 +309,7 @@ class MCPServer:
"error": "Authentication required",
"message": "Please authenticate with Basecamp first. Visit http://localhost:8000 to log in."
}
try:
if tool_name == "get_projects":
projects = client.get_projects()
@@ -318,7 +318,7 @@ class MCPServer:
"projects": projects,
"count": len(projects)
}
elif tool_name == "get_project":
project_id = arguments.get("project_id")
project = client.get_project(project_id)
@@ -326,7 +326,7 @@ class MCPServer:
"status": "success",
"project": project
}
elif tool_name == "get_todolists":
project_id = arguments.get("project_id")
todolists = client.get_todolists(project_id)
@@ -335,7 +335,7 @@ class MCPServer:
"todolists": todolists,
"count": len(todolists)
}
elif tool_name == "get_todos":
todolist_id = arguments.get("todolist_id")
project_id = arguments.get("project_id")
@@ -345,14 +345,14 @@ class MCPServer:
"todos": todos,
"count": len(todos)
}
elif tool_name == "search_basecamp":
query = arguments.get("query")
project_id = arguments.get("project_id")
search = BasecampSearch(client=client)
results = {}
if project_id:
# Search within specific project
results["todolists"] = search.search_todolists(query, project_id)
@@ -362,13 +362,13 @@ class MCPServer:
results["projects"] = search.search_projects(query)
results["todos"] = search.search_todos(query)
results["messages"] = search.search_messages(query)
return {
"status": "success",
"query": query,
"results": results
}
elif tool_name == "get_comments":
recording_id = arguments.get("recording_id")
project_id = arguments.get("project_id")
@@ -378,7 +378,7 @@ class MCPServer:
"comments": comments,
"count": len(comments)
}
elif tool_name == "get_campfire_lines":
project_id = arguments.get("project_id")
campfire_id = arguments.get("campfire_id")
@@ -413,7 +413,7 @@ class MCPServer:
"error": "Unknown tool",
"message": f"Tool '{tool_name}' is not supported"
}
except Exception as e:
logger.error(f"Error executing tool {tool_name}: {e}")
# Check if it's a 401 error (token expired during API call)
@@ -426,24 +426,24 @@ class MCPServer:
"error": "Execution error",
"message": str(e)
}
def run(self):
"""Run the MCP server, reading from stdin and writing to stdout."""
logger.info("Starting MCP CLI server")
for line in sys.stdin:
try:
line = line.strip()
if not line:
continue
request = json.loads(line)
response = self.handle_request(request)
# Write response to stdout (only if there's a response)
if response is not None:
print(json.dumps(response), flush=True)
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON received: {e}")
error_response = {
@@ -455,11 +455,11 @@ class MCPServer:
}
}
print(json.dumps(error_response), flush=True)
except Exception as e:
logger.error(f"Unexpected error: {e}")
error_response = {
"jsonrpc": "2.0",
"jsonrpc": "2.0",
"id": None,
"error": {
"code": -32603,
@@ -470,4 +470,4 @@ class MCPServer:
if __name__ == "__main__":
server = MCPServer()
server.run()
server.run()