Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Releases/0.10.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 0.10.10 release

- Adds `WithMaxIterations()` to `ToolsConfigurationBuilder` and `McpContext` to override the default tool-call iteration limit.
- Fix: MCP loop now sends a final synthesis request instead of returning an error string when the iteration cap is reached.
- Fix: Gemini/Vertex backends now throw `NotSupportedException` when `WithMaxIterations` is used instead of silently ignoring the value.
4 changes: 2 additions & 2 deletions src/MaIN.Core/.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package>
<metadata>
<id>MaIN.NET</id>
<version>0.10.9</version>
<version>0.10.10</version>
<authors>Wisedev</authors>
<owners>Wisedev</owners>
<icon>favicon.png</icon>
Expand Down Expand Up @@ -34,4 +34,4 @@
<file src="..\MaIN.Domain\bin\Release\net8.0\MaIN.Domain.dll" target="lib\net8.0" />
<file src="..\MaIN.Infrastructure\bin\Release\net8.0\MaIN.Infrastructure.dll" target="lib\net8.0" />
</files>
</package>
</package>
14 changes: 12 additions & 2 deletions src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using MaIN.Domain.Configuration;
using MaIN.Domain.Configuration;
using MaIN.Domain.Entities;
using MaIN.Services.Services.Models;

Expand All @@ -22,10 +22,20 @@ public interface IMcpContext
/// <returns>The context instance implementing <see cref="IMcpContext"/> for method chaining.</returns>
IMcpContext WithBackend(BackendType backendType);

/// <summary>
/// Sets the maximum number of tool-call iterations allowed in a single MCP prompt.
/// Overrides the default limit of 10. Must be at least 1.
/// </summary>
/// <remarks>
/// Not supported for <see cref="BackendType.Gemini"/> and <see cref="BackendType.Vertex"/> backends -
/// a <see cref="NotSupportedException"/> will be thrown at runtime when <see cref="PromptAsync"/> is called.
/// </remarks>
IMcpContext WithMaxIterations(int maxIterations);

/// <summary>
/// Asynchronously processes a prompt through the configured MCP service, sending the prompt to the MCP server and returning the processed result.
/// </summary>
/// <param name="prompt">The text prompt to be processed by the MCP service</param>
/// <returns>A <see cref="McpResult"/> object containing the processed response from the MCP server.</returns>
Task<McpResult> PromptAsync(string prompt);
}
}
20 changes: 16 additions & 4 deletions src/MaIN.Core/Hub/Contexts/McpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using MaIN.Domain.Configuration;
using MaIN.Domain.Entities;
using MaIN.Domain.Exceptions.MPC;
using MaIN.Domain.Exceptions.Tools;
using MaIN.Services.Constants;
using MaIN.Services.Services.Abstract;
using MaIN.Services.Services.Models;
Expand All @@ -13,6 +14,7 @@ public sealed class McpContext : IMcpContext
private readonly IMcpService _mcpService;
private Mcp? _mcpConfig;
private BackendType? _explicitBackend;
private int? _maxIterations;

internal McpContext(IMcpService mcpService)
{
Expand All @@ -24,7 +26,10 @@ public IMcpContext WithConfig(Mcp mcpConfig)
{
_mcpConfig = mcpConfig;
if (_explicitBackend.HasValue)
{
_mcpConfig.Backend = _explicitBackend;
}

return this;
}

Expand All @@ -35,18 +40,25 @@ public IMcpContext WithBackend(BackendType backendType)
return this;
}

public IMcpContext WithMaxIterations(int maxIterations)
{
InvalidToolIterationsException.ThrowIfInvalid(maxIterations);
_maxIterations = maxIterations;
return this;
}

public async Task<McpResult> PromptAsync(string prompt)
{
if (_mcpConfig == null)
{
throw new MPCConfigNotFoundException();
}
return await _mcpService.Prompt(_mcpConfig!, [new Message()

return await _mcpService.Prompt(_mcpConfig, [new Message()
{
Content = prompt,
Role = ServiceConstants.Roles.User,
Type = MessageType.CloudLLM
}]);
}], _maxIterations);
}
}
}
153 changes: 53 additions & 100 deletions src/MaIN.Core/Hub/Utils/ToolConfigurationBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,150 +1,96 @@
using System.Text.Json;
using MaIN.Domain.Entities.Tools;
using MaIN.Domain.Exceptions.Tools;

namespace MaIN.Core.Hub.Utils;
//TODO try to share logic of adding tool to the list across methods https://github.com/wisedev-code/MaIN.NET/pull/98#discussion_r2454997846

public sealed class ToolsConfigurationBuilder
{
private static readonly JsonSerializerOptions s_deserializeOptions = new() { PropertyNameCaseInsensitive = true };
private readonly ToolsConfiguration _config = new() { Tools = [] };

public ToolsConfigurationBuilder AddDefaultTool(
string type)

public ToolsConfigurationBuilder AddDefaultTool(string type)
{
_config.Tools.Add(new ToolDefinition
{
Type = type
});
_config.Tools.Add(new ToolDefinition { Type = type });
return this;
}

public ToolsConfigurationBuilder AddTool(
string name,
string description,
string name,
string description,
object parameters,
Func<string, Task<string>> execute)
{
_config.Tools.Add(new ToolDefinition
{
Function = new FunctionDefinition
{
Name = name,
Description = description,
Parameters = parameters
},
Execute = execute
});
return this;
return AddToolCore(name, description, parameters, execute);
}

public ToolsConfigurationBuilder AddTool(
string name,
string description,
string name,
string description,
object parameters,
Func<string, string> execute)
{
_config.Tools!.Add(new ToolDefinition
{
Function = new FunctionDefinition
{
Name = name,
Description = description,
Parameters = parameters
},
Execute = args => Task.FromResult(execute(args))
});
return this;
return AddToolCore(name, description, parameters, args => Task.FromResult(execute(args)));
}

public ToolsConfigurationBuilder AddTool<TArgs>(
string name,
string description,
string name,
string description,
object parameters,
Func<TArgs, Task<object>> execute) where TArgs : class
{
_config.Tools.Add(new ToolDefinition
{
Function = new FunctionDefinition
return AddToolCore(name, description, parameters, async argsJson =>
{
Name = name,
Description = description,
Parameters = parameters
},
Execute = async (argsJson) =>
{
var args = JsonSerializer.Deserialize<TArgs>(argsJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
var result = await execute(args);
return JsonSerializer.Serialize(result);
}
});
return this;
var args = JsonSerializer.Deserialize<TArgs>(argsJson, s_deserializeOptions)!;
return JsonSerializer.Serialize(await execute(args));
});
}

public ToolsConfigurationBuilder AddTool<TArgs>(
string name,
string description,
string name,
string description,
object parameters,
Func<TArgs, object> execute) where TArgs : class
{
_config.Tools!.Add(new ToolDefinition
{
Function = new FunctionDefinition
return AddToolCore(name, description, parameters, argsJson =>
{
Name = name,
Description = description,
Parameters = parameters
},
Execute = (argsJson) =>
{
var args = JsonSerializer.Deserialize<TArgs>(argsJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
var result = execute(args);
return Task.FromResult(JsonSerializer.Serialize(result));
}
});
return this;
var args = JsonSerializer.Deserialize<TArgs>(argsJson, s_deserializeOptions)!;
return Task.FromResult(JsonSerializer.Serialize(execute(args)));
});
}

public ToolsConfigurationBuilder AddTool(
string name,
string name,
string description,
Func<Task<object>> execute)
{
_config.Tools.Add(new ToolDefinition
{
Function = new FunctionDefinition
{
Name = name,
Description = description,
Parameters = new { type = "object", properties = new { } }
},
Execute = async (args) =>
{
var result = await execute();
return JsonSerializer.Serialize(result);
}
});
return this;
return AddToolCore(
name,
description,
new { type = "object", properties = new { } },
async _ => JsonSerializer.Serialize(await execute()));
}

public ToolsConfigurationBuilder AddTool(
string name,
string name,
string description,
Func<object> execute)
=> AddToolCore(
name,
description,
new { type = "object", properties = new { } },
_ => Task.FromResult(JsonSerializer.Serialize(execute())));

private ToolsConfigurationBuilder AddToolCore(
string name,
string description,
object parameters,
Func<string, Task<string>> execute)
{
_config.Tools.Add(new ToolDefinition
{
Function = new FunctionDefinition
{
Name = name,
Description = description,
Parameters = new { type = "object", properties = new { } }
},
Execute = (args) =>
{
var result = execute();
return Task.FromResult(JsonSerializer.Serialize(result));
}
Function = new FunctionDefinition { Name = name, Description = description, Parameters = parameters },
Execute = execute
});
return this;
}
Expand All @@ -155,5 +101,12 @@ public ToolsConfigurationBuilder WithToolChoice(string choice)
return this;
}

public ToolsConfigurationBuilder WithMaxIterations(int maxIterations)
{
InvalidToolIterationsException.ThrowIfInvalid(maxIterations);
_config.MaxIterations = maxIterations;
return this;
}

public ToolsConfiguration Build() => _config;
}
}
5 changes: 3 additions & 2 deletions src/MaIN.Domain/Entities/Tools/ToolsConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ public class ToolsConfiguration
{
public required List<ToolDefinition> Tools { get; set; }
public string? ToolChoice { get; set; }

public int? MaxIterations { get; set; }

public Func<string, Task<string>>? GetExecutor(string functionName)
{
return Tools.FirstOrDefault(t => t.Function!.Name == functionName)?.Execute;
}
}
}
18 changes: 18 additions & 0 deletions src/MaIN.Domain/Exceptions/Tools/InvalidToolIterationsException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Net;

namespace MaIN.Domain.Exceptions.Tools;

public class InvalidToolIterationsException(int value)
: MaINCustomException($"MaxIterations must be at least 1, but received {value}.")
{
public override string PublicErrorMessage => Message;
public override HttpStatusCode HttpStatusCode => HttpStatusCode.BadRequest;

public static void ThrowIfInvalid(int value)
{
if (value < 1)
{
throw new InvalidToolIterationsException(value);
}
}
}
6 changes: 3 additions & 3 deletions src/MaIN.Services/Services/Abstract/IMcpService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using MaIN.Domain.Entities;
using MaIN.Domain.Entities;
using MaIN.Services.Services.Models;

namespace MaIN.Services.Services.Abstract;

public interface IMcpService
{
Task<McpResult> Prompt(Mcp config, List<Message> messageHistory);
}
Task<McpResult> Prompt(Mcp config, List<Message> messageHistory, int? maxIterations = null);
}
Loading