From fa19ee613e44549de2a8cee071ebddab0050f5a9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 20:45:43 +0000
Subject: [PATCH 01/14] Initial plan
From 44709fdae1fadd57eb926da901658a62e04e9a91 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 20:51:42 +0000
Subject: [PATCH 02/14] Add NetInteractor.Mcp project with MCP server tools and
unit tests
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
Directory.Build.props | 1 +
Directory.Packages.props | 1 +
NetInteractor.sln | 32 +++
src/NetInteractor.Mcp/ExecuteScriptResult.cs | 25 ++
src/NetInteractor.Mcp/GetRequestResult.cs | 38 +++
.../NetInteractor.Mcp.csproj | 23 ++
src/NetInteractor.Mcp/NetInteractorTools.cs | 218 ++++++++++++++++
src/NetInteractor.Mcp/PostRequestResult.cs | 38 +++
.../NetInteractor.Mcp.Test.csproj | 18 ++
.../NetInteractorToolsTests.cs | 240 ++++++++++++++++++
10 files changed, 634 insertions(+)
create mode 100644 src/NetInteractor.Mcp/ExecuteScriptResult.cs
create mode 100644 src/NetInteractor.Mcp/GetRequestResult.cs
create mode 100644 src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
create mode 100644 src/NetInteractor.Mcp/NetInteractorTools.cs
create mode 100644 src/NetInteractor.Mcp/PostRequestResult.cs
create mode 100644 test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj
create mode 100644 test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
diff --git a/Directory.Build.props b/Directory.Build.props
index 0f151ed..e794c6e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,7 @@
netstandard2.0;net8.0;net9.0;net10.0
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7238e77..da9a6c7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,6 +10,7 @@
+
diff --git a/NetInteractor.sln b/NetInteractor.sln
index 8a05bf0..46b0362 100644
--- a/NetInteractor.sln
+++ b/NetInteractor.sln
@@ -13,6 +13,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.PuppeteerShar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.Playwright", "src\NetInteractor.Playwright\NetInteractor.Playwright.csproj", "{1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.Mcp", "src\NetInteractor.Mcp\NetInteractor.Mcp.csproj", "{55944B84-EAC8-4723-90BB-C29B9FD433E4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.Mcp.Test", "test\NetInteractor.Mcp.Test\NetInteractor.Mcp.Test.csproj", "{DD2E1705-6543-4068-8293-7531AEEDDF4F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -71,6 +77,30 @@ Global
{1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x64.Build.0 = Release|Any CPU
{1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x86.ActiveCfg = Release|Any CPU
{1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x86.Build.0 = Release|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Debug|x64.Build.0 = Debug|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Debug|x86.Build.0 = Debug|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Release|x64.ActiveCfg = Release|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Release|x64.Build.0 = Release|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Release|x86.ActiveCfg = Release|Any CPU
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4}.Release|x86.Build.0 = Release|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Debug|x64.Build.0 = Debug|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Debug|x86.Build.0 = Debug|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Release|x64.ActiveCfg = Release|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Release|x64.Build.0 = Release|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Release|x86.ActiveCfg = Release|Any CPU
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -80,5 +110,7 @@ Global
{A9186BD7-EFFC-4B4D-B828-C4F4AA924C50} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
{96C0F6DB-7F80-4E41-B9E8-75C5F750B4BF} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
{1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
+ {55944B84-EAC8-4723-90BB-C29B9FD433E4} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
+ {DD2E1705-6543-4068-8293-7531AEEDDF4F} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
EndGlobalSection
EndGlobal
diff --git a/src/NetInteractor.Mcp/ExecuteScriptResult.cs b/src/NetInteractor.Mcp/ExecuteScriptResult.cs
new file mode 100644
index 0000000..37a7a06
--- /dev/null
+++ b/src/NetInteractor.Mcp/ExecuteScriptResult.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace NetInteractor.Mcp
+{
+ ///
+ /// Result of executing a NetInteractor script.
+ ///
+ public class ExecuteScriptResult
+ {
+ ///
+ /// Indicates whether the script execution was successful.
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Message describing the result or error.
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// Extracted output values from the script execution.
+ ///
+ public Dictionary? Outputs { get; set; }
+ }
+}
diff --git a/src/NetInteractor.Mcp/GetRequestResult.cs b/src/NetInteractor.Mcp/GetRequestResult.cs
new file mode 100644
index 0000000..c658c9e
--- /dev/null
+++ b/src/NetInteractor.Mcp/GetRequestResult.cs
@@ -0,0 +1,38 @@
+namespace NetInteractor.Mcp
+{
+ ///
+ /// Result of an HTTP GET request.
+ ///
+ public class GetRequestResult
+ {
+ ///
+ /// Indicates whether the request was successful (2xx status code).
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// HTTP status code of the response.
+ ///
+ public int StatusCode { get; set; }
+
+ ///
+ /// Final URL after any redirects.
+ ///
+ public string? Url { get; set; }
+
+ ///
+ /// Value extracted using XPath (if xpath parameter was provided).
+ ///
+ public string? ExtractedValue { get; set; }
+
+ ///
+ /// Raw HTML content (if no xpath was specified).
+ ///
+ public string? Html { get; set; }
+
+ ///
+ /// Error message if the request failed.
+ ///
+ public string? Message { get; set; }
+ }
+}
diff --git a/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj b/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
new file mode 100644
index 0000000..0bd4d01
--- /dev/null
+++ b/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
@@ -0,0 +1,23 @@
+
+
+ net8.0;net9.0;net10.0
+ enable
+ Kerry Jiang
+ MCP (Model Context Protocol) Server Tools for NetInteractor - enables AI agents like GitHub Copilot and Claude to execute web automation scripts.
+ Apache-2.0
+ Web;Automation;MCP;ModelContextProtocol;AI;Copilot;Claude;WebAutomation;LLM;Tools
+ https://github.com/kerryjiang/NetInteractor.git
+
+
+ v$(PackageVersion).md
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
new file mode 100644
index 0000000..2b88d92
--- /dev/null
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using ModelContextProtocol.Server;
+using NetInteractor.Config;
+using NetInteractor.WebAccessors;
+
+namespace NetInteractor.Mcp
+{
+ ///
+ /// MCP Server Tools for NetInteractor web automation.
+ /// These tools enable AI agents to execute web interactions and automation scripts.
+ ///
+ [McpServerToolType]
+ public static class NetInteractorTools
+ {
+ ///
+ /// Executes a NetInteractor XML script for web automation.
+ ///
+ /// The XML script defining the web interaction workflow. Example:
+ /// <InteractConfig defaultTarget='Main'>
+ /// <target name='Main'>
+ /// <get url='https://example.com'>
+ /// <output name='title' xpath='//title' attr='text()' />
+ /// </get>
+ /// </target>
+ /// </InteractConfig>
+ /// Optional comma-separated key=value pairs for script inputs (e.g., "BaseUrl=https://example.com,Username=user").
+ /// Optional target name to execute. If not specified, the default target will be used.
+ /// The execution result containing outputs and status.
+ [McpServerTool(Name = "netinteractor_execute_script")]
+ [Description("Executes a NetInteractor XML script for web automation. Use this to run complex multi-step web workflows including GET requests, form submissions, data extraction, and conditional logic.")]
+ public static async Task ExecuteScriptAsync(
+ string script,
+ string? inputs = null,
+ string? target = null)
+ {
+ try
+ {
+ var webAccessor = new HttpClientWebAccessor();
+ var executor = new InterationExecutor(webAccessor);
+
+ var inputValues = ParseInputs(inputs);
+ var result = await executor.ExecuteAsync(script, inputValues, target);
+
+ return new ExecuteScriptResult
+ {
+ Success = result.Ok,
+ Message = result.Message,
+ Outputs = result.Outputs != null ? ConvertOutputs(result.Outputs) : null
+ };
+ }
+ catch (Exception ex)
+ {
+ return new ExecuteScriptResult
+ {
+ Success = false,
+ Message = $"Script execution failed: {ex.Message}"
+ };
+ }
+ }
+
+ ///
+ /// Performs an HTTP GET request and extracts data using XPath.
+ ///
+ /// The URL to fetch.
+ /// Optional XPath expression to extract content from the HTML.
+ /// Optional attribute to extract from the XPath result (e.g., 'href', 'src'). Use 'text()' for text content.
+ /// The result containing the extracted content or raw HTML.
+ [McpServerTool(Name = "netinteractor_get")]
+ [Description("Performs an HTTP GET request and optionally extracts data using XPath. Use this for simple web scraping and data extraction from web pages.")]
+ public static async Task GetAsync(
+ string url,
+ string? xpath = null,
+ string? attribute = null)
+ {
+ try
+ {
+ var webAccessor = new HttpClientWebAccessor();
+ var response = await webAccessor.GetAsync(url);
+
+ var result = new GetRequestResult
+ {
+ Success = response.StatusCode >= 200 && response.StatusCode < 300,
+ StatusCode = response.StatusCode,
+ Url = response.Url
+ };
+
+ if (!string.IsNullOrEmpty(xpath))
+ {
+ var pageInfo = new PageInfo(response.Url ?? url, response.Html ?? string.Empty);
+ var extractedValue = ExtractValue(pageInfo, xpath, attribute);
+ result.ExtractedValue = extractedValue;
+ }
+ else
+ {
+ result.Html = response.Html;
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ return new GetRequestResult
+ {
+ Success = false,
+ StatusCode = 0,
+ Message = $"GET request failed: {ex.Message}"
+ };
+ }
+ }
+
+ ///
+ /// Performs an HTTP POST request with form data.
+ ///
+ /// The URL to post to.
+ /// Comma-separated key=value pairs for form data (e.g., "name=John,email=john@example.com").
+ /// Optional XPath expression to extract content from the response HTML.
+ /// Optional attribute to extract from the XPath result.
+ /// The result containing the extracted content or raw HTML.
+ [McpServerTool(Name = "netinteractor_post")]
+ [Description("Performs an HTTP POST request with form data and optionally extracts data from the response using XPath. Use this for submitting forms and processing responses.")]
+ public static async Task PostAsync(
+ string url,
+ string formData,
+ string? xpath = null,
+ string? attribute = null)
+ {
+ try
+ {
+ var webAccessor = new HttpClientWebAccessor();
+ var formValues = ParseInputs(formData);
+ var response = await webAccessor.PostAsync(url, formValues);
+
+ var result = new PostRequestResult
+ {
+ Success = response.StatusCode >= 200 && response.StatusCode < 300,
+ StatusCode = response.StatusCode,
+ Url = response.Url
+ };
+
+ if (!string.IsNullOrEmpty(xpath))
+ {
+ var pageInfo = new PageInfo(response.Url ?? url, response.Html ?? string.Empty);
+ var extractedValue = ExtractValue(pageInfo, xpath, attribute);
+ result.ExtractedValue = extractedValue;
+ }
+ else
+ {
+ result.Html = response.Html;
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ return new PostRequestResult
+ {
+ Success = false,
+ StatusCode = 0,
+ Message = $"POST request failed: {ex.Message}"
+ };
+ }
+ }
+
+ private static NameValueCollection ParseInputs(string? inputs)
+ {
+ var result = new NameValueCollection();
+
+ if (string.IsNullOrEmpty(inputs))
+ return result;
+
+ var pairs = inputs.Split(',');
+ foreach (var pair in pairs)
+ {
+ var keyValue = pair.Split(['='], 2);
+ if (keyValue.Length == 2)
+ {
+ result[keyValue[0].Trim()] = keyValue[1].Trim();
+ }
+ }
+
+ return result;
+ }
+
+ private static string? ExtractValue(PageInfo pageInfo, string xpath, string? attribute)
+ {
+ var node = pageInfo.Document.DocumentNode.SelectSingleNode(xpath);
+
+ if (node == null)
+ return null;
+
+ if (string.IsNullOrEmpty(attribute) || attribute.Equals("text()", StringComparison.OrdinalIgnoreCase))
+ {
+ return node.InnerText?.Trim();
+ }
+
+ return node.GetAttributeValue(attribute, null);
+ }
+
+ private static System.Collections.Generic.Dictionary? ConvertOutputs(NameValueCollection outputs)
+ {
+ if (outputs == null || outputs.Count == 0)
+ return null;
+
+ var dict = new System.Collections.Generic.Dictionary();
+ foreach (string? key in outputs.AllKeys)
+ {
+ if (key != null)
+ {
+ dict[key] = outputs[key] ?? string.Empty;
+ }
+ }
+ return dict;
+ }
+ }
+}
diff --git a/src/NetInteractor.Mcp/PostRequestResult.cs b/src/NetInteractor.Mcp/PostRequestResult.cs
new file mode 100644
index 0000000..3a54af4
--- /dev/null
+++ b/src/NetInteractor.Mcp/PostRequestResult.cs
@@ -0,0 +1,38 @@
+namespace NetInteractor.Mcp
+{
+ ///
+ /// Result of an HTTP POST request.
+ ///
+ public class PostRequestResult
+ {
+ ///
+ /// Indicates whether the request was successful (2xx status code).
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// HTTP status code of the response.
+ ///
+ public int StatusCode { get; set; }
+
+ ///
+ /// Final URL after any redirects.
+ ///
+ public string? Url { get; set; }
+
+ ///
+ /// Value extracted using XPath (if xpath parameter was provided).
+ ///
+ public string? ExtractedValue { get; set; }
+
+ ///
+ /// Raw HTML content (if no xpath was specified).
+ ///
+ public string? Html { get; set; }
+
+ ///
+ /// Error message if the request failed.
+ ///
+ public string? Message { get; set; }
+ }
+}
diff --git a/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj b/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj
new file mode 100644
index 0000000..9e8c847
--- /dev/null
+++ b/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
new file mode 100644
index 0000000..32627e4
--- /dev/null
+++ b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
@@ -0,0 +1,240 @@
+using System.Threading.Tasks;
+using NetInteractor.Mcp;
+using NetInteractor.Test.TestWebApp;
+using Xunit;
+
+namespace NetInteractor.Mcp.Test
+{
+ public class NetInteractorToolsTests : IClassFixture
+ {
+ private readonly TestWebApplicationFactory _factory;
+ private readonly string _baseUrl;
+
+ public NetInteractorToolsTests(TestWebApplicationFixture fixture)
+ {
+ _factory = fixture.Factory;
+ _baseUrl = fixture.Factory.ServerUrl;
+ }
+
+ [Fact]
+ public async Task GetAsync_SimpleRequest_ReturnsHtml()
+ {
+ // Act
+ var result = await NetInteractorTools.GetAsync($"{_baseUrl}/");
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.Equal(200, result.StatusCode);
+ Assert.NotNull(result.Html);
+ Assert.Contains("Welcome to Test Shop", result.Html);
+ }
+
+ [Fact]
+ public async Task GetAsync_WithXPath_ExtractsTitle()
+ {
+ // Act
+ var result = await NetInteractorTools.GetAsync($"{_baseUrl}/", "//h1", "text()");
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.Equal(200, result.StatusCode);
+ Assert.Equal("Welcome to Test Shop", result.ExtractedValue);
+ Assert.Null(result.Html);
+ }
+
+ [Fact]
+ public async Task GetAsync_WithXPath_ExtractsAttribute()
+ {
+ // Act
+ var result = await NetInteractorTools.GetAsync($"{_baseUrl}/data", "//img", "src");
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.Equal(200, result.StatusCode);
+ Assert.Equal("/images/test.png", result.ExtractedValue);
+ }
+
+ [Fact]
+ public async Task GetAsync_InvalidUrl_ReturnsFailed()
+ {
+ // Act
+ var result = await NetInteractorTools.GetAsync("http://invalid-nonexistent-domain-123456789.com/");
+
+ // Assert
+ Assert.False(result.Success);
+ Assert.NotNull(result.Message);
+ }
+
+ [Fact]
+ public async Task PostAsync_SimplePost_ReturnsSuccess()
+ {
+ // Arrange
+ var formData = "billing_name=Test User,email=test@example.com,billing_address=123 Main St,billing_city=Seattle,billing_state=WA,billing_zip=98101,billing_country=USA,credit_card_number=4111111111111111,credit_card_month=12,credit_card_year=2027";
+
+ // Act
+ var result = await NetInteractorTools.PostAsync($"{_baseUrl}/checkout/submit", formData);
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.Equal(200, result.StatusCode);
+ Assert.NotNull(result.Html);
+ Assert.Contains("Test User", result.Html);
+ }
+
+ [Fact]
+ public async Task PostAsync_WithXPath_ExtractsValue()
+ {
+ // Arrange
+ var formData = "billing_name=Jane Doe,email=jane@example.com,billing_address=456 Oak St,billing_city=Portland,billing_state=OR,billing_zip=97201,billing_country=USA,credit_card_number=4111111111111111,credit_card_month=12,credit_card_year=2027";
+
+ // Act
+ var result = await NetInteractorTools.PostAsync($"{_baseUrl}/checkout/submit", formData, "//span[@class='customer-name']", "text()");
+
+ // Assert
+ Assert.True(result.Success);
+ Assert.Equal(200, result.StatusCode);
+ Assert.Equal("Jane Doe", result.ExtractedValue);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptAsync_SimpleGetScript_ExtractsTitle()
+ {
+ // Arrange
+ var script = @"
+
+
+
+
+
+ ";
+
+ // Act
+ var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+
+ // Assert
+ Assert.True(result.Success, result.Message);
+ Assert.NotNull(result.Outputs);
+ Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptAsync_WithSpecificTarget_ExecutesTarget()
+ {
+ // Arrange
+ var script = @"
+
+
+
+
+
+
+
+
+
+
+ ";
+
+ // Act
+ var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}", "Products");
+
+ // Assert
+ Assert.True(result.Success, result.Message);
+ Assert.NotNull(result.Outputs);
+ Assert.Equal("Products", result.Outputs["productTitle"]);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptAsync_InvalidScript_ReturnsFailed()
+ {
+ // Arrange
+ var invalidScript = @"";
+
+ // Act
+ var result = await NetInteractorTools.ExecuteScriptAsync(invalidScript);
+
+ // Assert
+ Assert.False(result.Success);
+ Assert.NotNull(result.Message);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptAsync_NoTargetSpecified_ReturnsError()
+ {
+ // Arrange - Script without defaultTarget and no target parameter
+ var script = @"
+
+
+
+ ";
+
+ // Act
+ var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+
+ // Assert
+ Assert.False(result.Success);
+ Assert.Contains("target", result.Message, System.StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptAsync_MultipleOutputs_ExtractsAllValues()
+ {
+ // Arrange
+ var script = @"
+
+
+
+
+
+
+ ";
+
+ // Act
+ var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+
+ // Assert
+ Assert.True(result.Success, result.Message);
+ Assert.NotNull(result.Outputs);
+ Assert.Equal("Data Extraction Test", result.Outputs["title"]);
+ Assert.Equal("/images/test.png", result.Outputs["imageSrc"]);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptAsync_WithNullInputs_ExecutesSuccessfully()
+ {
+ // Arrange
+ var script = @"
+
+
+
+
+
+ ";
+
+ // Act
+ var result = await NetInteractorTools.ExecuteScriptAsync(script, null);
+
+ // Assert
+ Assert.True(result.Success, result.Message);
+ Assert.NotNull(result.Outputs);
+ Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
+ }
+ }
+
+ ///
+ /// Shared test fixture that provides a single TestWebApplicationFactory instance for all tests.
+ ///
+ public class TestWebApplicationFixture : System.IDisposable
+ {
+ public TestWebApplicationFactory Factory { get; }
+
+ public TestWebApplicationFixture()
+ {
+ Factory = new TestWebApplicationFactory(ServerMode.Kestrel);
+ }
+
+ public void Dispose()
+ {
+ Factory?.Dispose();
+ }
+ }
+}
From 963e8035c1a7afcc8b542944d63de2677f5cd65b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 20:55:05 +0000
Subject: [PATCH 03/14] Fix nullable warning in NetInteractorTools.cs
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/NetInteractorTools.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index 2b88d92..f8c11e0 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -196,7 +196,7 @@ private static NameValueCollection ParseInputs(string? inputs)
return node.InnerText?.Trim();
}
- return node.GetAttributeValue(attribute, null);
+ return node.GetAttributeValue(attribute, string.Empty) is { Length: > 0 } value ? value : null;
}
private static System.Collections.Generic.Dictionary? ConvertOutputs(NameValueCollection outputs)
From be23b4fe2bc36a7a139ff2a07be4b304c1962782 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 21:11:56 +0000
Subject: [PATCH 04/14] Refactor NetInteractor.Mcp to class-based MCP tool,
remove Get/Post tools
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/GetRequestResult.cs | 38 ----
src/NetInteractor.Mcp/NetInteractorTools.cs | 205 +++++++++---------
src/NetInteractor.Mcp/PostRequestResult.cs | 38 ----
.../NetInteractorToolsTests.cs | 132 ++++-------
4 files changed, 141 insertions(+), 272 deletions(-)
delete mode 100644 src/NetInteractor.Mcp/GetRequestResult.cs
delete mode 100644 src/NetInteractor.Mcp/PostRequestResult.cs
diff --git a/src/NetInteractor.Mcp/GetRequestResult.cs b/src/NetInteractor.Mcp/GetRequestResult.cs
deleted file mode 100644
index c658c9e..0000000
--- a/src/NetInteractor.Mcp/GetRequestResult.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-namespace NetInteractor.Mcp
-{
- ///
- /// Result of an HTTP GET request.
- ///
- public class GetRequestResult
- {
- ///
- /// Indicates whether the request was successful (2xx status code).
- ///
- public bool Success { get; set; }
-
- ///
- /// HTTP status code of the response.
- ///
- public int StatusCode { get; set; }
-
- ///
- /// Final URL after any redirects.
- ///
- public string? Url { get; set; }
-
- ///
- /// Value extracted using XPath (if xpath parameter was provided).
- ///
- public string? ExtractedValue { get; set; }
-
- ///
- /// Raw HTML content (if no xpath was specified).
- ///
- public string? Html { get; set; }
-
- ///
- /// Error message if the request failed.
- ///
- public string? Message { get; set; }
- }
-}
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index f8c11e0..3c7d46d 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -1,20 +1,38 @@
using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading.Tasks;
using ModelContextProtocol.Server;
-using NetInteractor.Config;
using NetInteractor.WebAccessors;
namespace NetInteractor.Mcp
{
///
- /// MCP Server Tools for NetInteractor web automation.
- /// These tools enable AI agents to execute web interactions and automation scripts.
+ /// MCP Server Tool for NetInteractor web automation.
+ /// This tool enables AI agents to execute web interactions and automation scripts using InteractExecutor.
///
- [McpServerToolType]
- public static class NetInteractorTools
+ public class NetInteractorTool
{
+ private readonly IWebAccessor _webAccessor;
+
+ ///
+ /// Initializes a new instance of the NetInteractorTool class with the default HttpClientWebAccessor.
+ ///
+ public NetInteractorTool()
+ : this(new HttpClientWebAccessor())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the NetInteractorTool class with a custom web accessor.
+ ///
+ /// The web accessor to use for HTTP requests.
+ public NetInteractorTool(IWebAccessor webAccessor)
+ {
+ _webAccessor = webAccessor ?? throw new ArgumentNullException(nameof(webAccessor));
+ }
+
///
/// Executes a NetInteractor XML script for web automation.
///
@@ -31,15 +49,14 @@ public static class NetInteractorTools
/// The execution result containing outputs and status.
[McpServerTool(Name = "netinteractor_execute_script")]
[Description("Executes a NetInteractor XML script for web automation. Use this to run complex multi-step web workflows including GET requests, form submissions, data extraction, and conditional logic.")]
- public static async Task ExecuteScriptAsync(
+ public async Task ExecuteScriptAsync(
string script,
string? inputs = null,
string? target = null)
{
try
{
- var webAccessor = new HttpClientWebAccessor();
- var executor = new InterationExecutor(webAccessor);
+ var executor = new InterationExecutor(_webAccessor);
var inputValues = ParseInputs(inputs);
var result = await executor.ExecuteAsync(script, inputValues, target);
@@ -62,106 +79,67 @@ public static async Task ExecuteScriptAsync(
}
///
- /// Performs an HTTP GET request and extracts data using XPath.
+ /// Gets the input metadata for the ExecuteScript tool.
///
- /// The URL to fetch.
- /// Optional XPath expression to extract content from the HTML.
- /// Optional attribute to extract from the XPath result (e.g., 'href', 'src'). Use 'text()' for text content.
- /// The result containing the extracted content or raw HTML.
- [McpServerTool(Name = "netinteractor_get")]
- [Description("Performs an HTTP GET request and optionally extracts data using XPath. Use this for simple web scraping and data extraction from web pages.")]
- public static async Task GetAsync(
- string url,
- string? xpath = null,
- string? attribute = null)
+ /// A dictionary describing the input parameters.
+ public static Dictionary GetInputMetadata()
{
- try
+ return new Dictionary
{
- var webAccessor = new HttpClientWebAccessor();
- var response = await webAccessor.GetAsync(url);
-
- var result = new GetRequestResult
+ ["script"] = new ToolParameterMetadata
{
- Success = response.StatusCode >= 200 && response.StatusCode < 300,
- StatusCode = response.StatusCode,
- Url = response.Url
- };
-
- if (!string.IsNullOrEmpty(xpath))
+ Name = "script",
+ Description = "The XML script defining the web interaction workflow",
+ Type = "string",
+ Required = true
+ },
+ ["inputs"] = new ToolParameterMetadata
{
- var pageInfo = new PageInfo(response.Url ?? url, response.Html ?? string.Empty);
- var extractedValue = ExtractValue(pageInfo, xpath, attribute);
- result.ExtractedValue = extractedValue;
- }
- else
+ Name = "inputs",
+ Description = "Optional comma-separated key=value pairs for script inputs (e.g., 'BaseUrl=https://example.com,Username=user')",
+ Type = "string",
+ Required = false
+ },
+ ["target"] = new ToolParameterMetadata
{
- result.Html = response.Html;
+ Name = "target",
+ Description = "Optional target name to execute. If not specified, the default target will be used",
+ Type = "string",
+ Required = false
}
-
- return result;
- }
- catch (Exception ex)
- {
- return new GetRequestResult
- {
- Success = false,
- StatusCode = 0,
- Message = $"GET request failed: {ex.Message}"
- };
- }
+ };
}
///
- /// Performs an HTTP POST request with form data.
+ /// Gets the output metadata for the ExecuteScript tool.
///
- /// The URL to post to.
- /// Comma-separated key=value pairs for form data (e.g., "name=John,email=john@example.com").
- /// Optional XPath expression to extract content from the response HTML.
- /// Optional attribute to extract from the XPath result.
- /// The result containing the extracted content or raw HTML.
- [McpServerTool(Name = "netinteractor_post")]
- [Description("Performs an HTTP POST request with form data and optionally extracts data from the response using XPath. Use this for submitting forms and processing responses.")]
- public static async Task PostAsync(
- string url,
- string formData,
- string? xpath = null,
- string? attribute = null)
+ /// A dictionary describing the output properties.
+ public static Dictionary GetOutputMetadata()
{
- try
+ return new Dictionary
{
- var webAccessor = new HttpClientWebAccessor();
- var formValues = ParseInputs(formData);
- var response = await webAccessor.PostAsync(url, formValues);
-
- var result = new PostRequestResult
+ ["Success"] = new ToolParameterMetadata
{
- Success = response.StatusCode >= 200 && response.StatusCode < 300,
- StatusCode = response.StatusCode,
- Url = response.Url
- };
-
- if (!string.IsNullOrEmpty(xpath))
+ Name = "Success",
+ Description = "Indicates whether the script execution was successful",
+ Type = "boolean",
+ Required = true
+ },
+ ["Message"] = new ToolParameterMetadata
{
- var pageInfo = new PageInfo(response.Url ?? url, response.Html ?? string.Empty);
- var extractedValue = ExtractValue(pageInfo, xpath, attribute);
- result.ExtractedValue = extractedValue;
- }
- else
+ Name = "Message",
+ Description = "Message describing the result or error",
+ Type = "string",
+ Required = false
+ },
+ ["Outputs"] = new ToolParameterMetadata
{
- result.Html = response.Html;
+ Name = "Outputs",
+ Description = "Extracted output values from the script execution as key-value pairs",
+ Type = "object",
+ Required = false
}
-
- return result;
- }
- catch (Exception ex)
- {
- return new PostRequestResult
- {
- Success = false,
- StatusCode = 0,
- Message = $"POST request failed: {ex.Message}"
- };
- }
+ };
}
private static NameValueCollection ParseInputs(string? inputs)
@@ -184,27 +162,12 @@ private static NameValueCollection ParseInputs(string? inputs)
return result;
}
- private static string? ExtractValue(PageInfo pageInfo, string xpath, string? attribute)
- {
- var node = pageInfo.Document.DocumentNode.SelectSingleNode(xpath);
-
- if (node == null)
- return null;
-
- if (string.IsNullOrEmpty(attribute) || attribute.Equals("text()", StringComparison.OrdinalIgnoreCase))
- {
- return node.InnerText?.Trim();
- }
-
- return node.GetAttributeValue(attribute, string.Empty) is { Length: > 0 } value ? value : null;
- }
-
- private static System.Collections.Generic.Dictionary? ConvertOutputs(NameValueCollection outputs)
+ private static Dictionary? ConvertOutputs(NameValueCollection outputs)
{
if (outputs == null || outputs.Count == 0)
return null;
- var dict = new System.Collections.Generic.Dictionary();
+ var dict = new Dictionary();
foreach (string? key in outputs.AllKeys)
{
if (key != null)
@@ -215,4 +178,30 @@ private static NameValueCollection ParseInputs(string? inputs)
return dict;
}
}
+
+ ///
+ /// Metadata describing a tool parameter (input or output).
+ ///
+ public class ToolParameterMetadata
+ {
+ ///
+ /// The name of the parameter.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// A description of the parameter.
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// The type of the parameter (e.g., "string", "boolean", "object").
+ ///
+ public string Type { get; set; } = string.Empty;
+
+ ///
+ /// Indicates whether the parameter is required.
+ ///
+ public bool Required { get; set; }
+ }
}
diff --git a/src/NetInteractor.Mcp/PostRequestResult.cs b/src/NetInteractor.Mcp/PostRequestResult.cs
deleted file mode 100644
index 3a54af4..0000000
--- a/src/NetInteractor.Mcp/PostRequestResult.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-namespace NetInteractor.Mcp
-{
- ///
- /// Result of an HTTP POST request.
- ///
- public class PostRequestResult
- {
- ///
- /// Indicates whether the request was successful (2xx status code).
- ///
- public bool Success { get; set; }
-
- ///
- /// HTTP status code of the response.
- ///
- public int StatusCode { get; set; }
-
- ///
- /// Final URL after any redirects.
- ///
- public string? Url { get; set; }
-
- ///
- /// Value extracted using XPath (if xpath parameter was provided).
- ///
- public string? ExtractedValue { get; set; }
-
- ///
- /// Raw HTML content (if no xpath was specified).
- ///
- public string? Html { get; set; }
-
- ///
- /// Error message if the request failed.
- ///
- public string? Message { get; set; }
- }
-}
diff --git a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
index 32627e4..f08be5d 100644
--- a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
+++ b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
@@ -5,95 +5,17 @@
namespace NetInteractor.Mcp.Test
{
- public class NetInteractorToolsTests : IClassFixture
+ public class NetInteractorToolTests : IClassFixture
{
private readonly TestWebApplicationFactory _factory;
private readonly string _baseUrl;
+ private readonly NetInteractorTool _tool;
- public NetInteractorToolsTests(TestWebApplicationFixture fixture)
+ public NetInteractorToolTests(TestWebApplicationFixture fixture)
{
_factory = fixture.Factory;
_baseUrl = fixture.Factory.ServerUrl;
- }
-
- [Fact]
- public async Task GetAsync_SimpleRequest_ReturnsHtml()
- {
- // Act
- var result = await NetInteractorTools.GetAsync($"{_baseUrl}/");
-
- // Assert
- Assert.True(result.Success);
- Assert.Equal(200, result.StatusCode);
- Assert.NotNull(result.Html);
- Assert.Contains("Welcome to Test Shop", result.Html);
- }
-
- [Fact]
- public async Task GetAsync_WithXPath_ExtractsTitle()
- {
- // Act
- var result = await NetInteractorTools.GetAsync($"{_baseUrl}/", "//h1", "text()");
-
- // Assert
- Assert.True(result.Success);
- Assert.Equal(200, result.StatusCode);
- Assert.Equal("Welcome to Test Shop", result.ExtractedValue);
- Assert.Null(result.Html);
- }
-
- [Fact]
- public async Task GetAsync_WithXPath_ExtractsAttribute()
- {
- // Act
- var result = await NetInteractorTools.GetAsync($"{_baseUrl}/data", "//img", "src");
-
- // Assert
- Assert.True(result.Success);
- Assert.Equal(200, result.StatusCode);
- Assert.Equal("/images/test.png", result.ExtractedValue);
- }
-
- [Fact]
- public async Task GetAsync_InvalidUrl_ReturnsFailed()
- {
- // Act
- var result = await NetInteractorTools.GetAsync("http://invalid-nonexistent-domain-123456789.com/");
-
- // Assert
- Assert.False(result.Success);
- Assert.NotNull(result.Message);
- }
-
- [Fact]
- public async Task PostAsync_SimplePost_ReturnsSuccess()
- {
- // Arrange
- var formData = "billing_name=Test User,email=test@example.com,billing_address=123 Main St,billing_city=Seattle,billing_state=WA,billing_zip=98101,billing_country=USA,credit_card_number=4111111111111111,credit_card_month=12,credit_card_year=2027";
-
- // Act
- var result = await NetInteractorTools.PostAsync($"{_baseUrl}/checkout/submit", formData);
-
- // Assert
- Assert.True(result.Success);
- Assert.Equal(200, result.StatusCode);
- Assert.NotNull(result.Html);
- Assert.Contains("Test User", result.Html);
- }
-
- [Fact]
- public async Task PostAsync_WithXPath_ExtractsValue()
- {
- // Arrange
- var formData = "billing_name=Jane Doe,email=jane@example.com,billing_address=456 Oak St,billing_city=Portland,billing_state=OR,billing_zip=97201,billing_country=USA,credit_card_number=4111111111111111,credit_card_month=12,credit_card_year=2027";
-
- // Act
- var result = await NetInteractorTools.PostAsync($"{_baseUrl}/checkout/submit", formData, "//span[@class='customer-name']", "text()");
-
- // Assert
- Assert.True(result.Success);
- Assert.Equal(200, result.StatusCode);
- Assert.Equal("Jane Doe", result.ExtractedValue);
+ _tool = new NetInteractorTool();
}
[Fact]
@@ -109,7 +31,7 @@ public async Task ExecuteScriptAsync_SimpleGetScript_ExtractsTitle()
";
// Act
- var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
// Assert
Assert.True(result.Success, result.Message);
@@ -135,7 +57,7 @@ public async Task ExecuteScriptAsync_WithSpecificTarget_ExecutesTarget()
";
// Act
- var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}", "Products");
+ var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}", "Products");
// Assert
Assert.True(result.Success, result.Message);
@@ -150,7 +72,7 @@ public async Task ExecuteScriptAsync_InvalidScript_ReturnsFailed()
var invalidScript = @"";
// Act
- var result = await NetInteractorTools.ExecuteScriptAsync(invalidScript);
+ var result = await _tool.ExecuteScriptAsync(invalidScript);
// Assert
Assert.False(result.Success);
@@ -168,7 +90,7 @@ public async Task ExecuteScriptAsync_NoTargetSpecified_ReturnsError()
";
// Act
- var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
// Assert
Assert.False(result.Success);
@@ -189,7 +111,7 @@ public async Task ExecuteScriptAsync_MultipleOutputs_ExtractsAllValues()
";
// Act
- var result = await NetInteractorTools.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
// Assert
Assert.True(result.Success, result.Message);
@@ -211,13 +133,47 @@ public async Task ExecuteScriptAsync_WithNullInputs_ExecutesSuccessfully()
";
// Act
- var result = await NetInteractorTools.ExecuteScriptAsync(script, null);
+ var result = await _tool.ExecuteScriptAsync(script, null);
// Assert
Assert.True(result.Success, result.Message);
Assert.NotNull(result.Outputs);
Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
}
+
+ [Fact]
+ public void GetInputMetadata_ReturnsExpectedParameters()
+ {
+ // Act
+ var metadata = NetInteractorTool.GetInputMetadata();
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.True(metadata.ContainsKey("script"));
+ Assert.True(metadata.ContainsKey("inputs"));
+ Assert.True(metadata.ContainsKey("target"));
+
+ Assert.True(metadata["script"].Required);
+ Assert.False(metadata["inputs"].Required);
+ Assert.False(metadata["target"].Required);
+ }
+
+ [Fact]
+ public void GetOutputMetadata_ReturnsExpectedParameters()
+ {
+ // Act
+ var metadata = NetInteractorTool.GetOutputMetadata();
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.True(metadata.ContainsKey("Success"));
+ Assert.True(metadata.ContainsKey("Message"));
+ Assert.True(metadata.ContainsKey("Outputs"));
+
+ Assert.Equal("boolean", metadata["Success"].Type);
+ Assert.Equal("string", metadata["Message"].Type);
+ Assert.Equal("object", metadata["Outputs"].Type);
+ }
}
///
From 4cd079f018fd8c9e109aedf3ee79f7ae1f377615 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 21:32:27 +0000
Subject: [PATCH 05/14] Address PR feedback: inherit from McpServerTool, use
PlaywrightWebAccessor, use InteractionResult directly, add comprehensive
script documentation
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/ExecuteScriptResult.cs | 25 --
.../NetInteractor.Mcp.csproj | 1 +
src/NetInteractor.Mcp/NetInteractorTools.cs | 295 +++++++++++++++---
.../NetInteractorToolsTests.cs | 22 +-
4 files changed, 267 insertions(+), 76 deletions(-)
delete mode 100644 src/NetInteractor.Mcp/ExecuteScriptResult.cs
diff --git a/src/NetInteractor.Mcp/ExecuteScriptResult.cs b/src/NetInteractor.Mcp/ExecuteScriptResult.cs
deleted file mode 100644
index 37a7a06..0000000
--- a/src/NetInteractor.Mcp/ExecuteScriptResult.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Collections.Generic;
-
-namespace NetInteractor.Mcp
-{
- ///
- /// Result of executing a NetInteractor script.
- ///
- public class ExecuteScriptResult
- {
- ///
- /// Indicates whether the script execution was successful.
- ///
- public bool Success { get; set; }
-
- ///
- /// Message describing the result or error.
- ///
- public string? Message { get; set; }
-
- ///
- /// Extracted output values from the script execution.
- ///
- public Dictionary? Outputs { get; set; }
- }
-}
diff --git a/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj b/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
index 0bd4d01..c3346c0 100644
--- a/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
+++ b/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index 3c7d46d..a09847a 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
-using System.ComponentModel;
+using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
+using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using NetInteractor.WebAccessors;
@@ -11,16 +13,117 @@ namespace NetInteractor.Mcp
///
/// MCP Server Tool for NetInteractor web automation.
/// This tool enables AI agents to execute web interactions and automation scripts using InteractExecutor.
+ ///
+ /// NetInteractor scripts are XML-based configurations that define web automation workflows.
+ ///
+ /// ## Script Structure
+ ///
+ /// A script consists of:
+ /// - **InteractConfig**: The root element with an optional 'defaultTarget' attribute
+ /// - **target**: Named workflow targets containing action sequences
+ ///
+ /// ## Supported Actions
+ ///
+ /// ### 1. get - HTTP GET Request
+ /// Fetches a web page and optionally extracts data.
+ /// ```xml
+ /// <get url="https://example.com">
+ /// <output name="title" xpath="//h1" attr="text()" />
+ /// <output name="link" xpath="//a[@class='main']" attr="href" />
+ /// </get>
+ /// ```
+ /// Attributes:
+ /// - url: The URL to fetch (supports $(InputName) variable substitution)
+ /// - expectedHttpStatusCodes: Comma-separated list of expected HTTP status codes
+ ///
+ /// ### 2. post - HTTP POST Form Submission
+ /// Submits a form on the current page.
+ /// ```xml
+ /// <post formIndex="0">
+ /// <formValue name="username" value="$(Username)" />
+ /// <formValue name="password" value="$(Password)" />
+ /// <output name="result" xpath="//div[@class='message']" attr="text()" />
+ /// </post>
+ /// ```
+ /// Attributes (use one to identify the form):
+ /// - formIndex: Zero-based index of the form on the page
+ /// - formName: The name attribute of the form
+ /// - action: The action URL of the form
+ /// - clientID: The id attribute of the form
+ ///
+ /// ### 3. if - Conditional Execution
+ /// Executes a child action only if a condition is met.
+ /// ```xml
+ /// <if property="$(ShouldLogin)" value="true">
+ /// <call target="Login" />
+ /// </if>
+ /// ```
+ /// Attributes:
+ /// - property: The property/input to check
+ /// - value: The expected value
+ ///
+ /// ### 4. call - Call Another Target
+ /// Executes another named target.
+ /// ```xml
+ /// <call target="ExtractData" />
+ /// ```
+ /// Attributes:
+ /// - target: Name of the target to call
+ ///
+ /// ## Output Extraction
+ ///
+ /// The output element extracts data from HTML responses:
+ /// ```xml
+ /// <output name="outputName" xpath="//selector" attr="text()" />
+ /// <output name="price" xpath="//span[@class='price']" regex="\\$([\\d.]+)" />
+ /// ```
+ /// Attributes:
+ /// - name: Name of the output variable
+ /// - xpath: XPath expression to select element
+ /// - attr: Attribute to extract ('text()' for inner text, or attribute name like 'href')
+ /// - regex: Optional regex to extract from the selected content
+ /// - isMultipleValue: If true, extracts all matching values
+ /// - expectedValue: Validates the extracted value matches expected
+ ///
+ /// ## Variable Substitution
+ ///
+ /// Use $(VariableName) syntax to reference input values:
+ /// ```xml
+ /// <get url="$(BaseUrl)/products" />
+ /// ```
+ ///
+ /// ## Example Complete Script
+ ///
+ /// ```xml
+ /// <InteractConfig defaultTarget="Main">
+ /// <target name="Main">
+ /// <get url="$(BaseUrl)/">
+ /// <output name="title" xpath="//h1" attr="text()" />
+ /// </get>
+ /// <if property="$(ShouldLogin)" value="true">
+ /// <call target="Login" />
+ /// </if>
+ /// </target>
+ /// <target name="Login">
+ /// <get url="$(BaseUrl)/login" />
+ /// <post formIndex="0">
+ /// <formValue name="username" value="$(Username)" />
+ /// <formValue name="password" value="$(Password)" />
+ /// </post>
+ /// </target>
+ /// </InteractConfig>
+ /// ```
///
- public class NetInteractorTool
+ public class NetInteractorTool : McpServerTool
{
private readonly IWebAccessor _webAccessor;
+ private readonly Tool _protocolTool;
///
- /// Initializes a new instance of the NetInteractorTool class with the default HttpClientWebAccessor.
+ /// Initializes a new instance of the NetInteractorTool class with the default PlaywrightWebAccessor.
///
public NetInteractorTool()
- : this(new HttpClientWebAccessor())
+ : this(new PlaywrightWebAccessor())
{
}
@@ -31,25 +134,115 @@ public NetInteractorTool()
public NetInteractorTool(IWebAccessor webAccessor)
{
_webAccessor = webAccessor ?? throw new ArgumentNullException(nameof(webAccessor));
+
+ // Define the input schema as JSON
+ var inputSchemaJson = """
+ {
+ "type": "object",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "XML script defining the web automation workflow. Structure:\n\n \n \n \n\n\nActions:\n- \n- \n- \n- \n\nVariables: Use $(InputName) syntax for input substitution."
+ },
+ "inputs": {
+ "type": "string",
+ "description": "Comma-separated key=value pairs for script variable substitution. Example: 'BaseUrl=https://example.com,Username=admin,Password=secret'. These values replace $(Key) placeholders in the script."
+ },
+ "target": {
+ "type": "string",
+ "description": "Name of the target to execute. If omitted, uses the defaultTarget specified in InteractConfig."
+ }
+ },
+ "required": ["script"]
+ }
+ """;
+
+ // Define the protocol tool representation
+ _protocolTool = new Tool
+ {
+ Name = "netinteractor_execute_script",
+ Description = "Executes a NetInteractor XML script for web automation. Supports GET requests, form POST submissions, conditional logic (if), and calling other targets. Use XPath expressions to extract data from HTML. Variables use $(Name) syntax.",
+ InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement
+ };
+ }
+
+ ///
+ /// Gets the protocol tool representation.
+ ///
+ public override Tool ProtocolTool => _protocolTool;
+
+ ///
+ /// Gets the tool metadata.
+ ///
+ public override IReadOnlyList
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index a09847a..d435cf3 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -118,6 +118,7 @@ public class NetInteractorTool : McpServerTool
{
private readonly IWebAccessor _webAccessor;
private readonly Tool _protocolTool;
+ private readonly IReadOnlyList _outputMetadata;
///
/// Initializes a new instance of the NetInteractorTool class with the default PlaywrightWebAccessor.
@@ -164,6 +165,9 @@ public NetInteractorTool(IWebAccessor webAccessor)
Description = "Executes a NetInteractor XML script for web automation. Supports GET requests, form POST submissions, conditional logic (if), and calling other targets. Use XPath expressions to extract data from HTML. Variables use $(Name) syntax.",
InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement
};
+
+ // Build output metadata
+ _outputMetadata = new List(GetOutputMetadata().Values);
}
///
@@ -172,9 +176,9 @@ public NetInteractorTool(IWebAccessor webAccessor)
public override Tool ProtocolTool => _protocolTool;
///
- /// Gets the tool metadata.
+ /// Gets the tool metadata including output schema information.
///
- public override IReadOnlyList Metadata => Array.Empty();
+ public override IReadOnlyList Metadata => _outputMetadata;
///
/// Invokes the tool with the given parameters.
@@ -211,38 +215,38 @@ public override async ValueTask InvokeAsync(
};
}
- var result = await ExecuteScriptAsync(script, inputs, target);
-
- var outputText = result.Ok
- ? $"Success: {result.Message ?? "Script executed successfully."}\nOutputs: {FormatOutputs(result.Outputs)}"
- : $"Failed: {result.Message}";
+ var result = await ExecuteScriptInternalAsync(script, inputs, target);
- return new CallToolResult
+ if (result.Ok)
{
- Content = new List
+ // Return the outputs directly as JSON for the AI agent
+ var outputsJson = FormatOutputs(result.Outputs);
+ return new CallToolResult
{
- new TextContentBlock { Text = outputText }
- },
- IsError = !result.Ok
- };
+ Content = new List
+ {
+ new TextContentBlock { Text = outputsJson }
+ },
+ IsError = false
+ };
+ }
+ else
+ {
+ return new CallToolResult
+ {
+ Content = new List
+ {
+ new TextContentBlock { Text = $"Error: {result.Message}" }
+ },
+ IsError = true
+ };
+ }
}
///
- /// Executes a NetInteractor XML script for web automation.
- ///
- /// The script defines a workflow with actions like:
- /// - get: Fetch a URL and extract data using XPath
- /// - post: Submit forms with field values
- /// - if: Conditional execution based on input values
- /// - call: Call another named target
- ///
- /// See the class documentation for full script syntax and examples.
+ /// Internal method for testing - executes a script and returns the result.
///
- /// The XML script defining the web interaction workflow.
- /// Optional comma-separated key=value pairs for script inputs (e.g., "BaseUrl=https://example.com,Username=user").
- /// Optional target name to execute. If not specified, the default target will be used.
- /// The InteractionResult containing Ok status, Message, and extracted Outputs.
- public async Task ExecuteScriptAsync(
+ internal async Task ExecuteScriptInternalAsync(
string script,
string? inputs = null,
string? target = null)
@@ -264,56 +268,7 @@ public async Task ExecuteScriptAsync(
}
}
- ///
- /// Gets the input metadata for the ExecuteScript tool.
- ///
- /// A dictionary describing the input parameters.
- public static Dictionary GetInputMetadata()
- {
- return new Dictionary
- {
- ["script"] = new ToolParameterMetadata
- {
- Name = "script",
- Description = @"XML script defining the web automation workflow. Structure:
-
-
-
-
-
-
-Actions:
--
--
--
--
-
-Variables: Use $(InputName) syntax for input substitution.",
- Type = "string",
- Required = true
- },
- ["inputs"] = new ToolParameterMetadata
- {
- Name = "inputs",
- Description = "Comma-separated key=value pairs for script variable substitution. Example: 'BaseUrl=https://example.com,Username=admin,Password=secret'. These values replace $(Key) placeholders in the script.",
- Type = "string",
- Required = false
- },
- ["target"] = new ToolParameterMetadata
- {
- Name = "target",
- Description = "Name of the target to execute. If omitted, uses the defaultTarget specified in InteractConfig. Targets are named workflow steps that can call each other.",
- Type = "string",
- Required = false
- }
- };
- }
-
- ///
- /// Gets the output metadata for the ExecuteScript tool.
- ///
- /// A dictionary describing the output properties of InteractionResult.
- public static Dictionary GetOutputMetadata()
+ private static Dictionary GetOutputMetadata()
{
return new Dictionary
{
diff --git a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
index 9551e58..accaa4c 100644
--- a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
+++ b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
@@ -21,19 +21,19 @@ public NetInteractorToolTests(TestWebApplicationFixture fixture)
}
[Fact]
- public async Task ExecuteScriptAsync_SimpleGetScript_ExtractsTitle()
+ public async Task ExecuteScriptInternalAsync_SimpleGetScript_ExtractsTitle()
{
// Arrange
- var script = @"
+ var script = $@"
-
+
";
// Act
- var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptInternalAsync(script);
// Assert
Assert.True(result.Ok, result.Message);
@@ -42,7 +42,7 @@ public async Task ExecuteScriptAsync_SimpleGetScript_ExtractsTitle()
}
[Fact]
- public async Task ExecuteScriptAsync_WithSpecificTarget_ExecutesTarget()
+ public async Task ExecuteScriptInternalAsync_WithInputs_ExtractsTitle()
{
// Arrange
var script = @"
@@ -51,130 +51,101 @@ public async Task ExecuteScriptAsync_WithSpecificTarget_ExecutesTarget()
-
-
-
-
-
";
// Act
- var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}", "Products");
+ var result = await _tool.ExecuteScriptInternalAsync(script, $"BaseUrl={_baseUrl}");
// Assert
Assert.True(result.Ok, result.Message);
Assert.NotNull(result.Outputs);
- Assert.Equal("Products", result.Outputs["productTitle"]);
+ Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
}
[Fact]
- public async Task ExecuteScriptAsync_InvalidScript_ReturnsFailed()
+ public async Task ExecuteScriptInternalAsync_WithSpecificTarget_ExecutesTarget()
{
// Arrange
- var invalidScript = @"";
-
- // Act
- var result = await _tool.ExecuteScriptAsync(invalidScript);
-
- // Assert
- Assert.False(result.Ok);
- Assert.NotNull(result.Message);
- }
-
- [Fact]
- public async Task ExecuteScriptAsync_NoTargetSpecified_ReturnsError()
- {
- // Arrange - Script without defaultTarget and no target parameter
- var script = @"
+ var script = $@"
-
+
+
+
+
+
+
+
+
";
// Act
- var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptInternalAsync(script, null, "Products");
// Assert
- Assert.False(result.Ok);
- Assert.Contains("target", result.Message, System.StringComparison.OrdinalIgnoreCase);
+ Assert.True(result.Ok, result.Message);
+ Assert.NotNull(result.Outputs);
+ Assert.Equal("Products", result.Outputs["productTitle"]);
}
[Fact]
- public async Task ExecuteScriptAsync_MultipleOutputs_ExtractsAllValues()
+ public async Task ExecuteScriptInternalAsync_InvalidScript_ReturnsError()
{
// Arrange
- var script = @"
-
-
-
-
-
-
- ";
+ var invalidScript = @"";
// Act
- var result = await _tool.ExecuteScriptAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptInternalAsync(invalidScript);
// Assert
- Assert.True(result.Ok, result.Message);
- Assert.NotNull(result.Outputs);
- Assert.Equal("Data Extraction Test", result.Outputs["title"]);
- Assert.Equal("/images/test.png", result.Outputs["imageSrc"]);
+ Assert.False(result.Ok);
+ Assert.NotNull(result.Message);
}
[Fact]
- public async Task ExecuteScriptAsync_WithNullInputs_ExecutesSuccessfully()
+ public async Task ExecuteScriptInternalAsync_MultipleOutputs_ExtractsAllValues()
{
// Arrange
- var script = @"
+ var script = $@"
-
+
+
";
// Act
- var result = await _tool.ExecuteScriptAsync(script, null);
+ var result = await _tool.ExecuteScriptInternalAsync(script);
// Assert
Assert.True(result.Ok, result.Message);
Assert.NotNull(result.Outputs);
- Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
+ Assert.Equal("Data Extraction Test", result.Outputs["title"]);
+ Assert.Equal("/images/test.png", result.Outputs["imageSrc"]);
}
[Fact]
- public void GetInputMetadata_ReturnsExpectedParameters()
+ public void ProtocolTool_ReturnsValidTool()
{
// Act
- var metadata = NetInteractorTool.GetInputMetadata();
+ var protocolTool = _tool.ProtocolTool;
// Assert
- Assert.NotNull(metadata);
- Assert.True(metadata.ContainsKey("script"));
- Assert.True(metadata.ContainsKey("inputs"));
- Assert.True(metadata.ContainsKey("target"));
-
- Assert.True(metadata["script"].Required);
- Assert.False(metadata["inputs"].Required);
- Assert.False(metadata["target"].Required);
+ Assert.NotNull(protocolTool);
+ Assert.Equal("netinteractor_execute_script", protocolTool.Name);
+ Assert.NotNull(protocolTool.Description);
}
[Fact]
- public void GetOutputMetadata_ReturnsExpectedParameters()
+ public void Metadata_ReturnsOutputMetadata()
{
// Act
- var metadata = NetInteractorTool.GetOutputMetadata();
+ var metadata = _tool.Metadata;
// Assert
Assert.NotNull(metadata);
- Assert.True(metadata.ContainsKey("Ok"));
- Assert.True(metadata.ContainsKey("Message"));
- Assert.True(metadata.ContainsKey("Outputs"));
-
- Assert.Equal("boolean", metadata["Ok"].Type);
- Assert.Equal("string", metadata["Message"].Type);
- Assert.Equal("NameValueCollection", metadata["Outputs"].Type);
+ Assert.True(metadata.Count > 0);
}
}
From cc007f33e83748beddf4e26c93f6be306e20ddd6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 22:25:33 +0000
Subject: [PATCH 07/14] Address PR feedback: inputs as array, StructuredContent
for outputs, extract GetInputMetadata
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/NetInteractorTools.cs | 97 +++++++++++--------
.../NetInteractorToolsTests.cs | 2 +-
2 files changed, 58 insertions(+), 41 deletions(-)
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index d435cf3..4efa5ac 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
@@ -136,29 +137,8 @@ public NetInteractorTool(IWebAccessor webAccessor)
{
_webAccessor = webAccessor ?? throw new ArgumentNullException(nameof(webAccessor));
- // Define the input schema as JSON
- var inputSchemaJson = """
- {
- "type": "object",
- "properties": {
- "script": {
- "type": "string",
- "description": "XML script defining the web automation workflow. Structure:\n\n \n \n \n\n\nActions:\n- \n- \n- \n- \n\nVariables: Use $(InputName) syntax for input substitution."
- },
- "inputs": {
- "type": "string",
- "description": "Comma-separated key=value pairs for script variable substitution. Example: 'BaseUrl=https://example.com,Username=admin,Password=secret'. These values replace $(Key) placeholders in the script."
- },
- "target": {
- "type": "string",
- "description": "Name of the target to execute. If omitted, uses the defaultTarget specified in InteractConfig."
- }
- },
- "required": ["script"]
- }
- """;
-
- // Define the protocol tool representation
+ // Define the protocol tool representation using GetInputMetadata
+ var inputSchemaJson = GetInputMetadata();
_protocolTool = new Tool
{
Name = "netinteractor_execute_script",
@@ -190,15 +170,26 @@ public override async ValueTask InvokeAsync(
var arguments = request.Params?.Arguments;
string? script = null;
- string? inputs = null;
+ string[]? inputs = null;
string? target = null;
if (arguments != null)
{
if (arguments.TryGetValue("script", out var scriptValue) && scriptValue.ValueKind != JsonValueKind.Undefined && scriptValue.ValueKind != JsonValueKind.Null)
script = scriptValue.GetString();
- if (arguments.TryGetValue("inputs", out var inputsValue) && inputsValue.ValueKind != JsonValueKind.Undefined && inputsValue.ValueKind != JsonValueKind.Null)
- inputs = inputsValue.GetString();
+ if (arguments.TryGetValue("inputs", out var inputsValue) && inputsValue.ValueKind == JsonValueKind.Array)
+ {
+ var inputList = new List();
+ foreach (var item in inputsValue.EnumerateArray())
+ {
+ if (item.ValueKind == JsonValueKind.String)
+ {
+ var val = item.GetString();
+ if (val != null) inputList.Add(val);
+ }
+ }
+ inputs = inputList.ToArray();
+ }
if (arguments.TryGetValue("target", out var targetValue) && targetValue.ValueKind != JsonValueKind.Undefined && targetValue.ValueKind != JsonValueKind.Null)
target = targetValue.GetString();
}
@@ -219,14 +210,11 @@ public override async ValueTask InvokeAsync(
if (result.Ok)
{
- // Return the outputs directly as JSON for the AI agent
- var outputsJson = FormatOutputs(result.Outputs);
+ // Return the outputs as structured content for the AI agent
+ var outputsObject = ConvertOutputsToJsonNode(result.Outputs);
return new CallToolResult
{
- Content = new List
- {
- new TextContentBlock { Text = outputsJson }
- },
+ StructuredContent = outputsObject,
IsError = false
};
}
@@ -248,7 +236,7 @@ public override async ValueTask InvokeAsync(
///
internal async Task ExecuteScriptInternalAsync(
string script,
- string? inputs = null,
+ string[]? inputs = null,
string? target = null)
{
try
@@ -268,6 +256,36 @@ internal async Task ExecuteScriptInternalAsync(
}
}
+ ///
+ /// Gets the input metadata schema as JSON string.
+ ///
+ private static string GetInputMetadata()
+ {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "XML script defining the web automation workflow.\n\nStructure:\n\n \n \n \n\n\nSupported Actions:\n\n1. GET Request - Fetches a web page and extracts data:\n \n \n \n \n\n2. POST Form Submission - Submits form data:\n \n \n \n \n \n\n3. Conditional Execution:\n \n \n \n\n4. Call Another Target:\n \n\nOutput Extraction Attributes:\n- name: Output variable name\n- xpath: XPath selector\n- attr: Attribute to extract ('text()' for inner text, or attribute name like 'href')\n- regex: Optional regex pattern\n- isMultipleValue: Extract all matching values\n- expectedValue: Validate extracted value\n\nVariables: Use $(InputName) syntax for substitution."
+ },
+ "inputs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Array of key=value pairs for script variable substitution. Example: ['BaseUrl=https://example.com', 'Username=admin', 'Password=secret']. These values replace $(Key) placeholders in the script."
+ },
+ "target": {
+ "type": "string",
+ "description": "Name of the target to execute. If omitted, uses the defaultTarget specified in InteractConfig."
+ }
+ },
+ "required": ["script"]
+ }
+ """;
+ }
+
private static Dictionary GetOutputMetadata()
{
return new Dictionary
@@ -310,15 +328,14 @@ private static Dictionary GetOutputMetadata()
};
}
- private static NameValueCollection ParseInputs(string? inputs)
+ private static NameValueCollection ParseInputs(string[]? inputs)
{
var result = new NameValueCollection();
- if (string.IsNullOrEmpty(inputs))
+ if (inputs == null || inputs.Length == 0)
return result;
- var pairs = inputs.Split(',');
- foreach (var pair in pairs)
+ foreach (var pair in inputs)
{
var keyValue = pair.Split(['='], 2);
if (keyValue.Length == 2)
@@ -330,10 +347,10 @@ private static NameValueCollection ParseInputs(string? inputs)
return result;
}
- private static string FormatOutputs(NameValueCollection? outputs)
+ private static JsonNode? ConvertOutputsToJsonNode(NameValueCollection? outputs)
{
if (outputs == null || outputs.Count == 0)
- return "{}";
+ return JsonNode.Parse("{}");
var dict = new Dictionary();
foreach (string? key in outputs.AllKeys)
@@ -343,7 +360,7 @@ private static string FormatOutputs(NameValueCollection? outputs)
dict[key] = outputs[key] ?? string.Empty;
}
}
- return JsonSerializer.Serialize(dict);
+ return JsonNode.Parse(JsonSerializer.Serialize(dict));
}
}
diff --git a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
index accaa4c..1ff0282 100644
--- a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
+++ b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
@@ -54,7 +54,7 @@ public async Task ExecuteScriptInternalAsync_WithInputs_ExtractsTitle()
";
// Act
- var result = await _tool.ExecuteScriptInternalAsync(script, $"BaseUrl={_baseUrl}");
+ var result = await _tool.ExecuteScriptInternalAsync(script, new[] { $"BaseUrl={_baseUrl}" });
// Assert
Assert.True(result.Ok, result.Message);
From 211ad98a964c292d7dac25a9deaa0781964357f3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 22:28:13 +0000
Subject: [PATCH 08/14] Optimize ConvertOutputsToJsonNode to use JsonObject
directly
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/NetInteractorTools.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index 4efa5ac..3a7c1f6 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -350,17 +350,17 @@ private static NameValueCollection ParseInputs(string[]? inputs)
private static JsonNode? ConvertOutputsToJsonNode(NameValueCollection? outputs)
{
if (outputs == null || outputs.Count == 0)
- return JsonNode.Parse("{}");
+ return new JsonObject();
- var dict = new Dictionary();
+ var jsonObject = new JsonObject();
foreach (string? key in outputs.AllKeys)
{
if (key != null)
{
- dict[key] = outputs[key] ?? string.Empty;
+ jsonObject[key] = outputs[key] ?? string.Empty;
}
}
- return JsonNode.Parse(JsonSerializer.Serialize(dict));
+ return jsonObject;
}
}
From 1029f29f0b5423d4385c9ff19648c55013a4dca6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 23:18:30 +0000
Subject: [PATCH 09/14] Address PR feedback: inputs as JsonElement object,
return JsonElement from metadata methods, set OutputSchema, return empty
Metadata, add detailed target documentation
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/NetInteractorTools.cs | 157 +++++++-----------
.../NetInteractorToolsTests.cs | 12 +-
2 files changed, 64 insertions(+), 105 deletions(-)
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index 3a7c1f6..0295ae7 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -119,7 +119,6 @@ public class NetInteractorTool : McpServerTool
{
private readonly IWebAccessor _webAccessor;
private readonly Tool _protocolTool;
- private readonly IReadOnlyList _outputMetadata;
///
/// Initializes a new instance of the NetInteractorTool class with the default PlaywrightWebAccessor.
@@ -137,17 +136,14 @@ public NetInteractorTool(IWebAccessor webAccessor)
{
_webAccessor = webAccessor ?? throw new ArgumentNullException(nameof(webAccessor));
- // Define the protocol tool representation using GetInputMetadata
- var inputSchemaJson = GetInputMetadata();
+ // Define the protocol tool representation using GetInputMetadata and GetOutputMetadata
_protocolTool = new Tool
{
Name = "netinteractor_execute_script",
Description = "Executes a NetInteractor XML script for web automation. Supports GET requests, form POST submissions, conditional logic (if), and calling other targets. Use XPath expressions to extract data from HTML. Variables use $(Name) syntax.",
- InputSchema = JsonDocument.Parse(inputSchemaJson).RootElement
+ InputSchema = GetInputMetadata(),
+ OutputSchema = GetOutputMetadata()
};
-
- // Build output metadata
- _outputMetadata = new List(GetOutputMetadata().Values);
}
///
@@ -156,9 +152,9 @@ public NetInteractorTool(IWebAccessor webAccessor)
public override Tool ProtocolTool => _protocolTool;
///
- /// Gets the tool metadata including output schema information.
+ /// Gets the tool metadata - returns empty list as output metadata is returned via OutputSchema.
///
- public override IReadOnlyList Metadata => _outputMetadata;
+ public override IReadOnlyList Metadata => Array.Empty();
///
/// Invokes the tool with the given parameters.
@@ -170,25 +166,16 @@ public override async ValueTask InvokeAsync(
var arguments = request.Params?.Arguments;
string? script = null;
- string[]? inputs = null;
+ NameValueCollection? inputs = null;
string? target = null;
if (arguments != null)
{
if (arguments.TryGetValue("script", out var scriptValue) && scriptValue.ValueKind != JsonValueKind.Undefined && scriptValue.ValueKind != JsonValueKind.Null)
script = scriptValue.GetString();
- if (arguments.TryGetValue("inputs", out var inputsValue) && inputsValue.ValueKind == JsonValueKind.Array)
+ if (arguments.TryGetValue("inputs", out var inputsValue) && inputsValue.ValueKind == JsonValueKind.Object)
{
- var inputList = new List();
- foreach (var item in inputsValue.EnumerateArray())
- {
- if (item.ValueKind == JsonValueKind.String)
- {
- var val = item.GetString();
- if (val != null) inputList.Add(val);
- }
- }
- inputs = inputList.ToArray();
+ inputs = ParseInputsFromJsonElement(inputsValue);
}
if (arguments.TryGetValue("target", out var targetValue) && targetValue.ValueKind != JsonValueKind.Undefined && targetValue.ValueKind != JsonValueKind.Null)
target = targetValue.GetString();
@@ -236,13 +223,13 @@ public override async ValueTask InvokeAsync(
///
internal async Task ExecuteScriptInternalAsync(
string script,
- string[]? inputs = null,
+ NameValueCollection? inputs = null,
string? target = null)
{
try
{
var executor = new InterationExecutor(_webAccessor);
- var inputValues = ParseInputs(inputs);
+ var inputValues = inputs ?? new NameValueCollection();
return await executor.ExecuteAsync(script, inputValues, target);
}
catch (Exception ex)
@@ -257,24 +244,24 @@ internal async Task ExecuteScriptInternalAsync(
}
///
- /// Gets the input metadata schema as JSON string.
+ /// Gets the input metadata schema as JsonElement.
///
- private static string GetInputMetadata()
+ private static JsonElement GetInputMetadata()
{
- return """
+ var schemaJson = """
{
"type": "object",
"properties": {
"script": {
"type": "string",
- "description": "XML script defining the web automation workflow.\n\nStructure:\n\n \n \n \n\n\nSupported Actions:\n\n1. GET Request - Fetches a web page and extracts data:\n \n \n \n \n\n2. POST Form Submission - Submits form data:\n \n \n \n \n \n\n3. Conditional Execution:\n \n \n \n\n4. Call Another Target:\n \n\nOutput Extraction Attributes:\n- name: Output variable name\n- xpath: XPath selector\n- attr: Attribute to extract ('text()' for inner text, or attribute name like 'href')\n- regex: Optional regex pattern\n- isMultipleValue: Extract all matching values\n- expectedValue: Validate extracted value\n\nVariables: Use $(InputName) syntax for substitution."
+ "description": "XML script defining the web automation workflow.\n\n## Script Structure\n\n\n \n \n \n \n \n \n\n\n## Target Element\n\nTargets are named workflow steps. The 'defaultTarget' attribute specifies which target runs first.\nTargets can call other targets using the action for modular workflows.\n\n## Supported Action Types\n\n### 1. GET Request ()\nFetches a web page via HTTP GET and optionally extracts data.\n\nAttributes:\n- url (required): URL to fetch. Supports $(Variable) substitution.\n- expectedHttpStatusCodes: Comma-separated valid status codes (default: 200).\n\nChild Elements:\n-
private static JsonElement GetInputMetadata()
{
- var schemaJson = """
+ var schema = new JsonObject
{
- "type": "object",
- "properties": {
- "script": {
- "type": "string",
- "description": "XML script defining the web automation workflow.\n\n## Script Structure\n\n\n \n \n \n \n \n \n\n\n## Target Element\n\nTargets are named workflow steps. The 'defaultTarget' attribute specifies which target runs first.\nTargets can call other targets using the action for modular workflows.\n\n## Supported Action Types\n\n### 1. GET Request ()\nFetches a web page via HTTP GET and optionally extracts data.\n\nAttributes:\n- url (required): URL to fetch. Supports $(Variable) substitution.\n- expectedHttpStatusCodes: Comma-separated valid status codes (default: 200).\n\nChild Elements:\n- : Extract data from the response (see Output Extraction).\n\nExample:\n\n \n \n\n\n### 2. POST Form Submission ()\nSubmits an HTML form. Must be preceded by a GET to load the page with the form.\n\nAttributes (use ONE to identify the form):\n- formIndex: Zero-based index of form on page (e.g., formIndex='0' for first form).\n- formName: The 'name' attribute of the form element.\n- action: The 'action' attribute/URL of the form.\n- clientID: The 'id' attribute of the form element.\n\nChild Elements:\n- : Set form field values.\n - name (required): Form field name.\n - value (required): Value to set. Supports $(Variable) substitution.\n- : Extract data from the response after submission.\n\nExample:\n\n \n \n \n\n\n### 3. Conditional Execution ()\nExecutes child action only when a condition is met.\n\nAttributes:\n- property (required): Input variable to check. Use $(VariableName) syntax.\n- value (required): Expected value to match.\n\nChild Elements: Any single action element (get, post, call, if).\n\nExample:\n\n \n\n\n### 4. Call Another Target ()\nExecutes another named target, enabling modular workflows.\n\nAttributes:\n- target (required): Name of the target to execute.\n\nExample:\n\n\n## Output Extraction ()\n\nExtracts data from HTML responses using XPath.\n\nAttributes:\n- name (required): Variable name for extracted value.\n- xpath (required): XPath expression to select element(s).\n- attr (required): What to extract:\n - 'text()': Inner text content.\n - Any attribute name: e.g., 'href', 'src', 'class'.\n- regex: Optional regex to further extract from the selected content.\n- isMultipleValue: Set 'true' to extract all matching elements as comma-separated values.\n- expectedValue: Validation - fails if extracted value doesn't match.\n\nExamples:\n\n\n\n\n\n## Variable Substitution\n\nUse $(VariableName) syntax anywhere in attribute values. Variables are provided via the 'inputs' parameter as key-value pairs.\n\nExample:\n"
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["script"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["description"] = BuildScriptDescription()
},
- "inputs": {
- "type": "object",
- "additionalProperties": {
- "type": "string"
+ ["inputs"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["additionalProperties"] = new JsonObject
+ {
+ ["type"] = "string"
},
- "description": "Object with key-value string pairs for script variable substitution. Example: {\"BaseUrl\": \"https://example.com\", \"Username\": \"admin\", \"Password\": \"secret\"}. These values replace $(Key) placeholders in the script."
+ ["description"] = "Object with key-value string pairs for script variable substitution. Example: {\"BaseUrl\": \"https://example.com\", \"Username\": \"admin\", \"Password\": \"secret\"}. These values replace $(Key) placeholders in the script."
},
- "target": {
- "type": "string",
- "description": "Name of the target to execute. If omitted, uses the defaultTarget specified in InteractConfig."
+ ["target"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["description"] = "Name of the target to execute. If omitted, uses the defaultTarget specified in InteractConfig."
}
},
- "required": ["script"]
- }
- """;
- return JsonDocument.Parse(schemaJson).RootElement;
+ ["required"] = new JsonArray { "script" }
+ };
+
+ return JsonDocument.Parse(schema.ToJsonString()).RootElement;
}
///
@@ -279,34 +285,158 @@ private static JsonElement GetInputMetadata()
///
private static JsonElement GetOutputMetadata()
{
- var schemaJson = """
+ var schema = new JsonObject
{
- "type": "object",
- "properties": {
- "Ok": {
- "type": "boolean",
- "description": "True if the script execution completed successfully, false if any action failed"
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["Ok"] = new JsonObject
+ {
+ ["type"] = "boolean",
+ ["description"] = "True if the script execution completed successfully, false if any action failed"
},
- "Message": {
- "type": "string",
- "description": "Descriptive message about the result, especially useful for errors"
+ ["Message"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["description"] = "Descriptive message about the result, especially useful for errors"
},
- "Outputs": {
- "type": "object",
- "additionalProperties": {
- "type": "string"
+ ["Outputs"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["additionalProperties"] = new JsonObject
+ {
+ ["type"] = "string"
},
- "description": "Object containing all extracted output values defined by elements in the script. Keys are output names, values are extracted strings."
+ ["description"] = "Object containing all extracted output values defined by elements in the script. Keys are output names, values are extracted strings."
},
- "Target": {
- "type": "string",
- "description": "The name of the next target to execute (used for workflow chaining)"
+ ["Target"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["description"] = "The name of the next target to execute (used for workflow chaining)"
}
},
- "required": ["Ok"]
- }
- """;
- return JsonDocument.Parse(schemaJson).RootElement;
+ ["required"] = new JsonArray { "Ok" }
+ };
+
+ return JsonDocument.Parse(schema.ToJsonString()).RootElement;
+ }
+
+ ///
+ /// Builds the comprehensive script description for AI agents.
+ ///
+ private static string BuildScriptDescription()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("XML script defining the web automation workflow.");
+ sb.AppendLine();
+ sb.AppendLine("## Script Structure");
+ sb.AppendLine();
+ sb.AppendLine("");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine("");
+ sb.AppendLine();
+ sb.AppendLine("## Target Element");
+ sb.AppendLine();
+ sb.AppendLine("Targets are named workflow steps. The 'defaultTarget' attribute specifies which target runs first.");
+ sb.AppendLine("Targets can call other targets using the action for modular workflows.");
+ sb.AppendLine();
+ sb.AppendLine("## Supported Action Types");
+ sb.AppendLine();
+
+ // GET action
+ sb.AppendLine("### 1. GET Request ()");
+ sb.AppendLine("Fetches a web page via HTTP GET and optionally extracts data.");
+ sb.AppendLine();
+ sb.AppendLine("Attributes:");
+ sb.AppendLine("- url (required): URL to fetch. Supports $(Variable) substitution.");
+ sb.AppendLine("- expectedHttpStatusCodes: Comma-separated valid status codes (default: 200).");
+ sb.AppendLine();
+ sb.AppendLine("Child Elements:");
+ sb.AppendLine("- : Extract data from the response (see Output Extraction).");
+ sb.AppendLine();
+ sb.AppendLine("Example:");
+ sb.AppendLine("");
+ sb.AppendLine(" ");
+ sb.AppendLine("");
+ sb.AppendLine();
+
+ // POST action
+ sb.AppendLine("### 2. POST Form Submission ()");
+ sb.AppendLine("Submits an HTML form. Must be preceded by a GET to load the page with the form.");
+ sb.AppendLine();
+ sb.AppendLine("Attributes (use ONE to identify the form):");
+ sb.AppendLine("- formIndex: Zero-based index of form on page (e.g., formIndex='0').");
+ sb.AppendLine("- formName: The 'name' attribute of the form element.");
+ sb.AppendLine("- action: The 'action' attribute/URL of the form.");
+ sb.AppendLine("- clientID: The 'id' attribute of the form element.");
+ sb.AppendLine();
+ sb.AppendLine("Child Elements:");
+ sb.AppendLine("- : Set form field values.");
+ sb.AppendLine("- : Extract data from the response after submission.");
+ sb.AppendLine();
+ sb.AppendLine("Example:");
+ sb.AppendLine("");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.AppendLine("");
+ sb.AppendLine();
+
+ // IF action
+ sb.AppendLine("### 3. Conditional Execution ()");
+ sb.AppendLine("Executes child action only when a condition is met.");
+ sb.AppendLine();
+ sb.AppendLine("Attributes:");
+ sb.AppendLine("- property (required): Input variable to check. Use $(VariableName) syntax.");
+ sb.AppendLine("- value (required): Expected value to match.");
+ sb.AppendLine();
+ sb.AppendLine("Example:");
+ sb.AppendLine("");
+ sb.AppendLine(" ");
+ sb.AppendLine("");
+ sb.AppendLine();
+
+ // CALL action
+ sb.AppendLine("### 4. Call Another Target ()");
+ sb.AppendLine("Executes another named target, enabling modular workflows.");
+ sb.AppendLine();
+ sb.AppendLine("Attributes:");
+ sb.AppendLine("- target (required): Name of the target to execute.");
+ sb.AppendLine();
+ sb.AppendLine("Example:");
+ sb.AppendLine("");
+ sb.AppendLine();
+
+ // Output extraction
+ sb.AppendLine("## Output Extraction ()");
+ sb.AppendLine();
+ sb.AppendLine("Extracts data from HTML responses using XPath.");
+ sb.AppendLine();
+ sb.AppendLine("Attributes:");
+ sb.AppendLine("- name (required): Variable name for extracted value.");
+ sb.AppendLine("- xpath (required): XPath expression to select element(s).");
+ sb.AppendLine("- attr (required): What to extract ('text()' for inner text, or attribute name like 'href').");
+ sb.AppendLine("- regex: Optional regex to further extract from the selected content.");
+ sb.AppendLine("- isMultipleValue: Set 'true' to extract all matching elements as comma-separated values.");
+ sb.AppendLine("- expectedValue: Validation - fails if extracted value doesn't match.");
+ sb.AppendLine();
+ sb.AppendLine("Example:");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine();
+
+ // Variable substitution
+ sb.AppendLine("## Variable Substitution");
+ sb.AppendLine();
+ sb.AppendLine("Use $(VariableName) syntax anywhere in attribute values.");
+ sb.AppendLine("Variables are provided via the 'inputs' parameter as key-value pairs.");
+ sb.AppendLine();
+ sb.AppendLine("Example:");
+ sb.AppendLine("");
+
+ return sb.ToString();
}
private static NameValueCollection ParseInputsFromJsonElement(JsonElement inputsElement)
From c408d0b6449422250bb261ceff8bc02576d76050 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 00:03:04 +0000
Subject: [PATCH 11/14] Optimize JsonObject to JsonElement conversion using
SerializeToElement
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/NetInteractorTools.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index c10a5cd..ee7bdf3 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -277,7 +277,7 @@ private static JsonElement GetInputMetadata()
["required"] = new JsonArray { "script" }
};
- return JsonDocument.Parse(schema.ToJsonString()).RootElement;
+ return JsonSerializer.SerializeToElement(schema);
}
///
@@ -318,7 +318,7 @@ private static JsonElement GetOutputMetadata()
["required"] = new JsonArray { "Ok" }
};
- return JsonDocument.Parse(schema.ToJsonString()).RootElement;
+ return JsonSerializer.SerializeToElement(schema);
}
///
@@ -327,7 +327,6 @@ private static JsonElement GetOutputMetadata()
private static string BuildScriptDescription()
{
var sb = new StringBuilder();
-
sb.AppendLine("XML script defining the web automation workflow.");
sb.AppendLine();
sb.AppendLine("## Script Structure");
From 756f88f4c9ae8de6356f72d302c753c468c47b3b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 01:30:51 +0000
Subject: [PATCH 12/14] Move script description to external file
ScriptDescription.txt for easier maintenance
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
.../NetInteractor.Mcp.csproj | 3 +
src/NetInteractor.Mcp/NetInteractorTools.cs | 126 ++----------------
src/NetInteractor.Mcp/ScriptDescription.txt | 95 +++++++++++++
3 files changed, 112 insertions(+), 112 deletions(-)
create mode 100644 src/NetInteractor.Mcp/ScriptDescription.txt
diff --git a/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj b/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
index 86619e7..24596c5 100644
--- a/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
+++ b/src/NetInteractor.Mcp/NetInteractor.Mcp.csproj
@@ -18,6 +18,9 @@
+
+
+
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index ee7bdf3..3fc45b0 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
-using System.Text;
+using System.IO;
+using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
@@ -257,7 +258,7 @@ private static JsonElement GetInputMetadata()
["script"] = new JsonObject
{
["type"] = "string",
- ["description"] = BuildScriptDescription()
+ ["description"] = LoadScriptDescription()
},
["inputs"] = new JsonObject
{
@@ -322,120 +323,21 @@ private static JsonElement GetOutputMetadata()
}
///
- /// Builds the comprehensive script description for AI agents.
+ /// Loads the script description from the embedded resource file.
///
- private static string BuildScriptDescription()
+ private static string LoadScriptDescription()
{
- var sb = new StringBuilder();
- sb.AppendLine("XML script defining the web automation workflow.");
- sb.AppendLine();
- sb.AppendLine("## Script Structure");
- sb.AppendLine();
- sb.AppendLine("");
- sb.AppendLine(" ");
- sb.AppendLine(" ");
- sb.AppendLine(" ");
- sb.AppendLine("");
- sb.AppendLine();
- sb.AppendLine("## Target Element");
- sb.AppendLine();
- sb.AppendLine("Targets are named workflow steps. The 'defaultTarget' attribute specifies which target runs first.");
- sb.AppendLine("Targets can call other targets using the action for modular workflows.");
- sb.AppendLine();
- sb.AppendLine("## Supported Action Types");
- sb.AppendLine();
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceName = "NetInteractor.Mcp.ScriptDescription.txt";
- // GET action
- sb.AppendLine("### 1. GET Request ()");
- sb.AppendLine("Fetches a web page via HTTP GET and optionally extracts data.");
- sb.AppendLine();
- sb.AppendLine("Attributes:");
- sb.AppendLine("- url (required): URL to fetch. Supports $(Variable) substitution.");
- sb.AppendLine("- expectedHttpStatusCodes: Comma-separated valid status codes (default: 200).");
- sb.AppendLine();
- sb.AppendLine("Child Elements:");
- sb.AppendLine("- : Extract data from the response (see Output Extraction).");
- sb.AppendLine();
- sb.AppendLine("Example:");
- sb.AppendLine("");
- sb.AppendLine(" ");
- sb.AppendLine("");
- sb.AppendLine();
-
- // POST action
- sb.AppendLine("### 2. POST Form Submission ()");
- sb.AppendLine("Submits an HTML form. Must be preceded by a GET to load the page with the form.");
- sb.AppendLine();
- sb.AppendLine("Attributes (use ONE to identify the form):");
- sb.AppendLine("- formIndex: Zero-based index of form on page (e.g., formIndex='0').");
- sb.AppendLine("- formName: The 'name' attribute of the form element.");
- sb.AppendLine("- action: The 'action' attribute/URL of the form.");
- sb.AppendLine("- clientID: The 'id' attribute of the form element.");
- sb.AppendLine();
- sb.AppendLine("Child Elements:");
- sb.AppendLine("- : Set form field values.");
- sb.AppendLine("- : Extract data from the response after submission.");
- sb.AppendLine();
- sb.AppendLine("Example:");
- sb.AppendLine("");
- sb.AppendLine(" ");
- sb.AppendLine(" ");
- sb.AppendLine("");
- sb.AppendLine();
-
- // IF action
- sb.AppendLine("### 3. Conditional Execution ()");
- sb.AppendLine("Executes child action only when a condition is met.");
- sb.AppendLine();
- sb.AppendLine("Attributes:");
- sb.AppendLine("- property (required): Input variable to check. Use $(VariableName) syntax.");
- sb.AppendLine("- value (required): Expected value to match.");
- sb.AppendLine();
- sb.AppendLine("Example:");
- sb.AppendLine("");
- sb.AppendLine(" ");
- sb.AppendLine("");
- sb.AppendLine();
-
- // CALL action
- sb.AppendLine("### 4. Call Another Target ()");
- sb.AppendLine("Executes another named target, enabling modular workflows.");
- sb.AppendLine();
- sb.AppendLine("Attributes:");
- sb.AppendLine("- target (required): Name of the target to execute.");
- sb.AppendLine();
- sb.AppendLine("Example:");
- sb.AppendLine("");
- sb.AppendLine();
-
- // Output extraction
- sb.AppendLine("## Output Extraction ()");
- sb.AppendLine();
- sb.AppendLine("Extracts data from HTML responses using XPath.");
- sb.AppendLine();
- sb.AppendLine("Attributes:");
- sb.AppendLine("- name (required): Variable name for extracted value.");
- sb.AppendLine("- xpath (required): XPath expression to select element(s).");
- sb.AppendLine("- attr (required): What to extract ('text()' for inner text, or attribute name like 'href').");
- sb.AppendLine("- regex: Optional regex to further extract from the selected content.");
- sb.AppendLine("- isMultipleValue: Set 'true' to extract all matching elements as comma-separated values.");
- sb.AppendLine("- expectedValue: Validation - fails if extracted value doesn't match.");
- sb.AppendLine();
- sb.AppendLine("Example:");
- sb.AppendLine("");
- sb.AppendLine("");
- sb.AppendLine();
-
- // Variable substitution
- sb.AppendLine("## Variable Substitution");
- sb.AppendLine();
- sb.AppendLine("Use $(VariableName) syntax anywhere in attribute values.");
- sb.AppendLine("Variables are provided via the 'inputs' parameter as key-value pairs.");
- sb.AppendLine();
- sb.AppendLine("Example:");
- sb.AppendLine("");
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ return "XML script defining the web automation workflow. See documentation for details.";
+ }
- return sb.ToString();
+ using var reader = new StreamReader(stream);
+ return reader.ReadToEnd();
}
private static NameValueCollection ParseInputsFromJsonElement(JsonElement inputsElement)
diff --git a/src/NetInteractor.Mcp/ScriptDescription.txt b/src/NetInteractor.Mcp/ScriptDescription.txt
new file mode 100644
index 0000000..d0c0c66
--- /dev/null
+++ b/src/NetInteractor.Mcp/ScriptDescription.txt
@@ -0,0 +1,95 @@
+XML script defining the web automation workflow.
+
+## Script Structure
+
+
+
+
+
+
+
+## Target Element
+
+Targets are named workflow steps. The 'defaultTarget' attribute specifies which target runs first.
+Targets can call other targets using the action for modular workflows.
+
+## Supported Action Types
+
+### 1. GET Request ()
+Fetches a web page via HTTP GET and optionally extracts data.
+
+Attributes:
+- url (required): URL to fetch. Supports $(Variable) substitution.
+- expectedHttpStatusCodes: Comma-separated valid status codes (default: 200).
+
+Child Elements:
+- : Extract data from the response (see Output Extraction).
+
+Example:
+
+
+
+
+### 2. POST Form Submission ()
+Submits an HTML form. Must be preceded by a GET to load the page with the form.
+
+Attributes (use ONE to identify the form):
+- formIndex: Zero-based index of form on page (e.g., formIndex='0').
+- formName: The 'name' attribute of the form element.
+- action: The 'action' attribute/URL of the form.
+- clientID: The 'id' attribute of the form element.
+
+Child Elements:
+- : Set form field values.
+- : Extract data from the response after submission.
+
+Example:
+
+
+
+
+
+### 3. Conditional Execution ()
+Executes child action only when a condition is met.
+
+Attributes:
+- property (required): Input variable to check. Use $(VariableName) syntax.
+- value (required): Expected value to match.
+
+Example:
+
+
+
+
+### 4. Call Another Target ()
+Executes another named target, enabling modular workflows.
+
+Attributes:
+- target (required): Name of the target to execute.
+
+Example:
+
+
+## Output Extraction ()
+
+Extracts data from HTML responses using XPath.
+
+Attributes:
+- name (required): Variable name for extracted value.
+- xpath (required): XPath expression to select element(s).
+- attr (required): What to extract ('text()' for inner text, or attribute name like 'href').
+- regex: Optional regex to further extract from the selected content.
+- isMultipleValue: Set 'true' to extract all matching elements as comma-separated values.
+- expectedValue: Validation - fails if extracted value doesn't match.
+
+Example:
+
+
+
+## Variable Substitution
+
+Use $(VariableName) syntax anywhere in attribute values.
+Variables are provided via the 'inputs' parameter as key-value pairs.
+
+Example:
+
From a966968c99c57fc6560620493234d2befd9e4af2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 01:33:12 +0000
Subject: [PATCH 13/14] Improve fallback message when embedded resource is
missing
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
src/NetInteractor.Mcp/NetInteractorTools.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/NetInteractor.Mcp/NetInteractorTools.cs b/src/NetInteractor.Mcp/NetInteractorTools.cs
index 3fc45b0..d40e6ad 100644
--- a/src/NetInteractor.Mcp/NetInteractorTools.cs
+++ b/src/NetInteractor.Mcp/NetInteractorTools.cs
@@ -333,7 +333,8 @@ private static string LoadScriptDescription()
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
- return "XML script defining the web automation workflow. See documentation for details.";
+ // Fallback message if embedded resource is missing (should not happen in normal builds)
+ return "XML script defining the web automation workflow. Warning: ScriptDescription.txt embedded resource not found. Please ensure the resource is properly embedded in the build.";
}
using var reader = new StreamReader(stream);
From b3caa10cf16f4fba907d62ea413263a52cb8c365 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 01:49:44 +0000
Subject: [PATCH 14/14] Refactor tests to test against public InvokeAsync
method, add Moq package
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
Directory.Packages.props | 1 +
.../NetInteractor.Mcp.Test.csproj | 2 +
.../NetInteractorToolsTests.cs | 136 ++++++++++++++----
3 files changed, 112 insertions(+), 27 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index da9a6c7..0f48180 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@
+
diff --git a/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj b/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj
index 9e8c847..0f6f42e 100644
--- a/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj
+++ b/test/NetInteractor.Mcp.Test/NetInteractor.Mcp.Test.csproj
@@ -10,6 +10,8 @@
+
+
diff --git a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
index 295a8e2..1e02c40 100644
--- a/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
+++ b/test/NetInteractor.Mcp.Test/NetInteractorToolsTests.cs
@@ -1,5 +1,11 @@
-using System.Collections.Specialized;
+#nullable enable
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using Moq;
using NetInteractor.Mcp;
using NetInteractor.Test.TestWebApp;
using NetInteractor.WebAccessors;
@@ -12,6 +18,7 @@ public class NetInteractorToolTests : IClassFixture
private readonly TestWebApplicationFactory _factory;
private readonly string _baseUrl;
private readonly NetInteractorTool _tool;
+ private readonly Mock _mockServer;
public NetInteractorToolTests(TestWebApplicationFixture fixture)
{
@@ -19,10 +26,52 @@ public NetInteractorToolTests(TestWebApplicationFixture fixture)
_baseUrl = fixture.Factory.ServerUrl;
// Use HttpClientWebAccessor for tests since Playwright requires browser installation
_tool = new NetInteractorTool(new HttpClientWebAccessor());
+ _mockServer = new Mock();
+ }
+
+ private RequestContext CreateRequestContext(IDictionary? arguments = null)
+ {
+ var jsonRpcRequest = new JsonRpcRequest
+ {
+ Id = new RequestId("test-id"),
+ Method = "tools/call",
+ Params = null
+ };
+
+ var context = new RequestContext(_mockServer.Object, jsonRpcRequest)
+ {
+ Params = new CallToolRequestParams
+ {
+ Name = "netinteractor_execute_script",
+ Arguments = arguments
+ }
+ };
+
+ return context;
+ }
+
+ private static IDictionary CreateArguments(string script, IDictionary? inputs = null, string? target = null)
+ {
+ var args = new Dictionary
+ {
+ ["script"] = JsonSerializer.SerializeToElement(script)
+ };
+
+ if (inputs != null)
+ {
+ args["inputs"] = JsonSerializer.SerializeToElement(inputs);
+ }
+
+ if (target != null)
+ {
+ args["target"] = JsonSerializer.SerializeToElement(target);
+ }
+
+ return args;
}
[Fact]
- public async Task ExecuteScriptInternalAsync_SimpleGetScript_ExtractsTitle()
+ public async Task InvokeAsync_SimpleGetScript_ExtractsTitle()
{
// Arrange
var script = $@"
@@ -33,17 +82,20 @@ public async Task ExecuteScriptInternalAsync_SimpleGetScript_ExtractsTitle()
";
+ var context = CreateRequestContext(CreateArguments(script));
+
// Act
- var result = await _tool.ExecuteScriptInternalAsync(script);
+ var result = await _tool.InvokeAsync(context, CancellationToken.None);
// Assert
- Assert.True(result.Ok, result.Message);
- Assert.NotNull(result.Outputs);
- Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
+ Assert.NotNull(result);
+ Assert.False(result.IsError);
+ Assert.NotNull(result.StructuredContent);
+ Assert.Equal("Welcome to Test Shop", result.StructuredContent["title"]?.ToString());
}
[Fact]
- public async Task ExecuteScriptInternalAsync_WithInputs_ExtractsTitle()
+ public async Task InvokeAsync_WithInputs_ExtractsTitle()
{
// Arrange
var script = @"
@@ -54,19 +106,21 @@ public async Task ExecuteScriptInternalAsync_WithInputs_ExtractsTitle()
";
- var inputs = new NameValueCollection { { "BaseUrl", _baseUrl } };
+ var inputs = new Dictionary { { "BaseUrl", _baseUrl } };
+ var context = CreateRequestContext(CreateArguments(script, inputs));
// Act
- var result = await _tool.ExecuteScriptInternalAsync(script, inputs);
+ var result = await _tool.InvokeAsync(context, CancellationToken.None);
// Assert
- Assert.True(result.Ok, result.Message);
- Assert.NotNull(result.Outputs);
- Assert.Equal("Welcome to Test Shop", result.Outputs["title"]);
+ Assert.NotNull(result);
+ Assert.False(result.IsError);
+ Assert.NotNull(result.StructuredContent);
+ Assert.Equal("Welcome to Test Shop", result.StructuredContent["title"]?.ToString());
}
[Fact]
- public async Task ExecuteScriptInternalAsync_WithSpecificTarget_ExecutesTarget()
+ public async Task InvokeAsync_WithSpecificTarget_ExecutesTarget()
{
// Arrange
var script = $@"
@@ -82,31 +136,56 @@ public async Task ExecuteScriptInternalAsync_WithSpecificTarget_ExecutesTarget()
";
+ var context = CreateRequestContext(CreateArguments(script, target: "Products"));
+
// Act
- var result = await _tool.ExecuteScriptInternalAsync(script, null, "Products");
+ var result = await _tool.InvokeAsync(context, CancellationToken.None);
// Assert
- Assert.True(result.Ok, result.Message);
- Assert.NotNull(result.Outputs);
- Assert.Equal("Products", result.Outputs["productTitle"]);
+ Assert.NotNull(result);
+ Assert.False(result.IsError);
+ Assert.NotNull(result.StructuredContent);
+ Assert.Equal("Products", result.StructuredContent["productTitle"]?.ToString());
}
[Fact]
- public async Task ExecuteScriptInternalAsync_InvalidScript_ReturnsError()
+ public async Task InvokeAsync_InvalidScript_ReturnsError()
{
// Arrange
var invalidScript = @"";
+ var context = CreateRequestContext(CreateArguments(invalidScript));
// Act
- var result = await _tool.ExecuteScriptInternalAsync(invalidScript);
+ var result = await _tool.InvokeAsync(context, CancellationToken.None);
// Assert
- Assert.False(result.Ok);
- Assert.NotNull(result.Message);
+ Assert.NotNull(result);
+ Assert.True(result.IsError);
+ Assert.NotNull(result.Content);
+ Assert.NotEmpty(result.Content);
}
[Fact]
- public async Task ExecuteScriptInternalAsync_MultipleOutputs_ExtractsAllValues()
+ public async Task InvokeAsync_MissingScript_ReturnsError()
+ {
+ // Arrange - No script argument
+ var context = CreateRequestContext(new Dictionary());
+
+ // Act
+ var result = await _tool.InvokeAsync(context, CancellationToken.None);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.True(result.IsError);
+ Assert.NotNull(result.Content);
+ Assert.NotEmpty(result.Content);
+ var textContent = result.Content[0] as TextContentBlock;
+ Assert.NotNull(textContent);
+ Assert.Contains("script", textContent.Text.ToLower());
+ }
+
+ [Fact]
+ public async Task InvokeAsync_MultipleOutputs_ExtractsAllValues()
{
// Arrange
var script = $@"
@@ -118,14 +197,17 @@ public async Task ExecuteScriptInternalAsync_MultipleOutputs_ExtractsAllValues()
";
+ var context = CreateRequestContext(CreateArguments(script));
+
// Act
- var result = await _tool.ExecuteScriptInternalAsync(script);
+ var result = await _tool.InvokeAsync(context, CancellationToken.None);
// Assert
- Assert.True(result.Ok, result.Message);
- Assert.NotNull(result.Outputs);
- Assert.Equal("Data Extraction Test", result.Outputs["title"]);
- Assert.Equal("/images/test.png", result.Outputs["imageSrc"]);
+ Assert.NotNull(result);
+ Assert.False(result.IsError);
+ Assert.NotNull(result.StructuredContent);
+ Assert.Equal("Data Extraction Test", result.StructuredContent["title"]?.ToString());
+ Assert.Equal("/images/test.png", result.StructuredContent["imageSrc"]?.ToString());
}
[Fact]