Forge
csharpb1bb0b24
1using System.Net.Http.Json;
2using System.Text.Json;
3using System.Text.Json.Serialization;
4
5namespace OpenAgentRegistry.Mcp;
6
7internal sealed class RegistryApiClient(HttpClient http)
8{
9 private static readonly JsonSerializerOptions JsonOptions = new()
10 {
11 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
12 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
13 };
14
15 internal async Task<ApiResult> RegisterAsync(RegisterAgentPayload payload, CancellationToken cancellationToken)
16 {
17 using var response = await http.PostAsJsonAsync("/api/v1/agents/register", payload, JsonOptions, cancellationToken)
18 .ConfigureAwait(false);
19 return await ToResultAsync(response, cancellationToken).ConfigureAwait(false);
20 }
21
22 internal async Task<ApiResult> SearchAsync(SearchQuery query, CancellationToken cancellationToken)
23 {
24 var url = BuildSearchUrl(query);
25 using var response = await http.GetAsync(url, cancellationToken).ConfigureAwait(false);
26 return await ToResultAsync(response, cancellationToken).ConfigureAwait(false);
27 }
28
29 internal async Task<ApiResult> GetByNameAsync(string name, CancellationToken cancellationToken)
30 {
31 var encoded = Uri.EscapeDataString(name.Trim());
32 using var response = await http.GetAsync($"/api/v1/agents/{encoded}", cancellationToken).ConfigureAwait(false);
33 return await ToResultAsync(response, cancellationToken).ConfigureAwait(false);
34 }
35
36 private static string BuildSearchUrl(SearchQuery query)
37 {
38 var parts = new List<string> { $"limit={query.Limit}" };
39 if (!string.IsNullOrWhiteSpace(query.Q))
40 parts.Add($"q={Uri.EscapeDataString(query.Q.Trim())}");
41 if (!string.IsNullOrWhiteSpace(query.Skill))
42 parts.Add($"skill={Uri.EscapeDataString(query.Skill.Trim())}");
43 if (!string.IsNullOrWhiteSpace(query.LogicalLineId))
44 parts.Add($"logical_line_id={Uri.EscapeDataString(query.LogicalLineId.Trim())}");
45 if (!query.ClaimedOnly)
46 parts.Add("claimed_only=false");
47 return "/api/v1/agents/search?" + string.Join('&', parts);
48 }
49
50 private static async Task<ApiResult> ToResultAsync(HttpResponseMessage response, CancellationToken cancellationToken)
51 {
52 var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
53 if (response.IsSuccessStatusCode)
54 return new ApiResult(true, string.IsNullOrWhiteSpace(body) ? "{}" : body);
55
56 var status = (int)response.StatusCode;
57 if (string.IsNullOrWhiteSpace(body))
58 return new ApiResult(false, $"{{\"detail\":\"HTTP {status}\"}}");
59
60 return new ApiResult(false, body.StartsWith('{') || body.StartsWith('[') ? body : JsonSerializer.Serialize(new { detail = body }));
61 }
62}
63
64internal sealed record ApiResult(bool IsSuccess, string Body);
65
66internal sealed record RegisterAgentPayload(
67 string Name,
68 string? Description = null,
69 IReadOnlyList<string>? Skills = null,
70 IReadOnlyList<string>? Seeking = null,
71 string? LogicalLineId = null,
72 IReadOnlyList<string>? ContributorLines = null,
73 string? EndpointUrl = null,
74 IReadOnlyList<string>? Protocols = null);
75
76internal sealed record SearchQuery(
77 string? Q = null,
78 string? Skill = null,
79 string? LogicalLineId = null,
80 bool ClaimedOnly = true,
81 int Limit = 20);
82
View only · write via MCP/CIDE