Skip to content

FsLangMCP

Unreleased

0.18.0 - 2026-09-07

0.18.0 makes find continuations query/snapshot-bound and isolates project-options reloads from the long-lived host. The 35-tool surface remains, but the find cursor wire contract changes: restart searches without a cursor after upgrading from 0.17.x. This is still a pre-1.0 release, not a contract freeze.

Changed

  • find now issues strict stateless v2 cursors bound to both the canonical request and the complete pre-pagination result, coverage, diagnostic, and bounded-source stream (#259). Request changes return cursor_query_mismatch; source, position-symbol, solution-membership, coverage, or result changes return cursor_stale. Deadline-incomplete initial results are useful but cursorless, while an incomplete continuation returns no sites and permits retrying the same cursor. Legacy offset-only cursors are rejected by find; all other tools retain their legacy minting while rejecting tagged find cursors. Tokens contain only version/tool/offset plus SHA-256 identities, retain no per-cursor server state, and make no atomic-filesystem- snapshot claim. Old find cursors return cursor_version_unsupported; discard them and repeat the original request without cursor. Other tools keep their existing cursor formats.
  • Unchanged find continuations restart as stale when the last or selected declared solution member is removed, while initial missing targets and malformed/mismatched cursors keep their distinct errors. The final error-response size guard checks the existing deadline before and after serialization; expiry of a well-formed continuation returns a bounded, empty, same-cursor-retry response, including when constructing stale/out-of-range/query errors.
  • Cursor validation rejects nonnumeric JSON versions and invalid Unicode without throwing, preserves same-cursor retries through final response serialization, and binds position cursors to the selected overload's semantic identity. Removed position targets restart as stale; unavailable resolution retries without delivering a page. Saturated linked-type alternatives expose their omitted offset. Regression coverage includes full alternating-budget linked-row traversal, stable failure recovery with identical sites, and refreshed diagnostics beyond the response prefix.

Added

  • check accepts snippetPosition="start"|"end" and reports its effective placement. Omitted/null placement stays end, preserving access to the final project source file. The explicit source-order contract keeps project references/compiler options and returns snippet-only diagnostics; it does not claim a reproduced regression in the old default (#196).

Fixed

  • Genuine project-options reloads evaluate ProjInfo/MSBuild in short-lived helper processes, so retained MSBuild node threads do not accumulate in the long-lived MCP host. The versioned bounded data protocol preserves F#/CLR references and evaluated settings; shared callers keep independent budgets, and cancellation drains the helper tree before releasing admission. Runtime status identifies evaluationMode="isolated_helper"; cache hits launch no helper (#150, #224).
  • Definition lookup now resolves compiled backtick-bound names by their source spelling, including unquoted queries, while preserving semantic identity, qualifier boundaries, exact/case-insensitive matching, and ambiguity (#267).
  • Test fixtures retain and drain nested outline workers before deleting their files; hostile regex correctness checks no longer depend on a one-second wall-clock assertion. Flushed lifecycle traces improve evidence for future stalls. This is bounded test hardening, not proof that the historical intermittent testhost hang has been eliminated (#199, PR #278).

Documentation

  • README separates project NuGet audits from libraries bundled inside external runtime tools, explains exact bootstrap pins/downgrades and updates, and records the known dependency risk. Closing #170 documents that risk; this release does not change those external-tool pins or claim that their advisory-listed assemblies have been patched.

0.17.1 - 2026-09-05

0.17.1 is a correctness-hardening patch for project discovery and the find request lifecycle. It keeps the 35-tool surface compatible while making incomplete solution membership, deadlines, response delivery, linked compile items, cancellation, and test cleanup explicit and bounded.

Fixed

  • find and project_health preserve every declared F# solution member, including missing .fsproj paths. Missing members count as requested evidence but never as swept projects; absence remains indeterminate until the declared solution is loadable (#165).
  • find uses one end-to-end request deadline across admission, position resolution, target and project discovery, project-options evaluation, FCS sweep, FSAC fallback, classification, and response construction. Discovery is streamed, source windows are read once per file/page, and retained workers keep bounded admission until their exact task completes. Each shared worker's callers retain independent deadlines; response expiry explicitly requires a new page without a cursor (#255).
  • find caps the complete production-serialized response at 60,000 UTF-16 code units, bounds source context, preserves an in-order site prefix, and exposes exact truncation/cursor ledgers. A blocked row never advances a cursor, and identity-changing recovery restarts without one (#258).
  • Project inspection now preserves evaluated external Compile items only when external linked files are explicitly enabled and the item carries Link metadata; directory and review scans remain workspace-bounded (#256).
  • Pre-cancelled process execution is rejected at the final controlled boundary before launch, avoiding an observable child-process side effect while retaining the documented post-check race boundary (#257).
  • check discovery now observes all live callers of a shared worker, not only the first caller's deadline. It does not start a fallback scan after all callers expire, retains admission until actual completion, and removes only the exact completed worker. Deterministic trusted/fast tests cover both deadline orders, fallback boundaries, busy/retry, and stale cleanup (#249).

Changed

  • Repository SDK selection declares .NET SDK 10.0.100 as the supported floor and uses latestFeature, so contributors may use any compatible stable .NET 10 feature band. CI still verifies the floor independently and pins release/runtime validation to 10.0.400 (#246).
  • CI records bounded VSTest hang diagnostics and uploads TestResults after a stalled coverage run, so intermittent testhost lifecycle failures tracked in #199 produce actionable evidence.
  • The macOS live-runtime job is temporarily pinned to macos-15 after the macos-latest image migration broke the pinned Fantomas daemon. The original smoke checks remain intact; restoring the rolling image is tracked separately in #271 (#269).

Deferred

  • Versioned query/snapshot-bound cursors are designed in ADR-0002 but remain a v0.18.0 runtime change (#259); this patch preserves the existing cursor compatibility contract.

0.17.0 - 2026-08-31

0.17.0 keeps the 35-tool surface intact and hardens the evidence agents use to plan changes: bounded results now say when they are incomplete, timeout budgets include admission waits, and test/outline helpers distinguish what they actually scanned and counted. It also rolls up the post-0.16.0 UX/runtime work from #215 and #232: honest project/workspace coverage, stale global-tool detection, dependency-closure corrections, bounded outline enrichment, exact NuGet metadata, and thread/reload telemetry. Together these changes tighten existing contracts without presenting a partial answer as exhaustive.

Fixed

  • find now separates project-sweep coverage from delivery of the complete site set (#235). coverage.complete can remain true when every project was analyzed, but resolution.complete is true only when the response contains the whole result set from offset zero. A maxResults page and every later cursor page therefore remain visibly incomplete even when the final page has truncated=false; totalEstimate.sites, truncated, and nextCursor describe how to retrieve the omitted sites.
  • FCS/LSP admission waits now observe MCP cancellation instead of starting cancelled handlers later (#236). For find and check, time spent waiting for the FCS gate consumes the same timeoutMs budget and only the remainder reaches the operation. An expired/cancelled queued call never enters protected work. An entered handler keeps its host slot until its returned task completes; an uncancellable project-use sweep additionally retains a bounded actual-worker slot until ParseAndCheckProject / GetAllUsesOfAllSymbols really finishes, so timed-out callers cannot accumulate distinct-key FCS workers in the background. Admission expiry is a typed fcs_admission_timeout payload, and a negative timeout is rejected before queueing instead of becoming an accidental infinite wait. Per-project admission rejection is reported as retryable status="busy" / errorKind="fcs_worker_busy", with a separate projectsBusy count, rather than being folded into a permanent-looking project failure.
  • fcs_tests_for_symbol now reports bounded, coverage-aware solution sweeps (#237). It accepts timeoutMs and cursor pagination, separates requested/scanned/failed/timed-out/busy projects with a perProject ledger, and returns unknown/indeterminate for an incomplete zero. A production .fsproj target reuses the active containing solution for reverse test-project discovery when available; without that context it no longer looks like confident zero coverage and instead returns a precise widening hint.
  • fcs_project_outline now applies filter / nameContains before file pagination (#238). Files with no matching entries are omitted, maxFiles and cursors operate on the matching-file set, and totalEstimate.files counts matching files. If a file outline aborts during a filtered sweep, or if the bounded per-file outline did not return every definition, the response is now partial with a bounded filterCoverage issue ledger and marks the matching count as a lower bound instead of silently treating the file as a non-match. Unfiltered requests keep their existing page-first path.
  • fcs_project_outline now has one admission-aware end-to-end deadline and a general partial- coverage ledger (#243). Optional non-negative timeoutMs defaults to 60 seconds; queue wait, one shared project-options evaluation, and sequential file outlines consume the same budget. Zero is a deterministic typed timeout. Expired/cancelled calls start no later file, and if a hot, non-cancellable FCS/MSBuild task outlives the response, the shared FCS gate remains retained until that actual task settles (including the pre-WaitAsync race and fault path). Responses use ok/partial/unknown with requested/scanned/timed-out/failed/not-started counts plus bounded phase/issues rows. Incomplete filtered discovery suppresses nextCursor and marks its estimate as a lower bound requiring a restart from offset zero.
  • A direct fcs_project_outline call on a test project now includes its evaluated compile sources by default (#239), including ordinary tests/.../Program.fs and Tests.fs files. The shared filter distinguishes test_source from test_result_artifact; TestResults, test-results, coverage, bin, and obj artifacts remain excluded, and explicit includeTests=false still opts out of test sources.
  • fcs_tests_for_symbol now excludes definition sites, stops enclosing-test attribution at the next binding/module/namespace/type boundary, and leaves top-level fixture sites unassigned instead of borrowing the preceding test (#240). Compatibility testCount and siteCount both count all reference sites; additive uniqueTestCount counts distinct enclosing tests. Project identity and the enclosing declaration's source position participate in de-duplication, so one linked test file compiled by two test projects remains two pieces of coverage evidence and separate same-named declarations in one file remain distinct tests. Pagination is by site and preserves the full counts. The enclosing-test scan now advances past rejected test attributes, checks the whole-sweep budget inside its synchronous loops, and uses bounded non-backtracking regexes. An unrelated attribute can no longer trap the tool after FCS analysis has already completed.
  • fcs_refactor_impact now preserves the active solution as a separate discovery context when an explicit production .fsproj is supplied. It propagates test coverage/outcome and pagination metadata, returns status="partial" for incomplete evidence, and labels page-derived file/project counts as lower bounds. crossProject stays unknown until delivery is complete instead of turning an incomplete page or test sweep into a successful false-negative.
  • Shared project-file filtering now uses path-segment containment rather than a raw string prefix (#241), so a sibling such as ProjectSibling/ is no longer mistaken for Project/; filesystem roots remain valid workspaces. Test-source filename detection also avoids false positives such as Contest.fs, Latest.fs, and Protest.fs while preserving conventional FooTests.fs and delimited _test/-test names.
  • The FsMcp-to-official-SDK adapter now forwards structured TransportError JSON byte-for-byte instead of wrapping it in F# %A debug syntax (#242). Typed InvalidArgs, NotReady, FcsAborted, and infrastructure envelopes therefore remain directly machine-readable in isError=true MCP responses.
  • find(kind="members", query="Resolve", member="Resolve") now treats query as matching either the requested member or its declaring type (#219). The documented member-name form previously applied query only to the declaring entity, so four real internal-member calls could produce a 43-second not_found; the declaring-type form remains supported.
  • Long-lived global-tool processes now fail with a typed stale_tool_process envelope before entering ProjInfo when dotnet tool update has replaced their version directory (#220). The payload names the running version, installation directory, missing dependencies, and required parent-process restart, separating stale-process skew from a genuinely incomplete package. Tool handlers are passed as cold thunks, so the guard runs before any handler code starts.
  • fcs_file_outline now surfaces attribute full names in both summary/full entries and emits a compact top-level CustomOperation index (customOperationCount + operation/member/range rows), so computation-expression builders can be inspected without raw source reads (#221). The index obeys both maxResults and the response budget and reports count/budget truncation explicitly. Entries and parse/check diagnostics now participate in the same budget; an exact final serialized-size check caps the complete response at 60,000 characters and reports full counts plus per-array truncation instead of allowing diagnostic-heavy malformed files to escape the cap.
  • check(scope="project") now makes its narrow evidence explicit with downstreamProjectsChecked=false, recommendedScope="workspace", and a coverage note (#222). It does not run another ProjInfo/MSBuild sweep merely to count consumers. A workspace request against one .fsproj is rejected instead of returning a misleading one-project workspace verdict.
  • check now preserves typed blocking causes when a semantic verdict is unknown (#244). An unavailable exact SDK pin remains verdict="unknown" / groundTruth=false, while blockingReason.errorKind="sdk_not_found" carries the requested SDK, visible installed SDKs, selected dotnet host/root evidence, global.json, and remedies without parsing prose. Project checks expose the cause at the top level; workspace checks keep it on the affected project. Timeout, cancellation, worker-busy, and generic project failures use distinct typed causes in trusted and fast modes. Fast file checks retain the SDK blocker even when the owning project is auto-discovered. find now attaches the same SDK evidence to its existing sdk_not_found per-project row, and SDK roots reported by dotnet --list-sdks are preserved.
  • fcs_nuget_members now recovers exact method accessibility from ECMA-335 metadata when available, keeps protected members in the default surface, reports isAbstract, and emits generic constraints both in signature and structured genericParameters (#223). Metadata is read without loading/executing the target assembly; it distinguishes ordinary CLR virtual methods from abstract methods. Ambiguous/unavailable rows fall back to FCS.
  • fsharp_runtime_status now reports project-options load attempts, true stale-cache reloads, validations, and in-flight evaluations. At 128+ OS-visible threads it emits a heuristic warning and parent-process restart recommendation (#150); the payload explicitly does not claim that thread count alone proves a ProjInfo/MSBuild leak.
  • project_health no longer reports tests.status="no_test_projects_found" when reverse test discovery actually FAILED (review finding on #171). Discovery evaluates every candidate .fsproj under the workspace root, and MSBuild evaluation is admitted one project at a time (ProjectEvaluationAdmission, capacity 1) while the host serves concurrent FCS handlers — so a sibling request in flight was enough to make every candidate come back ProjectEvaluationBusyException, and each of those failures was silently dropped into "no test project exists". The payload now separates the two: tests.discoveryComplete is always present, a candidate that could not be evaluated is listed in tests.unevaluatedProjects with its own reason plus tests.unevaluatedProjectCount, and a sweep that found nothing while some candidate failed reports the new status test_discovery_incomplete instead of claiming absence. no_test_projects_found now means what it says: every candidate was read, and none of them referenced this project.
  • set_project's not_warmed/warming readiness hints no longer promise that "find and check are unaffected" by the FSAC symbol index (review finding on #201). check never consults the index, and neither does any find site the FCS sweep produced — but find is precisely the consumer of the index fallback: on a sweep with zero hits it probes workspace/symbol and reports the match as via="fsac-symbol-index". While the index is cold that confirmation is unavailable, so a zero-hit find can read as not_found. The hints now scope the assurance to FCS-derived results, name find's zero-hit fallback as the affected path, and point at the fsacFallbackState / fsacFallbackReason fields find already returns. Docs corrected to match — docs/tools-detailed.md described the fallback and then claimed find never depends on it.
  • An SDK removed while the MCP host stays alive is now noticed at the next set_project (review finding on #200). The SDK-preflight list is cached for the life of the process and the cache is one-sided by design — it can pass a project but never reject one — which made installing the pinned SDK self-healing and uninstalling it invisible: the gate kept passing from a stale list and fsautocomplete died with the opaque connection-loss error the pre-flight exists to replace. set_project now drops the cached list before its check, one dotnet --list-sdks next to the FSAC restart and workspace load it precedes. Lazy per-project starts keep using the cache.
  • find's includeSiteTypes no longer lets one project's answer silently overwrite another's for the same physical site (review finding on #211). Sites are de-duplicated by source location, so a .fs linked into several .fsproj files is swept once per project — and those projects can resolve the same field to different types. The last project visited used to win the row outright. The first resolved type now keeps siteType and the project label that produced it, every other answer is listed in the new per-row siteTypeAlternatives — each entry naming the type and the projects that resolved it, so a contested site can be planned per project — and siteTypes.typedDifferentlyByAnotherProject counts those rows (a subset of typed — the typed + degraded = fieldSites identity is unchanged). The column is bounded so it cannot blow find's response ceiling on a file linked into many projects: at most 3 types per row and 3 projects per type, plus a page-wide 6000-character allowance, with every omission reported as siteTypeAlternativesOmitted / projectsOmitted / siteTypes.alternativesTruncatedRows. Single-project sweeps are byte-identical to before.
  • fcs_nuget_types / fcs_nuget_members no longer answer for a package that is only referenced under a different target framework (review finding on #203). project.assets.json describes every target the project restores while EnsureProjectResults evaluates exactly one, and the map folded all of them — so when an assembly reached the evaluated compile line through a different package, the query returned that unrelated assembly's types under the wrong package id. The assets-derived map is now intersected with the evaluated compile line: an assembly the compile line attributes to another package is dropped from the entry, while an assembly it cannot attribute at all (shared-framework ref pack, ProjectReference output) is kept, so framework unification still resolves. Package ids survive with an empty set, keeping the miss payload's "restored, but contributes no reference here" signal — now also naming the other-target case.

0.16.0 - 2026-08-16

0.16.0 closes three field-reported gaps where a payload's own numbers didn't explain themselves: check's totalDiagnostics now reconciles with errorCount + warningCount + infoCount (workspace-wide and per-project), NuGet package-id-to-assembly resolution no longer returns an empty-but-ok payload when the assembly ships under a different name, and an unsatisfiable SDK pin now returns a typed sdk_not_found error instead of a lost transport connection. set_project's not_warmed readiness state and find's new scopeNote report sweep/readiness coverage honestly — what wasn't observed, and how many projects a sweep actually covered — instead of guessing or staying silent. fcs_public_api and fcs_file_outline now close pages on a response-size budget as well as by count, so a page no longer goes oversized merely because maxResults still had room by count (a single pathological entity can still exceed the budget by design — at least one entity is always returned). find also gains a field-impact mode, classifying record-field sites into five shapes instead of three and annotating each with its current type, so a field-type change can be planned from the tool output alone.

Fixed

  • check's totalDiagnostics now explains itself instead of silently disagreeing with errorCount + warningCount (#190). totalDiagnostics counts the FULL diagnostic set across every severity (including Info/Hidden) — a deliberate #133 fix — but nothing in the payload said so, and agents read a nonzero totalDiagnostics alongside errorCount: 0, warningCount: 0 as hidden findings and burned time hunting for them. Two new fields close the identity: infoCount (so totalDiagnostics = errorCount + warningCount + infoCount, always) and belowSeverityFloorCount (how many of those full-set diagnostics the severity floor excluded from the diagnostics array — never the 50-item cap, which stays diagnosticsTruncated's job), plus a diagnosticsNote pointing at severity="all" when the gap is nonzero. Also fixes fcs_check_file's internal errorCount (an internal FcsBridge member since the 0.11.0 tool consolidation, not a registered tool — no consumer could have observed this), which was silently always 0: it read the JSON severity field as an int, but that field is serialized as text ("Error") — the read never matched.

  • check's scope='workspace' perProject entries now carry infoCount too (#205, deferred from #190), so a member project's own errorCount + warningCount + infoCount identity holds per-project, not just at the workspace total — the same genuine tally (countInfoDiagnostics) used everywhere else, not a total - error - warning remainder.

  • fcs_nuget_types / fcs_nuget_members now resolve a NuGet package id whose assembly is named something else. Both matched the assembly SimpleName alone, so Microsoft.Orleans.Core.Abstractions (which ships Orleans.Core.Abstractions.dll) resolved to zero assemblies and returned an empty — but status: "ok" — payload that reads as "the type does not exist" (#191, reported from the field in #100). packageId now accepts either spelling, resolved through a packageId → assembly map built from the project's own restore output: obj/project.assets.json when present, otherwise derived from the -r: reference paths under the NuGet global-packages cache (honouring NUGET_PACKAGES). Prefix matching remains rejected in both directions — System still does not match System.Text.Json, and Newtonsoft.Json.Schema still does not fall back to Newtonsoft.Json. Side effect: a package that ships several assemblies (e.g. TypeShapeTypeShape.dll + TypeShape.CSharp.dll) now returns all of them in one call instead of requiring one call per assembly.

  • Miss payloads on both tools now explain themselves. When no assembly matches the packageId, the response additively carries hint (which names the package-id-vs-assembly-name distinction, and separates "not in this project's restore graph" from "restored, but no assembly of it is on the compile line" — including analyzer/build-only packages, which are recorded with an empty assembly list rather than omitted) and candidatePackages (up to 5 { packageId, assemblies } entries from the restore graph whose id or assembly names relate to the query). fcs_nuget_members additionally emits a hint when the assembly resolved but exports no such type, naming the assembly searched and pointing at fcs_nuget_types. Success-path responses are unchanged — the fields are absent on a hit.

  • set_project's readiness.symbolIndexState no longer claims warming forever when no warm signal from the FSAC symbol index is ever observed (#194). assessSymbolIndex already compared elapsed-since-workspace-ready against a warm-up window for the internal FSAC workspace/symbol probe find falls back to, but setProjectReadiness never saw that signal. It now reuses the same workspaceReadyAt timestamp and warm-up window: past the window with no non-empty index result observed yet, the state reads not_warmed with a hint that find/check are unaffected (they use FCS sweeps, not the symbol index) — only the symbol-index fallback inside position-based LSP tools may be degraded. The hint reports what was not observed rather than asserting the index failed to warm, since the underlying FSAC probe is only sent as a fallback and a healthy session may never need it. warming still applies within the window; the boolean symbolIndex field and the other states are unchanged.

  • An unsatisfiable global.json SDK pin no longer reduces set_project to an opaque transport error (#192). When the nearest global.json pins a version with rollForward: "disable" and that exact SDK is not installed, both Ionide.ProjInfo's Init.init (in-process) and fsautocomplete (which calls it during its own startup, with nothing to catch the failure) die on the dotnet --version exit-155 the muxer returns. The FSAC child died before answering initialize, so the only thing reaching the agent was StreamJsonRpc's "The JSON-RPC connection with the remote party was lost" — naming neither the SDK nor the file. set_project now screens the pin before any MSBuild evaluation or FSAC process exists and returns { status: "infrastructure_error", errorKind: "sdk_not_found", … } carrying the requested version, the global.json path, the installed SDK list, and both remedies. The same typed envelope replaces the generic "Unable to load F# project options" for every tool that loads project options.

    The screen is deliberately one-sided: only rollForward: "disable" with an exact version confirmed absent from dotnet --list-sdks can fail it. Any other policy, an sdk.paths redirect, an unreadable global.json, or an SDK list we could not obtain proceeds to the normal load path unchanged.

    Behaviour change worth noting: set_project(restartLsp=false) on such a project previously answered status: "ok" with readiness.projectOptions: false. It now answers with the typed error, because the project genuinely cannot be evaluated.

Changed

  • find now reports five distinct record-field site kinds instead of three (#207). kind=field sites are tagged field-set-literal | field-set-update | field-set-mutation | field-pattern | field-read, with matching fieldSetMutation / fieldPattern counters added to breakdown. Each shape needs a different edit when the field's type changes. Behaviour change: x.Field <- v and | { Field = x } -> used to be reported as field-read — the first of those labelled a write as a read. Existing breakdown keys keep their meaning and the new ones are additive, but fieldRead now means only "an expression that reads the field", so a consumer that sums or switches on field kinds will see different numbers. No registered tool couples to the field kinds (fcs_refactor_impact compares only against "definition"), and fcs_record_field_audit's internal form contract (literal | with-update | unknown) is unchanged.

Added

  • find gains a field-impact mode for planning a record-field type change from the tool output alone (#207). The includeSiteTypes half is below; the site reclassification half is a behaviour change, filed under Changed.

    • includeSiteTypes (default false). When true, every record-field site row additionally carries siteType — the field's type as the CURRENT typecheck resolves it at that site, rendered with that site's own opens — so an agent reads the "old type" per row instead of opening each file. A siteTypes ledger (typed + degraded == fieldSites, counted over the whole matched set, not just the page) and a siteTypesNote accompany it. A site whose type FCS cannot produce, or one reached after the timeoutMs budget is exhausted, gets siteType: null and is counted as degraded — a per-site miss never fails the call. Annotates field sites only; a kind that produces none says so in the note rather than going silently no-op. siteType is capped at 200 chars, keeping a full default 80-site page under the MCP payload ceiling without lowering maxResults.
    • Explicit non-goal, stated in the docs and in every response note: siteType is never the type the field would have after the edit. That requires compiling the modified code — edit the enumerated sites, then run check(scope='project') for the verdict. Known limit: FCS exposes no per-use generic arguments for record fields, so a field on a generic record renders as its type parameter ('T), not the instantiation at the site.
  • find now reports a top-level scopeNote on every response that completes a sweep (succeeded, partial, and unknown), naming how many projects were actually swept (projectsSwept) and the recipe to change it: pass the solution's .sln/.slnx as projectPath (or set_project it) with scope='workspace' to widen from one project to every member project, or pass a single .fsproj as projectPath to narrow from a solution sweep to one project. Sweep breadth itself is unchanged — scope=auto/workspace always sweep every member project the resolved target has, scope=file/ project always narrow to one — this is purely a new response field surfacing that outcome so an agent does not have to pay for a whole-workspace sweep to discover which recipe would have been faster. The note never overstates coverage: when a project timed out or failed before analysis completed, it reports "analyzed K of N" instead of claiming a sweep that didn't happen, and for scope='file' it names the additional file-level filter, not just the sibling-project blind spot (#193).

  • fcs_public_api now closes a page on a shared 45,000-character response budget in addition to the existing maxResults type-count cap, so a handful of API-dense types (long member lists) can no longer produce an over-budget page just because maxResults still had "room" by count — the field failure behind this: a 2.5k-line project's default call spilled to a client-side file instead of returning inline. The additive truncatedByBudget: true field distinguishes a budget-close from a count-close; either way, whenever a page closes early with entities left over, an additive hint names namespaceFilter together with 2-3 real namespaces pulled from the remainder, so the next call can narrow instead of re-fetching the same oversized shape. fcs_file_outline shares the same budget: a summaryOnly=false request whose full per-member entries would cross it now downgrades to the header-only shape summaryOnly=true produces, flagged by an additive downgradedToSummary: true plus a hint naming maxResults as the narrowing knob, instead of ever emitting the oversized payload. Both changes are additive-only — a page/outline that already fit the budget is unchanged (#206).

0.15.0 - 2026-08-16

0.14.0 was prepared but never tagged or published to NuGet — the last release on NuGet is 0.13.2. Everything listed under 0.14.0 below therefore reaches consumers for the first time in 0.15.0, together with the changes here.

This is a minor bump rather than a 0.14.0 release because the FsMcp 2.0 migration tightened a wire contract after the 0.14.0 notes were finalized: typed tool input is now strict JSON. Pre-1.0, breaking surface changes ride minor bumps.

Fixed

  • fsharp_project_inspect(includeGeneratedFiles=true) now actually includes generated sources. classifyFile tested the obj/bin segment before the generated-file branches, and generated F# sources (*.AssemblyInfo.fs, the SDK's *.AssemblyAttributes.fs stub) essentially always live under obj/, so the flag was unreachable by construction (#186). Files are now classified by what they are before where they live, *.AssemblyAttributes.fs is recognized as generated, and when excluded by default these files report assembly_info_file instead of the uninformative obj_or_bin_directory.
  • check(scope="snippet") diagnostics now describe the snippet, not the harness (#187): the wrapper artifacts FS0222 (must begin with namespace/module) and FS0225 (source-file bookkeeping) are dropped — so a valid bare let snippet is clean, not errors — duplicates arriving via both the parse and check halves are collapsed, diagnostics attached to other project files no longer leak in, and file reads "snippet" instead of a temp path the caller never created.

Changed

  • Dependency sweep: Nerdbank.MessagePack 1.2.30 → 1.2.36, Microsoft.NET.Test.Sdk 17.12.0 → 18.9.0, xunit.runner.visualstudio 3.0.2 → 4.0.0, and the pinned fsharp-analyzers runtime tool 0.36.0 → 0.37.2. Microsoft.VisualStudio.Threading.Only deliberately stays at 17.14.15: StreamJsonRpc 2.25.29 (latest) binds that exact assembly version, and moving it reintroduces the #121 startup FileNotFoundException.

  • fcs_analyzer_setup_preview snippets no longer recommend analyzer versions older than the ones this project pins. The grouped analyzer bump moved G-Research.FSharp.Analyzers to 0.23 and Ionide.Analyzers to 0.16 in the project files but left the emitted paste-ready snippets on 0.22/0.15.

  • FsMcp.Server / FsMcp.TaskApi 1.2.2 → 2.0.0. Handlers are now cancellation-aware end to end: the MCP SDK's per-request token is forwarded through the stdio adapter into every tool handler, so client-side request cancellation reaches the tool body instead of being dropped at the boundary.

  • Typed tool input is strict JSON (FsMcp 2.0): string-encoded scalars such as "42" or "true" are no longer coerced into numeric or boolean fields and fail with a structured invalid-arguments error. Agents sending properly typed arguments are unaffected.

[0.14.0] - 2026-08-14

This is a pre-1.0 SemVer minor because it deliberately tightens several response contracts that callers may have treated as unconditional success.

Added

  • FSAC-backed responses carry the active project context and live session generation. Cached diagnostics also report expected, received, missing, and stale files, making incomplete evidence explicit.
  • Fast diagnostics bind each FSAC generation to canonical file identities, content hashes, and the evaluated project/options/reference closure. This supports FSAC's versionless publications without trusting receipt time and makes same-mtime source, reference, and MSBuild-option changes fail closed.
  • fslangmcp --project <path> (and FSA_PROJECT_PATH) runs the same project-load/readiness pipeline as set_project before MCP starts accepting requests. fslangmcp --version reports the packaged product version.
  • A pinned runtime manifest and Linux/macOS/Windows live matrix exercise the supported fsautocomplete, ProjInfo, and Fantomas versions through real diagnostics, rename, formatting, process-crash, and restart flows.

Changed

  • Migration: find.resolution.matched is now nullable. It is false only after every requested FCS project completed; an incomplete sweep with no hit is status="unknown", outcome="indeterminate", and matched=null. Positive partial results remain actionable and advertise incomplete coverage.

  • Migration: check(speed="fast") returns clean only when the active FSAC generation has a current diagnostics publication for every evaluated in-scope source file. Missing, stale, context-mismatched, or empty-glob coverage returns unknown; current errors remain visible even when the rest is incomplete.

  • Migration: a cross-project set_project(restartLsp=false) leaves the existing context untouched and returns status="restart_required"; retry with restartLsp=true to switch.

  • project_health and project inspection use the evaluated MSBuild/ProjInfo model used by semantic analysis, including imports, resolved compile items, references, output paths, and configuration. LSP readiness is context-bound.

  • Directory targets discover nested workspaces without descending into ignored build/dependency or reparse-point trees. Evaluated linked <Compile> files outside the workspace root remain valid members of the selected context.

  • Reproducible restores (#167): every direct PackageReference now uses a true exact NuGet range ([x.y.z]; bare x.y.z was only a minimum), both lock files record equal lower/upper bounds, and CI/Justfile restore with --locked-mode. Dependency updates arrive as explicit Dependabot PRs; after intentionally editing a pin, just restore-update regenerates the locks without selecting a newer version on its own.

  • The build SDK is pinned to 10.0.400 with roll-forward disabled. External runtime tools are pinned to fsautocomplete 0.83.0, Ionide.ProjInfo.Tool 0.74.2, and Fantomas 7.0.5; --bootstrap-tools installs or downgrades to those exact embedded-manifest versions.

  • External-tool security note: the FsLangMCP package graph itself has no known NuGet advisories, but the latest available fsautocomplete and Fantomas packages still bundle MessagePack versions covered by GHSA-hv8m-jj95-wg3x and GHSA-vh6j-jc39-fggf. FsLangMCP's supervised LSP/formatting paths explicitly use JSON rather than a MessagePack formatter, and no patched upstream tool release exists as of this release. The pins remain exact while upstream remediation is tracked; this is a documented risk acceptance, not a claim that every assembly bundled by an external tool is advisory-free.

  • The autonomous orchestrator workflow is now a manually dispatched, read-only analysis job. It no longer accepts public issue text as an agent prompt, has no repository-write token, does not persist checkout credentials, requires an explicitly reviewed exact Claude Code version, excludes project hooks, skills, and MCP servers, exposes only read tools, and emits only an Actions artifact for human review.

Fixed

  • find no longer converts failed/timed-out project sweeps or unavailable FSAC state into authoritative absence, and its workspace-symbol fallback cannot leak a hit from another file, project, or active workspace.

  • find rejects unsupported enum values, invalid ranges, unsafe scope/path combinations, and non-advancing pagination sizes with structured invalid_args responses instead of silently broadening the search or looping.

  • Dead RPC/process pairs are reaped and transparently restarted; old-generation diagnostics cannot contaminate a replacement session, failed handshakes clear every diagnostic store, and dead generations are never advertised as ready.

  • Rename preview distinguishes genuine no_symbol from timeout, disconnect, missing executable, malformed protocol data, and other infrastructure failures. Formatting and diagnostic-fix calls are project-bound, and fixes require the exact open-document diagnostics version instead of wall-clock freshness.

  • Relative check.fileGlob patterns are evaluated from the selected workspace; a glob matching no evaluated source file cannot produce a false clean result.

  • Closed-file, unsaved-buffer, and Windows-URI diagnostics share one generation-safe freshness model. Disk or evaluated-context changes retire the old generation and return not_ready until the rebased FSAC publication is available; they can no longer reuse an old empty payload as clean.

  • Analysis caches use ordered, content-addressed source/options/reference/project dependency keys. Same-timestamp edits, compile-order changes, imported MSBuild changes, linked source changes, and referenced-project edits invalidate stale results.

  • Subprocess deadlines cover process exit and both redirected stream drains; output remains bounded while drained, and timeout cleanup terminates the OS process tree rather than leaving descendants holding inherited pipes.

  • Project evaluation and readiness probes now share a bounded ProjInfo deadline, and check(timeoutMs=...) applies one overall project/workspace budget to discovery, evaluated-option validation, reference probes, fingerprints, and FCS/FSAC work. Blocking operations use exact-key single-flight plus bounded, no-queue admission held until the real worker settles, so timed-out callers cannot accumulate abandoned MSBuild, filesystem, or compiler work.

  • Unix hosts without an external setsid use an internal session wrapper. Timeout cleanup waits, within the existing cleanup budget, until the process group has no live descendants; Linux zombie/exited states are not mistaken for surviving work.

  • The FSAC client handles window message requests/notifications used by real formatting flows instead of failing with RemoteMethodNotFoundException.

  • MCP initialize.serverInfo.version now preserves the exact product SemVer (0.14.0, not the four-component 0.14.0.0 normalized by FsMcp 1.2.2).

  • Release builds now derive and validate the version before compilation, pass that same version through build, test, and pack, install-smoke the resulting local tool package, and publish the single verified package (plus its SHA-256) to both GitHub Releases and NuGet without rebuilding or silently skipping a duplicate package. This prevents a tag-only release from packaging an assembly that still reports the previous project-file version.

  • MSBuild-style backslash separators in <Compile Include> paths are now normalized to the host OS before any filesystem access, matching MSBuild semantics. Previously on macOS/Linux a Windows-authored Domain\Money.fs resolved to a literal /workspace/Domain\Money.fs, so fcs_project_outline errored on every subdirectory file, fcs_review_scan silently scanned only root-level files, and project_health reported subdirectory files as missing (#160, reported by @PlandriousX). <ProjectReference Include> paths reach fsharp_project_inspect through MSBuild evaluation and are normalized by the engine itself; a regression test pins that behaviour.

  • fcs_review_scan project mode no longer claims succeeded over a partial file set: in-scope compiled files that cannot be found on disk are listed in a new unresolvedFiles response field and downgrade status to partial. Filter-excluded entries (generated, obj/bin, tests) are deliberately not counted — their absence is expected pre-build in parse-only mode; the unfiltered view remains project_health.missingFiles (#160).

  • fcs_project_outline reports the same aggregate unresolvedFiles list over all in-scope files, so missing files stay visible even when the per-file outlineStatus errors are on another page (#160).

0.13.2 - 2026-08-10

Added

  • fsharp_runtime_status now reports OS-visible process threads plus .NET thread-pool, pending-work, and completed-work counters under process.threads, making long-session growth measurable without attaching a profiler.
  • set_project.readiness now explains an unwarmed symbol index through additive symbolIndexState and symbolIndexHint fields. lspRestartRequested records intent, while lspReplacedExistingProcess reports whether an existing FSAC process was actually replaced. The legacy lspRestarted field keeps its request-mirroring behavior for patch-release compatibility.
  • project_health reports the configuration associated with its selected build artifact.

Fixed

  • MCP stdio startup disables Generic Host configuration reload only while constructing the server, preventing pre-stdin ChangeToken.OnChange recursion when a restricted file provider returns an already-changed watch token. The caller's environment is restored before FSAC child processes can inherit it.
  • Repeated set_project(restartLsp=true) now retains and drains the retired FSAC JSON-RPC generation before starting another one. Unchanged projects also reuse bounded, input-stamped Ionide/MSBuild options instead of evaluating the project again on every call; this removes the observed linear thread growth from #150. Project, import, restore, generated-props/targets, and source-directory changes invalidate the entry. The underlying MSBuild in-process-node lifecycle remains an upstream Ionide/MSBuild limitation on genuine cache misses.
  • EnsureProjectResults cache entries now include source stamps. Public API and referenced-assembly tools therefore see both edits to known files and additions/removals in the <Compile> graph (#148).
  • check(scope="file") performs one fresh parse/type-check after invalidation instead of computing and discarding an initial result. Per-response source-line reads are memoized, and test-discovery regexes are compiled once (#148).
  • Source-tool validation rejects project/solution files with a structured InvalidArgument while preserving .fs, .fsi, unsaved-buffer, and .fsx script-inference flows.
  • project_health uses the active solution root for sibling test discovery, searches MSBuild/analyzer config files through ancestors, reads package metadata from attributes or child elements, and no longer treats a bare config file as an analyzer signal (#100).
  • fcs_public_api preserves generic arguments in readable signatures (for example string option, string list, and user-defined Box<string>), so incompatible generic surfaces no longer compare as identical (#100).

Changed

  • fcs_dead_code moves the repeated removal warning to one response-level verificationHint; the existing candidate note string remains for compatibility as a compact pointer to that hint.
  • Agent integration and tool-reference docs now match the shipped 35-tool surface and explicitly route removed aliases through find and check (#100).

0.13.1 - 2026-08-02

Fixed

  • Windows startup crash inherited from the bundled FsMcp. 0.13.0 shipped with FsMcp 1.2.1, whose stderr sink opened Unix file descriptor 2 directly. On Windows that throws The handle is invalid while the logger provider is constructed, so the server failed at startup. As a DotnetTool package the FsMcp assemblies are bundled rather than resolved at install time, so the only remedy is a rebuild. 0.13.1 bundles FsMcp 1.2.2, which selects the sink per platform.

    Windows users on 0.13.0 must upgrade. macOS and Linux are unaffected.

0.13.0 - 2026-08-02

Fixed

  • BoundedCache.TryRemove now removes its FIFO node as well as the dictionary entry. Repeated fresh checks can no longer grow the eviction queue without bound, exceed cache capacity, or evict a newly reinserted value through a stale queue entry.

    Impact. Before this fix the cache was not bounded at all. TryRemove dropped the dictionary entry but left the key in the eviction queue, and eviction only fires on dict.Count >= maxSize while removing a single queue entry per insert. Landing on a stale key freed no slot yet still admitted the new entry, so every invalidation permanently raised the effective ceiling by one. Applied to projectResultsCache — declared capacity 3, values of type FSharpCheckProjectResults, i.e. a whole project's type-check state — a long editing session accumulated tens of gigabytes. Observed in the field: a server reaching 34.7 GB RSS after 14 hours, which on a 32 GB machine drove the OS into swap thrash (84 swapfiles, VM compressor at 100% of its segment limit) until the kernel watchdog panicked the host. Reproduced against the released 0.12.3 implementation: a cache declared with capacity 10 held 20 entries after 10 invalidations; with this fix it holds 10.

  • All FSAC handshakes and live LSP calls now have bounded, configurable timeouts. A timeout cancels the StreamJsonRpc request, kills/resets the current FSAC child, and releases the serialized LSP slot.

  • LSP lifecycle, document synchronization, and invocation are serialized inside FsAutoCompleteBridge; set_project can no longer dispose an RPC while find, refactor orchestration, or a raw LSP tool is using it. Concurrent project switches are serialized through a separate lifecycle gate.

  • FSAC restart now clears diagnostics freshness timestamps together with diagnostics payloads, preventing an empty new workspace from reporting an old analyzedAt / mostRecentAnalyzedAt.

  • Bootstrap, proj-info, and analyzer subprocesses now share a kill-on-timeout runner that drains stdout and stderr concurrently. Chatty or wedged children no longer deadlock a tool slot or survive timeout cleanup; analyzer report files are removed on every exit path.

  • Removed the untracked fire-and-forget solution prewarm started by set_project; repeated project switches no longer accumulate stale FCS jobs competing for the global FCS gate.

  • find now enforces its timeout with Task.WaitAsync around project-option loading and the complete type-check + synchronous symbol-use walk. Since FCS cannot preempt work already in progress, one in-flight computation is retained per project/cache key and reused by retries instead of spawning additional abandoned workers; cache-generation checks prevent late completion from repopulating caches after set_project.

Changed

  • LSP concurrency is fixed at one. FSLANGMCP_MAX_CONCURRENT_LSP is no longer honored because FSAC exposes one mutable workspace; FCS concurrency remains configurable with FSLANGMCP_MAX_CONCURRENT_FCS.
  • Added timeout settings: FSLANGMCP_LSP_STARTUP_TIMEOUT_MS, FSLANGMCP_LSP_REQUEST_TIMEOUT_MS, FSLANGMCP_PROJ_INFO_TIMEOUT_MS, and FSLANGMCP_BOOTSTRAP_TIMEOUT_MS.
  • Requires FsMcp 1.2.x (was 1.1.x). FsMcp 1.2.0 fixes a stdio deadlock: hosts that never read the server's stderr filled the 64 KB pipe, and because the built-in console logger wrote through ConsolePal's process-wide monitor — shared with stdout — the parked write blocked the next response. The server went silent while still holding live requests. Affects any host that discards stderr.

0.12.3 - 2026-07-05

Follow-up patch addressing two P2 findings from Codex's review of the 0.12.2 correctness patch — both on the 0.12.2 fixes themselves.

Fixed

  • find timeout now covers the whole sweep, including the symbol-use walk. In 0.12.2 the per-project cancellation token bounded only ParseAndCheckProject; the synchronous GetAllUsesOfAllSymbols() re-walk ran afterwards outside the token, so a type-check that finished near the deadline could let the use-walk overrun the advertised timeoutMs with no per-project timeout entry. Both steps now run inside one token-scoped async, so the walk won't start once the deadline has passed.
  • Analyzer detection now honors analyzer refs centralized in Directory.Build.props. The 0.12.2 fix based Configured on analyzer PackageReferences scanned from the .fsproj only. Projects that centralize analyzer packages in an MSBuild import (Directory.Build.props/.targets, or Directory.Packages.props via GlobalPackageReference) were wrongly reported as un-configured, so fcs_analyzer_diagnostics skipped the CLI and project_health reported no analyzers. Detection now also scans those XML imports for analyzer package refs. A bare .editorconfig (non-XML, no analyzer package) still correctly does NOT count.

0.12.2 - 2026-07-05

Correctness patch from a whole-codebase multi-agent review (20 verified findings) plus the #100 hang report. All fixes are backward-compatible (one new optional find arg, one additive response field).

Fixed

  • find no longer hangs indefinitely on a large/cold multi-project solution (#100). A cold ParseAndCheckProject on a 12-project solution could block an agent for 30+ minutes with no error and no way to regain control. The per-project sweep now runs under an overall wall-clock budget (find arg timeoutMs, default 120 s): each project is cancelled at the remaining budget and surfaces as a perProject timeout entry, so the call always returns a partial result with an actionable reason instead of blocking. Verified on the reporter's 12-project Sailer.slnx (was 300 s+ no-return → now returns in ~66 s with the un-warmed projects reported as timed out). The other sweep tools (fcs_tests_for_symbol, fcs_check_compile_order, fcs_dead_code) get the same bound.
  • fcs_analyzer_diagnostics no longer deadlocks on a chatty analyzer run. runAnalyzers redirected the fsharp-analyzers child's stdout but never drained it while reading stderr to end under an untimed wait — once the CLI's stdout exceeded the ~64 KB OS pipe buffer the child blocked, wedging the tool (and permanently holding an FCS concurrency slot). Both pipes are now drained concurrently under a 120 s timeout with kill-on-timeout.
  • FSAC (fsautocomplete) child process no longer leaks on a failed set_project handshake. If the initialize / fsharp/workspaceLoad RPC threw, the already-started process was abandoned un-killed (its handle was only committed after the handshake). The handshake is now wrapped so a failure kills/disposes the child before re-raising.
  • fcs_public_api, fcs_suggest_open, fcs_referenced_symbols, fcs_nuget_types, fcs_nuget_members now work in solution mode. After set_project on a .sln/.slnx, a bare call defaulted projectPath to the solution file, which WorkspaceLoader cannot load — the tools threw. They now resolve a solution to its single .fsproj (or return a clear "pass one explicitly" error when ambiguous), matching the sweep tools.
  • fcs_dead_code / fcs_tests_for_symbol no longer over-report projectsScanned. A project that failed to load was silently swallowed, yet the response claimed all projects were scanned — an agent could read "N scanned, 0 dead" when 0 were actually analyzed. projectsScanned now counts only successful sweeps; a new projectsRequested field shows the total, so a gap is visible.
  • find with includePerProject=false still surfaces per-project sweep failures. The 0.12.1 opt-out omitted the whole perProject array — including the error entries that are the only signal a project failed to sweep — so a failed project could be misread as "zero uses". Error entries are now always surfaced; only zero-match noise is trimmed.
  • fcs_analyzer_diagnostics no longer runs the analyzer CLI on projects without analyzers, and no longer reports run_failed spuriously. It ran the CLI on every project whose .fsproj merely parsed; an analyzer-less project's run error could flip a genuinely-clean configured project to run_failed. The CLI now runs only on projects with a real analyzer package reference.
  • set_project on a non-existent path returns the {status:"invalid_args"} envelope instead of throwing a raw ArgumentException (matching every other validation in the handler).
  • project_health in solution mode no longer silently ignores a requested compileCheck. It returned status:"ok" while never running the check; it now reports compileStatus as not-supported-in-solution-mode with a hint to re-target a single .fsproj.
  • project_health / analyzer detection no longer reports "analyzers configured" for a bare .editorconfig. Configured now requires a real analyzer PackageReference rather than the mere existence of a Directory.Build.* / .editorconfig file (which nearly every project has).

0.12.1 - 2026-06-29

Patch release — three fixes from a 0.12.0-binary reproducibility pass over the #100 UX backlog (most prior findings turned out to be stale-version artifacts; these three reproduced).

Fixed

  • find now resolves a module-qualified (dotted) query (#100). A query like Roles.appRole for App.Roles.appRole previously matched nothing and returned a silent matched:false — which reads like "symbol doesn't exist" and can mislead a dead-code/refactor decision. symbolMatches now also accepts a dotted suffix on a . boundary (gated on the query containing a ., so bare-name matching is byte-for-byte unchanged). A genuine dotted miss now also carries a hint pointing at the bare identifier.
  • project_health understands solution mode (#100). Pointed at a .slnx/.sln/directory with multiple projects it previously returned overall:"blocked" ("Multiple .fsproj found"), which read as an error. It now returns reportKind:"solution" with toolingReadiness.overall:"solution" and a solution summary (projectCount + per-project {name, fsproj, exists}) plus a hint to pass one .fsproj for full readiness. Single-project inputs are unchanged.

Changed

  • find trims per-project noise (#100). The perProject breakdown now omits projects that matched nothing and didn't error (on a large solution that was one noise line per project, dwarfing a small result); projectsSwept still reports the full sweep breadth. New includePerProject arg (default true) drops the array entirely when set to false for repeated sweeps. Additive — no field renamed or removed.

0.12.0 - 2026-06-29

Added

  • fcs_explain_diagnostic (#61) — explain an F# compiler diagnostic in plain language with repair context (title, explanation, likelyCauses, repairHints, relatedTools). Pass an FS code / errorNumber or a file position to auto-fetch via FCS; curated map of ~24 common diagnostics; enriches hints from the raw message. Pairs with check.
  • fcs_tests_for_symbol (#60) — list the tests that likely cover a symbol: sweeps the solution's test projects (<IsTestProject> / xunit·nunit·expecto refs), filters FCS symbol-uses to test files, and tags each site with its enclosing test. Complements find (the test-coverage slice).
  • fcs_rename_preview (#64) — preview a semantic rename's full impact (edits grouped by file with original + post-rename lines, totalEdits, fileCount, crossProject) without applying it. Non-destructive; inspect blast radius before textDocument_rename.
  • fcs_diagnostic_fixes (#53) — agent-friendly diagnostics→fixes for a file: fetches the file's diagnostics and requests code-actions with the real diagnostic context that raw textDocument_codeAction leaves empty, grouped per diagnostic.
  • fcs_check_compile_order (#58) — detect F#'s file-ordering gotcha: a symbol used before the file that defines it in <Compile> order reads as FS0039 "not defined" though it exists. Correlates each FS0039 with a later-compiling definition and reports the reorder fix. Use when check reports "not defined" to tell an order problem from a missing open.
  • fcs_public_api (#59) — emit a project's public API surface (every public type + member with signatures, stable-sorted) for snapshot/diff breaking-change detection. Public-only by default (includeInternal adds internals); namespaceFilter + pagination.
  • fcs_refactor_impact (#71) — preview a change's blast radius + a verify checklist without editing: orchestrates find (cross-project use sites), fcs_tests_for_symbol, fcs_check_compile_order and fcs_public_api into { target, impact, tests, compileOrder?, apiSurface?, verify[] }. kind=rename|signature|move|delete|auto. Use before a rename/move/delete; pair with fcs_rename_preview for the exact edits.
  • fcs_signature_status (#74) — report the .fsi-vs-impl public-surface gap for one .fs without editing: type-checks the impl with its sibling .fsi stripped, then diffs — members public in the impl but missing from the .fsimissingFromSig; .fsi entries with no impl match → staleInSig, each with a signaturePreview. Use for .fsi drift; prefer fcs_public_api for the whole public surface.
  • fcs_review_scan (#65) — scan F# source for review candidates from the untyped AST (interesting spots to eyeball, not a linter and never bugs): match_wildcard, try_with, raise_or_failwith, mutable_binding, blocking_call, cast_or_box, reflection, large_function. One file (path) or whole project; parse-only, writes nothing; each candidate carries range, lineText, a neutral note + counts.byCategory.
  • fcs_dead_code (#70) — list likely-unused F# symbols as conservative cleanup candidates (not deletions; verify each with find before removing). Sweeps the project (GetAllUsesOfAllSymbols) and flags private/internal value & function bindings whose only use is their own definition. Public excluded by default; skips compiler-generated, [<EntryPoint>], overrides/interface impls, ctors. Always emits caveats.
  • fcs_create_file_plan (#66) — plan where a new .fs file belongs without creating it: loads the resolved <Compile> order, recommends an insertion index, infers the namespace/module convention from neighbours, and emits the exact <Compile Include=…> edit plus a dependency note. Use before adding an .fs file; pair with fcs_check_compile_order after.
  • fcs_analyzer_diagnostics (#72) — report F# analyzer diagnostics (not compiler diagnostics), grouped: analyzersConfigured, analyzerPackages, diagnostics [{analyzer, code, severity, message, file, range}], counts {byAnalyzer, bySeverity}. Detects analyzer config like project_health, runs the fsharp-analyzers CLI when available and parses its SARIF; none configured → no_analyzers. severity filters; projectPath falls back to set_project.
  • fcs_analyzer_setup_preview (#75) — plan what to add to enable F# analyzers without applying it — read-only, writes nothing. Reads the .fsproj + Directory.Build.props/.targets + dotnet-tools.json, diffs current wiring against the required set (analyzer package refs + GeneratePathProperty, FSharp.Analyzers.Build, the FSharpAnalyzersOtherFlags property, a local manifest), and emits each gap as an exact XML/JSON snippet + reason. Pair with fcs_analyzer_diagnostics to read diagnostics after.

Fixed

  • FSAC initialize handshake now declares codeActionLiteralSupport / publishDiagnostics / synchronization client capabilities — without them FSAC returned null code-actions and pushed zero diagnostics, which also silently degraded the existing textDocument_codeAction and workspace_diagnostics tools. (#53)
  • check (project/workspace) no longer returns a wall of spurious FS0039 "not defined" cascades on an unrestored/unbuilt project (#100, #138). A deterministic reference-resolution probe (how many -r: reference assemblies exist on disk) detects the unrestored state and returns verdict: "unknown" + a "run dotnet restore/build" reason instead. Independently, the surfaced diagnostics array is now always capped (≤50 + diagnosticsTruncated) so a large erroring project can't overflow the MCP token ceiling. The restored/normal clean+errors verdict path is unchanged.
  • fcs_file_outline gained summaryOnly (default true) — headers + per-kind member counts only — so a large file no longer overflows the token ceiling and pushes agents back to grep. summaryOnly=false restores full per-member output. (#100)
  • set_project / project_health now surface restoreStatus: "unrestored" + a hint when reference assemblies are missing — so an empty symbolIndex after ready is no longer mistaken for "no symbols" / cryptic FSharp.Core not found. (#138)
  • textDocument_codeAction request-shape is now regression-guarded — the range.start/range.end JSON nodes are built as distinct instances (guarding the #43 InvalidOperationException "node already has a parent") and the context.diagnostics round-trip from #53 is asserted. The request builder was extracted into an internal CodeActionRequest.buildParams seam to make it directly testable without a live FSAC. (#99)

Changed

  • Dropped the redundant file from nested range objects in site-list responsesfind, fcs_project_symbol_uses, fcs_find_member_usages, fcs_record_field_audit, and diagnostics each already carry a top-level file, so range.file duplicated it (a hot find = 75 sites × one redundant absolute path each). Added rangeToJsonNoFile and applied it only where the enclosing object already has file. Standalone ranges that are the sole carrier of the path — declaration locations (declarationLocation, declarationRange) and the symbol-at-position map — keep file unchanged. Response-shape trim only (consumers read the parent file); ships in 0.12.0. (#139)

0.11.0 - 2026-06-28

Removed

  • BREAKING — the agent-facing MCP tool surface shrank from 36 → 22 tools: 14 thin single-tool aliases over the consolidated find / check were removed (#136). Since 0.10.0 these were already steered toward find / check via Prefer … description hints; #136 completes the consolidation by dropping the registrations. The backend FcsBridge / FsAutoCompleteBridge methods (FindSymbol, References, WorkspaceSymbol, CompileProject, ParseAndCheckFile, ValidateSnippet, etc.) are unchanged — only the agent-facing tool registrations were removed. Removed tools, grouped:

    • find cluster (7): textDocument_definition, textDocument_references, workspace_symbol, fcs_find_symbol, fcs_project_symbol_uses, fcs_find_member_usages, fcs_record_field_audit
    • check cluster (5): workspace_diagnostics, fsharp_compile, fcs_parse_and_check_file, fcs_check_file, fcs_validate_snippet
    • low-level position/symbol (2): fcs_file_symbols, fcs_type_at_position

    Every capability remains reachable: cross-project and single-project symbol / reference / record-field-set / member-usage search via find (with kind + scope narrowing); type-check verdicts for file / project / workspace / inline snippet via check; raw per-file symbol extraction via fcs_file_outline; and position- or unsaved-buffer symbol / type lookups via fcs_symbol_at_word.

Changed

  • [FCS in-process] / [FSAC] / [meta] description prefixes were stripped from all 22 remaining tool descriptions — each now leads with its action verb. The implementation-layer tag leaked an internal backend distinction into the most valuable routing position (the first token the agent reads) without helping the call/skip decision. The ToolDescriptionSchemaTests (dotnet test gate) and scripts/audit-tool-descriptions.py schema rule were inverted accordingly: from "every description must start with a recognized tag" to "no description may start with a [-bracket tag", and the overlap-pair set was pruned to the three pairs whose members both still exist (fcs_referenced_symbolsfcs_nuget_types, fcs_nuget_typesfcs_nuget_members, fcs_signature_helpfsharp_signature_data). docs/tool-description-schema.md documents the new rule.
  • find's description was rewritten to advertise member call-site search and to drop now-stale references. A tool-selection A/B (120 agents — the pre-consolidation 34-tool surface vs this 22-tool surface) caught a single regression from the removals: with the dedicated fcs_find_member_usages gone, agents fell back to rg for "find call sites of a .Member() on a type" in 5/5 trials instead of reaching for find — its old text under-sold the member-usage intent and still named the deleted fcs_find_symbol / fcs_record_field_audit. The rewrite leads with the four intents (definitions, references, record-field set sites, member call-sites on a type x.Foo) and contrasts against textual rg. A re-test dropped member-usage fallback from 100% → 0% (36/36 trials chose find), no regression on the other find tasks.

Notes

  • Accepted limitations of the consolidated path (carried over from the 0.10.0 find / check design): find / check do not accept text (unsaved buffer content) or raw projectOptions / workspacePath overrides — Write the file to disk first, then call find / check. Position-based and unsaved-buffer symbol / type lookups remain available through fcs_symbol_at_word.

0.10.1 - 2026-06-28

Fixed

  • find's multi-project sweep now caches each project's symbol-use enumeration, so a warm find is dramatically faster (#131). FCS keeps ParseAndCheckProject warm via its project cache, but GetAllUsesOfAllSymbols() is NOT memoized — it re-walks every recorded symbol use (~16–18k per project on this repo) on EVERY find, so a warm find cost the same as a cold one (the projectCacheSize bump from 0.10.0 could not help: the cost was use-enumeration, not the type-check). The sweep now memoizes (all-uses, diagnostics) per project, keyed by the resolved-options cache key plus a source-file write-time stamp. Measured over stdio on this 2-project solution (find("FindArgs"), 69 sites): cold sweepElapsedMs 11307 → warm 82/78 (≈ 138× faster, warm sub-100 ms). Correctness is preserved unconditionally: the stamp changes the instant any swept source file's on-disk mtime moves, so an edit is a cache MISS that runs the identical original ParseAndCheckProject + GetAllUsesOfAllSymbols path — a cached find is never staler than an uncached one — and set_project's ClearCaches() drops the memo on a project switch. The cache is query-independent (it holds the raw enumeration), so different queries on an unchanged project all reuse it. +2 tests (warm-reuse + on-disk-edit invalidation).
  • The find sweep cache key now also includes the referenced-assembly mtimes, so a dependency rebuild invalidates dependent projects — no stale cross-project results (Codex P1 review of #134). The #131 cache key stamped only each project's OWN SourceFiles mtimes. For a project A that references project B (P2P) or any other assembly, A's key therefore moved only when A's own source changed: rebuilding B (its output DLL changes) while A's source was untouched left A's key unchanged, so the sweep served A its stale cached (all-uses, diagnostics) even though a fresh ParseAndCheckProject would see B's new metadata — workspace find kept reporting stale cross-project usages until A's file was edited or ClearCaches() ran. The key now folds in a referenced-assembly write-time stamp (the -r:/--reference: targets in OtherOptions, each existing file's LastWriteTimeUtc.Ticks, missing files mapped to -1, never throwing): when B's output (the consumer's obj/.../ref/B.dll reference assembly) is rebuilt, its mtime moves → the consumer's key changes → cache MISS → fresh sweep. Every reference is stamped (framework/NuGet refs included) — correctness-first, since a path-based "immutable ref" skip risks misclassifying a mutable ref, and the extra stats are OS-metadata-cached and sub-millisecond, so the warm-cache win still holds for the no-change case. +1 cross-project invalidation test (the existing #131 test only edited a file in the SAME project, which is why the P1 slipped through).
  • The find sweep cache key now also includes the source-file mtimes of directly and transitively referenced F# projects, so a dependency SOURCE edit (without a rebuild) also invalidates dependent projects (Codex P2 review of #134). P1 closed the DLL-rebuild staleness gap; P2 closes the complementary scenario: a developer edits a dependency's .fs file on disk without immediately rebuilding its output assembly. FCS's ParseAndCheckProject for a consumer reads referenced F# project sources directly via ReferencedProjects[FSharpReference].SourceFiles — so a fresh consumer check WOULD see the edited source, but the P1 DLL mtime hasn't moved → the P1 stamp alone would leave the consumer serving stale results. The key now additionally folds in a referencedProjectSourcesStamp: walks options.ReferencedProjects, and for each FSharpReferencedProject.FSharpReference case (the F# P2P case, carrying the referenced project's FSharpProjectOptions) stamps its SourceFiles mtimes with the same try/with-resilient approach. The walk recurses transitively (FCS's ReferencedProjects is NOT pre-flattened — only direct references appear at each level) using a HashSet<string> visited-set keyed on ProjectFileName to avoid re-stamping shared deps and guard against cycles. PEReference / ILModuleReference cases carry no FSharpProjectOptions and fall through, covered by the P1 DLL stamp. +1 proof-by-breaking test: the test bumps only Domain.fs mtime (no DLL touched), asserts the consumer cache count grows by MORE than 1 (both Stubs and App were re-keyed, not just Domain's own entry); on the pre-P2-fix key the count would grow by exactly 1 (only Domain), causing the assertion to fail.
  • check(speed="fast") now reports totalDiagnostics consistent with the surfaced diagnostics list (#133). Since 0.10.0 the fast path surfaces severity-3/4 (information/hint) diagnostics when the caller raises the severity floor, but the response still set totalDiagnostics = ErrorCount + WarningCount, so an info-only snapshot returned a non-empty diagnostics list with totalDiagnostics: 0 — list and count disagreeing, unlike the trusted path. totalDiagnostics is now the full-set count across all severities (mirroring the trusted path's allDiags.Length): with severity="all" the surfaced list length and totalDiagnostics agree exactly. verdict stays error-based and errorCount/warningCount stay full-set tallies; the trusted path is unchanged. +1 test.
  • Release build is now statically compilable (FS3511 in find's position resolution), so dotnet build -c Release --warnaserror is robust across SDK versions. ResolveQueryAtPosition's five-level-deep match inside a task {} prevented the F# compiler from reducing the state machine statically under Release optimization (FS3511). The purely synchronous inner block — match checkedResults with … Some checkResults -> symbol-lookup … — was extracted into a dedicated ResolvePositionInFile member returning plain Result<string, JsonNode> (no awaits needed), leaving the outer task {} with a single return at its deepest branch, which the compiler can reduce statically. Same fix pattern as ProjectSweepUses from #131. No behaviour change.

0.10.0 - 2026-06-28

Added

  • find <query> — one multi-project symbol search that supersedes the seven single-project search tools (#128). Sweeps every member .fsproj of the active solution and unions four site kinds in a single call: definitions, references, record-field set sites ({ Field = expr } and { x with Field = expr }), and member-usage sites. This recovers cross-project usage sites that the single-project fcs_find_symbol / fcs_record_field_audit miss — on the consolidation validation fixture the single-project path surfaced 1 site where the whole-solution find sweep surfaced 11. Bare find(query) suffices (kind=auto, scope=auto over the whole solution); optional kind (auto|symbol|members|field|definition|position) and scope (auto|file|project|workspace) narrow it. Falls back to the FSAC workspace/symbol index when the FCS sweep is empty, so matched=false is a real negative. Mechanics: docs/tools-detailed.md#find.
  • check — one trustworthy type-check verdict that supersedes the five lower-level check tools (#128). Returns a single verdict (clean | errors | unknown) collapsed from a FRESH in-process FCS type-check, so it never reports the stale-{} false-clean that workspace_diagnostics can serve right after an Edit/Write — the false-clean that historically drove agents to fall back to dotnet build. Bare check() covers the active project (scope=auto, speed=trusted); optional scope (auto|file|project|workspace|snippet), path, snippet (inline source), speed (trusted default | fast = cached FSAC snapshot), and severity. unknown means the check could not run (no project context), not a pass. Mechanics: docs/tools-detailed.md#check.

Changed

  • The 12 legacy "cluster" tools are now routing-steered toward find / check. The seven find-cluster descriptions (fcs_find_symbol, fcs_record_field_audit, fcs_find_member_usages, workspace_symbol, fcs_project_symbol_uses, textDocument_references, textDocument_definition) gained a trailing Prefer find for cross-project work. steer; the five check-cluster descriptions (workspace_diagnostics, fsharp_compile, fcs_check_file, fcs_parse_and_check_file, fcs_validate_snippet) gained Prefer check for a yes/no verdict. fcs_record_field_audit was trimmed before the steer (491 → 497 after; cut the during widening refactors clause and a see filler word) to stay within the 500-char ToolDescriptionSchemaTests / audit-tool-descriptions.py ceiling. README, AGENT_INTEGRATION.md, and docs/tools-detailed.md now lead with find / check and mark the twelve cluster tools as lower-level primitives. The cluster tools remain fully registered — actual removal is telemetry-gated and deferred to a future release.
  • find now defaults to compact one-line-per-site output (contextLines defaults to 0, down from 1; default maxResults 500 → 80). Each site emits file / range / kind / project / symbolFullName / lineText with no before/after context arrays unless contextLines > 0 is passed, and the breakdown + resolution summary is always present (the full-set counts are independent of the page cap). Native dogfooding found a bare find(query) on a hot symbol overflowed the MCP token ceiling (~73k chars, over the ~25k-token limit). With the compact default plus the de-duplication below, a 69-site hot symbol (find("FindArgs")) now fits one page at ~37k chars (down from a truncated 40-site page at ~41k chars), and pagination (cursor/nextCursor) remains the backstop for genuinely huge result sets. Pass contextLines:2 to restore the previous richer output.
  • find now returns a single de-duplicated flat sites list — the grouped definitions / references / fieldSites / memberSites buckets were removed. Every site was previously serialized twice (once in its grouped bucket, once in the flat sites superset), doubling the payload — the reason the default page cap had been forced down to 40. Each flat-sites entry is already self-describing (file / range / kind / project / symbolFullName / lineText), so an agent filters by kind and reads breakdown for per-kind counts; breakdown, resolution, perProject, and the pagination envelope are unchanged. The per-site cost halved (~1030 → ~527 chars; at a fixed 54-site page find("FindArgs") dropped from ~55.6k chars to ~28.4k chars), so the default maxResults was retuned 40 → 80 to fit more results in one call while staying safely under the MCP token ceiling.

Fixed

Pre-publish fixes folded into 0.10.0 from the Codex review of #129 / #126 (all in unreleased code):

  • find(kind=position) now keys the sweep on the resolved symbol's FullName, not its DisplayName. A cursor resolves THE specific symbol under it; keying the subsequent multi-project sweep on DisplayName matched every same-named symbol, so positioning on one Config swept an unrelated Config in another namespace (and every same-named overload). The sweep key now prefers FullName, falling back to DisplayName only for locals/synthetic symbols that lack a useful one.
  • find(kind=field|members) now honors exact=false on the declaring type. The generic symbol branch already did substring matching, but the field/member declaring-type predicate still used ordinal/full-name-boundary matching — so find(query="role", exact=false, field="Propose") missed TraderRole.Propose. The declaring-type predicate now threads exact through, matching the symbol branch.
  • check(speed="fast") now surfaces diagnostics at the requested severity floor. The cached FSAC snapshot stored only error-severity (code 1) entries, so check(speed="fast", severity="warning"|"all") omitted the warnings the caller asked for (and the reported warningCount was inconsistent with the empty list). The snapshot now retains all severities and the fast path filters them by the requested floor; errorCount/warningCount stay full-set tallies and verdict stays error-based.
  • fcs_nuget_members now enumerates class fields. The field guard was IsFSharpRecord || IsValueType, which skipped public val fields on reference-type classes (they live only in FSharpFields, not MembersFunctionsAndValues). Classes are now enumerated too, with compiler-generated backing fields (Name@ / <Prop>k__BackingField) filtered so an auto-property is not duplicated by its hidden field. (FCS still returns an empty FSharpFields for classes imported from a C#/IL assembly, so C# instance/const fields remain unrecoverable through the symbol API — now documented in docs/tools-detailed.md.)

[0.9.3] - 2026-06-28

Added

  • fcs_nuget_members <packageId> <typeName> — member-level companion to fcs_nuget_types (#125). Enumerates the members of one type from a referenced NuGet assembly (methods, properties, fields, constructors, events, union cases) with formatted FCS signatures, accessibility, [Obsolete] flag, and XML-doc summary. Resolves the assembly through the same GetReferencedAssemblies() path as fcs_nuget_types; paginated (default 500, max 2000), returns an empty matchedTypes marker (not an error) on no match. Type matching is generic-arity-aware (a bare FSharpOption resolves the arity-suffixed compiled type), compiler-generated property accessors (get_/set_) are filtered so a property isn't duplicated by its accessor, and identical member rows are de-duplicated. +8 tests; the tool description passes the docs/tool-description-schema.md audit.

Fixed

  • set_project now returns a clear invalid_args envelope naming projectPath when the argument is missing or supplied under the wrong key (#100). Previously a wrong/missing key let Path.GetFullPath(null) throw ArgumentNullException whose message named the internal path parameter (Value cannot be null. (Parameter 'path')), misdirecting callers to the wrong field. The handler now routes through ArgsValidation.requireNonBlank "projectPath" before any path operation, matching the other six handlers standardised in #120. +2 tests.

Security

  • Patched the MessagePack serialization chain behind the StreamJsonRpc transport (DoS advisories). StreamJsonRpc 2.24.* → 2.25.* (pulls patched MessagePack 2.5.302) and Nerdbank.MessagePack 1.1.62 → 1.2.30 (clears GHSA-92vj-hp7m-gwcj and GHSA-qjvr-435c-5fjh). Pinned Microsoft.NET.StringTools to 18.4.0: Nerdbank.MessagePack 1.2.30 drops the transitive StringTools 18.4.0 that had been masking MessagePack's stale 17.6.3 — which lacks SpanBasedStringBuilder.Equals(string, StringComparison) and silently breaks Ionide.ProjInfo's MSBuild project loading. dotnet restore now passes with NuGet audit enabled; 301/301 tests green.

0.9.2 - 2026-05-21

Changed

  • Tool description sweep against docs/tool-description-schema.md (5-slot schema, 500-char ceiling). All 33 registered tool descriptions in Program.fs re-audited. Four routing-blockers fixed (descriptions that exceeded the 500-char ceiling and pushed implementation detail into the agent-routing prompt budget): fcs_check_file 610 → 452, fcs_find_member_usages 581 → 448, fcs_make_internal_visible 699 → 419, fcs_type_at_position 622 → 451. Mechanics moved to dedicated H2 sections in docs/tools-detailed.md (now covers 9 tools, up from 5). Eight additional descriptions gained explicit prefer X / avoid Y callouts to close overlap-pair ambiguity (workspace_diagnosticsfcs_check_file stale-cache hint, fcs_referenced_symbolsfcs_nuget_types substring-vs-exact routing, fcs_validate_snippetfcs_parse_and_check_file when-file-exists hint, fsharp_compiledotnet build for IL emission, fsharp_project_inspect over textual .fsproj reads, fcs_project_outline over workspace_symbol for whole-project overview, textDocument_rename over textual rename).

Added

  • scripts/audit-tool-descriptions.py + just audit-descriptions. Python analyzer enforcing the schema: parses TypedTool.define entries from Program.fs, applies 500-char ceiling + tag-prefix rule as hard failures (non-zero exit), reports under-budget descriptions and missing overlap-pair callouts as soft warnings. Closes the long-standing reference in docs/tool-description-schema.md §"Adding a new tool" → step 4 ("Run the length analyzer") to a previously non-existent tool.
  • ToolDescriptionSchemaTests.fs in the xunit suite (+4 tests, 289 → 293 passing). F#-side enforcement so dotnet test is the canonical gate (CI already runs it; the Python script is the local dev convenience). Three rules enforced: every description starts with a recognized tag prefix ([FSAC] / [FCS in-process] / [meta]), no description exceeds 500 chars, every overlap pair has at least one prefer X / avoid Y callout. Violations are aggregated per rule so the failure message lists all offenders at once (not just the first).
  • 4 new H2 sections in docs/tools-detailed.md: fcs_check_file, fcs_find_member_usages, fcs_make_internal_visible, fcs_type_at_position. Each follows the established template (How it works internally, Caveats, Related tools). Routing descriptions in Program.fs cross-reference these via Mechanics: docs/tools-detailed.md#<anchor> footers.

Notes

  • No behaviour changes. Descriptions are routing-time metadata only; agent prompt-budget pressure shrank by ~750 chars / ~190 tokens across the four over-budget tools combined.

0.9.1 - 2026-05-21

Documentation

  • AGENT_INTEGRATION.md added at repo root (158 + 16-line refresh = 174 lines). Salvaged from a prior orphan branch (docs/agent-integration-guide, authored 2026-05-17), then surgically refreshed for tools shipped since: boundary heuristic table now points to fcs_nuget_types / fcs_referenced_symbols (v0.7.0, #113) for NuGet type enumeration instead of dotnet fsi + reflection; points to fcs_validate_snippet with mode="fsi" (v0.7.0, #112) for .fsi sketch validation instead of "until parse_fsi_sketch lands". Subagent-brief template lists newer tools (fcs_suggest_open from v0.9.0 #67, fcs_record_field_audit from v0.8.0 #117). "See also" footer links docs/troubleshooting.md and docs/tool-description-schema.md. CLAUDE.md referenced this file as the canonical multi-agent integration guide for distribution to other contributors — now the file actually exists.
  • CONTRIBUTING.md added at repo root (86 lines). Build + test commands with current 289-test baseline. Step-by-step walkthrough for "Adding a new MCP tool" referencing real files (Types.fs args records with /// docs, FcsBridge member implementation, Program.fs TypedTool.define registration, description per docs/tool-description-schema.md, regression tests). Repo conventions (no Co-Authored-By, F# style, warnings-as-errors via Directory.Build.props, ArgsValidation.requireNonBlank at Types.fs:415, proof-by-breaking for regression tests). PR process. Bug/feature reporting routes (issue #100 catch-all for usage feedback, dedicated issues for concrete bugs).
  • docs/architecture.md added (102 lines). High-level diagram showing the two-bridge architecture (stdio MCP transport → FsAutoCompleteBridge for fsautocomplete child process + FcsBridge for in-process FCS). File-by-file tour in compile order matching FsLangMcp.fsproj. Design-choice paragraphs covering: two-bridge rationale, Workstation+Concurrent GC defaults (v0.5.1 fix for stdio MCP bursty workload), tool-description schema enforcement (~2700-token system-prompt budget), structured error envelope (v0.8.2 #120). "Where to add things" pointers cross-link to CONTRIBUTING.md and the schema doc.
  • docs/roadmap.md added (51 lines). Pre-1.0 status. 1.0 exit criteria (concrete, testable): all #100-class blockers closed, surface stable across 3 consecutive minor releases, /// field docs surface in MCP JSON schema (currently they don't — see v0.8.6 CHANGELOG), VS.Threading bind race fully closed (v0.8.2 #121 partial), performance baselines (warm set_project ≤2s on 10-project solution; cold fcs_record_field_audit ≤5s on 50k-LOC project), documentation reflects current surface, ≥1 third-party adoption report. Deferred items with issue links. "Will NOT do" section: not competing with Ionide for editor use, not replacing dotnet build for ground-truth project validation, not adding Paket support.
  • README polish:
    • Two new shields.io badges: nuget/dt/FsLangMcp (downloads total) and badge/target-net10.0.
    • New ### When to use FsLangMcp (vs alternatives) subsection between Components and Quickstart — three concrete "if your agent needs X, use Y" bullets covering raw FSAC, FsLangMcp itself, and raw FCS.
    • New "Which 'check this code' tool" callout inside the FCS in-process section before the tool table — explicit decision tree for fcs_parse_and_check_file / fcs_check_file / fsharp_compile.

(No code or behaviour changes. Closes the remaining developer-advocate audit findings; 1.0 readiness is now a documented track, not a vibe.)

0.9.0 - 2026-05-20

Added

  • fcs_suggest_open (closes #67) — given an unresolved symbol name (e.g. from an FS0039 'X' is not defined diagnostic), returns ranked open directive candidates from BOTH the project and referenced assemblies (BCL + NuGet). Composes the same EnsureProjectResults + allEntitiesFromAssembly primitives that fcs_referenced_symbols and fcs_nuget_types use. Resolves FSharpEntity.AccessPath for each match. Ranking: project hits first (more relevant), then references; dedup by (accessPath, fullName) within each tier; reference entries that duplicate a project entry are dropped from the reference tier entirely. Output shape: { candidateCount, candidates: [{ openPath, entityFullName, source: "project"|"reference", assembly, kind, accessibility }] }. Closes the most common F# error→fix loop where an agent would otherwise have to guess the namespace or read docs.

Changed

  • FieldFormClassifier walker reimplemented via ParsedInput.fold (closes #124). The previous 88-line hand-rolled walkExpr/walkDecl recursion only descended into a curated subset of SynExpr/SynModuleDecl arms — record literals inside class member bodies (SynModuleDecl.Types), for ... do loops (SynExpr.For/ForEach), CE-bind continuations (SynExpr.LetOrUseBang), function | ... ->, while, New, ObjExpr, Lazy were all missed and fell through to the textual heuristic fallback. New 33-line implementation uses ParsedInput.fold (the public FCS 43.12+ position-independent whole-tree fold API) with a single match arm on SynExpr.Record; every expression-containing AST node is covered automatically and the implementation has no maintenance burden as FCS adds new node types. The fallbackHeuristic callsites in formOf are preserved as defensive safety nets for parse failures and any future un-handled node kinds. Schema-compatible: form values remain "literal"/"with-update"/"unknown". +3 regression tests proven via proof-by-breaking (with the new walker bypassed to fallback-only, those 3 tests deterministically fail — the assertions distinguish parse-tree-path from fallback, not just smoke-test the audit).

Tests

  • +6 net new tests (3 for #124 walker coverage + 3 for #67 across BCL hit / project hit / ranking / no-hit / invalid-args). 283 → 289 passing.

Process notes

  • Code reviewer iter-1 caught test theatre in BOTH #124 and #67 tests during this batch (fixtures that didn't actually distinguish the path under test). Fixer applied proof-by-breaking to harden the regression tests so they fail deterministically when the production code reverts. Lesson recorded for future review-fix loops.

0.8.6 - 2026-05-20

Documentation

  • Tool description schema documented + 5 over-long descriptions compressed. An AI-engineer audit of all 32 tool registrations found 7× length variance (154-1096 chars). The longest five — fcs_validate_snippet, fcs_nuget_types, fcs_record_field_audit, fcs_find_symbol, fcs_referenced_symbols — were over-explaining implementation detail (temp-file splicing, walker internals, exact escape semantics) which belongs in deep docs, not the routing-prompt that LLMs read on every agent call. Compressed each to ≤500 chars following the new 5-slot schema ([Tag] What + Prefer-X-over-Y + Key-params + Caveat + Cross-ref); moved the implementation depth to a new docs/tools-detailed.md with one H2 section per compressed tool. Total system-prompt overhead dropped from 13,241 → 10,937 chars (-2,304 chars / ~575 tokens saved per agent call).
  • 3 FSAC tools gained "prefer FCS alternative" anti-recommendations. workspace_symbol, textDocument_completion, textDocument_definition were positive-only ("useful for...") and didn't tell agents when to route to the agent-shaped FCS-side equivalents. Each now explicitly points to fcs_find_symbol / fcs_symbol_at_word / fcs_type_at_position for non-IDE workflows.
  • 15 Args records documented with /// field doc-comments (~74 fields across CompletionArgs, PositionArgs, ReferencesArgs, SetProjectArgs, FSharpCompileArgs, ProjectHealthArgs, FcsParseAndCheckArgs, FcsFileSymbolsArgs, FcsFileOutlineArgs, FcsSymbolAtWordArgs, FSharpProjectInspectArgs, FcsSignatureHelpArgs, CodeActionArgs, RenameArgs, RuntimeStatusArgs). Defaults are stated explicitly (verified against defaultArg sites in the handlers — no hallucinations). Caveat for agent integrators: confirmed that /// doc-comments on F# record fields do NOT currently surface in the MCP JSON schema's properties[x].description. The value of this change is for IDE tooltips, source-readers, and future schema-gen work that may pick up XML docs — not (yet) for runtime agent arg-construction.
  • New docs/tool-description-schema.md documents the 5-slot schema for future tool authors with good-example + anti-pattern sections, so the next added tool stays within ~250-400 chars and includes a "prefer X over Y" callout when overlap exists.

(No code or behaviour changes. v0.8.6 ships discoverability and documentation polish.)

0.8.5 - 2026-05-20

Documentation

  • NuGet listing populated. The package previously shipped with description: "Package Description" (the literal SDK default), no tags, no project URL, no license. v0.8.5 adds <Description>, <Authors> (Roman Melnikov), <PackageTags> (fsharp;mcp;lsp;fsautocomplete;fcs;ai;agent;ionide;compiler-service;claude-code), <RepositoryUrl> / <RepositoryType> / <PackageProjectUrl> pointing at the GitHub repo, <PackageLicenseExpression>MIT</PackageLicenseExpression>, and <PackageReleaseNotes> pointing at CHANGELOG. A LICENSE file (MIT, copyright 2026 Roman Melnikov) was added at the repo root and packed into the nupkg so the tool ships with explicit licensing — required for corporate-procurement compliance pipelines.
  • README first-screen rewritten as a pitch. The first 16 lines now answer "who is this for" (AI coding agents on F# projects) and "what makes it different" (agent-shaped tool surface with paginated responses, structured invalid_args envelopes, cache-invalidating reanalysis, snippet validation against the loaded project's references, and record-field audits catching construction sites textual search misses). The pre-existing 2-line component list is demoted to a ### Components subsection.
  • New ## Quickstart section — 60-second on-ramp from dotnet tool install -g FsLangMcp through --bootstrap-tools, MCP client config, the first set_project call, and a project_health preflight. Each step has a copy-pasteable code block.
  • New docs/troubleshooting.md keyed by user-visible symptoms: status: "not_ready" after set_project, FileNotFoundException: Microsoft.VisualStudio.Threading 17.14.0.0 (affects ≤ 0.8.1), stale workspace_diagnostics after edit, and fcs_find_symbol zero-match-but-symbol-exists (covers both record-field-set sites and broken-project diagnostics-fallback). Linked from the README Quickstart close-out.
  • Customer-name leak in fcs_record_field_audit tool description removed. The string previously referenced "LlmTrader's port-record widening refactors" — a customer-specific anecdote that meant nothing to anyone outside that customer. Rewritten as the generic "port-record widening refactors (adding a field to a domain record type)" use case. Schema and behaviour unchanged.

(No code or behaviour changes. v0.8.5 ships purely to fix NuGet discoverability and onboarding friction surfaced in the developer-advocate audit after v0.8.4.)

0.8.4 - 2026-05-20

Documentation

  • CHANGELOG brought up to date. 11 missing release entries (0.5.3 → 0.8.3) added between Unreleased and the existing 0.5.2 entry. Compare-links footer extended. The CHANGELOG was last updated for 0.5.2 (2026-05-06); since then the project shipped 11 releases including substantial new tools (fcs_record_field_audit, fcs_make_internal_visible, fcs_validate_snippet, fcs_referenced_symbols, fcs_nuget_types, fcs_find_member_usages, fcs_check_file, fslangmcp_version) and a major correctness rework of fcs_make_internal_visible's text guard (v0.8.1) and fcs_record_field_audit's formOf classifier (v0.8.2). Each entry follows the existing Keep a Changelog tone and cites the originating issue.
  • README "What You Get" rewritten as three tables (LSP-proxy / FCS in-process / Meta) covering all 32 currently-registered MCP tools with 1-sentence descriptions. Previously the list was static at the v0.5.x surface and was missing roughly 10 tools added across 0.6.x-0.8.x.
  • README "Response Shape" extended with the standardised {"status": "invalid_args", "message": "..."} envelope (from v0.8.2 #120) and a new "Notable response fields" subsection covering set_project.fslangmcpVersion/loadedProjects/readiness, workspace_diagnostics.analyzedAt/mostRecentAnalyzedAt/analyzedAtByUri, fcs_find_symbol.projectDiagnosticsScope, and fsharp_runtime_status.fslangmcpVersion.
  • Issue-number attributions corrected in both CHANGELOG and README. The original v0.8.0 and v0.8.1 commit messages contained a systematic swap of #114/#115/#116/#117/#119 (issues created in a different order than the feature-by-feature mental model). The CHANGELOG now matches the actual GitHub issue titles for each cited number; commit history itself is left as-is (shipped tags are immutable). Verified across 29 issue citations.

(No code or behaviour changes in this release — pure documentation.)

0.8.3 - 2026-05-20

Changed

  • fcs_record_field_audit description clarifies optional path. Tool description now states explicitly that path is optional when projectPath is set, and recommends supplying path+text together when auditing against an unsaved buffer. Closes a doc gap noted in the v0.8.2 triage pass. (No behaviour change.)

Fixed

  • Comment-only follow-ups from the v0.8.2 review loop. No behaviour change; 280 tests unchanged.
    • FieldFormClassifier.classify now carries a comment above the SigFile arm explaining the intentionally-empty dictionary (.fsi files have no record-literal use sites), guarding against a future maintainer "completing" the empty branch.
    • RecordFieldAudit.parseFileForForms gains a PERF TODO noting the serial Async.RunSynchronously cold-cache cost; references the #122 follow-up issue (#124) as the natural place to fold in parallelization or batched-parse improvements.

0.8.2 - 2026-05-20

Sequential subagent pipeline closed the four-issue follow-up batch from the v0.8.1 triage. One real bug (#121) and three refactors (#120, #122, #123). 268 → 280 tests, all review-loop must-fix items closed.

Fixed

  • set_project no longer intermittently fails with Could not load Microsoft.VisualStudio.Threading 17.14.0.0 in subagent / multi-process hosts. The assembly was on disk; the failure was a transitive bind via StreamJsonRpc 2.24 → SolutionPersistence with rollForward defaulting to Minor in runtimeconfig. Fix promotes Microsoft.VisualStudio.Threading.Only 17.14.15 from a transitive to a top-level PackageReference so the bind is satisfied at startup probing rather than at first use of StreamJsonRpc internals, and adds rollForward: LatestMajor to runtimeconfig.template.json (matching peer F# tools — fsautocomplete, Ionide.ProjInfo.tool). Observed across 9 separate subagent sessions on 2026-05-19. (#121, P1)

Changed

  • workspace_diagnostics.mostRecentAnalyzedAt is now scoped to the fileGlob-filtered subset. Previously the field returned the workspace-wide max regardless of filter — misleading when callers scoped to src/Adapters/*.fs but got a timestamp from tests/. Reuses the same globMatches predicate that filters analyzedAtByUri, so the timestamp reflects exactly the scoped subset. Backward-compatible when fileGlob is None. (#123)
  • Standardised required-string arg validation across six MCP handlers (fcs_record_field_audit, fcs_project_symbol_uses, fcs_find_member_usages, fcs_find_symbol, fcs_referenced_symbols, fcs_nuget_types). Previously each used a different guard (Option.ofObj+default, invalidArg, or none); now all go through ArgsValidation.requireNonBlank : string -> string -> Result<string, JsonNode> in Types.fs and return the standard { "status": "invalid_args", "message": "…" } envelope. fcs_make_internal_visible gained validation it previously lacked. Behaviour change: fcs_project_symbol_uses / fcs_find_member_usages / fcs_find_symbol on blank required-string args previously raised ArgumentException (surfacing as MCP transport-level error); they now return the structured envelope, same as the other three handlers. Aligns the FCS tool surface to one error contract. (#120)
  • fcs_record_field_audit form classifier now uses an AST walker instead of a textual heuristic. Old heuristic scanned the field's start line + 2 preceding lines for with; multi-line record updates beyond 2 lines were misclassified as literal. New FieldFormClassifier walks SynExpr.Record and tags each field-name range based on copyInfo.IsSome (with-update) vs IsNone (literal). Strictly additive: when the walker doesn't visit a site (member-body, for-loop, computation-expression bind), falls back to the textual heuristic so behaviour never regresses below v0.8.1. Wire format unchanged — form still takes "literal" / "with-update" / "unknown". (#122)

0.8.1 - 2026-05-19

Four-iteration reviewer/fixer loop on v0.8.0 (engineering-code-reviewerengineering-fsharp-developer) until APPROVED-CLEAN. Geometric convergence (6 → 3 → 3 → 1 → 0), scope stayed inside areas v0.8.0 touched. 256 → 268 tests, no API change.

Fixed

  • fcs_make_internal_visible no longer risks corrupting string literals containing private. FindPrivateSpan could strip the substring private from inside a regular string literal on a recognized-keyword line — let msg = " private channel" was at risk. Now blocked by a state-machine PositionIsUnsafe helper that classifies each character as code / regular-string / verbatim-string / triple-quoted-string / block-comment / line-comment. (P1, close-call latent bug.) (#118 follow-up)
  • fcs_make_internal_visible handles F# verbatim (@"..." with "" escape, literal \) and triple-quoted ("""...""") strings correctly. Previously the classifier only handled C-style escaping. (#118 follow-up)
  • fcs_make_internal_visible handles (* doc *) let private foo = 1 correctly. Strips when block comment closes before the let; refuses when the position sits inside an open block comment; refuses on unclosed block-comment openers. Discriminator is pos < i (correct inside-span check), not the tautological pos > commentStart. FindPrivateSpan also peels (* ... *) and [<...>] prefix blocks from the before-text so post-comment / post-attribute declarations are recognised. (#118 follow-up)
  • fcs_make_internal_visible recognises and for mutually-recursive bindings. let rec foo = ... and private bar = ... now strips correctly. (#118 follow-up)
  • fcs_find_symbol surfaces error-severity diagnostics on zero-match queries instead of returning an empty result that hides project breakage. When matchedFileSet is empty, falls back to whole-project error-severity diagnostics with new response field projectDiagnosticsScope = "errors-only-no-matches" so callers can distinguish "no matches because the project is broken" from "no matches because the symbol doesn't exist". (#114 follow-up)
  • project_health no longer false-positives [<TestFixture>] as a test count. Test-attribute regex gained \b word-boundary so the TestFixture declaration doesn't count as one Test. (#119 follow-up)
  • project_health.binaryOutputPath no longer walks the entire monorepo. findLatestBuildArtifact restricted to the project's bin/ directory while retaining case-insensitive filename equality so Linux CI with different casing still resolves. (#119 follow-up)
  • Version.current no longer throws NullReferenceException on degenerate SDK builds. Null/empty AssemblyInformationalVersion is now guarded at module init. (#115 follow-up)

0.8.0 - 2026-05-19

Two-commit minor release. The first commit (b8a1207) wired version metadata and analysis timestamps; the second (b6a4aa8) added two new tools and enriched two existing ones. All changes additive.

Added

  • fcs_record_field_audit — new FCS tool that finds every construction site for a (typeName, fieldName) pair, covering both { Field = expr; ... } literal form and { existing with Field = expr } update form. Complements fcs_find_symbol / textDocument_references, which look up the type name and miss field-set uses. FCS predicate: symbol :? FSharpField && DeclaringEntity match && Name match && not IsFromDefinition. Each site reports file, range, form (literal / with-update / unknown via line-text heuristic — false positives possible if a comment contains with; replaced with parse-tree classifier in 0.8.2), and 2 lines of source context. Paginated; default 200, max 1000. path+text together supports unsaved-buffer audits. Validated on LlmTrader's TraderRole.Propose (28 caller sites; only 2 were found by fcs_find_symbol) and RiskDebatorRole. (#117)
  • fcs_make_internal_visible — new FCS tool (Variant A only) that drops the private keyword from a let / let rec / module / module rec / type / member / val / new / static / abstract / override declaration at the given (line, character). Returns a workspace edit { status: "ok", edits: [{ range, newText: "" }], appliedPreview, originalLineText } — does not write the file. When the position has no symbol or the line has no recognized private modifier, returns { status: "no_action", reason }. Detection is text-driven (FCS IsFromDefinition + Accessibility.IsPrivate proved brittle on module-level let bindings); FindPrivateSpan requires a recognized declaration keyword before private and peels off [<...>] attribute blocks. Variant B (auto-add InternalsVisibleTo on the test project) deferred to a follow-up. (#118)
  • fslangmcp_version — new zero-arg meta tool returning { fslangmcpVersion, productName } for explicit "what am I talking to" queries. Pure: no project context required, no side effects. (#115)
  • set_project response includes fslangmcpVersion as the first field in result. (#115)
  • fsharp_runtime_status response includes top-level fslangmcpVersion. (#115)
  • workspace_diagnostics response carries analysis timestamps. Single-file response gains analyzedAt; workspace-wide response gains mostRecentAnalyzedAt and analyzedAtByUri (per-URI DateTimeOffset map). Callers can now self-check "my last Write was at T1, the analyzedAt is T2 < T1, results are stale" without falling back to a build. DiagnosticsTarget records the timestamp whenever FSAC pushes publishDiagnostics. forceReanalyze arg + FSAC re-analyze hook deferred to a follow-up — needs protocol-level investigation; timestamps cover the highest-value half of the issue. (#116 partial)
  • project_health per-project enrichment. Reports isTestProject (xunit / NUnit / Expecto via PackageReference), testFrameworks, testCount (Fact / Theory / Test / TestCase / TestMethod regex, qualified-name- and Attribute-suffix-tolerant), lastBuildSucceeded, lastBuildAt, and binaryOutputPath (bin glob keyed off <AssemblyName> with fallback to project basename). Pure file-system + regex; no dotnet test invocation. (#119)
  • fcs_find_symbol projectDiagnostics scoping. FcsFindSymbolArgs gains includeInfo: bool option (default false). The projectDiagnostics field is now filtered to (a) files containing a match for symbolQuery AND (b) severity ≥ Warning unless includeInfo=true. New response fields: includeInfo, matchedFileCount, projectDiagnosticsScope: "matched-files". (#114)

Changed

  • MCP serverInfo.version now reflects the real product version. Previously hardcoded "0.4.0" — three minor versions stale. Now read from AssemblyInformationalVersion at runtime via a new Version module (strips any +commit suffix). (#115)

0.7.0 - 2026-05-19

Three new tools / arg-shape extensions, all additive. 217 → 231 tests.

Added

  • fcs_type_at_position gains fuzzy: bool. When true and the exact position misses, scans ±2 lines / ±5 cols (line-weight 2×) and snaps to the nearest symbol; the response then carries resolvedLine / resolvedCharacter and fuzzySnap=true. no_symbol responses always include lineText and surroundingLines so 1-based-vs-0-based mistakes are self-correcting. (#111)
  • fcs_validate_snippet — new FCS tool that compiles an arbitrary F# snippet (.fs or .fsi mode) against the loaded project's references without modifying the project on disk. Writes the snippet to a uniquely-named file under the OS temp dir, splices it into a fresh copy of project options (cached options not mutated), runs ParseAndCheckFileInProject, and deletes the temp file before returning. Useful for "does this signature parse?" probes without scaffolding a scratch .fsproj. Caveats documented in the tool description (an .fsi snippet without paired .fs produces "signature has no implementation"; snippet can reference any project type but not symbols defined later in the compile order). (#112)
  • fcs_referenced_symbols — new FCS tool that searches across the project's referenced assemblies (NuGet + framework) for types whose DisplayName or FullName contains the query (case-insensitive). Complements workspace_symbol (project-local). Each result reports assembly, kind (class / interface / struct / enum / record / union / module / ...), accessibility (public / internal / private / unknown), and isObsolete. includeNonPublic=true exposes internals. Lazy: first call triggers ParseAndCheckProject if not warm. Paginated; default 200, max 1000. Cursor stability is best-effort — if the project's references change between calls, the offset may shift; treat the cursor as ephemeral. (#113)
  • fcs_nuget_types — new FCS tool that enumerates all types exported by one referenced assembly, matched by exact SimpleName (case-insensitive). packageId='Spectre.Console' resolves to assembly Spectre.Console only — not Spectre.Console.Cli. packageId='System' resolves to the literal System assembly only — not every System.* assembly. When a NuGet package ships multiple assemblies, call once per assembly name. Returns matchedAssemblies=[] when no assembly matches; does not silently fall back to a less-specific assembly. Paginated; default 500, max 2000. (#113)

Changed

  • fcs_nuget_types matching is strict SimpleName-only. Rejected during review: reverse-prefix and dotted-prefix variants both produced misleading matches under BCL / multi-assembly NuGet packages (e.g. System would match every System.* assembly). To discover assembly names, callers should use fcs_referenced_symbols with a partial query first.
  • Structured invalid_args JSON for empty query / packageId / cursor and missing projectPath in fcs_referenced_symbols / fcs_nuget_types (previously raised; now consistent with fcs_validate_snippet).
  • Two-tier fallback for entity name resolution. entity.DisplayNameentity.LogicalName"<unknown>" (FCS throws on certain synthetic / anonymous symbols).

0.6.1 - 2026-05-19

Three issues closed (#108, #109, #110); 14 new tests (199 → 214). All changes additive.

Added

  • fcs_check_file — new FCS tool with surgical cache invalidation for one project. Drops only that project's options + project-results cache entries (other loaded projects keep their warm caches), then calls checker.InvalidateConfiguration on the same project before re-running parse + check. Returns a diagnostics-focused payload with errorCount + totalDiagnostics. Use this when workspace_diagnostics looks stale right after an Edit / Write. Honest docs about FCS's internal AST cache remaining for transitively-referenced files; for absolute cross-project ground truth, fall back to dotnet build. Adds BoundedCache.TryRemove for per-key removal. (#109)
  • workspace_diagnostics filter args. DiagnosticsArgs gains optional fileGlob and severity fields. Glob uses POSIX / gitignore semantics: * matches single segment, ** matches zero+ segments, /**/ between segments collapses zero+ dirs. severity accepts "error" | "warning" | "information" | "hint" and filters diagnostics inside each file; files that empty out after filtering are dropped from the workspace response. Implementation in pure LspResponseShape helpers (severityCodeOf, fileMatchesGlob, filterDiagnosticsBySeverity) wired into LspBridge.Diagnostics. (#108)
  • Accessibility metadata in symbol JSON. symbolToJson now exposes "accessibility": "private" | "internal" | "public" | "unknown". Wraps FSharpSymbol.Accessibility in try/with — FCS throws on certain synthetic / anonymous symbols. (#110)

Changed

  • Glob semantics rewritten from naive .* substitution to POSIX-style state machine (* → single segment, ** → cross-segment, /**/ → zero+ dirs, **/ → leading zero+ dirs). Code-review-driven.
  • fcs_check_file cache invalidation is surgical (per-project) rather than nuking caches for every loaded project.

0.6.0 - 2026-05-18

Two issues closed (#105, #107); 8 new tests (191 → 199). Minor bump because optional-projectPath is a public-arg-shape change.

Added

  • fcs_find_member_usages — new FCS tool finding all usage sites of a member declared on a specific type. Resolves via FCS so dotted access (x.Foreground), pipeline application, and overload resolution are handled correctly — unlike a textual rg. Pass typeName (DisplayName e.g. "Style" or FullName e.g. "MyApp.Theme.Style") and memberName. typeName matching is deliberately conservative: DisplayName equality always (so "Style" won't false-match "StyleSheet"); with exact=false the FullName may match at segment boundaries ("Theme.Style" yes, "Theme.StyleSheet" no). Pagination via cursor, same shape as fcs_project_symbol_uses. (#107)

Changed

  • projectPath is now optional on per-project tools. After set_project, omit projectPath and the active project is used; pass it explicitly to target a different .fsproj. Affected arg records (each stringstring option, hence the minor bump):

    • FSharpCompileArgs
    • ProjectHealthArgs
    • FcsProjectOutlineArgs
    • FSharpProjectInspectArgs
    • FcsGetProjectOptionsArgs

    Resolvers (ProjectFiles.resolveProjectPath, ProjectHealth.resolveHealthProjectPath, FcsBridge.CompileProject / ProjectOutline) accept string option and return a clear "projectPath is required. Either pass it or call set_project first" error when neither path nor active project is available. Reduces per-call token tax for multi-call agent flows. (#105)

0.5.6 - 2026-05-18

Added

  • set_project response surfaces loadedProjects and readiness signals so agents can tell what was warmed and which downstream tools will work. (#106)

    • loadedProjects: string array.fsproj paths discovered for the requested workspace. Single .fsproj[path]; .slnx / .sln → all .fsproj entries that exist on disk.
    • readiness: { lsp: bool, projectOptions: bool, symbolIndex: bool }:
      • lspworkspaceLoad notification received.
      • projectOptions — FCS can load options for the first .fsproj (probed via Ionide.ProjInfo in Program.fs).
      • symbolIndex — a non-empty workspace_symbol response observed since the last set_project (sticky flag, reset on LSP restart).

    Resolves the "agent has no way to tell what was warmed" gap from #100.

Changed

  • Internal refactor: .sln / .slnx parsing extracted to FsLangMcp.ProjectFiles.SolutionParsing (fsprojsFromSln, fsprojsFromSlnx, listProjects). Was duplicated between ProjectHealth and the new SetProject path. +6 tests for SolutionParsing. 191/191 passing.

0.5.5 - 2026-05-18

No user-facing changes. Bundles post-0.5.4 follow-ups.

Changed

  • LspResponseShape extracted into a pure module with 13 unit tests exercising lspState / symbolIndexReady / diagnostics response shapes. Bridge methods are now trivial wrappers; the full response-building path is unit-testable without spinning up FSAC.
  • CI coverage gate recalibrated from un-measured 55% to baseline-aligned 54% (current run measures 54.8% on FsLangMcp.dll only). The 55% threshold was set conservatively without measuring the actual coverage of the new test layout; 54% leaves a 0.8% buffer below current to catch real regressions. Raising the gate back is tracked as part of integration-test work.

0.5.4 - 2026-05-18

Three LSP-readiness issues closed (#102, #103, #104); all response shapes additive (status / result preserved, new fields appended). 172/172 tests pass.

Fixed

  • workspace_diagnostics returns lspState + diagnosticsFileCount so an empty payload during warm-up is distinguishable from "all clean". Previously an empty {} could mean either. (#102)
  • workspace_symbol returns lspState + symbolIndexReady. An empty result within 3s of workspaceReady=true is flagged as symbolIndexReady=false so agents can tell "no matches" from "index still building". (#103)
  • fcs_type_at_position tool description corrected. Now states explicitly that set_project / projectPath / projectOptions is required for resolved types and that the file at path must exist on disk. The previous description oversold standalone capability. (#104)

0.5.3 - 2026-05-08

Added

  • project_health accepts .sln and .slnx as projectPath. Resolves to the single .fsproj inside (error if 0 or > 1). workspacePath is normalized to its parent directory when a solution file is passed, so FCS does not consider source files outside the workspace. +5 tests covering .slnx → ready, .sln → ready, .slnx multi → blocked, .sln multi → blocked, workspacePath file → normalized to directory.

Security

  • Overrode transitive Nerdbank.MessagePack to 1.1.62 to address GHSA-2cwq-pwfr-wcw3. The entire 1.0.x series is vulnerable; 1.1.62 is the patched line. StreamJsonRpc 2.24.84 pins the transitive dep to 1.0.2 — a direct reference at 1.1.62 overrides it.

0.5.2 - 2026-05-06

Fixed

  • textDocument_codeAction no longer raises InvalidOperationException on every call. LspBridge.CodeAction was attaching the same JsonNode instance as both range.start and range.end of the request payload — the second attach failed because each JsonNode has a single Parent. Build two distinct position nodes. Originally caught in PR #43; landed in #98. (P1)
  • Cursor.tryDecode returns a structured Error for non-object JSON payloads ([], [1,2,3], "x", 42, true, null) instead of escaping as InvalidOperationException. Originally caught in PR #84; landed in #98.
  • fcs_project_outline rejects pathological pagination args up front. maxFiles=0 previously emitted empty pages with truncated=true and a non-advancing nextCursor — a non-terminating loop for cursor-following clients. maxResultsPerFile<0 was similarly silent. Both now raise ArgumentException immediately. Originally caught in PR #84; landed in #98.
  • validateSourcePath accepts text = Some "" as a valid unsaved-buffer state instead of forcing File.Exists on the path. Restores the new-empty-file editor flow. Originally caught in PR #83; landed in #98.
  • fsharp_runtime_status survives concurrent FSAC restarts. Process.HasExited could throw if the handle was disposed by a parallel set_project / stop. Wrapped in a small live-check helper that treats any read failure as "no live child", returning an empty children list rather than failing the whole diagnostic call. Originally caught in PR #85; landed in #98.
  • publish.yml workflow_dispatch fails fast on non-tag refs. Manual dispatches from the default branch produced PackageVersion = "main" and an opaque dotnet pack failure several minutes into the run. An early guard step now refuses anything that isn't a v* tag. Originally caught in PR #89; landed in #98.

Changed

  • Test project layout: linked-source <Compile Include="../../*.fs"><ProjectReference> with [<assembly: InternalsVisibleTo("FsLangMcp.Tests")>] baked into the production fsproj (per ADR-0001 in #95, implemented in #97). Closes the linked-source incidents that affected Stryker (#88) and the XPlat Code Coverage collector (#92). User-facing impact: the published nupkg now carries an InternalsVisibleTo metadata attribute referencing the test assembly — informational, no behavior change. Coverage gate recalibrated 70% → 55% to reflect the genuine production-only baseline (the pre-migration 78% was inflated by self-instrumented test code).

0.5.1 - 2026-05-06

Changed

  • Workstation GC + Concurrent GC defaults baked into the published tool via runtimeconfig.template.json. The default Server GC commits memory aggressively and only releases under OS pressure — a poor fit for stdio MCP servers, which are bursty (load → idle → next FCS request) and rarely trigger pressure events on dev laptops. Workstation GC returns committed memory promptly while idle, dramatically reducing apparent RSS growth without measurable throughput impact at typical agent request rates. Operators can opt back into Server GC with DOTNET_gcServer=1 (env always wins over runtimeconfig). Aligned with FsMcp 1.1.0's runtime-tuning guide.
  • Bumped FsMcp.Server and FsMcp.TaskApi from 1.0.1 to 1.1.1. No source changes required; build passes with 0 warnings at WarningLevel 5. New surface in 1.1.x (resources/subscribe, subscribeToolsChanged, HTTP RunSessionHandler) is not consumed by FsLangMCP — our tool list is static and we run on stdio only.

0.5.0 - 2026-05-06

Added

  • fsharp_runtime_status — observational MCP tool reporting runtime, FCS checker config, cache stats, child processes, and assembly counts. Useful for diagnosing FCS resource pressure during long agent sessions. (#79)
  • Cursor pagination across project-scoped tools — opaque, Base64-encoded cursor returned as nextCursor alongside truncated / pageOffset / pageSize / totalEstimate. Adopted in:
    • fcs_project_outline (paginates files) (#78)
    • fcs_project_symbol_uses (paginates uses) (#80)
    • fcs_find_symbol project-wide variant (paginates symbol groups) (#80)
  • memberCounts map per file in fcs_project_outline summaryOnly mode — top-level { kind: count } reflecting the full per-file outline, independent of maxResultsPerFile. Replaces the unreleased _summary sentinel node. (#82)
  • Filter args for fcs_project_outline: filter (regex with NonBacktracking + 250 ms timeout) and nameContains (substring list). Applied before truncation. (#78)

Changed

  • fcs_project_outline defaults are now conservative: summaryOnly = true, maxFiles = 50, maxResultsPerFile = 30 — keeps responses inside typical 25–50k token windows on ~50-file projects. Callers wanting full detail must opt in. (#78)
  • Tool descriptions sharpened to be agent-facing: clearer scope, when-to-use guidance, and prerequisites across the FCS tool surface. (#77, #81)
  • Internal: Cursor.paginationFields takes a unitName parameter so each tool's totalEstimate carries its own paginated unit (files / uses / symbols).

Removed

  • The synthetic { "kind": "_summary", "memberCount": N } node previously appended inside fcs_project_outline summaryOnly entries. Not breaking for any released consumer — the sentinel was introduced in #78 between 0.4.0 and 0.5.0 and never shipped to NuGet. (#82)

[0.4.0] - 2026-05-04

  • Added agent-friendly navigation tools: fcs_file_outline, fcs_project_outline, fcs_find_symbol, fcs_symbol_at_word.
  • Added fsharp_project_inspect for read-only .fsproj structure, compile order, references, and .fsi/.fs pairing.
  • Added shared project file filtering for generated/build/test artifacts used by project-wide scans.
  • Added fsharp_signature_data as a structured FSAC signature-data helper.
  • Improved set_project workspace selection for directories: single solutions/projects are selected explicitly; ambiguous directories return candidates instead of guessing.
  • Removed textDocument_hover from the exposed MCP tool surface; use fcs_symbol_at_word or exact-position FCS helpers instead.

[0.3.1] - 2026-05-04

  • Updated FSharp.Compiler.Service to 43.12.203 and aligned the implicit FSharp.Core package version to 10.1.203.
  • Centralized FCS / FSharp.Core package version settings in Directory.Build.props.
  • Adjusted signature-help type formatting for the latest FCS API shape.
  • Kept fsharp_compile on the FCS project typecheck path introduced in 0.3.0.

[0.3.0] - 2026-05-04

  • Added project_health for read-only project/tooling preflight.
  • Added fsharp_compile as an FCS-backed project parse+typecheck tool using FSharpChecker.ParseAndCheckProject.
  • Removed reliance on the unavailable fsautocomplete 0.83 fsharp/compile endpoint and the dotnet build fallback.
  • Added F# analyzer wiring and just analyze.
  • Added tests for project health, compile validation, cache behavior, and invalid compile inputs.

[0.2.0] - 2026-04-09

  • Breaking: all LSP proxy responses now wrapped in {status, result} envelope. (#39)
  • Added fcs_get_project_options tool wrapping proj-info. (#27, #37)
  • Migrated from Newtonsoft.Json to System.Text.Json. (#13, #43)
  • Performance: offload LoadProjectOptionsFromFsproj to Task.Run. (#32, #36)
  • Refactor: split Program.fs into logical modules. (#22, #40)
  • Structured ToolError DU with errorKind field. (#20, #34)