mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jessica Janiuk
parent
1fb3b58abe
commit
e041f9483e
@@ -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.
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user