Skip to content

feat: add Cursor SDK backend with MCP bridge and tool hooks#8

Merged
emanuilo merged 35 commits into
emanuilo:mainfrom
hadzija7:main
May 25, 2026
Merged

feat: add Cursor SDK backend with MCP bridge and tool hooks#8
emanuilo merged 35 commits into
emanuilo:mainfrom
hadzija7:main

Conversation

@hadzija7

Copy link
Copy Markdown
Contributor

Summary

  • Adds a pluggable Cursor SDK backend (AGENT_BACKEND=cursor) alongside Claude, selectable in the setup wizard.
  • Wires Memclaw tools (memory_save, reminders, etc.) into Cursor via a local HTTP MCP bridge, with process-scoped MCP lifecycle management.
  • Installs preToolUse hooks to block built-in Cursor tools while allowing Memclaw MCP tools, with audit logging and port-restricted hook MCP URLs.
  • Integrates cleanly with current upstream main (platform picker, memclaw doctor, curl-cffi link fetching, masked user IDs in logs).

Test plan

  • uv run pytest — 173 tests pass
  • Run memclaw configure, select Cursor backend, and verify keys are saved
  • Start memclaw in terminal mode with AGENT_BACKEND=cursor and confirm memory save/search works
  • Verify MCP bridge starts on the configured port and shuts down cleanly on exit
  • Confirm Cursor hooks block built-in tools but allow Memclaw MCP tool calls
  • Smoke-test Claude backend still works (AGENT_BACKEND=claude)

Made with Cursor

cursoragent and others added 30 commits May 20, 2026 10:04
Port CursorBackend into the upstream pluggable backend architecture
alongside ClaudeAgentBackend. Cursor registers in REGISTRY, uses the
v1.2.0 AgentBackend protocol (__init__, drop_keys wizard, num_turns),
and adds cursor_api_key/cursor_model config fields.

Co-authored-by: Aleksandar Hadzibabic <hadzija7@users.noreply.github.com>
Expose TOOL_DEFINITIONS through an ephemeral in-process HTTP MCP server
(EphemeralHttpMcpBridge) and pass mcp_servers to Cursor AgentOptions/
SendOptions. Images are sent via SDKImage/UserMessage. Adds shared
mcp_tools builder and tests.

Co-authored-by: Aleksandar Hadzibabic <hadzija7@users.noreply.github.com>
Wrap bridge listen wait in try/except so a timeout triggers the same
uvicorn shutdown as __aexit__, avoiding a leaked server task.

Cap Cursor agentic tool loops by cancelling the run after max_turns
tool invocations and clamp reported num_turns accordingly.
Use AsyncAgent.close() instead of aclose(), concatenate streamed text
blocks, and prefer the longer run.wait() result to avoid truncated replies.

Co-authored-by: Cursor <cursoragent@cursor.com>
…cpServerConfig

Avoid awaiting run.wait() after cancel() when the tool-step cap fires, since
wait() may not complete promptly on a cancelled run and would block agent cleanup.

Import HttpMcpServerConfig inside __aenter__ and under TYPE_CHECKING only so
loading memclaw.backends does not pull in cursor_sdk until the bridge runs.
…close

- Use tool_steps >= max_turns so the Cursor backend matches Claude max_turns.
- Add MemclawAgent.aclose() to await optional backend.close() (Cursor bridge).
- MemclawAgent.close() runs aclose via asyncio.run when no loop is active.
- Telegram/Slack shutdown and interactive CLI await aclose; async tests updated.
End users installing memclaw no longer pull the large native wheel unless
they use pip install memclaw[cursor]. Keep cursor-sdk in the dev extra so
CI and contributors running tests still resolve the dependency.
Keep the Cursor backend integration protocol-only by restoring the
original close() lifecycle instead of threading aclose() through core code.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rializable

_log_tool_call used json.dumps on SDK message args without handling
TypeError/ValueError; fall back to str() so logging cannot abort run_turn.
…atomically

Stop using str(result) in _extract_run_text so RunResult repr cannot replace
valid assistant text when wait() text is chosen by length.

Bind and listen on 127.0.0.1:0, pass the socket fd to uvicorn, and close the
listener after shutdown to remove the ephemeral-port TOCTOU window.
Ship and verify ~/.memclaw/.cursor hooks that allow only Memclaw MCP
tools, mirroring Claude disallowed_tools guardrail for Cursor backend.

Co-authored-by: Cursor <cursoragent@cursor.com>
…--memory-dir

Cursor requires a top-level numeric version in project hooks.json; the
packaged template now includes version 1 so Cursor loads preToolUse hooks.

