Skip to content

feat(mcp): create, complete, update, and search action items via MCP#8439

Merged
Git-on-my-level merged 8 commits into
BasedHardware:mainfrom
ZachL111:zach/mcp-action-item-writes
Jun 28, 2026
Merged

feat(mcp): create, complete, update, and search action items via MCP#8439
Git-on-my-level merged 8 commits into
BasedHardware:mainfrom
ZachL111:zach/mcp-action-item-writes

Conversation

@ZachL111

@ZachL111 ZachL111 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

The MCP server already lets ChatGPT/Claude read the user's tasks (get_action_items), but not change them. This adds the write half, so an assistant connected to Omi can manage tasks instead of only observing them: create a follow-up it identified, mark one done, reschedule one, delete a stale one, and find a specific task by what it is about before acting on it.

New MCP tools (with matching REST endpoints under /v1/mcp/action-items):

  • create_action_item(description, due_at?, completed?)
  • complete_action_item(action_item_id, completed?) (reopen by passing completed=false)
  • update_action_item(action_item_id, description?, due_at?)
  • delete_action_item(action_item_id)
  • search_action_items(query, limit?) (semantic, completes the search_memories / search_conversations pattern)

This rounds out action items as a two-way surface, the same way memories already have create/edit/delete tools, and moves in the direction of #4862 (make Omi an easily integratable memory bank for ChatGPT/Claude).

