Forge
csharp3407750f
1using System.Text.Json;
2using AgentForge.Abstractions;
3using AgentForge.Data;
4using AgentForge.Data.Entities;
5using AgentForge.Plugin.Ci.Contracts;
6using AgentForge.Plugin.Ci.Data;
7using AgentForge.Plugin.Ci.Execution;
8using AgentForge.Plugin.Ci.Ir;
9using AgentForge.Plugin.Sdk;
10using AgentForge.Services;
11using Microsoft.Extensions.DependencyInjection;
12
13namespace AgentForge.Plugin.Ci;
14
15internal static class ForgeCiMcpTools
16{
17 internal static void Register(ForgeMcpToolRegistry registry)
18 {
19 registry.Add(DefinitionListDescriptor, DefinitionListAsync);
20 registry.Add(DefinitionGetDescriptor, DefinitionGetAsync);
21 registry.Add(DefinitionCreateDescriptor, DefinitionCreateAsync);
22 registry.Add(RunQueueDescriptor, RunQueueAsync);
23 registry.Add(RunListDescriptor, RunListAsync);
24 registry.Add(RunGetDescriptor, RunGetAsync);
25 registry.Add(RunLogDescriptor, RunLogAsync);
26 }
27
28 private static readonly ForgeMcpToolDescriptor DefinitionListDescriptor = new()
29 {
30 Name = ForgeMcpToolNames.CiDefinitionList,
31 Description = "List CI pipeline definitions for a repository.",
32 InputSchema = JsonSerializer.SerializeToElement(new
33 {
34 type = "object",
35 properties = new { repo = new { type = "string", description = "Repository slug" } },
36 required = new[] { "repo" },
37 }),
38 };
39
40 private static readonly ForgeMcpToolDescriptor DefinitionGetDescriptor = new()
41 {
42 Name = ForgeMcpToolNames.CiDefinitionGet,
43 Description = "Get a CI pipeline definition (forge.ci/v1 IR) by name.",
44 InputSchema = JsonSerializer.SerializeToElement(new
45 {
46 type = "object",
47 properties = new
48 {
49 repo = new { type = "string" },
50 name = new { type = "string", description = "Definition name" },
51 },
52 required = new[] { "repo", "name" },
53 }),
54 };
55
56 private static readonly ForgeMcpToolDescriptor DefinitionCreateDescriptor = new()
57 {
58 Name = ForgeMcpToolNames.CiDefinitionCreate,
59 Description = "Create a CI pipeline definition with default forge.ci/v1 template.",
60 InputSchema = JsonSerializer.SerializeToElement(new
61 {
62 type = "object",
63 properties = new
64 {
65 repo = new { type = "string" },
66 name = new { type = "string", description = "Definition name (e.g. default)" },
67 },
68 required = new[] { "repo", "name" },
69 }),
70 };
71
72 private static readonly ForgeMcpToolDescriptor RunQueueDescriptor = new()
73 {
74 Name = ForgeMcpToolNames.CiRunQueue,
75 Description = "Queue a CI pipeline run for a repository definition.",
76 InputSchema = JsonSerializer.SerializeToElement(new
77 {
78 type = "object",
79 properties = new
80 {
81 repo = new { type = "string" },
82 definition = new { type = "string", description = "Definition name (e.g. default)" },
83 name = new { type = "string", description = "Alias for definition" },
84 @ref = new { type = "string", description = "Git ref (default main)" },
85 commit = new { type = "string" },
86 mrNumber = new { type = "integer" },
87 },
88 required = new[] { "repo" },
89 }),
90 };
91
92 private static readonly ForgeMcpToolDescriptor RunListDescriptor = new()
93 {
94 Name = ForgeMcpToolNames.CiRunList,
95 Description = "List CI pipeline runs for a repository.",
96 InputSchema = JsonSerializer.SerializeToElement(new
97 {
98 type = "object",
99 properties = new { repo = new { type = "string" } },
100 required = new[] { "repo" },
101 }),
102 };
103
104 private static readonly ForgeMcpToolDescriptor RunGetDescriptor = new()
105 {
106 Name = ForgeMcpToolNames.CiRunGet,
107 Description = "Get CI pipeline run status and step timeline.",
108 InputSchema = JsonSerializer.SerializeToElement(new
109 {
110 type = "object",
111 properties = new
112 {
113 repo = new { type = "string" },
114 runId = new { type = "string" },
115 },
116 required = new[] { "repo", "runId" },
117 }),
118 };
119
120 private static readonly ForgeMcpToolDescriptor RunLogDescriptor = new()
121 {
122 Name = ForgeMcpToolNames.CiRunLog,
123 Description = "Get log text for a CI pipeline step.",
124 InputSchema = JsonSerializer.SerializeToElement(new
125 {
126 type = "object",
127 properties = new
128 {
129 repo = new { type = "string" },
130 runId = new { type = "string" },
131 stepId = new { type = "string" },
132 },
133 required = new[] { "repo", "runId", "stepId" },
134 }),
135 };
136
137 private static Task<ForgeMcpInvokeResponse> DefinitionListAsync(
138 ForgeMcpToolInvocationContext context,
139 IReadOnlyDictionary<string, JsonElement> arguments,
140 CancellationToken cancellationToken)
141 {
142 var repoName = RequireString(arguments, "repo");
143 var repo = ResolveRepo(context, repoName);
144 var store = context.RequestServices.GetRequiredService<ForgeCiDefinitionStore>();
145 var rows = store.List(repo.Id);
146 var body = BuildDefinitionListBody(repo, rows);
147 return TaskOk(body);
148 }
149
150 private static Task<ForgeMcpInvokeResponse> DefinitionGetAsync(
151 ForgeMcpToolInvocationContext context,
152 IReadOnlyDictionary<string, JsonElement> arguments,
153 CancellationToken cancellationToken)
154 {
155 var repoName = RequireString(arguments, "repo");
156 var definitionName = RequireString(arguments, "name");
157 var repo = ResolveRepo(context, repoName);
158 var store = context.RequestServices.GetRequiredService<ForgeCiDefinitionStore>();
159 var normalized = ForgeCiTemplates.NormalizeDefinitionName(definitionName);
160 var record = store.GetByName(repo.Id, normalized);
161 if (record is null)
162 return TaskFail($"Definition '{normalized}' not found.");
163
164 var body = BuildDefinitionDetailBody(repo, record, store);
165 return TaskOk(body);
166 }
167
168 private static Task<ForgeMcpInvokeResponse> DefinitionCreateAsync(
169 ForgeMcpToolInvocationContext context,
170 IReadOnlyDictionary<string, JsonElement> arguments,
171 CancellationToken cancellationToken)
172 {
173 var repoName = RequireString(arguments, "repo");
174 var definitionName = RequireString(arguments, "name");
175 var repo = ResolveRepo(context, repoName);
176 var store = context.RequestServices.GetRequiredService<ForgeCiDefinitionStore>();
177 try
178 {
179 var document = ForgeCiTemplates.CreateDefault(definitionName);
180 var record = store.Insert(repo.Id, document, "mcp");
181 var body = JsonSerializer.Serialize(new
182 {
183 repo = repo.Name,
184 name = record.Name,
185 created = true,
186 });
187 return TaskOk(body);
188 }
189 catch (ForgeCiValidationException ex)
190 {
191 return TaskFail(ex.Message);
192 }
193 catch (InvalidOperationException ex)
194 {
195 return TaskFail(ex.Message);
196 }
197 }
198
199 private static async Task<ForgeMcpInvokeResponse> RunQueueAsync(
200 ForgeMcpToolInvocationContext context,
201 IReadOnlyDictionary<string, JsonElement> arguments,
202 CancellationToken cancellationToken)
203 {
204 var repoName = RequireString(arguments, "repo");
205 var definitionName = OptionalString(arguments, "definition")
206 ?? OptionalString(arguments, "name")
207 ?? "default";
208 var queue = context.RequestServices.GetRequiredService<ForgeCiRunQueueService>();
209 var repo = ResolveRepo(context, repoName);
210
211 int? mrNumber = null;
212 if (arguments.TryGetValue("mrNumber", out var mrEl) && mrEl.TryGetInt32(out var mr))
213 mrNumber = mr;
214
215 try
216 {
217 var detail = await queue.QueueAsync(
218 repo.Id,
219 repo.Name,
220 new QueueCiRunRequest(
221 definitionName,
222 OptionalString(arguments, "ref"),
223 OptionalString(arguments, "commit"),
224 mrNumber),
225 "mcp",
226 cancellationToken).ConfigureAwait(false);
227
228 var body = BuildRunQueuedBody(repo, detail);
229 return Ok(body);
230 }
231 catch (KeyNotFoundException ex)
232 {
233 return Fail(ex.Message);
234 }
235 catch (ForgeCiValidationException ex)
236 {
237 return Fail(ex.Message);
238 }
239 }
240
241 private static Task<ForgeMcpInvokeResponse> RunListAsync(
242 ForgeMcpToolInvocationContext context,
243 IReadOnlyDictionary<string, JsonElement> arguments,
244 CancellationToken cancellationToken)
245 {
246 var repoName = RequireString(arguments, "repo");
247 var repo = ResolveRepo(context, repoName);
248 var runs = context.RequestServices.GetRequiredService<ForgeCiRunStore>();
249 var items = ForgeCiRunResolve.ListSummaries(repo, runs);
250 var body = BuildRunListBody(repo, items);
251 return TaskOk(body);
252 }
253
254 private static Task<ForgeMcpInvokeResponse> RunGetAsync(
255 ForgeMcpToolInvocationContext context,
256 IReadOnlyDictionary<string, JsonElement> arguments,
257 CancellationToken cancellationToken)
258 {
259 var repoName = RequireString(arguments, "repo");
260 var runId = RequireString(arguments, "runId");
261
262 var runScope = GetRunScope(context, repoName, runId);
263 if (runScope is null)
264 return TaskFail($"Run '{runId}' not found.");
265
266 var body = BuildRunDetailBody(runScope);
267 return TaskOk(body);
268 }
269
270 private static Task<ForgeMcpInvokeResponse> RunLogAsync(
271 ForgeMcpToolInvocationContext context,
272 IReadOnlyDictionary<string, JsonElement> arguments,
273 CancellationToken cancellationToken)
274 {
275 var repoName = RequireString(arguments, "repo");
276 var runId = RequireString(arguments, "runId");
277 var stepId = RequireString(arguments, "stepId");
278
279 var runScope = GetRunScope(context, repoName, runId);
280 if (runScope is null)
281 return TaskFail($"Run '{runId}' not found.");
282
283 var step = GetRunStep(runScope, stepId);
284 if (step is null)
285 return TaskFail($"Step '{stepId}' not found.");
286
287 var body = BuildRunLogBody(runScope, step);
288 return TaskOk(body);
289 }
290
291 private static CiRunScope? GetRunScope(
292 ForgeMcpToolInvocationContext context,
293 string repoName,
294 string runId)
295 {
296 var repo = ResolveRepo(context, repoName);
297 var runs = context.RequestServices.GetRequiredService<ForgeCiRunStore>();
298 return ForgeCiRunResolve.TryGetRunScope(repo, runs, runId);
299 }
300
301 private static string BuildRunDetailBody(CiRunScope scope)
302 {
303 var steps = scope.Runs.ListSteps(scope.Run.Id);
304 var detail = ForgeCiRunQueueService.ToDetail(scope.Repo.Name, scope.Run, steps);
305 return JsonSerializer.Serialize(detail, ForgeCiDocument.JsonOptions);
306 }
307
308 private static ForgeCiStepRunRecord? GetRunStep(CiRunScope scope, string stepId) =>
309 ForgeCiRunResolve.TryGetStep(scope, stepId);
310
311 private static string BuildRunLogBody(CiRunScope scope, ForgeCiStepRunRecord step) =>
312 JsonSerializer.Serialize(new
313 {
314 repo = scope.Repo.Name,
315 runId = scope.Run.Id,
316 stepId = step.Id,
317 status = step.Status,
318 log = step.LogText,
319 });
320
321 private static string BuildDefinitionDetailBody(
322 ForgeRepoEntity repo,
323 ForgeCiDefinitionRecord record,
324 ForgeCiDefinitionStore store)
325 {
326 var document = store.LoadDocument(record);
327 return JsonSerializer.Serialize(new
328 {
329 repo = repo.Name,
330 name = record.Name,
331 document,
332 }, ForgeCiDocument.JsonOptions);
333 }
334
335 private static string BuildDefinitionListBody(
336 ForgeRepoEntity repo,
337 IReadOnlyList<ForgeCiDefinitionRecord> rows) =>
338 JsonSerializer.Serialize(new
339 {
340 repo = repo.Name,
341 count = rows.Count,
342 definitions = rows.Select(r => new
343 {
344 name = r.Name,
345 createdBy = r.CreatedBy,
346 updatedAt = r.UpdatedAt,
347 }),
348 });
349
350 private static string BuildRunListBody(ForgeRepoEntity repo, IReadOnlyList<CiRunSummaryResponse> runs) =>
351 JsonSerializer.Serialize(new CiRunListResponse(runs.Count, runs));
352
353 private static string BuildRunQueuedBody(ForgeRepoEntity repo, CiRunDetailResponse detail) =>
354 JsonSerializer.Serialize(new
355 {
356 repo = repo.Name,
357 runId = detail.Id,
358 status = detail.Status,
359 url = $"/view/repos/{ForgeRepoNames.ToUrlPath(repo.Name)}/ci/runs/{Uri.EscapeDataString(detail.Id)}",
360 });
361
362 private static ForgeRepoEntity ResolveRepo(ForgeMcpToolInvocationContext context, string repoName)
363 {
364 var repository = context.RequestServices.GetRequiredService<ForgeRepository>();
365 return ForgePluginEndpoints.RequireRepo(repository, repoName, context.HttpContext);
366 }
367
368 private static ForgeMcpInvokeResponse Ok(string body) =>
369 new() { Success = true, Body = body };
370
371 private static ForgeMcpInvokeResponse Fail(string body) =>
372 new() { Success = false, Body = body };
373
374 private static Task<ForgeMcpInvokeResponse> TaskOk(string body) =>
375 Task.FromResult(Ok(body));
376
377 private static Task<ForgeMcpInvokeResponse> TaskFail(string body) =>
378 Task.FromResult(Fail(body));
379
380 private static string? OptionalString(IReadOnlyDictionary<string, JsonElement> arguments, string key)
381 {
382 if (!arguments.TryGetValue(key, out var element) || element.ValueKind != JsonValueKind.String)
383 return null;
384 var value = element.GetString()?.Trim();
385 return string.IsNullOrEmpty(value) ? null : value;
386 }
387
388 private static string RequireString(IReadOnlyDictionary<string, JsonElement> arguments, string key)
389 {
390 if (!arguments.TryGetValue(key, out var element) || element.ValueKind != JsonValueKind.String)
391 throw new InvalidOperationException($"Missing required argument '{key}'.");
392 var value = element.GetString()?.Trim();
393 if (string.IsNullOrEmpty(value))
394 throw new InvalidOperationException($"Argument '{key}' must be non-empty.");
395 return value;
396 }
397}
398
View only · write via MCP/CIDE