👷‍♂️

開発日記: dbt fusion mcpを作ってみた

に公開

最近、dbt fusionのmcpサーバを作って、モデル・カラムリネージをAIエージェントに渡すことができました。
https://x.com/myshmeh/status/1974978689440919800

動くところまでは持って行けたので、dbt fusion mcpの開発に至るまでの過程を日記として綴ろうと思います。

きっかけ

正直、興味ドリブンです。

SerenaがIDEで開発者に与えられる情報をCoding Agentに与えて生成コードの品質を上げたならば、同様にFusionのVSCode拡張の情報で、より良質なモデル生成ができるのではと思いました。

今回実装した、モデル・カラムリネージを出力するツールは、その取り組みの一部です。

前提

dbt Fusionの制限

執筆時点では、dbtfはモデル・カラムリネージのインターフェースを提供してない認識です。
https://zenn.dev/myshmeh/articles/a27d5f48acaa91

Serenaと言語サーバー

Serenaの機能の要は、各プログラミング言語の言語サーバーと通信することで達成されています。ここでは便宜上、言語サーバーを、LSP (Language Server Protocol)という標準に従い実装された、IDEにintellisenseやリファクタなどの便利機能を提供するプログラムとします。

LSPの詳細に関しては、以下を参照ください。
https://engineer.crowdworks.jp/entry/2025/07/01/095126

Serenaは、言語サーバーを通じて、クラス・関数・変数などのコード中のシンボルを検索しています。これによって、Coding Agentはプログラム全体を読まずに、端的な形でプログラムを理解することができるわけです。Serenaの詳しい解説は、以下を参照ください。
https://zenn.dev/sc30gsw/articles/ff81891959aaef

dbt Fusionと言語サーバー

現代のプログラミング言語のVSCode拡張は、ほとんど言語サーバーを使って実装されています。それは、dbt Fusionの拡張も例外ではありません。
dbt FusionのVSCode拡張をインストールすると、実はmacOSでは以下のパスにdbt-lspという言語サーバーがインストールされます。

/Users/<username>/Library/Application Support/Code/User/globalStorage/dbtlabsinc.dbt/bin

これを見つけて、SerenaとFusionて連携できるのでは!と思い立ちました。

Serenaとの連携は頓挫

まず、Serenaの想定する通信プロトコル(stdio)とdbt-lspのそれ(tcp)がが合わず、proxy serverの作成から作業が始まりました。

最終的にproxy serverを介してSerenaにdbt-lspを連携させることはできたのですが、動作確認中にdbt-lsptextDocument/documentSymbolというLSPメソッドを実装してないことに気づきました。このメソッドは、コード中のシンボルを取得するもので、Serenaのコード検索機能の根幹を担うものでした。つまり、これなしではSerenaの重要な機能をほとんど提供できません。

よって、Serena x dbt-lspのアプローチは諦めることにしました。
先に実現可能性を確保すべきだったなと反省しました...

自前MCP実装に切り替える

Serena連携ができないとわかり、dbt-lspが実装しているコード理解に使えそうなメソッドを、直接MCPとして公開するアプローチに切り替えました。

モデル・カラムリネージは、VSCode拡張を使う中でまず思い浮かんだ、コード理解に役立ちそうな情報です。

リネージ取得方法の調査

VSCodeでは、LSP Log機能で、エディタ・言語サーバー間のやり取りを参照することができます。

リネージ情報を表示してlogを見てみると、以下のようなリクエストを発行してることがわかりました。

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "workspace/executeCommand",
  "params": {
    "command": "dbt.listNodes",
    "arguments": [
      "+model.jaffle_shop.customers+"
    ]
  }
}

workspace/executeCommandは、サーバー上で任意のコマンドを実行するメソッドです。
dbt.listNodesコマンドで任意のselectorを指定することで、以下のような条件に合うリネージ情報を返却していました。

