Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions demos/analytics-dashboard/src/hooks/useWebMCPQueryTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export default function useWebMCPQueryTool(executeQueryRef) {
return;
}

const controller = new AbortController();

navigator.modelContext.registerTool({
name: "query",
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.
Expand Down Expand Up @@ -74,10 +76,11 @@ CHART (required):
required: ["groupBy", "measure", "chartType"],
},
execute: (params) => executeQueryRef.current(params),
});
}, { signal: controller.signal });

return () => {
navigator.modelContext.unregisterTool("query");
navigator.modelContext.unregisterTool?.("query");
controller.abort();
};
}, [executeQueryRef]);
}
6 changes: 4 additions & 2 deletions demos/doors/magic.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ <h1>The Wizard's Attic</h1>
</form>

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

navigator.modelContext.unregisterTool('castLight');
navigator.modelContext.unregisterTool?.('castLight');
controller.abort();
document.querySelector('button').setAttribute('disabled', '');

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

Expand Down
6 changes: 4 additions & 2 deletions demos/hotel-chain/src/hooks/useWebMCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ export function useWebMCP(tools: WebMCPTool[]) {
}

const modelContext = window.navigator.modelContext;
const controller = new AbortController();

tools.forEach(tool => {
try {
modelContext.registerTool(tool);
modelContext.registerTool(tool, { signal: controller.signal });
registeredTools.current.add(tool.name);
} catch (error) {
console.error(`Failed to register WebMCP tool "${tool.name}":`, error);
Expand All @@ -38,7 +39,8 @@ export function useWebMCP(tools: WebMCPTool[]) {
return () => {
registeredTools.current.forEach(name => {
try {
modelContext.unregisterTool(name);
modelContext.unregisterTool?.(name);
controller.abort();
} catch (error) {
console.error(`Failed to unregister WebMCP tool "${name}":`, error);
}
Expand Down
60 changes: 27 additions & 33 deletions demos/react-flightsearch/src/webmcp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { flights, type Flight } from "./data/flights";

const registeredTools = {
listFlights: false,
setFilters: false,
resetFilters: false,
searchFlights: false,
const registeredTools: Record<string, AbortController | null> = {
searchTools: null,
resultsTools: null,
};

function dispatchAndWait(
Expand Down Expand Up @@ -306,54 +304,50 @@ export const searchFlightsTool = {
export function registerFlightSearchTools() {
const modelContext = window.navigator.modelContext;
if (modelContext) {
modelContext.registerTool(searchFlightsTool);
if (!registeredTools.searchTools) {
registeredTools.searchTools = new AbortController();
modelContext.registerTool(searchFlightsTool, { signal: registeredTools.searchTools.signal });
}
}
}

export function unregisterFlightSearchTools() {
const modelContext = window.navigator.modelContext;
if (modelContext) {
modelContext.unregisterTool(searchFlightsTool.name);
modelContext.unregisterTool?.(searchFlightsTool.name);
if (registeredTools.searchTools) {
registeredTools.searchTools.abort();
registeredTools.searchTools = null;
}
}
}

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

if (modelContext) {
if (!registeredTools.listFlights) {
modelContext.registerTool(listFlightsTool);
registeredTools.listFlights = true;
}

if (!registeredTools.setFilters) {
modelContext.registerTool(setFiltersTool);
registeredTools.setFilters = true;
}

if (!registeredTools.resetFilters) {
modelContext.registerTool(resetFiltersTool);
registeredTools.resetFilters = true;
}

if (!registeredTools.searchFlights) {
modelContext.registerTool(searchFlightsTool);
registeredTools.searchFlights = true;
if (!registeredTools.resultsTools) {
registeredTools.resultsTools = new AbortController();
const options = { signal: registeredTools.resultsTools.signal };
modelContext.registerTool(listFlightsTool, options);
modelContext.registerTool(setFiltersTool, options);
modelContext.registerTool(resetFiltersTool, options);
modelContext.registerTool(searchFlightsTool, options);
}
}
}

export function unregisterFlightResultsTools() {
const modelContext = window.navigator.modelContext;
if (modelContext) {
modelContext.unregisterTool(listFlightsTool.name);
modelContext.unregisterTool(setFiltersTool.name);
modelContext.unregisterTool(resetFiltersTool.name);
modelContext.unregisterTool(searchFlightsTool.name);
modelContext.unregisterTool?.(listFlightsTool.name);
modelContext.unregisterTool?.(setFiltersTool.name);
modelContext.unregisterTool?.(resetFiltersTool.name);
modelContext.unregisterTool?.(searchFlightsTool.name);

registeredTools.listFlights = false;
registeredTools.setFilters = false;
registeredTools.resetFilters = false;
registeredTools.searchFlights = false;
if (registeredTools.resultsTools) {
registeredTools.resultsTools.abort();
registeredTools.resultsTools = null;
}
}
}
6 changes: 3 additions & 3 deletions demos/shared/types/webmcp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ declare global {
/** The model context API exposed on `navigator.modelContext`. */
interface ModelContext {
/** Adds a single tool to the current context. */
registerTool(tool: ModelContextTool): void;
registerTool(tool: ModelContextTool, options?: { signal?: AbortSignal }): void;
Comment thread
beaufortfrancois marked this conversation as resolved.

/** Removes a tool by name. */
unregisterTool(name: string): void;
/** Removes a tool by name. (Deprecated) */
unregisterTool?(name: string): void;
}

interface Navigator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ export class CartModalComponent implements OnInit, OnDestroy {
this.unregisterCartTools();
}

private cartToolController: AbortController | null = null;

private registerCartTools() {
const modelContext = navigator.modelContext;
if (modelContext) {
this.cartToolController = new AbortController();
const signal = this.cartToolController.signal;

// 1. Remove from Cart Tool
modelContext.registerTool({
name: "remove_from_cart",
Expand Down Expand Up @@ -70,7 +75,7 @@ export class CartModalComponent implements OnInit, OnDestroy {
this.onRemove(product.id);
return { success: true, message: `Removed '${product.name}' from cart.` };
}
});
}, { signal });

// 2. Start Checkout Tool
modelContext.registerTool({
Expand All @@ -83,7 +88,7 @@ export class CartModalComponent implements OnInit, OnDestroy {
this.onCheckout();
return { success: true, message: "Checkout started." };
}
});
}, { signal });

// 3. Confirm Order Tool
modelContext.registerTool({
Expand All @@ -96,16 +101,17 @@ export class CartModalComponent implements OnInit, OnDestroy {
this.onConfirmOrder();
return { success: true, message: "Order confirmed and closed." };
}
});
}, { signal });
}
}

private unregisterCartTools() {
const modelContext = navigator.modelContext;
if (modelContext) {
modelContext.unregisterTool("remove_from_cart");
modelContext.unregisterTool("start_checkout");
modelContext.unregisterTool("confirm_order");
modelContext.unregisterTool?.("remove_from_cart");
modelContext.unregisterTool?.("start_checkout");
modelContext.unregisterTool?.("confirm_order");
this.cartToolController?.abort();
}
}

Expand Down
14 changes: 10 additions & 4 deletions demos/sport-shop-angular/src/app/pages/search/search.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ export class SearchComponent implements OnInit, OnDestroy {
this.unregisterSearchTools();
}

private searchToolController: AbortController | null = null;

private registerSearchTools() {
const modelContext = navigator.modelContext;
if (modelContext) {
this.searchToolController = new AbortController();
const signal = this.searchToolController.signal;

// 1. Refine Search Tool
modelContext.registerTool({
name: "refine_search",
Expand All @@ -89,7 +94,7 @@ export class SearchComponent implements OnInit, OnDestroy {
return { success: false, message: `Invalid price range '${params.priceRange}'. Must be one of: 'all', '0-49.99', '50-99.99', '100+'` };
}
}
});
}, { signal });

// 2. Add Search Result to Cart Tool
modelContext.registerTool({
Expand Down Expand Up @@ -122,15 +127,16 @@ export class SearchComponent implements OnInit, OnDestroy {
this.cartService.addToCart(product);
return { success: true, message: `Added '${product.name}' to cart.` };
}
});
}, { signal });
}
}

private unregisterSearchTools() {
const modelContext = navigator.modelContext;
if (modelContext) {
modelContext.unregisterTool("refine_search");
modelContext.unregisterTool("add_search_result_to_cart");
modelContext.unregisterTool?.("refine_search");
modelContext.unregisterTool?.("add_search_result_to_cart");
this.searchToolController?.abort();
}
}

Expand Down
8 changes: 6 additions & 2 deletions demos/webmcp-maze/src/webmcp/ToolRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export class ToolRegistry {
* Referenced by `window.gameTools.executeTool` at call time.
*/
private toolMap: Map<string, ModelContextTool> = new Map();

private toolController: AbortController | null = null;

/**
* @param game - The game orchestrator instance.
Expand Down Expand Up @@ -101,10 +103,12 @@ export class ToolRegistry {
if (this.supported) {
const ctx = navigator.modelContext!;
for (const name of this.toolMap.keys()) {
ctx.unregisterTool(name);
ctx.unregisterTool?.(name);
}
this.toolController?.abort();
this.toolController = new AbortController();
for (const tool of tools) {
ctx.registerTool(tool);
ctx.registerTool(tool, { signal: this.toolController.signal });
}
}
this.toolMap.clear();
Expand Down
Loading