Skip to content

Tools API

function defineTool<
TArgs = unknown,
TResult = unknown,
const TName extends string = string,
>(
tool: ToolDefinition<TArgs, TResult, TName>,
): ToolDefinition<TArgs, TResult, TName>

Returns the provided definition unchanged while preserving TypeScript generics and literal name types.

const updateCounter = defineTool<
{ delta: number },
{ value: number }
>({
name: "update_counter",
description: "Update the visible counter by delta.",
argsSchema: {
type: "object",
properties: {
delta: { type: "integer" },
},
required: ["delta"],
},
handler: ({ delta }) => ({ value: counter.add(delta) }),
});

defineTool() itself does not perform validation. createSession() and pedelec.webmcp() normalize and validate the tool list.

For usage and the mental model, see Runtime Modules. This page retains the exact declaration and preparation contract.

Vite applications can describe a local or package-backed Runtime module with one authoring shape. A minimal declaration can be used by workspace.run():

import { defineRuntimeModule } from "@kaoruisaac/pedelec";
const sceneTools = defineRuntimeModule({
name: "scene-tools",
entry: "./deno/scene-tools.ts",
});
const result = await workspace.run(
`import { buildScene } from "scene-tools";\nawait buildScene();`,
{ runtimeModules: [sceneTools] },
);

The same declaration can include Agent metadata when it is also used by skills.runtimeModules:

import { defineRuntimeModule } from "@kaoruisaac/pedelec";
const spriteTools = defineRuntimeModule({
name: "sprite-tools",
description: "Sprite authoring and preview utilities.",
entry: "./agent/sprite-tools.ts",
usage: `import { previewActorSource } from "sprite-tools";`,
preferStdinExecution: true,
});
const skills = {
guidance: "Use the module when sprite preview is needed.",
tools: [],
runtimeModules: [spriteTools],
};

defineRuntimeModule() preserves the concrete declaration shape, including whether optional metadata was supplied. pedelecVitePlugin() resolves the entry, bundles a self-contained ESM runtime, and rolls up a standalone public declaration artifact during vite build and vite dev. createSession() transfers each prepared artifact and Core materializes an immutable, thread-scoped package plus import map before the session is returned. workspace.run() uses the same prepared artifact format but caches packages per Workspace and authorized SDK origin, then creates an exact per-run import map for only the requested names.

Root SDK consumers that never import @kaoruisaac/pedelec/vite do not need Vite. The Vite peer is optional. Applications that use the plugin must install a supported Vite 8 release:

Terminal window
npm install @kaoruisaac/pedelec vite
vite.config.ts
import { defineConfig } from "vite";
import { pedelecVitePlugin } from "@kaoruisaac/pedelec/vite";
export default defineConfig({
plugins: [pedelecVitePlugin()],
});

The entry must be a static string. Relative entries resolve from the source file containing the declaration. Vite string aliases and installed package specifiers, including package.json exports subpaths, use the application’s Vite resolver for runtime bundling and TypeScript bundler resolution for public types. Remote URLs, computed values, inline source, and imported module objects are not supported. Regex-only Vite aliases that TypeScript paths cannot express are resolved through the Vite resolver for runtime code; declaration generation follows the same resolved files when Vite can locate them.

The plugin attaches a private, non-enumerable runtime/types artifact on the transformed module object and removes the original authoring entry path. It does not write into the application’s Pedelec workspace and does not mutate an already-created session.

Keep usage concise: it is the common-case agent hint, while the generated declaration bundle is the authoritative surface when an enum or option shape needs inspection.

The Agent imports the logical name from a JavaScript/TypeScript script run with pedelec-ts-runtime. It does not execute the module by its private filesystem path. Host Context lists the name, description, usage example, and a workspace-relative index.d.ts pointer for exact API discovery. The generated index.mjs is an implementation fallback, not the normal API reference.

preferStdinExecution is optional public authoring metadata for modules primarily used by short scripts. When true, Host Context exposes preferredExecution: "stdin"; the Agent should prefer the global runStdinCommand. The raw authoring flag is not exposed in Host Context, and provider-specific shell syntax is not part of the shared contract. This is a preference rather than a restriction: file-backed execution remains valid, and false or omission leaves preferredExecution absent. It does not affect bundling or artifact contents.

entry supports local relative files, Vite aliases, and installed bare/package exports specifiers. It is build-time only; Desktop does not resolve it and runtime npm, JSR, and network fetching are unavailable in Sandbox. Imported modules use the owning Thread mode: Sandbox by default, or explicitly authorized Full Access.