Plumb the active memory directory from run_setup into wizard_setup so the
Cursor wizard installs hooks under the same tree as AGENT_BACKEND=cursor,
not only the default ~/.memclaw path.
Start the local MCP server with agent.start() on a configurable port and reuse it across Cursor turns, stopping cleanly on shutdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
MemclawAgent.close() now tears down the local HTTP MCP server when the Cursor
backend still has it running, using asyncio.run when no event loop is active
and scheduling stop on the running loop otherwise. HttpMcpServer.start() calls
stop() before re-raising bind errors so partial executor/session state is cleared.
Recognize MCP:tool shorthand and CallMcpTool bridge payloads, broaden local MCP URL matching, and add deny audit logging plus incomplete-tool warnings for easier diagnosis.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Cursor backend counted each tool_call running event toward max_turns
and cancelled the run, unlike Claude which uses max_turns for agent
iterations. Remove that cancellation and derive num_turns from the SDK
wait result only.
preToolUse previously treated any MCP bridge payload with
providerIdentifier/server memclaw as allowed without checking
toolName against MEMCLAW_MCP_TOOL_NAMES, and is_allowed_tool
short-circuited the same way. Align bridge checks with the
allowlist and update the mcp_auth parametrized expectation.
Cursor MCP shutdown belongs in aclose(); close() stays sync-only for index and background sync cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add AgentBackend start/shutdown hooks so MemclawAgent stays SDK-agnostic,
move MCP ownership into the Cursor backend, and generate hook allowlists
from a single tool_policy module.

Co-authored-by: Cursor <cursoragent@cursor.com>
Derive the memory directory from the hook install path and set MEMCLAW_MEMORY_DIR when launching the Cursor backend so custom --memory-dir paths are respected.

Co-authored-by: Cursor <cursoragent@cursor.com>
MemclawAgent.close() now awaits backend on_agent_shutdown via asyncio.run
when no event loop is running, and aclose() calls shared _close_sync_only()
after async shutdown. Consolidation CLI uses await aclose() so shutdown
runs inside the async context.

Cursor beforeMCPExecution hook matches URL port to policy/env MCP port;
hook policy and bundled defaults bump to v9 with mcp_http_port.
Consolidation called _maybe_consolidate without starting the agent, so
CursorAgentBackend.on_agent_start never ran and ensure_cursor_hooks was
skipped. Match interactive mode by syncing the index and invoking
on_agent_start before the one-shot consolidation run.
feat: add Cursor SDK agent backend layer
Integrate upstream platform picker, doctor command, and curl-cffi link
fetching while preserving Cursor backend config, MCP bridge, and hooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use handlers.aclose() instead of close() so MemclawAgent runs
backend.on_agent_shutdown() while the asyncio loop is still active
(e.g. Cursor MCP HTTP server cleanup).
Sync upstream/main and resolve merge conflicts
Revert ruff-driven line wrapping in agent.py, cli.py, and setup.py
while keeping the Cursor backend functional changes from PR emanuilo#8.

Co-authored-by: Cursor <cursoragent@cursor.com>
Consolidation only needs index sync and run_one_shot, so starting the MCP server is unnecessary and fails when another memclaw instance is already running.

Co-authored-by: Cursor <cursoragent@cursor.com>
hadzija7 and others added 5 commits May 25, 2026 17:04
Make the model field optional with a visible default hint, escape Rich markup in the prompt, and drop the noisy hooks-ready info log during configure.

Co-authored-by: Cursor <cursoragent@cursor.com>
Enable SendOptions.on_delta so turn-ended usage reaches Memclaw, normalize token fields into TurnResult, and add Composer pricing fallback when cost is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Cursor SDK emits cache creation tokens as cacheWriteTokens in the
turn-ended usage payload. The lookup list previously only checked
Anthropic-style names (cache_creation_input_tokens, ...), so the
cache_create telemetry was silently always zero. Add the camelCase
Cursor key (and a snake_case variant as a defensive fallback) at the
front of the lookup.
Add an Agent Backend section right after Messaging Platforms with a
subsection per backend, move the existing Claude/Cursor auth details
there, and drop the duplicated Configuration subsections. Neutralize a
few Claude-specific mentions elsewhere (How It Works, Agent Layer,
Direct Commands, Architecture). Add a Cursor SDK badge, a billing note
for Cursor, and a dedicated Link row in the message-type table.
@emanuilo emanuilo self-requested a review May 25, 2026 18:04
@emanuilo

Copy link
Copy Markdown
Owner

Looks good! Thanks!

@emanuilo emanuilo merged commit 6d15b22 into emanuilo:main May 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants