OpenTelemetry: context propagation and semconv update (#262)
This commit is contained in:
parent
f2fd2046f3
commit
d6cc65aced
11 changed files with 398 additions and 89 deletions
|
|
@ -58,6 +58,10 @@
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
|
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
|
||||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.2" />
|
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.2" />
|
||||||
|
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
|
||||||
|
<PackageVersion Include="OpenTelemetry.Instrumentation.Http " Version="1.11.0" />
|
||||||
|
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
|
||||||
|
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
|
||||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||||
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
|
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,11 @@
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
using TestServerWithHosting.Tools;
|
using TestServerWithHosting.Tools;
|
||||||
|
using OpenTelemetry.Metrics;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
using OpenTelemetry;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddMcpServer()
|
builder.Services.AddMcpServer()
|
||||||
.WithTools<EchoTool>()
|
.WithTools<EchoTool>()
|
||||||
.WithTools<SampleLlmTool>();
|
.WithTools<SampleLlmTool>();
|
||||||
|
|
||||||
|
builder.Services.AddOpenTelemetry()
|
||||||
|
.WithTracing(b => b.AddSource("*")
|
||||||
|
.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation())
|
||||||
|
.WithMetrics(b => b.AddMeter("*")
|
||||||
|
.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation())
|
||||||
|
.WithLogging()
|
||||||
|
.UseOtlpExporter();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapMcp();
|
app.MapMcp();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "http://localhost:3001",
|
"applicationUrl": "http://localhost:3001",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"OTEL_SERVICE_NAME": "sse-server",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
|
|
@ -14,7 +15,8 @@
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "https://localhost:7133;http://localhost:3001",
|
"applicationUrl": "https://localhost:7133;http://localhost:3001",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"OTEL_SERVICE_NAME": "sse-server",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||||
<PackageReference Include="Anthropic.SDK" />
|
<PackageReference Include="Anthropic.SDK" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,49 @@ using ModelContextProtocol.Protocol.Transport;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using OpenAI;
|
using OpenAI;
|
||||||
|
|
||||||
|
using OpenTelemetry;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OpenTelemetry.Logs;
|
||||||
|
using OpenTelemetry.Metrics;
|
||||||
|
|
||||||
|
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddSource("*")
|
||||||
|
.AddOtlpExporter()
|
||||||
|
.Build();
|
||||||
|
using var metricsProvider = Sdk.CreateMeterProviderBuilder()
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddMeter("*")
|
||||||
|
.AddOtlpExporter()
|
||||||
|
.Build();
|
||||||
|
using var loggerFactory = LoggerFactory.Create(builder => builder.AddOpenTelemetry(opt => opt.AddOtlpExporter()));
|
||||||
|
|
||||||
// Connect to an MCP server
|
// Connect to an MCP server
|
||||||
Console.WriteLine("Connecting client to MCP 'everything' server");
|
Console.WriteLine("Connecting client to MCP 'everything' server");
|
||||||
|
|
||||||
|
// Create OpenAI client (or any other compatible with IChatClient)
|
||||||
|
// Provide your own OPENAI_API_KEY via an environment variable.
|
||||||
|
var openAIClient = new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).GetChatClient("gpt-4o-mini");
|
||||||
|
|
||||||
|
// Create a sampling client.
|
||||||
|
using IChatClient samplingClient = openAIClient.AsIChatClient()
|
||||||
|
.AsBuilder()
|
||||||
|
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
|
||||||
|
.Build();
|
||||||
|
|
||||||
var mcpClient = await McpClientFactory.CreateAsync(
|
var mcpClient = await McpClientFactory.CreateAsync(
|
||||||
new StdioClientTransport(new()
|
new StdioClientTransport(new()
|
||||||
{
|
{
|
||||||
Command = "npx",
|
Command = "npx",
|
||||||
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
|
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
|
||||||
Name = "Everything",
|
Name = "Everything",
|
||||||
}));
|
}),
|
||||||
|
clientOptions: new()
|
||||||
|
{
|
||||||
|
Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } },
|
||||||
|
},
|
||||||
|
loggerFactory: loggerFactory);
|
||||||
|
|
||||||
// Get all available tools
|
// Get all available tools
|
||||||
Console.WriteLine("Tools available:");
|
Console.WriteLine("Tools available:");
|
||||||
|
|
@ -20,13 +54,15 @@ foreach (var tool in tools)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" {tool}");
|
Console.WriteLine($" {tool}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
// Create an IChatClient. (This shows using OpenAIClient, but it could be any other IChatClient implementation.)
|
// Create an IChatClient that can use the tools.
|
||||||
// Provide your own OPENAI_API_KEY via an environment variable.
|
using IChatClient chatClient = openAIClient.AsIChatClient()
|
||||||
using IChatClient chatClient =
|
.AsBuilder()
|
||||||
new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).GetChatClient("gpt-4o-mini").AsIChatClient()
|
.UseFunctionInvocation()
|
||||||
.AsBuilder().UseFunctionInvocation().Build();
|
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
|
||||||
|
.Build();
|
||||||
|
|
||||||
// Have a conversation, making all tools available to the LLM.
|
// Have a conversation, making all tools available to the LLM.
|
||||||
List<ChatMessage> messages = [];
|
List<ChatMessage> messages = [];
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@ using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol;
|
using ModelContextProtocol;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
using OpenTelemetry;
|
||||||
|
using OpenTelemetry.Logs;
|
||||||
|
using OpenTelemetry.Metrics;
|
||||||
|
using OpenTelemetry.Resources;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
|
||||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
|
||||||
|
|
@ -186,6 +191,13 @@ builder.Services
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService("everything-server");
|
||||||
|
builder.Services.AddOpenTelemetry()
|
||||||
|
.WithTracing(b => b.AddSource("*").AddHttpClientInstrumentation().SetResourceBuilder(resource))
|
||||||
|
.WithMetrics(b => b.AddMeter("*").AddHttpClientInstrumentation().SetResourceBuilder(resource))
|
||||||
|
.WithLogging(b => b.SetResourceBuilder(resource))
|
||||||
|
.UseOtlpExporter();
|
||||||
|
|
||||||
builder.Services.AddSingleton(subscriptions);
|
builder.Services.AddSingleton(subscriptions);
|
||||||
builder.Services.AddHostedService<SubscriptionMessageSender>();
|
builder.Services.AddHostedService<SubscriptionMessageSender>();
|
||||||
builder.Services.AddHostedService<LoggingUpdateMessageSender>();
|
builder.Services.AddHostedService<LoggingUpdateMessageSender>();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
|
|
||||||
namespace ModelContextProtocol;
|
namespace ModelContextProtocol;
|
||||||
|
|
||||||
|
|
@ -34,4 +37,77 @@ internal static class Diagnostics
|
||||||
HistogramBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300],
|
HistogramBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300],
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
internal static ActivityContext ExtractActivityContext(this DistributedContextPropagator propagator, IJsonRpcMessage message)
|
||||||
|
{
|
||||||
|
propagator.ExtractTraceIdAndState(message, ExtractContext, out var traceparent, out var tracestate);
|
||||||
|
ActivityContext.TryParse(traceparent, tracestate, true, out var activityContext);
|
||||||
|
return activityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExtractContext(object? message, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues)
|
||||||
|
{
|
||||||
|
fieldValues = null;
|
||||||
|
fieldValue = null;
|
||||||
|
|
||||||
|
JsonNode? parameters = null;
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case JsonRpcRequest request:
|
||||||
|
parameters = request.Params;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JsonRpcNotification notification:
|
||||||
|
parameters = notification.Params;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters?[fieldName] is JsonValue value && value.GetValueKind() == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
fieldValue = value.GetValue<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void InjectActivityContext(this DistributedContextPropagator propagator, Activity? activity, IJsonRpcMessage message)
|
||||||
|
{
|
||||||
|
// noop if activity is null
|
||||||
|
propagator.Inject(activity, message, InjectContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InjectContext(object? message, string key, string value)
|
||||||
|
{
|
||||||
|
JsonNode? parameters = null;
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case JsonRpcRequest request:
|
||||||
|
parameters = request.Params;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JsonRpcNotification notification:
|
||||||
|
parameters = notification.Params;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters is JsonObject jsonObject && jsonObject[key] == null)
|
||||||
|
{
|
||||||
|
jsonObject[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool ShouldInstrumentMessage(IJsonRpcMessage message) =>
|
||||||
|
ActivitySource.HasListeners() &&
|
||||||
|
message switch
|
||||||
|
{
|
||||||
|
JsonRpcRequest => true,
|
||||||
|
JsonRpcNotification notification => notification.Method != NotificationMethods.LoggingMessageNotification,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static ActivityLink[] ActivityLinkFromCurrent() => Activity.Current is null ? [] : [new ActivityLink(Activity.Current.Context)];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
using ModelContextProtocol.Logging;
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
|
|
@ -10,7 +10,9 @@ using System.Diagnostics;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
#if !NET
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace ModelContextProtocol.Shared;
|
namespace ModelContextProtocol.Shared;
|
||||||
|
|
||||||
|
|
@ -23,10 +25,10 @@ internal sealed class McpSession : IDisposable
|
||||||
"mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true);
|
"mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true);
|
||||||
private static readonly Histogram<double> s_serverSessionDuration = Diagnostics.CreateDurationHistogram(
|
private static readonly Histogram<double> s_serverSessionDuration = Diagnostics.CreateDurationHistogram(
|
||||||
"mcp.server.session.duration", "Measures the duration of a server session.", longBuckets: true);
|
"mcp.server.session.duration", "Measures the duration of a server session.", longBuckets: true);
|
||||||
private static readonly Histogram<double> s_clientRequestDuration = Diagnostics.CreateDurationHistogram(
|
private static readonly Histogram<double> s_clientOperationDuration = Diagnostics.CreateDurationHistogram(
|
||||||
"rpc.client.duration", "Measures the duration of outbound RPC.", longBuckets: false);
|
"mcp.client.operation.duration", "Measures the duration of outbound message.", longBuckets: false);
|
||||||
private static readonly Histogram<double> s_serverRequestDuration = Diagnostics.CreateDurationHistogram(
|
private static readonly Histogram<double> s_serverOperationDuration = Diagnostics.CreateDurationHistogram(
|
||||||
"rpc.server.duration", "Measures the duration of inbound RPC.", longBuckets: false);
|
"mcp.server.operation.duration", "Measures the duration of inbound message processing.", longBuckets: false);
|
||||||
|
|
||||||
private readonly bool _isServer;
|
private readonly bool _isServer;
|
||||||
private readonly string _transportKind;
|
private readonly string _transportKind;
|
||||||
|
|
@ -35,6 +37,8 @@ internal sealed class McpSession : IDisposable
|
||||||
private readonly NotificationHandlers _notificationHandlers;
|
private readonly NotificationHandlers _notificationHandlers;
|
||||||
private readonly long _sessionStartingTimestamp = Stopwatch.GetTimestamp();
|
private readonly long _sessionStartingTimestamp = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
private readonly DistributedContextPropagator _propagator = DistributedContextPropagator.Current;
|
||||||
|
|
||||||
/// <summary>Collection of requests sent on this session and waiting for responses.</summary>
|
/// <summary>Collection of requests sent on this session and waiting for responses.</summary>
|
||||||
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests = [];
|
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests = [];
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -184,12 +188,17 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
|
private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Histogram<double> durationMetric = _isServer ? s_serverRequestDuration : s_clientRequestDuration;
|
Histogram<double> durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration;
|
||||||
string method = GetMethodName(message);
|
string method = GetMethodName(message);
|
||||||
|
|
||||||
long? startingTimestamp = durationMetric.Enabled ? Stopwatch.GetTimestamp() : null;
|
long? startingTimestamp = durationMetric.Enabled ? Stopwatch.GetTimestamp() : null;
|
||||||
Activity? activity = Diagnostics.ActivitySource.HasListeners() ?
|
|
||||||
Diagnostics.ActivitySource.StartActivity(CreateActivityName(method)) :
|
Activity? activity = Diagnostics.ShouldInstrumentMessage(message) ?
|
||||||
|
Diagnostics.ActivitySource.StartActivity(
|
||||||
|
CreateActivityName(method),
|
||||||
|
ActivityKind.Server,
|
||||||
|
parentContext: _propagator.ExtractActivityContext(message),
|
||||||
|
links: Diagnostics.ActivityLinkFromCurrent()) :
|
||||||
null;
|
null;
|
||||||
|
|
||||||
TagList tags = default;
|
TagList tags = default;
|
||||||
|
|
@ -198,18 +207,14 @@ internal sealed class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
if (addTags)
|
if (addTags)
|
||||||
{
|
{
|
||||||
AddStandardTags(ref tags, method);
|
AddTags(ref tags, activity, message, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
case JsonRpcRequest request:
|
case JsonRpcRequest request:
|
||||||
if (addTags)
|
var result = await HandleRequest(request, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
AddResponseTags(ref tags, activity, result, method);
|
||||||
AddRpcRequestTags(ref tags, activity, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
await HandleRequest(request, cancellationToken).ConfigureAwait(false);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case JsonRpcNotification notification:
|
case JsonRpcNotification notification:
|
||||||
|
|
@ -227,7 +232,7 @@ internal sealed class McpSession : IDisposable
|
||||||
}
|
}
|
||||||
catch (Exception e) when (addTags)
|
catch (Exception e) when (addTags)
|
||||||
{
|
{
|
||||||
AddExceptionTags(ref tags, e);
|
AddExceptionTags(ref tags, activity, e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -277,7 +282,7 @@ internal sealed class McpSession : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleRequest(JsonRpcRequest request, CancellationToken cancellationToken)
|
private async Task<JsonNode?> HandleRequest(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!_requestHandlers.TryGetValue(request.Method, out var handler))
|
if (!_requestHandlers.TryGetValue(request.Method, out var handler))
|
||||||
{
|
{
|
||||||
|
|
@ -294,6 +299,8 @@ internal sealed class McpSession : IDisposable
|
||||||
JsonRpc = "2.0",
|
JsonRpc = "2.0",
|
||||||
Result = result
|
Result = result
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenRegistration RegisterCancellation(CancellationToken cancellationToken, RequestId requestId)
|
private CancellationTokenRegistration RegisterCancellation(CancellationToken cancellationToken, RequestId requestId)
|
||||||
|
|
@ -340,12 +347,12 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Histogram<double> durationMetric = _isServer ? s_serverRequestDuration : s_clientRequestDuration;
|
Histogram<double> durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration;
|
||||||
string method = request.Method;
|
string method = request.Method;
|
||||||
|
|
||||||
long? startingTimestamp = durationMetric.Enabled ? Stopwatch.GetTimestamp() : null;
|
long? startingTimestamp = durationMetric.Enabled ? Stopwatch.GetTimestamp() : null;
|
||||||
using Activity? activity = Diagnostics.ActivitySource.HasListeners() ?
|
using Activity? activity = Diagnostics.ShouldInstrumentMessage(request) ?
|
||||||
Diagnostics.ActivitySource.StartActivity(CreateActivityName(method)) :
|
Diagnostics.ActivitySource.StartActivity(CreateActivityName(method), ActivityKind.Client) :
|
||||||
null;
|
null;
|
||||||
|
|
||||||
// Set request ID
|
// Set request ID
|
||||||
|
|
@ -354,6 +361,8 @@ internal sealed class McpSession : IDisposable
|
||||||
request.Id = new RequestId($"{_id}-{Interlocked.Increment(ref _nextRequestId)}");
|
request.Id = new RequestId($"{_id}-{Interlocked.Increment(ref _nextRequestId)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_propagator.InjectActivityContext(activity, request);
|
||||||
|
|
||||||
TagList tags = default;
|
TagList tags = default;
|
||||||
bool addTags = activity is { IsAllDataRequested: true } || startingTimestamp is not null;
|
bool addTags = activity is { IsAllDataRequested: true } || startingTimestamp is not null;
|
||||||
|
|
||||||
|
|
@ -363,8 +372,7 @@ internal sealed class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
if (addTags)
|
if (addTags)
|
||||||
{
|
{
|
||||||
AddStandardTags(ref tags, method);
|
AddTags(ref tags, activity, request, method);
|
||||||
AddRpcRequestTags(ref tags, activity, request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expensive logging, use the logging framework to check if the logger is enabled
|
// Expensive logging, use the logging framework to check if the logger is enabled
|
||||||
|
|
@ -396,6 +404,11 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
if (response is JsonRpcResponse success)
|
if (response is JsonRpcResponse success)
|
||||||
{
|
{
|
||||||
|
if (addTags)
|
||||||
|
{
|
||||||
|
AddResponseTags(ref tags, activity, success.Result, method);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null");
|
_logger.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null");
|
||||||
_logger.RequestResponseReceived(EndpointName, request.Method);
|
_logger.RequestResponseReceived(EndpointName, request.Method);
|
||||||
return success;
|
return success;
|
||||||
|
|
@ -407,7 +420,7 @@ internal sealed class McpSession : IDisposable
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (addTags)
|
catch (Exception ex) when (addTags)
|
||||||
{
|
{
|
||||||
AddExceptionTags(ref tags, ex);
|
AddExceptionTags(ref tags, activity, ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -429,22 +442,25 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Histogram<double> durationMetric = _isServer ? s_serverRequestDuration : s_clientRequestDuration;
|
Histogram<double> durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration;
|
||||||
string method = GetMethodName(message);
|
string method = GetMethodName(message);
|
||||||
|
|
||||||
long? startingTimestamp = durationMetric.Enabled ? Stopwatch.GetTimestamp() : null;
|
long? startingTimestamp = durationMetric.Enabled ? Stopwatch.GetTimestamp() : null;
|
||||||
using Activity? activity = Diagnostics.ActivitySource.HasListeners() ?
|
using Activity? activity = Diagnostics.ShouldInstrumentMessage(message) ?
|
||||||
Diagnostics.ActivitySource.StartActivity(CreateActivityName(method)) :
|
Diagnostics.ActivitySource.StartActivity(CreateActivityName(method), ActivityKind.Client) :
|
||||||
null;
|
null;
|
||||||
|
|
||||||
TagList tags = default;
|
TagList tags = default;
|
||||||
bool addTags = activity is { IsAllDataRequested: true } || startingTimestamp is not null;
|
bool addTags = activity is { IsAllDataRequested: true } || startingTimestamp is not null;
|
||||||
|
|
||||||
|
// propagate trace context
|
||||||
|
_propagator?.InjectActivityContext(activity, message);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (addTags)
|
if (addTags)
|
||||||
{
|
{
|
||||||
AddStandardTags(ref tags, method);
|
AddTags(ref tags, activity, message, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
|
@ -466,7 +482,7 @@ internal sealed class McpSession : IDisposable
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (addTags)
|
catch (Exception ex) when (addTags)
|
||||||
{
|
{
|
||||||
AddExceptionTags(ref tags, ex);
|
AddExceptionTags(ref tags, activity, ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -487,77 +503,118 @@ internal sealed class McpSession : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CreateActivityName(string method) =>
|
private string CreateActivityName(string method) => method;
|
||||||
$"mcp.{(_isServer ? "server" : "client")}.{_transportKind}/{method}";
|
|
||||||
|
|
||||||
private static string GetMethodName(IJsonRpcMessage message) =>
|
private static string GetMethodName(IJsonRpcMessage message) =>
|
||||||
message switch
|
message switch
|
||||||
{
|
{
|
||||||
JsonRpcRequest request => request.Method,
|
JsonRpcRequest request => request.Method,
|
||||||
JsonRpcNotification notification => notification.Method,
|
JsonRpcNotification notification => notification.Method,
|
||||||
_ => "unknownMethod",
|
_ => "unknownMethod"
|
||||||
};
|
};
|
||||||
|
|
||||||
private void AddStandardTags(ref TagList tags, string method)
|
private void AddTags(ref TagList tags, Activity? activity, IJsonRpcMessage message, string method)
|
||||||
{
|
{
|
||||||
tags.Add("session.id", _id);
|
tags.Add("mcp.method.name", method);
|
||||||
tags.Add("rpc.system", "jsonrpc");
|
|
||||||
tags.Add("rpc.jsonrpc.version", "2.0");
|
|
||||||
tags.Add("rpc.method", method);
|
|
||||||
tags.Add("network.transport", _transportKind);
|
tags.Add("network.transport", _transportKind);
|
||||||
|
|
||||||
// RPC spans convention also includes:
|
// TODO: When using SSE transport, add:
|
||||||
// server.address, server.port, client.address, client.port, network.peer.address, network.peer.port, network.type
|
// - server.address and server.port on client spans and metrics
|
||||||
}
|
// - client.address and client.port on server spans (not metrics because of cardinality) when using SSE transport
|
||||||
|
if (activity is { IsAllDataRequested: true })
|
||||||
private static void AddRpcRequestTags(ref TagList tags, Activity? activity, JsonRpcRequest request)
|
|
||||||
{
|
|
||||||
tags.Add("rpc.jsonrpc.request_id", request.Id.ToString());
|
|
||||||
|
|
||||||
if (request.Params is JsonObject paramsObj)
|
|
||||||
{
|
{
|
||||||
switch (request.Method)
|
// session and request id have high cardinality, so not applying to metric tags
|
||||||
{
|
activity.AddTag("mcp.session.id", _id);
|
||||||
case RequestMethods.ToolsCall:
|
|
||||||
case RequestMethods.PromptsGet:
|
|
||||||
if (paramsObj.TryGetPropertyValue("name", out var prop) && prop?.GetValueKind() is JsonValueKind.String)
|
|
||||||
{
|
|
||||||
string name = prop.GetValue<string>();
|
|
||||||
tags.Add("mcp.request.params.name", name);
|
|
||||||
if (activity is not null)
|
|
||||||
{
|
|
||||||
activity.DisplayName = $"{request.Method}({name})";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RequestMethods.ResourcesRead:
|
if (message is IJsonRpcMessageWithId withId)
|
||||||
if (paramsObj.TryGetPropertyValue("uri", out prop) && prop?.GetValueKind() is JsonValueKind.String)
|
{
|
||||||
{
|
activity.AddTag("mcp.request.id", withId.Id.Id?.ToString());
|
||||||
string uri = prop.GetValue<string>();
|
|
||||||
tags.Add("mcp.request.params.uri", uri);
|
|
||||||
if (activity is not null)
|
|
||||||
{
|
|
||||||
activity.DisplayName = $"{request.Method}({uri})";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonObject? paramsObj = message switch
|
||||||
|
{
|
||||||
|
JsonRpcRequest request => request.Params as JsonObject,
|
||||||
|
JsonRpcNotification notification => notification.Params as JsonObject,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paramsObj == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? target = null;
|
||||||
|
switch (method)
|
||||||
|
{
|
||||||
|
case RequestMethods.ToolsCall:
|
||||||
|
case RequestMethods.PromptsGet:
|
||||||
|
target = GetStringProperty(paramsObj, "name");
|
||||||
|
if (target is not null)
|
||||||
|
{
|
||||||
|
tags.Add(method == RequestMethods.ToolsCall ? "mcp.tool.name" : "mcp.prompt.name", target);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RequestMethods.ResourcesRead:
|
||||||
|
case RequestMethods.ResourcesSubscribe:
|
||||||
|
case RequestMethods.ResourcesUnsubscribe:
|
||||||
|
case NotificationMethods.ResourceUpdatedNotification:
|
||||||
|
target = GetStringProperty(paramsObj, "uri");
|
||||||
|
if (target is not null)
|
||||||
|
{
|
||||||
|
tags.Add("mcp.resource.uri", target);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity is { IsAllDataRequested: true })
|
||||||
|
{
|
||||||
|
activity.DisplayName = target == null ? method : $"{method} {target}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddExceptionTags(ref TagList tags, Exception e)
|
private static void AddExceptionTags(ref TagList tags, Activity? activity, Exception e)
|
||||||
{
|
{
|
||||||
if (e is AggregateException ae && ae.InnerException is not null and not AggregateException)
|
if (e is AggregateException ae && ae.InnerException is not null and not AggregateException)
|
||||||
{
|
{
|
||||||
e = ae.InnerException;
|
e = ae.InnerException;
|
||||||
}
|
}
|
||||||
|
|
||||||
tags.Add("error.type", e.GetType().FullName);
|
int? intErrorCode = (e as McpException)?.ErrorCode is int errorCode ? errorCode :
|
||||||
tags.Add("rpc.jsonrpc.error_code",
|
e is JsonException ? ErrorCodes.ParseError : null;
|
||||||
(e as McpException)?.ErrorCode is int errorCode ? errorCode :
|
|
||||||
e is JsonException ? ErrorCodes.ParseError :
|
tags.Add("error.type", intErrorCode == null ? e.GetType().FullName : intErrorCode.ToString());
|
||||||
ErrorCodes.InternalError);
|
if (intErrorCode is not null)
|
||||||
|
{
|
||||||
|
tags.Add("rpc.jsonrpc.error_code", intErrorCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity is { IsAllDataRequested: true })
|
||||||
|
{
|
||||||
|
activity.SetStatus(ActivityStatusCode.Error, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddResponseTags(ref TagList tags, Activity? activity, JsonNode? response, string method)
|
||||||
|
{
|
||||||
|
if (response is JsonObject jsonObject
|
||||||
|
&& jsonObject.TryGetPropertyValue("isError", out var isError)
|
||||||
|
&& isError?.GetValueKind() == JsonValueKind.True)
|
||||||
|
{
|
||||||
|
if (activity is { IsAllDataRequested: true })
|
||||||
|
{
|
||||||
|
string? content = null;
|
||||||
|
if (jsonObject.TryGetPropertyValue("content", out var prop) && prop != null)
|
||||||
|
{
|
||||||
|
content = prop.ToJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.SetStatus(ActivityStatusCode.Error, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.Add("error.type", method == RequestMethods.ToolsCall ? "tool_error" : "_OTHER");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void FinalizeDiagnostics(
|
private static void FinalizeDiagnostics(
|
||||||
|
|
@ -590,8 +647,10 @@ internal sealed class McpSession : IDisposable
|
||||||
if (durationMetric.Enabled)
|
if (durationMetric.Enabled)
|
||||||
{
|
{
|
||||||
TagList tags = default;
|
TagList tags = default;
|
||||||
tags.Add("session.id", _id);
|
|
||||||
tags.Add("network.transport", _transportKind);
|
tags.Add("network.transport", _transportKind);
|
||||||
|
|
||||||
|
// TODO: Add server.address and server.port on client-side when using SSE transport,
|
||||||
|
// client.* attributes are not added to metrics because of cardinality
|
||||||
durationMetric.Record(GetElapsed(_sessionStartingTimestamp).TotalSeconds, tags);
|
durationMetric.Record(GetElapsed(_sessionStartingTimestamp).TotalSeconds, tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -614,4 +673,14 @@ internal sealed class McpSession : IDisposable
|
||||||
#else
|
#else
|
||||||
new((long)(s_timestampToTicks * (Stopwatch.GetTimestamp() - startingTimestamp)));
|
new((long)(s_timestampToTicks * (Stopwatch.GetTimestamp() - startingTimestamp)));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private static string? GetStringProperty(JsonObject parameters, string propName)
|
||||||
|
{
|
||||||
|
if (parameters.TryGetPropertyValue(propName, out var prop) && prop?.GetValueKind() is JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return prop.GetValue<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,9 +33,91 @@ public class DiagnosticTests
|
||||||
|
|
||||||
Assert.NotEmpty(activities);
|
Assert.NotEmpty(activities);
|
||||||
|
|
||||||
Activity toolCallActivity = activities.First(a =>
|
var clientToolCall = Assert.Single(activities, a =>
|
||||||
a.Tags.Any(t => t.Key == "rpc.method" && t.Value == "tools/call"));
|
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "DoubleValue") &&
|
||||||
Assert.Equal("DoubleValue", toolCallActivity.Tags.First(t => t.Key == "mcp.request.params.name").Value);
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/call") &&
|
||||||
|
a.DisplayName == "tools/call DoubleValue" &&
|
||||||
|
a.Kind == ActivityKind.Client &&
|
||||||
|
a.Status == ActivityStatusCode.Unset);
|
||||||
|
|
||||||
|
var serverToolCall = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "DoubleValue") &&
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/call") &&
|
||||||
|
a.DisplayName == "tools/call DoubleValue" &&
|
||||||
|
a.Kind == ActivityKind.Server &&
|
||||||
|
a.Status == ActivityStatusCode.Unset);
|
||||||
|
|
||||||
|
Assert.Equal(clientToolCall.SpanId, serverToolCall.ParentSpanId);
|
||||||
|
Assert.Equal(clientToolCall.TraceId, serverToolCall.TraceId);
|
||||||
|
|
||||||
|
var clientListToolsCall = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/list") &&
|
||||||
|
a.DisplayName == "tools/list" &&
|
||||||
|
a.Kind == ActivityKind.Client &&
|
||||||
|
a.Status == ActivityStatusCode.Unset);
|
||||||
|
|
||||||
|
var serverListToolsCall = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/list") &&
|
||||||
|
a.DisplayName == "tools/list" &&
|
||||||
|
a.Kind == ActivityKind.Server &&
|
||||||
|
a.Status == ActivityStatusCode.Unset);
|
||||||
|
|
||||||
|
Assert.Equal(clientListToolsCall.SpanId, serverListToolsCall.ParentSpanId);
|
||||||
|
Assert.Equal(clientListToolsCall.TraceId, serverListToolsCall.TraceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Session_FailedToolCall()
|
||||||
|
{
|
||||||
|
var activities = new List<Activity>();
|
||||||
|
|
||||||
|
using (var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
|
||||||
|
.AddSource("Experimental.ModelContextProtocol")
|
||||||
|
.AddInMemoryExporter(activities)
|
||||||
|
.Build())
|
||||||
|
{
|
||||||
|
await RunConnected(async (client, server) =>
|
||||||
|
{
|
||||||
|
await client.CallToolAsync("Throw", cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
await Assert.ThrowsAsync<McpException>(() => client.CallToolAsync("does-not-exist", cancellationToken: TestContext.Current.CancellationToken));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.NotEmpty(activities);
|
||||||
|
|
||||||
|
var throwToolClient = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "Throw") &&
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/call") &&
|
||||||
|
a.DisplayName == "tools/call Throw" &&
|
||||||
|
a.Kind == ActivityKind.Client);
|
||||||
|
|
||||||
|
Assert.Equal(ActivityStatusCode.Error, throwToolClient.Status);
|
||||||
|
|
||||||
|
var throwToolServer = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "Throw") &&
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/call") &&
|
||||||
|
a.DisplayName == "tools/call Throw" &&
|
||||||
|
a.Kind == ActivityKind.Server);
|
||||||
|
|
||||||
|
Assert.Equal(ActivityStatusCode.Error, throwToolServer.Status);
|
||||||
|
|
||||||
|
var doesNotExistToolClient = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "does-not-exist") &&
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/call") &&
|
||||||
|
a.DisplayName == "tools/call does-not-exist" &&
|
||||||
|
a.Kind == ActivityKind.Client);
|
||||||
|
|
||||||
|
Assert.Equal(ActivityStatusCode.Error, doesNotExistToolClient.Status);
|
||||||
|
Assert.Equal("-32603", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value);
|
||||||
|
|
||||||
|
var doesNotExistToolServer = Assert.Single(activities, a =>
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "does-not-exist") &&
|
||||||
|
a.Tags.Any(t => t.Key == "mcp.method.name" && t.Value == "tools/call") &&
|
||||||
|
a.DisplayName == "tools/call does-not-exist" &&
|
||||||
|
a.Kind == ActivityKind.Server);
|
||||||
|
|
||||||
|
Assert.Equal(ActivityStatusCode.Error, doesNotExistToolServer.Status);
|
||||||
|
Assert.Equal("-32603", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task RunConnected(Func<IMcpClient, IMcpServer, Task> action)
|
private static async Task RunConnected(Func<IMcpClient, IMcpServer, Task> action)
|
||||||
|
|
@ -52,7 +134,10 @@ public class DiagnosticTests
|
||||||
{
|
{
|
||||||
Tools = new()
|
Tools = new()
|
||||||
{
|
{
|
||||||
ToolCollection = [McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." })],
|
ToolCollection = [
|
||||||
|
McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." }),
|
||||||
|
McpServerTool.Create(() => { throw new Exception("boom"); }, new() { Name = "Throw", Description = "Throws error." }),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue