Skip to content

Better mcp series - #18

Open
the-real-wizard-of-oz wants to merge 40 commits into
zeenix:mainfrom
the-real-wizard-of-oz:better-mcp-series
Open

the-real-wizard-of-oz wants to merge 40 commits into
zeenix:mainfrom
the-real-wizard-of-oz:better-mcp-series

Conversation

@the-real-wizard-of-oz

Copy link
Copy Markdown

better-mcp-series → main

Expands the MCP server from a 10-tool LSP shim into a 36-tool concurrent
service with cancellation, auto-restart, multi-workspace support, and
LLM-friendly output shaping.

Scope: 36 commits, +8,694 / −827 lines, 32 files.
Tests: 142 green (was ~30) — 89 unit + 28 integration + 7 property + 6 stress + 12 protocol.

Why

  • Token cost — raw LSP responses are verbose (a single documentSymbol
    could be 60+ KB; workspace/symbol returned duplicates; hover markdown
    was uncapped).
  • Round-trip latency — locating a symbol took 4–5 sequential MCP calls.
  • Operational fragility — rust-analyzer crashes wedged the server,
    cancellation couldn't reach in-flight LSP work, external edits silently
    desynced rust-analyzer's view, and the post-didOpen 200 ms sleep was
    a guess.

What's new

26 new tools. Highlights:

  • Composites (one round-trip, server-side fan-out via tokio::join!):
    explore_symbol (hover + def + type_def + parent + refs sample),
    impact (refs + callers + implementors + tests — blast radius),
    get_type_by_name (no file/position needed).
  • Missing LSP standards: rename + prepare_rename, signature_help,
    inlay_hints, workspace_symbol, type_definition, implementation,
    call/type hierarchy (both directions).
  • rust-analyzer extensions: expand_macro, parent_module, runnables
    (reshaped envelope) + run_runnable (opt-in cargo spawn), related_tests,
    open_docs, syntax_tree, view_hir, view_mir.
  • Refactor: move_file (uses willRenameFiles + applies the edit + renames
    on disk).
  • Workspace mgmt: add_workspace, remove_workspace, list_workspaces.

