Commit Graph

31 Commits

Author SHA1 Message Date
Markus Fritsche 08e0075d14 tests: Abnahmetest fuer Phase A der Fassungs-Meldung
Phase A des Umstiegs auf MCP 2026-07-28 ist "observe-only": erst
feststellen, welche Protokollfassungen die Gegenstellen ueberhaupt
verlangen, bevor irgendetwas durchgesetzt wird. Der Vertrag dafuer
steht im Kopf der Datei; dies ist der getrennte Test dazu.

GETRENNT ist hier die Bedingung, nicht die Beschreibung. An derselben
Aufgabe war der Defekt an vier aufeinanderfolgenden Laeufen der
SELBSTTEST, nie der Code - wer implementiert, schreibt den Test nicht.
Deshalb ist er hier im Repo und nicht in der Werkstatt der Erzeuger.

Nachgewiesen unterscheidungsfaehig, in beide Richtungen:
  * Rueckgabe 1 und zwei Fehlschlaege gegen eine absichtlich kaputte
    Fassung,
  * Rueckgabe 0 gegen eine korrekte.
Ein Test, der nur besteht, misst nichts.

Zehn Pruefungen, darunter die zwei, die in Handfassungen zuerst
fallen: der Deckel bei 50 darf nicht zu frueh zuschlagen, und das
Modul darf keine neue globale Variable hinterlassen.

Aufruf: lua5.4 tests/phase_a_acceptance.lua <impl.lua>
2026-08-08 08:02:21 +02:00
Markus Fritsche 250f3d38f0 hub: probe ssh-only backends with a TCP connect instead of leaving them unknown
The design note says the probe is lmcp-only because checking ssh "is expensive (3-6s per
offline host) and the hub exists specifically to absorb lots of offline hosts". That holds
for an ssh SESSION. A bare TCP connect to port 22 answers the only question a host card
asks — is the box there — with no handshake and no auth.

Measured on the room host: a dead target costs 1.05s, a live one milliseconds, and riding
the existing parallel fan-out keeps wall clock at one budget window — 3.07s for 14 lmcp
plus 9 ssh probes together, against 3.10s for the 14 lmcp probes alone. The cost objection
is answered rather than ignored.

Eight reachable hosts had been reported as "no probe result": dcw2, deus, escher, hermes,
nash, noether, orca, pipi. They now report UP via=ssh, and a host whose port 22 refuses
gets a real reason ("ssh port unreachable") instead of silence.

Deliberately unchanged: a backend with BOTH paths whose lmcp is down still reads DOWN, per
the existing note that remote_* falls through to ssh regardless. Only ssh-ONLY backends
get the new probe. Without nc the probe reports nothing rather than guessing DOWN — an
unprobed host is honest, a wrongly-asserted one is not.

Two detours worth recording, since both were self-inflicted and cost a restart each:

  * The first attempt edited /opt/lmcp/hub.lua and restarted the service, which loads
    /usr/share/lua/5.4/hub.lua — a separate copy. Nothing keeps the two in step; the log
    line still showed the old format string, which is the only reason it was noticed.
  * The second attempt compared this repository against the deployed file and concluded
    that 120 lines of MCP tool annotations had never been committed. They had. The working
    copy was 17 commits stale, so the "drift" was entirely an artefact of the comparison.
    Against the current tree the real change is 36 lines, and the patched file now hashes
    identical to the running one.
