From cccb86b339db8191ce11e7577b1ec695455d70e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 20:48:24 +0000 Subject: [PATCH 1/2] Add Rails WebMCP example with Stimulus controllers Introduces a bookmarks management application demonstrating WebMCP integration with Ruby on Rails using Stimulus controllers: - Stimulus controller with 6 AI-callable tools (add, delete, update, list, search, stats) - Pure business logic separated into lib/bookmarks.ts - Sample ERB view with custom event handling for real-time updates - Reference Rails model and controller implementations - Compatible with Vite, importmaps, or esbuild bundlers - Follows Rails 7+ conventions with app/javascript structure Updates CI workflow matrix and documentation to include the new example. --- .github/workflows/ci.yml | 4 +- AGENTS.md | 14 +- README.md | 24 +- rails/.gitignore | 28 + rails/README.md | 294 ++++ rails/app/assets/stylesheets/application.css | 274 ++++ rails/app/controllers/bookmarks_controller.rb | 108 ++ rails/app/javascript/application.ts | 16 + .../bookmarks_webmcp_controller.ts | 390 +++++ rails/app/javascript/controllers/index.ts | 20 + rails/app/javascript/lib/bookmarks.ts | 159 ++ rails/app/javascript/lib/types.ts | 29 + rails/app/models/bookmark.rb | 55 + rails/app/views/bookmarks/index.html.erb | 156 ++ rails/index.html | 143 ++ rails/package.json | 21 + rails/pnpm-lock.yaml | 1387 +++++++++++++++++ rails/tsconfig.json | 24 + rails/vite.config.ts | 8 + 19 files changed, 3149 insertions(+), 5 deletions(-) create mode 100644 rails/.gitignore create mode 100644 rails/README.md create mode 100644 rails/app/assets/stylesheets/application.css create mode 100644 rails/app/controllers/bookmarks_controller.rb create mode 100644 rails/app/javascript/application.ts create mode 100644 rails/app/javascript/controllers/bookmarks_webmcp_controller.ts create mode 100644 rails/app/javascript/controllers/index.ts create mode 100644 rails/app/javascript/lib/bookmarks.ts create mode 100644 rails/app/javascript/lib/types.ts create mode 100644 rails/app/models/bookmark.rb create mode 100644 rails/app/views/bookmarks/index.html.erb create mode 100644 rails/index.html create mode 100644 rails/package.json create mode 100644 rails/pnpm-lock.yaml create mode 100644 rails/tsconfig.json create mode 100644 rails/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3477a47..d69189e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - example: [vanilla, react] + example: [vanilla, react, rails] steps: - name: Checkout code uses: actions/checkout@v4 @@ -52,7 +52,7 @@ jobs: needs: lint-and-typecheck strategy: matrix: - example: [vanilla, react] + example: [vanilla, react, rails] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/AGENTS.md b/AGENTS.md index f6ef875..cd5bd26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,12 @@ Welcome! This document helps you navigate the WebMCP Examples repository efficie - **Key file**: `react/src/App.tsx` - React component with `useWebMCP` hooks - **API used**: `useWebMCP()` hook +#### Rails + Stimulus +- **[rails/README.md](./rails/README.md)** - Bookmarks manager using Stimulus controllers +- **Location**: `/rails` +- **Key file**: `rails/app/javascript/controllers/bookmarks_webmcp_controller.ts` - Stimulus controller with WebMCP tools +- **API used**: `navigator.modelContext.registerTool()` in Stimulus + ### Legacy Examples (Deprecated - DO NOT USE) - **[relegated/README.md](./relegated/README.md)** - Old examples using deprecated MCP SDK - **Warning**: These use the legacy `@modelcontextprotocol/sdk` API @@ -40,7 +46,7 @@ Welcome! This document helps you navigate the WebMCP Examples repository efficie ```bash # Navigate to the example -cd vanilla # or react +cd vanilla # or react, rails # Install dependencies pnpm install @@ -55,6 +61,7 @@ pnpm dev 2. **Choose the right location**: - `/vanilla` for pure TypeScript/JavaScript - `/react` for React-based examples + - `/rails` for Rails with Stimulus examples 3. **Create self-contained directory** with: - `README.md` - Documentation - `package.json` - Dependencies @@ -152,6 +159,11 @@ example-name/ - Root: `react/src/App.tsx` - Config: `react/vite.config.ts` +**Rails Example:** +- Entry: `rails/app/javascript/application.ts` +- Controller: `rails/app/javascript/controllers/bookmarks_webmcp_controller.ts` +- Config: `rails/vite.config.ts` + ## WebMCP Package Documentation - **[@mcp-b/global](https://docs.mcp-b.ai/packages/global)** - Core WebMCP polyfill for vanilla JS diff --git a/README.md b/README.md index 9cdf46c..395aa9b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ git clone https://github.com/WebMCP-org/examples.git cd examples # Choose an example -cd vanilla # or react +cd vanilla # or react, rails # Install and run pnpm install @@ -88,6 +88,25 @@ A task management application showcasing React integration with the `useWebMCP() --- +### Rails Example + +**Location:** `/rails` + +A bookmarks management application demonstrating Rails 7+ integration with Stimulus controllers. + +**Features:** +- Uses `navigator.modelContext.registerTool()` with Stimulus controllers +- Follows Rails conventions (`app/javascript/controllers/`) +- Pure business logic separated into `lib/` modules +- Compatible with Vite, importmaps, or esbuild +- 6 AI-callable tools (bookmark CRUD operations + search + stats) + +**Tech:** Rails 7+, Stimulus, TypeScript, Vite, `@mcp-b/global` + +[→ Documentation](./rails/README.md) + +--- + ### Legacy Examples (Deprecated) **Location:** `/relegated` @@ -204,7 +223,7 @@ WebMCP enables AI assistants to interact with websites through APIs instead of s ```bash # Development (per example) -cd vanilla # or react +cd vanilla # or react, rails pnpm dev # Run development server pnpm build # Build for production pnpm preview # Preview production build @@ -223,6 +242,7 @@ pnpm preview # Preview production build ### Example Documentation - [Vanilla Example](./vanilla/README.md) - Vanilla JavaScript implementation - [React Example](./react/README.md) - React with hooks implementation +- [Rails Example](./rails/README.md) - Rails with Stimulus controllers - [Legacy Examples](./relegated/README.md) - Deprecated implementations ## Tech Stack diff --git a/rails/.gitignore b/rails/.gitignore new file mode 100644 index 0000000..bb4e24f --- /dev/null +++ b/rails/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Vite +*.local + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* + +# Editor +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store + +# Rails specific (for reference when integrating) +/tmp/ +/log/ +/storage/ diff --git a/rails/README.md b/rails/README.md new file mode 100644 index 0000000..1f598be --- /dev/null +++ b/rails/README.md @@ -0,0 +1,294 @@ +# Rails WebMCP Example + +A bookmarks management application demonstrating **WebMCP integration with Ruby on Rails** using Stimulus controllers. + +## What's New + +This example shows how to integrate WebMCP with Rails 7+ using the modern API: + +- Uses `navigator.modelContext.registerTool()` with Stimulus controllers +- Follows Rails conventions with `app/javascript/controllers/` structure +- Pure business logic separated into `lib/` modules +- Compatible with both Vite and Rails importmaps + +## Quick Start + +```bash +# Install dependencies +pnpm install + +# Run development server +pnpm dev +``` + +Then open your browser and install the [MCP-B extension](https://github.com/WebMCP-org/WebMCP) to interact with the tools. + +## Available Tools + +This example exposes 6 AI-callable tools: + +1. **add_bookmark** - Save new bookmarks with title, URL, description, and tags +2. **delete_bookmark** - Remove bookmarks by ID +3. **update_bookmark** - Edit existing bookmark properties +4. **list_bookmarks** - View all bookmarks (optionally filter by tag) +5. **search_bookmarks** - Find bookmarks by title, description, or URL +6. **get_bookmark_stats** - Get statistics about saved bookmarks + +## How It Works + +The integration uses Stimulus controllers to register WebMCP tools: + +```typescript +// app/javascript/controllers/bookmarks_webmcp_controller.ts +import { Controller } from '@hotwired/stimulus'; +import '@mcp-b/global'; + +export default class BookmarksWebmcpController extends Controller { + connect() { + navigator.modelContext.registerTool({ + name: 'add_bookmark', + description: 'Add a new bookmark', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'Bookmark title' }, + url: { type: 'string', description: 'URL to bookmark' }, + }, + required: ['title', 'url'], + }, + execute: async (args) => { + // Your logic here + return { + content: [{ type: 'text', text: 'Bookmark added!' }], + }; + }, + }); + } +} +``` + +Then in your view: + +```erb +
+ +
+``` + +## Rails Integration Guide + +### Option 1: With Vite (Recommended) + +If using [vite_rails](https://vite-ruby.netlify.app/guide/rails.html): + +1. Add dependencies: + ```bash + pnpm add @mcp-b/global @hotwired/stimulus + ``` + +2. Copy the `app/javascript/` directory structure to your Rails app + +3. Import in your application entry point: + ```typescript + // app/javascript/application.ts + import '@mcp-b/global'; + import './controllers'; + ``` + +### Option 2: With Importmaps + +If using Rails importmaps: + +1. Pin the packages: + ```bash + bin/importmap pin @mcp-b/global + ``` + +2. Create the Stimulus controller in JavaScript (not TypeScript): + ```javascript + // app/javascript/controllers/bookmarks_webmcp_controller.js + import { Controller } from "@hotwired/stimulus" + + export default class extends Controller { + connect() { + navigator.modelContext.registerTool({ + name: 'add_bookmark', + // ... tool configuration + }); + } + } + ``` + +3. Register in your manifest: + ```javascript + // app/javascript/controllers/index.js + import { application } from "controllers/application" + import BookmarksWebmcpController from "./bookmarks_webmcp_controller" + application.register("bookmarks-webmcp", BookmarksWebmcpController) + ``` + +### Option 3: With esbuild + +If using jsbundling-rails with esbuild: + +1. Add dependencies: + ```bash + yarn add @mcp-b/global @hotwired/stimulus + ``` + +2. Import in your entry point: + ```javascript + // app/javascript/application.js + import "@mcp-b/global"; + import "./controllers"; + ``` + +## Project Structure + +``` +rails/ +├── README.md +├── package.json +├── vite.config.ts +├── tsconfig.json +├── index.html # Demo entry point +├── app/ +│ ├── assets/ +│ │ └── stylesheets/ +│ │ └── application.css # Styles +│ └── javascript/ +│ ├── application.ts # JS entry point +│ ├── controllers/ +│ │ ├── index.ts # Controller registration +│ │ └── bookmarks_webmcp_controller.ts +│ └── lib/ +│ ├── bookmarks.ts # Pure business logic +│ └── types.ts # Type definitions +└── app/ + └── views/ + └── bookmarks/ + └── index.html.erb # Sample ERB template +``` + +## Key Patterns + +### 1. Separation of Concerns + +Business logic is in pure functions (`lib/bookmarks.ts`), making it testable and reusable: + +```typescript +// lib/bookmarks.ts +export function createBookmark(data) { + return { + id: crypto.randomUUID(), + ...data, + createdAt: new Date().toISOString(), + }; +} +``` + +### 2. Stimulus Value Binding + +Use Stimulus values to manage state: + +```typescript +export default class extends Controller { + static values = { + bookmarks: { type: Array, default: [] } + }; + + declare bookmarksValue: Bookmark[]; + + bookmarksValueChanged() { + // Re-render when bookmarks change + } +} +``` + +### 3. Custom Events for UI Updates + +Emit custom events to update the UI: + +```typescript +this.element.dispatchEvent( + new CustomEvent('bookmarks:updated', { + detail: { bookmarks: this.bookmarksValue }, + bubbles: true, + }) +); +``` + +### 4. Tool Cleanup + +Properly cleanup tools when the controller disconnects: + +```typescript +private toolCleanups: Array<() => void> = []; + +connect() { + const cleanup = navigator.modelContext.registerTool({...}); + this.toolCleanups.push(cleanup); +} + +disconnect() { + this.toolCleanups.forEach(cleanup => cleanup()); + this.toolCleanups = []; +} +``` + +## Sample Rails Files + +### Model (for reference) + +```ruby +# app/models/bookmark.rb +class Bookmark < ApplicationRecord + validates :title, presence: true + validates :url, presence: true, format: URI::DEFAULT_PARSER.make_regexp + + scope :by_tag, ->(tag) { where("? = ANY(tags)", tag) } + scope :search, ->(query) { + where("title ILIKE :q OR description ILIKE :q OR url ILIKE :q", q: "%#{query}%") + } +end +``` + +### Controller (for reference) + +```ruby +# app/controllers/bookmarks_controller.rb +class BookmarksController < ApplicationController + def index + @bookmarks = Bookmark.order(created_at: :desc) + end +end +``` + +### View (for reference) + +```erb +<%# app/views/bookmarks/index.html.erb %> +
+ +

