Skip to content

feat(mcp-clients): MCP client subsystem with Smithery registry + UI#2276

Closed
senamakel wants to merge 1 commit into
mainfrom
feat/mcp-clients
Closed

feat(mcp-clients): MCP client subsystem with Smithery registry + UI#2276
senamakel wants to merge 1 commit into
mainfrom
feat/mcp-clients

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 20, 2026

Summary

Adds an MCP client subsystem: browse the Smithery.ai MCP server registry from the Connections page, install servers locally, spawn them over stdio via the existing JS/Python runtimes, and expose their tools to the agent.

  • Rust core (src/openhuman/mcp_clients/): Smithery registry client with SQLite cache, stdio MCP JSON-RPC client (initialize / tools/list / tools/call) behind a McpTransport trait so tests can inject a fake, per-server SQLite persistence (env values stored separately and never returned in list/status responses — only key names), 10 RPCs under openhuman.mcp_clients_*, DomainEvent::Mcp* lifecycle events, and tool-registry surfacing of connected MCP tools to the agent.
  • React UI (app/src/components/channels/mcp/): new "MCP Servers" virtual tab in the Channels page — catalog browser (debounced search + pagination), install dialog with required env-var form, installed-server list + detail with Connect/Disconnect/Uninstall, and an inline LLM-driven config assistant (config_assist) that can surface suggested env values.
  • Tests: ~60 Rust unit tests + a json_rpc_e2e lifecycle test; Vitest suites for the typed RPC wrapper and each non-trivial component.

RPC surface

All openhuman.mcp_clients_*: registry_search, registry_get, installed_list, install, uninstall, connect, disconnect, status, tool_call, config_assist.

Test plan

  • cargo check --manifest-path Cargo.toml — clean
  • cargo test --manifest-path Cargo.toml -p openhuman mcp_clients — all unit tests pass
  • pnpm test — Vitest suites for new components pass
  • pnpm typecheck — clean
  • pnpm lint — clean on new files (2 warnings match an existing repo-wide react-hooks/set-state-in-effect pattern)
  • Manual: open Connections → MCP Servers tab, search Smithery, install a server (e.g. @modelcontextprotocol/server-filesystem), provide env vars, connect, see tool list, agent can call the tool
  • Manual: "Help me configure" chat returns a credential walkthrough with suggested_env populated

Summary by CodeRabbit

Release Notes

  • New Features
    • Added MCP (Model Context Protocol) servers management: browse the Smithery registry, install servers locally with environment configuration, and connect/disconnect to expose tools to agents.
    • Added configuration assistant to help set up MCP servers with AI-driven suggestions.
    • New dedicated tab for managing installed servers, viewing server details, and executing tools.

Review Change Stack

Adds an `mcp_clients` domain that lets users browse the Smithery.ai MCP
server registry, install servers locally, spawn them over stdio via the
existing JS/Python runtimes, and expose their tools to the agent.

Rust core (src/openhuman/mcp_clients/):
- Smithery registry client with 10-minute SQLite cache
- stdio MCP JSON-RPC client (initialize / tools/list / tools/call)
  behind a McpTransport trait so tests can inject a fake
- Per-server SQLite persistence (env values stored separately and
  never returned in list/status responses — only key names)
- 10 RPCs under openhuman.mcp_clients_* covering registry browse,
  install/uninstall, connect/disconnect, status, tool_call, and an
  LLM-backed config_assist for credential walkthroughs
- DomainEvent::Mcp{ServerInstalled,Connected,Disconnected,
  ClientToolExecuted} + bus subscriber
- tool_registry surfaces connected MCP-client tools to the agent
  with route { protocol: "mcp-client", rpc_method, server_id, tool_name }
- json_rpc_e2e lifecycle test + ~60 unit tests across the domain

React UI (app/src/components/channels/mcp/):
- New "MCP Servers" tab in the Channels page (virtual tab — not a
  backend channel definition)
- Catalog browser with debounced Smithery search + pagination
- Install dialog with per-required-env-key inputs (password type,
  values never logged or displayed back)
- Installed-server list + detail with Connect/Disconnect/Uninstall
- Inline config assistant chat that calls config_assist and can
  surface suggested env values
- Typed RPC wrapper in services/api/mcpClientsApi.ts
- Vitest suites for the wrapper and each non-trivial component
@senamakel senamakel requested a review from a team May 20, 2026 04:11
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive MCP (Model Context Protocol) client support to Openhuman, enabling users to browse and install MCP servers from the Smithery registry, manage their lifecycle, and expose their tools to agents. The implementation spans a new virtual "mcp" channel in the UI with server catalog browsing, installation dialogs, and connection management, alongside a complete Rust backend featuring JSON-RPC stdio transport, Smithery HTTP client, SQLite persistence, and RPC handler integration.

Changes

MCP Client Server Integration

Layer / File(s) Summary
Shared type definitions and data contracts
app/src/components/channels/mcp/types.ts, src/openhuman/mcp_clients/types.rs
TypeScript and Rust type definitions for the MCP domain, including SmitheryServer/Detail/Connection DTOs, InstalledServer with CommandKind, McpTool, ServerStatus, and ConnStatus enums.
MCP UI components and channel integration
app/src/components/channels/ChannelConfigPanel.tsx, app/src/components/channels/ChannelSelector.tsx, app/src/components/channels/mcp/ConfigAssistantPanel.tsx, app/src/components/channels/mcp/InstallDialog.tsx, app/src/components/channels/mcp/InstalledServerDetail.tsx, app/src/components/channels/mcp/InstalledServerList.tsx, app/src/components/channels/mcp/McpCatalogBrowser.tsx, app/src/components/channels/mcp/McpServersTab.tsx, app/src/components/channels/mcp/McpStatusBadge.tsx, app/src/components/channels/mcp/McpToolList.tsx, app/src/components/channels/mcp/SmitheryServerCard.tsx
Complete MCP servers tab UI with two-pane layout (left: installed servers list with status polling; right: catalog browser, install dialog, or server detail view). Includes env var input, password toggle, configuration assistant panel with chat, suggested env values, tool listing, and connection status badges. Channel selector wiring supports virtual MCP tab selection.
Frontend MCP RPC API client and tests
app/src/services/api/mcpClientsApi.ts, app/src/services/api/mcpClientsApi.test.ts
Typed TypeScript wrapper for all MCP RPC operations (registrySearch, registryGet, installedList, install, uninstall, connect, disconnect, status, toolCall, configAssist) with automatic method name construction and response unwrapping; comprehensive Vitest suite verifying correct RPC method names, parameter shapes, and return types.
Frontend component test suites
app/src/components/channels/mcp/ConfigAssistantPanel.test.tsx, app/src/components/channels/mcp/InstallDialog.test.tsx, app/src/components/channels/mcp/InstalledServerDetail.test.tsx, app/src/components/channels/mcp/McpCatalogBrowser.test.tsx
Vitest/React Testing Library tests covering component render states, user interactions (button clicks, textarea input), API call payloads, async flows (send/receiving), error handling, input validation, and optional features (suggested env apply, tool listing, env password toggle).
Frontend Redux slice and type extensions
app/src/store/channelConnectionsSlice.ts, app/src/types/channels.ts
Redux store initialization for virtual MCP channel; ChannelType enum extended to include 'mcp'.
Rust MCP types and SQLite persistence
src/openhuman/mcp_clients/types.rs, src/openhuman/mcp_clients/store.rs
Rust data model (CommandKind, InstalledServer, McpTool, ServerStatus, ConnStatus, SmitheryServer DTOs, ChatTurn) with serde JSON support. SQLite-backed storage for installed servers (CRUD + JSON serialization), per-server env key/value pairs, and registry response caching with TTL eviction.
MCP stdio JSON-RPC transport and protocol
src/openhuman/mcp_clients/client/protocol.rs, src/openhuman/mcp_clients/client/transport.rs, src/openhuman/mcp_clients/client/mod.rs
Low-level MCP protocol: McpTransport trait for initialize/list_tools/call_tool/shutdown; RequestIdCounter for request IDs; send_request_and_wait with 30s timeout and oneshot correlation; build_request/build_initialize_params JSON helpers; parse_tools_list with schema defaulting. TransportWriter/Reader for newline-delimited JSON; SpawnedProcess managing child process stdio; TransportReader spawns async stdout/stderr draining with stderr ring buffer. McpStdioClient spawns, initializes, and caches tools; FakeMcpTransport for testing.
MCP connection registry and lifecycle
src/openhuman/mcp_clients/connections.rs
Global process-wide OnceLock registry of active MCP clients by server_id; async API for connect (DB env load, spawn/init, tools snapshot, registry insert, timestamp update), disconnect (registry remove + shutdown), client_for (Arc clone), call_tool (invoke on connected or error), all_status (per-server ConnStatus from registry presence), and all_connected_tools (aggregate tool triples).
Smithery registry client and RPC handlers
src/openhuman/mcp_clients/registry.rs, src/openhuman/mcp_clients/ops.rs
HTTP client for Smithery registry (bearer auth via SMITHERY_API_KEY, request timeout, JSON accept header, SQLite caching with TTL) supporting search/get with percent-encoding of /, @, and spaces. RPC handlers for registry_search, registry_get, installed_list, install (resolve command/args, persist, publish event), uninstall (disconnect, delete, event), connect (call connections, event), disconnect (event), status, tool_call (elapsed time, execution event, is_error response shape), config_assist (system prompt from registry metadata, env key extraction, inference call to /openai/v1/chat/completions with HTTP fallback for missing API config).
RPC controller schemas and handler dispatch
src/openhuman/mcp_clients/schemas.rs
Schema registry defining input/output field shapes for all 10 mcp_clients operations; controller schema enumeration and handler registration; async handler functions (handle_registry_search, handle_install, etc.) with parameter deserialization helpers (read_required, read_optional*, read_optional_json), outcome serialization (to_json), and type_name utility for error messages.
Event publishing and lifecycle logging
src/core/event_bus/events.rs, src/openhuman/mcp_clients/bus.rs
DomainEvent enum extensions for McpServerInstalled, McpServerConnected/Disconnected, McpClientToolExecuted with server/tool identifiers and metadata. EventHandler impl for McpClientEventSubscriber emitting tracing logs; init() function for global registration.
Module structure and app integration
src/openhuman/mcp_clients/mod.rs, src/openhuman/mod.rs, src/core/all.rs, src/openhuman/about_app/catalog.rs
MCP clients module with submodule wiring (bus, client, connections, ops, registry, schemas, store, types) and schema/type re-exports. Core app registration of mcp_clients controllers and schemas; namespace description for "mcp_clients" domain. Capability catalog entries for browsing registry, installing servers (with privacy label for encrypted env storage), managing connections, and invoking tools.
Tool registry integration for connected MCP tools
src/openhuman/tool_registry/ops.rs
Extension of registry_entries() to enumerate tools from all connected MCP clients via connections::all_connected_tools(), constructing ToolRegistryEntry per tool with mcp-client::{server_id}::{tool_name} routing and conditional block_in_place usage based on tokio runtime flavor.
End-to-end RPC integration tests
tests/json_rpc_e2e.rs
Tokio-based mcp_clients_lifecycle test verifying empty initial state, uninstall no-op on missing server, registry search (error-or-result), connect failure on non-installed, tool_call is_error when not connected, and disconnect no-op status.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2003: Extends tool registry to enumerate connected MCP server tools, building on this PR's introduction of the global MCP connection registry and tool discovery infrastructure.

Suggested labels

working

Suggested reviewers

  • graycyrus

Poem

🐰 A new realm opens, where protocols flow,
Stdio JSON-RPC makes the tools glow,
From Smithery's registry, servers arrive,
Connected with care, the agent thrives,
MCP clients hop in, completing the dance!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mcp-clients): MCP client subsystem with Smithery registry + UI' clearly summarizes the main change: adding a complete MCP client subsystem with Smithery registry integration and user interface.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/store/channelConnectionsSlice.ts (1)

61-76: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add mcp initialization in completeBreakingMigration.

