Skip to content

Commit 8a91d9f

Browse files
committed
Added digma sse server
1 parent 7fe9765 commit 8a91d9f

File tree

12 files changed

+480
-0
lines changed

12 files changed

+480
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Net.Http.Headers;
2+
3+
namespace DigmaSSEServer.Authentication;
4+
5+
public class BearerTokenHandler : DelegatingHandler
6+
{
7+
private readonly DigmaTokenProvider _tokenProvider;
8+
9+
public BearerTokenHandler(DigmaTokenProvider tokenProvider, HttpMessageHandler innerHandler = null)
10+
{
11+
_tokenProvider = tokenProvider;
12+
InnerHandler = innerHandler ?? new HttpClientHandler();
13+
}
14+
15+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
16+
{
17+
var token = await _tokenProvider.GetTokenAsync();
18+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
19+
return await base.SendAsync(request, cancellationToken);
20+
}
21+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Globalization;
2+
using System.Text.Json.Serialization;
3+
using DigmaSSEServer.Options;
4+
5+
namespace DigmaSSEServer.Authentication;
6+
7+
public sealed class DigmaTokenProvider
8+
{
9+
private const string LoginRoute = "/authentication/login";
10+
private const string RefreshRoute = "/authentication/refresh-token";
11+
private static readonly TimeSpan Skew = TimeSpan.FromSeconds(30);
12+
13+
private readonly HttpClient _httpClient;
14+
private readonly string _username;
15+
private readonly string _password;
16+
17+
private TokenInfo? _current;
18+
19+
public DigmaTokenProvider(HttpClient httpClient, AuthOptions options)
20+
{
21+
_httpClient = httpClient;
22+
_username = options.UserName;
23+
_password = options.Password;
24+
}
25+
26+
public async Task<string> GetTokenAsync(CancellationToken ct = default)
27+
{
28+
// Fast‑path: already have a non‑expired token
29+
if (IsValid(_current))
30+
return _current!.AccessToken;
31+
32+
// Try refresh if we have a refresh token
33+
if (_current is { RefreshToken: var refresh })
34+
{
35+
_current = await TryRefreshAsync(refresh);
36+
if (_current != null)
37+
{
38+
return _current.AccessToken;
39+
}
40+
}
41+
42+
// Fall back to log in
43+
_current = await LoginAsync();
44+
45+
return _current.AccessToken;
46+
}
47+
48+
private static bool IsValid(TokenInfo? t) =>
49+
t is { ExpiresAtUtc: var exp } && DateTime.UtcNow < exp - Skew;
50+
51+
private async Task<TokenInfo> LoginAsync()
52+
{
53+
var body = new { username = _username, password = _password };
54+
var resp = await _httpClient.PostAsJsonAsync(LoginRoute, body);
55+
resp.EnsureSuccessStatusCode();
56+
57+
var dto = await resp.Content.ReadFromJsonAsync<AuthResponse>().ConfigureAwait(false)
58+
?? throw new InvalidOperationException("Empty login payload");
59+
60+
return dto.ToTokenInfo();
61+
}
62+
63+
private async Task<TokenInfo?> TryRefreshAsync(string refreshToken)
64+
{
65+
var dtoIn = new { accessToken = _current!.AccessToken, refreshToken };
66+
var resp = await _httpClient.PostAsJsonAsync(RefreshRoute, dtoIn).ConfigureAwait(false);
67+
if (!resp.IsSuccessStatusCode) return null;
68+
69+
var dto = await resp.Content.ReadFromJsonAsync<AuthResponse>().ConfigureAwait(false);
70+
return dto?.ToTokenInfo();
71+
}
72+
73+
private sealed record AuthResponse(
74+
[property: JsonPropertyName("accessToken")] string AccessToken,
75+
[property: JsonPropertyName("refreshToken")] string RefreshToken,
76+
[property: JsonPropertyName("expiration")] string Expiration)
77+
{
78+
public TokenInfo ToTokenInfo() =>
79+
new(AccessToken,
80+
DateTime.Parse(Expiration, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)
81+
.ToUniversalTime(),
82+
RefreshToken);
83+
}
84+
85+
private sealed record TokenInfo(string AccessToken, DateTime ExpiresAtUtc, string RefreshToken);
86+
}

src/DigmaSSEServer/DigmaClient.cs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Net;
2+
using System.Text;
3+
using System.Text.Json;
4+
5+
namespace DigmaSSEServer;
6+
7+
public class DigmaClient
8+
{
9+
private readonly HttpClient _httpClient;
10+
11+
public DigmaClient(HttpClient httpClient)
12+
{
13+
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
14+
}
15+
16+
public async Task<string> GetTopIssues(string? environmentId, int page = 0, int pageSize = 10)
17+
{
18+
try
19+
{
20+
var request = new
21+
{
22+
environment = environmentId,
23+
sortBy = "criticality",
24+
sortOrder = "desc",
25+
page,
26+
pageSize
27+
};
28+
29+
var response = await _httpClient.PostAsync("/mcp/top-issues", GetJsonContent(request));
30+
await EnsureSuccessStatusCodeWithContentAsync(response);
31+
return await response.Content.ReadAsStringAsync();
32+
}
33+
catch (HttpRequestException ex)
34+
{
35+
throw new Exception($"Failed to get top issues: {ex.Message}", ex);
36+
}
37+
}
38+
39+
private static async Task EnsureSuccessStatusCodeWithContentAsync(HttpResponseMessage response)
40+
{
41+
if (!response.IsSuccessStatusCode)
42+
{
43+
var content = await response.Content.ReadAsStringAsync();
44+
throw new HttpRequestException(
45+
$"Request failed with status code {response.StatusCode}: {content}",
46+
null,
47+
response.StatusCode);
48+
}
49+
}
50+
51+
public async Task<string> GetAllIssuesForMethod(string environmentId, string className, string functionName)
52+
{
53+
var request = new
54+
{
55+
environmentId,
56+
methodName = functionName,
57+
className
58+
};
59+
var response = await _httpClient.PostAsync("/mcp/all-issues-for-method", GetJsonContent(request));
60+
response.EnsureSuccessStatusCode();
61+
return await response.Content.ReadAsStringAsync();
62+
}
63+
64+
public async Task<string> GetEnvironments()
65+
{
66+
try
67+
{
68+
var response = await _httpClient.GetAsync("/mcp/environments");
69+
var content = await response.Content.ReadAsStringAsync();
70+
Console.WriteLine($"Response: {content}");
71+
await EnsureSuccessStatusCodeWithContentAsync(response);
72+
return content;
73+
}
74+
catch (HttpRequestException ex)
75+
{
76+
Console.WriteLine($"Error getting environments: {ex.Message}");
77+
throw new Exception($"Failed to get environments: {ex.Message}", ex);
78+
}
79+
}
80+
81+
public async Task<string> GetUsagesForMethod(string environmentId, string className, string methodName)
82+
{
83+
try
84+
{
85+
var request = new
86+
{
87+
environmentId,
88+
className,
89+
methodName
90+
};
91+
92+
var response = await _httpClient.PostAsync("/mcp/usages-for-method", GetJsonContent(request));
93+
await EnsureSuccessStatusCodeWithContentAsync(response);
94+
return await response.Content.ReadAsStringAsync();
95+
}
96+
catch (HttpRequestException ex)
97+
{
98+
throw new Exception($"Failed to get method usages: {ex.Message}", ex);
99+
}
100+
}
101+
102+
public async Task<string> GetTrace(string traceId)
103+
{
104+
var response = await _httpClient.GetAsync($"/mcp/trace/{traceId}");
105+
response.EnsureSuccessStatusCode();
106+
return await response.Content.ReadAsStringAsync();
107+
}
108+
109+
public async Task<string> GetAssetsByCategory(string environmentId, string? category = null, int page = 0, int pageSize = 10)
110+
{
111+
var request = new
112+
{
113+
environmentId,
114+
category,
115+
page,
116+
pageSize
117+
};
118+
var response = await _httpClient.PostAsync("/mcp/assets", GetJsonContent(request));
119+
response.EnsureSuccessStatusCode();
120+
return await response.Content.ReadAsStringAsync();
121+
}
122+
123+
public async Task<string> GetAssetCategories(string environmentId)
124+
{
125+
var encodedEnv = WebUtility.UrlEncode(environmentId);
126+
var response = await _httpClient.GetAsync($"/mcp/assets-categories?environment={encodedEnv}");
127+
response.EnsureSuccessStatusCode();
128+
return await response.Content.ReadAsStringAsync();
129+
}
130+
131+
private static StringContent GetJsonContent(object request)
132+
{
133+
var json = JsonSerializer.Serialize(request);
134+
var content = new StringContent(json, Encoding.UTF8, "application/json");
135+
return content;
136+
}
137+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using DigmaSSEServer.Authentication;
2+
using DigmaSSEServer.Options;
3+
using Microsoft.Extensions.Options;
4+
5+
namespace DigmaSSEServer;
6+
7+
public static class DigmaConfigurator
8+
{
9+
private const string TokenClientName = "DigmaTokenClient";
10+
11+
public static IServiceCollection AddDigma(this IServiceCollection services,
12+
IConfiguration configuration)
13+
{
14+
services.AddTransient<BearerTokenHandler>();
15+
16+
services.Configure<DigmaOptions>(configuration.GetSection("Digma"));
17+
services.Configure<AuthOptions>(configuration.GetSection("Auth"));
18+
19+
static HttpClientHandler CreateHandler()
20+
{
21+
var handler = new HttpClientHandler();
22+
#if DEBUG
23+
handler.ServerCertificateCustomValidationCallback =
24+
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
25+
#endif
26+
return handler;
27+
}
28+
29+
services.AddHttpClient(TokenClientName, (sp, c) =>
30+
{
31+
var digma = sp.GetRequiredService<IOptions<DigmaOptions>>().Value;
32+
var auth = sp.GetRequiredService<IOptions<AuthOptions>>().Value;
33+
34+
c.BaseAddress = new Uri(digma.AnalyticsApi);
35+
c.DefaultRequestHeaders.Add("Digma-Access-Token", auth.Token);
36+
})
37+
.ConfigurePrimaryHttpMessageHandler(CreateHandler);
38+
39+
services.AddHttpClient<DigmaClient>((sp, c) =>
40+
{
41+
var digma = sp.GetRequiredService<IOptions<DigmaOptions>>().Value;
42+
var auth = sp.GetRequiredService<IOptions<AuthOptions>>().Value;
43+
44+
c.BaseAddress = new Uri(digma.AnalyticsApi);
45+
c.DefaultRequestHeaders.Add("Digma-Access-Token", auth.Token);
46+
})
47+
.ConfigurePrimaryHttpMessageHandler(sp =>
48+
{
49+
var delegating = sp.GetRequiredService<BearerTokenHandler>();
50+
delegating.InnerHandler = CreateHandler();
51+
return delegating;
52+
});
53+
54+
services.AddSingleton<DigmaTokenProvider>(sp =>
55+
{
56+
var http = sp.GetRequiredService<IHttpClientFactory>()
57+
.CreateClient(TokenClientName);
58+
var auth = sp.GetRequiredService<IOptions<AuthOptions>>().Value;
59+
return new DigmaTokenProvider(http, auth);
60+
});
61+
62+
return services;
63+
}
64+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
11+
<PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.10" />
12+
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.10" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace DigmaSSEServer.Options;
2+
3+
public class AuthOptions
4+
{
5+
public string Token { get; set; }
6+
public string UserName { get; set; }
7+
public string Password { get; set; }
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace DigmaSSEServer.Options;
2+
3+
public class DigmaOptions
4+
{
5+
public string AnalyticsApi { get; set; } = string.Empty;
6+
}

src/DigmaSSEServer/Program.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Net;
2+
using DigmaSSEServer;
3+
using DigmaSSEServer.Tools;
4+
using ModelContextProtocol.Protocol.Types;
5+
6+
var builder = WebApplication.CreateBuilder(args);
7+
8+
builder.Services.AddDigma(builder.Configuration);
9+
10+
builder.Services
11+
.AddMcpServer(options => options.ServerInfo = new Implementation()
12+
{
13+
Name = "Digma Server",
14+
Version = "1.0"
15+
})
16+
.WithHttpTransport() // OR WithStdioServerTransport()
17+
.WithTools<CodeObservabilityTool>();
18+
19+
var app = builder.Build();
20+
21+
app.MapMcp();
22+
23+
app.Run();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "http://localhost:5294",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
},
13+
"https": {
14+
"commandName": "Project",
15+
"dotnetRunMessages": true,
16+
"launchBrowser": true,
17+
"applicationUrl": "https://localhost:7150;http://localhost:5294",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)