@kaoruisaac/pedelec 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,7 @@ A web application can use Pedelec to:
27
27
  - open an application-managed workspace with the native directory picker or an explicit path;
28
28
  - send user instructions and receive streamed assistant text;
29
29
  - expose narrowly scoped browser-side tools to the agent;
30
+ - register those same tools with WebMCP when the browser provides `document.modelContext`;
30
31
  - resume or end sessions; and
31
32
  - upload and list completed workspace assets; and
32
33
  - show connection, approval, provider, and lifecycle state in the UI.
@@ -204,6 +205,47 @@ await session.sendText("Please help me analyze the current page state");
204
205
 
205
206
  `sendText()` resolves after Core reports the matching semantic operation as completed. An `idle` status or bridge request response alone does not complete it. If the session is already handling a previous prompt, the new `sendText()` call is rejected to prevent multiple concurrent requests from running in the same session.
206
207
 
208
+ ## Share App Tools with WebMCP
209
+
210
+ The same `defineTool()` definitions can be registered for a browser WebMCP agent. `webmcp()` does not start a Pedelec Session and does not require the Extension or Desktop. Pedelec binds the tools when `document.modelContext.registerTool` is available; otherwise `available` is `false`.
211
+
212
+ ```ts
213
+ import { Pedelec, defineTool } from "@kaoruisaac/pedelec";
214
+
215
+ const pedelec = new Pedelec();
216
+
217
+ const getCurrentPage = defineTool({
218
+ name: "get_current_page",
219
+ description: "Read the current browser page title and URL.",
220
+ argsSchema: {
221
+ type: "object",
222
+ properties: {},
223
+ required: [],
224
+ },
225
+ handler: async () => ({
226
+ title: document.title,
227
+ url: location.href,
228
+ }),
229
+ });
230
+
231
+ const tools = [getCurrentPage] as const;
232
+
233
+ const webMcpBinding = await pedelec.webmcp({
234
+ skills: { tools },
235
+ });
236
+
237
+ const session = await pedelec.createSession({
238
+ skills: {
239
+ guidance: "Use browser tools when useful.",
240
+ tools,
241
+ },
242
+ });
243
+
244
+ webMcpBinding.dispose();
245
+ ```
246
+
247
+ A handler shared by Session and WebMCP should depend only on `args` and ordinary application or browser state. WebMCP execution does not receive a Pedelec `ToolCallContext`.
248
+
207
249
  ## End and resume a session
208
250
 
