Documentation · v0.5.2
Loupe
Pin feedback to the live UI — inspect any element, drop a comment, capture a screenshot — then hand it to Claude Code as an actionable backlog. Comments persist and re-anchor across redeploys.
What Loupe is
An embeddable visual-feedback platform. A product manager inspects any element on a running product, pins a comment to it, and captures a screenshot. Comments persist and re-anchor across redeploys, and are handed to Claude Code via MCP as an actionable, fully-contextualized backlog. A browser extension does the same on any site without an SDK install, and a Laravel package embeds the whole loop into a PHP app.
It's built like a product, not a prototype: an SDK, a backend, a triage board, an MCP server, and a browser extension — all open source, all TypeScript — plus a first-party Laravel package. Everything runs locally with no external services (embedded Postgres via PGlite).
The packages
Contract layer
Canonical types + normalizeUrl(). Zero runtime deps; everyone imports from here.
The widget
Embeddable Shadow-DOM widget: inspect, comment, capture, re-anchor.
The API
node:http + Postgres + object storage + HMAC auth + static hosting.
Triage board
Kanban board for humans — status columns, filters, Copy for Claude.
For Claude
MCP server exposing the backlog to Claude Code as three tools.
Any site
MV3 extension reusing the SDK core with pixel-perfect capture.
Loupe for Laravel
Composer package: widget + your DB + gating + dashboard + MCP.
Author: Mohamed Ashraf Elsaed. Published to npm (@loupekit/*),
GitHub Packages, and Packagist (loupekit/laravel).
Quick start
Everything is orchestrated from the repo root. Build once, seed the demo project, and start the server — a single process hosts the API, dashboard, demo product, and SDK bundle on one port.
# install workspace deps
npm install
# build shared → sdk → dashboard → extension
npm run build
# create the demo project; prints its admin key + demo HMAC
npm run seed
# one process on http://localhost:8787 → API + dashboard + demo + SDK
npm start
Open these
- Demo product —
/demo/ - Triage board —
/dashboard/?key=<admin key> - Health —
GET /v1/health
Seeded demo credentials
- Project —
pk_demo_acme - Admin key —
sk_demo_acme_0f3b9c - Demo user —
u_92
Embed the SDK in your product
import { init } from "@loupekit/sdk";
init({
projectKey: "pk_demo_acme",
user: { id: "u_92", name: "Ada Lovelace" },
userHmac: "<HMAC-SHA256(user.id, PROJECT_SECRET)>", // from your server
apiBase: "http://localhost:8787", // omit → offline localStorage mode
});
Connect Claude Code over MCP
{
"mcpServers": {
"loupe": {
"command": "node",
"args": ["/abs/path/to/loupe/packages/mcp/index.ts"],
"env": {
"LOUPE_API": "http://localhost:8787",
"LOUPE_PROJECT_KEY": "pk_demo_acme",
"LOUPE_ADMIN_KEY": "<admin key>"
}
}
}
}
list_comments → get_comment (request + element HTML +
computed styles + screenshot URL) → makes the change → update_status(done).Verify everything with npm test (Vitest) and npm run test:coverage
for a v8 coverage report.
The feedback loop
SDK (in the product) → backend API → Postgres + object storage → dashboard (human triage) and MCP server (Claude reads it) → status flows back.
- Inspect & comment. A PM clicks an element or drags a region. The SDK records a multi-signal anchor, the element's HTML and computed styles, and a screenshot.
- Store. The screenshot uploads to
POST /v1/blobs; the comment (with the returned URL) is written to Postgres against the normalized page URL. - Triage. A human works the Kanban board — open → in progress → done — with screenshots and the exact selector.
- Ship with Claude. Claude Code reads the same backlog over MCP, makes the change, and marks it done. Status flows back either way.
Both capture surfaces (SDK and extension) and both triage surfaces (dashboard and MCP) share the same data and contracts — see Seams for the extension points.
Working in the repo
Loupe is an npm workspaces monorepo. Root scripts orchestrate the packages.
Node-native packages
server/ and mcp/ run
TypeScript directly on Node 24 (type-stripping) in dev — node index.ts, .ts
imports. mcp is additionally bundled (tsup → dist) for publishing,
since Node won't strip types under node_modules.
Bundled browser packages
sdk/, dashboard/,
and extension/ are bundled by tsup to dist/; imports
use .js (ESM) extensions.
@loupekit/shared is a built package (tsc →
dist) and must be built before the others. Keep changes behind the seams
(StorageAdapter, db.ts, blobs.ts, the
captureScreenshot hook) — change implementations, not contracts.
@loupekit/server, the extension manifest.json, and the Node
McpServer (all 0.2.0) are now 0.5.0. The Laravel
LoupeServer reports its own 1.0.0 — an independent MCP-server identity,
not the package version.Re-anchoring — the crown jewel
The hardest problem Loupe solves: a comment pinned to an element must find that same element
again after the product is redeployed and the markup has changed. fingerprint.ts
records a multi-signal fingerprint with captureAnchor(el): Anchor, then re-locates it
with resolveAnchor(anchor, root?): ResolveResult | null in tiers.
What the anchor records
Stable id / testid, a CSS path rooted at the nearest stable ancestor, XPath, normalized text,
identifying attributes, nth-of-type, and the bounding rect + viewport.
Resolution tiers
| Tier | Match | Score |
|---|---|---|
| 1 | Unique testid / id | 0.98 |
| 1.5 | cssPath rooted at a stable id, single match, right tag | 0.90 — survives changed content |
| 2 | Weighted similarity scan | accepted at ≥ 0.50 |
The similarity scan weights are: text .34 · attrs .22 · testid .22 · cssPath .20 ·
tag .12 · position .10. If the best candidate scores below 0.5, the pin
detaches rather than anchoring to the wrong element.
Authentication
Every project has a secret. There are two identities, and the browser never sees the
secret.
Writes — user identity (HMAC)
Your server computes
signUser(userId, secret) = HMAC-SHA256(userId, secret) and injects it into the page.
The SDK sends X-Loupe-User + X-Loupe-Hmac; the server verifies with a
timing-safe compare. A user may only post as themselves.
Read / triage — admin identity
The dashboard and MCP authenticate as
admin with X-Loupe-Admin = the project secret.
authenticate(projectKey, req) resolves to admin, user, or
a failure (400 missing key · 401 bad creds · 404 unknown project).
The Laravel package swaps this whole scheme for Laravel's own session + CSRF + guard + Gate authorization — no shared secret. See Authorization.
URL normalization
Comments are keyed by normalized URL, so tracking params never fragment a page's
feedback. normalizeUrl(input) strips utm_*, click ids
(gclid, fbclid, msclkid, …), and Loupe dev params
(api, key), sorts the remaining query pairs, and drops trailing slashes.
It's applied server-side on both write and the list filter.
normalizeUrl("/checkout?utm_source=x&gclid=123&step=2")
// → "/checkout?step=2"normalizeUrl on both
URLs first — they almost certainly collapse to the same key.Screenshots & storage
The SDK uploads a PNG to POST /v1/blobs and stores only the returned URL on the
comment. Two things are excluded before pixels ever leave the browser: any
[data-loupe-redact] region and Loupe's own UI.
- Capture engine. The SDK uses
modern-screenshot(DOM-based). The extension overrides this withchrome.tabs.captureVisibleTabfor real pixels, then crops to the element/region and paints over redacted areas (accounting fordevicePixelRatio). - Font embedding. Capture awaits
document.fonts.ready(capped at ~0.8s) with a 6s capture timeout and a hard outer timeout, so it renders with the right fonts but can never hang the widget. CORS-less fonts fall back to the pixel-perfect extension. - Blobs seam.
blobs.tsstores to local disk today (public by unguessable id); production swaps in S3/R2 with signed URLs.
Seams (extension points)
Four seams isolate the swappable parts. Change what's behind them, not their contracts.
| Seam | Where | Local | Production |
|---|---|---|---|
| Storage adapter | SDK store.ts / http-adapter.ts | localStorage / HTTP | HTTP |
| Database | server db.ts | PGlite | hosted Postgres via DATABASE_URL |
| Object storage | server blobs.ts | local disk | S3/R2 + signed URLs |
| Screenshot capture | SDK captureScreenshot config | modern-screenshot | extension captureVisibleTab |
Package
The embeddable browser widget — the thing a developer installs. Builds a Shadow-DOM host
(#loupe-root) so its CSS never leaks into the host page or vice-versa.
Public API
init(config: LoupeConfig): void— idempotent; requiresprojectKeyanduser.id. Starts onDOMContentLoaded.apiBaseset →HttpAdapter; omitted →LocalStorageAdapter(offline mode).destroy(): void— tears down the toolbar and all listeners.
Toolbar modes: Inspect & comment, Note (a free page-level
comment), Region shot, Comments. Drag the
◎ Loupe logo to move the whole bar anywhere on screen; click it to
collapse. A moved bar expands edge-aware so it's always on-screen — vertical against a left/right
edge, horizontal along the top/bottom.
LoupeConfig
| Field | Type | Purpose |
|---|---|---|
projectKey | string | Required. Project identifier. |
user | LoupeUser | Required. Who is commenting. |
userHmac | string? | HMAC-SHA256(user.id, secret), computed server-side. |
apiBase | string? | Set → HTTP mode; omitted → offline localStorage. |
autoOpen | boolean? | Open the toolbar immediately. |
captureScreenshot | fn? | Override (el) => Promise<string?> (the extension uses this). |
captureRegion | fn? | Region-shot override (rect) => Promise<string?>. |
headers | object? | Extra request headers (e.g. a CSRF token). |
credentials | RequestCredentials? | Fetch credentials mode (include for cross-origin cookies). |
Internal modules
fingerprint.ts
captureAnchor /
resolveAnchor — the re-anchoring engine.
capture.ts
captureElementContext +
captureScreenshot / captureRegionScreenshot.
http-adapter.ts
HttpAdapter — uploads the
blob first, stores only the URL.
store.ts
LocalStorageAdapter — the offline
fallback.
app.ts
LoupeApp — Shadow-DOM host: toolbar,
inspector, composer, pins, panel; repositions on scroll/resize + MutationObserver.
styles.ts
The Shadow-DOM stylesheet (theme vars on
:host so they inherit through the shadow tree).
The StorageAdapter contract: list(projectKey, url),
save(comment), update(id, patch), remove(id). A demo
product ("Acme Analytics") lives in demo/, served at /demo/.
Package
The API — node:http, Postgres, object storage, auth, and static hosting.
index.ts exports handler and start(port = PORT), and only
listens when run as the entry point (so tests mount handler on an ephemeral port).
HTTP routes
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | / | none | 302 → /dashboard/ |
| GET | /v1/health | none | { ok: true } |
| GET | /v1/blobs/:id | public | PNG, immutable cache |
| POST | /v1/blobs | authed | { projectKey, data } → { url } |
| GET | /v1/comments | authed | ?projectKey=&url= |
| POST | /v1/comments | authed | upsert; user may post only as self (else 403) |
| GET | /v1/comments/:id | authed | auth from the comment's project |
| PATCH | /v1/comments/:id | authed | patch status / body |
| DELETE | /v1/comments/:id | authed | 204 |
| GET | /dashboard/* · /demo/* · /sdk/* | static | served from disk |
CORS echoes the request Origin; allows
GET, POST, PATCH, DELETE, OPTIONS and headers Content-Type, X-Loupe-User,
X-Loupe-Hmac, X-Loupe-Admin, X-Loupe-Project. Body limit 12 MB.
Environment variables
| Variable | Default | Effect |
|---|---|---|
PORT | 8787 | Listen port. |
DATABASE_URL | — | Set → node-pg; unset → embedded PGlite. |
LOUPE_PG_DIR | ./data/pg | PGlite dir; memory:// → in-memory (tests). |
LOUPE_BLOB_DIR | ./data/blobs | Screenshot disk directory. |
LOUPE_PUBLIC_URL | localhost:8787 | Public base for blob URLs. |
LOUPE_DEMO_SECRET | sk_demo_acme_0f3b9c | Seed secret. |
Package
The Kanban triage board — vanilla TypeScript, tsup-bundled. Three columns (Open · In progress · Done) with a page filter, screenshot thumbnails, status moves (PATCH), two-step delete, Copy for Claude, and a ~4s live refresh.
Admin mode
Query params ?api / ?project /
?key. The key is sent as X-Loupe-Admin and persisted to
localStorage["loupe_admin"].
Session mode
Reads injected window.__LOUPE__ { api, project,
csrf } (used by the Laravel package) and sends X-CSRF-TOKEN instead of an
admin key.
Package
The MCP stdio server that exposes the backlog to Claude Code. Authenticates to the API as admin.
Install globally with npm i -g @loupekit/mcp (binary loupe-mcp), or point
Claude Code at the source entry directly.
What Claude gets
The natural-language request, the page URL, the target element (stable id or CSS path), its
outerHTML, a curated slice of computed styles, and the screenshot URL.
Tools
| Tool | Parameters | Returns |
|---|---|---|
list_comments | status?, url? | The backlog as a bulleted list. |
get_comment | id | The Claude-ready package (request + HTML + styles + screenshot URL). |
update_status | id, status | Moves the comment (PATCH). |
Environment
| Variable | Default |
|---|---|
LOUPE_API | http://localhost:8787 |
LOUPE_PROJECT_KEY | pk_demo_acme |
LOUPE_ADMIN_KEY | "" — sent as X-Loupe-Admin; required vs a real backend |
Transport is stdio via @modelcontextprotocol/sdk. The published package ships compiled
JS (dist/index.js via tsup), so npm i -g @loupekit/mcp gives a working
loupe-mcp binary; from a source checkout it runs directly with node index.ts.
See Quick start for the .mcp.json.
Package
An MV3 browser extension that reuses the exact SDK core — Loupe on any site, no install on the
target. The only difference from the SDK is the screenshot source:
chrome.tabs.captureVisibleTab gives real pixels, cropped to the element/region with
redaction, via the SDK's captureScreenshot override.
Parts
content.js— the injected widget (IIFE build artifact, gitignored)background.js— the capture workerpopup.html / popup.js— config + "Start Loupe on this tab"
Load it
- Permissions:
activeTab,scripting,storage - Build:
npm run build:extension - Load unpacked at
chrome://extensions(Developer mode)
Loupe for Laravel
loupekit/laravel
A Composer package that installs the entire loop into any Laravel app: an in-app widget, comments in your database, per-user access control, a Laravel-served triage dashboard, and a first-party MCP server — session-authenticated, no HMAC wiring, no secrets in the browser.
Requirements
- PHP 8.4+; Laravel 11 / 12 / 13 (9/10 intentionally unsupported).
- Any Eloquent-supported database — MySQL, PostgreSQL, SQLite, SQL Server.
- MCP is optional (
laravel/mcp ^0.8), guarded byclass_exists.
Install
composer require loupekit/laravel
php artisan loupe:install # config, migration, assets, provider stub
php artisan migrate
# add @loupeWidget before </body>, then open /loupe/dashboard
php artisan mcp:start loupe # expose the backlog to Claude (optional)loupe:install publishes the config, the migration, the browser assets to
public/vendor/loupe, and app/Providers/LoupeServiceProvider.php
(auto-registered in bootstrap/providers.php). The @loupeWidget directive
injects the SDK <script> + Loupe.init({...}) — but only for users who
pass loupe:use.
Publish individual pieces with the vendor:publish tags:
loupe-config, loupe-migrations, loupe-assets,
loupe-provider, loupe-views.
Routes
| Method | Path (under /{path}) | Authorize | Name |
|---|---|---|---|
| GET | v1/blobs/{id} | public | loupe.blobs.show |
| GET | v1/comments | use | loupe.comments.index |
| POST | v1/comments | use | loupe.comments.store |
| PATCH | v1/comments/{id} | use | loupe.comments.update |
| DELETE | v1/comments/{id} | use | loupe.comments.destroy |
| POST | v1/blobs | use | loupe.blobs.store |
| GET | dashboard | admin | loupe.dashboard |
Authorization
Two abilities gate everything: loupe:use (see the widget, create/update comments) and
loupe:admin (open the dashboard). Denied users never receive widget markup;
API/dashboard return 403.
Authorization is decided in this order:
Loupe::useWhen()/Loupe::adminWhen()closures (registered in code).- Config closures
authorize.use/authorize.dashboard. - Frictionless access in the
localenvironment, ifallow_in_local(default true). - Gate abilities
loupe:use/loupe:admin— deny by default (secure in production).
config:cache. If you cache config in production,
grant access via Gate abilities instead.Configuration
| Config · env | Default | Purpose |
|---|---|---|
enabled · LOUPE_ENABLED | true | Master switch. |
path · LOUPE_PATH | loupe | Route prefix. |
domain · LOUPE_DOMAIN | — | Route domain. |
project_key · LOUPE_PROJECT_KEY | app | Project identifier. |
middleware.api / .dashboard | ['web','loupe.auth'] | Route middleware stacks. |
guards · LOUPE_GUARDS | default guard | Ordered CSV of auth guards to try (e.g. web,admin). |
authorize.use / .dashboard | Gate abilities | Closures; null → the Gate abilities. |
allow_in_local | true | Frictionless auth in local. |
comment_model | Comment::class | Bring your own Eloquent model. |
user_resolver | — | Closure → {id, name, email?}. |
asset_url · LOUPE_ASSET_URL | app URL | Origin for bundled assets (bypasses ASSET_URL/CDN). |
disk · LOUPE_DISK | public | Filesystem disk for screenshots (loupe/screenshots). |
The dashboard is the exact @loupekit/dashboard bundle, vendored and configured via an
injected window.__LOUPE__ (API base, project key, CSRF) — no secret in the browser.
The data lands in a loupe_comments table (see the data model).
Multi-guard & CDN
Multiple auth guards
Apps using separate guards (e.g. web for users, admin for admins) could
render the widget under one guard and authenticate the API under another, mismatching ids and
producing a 403 "cannot post as another user". Set an ordered guard list and Loupe resolves
identity the same way everywhere — widget author, gates, API middleware
(loupe.auth), and the anti-spoof check.
LOUPE_GUARDS=web,admin # unset = the app's default guard (backward compatible)Behind a CDN / ASSET_URL
Loupe resolves its published assets from the app URL via
Url::asset(), bypassing ASSET_URL — so apps pointing ASSET_URL
at a CDN (with only their Vite build uploaded) don't get 404s and a silently missing widget. Serve
public/vendor/loupe elsewhere? Point LOUPE_ASSET_URL at it and re-run
vendor:publish --tag=loupe-assets --force.
MCP server (Laravel)
With laravel/mcp installed, a server named loupe is registered
automatically. Start it with php artisan mcp:start loupe. Unlike the Node MCP server,
the tools read the app's own database directly — no HTTP hop, no admin key.
list_comments(status?, url?)— the backlog, newest first.get_comment(id)— request + element HTML + computed styles + screenshot URL (or region rect).update_status(id, status)— validatesopen/in_progress/done.
For different-subdomain SPA frontends (Sanctum), add
EnsureFrontendRequestsAreStateful + auth:sanctum to
middleware.api and use credentials: 'include'.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Widget missing | User fails loupe:use, @loupeWidget not in layout, or LOUPE_ENABLED=false. |
| 419 on save | CSRF token missing — ensure the widget view sends X-CSRF-TOKEN. |
| 403 on dashboard | User fails loupe:admin. |
| Screenshots 404 | Disk not public — run php artisan storage:link. |
| 403 "cannot post as another user" | Multi-guard app — set LOUPE_GUARDS=web,admin. |
| Widget missing only behind a CDN | Set LOUPE_ASSET_URL; re-publish assets with --force. |
mcp:start unknown | Install laravel/mcp:^0.8. |
Feature guide
Cross-cutting SDK features ship simultaneously to the standalone SDK, the browser extension, and
the Laravel widget — all three share the @loupekit/sdk core.
| Feature | What it does | Since |
|---|---|---|
| Free comments | Drop a page-level note anywhere via the Note mode — no element, no screenshot (kind: "free"). | 0.5.0 |
| Draggable toolbar | Drag the ◎ logo anywhere; position persists and the bar expands edge-aware (vertical on a side, horizontal on top/bottom). | 0.5.0 |
| Re-anchoring | Multi-signal fingerprint survives redeploys; detaches rather than mis-pin. | 0.1.0 |
| Region shot | Drag a free box (no element); anchored to element under center as fractional coords — tracks reflow, viewport & scroll. | 0.2.0 |
| Offline mode | Omit apiBase → localStorage adapter. | 0.1.0 |
| Session auth (SDK) | headers + credentials config for CSRF / cookie auth. | 0.3.0 |
| Collapsible toolbar | Click ◎ Loupe to collapse to just the logo. | 0.3.3 |
| Font embedding | Awaits document.fonts.ready (capped) so screenshots use the right fonts, never hanging. | 0.3.4 / 0.4.1 |
| Device / viewport | Desktop/tablet/mobile badge end-to-end; deviceType() helper + DB column. | 0.4.0 |
| SPA per-page comments | Patches pushState/replaceState + popstate to reload comments on client-side navigation. | 0.4.0 |
| Mobile toolbar | Compact, icon-only, pinned left; consistent icon+label layout. | 0.4.0 |
| Instant region open | Composer opens immediately; capture runs in background, attached on submit. | 0.4.1 |
| Copy for Claude | Dashboard action copies a comment's full context to paste into Claude. | — |
| Redaction | [data-loupe-redact] regions painted over before pixels leave the browser. | 0.1.0 |
| Canary channel | Install prereleases with npm i @loupekit/sdk@next. | 0.2.0 |
Data model
Two tables in Postgres. Comments are keyed by normalized URL.
CREATE TABLE projects (
project_key TEXT PRIMARY KEY,
name TEXT,
secret TEXT,
allowed_origins TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE comments (
id TEXT PRIMARY KEY,
project_key TEXT REFERENCES projects ON DELETE CASCADE,
url TEXT,
status TEXT DEFAULT 'open',
body TEXT,
author JSONB, anchor JSONB, context JSONB, "offset" JSONB,
kind TEXT DEFAULT 'element', -- additive
region JSONB, viewport JSONB, -- additive
screenshot_url TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX comments_project_url ON comments (project_key, url);loupe_comments with a UUID id, a
timestamps pair, and an indexed denormalized author_id for ownership
checks. url is VARCHAR(500) and project_key
VARCHAR(191) so the composite index fits MySQL's key-length limit.Testing
Runner is Vitest + v8 coverage. DOM code runs under happy-dom (opt-in per file); the rest runs in Node.
npm test # run the suite once
npm run test:watch # watch mode
npm run test:coverage # text + HTML report in coverage/Coverage is ~91% lines / ~85% statements. Pure-logic modules sit at or near 100%
(server/store, server/blobs). Deliberately under-covered:
db.ts's pg branch (needs a live Postgres), the
modern-screenshot redaction filter, and the rAF/MutationObserver-driven app code that
needs a real layout engine. The extension's content.src.ts is validated by the
manifest test + build rather than executed (it needs Chrome APIs + canvas).
The Laravel package runs an Orchestra Testbench suite across every PHP × Laravel combination in
CI, under a 100% line-coverage gate
(composer test:coverage-100).
Releasing & publishing
Loupe uses Semantic Versioning and ships through GitHub Releases
backed by annotated tags (vX.Y.Z). MAJOR = breaking change to a public contract (SDK
init, HTTP routes, MCP tool shapes, DB schema); MINOR = compatible features;
PATCH = compatible fixes.
CHANGELOG.md, (2) the landing-page
version, (3) the wiki, (4) each published package's version. CI fails the release if
CHANGELOG.md has no entry for the tag.Two channels, two registries
| Channel | Trigger | dist-tag | npm | GitHub Packages |
|---|---|---|---|---|
| Canary | every push to main | next | @loupekit/* | @mohamed-ashraf-elsaed/* |
| Stable | tag vX.Y.Z | latest | @loupekit/* | @mohamed-ashraf-elsaed/* |
Publishable to npm: @loupekit/shared, @loupekit/sdk,
@loupekit/mcp. Publish shared first — mcp imports it at
runtime. The Laravel package is split to its own repo by CI and picked up by Packagist; one
vX.Y.Z tag ships npm + Packagist together. The extension ships to the Chrome Web Store.
Changelog
Follows Keep a Changelog + Semantic Versioning. Current release: 0.5.2 (2026-07-15).
loupe-mcpbinary now actually starts. After 0.5.1 shipped compiled JS, the installed binary still exited without serving: the entrypoint guard comparedimport.meta.urltoargv[1], but the npmbinis a symlink — Node resolves it forimport.meta.urlwhileargv[1]stays the symlink path. The check now resolves symlinks before comparing, soloupe-mcp(and Glama-style introspection) works.
@loupekit/mcpnow installs and runs from npm. It shipped its rawindex.tsand relied on runtime type-stripping, but Node won't strip types undernode_modules— sonpm i -g @loupekit/mcp+loupe-mcpfailed for every consumer (and blocked containerized introspection on directories like Glama). It now compiles todist/index.js(tsup) withbin/mainpointing there. Running from a checkout (node packages/mcp/index.ts) is unchanged.
- Free comments. A new Note toolbar mode drops a page-level
comment anywhere — no element, no screenshot. New
kind: "free", positioned via the existingoffsetfield as a document fraction, so no DB migration. Renders as a page-level item in the panel, dashboard, and both MCP servers. - Draggable, edge-aware toolbar. Drag the
◎logo anywhere; the position persists and the bar expands inward so it's always on-screen — vertical on a left/right edge, horizontal along the top/bottom. - All packages aligned to
0.5.0—@loupekit/server, the extensionmanifest.json, and the NodeMcpServerwere stale at0.2.0.
- Laravel: multi-guard identity resolution. Multi-guard apps no longer 403 with
"cannot post as another user". Identity resolves through an ordered guard list
(
LOUPE_GUARDS) used everywhere — widget author, gates, API middleware (loupe.auth), anti-spoof. Backward compatible.
- Laravel: assets load behind a CDN. Loupe resolves published assets from the
app URL via
Url::asset(), bypassingASSET_URL. NewLOUPE_ASSET_URLoverride.
- Captures never hang. Font wait capped (~0.8s), capture timeout lowered to 6s, every capture wrapped in a hard outer timeout.
- Region shot opens instantly. Composer appears immediately; screenshot captured in the background and attached on submit.
- Device / viewport tracking (end to end) — desktop/tablet/mobile badge in the
panel, dashboard, and MCP payload;
deviceType(width)helper + DB column. - Per-page comments on SPA navigation — reloads comments on client-side route changes.
- Mobile toolbar — compact, icon-only, pinned left.
- Region-shot renders the smallest covering element (faster, reliable font embedding).
- Screenshots no longer fall back to system fonts. Capture awaits
document.fonts.ready(30s timeout) before rasterizing.
- Collapsible toolbar across all SDK surfaces.
- Laravel: frictionless in
local(allow_in_local, default true); baseline gates now deny by default.
- Laravel migration on MySQL.
url→VARCHAR(500),project_key→VARCHAR(191)so the composite index fits MySQL's key-length limit.
- SEO / GEO — schema.org author & entity metadata, AI-crawler allow-list,
llms.txt. - Release/CI — synced
package-lock.jsonsonpm cipasses on tags.
- Loupe for Laravel (
loupekit/laravel) — the whole loop as a Composer package; PHP 8.4+, Laravel 11/12/13, 100% line-coverage gate. - SDK gained
headers+credentialsconfig; dashboard readswindow.__LOUPE__for session auth.
- Professional npm READMEs for
sdk,mcp,shared. No code changes.
- Free-region screenshots ("Region shot") — anchored to the element under its
center as fractional coords; tracks reflow, a different viewport, and scrolling. New
captureRegionhook. - Automated dual-registry publishing (
nextcanary +lateststable). - Fixed SDK Shadow-DOM theming — vars moved to
:hostso they inherit.
- The first release — the full loop, end to end:
sdk,server,dashboard,mcp,extension,shared. Vitest suite (~91%), Mermaid architecture docs, a GitHub Wiki, and an SEO/GEO landing page.