Skip to content

Commit a375395

Browse files
kblokclaude
andauthored
New Feature: Add WebMcpTool.ExecuteAsync() method (#14851) (#3421)
Implements upstream PR #14851 which adds direct tool execution support to WebMcpTool. Includes the full WebMCP API infrastructure (from #14814) plus the execute capability. WebMcpTool.ExecuteAsync(input) invokes the tool via CDP and returns a Task<WebMcpToolCallResult> that completes when the toolresponded event fires with a matching invocation ID. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fdcfc97 commit a375395

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

lib/PuppeteerSharp.Tests/WebMcpTests/PageWebMcpTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Threading.Tasks;
23
using NUnit.Framework;
34
using PuppeteerSharp.Cdp;
@@ -110,6 +111,52 @@ await page.EvaluateFunctionAsync(@"() => {
110111
Assert.That(removed, Has.Length.GreaterThanOrEqualTo(1));
111112
}
112113

114+
[Test, PuppeteerTest("webmcp.spec", "Page.webmcp", "should invoke tool")]
115+
public async Task ShouldInvokeTool()
116+
{
117+
await using var browser = await Puppeteer.LaunchAsync(WebMcpOptions(), TestConstants.LoggerFactory);
118+
var page = (CdpPage)await browser.NewPageAsync();
119+
await page.GoToAsync(TestConstants.HttpsPrefix + "/empty.html");
120+
121+
Assert.That(page.WebMcp, Is.Not.Null);
122+
123+
var toolAddedTcs = new TaskCompletionSource<bool>();
124+
page.WebMcp.ToolsAdded += (_, _) => toolAddedTcs.TrySetResult(true);
125+
126+
await page.EvaluateFunctionAsync(@"() => {
127+
window.navigator.modelContext.registerTool({
128+
name: 'test-tool-1',
129+
description: 'A test tool 1',
130+
inputSchema: {
131+
type: 'object',
132+
properties: { text: { type: 'string', description: 'Some text' } },
133+
required: ['text'],
134+
},
135+
execute: (params) => {
136+
return `hello ${params.text}`;
137+
},
138+
});
139+
}");
140+
141+
await toolAddedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
142+
143+
var tools = page.WebMcp.Tools();
144+
var tool = tools[0];
145+
146+
var toolCalledTcs = new TaskCompletionSource<WebMcpToolCall>();
147+
page.WebMcp.ToolInvoked += (_, call) => toolCalledTcs.TrySetResult(call);
148+
149+
var response = await tool.ExecuteAsync(new { text = "world" });
150+
var call = await toolCalledTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
151+
152+
Assert.That(response.Id, Is.EqualTo(call.Id));
153+
Assert.That(response.Call, Is.SameAs(call));
154+
Assert.That(response.Status, Is.EqualTo(WebMcpInvocationStatus.Completed));
155+
Assert.That(response.Output?.ToString(), Contains.Substring("hello world"));
156+
Assert.That(response.ErrorText, Is.Null);
157+
Assert.That(response.Exception, Is.Null);
158+
}
159+
113160
[Test, PuppeteerTest("webmcp.spec", "Page.webmcp", "should remove tools on frame navigation")]
114161
public async Task ShouldRemoveToolsOnFrameNavigation()
115162
{

lib/PuppeteerSharp/Cdp/CdpWebMcp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ private void OnToolsAdded(WebMcpToolsAddedProtocolEvent e)
168168
},
169169
Frame = frame,
170170
Location = location,
171+
WebMcp = this,
171172
};
172173

173174
frameTools[tool.Name] = webMcpTool;

lib/PuppeteerSharp/Cdp/WebMcpTool.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// * SOFTWARE.
2222

23+
using System;
24+
using System.Threading.Tasks;
25+
2326
namespace PuppeteerSharp.Cdp;
2427

2528
/// <summary>
@@ -44,4 +47,32 @@ public class WebMcpTool
4447

4548
/// <summary>Source location that defined the tool (if available).</summary>
4649
public ConsoleMessageLocation Location { get; init; }
50+
51+
internal CdpWebMcp WebMcp { get; init; }
52+
53+
/// <summary>
54+
/// Executes the tool with the given input parameters.
55+
/// </summary>
56+
/// <param name="input">Input object matching the tool's <c>inputSchema</c>. Defaults to empty object.</param>
57+
/// <returns>A task resolving to the tool call result.</returns>
58+
public async Task<WebMcpToolCallResult> ExecuteAsync(object input = null)
59+
{
60+
var invocationId = await WebMcp.InvokeToolAsync(this, input ?? new { }).ConfigureAwait(false);
61+
62+
var tcs = new TaskCompletionSource<WebMcpToolCallResult>(TaskCreationOptions.RunContinuationsAsynchronously);
63+
64+
EventHandler<WebMcpToolCallResult> handler = null;
65+
handler = (_, result) =>
66+
{
67+
if (result.Id == invocationId)
68+
{
69+
WebMcp.ToolResponded -= handler;
70+
tcs.TrySetResult(result);
71+
}
72+
};
73+
74+
WebMcp.ToolResponded += handler;
75+
76+
return await tcs.Task.ConfigureAwait(false);
77+
}
4778
}

0 commit comments

Comments
 (0)