diff --git a/README.md b/README.md index 8049f51..f3e0cb6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,9 @@ -# Typescript Type Definitions for WebMCP +# TypeScript definitions for WebMCP -This package defines Typescript types (`.d.ts`) for the [WebMCP specification](https://webmachinelearning.github.io/webmcp). +This package augments TypeScript's DOM types with definitions for the [WebMCP specification](https://webmachinelearning.github.io/webmcp/). +It also adds `agentInvoked` and `respondWith()` to `SubmitEvent` from the [declarative API explainer](https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md), which the draft does not specify yet. -Use this package to augment the ambient [`"dom"`](https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options) type definitions with the new definitions for WebMCP. - -## What are declaration files? - -See the [TypeScript handbook](http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html). - - -## How can I use them? - -### Install +## Install - npm: `npm install --save-dev webmcp-types` - yarn: `yarn add --dev webmcp-types` @@ -19,61 +11,31 @@ See the [TypeScript handbook](http://www.typescriptlang.org/docs/handbook/declar This package requires TypeScript 5.0 or newer. -### Configure +## Configure -Since this package is outside DefinitelyTyped, the dependency won't be picked up automatically. -There are several ways to add a additional TypeScript type definition dependencies to your TypeScript project: +Add `"webmcp-types"` to [`compilerOptions.types`](https://www.typescriptlang.org/tsconfig/types.html) in your `tsconfig.json`, keeping any entries you already have: -#### TypeScript `tsc` and `tsc`-based bundlers - -In `tsconfig.json`: - -```js +```json { - // ... "compilerOptions": { - // ... "types": ["webmcp-types"] } } ``` -Or you can use `typeRoots`: - -```js -{ - // ... - "compilerOptions": { - // ... - "typeRoots": ["./node_modules/webmcp-types", "./node_modules/@types"] - } -} -``` - -#### Inline in TypeScript - -This may work better if your toolchain doesn't read `tsconfig.json`. +Alternatively, add a [type reference](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of a `.d.ts` file included in your project: ```ts /// ``` -#### Webpack - -If you use Webpack and the options above aren't sufficient (this has not been verified), -you may need the following in `webpack.config.js`: - -```js -"types": ["webmcp-types"] -``` - -### Run the type tests +## Run the type tests - `npm install` - `npm test` The tests in `index.test-d.ts` are statically checked with [Vitest typecheck mode](https://vitest.dev/guide/testing-types) against `tsconfig.json`; they are never executed. -### Publish a new npm package version +## Publish [Release Please](https://github.com/googleapis/release-please) keeps a release pull request open with the next version and changelog, based on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) on `main`. Merging it creates the GitHub release and publishes to npm through [trusted publishing](https://docs.npmjs.com/trusted-publishers/); no npm token or local publish step is needed. diff --git a/index.d.ts b/index.d.ts index b743902..476d60c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,11 +1,10 @@ -export {}; - type IsUnion = T extends TWhole ? [TWhole] extends [T] ? false : true : never; type NonUnionTupleElements = { [TIndex in keyof TTuple]: true extends IsUnion ? never : TTuple[TIndex]; }[number]; type Simplify = { [TKey in keyof T]: T[TKey] } & {}; +// Only literal names in a literal tuple are required; widened or union shapes are runtime choices. type JsonSchemaRequiredKeys = TSchema extends { readonly required: infer TRequired extends readonly string[]; } ? number extends TRequired["length"] @@ -283,6 +282,7 @@ interface Document { interface ToolActivatedEventInit extends EventInit { /** * The name of the tool whose execution has started. + * @default "" */ toolName?: string; } @@ -305,6 +305,7 @@ var ToolActivatedEvent: { interface ToolCancelEventInit extends EventInit { /** * The name of the tool whose execution was cancelled. + * @default "" */ toolName?: string; } @@ -323,6 +324,24 @@ var ToolCancelEvent: { prototype: ToolCancelEvent; new(type: string, eventInitDict?: ToolCancelEventInit): ToolCancelEvent; }; + +interface SubmitEvent { + /** + * Whether an agent caused this submission by invoking the form's declarative tool. + * Absent in browsers without declarative tools, so check it before calling `respondWith()`. + * + * @see https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md#events + */ + readonly agentInvoked: boolean; + /** + * Responds to the agent that invoked the form's declarative tool instead of letting the form navigate. + * Call during submit event dispatch, after calling `preventDefault()`. + * + * @param agentResponse A promise that resolves to the response the agent will consume. + * @see https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md#events + */ + respondWith(agentResponse: PromiseLike): void; +} } export type { WebMCP }; diff --git a/index.test-d.ts b/index.test-d.ts index 56d0505..32dd057 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -194,7 +194,8 @@ test('supports the toolchange, toolactivated, and toolcancel events', () => { expectTypeOf(event).toEqualTypeOf(); }); - document.modelContext?.addEventListener('toolactivated', (event) => { + document.modelContext?.addEventListener('toolactivated', function (event) { + expectTypeOf(this).toEqualTypeOf(); expectTypeOf(event).toEqualTypeOf(); expectTypeOf(event.toolName).toEqualTypeOf(); }); @@ -203,12 +204,14 @@ test('supports the toolchange, toolactivated, and toolcancel events', () => { expectTypeOf(event).toEqualTypeOf(); expectTypeOf(event.toolName).toEqualTypeOf(); }); + window.addEventListener('toolactivated', (event) => expectTypeOf(event).toEqualTypeOf()); if (document.modelContext) { document.modelContext.ontoolchange = (event) => { expectTypeOf(event).toEqualTypeOf(); }; - document.modelContext.ontoolactivated = (event) => { + document.modelContext.ontoolactivated = function (event) { + expectTypeOf(this).toEqualTypeOf(); expectTypeOf(event).toEqualTypeOf(); }; document.modelContext.ontoolcancel = (event) => { @@ -217,12 +220,29 @@ test('supports the toolchange, toolactivated, and toolcancel events', () => { document.modelContext.ontoolchange = null; document.modelContext.ontoolactivated = null; document.modelContext.ontoolcancel = null; + // @ts-expect-error toolchange dispatches a plain Event. + document.modelContext.ontoolchange = (event: ToolActivatedEvent) => event.toolName; } - expectTypeOf(new ToolActivatedEvent('toolactivated')).toEqualTypeOf(); + const activated = new ToolActivatedEvent('toolactivated'); + expectTypeOf(activated).toEqualTypeOf(); expectTypeOf(new ToolActivatedEvent('toolactivated', { toolName: 'search', bubbles: true })) .toEqualTypeOf(); - expectTypeOf(new ToolCancelEvent('toolcancel', { toolName: 'search' })).toEqualTypeOf(); + expectTypeOf(new ToolCancelEvent('toolcancel', { toolName: 'search', bubbles: true })).toEqualTypeOf(); + + expectTypeOf<{}>().toExtend(); + expectTypeOf<{}>().toExtend(); + const event = new Event('toolactivated'); + if (event instanceof ToolActivatedEvent) { + expectTypeOf(event).toEqualTypeOf(); + } + + // @ts-expect-error The event type is required. + new ToolActivatedEvent(); + // @ts-expect-error toolName is a string. + new ToolCancelEvent('toolcancel', { toolName: 1 }); + // @ts-expect-error toolName is read-only. + activated.toolName = 'other'; expectTypeOf().toExtend(); expectTypeOf().toExtend(); @@ -233,3 +253,27 @@ test('supports the toolchange, toolactivated, and toolcancel events', () => { expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); + +test("adds the declarative explainer's SubmitEvent members", () => { + document.forms[0].addEventListener('submit', (event) => { + expectTypeOf(event.agentInvoked).toEqualTypeOf(); + expectTypeOf(event.respondWith).parameters + .toEqualTypeOf<[agentResponse: PromiseLike]>(); + expectTypeOf(event.respondWith).returns.toEqualTypeOf(); + + if (event.agentInvoked) { + event.preventDefault(); + event.respondWith(fetch('/search').then((response) => response.json())); + } + + // @ts-expect-error agentInvoked is read-only. + event.agentInvoked = true; + // @ts-expect-error respondWith() requires a response. + event.respondWith(); + // @ts-expect-error Pass the response promise, not a function that creates it. + event.respondWith(async () => 'done'); + }); + + // @ts-expect-error SubmitEventInit has no agentInvoked member; only Chromium adds one. + new SubmitEvent('submit', { agentInvoked: true }); +}); diff --git a/package.json b/package.json index f720de0..957f2ec 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "homepage": "https://github.com/webmachinelearning/webmcp-types", "bugs": "https://github.com/webmachinelearning/webmcp-types/issues", "description": "TypeScript type definitions for WebMCP", - "main": "", "types": "index.d.ts", "files": [ "index.d.ts"