every Tech Blog

株式会社エブリーのTech Blogです。

Go のエラーの扱いを振り返る

目次

この記事は every Tech Blog Advent Calendar 2025 の 1 日目の記事です。

はじめに

こんにちは、開発本部開発 1 部トモニテグループのエンジニアの rymiyamoto です。アドベントカレンダートップバッターを務めさせていただきます!

今回はまだ時期尚早ですが Go1.26 で errors.AsType が導入されることが予定されており、それに伴うエラーの扱いについて振り返ってみたいと思います。

tip.golang.org

※ この記事は執筆時点で最新の Go1.25.4 をベースに書いています。

Go でのエラー構造

Go のエラーは単なる Error メソッドを持つだけのインターフェースです。
このインターフェースを担保した型は error 型として扱うことができます。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

github.com

この状態でのエラーでは単純に Error メソッドを呼び出して文字列を取得するだけです。そのため、エラーの種類を識別するために文字列の比較を行うことになってしまいます。またそのままエラー同士の比較もできますが、これはエラーの値が完全に一致しない限り false になってしまいます。

そのため Go1.13 のタイミングで再帰的エラーハンドリングが導入されました。

go.dev

再帰的エラーハンドリング

発生したエラーに対して新たな情報を追加し、エラーチェーンを構築するアプローチです。この手法により、エラーが発生した元のコンテキストから、そのエラーをキャッチして処理した箇所までの全体像を把握することが可能になります。

実際に fmt.Errorf%w 返しているエラーの型は以下のようになっています。

type wrapError struct {
    msg string // 全体のエラーメッセージ
    err error // ラップ元のエラー
}

func (e *wrapError) Error() string {
    return e.msg
}
func (e *wrapError) Unwrap() error {
    return e.err
}

github.com

これによりラップする前のエラーを Unwrap メソッドで取得することができ、階層的な構造になっていてもエラーを辿ることができます。

エラーハンドリングのパターン

errors.As で値取り出してチェック

errors.As の実装は以下のようになっており、処理の流れをまとめるとこのようになります。

import (
    "internal/reflectlite"
)

func As(err error, target any) bool {
    if err == nil {
        return false
    }
    if target == nil {
        panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    targetType := typ.Elem()
    if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
        panic("errors: *target must be interface or implement error")
    }
    return as(err, target, val, targetType)
}

func as(err error, target any, targetVal reflectlite.Value, targetType reflectlite.Type) bool {
    for {
        // 現在のエラー値が、ターゲットの型に代入可能かをチェック
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            targetVal.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        // エラーが独自の As メソッドを実装している場合、それを呼び出してチェック
        if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
            return true
        }
        // エラーチェーンを辿る: Unwrap() メソッドでラップされた下位のエラーを取得
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            // 単一のエラーをラップしている場合: アンラップして次のループで再チェック
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            // 複数のエラーをラップしている場合: それぞれに対して再帰的にチェック
            for _, err := range x.Unwrap() {
                if err == nil {
                    continue
                }
                if as(err, target, targetVal, targetType) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

var errorType = reflectlite.TypeOf((*error)(nil)).Elem()

github.com

これにより比較対象のエラーの型に代入可能かをチェックし、可能であればそのエラーの値を取得することができます。

var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) {
    fmt.Println("MySQL error occurred:", mysqlErr.Number)
}

細かいですが errors.As のターゲットは必ずポインタである必要があります。これは、Go が関数の引数を値渡しするため、エラーチェーン内の見つかったエラーをターゲット変数に実際に書き込む(代入する)ためには、呼び出し元で定義した変数のメモリアドレス(ポインタ)を渡す必要があるためです。

errors.Is で値の一致

errors.Is の実装は以下のようになっており、基本の流れは errors.As と同じですが、比較対象のエラーの型に代入可能かをチェックする代わりに、現在のエラー値が、比較対象のターゲットエラー(target)と厳密に等しいか(err == target)をチェックします。

func Is(err, target error) bool {
    if err == nil || target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    return is(err, target, isComparable)
}

func is(err, target error, targetComparable bool) bool {
    for {
        // 現在のエラー値が、ターゲットエラーと厳密に等しいか(err == target)をチェック
        if targetComparable && err == target {
            return true
        }
        // エラーが独自の Is メソッドを実装している場合、それを呼び出してチェック
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // エラーチェーンを辿る: Unwrap() メソッドでラップされた下位のエラーを取得
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            // 単一のエラーをラップしている場合: アンラップして次のループで再チェック
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            // 複数のエラーをラップしている場合: それぞれに対して再帰的にチェック
            for _, err := range x.Unwrap() {
                if is(err, target, targetComparable) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

github.com

これにより比較対象のエラーと厳密に等しいか(含んでいるか)をチェックし、等しい場合は true を返します。

var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) {
    fmt.Println("not found")
}

Go1.26 で追加予定の errors.AsType

これまで errors.As では都度エラーを代入するためのポインタ変数を定義する必要がありましたが、Go1.26 では errors.AsType が追加されることで、エラーを代入するための変数を定義する必要がなくなります。呼び出し方からわかるように Go 1.18 で導入されたジェネリックを活用しています。

// ~ Go1.25
func FindMysqlErrorCode(err error) (bool, uint16) {
    var mysqlErr *mysql.MySQLError
    if errors.As(err, &mysqlErr) {
        return true, mysqlErr.Number
    }
    return false, 0
}

// Go1.26 ~
func FindMysqlErrorCode(err error) (bool, uint16) {
    if mysqlErr, ok := errors.AsType[*mysql.MySQLError](err); ok {
        return true, mysqlErr.Number
    }
    return false, 0
}

実装の方も基本はこれまでの errors.As と同様になっており、処理の流れはこのようになります。

func AsType[E error](err error) (E, bool) {
    if err == nil {
        var zero E
        return zero, false
    }
    var pe *E // lazily initialized
    return asType(err, &pe)
}

func asType[E error](err error, ppe **E) (_ E, _ bool) {
    for {
        // 現在のエラー値が、型パラメータ E の型に一致するかをチェック
        if e, ok := err.(E); ok {
            return e, true
        }
        // エラーが独自の As メソッドを実装している場合、それを呼び出してチェック
        if x, ok := err.(interface{ As(any) bool }); ok {
            if *ppe == nil {
                *ppe = new(E)
            }
            if x.As(*ppe) {
                return **ppe, true
            }
        }
        // エラーチェーンを辿る: Unwrap() メソッドでラップされた下位のエラーを取得
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            // 単一のエラーをラップしている場合: アンラップして次のループで再チェック
            err = x.Unwrap()
            if err == nil {
                return
            }
        case interface{ Unwrap() []error }:
            // 複数のエラーをラップしている場合: それぞれに対して再帰的にチェック
            for _, err := range x.Unwrap() {
                if err == nil {
                    continue
                }
                if x, ok := asType(err, ppe); ok {
                    return x, true
                }
            }
            return
        default:
            return
        }
    }
}

github.com

まとめ

error を普段から使うことは多かったですが、改めて実装の中身を除いてみると、開発者がエラーの抽出の仕方をあまり意識しなくても済むようになっていることがわかりました。

Go1.26 では errors.AsType が追加されることで、エラーを代入するための変数を定義する必要がなくなります。これによりエラーの抽出の仕方をより簡潔に、柔軟にすることができると思います。

今後とも Go の進化に食らいついていきながら、より良いエラーハンドリングを実現していきたいと思います。

いよいよ開幕!every Tech Blog Advent Calendar 2025

目次

はじめに

こんにちは、開発本部開発 1 部トモニテグループのエンジニアの rymiyamoto です。

今年も残り 1 ヶ月ちょっととなり、年末の恒例イベント every Tech Blog Advent Calendar 2025 を開催します!

このカレンダーでは、エブリーのエンジニアが日々の学びや実践的な技術ノウハウを発信していきます。

技術的な工夫や挑戦の裏側など、幅広いテーマでお届けしますので、ぜひチェックしてください!

過去のアドベントカレンダーはこちらからどうぞ!

tech.every.tv

tech.every.tv

tech.every.tv

every Tech Blog Advent Calendar 2025 の公開スケジュール

アドベントカレンダーの記事は、11/27~12/25 の日程で順次公開していきます!

tech.every.tv

最後に

エブリーでは、新しい技術に挑戦しながら成長したい仲間を募集中です。

もし、このブログを読んで「もっと話を聞いてみたい」と感じていただけたら、ぜひカジュアル面談にお越しください!

corp.every.tv

最後までお読みいただき、ありがとうございました!🎅✨

Grafana LGTMスタックをローカルで検証してみた

Grafana LGTMスタックをローカルで検証してみた

はじめに

こんにちは!デリッシュキッチンで主にバックエンドの開発を担当している秋山です。

オブザーバビリティの向上に向けてGrafanaやその関連ツールを検証する一環で、Grafana LGTMスタックをローカルに構築し実際に触ったので、そのあたりを紹介します。

オブザーバビリティについて

本題に入る前にオブザーバビリティについて簡単に説明できればと思います。

オブザーバビリティとは、システムの内部で起きていることを外部から把握する能力のことです。 日々のパフォーマンス確認/改善やエラー発生時の調査などに役立ちます。 システムの複雑性が増す中で必要性が高まっています。

オブザーバビリティの主要シグナルとしてメトリクス、トレース、ログが存在します。

メトリクス

システムの健康状態を数値で表すデータです。

  • CPU使用率
  • メモリ使用量
  • レイテンシー
  • エラーレート

など

トレース

1つのリクエストに対する一連の処理を可視化するデータです。リクエストが通過するマイクロサービスやデータストアへのアクセスなど、処理の流れ全体を追跡できます。

ログ

システムやアプリケーションが出力する記録です。

  • アプリケーションログ
  • セキュリティログ
  • システムログ
  • 監査ログ

など

LGTMスタックとは

LGTMスタックは、Grafana Labsが提供するオブザーバビリティ(可観測性)を実現するための統合スタックです。以下の4つのコンポーネントの頭文字から名付けられています:

  • L (Loki): ログを扱うツール
  • G (Grafana): メトリクス、ログ、トレースを統合的に可視化するダッシュボード
  • T (Tempo): 分散トレースを扱うツール
  • M (Mimir+Prometheus): メトリクスを扱うツール

これら4つを組み合わせることで、オブザーバビリティの主要シグナルであるメトリクス・トレース・ログを統合的に扱うことができます。

https://grafana.com/docs/opentelemetry/docker-lgtm/ より

今回は、これらのツールをローカル環境で構築し、実際にどのように連携するのかを検証してみました。

Opentelemtryとは

検証ではdocker-otel-lgtmを使用するのですが、その中でOpenTelemetry Collectorを使用しているため、Opentelemtryについて先んじて簡単に説明させていただきます。

Opentelemtryとは、アプリケーションからメトリクス・ログ・トレースといったテレメトリー(観測)データを統一的に収集・送信するためのOSSです。

アプリケーションを言語、インフラ、ランタイム環境に関係なく簡単に計装できることを目的としています。

ベンダーに依存することなくテレメトリーデータを扱えるメリットがあります。データの送信先としてOTLP(OpenTelemetry Protocol)対応しているツールであれば連携が可能です

上で紹介した図中のOpenTelemetry collectorはアプリケーションからテレメトリーデータを受け取り、各ツールに送信する役割を担っています。

ローカル検証

docker-otel-lgtmを使って検証しました。

docker-otel-lgtmはLGTMスタック+OpenTelemetry collectorを1つのdockerにまとめてくれている公式のプロジェクトです。 そのため、このdocker imageを使って起動するだけで即座にLGTM+OpenTelemetry collectorの環境を用意し、各ツールの機能検証を簡単に開始することができます。

1 . LGTM+OpenTelemetry collectorを起動する

起動はとてもシンプルで、下記のコマンドを実行するだけです。

# Unix/Linuxの場合
./run-lgtm.sh

2 . 起動したOtel collectorに向けて観測データを送信する

docker-otel-lgtmが起動すると、OpenTelemetry Collectorがポート4317(gRPC)と4318(HTTP)でリクエストを受け付けます。 そのため、アプリケーションからのデータ送信はgRPCかHTTPのどちらかの通信方法を選択できますが、今回の検証ではHTTPを使っています。

exampleのサーバーを使ってテレメトリデータを送信する場合

Grafana全体の使用感をサクッと知りたい時は既に用意されたexampleのサーバーを使うことができます。

Go,Java,Pythonなどを使ったexampleがあったので、今回はGoで試してみました。

cd examples/go
# 起動
./run.sh

サイコロを振るアプリケーションサーバーが起動します。 goの場合は8081ポートに立つので、curl http://127.0.0.1:8081/rolldiceのようにリクエストすれば、サイコロの数字が返ってきます。

リクエスト後、Grafana( http://127.0.0.1:3000 )にアクセスすると送られてきたテレメトリデータを使った情報を閲覧することができます。

デフォルトでは下記のようなダッシュボードがいくつか用意されていました。

ダッシュボード

また、ExploreページからTempoを使ったトレースデータの確認などもできました。

Exploreページ

自前のアプリケーションサーバーを使って観測データを送信する場合

自分が普段触っているサーバーで検証したい事もあると思います。

その場合も、起動したOtel Collectorに向けてデータを送信するだけです。

既にOpenTelemetryを使用して計装を行っている場合は、 送信先をローカルで起動したOtel Collector(httpであれば http://localhost::4318 )に変更するだけで済みます。

未計装の場合、まずは公式の記事を参考に計装を進めていただければと思います。

Goの場合は下記の記事が参考になりますが、試してみたところ案外すんなり計装できました。

opentelemetry.io

下記は実際に開発しているアプリケーションのトレースをしてみた画面です。

実際のトレース画面

他マイクロサービスやDBへのアクセスも含めたトレースを確認できました。

所感

検証の環境について

簡単に検証するためのプロジェクトを公式が用意してくれているのは非常に助かりました。

Grafana LGTMスタックについて

今回使用したdocker-otel-lgtmは検証用の環境を作るものなので簡単に構築できましたが、実運用では可用性やセキュリティ面などを考慮したサーバー構成やツールの設定が必要です。 トレース・メトリクス・ログを統合的に扱うために複数のツールを導入する必要があることを踏まえると、全部自前で用意する場合運用の難しさがありそうだなと思いました。

また、ツールごとの使用方法も理解する必要があるため学習コストが気になりましたが、AIを使うことによって一定の負荷は軽減できそうでした。

下記はAIに作ってもらったダッシュボードの画像です。 ダッシュボードの内容をJSONとして定義できるため、AIの活用が簡単にできます。

ダッシュボード

参考文献

関連記事

tech.every.tv

小売店管理機能を実装した話

はじめに

こんにちは!株式会社エブリーで約1か月間インターンシップに参加している山本です。配属チームはリテールハブ小売アプリチームで、主に小売店やそのお客さんに向けたサービスを開発しているチームになります。具体的には、スーパーなどの小売店がお客さんにお知らせをアプリ経由で配信するなどのサービスを手掛けています。本記事では、小売店向けのアプリの運用効率を向上させるために導入した管理機能と開発していく中で困ったことなどについてご紹介します。

背景と目的

現在、小売アプリにはお客さん向けアプリと小売店向けの管理画面の2つが存在します。小売店向けの管理画面では、お客さんに向けてお知らせやチラシなどを配布することができ、お客さん向けアプリではそれらを受け取り、利用することができます。

これらのアプリに関しては、マルチテナント化を進めており、単一コードで管理を行っています。しかし、現時点では小売店向けの管理画面を管理する管理機能のようなものが存在しません。そのため、各テナントの機能やカスタマイズを一元管理するような画面や利用状況などを監視、分析するような機能がありません。また、運営からメンテナンス等のお知らせを伝えることもできないため、各小売店に個別に連絡をする必要があります。対象の小売店が数店舗であれば運用可能ですが、これからさらに大規模になっていくことを考えると、小売店の管理機能を開発する必要があります。

このような背景のもと、本インターンでは運営効率の向上や顧客体験の統一化のために小売向けの管理機能の開発に取り組みました。

構成と技術スタック

今回、取り組んだタスクは一からのスタートだったため、技術選定から行う必要がありました。個人で開発を行う際は、特に何も考えず自分の好きな技術や触ってみたい技術を使っていたため、実際に必要な機能の実現可能性など様々なことを考慮しながら技術選定を行うのはとても難しかったです。

選定にあたっては、機能要件の実現可能性や開発効率などを多角的に検討した結果、以下の理由によりNext.jsによるフルスタック開発を採用しました。

  • 開発効率の向上:フロントエンドとバックエンドが同じ言語(TypeScript)であることで初期段階の開発をスムーズに進めることができる
  • コードの型安全性:フロントエンドとバックエンドで型定義を共有できるため、データの整合性を保ちやすく安全な開発が可能になる
  • ライブラリの充実:必要な機能である認証機能をはじめとしたライブラリが充実しており、複雑な機能も実装できる

また、インフラ構成に関しては、以下のような構成にしました。ALBやセキュリティグループでIP制限をかけることで、社外からのアクセスを制限しています。デプロイに関しては、ECRへのpushとECSのデプロイはecspressoで管理をして、それ以外のコンポーネントはTerraformで管理をしています。

インフラ構成図

技術スタック一覧

  • Next.js
  • AWS
  • Terraform
  • ecspresso
  • Github Actions
  • MySQL

実装した機能

本インターンは1か月という短い期間ということもあり、優先順位の高い以下の機能を実装しました。

  • 認証機能
    • ログイン/ログアウト
  • ユーザー管理
    • アカウント作成/削除
    • 管理者権限/閲覧権限
  • お知らせ管理
    • お知らせ作成/編集/削除
  • 操作ログ
    • 誰がいつ何を行ったかを記録

ログイン画面とお知らせ管理画面は現在以下のようになっています。

ログイン画面

お知らせ管理画面

困ったこと

認証機能について

認証機能に関しては、NextAuth.jsの最新バージョンであるAuth.js (v5から名称が変更) を採用しました。Auth.jsは様々な認証機能を提供しており、これらを少ないコード量で簡単に実装できるため、このライブラリを用いてEmailとパスワードでの認証機能を実装しました。

しかし、インターン期間中にXである記事が流れてきました。この記事ではAuth.jsはBetter Authに統合されることが発表され、今後はフレームワーク非依存のBetter Authに移行することが推奨されています。そのため、Auth.jsで書いたコードをBetter Authに移行する必要が発生しました。

当初実装していたAuth.jsの認証ではJWTを用いて、アプリケーション側でセッション情報を持たないステートレスな認証を行っていましたが、Better Authはステートレス認証をサポートしていませんでした。そのため、DB設計なども変更になり、完全にBetter Authで書き換えるという作業になりました。

予期せぬライブラリの移行作業は大変でしたが、結果的に数日間で複数の認証技術に触れることができ、非常に学びの多い経験となりました。また、Web技術の進化の速さをリアルタイムで体感すると同時に、実務開発のリアルな一面も経験することができました。

API呼び出しについて

Next.js App Routerでサーバーサイドの処理を行う方法として、Route Handlersを用いた実装方法とServer Functionsを用いた実装方法があります。

Route Handlers

Route HandlersはAPIエンドポイントをサーバーサイドで作り、それを呼び出します。app/api配下にroute.tsファイルを配置することで、ファイル構造がそのままAPIエンドポイントのURLとなり、フォルダとファイル名を見るだけでどのURLに対応するのかが直感的にわかるようになっています。

以下のコードをapp/api/hello/route.tsに配置した場合、クライアント側からfetch("/api/hello")で呼び出すことができます。

export async function GET() {
  return Response.json({ message: "Hello World" })
}

Server Functions

Server FunctionsはクライアントサイドからRPCスタイルで簡単にサーバサイドの関数を呼び出せる機能です。"use server"ディレクティブを加えることで、以下のようにサーバーサイドの関数を定義することができます。

"use server"

export async function createPost(formData: FormData) { 
  // update logic
}

そして、クライアントサイドではフォームなどに以下のように記述することで処理を行うことができます。stateを保持したり、handlerを定義する必要がなく、簡潔に書くことができるというメリットがあります。

"use client"
 
import { createPost } from "@/app/actions"
 
export function Button() {
  return <button formAction={createPost}>Create</button>
}

Server Functionsの簡潔な記述は魅力的でしたが、Next.jsのAPIを外部から呼び出す場合や、今後バックエンドをNext.jsから切り離すことも想定して、今回はRoute Handlersを用いて実装を行いました。

インフラ構成について

小売向けの管理機能はあまり使用頻度が高くない想定ということで、当初はLambdaを用いてデプロイを行う方針でした。LambdaはAPI Gatewayなどの何らかのイベントがトリガーとなりhandler関数が呼び出されるため、Lambda特有のインターフェースに沿った書き方を行う必要があります。しかし、Lambda Web Adapterを用いることで、元々サーバーレス環境のために作られたわけではないNext.jsなどのフレームワークをそのままLambda上で動かすことができるようになります。

当初は、このLambda Web Adapterを用いて、TerraformLambrollでインフラ構築を行っていました。しかし、実際にデプロイ作業を行っていく中で、DBのパスワードなどの外部公開しない環境変数の渡し方で困ってしまいました。外部公開したくないためECRにpushはせず、Secrets Managerを参照して取得したいですが、調べた限りではLambdaではそのためのコードを書いて環境変数の取得を行う必要がありました。(参考)

環境変数はSecrets Managerで管理して、それを直接参照して使えるようにしたかったため、Lambdaの使用はやめ、ECS (Fargate) を用いるように変更しました。ECSではコンテナの定義にSecrets Managerのパスを書くことで直接参照することができます。

以下はTerraformで定義したSecret Managerをecspressoで参照してデプロイを行う例です。

  {
    "name": "DATABASE_URL",
    "valueFrom": "{{ tfstate `module.secret_manager.aws_secretsmanager_secret.control_db.arn` }}:database_url::"
  }

さいごに

1ヶ月という短い間でしたが、技術選定からフロントエンド、バックエンド、インフラ構築、CI/CDと様々な技術領域に触れることができ、非常に貴重な経験となりました。特に、実際に業務を進めていく中で、当初の想定通りに進まない事態に直面し、その都度相談しながら解決策を探るという実務のリアルな側面を体験することで大きな学びを得ることができました。また、この経験を通じて、実務における技術選定や計画の難しさと、状況に応じて柔軟に対応していく重要性を実感することができました。

今回のインターンシップで得た学びと経験を元にこれからも成長していき、ユーザーに価値を届けられるようなエンジニアになっていきたいです。

Databricks Managed MCP ServerとUnity Catalog Functionでテーブルスキーマを取得する

はじめに

こんにちは。
開発本部 開発1部 デリッシュリサーチチームでデータエンジニアをしている吉田です。

本記事では、DatabricksのManaged MCP Serverを活用し、CursorからUnity Catalog Functionsをツールとして呼び出して、任意のUnity Catalogテーブルのスキーマ情報を取得するまでをまとめます。

背景

CursorでDatabricks上のコードを書く際、特定テーブルのスキーマ情報をCursor側(エージェント)に渡したい場面がありました。

どのようにして簡単にこの情報を取得して渡すか検討していたところ、Databricks Managed MCP Serverがベータリリースされていることを知り、早速使ってみることにしました。

Databricks Managed MCP Serverとは

Databricks Managed MCP Serverとは、Databricks上でホストされているMCP Serverです。

Use Databricks managed MCP servers

インフラはDatabricks側で管理されるため、すぐに利用できます。
現時点でベータ版として以下の機能が提供されています。

  • 提供MCPサーバー
    • Vector search: Vector Search Indexにクエリして関連ドキュメントを検索する
    • Genie space: Genie Spaceにクエリを実行し、自然言語を用いてクエリを実行する
    • Unity Catalog functions: Unity Catalog Functionを利用して、定義済みのSQLを実行する
    • DBSQL: AI生成のSQLを実行する

この記事ではUnity Catalog FunctionsのMCP Serverを扱います。

Unity Catalog Functionの実装

MCP Serverから呼び出すためのUnity Catalog Functionを作成します。
Unity Catalogのテーブルパスを受け取り、カラム名やデータ型をJSON文字列で返す関数を作成します。

Unity Catalogテーブルのスキーマを取得する方法はいくつかありますが、SQLだけで簡単に完結させたかったため、system.information_schema.columnsテーブルを参照する方法を採用しました。
system.information_schema.columnsテーブルには、管理下の全テーブルのカラム情報が含まれているため、これを利用します。

以下のSQLでmcp.unity_catalog配下にget_schema_infoという名前のUnity Catalog Functionを作成します。

CREATE OR REPLACE FUNCTION mcp.unity_catalog.get_schema_info(
  uc_full_path STRING COMMENT 'Unity Catalogテーブルのフルパス(例: catalog.schema.table)'
)
RETURNS STRING
LANGUAGE SQL
COMMENT "指定したUnity Catalogテーブルのカラム情報(カラム名、NULL許容、データ型、パーティションインデックス、コメント)をJSON形式で返す関数です。"
RETURN
SELECT
  to_json(collect_list(struct(
    column_name,
    is_nullable,
    full_data_type,
    partition_index,
    comment
  ))) AS s
FROM
  system.information_schema.columns
WHERE
  table_catalog = split(uc_full_path, '\\.')[0]
  AND table_schema = split(uc_full_path, '\\.')[1]
  AND table_name = split(uc_full_path, '\\.')[2];

Managed MCP Server(Unity Catalog Functions)として公開

Managed MCP ServerのFunctionsサーバーは https://<workspace-hostname>/api/2.0/mcp/functions/{catalog}/{schema} のURLパターンで提供されます。
今回の関数は mcp.unity_catalog.get_schema_info なので、クライアントから接続するサーバーURLは以下になります。

https://<workspace-hostname>/api/2.0/mcp/functions/mcp/unity_catalog

Cursor からの接続と実行

Cursorは、Cursor Settings -> Tools & MCPの項目から接続を設定できます。

Connect Cursor with PAT

mcp.jsonファイルを以下のように設定することで接続できます。
URLで指定した <catalog>.<schema> 配下の Unity Catalog Function が自動的にツールとして登録されます。

設定イメージ(例)

{
  "mcpServers": {
    "uc-function-mcp": {
      "type": "streamable-http",
      "url": "https://<workspace-hostname>/api/2.0/mcp/functions/<catalog_name>/<schema_name>",
      "headers": {
        "Authorization": "Bearer <YOUR_PAT>"
      },
      "note": "Databricks UC Functions"
    }
  }
}

Cursorから呼び出し

Databricksのサンプルデータを対象に実行してみます。

uc-function-mcpを利用して、samples.bakehouse.media_customer_reviewsのスキーマ情報を教えて下さい

Unity Catalog Functionの mcp.unity_catalog.get_schema_info が呼ばれ、以下のような JSON が返ります。

{
  "is_truncated": false,
  "columns": [
    "output"
  ],
  "rows": [
    [
      [
        {
          "column_name": "review",
          "is_nullable": "YES",
          "full_data_type": "string"
        },
        {
          "column_name": "franchiseID",
          "is_nullable": "YES",
          "full_data_type": "bigint"
        },
        {
          "column_name": "review_date",
          "is_nullable": "YES",
          "full_data_type": "timestamp"
        },
        {
          "column_name": "new_id",
          "is_nullable": "YES",
          "full_data_type": "int"
        }
      ]
    ]
  ]
}

最終的に、以下のように解釈した結果を出力してくれました。

まとめ

Managed MCP Serverを利用して、Unity Catalog Functionを呼び出すことで、Cursorから安全にテーブルスキーマを取得できるようになりました。