Skip to content

Commit 1b97181

Browse files
Merge pull request #104 from GoogleChromeLabs/signal
Use AbortSignal to unregister tools when available
2 parents dfbecee + 7577ff1 commit 1b97181

8 files changed

Lines changed: 71 additions & 54 deletions

File tree

demos/analytics-dashboard/src/hooks/useWebMCPQueryTool.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export default function useWebMCPQueryTool(executeQueryRef) {
1515
return;
1616
}
1717

18+
const controller = new AbortController();
19+
1820
navigator.modelContext.registerTool({
1921
name: "query",
2022
description: `Query the server logs. Sets all filters and visualization in one atomic call — every parameter is always applied together, so no stale state can carry over from a previous query.
@@ -74,10 +76,11 @@ CHART (required):
7476
required: ["groupBy", "measure", "chartType"],
7577
},
7678
execute: (params) => executeQueryRef.current(params),
77-
});
79+
}, { signal: controller.signal });
7880

7981
return () => {
80-
navigator.modelContext.unregisterTool("query");
82+
navigator.modelContext.unregisterTool?.("query");
83+
controller.abort();
8184
};
8285
}, [executeQueryRef]);
8386
}

demos/doors/magic.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ <h1>The Wizard's Attic</h1>
3131
</form>
3232

3333
<script>
34+
const controller = new AbortController();
3435
function cast() {
3536
document.body.style.background = "#fff";
3637
document.getElementById('status').innerText = "The owl blinks at the sudden light!";
3738

38-
navigator.modelContext.unregisterTool('castLight');
39+
navigator.modelContext.unregisterTool?.('castLight');
40+
controller.abort();
3941
document.querySelector('button').setAttribute('disabled', '');
4042

4143
document.querySelector('form').setAttribute('toolname', 'returnToHallway');
@@ -52,7 +54,7 @@ <h1>The Wizard's Attic</h1>
5254
await new Promise(r => setTimeout(r, 1000));
5355
return document.getElementById('status').innerText;
5456
},
55-
});
57+
}, { signal: controller.signal });
5658
</script>
5759
</body>
5860

demos/hotel-chain/src/hooks/useWebMCP.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ export function useWebMCP(tools: WebMCPTool[]) {
2525
}
2626

2727
const modelContext = window.navigator.modelContext;
28+
const controller = new AbortController();
2829

2930
tools.forEach(tool => {
3031
try {
31-
modelContext.registerTool(tool);
32+
modelContext.registerTool(tool, { signal: controller.signal });
3233
registeredTools.current.add(tool.name);
3334
} catch (error) {
3435
console.error(`Failed to register WebMCP tool "${tool.name}":`, error);
@@ -38,7 +39,8 @@ export function useWebMCP(tools: WebMCPTool[]) {
3839
return () => {
3940
registeredTools.current.forEach(name => {
4041
try {
41-
modelContext.unregisterTool(name);
42+
modelContext.unregisterTool?.(name);
43+
controller.abort();
4244
} catch (error) {
4345
console.error(`Failed to unregister WebMCP tool "${name}":`, error);
4446
}

demos/react-flightsearch/src/webmcp.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { flights, type Flight } from "./data/flights";
22

3-
const registeredTools = {
4-
listFlights: false,
5-
setFilters: false,
6-
resetFilters: false,
7-
searchFlights: false,
3+
const registeredTools: Record<string, AbortController | null> = {
4+
searchTools: null,
5+
resultsTools: null,
86
};
97

108
function dispatchAndWait(
@@ -306,54 +304,50 @@ export const searchFlightsTool = {
306304
export function registerFlightSearchTools() {
307305
const modelContext = window.navigator.modelContext;
308306
if (modelContext) {
309-
modelContext.registerTool(searchFlightsTool);
307+
if (!registeredTools.searchTools) {
308+
registeredTools.searchTools = new AbortController();
309+
modelContext.registerTool(searchFlightsTool, { signal: registeredTools.searchTools.signal });
310+
}
310311
}
311312
}
312313

313314
export function unregisterFlightSearchTools() {
314315
const modelContext = window.navigator.modelContext;
315316
if (modelContext) {
316-
modelContext.unregisterTool(searchFlightsTool.name);
317+
modelContext.unregisterTool?.(searchFlightsTool.name);
318+
if (registeredTools.searchTools) {
319+
registeredTools.searchTools.abort();
320+
registeredTools.searchTools = null;
321+
}
317322
}
318323
}
319324

320325
export function registerFlightResultsTools() {
321326
const modelContext = window.navigator.modelContext;
322327

323328
if (modelContext) {
324-
if (!registeredTools.listFlights) {
325-
modelContext.registerTool(listFlightsTool);
326-
registeredTools.listFlights = true;
327-
}
328-
329-
if (!registeredTools.setFilters) {
330-
modelContext.registerTool(setFiltersTool);
331-
registeredTools.setFilters = true;
332-
}
333-
334-
if (!registeredTools.resetFilters) {
335-
modelContext.registerTool(resetFiltersTool);
336-
registeredTools.resetFilters = true;
337-
}
338-
339-
if (!registeredTools.searchFlights) {
340-
modelContext.registerTool(searchFlightsTool);
341-
registeredTools.searchFlights = true;
329+
if (!registeredTools.resultsTools) {
330+
registeredTools.resultsTools = new AbortController();
331+
const options = { signal: registeredTools.resultsTools.signal };
332+
modelContext.registerTool(listFlightsTool, options);
333+
modelContext.registerTool(setFiltersTool, options);
334+
modelContext.registerTool(resetFiltersTool, options);
335+
modelContext.registerTool(searchFlightsTool, options);
342336
}
343337
}
344338
}
345339

346340
export function unregisterFlightResultsTools() {
347341
const modelContext = window.navigator.modelContext;
348342
if (modelContext) {
349-
modelContext.unregisterTool(listFlightsTool.name);
350-
modelContext.unregisterTool(setFiltersTool.name);
351-
modelContext.unregisterTool(resetFiltersTool.name);
352-
modelContext.unregisterTool(searchFlightsTool.name);
343+
modelContext.unregisterTool?.(listFlightsTool.name);
344+
modelContext.unregisterTool?.(setFiltersTool.name);
345+
modelContext.unregisterTool?.(resetFiltersTool.name);
346+
modelContext.unregisterTool?.(searchFlightsTool.name);
353347

354-
registeredTools.listFlights = false;
355-
registeredTools.setFilters = false;
356-
registeredTools.resetFilters = false;
357-
registeredTools.searchFlights = false;
348+
if (registeredTools.resultsTools) {
349+
registeredTools.resultsTools.abort();
350+
registeredTools.resultsTools = null;
351+
}
358352
}
359353
}

demos/shared/types/webmcp.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ declare global {
5353
/** The model context API exposed on `navigator.modelContext`. */
5454
interface ModelContext {
5555
/** Adds a single tool to the current context. */
56-
registerTool(tool: ModelContextTool): void;
56+
registerTool(tool: ModelContextTool, options?: { signal?: AbortSignal }): void;
5757

58-
/** Removes a tool by name. */
59-
unregisterTool(name: string): void;
58+
/** Removes a tool by name. (Deprecated) */
59+
unregisterTool?(name: string): void;
6060
}
6161

6262
interface Navigator {

demos/sport-shop-angular/src/app/components/cart-modal/cart-modal.component.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ export class CartModalComponent implements OnInit, OnDestroy {
3737
this.unregisterCartTools();
3838
}
3939

40+
private cartToolController: AbortController | null = null;
41+
4042
private registerCartTools() {
4143
const modelContext = navigator.modelContext;
4244
if (modelContext) {
45+
this.cartToolController = new AbortController();
46+
const signal = this.cartToolController.signal;
47+
4348
// 1. Remove from Cart Tool
4449
modelContext.registerTool({
4550
name: "remove_from_cart",
@@ -70,7 +75,7 @@ export class CartModalComponent implements OnInit, OnDestroy {
7075
this.onRemove(product.id);
7176
return { success: true, message: `Removed '${product.name}' from cart.` };
7277
}
73-
});
78+
}, { signal });
7479

7580
// 2. Start Checkout Tool
7681
modelContext.registerTool({
@@ -83,7 +88,7 @@ export class CartModalComponent implements OnInit, OnDestroy {
8388
this.onCheckout();
8489
return { success: true, message: "Checkout started." };
8590
}
86-
});
91+
}, { signal });
8792

8893
// 3. Confirm Order Tool
8994
modelContext.registerTool({
@@ -96,16 +101,17 @@ export class CartModalComponent implements OnInit, OnDestroy {
96101
this.onConfirmOrder();
97102
return { success: true, message: "Order confirmed and closed." };
98103
}
99-
});
104+
}, { signal });
100105
}
101106
}
102107

103108
private unregisterCartTools() {
104109
const modelContext = navigator.modelContext;
105110
if (modelContext) {
106-
modelContext.unregisterTool("remove_from_cart");
107-
modelContext.unregisterTool("start_checkout");
108-
modelContext.unregisterTool("confirm_order");
111+
modelContext.unregisterTool?.("remove_from_cart");
112+
modelContext.unregisterTool?.("start_checkout");
113+
modelContext.unregisterTool?.("confirm_order");
114+
this.cartToolController?.abort();
109115
}
110116
}
111117

demos/sport-shop-angular/src/app/pages/search/search.component.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,14 @@ export class SearchComponent implements OnInit, OnDestroy {
6363
this.unregisterSearchTools();
6464
}
6565

66+
private searchToolController: AbortController | null = null;
67+
6668
private registerSearchTools() {
6769
const modelContext = navigator.modelContext;
6870
if (modelContext) {
71+
this.searchToolController = new AbortController();
72+
const signal = this.searchToolController.signal;
73+
6974
// 1. Refine Search Tool
7075
modelContext.registerTool({
7176
name: "refine_search",
@@ -89,7 +94,7 @@ export class SearchComponent implements OnInit, OnDestroy {
8994
return { success: false, message: `Invalid price range '${params.priceRange}'. Must be one of: 'all', '0-49.99', '50-99.99', '100+'` };
9095
}
9196
}
92-
});
97+
}, { signal });
9398

9499
// 2. Add Search Result to Cart Tool
95100
modelContext.registerTool({
@@ -122,15 +127,16 @@ export class SearchComponent implements OnInit, OnDestroy {
122127
this.cartService.addToCart(product);
123128
return { success: true, message: `Added '${product.name}' to cart.` };
124129
}
125-
});
130+
}, { signal });
126131
}
127132
}
128133

129134
private unregisterSearchTools() {
130135
const modelContext = navigator.modelContext;
131136
if (modelContext) {
132-
modelContext.unregisterTool("refine_search");
133-
modelContext.unregisterTool("add_search_result_to_cart");
137+
modelContext.unregisterTool?.("refine_search");
138+
modelContext.unregisterTool?.("add_search_result_to_cart");
139+
this.searchToolController?.abort();
134140
}
135141
}
136142

demos/webmcp-maze/src/webmcp/ToolRegistry.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export class ToolRegistry {
3535
* Referenced by `window.gameTools.executeTool` at call time.
3636
*/
3737
private toolMap: Map<string, ModelContextTool> = new Map();
38+
39+
private toolController: AbortController | null = null;
3840

3941
/**
4042
* @param game - The game orchestrator instance.
@@ -101,10 +103,12 @@ export class ToolRegistry {
101103
if (this.supported) {
102104
const ctx = navigator.modelContext!;
103105
for (const name of this.toolMap.keys()) {
104-
ctx.unregisterTool(name);
106+
ctx.unregisterTool?.(name);
105107
}
108+
this.toolController?.abort();
109+
this.toolController = new AbortController();
106110
for (const tool of tools) {
107-
ctx.registerTool(tool);
111+
ctx.registerTool(tool, { signal: this.toolController.signal });
108112
}
109113
}
110114
this.toolMap.clear();

0 commit comments

Comments
 (0)