From 1266ecb921908e02971bbb27727c5d7f24f31a22 Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 20 Aug 2025 14:42:55 +1000 Subject: [PATCH 1/7] Add todo item management functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add create, update, delete, complete, and uncomplete methods to basecamp_client.py - Add tool definitions and execution logic to mcp_server_cli.py - Add async todo management functions to basecamp_fastmcp.py - Support all optional fields: description, assignees, due dates, notifications - Use lambda wrapper for proper keyword argument handling in FastMCP 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- basecamp_client.py | 135 ++++++++++++++++++++++++++++++ basecamp_fastmcp.py | 200 ++++++++++++++++++++++++++++++++++++++++++++ mcp_server_cli.py | 152 +++++++++++++++++++++++++++++++++ 3 files changed, 487 insertions(+) diff --git a/basecamp_client.py b/basecamp_client.py index e4eab37..fc1e697 100644 --- a/basecamp_client.py +++ b/basecamp_client.py @@ -159,6 +159,141 @@ class BasecampClient: else: raise Exception(f"Failed to get todo: {response.status_code} - {response.text}") + def create_todo(self, project_id, todolist_id, content, description=None, assignee_ids=None, + completion_subscriber_ids=None, notify=False, due_on=None, starts_on=None): + """ + Create a new todo item in a todolist. + + Args: + project_id (str): Project ID + todolist_id (str): Todolist ID + content (str): The todo item's text (required) + description (str, optional): HTML description + assignee_ids (list, optional): List of person IDs to assign + completion_subscriber_ids (list, optional): List of person IDs to notify on completion + notify (bool, optional): Whether to notify assignees + due_on (str, optional): Due date in YYYY-MM-DD format + starts_on (str, optional): Start date in YYYY-MM-DD format + + Returns: + dict: The created todo + """ + endpoint = f'buckets/{project_id}/todolists/{todolist_id}/todos.json' + data = {'content': content} + + if description: + data['description'] = description + if assignee_ids: + data['assignee_ids'] = assignee_ids + if completion_subscriber_ids: + data['completion_subscriber_ids'] = completion_subscriber_ids + if notify: + data['notify'] = notify + if due_on: + data['due_on'] = due_on + if starts_on: + data['starts_on'] = starts_on + + response = self.post(endpoint, data) + if response.status_code == 201: + return response.json() + else: + raise Exception(f"Failed to create todo: {response.status_code} - {response.text}") + + def update_todo(self, project_id, todo_id, content=None, description=None, assignee_ids=None, + completion_subscriber_ids=None, due_on=None, starts_on=None): + """ + Update an existing todo item. + + Args: + project_id (str): Project ID + todo_id (str): Todo ID + content (str, optional): The todo item's text + description (str, optional): HTML description + assignee_ids (list, optional): List of person IDs to assign + completion_subscriber_ids (list, optional): List of person IDs to notify on completion + due_on (str, optional): Due date in YYYY-MM-DD format + starts_on (str, optional): Start date in YYYY-MM-DD format + + Returns: + dict: The updated todo + """ + endpoint = f'buckets/{project_id}/todos/{todo_id}.json' + data = {} + + if content: + data['content'] = content + if description: + data['description'] = description + if assignee_ids: + data['assignee_ids'] = assignee_ids + if completion_subscriber_ids: + data['completion_subscriber_ids'] = completion_subscriber_ids + if due_on: + data['due_on'] = due_on + if starts_on: + data['starts_on'] = starts_on + + response = self.put(endpoint, data) + if response.status_code == 200: + return response.json() + else: + raise Exception(f"Failed to update todo: {response.status_code} - {response.text}") + + def delete_todo(self, project_id, todo_id): + """ + Delete a todo item. + + Args: + project_id (str): Project ID + todo_id (str): Todo ID + + Returns: + bool: True if successful + """ + endpoint = f'buckets/{project_id}/todos/{todo_id}.json' + response = self.delete(endpoint) + if response.status_code == 204: + return True + else: + raise Exception(f"Failed to delete todo: {response.status_code} - {response.text}") + + def complete_todo(self, project_id, todo_id): + """ + Mark a todo as complete. + + Args: + project_id (str): Project ID + todo_id (str): Todo ID + + Returns: + dict: Completion details + """ + endpoint = f'buckets/{project_id}/todos/{todo_id}/completion.json' + response = self.post(endpoint) + if response.status_code == 201: + return response.json() + else: + raise Exception(f"Failed to complete todo: {response.status_code} - {response.text}") + + def uncomplete_todo(self, project_id, todo_id): + """ + Mark a todo as incomplete. + + Args: + project_id (str): Project ID + todo_id (str): Todo ID + + Returns: + bool: True if successful + """ + endpoint = f'buckets/{project_id}/todos/{todo_id}/completion.json' + response = self.delete(endpoint) + if response.status_code == 204: + return True + else: + raise Exception(f"Failed to uncomplete todo: {response.status_code} - {response.text}") + # People methods def get_people(self): """Get all people in the account.""" diff --git a/basecamp_fastmcp.py b/basecamp_fastmcp.py index 0934f2a..8b2af4f 100644 --- a/basecamp_fastmcp.py +++ b/basecamp_fastmcp.py @@ -254,6 +254,206 @@ async def get_todos(project_id: str, todolist_id: str) -> Dict[str, Any]: "message": str(e) } +@mcp.tool() +async def create_todo(project_id: str, todolist_id: str, content: str, + description: Optional[str] = None, + assignee_ids: Optional[List[str]] = None, + completion_subscriber_ids: Optional[List[str]] = None, + notify: bool = False, + due_on: Optional[str] = None, + starts_on: Optional[str] = None) -> Dict[str, Any]: + """Create a new todo item in a todo list. + + Args: + project_id: Project ID + todolist_id: The todo list ID + content: The todo item's text (required) + description: HTML description of the todo + assignee_ids: List of person IDs to assign + completion_subscriber_ids: List of person IDs to notify on completion + notify: Whether to notify assignees + due_on: Due date in YYYY-MM-DD format + starts_on: Start date in YYYY-MM-DD format + """ + client = _get_basecamp_client() + if not client: + return _get_auth_error_response() + + try: + # Use lambda to properly handle keyword arguments + todo = await _run_sync( + lambda: client.create_todo( + project_id, todolist_id, content, + description=description, + assignee_ids=assignee_ids, + completion_subscriber_ids=completion_subscriber_ids, + notify=notify, + due_on=due_on, + starts_on=starts_on + ) + ) + return { + "status": "success", + "todo": todo, + "message": f"Todo '{content}' created successfully" + } + except Exception as e: + logger.error(f"Error creating todo: {e}") + if "401" in str(e) and "expired" in str(e).lower(): + return { + "error": "OAuth token expired", + "message": "Your Basecamp OAuth token expired during the API call. Please re-authenticate by visiting http://localhost:8000 and completing the OAuth flow again." + } + return { + "error": "Execution error", + "message": str(e) + } + +@mcp.tool() +async def update_todo(project_id: str, todo_id: str, + content: Optional[str] = None, + description: Optional[str] = None, + assignee_ids: Optional[List[str]] = None, + completion_subscriber_ids: Optional[List[str]] = None, + due_on: Optional[str] = None, + starts_on: Optional[str] = None) -> Dict[str, Any]: + """Update an existing todo item. + + Args: + project_id: Project ID + todo_id: The todo ID + content: The todo item's text + description: HTML description of the todo + assignee_ids: List of person IDs to assign + completion_subscriber_ids: List of person IDs to notify on completion + due_on: Due date in YYYY-MM-DD format + starts_on: Start date in YYYY-MM-DD format + """ + client = _get_basecamp_client() + if not client: + return _get_auth_error_response() + + try: + # Use lambda to properly handle keyword arguments + todo = await _run_sync( + lambda: client.update_todo( + project_id, todo_id, + content=content, + description=description, + assignee_ids=assignee_ids, + completion_subscriber_ids=completion_subscriber_ids, + due_on=due_on, + starts_on=starts_on + ) + ) + return { + "status": "success", + "todo": todo, + "message": "Todo updated successfully" + } + except Exception as e: + logger.error(f"Error updating todo: {e}") + if "401" in str(e) and "expired" in str(e).lower(): + return { + "error": "OAuth token expired", + "message": "Your Basecamp OAuth token expired during the API call. Please re-authenticate by visiting http://localhost:8000 and completing the OAuth flow again." + } + return { + "error": "Execution error", + "message": str(e) + } + +@mcp.tool() +async def delete_todo(project_id: str, todo_id: str) -> Dict[str, Any]: + """Delete a todo item. + + Args: + project_id: Project ID + todo_id: The todo ID + """ + client = _get_basecamp_client() + if not client: + return _get_auth_error_response() + + try: + await _run_sync(client.delete_todo, project_id, todo_id) + return { + "status": "success", + "message": "Todo deleted successfully" + } + except Exception as e: + logger.error(f"Error deleting todo: {e}") + if "401" in str(e) and "expired" in str(e).lower(): + return { + "error": "OAuth token expired", + "message": "Your Basecamp OAuth token expired during the API call. Please re-authenticate by visiting http://localhost:8000 and completing the OAuth flow again." + } + return { + "error": "Execution error", + "message": str(e) + } + +@mcp.tool() +async def complete_todo(project_id: str, todo_id: str) -> Dict[str, Any]: + """Mark a todo item as complete. + + Args: + project_id: Project ID + todo_id: The todo ID + """ + client = _get_basecamp_client() + if not client: + return _get_auth_error_response() + + try: + completion = await _run_sync(client.complete_todo, project_id, todo_id) + return { + "status": "success", + "completion": completion, + "message": "Todo marked as complete" + } + except Exception as e: + logger.error(f"Error completing todo: {e}") + if "401" in str(e) and "expired" in str(e).lower(): + return { + "error": "OAuth token expired", + "message": "Your Basecamp OAuth token expired during the API call. Please re-authenticate by visiting http://localhost:8000 and completing the OAuth flow again." + } + return { + "error": "Execution error", + "message": str(e) + } + +@mcp.tool() +async def uncomplete_todo(project_id: str, todo_id: str) -> Dict[str, Any]: + """Mark a todo item as incomplete. + + Args: + project_id: Project ID + todo_id: The todo ID + """ + client = _get_basecamp_client() + if not client: + return _get_auth_error_response() + + try: + await _run_sync(client.uncomplete_todo, project_id, todo_id) + return { + "status": "success", + "message": "Todo marked as incomplete" + } + except Exception as e: + logger.error(f"Error uncompleting todo: {e}") + if "401" in str(e) and "expired" in str(e).lower(): + return { + "error": "OAuth token expired", + "message": "Your Basecamp OAuth token expired during the API call. Please re-authenticate by visiting http://localhost:8000 and completing the OAuth flow again." + } + return { + "error": "Execution error", + "message": str(e) + } + @mcp.tool() async def global_search(query: str) -> Dict[str, Any]: """Search projects, todos and campfire messages across all projects. diff --git a/mcp_server_cli.py b/mcp_server_cli.py index 8a938bc..f115216 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -88,6 +88,79 @@ class MCPServer: "required": ["project_id", "todolist_id"] } }, + { + "name": "create_todo", + "description": "Create a new todo item in a todo list", + "inputSchema": { + "type": "object", + "properties": { + "project_id": {"type": "string", "description": "Project ID"}, + "todolist_id": {"type": "string", "description": "The todo list ID"}, + "content": {"type": "string", "description": "The todo item's text (required)"}, + "description": {"type": "string", "description": "HTML description of the todo"}, + "assignee_ids": {"type": "array", "items": {"type": "string"}, "description": "List of person IDs to assign"}, + "completion_subscriber_ids": {"type": "array", "items": {"type": "string"}, "description": "List of person IDs to notify on completion"}, + "notify": {"type": "boolean", "description": "Whether to notify assignees"}, + "due_on": {"type": "string", "description": "Due date in YYYY-MM-DD format"}, + "starts_on": {"type": "string", "description": "Start date in YYYY-MM-DD format"} + }, + "required": ["project_id", "todolist_id", "content"] + } + }, + { + "name": "update_todo", + "description": "Update an existing todo item", + "inputSchema": { + "type": "object", + "properties": { + "project_id": {"type": "string", "description": "Project ID"}, + "todo_id": {"type": "string", "description": "The todo ID"}, + "content": {"type": "string", "description": "The todo item's text"}, + "description": {"type": "string", "description": "HTML description of the todo"}, + "assignee_ids": {"type": "array", "items": {"type": "string"}, "description": "List of person IDs to assign"}, + "completion_subscriber_ids": {"type": "array", "items": {"type": "string"}, "description": "List of person IDs to notify on completion"}, + "due_on": {"type": "string", "description": "Due date in YYYY-MM-DD format"}, + "starts_on": {"type": "string", "description": "Start date in YYYY-MM-DD format"} + }, + "required": ["project_id", "todo_id"] + } + }, + { + "name": "delete_todo", + "description": "Delete a todo item", + "inputSchema": { + "type": "object", + "properties": { + "project_id": {"type": "string", "description": "Project ID"}, + "todo_id": {"type": "string", "description": "The todo ID"} + }, + "required": ["project_id", "todo_id"] + } + }, + { + "name": "complete_todo", + "description": "Mark a todo item as complete", + "inputSchema": { + "type": "object", + "properties": { + "project_id": {"type": "string", "description": "Project ID"}, + "todo_id": {"type": "string", "description": "The todo ID"} + }, + "required": ["project_id", "todo_id"] + } + }, + { + "name": "uncomplete_todo", + "description": "Mark a todo item as incomplete", + "inputSchema": { + "type": "object", + "properties": { + "project_id": {"type": "string", "description": "Project ID"}, + "todo_id": {"type": "string", "description": "The todo ID"} + }, + "required": ["project_id", "todo_id"] + } + }, { "name": "search_basecamp", "description": "Search across Basecamp projects, todos, and messages", @@ -813,6 +886,85 @@ class MCPServer: "count": len(todos) } + elif tool_name == "create_todo": + project_id = arguments.get("project_id") + todolist_id = arguments.get("todolist_id") + content = arguments.get("content") + description = arguments.get("description") + assignee_ids = arguments.get("assignee_ids") + completion_subscriber_ids = arguments.get("completion_subscriber_ids") + notify = bool(arguments.get("notify", False)) + due_on = arguments.get("due_on") + starts_on = arguments.get("starts_on") + + todo = client.create_todo( + project_id, todolist_id, content, + description=description, + assignee_ids=assignee_ids, + completion_subscriber_ids=completion_subscriber_ids, + notify=notify, + due_on=due_on, + starts_on=starts_on + ) + return { + "status": "success", + "todo": todo, + "message": f"Todo '{content}' created successfully" + } + + elif tool_name == "update_todo": + project_id = arguments.get("project_id") + todo_id = arguments.get("todo_id") + content = arguments.get("content") + description = arguments.get("description") + assignee_ids = arguments.get("assignee_ids") + completion_subscriber_ids = arguments.get("completion_subscriber_ids") + due_on = arguments.get("due_on") + starts_on = arguments.get("starts_on") + + todo = client.update_todo( + project_id, todo_id, + content=content, + description=description, + assignee_ids=assignee_ids, + completion_subscriber_ids=completion_subscriber_ids, + due_on=due_on, + starts_on=starts_on + ) + return { + "status": "success", + "todo": todo, + "message": "Todo updated successfully" + } + + elif tool_name == "delete_todo": + project_id = arguments.get("project_id") + todo_id = arguments.get("todo_id") + client.delete_todo(project_id, todo_id) + return { + "status": "success", + "message": "Todo deleted successfully" + } + + elif tool_name == "complete_todo": + project_id = arguments.get("project_id") + todo_id = arguments.get("todo_id") + completion = client.complete_todo(project_id, todo_id) + return { + "status": "success", + "completion": completion, + "message": "Todo marked as complete" + } + + elif tool_name == "uncomplete_todo": + project_id = arguments.get("project_id") + todo_id = arguments.get("todo_id") + client.uncomplete_todo(project_id, todo_id) + return { + "status": "success", + "message": "Todo marked as incomplete" + } + elif tool_name == "search_basecamp": query = arguments.get("query") project_id = arguments.get("project_id") From e51b272bd4592508034d64bb227f25a62eaab310 Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 27 Aug 2025 09:57:23 +1000 Subject: [PATCH 2/7] Update mcp_server_cli.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- mcp_server_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mcp_server_cli.py b/mcp_server_cli.py index f115216..d37d307 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -921,6 +921,7 @@ class MCPServer: completion_subscriber_ids = arguments.get("completion_subscriber_ids") due_on = arguments.get("due_on") starts_on = arguments.get("starts_on") + notify = arguments.get("notify") todo = client.update_todo( project_id, todo_id, @@ -928,6 +929,7 @@ class MCPServer: description=description, assignee_ids=assignee_ids, completion_subscriber_ids=completion_subscriber_ids, + notify=notify, due_on=due_on, starts_on=starts_on ) From 5aeec247432570dd804622e00ad5eab35022a39a Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 27 Aug 2025 09:59:19 +1000 Subject: [PATCH 3/7] Update basecamp_client.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- basecamp_client.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/basecamp_client.py b/basecamp_client.py index fc1e697..35dc48e 100644 --- a/basecamp_client.py +++ b/basecamp_client.py @@ -201,7 +201,7 @@ class BasecampClient: raise Exception(f"Failed to create todo: {response.status_code} - {response.text}") def update_todo(self, project_id, todo_id, content=None, description=None, assignee_ids=None, - completion_subscriber_ids=None, due_on=None, starts_on=None): + completion_subscriber_ids=None, notify=None, due_on=None, starts_on=None): """ Update an existing todo item. @@ -212,6 +212,7 @@ class BasecampClient: description (str, optional): HTML description assignee_ids (list, optional): List of person IDs to assign completion_subscriber_ids (list, optional): List of person IDs to notify on completion + notify (bool, optional): Whether to notify assignees due_on (str, optional): Due date in YYYY-MM-DD format starts_on (str, optional): Start date in YYYY-MM-DD format @@ -221,18 +222,23 @@ class BasecampClient: endpoint = f'buckets/{project_id}/todos/{todo_id}.json' data = {} - if content: + if content is not None: data['content'] = content - if description: + if description is not None: data['description'] = description - if assignee_ids: + if assignee_ids is not None: data['assignee_ids'] = assignee_ids - if completion_subscriber_ids: + if completion_subscriber_ids is not None: data['completion_subscriber_ids'] = completion_subscriber_ids - if due_on: + if notify is not None: + data['notify'] = notify + if due_on is not None: data['due_on'] = due_on - if starts_on: + if starts_on is not None: data['starts_on'] = starts_on + + if not data: + raise ValueError("No fields provided to update") response = self.put(endpoint, data) if response.status_code == 200: From 6eb134731b87bd29808df9e893bef9dc740ee084 Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 27 Aug 2025 09:59:40 +1000 Subject: [PATCH 4/7] Update basecamp_fastmcp.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- basecamp_fastmcp.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/basecamp_fastmcp.py b/basecamp_fastmcp.py index 8b2af4f..652cf88 100644 --- a/basecamp_fastmcp.py +++ b/basecamp_fastmcp.py @@ -315,6 +315,7 @@ async def update_todo(project_id: str, todo_id: str, description: Optional[str] = None, assignee_ids: Optional[List[str]] = None, completion_subscriber_ids: Optional[List[str]] = None, + notify: Optional[bool] = None, due_on: Optional[str] = None, starts_on: Optional[str] = None) -> Dict[str, Any]: """Update an existing todo item. @@ -334,6 +335,14 @@ async def update_todo(project_id: str, todo_id: str, return _get_auth_error_response() try: + # Guard against no-op updates + if all(v is None for v in [content, description, assignee_ids, + completion_subscriber_ids, notify, + due_on, starts_on]): + return { + "error": "Invalid input", + "message": "At least one field to update must be provided" + } # Use lambda to properly handle keyword arguments todo = await _run_sync( lambda: client.update_todo( @@ -342,6 +351,7 @@ async def update_todo(project_id: str, todo_id: str, description=description, assignee_ids=assignee_ids, completion_subscriber_ids=completion_subscriber_ids, + notify=notify, due_on=due_on, starts_on=starts_on ) @@ -352,6 +362,8 @@ async def update_todo(project_id: str, todo_id: str, "message": "Todo updated successfully" } except Exception as e: + # existing exception handling… + ... logger.error(f"Error updating todo: {e}") if "401" in str(e) and "expired" in str(e).lower(): return { From e22fa4783cd02524b59e46b7589d1fdc30f5d2cf Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 27 Aug 2025 09:59:56 +1000 Subject: [PATCH 5/7] Update mcp_server_cli.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- mcp_server_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mcp_server_cli.py b/mcp_server_cli.py index d37d307..28028d5 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -119,6 +119,7 @@ class MCPServer: "description": {"type": "string", "description": "HTML description of the todo"}, "assignee_ids": {"type": "array", "items": {"type": "string"}, "description": "List of person IDs to assign"}, "completion_subscriber_ids": {"type": "array", "items": {"type": "string"}, "description": "List of person IDs to notify on completion"}, + "notify": {"type": "boolean", "description": "Whether to notify assignees"}, "due_on": {"type": "string", "description": "Due date in YYYY-MM-DD format"}, "starts_on": {"type": "string", "description": "Start date in YYYY-MM-DD format"} }, From c9ac202bcd71e67bba568455556c1616bca0cb4d Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 27 Aug 2025 10:00:20 +1000 Subject: [PATCH 6/7] Update mcp_server_cli.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- mcp_server_cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mcp_server_cli.py b/mcp_server_cli.py index 28028d5..7981fc1 100755 --- a/mcp_server_cli.py +++ b/mcp_server_cli.py @@ -894,7 +894,11 @@ class MCPServer: description = arguments.get("description") assignee_ids = arguments.get("assignee_ids") completion_subscriber_ids = arguments.get("completion_subscriber_ids") - notify = bool(arguments.get("notify", False)) + notify_arg = arguments.get("notify", False) + if isinstance(notify_arg, str): + notify = notify_arg.strip().lower() in ("1", "true", "yes", "on") + else: + notify = bool(notify_arg) due_on = arguments.get("due_on") starts_on = arguments.get("starts_on") From b7e0bac71af762ba1381710631238b1aa1dcef9f Mon Sep 17 00:00:00 2001 From: Dominik Fretz Date: Wed, 27 Aug 2025 10:11:57 +1000 Subject: [PATCH 7/7] Fix conditional checks in todo payload builders to allow falsey values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed truthy checks to explicit 'is not None' checks in create_todo and update_todo methods to properly handle intentionally empty/false values (empty lists, False) being sent to the API for field clearing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- basecamp_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/basecamp_client.py b/basecamp_client.py index 35dc48e..612ac8b 100644 --- a/basecamp_client.py +++ b/basecamp_client.py @@ -181,17 +181,17 @@ class BasecampClient: endpoint = f'buckets/{project_id}/todolists/{todolist_id}/todos.json' data = {'content': content} - if description: + if description is not None: data['description'] = description - if assignee_ids: + if assignee_ids is not None: data['assignee_ids'] = assignee_ids - if completion_subscriber_ids: + if completion_subscriber_ids is not None: data['completion_subscriber_ids'] = completion_subscriber_ids - if notify: + if notify is not None: data['notify'] = notify - if due_on: + if due_on is not None: data['due_on'] = due_on - if starts_on: + if starts_on is not None: data['starts_on'] = starts_on response = self.post(endpoint, data)