2026-08-02 21:35:15 +02:00
noether (claude) 2bb7b94a66 tools.d: add boltzmann stash tool (fleet-memory CLI wrapper)
Wraps incus exec memory -- docker exec stash-stash-1 /stash <command> behind one
MCP tool so pi-agents call stash command:="recall ... -n /infra/hosts" instead of
hand-typing the incus/docker chain. Loaded via LMCP_TOOLS_DIR=/opt/lmcp/tools.d.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-18 13:23:29 +02:00
noether (claude) 80fb60c60f hub: serve apropos + wake_fleet (fleet-central tools) alongside remote_*
Consolidate the two fleet-central local tools (stash recall + pve WoL) onto
the hub broker so :8090 is a complete fleet-management endpoint. They run
locally on hertz via a new run_local() helper (no ssh backend hop). Still
also served by hertz-tools (:8080) for now; the hertz-side removal is the
follow-up once every client (pi-agents) has a @hub session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-18 08:51:16 +02:00
noether (claude) 6fa98dd655 shell: kill process group on timeout/cancel (no orphans) + shell_bg job registry + kill_job/list_jobs
Root cause of the shell->shell_bg thrash: run() backgrounded POSIX commands
with a bare & and never captured the pid, so on timeout it returned an error
while the children kept running. Now: setsid (own process group), capture the
leader pid, SIGTERM+SIGKILL the whole group on timeout/cancel, and a message
telling the model it was killed + to use shell_bg. Plus shell_bg registers jobs
to /tmp/lmcp-bg-jobs.tsv and new kill_job/list_jobs let a runaway job be reaped
without a reboot.
v1.2.4
2026-07-12 14:28:52 +02:00
noether (claude) a7b3c44f1c reconcile: sync repo to live deployment (wake_fleet + apropos tools, stash_recall helper) 2026-07-12 14:25:30 +02:00
marfrit 840341d2dd fix: replace LXC/LXD with Incus
Replace all /snap/bin/lxc references with incus in the
hertz-specific tool definitions. Tool names changed from
lxc_exec/lxc_list to incus_exec/incus_list.

