Fix Basecamp Campfire functionality and improve error handling

This commit is contained in:
George Antonopoulos
2025-03-09 17:16:07 +00:00
parent 9c49ce02b1
commit e08e48de50
4 changed files with 263 additions and 51 deletions

View File

@@ -148,4 +148,18 @@ This method also checks for OAuth authentication and returns appropriate error m
## License
This project is licensed under the MIT License - see the LICENSE file for details.
This project is licensed under the MIT License - see the LICENSE file for details.
## Recent Changes
### March 9, 2024 - Improved MCP Server Functionality
- Added standardized error and success response handling with new `mcp_response` helper function
- Fixed API endpoint issues for Basecamp Campfire (chat) functionality:
- Updated the URL format for retrieving campfires from `projects/{project_id}/campfire.json` to `buckets/{project_id}/chats.json`
- Added support for retrieving campfire chat lines
- Enhanced search capabilities to include campfire lines content
- Improved error handling and response formatting across all action handlers
- Fixed CORS support by adding the Flask-CORS package
These changes ensure more reliable and consistent responses from the MCP server, particularly for Campfire chat functionality.

View File

@@ -164,11 +164,19 @@ class BasecampClient:
# Campfire (chat) methods
def get_campfires(self, project_id):
"""Get the campfire for a project."""
response = self.get(f'projects/{project_id}/campfire.json')
response = self.get(f'buckets/{project_id}/chats.json')
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get campfire: {response.status_code} - {response.text}")
def get_campfire_lines(self, project_id, campfire_id):
"""Get chat lines from a campfire."""
response = self.get(f'buckets/{project_id}/chats/{campfire_id}/lines.json')
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get campfire lines: {response.status_code} - {response.text}")
# Message board methods
def get_message_board(self, project_id):

View File

@@ -12,6 +12,7 @@ from basecamp_client import BasecampClient
from search_utils import BasecampSearch
import token_storage # Import the token storage module
import requests # For token refresh
from flask_cors import CORS
# Import MCP integration components, using try/except to catch any import errors
try:
@@ -33,6 +34,34 @@ except Exception as e:
traceback.print_exc()
sys.exit(1)
# Helper function for consistent response format
def mcp_response(data=None, status="success", error=None, message=None, status_code=200):
"""
Generate a standardized MCP response.
Args:
data: The response data
status: 'success' or 'error'
error: Error code in case of an error
message: Human-readable message
status_code: HTTP status code
Returns:
tuple: JSON response and HTTP status code
"""
response = {
"status": status
}
if data is not None:
response.update(data)
if status == "error":
response["error"] = error
response["message"] = message
return jsonify(response), status_code
# Configure logging with more verbose output
logging.basicConfig(
level=logging.DEBUG,
@@ -317,57 +346,167 @@ def mcp_action():
"oauth_url": "http://localhost:8000/"
})
# Handle action based on type
try:
if action == 'get_projects':
client = get_basecamp_client(auth_mode='oauth')
projects = client.get_projects()
return jsonify({
"status": "success",
"projects": projects,
"count": len(projects)
})
elif action == 'search':
client = get_basecamp_client(auth_mode='oauth')
search = BasecampSearch(client=client)
query = params.get('query', '')
include_completed = params.get('include_completed', False)
logger.info(f"Searching with query: {query}")
results = {
"projects": search.search_projects(query),
"todos": search.search_todos(query, include_completed=include_completed),
"messages": search.search_messages(query),
}
return jsonify({
"status": "success",
"results": results
})
else:
logger.error(f"Unknown action: {action}")
return jsonify({
"status": "error",
"error": "unknown_action",
"message": f"Unknown action: {action}"
})
except Exception as action_error:
logger.error(f"Error executing action {action}: {str(action_error)}")
return jsonify({
"status": "error",
"error": "execution_failed",
"message": str(action_error)
# Create a Basecamp client
client = get_basecamp_client(auth_mode='oauth')
# Handle actions
if action == 'get_projects':
projects = client.get_projects()
return mcp_response({
"projects": projects,
"count": len(projects)
})
elif action == 'get_project':
project_id = params.get('project_id')
if not project_id:
return mcp_response(
status="error",
error="missing_parameter",
message="Missing project_id parameter",
status_code=400
)
project = client.get_project(project_id)
return mcp_response({
"project": project
})
elif action == 'get_todolists':
project_id = params.get('project_id')
if not project_id:
return mcp_response(
status="error",
error="missing_parameter",
message="Missing project_id parameter",
status_code=400
)
todolists = client.get_todolists(project_id)
return mcp_response({
"todolists": todolists,
"count": len(todolists)
})
elif action == 'get_todos':
todolist_id = params.get('todolist_id')
if not todolist_id:
return jsonify({"status": "error", "error": "missing_parameter", "message": "Missing todolist_id parameter"}), 400
todos = client.get_todos(todolist_id)
return jsonify({
"status": "success",
"todos": todos,
"count": len(todos)
})
elif action == 'create_todo':
todolist_id = params.get('todolist_id')
content = params.get('content')
if not todolist_id or not content:
return jsonify({"status": "error", "error": "missing_parameter", "message": "Missing todolist_id or content parameter"}), 400
todo = client.create_todo(
todolist_id=todolist_id,
content=content,
description=params.get('description', ''),
assignee_ids=params.get('assignee_ids', [])
)
return jsonify({
"status": "success",
"todo": todo
})
elif action == 'complete_todo':
todo_id = params.get('todo_id')
if not todo_id:
return jsonify({"status": "error", "error": "missing_parameter", "message": "Missing todo_id parameter"}), 400
result = client.complete_todo(todo_id)
return jsonify({
"status": "success",
"result": result
})
elif action == 'get_comments':
recording_id = params.get('recording_id')
bucket_id = params.get('bucket_id')
if not recording_id or not bucket_id:
return jsonify({"status": "error", "error": "missing_parameter", "message": "Missing recording_id or bucket_id parameter"}), 400
comments = client.get_comments(recording_id, bucket_id)
return jsonify({
"status": "success",
"comments": comments,
"count": len(comments)
})
elif action == 'get_campfire':
project_id = params.get('project_id')
if not project_id:
return jsonify({"status": "error", "error": "missing_parameter", "message": "Missing project_id parameter"}), 400
campfire = client.get_campfires(project_id)
return jsonify({
"status": "success",
"campfire": campfire
})
elif action == 'get_campfire_lines':
project_id = params.get('project_id')
campfire_id = params.get('campfire_id')
if not project_id or not campfire_id:
return jsonify({"status": "error", "error": "missing_parameter", "message": "Missing project_id or campfire_id parameter"}), 400
try:
lines = client.get_campfire_lines(project_id, campfire_id)
return jsonify({
"status": "success",
"lines": lines,
"count": len(lines)
})
except Exception as e:
return jsonify({
"status": "error",
"error": "api_error",
"message": str(e)
}), 500
elif action == 'search':
search = BasecampSearch(client=client)
query = params.get('query', '')
include_completed = params.get('include_completed', False)
logger.info(f"Searching with query: {query}")
results = {
"projects": search.search_projects(query),
"todos": search.search_todos(query, include_completed=include_completed),
"messages": search.search_messages(query),
}
return jsonify({
"status": "success",
"results": results
})
else:
return jsonify({
"status": "error",
"error": "unknown_action",
"message": f"Unknown action: {action}"
}), 400
except Exception as e:
logger.error(f"Error in MCP action endpoint: {str(e)}")
logger.error(f"Error in mcp_action: {str(e)}", exc_info=True)
return jsonify({
"error": str(e)
"status": "error",
"error": "server_error",
"message": str(e)
}), 500
@app.route('/')

View File

@@ -499,9 +499,60 @@ class BasecampSearch:
if content_matched:
filtered_comments.append(comment)
comments = filtered_comments
return filtered_comments
return comments
except Exception as e:
logger.error(f"Error searching comments: {str(e)}")
return []
def search_campfire_lines(self, query=None, project_id=None, campfire_id=None):
"""
Search for lines in campfire chats.
Args:
query (str, optional): Search term to filter lines
project_id (int, optional): Project ID
campfire_id (int, optional): Campfire ID
Returns:
list: Matching chat lines
"""
try:
if not project_id or not campfire_id:
logger.warning("Cannot search campfire lines without project_id and campfire_id")
return [{
"content": "To search campfire lines, you need to specify both a project ID and a campfire ID.",
"api_limitation": True,
"title": "Campfire Search Limitation"
}]
lines = self.client.get_campfire_lines(project_id, campfire_id)
if query and lines:
query = query.lower()
filtered_lines = []
for line in lines:
# Check content
content_matched = False
content = line.get('content', '')
if content and query in content.lower():
content_matched = True
# Check creator name
if not content_matched and line.get('creator'):
creator_name = line['creator'].get('name', '')
if creator_name and query in creator_name.lower():
content_matched = True
# If matched, add to results
if content_matched:
filtered_lines.append(line)
return filtered_lines
return lines
except Exception as e:
logger.error(f"Error searching campfire lines: {str(e)}")
return []