MCP resources (new protocol surface). resources/list + resources/read
with three URI families: workspace://files (tree, capped), workspace://crates
(reshaped cargo metadata), workspace://crate/<name>/Cargo.toml. Multi-workspace
variants are id-prefixed (workspace://ws-2/...); the default workspace keeps
emitting unprefixed legacy URIs.

Reliability & concurrency

  • True parallelism: &mut self → Arc<Self> + spawn-per-request.
  • End-to-end cancellation: notifications/cancelled aborts the spawned
    task and forwards $/cancelRequest to rust-analyzer. PendingTracker
    cross-indexes by LSP id and originating MCP id (via task_local), so handler
    signatures stay unchanged.
  • Auto-restart on rust-analyzer crash: a monitor task drains pending
    oneshots with LspError::ProcessDied, next request gets a fresh subprocess.
    Rate-limited (3 crashes / 60 s).
  • External-edit sync: per-uri version + mtime + content hash;
    textDocument/didChange on real edits instead of stale didOpen state.
  • $/progress integration: replaced the 200 ms post-didOpen sleep with
    a level-triggered watch on rust-analyzer's rustAnalyzer/cachePriming token.
  • Typed LspError distinguishes transport / timeout / cancel / process-died
    from "no result at this position" (still surfaced as null).

LLM-friendliness

  • Truncation defaults: hover ≤ 5 KB, completion ≤ 50 items, symbols
    collapses children (with child_count), workspace_symbol/diagnostics dedup
    • paginate. verbose=true bypasses caps.
  • Pagination: opaque cursor / next_cursor on symbols,
    workspace_symbol, workspace_diagnostics.
  • Snippet enrichment: 9 location-emitting tools auto-attach source
    snippets next to every { uri, range }, eliminating the follow-up Read.
    Per-call file cache, 50-hit budget, pagination runs before enrichment.
  • workspace_symbol dedup on (name, containerName, location) —
    rust-analyzer often returns each entry twice.
  • Diagnostics passthrough: data.rendered (cargo-formatted error block),
    codeDescription.href, and tags now reach the LLM (previously stripped).

Multi-workspace + persistence

  • WorkspaceRegistry owns one entry per registered root, each with its own
    rust-analyzer subprocess, restart history, open-doc cache, and stable id
    (ws-1, ws-2, ...).
  • Roots persist to workspaces.json under XDG state dir
    (RUST_ANALYZER_MCP_STATE_DIR → XDG_STATE_HOME → $HOME/.local/state).
    Best-effort; set the env var to "" to disable. Workspace ids are not
    persisted (reissued each boot).

Operational

Audit of 12 days of MCP-client logs (67 sessions, 17 workspaces) showed every
shutdown hitting the SIGTERM fallback. The final commit fixes that:

  • Default tracing filter info → warn (routine startup logs no longer
    pollute the MCP client's stderr-as-error channel).
  • Workspace shutdown parallelized via join_all with an 80 ms budget;
    kill_on_drop is the backstop.
  • std::process::exit(0) after run() returns — tokio::io::stdin()'s
    blocking-pool thread sits in read(2) on a pipe the parent hasn't closed,
    which deadlocks Runtime::drop. Shutdown latency: >1,000 ms → ~7–8 ms.

Backward compatibility

  • All 10 pre-existing tools keep their schemas. Response shapes for symbols,
    workspace_symbol, workspace_diagnostics are now wrapper objects
    ({ symbols, total, ... }) — additive, not breaking semantics, but callers
    that destructure the raw array need a one-line adjustment.
  • set_workspace keeps working (now replaces the default workspace's root
    in place, preserving its id).
  • Unprefixed workspace://... URIs still route to the default workspace.
  • workspace_id is optional on every workspace-scoped tool.

Known limitations / follow-ups

  • LSP $/cancelRequest arrives a few ms after the local task abort
    (acceptable in practice).
  • get_type_by_name only resolves what rust-analyzer's workspace/symbol
    indexes — typically workspace members; external-crate paths return
    primary: null.
  • update_document (write-through edits) intentionally out of scope; external
    edits via the host's Edit tool remain the canonical path.

Orest Zadarko and others added 30 commits April 28, 2026 09:02
Replace serial &mut self plumbing with Arc<Self> + spawn-per-request so
MCP tool calls run in parallel; clients use interior mutability throughout.
Introduce a typed LspError enum so transport, timeout, and cancellation
failures propagate distinctly from "no result at this position" (which
hover/definition/references/completion/symbols still surface as null for
backward compatibility). Drop dead diagnostics_test/simple_error magic
strings, eliminate handler boilerplate via a generic with_doc helper,
cache tools/list output in a OnceLock, fix the open_document race, skip
disk reads for already-open files, and migrate logging from log+env_logger
to tracing (with stderr writer so stdout stays clean for MCP traffic).
Narrow tokio features and switch pending_requests to parking_lot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Negotiates window/workDoneProgress, replies to server-initiated requests
(window/workDoneProgress/create, workspace/configuration), and tracks
$/progress tokens in a per-client watch map. New helper
`wait_for_progress_end(token, timeout)` is level-triggered so callers
that arrive after `end` return immediately. Stdin moved to Arc<Mutex>
so the response loop can write replies without owning the client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 200ms post-didOpen sleep was a guess. Replace it with a watch on
rust-analyzer's `rustAnalyzer/cachePriming` $/progress token, which is
the modern equivalent of the old "Indexing" signal — once it ends,
hover/definition/references see resolved symbols. Best-effort: on
timeout (5s) we fall through, since handlers already cope with null
during indexing. Old DOCUMENT_OPEN_DELAY_MILLIS removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s, workspace_symbol

New tools dispatched via the existing with_doc helper (workspace_symbol
takes the no-document path since it queries by name across the whole
workspace). Capabilities extended in initialize: rename with
prepareSupport, signatureHelp with parameter info, inlayHint, and
workspace.symbol.

Also harden handle_diagnostics: rust-analyzer cancels in-flight pull
diagnostics while it re-indexes, which used to be masked by the now-gone
200ms post-didOpen sleep. Swallow transient errors inside the polling
loop so the next iteration retries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A monitor task spawned at start() polls Child::try_wait() every 500ms.
When rust-analyzer exits, it flips a `process_died` flag on the client
and drains every pending oneshot::Sender with LspError::ProcessDied so
in-flight callers fail fast instead of timing out.

ensure_client_started now treats `is_dead()` as a signal to drop the
old client and start a fresh one in place. Restarts are rate-limited:
more than MAX_RESTART_COUNT (3) crashes in RESTART_WINDOW_SECS (60)
short-circuits with an error rather than spinning up another doomed
process. The old client's shutdown runs in a detached task so the next
request doesn't pay for cleanup latency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each spawned tool call now registers its AbortHandle in an `in_flight`
map keyed by the canonical-string form of the MCP request id. A
`notifications/cancelled` notification looks up the matching handle and
calls `abort()`; tasks remove themselves on completion via a small
cleanup task that awaits the JoinHandle.

Cancellation is coarse: the spawned tool task is killed but any LSP
requests it had in flight at rust-analyzer continue to completion (their
oneshot::Senders just go nowhere). Threading MCP-id → LSP-id mapping
all the way through send_request would touch every handler signature
and is left for a follow-up.

No response is written for cancelled requests, which matches what the
client already discarded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Open documents now carry per-uri version, mtime, and content_hash, so
open_document_if_needed can:
  1. skip work entirely when the file's mtime hasn't changed
  2. emit didChange (full-text variant) on real edits, with version++
  3. fall back to didOpen for first sight

mtime is the cheap freshness signal; the content hash guards against
mtime-only touches that didn't change bytes (saves rust-analyzer a
re-analysis pass). diagnostics for the uri are evicted on real changes
so handle_diagnostics doesn't hand back stale results.

open_documents migrated from HashSet<String> to HashMap<String,
OpenDocState>; the workspace_diagnostics fallback updated to iterate
keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
restart.rs: pgrep rust-analyzer under the MCP server, SIGKILL it, and
verify the next tool call succeeds with a different rust-analyzer pid.
Exercises the monitor → drain → restart path end-to-end.

cancellation.rs: send a notifications/cancelled for both an unknown
request id (must be ignored) and a real in-flight tool call (race-
sensitive — the test only asserts the server stays healthy, since the
exact "got cancelled vs. completed first" outcome depends on indexing
state). The dedicated smoke harness covers the active-cancel path.

Adds three small helpers on MCPTestClient: `server_pid`,
`send_notification`, and `send_tool_call_raw` for fire-and-forget calls
that don't read a response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rust-analyzer signals "no renamable symbol at this position" via
JSON-RPC code -32602 (InvalidParams) instead of returning null.
Map that to Ok(null) for prepare_rename and rename so the tools
behave like hover/definition/references when the position has no
relevant symbol.
The IPC test daemon is shared across integration tests in the same
project. test_workspace_change switched the daemon's rust-analyzer
to a throwaway workspace and never switched back, so any test that
ran afterwards on the same daemon couldn't find its files —
test_new_lsp_tools failed in the full suite with "No references
found at position" while passing in isolation.

Restore the original workspace at the end of the test.
Until now `notifications/cancelled` only aborted the MCP-side spawn.
The LSP requests it had in flight kept running inside rust-analyzer
and only got dropped when the response eventually arrived at a
closed oneshot channel — so a cancelled tool call still cost the
upstream work.

Wire the cancel through to rust-analyzer:
- PendingTracker now indexes pending LSP requests by both LSP id and
  the originating MCP request id, behind a single mutex so the two
  views can never disagree.
- `send_request` reads the current MCP id from a tokio task-local
  set by the request handler, so handler signatures stay unchanged.
- `cancel_mcp` resolves every affected sender with
  LspError::Cancelled and sends `$/cancelRequest` per LSP id so
  rust-analyzer can stop the work it was doing.
- `handle_cancellation` calls `cancel_mcp` before aborting the spawn,
  ensuring the LSP-side cancel goes out while the tracker still
  knows the ids.

Adds three PendingTracker unit tests covering the cross-index
consistency and per-MCP eviction.
The IPC test daemon picked target/release/rust-analyzer-mcp whenever
it existed, even when the developer was running tests in debug mode.
A stale release binary from an earlier session would silently mask
debug-build changes — new tools would land in the source and the
debug binary, but the daemon would spawn the old release binary and
report "Unknown tool".

Pick the binary that matches the daemon's own build profile (debug
or release), with the other as a fallback if the preferred one
hasn't been built.
LSP standard methods that were missing from the suite:
- rust_analyzer_type_definition  — textDocument/typeDefinition
- rust_analyzer_implementation   — textDocument/implementation