My Bookmarks

+ +
+ <% @bookmarks.each do |bookmark| %> + <%= render bookmark %> + <% end %> +
+
+``` + +## Learn More + +- [WebMCP Documentation](https://docs.mcp-b.ai) +- [Rails Integration Guide](https://docs.mcp-b.ai/frameworks/rails) +- [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction) +- [Vite Ruby](https://vite-ruby.netlify.app/) + +## License + +MIT diff --git a/rails/app/assets/stylesheets/application.css b/rails/app/assets/stylesheets/application.css new file mode 100644 index 0000000..a630117 --- /dev/null +++ b/rails/app/assets/stylesheets/application.css @@ -0,0 +1,274 @@ +/** + * Styles for the Rails WebMCP Bookmarks example + */ + +:root { + --primary: #cc0000; + --primary-dark: #990000; + --bg: #f8f9fa; + --card-bg: #ffffff; + --text: #212529; + --text-muted: #6c757d; + --border: #dee2e6; + --success: #28a745; + --error: #dc3545; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; +} + +.container { + max-width: 900px; + margin: 0 auto; + padding: 2rem; +} + +.header { + text-align: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2.5rem; + color: var(--primary); + margin-bottom: 0.5rem; +} + +.subtitle { + color: var(--text-muted); + font-size: 1.1rem; +} + +.notification { + position: fixed; + top: 1rem; + right: 1rem; + padding: 1rem 1.5rem; + border-radius: 8px; + font-weight: 500; + z-index: 1000; + transition: opacity 0.3s ease; +} + +.notification.hidden { + opacity: 0; + pointer-events: none; +} + +.notification.success { + background: var(--success); + color: white; +} + +.notification.error { + background: var(--error); + color: white; +} + +.content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.info-section { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; +} + +.info-card, +.tools-card { + background: var(--card-bg); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.info-card h2, +.tools-card h2 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--primary); +} + +.info-card ul, +.tools-card ul { + list-style: none; + padding: 0; +} + +.info-card li, +.tools-card li { + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); +} + +.info-card li:last-child, +.tools-card li:last-child { + border-bottom: none; +} + +.tools-card code { + background: #f1f3f4; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', monospace; + font-size: 0.9rem; + color: var(--primary-dark); +} + +.stats-section { + background: var(--card-bg); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.stats-section h2 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--primary); +} + +.stats-grid { + display: flex; + gap: 2rem; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-value { + font-size: 2rem; + font-weight: bold; + color: var(--primary); +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-muted); +} + +.bookmarks-section { + background: var(--card-bg); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.bookmarks-section h2 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--primary); +} + +.bookmarks-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.empty-state { + text-align: center; + color: var(--text-muted); + padding: 2rem; +} + +.bookmark-item { + padding: 1rem; + border: 1px solid var(--border); + border-radius: 8px; + transition: box-shadow 0.2s ease; +} + +.bookmark-item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.bookmark-header { + margin-bottom: 0.25rem; +} + +.bookmark-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--primary); + text-decoration: none; +} + +.bookmark-title:hover { + text-decoration: underline; +} + +.bookmark-url { + font-size: 0.85rem; + color: var(--text-muted); + word-break: break-all; + margin-bottom: 0.5rem; +} + +.bookmark-description { + font-size: 0.95rem; + color: var(--text); + margin-bottom: 0.5rem; +} + +.bookmark-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag { + background: #f1f3f4; + color: var(--text-muted); + padding: 0.25rem 0.75rem; + border-radius: 99px; + font-size: 0.8rem; +} + +.footer { + text-align: center; + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid var(--border); + color: var(--text-muted); +} + +.footer a { + color: var(--primary); + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + +@media (max-width: 600px) { + .container { + padding: 1rem; + } + + .header h1 { + font-size: 1.75rem; + } + + .stats-grid { + justify-content: center; + } +} diff --git a/rails/app/controllers/bookmarks_controller.rb b/rails/app/controllers/bookmarks_controller.rb new file mode 100644 index 0000000..8a48b8a --- /dev/null +++ b/rails/app/controllers/bookmarks_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# +# Bookmarks controller for the Rails WebMCP example +# +# This is a reference implementation showing how to structure +# the Rails controller. The actual WebMCP tools are handled +# client-side via Stimulus controllers for this demo. +# +# In a production app, you might want server-side persistence +# and would use this controller for CRUD operations. +# +class BookmarksController < ApplicationController + before_action :set_bookmark, only: %i[show update destroy] + + # GET /bookmarks + # Displays the bookmarks index page with WebMCP integration + def index + @bookmarks = Bookmark.recent + + # Apply tag filter if provided + @bookmarks = @bookmarks.by_tag(params[:tag]) if params[:tag].present? + + # Apply search if provided + @bookmarks = @bookmarks.search(params[:q]) if params[:q].present? + + respond_to do |format| + format.html + format.json { render json: @bookmarks } + end + end + + # GET /bookmarks/:id + def show + respond_to do |format| + format.html + format.json { render json: @bookmark } + end + end + + # POST /bookmarks + def create + @bookmark = Bookmark.new(bookmark_params) + + if @bookmark.save + respond_to do |format| + format.html { redirect_to bookmarks_path, notice: "Bookmark created." } + format.json { render json: @bookmark, status: :created } + end + else + respond_to do |format| + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @bookmark.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /bookmarks/:id + def update + if @bookmark.update(bookmark_params) + respond_to do |format| + format.html { redirect_to bookmarks_path, notice: "Bookmark updated." } + format.json { render json: @bookmark } + end + else + respond_to do |format| + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @bookmark.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /bookmarks/:id + def destroy + @bookmark.destroy + + respond_to do |format| + format.html { redirect_to bookmarks_path, notice: "Bookmark deleted." } + format.json { head :no_content } + end + end + + # GET /bookmarks/stats + # Returns statistics about bookmarks + def stats + bookmarks = Bookmark.all + all_tags = bookmarks.flat_map(&:tags) + tag_counts = all_tags.tally.sort_by { |_, count| -count }.first(5) + + stats = { + total: bookmarks.count, + unique_tags: all_tags.uniq.count, + top_tags: tag_counts.map { |tag, count| { tag: tag, count: count } } + } + + render json: stats + end + + private + + def set_bookmark + @bookmark = Bookmark.find(params[:id]) + end + + def bookmark_params + params.require(:bookmark).permit(:title, :url, :description, tags: []) + end +end diff --git a/rails/app/javascript/application.ts b/rails/app/javascript/application.ts new file mode 100644 index 0000000..5f50236 --- /dev/null +++ b/rails/app/javascript/application.ts @@ -0,0 +1,16 @@ +/** + * Application entry point + * + * This is the main JavaScript entry point for the Rails application. + * It imports and initializes Stimulus controllers and the WebMCP polyfill. + * + * @see https://docs.mcp-b.ai/frameworks/rails + */ + +// Import the WebMCP polyfill to enable navigator.modelContext +import '@mcp-b/global'; + +// Import Stimulus application and controllers +import './controllers'; + +console.log('WebMCP Rails application loaded'); diff --git a/rails/app/javascript/controllers/bookmarks_webmcp_controller.ts b/rails/app/javascript/controllers/bookmarks_webmcp_controller.ts new file mode 100644 index 0000000..e3da4c5 --- /dev/null +++ b/rails/app/javascript/controllers/bookmarks_webmcp_controller.ts @@ -0,0 +1,390 @@ +/** + * Stimulus controller for WebMCP bookmark tools + * + * This controller registers WebMCP tools that allow AI assistants to interact + * with the bookmarks application. It integrates with the Rails Stimulus pattern + * while providing the same WebMCP capabilities as other framework integrations. + * + * @see https://docs.mcp-b.ai/frameworks/rails + * @see https://stimulus.hotwired.dev/ + */ + +import { Controller } from '@hotwired/stimulus'; +import '@mcp-b/global'; +import type { Bookmark } from '../lib/types'; +import { + createBookmark, + removeBookmark, + updateBookmark, + filterBookmarksByTag, + searchBookmarks, + calculateBookmarkStats, + formatBookmarkList, +} from '../lib/bookmarks'; + +/** + * Bookmarks WebMCP Controller + * + * Manages bookmark state and registers AI-accessible tools via WebMCP. + * Tools are registered when the controller connects and cleaned up on disconnect. + * + * @example + * ```html + *
+ *
+ * ``` + */ +export default class BookmarksWebmcpController extends Controller { + static values = { + bookmarks: { type: Array, default: [] }, + }; + + declare bookmarksValue: Bookmark[]; + + private toolCleanups: Array<{ unregister: () => void }> = []; + + /** + * Called when the controller is connected to the DOM + * Registers all WebMCP tools for AI interaction + */ + connect(): void { + console.log('WebMCP Bookmarks controller connected'); + this.registerTools(); + console.log( + 'Available tools: add_bookmark, delete_bookmark, update_bookmark, list_bookmarks, search_bookmarks, get_bookmark_stats' + ); + } + + /** + * Called when the controller is disconnected from the DOM + * Cleans up all registered tools + */ + disconnect(): void { + this.toolCleanups.forEach((cleanup) => cleanup.unregister()); + this.toolCleanups = []; + console.log('WebMCP Bookmarks controller disconnected'); + } + + /** + * Register all WebMCP tools for bookmark management + */ + private registerTools(): void { + this.registerAddBookmarkTool(); + this.registerDeleteBookmarkTool(); + this.registerUpdateBookmarkTool(); + this.registerListBookmarksTool(); + this.registerSearchBookmarksTool(); + this.registerGetStatsTool(); + } + + /** + * WebMCP Tool: Add Bookmark + * Creates a new bookmark with the provided details + */ + private registerAddBookmarkTool(): void { + const cleanup = navigator.modelContext.registerTool({ + name: 'add_bookmark', + description: 'Add a new bookmark to save a URL for later', + inputSchema: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Bookmark title', + }, + url: { + type: 'string', + description: 'URL to bookmark', + }, + description: { + type: 'string', + description: 'Optional description of the bookmark', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags for organizing the bookmark (e.g., ["work", "reference"])', + }, + }, + required: ['title', 'url'], + }, + execute: async (args) => { + const bookmark = createBookmark({ + title: args.title as string, + url: args.url as string, + description: (args.description as string) ?? '', + tags: (args.tags as string[]) ?? [], + }); + + this.bookmarksValue = [...this.bookmarksValue, bookmark]; + this.showNotification(`Added bookmark: ${bookmark.title}`, 'success'); + this.renderBookmarks(); + + return { + content: [ + { + type: 'text', + text: `Successfully added bookmark "${bookmark.title}" (${bookmark.url})`, + }, + ], + }; + }, + }); + + this.toolCleanups.push(cleanup); + } + + /** + * WebMCP Tool: Delete Bookmark + * Removes a bookmark by its ID + */ + private registerDeleteBookmarkTool(): void { + const cleanup = navigator.modelContext.registerTool({ + name: 'delete_bookmark', + description: 'Delete a bookmark by its ID', + inputSchema: { + type: 'object', + properties: { + bookmarkId: { + type: 'string', + description: 'ID of the bookmark to delete', + }, + }, + required: ['bookmarkId'], + }, + execute: async (args) => { + const [updated, removed] = removeBookmark( + this.bookmarksValue, + args.bookmarkId as string + ); + + if (!removed) { + this.showNotification('Bookmark not found', 'error'); + return { + content: [{ type: 'text', text: 'Bookmark not found' }], + }; + } + + this.bookmarksValue = updated; + this.showNotification(`Deleted: ${removed.title}`, 'success'); + this.renderBookmarks(); + + return { + content: [{ type: 'text', text: `Deleted bookmark "${removed.title}"` }], + }; + }, + }); + + this.toolCleanups.push(cleanup); + } + + /** + * WebMCP Tool: Update Bookmark + * Updates an existing bookmark's properties + */ + private registerUpdateBookmarkTool(): void { + const cleanup = navigator.modelContext.registerTool({ + name: 'update_bookmark', + description: 'Update an existing bookmark', + inputSchema: { + type: 'object', + properties: { + bookmarkId: { + type: 'string', + description: 'ID of the bookmark to update', + }, + title: { + type: 'string', + description: 'New title (optional)', + }, + url: { + type: 'string', + description: 'New URL (optional)', + }, + description: { + type: 'string', + description: 'New description (optional)', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'New tags (optional)', + }, + }, + required: ['bookmarkId'], + }, + execute: async (args) => { + const updates: Partial = {}; + if (args.title) updates.title = args.title as string; + if (args.url) updates.url = args.url as string; + if (args.description !== undefined) updates.description = args.description as string; + if (args.tags) updates.tags = args.tags as string[]; + + const [updated, bookmark] = updateBookmark( + this.bookmarksValue, + args.bookmarkId as string, + updates + ); + + if (!bookmark) { + this.showNotification('Bookmark not found', 'error'); + return { + content: [{ type: 'text', text: 'Bookmark not found' }], + }; + } + + this.bookmarksValue = updated; + this.showNotification(`Updated: ${bookmark.title}`, 'success'); + this.renderBookmarks(); + + return { + content: [{ type: 'text', text: `Updated bookmark "${bookmark.title}"` }], + }; + }, + }); + + this.toolCleanups.push(cleanup); + } + + /** + * WebMCP Tool: List Bookmarks + * Returns all bookmarks, optionally filtered by tag + */ + private registerListBookmarksTool(): void { + const cleanup = navigator.modelContext.registerTool({ + name: 'list_bookmarks', + description: 'Get a list of all saved bookmarks', + inputSchema: { + type: 'object', + properties: { + tag: { + type: 'string', + description: 'Filter by tag (optional)', + }, + }, + }, + execute: async (args) => { + const filtered = filterBookmarksByTag( + this.bookmarksValue, + args.tag as string | undefined + ); + + return { + content: [ + { + type: 'text', + text: + filtered.length > 0 + ? formatBookmarkList(filtered) + : 'No bookmarks found', + }, + ], + }; + }, + }); + + this.toolCleanups.push(cleanup); + } + + /** + * WebMCP Tool: Search Bookmarks + * Searches bookmarks by title, description, or URL + */ + private registerSearchBookmarksTool(): void { + const cleanup = navigator.modelContext.registerTool({ + name: 'search_bookmarks', + description: 'Search bookmarks by title, description, or URL', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query', + }, + }, + required: ['query'], + }, + execute: async (args) => { + const results = searchBookmarks(this.bookmarksValue, args.query as string); + + return { + content: [ + { + type: 'text', + text: + results.length > 0 + ? `Found ${results.length} bookmark(s):\n${formatBookmarkList(results)}` + : `No bookmarks found matching "${args.query}"`, + }, + ], + }; + }, + }); + + this.toolCleanups.push(cleanup); + } + + /** + * WebMCP Tool: Get Bookmark Stats + * Returns statistics about saved bookmarks + */ + private registerGetStatsTool(): void { + const cleanup = navigator.modelContext.registerTool({ + name: 'get_bookmark_stats', + description: 'Get statistics about saved bookmarks', + inputSchema: { + type: 'object', + properties: {}, + }, + execute: async () => { + const stats = calculateBookmarkStats(this.bookmarksValue); + + const topTagsText = + stats.topTags.length > 0 + ? stats.topTags.map((t) => ` - ${t.tag}: ${t.count}`).join('\n') + : ' None'; + + return { + content: [ + { + type: 'text', + text: `Bookmark Statistics:\n- Total bookmarks: ${stats.total}\n- Unique tags: ${stats.uniqueTags}\n- Top tags:\n${topTagsText}`, + }, + ], + }; + }, + }); + + this.toolCleanups.push(cleanup); + } + + /** + * Show a notification to the user + */ + private showNotification(message: string, type: 'success' | 'error'): void { + const event = new CustomEvent('bookmarks:notification', { + detail: { message, type }, + bubbles: true, + }); + this.element.dispatchEvent(event); + } + + /** + * Trigger a re-render of the bookmarks list + */ + private renderBookmarks(): void { + const event = new CustomEvent('bookmarks:updated', { + detail: { bookmarks: this.bookmarksValue }, + bubbles: true, + }); + this.element.dispatchEvent(event); + } + + /** + * Called when the bookmarks value changes (for Stimulus value change callback) + */ + bookmarksValueChanged(): void { + this.renderBookmarks(); + } +} diff --git a/rails/app/javascript/controllers/index.ts b/rails/app/javascript/controllers/index.ts new file mode 100644 index 0000000..6d852ff --- /dev/null +++ b/rails/app/javascript/controllers/index.ts @@ -0,0 +1,20 @@ +/** + * Stimulus controllers index + * + * This file registers all Stimulus controllers for the application. + * In a Rails app, this is typically auto-generated by stimulus:manifest:update. + */ + +import { Application } from '@hotwired/stimulus'; +import BookmarksWebmcpController from './bookmarks_webmcp_controller'; + +const application = Application.start(); + +// Configure Stimulus development experience +application.debug = false; +(window as unknown as { Stimulus: Application }).Stimulus = application; + +// Register controllers +application.register('bookmarks-webmcp', BookmarksWebmcpController); + +export { application }; diff --git a/rails/app/javascript/lib/bookmarks.ts b/rails/app/javascript/lib/bookmarks.ts new file mode 100644 index 0000000..38844c5 --- /dev/null +++ b/rails/app/javascript/lib/bookmarks.ts @@ -0,0 +1,159 @@ +/** + * Pure business logic functions for bookmark operations + * + * These functions are framework-agnostic and can be used with any UI + */ + +import type { Bookmark, BookmarkStats, NotificationType } from './types'; + +/** + * Create a new bookmark with generated ID and timestamp + * + * @param data - Bookmark data without id and timestamp + * @returns Complete bookmark object + */ +export function createBookmark( + data: Pick & Partial> +): Bookmark { + return { + id: crypto.randomUUID(), + title: data.title, + url: data.url, + description: data.description ?? '', + tags: data.tags ?? [], + createdAt: new Date().toISOString(), + }; +} + +/** + * Update an existing bookmark + * + * @param bookmarks - Current bookmarks array + * @param id - ID of bookmark to update + * @param updates - Partial bookmark data to update + * @returns Tuple of [updated bookmarks array, updated bookmark or null] + */ +export function updateBookmark( + bookmarks: Bookmark[], + id: string, + updates: Partial> +): [Bookmark[], Bookmark | null] { + const index = bookmarks.findIndex((b) => b.id === id); + + if (index === -1) { + return [bookmarks, null]; + } + + const updated: Bookmark[] = [...bookmarks]; + updated[index] = { ...updated[index], ...updates }; + + return [updated, updated[index]]; +} + +/** + * Remove a bookmark by ID + * + * @param bookmarks - Current bookmarks array + * @param id - ID of bookmark to remove + * @returns Tuple of [updated bookmarks array, removed bookmark or null] + */ +export function removeBookmark( + bookmarks: Bookmark[], + id: string +): [Bookmark[], Bookmark | null] { + const index = bookmarks.findIndex((b) => b.id === id); + + if (index === -1) { + return [bookmarks, null]; + } + + const removed = bookmarks[index]; + const updated = bookmarks.filter((_, i) => i !== index); + + return [updated, removed]; +} + +/** + * Filter bookmarks by tag + * + * @param bookmarks - Array of bookmarks + * @param tag - Tag to filter by (optional) + * @returns Filtered bookmarks array + */ +export function filterBookmarksByTag( + bookmarks: Bookmark[], + tag?: string +): Bookmark[] { + if (!tag) { + return bookmarks; + } + + return bookmarks.filter((b) => b.tags.includes(tag)); +} + +/** + * Search bookmarks by title or description + * + * @param bookmarks - Array of bookmarks + * @param query - Search query string + * @returns Matching bookmarks + */ +export function searchBookmarks( + bookmarks: Bookmark[], + query: string +): Bookmark[] { + const lowerQuery = query.toLowerCase(); + + return bookmarks.filter( + (b) => + b.title.toLowerCase().includes(lowerQuery) || + b.description.toLowerCase().includes(lowerQuery) || + b.url.toLowerCase().includes(lowerQuery) + ); +} + +/** + * Calculate bookmark statistics + * + * @param bookmarks - Array of bookmarks + * @returns Statistics object + */ +export function calculateBookmarkStats(bookmarks: Bookmark[]): BookmarkStats { + const allTags = bookmarks.flatMap((b) => b.tags); + const tagCounts: Record = {}; + + for (const tag of allTags) { + tagCounts[tag] = (tagCounts[tag] ?? 0) + 1; + } + + const sortedTags = Object.entries(tagCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 5); + + return { + total: bookmarks.length, + uniqueTags: new Set(allTags).size, + topTags: sortedTags.map(([tag, count]) => ({ tag, count })), + }; +} + +/** + * Format bookmarks for display + * + * @param bookmarks - Array of bookmarks + * @returns Formatted string + */ +export function formatBookmarkList(bookmarks: Bookmark[]): string { + if (bookmarks.length === 0) { + return 'No bookmarks found'; + } + + return bookmarks + .map((b) => { + const tags = b.tags.length > 0 ? ` [${b.tags.join(', ')}]` : ''; + return `- ${b.title}: ${b.url}${tags}`; + }) + .join('\n'); +} + +export type { Bookmark, BookmarkStats, NotificationType }; diff --git a/rails/app/javascript/lib/types.ts b/rails/app/javascript/lib/types.ts new file mode 100644 index 0000000..d32c7f6 --- /dev/null +++ b/rails/app/javascript/lib/types.ts @@ -0,0 +1,29 @@ +/** + * Type definitions for the bookmarks application + */ + +/** + * Represents a saved bookmark + */ +export interface Bookmark { + id: string; + title: string; + url: string; + description: string; + tags: string[]; + createdAt: string; +} + +/** + * Statistics about saved bookmarks + */ +export interface BookmarkStats { + total: number; + uniqueTags: number; + topTags: Array<{ tag: string; count: number }>; +} + +/** + * Notification types for UI feedback + */ +export type NotificationType = 'success' | 'error'; diff --git a/rails/app/models/bookmark.rb b/rails/app/models/bookmark.rb new file mode 100644 index 0000000..7dee256 --- /dev/null +++ b/rails/app/models/bookmark.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# +# Bookmark model for storing saved URLs +# +# This is a reference implementation showing how to structure +# the Rails model for use with WebMCP tools. +# +# Database schema (for reference): +# create_table :bookmarks do |t| +# t.string :title, null: false +# t.string :url, null: false +# t.text :description +# t.string :tags, array: true, default: [] +# t.timestamps +# end +# +class Bookmark < ApplicationRecord + # Validations + validates :title, presence: true, length: { maximum: 255 } + validates :url, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp } + + # Scopes for filtering + scope :by_tag, ->(tag) { where("? = ANY(tags)", tag) } + scope :recent, -> { order(created_at: :desc) } + + # Search scope for finding bookmarks by text + scope :search, ->(query) { + where( + "title ILIKE :q OR description ILIKE :q OR url ILIKE :q", + q: "%#{query}%" + ) + } + + # Normalize URL before saving + before_validation :normalize_url + + # Convert to JSON format expected by the frontend + def as_json(options = {}) + super(options.merge( + only: [:id, :title, :url, :description, :tags, :created_at] + )).tap do |hash| + hash["createdAt"] = hash.delete("created_at")&.iso8601 + end + end + + private + + def normalize_url + return if url.blank? + + # Add https:// if no protocol specified + self.url = "https://#{url}" unless url.match?(%r{\Ahttps?://}) + end +end diff --git a/rails/app/views/bookmarks/index.html.erb b/rails/app/views/bookmarks/index.html.erb new file mode 100644 index 0000000..c4d7eda --- /dev/null +++ b/rails/app/views/bookmarks/index.html.erb @@ -0,0 +1,156 @@ +<%# + Sample ERB template for Rails integration + + This file shows how to integrate WebMCP tools in a Rails view. + The Stimulus controller handles all WebMCP tool registration. + + Usage: + 1. The data-controller attribute connects this element to the Stimulus controller + 2. The data-bookmarks-webmcp-bookmarks-value passes initial data from Rails + 3. JavaScript custom events update the UI when tools are called +%> + +
+ + <%# Notification area - updated via JavaScript %> + + +
+
+

Bookmarks Manager

+

AI-powered bookmarks with Rails + WebMCP

+
+ +
+ <%# Info section %> +
+
+

How This Works

+

+ This Rails app uses Stimulus controllers with WebMCP: +

+
    +
  • Install the MCP-B browser extension
  • +
  • Open the extension to see 6 available tools
  • +
  • Ask AI to manage your bookmarks
  • +
  • Watch the UI update in real-time!
  • +
+
+ +
+

Available Tools

+
    +
  • add_bookmark - Save new bookmarks
  • +
  • delete_bookmark - Remove bookmarks
  • +
  • update_bookmark - Edit existing bookmarks
  • +
  • list_bookmarks - View all bookmarks
  • +
  • search_bookmarks - Find bookmarks
  • +
  • get_bookmark_stats - View statistics
  • +
+
+
+ + <%# Statistics section %> +
+

Statistics

+
+
+ <%= @bookmarks.count %> + Total Bookmarks +
+
+ <%= @bookmarks.flat_map(&:tags).uniq.count %> + Unique Tags +
+
+
+ + <%# Bookmarks list %> +
+

Saved Bookmarks

+
+ <% if @bookmarks.empty? %> +

No bookmarks yet. Ask AI to add some!

+ <% else %> + <% @bookmarks.each do |bookmark| %> +
+
+ <%= link_to bookmark.title, bookmark.url, + target: '_blank', + rel: 'noopener noreferrer', + class: 'bookmark-title' %> +
+
<%= bookmark.url %>
+ <% if bookmark.description.present? %> +

<%= bookmark.description %>

+ <% end %> + <% if bookmark.tags.any? %> +
+ <% bookmark.tags.each do |tag| %> + <%= tag %> + <% end %> +
+ <% end %> +
+ <% end %> + <% end %> +
+
+
+ +
+

+ Built with + <%= link_to 'WebMCP', 'https://docs.mcp-b.ai', target: '_blank', rel: 'noopener noreferrer' %> + • Rails • Stimulus +

+
+
+
+ +<%# JavaScript for handling custom events %> +<%= javascript_tag do %> + // Handle notification events from Stimulus controller + document.addEventListener('bookmarks:notification', (event) => { + const { message, type } = event.detail; + const notification = document.getElementById('notification'); + if (notification) { + notification.textContent = message; + notification.className = `notification ${type}`; + setTimeout(() => { + notification.className = 'notification hidden'; + }, 3000); + } + }); + + // Handle bookmark updates from Stimulus controller + document.addEventListener('bookmarks:updated', (event) => { + const { bookmarks } = event.detail; + // Update statistics + document.getElementById('stat-total').textContent = bookmarks.length; + const allTags = bookmarks.flatMap(b => b.tags || []); + document.getElementById('stat-tags').textContent = new Set(allTags).size; + + // Re-render bookmarks list + const list = document.getElementById('bookmarks-list'); + if (bookmarks.length === 0) { + list.innerHTML = '

No bookmarks yet. Ask AI to add some!

'; + } else { + list.innerHTML = bookmarks.map(b => ` +
+ +
${b.url}
+ ${b.description ? `

${b.description}

` : ''} + ${b.tags && b.tags.length > 0 ? ` +
+ ${b.tags.map(t => `${t}`).join('')} +
+ ` : ''} +
+ `).join(''); + } + }); +<% end %> diff --git a/rails/index.html b/rails/index.html new file mode 100644 index 0000000..4cc6259 --- /dev/null +++ b/rails/index.html @@ -0,0 +1,143 @@ + + + + + + Rails WebMCP Bookmarks + + + +
+ +
+ + + +
+
+

Bookmarks Manager

+

AI-powered bookmarks with Rails + WebMCP

+
+ +
+
+
+

How This Works

+

+ This Rails app uses Stimulus controllers with WebMCP: +

+
    +
  • Install the MCP-B browser extension
  • +
  • Open the extension to see 6 available tools
  • +
  • Ask AI to manage your bookmarks
  • +
  • Watch the UI update in real-time!
  • +
+
+ +
+

Available Tools

+
    +
  • add_bookmark - Save new bookmarks
  • +
  • delete_bookmark - Remove bookmarks
  • +
  • update_bookmark - Edit existing bookmarks
  • +
  • list_bookmarks - View all bookmarks
  • +
  • search_bookmarks - Find bookmarks
  • +
  • get_bookmark_stats - View statistics
  • +
+
+
+ +
+

Statistics

+
+
+ 0 + Total Bookmarks +
+
+ 0 + Unique Tags +
+
+
+ +
+

Saved Bookmarks

+
+

No bookmarks yet. Ask AI to add some!

+
+
+
+ +
+

+ Built with + WebMCP + • Rails • Stimulus +

+
+
+
+
+ + + + + diff --git a/rails/package.json b/rails/package.json new file mode 100644 index 0000000..13ca517 --- /dev/null +++ b/rails/package.json @@ -0,0 +1,21 @@ +{ + "name": "rails-webmcp-example", + "version": "1.0.0", + "description": "Rails example using WebMCP with Stimulus controllers", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@hotwired/stimulus": "^3.2.2", + "@mcp-b/global": "latest" + }, + "devDependencies": { + "typescript": "^5.6.0", + "vite": "^6.0.0" + } +} diff --git a/rails/pnpm-lock.yaml b/rails/pnpm-lock.yaml new file mode 100644 index 0000000..397dd04 --- /dev/null +++ b/rails/pnpm-lock.yaml @@ -0,0 +1,1387 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hotwired/stimulus': + specifier: ^3.2.2 + version: 3.2.2 + '@mcp-b/global': + specifier: latest + version: 1.1.2(@modelcontextprotocol/sdk@1.15.0) + devDependencies: + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1 + +packages: + + '@composio/json-schema-to-zod@0.1.19': + resolution: {integrity: sha512-OynnORVWjsqDv13EvFa4Bb+B1SzBqpkWGi6qXm4vpB3EG65o3T9FbhDCqWB3ZufnMmH1T/NYS526O0lnn2LoCQ==} + peerDependencies: + zod: '>=3.25.76 <4 || >=4.1 <5' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hotwired/stimulus@3.2.2': + resolution: {integrity: sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==} + + '@mcp-b/global@1.1.2': + resolution: {integrity: sha512-62rFjtOY1z5X99ce9prvpIrmnxq9KwjYGl2NM+zfNDs+/73AZWFoCR9WIdF5EdefNiSe/wdV4WApKRriboyqcQ==} + engines: {node: '>=18'} + + '@mcp-b/transports@1.1.1': + resolution: {integrity: sha512-ur9cHLeJ/iA80DE+d66Fh5aQbwh6Lr/PXGhaemvy5tQotnbBS35u2BjROiXTJXkM2sNKKMCTh/sv0q/amyCZlg==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.15.0 + + '@mcp-b/webmcp-ts-sdk@1.0.1': + resolution: {integrity: sha512-M33389FMm6+gUsEOFNmw2zlXVdSK+J8PBVeBeM4s8ZXPGMWiK6qQLAovlT9DzcFaVTS1ped6E+bYv+BT3BvyLQ==} + engines: {node: '>=18'} + + '@modelcontextprotocol/sdk@1.15.0': + resolution: {integrity: sha512-67hnl/ROKdb03Vuu0YOr+baKTvf1/5YBHBm9KnZdjdAh8hjt4FRCPD5ucwxGB237sBpzlqQsLy1PFu7z/ekZ9Q==} + engines: {node: '>=18'} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + zod-to-json-schema@3.25.0: + resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@composio/json-schema-to-zod@0.1.19(zod@3.25.76)': + dependencies: + zod: 3.25.76 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@hotwired/stimulus@3.2.2': {} + + '@mcp-b/global@1.1.2(@modelcontextprotocol/sdk@1.15.0)': + dependencies: + '@composio/json-schema-to-zod': 0.1.19(zod@3.25.76) + '@mcp-b/transports': 1.1.1(@modelcontextprotocol/sdk@1.15.0) + '@mcp-b/webmcp-ts-sdk': 1.0.1 + zod: 3.25.76 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - supports-color + + '@mcp-b/transports@1.1.1(@modelcontextprotocol/sdk@1.15.0)': + dependencies: + '@modelcontextprotocol/sdk': 1.15.0 + zod: 3.25.76 + + '@mcp-b/webmcp-ts-sdk@1.0.1': + dependencies: + '@modelcontextprotocol/sdk': 1.15.0 + transitivePeerDependencies: + - supports-color + + '@modelcontextprotocol/sdk@1.15.0': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@types/estree@1.0.8': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + json-schema-traverse@0.4.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@8.3.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkce-challenge@5.0.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + statuses@2.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + toidentifier@1.0.1: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + unpipe@1.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vary@1.1.2: {} + + vite@6.4.1: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + zod-to-json-schema@3.25.0(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/rails/tsconfig.json b/rails/tsconfig.json new file mode 100644 index 0000000..10835eb --- /dev/null +++ b/rails/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["app/javascript/**/*"] +} diff --git a/rails/vite.config.ts b/rails/vite.config.ts new file mode 100644 index 0000000..fb57a6c --- /dev/null +++ b/rails/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + build: { + outDir: 'dist', + }, +}); From f8281167b2db37f078ea946af4cd68cfae1fa55f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Dec 2025 22:50:36 +0000 Subject: [PATCH 2/2] Clean up Rails example code and documentation - Fix tool cleanup example in README to match actual implementation - Simplify application.ts entry point - Clarify controller registration comment in index.ts - Remove unnecessary console.log and comments --- rails/README.md | 4 ++-- rails/app/javascript/application.ts | 10 +--------- rails/app/javascript/controllers/index.ts | 13 +++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/rails/README.md b/rails/README.md index 1f598be..1146235 100644 --- a/rails/README.md +++ b/rails/README.md @@ -224,7 +224,7 @@ this.element.dispatchEvent( Properly cleanup tools when the controller disconnects: ```typescript -private toolCleanups: Array<() => void> = []; +private toolCleanups: Array<{ unregister: () => void }> = []; connect() { const cleanup = navigator.modelContext.registerTool({...}); @@ -232,7 +232,7 @@ connect() { } disconnect() { - this.toolCleanups.forEach(cleanup => cleanup()); + this.toolCleanups.forEach((cleanup) => cleanup.unregister()); this.toolCleanups = []; } ``` diff --git a/rails/app/javascript/application.ts b/rails/app/javascript/application.ts index 5f50236..855a095 100644 --- a/rails/app/javascript/application.ts +++ b/rails/app/javascript/application.ts @@ -1,16 +1,8 @@ /** * Application entry point * - * This is the main JavaScript entry point for the Rails application. - * It imports and initializes Stimulus controllers and the WebMCP polyfill. - * - * @see https://docs.mcp-b.ai/frameworks/rails + * Initializes the WebMCP polyfill and Stimulus controllers. */ -// Import the WebMCP polyfill to enable navigator.modelContext import '@mcp-b/global'; - -// Import Stimulus application and controllers import './controllers'; - -console.log('WebMCP Rails application loaded'); diff --git a/rails/app/javascript/controllers/index.ts b/rails/app/javascript/controllers/index.ts index 6d852ff..de89078 100644 --- a/rails/app/javascript/controllers/index.ts +++ b/rails/app/javascript/controllers/index.ts @@ -1,8 +1,9 @@ /** - * Stimulus controllers index + * Stimulus controllers registration * - * This file registers all Stimulus controllers for the application. - * In a Rails app, this is typically auto-generated by stimulus:manifest:update. + * This file initializes the Stimulus application and registers all controllers. + * In a full Rails app, controllers are often auto-loaded, but for this demo + * we register them explicitly. */ import { Application } from '@hotwired/stimulus'; @@ -10,11 +11,7 @@ import BookmarksWebmcpController from './bookmarks_webmcp_controller'; const application = Application.start(); -// Configure Stimulus development experience -application.debug = false; -(window as unknown as { Stimulus: Application }).Stimulus = application; - -// Register controllers +// Register the WebMCP bookmarks controller application.register('bookmarks-webmcp', BookmarksWebmcpController); export { application };