ui-lift
Imports public websites and extracts UI components and design tokens, making them available via MCP for AI coding agents to reuse in implementations.
README
ui-lift
公開サイトを取り込み、そこで使われているUIコンポーネントとデザイントークンを、
AIがMCP経由で実装に使える形で取り出すツールです。主な利用者はAI(Claude Code等の
コーディングエージェント)で、人はMCPの open_gallery が開くギャラリーで取り込み
結果を眺め、不要なものを間引いてライブラリの品質を保ちます。
これは何ではないか
デザインの参考にするためのツールであり、取り込んだサイトの複製・再公開を 目的としません。
- 画像・ロゴ・フォントの実体(バイナリ)はダウンロードしません。加えて、
取り込み結果には元サイトを指すアセットURL参照も残りません(例外は
下記の「意図的に残る参照」の1点のみです)。属性は「捨てるものを列挙する」
のではなく「残すものを列挙する」許可リスト方式です(
capture/snapshot.pyのRETAINED_ATTRS/_prune_attrs()):type,alt,role,disabled,checked,required,readonly,selected,multiple,open,controls,placeholder,aria-*だけが値ごと残り、それ以外の属性は タグを問わずすべて破棄されます。したがって<img src>/srcset、<video poster>、<source src>はもちろん、<iframe src>、<embed src>、<object data>、<track src>、<body background>、 SVG の<image href>のように個別に列挙していなかった参照も、次に 出てくる未知のタグ・属性も同じ規則で自動的に落ちます。href(<a>/<area>/<link>など)だけは特別扱いで、仕様が「値」ではなく 「有無」を残すとしているとおり、値を空文字列""に置き換えて 「href が存在した」という構造情報だけを残します。altなどの意味を持つ 属性と、寸法・レイアウトを表すrectは変わらず残ります。CSS側は特定の プロパティ名を決め打ちにせず、CAPTURED_PROPS(取得対象のCSSプロパティ 一覧)に含まれるどのプロパティであっても、値にurl(...)参照が1つでも 含まれていれば取り除きます(background-imageはもちろん、カスタム カーソル画像を指定するcursor: url(...)のような他のプロパティでも 同様です)。値はカンマ区切りの「レイヤー」の並びとして扱い、url(...)を 含むレイヤーだけを落とします。linear-gradient()のような他のレイヤーや、cursor: url(...), autoのautoのようなキーワードフォールバックは、 元サイトの参照を持たない限りそのまま残ります。全レイヤーがurl(...)だけだった場合(安全に残せる断片が何もない場合)に限り、値全体をnoneに置き換えます。ページの表示自体にはブラウザが元サイトから画像・フォント を取得しますが、そのバイナリやURLがui-liftの保存物(スナップショット/ ライブラリ/生成コード)に載ることはありません。background-imageがlinear-gradient(...)とurl(...)を1つの値の 中で組み合わせている場合、url(...)レイヤーだけが落ち、グラデーション レイヤーは残ります。全レイヤーがurl(...)のときだけnone(背景なし) になり、複数の背景画像を重ねていたページでは結果がフラットな背景に 見えることがあります。- 意図的に残る参照:
get_component/get_design_tokensの出力先頭に 付く出典コメント(下記)には、取得元ページの URL(source_url、 ページ全体のURLであってアセットURLではない)がそのまま文字として 載ります。これは帰属表示のための意図した仕様で、render/html_css.pyの_neutralize_html_comment_valueにより HTML コメント終端シーケンス (-->/--!>)は無害化されるため、値がコメントを抜け出して生きた マークアップになることはありません。この1箇所以外に、元サイトを指す 参照が出力に残る経路は現時点でありません。 <style>要素に埋め込まれる CSS 値(StyleValue.raw、取得したページの computed style をそのまま使う)についても、</style>のような HTML の 生テキスト終了パターンを含む宣言は丸ごと破棄します(render/html_css.pyの_declaration)。HTML は<style>を「生テキスト要素」として扱い、 CSS の文字列・コメント構文に関係なく</+ 英字で終端するため、</style>の文字列一致だけでは</style foo>や</STYLE>を防げず、 ブラウザのgetComputedStyleが返した値をそのまま埋め込む以上、CSS引用符 の外側にリテラルに現れうる値として一般化して扱う必要があるためです。
- 元サイトの
class/idは保存しません(構造シグネチャの安定のためにも 意図的に捨てています)。生成されるクラス名はuil-<kind>形式の独自命名です。 - robots.txt を尊重します。
Disallowされているパスは取り込み時に例外 (RobotsDisallowed) になり、取り込まれません。 - ログインが必要なページは対象外です。ブラウザは毎回まっさらなコンテキストで 開くだけで、Cookie・認証情報を扱う仕組みは持ちません。
get_component/get_design_tokensが返すコードには、必ず取得元URLと 取得日時(と「デザインの参考用であり複製を目的としない」旨)がコメントとして 先頭に入ります。
セットアップ
uv sync
uv run playwright install chromium
Playwrightは取り込み(ingest_site)のたびにHeadless Chromiumを起動します。
playwright install chromium を済ませていないと、取り込み時にブラウザ未検出の
エラーになります。
Claude Code への登録
claude mcp add ui-lift -- uv --directory /path/to/ui-lift run ui-lift
/path/to/ui-lift はこのリポジトリの絶対パスに置き換えてください。PyPIには
まだ公開していないので uvx ui-lift は使えません。
MCPツール
| ツール | 用途 |
|---|---|
ingest_site(url, viewports=["desktop"], min_occurrences=2) |
サイトを取り込む(ネットワークアクセスあり) |
reanalyze_site(site, min_occurrences=2) |
保存済みスナップショットから、ネットワークに触れずコンポーネント・トークンを作り直す |
list_sites() |
取り込み済みサイト一覧 |
search_components(query=None, kind=None, site=None, limit=20) |
検索(要約のみ返す) |
get_component(id, format="html-css", variant="default") |
貼れるコードを取り出す |
get_design_tokens(site, format="css-vars") |
トークン一式を取り出す |
open_gallery(site=None) |
ギャラリーを生成しブラウザで開く |
search_components と get_component を分けているのは、AIが検索結果を全部
読み込まずに済むようにするためです。検索は軽い要約(id / site / kind / name /
occurrences / summary)だけを返し、1つに絞ってから重いコードを get_component
で取りに行く想定です。
viewports に渡せるのは mobile(390×844) / tablet(820×1180) / desktop
(1440×900) の3つです。複数指定すると、desktop(無ければ指定した中の最初の
ビューポート)を基準に、他のビューポートとの間でスタイルの差分を
Component.responsive にビューポートごと・ノードパスごとで記録します。
副ビューポートの解析はベストエフォートで、失敗しても ingest_site の結果の
failed_viewports に記録されるだけで取り込み全体は失敗しません。ただし現状
responsive の中身をレンダー出力やMCPのレスポンスとして直接取り出す経路は
まだありません(ライブラリのJSONに保存されるのみです)。
format は html-css / css-vars / tailwind の3つです。
html-css: HTML断片+<style>ブロック。クラス名はuil-<kind>または そこにタグ・パスを付与したもの。単独では自己完結しません: トークンに乗った値はvar(--color-brand)のような参照のまま出力され、 そのカスタムプロパティ自体はここでは定義されません(定義するのはcss-vars/get_design_tokensの役目)。実装先のページに同じトークン 定義がまだ無い場合はget_design_tokens(site, format="css-vars")も 合わせて取得し、一緒に貼り付けてください。get_componentのレスポンス のnotesにもこの旨が明記されます。ギャラリー(後述)のプレビューは この理由から、各コンポーネントのhtml-css出力だけでなくcss-vars出力もあわせて同じ<iframe srcdoc>に埋め込んでいます。css-vars::root { --color-brand: #...; }形式のCSSカスタムプロパティ。 トークンのみを扱う形式で、個々のコンポーネントの構造には触れません。get_component(..., format="css-vars")はエラーにならず、指定したid/variantを無視してサイト全体のトークンをそのまま返します (実質get_design_tokens(site="css-vars")と同じ結果)。この経路を 使ったことに呼び出し側(AI)が気づけるよう、レスポンスのnotesに 「id / variant は無視されている」旨が明記されます。出典コメント(取得元 URL・取得日時)はget_component/get_design_tokensのどちらの経路 でも必ず付きます。コンポーネント個別のコードが欲しい場合はhtml-cssかtailwindを使ってください。tailwind:tailwind.config.jsのtheme.extendスニペット+(コンポーネント 指定時は)Tailwindユーティリティクラス付きマークアップ。
kind は button / input / nav / card / table / list-item /
badge / hero / block の9種類です。DOM構造とスタイルからの推定なので
外れることがあります。ギャラリーの「種類」ボタンで直せます。
Tailwind出力のキーに uil- 接頭辞が付く理由
tailwind.config.js の theme.extend はTailwindの組み込みスケールに
マージされ、同名キーを上書きします(別名前空間を作るのではありません)。
つまり生成した spacing["2"] や borderRadius["md"] をそのまま貼り付けると、
既存プロジェクトの p-2 や rounded-md の意味を静かに書き換えてしまいます。
これを避けるため、生成するスケールキーには必ず uil- を前置します
(spacing["uil-2"], borderRadius["uil-md"] のように)。Tailwindの
組み込みキーは数字や sm/md/lg のような短い名前なので、uil- を
付けることで衝突がなくなることを保証しています。
使い方の流れ
ingest_siteでサイトを取り込むopen_galleryで結果を眺め、ゴミを捨てて(keep=false)、必要なら名前と 種類を直す- 以降は
search_components→get_componentでAIが必要なものを取り出す
2 の選別をやるほど 3 の質が上がります。同じURLをingest_siteで再取り込み
すると、人が付けた名前・種類・keep(残す/捨てる)の判定はそのまま引き継がれ
ます。 前回あったコンポーネントが今回のページに見当たらなくなった場合も
削除はせず stale=true を立てるだけです(再度現れれば stale は自動的に
外れます)。stale のコンポーネントは検索から除外されません
(削除ではなく印を付けるだけにしているのが目的なので、引き続き
search_components / get_component で取り出せます)が、結果の各項目に
stale が含まれるので呼び出し側(AI)は見分けられ、出現数が同じ場合は
stale のものが後ろに並びます。
ギャラリー
open_gallery はローカルのみで待ち受けるHTTPサーバ(127.0.0.1、ポートは
自動割当)を起動し、既定のブラウザでそのURLを開きます。各コンポーネントは
sandbox="" を付けた <iframe srcdoc="..."> の中に個別描画されます。これは、
取り込んだページ由来のCSS(クラス名がコンポーネント間で衝突しうる、任意の
セレクタを含みうる等)がギャラリー自身のUI(他のタイルやボタン)を壊したり
隠したりしないようにするためです。iframeの外に一切漏れません。
画面でできること:
- 名前 ボタン: コンポーネント名を変更
- 種類 ボタン:
kindを直接入力し直す(無効な値はサーバ側のPOST /editが400で拒否) - 捨てる ボタン:
keep=falseにする。以後search_componentsの結果や ギャラリー自体(次回生成時)から外れます(ライブラリのJSONからは消えません)
サムネイル画像は撮っていません。ギャラリーは html-css 出力をその場で
レンダリングして並べる作りなので、事前に画像を用意する必要がありません
(Component.screenshot と Store.thumb_path は将来のための予約フィールドで、
現状どこからも書き込まれません)。
保存先
既定は ~/.ui-lift/。環境変数 UI_LIFT_HOME で変更できます。
<UI_LIFT_HOME>/library/<site-slug>/
site.json # URL・取得日時・デザイントークンを含むサイト情報
components/<id>.json # コンポーネント(IR)。keep=false でも消えず残る
snapshots/<viewport>.json # 再解析用の生スナップショット(ビューポートごと)
site-slug はURLのホスト名から生成されます(例: https://example.com/ →
example-com)。
スナップショットを残しているので、検出アルゴリズムを改善したときに再取得
せずにライブラリ全体を作り直せます。実際にそれをやるのが reanalyze_site
MCPツールです。指定したサイトの保存済みスナップショット(全ビューポート)
を読み、ingest_site と全く同じマージ(人が付けた名前・種類・keep の
引き継ぎ、消えたコンポーネントへの stale 印付け)を経てライブラリを
更新します。ネットワークには一切触れません。保存済みスナップショットが
1つも無いサイトを指定すると、そのサイト名を含むエラーになります。
開発
uv run pytest
analyze/ と render/ は純粋関数のみで構成されており、テストは保存済みの
スナップショットJSON(または手書きのフィクスチャ)に対して行います。テスト内で
外部サイトにはアクセスしません。E2Eテスト(tests/test_e2e.py)は
tests/fixtures/pages/ のローカルHTMLをテスト内で自前のHTTPサーバから配信し、
それに対してのみ実行します。
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.