{
  "jsonrpc": "2.0",
  "id": 28,
  "result": {
    "error": null,
    "grain": "project",
    "nodes": [
      {
        "access": "protected",
        "alias": "customers",
        "checksum": {
          "checksum": "664488ec4c2dc2e6b48f35ec05d81258c5c7716af3f044435815046378f1d224",
          "name": "SHA256"
        },
        "columns": {
          "COUNT_LIFETIME_ORDERS": {
            "config": {},
            "constraints": [],
            "data_type": "NUMBER(18,0)",
            "meta": {},
            "name": "COUNT_LIFETIME_ORDERS",
            "tags": []
          },
          /* ... */
        },
        "config": {
          /* ... */
        },
        "constraints": [],
        "contract": null,
        "database": "...",
        "depends_on": {
          "macros": [],
          "nodes": [
            "model.jaffle_shop.stg_customers",
            "model.jaffle_shop.orders"
          ],
          "nodes_with_ref_location": [
            [
              "model.jaffle_shop.stg_customers",
              {
                "col": 19,
                "expanded": null,
                "file": "models/marts/customers.sql",
                "index": 40,
                "line": 5
              }
            ],
            [
              "model.jaffle_shop.orders",
              {
                "col": 19,
                "expanded": null,
                "file": "models/marts/customers.sql",
                "index": 103,
                "line": 11
              }
            ]
          ]
        },
        "deprecation_date": null,
        "description": "Customer overview data mart, offering key details for each unique customer. One row per customer.",
        "enabled": true,
        "event_time": null,
        "fqn": [
          "jaffle_shop",
          "marts",
          "customers"
        ],
        "freshness": null,
        "group": null,
        "incremental_strategy": null,
        "language": "sql",
        "latest_version": null,
        "materialized": "table",
        "metrics": [],
        "name": "customers",
        "name_span": {
        },
        "original_file_path": "models/marts/customers.sql",
        "package_name": "jaffle_shop",
        "patch_path": "models/marts/customers.yml",
        "path": "models/marts/customers.sql",
        "primary_key": [],
        "quoting": {
          "database": false,
          "identifier": false,
          "schema": false
        },
        "quoting_ignore_case": false,
        "raw_code": "--placeholder--",
        "refs": [
          {
            "name": "stg_customers"
          },
          {
            "name": "orders"
          }
        ],
        "relation_name": "...",
        "resource_type": "model",
        "schema": "dev",
        "snowflake_attr": {},
        "sources": [],
        "static_analysis": "on",
        "tags": [],
        "time_spine": null,
        "unique_id": "model.jaffle_shop.customers",
        "version": null
      },
      /* ... */
    ]
  }
}

さらに調査を進めると、argumentsに指定したselectorの記法は以下であることがわかりました。

  • model.<project_name>.<model_name>: model_nameのモデルリネージ取得
  • column:model.<project_name>.<model_name>.<column_name>: model_name.column_nameのカラムリネージ(column_nameは大文字でないと動作しなかった)
  • dbt cliのselectorと同様、prefix, suffixに+をつけることで、upstream, downstreamのモデル情報を表示することができる
  • model.*で全モデルリネージ取得が可能

ここまで確認できれば、あとは実装するのみです。

実装

mcpのツールとして、dbt-lspに対してdbt.listNodesを実行する仕組みを実装しました。

実装したツールは以下の通りです。

  • get_all_model_lineage(verbose=false): 全モデルのリネージ取得
  • get_model_lineage(modelName, verbose=false): +modelName+のリネージ情報
  • get_column_lineage(modelName, columnName, verbose=false): +modelName.columnName+のリネージ情報

verboseは、dbt-lspの返り値をそのまま返すか、ミニマムなものだけ返すか制御可能にしています。

今回は、シンプルにツール呼び出しごとにdbt-lspと新しいセッションを作り、initalize -> compile -> dbt.listNodesまで流す構成にしました。

実装結果

https://x.com/myshmeh/status/1974978689440919800
ポストした通りです。

いくつかプロンプトを投げてみましたが、jaffle-shopという小さいプロジェクトで試していることもあってか、モデル開発に見違えるように良くなったことは体感できてません。。

興味本意で進めてしまったので、探索的に開発するにももう少し評価方法を考えて進めたいなという反省の気持ちがあります。

dbt開発にSerena的アプローチは必要なのか?

tsudash0さんのもくもく会で本開発の話をした時に、そもそもclaude codeのタスクが簡単になるような仕組みづくりを頑張った方が筋が良さそうという金言をいただきました(率直に意見くださり大変ありがたい。。)。
https://x.com/syou6162/status/1975162373905252602

完全同意です。最近sonnet 4.5が出たりとコード生成の質はますます高まってますし、(上がって悪いことはないが)AIを能力を上げようとするより、もしもの時のガードレールを確保する方向にfusionの活用方法を考えた方が良いなと思いました。

だが、リネージ情報は欲しいぞ

一方で、モデル・カラムリネージ情報は厳密なカラム説明の伝播など、色々な活用の道はありそうです。

なのに、dbt community slackによれば、既にインターナルではlineage情報を出すコマンドがあるが、公開はしていないようです。。なぜだ。。

the answer I'd give if we had a scarier PR team: the lineage subcommand is not a documented feature of the dbt Fusion engine
But actually, yeah [community-member] is right! This is mostly SDF ghosts :ghost: (see also lint :sweat_smile:)
The command does work for internal staff for debugging, but it shouldn't really be exposed here

fusionのcliがこれのインターフェースを提供しないのは本当に辛いので、workaroundとしてdbt-lspからリネージを引っこ抜く需要はあるのかなと感じております。

まとめ

そんな、なんか勢いで作っちゃった結果、なんともいえないブツを生み出したという日記でございました。

とはいえ、dbt-lspで解決できる課題はたくさんあると思うので、課題ドリブンで活用方法を模索したいと思います!

Discussion