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..1146235
--- /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<{ unregister: () => void }> = [];
+
+connect() {
+ const cleanup = navigator.modelContext.registerTool({...});
+ this.toolCleanups.push(cleanup);
+}
+
+disconnect() {
+ this.toolCleanups.forEach((cleanup) => cleanup.unregister());
+ 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..855a095
--- /dev/null
+++ b/rails/app/javascript/application.ts
@@ -0,0 +1,8 @@
+/**
+ * Application entry point
+ *
+ * Initializes the WebMCP polyfill and Stimulus controllers.
+ */
+
+import '@mcp-b/global';
+import './controllers';
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..de89078
--- /dev/null
+++ b/rails/app/javascript/controllers/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Stimulus controllers registration
+ *
+ * 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';
+import BookmarksWebmcpController from './bookmarks_webmcp_controller';
+
+const application = Application.start();
+
+// Register the WebMCP bookmarks controller
+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 %>
+
+
+
+
+
+
+ <%# 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!
+
+
+
+
+
+
+ <%# 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| %>
+
+
+
<%= bookmark.url %>
+ <% if bookmark.description.present? %>
+
<%= bookmark.description %>
+ <% end %>
+ <% if bookmark.tags.any? %>
+
+ <% bookmark.tags.each do |tag| %>
+ <%= tag %>
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+ <% end %>
+
+
+
+
+
+
+
+
+<%# 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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!
+
+
+
+
+
+
+
+ Statistics
+
+
+ 0
+ Total Bookmarks
+
+
+ 0
+ Unique Tags
+
+
+
+
+
+ Saved Bookmarks
+
+
No bookmarks yet. Ask AI to add some!
+
+
+
+
+
+
+
+
+
+
+
+
+
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',
+ },
+});