Context: LXC snap was removed from hertz; the system
now uses incus (Debian package) for container management.
2026-06-12 23:18:58 +02:00
marfrit 8748fe53bc Merge pull request 'Add nash memory tools as lmcp plugin' (#26) from williams/lmcp:master into master
Reviewed-on: #26
v1.2.3
2026-06-05 15:54:26 +00:00
williams 8d8d8fac65 Add nash memory tools (nash_add/search/list/delete) 2026-06-05 15:51:51 +00:00
marfrit 3dd01e5313 Merge pull request 'fix: case-insensitive Bearer token parsing in auth header' (#25) from williams/lmcp:fix/case-insensitive-bearer-auth into master
Reviewed-on: #25
v1.2.2
2026-05-30 14:43:37 +00:00
williams d2c2962ad1 fix: case-insensitive Bearer token parsing in auth header 2026-05-30 12:55:02 +00:00
Markus Fritsche c5375b8a77 v1.2.1/#22: LMCP_HOST + LMCP_CONF env support
Adds two env vars to the packaged server.lua so hosts can switch
fully to the packaged entrypoint (combined with v1.2.0's tools.d/
plugin scan):

  LMCP_HOST — interface to bind on (default 0.0.0.0). Hosts that
              need .18-only binding (hertz) or similar single-NIC
              constraints set this. Threaded into lmcp.new opts.host.
  LMCP_CONF — path to a conf file with bearer-token entries (e.g.
              /opt/herding/etc/hertz-tools.conf). Read by lmcp.lua's
              read_conf; the `.godparticle` entry becomes the bearer
              token. Threaded into lmcp.new opts.conf.

Both unset → unchanged behavior (binds 0.0.0.0, no conf file).

Together with v1.2.0's tools.d/ scan, this lets a host like hertz
ship NO override server.lua — just an /opt/lmcp/tools.d/hertz.lua
plugin file and a systemd unit that points at the packaged
server.lua with LMCP_HOST=192.168.88.18 + LMCP_CONF=/opt/herding/
etc/hertz-tools.conf. apt upgrade then delivers all packaged
improvements automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.2.1
2026-05-17 23:33:30 +00:00
Markus Fritsche e05438f0e3 v1.2.0/#22: tools.d/ plugin scan — host-local tool extensions
Adds a directory-scan plugin mechanism to the packaged server.lua
so hosts can drop their own tools alongside the packaged generics
without forking server.lua.

Mechanism:
- After all packaged tool registrations + before transport selection,
  the server scans LMCP_TOOLS_DIR (default /opt/lmcp/tools.d on POSIX,
  %ProgramData%\lmcp\tools.d on Windows) for *.lua files.
- Each plugin file is invoked as a function receiving (server, run):
    local server, run = ...
    server:tool("my_local_tool", "...", {...}, function(a) return ... end)
- Load errors and runtime errors are reported on stderr and skipped;
  the server continues with the tools it successfully loaded.

Why:
Hosts like hertz and ampere have always carried local /opt/lmcp/server.lua
overrides containing both packaged-overlap tools (shell, read_file, …)
AND host-specific tools (fritz, ha_api, mqtt_*, lxc_exec, …). When the
override drifts, the host either loses packaged improvements (the v1.1.1
fetch/web_search regression on hertz/ampere) or accumulates hand-merged
patches that vanish on shutdown (the original symptom in issue #22).
With tools.d/, hosts drop ONLY their custom tools as plugin files; the
packaged server.lua stays canonical. apt upgrade picks up new packaged
tools automatically.

Smoke-tested:
  $ mkdir -p /tmp/probe && cat > /tmp/probe/p.lua <<E
  local server, run = ...
  server:tool("plugin_probe", "test", {type="object"},
              function() return "ok" end)
  E
  $ LMCP_TOOLS_DIR=/tmp/probe lua server.lua
  lmcp: loaded plugin /tmp/probe/p.lua
  $ curl POST tools/list → plugin_probe present in the 10 tools listed

Existing single-file server deployments (no /opt/lmcp/tools.d/) keep
working unchanged — io.popen on a non-existent directory returns nil
and the plugin loop no-ops. Backwards compatible.

Closes the structural side of #22 (the ad-hoc-override pattern); ampere
+ hertz migration to use tools.d/ for their custom tools is the operator
follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.2.0
2026-05-17 23:32:12 +00:00
Markus Fritsche 9707f7ae93 v1.1.1: omit empty inputSchema.properties at registration
Same json.lua empty-table → [] gotcha that bit `ping` in v1.0.0-rc1
(project_json_empty_table_gotcha memory) bit again — this time on
tool inputSchemas with `properties = {}`. Symptom: spec-strict MCP
clients (Zod et al.) reject tools/list with:

  expected: record, code: invalid_type,
  path: [tools, N, inputSchema, properties],
  message: "Invalid input: expected record, received array"

Fix: in `lmcp:tool()`, normalise the registered inputSchema —
when `properties` is an empty Lua table, drop the key entirely.
JSON Schema permits omitting `properties` on `type: "object"`
(means "any object, no constraints" — exactly what a no-arg tool
wants).

Clone-before-mutate so the caller's table isn't trampled (matters
when a server author shares one schema across multiple
registrations).

Smoke tested locally with 3 tools (empty, default-nil, populated):
- `properties = {}` → emitted as `{"type":"object"}`
- nil schema → same default, same output
- populated properties → emitted intact with full shape

Discovered against hertz-tools live (lxc_list, network_status had
`properties = {}` — hertz hotfixed by hand before this commit;
this protects every future tool author from the same trap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.1
2026-05-17 22:39:56 +00:00
Markus Fritsche 9e53b23b11 windows/build-msi.sh: cross-build the MSI on Linux via wixl + mingw-w64
Discovered building v1.1.0 that the MSI can be produced entirely on
Linux — no Windows VM, no manual WiX install, no GUI babysitting:

  apt install wixl unzip gcc-mingw-w64-x86-64 binutils-mingw-w64-x86-64 \
              mingw-w64-x86-64-dev curl

The new build-msi.sh script:
  1. Runs sync.sh to refresh pkg/{lmcp,server,json}.lua from root.
  2. Downloads Lua 5.4.2 Win64 binaries from LuaBinaries (Tools +
     Library zips — interpreter + headers + import lib).
  3. Cross-compiles LuaSocket 3.1.0 via x86_64-w64-mingw32-gcc
     (produces socket-3.0.0.dll + mime-1.0.3.dll for Win64).
  4. Stages pkg/lua/{lua.exe, lua54.dll, socket/, mime/, *.lua} per
     the WiX manifest layout.
  5. Invokes wixl on the lmcp.wxs manifest (with sed for the
     Windows backslash path separators → forward slashes).

Output: lmcp-<version>.msi. Version is read from lmcp.wxs
Version="…", so bump that before each release.

Cold build: ~30s. Warm cache: ~5s. The artifact contains all 17
files the WiX manifest expects, ProductVersion matches lmcp.wxs.

README updated to point at build-msi.sh as the recommended path;
the Windows-side candle/light recipe kept as an alternative.

Reproducibility note (deferred): the MSI is not yet bit-reproducible
across builds — file mtimes in the Lua binaries' zip propagate to
the cab inside the MSI. The debian/lmcp/build-deb.sh in marfrit-
packages uses SOURCE_DATE_EPOCH to fix this; same pattern would
apply here. Out of scope for the first cut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:27:32 +00:00
Markus Fritsche 7e62f71931 v1.1.0/#18: windows MSI build — sync.sh + tracked manifest
windows/ was previously an untracked working tree with manually-
copied .lua files that drifted ~6 months out of date (missed every
feature added since April 2026). #18 introduces Option 1 from the
issue body: build-time sync.

New tracked files:
  - windows/sync.sh — copies root {lmcp,server,json}.lua to pkg/.
    Idempotent; run before WiX. Catches missing source files; logs
    each sync.
  - windows/README.md — workflow doc + tracked-vs-generated map.
  - windows/lmcp.wxs — MSI manifest (Version bumped 0.1.0 → 1.1.0).
  - windows/pkg/{install_service,start}.bat — Windows service
    installer + launcher (now tracked; they were already in pkg/).

New .gitignore at repo root:
  - windows/pkg/{lmcp,server,json}.lua — regenerated by sync.sh
  - windows/pkg/lua/ — bundled Lua + LuaSocket runtime (downloaded
    separately, not in git)
  - editor noise (*.swp, *.swo, .DS_Store)

Verification (Phase 7):
  $ ./windows/sync.sh
    synced lmcp.lua
    synced server.lua
    synced json.lua
  $ diff lmcp.lua windows/pkg/lmcp.lua  → empty
  $ git ls-files -o --exclude-standard windows/
    windows/README.md
    windows/lmcp.wxs
    windows/pkg/install_service.bat
    windows/pkg/start.bat
    windows/sync.sh
  $ git check-ignore windows/pkg/{lmcp,server,json}.lua  → all 3 ignored

The "missed every feature since April" failure mode this fixes:
running sync.sh before each MSI build now guarantees pkg/ matches
master. Forgetting to run it is failure-loud (the MSI ships the
last sync's snapshot, easy to spot in QA), not silent (the manifest
points at fresh files that mismatch root).

Closes v1.1.0 milestone with #11, #20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.0
2026-05-17 19:43:27 +00:00
Markus Fritsche 55ead8041f v1.1.0/#11: progress + cancellation notifications
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>
2026-05-17 19:29:00 +00:00
Markus Fritsche 2ac502e50f v1.1.0/#20: concurrent handler dispatch
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>
2026-05-17 19:03:06 +00:00
Markus Fritsche deb73d129e v1.0.0-rc1: full MCP 2025-06-18 surface
Closes 14 issues; lmcp now implements the complete client-facing
surface of MCP spec 2025-06-18.

New primitives:
  - fetch (#3)              HTTP GET/HEAD with bounded body + render chain
  - web_search (#4)         pluggable backend (SearXNG/DDG/Tavily/Brave)
  - Resources (#5)          resources/list, /read, /templates/list + list_changed
  - Prompts (#6)            prompts/list, /get + list_changed
  - Completion (#7)         completion/complete for prompt/template args
  - Logging (#8)            logging/setLevel + notifications/message
  - Sampling (#9)           server-initiated sampling/createMessage
  - Roots (#10)             roots/list + cache + path_in_roots helper

Protocol / wire:
  - Pagination (#12)        cursor on tools|resources|prompts/list
  - Structured tool output (#13)  structuredContent + _meta + protoV bump to 2025-06-18
  - Tool annotations (#14)  readOnlyHint/destructive/idempotent/openWorld on all tools
  - stdio transport (#15)   LMCP_TRANSPORT=stdio for Claude Desktop / IDE clients
  - Streamable HTTP (#16)   select()-based event loop, sessions, persistent SSE,
                            DELETE, heartbeat, server-initiated request helper
  - ping (#19)              now emits result:{} not result:[] via json.empty_object

Cross-cutting fixes:
  - json.lua: UTF-16 surrogate pair combination (emoji/non-BMP CJK round-trip)
  - json.lua: json.empty_object sentinel for spec-correct {} emission
  - handle_request: generic notification suppression (id==nil → return nil)
    eliminates malformed -32601 with id:null on stdio and HTTP transports

Tool annotations backfilled across all registrations:
  - server.lua: 10 tools (shell, shell_bg, read_file, write_file, edit_file,
    list_dir, search_files, fetch, web_search, systeminfo)
  - hub.lua:   8 remote_* tools
  - example_server.lua: 4 demo tools + 3 sample resources + 1 sample prompt
                        + 1 sample completer

Honest limits, filed as follow-up issues:
  - #11 progress + cancellation — gated on #20 (handler concurrency)
  - #18 windows/pkg sync         — stale April-2026 snapshot, packaging decision
  - #20 concurrent handler dispatch — select() loop concurrencies I/O, not
                                      handler execution; synchronous tool
                                      handlers still serialise (shell sleep 3
                                      blocks a parallel ping)

Backwards compatible: every previously-deployed lmcp client (sessionless
POST, HTTP-only, no Mcp-Session-Id awareness) keeps working unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.0.0-rc1
2026-05-17 17:15:54 +00:00
Markus Fritsche b81b021b5b v0.5.4: ship examples/lmcp.service template
Companion to lmcp-hub.service. Gives a copy-and-edit starting point for
per-host lmcp instances (foo-tools style). Handles the Arch-vs-Debian
/usr/bin/lua vs /usr/bin/lua5.4 split via a comment pointing users to
override ExecStart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.5.4
2026-04-21 20:12:43 +00:00
Markus Fritsche 17af91a99b v0.5.3: hub hardening — hard ssh timeout, parallel probes, sticky DOWN cache
Three fixes addressing the recurring "hub wedges on offline backends" class
of failures (2 incidents in 24h, root-caused to single-threaded Lua + an
uninterruptible os.execute ssh call):

1. Hard wall-clock cap on ssh fallback via GNU `timeout --kill-after=2 30`.
   ConnectTimeout alone only bounds TCP connect; a half-dead sshd (auth
   stall, remote bash-s hang) used to freeze the whole event loop
   indefinitely. Configurable via LMCP_HUB_SSH_HARD_TIMEOUT. Also adds
   ServerAliveInterval=5/Count=2 so an established-but-dead tunnel dies.

2. Parallel lmcp probes for remote_list_hosts. Shells out a single bash
   fan-out of curl -m 3 calls, bounded by PROBE_BUDGET. Wall clock for a
   full 12-backend probe went from ~28 s (sum of per-host ssh connect
   timeouts) to ~3 s.

3. Probe is lmcp-only — ssh is no longer used as health check. The hub
   exists to absorb lots of offline hosts, so an expensive ssh per probe
   was the exact wrong tradeoff. Actual remote_* tool calls still fall
   through to ssh fallback when lmcp is down.

4. Sticky DOWN cache with exponential backoff: 60 → 120 → 240 → 480 →
   900 s. Prevents a sleeping fleet from burning probe budget on every
   health check. UP hosts still use 30 s TTL. Tunable via
   LMCP_HUB_PROBE_TTL_{UP,DOWN_MIN,DOWN_MAX}.

5. Per-request logging to stderr (tool, host, via, elapsed) — invisible
   before, now captured in journal for the next hang's RCA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.5.3
2026-04-21 11:58:44 +00:00
Markus Fritsche b29a2716d1 v0.5.2: shell_bg / remote_shell_bg — background launch tools
server.lua gains a shell_bg tool that launches a detached command via
setsid + nohup + stdio-redirect + &, returns immediately with PID and
log path. Linux-only for MVP (Windows Start-Process equivalent TBD).

hub.lua gains remote_shell_bg, forwarding to backend shell_bg. lmcp-only,
no ssh fallback — fallback for fire-and-forget is semantically murky.

Addresses the 'how do I launch a daemon over lmcp without the sentinel-
file wrapper blocking forever' question. Existing remote_shell keeps
its current synchronous-with-timeout behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.5.2
2026-04-20 09:34:24 +00:00
Markus Fritsche 555beb9fd9 v0.5.1: search_files uses find -L for macOS symlink start paths
BSD find on macOS silently emits nothing when the starting path is
itself a symlink (no trailing slash, no -L). On riemann with Homebrew,
/usr/local/share/lua is a symlink to /usr/local/Cellar/luarocks/.../share/lua
which tripped this — search_files returned empty for clearly-matching
patterns. GNU find on Linux follows the starting arg by default, so the
bug was invisible on every other host.

Add -L explicitly. Both BSD and GNU find accept it, both detect cycles,
and behavior becomes consistent.

Fixes marfrit-tracker task #16 (opened 2026-04-18 while stress-testing
riemann-tools MCP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.5.1
2026-04-19 12:48:05 +00:00
Markus Fritsche 490e688cc1 v0.5.0: add hub — fleet-wide MCP broker
One lmcp server on a central host (typically hertz) that proxies
remote_* tools to every backend in a registry, with a clean SSH
fallback for hosts whose lmcp is temporarily down or not installed.

Tools: remote_list_hosts, remote_{shell,read_file,write_file,edit_file,
list_dir,search_files}. Each takes a `host` argument naming the target
in /opt/herding/etc/hub-backends.conf (or $LMCP_HUB_BACKENDS).

Lazy 30s health cache; `remote_list_hosts force=true` bypasses it.
Bearer auth on inbound (standard lmcp opts.conf / LMCP_TOKEN machinery);
backend Bearer tokens kept in the registry and forwarded per-call.

SSH fallback uses `ssh host 'bash -s' < local_script` — stdin-piped
script body is the canonical shell-escape-free technique. Covers
shell/read_file/write_file/list_dir/search_files. edit_file is lmcp-only
because the literal-match + uniqueness check is nontrivial to replicate
safely in shell.

Ships an example systemd unit and a commented backends.conf template
in examples/. No migration required for existing lmcp deployments —
hub.lua is additive alongside the existing server.lua.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.5.0
2026-04-19 12:29:13 +00:00
Markus Fritsche c5884d6a97 scripts/lmcp-install-macos.sh: fix for real-world Homebrew
Original draft assumed `brew install lua luasocket` works. It doesn't:
luasocket isn't a brew formula (install via luarocks), and default `lua`
is 5.5 while the rest of the fleet is on 5.4. Fix tested on riemann
(Intel Mac, macOS 14.8.3):

- Pin to lua@5.4 (keg-only brew formula) — matches fleet library paths.
- Install luasocket via luarocks into the user-local rocks tree.
- Source brew shellenv ourselves so non-login bash shells can find brew.
- Bake LUA_PATH / LUA_CPATH into the LaunchAgent plist so the service
  resolves `require 'socket'` from ~/.luarocks/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.4.2
2026-04-18 12:37:05 +00:00
Markus Fritsche 2f2c1f3036 Add scripts/lmcp-install-macos.sh + LMCP_TOKEN env fallback
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>
v0.4.1
2026-04-18 10:45:54 +00:00
Markus Fritsche b00b8ef63c Add edit_file tool — Claude Code Edit semantics
Literal string replacement with uniqueness check. Fails if old_string
is not found or matches multiple times (unless replace_all=true).

Matches the Claude Code harness Edit tool so sibling lmcp clients get
the same behaviour they already expect for in-place patches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.4.0
2026-04-17 15:47:17 +00:00
marfrit c6efc8f685 initial import: lmcp 0.1.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.3.0
2026-04-14 19:58:40 +00:00
Markus Fritsche 6bf0f450dc Security hardening: body size limit, JSON depth limit, timing-safe auth
- 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>
2026-04-11 20:45:16 +02:00
Markus Fritsche abd9db30f2 Add Bearer auth, rewrite example server, add README
- lmcp.lua: optional Bearer token auth via conf file or explicit token
- lmcp.lua: CORS Authorization header allowed
- example_server.lua: rewritten with non-blocking shell, file ops, search
- README.md: usage, auth config, Claude Code integration, tool examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.2.0
2026-04-11 19:15:27 +02:00
Markus Fritsche 2bd661a8c9 Initial release: Lua MCP server library
Zero-dependency MCP (Model Context Protocol) server in pure Lua.
Only requires luasocket. 2MB RSS vs Python FastMCP's 97MB.

- json.lua: pure Lua JSON encoder/decoder (~150 lines)
- lmcp.lua: MCP server with streamable-http transport (~230 lines)
- example_server.lua: shell/file tools demo

Implements MCP 2025-03-26: initialize, tools/list, tools/call,
notifications/initialized, ping. JSON-RPC 2.0. SSE support. CORS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:54:25 +00:00