diff --git a/Releases/0.10.10.md b/Releases/0.10.10.md
new file mode 100644
index 00000000..66e6b23d
--- /dev/null
+++ b/Releases/0.10.10.md
@@ -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.
diff --git a/src/MaIN.Core/.nuspec b/src/MaIN.Core/.nuspec
index 8ed72828..ba30e6a5 100644
--- a/src/MaIN.Core/.nuspec
+++ b/src/MaIN.Core/.nuspec
@@ -2,7 +2,7 @@
MaIN.NET
- 0.10.9
+ 0.10.10
Wisedev
Wisedev
favicon.png
@@ -34,4 +34,4 @@
-
\ No newline at end of file
+
diff --git a/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs b/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs
index e0c0aa5b..166f7ea9 100644
--- a/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs
+++ b/src/MaIN.Core/Hub/Contexts/Interfaces/McpContext/IMcpContext.cs
@@ -1,4 +1,4 @@
-using MaIN.Domain.Configuration;
+using MaIN.Domain.Configuration;
using MaIN.Domain.Entities;
using MaIN.Services.Services.Models;
@@ -22,10 +22,20 @@ public interface IMcpContext
/// The context instance implementing for method chaining.
IMcpContext WithBackend(BackendType backendType);
+ ///
+ /// 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.
+ ///
+ ///
+ /// Not supported for and backends -
+ /// a will be thrown at runtime when is called.
+ ///
+ IMcpContext WithMaxIterations(int maxIterations);
+
///
/// Asynchronously processes a prompt through the configured MCP service, sending the prompt to the MCP server and returning the processed result.
///
/// The text prompt to be processed by the MCP service
/// A object containing the processed response from the MCP server.
Task PromptAsync(string prompt);
-}
\ No newline at end of file
+}
diff --git a/src/MaIN.Core/Hub/Contexts/McpContext.cs b/src/MaIN.Core/Hub/Contexts/McpContext.cs
index a29b45d4..fccb40a8 100644
--- a/src/MaIN.Core/Hub/Contexts/McpContext.cs
+++ b/src/MaIN.Core/Hub/Contexts/McpContext.cs
@@ -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;
@@ -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)
{
@@ -24,7 +26,10 @@ public IMcpContext WithConfig(Mcp mcpConfig)
{
_mcpConfig = mcpConfig;
if (_explicitBackend.HasValue)
+ {
_mcpConfig.Backend = _explicitBackend;
+ }
+
return this;
}
@@ -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 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);
}
-}
\ No newline at end of file
+}
diff --git a/src/MaIN.Core/Hub/Utils/ToolConfigurationBuilder.cs b/src/MaIN.Core/Hub/Utils/ToolConfigurationBuilder.cs
index 3c353bb6..a6f7ce06 100644
--- a/src/MaIN.Core/Hub/Utils/ToolConfigurationBuilder.cs
+++ b/src/MaIN.Core/Hub/Utils/ToolConfigurationBuilder.cs
@@ -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> 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 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(
- string name,
- string description,
+ string name,
+ string description,
object parameters,
Func> 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(argsJson,
- new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
- var result = await execute(args);
- return JsonSerializer.Serialize(result);
- }
- });
- return this;
+ var args = JsonSerializer.Deserialize(argsJson, s_deserializeOptions)!;
+ return JsonSerializer.Serialize(await execute(args));
+ });
}
public ToolsConfigurationBuilder AddTool(
- string name,
- string description,
+ string name,
+ string description,
object parameters,
Func 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(argsJson,
- new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
- var result = execute(args);
- return Task.FromResult(JsonSerializer.Serialize(result));
- }
- });
- return this;
+ var args = JsonSerializer.Deserialize(argsJson, s_deserializeOptions)!;
+ return Task.FromResult(JsonSerializer.Serialize(execute(args)));
+ });
}
public ToolsConfigurationBuilder AddTool(
- string name,
+ string name,
string description,
Func> 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