209
251
  ```ts
package/dist/index.d.ts CHANGED
@@ -101,6 +101,16 @@ export type SkillsInput<TTools extends readonly ToolDefinition[] = readonly Tool
101
101
  tools: TTools;
102
102
  denoModules?: readonly AgentDenoModuleDefinition[];
103
103
  };
104
+ export type WebMcpSkillsInput<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
105
+ tools: TTools;
106
+ };
107
+ export type WebMcpInput<TTools extends readonly ToolDefinition[] = readonly ToolDefinition[]> = {
108
+ skills: WebMcpSkillsInput<TTools>;
109
+ };
110
+ export type WebMcpBinding = {
111
+ available: boolean;
112
+ dispose(): void;
113
+ };
104
114
  export type ToolNameOf<TTools extends readonly ToolDefinition[]> = Extract<TTools[number]["name"], string>;
105
115
  export type SerializableToolManifest = {
106
116
  name: string;
@@ -388,6 +398,7 @@ export declare class Pedelec {
388
398
  constructor(options?: PedelecOptions);
389
399
  createSession(): Promise<PedelecSession<string>>;
390
400
  createSession<const TTools extends readonly ToolDefinition[]>(input: CreateSessionInput<TTools>): Promise<PedelecSession<ToolNameOf<TTools>>>;
401
+ webmcp<const TTools extends readonly ToolDefinition[]>(input: WebMcpInput<TTools>): Promise<WebMcpBinding>;
391
402
  /** @internal */
392
403
  createSessionInternal<const TTools extends readonly ToolDefinition[]>(input?: CreateSessionInput<TTools>, workspace?: PedelecWorkspace): Promise<PedelecSession<ToolNameOf<TTools>>>;
393
404
  private uploadDenoModuleArtifact;
package/dist/index.js CHANGED
@@ -145,6 +145,7 @@ function downloadBrowserFile(client, request) {
145
145
  }
146
146
  const MAX_ASSET_UPLOAD_BYTES = 100 * 1024 * 1024;
147
147
  const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_.-]*$/;
148
+ const WEBMCP_TOOL_NAME_MAX_LENGTH = 128;
148
149
  export function defineTool(tool) {
149
150
  return tool;
150
151
  }
@@ -191,26 +192,13 @@ function normalizeWorkspaceDenoModules(value) {
191
192
  };
192
193
  });
193
194
  }
194
- function normalizeSkillsInput(value) {
195
- const handlers = new Map();
196
- if (value === undefined)
197
- return { handlers, denoModules: [] };
198
- if (!value || typeof value !== "object" || Array.isArray(value)) {
199
- throw makeError("INVALID_INPUT", "skills must be an object");
200
- }
201
- const skills = value;
202
- if (typeof skills.guidance !== "string") {
203
- throw makeError("INVALID_INPUT", "skills.guidance must be a string");
204
- }
205
- if (!Array.isArray(skills.tools)) {
195
+ function normalizeToolDefinitions(value, options = {}) {
196
+ if (!Array.isArray(value)) {
206
197
  throw makeError("INVALID_INPUT", "skills.tools must be an array");
207
198
  }
208
- if (skills.denoModules !== undefined && !Array.isArray(skills.denoModules)) {
209
- throw makeError("INVALID_INPUT", "skills.denoModules must be an array");
210
- }
199
+ const handlers = new Map();
211
200
  const seen = new Set();
212
- const seenDenoModuleNames = new Set();
213
- const tools = skills.tools.map((tool, index) => {
201
+ const tools = value.map((tool, index) => {
214
202
  if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
215
203
  throw makeError("INVALID_INPUT", "skills.tools entries must be objects", { index });
216
204
  }
@@ -218,6 +206,12 @@ function normalizeSkillsInput(value) {
218
206
  if (typeof rawTool.name !== "string" || !TOOL_NAME_PATTERN.test(rawTool.name)) {
219
207
  throw makeError("INVALID_INPUT", "tool name is invalid", { index, toolName: rawTool.name });
220
208
  }
209
+ if (options.enforceWebMcpNameLimit && rawTool.name.length > WEBMCP_TOOL_NAME_MAX_LENGTH) {
210
+ throw makeError("INVALID_INPUT", "WebMCP tool name must be at most 128 characters", {
211
+ index,
212
+ toolName: rawTool.name,
213
+ });
214
+ }
221
215
  if (seen.has(rawTool.name)) {
222
216
  throw makeError("INVALID_INPUT", "duplicate tool name", { toolName: rawTool.name });
223
217
  }
@@ -236,6 +230,11 @@ function normalizeSkillsInput(value) {
236
230
  if (rawTool.handler !== undefined && typeof rawTool.handler !== "function") {
237
231
  throw makeError("INVALID_INPUT", "tool handler must be a function", { toolName: rawTool.name });
238
232
  }
233
+ if (options.requireHandler && typeof rawTool.handler !== "function") {
234
+ throw makeError("INVALID_INPUT", "WebMCP tool handler must be a function", {
235
+ toolName: rawTool.name,
236
+ });
237
+ }
239
238
  if (rawTool.input !== undefined) {
240
239
  throw makeError("INVALID_INPUT", "tool input is no longer supported; use argsSchema", {
241
240
  toolName: rawTool.name,
@@ -251,6 +250,26 @@ function normalizeSkillsInput(value) {
251
250
  ...(rawTool.timeoutMs === undefined ? {} : { timeoutMs: rawTool.timeoutMs }),
252
251
  };
253
252
  });
253
+ return { tools, handlers };
254
+ }
255
+ function normalizeSkillsInput(value) {
256
+ if (value === undefined)
257
+ return { handlers: new Map(), denoModules: [] };
258
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
259
+ throw makeError("INVALID_INPUT", "skills must be an object");
260
+ }
261
+ const skills = value;
262
+ if (typeof skills.guidance !== "string") {
263
+ throw makeError("INVALID_INPUT", "skills.guidance must be a string");
264
+ }
265
+ if (!Array.isArray(skills.tools)) {
266
+ throw makeError("INVALID_INPUT", "skills.tools must be an array");
267
+ }
268
+ if (skills.denoModules !== undefined && !Array.isArray(skills.denoModules)) {
269
+ throw makeError("INVALID_INPUT", "skills.denoModules must be an array");
270
+ }
271
+ const { tools, handlers } = normalizeToolDefinitions(skills.tools);
272
+ const seenDenoModuleNames = new Set();
254
273
  const denoModules = (skills.denoModules ?? []).map((module, index) => {
255
274
  if (!module || typeof module !== "object" || Array.isArray(module)) {
256
275
  throw makeError("INVALID_INPUT", "skills.denoModules entries must be objects", { index });
@@ -342,6 +361,43 @@ function normalizeToolArgsSchema(argsSchema, toolName) {
342
361
  });
343
362
  }
344
363
  }
364
+ function normalizeWebMcpInput(value) {
365
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
366
+ throw makeError("INVALID_INPUT", "input must be an object");
367
+ }
368
+ const input = value;
369
+ if (!input.skills || typeof input.skills !== "object" || Array.isArray(input.skills)) {
370
+ throw makeError("INVALID_INPUT", "skills must be an object");
371
+ }
372
+ return normalizeToolDefinitions(input.skills.tools, {
373
+ requireHandler: true,
374
+ enforceWebMcpNameLimit: true,
375
+ });
376
+ }
377
+ function webMcpModelContext(pageWindow) {
378
+ const document = pageWindow?.document;
379
+ const modelContext = document?.modelContext;
380
+ if (!modelContext || typeof modelContext.registerTool !== "function")
381
+ return null;
382
+ return modelContext;
383
+ }
384
+ function unavailableWebMcpBinding() {
385
+ return {
386
+ available: false,
387
+ dispose() { },
388
+ };
389
+ }
390
+ function invokeWebMcpHandler(handler, args) {
391
+ // WebMCP has no Pedelec session. Call the shared handler with args only.
392
+ return handler(args);
393
+ }
394
+ function webMcpRegistrationError(error, toolName) {
395
+ const normalized = normalizeError(error, "WEBMCP_REGISTRATION_FAILED", "WebMCP tool registration failed");
396
+ if (normalized.code === "WEBMCP_REGISTRATION_FAILED" && normalized.details === undefined && toolName) {
397
+ return makeError(normalized.code, normalized.message, { toolName });
398
+ }
399
+ return normalized;
400
+ }
345
401
  export class PedelecWorkspace {
346
402
  client;
347
403
  workspaceId;
@@ -498,6 +554,41 @@ export class Pedelec {
498
554
  async createSession(input = {}) {
499
555
  return this.createSessionInternal(input);
500
556
  }
557
+ async webmcp(input) {
558
+ const normalized = normalizeWebMcpInput(input);
559
+ const modelContext = webMcpModelContext(this.pageWindow);
560
+ if (!modelContext)
561
+ return unavailableWebMcpBinding();
562
+ const controller = new AbortController();
563
+ let registeringToolName;
564
+ try {
565
+ for (const tool of normalized.tools) {
566
+ registeringToolName = tool.name;
567
+ const handler = normalized.handlers.get(tool.name);
568
+ if (!handler) {
569
+ throw makeError("INVALID_INPUT", "WebMCP tool handler must be a function", {
570
+ toolName: tool.name,
571
+ });
572
+ }
573
+ await modelContext.registerTool({
574
+ name: tool.name,
575
+ description: tool.description,
576
+ inputSchema: tool.argsSchema,
577
+ execute: async (args) => invokeWebMcpHandler(handler, args),
578
+ }, { signal: controller.signal });
579
+ }
580
+ }
581
+ catch (error) {
582
+ controller.abort();
583
+ throw webMcpRegistrationError(error, registeringToolName);
584
+ }
585
+ return {
586
+ available: true,
587
+ dispose() {
588
+ controller.abort();
589
+ },
590
+ };
591
+ }
501
592
  /** @internal */
502
593
  async createSessionInternal(input = {}, workspace) {
503
594
  const resolvedOrPromise = this.resolveCreateSessionInput(input);
@@ -265,6 +265,50 @@ async function workspaceFileTransferTypes() {
265
265
  void parsed;
266
266
  void downloaded;
267
267
  }
268
+ async function sharedAppToolsBindToWebMcpAndSession() {
269
+ const pedelec = new Pedelec();
270
+ const sharedTool = defineTool({
271
+ name: "get_selection",
272
+ description: "Get selected text.",
273
+ argsSchema: {
274
+ type: "object",
275
+ properties: {},
276
+ required: [],
277
+ },
278
+ handler: async () => ({ text: "..." }),
279
+ });
280
+ const binding = await pedelec.webmcp({
281
+ skills: {
282
+ tools: [sharedTool],
283
+ },
284
+ });
285
+ binding.available;
286
+ binding.dispose();
287
+ const tools = [sharedTool];
288
+ await pedelec.webmcp({
289
+ skills: { tools },
290
+ });
291
+ await pedelec.createSession({
292
+ skills: {
293
+ guidance: "Use available browser tools.",
294
+ tools,
295
+ },
296
+ });
297
+ await pedelec.webmcp({
298
+ skills: {
299
+ tools,
300
+ // @ts-expect-error WebMCP skills do not include guidance
301
+ guidance: "not part of WebMCP",
302
+ },
303
+ });
304
+ await pedelec.webmcp({
305
+ skills: {
306
+ tools,
307
+ // @ts-expect-error WebMCP skills do not include denoModules
308
+ denoModules: [spriteTools],
309
+ },
310
+ });
311
+ }
268
312
  function directoryPickerIsRemoved() {
269
313
  const pedelec = new Pedelec();
270
314
  // @ts-expect-error directoryPicker was removed in favor of openWorkspace
@@ -292,6 +336,7 @@ void listAssetsHasPublicTypes;
292
336
  void availabilityHasPublicType;
293
337
  void workspacePublicTypeContract;
294
338
  void workspaceFileTransferTypes;
339
+ void sharedAppToolsBindToWebMcpAndSession;
295
340
  void directoryPickerIsRemoved;
296
341
  void publicSecurityTypesAreRestricted;
297
342
  const baseContext = {
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "0.4.8";
1
+ export declare const SDK_VERSION = "0.4.10";
@@ -1,2 +1,2 @@
1
1
  // Generated from sdk/package.json. Do not edit manually.
2
- export const SDK_VERSION = "0.4.8";
2
+ export const SDK_VERSION = "0.4.10";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",