Skip to main content
Glama

ui-lift

公開サイトを取り込み、そこで使われているUIコンポーネントとデザイントークンを、 AIがMCP経由で実装に使える形で取り出すツールです。主な利用者はAI(Claude Code等の コーディングエージェント)で、人はMCPの open_gallery が開くギャラリーで取り込み 結果を眺め、不要なものを間引いてライブラリの品質を保ちます。

これは何ではないか

デザインの参考にするためのツールであり、取り込んだサイトの複製・再公開を 目的としません。

  • 画像・ロゴ・フォントの実体(バイナリ)はダウンロードしません。加えて、 取り込み結果には元サイトを指すアセットURL参照も残りません(例外は 下記の「意図的に残る参照」の1点のみです)。属性は「捨てるものを列挙する」 のではなく「残すものを列挙する」許可リスト方式です(capture/snapshot.pyRETAINED_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(...), autoauto のようなキーワードフォールバックは、 元サイトの参照を持たない限りそのまま残ります。全レイヤーが url(...) だけだった場合(安全に残せる断片が何もない場合)に限り、値全体を none に置き換えます。ページの表示自体にはブラウザが元サイトから画像・フォント を取得しますが、そのバイナリやURLがui-liftの保存物(スナップショット/ ライブラリ/生成コード)に載ることはありません。

    • background-imagelinear-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_componentsget_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に保存されるのみです)。

formathtml-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-csstailwind を使ってください。

  • tailwind: tailwind.config.jstheme.extend スニペット+(コンポーネント 指定時は)Tailwindユーティリティクラス付きマークアップ。

kindbutton / input / nav / card / table / list-item / badge / hero / block の9種類です。DOM構造とスタイルからの推定なので 外れることがあります。ギャラリーの「種類」ボタンで直せます。

Tailwind出力のキーに uil- 接頭辞が付く理由

tailwind.config.jstheme.extend はTailwindの組み込みスケールに マージされ、同名キーを上書きします(別名前空間を作るのではありません)。 つまり生成した spacing["2"]borderRadius["md"] をそのまま貼り付けると、 既存プロジェクトの p-2rounded-md の意味を静かに書き換えてしまいます。 これを避けるため、生成するスケールキーには必ず uil- を前置します (spacing["uil-2"], borderRadius["uil-md"] のように)。Tailwindの 組み込みキーは数字や sm/md/lg のような短い名前なので、uil- を 付けることで衝突がなくなることを保証しています。

使い方の流れ

  1. ingest_site でサイトを取り込む

  2. open_gallery で結果を眺め、ゴミを捨てて(keep=false)、必要なら名前と 種類を直す

  3. 以降は search_componentsget_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.screenshotStore.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サーバから配信し、 それに対してのみ実行します。

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jinshin2534/ui-lift'

If you have feedback or need assistance with the MCP directory API, please join our Discord server