Rails WebMCP Example
March 3, 2026 · View on GitHub
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
# Install dependencies
pnpm install
# Run development server
pnpm dev
Then open your browser and install the MCP-B extension to interact with the tools.
Available Tools
This example exposes 6 AI-callable tools:
- add_bookmark - Save new bookmarks with title, URL, description, and tags
- delete_bookmark - Remove bookmarks by ID
- update_bookmark - Edit existing bookmark properties
- list_bookmarks - View all bookmarks (optionally filter by tag)
- search_bookmarks - Find bookmarks by title, description, or URL
- get_bookmark_stats - Get statistics about saved bookmarks
How It Works
The integration uses Stimulus controllers to register WebMCP tools:
// 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:
<div data-controller="bookmarks-webmcp"
data-bookmarks-webmcp-bookmarks-value="<%= @bookmarks.to_json %>">
<!-- Your UI here -->
</div>
Rails Integration Guide
Option 1: With Vite (Recommended)
If using vite_rails:
-
Add dependencies:
pnpm add @mcp-b/global @hotwired/stimulus -
Copy the
app/javascript/directory structure to your Rails app -
Import in your application entry point:
// app/javascript/application.ts import '@mcp-b/global'; import './controllers';
Option 2: With Importmaps
If using Rails importmaps:
-
Pin the packages:
bin/importmap pin @mcp-b/global -
Create the Stimulus controller in JavaScript (not TypeScript):
// 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 }); } } -
Register in your manifest:
// 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:
-
Add dependencies:
yarn add @mcp-b/global @hotwired/stimulus -
Import in your entry point:
// 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:
// 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:
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:
this.element.dispatchEvent(
new CustomEvent('bookmarks:updated', {
detail: { bookmarks: this.bookmarksValue },
bubbles: true,
})
);
4. Tool Cleanup
Properly cleanup tools when the controller disconnects:
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)
# 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)
# app/controllers/bookmarks_controller.rb
class BookmarksController < ApplicationController
def index
@bookmarks = Bookmark.order(created_at: :desc)
end
end
View (for reference)
<%# app/views/bookmarks/index.html.erb %>
<div data-controller="bookmarks-webmcp"
data-bookmarks-webmcp-bookmarks-value="<%= @bookmarks.to_json %>">
<h1>My Bookmarks</h1>
<div id="bookmarks-list">
<% @bookmarks.each do |bookmark| %>
<%= render bookmark %>
<% end %>
</div>
</div>
Learn More
License
MIT