From ec56f58d41cf11af3467c4f537e9b4b5880d212d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Fri, 21 Aug 2026 15:19:17 +0200 Subject: [PATCH 1/7] Add outputSchema to ModelContextTool and RegisteredTool --- README.md | 5 +- index.bs | 89 +++++++++++++++++++++++-------- security-privacy-questionnaire.md | 2 +- 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index fd97fea..6d90ec3 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,7 @@ Any document in the tree matching these origins (and allowed to use `tools` perm Once tools are registered, in-page agents can discover and invoke them using `getTools()` and `executeTool()`. -Calling `document.modelContext.getTools()` returns a promise that resolves with an array of `RegisteredTool` dictionary objects. Each object contains the tool's `name`, `description`, `inputSchema`, `origin`, and owner `window`. By default, `getTools()` only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the `fromOrigins` option. This array only supports secure origins. +Calling `document.modelContext.getTools()` returns a promise that resolves with an array of `RegisteredTool` dictionary objects. Each object contains the tool's `name`, `description`, `inputSchema`, `outputSchema`, `origin`, and owner `window`. By default, `getTools()` only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the `fromOrigins` option. This array only supports secure origins. ```js // Discover tools exposed by same-origin frames in the tree (default) @@ -365,6 +365,7 @@ for (const tool of tools) { console.log(`Tool: ${tool.name} (from ${tool.origin})`); console.log(`Description: ${tool.description}`); console.log(`Parameters schema:`, tool.inputSchema); + console.log(`Output schema:`, tool.outputSchema); } // Discover additional tools provided by a cross-origin frame (in addition to @@ -526,8 +527,6 @@ As the WebMCP proposal continues to evolve with community and stakeholder feedba - **Skills Integration**: Determining if the author should expose a higher-level "skill" to help the agent coordinate multiple related tools to fulfill a user journey. See [Issue #161](https://github.com/webmachinelearning/webmcp/issues/161). -- **Output schema**: Supporting structured `outputSchema` contracts (complementing `inputSchema`) to help LLMs reliably reason about the return values of tools. See [Issue #9](https://github.com/webmachinelearning/webmcp/issues/9). - - **User prompting and elicitation**: Exploring a way for a tool to prompt the user for confirmation when tools require explicit user authorization. This could be done by delegating to the agent and its harness, or by invoking native browser permission dialogue outside of the agent loop. See [Issue #165](https://github.com/webmachinelearning/webmcp/issues/165) and [Issue #50](https://github.com/webmachinelearning/webmcp/issues/50) for discussion about the `ModelContextClient` interface. - **Tool progress reporting**: For long-running tasks (e.g., batch processing or generating content), the agent may want a way to track a tool's progress. We are exploring how this intersects with the established [MCP Progress](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress) specification. diff --git a/index.bs b/index.bs index da8b99a..6020ddf 100644 --- a/index.bs +++ b/index.bs @@ -170,12 +170,21 @@ A tool definition is a [=struct=] with the following [=struct/items=] Note: For tools registered by the imperative form of this API (i.e., {{ModelContext/registerTool()}}), this is the stringified representation of - {{ModelContextTool/inputSchema}}. For tools registered + {{ModelContextTool/inputSchema}}, created by the [=serialize a tool schema=] + algorithm. For tools registered [declaratively](https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md), this will be a stringified JSON Schema object created by the [=synthesize a declarative JSON Schema object algorithm=]. [[!JSON-SCHEMA]] + : output schema + :: a [=string=]. + + Note: For tools registered by the imperative form of this API (i.e., + {{ModelContext/registerTool()}}), this is the stringified representation of + {{ModelContextTool/outputSchema}}, created by the [=serialize a tool schema=] + algorithm. + : execute steps :: an algorithm that takes a {{Document}} targetDocument, a [=string=] inputArguments, an algorithm completionSteps that takes a @@ -576,6 +585,31 @@ To unregister a tool given a {{ModelContext}} |mo +
+To serialize a tool schema given a {{ModelContextTool}} |tool|'s +{{ModelContextTool/inputSchema}} or {{ModelContextTool/outputSchema}} |schema|: + +1. If |tool|'s |schema| does not [=map/exist=], then return the empty string. + +1. Return the result of [=serializing a JavaScript value to a JSON string=] given |tool|'s |schema|. + +
+

The [=serialize a JavaScript value to a JSON string=] algorithm throws exceptions in the + following cases:

+ +
    +
  1. Throws a new {{TypeError}} when the backing "JSON.stringify()" + yields undefined, e.g., + "inputSchema: { toJSON() {return HTMLDivElement;}}", or + "outputSchema: { toJSON() {return undefined;}}".

  2. + +
  3. Re-throws exceptions thrown by "JSON.stringify()", e.g., when + |tool|'s |schema| is an object with a circular reference, etc.

  4. +
+
+ +
+

API

@@ -631,7 +665,7 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}.

Registers a tool that [=agents=] can invoke. Returns a rejected promise if a tool with the same name is already registered, if the given {{ModelContextTool/name}} or {{ModelContextTool/description}} are empty strings, or if the {{ModelContextTool/inputSchema}} - is invalid.

+ or {{ModelContextTool/outputSchema}} is invalid.

document.{{Document/modelContext}}.{{ModelContext/getTools(options)}}
@@ -699,26 +733,13 @@ The registerTool(tool, options -

The serialization algorithm above throws exceptions in the following cases:

- -
    -
  1. Throws a new {{TypeError}} when the backing "JSON.stringify()" - yields undefined, e.g., - "inputSchema: { toJSON() {return HTMLDivElement;}}", or - "inputSchema: { toJSON() {return undefined;}}".

  2. - -
  3. Re-throws exceptions thrown by "JSON.stringify()", e.g., when - "inputSchema" is an object with a circular reference, etc.

  4. -
- +1. Let |stringified output schema| be the result of [=serializing a tool schema=] + given |tool|'s {{ModelContextTool/outputSchema}}. If this threw an exception, then return [=a + promise rejected with=] that exception. 1. If |options|'s {{ModelContextRegisterToolOptions/signal}} [=map/exists=] and is [=AbortSignal/aborted=], then return [=a promise rejected with=] |options|'s @@ -767,6 +788,9 @@ The registerTool(tool, optionsgetTools(options) method steps Note: This will never throw an exception, because the string stored in the tool definition is always a valid JSON string. + : {{RegisteredTool/outputSchema}} + :: the result of [=parse a JSON string to a JavaScript value=] given |tool + definition|'s [=tool definition/output schema=], if |tool definition|'s [=tool + definition/output schema=] is not the empty string; otherwise undefined. + + Note: This will never throw an exception, because the string stored in the tool + definition is always a valid JSON string. + : {{RegisteredTool/window}} :: |targetDocument|'s [=relevant global object=] @@ -1089,6 +1121,7 @@ dictionary ModelContextTool { USVString title; required DOMString description; object inputSchema; + object outputSchema; required ToolExecuteCallback execute; ToolAnnotations annotations; }; @@ -1132,6 +1165,13 @@ callback ToolExecuteCallback = Promise (object inputObject, ToolExecuteCall

A JSON Schema object describing the expected input parameters for the tool [[!JSON-SCHEMA]]. +

tool["{{ModelContextTool/outputSchema}}"]
+
+

A JSON Schema object describing the expected output structure for the tool [[!JSON-SCHEMA]], + so that [=agents=] may use it to insert an [=implementation-defined=] message to guide the + language model’s behavior. +

+
tool["{{ModelContextTool/execute}}"]

A callback function that is invoked when an [=agent=] calls the tool. The function receives @@ -1241,6 +1281,7 @@ dictionary RegisteredTool { DOMString title; required DOMString description; object inputSchema; + object outputSchema; required Window window; required USVString origin; ToolAnnotations annotations; @@ -1265,6 +1306,12 @@ dictionary RegisteredTool { [[!JSON-SCHEMA]]. It is a deep copy of the schema provided at tool registration, via {{ModelContextTool/inputSchema}}. + : tool["{{RegisteredTool/outputSchema}}"] + :: A JSON Schema object describing the expected output structure for the tool [[!JSON-SCHEMA]], so + that [=agents=] may use it to insert an [=implementation-defined=] message to guide the + language model’s behavior. It is a deep copy of the schema provided at tool registration, via + {{ModelContextTool/outputSchema}}. + : tool["{{RegisteredTool/window}}"] :: The {{Window}} of the document that registered the tool. diff --git a/security-privacy-questionnaire.md b/security-privacy-questionnaire.md index dc0e7df..6e2d7d0 100644 --- a/security-privacy-questionnaire.md +++ b/security-privacy-questionnaire.md @@ -101,7 +101,7 @@ Note: this behavior is not yet spec'd but is the intended direction. > 20. Does your spec define when and how new kinds of errors should be raised? -Yes. `registerTool()` throws `InvalidStateError` for inactive documents, duplicate names, or invalid name/description; `NotAllowedError` when the `"tools"` Permissions Policy is disallowed; `SecurityError` for non-trustworthy [`exposedTo`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextregistertooloptions-exposedto) origins; and `TypeError` when `inputSchema` serialization fails. These errors only reflect the page's own state and inputs, so they do not leak new information. +Yes. `registerTool()` throws `InvalidStateError` for inactive documents, duplicate names, or invalid name/description; `NotAllowedError` when the `"tools"` Permissions Policy is disallowed; `SecurityError` for non-trustworthy [`exposedTo`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextregistertooloptions-exposedto) origins; and `TypeError` when `inputSchema` or `outputSchema` serialization fails. These errors only reflect the page's own state and inputs, so they do not leak new information. > 21. Does your feature allow sites to learn about the user's use of assistive technology? From ea1d089d707987f91e6735d1844e2bb0239387df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Fri, 9 Oct 2026 09:22:18 +0200 Subject: [PATCH 2/7] Address review feedback on outputSchema --- index.bs | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/index.bs b/index.bs index 1901da1..9fd9287 100644 --- a/index.bs +++ b/index.bs @@ -165,16 +165,14 @@ A tool definition is a [=struct=] with the following [=struct/items=] :: a [=string=]. : input schema - :: a [=string=]. - - Note: This is the stringified representation of the {{ModelContextTool/inputSchema}} passed to - {{ModelContext/registerTool()}}. [[!JSON-SCHEMA]] + :: a [=string=] that is either the empty string, indicating that no input schema was provided, or + a JSON string representing a JSON Schema object [[!JSON-SCHEMA]], as returned by the + [=serialize a tool schema=] algorithm. : output schema - :: a [=string=]. - - Note: This is the stringified representation of the {{ModelContextTool/outputSchema}} passed to - {{ModelContext/registerTool()}}. [[!JSON-SCHEMA]] + :: a [=string=] that is either the empty string, indicating that no output schema was provided, + or a JSON string representing a JSON Schema object [[!JSON-SCHEMA]], as returned by the + [=serialize a tool schema=] algorithm. : execute steps :: an algorithm that takes a {{Document}} targetDocument, a [=string=] unregister a tool given a {{ModelContext}} |mo

-To serialize a tool schema given a {{ModelContextTool}} |tool|'s -{{ModelContextTool/inputSchema}} or {{ModelContextTool/outputSchema}} |schema|: +To serialize a tool schema given an {{object}}-or-undefined |schema|: -1. If |tool|'s |schema| does not [=map/exist=], then return the empty string. +1. If |schema| is undefined, then return the empty string. -1. Return the result of [=serializing a JavaScript value to a JSON string=] given |tool|'s |schema|. +1. Return the result of [=serializing a JavaScript value to a JSON string=] given |schema|.

The [=serialize a JavaScript value to a JSON string=] algorithm throws exceptions in the @@ -627,7 +624,7 @@ To serialize a tool schema given a {{ModelContextTool}} |tool|'s "outputSchema: { toJSON() {return undefined;}}".

  • Re-throws exceptions thrown by "JSON.stringify()", e.g., when - |tool|'s |schema| is an object with a circular reference, etc.

  • + |schema| is an object with a circular reference, etc.

    @@ -756,13 +753,13 @@ The registerTool(tool, options (object inputObject, ToolExecuteCall
    tool["{{ModelContextTool/outputSchema}}"]
    -

    A JSON Schema object describing the expected output structure for the tool [[!JSON-SCHEMA]], - so that [=agents=] may use it to insert an [=implementation-defined=] message to guide the - language model’s behavior. +

    A JSON Schema object describing the expected output structure for the tool [[!JSON-SCHEMA]]. + Tool output is expected to conform to the provided schema, and [=agents=] may reject outputs + that do not validate.

    tool["{{ModelContextTool/execute}}"]
    @@ -1333,9 +1330,9 @@ dictionary RegisteredTool { {{ModelContextTool/inputSchema}}. : tool["{{RegisteredTool/outputSchema}}"] - :: A JSON Schema object describing the expected output structure for the tool [[!JSON-SCHEMA]], so - that [=agents=] may use it to insert an [=implementation-defined=] message to guide the - language model’s behavior. It is a deep copy of the schema provided at tool registration, via + :: A JSON Schema object describing the expected output structure for the tool [[!JSON-SCHEMA]]. + Tool output is expected to conform to the provided schema, and [=agents=] may reject outputs + that do not validate. It is a deep copy of the schema provided at tool registration, via {{ModelContextTool/outputSchema}}. : tool["{{RegisteredTool/window}}"] From bbfa0b012b2bedb9900aa0a1800d5431c960c5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Mon, 12 Oct 2026 13:19:35 +0200 Subject: [PATCH 3/7] Fix note markup in serialize a tool schema --- index.bs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.bs b/index.bs index 9fd9287..2886779 100644 --- a/index.bs +++ b/index.bs @@ -613,9 +613,9 @@ To serialize a tool schema given an {{object}}-or-undefined |schema|: 1. Return the result of [=serializing a JavaScript value to a JSON string=] given |schema|. -
    -

    The [=serialize a JavaScript value to a JSON string=] algorithm throws exceptions in the - following cases:

    +
    Note: + The [=serialize a JavaScript value to a JSON string=] algorithm throws exceptions in the + following cases:
    1. Throws a new {{TypeError}} when the backing "JSON.stringify()" From 2ab2c3d540ff54d001595037493f1c13d2663aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Mon, 12 Oct 2026 13:23:16 +0200 Subject: [PATCH 4/7] Clarify circular reference example in serialize a tool schema note --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 2886779..0e69b6f 100644 --- a/index.bs +++ b/index.bs @@ -624,7 +624,7 @@ To serialize a tool schema given an {{object}}-or-undefined |schema|: "outputSchema: { toJSON() {return undefined;}}".

    2. Re-throws exceptions thrown by "JSON.stringify()", e.g., when - |schema| is an object with a circular reference, etc.

    3. + the object value has a circular reference.

    From 6e215488961825f2fbc21f96be5a5b40058df462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Mon, 12 Oct 2026 13:24:58 +0200 Subject: [PATCH 5/7] Make serialize a tool schema exception reference explicit --- index.bs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 0e69b6f..d03f653 100644 --- a/index.bs +++ b/index.bs @@ -754,12 +754,14 @@ The registerTool(tool, options Date: Mon, 12 Oct 2026 13:25:40 +0200 Subject: [PATCH 6/7] Rename Parameters schema to Input schema in README example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b96478f..bf2dfb3 100644 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ const tools = await document.modelContext.getTools(); for (const tool of tools) { console.log(`Tool: ${tool.name} (from ${tool.origin})`); console.log(`Description: ${tool.description}`); - console.log(`Parameters schema:`, tool.inputSchema); + console.log(`Input schema:`, tool.inputSchema); console.log(`Output schema:`, tool.outputSchema); } From cc2ec9916f422d89d0879da030e040be2c3f3d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?= Date: Mon, 26 Oct 2026 16:00:06 +0100 Subject: [PATCH 7/7] Clarify when tool schemas are the empty string --- index.bs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.bs b/index.bs index d03f653..669d4d1 100644 --- a/index.bs +++ b/index.bs @@ -174,6 +174,13 @@ A tool definition is a [=struct=] with the following [=struct/items=] or a JSON string representing a JSON Schema object [[!JSON-SCHEMA]], as returned by the [=serialize a tool schema=] algorithm. + Note: The [=tool definition/input schema=] and [=tool definition/output schema=] are only the + empty string when the corresponding {{ModelContextTool/inputSchema}} or + {{ModelContextTool/outputSchema}} member is omitted or explicitly set to undefined. Because + these members are of type {{object}}, a non-object value such as + "inputSchema: """ does not produce the empty string; instead it causes + {{ModelContext/registerTool()}} to throw a {{TypeError}} during dictionary conversion. + : execute steps :: an algorithm that takes a {{Document}} targetDocument, a [=string=] inputArguments, an algorithm completionSteps that takes a