Install
$ agentstack add mcp-makethisbetter-makethisbetter-js ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
makethisbetter
Drop-in widget for AI-powered user feedback and automated fixes.
makethisbetter.dev ·
Why
You ship with AI agents. Your users hit bugs you never see in dev. They leave. You never find out why.
This widget gives your users a way to report exactly what went wrong — annotated screenshot, console errors, DOM state, browser info — in two clicks. AI triage turns that into a structured task your coding agent (Claude Code, Cursor, Codex) picks up automatically. The agent ships the fix. The user gets notified.
No more "can you describe what happened?" No more lost screenshots in Slack. The full loop, from frustrated user to shipped fix, runs without you context-switching.
Quick Start
CDN (2 lines)
MakeThisBetter.init({ projectKey: 'mtb_proj_YOUR_KEY' })
npm
npm install makethisbetter
import { MakeThisBetter } from 'makethisbetter'
MakeThisBetter.init({ projectKey: 'mtb_proj_YOUR_KEY' })
That's it. A feedback tab appears on your page.
How It Works
User clicks feedback tab
-> Annotates the problem (click to pin, drag to draw)
-> Adds a comment
-> Submits
+-- Screenshot captured automatically
+-- Console errors collected
+-- Page context assembled (URL, browser, OS, selectors)
+-- Sent to Make This Better API
+-- AI asks a clarifying question if needed
-> Dashboard shows structured feedback
-> AI triage produces an agent-ready task
-> Your coding agent picks it up and ships the fix
-> User gets notified: the fix is live
Framework Guides
React / Next.js
// app/providers.tsx (App Router) or pages/_app.tsx (Pages Router)
'use client'
import { useEffect } from 'react'
export function FeedbackProvider() {
useEffect(() => {
import('makethisbetter').then(({ MakeThisBetter }) => {
MakeThisBetter.init({
projectKey: process.env.NEXT_PUBLIC_MTB_KEY!,
user: { id: userId, email }
})
})
return () => {
import('makethisbetter').then(({ MakeThisBetter }) => MakeThisBetter.destroy())
}
}, [])
return null
}
Vue / Nuxt
// plugins/makethisbetter.client.ts (Nuxt) or main.ts (Vue)
import { MakeThisBetter } from 'makethisbetter'
export default defineNuxtPlugin(() => {
MakeThisBetter.init({
projectKey: useRuntimeConfig().public.mtbKey,
})
return {
provide: { mtbDestroy: () => MakeThisBetter.destroy() }
}
})
Astro
import { MakeThisBetter } from 'makethisbetter'
MakeThisBetter.init({ projectKey: import.meta.env.PUBLIC_MTB_KEY })
Rails
on every visit, taking any script-appended element
with it. Render the host yourself and mark it permanent, or the widget is
rebuilt after each navigation and anything mid-flight — a half-written
report, a screen recording — is lost. %>
MakeThisBetter.init({
projectKey: '',
user: { id: '', email: '' }
})
The host must sit inside ` — Turbo pairs permanent elements by id within the body snapshot, so one placed in is never matched and nothing happens. It needs both the id` and the attribute; either alone does nothing.
Only Turbo-driven apps need this. React, Vue and Svelte routers re-render inside their own container and never replace ``, so the host survives on its own.
Plain HTML / Static Sites
MakeThisBetter.init({ projectKey: 'mtb_proj_YOUR_KEY' })
Features
Annotation
Click any element to pin it, or drag to draw a freeform highlight. The SDK captures the element's CSS selector, text content, and position.
Interaction Replay
Switch to Replay mode in the toolbar to capture an Interaction Replay (up to 60 seconds). It records rrweb DOM mutations and interaction events. It does not capture screen video or audio and does not request browser media permissions. The recorder loads lazily, so there is zero cost until the reporter starts a replay.
What a replay contains
| Captured | Not captured | |----------|--------------| | The page's DOM structure and every mutation to it | Passwords, payment-card data, OTPs, access tokens, private keys, and other high-confidence credentials — replaced with [Filtered] | | Visible text content and ordinary form values | Content inside an element you mark rr-block or rr-mask | | Mouse positions, clicks, scrolls, viewport size | Screen video, audio, camera, microphone | | Stylesheets needed to render the replay | Cookies, localStorage, HTTP request or response bodies |
Sensitive-data filtering is fixed and cannot be disabled through SDK configuration. Ordinary values such as search queries, issue descriptions, and internal form fields remain available because they are often necessary to reproduce a problem.
Excluding an element. Add rrweb's privacy classes to any page region that the SDK must not capture. These classes apply consistently to Interaction Replay, click and input breadcrumbs, annotation metadata, and screenshots:
class="rr-block"— hides the entire marked region while preserving its footprint.class="rr-mask"— hides text and form-control content while preserving the surrounding layout.
Account balance: $12,400
If screenshot or Replay filtering cannot complete, that attachment is silently omitted and the text feedback still submits. Automated filtering cannot identify every site-specific secret, so use rr-block or rr-mask on sensitive application regions.
Frustration Detection
The SDK watches for signals that a user is struggling and proactively offers to collect feedback:
| Signal | Trigger | |--------|---------| | Rage click | 3+ clicks on the same element within 1 second | | Dead click | Click on a non-interactive element (with console errors) | | Dead click (DOM) | Click on interactive-looking element with no DOM response | | Rapid navigation | 3+ page navigations within 5 seconds | | Form failure | Form submission with validation errors | | Error page | Landing on a 404/500 error page |
Disable with frustrationDetection: false.
AI Clarification
Before submission, an AI assistant may ask one short follow-up question to clarify the real need — avoiding XY problems where users describe their attempted solution instead of the actual problem. Once the exchange is complete, the widget submits the feedback automatically.
Auto-Collected Context
Every submission automatically includes:
- Page URL origin and pathname, browser, OS, screen resolution
- Error type, script pathname, and line/column location (via
window.onerrorandwindow.onunhandledrejection) - Target element selector and text
- Annotated screenshot with privacy covers applied before upload (via
html-to-image) - Annotation coordinates and draw paths
Internationalization
Built-in support for 7 languages:
| Code | Language | |------|----------| | en | English (default) | | zh-CN | Chinese (Simplified) | | ja | Japanese | | ko | Korean | | es | Spanish | | fr | French | | de | German |
Configuration
MakeThisBetter.init({
// Required
projectKey: 'mtb_proj_xxx',
// Optional
locale: 'en', // UI language. Unset: falls back to , then 'en'
position: 'right', // Tab position: 'left' | 'right'
tabText: 'Feedback', // Label on the docked tab. Unset: the locale's own wording
entryMode: 'button', // 'button' docks a tab | 'api' renders none
theme: 'auto', // 'light' | 'dark' | 'auto'
frustrationDetection: true, // Proactive frustration prompts
apiUrl: 'https://...', // Self-hosted API endpoint
// User identification (recommended).
// Ignored entirely when a valid userToken/userTokenFn JWT is present —
// see Identity Verification below.
user: {
id: 'usr_123',
email: 'alex@example.com',
name: 'Alex Chen',
},
})
locale is resolved once, in this order: the locale you pass, then the page's ` attribute, then en. A tag with no exact match is retried without its region (fr-CA → fr), and anything still unmatched falls back to en`.
Changing the language at runtime
MakeThisBetter.setLocale('zh-CN') switches the language for the tab and for anything opened afterwards. A popup that is already on screen keeps the language it was opened in, so a reporter is never re-rendered mid-sentence. Call it before the reporter opens the widget — for example, in the same place your app applies a language change.
MakeThisBetter.setLocale('zh-CN')
Identity Verification
Identity verification links feedback to authenticated users and lets them view their own submissions on the feedback board.
Level 0 -- Anonymous (default): No user token. Feedback is anonymous.
MakeThisBetter.init({ projectKey: 'mtb_proj_xxx' })
Anonymous reporters are offered a follow-up: the success card shows an optional email field, and an address entered there is sent to the reporter endpoint and kept in localStorage under mtb_reporter_email so the field is not asked for again on later reports from the same browser. The field is skipped entirely when user is set or when a JWT already identifies the reporter. Clearing site data clears it.
Level 1 -- Static token: Pass a pre-generated JWT. Simple, but the token may expire during long sessions.
MakeThisBetter.init({
projectKey: 'mtb_proj_xxx',
userToken: 'eyJhbGciOiJIUzI1NiIs...',
})
Level 2 -- Dynamic token (recommended): Pass an async function that returns a fresh JWT. The SDK calls it before each API request, so tokens never go stale.
MakeThisBetter.init({
projectKey: 'mtb_proj_xxx',
userTokenFn: async () => {
const res = await fetch('/api/mtb-token')
const { token } = await res.json()
return token
},
})
When userToken or userTokenFn is set, the widget sends an X-User-Token header with every request. After a successful submission, a "View my feedback" link appears that opens the project board filtered to the user's submissions.
The JWT wins over user. These are not two independent ways to name the reporter. Whenever the server receives a valid token, it takes the reporter's id, email and name from the token's sub, email and name claims and discards the user fields the widget sent alongside them — a claim you leave out is simply not recorded, even if user carried it. Put everything you want attributed in the token, and treat user as the anonymous-only path.
Generate tokens server-side using your project's Signing Secret (available in your project settings):
# Rails example
payload = {
sub: current_user.id,
email: current_user.email,
name: current_user.name,
exp: 1.hour.from_now.to_i,
}
JWT.encode(payload, project.signing_secret, 'HS256')
Conditional Loading
// Only show to beta users
if (user.isBetaTester) {
MakeThisBetter.init({ projectKey: 'mtb_proj_xxx', user: { id: user.id } })
}
Self-Hosting
The Widget works with any backend — not just makethisbetter.dev. Implement the Submission Session profile you need from the Self-Hosting API Specification, then point apiUrl at your API version root:
MakeThisBetter.init({
projectKey: 'your-key',
apiUrl: 'https://feedback.yoursite.com/api/v1',
})
The minimum backend supports the Submission Session flow: create a Session with multipart context, optionally clarify it with the in-memory Submission Token, then explicitly finalize or abandon it. The Widget takes care of annotation, Interaction Replay, frustration detection, and context collection. Anonymous board handoff and post-submit email capture use two additional optional operations documented in the specification.
The cloud platform at makethisbetter.dev adds AI triage, dashboard, GitHub/Linear sync, and email notifications on top.
API
import { MakeThisBetter } from 'makethisbetter'
// Start the widget
MakeThisBetter.init(config: MakeThisBetterConfig): void
// Remove the widget and clean up all listeners
MakeThisBetter.destroy(): void
// Open annotation mode from your own UI. Idempotent while already open.
MakeThisBetter.open(): void
// Close annotation mode and restore the page
MakeThisBetter.close(): void
// Show or remove the docked tab on non-touch devices without tearing down
MakeThisBetter.showLauncher(): void
MakeThisBetter.hideLauncher(): void
// Switch the UI language for anything opened afterwards
MakeThisBetter.setLocale(locale: string): void
Your own entry point
A tab docked to the edge of the screen is the right default, but it is not always right — a full-screen editor, a map, or a phone where every pixel is spoken for. Set entryMode: 'api' to render no tab at all and open the widget from wherever the entry point belongs in your product:
MakeThisBetter.init({ projectKey: 'mtb_proj_xxx', entryMode: 'api' })
document.querySelector('#menu-feedback')
.addEventListener('click', () => MakeThisBetter.open())
In api mode nothing is reachable until you wire up that call, so the widget logs a warning on init as a reminder. On non-touch devices, showLauncher() can bring the docked tab back at runtime. Touch devices never render the SDK tab; put the entry in your own mobile UI and call MakeThisBetter.open() from it.
Architecture
The widget runs inside a Shadow DOM container, isolating its styles from your page. No CSS conflicts, no z-index wars.
Shadow DOM Host (#mtb-widget-host)
+-- Feedback Tab (entry point)
+-- Annotation Toolbar (Mark up / Replay toggle)
+-- Annotation Session (pin + draw overlays)
+-- Comment Popup (description + submit)
+-- AI Clarification Card (follow-up conversation)
+-- Success Card (confirmation)
+-- Frustration Prompt (proactive trigger)
Bundle Size
| Format | Size | gzip | |--------|------|------| | IIFE | ~77 KB | ~23 KB | | ESM | ~90 KB | ~24 KB |
The rrweb recorder (~78 KB) is loaded on demand when Interaction Replay starts and is not included in these numbers.
Development
git clone https://github.com/makethisbetter/makethisbetter-js.git
cd makethisbetter-js
npm install
npm run dev # Dev server at localhost:5173
npm run build # Build all formats to dist/
npm test # Run tests
npm run type-check # TypeScript validation
Related
| Package | What it does | |---------|-------------| | Make This Better | The platform — dashboard, AI triage, feedback board | | @makethisbetter/mcp | MCP server — your coding agent reads feedback directly | | makethisbetter CLI | Terminal tool for managing feedback | | makethisbetter Skills | Claude Code skills — /makethisbetter in your editor |
License
[MIT](LICENSE)
GitHub repo settings
- Description: Drop-in widget for AI-powered user feedback and automated fixes
- Homepage: https://makethisbetter.dev
- Topics: feedback, widget, ai, mcp, claude-code, cursor, vibe-coding
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: makethisbetter
- Source: makethisbetter/makethisbetter-js
- License: MIT
- Homepage: https://makethisbetter.dev
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.