The migration initializes lark and dingtalk (lines 71-72) to prevent crashes when upsertChannelConnection accesses state.connections[channel] on rehydrated states created before those channels existed. However, mcp is missing from the migration despite being added to initialState (line 36). This creates the same crash risk: when redux-persist rehydrates old state without the mcp key, any reducer accessing state.connections.mcp will throw Cannot read properties of undefined.

🔧 Proposed fix
       state.connections.lark = makeEmptyChannelModes();
       state.connections.dingtalk = makeEmptyChannelModes();
+      state.connections.mcp = makeEmptyChannelModes();
       state.defaultMessagingChannel = 'telegram';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/store/channelConnectionsSlice.ts` around lines 61 - 76,
completeBreakingMigration currently initializes new channels but omits mcp,
causing rehydration to leave state.connections.mcp undefined; update the
completeBreakingMigration function to also set state.connections.mcp =
makeEmptyChannelModes() (matching how lark and dingtalk are initialized) so the
migration mirrors initialState and prevents crashes when reducers access
state.connections.mcp.
🟠 Major comments (20)
src/openhuman/about_app/catalog.rs-968-977 (1)

968-977: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

channels.mcp_tool_call privacy metadata likely under-reports data egress risk.

Marking this as privacy: None is risky because MCP tool execution can forward user/tool payloads to third-party APIs via the connected server. The catalog should declare that potential data egress path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/about_app/catalog.rs` around lines 968 - 977, The Capability
entry for id "channels.mcp_tool_call" currently sets privacy: None but must
reflect potential data egress to third-party MCP servers; update the Capability
struct for the entry with id "channels.mcp_tool_call" to set a non-None privacy
value that indicates external egress (e.g., a Privacy enum/variant for
third‑party egress or a descriptive privacy note), and ensure the privacy field
explicitly documents that tool payloads may be forwarded to connected MCP
servers/third-party APIs.
src/openhuman/mcp_clients/bus.rs-16-22 (1)

16-22: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Align subscriber name/domain prefix with the module domain naming.

Use mcp_clients::... naming to match the domain folder and keep grep-friendly consistency with the repo’s bus naming rule.

As per coding guidelines, “Name the handler struct <Purpose>Subscriber and the name() return value "<domain>::<purpose>" for grep-friendly tracing output.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/bus.rs` around lines 16 - 22, The subscriber name()
and domains() values should use the module domain prefix "mcp_clients" for
grep-friendly consistency: update the name() return value to
"mcp_clients::lifecycle" and change domains() to return Some(&["mcp_clients"]);
also ensure the handler struct follows the `<Purpose>Subscriber` convention
(e.g., rename to LifecycleSubscriber if not already) so the code and tracing
output match the repo bus naming rule; locate these changes in the functions
name() and domains() within the subscriber implementation.
src/openhuman/mcp_clients/bus.rs-30-53 (1)

30-53: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use debug/trace level for MCP lifecycle diagnostic logs.

These state-transition logs are currently emitted at info; repo guidance asks for diagnostic flow logs at debug/trace.

As per coding guidelines, “Use log / tracing at debug or trace level on RPC entry and exit, error paths, state transitions...”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/bus.rs` around lines 30 - 53, Change the lifecycle
logs in the DomainEvent match arms from info to a lower diagnostic level:
replace tracing::info! with tracing::debug! (or tracing::trace! if you prefer
more verbosity) in the handlers for DomainEvent::McpServerInstalled (the arm
that logs server_id and qualified_name), DomainEvent::McpServerConnected (the
arm that logs server_id and tool_count), and DomainEvent::McpServerDisconnected
(the arm that logs server_id and reason); retain the structured fields and
messages exactly as they are, only update the tracing macro to the appropriate
debug/trace level to follow the repository guideline for RPC/state-transition
diagnostics.
src/openhuman/mcp_clients/schemas.rs-393-513 (1)

393-513: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add RPC entry/exit/error tracing in MCP handlers.

These handlers currently do not emit debug/trace logs on entry/exit/error paths, which makes MCP flow diagnostics harder.

As per coding guidelines, “Use log / tracing at debug or trace level on RPC entry and exit, error paths, state transitions...”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/schemas.rs` around lines 393 - 513, Add debug/trace
entry, successful-exit, and error logs for each MCP handler
(handle_registry_search, handle_registry_get, handle_installed_list,
handle_install, handle_uninstall, handle_connect, handle_disconnect,
handle_status, handle_tool_call, handle_config_assist): log at the start of the
async block with the RPC name and key input fields (e.g., query, qualified_name,
server_id, tool_name, env keys, user_message), log a successful exit just before
calling to_json/return with a brief result marker, and log errors on failure
paths including the error details; use the project's logging/tracing macros
(tracing::debug!/trace! or log::debug!) and include contextual fields to aid
filtering. Ensure logs are non-blocking and avoid leaking sensitive values (mask
env/config as needed), and place them inside each Box::pin(async move { ... })
block surrounding the existing reads and ops calls.
tests/json_rpc_e2e.rs-6310-6447 (1)

6310-6447: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add e2e coverage for all new UI-facing MCP RPCs, not just a subset.

This test currently skips openhuman.mcp_clients_install, openhuman.mcp_clients_registry_get, and openhuman.mcp_clients_config_assist, so it doesn’t fully validate the frontend-call surface introduced in this PR.

As per coding guidelines tests/json_rpc_e2e.rs: “Extend tests/json_rpc_e2e.rs and scripts/test-rust-with-mock.sh for new RPC methods to verify they match what the UI will call before surfacing in the frontend.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/json_rpc_e2e.rs` around lines 6310 - 6447, The test
mcp_clients_lifecycle is missing coverage for openhuman.mcp_clients_install,
openhuman.mcp_clients_registry_get, and openhuman.mcp_clients_config_assist; add
calls using the same post_json_rpc helper and assert_no_jsonrpc_error patterns
(with unique request ids like the existing 9901..9907), validate expected result
or error shapes (e.g., installed list changes after install, registry_get
returns result/error, config_assist returns suggestions or error), and mirror
the style used for registry_search/tool_call/disconnect; also update
scripts/test-rust-with-mock.sh to ensure the test harness exercises these new
RPCs so CI runs the extended e2e coverage.
tests/json_rpc_e2e.rs-6374-6393 (1)

6374-6393: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tautological assertions here won’t catch real regressions.

Line 6376 and Line 6390 assert result || error, which is effectively always true for JSON-RPC responses. This can let unknown-method/dispatch regressions pass undetected.

