Fix Basecamp Campfire functionality and improve error handling
This commit is contained in:
16
README.md
16
README.md
@@ -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.
|
||||
@@ -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):
|
||||
|
||||
233
mcp_server.py
233
mcp_server.py
@@ -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('/')
|
||||
|
||||
@@ -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 []
|
||||
Reference in New Issue
Block a user