-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathProgram.fs
More file actions
667 lines (573 loc) · 41.2 KB
/
Copy pathProgram.fs
File metadata and controls
667 lines (573 loc) · 41.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
module FsLangMcp.Program
open System
open System.IO
open System.Diagnostics
open System.Threading
open System.Threading.Tasks
open FsLangMcp.Types
open FsLangMcp.LspBridge
open FsLangMcp.FcsBridge
open FsLangMcp.ProjectHealth
open FsLangMcp.ProjectInspection
open FsLangMcp.Tools
open FsLangMcp.RuntimeStatus
open FsLangMcp.ProcessRunner
open FsMcp.Core
open FsMcp.Core.Validation
open FsMcp.Server
open System.Text.Json
open System.Text.Json.Nodes
// ─── CLI helpers ───────────────────────────────────────────────────────────────
let private readPositiveIntEnv (name: string) (defaultValue: int) =
match Environment.GetEnvironmentVariable(name) with
| null -> defaultValue
| value ->
match Int32.TryParse(value) with
| true, parsed when parsed > 0 -> parsed
| _ -> defaultValue
let private timeoutFromEnv (name: string) (defaultMilliseconds: int) =
TimeSpan.FromMilliseconds(float (readPositiveIntEnv name defaultMilliseconds))
let private runProcess (fileName: string) (args: string list) =
let result =
ProcessRunner.run
fileName
args
(timeoutFromEnv "FSLANGMCP_BOOTSTRAP_TIMEOUT_MS" 300_000)
result.ExitCode, result.StandardOutput, result.StandardError
let private parseProjInfoOutput (path: string) (exitCode: int) (stdout: string) (stderr: string) : JsonNode =
if exitCode <> 0 then
jobj [ "error", jstr $"proj-info failed (exit %d{exitCode})"; "stderr", jstr stderr ] :> JsonNode
elif String.IsNullOrWhiteSpace(stdout) then
jobj [ "error", jstr "proj-info produced no output" ] :> JsonNode
else
let token = JsonNode.Parse(stdout)
let json =
match token with
| :? JsonArray as arr when arr.Count > 0 ->
match arr.[0] with
| :? JsonObject as obj -> obj
| other ->
let kind = other.GetValueKind()
jobj [ "warning", jstr (sprintf "unexpected element type: %s" (kind.ToString())) ]
| :? JsonObject as obj -> obj
| _ -> JsonObject()
let otherOptions =
json["OtherOptions"]
|> Option.ofObj
|> Option.map (fun t -> t.Deserialize<string array>() |> Option.ofObj |> Option.defaultValue [||])
|> Option.defaultValue [||]
jobj
[ "projectPath", jstr path
"otherOptions", JsonArray(otherOptions |> Array.map jstr) :> JsonNode
"optionsCount", jint otherOptions.Length ]
:> JsonNode
let private runProjInfoAsync (path: string) : Task<JsonNode> =
task {
try
let! result =
ProcessRunner.runAsync
"proj-info"
[ "--project"; path; "--fcs"; "--serialize" ]
(timeoutFromEnv "FSLANGMCP_PROJ_INFO_TIMEOUT_MS" 120_000)
CancellationToken.None
return parseProjInfoOutput path result.ExitCode result.StandardOutput result.StandardError
with
| :? System.ComponentModel.Win32Exception
| :? IOException ->
return
raise (
FileNotFoundException
"proj-info not found on PATH. Install with: dotnet tool install -g ionide.projinfo.tool"
)
}
let private runLimited (gate: SemaphoreSlim) (work: unit -> Task<JsonNode>) : Task<JsonNode> =
task {
do! gate.WaitAsync()
try
return! work ()
finally
gate.Release() |> ignore
}
let private ensureDotnetGlobalTool (toolId: string) =
let updateCode, _, updateErr =
runProcess "dotnet" [ "tool"; "update"; "-g"; toolId ]
if updateCode = 0 then
Console.Error.WriteLine($"[bootstrap] updated %s{toolId}")
true
else
let installCode, _, installErr =
runProcess "dotnet" [ "tool"; "install"; "-g"; toolId ]
if installCode = 0 then
Console.Error.WriteLine($"[bootstrap] installed %s{toolId}")
true
else
Console.Error.WriteLine($"[bootstrap] failed for %s{toolId}")
if not (String.IsNullOrWhiteSpace(updateErr)) then
Console.Error.WriteLine(updateErr)
if not (String.IsNullOrWhiteSpace(installErr)) then
Console.Error.WriteLine(installErr)
false
let private bootstrapTools () =
[ "fsautocomplete"; "ionide.projinfo.tool" ]
|> List.map ensureDotnetGlobalTool
|> List.forall id
let private applyCliOverrides (argv: string array) =
let rec loop index =
if index >= argv.Length then
Start
else
match argv[index] with
| "--project"
| "-p" ->
if index + 1 >= argv.Length then
Fail "--project requires a value."
else
Environment.SetEnvironmentVariable("FSA_PROJECT_PATH", argv[index + 1])
loop (index + 2)
| "--fsac-command" ->
if index + 1 >= argv.Length then
Fail "--fsac-command requires a value."
else
Environment.SetEnvironmentVariable("FSAC_COMMAND", argv[index + 1])
loop (index + 2)
| "--fsac-args" ->
if index + 1 >= argv.Length then
Fail "--fsac-args requires a value."
else
Environment.SetEnvironmentVariable("FSAC_ARGS", argv[index + 1])
loop (index + 2)
| "--help"
| "-h" ->
ShowHelp
"Usage: fslangmcp [--project <path-to-fsproj>] [--fsac-command <cmd>] [--fsac-args \"...\"] [--bootstrap-tools]"
| "--bootstrap-tools" -> BootstrapTools
| unknown -> Fail $"Unknown argument: %s{unknown}"
loop 0
// ─── Entry point ───────────────────────────────────────────────────────────────
[<EntryPoint>]
let main argv =
match applyCliOverrides argv with
| BootstrapTools -> if bootstrapTools () then 0 else 1
| ShowHelp message ->
Console.WriteLine(message)
0
| Fail message ->
Console.Error.WriteLine(message)
1
| Start ->
use bridge = new FsAutoCompleteBridge()
let fcsBridge = new FcsBridge()
use fcsGate = new SemaphoreSlim(readPositiveIntEnv "FSLANGMCP_MAX_CONCURRENT_FCS" 2)
// FSAC owns one mutable workspace. Keep the public gate fixed at one; the bridge
// also serializes internally because some FCS orchestrators call LSP directly.
use lspGate = new SemaphoreSlim(1, 1)
let versionResponse: Task<JsonNode> =
task {
return
jobj
[ "status", jstr "ok"
"fslangmcpVersion", jstr FsLangMcp.Version.current
"productName", jstr FsLangMcp.Version.productName ]
:> JsonNode
}
let server =
mcpServer {
name "fsharp-fsautocomplete"
version FsLangMcp.Version.current
tool (
TypedTool.define<CompletionArgs>
"textDocument_completion"
"Raw LSP proxy to fsautocomplete textDocument/completion. Exact-position IDE primitive; requires set_project first. line/character are 0-based. Pass 'text' for unsaved content. Avoid for free-form agent flows — completion is exact-position editor IO; for symbol semantics use fcs_symbol_at_word or fcs_type_at_position instead."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.Completion args)))
|> unwrapResult
)
tool (
TypedTool.define<CheckArgs>
"check"
"One trustworthy verdict for the active F# context. Bare check() suffices: returns `verdict` (clean|errors|unknown) after a FRESH in-process type-check, so it never reports a stale-`{}` false-clean and you don't fall back to dotnet build. Optional: scope (auto|file|project|workspace|snippet), path, snippet (inline source), speed (trusted default | fast = cached FSAC snapshot), severity. Prefer over workspace_diagnostics / fcs_check_file when you just need a yes/no answer."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (
runLimited fcsGate (fun () ->
Dispatcher.CheckDispatch.run fcsBridge bridge (Dispatcher.Check args))))
|> unwrapResult
)
tool (
TypedTool.define<SetProjectArgs>
"set_project"
"Initialize or switch the FSAC/LSP project context. Must be called before textDocument_* and workspace_* tools. Accepts .fsproj, .sln, .slnx, or directory. Waits up to 30s for workspace load and clears FCS caches. Response includes loadedProjects (.fsproj paths discovered) and readiness (lsp / projectOptions / symbolIndex flags)."
(fun args ->
toolResult (
runLimited lspGate (fun () ->
task {
let! result = bridge.SetProject args
fcsBridge.ClearCaches()
// Enrich readiness.projectOptions by probing the first loaded .fsproj.
// Bridge cannot do this itself (no FCS handle); we own that wiring here.
match result["result"] with
| :? JsonObject as resultObj ->
let probeTarget =
match resultObj["loadedProjects"] with
| :? JsonArray as arr when arr.Count > 0 ->
match arr[0] with
| null -> None
| node ->
let v = node.GetValue<string>()
if System.String.IsNullOrWhiteSpace v then None else Some v
| _ -> None
match probeTarget with
| Some path ->
let! probe = fcsBridge.ProbeProjectOptions path
match resultObj["readiness"] with
| :? JsonObject as readinessObj ->
match probe with
| Ok info ->
readinessObj["projectOptions"] <- jbool true
// Restore-awareness (#138): options can load while the
// project's external references are absent on disk, which
// leaves symbolIndex empty and makes FCS tools fail with
// 'FSharp.Core.dll not found'. Surface "restore first"
// rather than letting `ready` imply it's usable.
if FsLangMcp.Types.ReferenceResolution.looksUnrestored
info.ReferencesExisting
info.ReferencesTotal then
readinessObj["restoreStatus"] <- jstr "unrestored"
readinessObj["restoreHint"] <-
jstr
"external references unresolved — run dotnet restore && dotnet build before using FCS tools"
| Error _ -> readinessObj["projectOptions"] <- jbool false
| _ -> ()
| None -> ()
| _ -> ()
return result
})
))
|> unwrapResult
)
tool (
TypedTool.define<ProjectHealthArgs>
"project_health"
"Fast read-only preflight for one F# project. Reports whether FsLangMCP can trust semantic tooling, project options availability, source file readability, analyzer setup, test project discovery, and current LSP readiness. projectPath is optional after set_project (falls back to the active project); pass it explicitly to inspect a different .fsproj/.sln/.slnx. Does not start/switch FSAC, run compile, or run tests."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
let snapshot =
{ ProjectPath = bridge.CurrentProjectPath
WorkspaceRoot = bridge.CurrentWorkspaceRoot
WorkspaceReady = bridge.IsWorkspaceReady
DiagnosticsFileCount = bridge.DiagnosticsFileCount }
let probe path =
fcsBridge.ProbeProjectOptions(path) |> Async.AwaitTask
toolResult (
runLimited fcsGate (fun () -> createReport args snapshot probe |> Async.StartAsTask)
))
|> unwrapResult
)
tool (
TypedTool.define<FSharpProjectInspectArgs>
"fsharp_project_inspect"
"Read-only .fsproj inspection for agents. Prefer over textual reads of `.fsproj` — handles MSBuild evaluation correctly. Returns project identity, compile order, package/project references, signature/implementation pairing, and shared scan filtering summary. `projectPath` is optional after `set_project`. Does not build, restore, test, or edit files."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (Task.FromResult(inspectProject args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsReferencedSymbolsArgs>
"fcs_referenced_symbols"
"Substring search across the project's referenced assemblies (NuGet + framework) by DisplayName or FullName (case-insensitive). Prefer `fcs_nuget_types` when you already know the exact assembly name. Complements `workspace_symbol` (project-local). Reports assembly, kind, accessibility, isObsolete. `includeNonPublic=true` for internals. Paginated; default 200, max 1000. First call triggers ParseAndCheckProject. Details: docs/tools-detailed.md#fcs_referenced_symbols."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.ReferencedSymbols args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsSuggestOpenArgs>
"fcs_suggest_open"
"Given an unresolved symbol name (FS0039), returns ranked `open` directive candidates — project-local first, then referenced assemblies. Use when an agent sees 'X is not defined' to get the right namespace instantly. Set includeReferences=false for project-only. Caveat: openPath is empty for global-namespace symbols; doesn't deduplicate the same name across multiple assemblies."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.SuggestOpen args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsNugetTypesArgs>
"fcs_nuget_types"
"Enumerate all types in one referenced assembly matched by EXACT SimpleName (case-insensitive). Prefer `fcs_referenced_symbols` when you need substring search across assemblies. `Spectre.Console` resolves only to that assembly — not `Spectre.Console.Cli`; call once per assembly. Each entry: displayName, fullName, kind, accessibility, isObsolete. Paginated; default 500, max 2000. Returns `matchedAssemblies=[]` on no match. Mechanics: docs/tools-detailed.md#fcs_nuget_types."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.NugetTypes args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsNugetMembersArgs>
"fcs_nuget_members"
"Enumerate members of one type from a referenced assembly (matched by packageId + typeName). Prefer `fcs_nuget_types` to discover available type names first. Each entry: name, kind, signature, accessibility, isObsolete, xmlDocSummary. Paginated; default 500, max 2000. Returns `matchedTypes=[]` on no type match. Mechanics: docs/tools-detailed.md#fcs_nuget_members."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.NugetMembers args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsFileOutlineArgs>
"fcs_file_outline"
"Agent-friendly compact F# outline for one file. Defaults to summaryOnly=true: module/type headers + per-kind memberCounts only (no per-member signatures), so large files never overflow the token ceiling. Set summaryOnly=false for full name/kind/range/signature/accessibility entries. Filters local/noisy symbols by default. Prefer fcs_project_outline for a whole-project overview; fcs_file_symbols for raw unfiltered symbols."
(fun args -> toolResult (runLimited fcsGate (fun () -> fcsBridge.FileOutline args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsMakeInternalVisibleArgs>
"fcs_make_internal_visible"
"Drop the `private` keyword from a declaration at `(line, character)`. Returns a non-destructive workspace edit `{ status, edits, appliedPreview, originalLineText }` — does NOT write the file. Use before tests need to call internals. Returns `{ status: 'no_action', reason }` on no symbol or no recognized modifier. Supported forms and Variant B status: docs/tools-detailed.md#fcs_make_internal_visible."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.MakeInternalVisible args)))
|> unwrapResult
)
tool (
TypedTool.define<FindArgs>
"find"
"Multi-project F# semantic search: definitions, references, record-field set sites, and member call-sites on a type (`x.Foo`), unioned across every solution .fsproj. FCS-resolved — beats a textual rg that over-matches `.Member()` on unrelated types or can't cross projects. Bare find(query) gives a compact one-line-per-site list (file/line/kind/lineText); contextLines adds code. Narrow with kind (symbol|members|field|definition|position)+scope; member call sites = kind=members + member=Name."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (
runLimited fcsGate (fun () ->
Dispatcher.FindDispatch.run fcsBridge bridge (Dispatcher.Find args))))
|> unwrapResult
)
tool (
TypedTool.define<FcsSymbolAtWordArgs>
"fcs_symbol_at_word"
"Tolerant FCS symbol lookup for agent workflows. Accepts a line plus word/occurrence, finds the candidate span, and returns symbol identity, kind, type string, definition range, and optional documentation. Prefer over exact-position hover/type queries."
(fun args -> toolResult (runLimited fcsGate (fun () -> fcsBridge.SymbolAtWord args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsProjectOutlineArgs>
"fcs_project_outline"
"Agent-friendly project outline over filtered compile files. Prefer over `workspace_symbol` for whole-project structural overview — skips generated/build artifacts and returns compact per-file outlines. `projectPath` is optional after `set_project` (falls back to the active project); pass it explicitly for a different .fsproj. Use `maxFiles`/`maxResultsPerFile` on large projects."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.ProjectOutline args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsSignatureHelpArgs>
"fcs_signature_help"
"Low-level exact-position FCS signature help. Returns overloads/parameters around a call site. line/character are 0-based. Pass projectPath/projectOptions and 'text' when available."
(fun args -> toolResult (runLimited fcsGate (fun () -> fcsBridge.SignatureHelp args)))
|> unwrapResult
)
tool (
TypedTool.define<PositionArgs>
"fsharp_signature_data"
"Structured FSAC signature help via fsharp/signatureData. Requires set_project and an exact call-site position. Use this when FCS fallback is insufficient or when validating FSAC's current workspace view."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.SignatureData args)))
|> unwrapResult
)
tool (
TypedTool.define<FormattingArgs>
"textDocument_formatting"
"Raw LSP formatting proxy via fsautocomplete/Fantomas. Requires set_project first. Returns formatted text and edits; it does not write to disk. Pass 'text' for unsaved content."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.Formatting args)))
|> unwrapResult
)
tool (
TypedTool.define<CodeActionArgs>
"textDocument_codeAction"
"Raw LSP codeAction proxy at an exact position with empty diagnostic context. Requires set_project first. Useful for debugging FSAC; prefer future diagnostics-to-fix workflows for agent repairs. Pass 'text' for unsaved content."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.CodeAction args)))
|> unwrapResult
)
tool (
TypedTool.define<RenameArgs>
"textDocument_rename"
"Raw LSP semantic rename at an exact position. Requires `set_project` first. Prefer over textual rename — handles shadowing and aliased opens safely. Returns raw WorkspaceEdit; needs a precise target. Pass `text` for unsaved content."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.Rename args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsGetProjectOptionsArgs>
"fcs_get_project_options"
"Diagnostic helper: get FSharp compiler OtherOptions for a .fsproj via proj-info. projectPath is optional after set_project (falls back to the active project); pass it explicitly to inspect a different one."
(fun args ->
let resolved =
args.projectPath
|> Option.orElse bridge.CurrentProjectPath
|> Option.filter (System.String.IsNullOrWhiteSpace >> not)
match resolved with
| None ->
toolResult (
Task.FromException<JsonNode>(
ArgumentException
"projectPath is required. Either pass it explicitly or call set_project first to establish a default."
)
)
| Some path -> toolResult (runLimited fcsGate (fun () -> runProjInfoAsync path)))
|> unwrapResult
)
tool (
TypedTool.define<FcsCheckCompileOrderArgs>
"fcs_check_compile_order"
"Detect F#'s file-ordering gotcha: a symbol used before the file that DEFINES it in <Compile> order reads as FS0039 'not defined' though it exists. Returns { symbol, definedIn, usedIn{file,compileIndex,range,lineText}, fix } so an agent reorders the .fsproj. Use when `check` reports FS0039 'X is not defined' to tell a compile-ORDER problem from a missing `open` (fcs_suggest_open handles that). projectPath optional after set_project; `symbol` narrows to one name."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.CheckCompileOrder args)))
|> unwrapResult
)
tool (
TypedTool.define<FslangmcpVersionArgs>
"fslangmcp_version"
"Returns the installed FsLangMCP product version and name. Zero-arg (pass {}). Same value is also surfaced in the set_project response (fslangmcpVersion field) and the fsharp_runtime_status response. Use this tool when filing UX feedback so reports can be matched to a specific release of the MCP server. Pure: no project context required, no side effects, no caches read."
(fun _ -> toolResult versionResponse)
|> unwrapResult
)
tool (
TypedTool.define<DiagnosticFixesArgs>
"fcs_diagnostic_fixes"
"Fetch a file's diagnostics, then request code-action fixes for each and group them per diagnostic: range, severity, code, message, fixes [{title, kind, editSummary}], plus diagnosticCount/fixCount. Agent-friendly wrapper over raw textDocument_codeAction: supplies the diagnostic context the raw proxy leaves empty and groups the fixes. Requires set_project first. Pass line(+character) to narrow to one position, else all; pass text for unsaved content."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.DiagnosticFixes args)))
|> unwrapResult
)
tool (
TypedTool.define<RuntimeStatusArgs>
"fsharp_runtime_status"
"Read-only observational snapshot of the FsLangMCP process runtime state: managed-heap sizes by generation/LOH/POH, GC collection counts, isServerGC flag, assembly load count, FCS checker configuration flags and project-results cache size, and the FSAC child-process working set. Returns numbers only — no interpretation. Never triggers a GC collection, never walks the heap, never attaches diagnostic listeners."
(fun args ->
toolResult (
Task.FromResult(
buildSnapshot
args
fcsBridge.CheckerConfig
bridge.FsacProcess
)
))
|> unwrapResult
)
tool (
TypedTool.define<FcsExplainDiagnosticArgs>
"fcs_explain_diagnostic"
"Explain an F# compiler diagnostic in plain language with repair context: title, explanation, likelyCauses, repairHints, relatedTools. Pass `code` (\"FS0039\"), `errorNumber` (39), or path+line+character to auto-fetch it via FCS. Use this when `check` reports an FS error you need to turn into a fix — feed it check's errorNumberText. Curated map of ~25 common diagnostics; pass the raw `message` to enrich hints (FS0039 → fcs_suggest_open). Unknown codes return status=unknown_code."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.ExplainDiagnostic args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsTestsForSymbolArgs>
"fcs_tests_for_symbol"
"List the tests that likely cover a symbol. Sweeps the active solution's test projects (detected as project_health does — <IsTestProject> or an xunit/nunit/expecto ref), filters FCS symbol uses to test files, and tags each site with its enclosing test ([<Fact>]/[<Theory>]/[<Test>]/testCase). Use it for the test-coverage slice `find` lacks — find returns every use; this returns only test-file sites plus the enclosing test name. projectPath falls back to set_project."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.TestsForSymbol args)))
|> unwrapResult
)
tool (
TypedTool.define<RenamePreviewArgs>
"fcs_rename_preview"
"Preview a semantic rename's full impact WITHOUT applying it — non-destructive, writes nothing. Runs the same FSAC machinery as `textDocument_rename` but returns edits grouped by file, each with originalLineText and previewLineText, plus totalEdits, fileCount, and a crossProject flag. Use it to inspect blast radius before `textDocument_rename` applies the change. Requires `set_project`. Returns `no_symbol` when the position has no renamable symbol. Pass `text` for unsaved buffers."
(fun args -> toolResult (runLimited lspGate (fun () -> bridge.RenamePreview args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsPublicApiArgs>
"fcs_public_api"
"Emit an F# project's public API surface: every public type and its public members with signatures, sorted stably by fullName then member name so two version snapshots diff cleanly. Prefer over `fcs_project_outline` for API-stability/breaking-change diffs — public-only (includeInternal=true adds internals), signature-complete, deterministic order. projectPath optional after set_project. Narrow with namespaceFilter (substring on FullName); paginated via maxResults + cursor."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.PublicApi args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsRefactorImpactArgs>
"fcs_refactor_impact"
"Preview a change's blast radius + a verify checklist WITHOUT editing. Orchestrates find (cross-project use sites), fcs_tests_for_symbol, fcs_check_compile_order (kind=move) and fcs_public_api (kind=signature|delete, when public) into { target, impact, tests, compileOrder?, apiSurface?, verify[] }. Pass `symbol` or path+line+character; kind=rename|signature|move|delete|auto. Use before a rename/move/delete; prefer `fcs_rename_preview` for the exact edits, this for project-wide impact."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (
runLimited fcsGate (fun () ->
fcsBridge.RefactorImpact(args, (fun rp -> bridge.RenamePreview rp)))))
|> unwrapResult
)
tool (
TypedTool.define<FcsSignatureStatusArgs>
"fcs_signature_status"
"Report the .fsi-vs-impl public-surface gap for one .fs WITHOUT editing: type-checks the impl with its sibling .fsi stripped, then diffs — members public in the impl but missing from the .fsi (silently hidden) → missingFromSig; .fsi entries with no impl match → staleInSig, each with a val/type signaturePreview. No .fsi? lists the would-be signature. Use for .fsi drift (members hidden/stale); prefer `fcs_public_api` for the whole public surface. projectPath falls back to set_project."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.SignatureStatus args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsReviewScanArgs>
"fcs_review_scan"
"Scan F# source for review CANDIDATES from the untyped AST — interesting spots to eyeball, not a linter and never bugs. Categories: match_wildcard, try_with, raise_or_failwith, mutable_binding, blocking_call, cast_or_box, reflection, large_function. Pass `path` (one file) or `projectPath` (whole project; falls back to set_project), narrow with `categories`, cap with `maxResults`. Parse-only, writes nothing; candidates carry range, lineText and a neutral note, plus counts.byCategory."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.ReviewScan args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsDeadCodeArgs>
"fcs_dead_code"
"List likely-unused F# symbols as cleanup candidates — a conservative cleanup pass (candidates, not deletions); use `find` to verify each candidate's real usage before removing. Sweeps the project (GetAllUsesOfAllSymbols) and flags private/internal value & function bindings whose only use is their own definition. Public is excluded (includePublic=true adds it); skips compiler-generated, [<EntryPoint>], overrides/interface impls, ctors. Always emits caveats. projectPath falls back to set_project."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.DeadCode args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsCreateFilePlanArgs>
"fcs_create_file_plan"
"Plan WHERE a new .fs file belongs WITHOUT creating it — read-only, writes nothing. Loads the resolved <Compile> order, recommends an insertion index (right after `afterFile`, else namespace-neighbour/end), infers the namespace/module convention from neighbours, and emits the exact <Compile Include=...> edit plus a dependency note (a file may only reference EARLIER files). Use before adding an .fs file to pick the right <Compile> position; pair with `fcs_check_compile_order` after."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.CreateFilePlan args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsAnalyzerDiagnosticsArgs>
"fcs_analyzer_diagnostics"
"Report F# ANALYZER diagnostics (not compiler diagnostics), grouped: analyzersConfigured, analyzerPackages, diagnostics [{analyzer, code, severity, message, file, range}], counts {byAnalyzer, bySeverity}. Detects analyzer config like project_health, runs the fsharp-analyzers CLI when available, parses its SARIF; none configured → no_analyzers. Use to read analyzer DIAGNOSTICS; project_health reports whether analyzers are CONFIGURED. severity filters; projectPath falls back to set_project."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.AnalyzerDiagnostics args)))
|> unwrapResult
)
tool (
TypedTool.define<FcsAnalyzerSetupPreviewArgs>
"fcs_analyzer_setup_preview"
"Plan what to add to enable F# analyzers WITHOUT applying it — read-only, writes nothing. Reads the .fsproj + Directory.Build.props/.targets + dotnet-tools.json, diffs current wiring against the required set: analyzer package refs + GeneratePathProperty, FSharp.Analyzers.Build, the FSharpAnalyzersOtherFlags property, a local fsharp-analyzers manifest. Emits each gap as an exact XML/JSON snippet + reason. Use this to set analyzers up; pair with fcs_analyzer_diagnostics to read diagnostics after."
(fun args ->
let args =
{ args with projectPath = args.projectPath |> Option.orElse bridge.CurrentProjectPath }
toolResult (runLimited fcsGate (fun () -> fcsBridge.AnalyzerSetupPreview args)))
|> unwrapResult
)
useStdio
}
try
Server.run server |> fun t -> t.GetAwaiter().GetResult()
0
with ex ->
Console.Error.WriteLine($"Fatal error: %s{ex.Message}")
1