refactor(core): pass AbortSignal to WebMCP tools to handle cancellation

This forwards the `AbortSignal` natively supplied by Chrome 153.0.8009.0's update to `registerTool` executions down into the Angular context. This helps tools gracefully handle execution cancellations initiated by agents or users, preventing unnecessary background work and leaking resources.

We combine the injector/component destruction abort signals with the native WebMCP execution cancellation signals so the underlying tools correctly cancel long-running requests without producing invalid states.

Justification:
https://groups.google.com/a/chromium.org/g/chrome-ai-dev-preview/c/9291sjhIRz0/m/RFuyRrs5AAAJ
This commit is contained in:
Doug Parker
2026-08-18 11:14:31 -07:00
committed by Jessica Janiuk
parent 1fb3b58abe
commit e041f9483e
2 changed files with 44 additions and 5 deletions
+11 -4
View File
@@ -59,13 +59,20 @@ export async function declareExperimentalWebMcpTool<
const abortCtrl = new AbortController();
const wrappedTool: ToolDescriptor<InputSchema> = {
...tool,
execute: (args, client) =>
runInInjectionContext(currentInjector, () =>
execute: (args, client) => {
// TODO: `@mcp-b/webmcp-polyfill` currently lacks `AbortSignal` in its mock client.
// Remove the optional chaining when it is updated to match Chrome 153 spec.
const signal = client?.signal
? AbortSignal.any([abortCtrl.signal, client.signal])
: abortCtrl.signal;
return runInInjectionContext(currentInjector, () =>
tool.execute(args, {
...client,
signal: abortCtrl.signal,
signal,
}),
),
);
},
};
// Unregister when the associated `Injector` is destroyed.
+33 -1
View File
@@ -10,7 +10,7 @@ import {initializeWebMCPPolyfill, cleanupWebMCPPolyfill} from '@mcp-b/webmcp-pol
import type {JsonSchemaForInference} from '../../third_party/@mcp-b/webmcp-types';
import {inject, Injectable, Injector, runInInjectionContext} from '../../src/di';
import {declareExperimentalWebMcpTool} from '../../src/webmcp/declare_tool';
import {Execute} from '../../src/webmcp/types';
import {Execute, ModelContext, ToolDescriptor} from '../../src/webmcp/types';
import {RuntimeErrorCode} from '../../src/errors';
// Whether or not the input type is `any`.
@@ -168,6 +168,38 @@ describe('declareExperimentalWebMcpTool', () => {
expect(signal.aborted).toBeTrue();
});
it('should pass an `AbortSignal` to the tool and abort it when the client signal aborts', async () => {
const injector = Injector.create({providers: []});
const execute = jasmine
.createSpy<Execute<JsonSchemaForInference>>('execute')
.and.returnValue({content: []});
const modelContext = (globalThis.document as any).modelContext;
const registerToolSpy = spyOn(modelContext, 'registerTool').and.callThrough();
await declareExperimentalWebMcpTool(
{
name: 'testTool',
description: 'A test tool',
inputSchema: {type: 'object', properties: {}},
execute,
},
injector,
);
const wrappedTool = registerToolSpy.calls.first()
.args[0] as ToolDescriptor<JsonSchemaForInference>;
const clientAbortCtrl = new AbortController();
await wrappedTool.execute({}, {signal: clientAbortCtrl.signal});
const [, {signal}] = execute.calls.first().args;
expect(signal.aborted).toBeFalse();
clientAbortCtrl.abort();
expect(signal.aborted).toBeTrue();
});
it('should run `execute` in an injection context', async () => {
@Injectable()
class TestService {}