Fix Basecamp Campfire functionality and improve error handling
This commit is contained in:
14
README.md
14
README.md
@@ -149,3 +149,17 @@ This method also checks for OAuth authentication and returns appropriate error m
|
|||||||
## License
|
## 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,12 +164,20 @@ class BasecampClient:
|
|||||||
# Campfire (chat) methods
|
# Campfire (chat) methods
|
||||||
def get_campfires(self, project_id):
|
def get_campfires(self, project_id):
|
||||||
"""Get the campfire for a project."""
|
"""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:
|
if response.status_code == 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Failed to get campfire: {response.status_code} - {response.text}")
|
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
|
# Message board methods
|
||||||
def get_message_board(self, project_id):
|
def get_message_board(self, project_id):
|
||||||
"""Get the message board for a project."""
|
"""Get the message board for a project."""
|
||||||
|
|||||||
231
mcp_server.py
231
mcp_server.py
@@ -12,6 +12,7 @@ from basecamp_client import BasecampClient
|
|||||||
from search_utils import BasecampSearch
|
from search_utils import BasecampSearch
|
||||||
import token_storage # Import the token storage module
|
import token_storage # Import the token storage module
|
||||||
import requests # For token refresh
|
import requests # For token refresh
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
# Import MCP integration components, using try/except to catch any import errors
|
# Import MCP integration components, using try/except to catch any import errors
|
||||||
try:
|
try:
|
||||||
@@ -33,6 +34,34 @@ except Exception as e:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1)
|
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
|
# Configure logging with more verbose output
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
@@ -317,57 +346,167 @@ def mcp_action():
|
|||||||
"oauth_url": "http://localhost:8000/"
|
"oauth_url": "http://localhost:8000/"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Handle action based on type
|
# Create a Basecamp client
|
||||||
try:
|
client = get_basecamp_client(auth_mode='oauth')
|
||||||
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':
|
# Handle actions
|
||||||
client = get_basecamp_client(auth_mode='oauth')
|
if action == 'get_projects':
|
||||||
search = BasecampSearch(client=client)
|
projects = client.get_projects()
|
||||||
|
return mcp_response({
|
||||||
query = params.get('query', '')
|
"projects": projects,
|
||||||
include_completed = params.get('include_completed', False)
|
"count": len(projects)
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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:
|
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({
|
return jsonify({
|
||||||
"error": str(e)
|
"status": "error",
|
||||||
|
"error": "server_error",
|
||||||
|
"message": str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|||||||
@@ -499,9 +499,60 @@ class BasecampSearch:
|
|||||||
if content_matched:
|
if content_matched:
|
||||||
filtered_comments.append(comment)
|
filtered_comments.append(comment)
|
||||||
|
|
||||||
comments = filtered_comments
|
return filtered_comments
|
||||||
|
|
||||||
return comments
|
return comments
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error searching comments: {str(e)}")
|
logger.error(f"Error searching comments: {str(e)}")
|
||||||
return []
|
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