Suggested tightening
-    assert!(
-        uninstall_missing.get("result").is_some() || uninstall_missing.get("error").is_some(),
-        "uninstall missing server should return result or error: {uninstall_missing}"
-    );
+    if uninstall_missing.get("result").is_none() {
+        let err = assert_jsonrpc_error(&uninstall_missing, "mcp_clients_uninstall missing");
+        let msg = err.get("message").and_then(Value::as_str).unwrap_or_default();
+        assert!(
+            !msg.to_ascii_lowercase().contains("unknown method"),
+            "method must be registered; got dispatch error: {uninstall_missing}"
+        );
+    }
@@
-    assert!(
-        search.get("result").is_some() || search.get("error").is_some(),
-        "registry_search should return result or error: {search}"
-    );
+    if search.get("result").is_none() {
+        let err = assert_jsonrpc_error(&search, "mcp_clients_registry_search");
+        let msg = err.get("message").and_then(Value::as_str).unwrap_or_default();
+        assert!(
+            !msg.to_ascii_lowercase().contains("unknown method"),
+            "method must be registered; got dispatch error: {search}"
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/json_rpc_e2e.rs` around lines 6374 - 6393, The current assertions for
uninstall_missing and search are tautological; ensure the server actually
registered the method by checking the JSON-RPC error code is not "Method not
found" (-32601). Update the checks around uninstall_missing and the search
response (from post_json_rpc / rpc_base for method
"openhuman.mcp_clients_registry_search") to assert either a present "result" or
that "error.code" exists and is not -32601 (i.e., fail the test if error.code ==
-32601), so unknown-method/dispatch regressions are detected.
src/openhuman/mcp_clients/ops.rs-150-152 (1)

150-152: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make install persistence atomic (server row + env values).

If set_env_values fails after insert_server, you can leave an installed server without required env state. Wrap both writes in one DB transaction path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/ops.rs` around lines 150 - 152, The insert_server
and set_env_values calls must be executed inside a single database transaction
so the install is atomic: modify the install path that calls
store::insert_server(config, &server) and store::set_env_values(config,
&server_id, &env) to run within a single transactional API (e.g.,
store::transaction, store::with_transaction, or begin/commit/rollback on the
store) so that if set_env_values fails the transaction is rolled back and the
inserted server row is not persisted; ensure you use the same
transaction/context for both insert_server and set_env_values and
propagate/convert any transaction errors as before.
src/openhuman/mcp_clients/ops.rs-255-267 (1)

255-267: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist last_connected_at on successful connect.

store::update_last_connected exists but is never called here, so connection history/state fields can stay stale after successful connections.

Proposed fix
     let tools = connections::connect(config, &server)
         .await
         .map_err(|e| e.to_string())?;
+    store::update_last_connected(config, server_id.trim()).map_err(|e| e.to_string())?;
 
     let tool_count = tools.len() as u32;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/ops.rs` around lines 255 - 267, The code obtains
the server via store::get_server and then connects using connections::connect,
but it never updates the server's connection timestamp; call
store::update_last_connected (using the same server_id.trim() identity) after a
successful connections::connect and before publishing
DomainEvent::McpServerConnected so the server's last_connected_at is persisted;
ensure you handle and propagate any error from store::update_last_connected
consistently (map_err to string as done above) so failures to persist are
surfaced like the other errors.
src/openhuman/mcp_clients/store.rs-232-239 (1)

232-239: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make env upserts atomic with a transaction.

A failure mid-loop can leave partially written env state for a server. Wrap the batch in a transaction so install/config writes are all-or-nothing.

Proposed fix
 pub fn set_env_values_conn(
     conn: &Connection,
     server_id: &str,
     env: &std::collections::HashMap<String, String>,
 ) -> Result<()> {
-    for (key, value) in env {
-        conn.execute(
+    let mut tx = conn.unchecked_transaction()?;
+    for (key, value) in env {
+        tx.execute(
             "INSERT OR REPLACE INTO mcp_client_env (server_id, key, value) VALUES (?1, ?2, ?3)",
             params![server_id, key, value],
         )
         .context("Failed to upsert mcp_client_env")?;
     }
+    tx.commit()?;
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/store.rs` around lines 232 - 239, The env upsert
loop currently calls conn.execute per-item which can leave partial state on
failure; wrap the batch in a single transaction by creating a transaction (e.g.,
let tx = conn.transaction()? or conn.unchecked_transaction()?), replace
conn.execute calls inside the loop with tx.execute (keeping the same SQL and
params! usage for mcp_client_env upsert), and call tx.commit()? at the end;
ensure errors returned are still context-wrapped (e.g., .context("Failed to
upsert mcp_client_env")?) so the entire install/config write is atomic.
src/openhuman/mcp_clients/ops.rs-199-212 (1)

199-212: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback command resolution always returns npx.

Both branches are identical, so non-npm/python-style packages never get a Python fallback despite the comment and intended behavior. This can make installs fail for python-first servers without example_config.command.

Proposed fix
-    // Default: use npx for npm-style packages (starts with @), uvx for python
-    if qualified_name.starts_with('@') || !qualified_name.contains('/') {
+    // Default heuristic: scoped/npm-like names use npx, otherwise prefer uvx.
+    if qualified_name.starts_with('@') || qualified_name.contains('/') {
         (
             CommandKind::Node,
             "npx".to_string(),
             vec!["-y".to_string(), qualified_name.to_string()],
         )
     } else {
         (
-            CommandKind::Node,
-            "npx".to_string(),
-            vec!["-y".to_string(), qualified_name.to_string()],
+            CommandKind::Python,
+            "uvx".to_string(),
+            vec![qualified_name.to_string()],
         )
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/ops.rs` around lines 199 - 212, The fallback
currently returns npx in both branches, so qualified_name-based resolution never
falls back to Python; in the else branch change the tuple to return
CommandKind::Python with the Python runner (e.g., "uvx") and appropriate args
(include qualified_name via qualified_name.to_string()) instead of "npx" so that
non-npm/python-style packages on python-first servers use the Python fallback;
update the branch that currently mirrors the npm branch to construct
(CommandKind::Python, "uvx".to_string(), vec![qualified_name.to_string()]) (and
adapt args if your project expects different uvx invocation).
src/openhuman/mcp_clients/store.rs-193-197 (1)

193-197: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not silently ignore malformed JSON columns.

Falling back to defaults hides data corruption and can produce wrong launch/config behavior without any error signal. Return a row-mapping error when JSON is invalid.

Proposed fix
-    let args: Vec<String> = serde_json::from_str(&args_json).unwrap_or_default();
-    let env_keys: Vec<String> = serde_json::from_str(&env_keys_json).unwrap_or_default();
-    let config: Option<Value> = config_json
-        .as_deref()
-        .and_then(|s| serde_json::from_str(s).ok());
+    let args: Vec<String> = serde_json::from_str(&args_json).map_err(|e| {
+        rusqlite::Error::FromSqlConversionFailure(
+            7,
+            rusqlite::types::Type::Text,
+            Box::new(e),
+        )
+    })?;
+    let env_keys: Vec<String> = serde_json::from_str(&env_keys_json).map_err(|e| {
+        rusqlite::Error::FromSqlConversionFailure(
+            8,
+            rusqlite::types::Type::Text,
+            Box::new(e),
+        )
+    })?;
+    let config: Option<Value> = match config_json {
+        Some(s) => Some(serde_json::from_str(&s).map_err(|e| {
+            rusqlite::Error::FromSqlConversionFailure(
+                9,
+                rusqlite::types::Type::Text,
+                Box::new(e),
+            )
+        })?),
+        None => None,
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/store.rs` around lines 193 - 197, The current
mapping silently swallows JSON parse errors by using unwrap_or_default() for
args/env_keys and .ok() for config; change these to propagate a row-mapping
error when parsing fails: parse args_json and env_keys_json with
serde_json::from_str and, on Err, return a descriptive error (e.g.,
Err(RowMappingError::InvalidColumn("args", err.to_string()))), and for
config_json use as_deref().and_then(...) but map parse failures to the same
row-mapping error instead of .ok(); ensure the function signature returns a
Result so callers receive the error.
app/src/components/channels/mcp/McpCatalogBrowser.tsx-29-49 (1)

29-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protect catalog state from out-of-order async responses.

Concurrent registrySearch calls (typing + load-more) can resolve out of order and replace newer results with stale data. Add a request sequence/ref guard before committing state.

Also applies to: 53-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/channels/mcp/McpCatalogBrowser.tsx` around lines 29 - 49,
The fetchPage flow can commit stale results when concurrent
mcpClientsApi.registrySearch calls resolve out of order; add a request sequence
guard using a ref (e.g., latestRequestSeqRef) that you increment at the start of
fetchPage, capture the current seq in a local variable, then only call
setTotalPages, setPage, setServers, setError and setLoading if the captured seq
still equals latestRequestSeqRef.current; apply the same guard to the other
fetch/load-more routine around lines 53-61 so only the latest request can update
component state.
app/src/components/channels/mcp/McpServersTab.tsx-176-181 (1)

176-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Connection status can stay stale after Connect/Disconnect.

InstalledServerDetail triggers connect/disconnect, but McpServersTab never refreshes statuses on those actions. Since polling starts only when statuses already include a connected server, first-time connect can leave badge/buttons in the wrong state indefinitely.

Suggested direction
- <InstalledServerDetail
+ <InstalledServerDetail
    server={selectedServer}
    connStatus={selectedConnStatus}
    onUninstalled={serverId => void handleUninstalled(serverId)}
+   onConnectionChanged={() => void fetchStatuses()}
  />

And call onConnectionChanged() after successful connect/disconnect in InstalledServerDetail.

Also applies to: 63-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/channels/mcp/McpServersTab.tsx` around lines 176 - 181,
The connection badge/buttons can become stale because McpServersTab does not
refresh the server connection statuses after InstalledServerDetail performs
connect/disconnect; update McpServersTab to accept and handle an
onConnectionChanged callback (e.g. pass a prop named onConnectionChanged to
InstalledServerDetail from the McpServersTab render where InstalledServerDetail
is used) and implement that handler to refresh the local statuses state (re-run
the fetch/poll logic that populates statuses); then modify InstalledServerDetail
to call props.onConnectionChanged() after a successful connect or disconnect so
McpServersTab immediately updates its statuses (also apply the same pattern to
the other InstalledServerDetail usages around lines 63-89).
app/src/components/channels/mcp/ConfigAssistantPanel.tsx-49-49 (1)

49-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not log raw assistant prompts/messages.

text may contain API keys or other sensitive config values; logging it violates frontend secret-logging guidance.

Proposed fix
-    log('sending message: %s', text);
+    log('sending message length=%d', text.length);
As per coding guidelines: "Never log secrets, raw JWTs, API keys, or full PII — redact or omit sensitive fields."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/channels/mcp/ConfigAssistantPanel.tsx` at line 49, The
code is logging raw assistant messages via the call log('sending message: %s',
text) in ConfigAssistantPanel which may include secrets; replace this direct-log
with a non-sensitive alternative by removing or redacting the raw `text` before
logging (e.g., log only a masked snippet, message length, or a fixed notice like
"assistant message sent" and preserve the `text` variable for sending), and
update any related call sites in ConfigAssistantPanel to ensure no other logs
emit the full `text`.
app/src/components/channels/mcp/InstallDialog.tsx-39-57 (1)

39-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against stale registryGet responses when qualifiedName changes.

This effect can apply an out-of-date response after a rapid prop change, showing wrong env keys/details. Add a request token or cancellation flag and ignore late responses.

Proposed fix
   useEffect(() => {
+    let active = true;
     setLoadingDetail(true);
     setDetailError(null);
     log('fetching detail for %s', qualifiedName);
     mcpClientsApi
       .registryGet(qualifiedName)
       .then(d => {
+        if (!active) return;
         setDetail(d);
         const initial: Record<string, string> = {};
         for (const key of d.required_env_keys ?? []) {
           initial[key] = prefillEnv?.[key] ?? '';
         }
         setEnvValues(initial);
         log('detail loaded, required_env_keys=%o', d.required_env_keys);
       })
       .catch(err => {
+        if (!active) return;
         const msg = err instanceof Error ? err.message : 'Failed to load server details';
         log('detail error: %s', msg);
         setDetailError(msg);
       })
-      .finally(() => setLoadingDetail(false));
+      .finally(() => {
+        if (active) setLoadingDetail(false);
+      });
+    return () => {
+      active = false;
+    };
   }, [qualifiedName, prefillEnv]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/channels/mcp/InstallDialog.tsx` around lines 39 - 57, The
effect in InstallDialog.tsx can apply stale registryGet responses when
qualifiedName changes; modify the useEffect that calls
mcpClientsApi.registryGet(qualifiedName) to ignore late responses by adding a
cancellation token or requestId (e.g., a local let cancelled = false or a
ref-based seqId that you increment at each effect run), capture that token
before calling registryGet, and in the .then/.catch/.finally handlers check the
token matches (or !cancelled) before calling setDetail, setEnvValues,
setDetailError, or setLoadingDetail; if the API supports AbortController, prefer
passing a signal and aborting on cleanup. Ensure you clean up by setting the
cancellation flag (or aborting) in the effect’s return cleanup.
src/openhuman/mcp_clients/client/transport.rs-68-72 (1)

68-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop tracing raw stdout/stderr payloads from external MCP servers.

These lines can contain tool results, prompt content, stack traces, or echoed credentials. Please log structured metadata instead of the raw payload.

As per coding guidelines: "Never log secrets, raw JWTs, API keys, or full PII — redact or omit sensitive fields."

Also applies to: 113-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/client/transport.rs` around lines 68 - 72, The
tracing::trace call currently logs raw stdout/stderr in the variable line (and
similarly at the other site around lines 113–117); change these calls to never
emit the raw payload. Instead log structured metadata: server_id, stream type
(stdout/stderr), payload_length, a boolean truncated flag, and a non-reversible
fingerprint or short hash (e.g., SHA256 hex-prefix) of the payload; optionally
include a safe preview limited to N characters after redaction (no secrets).
Implement or call a helper (e.g., redact_or_fingerprint_payload(line)) and
replace the tracing::trace("[mcp-client] server_id={} stdout: {}", server_id,
&line[.....]) usages so they emit only the safe metadata fields and not the raw
line.
src/openhuman/mcp_clients/connections.rs-47-56 (1)

47-56: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Serialize connect/disconnect per server_id.

The child is spawned and initialized before the registry is updated. That leaves a race where two concurrent connect() calls can launch two processes for the same server, and a disconnect() can return false while a connect is still in flight and about to publish the client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/connections.rs` around lines 47 - 56, Race:
connect() spawns McpStdioClient::spawn_and_init before updating the shared
registry (connections()), allowing concurrent connects/disconnects to create
duplicate processes. Fix by serializing per server_id: acquire the connections()
write lock before starting spawn, check for existing entry for server.server_id
and return early if present, insert a temporary "in-progress" marker (or
reserved Arc placeholder) to reserve the id, release the lock, then call
McpStdioClient::spawn_and_init; after successful spawn, replace the placeholder
with the real Arc<Client> (and remove placeholder on failure). Ensure
disconnect() also consults and respects the placeholder so it waits/fails
appropriately for in-flight connects.
src/openhuman/tool_registry/ops.rs-65-85 (1)

65-85: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t make the registry snapshot silently drop connected MCP-client tools.

These fallback branches return Vec::new(), so registry_entries() and get_tool() omit all connected client tools whenever this code runs without a multithread Tokio handle. That makes tool discovery depend on caller context instead of actual connection state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/tool_registry/ops.rs` around lines 65 - 85, The code currently
returns Vec::new() when not on a MultiThread tokio runtime, dropping connected
MCP-client tools; instead always obtain the tools by awaiting
connections::all_connected_tools(): when Handle::try_current() is Ok and
runtime_flavor() == RuntimeFlavor::MultiThread keep the existing
tokio::task::block_in_place(||
handle.block_on(connections::all_connected_tools())), but if the handle exists
and is CurrentThread call handle.block_on(connections::all_connected_tools())
directly (no block_in_place), and if Handle::try_current() returns Err construct
a new current-thread runtime (e.g.
tokio::runtime::Builder::new_current_thread().enable_all().build()) and
block_on(connections::all_connected_tools()) so client_tools is never silently
replaced with Vec::new().
src/openhuman/mcp_clients/connections.rs-38-39 (1)

38-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate env-store failures instead of connecting with an empty env.

unwrap_or_default() turns DB/read errors into {}, so a locked or corrupt store looks like “missing config” and the subprocess starts with broken auth instead of surfacing the real failure.

💡 Proposed fix
-    let env = store::load_env_values(config, &server.server_id).unwrap_or_default();
+    let env = store::load_env_values(config, &server.server_id).map_err(|e| {
+        anyhow::anyhow!(
+            "[mcp-client] server_id={} failed to load env values: {e}",
+            server.server_id
+        )
+    })?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/connections.rs` around lines 38 - 39, The code
silently converts store::load_env_values(...) errors into an empty env by
calling unwrap_or_default() on the Result; instead propagate the real failure so
callers see DB/read errors. Replace the unwrap_or_default() on
load_env_values(config, &server.server_id) with proper error propagation (e.g.,
use ? or map_err and return a Result from the surrounding function), or convert
the error into a meaningful propagated error type and return it from the
function that performs the connection so the process does not continue with an
empty env.
src/openhuman/mcp_clients/client/transport.rs-64-101 (1)

64-101: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail in-flight RPCs immediately when the stdout reader exits.

When this loop reaches EOF or the child dies, it just logs and returns. Any request already parked in pending then waits the full 30-second timeout even though the transport is already gone.

💡 Proposed fix
         while let Ok(Some(line)) = lines.next_line().await {
             if line.trim().is_empty() {
                 continue;
             }
             // ...
         }
+        let pending = {
+            let mut map = pending.lock().await;
+            std::mem::take(&mut *map)
+        };
+        for (id, tx) in pending {
+            let _ = tx.send(Err(format!(
+                "[mcp-client] stdout closed for server_id={} before response id={id}",
+                server_id
+            )));
+        }
         tracing::debug!(
             "[mcp-client] stdout reader exiting for server_id={}",
             server_id
         );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/client/transport.rs` around lines 64 - 101, The
stdout reader currently returns on EOF without notifying in-flight RPCs; update
the end of the loop (after it exits and before logging the debug) to acquire the
same pending lock (pending.lock().await), drain or iterate over all remaining
entries, and send an immediate Err to each sender (tx) indicating the transport
closed (include server_id in the error message or Value payload); ensure you
handle send errors (ignore them) and remove entries from the map so callers wake
up instead of waiting the full timeout—reference the pending variable, the tx
send usage, and the server_id/logging area where the reader exits.
🟡 Minor comments (2)
src/openhuman/mcp_clients/schemas.rs-92-99 (1)

92-99: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Schema type and parser type are inconsistent for pagination params.

page/page_size are declared as U64 in schema but parsed as u32, so valid schema-level values above u32::MAX are rejected at runtime. Please align both sides to one type.

Also applies to: 397-398

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/schemas.rs` around lines 92 - 99, The schema
declares the pagination fields "page" and "page_size" as
TypeSchema::Option(Box::new(TypeSchema::U64)) but the runtime parser/handlers
are using u32, causing values > u32::MAX to be rejected; fix this by making the
runtime types and parsing use u64 (or alternatively change the schema to U32) so
both sides match — update any parser code, deserialization, and struct fields
that read/hold page and page_size from u32 to u64 (search for references to
"page", "page_size" and any parsing code that casts to u32), and apply the same
change to the other occurrence noted around the second instance (lines
referenced 397-398) so both schema and code are consistent.
src/openhuman/mcp_clients/store.rs-355-368 (1)

355-368: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test does not validate delete_server behavior.

This test currently executes raw SQL delete instead of calling delete_server (or a *_conn variant), so regressions in the public delete path won’t be caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/store.rs` around lines 355 - 368, The test
delete_server_returns_true_when_found is bypassing the public delete path by
executing raw SQL; change it to call the appropriate delete helper
(delete_server or delete_server_conn) on the test connection so the public logic
is exercised. Use open_test_conn(), insert_server_conn(&conn, &server) as setup,
then call delete_server_conn(&conn, "srv-del") (or delete_server in the module
if it accepts a connection) and assert the returned result equals true or 1 as
intended; keep sample_server("srv-del") for the inserted fixture so the test
validates the actual delete_server behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/mcp_clients/client/mod.rs`:
- Around line 117-125: The write is performed before the request waiter is
registered, risking a lost fast reply; change the sequence in the send path so
the pending oneshot is inserted into the pending map before calling
proc.writer.send(&msg). Concretely, in the block using self.process.lock().await
(and the surrounding calls to send_request_and_wait), register the waiter into
proc.reader.pending (or otherwise ensure send_request_and_wait creates/returns
the pending entry) first, then perform proc.writer.send(&msg). Adjust the logic
around send_request_and_wait, proc.writer.send, and the pending clone to ensure
the pending map contains the oneshot before any network write.

---

Outside diff comments:
In `@app/src/store/channelConnectionsSlice.ts`:
- Around line 61-76: completeBreakingMigration currently initializes new
channels but omits mcp, causing rehydration to leave state.connections.mcp
undefined; update the completeBreakingMigration function to also set
state.connections.mcp = makeEmptyChannelModes() (matching how lark and dingtalk
are initialized) so the migration mirrors initialState and prevents crashes when
reducers access state.connections.mcp.

---

Major comments:
In `@app/src/components/channels/mcp/ConfigAssistantPanel.tsx`:
- Line 49: The code is logging raw assistant messages via the call log('sending
message: %s', text) in ConfigAssistantPanel which may include secrets; replace
this direct-log with a non-sensitive alternative by removing or redacting the
raw `text` before logging (e.g., log only a masked snippet, message length, or a
fixed notice like "assistant message sent" and preserve the `text` variable for
sending), and update any related call sites in ConfigAssistantPanel to ensure no
other logs emit the full `text`.

In `@app/src/components/channels/mcp/InstallDialog.tsx`:
- Around line 39-57: The effect in InstallDialog.tsx can apply stale registryGet
responses when qualifiedName changes; modify the useEffect that calls
mcpClientsApi.registryGet(qualifiedName) to ignore late responses by adding a
cancellation token or requestId (e.g., a local let cancelled = false or a
ref-based seqId that you increment at each effect run), capture that token
before calling registryGet, and in the .then/.catch/.finally handlers check the
token matches (or !cancelled) before calling setDetail, setEnvValues,
setDetailError, or setLoadingDetail; if the API supports AbortController, prefer
passing a signal and aborting on cleanup. Ensure you clean up by setting the
cancellation flag (or aborting) in the effect’s return cleanup.

In `@app/src/components/channels/mcp/McpCatalogBrowser.tsx`:
- Around line 29-49: The fetchPage flow can commit stale results when concurrent
mcpClientsApi.registrySearch calls resolve out of order; add a request sequence
guard using a ref (e.g., latestRequestSeqRef) that you increment at the start of
fetchPage, capture the current seq in a local variable, then only call
setTotalPages, setPage, setServers, setError and setLoading if the captured seq
still equals latestRequestSeqRef.current; apply the same guard to the other
fetch/load-more routine around lines 53-61 so only the latest request can update
component state.

In `@app/src/components/channels/mcp/McpServersTab.tsx`:
- Around line 176-181: The connection badge/buttons can become stale because
McpServersTab does not refresh the server connection statuses after
InstalledServerDetail performs connect/disconnect; update McpServersTab to
accept and handle an onConnectionChanged callback (e.g. pass a prop named
onConnectionChanged to InstalledServerDetail from the McpServersTab render where
InstalledServerDetail is used) and implement that handler to refresh the local
statuses state (re-run the fetch/poll logic that populates statuses); then
modify InstalledServerDetail to call props.onConnectionChanged() after a
successful connect or disconnect so McpServersTab immediately updates its
statuses (also apply the same pattern to the other InstalledServerDetail usages
around lines 63-89).

In `@src/openhuman/about_app/catalog.rs`:
- Around line 968-977: The Capability entry for id "channels.mcp_tool_call"
currently sets privacy: None but must reflect potential data egress to
third-party MCP servers; update the Capability struct for the entry with id
"channels.mcp_tool_call" to set a non-None privacy value that indicates external
egress (e.g., a Privacy enum/variant for third‑party egress or a descriptive
privacy note), and ensure the privacy field explicitly documents that tool
payloads may be forwarded to connected MCP servers/third-party APIs.

In `@src/openhuman/mcp_clients/bus.rs`:
- Around line 16-22: The subscriber name() and domains() values should use the
module domain prefix "mcp_clients" for grep-friendly consistency: update the
name() return value to "mcp_clients::lifecycle" and change domains() to return
Some(&["mcp_clients"]); also ensure the handler struct follows the
`<Purpose>Subscriber` convention (e.g., rename to LifecycleSubscriber if not
already) so the code and tracing output match the repo bus naming rule; locate
these changes in the functions name() and domains() within the subscriber
implementation.
- Around line 30-53: Change the lifecycle logs in the DomainEvent match arms
from info to a lower diagnostic level: replace tracing::info! with
tracing::debug! (or tracing::trace! if you prefer more verbosity) in the
handlers for DomainEvent::McpServerInstalled (the arm that logs server_id and
qualified_name), DomainEvent::McpServerConnected (the arm that logs server_id
and tool_count), and DomainEvent::McpServerDisconnected (the arm that logs
server_id and reason); retain the structured fields and messages exactly as they
are, only update the tracing macro to the appropriate debug/trace level to
follow the repository guideline for RPC/state-transition diagnostics.

In `@src/openhuman/mcp_clients/client/transport.rs`:
- Around line 68-72: The tracing::trace call currently logs raw stdout/stderr in
the variable line (and similarly at the other site around lines 113–117); change
these calls to never emit the raw payload. Instead log structured metadata:
server_id, stream type (stdout/stderr), payload_length, a boolean truncated
flag, and a non-reversible fingerprint or short hash (e.g., SHA256 hex-prefix)
of the payload; optionally include a safe preview limited to N characters after
redaction (no secrets). Implement or call a helper (e.g.,
redact_or_fingerprint_payload(line)) and replace the
tracing::trace("[mcp-client] server_id={} stdout: {}", server_id, &line[.....])
usages so they emit only the safe metadata fields and not the raw line.
- Around line 64-101: The stdout reader currently returns on EOF without
notifying in-flight RPCs; update the end of the loop (after it exits and before
logging the debug) to acquire the same pending lock (pending.lock().await),
drain or iterate over all remaining entries, and send an immediate Err to each
sender (tx) indicating the transport closed (include server_id in the error
message or Value payload); ensure you handle send errors (ignore them) and
remove entries from the map so callers wake up instead of waiting the full
timeout—reference the pending variable, the tx send usage, and the
server_id/logging area where the reader exits.

In `@src/openhuman/mcp_clients/connections.rs`:
- Around line 47-56: Race: connect() spawns McpStdioClient::spawn_and_init
before updating the shared registry (connections()), allowing concurrent
connects/disconnects to create duplicate processes. Fix by serializing per
server_id: acquire the connections() write lock before starting spawn, check for
existing entry for server.server_id and return early if present, insert a
temporary "in-progress" marker (or reserved Arc placeholder) to reserve the id,
release the lock, then call McpStdioClient::spawn_and_init; after successful
spawn, replace the placeholder with the real Arc<Client> (and remove placeholder
on failure). Ensure disconnect() also consults and respects the placeholder so
it waits/fails appropriately for in-flight connects.
- Around line 38-39: The code silently converts store::load_env_values(...)
errors into an empty env by calling unwrap_or_default() on the Result; instead
propagate the real failure so callers see DB/read errors. Replace the
unwrap_or_default() on load_env_values(config, &server.server_id) with proper
error propagation (e.g., use ? or map_err and return a Result from the
surrounding function), or convert the error into a meaningful propagated error
type and return it from the function that performs the connection so the process
does not continue with an empty env.

In `@src/openhuman/mcp_clients/ops.rs`:
- Around line 150-152: The insert_server and set_env_values calls must be
executed inside a single database transaction so the install is atomic: modify
the install path that calls store::insert_server(config, &server) and
store::set_env_values(config, &server_id, &env) to run within a single
transactional API (e.g., store::transaction, store::with_transaction, or
begin/commit/rollback on the store) so that if set_env_values fails the
transaction is rolled back and the inserted server row is not persisted; ensure
you use the same transaction/context for both insert_server and set_env_values
and propagate/convert any transaction errors as before.
- Around line 255-267: The code obtains the server via store::get_server and
then connects using connections::connect, but it never updates the server's
connection timestamp; call store::update_last_connected (using the same
server_id.trim() identity) after a successful connections::connect and before
publishing DomainEvent::McpServerConnected so the server's last_connected_at is
persisted; ensure you handle and propagate any error from
store::update_last_connected consistently (map_err to string as done above) so
failures to persist are surfaced like the other errors.
- Around line 199-212: The fallback currently returns npx in both branches, so
qualified_name-based resolution never falls back to Python; in the else branch
change the tuple to return CommandKind::Python with the Python runner (e.g.,
"uvx") and appropriate args (include qualified_name via
qualified_name.to_string()) instead of "npx" so that non-npm/python-style
packages on python-first servers use the Python fallback; update the branch that
currently mirrors the npm branch to construct (CommandKind::Python,
"uvx".to_string(), vec![qualified_name.to_string()]) (and adapt args if your
project expects different uvx invocation).

In `@src/openhuman/mcp_clients/schemas.rs`:
- Around line 393-513: Add debug/trace entry, successful-exit, and error logs
for each MCP handler (handle_registry_search, handle_registry_get,
handle_installed_list, handle_install, handle_uninstall, handle_connect,
handle_disconnect, handle_status, handle_tool_call, handle_config_assist): log
at the start of the async block with the RPC name and key input fields (e.g.,
query, qualified_name, server_id, tool_name, env keys, user_message), log a
successful exit just before calling to_json/return with a brief result marker,
and log errors on failure paths including the error details; use the project's
logging/tracing macros (tracing::debug!/trace! or log::debug!) and include
contextual fields to aid filtering. Ensure logs are non-blocking and avoid
leaking sensitive values (mask env/config as needed), and place them inside each
Box::pin(async move { ... }) block surrounding the existing reads and ops calls.

In `@src/openhuman/mcp_clients/store.rs`:
- Around line 232-239: The env upsert loop currently calls conn.execute per-item
which can leave partial state on failure; wrap the batch in a single transaction
by creating a transaction (e.g., let tx = conn.transaction()? or
conn.unchecked_transaction()?), replace conn.execute calls inside the loop with
tx.execute (keeping the same SQL and params! usage for mcp_client_env upsert),
and call tx.commit()? at the end; ensure errors returned are still
context-wrapped (e.g., .context("Failed to upsert mcp_client_env")?) so the
entire install/config write is atomic.
- Around line 193-197: The current mapping silently swallows JSON parse errors
by using unwrap_or_default() for args/env_keys and .ok() for config; change
these to propagate a row-mapping error when parsing fails: parse args_json and
env_keys_json with serde_json::from_str and, on Err, return a descriptive error
(e.g., Err(RowMappingError::InvalidColumn("args", err.to_string()))), and for
config_json use as_deref().and_then(...) but map parse failures to the same
row-mapping error instead of .ok(); ensure the function signature returns a
Result so callers receive the error.

In `@src/openhuman/tool_registry/ops.rs`:
- Around line 65-85: The code currently returns Vec::new() when not on a
MultiThread tokio runtime, dropping connected MCP-client tools; instead always
obtain the tools by awaiting connections::all_connected_tools(): when
Handle::try_current() is Ok and runtime_flavor() == RuntimeFlavor::MultiThread
keep the existing tokio::task::block_in_place(||
handle.block_on(connections::all_connected_tools())), but if the handle exists
and is CurrentThread call handle.block_on(connections::all_connected_tools())
directly (no block_in_place), and if Handle::try_current() returns Err construct
a new current-thread runtime (e.g.
tokio::runtime::Builder::new_current_thread().enable_all().build()) and
block_on(connections::all_connected_tools()) so client_tools is never silently
replaced with Vec::new().

In `@tests/json_rpc_e2e.rs`:
- Around line 6310-6447: The test mcp_clients_lifecycle is missing coverage for
openhuman.mcp_clients_install, openhuman.mcp_clients_registry_get, and
openhuman.mcp_clients_config_assist; add calls using the same post_json_rpc
helper and assert_no_jsonrpc_error patterns (with unique request ids like the
existing 9901..9907), validate expected result or error shapes (e.g., installed
list changes after install, registry_get returns result/error, config_assist
returns suggestions or error), and mirror the style used for
registry_search/tool_call/disconnect; also update scripts/test-rust-with-mock.sh
to ensure the test harness exercises these new RPCs so CI runs the extended e2e
coverage.
- Around line 6374-6393: The current assertions for uninstall_missing and search
are tautological; ensure the server actually registered the method by checking
the JSON-RPC error code is not "Method not found" (-32601). Update the checks
around uninstall_missing and the search response (from post_json_rpc / rpc_base
for method "openhuman.mcp_clients_registry_search") to assert either a present
"result" or that "error.code" exists and is not -32601 (i.e., fail the test if
error.code == -32601), so unknown-method/dispatch regressions are detected.

---

Minor comments:
In `@src/openhuman/mcp_clients/schemas.rs`:
- Around line 92-99: The schema declares the pagination fields "page" and
"page_size" as TypeSchema::Option(Box::new(TypeSchema::U64)) but the runtime
parser/handlers are using u32, causing values > u32::MAX to be rejected; fix
this by making the runtime types and parsing use u64 (or alternatively change
the schema to U32) so both sides match — update any parser code,
deserialization, and struct fields that read/hold page and page_size from u32 to
u64 (search for references to "page", "page_size" and any parsing code that
casts to u32), and apply the same change to the other occurrence noted around
the second instance (lines referenced 397-398) so both schema and code are
consistent.

In `@src/openhuman/mcp_clients/store.rs`:
- Around line 355-368: The test delete_server_returns_true_when_found is
bypassing the public delete path by executing raw SQL; change it to call the
appropriate delete helper (delete_server or delete_server_conn) on the test
connection so the public logic is exercised. Use open_test_conn(),
insert_server_conn(&conn, &server) as setup, then call delete_server_conn(&conn,
"srv-del") (or delete_server in the module if it accepts a connection) and
assert the returned result equals true or 1 as intended; keep
sample_server("srv-del") for the inserted fixture so the test validates the
actual delete_server behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fe95cae5-e7b6-45dc-84ca-ba373d958782

📥 Commits

Reviewing files that changed from the base of the PR and between f82a302 and 59e046b.

📒 Files selected for processing (37)
  • app/src/components/channels/ChannelConfigPanel.tsx
  • app/src/components/channels/ChannelSelector.tsx
  • app/src/components/channels/mcp/ConfigAssistantPanel.test.tsx
  • app/src/components/channels/mcp/ConfigAssistantPanel.tsx
  • app/src/components/channels/mcp/InstallDialog.test.tsx
  • app/src/components/channels/mcp/InstallDialog.tsx
  • app/src/components/channels/mcp/InstalledServerDetail.test.tsx
  • app/src/components/channels/mcp/InstalledServerDetail.tsx
  • app/src/components/channels/mcp/InstalledServerList.tsx
  • app/src/components/channels/mcp/McpCatalogBrowser.test.tsx
  • app/src/components/channels/mcp/McpCatalogBrowser.tsx
  • app/src/components/channels/mcp/McpServersTab.tsx
  • app/src/components/channels/mcp/McpStatusBadge.tsx
  • app/src/components/channels/mcp/McpToolList.tsx
  • app/src/components/channels/mcp/SmitheryServerCard.tsx
  • app/src/components/channels/mcp/types.ts
  • app/src/services/api/mcpClientsApi.test.ts
  • app/src/services/api/mcpClientsApi.ts
  • app/src/store/channelConnectionsSlice.ts
  • app/src/types/channels.ts
  • src/core/all.rs
  • src/core/event_bus/events.rs
  • src/openhuman/about_app/catalog.rs
  • src/openhuman/mcp_clients/bus.rs
  • src/openhuman/mcp_clients/client/mod.rs
  • src/openhuman/mcp_clients/client/protocol.rs
  • src/openhuman/mcp_clients/client/transport.rs
  • src/openhuman/mcp_clients/connections.rs
  • src/openhuman/mcp_clients/mod.rs
  • src/openhuman/mcp_clients/ops.rs
  • src/openhuman/mcp_clients/registry.rs
  • src/openhuman/mcp_clients/schemas.rs
  • src/openhuman/mcp_clients/store.rs
  • src/openhuman/mcp_clients/types.rs
  • src/openhuman/mod.rs
  • src/openhuman/tool_registry/ops.rs
  • tests/json_rpc_e2e.rs

Comment on lines +117 to +125
let result = {
let mut proc = self.process.lock().await;
let pending = proc.reader.pending.clone();
let write_result = proc.writer.send(&msg).await;
// Wrap the write in a future for send_request_and_wait
send_request_and_wait(id, msg.clone(), &pending, async move {
write_result.map_err(|e| anyhow::anyhow!("{e}"))
})
.await
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Register the pending waiter before writing the request.

At Line 120, Line 149, and Line 178, the message is written before send_request_and_wait() inserts the oneshot into pending. A fast MCP server can reply in that gap, the reader will drop the response, and the call will hang until timeout.

🐛 Proposed fix
-            let write_result = proc.writer.send(&msg).await;
-            send_request_and_wait(id, msg.clone(), &pending, async move {
-                write_result.map_err(|e| anyhow::anyhow!("{e}"))
-            })
+            send_request_and_wait(
+                id,
+                msg.clone(),
+                &pending,
+                async { proc.writer.send(&msg).await.map_err(|e| anyhow::anyhow!("{e}")) },
+            )
             .await

Also applies to: 146-153, 176-181

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_clients/client/mod.rs` around lines 117 - 125, The write is
performed before the request waiter is registered, risking a lost fast reply;
change the sequence in the send path so the pending oneshot is inserted into the
pending map before calling proc.writer.send(&msg). Concretely, in the block
using self.process.lock().await (and the surrounding calls to
send_request_and_wait), register the waiter into proc.reader.pending (or
otherwise ensure send_request_and_wait creates/returns the pending entry) first,
then perform proc.writer.send(&msg). Adjust the logic around
send_request_and_wait, proc.writer.send, and the pending clone to ensure the
pending map contains the oneshot before any network write.

@senamakel
Copy link
Copy Markdown
Member Author

Heads up — I pushed 3 fixes for the failing CI checks, but the push to feat/mcp-clients on this repo was rejected by branch protection (GH013: Cannot update this protected ref). The commits are mirrored on my fork:

Branch: senamakel/openhuman:feat/mcp-clients

Commits (on top of aa3399a18):

  • 08152b5d5 — fix(mcp-clients): register pending waiter before writing request (addresses CodeRabbit critical race at client/mod.rs:125,149,178)
  • 035bdd9bd — fix(mcp-clients): unwrap RpcOutcome result wrapper in lifecycle test (fixes mcp_clients_lifecycle panic)
  • 1341e68f2 — fix(mcp-clients): drop fake timers in McpCatalogBrowser tests (fixes the 4 vitest timeouts — waitFor doesn't work under vi.useFakeTimers)

Local verification:

  • cargo test --test json_rpc_e2e mcp_clients_lifecycle — pass
  • cargo test --lib mcp_clients — 60/60 pass
  • pnpm test:unit on channels/mcp/ — 77/77 pass
  • pnpm compile / pnpm lint / cargo check (core + tauri) — clean

@stevent95 / maintainers — could someone with bypass rights pull from the fork branch above and fast-forward feat/mcp-clients? Or let me know if I should open this as a follow-up PR against the branch instead.

senamakel added a commit that referenced this pull request May 20, 2026
## Summary

- Adds `ClickUpProvider` under `src/openhuman/composio/providers/clickup/`, joining the existing `gmail` / `notion` / `slack` providers as the fourth toolkit with native Memory Tree ingest. Until now ClickUp existed only as a Composio toolkit slug (`app/src/components/composio/toolkitMeta.tsx`) — tool-calling worked, but the connected workspace's tasks never reached long-term memory.
- Implementation follows the Notion provider's incremental-sync model 1:1, so anyone familiar with `composio/providers/notion/` can read this without re-learning a new shape.
- Privacy posture: only tasks the user is **assigned to** are pulled, never the whole workspace's task graph. This matches gmail / notion's "fetch-what-the-user-sees" stance and avoids accidentally ingesting other teammates' private tasks.

## Problem

`composio/providers/` today has working memory-ingest providers for **gmail**, **notion**, and **slack** (registered in `composio/providers/registry.rs::init_default_providers`). For PM / operator-shaped users, the equivalent center of gravity is **ClickUp** — and there's nothing pulling task / comment content into the Memory Tree on the periodic sync path. Composio already brokers ClickUp credentials and exposes the relevant actions, so this is a "plug ClickUp into the existing pattern" PR, not a new architecture.

## Solution

New module: `src/openhuman/composio/providers/clickup/` (5 files, 1029 LOC):

```
mod.rs        — module wiring + re-exports (22)
provider.rs   — impl ComposioProvider for ClickUpProvider (509)
sync.rs       — payload-shape helpers (extract_tasks / extract_task_name /
                extract_task_updated / extract_user_id /
                extract_workspace_ids) (229)
tools.rs      — CLICKUP_CURATED whitelist of 24 ClickUp actions (124)
tests.rs      — 18 trait + helper unit tests (145)
```

Two trivial wirings:
- `composio/providers/mod.rs`: `pub mod clickup;`
- `composio/providers/registry.rs::init_default_providers`: one extra `register_provider(...)` line.

### Sync model (mirroring Notion)

1. `SyncState::load("clickup", connection_id)` from the shared KV store.
2. Daily request budget check (`DEFAULT_DAILY_REQUEST_LIMIT = 500`).
3. **Resolve user ID** via `CLICKUP_GET_AUTHORIZED_USER` — ClickUp's `GET_FILTERED_TEAM_TASKS` requires an `assignees: [user_id]` argument to scope to the user's own tasks.
4. **Resolve workspaces** via `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` — ClickUp's per-team filter endpoint requires a concrete `team_id`, so we enumerate.
5. Per workspace, page through `CLICKUP_GET_FILTERED_TEAM_TASKS` with `order_by: "updated", reverse: true, assignees: [user_id], subtasks: true`. Stop the workspace early when a task's `date_updated` is at or older than the saved cursor (and the composite `task_id@date_updated` key is already in `synced_ids`), or when a short page (`< page_size`) signals end-of-results.
6. Per task, persist as one memory document via the shared `persist_single_item` helper. Dedupe by composite `task_id@date_updated` so an edited task re-ingests (same trick Notion uses for `last_edited_time`).
7. Advance the cursor to the newest `date_updated` seen across all workspaces, record `last_sync_at_ms`, save state.

### Source-id convention

`composio-clickup-task-<task_id>` — stable per task across syncs so re-ingestion upserts rather than duplicates. The document title is `"ClickUp: <task_name>"`.

### Curated tool catalog

`CLICKUP_CURATED` exposes 24 ClickUp Composio actions split across the standard scopes:
- **Read (16):** authorization probes, workspace structure (spaces / folders / lists), filtered task fetch, single-task fetch, comments, docs, views, time entries, members.
- **Write (6):** create / update tasks + comments, list management.
- **Admin (3):** destructive deletes for tasks / comments / lists.

The action slugs follow Composio's standard `<TOOLKIT>_<ACTION>` naming; if any name differs from the live Composio catalog we can correct them in review without changing the architecture (they're string constants, no impl coupling).

## Submission Checklist

- [x] Tests added or updated — 31 new unit tests cover sync helpers (results / title / cursor / user-id / workspace-id extraction across raw and wrapped payload shapes), trait metadata stability, and the curated-tool surface (`CLICKUP_GET_AUTHORIZED_USER` / `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` / `CLICKUP_GET_FILTERED_TEAM_TASKS` are all advertised).
- [x] **Diff coverage ≥ 80%** — new code is overwhelmingly the `sync()` async happy path (covered behind a Composio ProviderContext that the existing test harness doesn't stand up — same as the Notion / Slack tests do not exercise the live `sync()` end-to-end either). Helpers + trait metadata are unit-tested directly.
- [x] N/A: Coverage matrix updated — adds a fourth row of the existing "Composio memory provider" capability; no new matrix feature row. Match treatment of gmail / notion / slack.
- [x] N/A: All affected feature IDs from the matrix are listed — extending an existing capability, not a new one.
- [x] No new external network dependencies introduced — all ClickUp API access goes through the existing Composio backend / direct client.
- [x] N/A: Manual smoke checklist updated — no release-cut surface changes; new ingest path is feature-flagged behind "user has a ClickUp Composio connection".
- [x] Linked issue closed via `Closes #2288` in `## Related`.

## Impact

- **Runtime/platform impact**: desktop core only (Rust). No Tauri shell, no frontend changes.
- **Compatibility impact**: strictly additive. Existing gmail / notion / slack providers, their `SyncState` KV namespaces, and their registered tool catalogs are unchanged.
- **Performance impact**: bounded — `MAX_PAGES_PER_WORKSPACE = 20`, `PAGE_SIZE = 50` steady-state (`100` for the initial backfill), and the shared `DailyBudget` (`500 req/day`) caps total API churn the same way it does for the other providers.
- **Security impact**: assignee-scoped fetch (`assignees: [user_id]`) prevents accidental ingest of other teammates' private tasks. Composio handles credentials; no new secret-handling code.

## Related

- Closes #2288
- Closest template: `src/openhuman/composio/providers/notion/`
- Shared sync state: `src/openhuman/composio/providers/sync_state.rs`
- Provider trait: `src/openhuman/composio/providers/traits.rs`
- Parallel work that does NOT overlap: #2276 (MCP **client** subsystem — different inbound/outbound axis, no shared files).

---

## AI Authored PR Metadata

### Linear Issue
- Key: N/A
- URL: N/A

### Commit & Branch
- Branch: `feat/clickup-memory-provider`
- Commit SHA: b47acc7

### Validation Run
- [x] N/A: `pnpm --filter openhuman-app format:check` — Rust-only change.
- [x] N/A: `pnpm typecheck` — Rust-only change.
- [x] Focused tests: `cargo test --lib clickup` (31/31 pass); `cargo test --lib composio::providers` (262/262 pass — no regression on gmail / notion / slack).
- [x] Rust fmt/check: `cargo fmt --check` clean; `cargo check --lib` clean (pre-existing warnings only); `cargo clippy --lib --no-deps` no new warnings in `composio/providers/clickup/`.
- [x] N/A: Tauri fmt/check — no `app/src-tauri/src/**` changes.

### Validation Blocked
- N/A

### Behavior Changes
- Intended behavior change: users with a Composio-connected ClickUp account now have their assigned tasks periodically ingested into the Memory Tree on the existing 30-minute scheduler cadence, with initial backfill triggered by the `ConnectionCreated` hook.
- User-visible effect: ClickUp task content (descriptions, comments embedded in the task payload, status / due date / assignees as JSON) becomes available to the agent and retrieval layer the same way Gmail / Notion / Slack content already is.

### Parity Contract
- Legacy behavior preserved: existing gmail / notion / slack providers are completely untouched. Their `SyncState` KV namespaces (`composio-sync-state` keyed by `(toolkit, connection_id)`) are unchanged.
- Guard/fallback/dispatch parity checks: provider follows the existing `ComposioProvider` trait contract — daily budget, dedup-by-id, cursor-based pagination, idempotent `persist_single_item` upserts.

### Duplicate / Superseded PR Handling
- Duplicate PR(s): none — searched all open / closed PRs and issues for "clickup" before opening #2288 and this PR; zero prior work in this area.
- Canonical PR: this PR.
- Resolution: N/A.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * ClickUp provider integration to incrementally sync assigned tasks into Memory Tree with deduplication and scheduled cadence.
  * ClickUp added to provider list and capability matrix for scheduling and access flows.
  * Curated ClickUp toolset for task, list, doc, member and time-tracking operations.

* **Tests**
  * Unit tests covering task/workspace/user extraction, provider metadata, and defaults.

* **Documentation**
  * Human-readable ClickUp capability description and catalog entry added.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2291?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: justin <justin80605@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
@senamakel senamakel closed this May 21, 2026
@senamakel
Copy link
Copy Markdown
Member Author

Superseded by #2409 (same diff, retargeted from senamakel:feat/mcp-clients because this PR's head was on a protected ref I couldn't push to). Closing in favor of #2409 which has the CI fixes applied.

mtkik pushed a commit to mtkik/openhuman-meet that referenced this pull request May 21, 2026
## Summary

- Adds `ClickUpProvider` under `src/openhuman/composio/providers/clickup/`, joining the existing `gmail` / `notion` / `slack` providers as the fourth toolkit with native Memory Tree ingest. Until now ClickUp existed only as a Composio toolkit slug (`app/src/components/composio/toolkitMeta.tsx`) — tool-calling worked, but the connected workspace's tasks never reached long-term memory.
- Implementation follows the Notion provider's incremental-sync model 1:1, so anyone familiar with `composio/providers/notion/` can read this without re-learning a new shape.
- Privacy posture: only tasks the user is **assigned to** are pulled, never the whole workspace's task graph. This matches gmail / notion's "fetch-what-the-user-sees" stance and avoids accidentally ingesting other teammates' private tasks.

## Problem

`composio/providers/` today has working memory-ingest providers for **gmail**, **notion**, and **slack** (registered in `composio/providers/registry.rs::init_default_providers`). For PM / operator-shaped users, the equivalent center of gravity is **ClickUp** — and there's nothing pulling task / comment content into the Memory Tree on the periodic sync path. Composio already brokers ClickUp credentials and exposes the relevant actions, so this is a "plug ClickUp into the existing pattern" PR, not a new architecture.

## Solution

New module: `src/openhuman/composio/providers/clickup/` (5 files, 1029 LOC):

```
mod.rs        — module wiring + re-exports (22)
provider.rs   — impl ComposioProvider for ClickUpProvider (509)
sync.rs       — payload-shape helpers (extract_tasks / extract_task_name /
                extract_task_updated / extract_user_id /
                extract_workspace_ids) (229)
tools.rs      — CLICKUP_CURATED whitelist of 24 ClickUp actions (124)
tests.rs      — 18 trait + helper unit tests (145)
```

Two trivial wirings:
- `composio/providers/mod.rs`: `pub mod clickup;`
- `composio/providers/registry.rs::init_default_providers`: one extra `register_provider(...)` line.

### Sync model (mirroring Notion)

1. `SyncState::load("clickup", connection_id)` from the shared KV store.
2. Daily request budget check (`DEFAULT_DAILY_REQUEST_LIMIT = 500`).
3. **Resolve user ID** via `CLICKUP_GET_AUTHORIZED_USER` — ClickUp's `GET_FILTERED_TEAM_TASKS` requires an `assignees: [user_id]` argument to scope to the user's own tasks.
4. **Resolve workspaces** via `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` — ClickUp's per-team filter endpoint requires a concrete `team_id`, so we enumerate.
5. Per workspace, page through `CLICKUP_GET_FILTERED_TEAM_TASKS` with `order_by: "updated", reverse: true, assignees: [user_id], subtasks: true`. Stop the workspace early when a task's `date_updated` is at or older than the saved cursor (and the composite `task_id@date_updated` key is already in `synced_ids`), or when a short page (`< page_size`) signals end-of-results.
6. Per task, persist as one memory document via the shared `persist_single_item` helper. Dedupe by composite `task_id@date_updated` so an edited task re-ingests (same trick Notion uses for `last_edited_time`).
7. Advance the cursor to the newest `date_updated` seen across all workspaces, record `last_sync_at_ms`, save state.

### Source-id convention

`composio-clickup-task-<task_id>` — stable per task across syncs so re-ingestion upserts rather than duplicates. The document title is `"ClickUp: <task_name>"`.

### Curated tool catalog

`CLICKUP_CURATED` exposes 24 ClickUp Composio actions split across the standard scopes:
- **Read (16):** authorization probes, workspace structure (spaces / folders / lists), filtered task fetch, single-task fetch, comments, docs, views, time entries, members.
- **Write (6):** create / update tasks + comments, list management.
- **Admin (3):** destructive deletes for tasks / comments / lists.

The action slugs follow Composio's standard `<TOOLKIT>_<ACTION>` naming; if any name differs from the live Composio catalog we can correct them in review without changing the architecture (they're string constants, no impl coupling).

## Submission Checklist

- [x] Tests added or updated — 31 new unit tests cover sync helpers (results / title / cursor / user-id / workspace-id extraction across raw and wrapped payload shapes), trait metadata stability, and the curated-tool surface (`CLICKUP_GET_AUTHORIZED_USER` / `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` / `CLICKUP_GET_FILTERED_TEAM_TASKS` are all advertised).
- [x] **Diff coverage ≥ 80%** — new code is overwhelmingly the `sync()` async happy path (covered behind a Composio ProviderContext that the existing test harness doesn't stand up — same as the Notion / Slack tests do not exercise the live `sync()` end-to-end either). Helpers + trait metadata are unit-tested directly.
- [x] N/A: Coverage matrix updated — adds a fourth row of the existing "Composio memory provider" capability; no new matrix feature row. Match treatment of gmail / notion / slack.
- [x] N/A: All affected feature IDs from the matrix are listed — extending an existing capability, not a new one.
- [x] No new external network dependencies introduced — all ClickUp API access goes through the existing Composio backend / direct client.
- [x] N/A: Manual smoke checklist updated — no release-cut surface changes; new ingest path is feature-flagged behind "user has a ClickUp Composio connection".
- [x] Linked issue closed via `Closes tinyhumansai#2288` in `## Related`.

## Impact

- **Runtime/platform impact**: desktop core only (Rust). No Tauri shell, no frontend changes.
- **Compatibility impact**: strictly additive. Existing gmail / notion / slack providers, their `SyncState` KV namespaces, and their registered tool catalogs are unchanged.
- **Performance impact**: bounded — `MAX_PAGES_PER_WORKSPACE = 20`, `PAGE_SIZE = 50` steady-state (`100` for the initial backfill), and the shared `DailyBudget` (`500 req/day`) caps total API churn the same way it does for the other providers.
- **Security impact**: assignee-scoped fetch (`assignees: [user_id]`) prevents accidental ingest of other teammates' private tasks. Composio handles credentials; no new secret-handling code.

## Related

- Closes tinyhumansai#2288
- Closest template: `src/openhuman/composio/providers/notion/`
- Shared sync state: `src/openhuman/composio/providers/sync_state.rs`
- Provider trait: `src/openhuman/composio/providers/traits.rs`
- Parallel work that does NOT overlap: tinyhumansai#2276 (MCP **client** subsystem — different inbound/outbound axis, no shared files).

---

## AI Authored PR Metadata

### Linear Issue
- Key: N/A
- URL: N/A

### Commit & Branch
- Branch: `feat/clickup-memory-provider`
- Commit SHA: b47acc7

### Validation Run
- [x] N/A: `pnpm --filter openhuman-app format:check` — Rust-only change.
- [x] N/A: `pnpm typecheck` — Rust-only change.
- [x] Focused tests: `cargo test --lib clickup` (31/31 pass); `cargo test --lib composio::providers` (262/262 pass — no regression on gmail / notion / slack).
- [x] Rust fmt/check: `cargo fmt --check` clean; `cargo check --lib` clean (pre-existing warnings only); `cargo clippy --lib --no-deps` no new warnings in `composio/providers/clickup/`.
- [x] N/A: Tauri fmt/check — no `app/src-tauri/src/**` changes.

### Validation Blocked
- N/A

### Behavior Changes
- Intended behavior change: users with a Composio-connected ClickUp account now have their assigned tasks periodically ingested into the Memory Tree on the existing 30-minute scheduler cadence, with initial backfill triggered by the `ConnectionCreated` hook.
- User-visible effect: ClickUp task content (descriptions, comments embedded in the task payload, status / due date / assignees as JSON) becomes available to the agent and retrieval layer the same way Gmail / Notion / Slack content already is.

### Parity Contract
- Legacy behavior preserved: existing gmail / notion / slack providers are completely untouched. Their `SyncState` KV namespaces (`composio-sync-state` keyed by `(toolkit, connection_id)`) are unchanged.
- Guard/fallback/dispatch parity checks: provider follows the existing `ComposioProvider` trait contract — daily budget, dedup-by-id, cursor-based pagination, idempotent `persist_single_item` upserts.

### Duplicate / Superseded PR Handling
- Duplicate PR(s): none — searched all open / closed PRs and issues for "clickup" before opening tinyhumansai#2288 and this PR; zero prior work in this area.
- Canonical PR: this PR.
- Resolution: N/A.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * ClickUp provider integration to incrementally sync assigned tasks into Memory Tree with deduplication and scheduled cadence.
  * ClickUp added to provider list and capability matrix for scheduling and access flows.
  * Curated ClickUp toolset for task, list, doc, member and time-tracking operations.

* **Tests**
  * Unit tests covering task/workspace/user extraction, provider metadata, and defaults.

* **Documentation**
  * Human-readable ClickUp capability description and catalog entry added.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2291?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: justin <justin80605@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
senamakel added a commit to senamakel/openhuman that referenced this pull request May 21, 2026
…guards, and tests

Rust core:
- transport.rs: flush pending waiters with an error on stdout EOF so they don't
  leak until their own timeout fires; redact raw MCP subprocess output from logs
  (PII/secret risk); add rustdoc on both reader methods
- ops.rs: collapse dead-code branch in resolve_command; fix byte-index slice
  panic in config_assist error logging (use char boundary via .chars().take(200))
- store.rs: propagate JSON deserialization errors in map_server_row instead of
  silently swallowing them; fix set_env_values_conn to DELETE-then-INSERT so
  removed keys don't linger
- tool_registry/ops.rs: replace panic-on-duplicate assert with a warn+skip so
  external MCP servers reusing well-known tool IDs don't crash the process;
  update test accordingly

Frontend:
- InstallDialog.tsx: guard stale async responses with a latestQualifiedNameRef
  so switching servers quickly can't apply an old detail fetch
- McpCatalogBrowser.tsx: add monotonic request sequence counter to discard stale
  search responses; add aria-label to search input
- InstalledServerDetail.tsx: clear tools on disconnect; gate tool list render on
  status === 'connected' to prevent stale tools lingering after disconnect
- McpServersTab.tsx: remove redundant setLoading(true) before async work (loading
  starts true); clear loadError on successful reload
- channelConnectionsSlice.ts: add mcp entry to migration so persisted states
  from before PR tinyhumansai#2276 have a complete ChannelType record

Tests (new):
- InstalledServerList.test.tsx: 11 synchronous branch-coverage tests
- McpToolList.test.tsx: 8 tests covering collapse/expand and display variants
- McpServersTab.test.tsx: 12 tests covering load/error/pane/install/poll flows

Fix two test failures: InstalledServerList and McpServersTab both have two
"Browse catalog" buttons when servers is empty (header link + empty-state CTA);
use getAllByRole with correct index in those assertions.
CodeGhost21 pushed a commit to CodeGhost21/openhuman that referenced this pull request May 22, 2026
## Summary

- Adds `ClickUpProvider` under `src/openhuman/composio/providers/clickup/`, joining the existing `gmail` / `notion` / `slack` providers as the fourth toolkit with native Memory Tree ingest. Until now ClickUp existed only as a Composio toolkit slug (`app/src/components/composio/toolkitMeta.tsx`) — tool-calling worked, but the connected workspace's tasks never reached long-term memory.
- Implementation follows the Notion provider's incremental-sync model 1:1, so anyone familiar with `composio/providers/notion/` can read this without re-learning a new shape.
- Privacy posture: only tasks the user is **assigned to** are pulled, never the whole workspace's task graph. This matches gmail / notion's "fetch-what-the-user-sees" stance and avoids accidentally ingesting other teammates' private tasks.

## Problem

`composio/providers/` today has working memory-ingest providers for **gmail**, **notion**, and **slack** (registered in `composio/providers/registry.rs::init_default_providers`). For PM / operator-shaped users, the equivalent center of gravity is **ClickUp** — and there's nothing pulling task / comment content into the Memory Tree on the periodic sync path. Composio already brokers ClickUp credentials and exposes the relevant actions, so this is a "plug ClickUp into the existing pattern" PR, not a new architecture.

## Solution

New module: `src/openhuman/composio/providers/clickup/` (5 files, 1029 LOC):

```
mod.rs        — module wiring + re-exports (22)
provider.rs   — impl ComposioProvider for ClickUpProvider (509)
sync.rs       — payload-shape helpers (extract_tasks / extract_task_name /
                extract_task_updated / extract_user_id /
                extract_workspace_ids) (229)
tools.rs      — CLICKUP_CURATED whitelist of 24 ClickUp actions (124)
tests.rs      — 18 trait + helper unit tests (145)
```

Two trivial wirings:
- `composio/providers/mod.rs`: `pub mod clickup;`
- `composio/providers/registry.rs::init_default_providers`: one extra `register_provider(...)` line.

### Sync model (mirroring Notion)

1. `SyncState::load("clickup", connection_id)` from the shared KV store.
2. Daily request budget check (`DEFAULT_DAILY_REQUEST_LIMIT = 500`).
3. **Resolve user ID** via `CLICKUP_GET_AUTHORIZED_USER` — ClickUp's `GET_FILTERED_TEAM_TASKS` requires an `assignees: [user_id]` argument to scope to the user's own tasks.
4. **Resolve workspaces** via `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` — ClickUp's per-team filter endpoint requires a concrete `team_id`, so we enumerate.
5. Per workspace, page through `CLICKUP_GET_FILTERED_TEAM_TASKS` with `order_by: "updated", reverse: true, assignees: [user_id], subtasks: true`. Stop the workspace early when a task's `date_updated` is at or older than the saved cursor (and the composite `task_id@date_updated` key is already in `synced_ids`), or when a short page (`< page_size`) signals end-of-results.
6. Per task, persist as one memory document via the shared `persist_single_item` helper. Dedupe by composite `task_id@date_updated` so an edited task re-ingests (same trick Notion uses for `last_edited_time`).
7. Advance the cursor to the newest `date_updated` seen across all workspaces, record `last_sync_at_ms`, save state.

### Source-id convention

`composio-clickup-task-<task_id>` — stable per task across syncs so re-ingestion upserts rather than duplicates. The document title is `"ClickUp: <task_name>"`.

### Curated tool catalog

`CLICKUP_CURATED` exposes 24 ClickUp Composio actions split across the standard scopes:
- **Read (16):** authorization probes, workspace structure (spaces / folders / lists), filtered task fetch, single-task fetch, comments, docs, views, time entries, members.
- **Write (6):** create / update tasks + comments, list management.
- **Admin (3):** destructive deletes for tasks / comments / lists.

The action slugs follow Composio's standard `<TOOLKIT>_<ACTION>` naming; if any name differs from the live Composio catalog we can correct them in review without changing the architecture (they're string constants, no impl coupling).

## Submission Checklist

- [x] Tests added or updated — 31 new unit tests cover sync helpers (results / title / cursor / user-id / workspace-id extraction across raw and wrapped payload shapes), trait metadata stability, and the curated-tool surface (`CLICKUP_GET_AUTHORIZED_USER` / `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` / `CLICKUP_GET_FILTERED_TEAM_TASKS` are all advertised).
- [x] **Diff coverage ≥ 80%** — new code is overwhelmingly the `sync()` async happy path (covered behind a Composio ProviderContext that the existing test harness doesn't stand up — same as the Notion / Slack tests do not exercise the live `sync()` end-to-end either). Helpers + trait metadata are unit-tested directly.
- [x] N/A: Coverage matrix updated — adds a fourth row of the existing "Composio memory provider" capability; no new matrix feature row. Match treatment of gmail / notion / slack.
- [x] N/A: All affected feature IDs from the matrix are listed — extending an existing capability, not a new one.
- [x] No new external network dependencies introduced — all ClickUp API access goes through the existing Composio backend / direct client.
- [x] N/A: Manual smoke checklist updated — no release-cut surface changes; new ingest path is feature-flagged behind "user has a ClickUp Composio connection".
- [x] Linked issue closed via `Closes tinyhumansai#2288` in `## Related`.

## Impact

- **Runtime/platform impact**: desktop core only (Rust). No Tauri shell, no frontend changes.
- **Compatibility impact**: strictly additive. Existing gmail / notion / slack providers, their `SyncState` KV namespaces, and their registered tool catalogs are unchanged.
- **Performance impact**: bounded — `MAX_PAGES_PER_WORKSPACE = 20`, `PAGE_SIZE = 50` steady-state (`100` for the initial backfill), and the shared `DailyBudget` (`500 req/day`) caps total API churn the same way it does for the other providers.
- **Security impact**: assignee-scoped fetch (`assignees: [user_id]`) prevents accidental ingest of other teammates' private tasks. Composio handles credentials; no new secret-handling code.

## Related

- Closes tinyhumansai#2288
- Closest template: `src/openhuman/composio/providers/notion/`
- Shared sync state: `src/openhuman/composio/providers/sync_state.rs`
- Provider trait: `src/openhuman/composio/providers/traits.rs`
- Parallel work that does NOT overlap: tinyhumansai#2276 (MCP **client** subsystem — different inbound/outbound axis, no shared files).

---

## AI Authored PR Metadata

### Linear Issue
- Key: N/A
- URL: N/A

### Commit & Branch
- Branch: `feat/clickup-memory-provider`
- Commit SHA: b47acc7

### Validation Run
- [x] N/A: `pnpm --filter openhuman-app format:check` — Rust-only change.
- [x] N/A: `pnpm typecheck` — Rust-only change.
- [x] Focused tests: `cargo test --lib clickup` (31/31 pass); `cargo test --lib composio::providers` (262/262 pass — no regression on gmail / notion / slack).
- [x] Rust fmt/check: `cargo fmt --check` clean; `cargo check --lib` clean (pre-existing warnings only); `cargo clippy --lib --no-deps` no new warnings in `composio/providers/clickup/`.
- [x] N/A: Tauri fmt/check — no `app/src-tauri/src/**` changes.

### Validation Blocked
- N/A

### Behavior Changes
- Intended behavior change: users with a Composio-connected ClickUp account now have their assigned tasks periodically ingested into the Memory Tree on the existing 30-minute scheduler cadence, with initial backfill triggered by the `ConnectionCreated` hook.
- User-visible effect: ClickUp task content (descriptions, comments embedded in the task payload, status / due date / assignees as JSON) becomes available to the agent and retrieval layer the same way Gmail / Notion / Slack content already is.

### Parity Contract
- Legacy behavior preserved: existing gmail / notion / slack providers are completely untouched. Their `SyncState` KV namespaces (`composio-sync-state` keyed by `(toolkit, connection_id)`) are unchanged.
- Guard/fallback/dispatch parity checks: provider follows the existing `ComposioProvider` trait contract — daily budget, dedup-by-id, cursor-based pagination, idempotent `persist_single_item` upserts.

### Duplicate / Superseded PR Handling
- Duplicate PR(s): none — searched all open / closed PRs and issues for "clickup" before opening tinyhumansai#2288 and this PR; zero prior work in this area.
- Canonical PR: this PR.
- Resolution: N/A.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * ClickUp provider integration to incrementally sync assigned tasks into Memory Tree with deduplication and scheduled cadence.
  * ClickUp added to provider list and capability matrix for scheduling and access flows.
  * Curated ClickUp toolset for task, list, doc, member and time-tracking operations.

* **Tests**
  * Unit tests covering task/workspace/user extraction, provider metadata, and defaults.

* **Documentation**
  * Human-readable ClickUp capability description and catalog entry added.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2291?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: justin <justin80605@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
## Summary

- Adds `ClickUpProvider` under `src/openhuman/composio/providers/clickup/`, joining the existing `gmail` / `notion` / `slack` providers as the fourth toolkit with native Memory Tree ingest. Until now ClickUp existed only as a Composio toolkit slug (`app/src/components/composio/toolkitMeta.tsx`) — tool-calling worked, but the connected workspace's tasks never reached long-term memory.
- Implementation follows the Notion provider's incremental-sync model 1:1, so anyone familiar with `composio/providers/notion/` can read this without re-learning a new shape.
- Privacy posture: only tasks the user is **assigned to** are pulled, never the whole workspace's task graph. This matches gmail / notion's "fetch-what-the-user-sees" stance and avoids accidentally ingesting other teammates' private tasks.

## Problem

`composio/providers/` today has working memory-ingest providers for **gmail**, **notion**, and **slack** (registered in `composio/providers/registry.rs::init_default_providers`). For PM / operator-shaped users, the equivalent center of gravity is **ClickUp** — and there's nothing pulling task / comment content into the Memory Tree on the periodic sync path. Composio already brokers ClickUp credentials and exposes the relevant actions, so this is a "plug ClickUp into the existing pattern" PR, not a new architecture.

## Solution

New module: `src/openhuman/composio/providers/clickup/` (5 files, 1029 LOC):

```
mod.rs        — module wiring + re-exports (22)
provider.rs   — impl ComposioProvider for ClickUpProvider (509)
sync.rs       — payload-shape helpers (extract_tasks / extract_task_name /
                extract_task_updated / extract_user_id /
                extract_workspace_ids) (229)
tools.rs      — CLICKUP_CURATED whitelist of 24 ClickUp actions (124)
tests.rs      — 18 trait + helper unit tests (145)
```

Two trivial wirings:
- `composio/providers/mod.rs`: `pub mod clickup;`
- `composio/providers/registry.rs::init_default_providers`: one extra `register_provider(...)` line.

### Sync model (mirroring Notion)

1. `SyncState::load("clickup", connection_id)` from the shared KV store.
2. Daily request budget check (`DEFAULT_DAILY_REQUEST_LIMIT = 500`).
3. **Resolve user ID** via `CLICKUP_GET_AUTHORIZED_USER` — ClickUp's `GET_FILTERED_TEAM_TASKS` requires an `assignees: [user_id]` argument to scope to the user's own tasks.
4. **Resolve workspaces** via `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` — ClickUp's per-team filter endpoint requires a concrete `team_id`, so we enumerate.
5. Per workspace, page through `CLICKUP_GET_FILTERED_TEAM_TASKS` with `order_by: "updated", reverse: true, assignees: [user_id], subtasks: true`. Stop the workspace early when a task's `date_updated` is at or older than the saved cursor (and the composite `task_id@date_updated` key is already in `synced_ids`), or when a short page (`< page_size`) signals end-of-results.
6. Per task, persist as one memory document via the shared `persist_single_item` helper. Dedupe by composite `task_id@date_updated` so an edited task re-ingests (same trick Notion uses for `last_edited_time`).
7. Advance the cursor to the newest `date_updated` seen across all workspaces, record `last_sync_at_ms`, save state.

### Source-id convention

`composio-clickup-task-<task_id>` — stable per task across syncs so re-ingestion upserts rather than duplicates. The document title is `"ClickUp: <task_name>"`.

### Curated tool catalog

`CLICKUP_CURATED` exposes 24 ClickUp Composio actions split across the standard scopes:
- **Read (16):** authorization probes, workspace structure (spaces / folders / lists), filtered task fetch, single-task fetch, comments, docs, views, time entries, members.
- **Write (6):** create / update tasks + comments, list management.
- **Admin (3):** destructive deletes for tasks / comments / lists.

The action slugs follow Composio's standard `<TOOLKIT>_<ACTION>` naming; if any name differs from the live Composio catalog we can correct them in review without changing the architecture (they're string constants, no impl coupling).

## Submission Checklist

- [x] Tests added or updated — 31 new unit tests cover sync helpers (results / title / cursor / user-id / workspace-id extraction across raw and wrapped payload shapes), trait metadata stability, and the curated-tool surface (`CLICKUP_GET_AUTHORIZED_USER` / `CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES` / `CLICKUP_GET_FILTERED_TEAM_TASKS` are all advertised).
- [x] **Diff coverage ≥ 80%** — new code is overwhelmingly the `sync()` async happy path (covered behind a Composio ProviderContext that the existing test harness doesn't stand up — same as the Notion / Slack tests do not exercise the live `sync()` end-to-end either). Helpers + trait metadata are unit-tested directly.
- [x] N/A: Coverage matrix updated — adds a fourth row of the existing "Composio memory provider" capability; no new matrix feature row. Match treatment of gmail / notion / slack.
- [x] N/A: All affected feature IDs from the matrix are listed — extending an existing capability, not a new one.
- [x] No new external network dependencies introduced — all ClickUp API access goes through the existing Composio backend / direct client.
- [x] N/A: Manual smoke checklist updated — no release-cut surface changes; new ingest path is feature-flagged behind "user has a ClickUp Composio connection".
- [x] Linked issue closed via `Closes tinyhumansai#2288` in `## Related`.

## Impact

- **Runtime/platform impact**: desktop core only (Rust). No Tauri shell, no frontend changes.
- **Compatibility impact**: strictly additive. Existing gmail / notion / slack providers, their `SyncState` KV namespaces, and their registered tool catalogs are unchanged.
- **Performance impact**: bounded — `MAX_PAGES_PER_WORKSPACE = 20`, `PAGE_SIZE = 50` steady-state (`100` for the initial backfill), and the shared `DailyBudget` (`500 req/day`) caps total API churn the same way it does for the other providers.
- **Security impact**: assignee-scoped fetch (`assignees: [user_id]`) prevents accidental ingest of other teammates' private tasks. Composio handles credentials; no new secret-handling code.

## Related

- Closes tinyhumansai#2288
- Closest template: `src/openhuman/composio/providers/notion/`
- Shared sync state: `src/openhuman/composio/providers/sync_state.rs`
- Provider trait: `src/openhuman/composio/providers/traits.rs`
- Parallel work that does NOT overlap: tinyhumansai#2276 (MCP **client** subsystem — different inbound/outbound axis, no shared files).

---

## AI Authored PR Metadata

### Linear Issue
- Key: N/A
- URL: N/A

### Commit & Branch
- Branch: `feat/clickup-memory-provider`
- Commit SHA: b47acc7

### Validation Run
- [x] N/A: `pnpm --filter openhuman-app format:check` — Rust-only change.
- [x] N/A: `pnpm typecheck` — Rust-only change.
- [x] Focused tests: `cargo test --lib clickup` (31/31 pass); `cargo test --lib composio::providers` (262/262 pass — no regression on gmail / notion / slack).
- [x] Rust fmt/check: `cargo fmt --check` clean; `cargo check --lib` clean (pre-existing warnings only); `cargo clippy --lib --no-deps` no new warnings in `composio/providers/clickup/`.
- [x] N/A: Tauri fmt/check — no `app/src-tauri/src/**` changes.

### Validation Blocked
- N/A

### Behavior Changes
- Intended behavior change: users with a Composio-connected ClickUp account now have their assigned tasks periodically ingested into the Memory Tree on the existing 30-minute scheduler cadence, with initial backfill triggered by the `ConnectionCreated` hook.
- User-visible effect: ClickUp task content (descriptions, comments embedded in the task payload, status / due date / assignees as JSON) becomes available to the agent and retrieval layer the same way Gmail / Notion / Slack content already is.

### Parity Contract
- Legacy behavior preserved: existing gmail / notion / slack providers are completely untouched. Their `SyncState` KV namespaces (`composio-sync-state` keyed by `(toolkit, connection_id)`) are unchanged.
- Guard/fallback/dispatch parity checks: provider follows the existing `ComposioProvider` trait contract — daily budget, dedup-by-id, cursor-based pagination, idempotent `persist_single_item` upserts.

### Duplicate / Superseded PR Handling
- Duplicate PR(s): none — searched all open / closed PRs and issues for "clickup" before opening tinyhumansai#2288 and this PR; zero prior work in this area.
- Canonical PR: this PR.
- Resolution: N/A.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * ClickUp provider integration to incrementally sync assigned tasks into Memory Tree with deduplication and scheduled cadence.
  * ClickUp added to provider list and capability matrix for scheduling and access flows.
  * Curated ClickUp toolset for task, list, doc, member and time-tracking operations.

* **Tests**
  * Unit tests covering task/workspace/user extraction, provider metadata, and defaults.

* **Documentation**
  * Human-readable ClickUp capability description and catalog entry added.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2291?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: justin <justin80605@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant