summaryrefslogtreecommitdiff
path: root/src/Agent.Mcp/AgentTools.cs
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
commitb07b41cd435991cd254957af79055080c85b346e (patch)
tree27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /src/Agent.Mcp/AgentTools.cs
parent2fefd29e86c3b926d0cf4a362251d3f119688612 (diff)
feat: AI Agent Sandbox MCP Server v1を実装
Specifications、Design、ImplementationPlanの優先順位を整理し、単一Task実行モデル、typed option優先、設定可能なresource上限、local Ollamaを必須とするRelease Gateを文書化。 .NET 10/C# 14の5 production project、3 test project、E2E Clientを構築。typed YAML、en/ja locale、fixed/user prompt、stdioおよびloopback限定Streamable HTTP、agent_run/agent_status、Session、Ollama text/tool/Vision、TaskGraphとtoken accountingを実装。 Windows sandboxとしてfixed AppContainer、canonical/reparse検査、能力別ACL、path+SHA-256 executable approval、child-process restriction、Job Object、task journal、atomic file transaction、crash recoveryを実装。WFPのIPv4/IPv6 default denyとexact allow、dynamic session、temporary loopback exemption、管理者/BFE診断を追加。 Security/Recovery test、MCP/Ollama integration test、stdio/HTTP Operational E2E、Release Bundle生成・checksum・manifest・fresh extraction検査を追加。Requirement TraceabilityとPhase 0〜11の進捗、初回セットアップ可能なREADMEを整備。 Debug/Release/publish/objをrepository rootのbuild/へ集約し、旧project-local bin/objへの依存を除去。artifacts/とbuild/をGit管理外に設定し、clean checkoutでもsolution buildからE2E Clientまで生成可能にした。
Diffstat (limited to 'src/Agent.Mcp/AgentTools.cs')
-rw-r--r--src/Agent.Mcp/AgentTools.cs90
1 files changed, 90 insertions, 0 deletions
diff --git a/src/Agent.Mcp/AgentTools.cs b/src/Agent.Mcp/AgentTools.cs
new file mode 100644
index 0000000..c44d6ec
--- /dev/null
+++ b/src/Agent.Mcp/AgentTools.cs
@@ -0,0 +1,90 @@
+using System.ComponentModel;
+using Agent.Core.Localization;
+using Agent.Core.Tasks;
+using ModelContextProtocol.Server;
+using ModelContextProtocol.Protocol;
+
+namespace Agent.Mcp;
+
+[McpServerToolType]
+public sealed class AgentTools(TaskRegistry registry, LocaleCatalog locale, VisionLimits visionLimits)
+{
+ [McpServerTool(Name = "agent_run", ReadOnly = false, Destructive = true, OpenWorld = false, UseStructuredContent = false)]
+ [Description("Queue a natural-language request for asynchronous execution.")]
+ public string AgentRun(
+ RequestContext<CallToolRequestParams> context,
+ [Description("Non-empty natural-language work request.")] string request,
+ [Description("Existing session UUID, or omit to create a session.")] Guid? session_id = null,
+ [Description("Task-scoped temperature from 0.0 through 2.0.")] double? temperature = null,
+ [Description("Optional PNG, JPEG, or WebP images.")] IReadOnlyList<McpImageInput>? images = null)
+ {
+ RequireOnly(context, "request", "session_id", "temperature", "images");
+ ValidateImages(images);
+ TaskSnapshot task = registry.Enqueue(request, session_id, temperature, images?.Select(static image => image.DataBase64).ToArray());
+ string message = locale.Format("mcp.task_queued", new Dictionary<string, object?>
+ {
+ ["task"] = task.TaskId,
+ ["session"] = task.SessionId
+ });
+ return string.Join('\n',
+ $"task_id: {task.TaskId}",
+ $"session_id: {task.SessionId}",
+ "status: queued",
+ $"message: {message}");
+ }
+
+ private void ValidateImages(IReadOnlyList<McpImageInput>? images)
+ {
+ if (images is null) return;
+ if (images.Count > visionLimits.MaxCount) throw new ArgumentException($"Image count exceeds configured limit {visionLimits.MaxCount}.");
+ long total = 0;
+ foreach (McpImageInput image in images)
+ {
+ if (image.MimeType is not ("image/png" or "image/jpeg" or "image/webp")) throw new ArgumentException($"Unsupported image MIME type '{image.MimeType}'.");
+ byte[] bytes;
+ try { bytes = Convert.FromBase64String(image.DataBase64); }
+ catch (FormatException ex) { throw new ArgumentException("Image data_base64 is invalid.", ex); }
+ if (bytes.LongLength > visionLimits.MaxBytesEach) throw new ArgumentException($"Image exceeds configured per-image limit {visionLimits.MaxBytesEach} bytes.");
+ total = checked(total + bytes.LongLength);
+ if (total > visionLimits.MaxTotalBytes) throw new ArgumentException($"Images exceed configured total limit {visionLimits.MaxTotalBytes} bytes.");
+ }
+ }
+
+ [McpServerTool(Name = "agent_status", ReadOnly = true, Destructive = false, OpenWorld = false, UseStructuredContent = false)]
+ [Description("Get the current state and result of a queued agent task.")]
+ public string AgentStatus(
+ RequestContext<CallToolRequestParams> context,
+ [Description("Task UUID returned by agent_run.")] Guid task_id,
+ [Description("Optional session UUID used to verify task ownership.")] Guid? session_id = null)
+ {
+ RequireOnly(context, "task_id", "session_id");
+ if (!registry.TryGet(task_id, session_id, out TaskSnapshot? task) || task is null)
+ {
+ return string.Join('\n',
+ $"task_id: {task_id}",
+ "status: failed",
+ $"message: {locale.Format("mcp.task_not_found", new Dictionary<string, object?> { ["task"] = task_id })}");
+ }
+
+ string state = task.State.ToString().ToLowerInvariant();
+ bool terminal = task.State is AgentTaskState.Completed or AgentTaskState.Failed or AgentTaskState.Cancelled;
+ return string.Join('\n',
+ $"task_id: {task.TaskId}",
+ $"session_id: {task.SessionId}",
+ $"status: {state}",
+ $"completed_tasks: {task.CompletedTasks}/{task.TotalTasks}",
+ $"current_task: {task.CurrentTask ?? string.Empty}",
+ $"token_usage: input={task.InputTokens} output={task.OutputTokens} total={task.InputTokens + task.OutputTokens}",
+ $"critical_path_tokens: {task.CriticalPathTokens.ToString(System.Globalization.CultureInfo.InvariantCulture)}",
+ $"message: {locale.Get("mcp.status." + state)}",
+ terminal ? $"result: {task.Result}" : "result:");
+ }
+
+ private static void RequireOnly(RequestContext<CallToolRequestParams> context, params string[] allowed)
+ {
+ if (context.Params?.Arguments is null) return;
+ HashSet<string> allowedSet = allowed.ToHashSet(StringComparer.Ordinal);
+ string? unknown = context.Params.Arguments.Keys.FirstOrDefault(key => !allowedSet.Contains(key));
+ if (unknown is not null) throw new ArgumentException($"Unknown tool property '{unknown}'.");
+ }
+}