Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ you may need the following in `webpack.config.js`:
"types": ["webmcp-types"]
```

### 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

(only for people who have npm publish access)
Expand Down
11 changes: 7 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ declare namespace WebMCP {

/**
* Callback for tool execution.
* @template T The type of the input object.
* @param inputObject The input parameters for the tool, as defined by its inputSchema.
* @param options Options passed when executing the tool.
* @returns A promise that resolves with the tool's output.
*/
type ToolExecuteCallback<T extends Record<string, unknown> = Record<string, unknown>> = (inputObject: T, options: ToolExecuteCallbackOptions) => MaybePromise<unknown>;
type ToolExecuteCallback<T extends object = Record<string, unknown>> = (inputObject: T, options: ToolExecuteCallbackOptions) => MaybePromise<unknown>;

/**
* Metadata about a tool's behavior.
Expand All @@ -43,8 +44,10 @@ declare namespace WebMCP {

/**
* Describes a tool to be registered with the model context.
* @template T The type of the input object passed to the tool's execute callback.
* TypeScript does not check this type against inputSchema.
*/
interface ModelContextTool {
interface ModelContextTool<T extends object = Record<string, unknown>> {
/**
* The name of the tool. Must be 1-128 characters, ASCII alphanumeric, '_', '-', or '.'.
*/
Expand All @@ -64,7 +67,7 @@ declare namespace WebMCP {
/**
* The function to execute when the tool is called.
*/
execute: ToolExecuteCallback;
execute: ToolExecuteCallback<T>;
/**
* Metadata about the tool's behavior.
*/
Expand Down Expand Up @@ -143,7 +146,7 @@ declare namespace WebMCP {
* @param tool The tool definition.
* @param options Registration options.
*/
registerTool(tool: ModelContextTool, options?: ModelContextRegisterToolOptions): Promise<void>;
registerTool<T extends object = Record<string, unknown>>(tool: ModelContextTool<T>, options?: ModelContextRegisterToolOptions): Promise<void>;
/**
* Returns a list of registered tools exposed to this document.
* @param options Filtering options.
Expand Down
117 changes: 117 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Type-level tests for the WebMCP declarations.
*
* These are statically checked by the TypeScript compiler through vitest's
* typecheck mode (`npm test`); they are never executed.
*/
import { expectTypeOf, test } from 'vitest';

test('the WebMCP global declarations are ambient', () => {
expectTypeOf<Pick<Document, 'modelContext'>>().toEqualTypeOf<{
readonly modelContext?: WebMCP.ModelContext;
}>();
});

test('execute receives the input object and ToolExecuteCallbackOptions', () => {
expectTypeOf<WebMCP.ToolExecuteCallback<{ text: string }>>()
.parameter(0)
.toEqualTypeOf<{ text: string }>();
expectTypeOf<WebMCP.ToolExecuteCallback<{ text: string }>>()
.parameter(1)
.toEqualTypeOf<WebMCP.ToolExecuteCallbackOptions>();
expectTypeOf<WebMCP.ToolExecuteCallbackOptions['signal']>().toEqualTypeOf<AbortSignal>();
});

test('registerTool accepts a handler with a narrowed input type', () => {
void document.modelContext?.registerTool({
name: 'add_todo',
description: 'Adds a todo item.',
inputSchema: { type: 'object', properties: { text: { type: 'string' } } },
execute: (input: { text: string }) => input.text,
});
});

test('registerTool accepts a method-shorthand handler', () => {
void document.modelContext?.registerTool({
name: 'add_todo',
description: 'Adds a todo item.',
execute(input: { text: string }) {
return input.text;
},
});
});

test('an interface input type satisfies the constraint', () => {
interface AddTodoInput {
text: string;
done?: boolean;
}
void document.modelContext?.registerTool<AddTodoInput>({
name: 'add_todo',
description: 'Adds a todo item.',
execute: (input) => {
expectTypeOf(input).toEqualTypeOf<AddTodoInput>();
return input.text;
},
});
});

test('a prebuilt typed callback is accepted by registerTool', () => {
const execute: WebMCP.ToolExecuteCallback<{ text: string }> = (input) => input.text;
void document.modelContext?.registerTool({
name: 'add_todo',
description: 'Adds a todo item.',
execute,
});
});

test('an explicit type argument threads through to the handler', () => {
const registration = document.modelContext?.registerTool<{ query: string }>({
name: 'search',
description: 'Searches the page.',
execute: (input) => {
expectTypeOf(input).toEqualTypeOf<{ query: string }>();
return input.query;
},
});
expectTypeOf(registration).toEqualTypeOf<Promise<void> | undefined>();
});

test('a mismatched handler is rejected', () => {
expectTypeOf<(input: { count: number }) => string>()
.not.toExtend<WebMCP.ToolExecuteCallback<{ query: string }>>();
expectTypeOf<(input: { query: string; count: number }) => string>()
.not.toExtend<WebMCP.ToolExecuteCallback<{ query: string }>>();
expectTypeOf<(input: Record<never, never>) => string>().toExtend<
WebMCP.ToolExecuteCallback<{ query: string }>
>();
});

test('untyped registrations keep the Record<string, unknown> default', () => {
expectTypeOf<WebMCP.ModelContextTool['execute']>()
.parameter(0)
.toEqualTypeOf<Record<string, unknown>>();
void document.modelContext?.registerTool({
name: 'echo',
description: 'Echoes its input.',
inputSchema: { type: 'object' },
execute: async (input) => {
expectTypeOf(input).toEqualTypeOf<Record<string, unknown>>();
return JSON.stringify(input);
},
});
});

test('tools typed without a type argument keep working', () => {
const tool: WebMCP.ModelContextTool = {
name: 'echo',
description: 'Echoes its input.',
execute: (input) => JSON.stringify(input),
};
const tools: WebMCP.ModelContextTool[] = [tool];
const registerAll = (context: WebMCP.ModelContext, list: WebMCP.ModelContextTool[]) =>
Promise.all(list.map((entry) => context.registerTool(entry)));
void document.modelContext?.registerTool(tool);
void tools;
void registerAll;
});
Loading