Ending and resuming a session keeps the same module snapshot while Core still knows the thread. pedelec.resumeSession(sessionId) reattaches to that snapshot; create a new session to use a rebuilt artifact. skills.tools remains a separate App Tool capability: its calls use pedelec-cli --thread-id ... tool-spec and tool-call, while Runtime Modules stay inside the pedelec-ts-runtime script.

type ToolDefinition<
TArgs = unknown,
TResult = unknown,
TName extends string = string,
> = {
name: TName;
description: string;
argsSchema: ToolArgsSchema;
timeoutMs?: number;
handler?: ToolSpecificHandler<TArgs, TResult>;
};
Field Required Description
name Yes Agent-facing name. Must start with a letter and contain only letters, digits, _, ., -.
description Yes Non-empty explanation of behavior, side effects, and selection criteria.
argsSchema Yes Pedelec Tool Args Schema. Root must be an object.
timeoutMs No Positive integer runtime wait limit defined by the tool, not overridable through agent tool arguments. Core default is currently 60 seconds.
handler No Inline browser handler retained by the SDK and not sent to Core. Required for pedelec.webmcp().

Names must be unique within a skills manifest.

type ToolSpecificHandler<TArgs = unknown, TResult = unknown> = (
args: TArgs,
ctx: ToolCallContext,
) => TResult | Promise<TResult>;

The handler executes in browser page context. Type parameters are compile-time only; validate runtime args.

Return values must be compatible with the bridge’s JSON data model.

type AgentModuleDefinition<TName extends string = string> =
RuntimeModuleDefinition<TName> & {
description: string;
usage: string;
};
type SkillsInput<
TTools extends readonly ToolDefinition[] = readonly ToolDefinition[],
> = {
guidance: string;
tools: TTools;
runtimeModules?: readonly AgentModuleDefinition[];
/** @deprecated Use runtimeModules. Do not provide both fields. */
denoModules?: readonly AgentModuleDefinition[];
};
const tools = [getCurrentPage, updateCounter] as const;
const skills = {
guidance: "Read current state before making changes.",
tools,
} satisfies SkillsInput<typeof tools>;

guidance must be a string and tools must be an array. runtimeModules is optional; Session/Agent modules require name, description, entry, and usage, with non-empty Agent metadata. A Workspace-only workspace.run() declaration requires only name and entry, plus its prepared Vite artifact. preferStdinExecution is an optional boolean preference and has no effect on Workspace execution. Session artifacts are transported during createSession(); Workspace artifacts are prepared on demand and reused while the Core runtime cache remains valid.

type ToolNameOf<TTools extends readonly ToolDefinition[]> = Extract<
TTools[number]["name"],
string
>;

Extracts the union of tool names from a readonly definitions array.

const tools = [readPage, updateCounter] as const;
type AppToolName = ToolNameOf<typeof tools>;
// "get_current_page" | "update_counter"

createSession() uses this type to narrow named session.onTool() calls.

type SerializableToolManifest = {
name: string;
description: string;
argsSchema: ToolArgsSchema;
timeoutMs?: number;
};

This is the tool shape sent to Core. It deliberately excludes handler.

type SerializableSkillsManifest = {
guidance: string;
tools: SerializableToolManifest[];
denoModules?: Array<{
name: string;
description: string;
usage: string;
preferStdinExecution?: boolean;
}>;
};

This is the normalized skills metadata sent through the bridge. Prepared runtime and declaration sources are transferred through the internal session setup path; entry is not sent to Core.

During createSession():

  1. tool entries are checked;
  2. argsSchema is cloned through JSON serialization;
  3. inline handlers are stored in a browser-side map by name;
  4. serializable definitions are sent to Core.

A duplicate tool name rejects with INVALID_INPUT.

Legacy input shorthand is rejected. Use argsSchema.

For an incoming tool call:

  1. named handler registered on the session;
  2. inline handler from the original tool definition;
  3. generic fallback handler;
  4. SDK-created TOOL_HANDLER_NOT_FOUND result.

Handler exceptions become TOOL_HANDLER_ERROR results. Failure to deliver the result emits SUBMIT_TOOL_RESULT_FAILED.

async webmcp<const TTools extends readonly ToolDefinition[]>(
input: WebMcpInput<TTools>,
): Promise<WebMcpBinding>

Registers existing App Tool definitions with the browser WebMCP imperative API, document.modelContext.registerTool, when that function exists. This does not create a Pedelec Session and does not call the Extension, Desktop, Provider, or Core.

type WebMcpSkillsInput<
TTools extends readonly ToolDefinition[] = readonly ToolDefinition[],
> = {
tools: TTools;
};
type WebMcpInput<
TTools extends readonly ToolDefinition[] = readonly ToolDefinition[],
> = {
skills: WebMcpSkillsInput<TTools>;
};
type WebMcpBinding = {
available: boolean;
dispose(): void;
};

skills.tools is the only skills field. guidance and runtimeModules are Session capabilities and are not part of this input. Every tool passed to webmcp() must include an inline handler. There is no session.onTool() step for this binding.

const tools = [getCurrentPage] as const;
const binding = await pedelec.webmcp({
skills: { tools },
});
if (binding.available) {
// A browser WebMCP client can call the registered tools until dispose().
}
binding.dispose();

The same tools array can also be passed to createSession().

App Tool WebMCP registration
name name
description description
normalized argsSchema inputSchema
inline handler execute

timeoutMs remains a Pedelec Session wait limit. WebMCP registration has no per-tool timeout, and this binding does not add one. Registration does not include title, annotations, exposedTo, handler, guidance, or runtimeModules.

Names keep the Pedelec rule: a letter, then letters, digits, _, ., or -. WebMCP also requires the name to be at most 128 characters. inputSchema is the same normalized argsSchema clone that createSession() would send. Pedelec does not add additionalProperties or rewrite required, enum, or oneOf.

webmcp() validates input first, then checks document.modelContext.registerTool. Invalid tools, duplicate names, an empty description, a name longer than 128 characters, or a missing inline handler still reject with INVALID_INPUT. A missing handler uses the message WebMCP tool handler must be a function and includes toolName in details.

If there is no window or document, or registerTool is not a function, the method resolves:

{ available: false, dispose() {} }

It does not throw for an unsupported browser, does not load a polyfill, and does not use navigator.modelContext as its registration API. Pedelec binds tools when document.modelContext.registerTool is available; otherwise available is false.

One webmcp() call creates one AbortController and passes that same signal to every registerTool. dispose() aborts the signal. It is synchronous, safe to call more than once, and does not end a Session or contact Core. Disposing one binding does not remove tools registered by another webmcp() call or by other page scripts.

If registration fails partway through the list, webmcp() aborts that shared signal and rejects. No binding is returned. The error code is WEBMCP_REGISTRATION_FAILED unless the failure is already a PedelecError, which is preserved. A name already registered by another script is left to registerTool(); Pedelec does not rename its tool or unregister someone else’s.

WebMCP execution does not have a Pedelec Session ToolCallContext. Shared inline handlers should be context-independent.

The adapter calls handler(args) and does not invent sessionId, provider, turnId, or toolRequestId. It also does not forward the WebMCP execution AbortSignal to the existing handler. A handler that reads ctx.sessionId still works for a Pedelec Session, but it is not a portable WebMCP handler.

The root alias:

type ToolArgsSchema = ToolArgsObjectSchema;

Node union:

type ToolArgsSchemaNode =
| ToolArgsStringSchema
| ToolArgsNumberSchema
| ToolArgsIntegerSchema
| ToolArgsBooleanSchema
| ToolArgsArraySchema
| ToolArgsObjectSchema
| ToolArgsOneOfSchema;

See Types for every field and Tool Args Schema for usage guidance.

DenoModuleDefinition, defineDenoModule() and denoModules remain usable and are marked @deprecated in TypeScript. Migrate to RuntimeModuleDefinition, defineRuntimeModule() and runtimeModules. Supply exactly one module field per skills/options object; supplying both rejects with INVALID_INPUT, even when either list is empty. The internal SerializableSkillsManifest.denoModules wire field remains unchanged.

// Existing integrations remain valid:
import { defineDenoModule, defineRuntimeModule, type DenoModuleDefinition } from "@kaoruisaac/pedelec";
const legacy: DenoModuleDefinition = defineDenoModule({ name: "project-tools", entry: "./agent/project-tools.ts" });
await workspace.run('import "project-tools";', { denoModules: [legacy] });
// New integrations:
const modern = defineRuntimeModule({ name: "project-tools", entry: "./agent/project-tools.ts" });
await workspace.run('import "project-tools";', { runtimeModules: [modern] });