Skip to content

Refactor NetInteractor.Mcp to inherit from McpServerTool with PlaywrightWebAccessor default#7

Merged
kerryjiang merged 14 commits intomasterfrom
copilot/add-netinteractor-mcp-project
Feb 15, 2026
Merged

Refactor NetInteractor.Mcp to inherit from McpServerTool with PlaywrightWebAccessor default#7
kerryjiang merged 14 commits intomasterfrom
copilot/add-netinteractor-mcp-project

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 14, 2026

  • Refactor NetInteractor.Mcp project to use class-based MCP tool instead of static class
  • Keep only the ExecuteScriptAsync tool (using InteractionExecutor)
  • Remove GetAsync and PostAsync tools as they are not needed

PR Feedback Addressed (Round 1-5):

  • All previous feedback items addressed

PR Feedback Addressed (Round 6):

  • Moved script description to external file ScriptDescription.txt

PR Feedback Addressed (Round 7):

  • Tests now test against public InvokeAsync method (comment 2808261427)
  • Added CreateRequestContext helper method for creating MCP request contexts
  • Added CreateArguments helper method for building tool arguments
  • Added InvokeAsync_MissingScript_ReturnsError test case for missing script validation
  • Added Moq package for mocking McpServer

Tests

  • All 30 tests pass (22 existing + 8 new MCP tests)

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits February 14, 2026 20:51
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Copilot AI changed the title [WIP] Add NetInteractor.Mcp project with McpServerTool implementation Add NetInteractor.Mcp project with MCP server tools Feb 14, 2026
Copilot AI requested a review from kerryjiang February 14, 2026 20:57
Copilot stopped work on behalf of kerryjiang due to an error February 14, 2026 21:05
…ools

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Copilot AI changed the title Add NetInteractor.Mcp project with MCP server tools Refactor NetInteractor.Mcp to class-based MCP tool with metadata support Feb 14, 2026
{
/// <summary>
/// Result of executing a NetInteractor script.
/// </summary>
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use InteractionResult directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 4cd079f. Deleted ExecuteScriptResult.cs and now ExecuteScriptAsync returns InteractionResult directly.

/// MCP Server Tool for NetInteractor web automation.
/// This tool enables AI agents to execute web interactions and automation scripts using InteractExecutor.
/// </summary>
public class NetInteractorTool
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the class should be base on McpServerTool:
NetInteractorTool : McpServerTool

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 4cd079f. NetInteractorTool now inherits from McpServerTool and implements the required abstract members (ProtocolTool, Metadata, InvokeAsync).

/// Initializes a new instance of the NetInteractorTool class with the default HttpClientWebAccessor.
/// </summary>
public NetInteractorTool()
: this(new HttpClientWebAccessor())
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use PlaywrightWebAccessor by default

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 4cd079f. Default constructor now uses PlaywrightWebAccessor() instead of HttpClientWebAccessor().

["script"] = new ToolParameterMetadata
{
Name = "script",
Description = "The XML script defining the web interaction workflow",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give more explanation so AI can understand how to write NetInteractor script? We better have a way to explain all the targets supported by NetInteractor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 4cd079f. Added comprehensive XML documentation explaining:

  • Script structure (InteractConfig, targets)
  • All supported actions: get, post, if, call
  • Output extraction with XPath, regex, attributes
  • Variable substitution $(Name) syntax
  • Complete example scripts

Also updated the MCP input schema description with inline documentation.


return new ExecuteScriptResult
{
Success = result.Ok,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, we can use ExecuteResult of InterationExecutor directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 4cd079f. ExecuteScriptAsync now returns InteractionResult directly from the InterationExecutor. Deleted ExecuteScriptResult.cs.

…essor, use InteractionResult directly, add comprehensive script documentation

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 14, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • modelcontextprotocol.github.io
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Refactor NetInteractor.Mcp to class-based MCP tool with metadata support Refactor NetInteractor.Mcp to inherit from McpServerTool with PlaywrightWebAccessor default Feb 14, 2026
Copilot AI requested a review from kerryjiang February 14, 2026 21:35
/// <param name="inputs">Optional comma-separated key=value pairs for script inputs (e.g., "BaseUrl=https://example.com,Username=user").</param>
/// <param name="target">Optional target name to execute. If not specified, the default target will be used.</param>
/// <returns>The InteractionResult containing Ok status, Message, and extracted Outputs.</returns>
public async Task<InteractionResult> ExecuteScriptAsync(
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method doesn't seem required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 88155fa. Made ExecuteScriptAsync internal (renamed to ExecuteScriptInternalAsync) for testing purposes only.


return new CallToolResult
{
Content = new List<ContentBlock>
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We better return result.Output to ai agent through CallToolResult

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 88155fa. InvokeAsync now returns result.Outputs directly as JSON in the CallToolResult.Content instead of wrapping it in "Success" text.

{
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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use the return of GetOutputMetadata

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 88155fa. The Metadata property now returns the output metadata from GetOutputMetadata(). The InputSchema is set on ProtocolTool using JSON schema format.

/// <summary>
/// Gets the tool metadata.
/// </summary>
public override IReadOnlyList<object> Metadata => Array.Empty<object>();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not return empty here, right? OutputMetadata should be returned here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 88155fa. Metadata now returns the values from GetOutputMetadata() instead of an empty array.

/// Gets the input metadata for the ExecuteScript tool.
/// </summary>
/// <returns>A dictionary describing the input parameters.</returns>
public static Dictionary<string, ToolParameterMetadata> GetInputMetadata()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be private

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 88155fa. GetInputMetadata() is now private.

/// Gets the output metadata for the ExecuteScript tool.
/// </summary>
/// <returns>A dictionary describing the output properties of InteractionResult.</returns>
public static Dictionary<string, ToolParameterMetadata> GetOutputMetadata()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be private too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 88155fa. GetOutputMetadata() is now private.

};
}

private static NameValueCollection ParseInputs(string? inputs)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should need these two methods, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, both ParseInputs and FormatOutputs are needed - they're used internally by the tool. Kept them as private helper methods.

…, populate Metadata with output schema

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
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();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should read an array here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit cc007f3. Changed inputs from string? to string[]? and updated the schema to "type": "array", "items": { "type": "string" }. Now reads inputs as an array from JsonValueKind.Array.

Comment on lines +201 to +228
inputs = inputsValue.GetString();
if (arguments.TryGetValue("target", out var targetValue) && targetValue.ValueKind != JsonValueKind.Undefined && targetValue.ValueKind != JsonValueKind.Null)
target = targetValue.GetString();
}

if (string.IsNullOrEmpty(script))
{
return new CallToolResult
{
Content = new List<ContentBlock>
{
new TextContentBlock { Text = "Error: 'script' parameter is required." }
},
IsError = true
};
}

var result = await ExecuteScriptInternalAsync(script, inputs, target);

if (result.Ok)
{
// Return the outputs directly as JSON for the AI agent
var outputsJson = FormatOutputs(result.Outputs);
return new CallToolResult
{
Content = new List<ContentBlock>
{
new TextContentBlock { Text = outputsJson }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a ContentBlock we can put an object here directly without serializing it into json string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commits cc007f3 and 211ad98. Now using CallToolResult.StructuredContent property to return outputs directly as a JsonObject without serializing to a string first.

{
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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put the code into a method? Probably just add back the method GetInputMetadata() and use it directly here. That one has more detailed information about how the script can be used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit cc007f3. Created private GetInputMetadata() method that returns the JSON schema with comprehensive documentation including all supported actions (get, post, if, call), output extraction attributes, and variable substitution syntax.

Copilot AI and others added 2 commits February 14, 2026 22:25
…extract GetInputMetadata

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 14, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • http://api.nuget.org:443/DigiCertCSRSA4096RootG5.crl
    • Triggering command: /usr/bin/dotnet dotnet build (http block)
  • http://api.nuget.org:443/NETFoundationProjectsCodeSigningCA2.crl
    • Triggering command: /usr/bin/dotnet dotnet build (http block)

If you need me to access, download, or install something from one of these locations, you can either:

{
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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inputs can be JsonElement, all the attributes of it can be used key value string pairs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 1029f29. Changed inputs to read from a JsonElement object. Schema now uses "type": "object", "additionalProperties": { "type": "string" }. Added ParseInputsFromJsonElement() to extract key-value pairs from the object properties.

/// <summary>
/// Gets the input metadata schema as JSON string.
/// </summary>
private static string GetInputMetadata()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method can return type JsonElement directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 1029f29. GetInputMetadata() now returns JsonElement directly using JsonDocument.Parse(schemaJson).RootElement.

""";
}

private static Dictionary<string, ToolParameterMetadata> GetOutputMetadata()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return type can be JsonElement directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 1029f29. GetOutputMetadata() now returns JsonElement directly using JsonDocument.Parse(schemaJson).RootElement.

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
};
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assign output schema here too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 1029f29. Added OutputSchema = GetOutputMetadata() to the Tool definition.

{
"type": "object",
"properties": {
"script": {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need more detailed information about all the target types can be used in the script to make sure the AI agent know how to pass the script

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 1029f29. Added comprehensive documentation in GetInputMetadata() covering:

  • Script structure with InteractConfig and targets
  • GET request with all attributes (url, expectedHttpStatusCodes)
  • POST form submission with all form identifiers (formIndex, formName, action, clientID) and formValue
  • Conditional execution with property and value
  • Call target action
  • Output extraction with all attributes (name, xpath, attr, regex, isMultipleValue, expectedValue)
  • Variable substitution syntax

/// <summary>
/// Gets the tool metadata including output schema information.
/// </summary>
public override IReadOnlyList<object> Metadata => _outputMetadata;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should return empty list here. outputMetadata is not returned from here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 1029f29. Metadata now returns Array.Empty<object>(). Output schema is returned via ProtocolTool.OutputSchema instead.

… from metadata methods, set OutputSchema, return empty Metadata, add detailed target documentation

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
/// </summary>
private static JsonElement GetInputMetadata()
{
var schemaJson = """
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you operate JsonElement directly instead of construct a long string making the code look bad.
For the description of script, we better have a way to manage those kind data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commits e980c19 and c408d0b. Refactored GetInputMetadata() to build JSON schema using JsonObject programmatically. Script description is now in a separate BuildScriptDescription() method using StringBuilder for better maintainability. Also optimized using JsonSerializer.SerializeToElement() instead of round-trip through string.

/// </summary>
private static JsonElement GetOutputMetadata()
{
var schemaJson = """
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you operate JsonElement directly instead of construct a long string making the code look bad.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commits e980c19 and c408d0b. Refactored GetOutputMetadata() to build JSON schema using JsonObject programmatically. Also optimized using JsonSerializer.SerializeToElement() instead of round-trip through string.

Copilot AI and others added 2 commits February 15, 2026 00:00
…tead of string literals

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
sb.AppendLine("Variables are provided via the 'inputs' parameter as key-value pairs.");
sb.AppendLine();
sb.AppendLine("Example:");
sb.AppendLine("<get url='$(BaseUrl)/api/$(Endpoint)?id=$(ItemId)' />");
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put them into a file and we load that file from here. If there is any change for the features, we will just need to update that file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commits 756f88f and a966968. Moved the script description to ScriptDescription.txt as an embedded resource. Now LoadScriptDescription() loads it from the embedded resource at runtime, making it easy to update the documentation by editing the text file without code changes.

Copilot AI and others added 2 commits February 15, 2026 01:30
…sier maintenance

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
{
// Arrange
var script = $@"<InteractConfig defaultTarget='Main'>
<target name='Main'>
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests should test against the public method InvokeAsync to get more test coverage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit b3caa10. Tests now call InvokeAsync instead of the internal method. Added CreateRequestContext and CreateArguments helpers to construct MCP request contexts. Also added a new test for missing script validation (InvokeAsync_MissingScript_ReturnsError).

…kage

Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 15, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • modelcontextprotocol.github.io
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from kerryjiang February 15, 2026 01:50
@kerryjiang kerryjiang marked this pull request as ready for review February 15, 2026 01:56
@kerryjiang kerryjiang merged commit 3972bf3 into master Feb 15, 2026
3 checks passed
@kerryjiang kerryjiang deleted the copilot/add-netinteractor-mcp-project branch February 15, 2026 01:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants