Fix token_storage function name and ensure Composio integration is fully functional
This commit is contained in:
81
README.md
81
README.md
@@ -49,6 +49,7 @@ The project consists of the following components:
|
||||
BASECAMP_ACCOUNT_ID=your_account_id
|
||||
FLASK_SECRET_KEY=random_secret_key
|
||||
MCP_API_KEY=your_api_key
|
||||
COMPOSIO_API_KEY=your_composio_api_key
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -162,4 +163,82 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
||||
- 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.
|
||||
These changes ensure more reliable and consistent responses from the MCP server, particularly for Campfire chat functionality.
|
||||
|
||||
### March 9, 2024 - Added Composio Integration
|
||||
|
||||
Added support for [Composio](https://composio.dev/) integration, allowing the Basecamp MCP server to be used with Composio for AI-powered workflows. This integration follows the Model Context Protocol (MCP) standards and includes:
|
||||
|
||||
- New endpoints for Composio compatibility:
|
||||
- `/composio/schema` - Returns the schema of available tools in Composio-compatible format
|
||||
- `/composio/tool` - Handles Composio tool calls with standardized parameters
|
||||
- `/composio/check_auth` - Checks authentication status for Composio requests
|
||||
|
||||
- Standardized tool naming and parameter formats to work with Composio's MCP specifications
|
||||
- A standalone example client for testing and demonstrating the integration
|
||||
|
||||
## Using with Composio
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Create a Composio account at [https://app.composio.dev](https://app.composio.dev)
|
||||
2. Obtain a Composio API key from your Composio dashboard
|
||||
3. Add your API key to your `.env` file:
|
||||
```
|
||||
COMPOSIO_API_KEY=your_composio_api_key
|
||||
```
|
||||
|
||||
### Setting Up Composio Integration
|
||||
|
||||
1. Make sure you have authenticated with Basecamp using the OAuth app (http://localhost:8000/)
|
||||
2. Run the MCP server with the Composio integration enabled:
|
||||
```
|
||||
python mcp_server.py
|
||||
```
|
||||
|
||||
3. In your Composio dashboard, add a new custom integration:
|
||||
- Integration URL: `http://localhost:5001/composio/schema`
|
||||
- Authentication: OAuth (managed by our implementation)
|
||||
|
||||
4. You can now use Composio to connect to your Basecamp account through the MCP server:
|
||||
- Composio will discover available tools via the schema endpoint
|
||||
- Tool executions will be handled by the `/composio/tool` endpoint
|
||||
- Authentication status is checked via the `/composio/check_auth` endpoint
|
||||
|
||||
### Example Composio Client
|
||||
|
||||
We provide a simple client example in `composio_client_example.py` that demonstrates how to:
|
||||
|
||||
1. Check authentication status
|
||||
2. Retrieve the tool schema
|
||||
3. Execute various Basecamp operations through the Composio integration
|
||||
|
||||
Run the example with:
|
||||
```
|
||||
python composio_client_example.py
|
||||
```
|
||||
|
||||
### Testing the Integration
|
||||
|
||||
To test the integration without connecting to Composio:
|
||||
|
||||
1. Run the MCP server:
|
||||
```
|
||||
python mcp_server.py
|
||||
```
|
||||
|
||||
2. Use curl to test the endpoints directly:
|
||||
```bash
|
||||
# Check authentication status
|
||||
curl http://localhost:5001/composio/check_auth
|
||||
|
||||
# Get the schema
|
||||
curl http://localhost:5001/composio/schema
|
||||
|
||||
# Execute a tool (get projects)
|
||||
curl -X POST http://localhost:5001/composio/tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tool": "GET_PROJECTS", "params": {}}'
|
||||
```
|
||||
|
||||
For more detailed documentation on Composio integration, refer to the [official Composio documentation](https://docs.composio.dev).
|
||||
185
composio_client_example.py
Normal file
185
composio_client_example.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Example client for using the Basecamp MCP server with Composio.
|
||||
This example demonstrates MCP protocol integration requirements.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configuration
|
||||
MCP_SERVER_URL = "http://localhost:5001"
|
||||
COMPOSIO_API_KEY = os.getenv("COMPOSIO_API_KEY")
|
||||
|
||||
class BasecampComposioClient:
|
||||
"""A client for interacting with Basecamp through Composio's MCP protocol."""
|
||||
|
||||
def __init__(self, mcp_server_url=MCP_SERVER_URL):
|
||||
"""Initialize the client with the MCP server URL."""
|
||||
self.mcp_server_url = mcp_server_url
|
||||
self.headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
# Add Composio API key if available
|
||||
if COMPOSIO_API_KEY:
|
||||
self.headers["X-Composio-API-Key"] = COMPOSIO_API_KEY
|
||||
|
||||
def check_auth(self):
|
||||
"""Check if OAuth authentication is available."""
|
||||
response = requests.get(
|
||||
f"{self.mcp_server_url}/composio/check_auth",
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def get_schema(self):
|
||||
"""Get the schema of available tools."""
|
||||
response = requests.get(
|
||||
f"{self.mcp_server_url}/composio/schema",
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def execute_tool(self, tool_name, params=None):
|
||||
"""Execute a tool with the given parameters."""
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
response = requests.post(
|
||||
f"{self.mcp_server_url}/composio/tool",
|
||||
headers=self.headers,
|
||||
json={"tool": tool_name, "params": params}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def get_projects(self):
|
||||
"""Get all projects from Basecamp."""
|
||||
return self.execute_tool("GET_PROJECTS")
|
||||
|
||||
def get_project(self, project_id):
|
||||
"""Get details for a specific project."""
|
||||
return self.execute_tool("GET_PROJECT", {"project_id": project_id})
|
||||
|
||||
def get_todolists(self, project_id):
|
||||
"""Get all todo lists for a project."""
|
||||
return self.execute_tool("GET_TODOLISTS", {"project_id": project_id})
|
||||
|
||||
def get_todos(self, todolist_id):
|
||||
"""Get all todos for a specific todolist."""
|
||||
return self.execute_tool("GET_TODOS", {"todolist_id": todolist_id})
|
||||
|
||||
def get_campfire(self, project_id):
|
||||
"""Get all chat rooms (campfires) for a project."""
|
||||
return self.execute_tool("GET_CAMPFIRE", {"project_id": project_id})
|
||||
|
||||
def get_campfire_lines(self, project_id, campfire_id):
|
||||
"""Get messages from a specific chat room."""
|
||||
return self.execute_tool("GET_CAMPFIRE_LINES", {
|
||||
"project_id": project_id,
|
||||
"campfire_id": campfire_id
|
||||
})
|
||||
|
||||
def search(self, query, project_id=None):
|
||||
"""Search across Basecamp resources."""
|
||||
params = {"query": query}
|
||||
if project_id:
|
||||
params["project_id"] = project_id
|
||||
return self.execute_tool("SEARCH", params)
|
||||
|
||||
def get_comments(self, recording_id, bucket_id):
|
||||
"""Get comments for a specific Basecamp object."""
|
||||
return self.execute_tool("GET_COMMENTS", {
|
||||
"recording_id": recording_id,
|
||||
"bucket_id": bucket_id
|
||||
})
|
||||
|
||||
def create_comment(self, recording_id, bucket_id, content):
|
||||
"""Create a new comment on a Basecamp object."""
|
||||
return self.execute_tool("CREATE_COMMENT", {
|
||||
"recording_id": recording_id,
|
||||
"bucket_id": bucket_id,
|
||||
"content": content
|
||||
})
|
||||
|
||||
def main():
|
||||
"""Main function to demonstrate the client."""
|
||||
client = BasecampComposioClient()
|
||||
|
||||
# Verify connectivity to MCP server
|
||||
print("==== Basecamp MCP-Composio Integration Test ====")
|
||||
|
||||
# Check authentication
|
||||
print("\n1. Checking authentication status...")
|
||||
auth_status = client.check_auth()
|
||||
print(f"Authentication Status: {json.dumps(auth_status, indent=2)}")
|
||||
|
||||
if auth_status.get("status") == "error":
|
||||
print(f"Please authenticate at: {auth_status.get('error', {}).get('auth_url', '')}")
|
||||
return
|
||||
|
||||
# Get available tools
|
||||
print("\n2. Retrieving tool schema...")
|
||||
schema = client.get_schema()
|
||||
print(f"Server Name: {schema.get('name')}")
|
||||
print(f"Version: {schema.get('version')}")
|
||||
print(f"Authentication Type: {schema.get('auth', {}).get('type')}")
|
||||
print("\nAvailable Tools:")
|
||||
for tool in schema.get("tools", []):
|
||||
required_params = tool.get("parameters", {}).get("required", [])
|
||||
required_str = ", ".join(required_params) if required_params else "None"
|
||||
print(f"- {tool['name']}: {tool['description']} (Required params: {required_str})")
|
||||
|
||||
# Get projects
|
||||
print("\n3. Fetching projects...")
|
||||
projects_response = client.get_projects()
|
||||
|
||||
if projects_response.get("status") == "error":
|
||||
print(f"Error: {projects_response.get('error', {}).get('message', 'Unknown error')}")
|
||||
else:
|
||||
project_data = projects_response.get("data", [])
|
||||
print(f"Found {len(project_data)} projects:")
|
||||
for i, project in enumerate(project_data[:3], 1): # Show first 3 projects
|
||||
print(f"{i}. {project.get('name')} (ID: {project.get('id')})")
|
||||
|
||||
if project_data:
|
||||
# Use the first project for further examples
|
||||
project_id = project_data[0].get("id")
|
||||
|
||||
# Get campfires for the project
|
||||
print(f"\n4. Fetching campfires for project {project_id}...")
|
||||
campfires_response = client.get_campfire(project_id)
|
||||
|
||||
if campfires_response.get("status") == "error":
|
||||
print(f"Error: {campfires_response.get('error', {}).get('message', 'Unknown error')}")
|
||||
else:
|
||||
campfire_data = campfires_response.get("data", {}).get("campfire", [])
|
||||
print(f"Found {len(campfire_data)} campfires:")
|
||||
for i, campfire in enumerate(campfire_data[:2], 1): # Show first 2 campfires
|
||||
print(f"{i}. {campfire.get('title')} (ID: {campfire.get('id')})")
|
||||
|
||||
if campfire_data:
|
||||
# Get messages from the first campfire
|
||||
campfire_id = campfire_data[0].get("id")
|
||||
print(f"\n5. Fetching messages from campfire {campfire_id}...")
|
||||
messages_response = client.get_campfire_lines(project_id, campfire_id)
|
||||
|
||||
if messages_response.get("status") == "error":
|
||||
print(f"Error: {messages_response.get('error', {}).get('message', 'Unknown error')}")
|
||||
else:
|
||||
message_data = messages_response.get("data", {}).get("lines", [])
|
||||
print(f"Found {len(message_data)} messages:")
|
||||
for i, message in enumerate(message_data[:3], 1): # Show first 3 messages
|
||||
creator = message.get("creator", {}).get("name", "Unknown")
|
||||
content = message.get("title", "No content")
|
||||
print(f"{i}. From {creator}: {content[:50]}...")
|
||||
|
||||
print("\n==== Test completed ====")
|
||||
print("This example demonstrates how to connect to the Basecamp MCP server")
|
||||
print("and use it with the Composio MCP protocol.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
420
composio_integration.py
Normal file
420
composio_integration.py
Normal file
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from flask import Flask, request, jsonify
|
||||
from basecamp_client import BasecampClient
|
||||
from search_utils import BasecampSearch
|
||||
import token_storage
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def get_basecamp_client(auth_mode='oauth'):
|
||||
"""
|
||||
Returns a BasecampClient instance with appropriate authentication.
|
||||
|
||||
Args:
|
||||
auth_mode (str): The authentication mode to use ('oauth' or 'basic')
|
||||
|
||||
Returns:
|
||||
BasecampClient: A client for interacting with Basecamp
|
||||
"""
|
||||
if auth_mode == 'oauth':
|
||||
# Get the OAuth token
|
||||
token_data = token_storage.get_token()
|
||||
if not token_data or 'access_token' not in token_data:
|
||||
logger.error("No OAuth token available")
|
||||
return None
|
||||
|
||||
# Create a client using the OAuth token
|
||||
account_id = os.getenv('BASECAMP_ACCOUNT_ID')
|
||||
user_agent = os.getenv('USER_AGENT')
|
||||
client = BasecampClient(
|
||||
account_id=account_id,
|
||||
user_agent=user_agent,
|
||||
access_token=token_data['access_token'],
|
||||
auth_mode='oauth'
|
||||
)
|
||||
return client
|
||||
else:
|
||||
# Basic auth is not recommended but keeping for compatibility
|
||||
username = os.getenv('BASECAMP_USERNAME')
|
||||
password = os.getenv('BASECAMP_PASSWORD')
|
||||
account_id = os.getenv('BASECAMP_ACCOUNT_ID')
|
||||
user_agent = os.getenv('USER_AGENT')
|
||||
|
||||
client = BasecampClient(
|
||||
username=username,
|
||||
password=password,
|
||||
account_id=account_id,
|
||||
user_agent=user_agent,
|
||||
auth_mode='basic'
|
||||
)
|
||||
return client
|
||||
|
||||
def get_schema():
|
||||
"""
|
||||
Returns the schema for Basecamp tools compatible with Composio's MCP format.
|
||||
|
||||
Returns:
|
||||
dict: A schema describing available tools and their parameters according to Composio specs
|
||||
"""
|
||||
schema = {
|
||||
"name": "Basecamp MCP Server",
|
||||
"description": "Integration with Basecamp 3 for project management and team collaboration",
|
||||
"version": "1.0.0",
|
||||
"auth": {
|
||||
"type": "oauth2",
|
||||
"redirect_url": "http://localhost:8000",
|
||||
"token_url": "http://localhost:8000/token/info"
|
||||
},
|
||||
"contact": {
|
||||
"name": "Basecamp MCP Server Team",
|
||||
"url": "https://github.com/georgeantonopoulos/Basecamp-MCP-Server"
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"name": "GET_PROJECTS",
|
||||
"description": "Get all projects from Basecamp",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GET_PROJECT",
|
||||
"description": "Get details for a specific project",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "string", "description": "The ID of the project"}
|
||||
},
|
||||
"required": ["project_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GET_TODOLISTS",
|
||||
"description": "Get all todo lists for a project",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "string", "description": "The ID of the project"}
|
||||
},
|
||||
"required": ["project_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GET_TODOS",
|
||||
"description": "Get all todos for a specific todolist",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"todolist_id": {"type": "string", "description": "The ID of the todolist"}
|
||||
},
|
||||
"required": ["todolist_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GET_CAMPFIRE",
|
||||
"description": "Get all chat rooms (campfires) for a project",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "string", "description": "The ID of the project"}
|
||||
},
|
||||
"required": ["project_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GET_CAMPFIRE_LINES",
|
||||
"description": "Get messages from a specific chat room",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "string", "description": "The ID of the project"},
|
||||
"campfire_id": {"type": "string", "description": "The ID of the campfire/chat room"}
|
||||
},
|
||||
"required": ["project_id", "campfire_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SEARCH",
|
||||
"description": "Search across Basecamp resources",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "The search query"},
|
||||
"project_id": {"type": "string", "description": "Optional project ID to limit search scope"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GET_COMMENTS",
|
||||
"description": "Get comments for a specific Basecamp object",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recording_id": {"type": "string", "description": "The ID of the object to get comments for"},
|
||||
"bucket_id": {"type": "string", "description": "The bucket ID"}
|
||||
},
|
||||
"required": ["recording_id", "bucket_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CREATE_COMMENT",
|
||||
"description": "Create a new comment on a Basecamp object",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recording_id": {"type": "string", "description": "The ID of the object to comment on"},
|
||||
"bucket_id": {"type": "string", "description": "The bucket ID"},
|
||||
"content": {"type": "string", "description": "The comment content"}
|
||||
},
|
||||
"required": ["recording_id", "bucket_id", "content"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
return schema
|
||||
|
||||
def handle_composio_request(data):
|
||||
"""
|
||||
Handle a request from Composio following MCP standards.
|
||||
|
||||
Args:
|
||||
data (dict): The request data containing tool name and parameters
|
||||
|
||||
Returns:
|
||||
dict: The result of the tool execution in MCP-compliant format
|
||||
"""
|
||||
# Check if the API key is valid (if provided)
|
||||
composio_api_key = os.getenv('COMPOSIO_API_KEY')
|
||||
request_api_key = request.headers.get('X-Composio-API-Key')
|
||||
|
||||
if composio_api_key and request_api_key and composio_api_key != request_api_key:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "authentication_error",
|
||||
"message": "Invalid API key provided"
|
||||
}
|
||||
}
|
||||
|
||||
tool_name = data.get('tool')
|
||||
params = data.get('params', {})
|
||||
|
||||
# Get a Basecamp client
|
||||
client = get_basecamp_client(auth_mode='oauth')
|
||||
if not client:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "authentication_required",
|
||||
"message": "OAuth authentication required",
|
||||
"auth_url": "http://localhost:8000/"
|
||||
}
|
||||
}
|
||||
|
||||
# Route to the appropriate handler based on tool_name
|
||||
try:
|
||||
if tool_name == "GET_PROJECTS":
|
||||
result = client.get_projects()
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
elif tool_name == "GET_PROJECT":
|
||||
project_id = params.get('project_id')
|
||||
if not project_id:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameter: project_id"
|
||||
}
|
||||
}
|
||||
result = client.get_project(project_id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
elif tool_name == "GET_TODOLISTS":
|
||||
project_id = params.get('project_id')
|
||||
if not project_id:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameter: project_id"
|
||||
}
|
||||
}
|
||||
result = client.get_todolists(project_id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
elif tool_name == "GET_TODOS":
|
||||
todolist_id = params.get('todolist_id')
|
||||
if not todolist_id:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameter: todolist_id"
|
||||
}
|
||||
}
|
||||
result = client.get_todos(todolist_id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
elif tool_name == "GET_CAMPFIRE":
|
||||
project_id = params.get('project_id')
|
||||
if not project_id:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameter: project_id"
|
||||
}
|
||||
}
|
||||
result = client.get_campfires(project_id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"campfire": result
|
||||
}
|
||||
}
|
||||
|
||||
elif tool_name == "GET_CAMPFIRE_LINES":
|
||||
project_id = params.get('project_id')
|
||||
campfire_id = params.get('campfire_id')
|
||||
if not project_id or not campfire_id:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameters: project_id and/or campfire_id"
|
||||
}
|
||||
}
|
||||
result = client.get_campfire_lines(project_id, campfire_id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
elif tool_name == "SEARCH":
|
||||
query = params.get('query')
|
||||
project_id = params.get('project_id')
|
||||
if not query:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameter: query"
|
||||
}
|
||||
}
|
||||
|
||||
search = BasecampSearch(client=client)
|
||||
results = []
|
||||
|
||||
# Search projects
|
||||
if not project_id:
|
||||
projects = search.search_projects(query)
|
||||
if projects:
|
||||
results.extend([{"type": "project", "data": p} for p in projects])
|
||||
|
||||
# If project_id is provided, search within that project
|
||||
if project_id:
|
||||
# Search todolists
|
||||
todolists = search.search_todolists(query, project_id)
|
||||
if todolists:
|
||||
results.extend([{"type": "todolist", "data": t} for t in todolists])
|
||||
|
||||
# Search todos
|
||||
todos = search.search_todos(query, project_id)
|
||||
if todos:
|
||||
results.extend([{"type": "todo", "data": t} for t in todos])
|
||||
|
||||
# Search campfire lines
|
||||
campfires = client.get_campfires(project_id)
|
||||
for campfire in campfires:
|
||||
campfire_id = campfire.get('id')
|
||||
lines = search.search_campfire_lines(query, project_id, campfire_id)
|
||||
if lines:
|
||||
results.extend([{"type": "campfire_line", "data": l} for l in lines])
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
}
|
||||
|
||||
elif tool_name == "GET_COMMENTS":
|
||||
recording_id = params.get('recording_id')
|
||||
bucket_id = params.get('bucket_id')
|
||||
if not recording_id or not bucket_id:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameters: recording_id and/or bucket_id"
|
||||
}
|
||||
}
|
||||
result = client.get_comments(recording_id, bucket_id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
elif tool_name == "CREATE_COMMENT":
|
||||
recording_id = params.get('recording_id')
|
||||
bucket_id = params.get('bucket_id')
|
||||
content = params.get('content')
|
||||
if not recording_id or not bucket_id or not content:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "invalid_parameters",
|
||||
"message": "Missing required parameters"
|
||||
}
|
||||
}
|
||||
result = client.create_comment(recording_id, bucket_id, content)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "unknown_tool",
|
||||
"message": f"Unknown tool: {tool_name}"
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling tool {tool_name}: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"type": "server_error",
|
||||
"message": f"Error executing tool: {str(e)}"
|
||||
}
|
||||
}
|
||||
147
mcp_server.py
147
mcp_server.py
@@ -34,6 +34,14 @@ except Exception as e:
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Import Composio integration components
|
||||
try:
|
||||
from composio_integration import get_schema, handle_composio_request
|
||||
except Exception as e:
|
||||
print(f"Error importing Composio integration: {str(e)}")
|
||||
traceback.print_exc()
|
||||
# Don't exit since Composio integration is optional
|
||||
|
||||
# Helper function for consistent response format
|
||||
def mcp_response(data=None, status="success", error=None, message=None, status_code=200):
|
||||
"""
|
||||
@@ -243,65 +251,38 @@ def handle_options(path):
|
||||
# MCP Info endpoint
|
||||
@app.route('/mcp/info', methods=['GET'])
|
||||
def mcp_info():
|
||||
"""Return information about this MCP server."""
|
||||
logger.info("MCP info endpoint called")
|
||||
try:
|
||||
# Keep this operation lightweight - no external API calls here
|
||||
return jsonify({
|
||||
"name": "Basecamp",
|
||||
"version": "1.0.0",
|
||||
"description": "Basecamp 3 API integration for Cursor",
|
||||
"author": "Cursor",
|
||||
"actions": [
|
||||
{
|
||||
"name": "get_required_parameters",
|
||||
"description": "Get required parameters for connecting to Basecamp"
|
||||
},
|
||||
{
|
||||
"name": "initiate_connection",
|
||||
"description": "Connect to Basecamp using credentials"
|
||||
},
|
||||
{
|
||||
"name": "check_active_connection",
|
||||
"description": "Check if the connection to Basecamp is active"
|
||||
},
|
||||
{
|
||||
"name": "get_projects",
|
||||
"description": "Get all projects with optional filtering"
|
||||
},
|
||||
{
|
||||
"name": "get_todo_lists",
|
||||
"description": "Get all to-do lists for a project"
|
||||
},
|
||||
{
|
||||
"name": "get_todos",
|
||||
"description": "Get all to-dos with various filters"
|
||||
},
|
||||
{
|
||||
"name": "get_comments",
|
||||
"description": "Get comments for a specific recording (todo, message, etc.)"
|
||||
},
|
||||
{
|
||||
"name": "create_comment",
|
||||
"description": "Create a comment on a recording"
|
||||
},
|
||||
{
|
||||
"name": "update_comment",
|
||||
"description": "Update a comment"
|
||||
},
|
||||
{
|
||||
"name": "delete_comment",
|
||||
"description": "Delete a comment"
|
||||
},
|
||||
{
|
||||
"name": "search_all",
|
||||
"description": "Search across all Basecamp resources"
|
||||
}
|
||||
]
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in mcp_info: {str(e)}", exc_info=True)
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
"""
|
||||
Provides information about this MCP server.
|
||||
"""
|
||||
server_info = {
|
||||
"name": "Basecamp MCP Server",
|
||||
"version": "1.0.0",
|
||||
"description": "A MCP server for Basecamp 3",
|
||||
"auth_modes": ["oauth"],
|
||||
"actions": [
|
||||
"get_projects",
|
||||
"get_project",
|
||||
"get_todosets",
|
||||
"get_todolists",
|
||||
"get_todolist",
|
||||
"get_todos",
|
||||
"get_todo",
|
||||
"get_people",
|
||||
"get_campfire",
|
||||
"get_campfire_lines",
|
||||
"get_message_board",
|
||||
"get_messages",
|
||||
"get_schedule",
|
||||
"get_schedule_entries",
|
||||
"get_comments",
|
||||
"create_comment",
|
||||
"update_comment",
|
||||
"delete_comment",
|
||||
"search"
|
||||
],
|
||||
"composio_compatible": True # Add this flag to indicate Composio compatibility
|
||||
}
|
||||
return jsonify(server_info)
|
||||
|
||||
# MCP Action endpoint with improved error handling
|
||||
@app.route('/mcp/action', methods=['POST'])
|
||||
@@ -783,6 +764,54 @@ def tool(connection_id):
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
# Add Composio-specific routes
|
||||
@app.route('/composio/schema', methods=['GET'])
|
||||
def composio_schema():
|
||||
"""
|
||||
Returns the schema for Basecamp tools compatible with Composio.
|
||||
"""
|
||||
try:
|
||||
schema = get_schema()
|
||||
return jsonify(schema)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Composio schema: {str(e)}")
|
||||
return jsonify({"error": "server_error", "message": f"Error generating schema: {str(e)}"}), 500
|
||||
|
||||
@app.route('/composio/tool', methods=['POST'])
|
||||
def composio_tool():
|
||||
"""
|
||||
Handles tool calls from Composio.
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({"error": "invalid_request", "message": "Invalid request data"}), 400
|
||||
|
||||
result = handle_composio_request(data)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling Composio tool call: {str(e)}")
|
||||
return jsonify({"error": "server_error", "message": f"Error handling tool call: {str(e)}"}), 500
|
||||
|
||||
@app.route('/composio/check_auth', methods=['GET'])
|
||||
def composio_check_auth():
|
||||
"""
|
||||
Checks if OAuth authentication is available for Composio.
|
||||
"""
|
||||
try:
|
||||
token_data = token_storage.get_token()
|
||||
if token_data and 'access_token' in token_data:
|
||||
return jsonify({"authenticated": True})
|
||||
else:
|
||||
return jsonify({
|
||||
"authenticated": False,
|
||||
"auth_url": f"http://localhost:8000/",
|
||||
"message": "OAuth authentication required. Please visit the auth URL to authenticate."
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking Composio auth: {str(e)}")
|
||||
return jsonify({"error": "server_error", "message": f"Error checking auth: {str(e)}"}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
logger.info(f"Starting MCP server on port {MCP_PORT}")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
requests==2.31.0
|
||||
python-dotenv==1.0.0
|
||||
flask==2.3.3
|
||||
flask==2.3.3
|
||||
flask-cors==4.0.0
|
||||
Reference in New Issue
Block a user