Design notes

  • Shared orchestration. Create/complete/update/delete/search all run through a single utils/mcp_action_items.py helper that both transports (REST mcp.py and SSE mcp_sse.py) call thinly. The two MCP paths have drifted before (for example the SSE/REST search alignment in Align SSE MCP memory search filtering with REST #7788), so the write logic lives in one place rather than being duplicated. utils sits below routers, so neither router imports the other.
  • Idempotent create. Creation is content-idempotent on (uid, normalized description) using the existing create_action_item idempotency key, so a model client that retries a tool call after a transport hiccup updates the same task instead of creating duplicates. This mirrors the app's own create endpoint.
  • Searchable immediately. A newly created or re-described task is indexed into the action-item vector store, so it shows up in search_action_items (and the app's task search) the same way app-created tasks do. Vector writes are best-effort: the task is already persisted, so a vector failure only degrades ranking and never loses the task.
  • Paywall and ownership. Complete/update/delete fetch the item first (uid-scoped, so a foreign id can never resolve) and return 404 / MCP -32001 when missing and 402 / MCP -32002 when the item is locked behind a paid plan, matching how the memory writes behave.
  • New action_items.write OAuth scope, advertised in the server metadata. Writes are annotated (delete as destructive). Creation is rate limited (action_items:write, 120/hr) since it is the only unbounded-growth operation; the others act on existing tasks.
  • Due dates accept ISO 8601 or YYYY-MM-DD and are normalized to UTC, so due-date filtering never compares naive and aware datetimes.

Out of scope

MCP-created tasks are not pushed to external task managers (Apple Reminders / Todoist) or armed with a reminder notification. Those are client-side concerns today, and silently syncing AI-created tasks into a user's external tools felt like the wrong default for a first cut. Straightforward to add later if maintainers want it.

Testing

  • New tests/unit/test_mcp_action_item_writes.py (33 tests), registered in test.sh. Covers the shared util (idempotency key, due-date parsing, create/complete/update/delete/search, vector-failure resilience, paywall and not-found guards) and both transports (REST status codes, MCP error codes, tool registration and scope).
  • backend/test.sh passes locally, and the existing MCP suites (data endpoints, search, profile) still pass.
  • black --line-length 120 --skip-string-normalization clean. scripts/lint_async_blockers.py reports no violations.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 6 files

Confidence score: 2/5

  • In backend/routers/mcp_sse.py, SSE write tools (including create_action_item) appear to enforce securitySchemes only in metadata and do not verify action_items.write scope at execution time, so clients with a valid key but insufficient scope could still perform writes — add explicit scope checks in the SSE executor before merging.
  • In backend/routers/mcp_sse.py (create_action_item), the action_items:write limiter is not enforced, which lets clients bypass the intended 120/hour throttle and could lead to abuse or noisy write amplification — wire the same rate-limit guard into the SSE path before merge.
  • In backend/utils/mcp_action_items.py, delete_action_item ignores the DB delete boolean and can report success on no-op/race-condition deletes, which can mislead clients and mask missing-record behavior — raise ActionItemNotFound when the delete result is false.
  • In backend/utils/mcp_action_items.py, update_action_item allows empty-string due_at to clear the field despite the documented contract, and set_completed can throw an unhandled AttributeError if the post-write fetch returns None; with missing REST coverage for some write/search handlers in backend/tests/unit/test_mcp_action_item_writes.py, these regressions are easier to ship — tighten input validation/None checks and add the missing handler tests before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/routers/mcp_sse.py">

<violation number="1" location="backend/routers/mcp_sse.py:929">
P1: `create_action_item` in the SSE tool executor does not enforce the `action_items:write` rate limit, allowing clients to bypass the intended 120/hr creation throttle.</violation>

<violation number="2" location="backend/routers/mcp_sse.py:929">
P1: New write tools rely on metadata-only `securitySchemes` declarations (`ACTION_ITEMS_WRITE_SECURITY`) without enforcing `action_items.write` scope during SSE tool execution. `authenticate_api_key` returns only a `user_id`; the `handle_mcp_message` → `execute_tool` path never validates scopes before allowing creates, updates, completions, or deletes.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

raise ToolExecutionError(str(e), code=-32602)
return {"action_items": items}

elif tool_name == "create_action_item":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: New write tools rely on metadata-only securitySchemes declarations (ACTION_ITEMS_WRITE_SECURITY) without enforcing action_items.write scope during SSE tool execution. authenticate_api_key returns only a user_id; the handle_mcp_messageexecute_tool path never validates scopes before allowing creates, updates, completions, or deletes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/mcp_sse.py, line 929:

<comment>New write tools rely on metadata-only `securitySchemes` declarations (`ACTION_ITEMS_WRITE_SECURITY`) without enforcing `action_items.write` scope during SSE tool execution. `authenticate_api_key` returns only a `user_id`; the `handle_mcp_message` → `execute_tool` path never validates scopes before allowing creates, updates, completions, or deletes.</comment>

<file context>
@@ -820,6 +917,74 @@ def execute_tool(user_id: str, tool_name: str, arguments: dict) -> dict:
+            raise ToolExecutionError(str(e), code=-32602)
+        return {"action_items": items}
+
+    elif tool_name == "create_action_item":
+        try:
+            completed = parse_mcp_bool(arguments.get("completed"), "completed", default=False)
</file context>

raise ToolExecutionError(str(e), code=-32602)
return {"action_items": items}

elif tool_name == "create_action_item":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: create_action_item in the SSE tool executor does not enforce the action_items:write rate limit, allowing clients to bypass the intended 120/hr creation throttle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/mcp_sse.py, line 929:

<comment>`create_action_item` in the SSE tool executor does not enforce the `action_items:write` rate limit, allowing clients to bypass the intended 120/hr creation throttle.</comment>

<file context>
@@ -820,6 +917,74 @@ def execute_tool(user_id: str, tool_name: str, arguments: dict) -> dict:
+            raise ToolExecutionError(str(e), code=-32602)
+        return {"action_items": items}
+
+    elif tool_name == "create_action_item":
+        try:
+            completed = parse_mcp_bool(arguments.get("completed"), "completed", default=False)
</file context>

Comment thread backend/utils/mcp_action_items.py Outdated
Comment thread backend/tests/unit/test_mcp_action_item_writes.py
Comment thread backend/utils/mcp_action_items.py Outdated
Comment thread backend/utils/mcp_action_items.py
@ZachL111

Copy link
Copy Markdown
Contributor Author

Thanks cubic. Addressed the four in-scope issues and pushed a fix:

  • delete_action_item now honors the DB delete result and raises ActionItemNotFound when it returns False (a concurrent no-op after the existence check), instead of reporting success.
  • update_action_item no longer clears the due date on a blank value. A due_at that parses to None is treated as "not provided" and dropped from the update, matching the documented "clearing not supported" contract; passing only a blank due_at now raises a "nothing to update" error.
  • set_completed and update_action_item now reload through a shared _reload() helper that raises ActionItemNotFound if the row vanished between the write and the response read, instead of dereferencing None.
  • Added the missing REST coverage: update_action_item (success + 404) and search_action_items (success + blank-query 422), plus tests for the three fixes above.

On the two SSE findings (scope enforcement and the per-tool rate limit): both are accurate, but they describe the existing MCP server design rather than anything specific to these tools. The SSE executor authenticates with a full-access per-user MCP API key and has never enforced per-tool OAuth scopes for any write tool, including the existing create_memory / edit_memory / delete_memory. And SSE tool calls are already rate limited at the transport layer via check_rate_limit_inline(user_id, "mcp:sse") (2000/hr); the per-tool action_items:write (120/hr) limit is a REST-only tightening, consistent with how memories:create is applied on the REST create_memory and not its SSE counterpart. Enforcing per-tool scopes and per-tool limits inside execute_tool is worth doing, but as a server-wide change across all MCP write tools, since doing it only for action items would be inconsistent and leave the same gap open for memories. Happy to follow up with that as a separate PR if maintainers want it.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @ZachL111 — this is a well-built feature. The centralized orchestration in utils/mcp_action_items.py (so REST and SSE can't drift), content-idempotent create, paywall enforcement via _require_unlocked, uid-scoped DB + vector writes, bounded input (2000-char description, search limit ≤ 50), and tz-normalized due_at parsing all look solid, and the unit coverage across both transports is thorough.

Two notes for a human maintainer, not blockers from me:

  • SSE scope / per-tool rate limit. Agree with your read that enforcing per-tool OAuth scopes and per-tool limits inside execute_tool is a server-wide concern (it affects the existing memory write tools the same way) rather than something to bolt on just for action items. Worth tracking as a follow-up, but not a reason to hold this.
  • Clearing due_at not supported. Documented honestly in the tool description and enforced consistently. Fine for a first cut; just flagging it as a known limitation for whoever owns the action-items surface.

Labeling for product/maintainer review before merge — this rounds out action items as a two-way MCP surface, which is a direction call (whether Omi should let assistants mutate tasks, not just read them), so I'm not approving from here. The implementation itself is in good shape.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to review/approve (e.g. stacked, product, or architecture judgment) feature-fit-review PR touches feature direction; qualitative fit assessed labels Jun 27, 2026

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MCP write tools for action items (create/complete/update/delete/search). New capability; approving as feature.

@Git-on-my-level Git-on-my-level merged commit df09655 into BasedHardware:main Jun 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-fit-review PR touches feature direction; qualitative fit assessed needs-maintainer-review Needs a human maintainer to review/approve (e.g. stacked, product, or architecture judgment)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants