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
findnow 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 returncursor_query_mismatch; source, position-symbol, solution-membership, coverage, or result changes returncursor_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 byfind; all other tools retain their legacy minting while rejecting taggedfindcursors. Tokens contain only version/tool/offset plus SHA-256 identities, retain no per-cursor server state, and make no atomic-filesystem- snapshot claim. Oldfindcursors returncursor_version_unsupported; discard them and repeat the original request withoutcursor. Other tools keep their existing cursor formats.- Unchanged
findcontinuations 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
checkacceptssnippetPosition="start"|"end"and reports its effective placement. Omitted/null placement staysend, 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
findandproject_healthpreserve every declared F# solution member, including missing.fsprojpaths. Missing members count as requested evidence but never as swept projects; absence remains indeterminate until the declared solution is loadable (#165).finduses 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).findcaps 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
Compileitems only when external linked files are explicitly enabled and the item carriesLinkmetadata; 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).
checkdiscovery 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.100as the supported floor and useslatestFeature, so contributors may use any compatible stable .NET 10 feature band. CI still verifies the floor independently and pins release/runtime validation to10.0.400(#246). - CI records bounded VSTest hang diagnostics and uploads
TestResultsafter 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-15after themacos-latestimage 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
findnow separates project-sweep coverage from delivery of the complete site set (#235).coverage.completecan remain true when every project was analyzed, butresolution.completeis true only when the response contains the whole result set from offset zero. AmaxResultspage and every later cursor page therefore remain visibly incomplete even when the final page hastruncated=false;totalEstimate.sites,truncated, andnextCursordescribe how to retrieve the omitted sites.- FCS/LSP admission waits now observe MCP cancellation instead of starting cancelled handlers later (#236). For
findandcheck, time spent waiting for the FCS gate consumes the sametimeoutMsbudget 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 untilParseAndCheckProject/GetAllUsesOfAllSymbolsreally finishes, so timed-out callers cannot accumulate distinct-key FCS workers in the background. Admission expiry is a typedfcs_admission_timeoutpayload, and a negative timeout is rejected before queueing instead of becoming an accidental infinite wait. Per-project admission rejection is reported as retryablestatus="busy"/errorKind="fcs_worker_busy", with a separateprojectsBusycount, rather than being folded into a permanent-looking project failure. fcs_tests_for_symbolnow reports bounded, coverage-aware solution sweeps (#237). It acceptstimeoutMsand cursor pagination, separates requested/scanned/failed/timed-out/busy projects with aperProjectledger, and returnsunknown/indeterminatefor an incomplete zero. A production.fsprojtarget 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_outlinenow appliesfilter/nameContainsbefore file pagination (#238). Files with no matching entries are omitted,maxFilesand cursors operate on the matching-file set, andtotalEstimate.filescounts 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 nowpartialwith a boundedfilterCoverageissue 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_outlinenow has one admission-aware end-to-end deadline and a general partial- coverage ledger (#243). Optional non-negativetimeoutMsdefaults 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-WaitAsyncrace and fault path). Responses useok/partial/unknownwith requested/scanned/timed-out/failed/not-started counts plus bounded phase/issues rows. Incomplete filtered discovery suppressesnextCursorand marks its estimate as a lower bound requiring a restart from offset zero.- A direct
fcs_project_outlinecall on a test project now includes its evaluated compile sources by default (#239), including ordinarytests/.../Program.fsandTests.fsfiles. The shared filter distinguishestest_sourcefromtest_result_artifact;TestResults,test-results, coverage,bin, andobjartifacts remain excluded, and explicitincludeTests=falsestill opts out of test sources. fcs_tests_for_symbolnow 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). CompatibilitytestCountandsiteCountboth count all reference sites; additiveuniqueTestCountcounts 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_impactnow preserves the active solution as a separate discovery context when an explicit production.fsprojis supplied. It propagates test coverage/outcome and pagination metadata, returnsstatus="partial"for incomplete evidence, and labels page-derived file/project counts as lower bounds.crossProjectstays 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 forProject/; filesystem roots remain valid workspaces. Test-source filename detection also avoids false positives such asContest.fs,Latest.fs, andProtest.fswhile preserving conventionalFooTests.fsand delimited_test/-testnames. - The FsMcp-to-official-SDK adapter now forwards structured
TransportErrorJSON byte-for-byte instead of wrapping it in F#%Adebug syntax (#242). TypedInvalidArgs,NotReady,FcsAborted, and infrastructure envelopes therefore remain directly machine-readable inisError=trueMCP responses. find(kind="members", query="Resolve", member="Resolve")now treatsqueryas matching either the requested member or its declaring type (#219). The documented member-name form previously appliedqueryonly to the declaring entity, so four real internal-member calls could produce a 43-secondnot_found; the declaring-type form remains supported.- Long-lived global-tool processes now fail with a typed
stale_tool_processenvelope before entering ProjInfo whendotnet tool updatehas 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_outlinenow 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 bothmaxResultsand 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 withdownstreamProjectsChecked=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.fsprojis rejected instead of returning a misleading one-project workspace verdict.checknow preserves typed blocking causes when a semantic verdict isunknown(#244). An unavailable exact SDK pin remainsverdict="unknown"/groundTruth=false, whileblockingReason.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.findnow attaches the same SDK evidence to its existingsdk_not_foundper-project row, and SDK roots reported bydotnet --list-sdksare preserved.fcs_nuget_membersnow recovers exact method accessibility from ECMA-335 metadata when available, keeps protected members in the default surface, reportsisAbstract, and emits generic constraints both insignatureand structuredgenericParameters(#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_statusnow 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_healthno longer reportstests.status="no_test_projects_found"when reverse test discovery actually FAILED (review finding on #171). Discovery evaluates every candidate.fsprojunder 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 backProjectEvaluationBusyException, and each of those failures was silently dropped into "no test project exists". The payload now separates the two:tests.discoveryCompleteis always present, a candidate that could not be evaluated is listed intests.unevaluatedProjectswith its own reason plustests.unevaluatedProjectCount, and a sweep that found nothing while some candidate failed reports the new statustest_discovery_incompleteinstead of claiming absence.no_test_projects_foundnow means what it says: every candidate was read, and none of them referenced this project.set_project'snot_warmed/warmingreadiness hints no longer promise that "find and check are unaffected" by the FSAC symbol index (review finding on #201).checknever consults the index, and neither does anyfindsite the FCS sweep produced — butfindis precisely the consumer of the index fallback: on a sweep with zero hits it probesworkspace/symboland reports the match asvia="fsac-symbol-index". While the index is cold that confirmation is unavailable, so a zero-hitfindcan read asnot_found. The hints now scope the assurance to FCS-derived results, namefind's zero-hit fallback as the affected path, and point at thefsacFallbackState/fsacFallbackReasonfieldsfindalready returns. Docs corrected to match —docs/tools-detailed.mddescribed the fallback and then claimedfindnever 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 andfsautocompletedied with the opaque connection-loss error the pre-flight exists to replace.set_projectnow drops the cached list before its check, onedotnet --list-sdksnext to the FSAC restart and workspace load it precedes. Lazy per-project starts keep using the cache. find'sincludeSiteTypesno 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.fslinked into several.fsprojfiles 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 keepssiteTypeand theprojectlabel that produced it, every other answer is listed in the new per-rowsiteTypeAlternatives— each entry naming the type and the projects that resolved it, so a contested site can be planned per project — andsiteTypes.typedDifferentlyByAnotherProjectcounts those rows (a subset oftyped— thetyped + degraded = fieldSitesidentity is unchanged). The column is bounded so it cannot blowfind'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 assiteTypeAlternativesOmitted/projectsOmitted/siteTypes.alternativesTruncatedRows. Single-project sweeps are byte-identical to before.fcs_nuget_types/fcs_nuget_membersno longer answer for a package that is only referenced under a different target framework (review finding on #203).project.assets.jsondescribes every target the project restores whileEnsureProjectResultsevaluates 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'stotalDiagnosticsnow explains itself instead of silently disagreeing witherrorCount + warningCount(#190).totalDiagnosticscounts 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 nonzerototalDiagnosticsalongsideerrorCount: 0, warningCount: 0as hidden findings and burned time hunting for them. Two new fields close the identity:infoCount(sototalDiagnostics = errorCount + warningCount + infoCount, always) andbelowSeverityFloorCount(how many of those full-set diagnostics theseverityfloor excluded from thediagnosticsarray — never the 50-item cap, which staysdiagnosticsTruncated's job), plus adiagnosticsNotepointing atseverity="all"when the gap is nonzero. Also fixesfcs_check_file's internalerrorCount(an internalFcsBridgemember since the 0.11.0 tool consolidation, not a registered tool — no consumer could have observed this), which was silently always0: it read the JSONseverityfield as an int, but that field is serialized as text ("Error") — the read never matched.check'sscope='workspace'perProjectentries now carryinfoCounttoo (#205, deferred from #190), so a member project's ownerrorCount + warningCount + infoCountidentity holds per-project, not just at the workspace total — the same genuine tally (countInfoDiagnostics) used everywhere else, not atotal - error - warningremainder.fcs_nuget_types/fcs_nuget_membersnow resolve a NuGet package id whose assembly is named something else. Both matched the assemblySimpleNamealone, soMicrosoft.Orleans.Core.Abstractions(which shipsOrleans.Core.Abstractions.dll) resolved to zero assemblies and returned an empty — butstatus: "ok"— payload that reads as "the type does not exist" (#191, reported from the field in #100).packageIdnow accepts either spelling, resolved through a packageId → assembly map built from the project's own restore output:obj/project.assets.jsonwhen present, otherwise derived from the-r:reference paths under the NuGet global-packages cache (honouringNUGET_PACKAGES). Prefix matching remains rejected in both directions —Systemstill does not matchSystem.Text.Json, andNewtonsoft.Json.Schemastill does not fall back toNewtonsoft.Json. Side effect: a package that ships several assemblies (e.g.TypeShape→TypeShape.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 carrieshint(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) andcandidatePackages(up to 5{ packageId, assemblies }entries from the restore graph whose id or assembly names relate to the query).fcs_nuget_membersadditionally emits ahintwhen the assembly resolved but exports no such type, naming the assembly searched and pointing atfcs_nuget_types. Success-path responses are unchanged — the fields are absent on a hit.set_project'sreadiness.symbolIndexStateno longer claimswarmingforever when no warm signal from the FSAC symbol index is ever observed (#194).assessSymbolIndexalready compared elapsed-since-workspace-ready against a warm-up window for the internal FSACworkspace/symbolprobefindfalls back to, butsetProjectReadinessnever saw that signal. It now reuses the sameworkspaceReadyAttimestamp and warm-up window: past the window with no non-empty index result observed yet, the state readsnot_warmedwith a hint thatfind/checkare 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.warmingstill applies within the window; the booleansymbolIndexfield and the other states are unchanged.An unsatisfiable
global.jsonSDK pin no longer reducesset_projectto an opaque transport error (#192). When the nearestglobal.jsonpins a version withrollForward: "disable"and that exact SDK is not installed, bothIonide.ProjInfo'sInit.init(in-process) andfsautocomplete(which calls it during its own startup, with nothing to catch the failure) die on thedotnet --versionexit-155 the muxer returns. The FSAC child died before answeringinitialize, 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_projectnow screens the pin before any MSBuild evaluation or FSAC process exists and returns{ status: "infrastructure_error", errorKind: "sdk_not_found", … }carrying the requested version, theglobal.jsonpath, 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 fromdotnet --list-sdkscan fail it. Any other policy, ansdk.pathsredirect, an unreadableglobal.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 answeredstatus: "ok"withreadiness.projectOptions: false. It now answers with the typed error, because the project genuinely cannot be evaluated.
Changed
findnow reports five distinct record-field site kinds instead of three (#207).kind=fieldsites are taggedfield-set-literal|field-set-update|field-set-mutation|field-pattern|field-read, with matchingfieldSetMutation/fieldPatterncounters added tobreakdown. Each shape needs a different edit when the field's type changes. Behaviour change:x.Field <- vand| { Field = x } ->used to be reported asfield-read— the first of those labelled a write as a read. Existingbreakdownkeys keep their meaning and the new ones are additive, butfieldReadnow 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_impactcompares only against"definition"), andfcs_record_field_audit's internalformcontract (literal|with-update|unknown) is unchanged.
Added
findgains a field-impact mode for planning a record-field type change from the tool output alone (#207). TheincludeSiteTypeshalf is below; the site reclassification half is a behaviour change, filed under Changed.includeSiteTypes(defaultfalse). When true, every record-field site row additionally carriessiteType— the field's type as the CURRENT typecheck resolves it at that site, rendered with that site's ownopens — so an agent reads the "old type" per row instead of opening each file. AsiteTypesledger (typed + degraded == fieldSites, counted over the whole matched set, not just the page) and asiteTypesNoteaccompany it. A site whose type FCS cannot produce, or one reached after thetimeoutMsbudget is exhausted, getssiteType: nulland is counted as degraded — a per-site miss never fails the call. Annotates field sites only; akindthat produces none says so in the note rather than going silently no-op.siteTypeis capped at 200 chars, keeping a full default 80-site page under the MCP payload ceiling without loweringmaxResults.- Explicit non-goal, stated in the docs and in every response note:
siteTypeis never the type the field would have after the edit. That requires compiling the modified code — edit the enumerated sites, then runcheck(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.
findnow reports a top-levelscopeNoteon every response that completes a sweep (succeeded,partial, andunknown), naming how many projects were actually swept (projectsSwept) and the recipe to change it: pass the solution's.sln/.slnxasprojectPath(orset_projectit) withscope='workspace'to widen from one project to every member project, or pass a single.fsprojasprojectPathto narrow from a solution sweep to one project. Sweep breadth itself is unchanged —scope=auto/workspacealways sweep every member project the resolved target has,scope=file/projectalways 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 forscope='file'it names the additional file-level filter, not just the sibling-project blind spot (#193).fcs_public_apinow closes a page on a shared 45,000-character response budget in addition to the existingmaxResultstype-count cap, so a handful of API-dense types (long member lists) can no longer produce an over-budget page just becausemaxResultsstill 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 additivetruncatedByBudget: truefield distinguishes a budget-close from a count-close; either way, whenever a page closes early with entities left over, an additivehintnamesnamespaceFiltertogether 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_outlineshares the same budget: asummaryOnly=falserequest whose full per-member entries would cross it now downgrades to the header-only shapesummaryOnly=trueproduces, flagged by an additivedowngradedToSummary: trueplus ahintnamingmaxResultsas 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.classifyFiletested the obj/bin segment before the generated-file branches, and generated F# sources (*.AssemblyInfo.fs, the SDK's*.AssemblyAttributes.fsstub) essentially always live underobj/, so the flag was unreachable by construction (#186). Files are now classified by what they are before where they live,*.AssemblyAttributes.fsis recognized as generated, and when excluded by default these files reportassembly_info_fileinstead of the uninformativeobj_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 bareletsnippet isclean, noterrors— duplicates arriving via both the parse and check halves are collapsed, diagnostics attached to other project files no longer leak in, andfilereads"snippet"instead of a temp path the caller never created.
Changed
Dependency sweep:
Nerdbank.MessagePack1.2.30 → 1.2.36,Microsoft.NET.Test.Sdk17.12.0 → 18.9.0,xunit.runner.visualstudio3.0.2 → 4.0.0, and the pinnedfsharp-analyzersruntime tool 0.36.0 → 0.37.2.Microsoft.VisualStudio.Threading.Onlydeliberately stays at 17.14.15: StreamJsonRpc 2.25.29 (latest) binds that exact assembly version, and moving it reintroduces the #121 startupFileNotFoundException.fcs_analyzer_setup_previewsnippets no longer recommend analyzer versions older than the ones this project pins. The grouped analyzer bump movedG-Research.FSharp.Analyzersto 0.23 andIonide.Analyzersto 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>(andFSA_PROJECT_PATH) runs the same project-load/readiness pipeline asset_projectbefore MCP starts accepting requests.fslangmcp --versionreports 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.matchedis now nullable. It isfalseonly after every requested FCS project completed; an incomplete sweep with no hit isstatus="unknown",outcome="indeterminate", andmatched=null. Positive partial results remain actionable and advertise incomplete coverage.Migration:
check(speed="fast")returnscleanonly 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 returnsunknown; current errors remain visible even when the rest is incomplete.Migration: a cross-project
set_project(restartLsp=false)leaves the existing context untouched and returnsstatus="restart_required"; retry withrestartLsp=trueto switch.project_healthand 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
PackageReferencenow uses a true exact NuGet range ([x.y.z]; barex.y.zwas 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-updateregenerates the locks without selecting a newer version on its own.The build SDK is pinned to
10.0.400with roll-forward disabled. External runtime tools are pinned to fsautocomplete0.83.0, Ionide.ProjInfo.Tool0.74.2, and Fantomas7.0.5;--bootstrap-toolsinstalls 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
findno 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.findrejects unsupported enum values, invalid ranges, unsafe scope/path combinations, and non-advancing pagination sizes with structuredinvalid_argsresponses 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_symbolfrom 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.fileGlobpatterns are evaluated from the selected workspace; a glob matching no evaluated source file cannot produce a falsecleanresult.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_readyuntil the rebased FSAC publication is available; they can no longer reuse an old empty payload asclean.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
setsiduse 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.versionnow preserves the exact product SemVer (0.14.0, not the four-component0.14.0.0normalized 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-authoredDomain\Money.fsresolved to a literal/workspace/Domain\Money.fs, sofcs_project_outlineerrored on every subdirectory file,fcs_review_scansilently scanned only root-level files, andproject_healthreported subdirectory files as missing (#160, reported by @PlandriousX).<ProjectReference Include>paths reachfsharp_project_inspectthrough MSBuild evaluation and are normalized by the engine itself; a regression test pins that behaviour.fcs_review_scanproject mode no longer claimssucceededover a partial file set: in-scope compiled files that cannot be found on disk are listed in a newunresolvedFilesresponse field and downgradestatustopartial. Filter-excluded entries (generated, obj/bin, tests) are deliberately not counted — their absence is expected pre-build in parse-only mode; the unfiltered view remainsproject_health.missingFiles(#160).fcs_project_outlinereports the same aggregateunresolvedFileslist over all in-scope files, so missing files stay visible even when the per-fileoutlineStatuserrors are on another page (#160).
0.13.2 - 2026-08-10
Added
fsharp_runtime_statusnow reports OS-visible process threads plus .NET thread-pool, pending-work, and completed-work counters underprocess.threads, making long-session growth measurable without attaching a profiler.set_project.readinessnow explains an unwarmed symbol index through additivesymbolIndexStateandsymbolIndexHintfields.lspRestartRequestedrecords intent, whilelspReplacedExistingProcessreports whether an existing FSAC process was actually replaced. The legacylspRestartedfield keeps its request-mirroring behavior for patch-release compatibility.project_healthreports 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.OnChangerecursion 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. EnsureProjectResultscache 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
InvalidArgumentwhile preserving.fs,.fsi, unsaved-buffer, and.fsxscript-inference flows. project_healthuses 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_apipreserves generic arguments in readable signatures (for examplestring option,string list, and user-definedBox<string>), so incompatible generic surfaces no longer compare as identical (#100).
Changed
fcs_dead_codemoves the repeated removal warning to one response-levelverificationHint; the existing candidatenotestring 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
findandcheck(#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 invalidwhile the logger provider is constructed, so the server failed at startup. As aDotnetToolpackage 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.TryRemovenow 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.
TryRemovedropped the dictionary entry but left the key in the eviction queue, and eviction only fires ondict.Count >= maxSizewhile 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 toprojectResultsCache— declared capacity 3, values of typeFSharpCheckProjectResults, 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_projectcan no longer dispose an RPC whilefind, 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.findnow enforces its timeout withTask.WaitAsyncaround 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 afterset_project.
Changed
- LSP concurrency is fixed at one.
FSLANGMCP_MAX_CONCURRENT_LSPis no longer honored because FSAC exposes one mutable workspace; FCS concurrency remains configurable withFSLANGMCP_MAX_CONCURRENT_FCS. - Added timeout settings:
FSLANGMCP_LSP_STARTUP_TIMEOUT_MS,FSLANGMCP_LSP_REQUEST_TIMEOUT_MS,FSLANGMCP_PROJ_INFO_TIMEOUT_MS, andFSLANGMCP_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
findtimeout now covers the whole sweep, including the symbol-use walk. In 0.12.2 the per-project cancellation token bounded onlyParseAndCheckProject; the synchronousGetAllUsesOfAllSymbols()re-walk ran afterwards outside the token, so a type-check that finished near the deadline could let the use-walk overrun the advertisedtimeoutMswith 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 basedConfiguredon analyzerPackageReferences scanned from the.fsprojonly. Projects that centralize analyzer packages in an MSBuild import (Directory.Build.props/.targets, orDirectory.Packages.propsviaGlobalPackageReference) were wrongly reported as un-configured, sofcs_analyzer_diagnosticsskipped the CLI andproject_healthreported 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
findno longer hangs indefinitely on a large/cold multi-project solution (#100). A coldParseAndCheckProjecton 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 (findargtimeoutMs, default 120 s): each project is cancelled at the remaining budget and surfaces as aperProjecttimeout entry, so the call always returns a partial result with an actionable reason instead of blocking. Verified on the reporter's 12-projectSailer.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_diagnosticsno longer deadlocks on a chatty analyzer run.runAnalyzersredirected thefsharp-analyzerschild'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 failedset_projecthandshake. If theinitialize/fsharp/workspaceLoadRPC 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_membersnow work in solution mode. Afterset_projecton a.sln/.slnx, a bare call defaultedprojectPathto the solution file, whichWorkspaceLoadercannot 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_symbolno longer over-reportprojectsScanned. 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.projectsScannednow counts only successful sweeps; a newprojectsRequestedfield shows the total, so a gap is visible.findwithincludePerProject=falsestill surfaces per-project sweep failures. The 0.12.1 opt-out omitted the wholeperProjectarray — 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_diagnosticsno longer runs the analyzer CLI on projects without analyzers, and no longer reportsrun_failedspuriously. It ran the CLI on every project whose.fsprojmerely parsed; an analyzer-less project's run error could flip a genuinely-clean configured project torun_failed. The CLI now runs only on projects with a real analyzer package reference.set_projecton a non-existent path returns the{status:"invalid_args"}envelope instead of throwing a rawArgumentException(matching every other validation in the handler).project_healthin solution mode no longer silently ignores a requestedcompileCheck. It returnedstatus:"ok"while never running the check; it now reportscompileStatusas 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.Configurednow requires a real analyzerPackageReferencerather than the mere existence of aDirectory.Build.*/.editorconfigfile (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
findnow resolves a module-qualified (dotted) query (#100). A query likeRoles.appRoleforApp.Roles.appRolepreviously matched nothing and returned a silentmatched:false— which reads like "symbol doesn't exist" and can mislead a dead-code/refactor decision.symbolMatchesnow 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 ahintpointing at the bare identifier.project_healthunderstands solution mode (#100). Pointed at a.slnx/.sln/directory with multiple projects it previously returnedoverall:"blocked"("Multiple .fsproj found"), which read as an error. It now returnsreportKind:"solution"withtoolingReadiness.overall:"solution"and asolutionsummary (projectCount+ per-project{name, fsproj, exists}) plus a hint to pass one.fsprojfor full readiness. Single-project inputs are unchanged.
Changed
findtrims per-project noise (#100). TheperProjectbreakdown 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);projectsSweptstill reports the full sweep breadth. NewincludePerProjectarg (defaulttrue) drops the array entirely when set tofalsefor 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 withcheck.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. Complementsfind(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 beforetextDocument_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 rawtextDocument_codeActionleaves 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 asFS0039"not defined" though it exists. Correlates eachFS0039with a later-compiling definition and reports the reorder fix. Use whencheckreports "not defined" to tell an order problem from a missingopen.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 (includeInternaladds internals);namespaceFilter+ pagination.fcs_refactor_impact(#71) — preview a change's blast radius + a verify checklist without editing: orchestratesfind(cross-project use sites),fcs_tests_for_symbol,fcs_check_compile_orderandfcs_public_apiinto{ target, impact, tests, compileOrder?, apiSurface?, verify[] }.kind=rename|signature|move|delete|auto. Use before a rename/move/delete; pair withfcs_rename_previewfor the exact edits.fcs_signature_status(#74) — report the.fsi-vs-impl public-surface gap for one.fswithout editing: type-checks the impl with its sibling.fsistripped, then diffs — members public in the impl but missing from the.fsi→missingFromSig;.fsientries with no impl match →staleInSig, each with asignaturePreview. Use for.fsidrift; preferfcs_public_apifor 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 withfindbefore 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.fsfile 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.fsfile; pair withfcs_check_compile_orderafter.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 likeproject_health, runs thefsharp-analyzersCLI when available and parses its SARIF; none configured →no_analyzers.severityfilters;projectPathfalls back toset_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, theFSharpAnalyzersOtherFlagsproperty, a local manifest), and emits each gap as an exact XML/JSON snippet + reason. Pair withfcs_analyzer_diagnosticsto read diagnostics after.
Fixed
- FSAC
initializehandshake now declarescodeActionLiteralSupport/publishDiagnostics/synchronizationclient capabilities — without them FSAC returned null code-actions and pushed zero diagnostics, which also silently degraded the existingtextDocument_codeActionandworkspace_diagnosticstools. (#53) check(project/workspace) no longer returns a wall of spuriousFS0039"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 returnsverdict: "unknown"+ a "run dotnet restore/build" reason instead. Independently, the surfaceddiagnosticsarray 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_outlinegainedsummaryOnly(default true) — headers + per-kind member counts only — so a large file no longer overflows the token ceiling and pushes agents back togrep.summaryOnly=falserestores full per-member output. (#100)set_project/project_healthnow surfacerestoreStatus: "unrestored"+ a hint when reference assemblies are missing — so an emptysymbolIndexafterreadyis no longer mistaken for "no symbols" / crypticFSharp.Core not found. (#138)textDocument_codeActionrequest-shape is now regression-guarded — therange.start/range.endJSON nodes are built as distinct instances (guarding the #43InvalidOperationException"node already has a parent") and thecontext.diagnosticsround-trip from #53 is asserted. The request builder was extracted into an internalCodeActionRequest.buildParamsseam to make it directly testable without a live FSAC. (#99)
Changed
- Dropped the redundant
filefrom nestedrangeobjects in site-list responses —find,fcs_project_symbol_uses,fcs_find_member_usages,fcs_record_field_audit, and diagnostics each already carry a top-levelfile, sorange.fileduplicated it (a hotfind= 75 sites × one redundant absolute path each). AddedrangeToJsonNoFileand applied it only where the enclosing object already hasfile. Standalone ranges that are the sole carrier of the path — declaration locations (declarationLocation,declarationRange) and the symbol-at-position map — keepfileunchanged. Response-shape trim only (consumers read the parentfile); 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/checkwere removed (#136). Since 0.10.0 these were already steered towardfind/checkviaPrefer …description hints; #136 completes the consolidation by dropping the registrations. The backendFcsBridge/FsAutoCompleteBridgemethods (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(withkind+scopenarrowing); type-check verdicts for file / project / workspace / inline snippet viacheck; raw per-file symbol extraction viafcs_file_outline; and position- or unsaved-buffer symbol / type lookups viafcs_symbol_at_word.- find cluster (7):
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. TheToolDescriptionSchemaTests(dotnet testgate) andscripts/audit-tool-descriptions.pyschema 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_symbols↔fcs_nuget_types,fcs_nuget_types↔fcs_nuget_members,fcs_signature_help↔fsharp_signature_data).docs/tool-description-schema.mddocuments 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 dedicatedfcs_find_member_usagesgone, agents fell back torgfor "find call sites of a.Member()on a type" in 5/5 trials instead of reaching forfind— its old text under-sold the member-usage intent and still named the deletedfcs_find_symbol/fcs_record_field_audit. The rewrite leads with the four intents (definitions, references, record-field set sites, member call-sites on a typex.Foo) and contrasts against textualrg. A re-test dropped member-usage fallback from 100% → 0% (36/36 trials chosefind), no regression on the other find tasks.
Notes
- Accepted limitations of the consolidated path (carried over from the 0.10.0
find/checkdesign):find/checkdo not accepttext(unsaved buffer content) or rawprojectOptions/workspacePathoverrides — Write the file to disk first, then callfind/check. Position-based and unsaved-buffer symbol / type lookups remain available throughfcs_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 warmfindis dramatically faster (#131). FCS keepsParseAndCheckProjectwarm via its project cache, butGetAllUsesOfAllSymbols()is NOT memoized — it re-walks every recorded symbol use (~16–18k per project on this repo) on EVERYfind, so a warmfindcost the same as a cold one (theprojectCacheSizebump 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): coldsweepElapsedMs11307 → 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 originalParseAndCheckProject+GetAllUsesOfAllSymbolspath — a cachedfindis never staler than an uncached one — andset_project'sClearCaches()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
findsweep 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 OWNSourceFilesmtimes. 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 freshParseAndCheckProjectwould see B's new metadata — workspacefindkept reporting stale cross-project usages until A's file was edited orClearCaches()ran. The key now folds in a referenced-assembly write-time stamp (the-r:/--reference:targets inOtherOptions, each existing file'sLastWriteTimeUtc.Ticks, missing files mapped to-1, never throwing): when B's output (the consumer'sobj/.../ref/B.dllreference 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
findsweep 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.fsfile on disk without immediately rebuilding its output assembly. FCS'sParseAndCheckProjectfor a consumer reads referenced F# project sources directly viaReferencedProjects[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 areferencedProjectSourcesStamp: walksoptions.ReferencedProjects, and for eachFSharpReferencedProject.FSharpReferencecase (the F# P2P case, carrying the referenced project'sFSharpProjectOptions) stamps itsSourceFilesmtimes with the same try/with-resilient approach. The walk recurses transitively (FCS'sReferencedProjectsis NOT pre-flattened — only direct references appear at each level) using aHashSet<string>visited-set keyed onProjectFileNameto avoid re-stamping shared deps and guard against cycles.PEReference/ILModuleReferencecases carry noFSharpProjectOptionsand 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 reportstotalDiagnosticsconsistent with the surfaceddiagnosticslist (#133). Since 0.10.0 the fast path surfaces severity-3/4 (information/hint) diagnostics when the caller raises theseverityfloor, but the response still settotalDiagnostics = ErrorCount + WarningCount, so an info-only snapshot returned a non-emptydiagnosticslist withtotalDiagnostics: 0— list and count disagreeing, unlike the trusted path.totalDiagnosticsis now the full-set count across all severities (mirroring the trusted path'sallDiags.Length): withseverity="all"the surfaced list length andtotalDiagnosticsagree exactly.verdictstays error-based anderrorCount/warningCountstay full-set tallies; the trusted path is unchanged. +1 test.- Release build is now statically compilable (FS3511 in
find's position resolution), sodotnet build -c Release --warnaserroris robust across SDK versions.ResolveQueryAtPosition's five-level-deep match inside atask {}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 dedicatedResolvePositionInFilemember returning plainResult<string, JsonNode>(no awaits needed), leaving the outertask {}with a singlereturnat its deepest branch, which the compiler can reduce statically. Same fix pattern asProjectSweepUsesfrom #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.fsprojof 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-projectfcs_find_symbol/fcs_record_field_auditmiss — on the consolidation validation fixture the single-project path surfaced 1 site where the whole-solutionfindsweep surfaced 11. Barefind(query)suffices (kind=auto,scope=autoover the whole solution); optionalkind(auto|symbol|members|field|definition|position) andscope(auto|file|project|workspace) narrow it. Falls back to the FSACworkspace/symbolindex when the FCS sweep is empty, somatched=falseis 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 singleverdict(clean|errors|unknown) collapsed from a FRESH in-process FCS type-check, so it never reports the stale-{}false-clean thatworkspace_diagnosticscan serve right after anEdit/Write— the false-clean that historically drove agents to fall back todotnet build. Barecheck()covers the active project (scope=auto,speed=trusted); optionalscope(auto|file|project|workspace|snippet),path,snippet(inline source),speed(trusteddefault |fast= cached FSAC snapshot), andseverity.unknownmeans 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 trailingPrefer 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) gainedPrefer check for a yes/no verdict.fcs_record_field_auditwas trimmed before the steer (491 → 497 after; cut theduring widening refactorsclause and aseefiller word) to stay within the 500-charToolDescriptionSchemaTests/audit-tool-descriptions.pyceiling. README,AGENT_INTEGRATION.md, anddocs/tools-detailed.mdnow lead withfind/checkand 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. findnow defaults to compact one-line-per-site output (contextLinesdefaults to0, down from1; defaultmaxResults500 → 80). Each site emitsfile/range/kind/project/symbolFullName/lineTextwith nobefore/aftercontext arrays unlesscontextLines > 0is passed, and thebreakdown+resolutionsummary is always present (the full-set counts are independent of the page cap). Native dogfooding found a barefind(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. PasscontextLines:2to restore the previous richer output.findnow returns a single de-duplicated flatsiteslist — the groupeddefinitions/references/fieldSites/memberSitesbuckets were removed. Every site was previously serialized twice (once in its grouped bucket, once in the flatsitessuperset), doubling the payload — the reason the default page cap had been forced down to 40. Each flat-sitesentry is already self-describing (file/range/kind/project/symbolFullName/lineText), so an agent filters bykindand readsbreakdownfor 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 pagefind("FindArgs")dropped from ~55.6k chars to ~28.4k chars), so the defaultmaxResultswas 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'sFullName, not itsDisplayName. A cursor resolves THE specific symbol under it; keying the subsequent multi-project sweep onDisplayNamematched every same-named symbol, so positioning on oneConfigswept an unrelatedConfigin another namespace (and every same-named overload). The sweep key now prefersFullName, falling back toDisplayNameonly for locals/synthetic symbols that lack a useful one.find(kind=field|members)now honorsexact=falseon 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 — sofind(query="role", exact=false, field="Propose")missedTraderRole.Propose. The declaring-type predicate now threadsexactthrough, matching the symbol branch.check(speed="fast")now surfaces diagnostics at the requestedseverityfloor. The cached FSAC snapshot stored only error-severity (code 1) entries, socheck(speed="fast", severity="warning"|"all")omitted the warnings the caller asked for (and the reportedwarningCountwas inconsistent with the empty list). The snapshot now retains all severities and the fast path filters them by the requested floor;errorCount/warningCountstay full-set tallies andverdictstays error-based.fcs_nuget_membersnow enumerates class fields. The field guard wasIsFSharpRecord || IsValueType, which skipped publicvalfields on reference-type classes (they live only inFSharpFields, notMembersFunctionsAndValues). 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 emptyFSharpFieldsfor classes imported from a C#/IL assembly, so C# instance/const fields remain unrecoverable through the symbol API — now documented indocs/tools-detailed.md.)
[0.9.3] - 2026-06-28
Added
fcs_nuget_members <packageId> <typeName>— member-level companion tofcs_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 sameGetReferencedAssemblies()path asfcs_nuget_types; paginated (default 500, max 2000), returns an emptymatchedTypesmarker (not an error) on no match. Type matching is generic-arity-aware (a bareFSharpOptionresolves 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 thedocs/tool-description-schema.mdaudit.
Fixed
set_projectnow returns a clearinvalid_argsenvelope namingprojectPathwhen the argument is missing or supplied under the wrong key (#100). Previously a wrong/missing key letPath.GetFullPath(null)throwArgumentNullExceptionwhose message named the internalpathparameter (Value cannot be null. (Parameter 'path')), misdirecting callers to the wrong field. The handler now routes throughArgsValidation.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).
StreamJsonRpc2.24.* → 2.25.* (pulls patchedMessagePack2.5.302) andNerdbank.MessagePack1.1.62 → 1.2.30 (clears GHSA-92vj-hp7m-gwcj and GHSA-qjvr-435c-5fjh). PinnedMicrosoft.NET.StringToolsto 18.4.0:Nerdbank.MessagePack1.2.30 drops the transitive StringTools 18.4.0 that had been masking MessagePack's stale 17.6.3 — which lacksSpanBasedStringBuilder.Equals(string, StringComparison)and silently breaks Ionide.ProjInfo's MSBuild project loading.dotnet restorenow 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 inProgram.fsre-audited. Four routing-blockers fixed (descriptions that exceeded the 500-char ceiling and pushed implementation detail into the agent-routing prompt budget):fcs_check_file610 → 452,fcs_find_member_usages581 → 448,fcs_make_internal_visible699 → 419,fcs_type_at_position622 → 451. Mechanics moved to dedicated H2 sections indocs/tools-detailed.md(now covers 9 tools, up from 5). Eight additional descriptions gained explicitprefer X/avoid Ycallouts to close overlap-pair ambiguity (workspace_diagnostics→fcs_check_filestale-cache hint,fcs_referenced_symbols↔fcs_nuget_typessubstring-vs-exact routing,fcs_validate_snippet→fcs_parse_and_check_filewhen-file-exists hint,fsharp_compile→dotnet buildfor IL emission,fsharp_project_inspectover textual.fsprojreads,fcs_project_outlineoverworkspace_symbolfor whole-project overview,textDocument_renameover textual rename).
Added
scripts/audit-tool-descriptions.py+just audit-descriptions. Python analyzer enforcing the schema: parsesTypedTool.defineentries fromProgram.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 indocs/tool-description-schema.md§"Adding a new tool" → step 4 ("Run the length analyzer") to a previously non-existent tool.ToolDescriptionSchemaTests.fsin the xunit suite (+4 tests, 289 → 293 passing). F#-side enforcement sodotnet testis 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 oneprefer X/avoid Ycallout. 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 inProgram.fscross-reference these viaMechanics: 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.mdadded 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 tofcs_nuget_types/fcs_referenced_symbols(v0.7.0, #113) for NuGet type enumeration instead ofdotnet fsi + reflection; points tofcs_validate_snippetwithmode="fsi"(v0.7.0, #112) for.fsisketch validation instead of "untilparse_fsi_sketchlands". Subagent-brief template lists newer tools (fcs_suggest_openfrom v0.9.0 #67,fcs_record_field_auditfrom v0.8.0 #117). "See also" footer linksdocs/troubleshooting.mdanddocs/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.mdadded 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.fsTypedTool.defineregistration, description perdocs/tool-description-schema.md, regression tests). Repo conventions (noCo-Authored-By, F# style, warnings-as-errors viaDirectory.Build.props,ArgsValidation.requireNonBlankat 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.mdadded (102 lines). High-level diagram showing the two-bridge architecture (stdio MCP transport →FsAutoCompleteBridgefor fsautocomplete child process +FcsBridgefor in-process FCS). File-by-file tour in compile order matchingFsLangMcp.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.mdadded (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 (warmset_project≤2s on 10-project solution; coldfcs_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 replacingdotnet buildfor ground-truth project validation, not adding Paket support.- README polish:
- Two new shields.io badges:
nuget/dt/FsLangMcp(downloads total) andbadge/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.
- Two new shields.io badges:
(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 anFS0039 'X' is not defineddiagnostic), returns rankedopendirective candidates from BOTH the project and referenced assemblies (BCL + NuGet). Composes the sameEnsureProjectResults+allEntitiesFromAssemblyprimitives thatfcs_referenced_symbolsandfcs_nuget_typesuse. ResolvesFSharpEntity.AccessPathfor 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
FieldFormClassifierwalker reimplemented viaParsedInput.fold(closes #124). The previous 88-line hand-rolledwalkExpr/walkDeclrecursion only descended into a curated subset ofSynExpr/SynModuleDeclarms — record literals inside class member bodies (SynModuleDecl.Types),for ... doloops (SynExpr.For/ForEach), CE-bind continuations (SynExpr.LetOrUseBang),function | ... ->,while,New,ObjExpr,Lazywere all missed and fell through to the textual heuristic fallback. New 33-line implementation usesParsedInput.fold(the public FCS 43.12+ position-independent whole-tree fold API) with a single match arm onSynExpr.Record; every expression-containing AST node is covered automatically and the implementation has no maintenance burden as FCS adds new node types. ThefallbackHeuristiccallsites informOfare preserved as defensive safety nets for parse failures and any future un-handled node kinds. Schema-compatible:formvalues 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 newdocs/tools-detailed.mdwith 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_definitionwere positive-only ("useful for...") and didn't tell agents when to route to the agent-shaped FCS-side equivalents. Each now explicitly points tofcs_find_symbol/fcs_symbol_at_word/fcs_type_at_positionfor non-IDE workflows. - 15 Args records documented with
///field doc-comments (~74 fields acrossCompletionArgs,PositionArgs,ReferencesArgs,SetProjectArgs,FSharpCompileArgs,ProjectHealthArgs,FcsParseAndCheckArgs,FcsFileSymbolsArgs,FcsFileOutlineArgs,FcsSymbolAtWordArgs,FSharpProjectInspectArgs,FcsSignatureHelpArgs,CodeActionArgs,RenameArgs,RuntimeStatusArgs). Defaults are stated explicitly (verified againstdefaultArgsites 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'sproperties[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.mddocuments 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. ALICENSEfile (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_argsenvelopes, 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### Componentssubsection. - New
## Quickstartsection — 60-second on-ramp fromdotnet tool install -g FsLangMcpthrough--bootstrap-tools, MCP client config, the firstset_projectcall, and aproject_healthpreflight. Each step has a copy-pasteable code block. - New
docs/troubleshooting.mdkeyed by user-visible symptoms:status: "not_ready"afterset_project,FileNotFoundException: Microsoft.VisualStudio.Threading 17.14.0.0(affects ≤ 0.8.1), staleworkspace_diagnosticsafter edit, andfcs_find_symbolzero-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_audittool 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 offcs_make_internal_visible's text guard (v0.8.1) andfcs_record_field_audit'sformOfclassifier (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 coveringset_project.fslangmcpVersion/loadedProjects/readiness,workspace_diagnostics.analyzedAt/mostRecentAnalyzedAt/analyzedAtByUri,fcs_find_symbol.projectDiagnosticsScope, andfsharp_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_auditdescription clarifies optionalpath. Tool description now states explicitly thatpathis optional whenprojectPathis set, and recommends supplyingpath+texttogether 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.classifynow carries a comment above theSigFilearm explaining the intentionally-empty dictionary (.fsifiles have no record-literal use sites), guarding against a future maintainer "completing" the empty branch.RecordFieldAudit.parseFileForFormsgains aPERF TODOnoting the serialAsync.RunSynchronouslycold-cache cost; references the#122follow-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_projectno longer intermittently fails withCould not load Microsoft.VisualStudio.Threading 17.14.0.0in subagent / multi-process hosts. The assembly was on disk; the failure was a transitive bind via StreamJsonRpc 2.24 → SolutionPersistence withrollForwarddefaulting toMinorinruntimeconfig. Fix promotesMicrosoft.VisualStudio.Threading.Only 17.14.15from a transitive to a top-levelPackageReferenceso the bind is satisfied at startup probing rather than at first use of StreamJsonRpc internals, and addsrollForward: LatestMajortoruntimeconfig.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.mostRecentAnalyzedAtis now scoped to thefileGlob-filtered subset. Previously the field returned the workspace-wide max regardless of filter — misleading when callers scoped tosrc/Adapters/*.fsbut got a timestamp fromtests/. Reuses the sameglobMatchespredicate that filtersanalyzedAtByUri, so the timestamp reflects exactly the scoped subset. Backward-compatible whenfileGlobisNone. (#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 throughArgsValidation.requireNonBlank : string -> string -> Result<string, JsonNode>inTypes.fsand return the standard{ "status": "invalid_args", "message": "…" }envelope.fcs_make_internal_visiblegained validation it previously lacked. Behaviour change:fcs_project_symbol_uses/fcs_find_member_usages/fcs_find_symbolon blank required-string args previously raisedArgumentException(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_auditformclassifier now uses an AST walker instead of a textual heuristic. Old heuristic scanned the field's start line + 2 preceding lines forwith; multi-line record updates beyond 2 lines were misclassified asliteral. NewFieldFormClassifierwalksSynExpr.Recordand tags each field-name range based oncopyInfo.IsSome(with-update) vsIsNone(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 —formstill takes"literal"/"with-update"/"unknown". (#122)
0.8.1 - 2026-05-19
Four-iteration reviewer/fixer loop on v0.8.0 (engineering-code-reviewer ↔ engineering-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_visibleno longer risks corrupting string literals containingprivate.FindPrivateSpancould strip the substringprivatefrom inside a regular string literal on a recognized-keyword line —let msg = " private channel"was at risk. Now blocked by a state-machinePositionIsUnsafehelper 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_visiblehandles F# verbatim (@"..."with""escape, literal\) and triple-quoted ("""...""") strings correctly. Previously the classifier only handled C-style escaping. (#118 follow-up)fcs_make_internal_visiblehandles(* doc *) let private foo = 1correctly. Strips when block comment closes before thelet; refuses when the position sits inside an open block comment; refuses on unclosed block-comment openers. Discriminator ispos < i(correct inside-span check), not the tautologicalpos > commentStart.FindPrivateSpanalso peels(* ... *)and[<...>]prefix blocks from thebefore-text so post-comment / post-attribute declarations are recognised. (#118 follow-up)fcs_make_internal_visiblerecognisesandfor mutually-recursive bindings.let rec foo = ... and private bar = ...now strips correctly. (#118 follow-up)fcs_find_symbolsurfaces error-severity diagnostics on zero-match queries instead of returning an empty result that hides project breakage. WhenmatchedFileSetis empty, falls back to whole-project error-severity diagnostics with new response fieldprojectDiagnosticsScope = "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_healthno longer false-positives[<TestFixture>]as a test count. Test-attribute regex gained\bword-boundary so theTestFixturedeclaration doesn't count as oneTest. (#119 follow-up)project_health.binaryOutputPathno longer walks the entire monorepo.findLatestBuildArtifactrestricted to the project'sbin/directory while retaining case-insensitive filename equality so Linux CI with different casing still resolves. (#119 follow-up)Version.currentno longer throwsNullReferenceExceptionon degenerate SDK builds. Null/emptyAssemblyInformationalVersionis 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. Complementsfcs_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/unknownvia line-text heuristic — false positives possible if a comment containswith; replaced with parse-tree classifier in 0.8.2), and 2 lines of source context. Paginated; default 200, max 1000.path+texttogether supports unsaved-buffer audits. Validated on LlmTrader'sTraderRole.Propose(28 caller sites; only 2 were found byfcs_find_symbol) andRiskDebatorRole. (#117)fcs_make_internal_visible— new FCS tool (Variant A only) that drops theprivatekeyword from alet/let rec/module/module rec/type/member/val/new/static/abstract/overridedeclaration 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 recognizedprivatemodifier, returns{ status: "no_action", reason }. Detection is text-driven (FCSIsFromDefinition+Accessibility.IsPrivateproved brittle on module-level let bindings);FindPrivateSpanrequires a recognized declaration keyword beforeprivateand peels off[<...>]attribute blocks. Variant B (auto-addInternalsVisibleToon 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_projectresponse includesfslangmcpVersionas the first field inresult. (#115)fsharp_runtime_statusresponse includes top-levelfslangmcpVersion. (#115)workspace_diagnosticsresponse carries analysis timestamps. Single-file response gainsanalyzedAt; workspace-wide response gainsmostRecentAnalyzedAtandanalyzedAtByUri(per-URIDateTimeOffsetmap). Callers can now self-check "my lastWritewas at T1, theanalyzedAtis T2 < T1, results are stale" without falling back to a build.DiagnosticsTargetrecords the timestamp whenever FSAC pushespublishDiagnostics.forceReanalyzearg + 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_healthper-project enrichment. ReportsisTestProject(xunit / NUnit / Expecto viaPackageReference),testFrameworks,testCount(Fact / Theory / Test / TestCase / TestMethod regex, qualified-name- andAttribute-suffix-tolerant),lastBuildSucceeded,lastBuildAt, andbinaryOutputPath(bin glob keyed off<AssemblyName>with fallback to project basename). Pure file-system + regex; nodotnet testinvocation. (#119)fcs_find_symbolprojectDiagnostics scoping.FcsFindSymbolArgsgainsincludeInfo: bool option(defaultfalse). TheprojectDiagnosticsfield is now filtered to (a) files containing a match forsymbolQueryAND (b) severity ≥WarningunlessincludeInfo=true. New response fields:includeInfo,matchedFileCount,projectDiagnosticsScope: "matched-files". (#114)
Changed
- MCP
serverInfo.versionnow reflects the real product version. Previously hardcoded"0.4.0"— three minor versions stale. Now read fromAssemblyInformationalVersionat runtime via a newVersionmodule (strips any+commitsuffix). (#115)
0.7.0 - 2026-05-19
Three new tools / arg-shape extensions, all additive. 217 → 231 tests.
Added
fcs_type_at_positiongainsfuzzy: bool. Whentrueand the exact position misses, scans ±2 lines / ±5 cols (line-weight 2×) and snaps to the nearest symbol; the response then carriesresolvedLine/resolvedCharacterandfuzzySnap=true.no_symbolresponses always includelineTextandsurroundingLinesso 1-based-vs-0-based mistakes are self-correcting. (#111)fcs_validate_snippet— new FCS tool that compiles an arbitrary F# snippet (.fsor.fsimode) 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), runsParseAndCheckFileInProject, 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.fsisnippet without paired.fsproduces "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 whoseDisplayNameorFullNamecontains the query (case-insensitive). Complementsworkspace_symbol(project-local). Each result reports assembly, kind (class / interface / struct / enum / record / union / module / ...), accessibility (public / internal / private / unknown), andisObsolete.includeNonPublic=trueexposes internals. Lazy: first call triggersParseAndCheckProjectif 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 exactSimpleName(case-insensitive).packageId='Spectre.Console'resolves to assemblySpectre.Consoleonly — notSpectre.Console.Cli.packageId='System'resolves to the literalSystemassembly only — not everySystem.*assembly. When a NuGet package ships multiple assemblies, call once per assembly name. ReturnsmatchedAssemblies=[]when no assembly matches; does not silently fall back to a less-specific assembly. Paginated; default 500, max 2000. (#113)
Changed
fcs_nuget_typesmatching is strictSimpleName-only. Rejected during review: reverse-prefix and dotted-prefix variants both produced misleading matches under BCL / multi-assembly NuGet packages (e.g.Systemwould match everySystem.*assembly). To discover assembly names, callers should usefcs_referenced_symbolswith a partial query first.- Structured
invalid_argsJSON for emptyquery/packageId/cursorand missingprojectPathinfcs_referenced_symbols/fcs_nuget_types(previously raised; now consistent withfcs_validate_snippet). - Two-tier fallback for entity name resolution.
entity.DisplayName→entity.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 callschecker.InvalidateConfigurationon the same project before re-running parse + check. Returns a diagnostics-focused payload witherrorCount+totalDiagnostics. Use this whenworkspace_diagnosticslooks stale right after anEdit/Write. Honest docs about FCS's internal AST cache remaining for transitively-referenced files; for absolute cross-project ground truth, fall back todotnet build. AddsBoundedCache.TryRemovefor per-key removal. (#109)workspace_diagnosticsfilter args.DiagnosticsArgsgains optionalfileGlobandseverityfields. Glob uses POSIX / gitignore semantics:*matches single segment,**matches zero+ segments,/**/between segments collapses zero+ dirs.severityaccepts"error" | "warning" | "information" | "hint"and filters diagnostics inside each file; files that empty out after filtering are dropped from the workspace response. Implementation in pureLspResponseShapehelpers (severityCodeOf,fileMatchesGlob,filterDiagnosticsBySeverity) wired intoLspBridge.Diagnostics. (#108)- Accessibility metadata in symbol JSON.
symbolToJsonnow exposes"accessibility": "private" | "internal" | "public" | "unknown". WrapsFSharpSymbol.Accessibilityintry/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_filecache 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 textualrg. PasstypeName(DisplayNamee.g."Style"orFullNamee.g."MyApp.Theme.Style") andmemberName.typeNamematching is deliberately conservative:DisplayNameequality always (so"Style"won't false-match"StyleSheet"); withexact=falsetheFullNamemay match at segment boundaries ("Theme.Style"yes,"Theme.StyleSheet"no). Pagination via cursor, same shape asfcs_project_symbol_uses. (#107)
Changed
projectPathis now optional on per-project tools. Afterset_project, omitprojectPathand the active project is used; pass it explicitly to target a different.fsproj. Affected arg records (eachstring→string option, hence the minor bump):FSharpCompileArgsProjectHealthArgsFcsProjectOutlineArgsFSharpProjectInspectArgsFcsGetProjectOptionsArgs
Resolvers (
ProjectFiles.resolveProjectPath,ProjectHealth.resolveHealthProjectPath,FcsBridge.CompileProject/ProjectOutline) acceptstring optionand 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_projectresponse surfacesloadedProjectsandreadinesssignals so agents can tell what was warmed and which downstream tools will work. (#106)loadedProjects: string array—.fsprojpaths discovered for the requested workspace. Single.fsproj→[path];.slnx/.sln→ all.fsprojentries that exist on disk.readiness: { lsp: bool, projectOptions: bool, symbolIndex: bool }:lsp—workspaceLoadnotification received.projectOptions— FCS can load options for the first.fsproj(probed viaIonide.ProjInfoinProgram.fs).symbolIndex— a non-emptyworkspace_symbolresponse observed since the lastset_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/.slnxparsing extracted toFsLangMcp.ProjectFiles.SolutionParsing(fsprojsFromSln,fsprojsFromSlnx,listProjects). Was duplicated betweenProjectHealthand the newSetProjectpath. +6 tests forSolutionParsing. 191/191 passing.
0.5.5 - 2026-05-18
No user-facing changes. Bundles post-0.5.4 follow-ups.
Changed
LspResponseShapeextracted into a pure module with 13 unit tests exercisinglspState/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.dllonly). 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_diagnosticsreturnslspState+diagnosticsFileCountso an empty payload during warm-up is distinguishable from "all clean". Previously an empty{}could mean either. (#102)workspace_symbolreturnslspState+symbolIndexReady. An empty result within 3s ofworkspaceReady=trueis flagged assymbolIndexReady=falseso agents can tell "no matches" from "index still building". (#103)fcs_type_at_positiontool description corrected. Now states explicitly thatset_project/projectPath/projectOptionsis required for resolved types and that the file atpathmust exist on disk. The previous description oversold standalone capability. (#104)
0.5.3 - 2026-05-08
Added
project_healthaccepts.slnand.slnxasprojectPath. Resolves to the single.fsprojinside (error if 0 or > 1).workspacePathis 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.MessagePackto1.1.62to address GHSA-2cwq-pwfr-wcw3. The entire 1.0.x series is vulnerable; 1.1.62 is the patched line.StreamJsonRpc 2.24.84pins the transitive dep to1.0.2— a direct reference at1.1.62overrides it.
0.5.2 - 2026-05-06
Fixed
textDocument_codeActionno longer raisesInvalidOperationExceptionon every call.LspBridge.CodeActionwas attaching the sameJsonNodeinstance as bothrange.startandrange.endof the request payload — the second attach failed because eachJsonNodehas a singleParent. Build two distinct position nodes. Originally caught in PR #43; landed in #98. (P1)Cursor.tryDecodereturns a structuredErrorfor non-object JSON payloads ([],[1,2,3],"x",42,true,null) instead of escaping asInvalidOperationException. Originally caught in PR #84; landed in #98.fcs_project_outlinerejects pathological pagination args up front.maxFiles=0previously emitted empty pages withtruncated=trueand a non-advancingnextCursor— a non-terminating loop for cursor-following clients.maxResultsPerFile<0was similarly silent. Both now raiseArgumentExceptionimmediately. Originally caught in PR #84; landed in #98.validateSourcePathacceptstext = Some ""as a valid unsaved-buffer state instead of forcingFile.Existson the path. Restores the new-empty-file editor flow. Originally caught in PR #83; landed in #98.fsharp_runtime_statussurvives concurrent FSAC restarts.Process.HasExitedcould throw if the handle was disposed by a parallelset_project/ stop. Wrapped in a small live-check helper that treats any read failure as "no live child", returning an emptychildrenlist rather than failing the whole diagnostic call. Originally caught in PR #85; landed in #98.publish.ymlworkflow_dispatchfails fast on non-tag refs. Manual dispatches from the default branch producedPackageVersion = "main"and an opaquedotnet packfailure several minutes into the run. An early guard step now refuses anything that isn't av*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 anInternalsVisibleTometadata 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 withDOTNET_gcServer=1(env always wins overruntimeconfig). Aligned with FsMcp 1.1.0's runtime-tuning guide. - Bumped
FsMcp.ServerandFsMcp.TaskApifrom 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
nextCursoralongsidetruncated/pageOffset/pageSize/totalEstimate. Adopted in:fcs_project_outline(paginates files) (#78)fcs_project_symbol_uses(paginates uses) (#80)fcs_find_symbolproject-wide variant (paginates symbol groups) (#80)
memberCountsmap per file infcs_project_outlinesummaryOnly mode — top-level{ kind: count }reflecting the full per-file outline, independent ofmaxResultsPerFile. Replaces the unreleased_summarysentinel node. (#82)- Filter args for
fcs_project_outline:filter(regex withNonBacktracking+ 250 ms timeout) andnameContains(substring list). Applied before truncation. (#78)
Changed
fcs_project_outlinedefaults 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.paginationFieldstakes aunitNameparameter so each tool'stotalEstimatecarries its own paginated unit (files/uses/symbols).
Removed
- The synthetic
{ "kind": "_summary", "memberCount": N }node previously appended insidefcs_project_outlinesummaryOnly 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_inspectfor read-only.fsprojstructure, compile order, references, and.fsi/.fspairing. - Added shared project file filtering for generated/build/test artifacts used by project-wide scans.
- Added
fsharp_signature_dataas a structured FSAC signature-data helper. - Improved
set_projectworkspace selection for directories: single solutions/projects are selected explicitly; ambiguous directories return candidates instead of guessing. - Removed
textDocument_hoverfrom the exposed MCP tool surface; usefcs_symbol_at_wordor exact-position FCS helpers instead.
[0.3.1] - 2026-05-04
- Updated
FSharp.Compiler.Serviceto43.12.203and aligned the implicitFSharp.Corepackage version to10.1.203. - Centralized FCS /
FSharp.Corepackage version settings inDirectory.Build.props. - Adjusted signature-help type formatting for the latest FCS API shape.
- Kept
fsharp_compileon the FCS project typecheck path introduced in 0.3.0.
[0.3.0] - 2026-05-04
- Added
project_healthfor read-only project/tooling preflight. - Added
fsharp_compileas an FCS-backed project parse+typecheck tool usingFSharpChecker.ParseAndCheckProject. - Removed reliance on the unavailable
fsautocomplete 0.83fsharp/compileendpoint and thedotnet buildfallback. - 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_optionstool wrapping proj-info. (#27, #37) - Migrated from
Newtonsoft.JsontoSystem.Text.Json. (#13, #43) - Performance: offload
LoadProjectOptionsFromFsprojtoTask.Run. (#32, #36) - Refactor: split
Program.fsinto logical modules. (#22, #40) - Structured
ToolErrorDU witherrorKindfield. (#20, #34)