ctx augmentation:
- ctx.progress(p, total?, message?) emits notifications/progress on
the session's notify_q. No-op when the original request omitted
_meta.progressToken (per spec: only emit when client opted in).
Type-checks numeric args; passes progressToken through unchanged
(spec allows number OR string keys).
- ctx.cancelled() returns true once the client has sent a
notifications/cancelled for this request's id.
handle_request:
- New side-effect in the id==nil branch: notifications/cancelled
scans the module-level _ctx_by_co for an in-flight ctx whose
request_id matches; flips self._cancelled_ids[rid_str] only when
found. Unknown rids drop silently (no map growth).
- Pre-handler short-circuit: if cancel arrived before dispatch
reached tools/call, skip the handler entirely.
Cross-module ctx lookup:
- Module-level weak _ctx_by_co table in lmcp.lua keyed by
coroutine. lmcp.current_ctx() returns the ctx of the running
coroutine. server.lua's run() lazy-requires lmcp and uses it
to opt into auto-cancellation without depending on lmcp internals.
server.lua:run():
- After each sleep_ms cycle, check ctx.cancelled(); exit poll loop
with cancelled=true if set.
- Poll interval capped at 500ms when a ctx is present so worst-case
cancel latency stays ≤500ms (vs. 2s default growth).
- Returns "(cancelled)" sentinel; handler propagates normally.
_finalise_dispatch:
- Single cleanup site for both _cancelled_ids and _ctx_by_co (per
Phase 5 review).
- When was_cancelled: emit JSON-RPC -32800 "Request cancelled"
(deviation from Phase 4 plan; documented).
Phase 4 deviation explained: plan was silent TCP close (per spec
"SHOULD NOT respond"). Empirically: os.execute's fork+exec
inherits the parent's TCP socket FD into the spawned shell, so
sock:close() doesn't actually deliver FIN until the subshell exits
(i.e. the long-running command completes anyway). Verified
luasocket close() works on bare sockets (curl exits with RST in
511ms). The fix would be FD_CLOEXEC on accepted sockets, which
luasocket doesn't expose — needs a C shim or luaposix. Deferred.
Captured in memory project_fd_inheritance_in_run.
Practical UX with the deviation: client receives a structured
-32800 error within ~420ms of POSTing the cancel notification.
Measurements (Phase 7):
cancel timing (3 runs, sleep 10 with cancel at 0.4s):
run 1: t=0.42s code=-32800
run 2: t=0.42s code=-32800
run 3: t=0.42s code=-32800
progress: 3/3 events arrived on SSE; spec-shaped payload
concurrent fast+slow (#20 regression): unchanged (fast 0.01s)
all previously-closed issues regression-test green
Zero handler source-code changes. Existing tools (shell, fetch,
web_search, hub remote_*) get cancellation for free via run().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the synchronous tools/call path with a coroutine-wrapped
dispatch. The select()-based event loop from v1.0.0-rc1 already
multiplexes I/O; this change extends the same single-thread
cooperative scheduling to tool handler execution.
How:
- server.lua:sleep_ms detects coroutine context and yields with
{ wake_at = gettime() + ms/1000 } instead of blocking. Falls back
to today's busy-blocking sleep when on the main thread (stdio
dispatch, init code).
- server.lua:run() now uses gettime() deltas for timeout accounting
(Phase 5 review fix — the prior interval-accumulator diverged
from wall-clock when scheduler delayed resumes).
- lmcp.lua wraps the handle_request call inside _dispatch_post in a
coroutine. Synchronous completion (no yield) takes the inline-
response path; if the handler yields, the coroutine parks in
self._pending_handlers and the conn enters dispatching_async.
- New _scheduler_tick services pending coroutines whose wake_at has
passed; on completion calls the shared _finalise_dispatch helper
to build the deferred HTTP response (Accept-aware: SSE or JSON).
- select() timeout tightens to the next pending wake_at so short
yields don't pay the full 100ms tick.
Measurement (Phase 7):
before: fast ping during slow shell sleep 3 = 4.28s
after: fast ping during slow shell sleep 3 = 0.01s (~400×)
3 parallel slow shells: 3.77s total wall (was ~9s).
Zero handler source-code changes. Every existing tool that goes
through run() (shell, shell_bg, fetch, web_search, list_dir,
search_files, systeminfo, hub remote_*) gets concurrency for free.
Pure-Lua handlers (ping, read_file, write_file, edit_file) continue
to complete inline. stdio transport stays serialised by design
(single-client per stdio process).
Known limits documented in memory project_handler_coroutines:
- socket.gettime() is wall-clock not monotonic; large NTP steps may
bunch resumes. Acceptable on chrony-slewed fleet.
- Cancellation (#11) is now tractable since the scheduler can flip a
flag between resumes — implementation pending.
- Server-initiated request await (sampling/roots from inside a
handler) still requires a future yield-on-pending helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lmcp.lua: if opts.auth_token and opts.conf are both unset, fall back to
the LMCP_TOKEN environment variable. Empty string treated as unset.
This is the primitive launchd/systemd drop-ins need — no conf file
bookkeeping on hosts that don't already use one.
scripts/lmcp-install-macos.sh: macOS installer via Homebrew. Drops the
Lua library files into $(brew --prefix)/share/lua/5.4/, mints (or
reuses) a Bearer token stored at $(brew --prefix)/etc/lmcp/token,
installs a ~/Library/LaunchAgents/ plist with LMCP_TOKEN baked in,
launchctl-loads it, and smoke-tests. Prints the Claude Code ~/.claude.json
snippet at the end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add MAX_BODY_SIZE (64KB) check before reading body — prevents pre-auth
OOM on internet-facing deployments
- Add JSON nesting depth limit (64 levels) — prevents C stack overflow
that bypasses pcall and crashes the process
- Timing-safe token comparison via XOR accumulate — prevents timing
oracle on Bearer token
- Auth token from LMCP_TOKEN env var (highest priority) — avoids storing
token in a file readable by the read_file tool
- Silent handling of unknown JSON-RPC notifications (spec compliance)
- Exact path matching on /mcp endpoint (was prefix-based)
- Remove dead json.array() function
Findings from architecture review + security audit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>