Forge
csharpc0b81713
1using System.Text.Json;
2using AgentNotes.Core;
3using Microsoft.AspNetCore.Builder;
4using Microsoft.AspNetCore.Hosting;
5using Microsoft.AspNetCore.Http;
6using Microsoft.Extensions.Hosting;
7using Microsoft.Extensions.Logging;
8
9namespace AgentNotesMcp.Status;
10
11internal sealed class AgentNotesStatusHost
12{
13 private readonly NotesStorage _storage;
14 private readonly DateTimeOffset _startedAt;
15 private string _statusUrl;
16 private readonly string? _bindWarning;
17
18 internal AgentNotesStatusHost(NotesStorage storage, DateTimeOffset startedAt, string statusUrl, string? bindWarning)
19 {
20 _storage = storage;
21 _startedAt = startedAt;
22 _statusUrl = statusUrl;
23 _bindWarning = bindWarning;
24 }
25
26 internal static bool TryStartBackground(
27 NotesStorage storage,
28 DateTimeOffset startedAt,
29 out AgentNotesStatusHost? host,
30 out string? statusUrl,
31 out string? error)
32 {
33 host = null;
34 statusUrl = null;
35 error = null;
36
37 if (!AgentNotesRuntime.Settings.Status.Enabled)
38 return false;
39
40 try
41 {
42 var (bindHost, bindWarning) = AgentNotesStatusBind.Resolve(AgentNotesRuntime.Settings.Status.Bind);
43 statusUrl = AgentNotesStatusBind.BuildBaseUrl(bindHost, AgentNotesRuntime.Settings.Status.Port);
44 host = new AgentNotesStatusHost(storage, startedAt, statusUrl, bindWarning);
45 host.Start();
46 return true;
47 }
48 catch (Exception ex)
49 {
50 error = ex.Message;
51 return false;
52 }
53 }
54
55 internal bool TryStartForeground(out string? statusUrl, out string? error)
56 {
57 statusUrl = null;
58 error = null;
59
60 try
61 {
62 var (bindHost, bindWarning) = AgentNotesStatusBind.Resolve(AgentNotesRuntime.Settings.Status.Bind);
63 statusUrl = AgentNotesStatusBind.BuildBaseUrl(bindHost, AgentNotesRuntime.Settings.Status.Port);
64 _statusUrl = statusUrl;
65 Start();
66 return true;
67 }
68 catch (Exception ex)
69 {
70 error = ex.Message;
71 return false;
72 }
73 }
74
75 private WebApplication? _app;
76 private Task? _runTask;
77 private CancellationTokenSource? _cts;
78
79 private void Start()
80 {
81 var settings = AgentNotesRuntime.Settings;
82 var (bindHost, _) = AgentNotesStatusBind.Resolve(settings.Status.Bind);
83 var url = AgentNotesStatusBind.BuildBaseUrl(bindHost, settings.Status.Port);
84 _statusUrl = url;
85
86 var builder = WebApplication.CreateBuilder(new WebApplicationOptions
87 {
88 ApplicationName = "AgentNotesMcp",
89 Args = Array.Empty<string>()
90 });
91
92 builder.WebHost.UseUrls(url);
93 builder.Logging.ClearProviders();
94
95 var app = builder.Build();
96 _app = app;
97
98 app.MapGet("/health", () => Results.Text("OK", "text/plain"));
99
100 app.MapGet("/status.json", (HttpContext ctx) => WriteJson(ctx));
101
102 app.MapGet("/hot-preview", (HttpContext ctx) => WriteHotPreview(ctx));
103
104 app.MapGet("/tools", () =>
105 {
106 var html = AgentNotesStatusHtmlRenderer.RenderToolsPage(ToolCatalog.ListSummaries());
107 return Results.Content(html, "text/html; charset=utf-8");
108 });
109
110 app.MapGet("/", (HttpContext ctx) =>
111 {
112 var workspace = ResolveWorkspaceQuery(ctx);
113 var snapshot = BuildSnapshot(workspace, verbose: false);
114 var html = AgentNotesStatusHtmlRenderer.Render(snapshot, workspace);
115 return Results.Content(html, "text/html; charset=utf-8");
116 });
117
118 _cts = new CancellationTokenSource();
119 _runTask = StartAndWaitForShutdownAsync(app, _cts.Token);
120 }
121
122 private static async Task StartAndWaitForShutdownAsync(WebApplication app, CancellationToken cancellationToken)
123 {
124 await app.StartAsync(cancellationToken).ConfigureAwait(false);
125 await ((IHost)app).WaitForShutdownAsync(cancellationToken).ConfigureAwait(false);
126 }
127
128 internal async Task StopAsync()
129 {
130 if (_cts is null)
131 return;
132
133 try
134 {
135 await _cts.CancelAsync();
136 if (_runTask is not null)
137 await _runTask.ConfigureAwait(false);
138 }
139 catch (OperationCanceledException)
140 {
141 // Expected on shutdown.
142 }
143 finally
144 {
145 if (_app is not null)
146 await _app.DisposeAsync().ConfigureAwait(false);
147 }
148 }
149
150 private IResult WriteJson(HttpContext ctx)
151 {
152 var workspace = ResolveWorkspaceQuery(ctx);
153 var verbose = ctx.Request.Query.ContainsKey("verbose");
154 var snapshot = BuildSnapshot(workspace, verbose);
155
156 using var stream = new MemoryStream();
157 using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
158 snapshot.WriteJson(writer, verbose);
159
160 return Results.Content(
161 System.Text.Encoding.UTF8.GetString(stream.ToArray()),
162 "application/json; charset=utf-8");
163 }
164
165 private IResult WriteHotPreview(HttpContext ctx)
166 {
167 var workspace = ResolveWorkspaceQuery(ctx);
168 if (string.IsNullOrWhiteSpace(workspace))
169 {
170 return Results.Json(new
171 {
172 error = "workspace_path required (query or [status.preview].workspace in TOML)."
173 }, statusCode: StatusCodes.Status400BadRequest);
174 }
175
176 var json = _storage.HotPreview(workspace, activeScope: null);
177 return Results.Content(json, "application/json; charset=utf-8");
178 }
179
180 private static string? ResolveWorkspaceQuery(HttpContext ctx)
181 {
182 var workspace = ctx.Request.Query["workspace_path"].ToString();
183 if (!string.IsNullOrWhiteSpace(workspace))
184 return Path.GetFullPath(workspace.Trim());
185
186 return AgentNotesRuntime.Settings.Status.PreviewWorkspace;
187 }
188
189 private AgentNotesStatusSnapshot BuildSnapshot(string? workspacePath, bool verbose) =>
190 AgentNotesStatusSnapshot.Create(
191 _storage,
192 _startedAt,
193 _statusUrl,
194 _bindWarning,
195 workspacePath,
196 verbose);
197}
198
View only · write via MCP/CIDE