rust-analyzer LSP extensions:
- rust_analyzer_expand_macro     — rust-analyzer/expandMacro
- rust_analyzer_parent_module    — experimental/parentModule
- rust_analyzer_runnables        — experimental/runnables
                                   (position optional; without it returns
                                   every runnable in the file)
- rust_analyzer_related_tests    — rust-analyzer/relatedTests
- rust_analyzer_open_docs        — experimental/externalDocs

All tools follow the existing handler pattern (`with_doc` for
position-based tools, `lookup_to_null` for "no symbol here / index
not ready"). typeDefinition and implementation also got their
respective entries in the initialize capability block.

Adds test_phase1_tools integration test that exercises every new
tool against test-project and accepts either a payload or null.
Hover markdown caps at 5 KB, completion items at 50 (sorted by sortText),
workspace_symbol pages at 100 with opaque cursor/next_cursor round-trip,
workspace_diagnostics paginates files (50 per page) while keeping the
workspace-wide summary intact. All four tools accept verbose=true to bypass
the default cap (still subject to a 1000-item absolute guardrail), and the
two paginated tools accept explicit limit + cursor params.

Reshaping happens in mcp::handlers via a new mcp::truncate module — the
LSP layer stays a thin passthrough. Null responses are preserved so the
indexer-not-ready signal continues to round-trip.

Output shapes are wrapper objects (e.g. { symbols, total, returned,
next_cursor? }) instead of raw LSP arrays — explicit metadata is more
useful to LLM callers than LSP-spec fidelity here.

19 truncate-module unit tests + 1 phase-2 integration test added. Full
suite: 62/62 green (was 42/42).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tree

Adds resources/list and resources/read JSON-RPC methods to the server, plus
the resources capability in initialize. First and only resource so far is
workspace://files: a recursive file tree of the workspace root, capped at
5000 entries and 16 levels deep, with target/, .git/, node_modules/ and
similar dirs filtered out. Symlinks are reported but not followed (loop
guard).

The walk runs sync std::fs in spawn_blocking so a large tree doesn't stall
the request reader. Output is application/json text inside the standard
MCP contents wrapper. Truncation surfaces as `truncated: true` on the
relevant subtree (max-depth) or `stats.truncated` (max-entries).

Step 2 will add workspace://crates (cargo metadata) and per-crate Cargo.toml
resources; Phase 3.2 (multi-workspace) is still future work.

6 unit tests + 1 integration test added — 69/69 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the resources catalogue with two new URI families. workspace://crates
returns a reshaped cargo metadata --no-deps summary (per-package name,
version, manifest path, targets, declared dependency names — the bulky
fields like license, registry source, and resolution graph are dropped).
workspace://crate/<name>/Cargo.toml returns the actual TOML manifest text
for the named crate.

resources/list now runs cargo metadata at list-time (off the request reader
via spawn_blocking) so each workspace crate gets its own concrete URI.
Workspaces without a Cargo.toml gracefully fall back to just the file-tree
resource — no error.

Path-traversal guard: per-crate URIs resolve via cargo metadata's
manifest_path lookup rather than concatenating the caller-supplied name
into a filesystem path, so crafted names like ../../etc/passwd just fail
with "Unknown crate" instead of escaping the workspace.

5 new unit tests + extended integration test (cover crates summary,
per-crate manifest read, unknown name, traversal block). 74/74 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single-workspace state on RustAnalyzerMCPServer with a
WorkspaceRegistry that owns one WorkspaceEntry per registered root. Each
entry has its own rust-analyzer subprocess (lazy), its own
restart_history rate-limit window, its own open-document cache, and a
stable opaque id (ws-1, ws-2, ...). The first inserted entry stays the
default — every tool call without an explicit workspace_id resolves
there.

Three new tools manage the registry:

- rust_analyzer_add_workspace(path) → { workspace_id, root }
  (boots the client eagerly so the next call doesn't pay the cold start)
- rust_analyzer_remove_workspace(workspace_id)
- rust_analyzer_list_workspaces() → { workspaces: [{id, root, default}] }

Every workspace-scoped tool now accepts an optional workspace_id
argument. Schemas are augmented automatically via inject_workspace_id at
build_tools() time so individual definitions don't have to spell it out;
the four workspace-management tools are excluded by name.

set_workspace stays backward-compatible: it replaces the default
workspace's root in place (preserving its id) and shuts down the
existing client so the next call boots rust-analyzer in the new
directory. test_workspace_change keeps passing as-is.

Cancellation goes from a single-client lookup to a broadcast across all
registered clients — each client's cancel_mcp is O(1) when the MCP id
isn't in its tracker, so the cost is negligible at typical 1–2
workspaces and avoids a second index in in_flight.

MCP resources become per-workspace:

- resources/list emits id-prefixed URIs (workspace://ws-2/files,
  workspace://ws-2/crate/foo/Cargo.toml, ...). For the default
  workspace it ALSO emits the unprefixed legacy aliases
  (workspace://files etc.) so single-workspace clients see no change.
- resources/read parses an optional id segment after workspace://. If
  the segment matches a registered id, the URI is normalized (id
  stripped) and routed to that workspace; otherwise it falls back to
  the default — which keeps legacy paths like
  workspace://crate/foo/Cargo.toml working.
- The response URI in `contents[].uri` is rewritten back to the exact
  caller-supplied URI so the round-trip is lossless even when an id
  prefix was resolved.

Two new integration tests cover the end-to-end add → use → remove flow
and the multi-workspace resource routing (prefixed URI returns the
right tree, unprefixed URI stays on default, response URI round-trips).

76/76 tests green (was 74). cargo +nightly fmt and cargo clippy --lib
--bin rust-analyzer-mcp -- -D warnings: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more rust-analyzer custom methods, useful when the LLM is
debugging a parse / lowering issue rather than navigating code:

- rust_analyzer_syntax_tree (rust-analyzer/syntaxTree) — printed
  parser-level syntax tree. file_path required; range coords are
  optional but all-or-nothing (line+character+end_line+end_character
  together, or none for whole-file). Partial coords are a tool-call
  error rather than a silent fallback to whole-file, so the LLM can't
  send an under-specified range and quietly get the wrong scope.
- rust_analyzer_view_hir (rust-analyzer/viewHir) — debug-printed HIR
  for the function body containing the given position.
- rust_analyzer_view_mir (rust-analyzer/viewMir) — same for MIR.

All three follow the existing with_doc + lookup_to_null pattern, so
"position not inside a function body" / "indexer not ready" collapse
to null instead of an error — matching the rest of the rust-analyzer/*
suite. Output is a free-form string; no truncation, since the value of
these tools is in seeing the full lowering and a 5KB cap would cut it
mid-statement.

New ToolParams::extract_optional_range helper covers the all-or-nothing
range pattern for any future optional-range tool.

77/77 tests green (was 76 — one new integration test exercises whole
file, narrowed range, partial-range guard, and both view_* tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks LSP responses and attaches a `snippet` sibling to every Location
(`{ uri, range }`) and LocationLink (`{ targetUri, targetRange }`), so the
LLM consumer doesn't need a follow-up file read after every reference,
definition, or workspace_symbol hit.

- New `src/mcp/snippets.rs` — generic walker `enrich_locations` plus
  `enrich_workspace_diagnostics` for the `{ files: { uri: [...] } }` shape.
  Per-call file cache, 50-hit budget, ~400 bytes per snippet.
- 9 tools opt in: definition, references, type_definition, implementation,
  parent_module, runnables, related_tests, workspace_symbol,
  workspace_diagnostics. Each accepts `include_snippets` (default true)
  and `snippet_context_lines` (default 2) via `inject_snippet_opts` in
  `tools.rs`.
- Pagination runs *before* enrichment so workspace_symbol and
  workspace_diagnostics only read files for the page actually returned.
- Integration test: `test_phase2_1_snippets`.

Folded in: working-tree refactor that was sitting unposted (LSP error
plumbing, server/handlers/tools tidy-up, schema helpers, src/util.rs
extraction). Tests 90/90, fmt clean, clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single tool that fans hover + definition + type_definition + parent_module +
references_sample out concurrently via tokio::join! on a once-opened
document, returning all five in one round-trip. Replaces the typical
4-5 follow-up calls a caller makes after locating a symbol.

- references is wrapped in a 2 s timeout so a slow workspace-wide search
  can't block the rest of the composite; on timeout the response still
  comes back with `references_timed_out: true`.
- references_sample shape: { items: [up to 5], total, shown } — enough
  to navigate without burning token budget.
- Per-subcall errors degrade to null (best-effort) rather than poisoning
  the whole response.
- Tool is in SNIPPET_ENRICHED_TOOLS, so every nested location gets
  pre-attached source snippets.

All 5 sub-calls share the same MCP request id via task_local, so the
existing PendingTracker cancels them as a group with no extra plumbing.

Tests 91/91, fmt clean, clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
§2.4 — Call-Hierarchy + Type-Hierarchy (3 new tools):
- rust_analyzer_call_hierarchy_incoming / _outgoing — prepare→fan-out
  composites driven server-side. fromRanges are reshaped into self-
  contained {uri, range} call_sites so the snippet walker can enrich
  every call site, not just the caller header.
- rust_analyzer_type_hierarchy with direction ∈ {supertypes, subtypes,
  both}; super/sub queries run via tokio::join! when both requested.
- 6 thin LSP wrappers (prepare* + incoming/outgoing/super/sub) with
  lookup_to_null. callHierarchy/typeHierarchy capabilities declared
  in initialize.
- Snippet walker now prefers selectionRange over range when both are
  present, so call/type hierarchy items get a name-sized snippet
  rather than a full-body one.

§5.3 — impact composite (1 new tool, blast-radius for refactors):
- rust_analyzer_impact aggregates four buckets in parallel: references,
  callers (incoming calls flattened to from-items), implementors
  (subtypes), and tests (related_tests). Each bucket has shape
  { items: [...up to 10], total, shown, timed_out? }.
- Two-stage parallelism via tokio::join!: stage 1 runs references +
  prepareCallHierarchy + prepareTypeHierarchy + relatedTests; stage 2
  fans out incomingCalls + subtypes on prepared items. Per-bucket 2 s
  timeout so one slow workspace-wide search can't block the rest.

Tests: +2 integration tests, 93/93 green (was 91).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds rust_analyzer_get_type_by_name(name): a composite that resolves
a path-style symbol name without needing a file/position. Splits the
input on "::", fuzzy-searches via workspace_symbol on the last
segment, filters surviving entries against the path prefix
(exact match / endsWith / per-segment substring on containerName),
then runs hover + type_definition in parallel on the first match.

Output shape: { matches: [...up to 10], total, shown,
primary?: { hover, type_definition, location } }. The primary block
is omitted when no match is found.

Limitation: rust-analyzer's workspace_symbol typically only indexes
the local workspace, so external-crate paths like serde_json::Value
won't resolve unless the crate is a workspace member. Documented
implicitly via primary=null on no-match.

Tests: +1 integration test, 94/94 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30s TTL plus Cargo.toml/Cargo.lock mtime keying so resources/list and
resources/read no longer fork cargo on every probe. Manual invalidation
on replace_root / shutdown_client. Lazy provider plumbed through
list_resources/read_resource so workspace://files reads still skip
metadata entirely.
New rust_analyzer_move_file tool fixes the "rename a Rust file but
forget to update mod-decls/imports" trap. Sequence: workspace/willRenameFiles
gives us a WorkspaceEdit, the new mcp::workspace_edit module applies
those text edits on disk (reverse-order TextEdit splicing, scoped to
within the workspace root), then std::fs::rename does the physical move.
Affected URIs are dropped from rust-analyzer's open-doc state so the
next tool call reloads fresh content.

Also adds RustAnalyzerClient::close_document and the
workspace.fileOperations.willRename client capability so rust-analyzer
will emit the edit at all.
Reshape rust-analyzer's raw runnables to a compact envelope:
  { runnables: [{ kind, label, fq_name?, cargo_args, location,
                  can_run_via_mcp }], total, can_run_via_mcp }

`cargo_args` is a flat argv (cargoArgs + cargoExtraArgs + ["--"] +
executableArgs) ready to feed into the new run_runnable tool. `kind`
is parsed from the leading word of rust-analyzer's label; `fq_name`
is best-effort from executableArgs[0] or label tail. `location`
stays as the original LocationLink so the snippet walker keeps
enriching it.

New tool rust_analyzer_run_runnable(cargo_args, timeout_secs?):
spawns `cargo` inside the workspace root with kill_on_drop(true),
captures stdout/stderr capped at 5 KiB each, default timeout 60 s
(hard cap 600 s). Gated behind RUST_ANALYZER_MCP_ALLOW_RUN=1 so
hosts must opt into in-MCP code execution. Subcommand is
constrained to a whitelist (test/bench/run/build/check/clippy/
doc/nextest); anything else is rejected before spawning.

Cancellation reuses the existing in_flight AbortHandle path —
aborting the tool task drops the Child, kill_on_drop SIGKILLs
cargo. No extra plumbing.

12 new unit tests (reshape/validate/cap), 2 new integration tests
(reshape envelope shape, run_runnable input rejection). Total
120/120 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion, tags

format_diagnostics swallowed three LSP fields that rust-analyzer emits and
that an LLM wants first when fixing a bug:

- `data.rendered` — cargo-formatted error block with ASCII pointers
- `codeDescription.href` — link to the error-index doc page
- `tags` — DiagnosticTag (e.g. [1] = unused)

The workspace_diagnostics path already passed these through (raw passthrough
of each diag value); only the single-file diagnostics path was reshaping
them away. Stable shape: keys exist, values are null when absent upstream.

Tool description for `rust_analyzer_diagnostics` updated to flag
`data.rendered` as the first thing to read on a bug-fix loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…location)

rust-analyzer's `workspace/symbol` returns each match twice in many cases
(once per index source). The duplicates eat token budget without adding
signal — confirmed in a spike against eza where workspace_symbol("UseColours")
returned `total: 2` for a single enum and get_type_by_name("UseColours")
likewise reported two identical matches.

Add `dedup_workspace_symbols` that collapses on the
`(name, containerName, location)` triple and apply it before pagination
in `paginate_workspace_symbol` plus inside `handle_get_type_by_name`
(which bypasses the pagination wrapper). First-seen wins so list order
stays stable, and `containerName` is part of the key so co-located
methods on different impl blocks (DirAction::deduce vs RecurseOptions::deduce)
remain distinct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rust-analyzer's textDocument/documentSymbol returns a hierarchical
DocumentSymbol[] where every nested child carries its own range,
selectionRange, and detail string. For dense files this explodes into
tens of kilobytes (eza's theme/mod.rs hit ~64 KB in the spike).

Default behavior changes to top-level only: each top-level symbol keeps
its name, kind, detail, range, selectionRange and gets a numeric
`child_count` in place of the `children` array. Pass verbose=true to get
the full nested tree unchanged. Output wrapper is `{ symbols,
total_top_level, verbose }` so the LLM can tell at a glance how much
was elided.

Tool description updated to call out the cap and the verbose escape
hatch. Two integration test helpers were reading the legacy flat array
shape; both updated to read the new wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the symbols-cap reshape (0d6d869), `rust_analyzer_symbols` returns
`{ symbols, total_top_level, verbose }` instead of a flat
`DocumentSymbol[]`. The MCPTestClient readiness probe still parsed the
response as `Vec<Value>`, which never succeeded against the new shape,
so `initialize_and_wait` looped to its 30s timeout. Surfaced as flakes
in test_subprocess_restart_on_crash and test_cancellation_doesnt_break_server.

Probe now accepts both shapes: a top-level Object reads `.symbols`,
a top-level Array is treated as legacy. Either way a non-empty list
counts as ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The workspace registry has lived in memory only — every server restart
forced the LLM to call `add_workspace` again for any workspaces it had
previously registered. Spike against eza confirmed this as a real
papercut: after a daemon rebuild the secondary workspace was silently
gone.

Add a small file-backed persistence layer (`src/mcp/persistence.rs`)
that mirrors the registered roots to `workspaces.json` under a state
directory:

- `RUST_ANALYZER_MCP_STATE_DIR` — explicit override; empty string means
  "persistence disabled".
- `XDG_STATE_HOME/rust-analyzer-mcp` — XDG-compliant default.
- `$HOME/.local/state/rust-analyzer-mcp` — fallback.

`add_workspace`, `remove_workspace`, and `set_workspace_root` mirror
the registry to disk via an atomic tempfile-rename. A tokio Mutex
serialises writes so concurrent mutations can't interleave. On boot
`with_workspace` re-registers any persisted roots, skipping ones that
collide with the initial CLI-arg root and warning on roots that no
longer exist on disk.

Persistence is best-effort: I/O errors are logged but never propagated.
Workspace ids are *not* persisted — they're freshly issued each boot in
registration order. The two test-support spawn paths set
`RUST_ANALYZER_MCP_STATE_DIR=""` so test daemons don't write to the
user's real state dir.

7 unit tests cover the state-dir resolver, save/load roundtrip,
phantom-path skip, malformed-file handling, and atomic mkdir-on-write.
4 server-level tests cover the boot-replay roundtrip, post-remove
absence, initial-root dedup, and the disabled-persistence path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Orest Zadarko and others added 10 commits April 28, 2026 21:38
Spike against eza surfaced a real footgun: after editing files via the
Edit tool, rust_analyzer_workspace_diagnostics returned `total_errors: 0`
while `cargo check` found three missing-match-arm errors. The cause is
that rust-analyzer's diagnostics reflect its in-memory view of each file,
and external edits never went through `textDocument/didChange`, so the
analyser is reading the pre-edit content.

Document the failure mode in the tool descriptions for both `diagnostics`
and `workspace_diagnostics`. The single-file variant points at any
positional tool as the cheap re-open lever; the workspace variant pins
`cargo check` as the ground truth when the two disagree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spike 1 + Spike 2 (notes.md sequences 2 + 12) both lost an iteration to
the same off-by-one trap: copying a line number from a `Read`-style
1-based output directly into a position tool (which is 0-based per LSP)
silently misses the symbol — `references` returns 10 unrelated module
matches, `call_hierarchy_incoming` returns total: 0, etc., with no hint
that the position is wrong.

Append a uniform suffix to every line/character JSON-schema description
"— 0-based per LSP. If copied from a `Read`-style 1-based output,
subtract 1." Centralized via line_prop / char_prop so all 12 position
schemas (incl. the four optional inline cases for runnables and
syntax_tree range) carry the same reminder.

Tests: 140/140 still green; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spike notes.md sequence 10 confirmed the 0d6d869 wrapper kicks in but
isn't enough for files with hundreds of top-level items: eza's
theme/mod.rs still serializes to ~70 KB (3102 JSON lines) under
verbose=false because the children-collapse only flattens nested
structure, not list breadth.

Mirror the workspace_symbol pagination shape: shape_document_symbols
now takes (cursor, limit, verbose), default page size 100,
DOCUMENT_SYMBOLS_DEFAULT_LIMIT exposed for handlers. Output gains
`returned` and optional `next_cursor`. verbose=true keeps the full
subtree of the page.

Tool description in tools.rs now advertises cursor/limit and the
new envelope keys.

Tests: 142/142 (87 lib + 28 integration + 7 property + 6 stress + 12
protocol + 2 new pagination unit tests). 4 existing
shape_document_symbols tests updated for the new arity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spike 3 (notes.md sequence 14) hit a silent-no-op scenario: the
laufender daemon was the cargo-installed binary (built after commit
39a7965 added persistence) yet add_workspace produced no
workspaces.json. Most likely cause is the MCP host spawning the
server with a sanitized environment that drops HOME, so
default_state_dir() falls through to None.

The code is correct (tests stay green); what was missing was a
visible warning that persistence requires the spawn env to preserve
HOME/XDG_STATE_HOME, plus a knob to set RUST_ANALYZER_MCP_STATE_DIR
explicitly when the host doesn't.

Add a short README section documenting the resolution order and the
silent-no-op failure mode, and append the same caveat to the
add_workspace tool description so an LLM caller sees it in
tools/list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user-facing tool list in README was 9 entries deep and stuck on the
pre-Phase-1 surface. The server now exposes 36 tools (Phase 1 LSP
standards + rust-analyzer specials, Phase 2 truncation/pagination,
Phase 3 multi-workspace + resources, ANALYSIS §2.1-§5.3 composites and
move_file, plus the post-spike polish patches). All are dropped into 11
thematic groups: diagnostics & hover, navigation, search & discovery,
composite (explore_symbol/impact), hierarchy, refactoring (incl.
move_file), runnables (incl. run_runnable subprocess), compiler
internals, other (completion/signature_help/open_docs), and workspace
management.

Add a Resources section for the three workspace:// URIs (files, crates,
crate manifests) plus the per-workspace prefixed variants for
multi-workspace setups.

Add an Operational Features section covering the cross-cutting work
that doesn't surface as a tool: concurrency + cancellation forwarding,
subprocess auto-restart, external-edit didChange sync, snippet
enrichment, token-cost guardrails, cargo-metadata cache, and the
build-profile-aware spawn lock.

Update Project Structure to reflect the actual src/ layout (lsp/, mcp/,
diagnostics/, protocol/) and tests/ split. Refresh Contributing with
the realistic remaining gaps (update_document, null-result hint, more
config knobs) instead of items that have shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three findings from auditing 12 days of MCP client logs (67 sessions,
17 workspaces): every observed shutdown went through the SIGTERM
fallback path, and all "errors" in the log were just routine INFO
startup messages bleeding through stderr.

- Default tracing filter `info` → `warn`. The MCP client classifies all
  stderr output as error; routine startup logs no longer pollute that
  channel. `RUST_LOG=info` (or debug) still works for verbose runs.

- Parallelize workspace shutdown via `join_all` with an 80ms budget.
  Was sequential and unbounded; with multiple workspaces this trivially
  blew past the client's ~100ms SIGINT grace window.

- `std::process::exit(0)` after `run()` returns. `tokio::io::stdin()` is
  backed by a blocking-pool thread sitting in `read(2)` on a pipe the
  parent hasn't closed; `Runtime::drop` blocks forever waiting for that
  thread to finish, so SIGINT-triggered shutdowns never actually exited
  before the client escalated to SIGTERM. Manual test: shutdown latency
  dropped from >1000ms (always SIGKILL'd) to ~7-8ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rust-analyzer keeps every didOpen'ed file as an in-memory overlay and
discards its own file-watcher events for it — the LSP client is assumed
to own that buffer. An MCP tool call owns nothing: every edit lands on
disk, from the Edit tool, another editor, or a git checkout. Since
close_document was only ever called on move_file, any file a tool call
touched stayed frozen at that content for the rest of the server's life.

Measured effects, on isolated copies of test-project:

- workspace_symbol never saw a function added to a previously-opened
  file — the same edit was picked up immediately when the file had
  never been opened.
- references on lib.rs lost a call site added to an already-open
  utils.rs, i.e. a stale overlay on one file silently corrupted the
  answer for another file that *was* re-read.

resync_open_documents() now stats every open document before dispatch
and pushes a didChange for anything whose mtime moved (didClose for
anything that vanished). Steady-state cost is one stat per open
document, no reads.

The regression tests poll until rust-analyzer demonstrably answers
correctly before editing, so "still indexing" cannot be mistaken for
"answered from stale content". Against the unfixed server both time
out after 60s — the overlay never thaws on its own.

Also drops the "treat cargo check as ground truth after external
edits" advice from the diagnostics tool descriptions, which would now
steer callers the wrong way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
At full `cargo test` parallelism the suite ran 31 integration tests, each
spawning its own rust-analyzer that indexes the standard library. Adding
three more tipped it over a limit on this box: rust-analyzer would
occasionally exit with a panic mid-run, failing whichever test was
holding it (`inotify` max_user_instances is 128 on stock Linux).

Measured, 6 runs each: 28 tests alone → 0 crashes; the 3 new tests alone
→ 0 crashes; all 31 together → crashes in 4 of 6 runs; all 31 with
--test-threads=4 → 0 crashes. So it is concurrent-instance pressure, not
a defect in what the tests exercise.

Folding the three into one test that reuses a single client removes two
instances and the flakiness with it: 6 full-suite runs, 0 crashes. It is
also faster, since rust-analyzer only warms up once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The re-sync commit documented a "rust-analyzer's own watcher needs
5-10s" caveat and filed a matching contributing item: build an FS
watcher that relays workspace/didChangeWatchedFiles. Both were wrong,
and the item would have sent the next contributor down a dead end.

Measured with a notify-based watcher actually built and wired up:

- A change to a never-opened file reaches workspace_symbol immediately,
  with and without the watcher. rust-analyzer's own watching is prompt;
  the VFS is current within milliseconds.
- Reference-style queries lag ~5s behind such a change — 5.01s in every
  run, with the watcher (5.01s), without it (5.01s), and no LSP traffic
  at all during the wait. Opening the file explicitly makes it worse
  (10.01s), since that path waits on cache priming.

So the lag is internal to rust-analyzer and not a file-sync problem at
all. The watcher was dropped rather than committed: no measurable
benefit, and it would have doubled this server's use of inotify
instances (128 per user on stock Linux).

The references tool description now warns about the ~5s window
directly, since an agent that edits and immediately queries is exactly
who gets caught by it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up the open-document re-sync fix: tool calls no longer answer
from content rust-analyzer froze when it first opened a file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@zeenix zeenix left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: Apologies this sat unreviewed: notifications for this repository were accidentally disabled, so it wasn’t seen until now.

This special-care pass found several independent merge blockers in cancellation/transport handling, mutating tools, URI/position correctness, and resource bounds. Please do not merge the current head. The safest sequence is to land the smaller prerequisites first (#15 notification handling, #14 workspace startup, corrected #19 URI conversion, corrected #20 LSP lifecycle, a ping-only #9, and #8’s stream abstraction), then rebase this series and drop/port the overlapping implementations. The rebase must preserve all of current main/#23: cross-platform ShutdownSignal, explicit runtime shutdown, CLI parsing, package-derived server version, shared IPC fixes, and shutdown tests.

Given the 9k-line scope, I strongly recommend splitting this into staged PRs—runtime/lifecycle; read-only tools and shaping; multi-workspace/resources; and the security-sensitive move_file/run_runnable tools—rather than landing all 40 commits together. CONTRIBUTING.md also requires consistent package/component prefixes; commit 46b38cb is not atomic, and follow-up fixes/tests should be absorbed into their originating commits rather than appended. Finally, the public API and array-to-object response changes are breaking, not additive, so a 0.2.1 patch release is not appropriate unless compatibility is preserved. GitHub Actions currently shows action_required, so there is no green CI result; after rebasing, please run the full cargo test --all-features, nightly fmt, and clippy checks.

Comment thread src/mcp/server.rs
// tracker still has the LSP ids registered.
let entries = self.workspaces.read().await.list();
for entry in entries {
if let Some(client) = entry.maybe_client().await {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: [blocking] Cancellation is blocked before it reaches handle.abort(). A request cold-starting rust-analyzer holds WorkspaceEntry.client’s write lock across client.start().await; cancelling that request waits here for maybe_client()’s read lock, so cancellation cannot take effect until initialization/reload has already finished, potentially after multiple LSP timeouts. Abort promptly before awaiting workspace locks, or make upstream cancellation forwarding non-blocking, and add a cold-start cancellation test.

Comment thread src/mcp/server.rs
}
};

let mut writer = stdout.lock().await;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: [blocking] This request remains registered in in_flight while writing its response. A concurrent cancellation can abort write_all after only a prefix has reached the pipe, or between the JSON and newline/flush; the next response then appends to an unterminated frame and corrupts stdio for every later request. Remove the request from cancellation tracking before response emission, or enqueue complete frames to a dedicated non-abortable writer. Fatal writer errors must also be sent back to the main loop so cleanup runs.

Comment thread src/lsp/client.rs
let mcp_request_id = CURRENT_MCP_REQUEST_ID.try_with(|s| s.clone()).ok();
let (tx, rx) = oneshot::channel();
self.pending_requests.lock().await.insert(id, tx);
self.pending_requests.insert(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: [blocking] This pending entry has no drop guard. The composite handlers wrap calls in 2-second tokio::time::timeouts (for example handlers.rs:404 and 840–849), so expiry drops send_request before its own timeout reaches the cleanup at line 280. If rust-analyzer never answers, both pending indexes remain forever and no $/cancelRequest is sent. Make registration RAII/cancellation-safe and test that dropping a request future removes both indexes and cancels upstream.

Comment thread src/lsp/connection.rs
let response = json!({
"jsonrpc": "2.0",
"id": id,
"result": null,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: [blocking] workspace/configuration cannot be acknowledged with null: its result must be an array with one entry per params.items. Returning null can make rust-analyzer reject configuration or continue with unusable settings. Dispatch server-initiated requests by method—#20 already contains the item-count implementation—and return -32601 for unsupported methods rather than falsely acknowledging them.

Comment thread src/mcp/server.rs
info!("Received shutdown signal");
*running_clone.lock().await = false;
});
let mut shutdown = std::pin::pin!(tokio::signal::ctrl_c());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: [blocking] This branch predates current main/#23 and replaces its persistent cross-platform ShutdownSignal with one ctrl_c future. Keeping this during rebase would again skip SIGTERM/SIGHUP and Windows console-close cleanup, ignore signal-registration errors, and discard the new shutdown coverage. Please adapt the concurrent loop around current main’s signal abstraction and preserve tests/integration/shutdown.rs.

Comment thread src/mcp/snippets.rs
for line in &lines[from..to] {
// +1 for the implicit '\n' so the cap reflects rendered size
let line_bytes = line.len().saturating_add(1);
if total_bytes.saturating_add(line_bytes) > SNIPPET_MAX_BYTES_PER_HIT && !out.is_empty()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: The !out.is_empty() exception admits an arbitrarily long first line whole, so a 1 MiB line produces a 1 MiB “400-byte-capped” snippet. The implementation also reads and splits the entire file synchronously on an async worker before enforcing the output cap. Bound input reads/cache, move blocking I/O off the Tokio worker, and truncate a long individual line at a UTF-8 boundary.

Comment thread src/mcp/truncate.rs
let items = dedup_workspace_symbols(items);
let total = items.len();
let start = cursor.min(total);
let take = if verbose { total - start } else { limit };

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: verbose=true ignores the already-resolved limit, including the schema’s documented 1,000-item absolute cap, and returns every remaining symbol. The same pattern appears for completion and workspace diagnostics. Always take limit.min(remaining); verbose should change the default limit, not bypass the absolute/explicit one. Add >1,000-item and verbose + explicit limit tests.

Comment thread src/mcp/persistence.rs
.with_context(|| format!("writing tempfile {}", tmp.display()))?;
f.sync_all().ok();
}
fs::rename(&tmp, &target)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: std::fs::rename(tmp, target) does not replace an existing destination on Windows. The first save succeeds, later registry saves fail and are only logged, so restart restores stale workspace roots. Use a cross-platform atomic-replace strategy and test two consecutive saves/replacements on Windows.

Comment thread src/main.rs
.init();

// Get workspace path from command line or use current directory.
let workspace_path = std::env::args()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: Current main now uses args_os with help/version/unknown-option/extra-argument handling and an explicit runtime with shutdown_background plus panic handling. This branch restores args() (including the non-UTF-8 panic and treating flags as workspace paths) and ends with process::exit(0), which skips destructors. Resolve this conflict in favor of current main’s CLI/runtime structure while adapting the new server call.

Comment thread Cargo.toml
[package]
name = "rust-analyzer-mcp"
version = "0.2.0"
version = "0.2.1"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🤖 Chatgpt GPT-5.6 on behalf of Zeeshan: This series changes several public response types from arrays to wrapper objects and changes the public server API, so 0.2.1 is not an appropriate patch bump unless backward compatibility is restored; for a pre-1.0 crate this should be a 0.3.0-style breaking release. Also preserve current main’s env!("CARGO_PKG_VERSION") initialize response—the PR still reports hard-coded 0.1.0 at src/mcp/server.rs:480.

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.

2 participants