forked from admin/VolcanicAgenticCore
McpServerConnection starts the session's server lazily through PrepareProcessAsync, applies the call timeout, reports exits with the stderr tail and restarts on the next call, stops when idle, and supports StopAsync (reusable) and idempotent DisposeAsync. McpTool exposes a catalogue entry as an ITool and maps results with McpResultMapper. Spec change (ยง5, Decisions): ModelContextProtocol.Core 2.2.0 does not send notifications/cancelled when a request's token fires (verified against the fake server), so calls go out through SendRequestAsync with their own request id and the connection sends notifications/cancelled itself on abort and timeout. Also documents exit detection, StopAsync vs DisposeAsync and JSON-RPC errors as ordinary error results, per the plan.
50 lines
2.2 KiB
C#
50 lines
2.2 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Volcanic.AgenticCore.Abstractions.Messages;
|
|
using Volcanic.AgenticCore.Abstractions.Tools;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Mcp;
|
|
|
|
/// <summary>An MCP server tool as an agent tool. The schema passes through; the server validates what the core doesn't.</summary>
|
|
internal sealed class McpTool : ITool
|
|
{
|
|
private static readonly JsonElement ObjectSchema = JsonSerializer.SerializeToElement(new { type = "object" });
|
|
|
|
private readonly McpCatalogTool _tool;
|
|
private readonly McpServerConnection _connection;
|
|
private readonly bool _parallel;
|
|
|
|
public McpTool(McpCatalogTool tool, McpServerConnection connection, bool parallel)
|
|
{
|
|
_tool = tool;
|
|
_connection = connection;
|
|
_parallel = parallel;
|
|
Description = !string.IsNullOrWhiteSpace(tool.Description) ? tool.Description!
|
|
: !string.IsNullOrWhiteSpace(tool.Title) ? tool.Title!
|
|
: tool.McpName;
|
|
Parameters = NormalizeSchema(tool.InputSchema);
|
|
}
|
|
|
|
public string Name => _tool.ExposedName;
|
|
public string Description { get; }
|
|
public JsonElement Parameters { get; }
|
|
public string? PromptSnippet => null;
|
|
public ToolExecutionMode ExecutionMode => _parallel ? ToolExecutionMode.Parallel : ToolExecutionMode.Sequential;
|
|
|
|
public async Task<ToolResult> ExecuteAsync(ToolCallContent call, ToolInvocation invocation, CancellationToken cancellationToken)
|
|
{
|
|
var result = await _connection.CallAsync(_tool.McpName, call.Arguments, invocation.Context, cancellationToken).ConfigureAwait(false);
|
|
return McpResultMapper.Map(_connection.Name, _tool.McpName, result);
|
|
}
|
|
|
|
/// <summary>A missing or non-object schema becomes <c>{"type":"object"}</c>; a top-level <c>$schema</c> is dropped.</summary>
|
|
internal static JsonElement NormalizeSchema(JsonElement schema)
|
|
{
|
|
if (schema.ValueKind != JsonValueKind.Object) return ObjectSchema;
|
|
if (!schema.TryGetProperty("$schema", out _)) return schema.Clone();
|
|
var node = JsonNode.Parse(schema.GetRawText())!.AsObject();
|
|
node.Remove("$schema");
|
|
return JsonSerializer.SerializeToElement(node);
|
|
}
|
|
}
|