mcp-database-server × GitHub Copilotを使用して、真なるDatabase First な開発を行う
はじめに
システム構築にてアプリ開発を行うとき、データベースは切り離せない存在かと思います。経験上、アプリ開発ではデータベースを必ず意識して構築を進める必要があります。
さらに、システム改造案件などでは既存のデータベースを参照しながらアプリ開発を進めることがほとんどです。
アプリ開発をしているのに、データベースの知識がないとトラブルになることも多々あります。
また昨今はAIによるコーディングやMCPが流行っており、かつ私はめんどくさがり屋なので、「AIにデータベース参照させながら、コーディングしてくれればうれしいな」、などと考えておりました。
そんななか、「GitHub CopilotとMCPサーバー(mcp-database-server)を使用すればできるのではないか。。?」と思い、データベースを参照させながらAIによるコーディングで、真なるDatabase First(?)な開発を行うべく、本記事の作成に至ります。
Database First とは
やりたいこと
mcp-database-serverを使用して、VS CodeのGitHub Copilotにデータベースの情報を参照させながら、アプリ開発を実施したいです。
mcp-database-serverとは
以前の記事で紹介してますので、セットアップや使い方はこちらをご参照いただければ幸いです。
環境
VS Codeをmcpクライアントとして、mcp-database-serverを使用します。
- VS Code (V1.104.0)
- VS Code 拡張機能
- GitHub Copilot
- Mode : Agent Mode
- Model : GPT-5
- C# Dev Kit
- GitHub Copilot
- VS Code 拡張機能
- mcp-database-server
- .NET 9
- node.js (v22.14.0)
- SQL Server 2022 (オンプレミス)
今回はSQL Serverへの接続を想定とします。
前提
参照するデータベース
以前の記事にて作成した、TEST_DB_2を参照します。
TEST_DB_2は、SQL Server 2022で作成された超簡素なショッピング(ECサイト)データベースです。
テーブル:
- Users(ユーザー情報)
- Products(商品情報)
- Orders(注文)
- OrderDetails(注文内の商品詳細)
データベース情報
テーブル構成について以下にER図を示します。
もう少し詳細は、以前の記事をご参照いただけると幸いです。
構築するアプリ
開発するアプリは.NET 9 にて、Blazor Interactive Server Rendering (旧 Blazor Server)とします。
構築機能
シンプルなCRUDができるアプリを構築するとします。
図示すると以下のようなアプリ構成を想像しています。
実践
いざいざGitHub Copilotとmcp-database-serverを使用した開発を行います。
構築
VS Code で作業フォルダを開き、ターミナルより以下コマンドでBlazor Interactive Server Rendering 用のプロジェクトを作成します。
dotnet new blazor `
--auth None `
-o MyBlazorServerApp
MCPサーバーの設定は、mcp-database-server(sqlserver_mcp)を設定し、必要最小限とします。
GitHub Copilotへの指示は以下のプロンプトを使用します。念のため、データベース情報は sqlserver_mcp の MCP サーバーを使用するように指示します。(何回か試しましたが、指定しないと、たまにMCPサーバーを使用せずに想像で作りにいってしまう節があったので、プロンプトで明示的に指示したほうが良いと考えます。)
Blazor Server (.NET 9) のプロジェクト「MyBlazorServerApp」に対して、SQL Server データベース TEST_DB_2 の各テーブルに CRUD 操作を行う画面を追加してください。
前提条件:
- データベース情報は sqlserver_mcp の MCP サーバーを使ってスキーマを確認してください。
- TEST_DB_2 は簡易的なショッピングサイト (EC) 用データベースと想定してください。
- DB接続情報は appsettings.json に仮決めで設定し、DBユーザーで接続する前提としてください。
ライブラリ/レンダリング要件:
- Blazor のレンダリングモードはすべて @rendermode="InteractiveServer" を使用してください。
- トースト通知は NuGet パッケージ「Sotsera.Blazor.Toaster」を利用してください。(@rendermode="InteractiveServer" で動作するように実装してください)
- 必要に応じて MIT ライセンスの NuGet パッケージを利用してください。
UI/機能要件:
1. 各テーブルごとに CRUD 画面を作成してください。
2. ナビゲーションメニューから CRUD 画面に遷移できるようにしてください。
3. データはグリッド表示し、インライン編集・削除・追加を行えるようにしてください。
4. データ追加の際に制約上必要な外部キーが存在する場合は、追加の入力ダイアログを表示してください。
5. 編集や削除が制約上不可能な場合は、「Sotsera.Blazor.Toaster」で「操作できません」と表示してください。
6. CRUD 操作実行時は Yes/No 確認ダイアログを出してください。
7. 処理が完了した場合は成功メッセージを、失敗した場合はエラー詳細を「Sotsera.Blazor.Toaster」で表示してください。
8. 各処理がExceptionを発生する場合を考慮して、try catchでexmessageを「Sotsera.Blazor.Toaster」で表示してください。
実装ステップ:
1. MCP サーバーを用いて TEST_DB_2 のスキーマを確認してください。
2. Entity Framework Core を利用して DB コンテキストとモデルを自動生成してください。
3. テーブルごとに CRUD ページを生成し、ナビゲーションに追加してください。
4. 各ページで UI 要件・ライブラリ要件を満たすように実装してください。
Todosタスクを作成して作業に入ったようです。
意図通り、まずはmcp-database-serverを使用してDBを参照後、めきめき作業しています。
DBを参照後はバリバリとソースコードを変更しています。
GitHub Copilotで一通りの作業が完了すると、作業一式のサマリを出力してくれました。以下は出力の一部抜粋です。
## いまの状態
- MCP で取得したスキーマ(Users/Products/Orders/OrderDetails と FK 関係)に合わせて、EF Core の DbContext/エンティティを作成済み。
- 各テーブルの CRUD ページ(インライン編集/追加/削除、FK ドロップダウン、Yes/No 確認、トースト通知、例外ハンドリング)を追加済み。
- ナビゲーションに CRUD へのリンクを追加済み。
- 全ページで `@rendermode="InteractiveServer"` を付与済み。
- Toaster コンテナは MainLayout.razor で設置済み。
## 起動と確認(任意)
以下のコマンドで起動し、左ナビから各 CRUD を確認してください。
```powershell
dotnet run --project .\MyBlazorServerApp\MyBlazorServerApp.csproj
```
- Users/Products/Orders/OrderDetails の各ページで
- 追加/編集/削除時に Yes/No ダイアログ表示
- 成功/失敗/制約違反時にトースト通知
- 参照制約で不可な場合は「操作できません」を表示
作成されたEntity
以下はGitHub Copilotによって作成されたEntityです。ご参考まで。
DBContext
using Microsoft.EntityFrameworkCore;
using MyBlazorServerApp.Data.Entities;
namespace MyBlazorServerApp.Data;
public class EcDbContext : DbContext
{
public EcDbContext(DbContextOptions<EcDbContext> options) : base(options) { }
public DbSet<User> Users => Set<User>();
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderDetail> OrderDetails => Set<OrderDetail>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>(entity =>
{
entity.ToTable("Users", "dbo");
entity.HasKey(e => e.UserID);
entity.Property(e => e.UserID).ValueGeneratedNever();
entity.Property(e => e.Email).IsRequired().HasMaxLength(320);
entity.Property(e => e.PasswordHash).IsRequired().HasMaxLength(256);
entity.Property(e => e.FirstName).IsRequired().HasMaxLength(50);
entity.Property(e => e.LastName).IsRequired().HasMaxLength(50);
entity.Property(e => e.Phone).HasMaxLength(30);
entity.Property(e => e.IsActive).IsRequired();
});
modelBuilder.Entity<Product>(entity =>
{
entity.ToTable("Products", "dbo");
entity.HasKey(e => e.ProductID);
entity.Property(e => e.ProductID).ValueGeneratedNever();
entity.Property(e => e.SKU).IsRequired().HasMaxLength(50);
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
entity.Property(e => e.Currency).IsRequired().HasMaxLength(3).IsFixedLength();
entity.Property(e => e.Price).HasPrecision(19, 4);
entity.Property(e => e.StockQuantity).IsRequired();
entity.Property(e => e.IsActive).IsRequired();
});
modelBuilder.Entity<Order>(entity =>
{
entity.ToTable("Orders", "dbo");
entity.HasKey(e => e.OrderID);
entity.Property(e => e.OrderID).ValueGeneratedNever();
entity.Property(e => e.OrderNumber).IsRequired().HasMaxLength(30);
entity.Property(e => e.Status).IsRequired().HasMaxLength(20);
entity.Property(e => e.Subtotal).HasPrecision(19, 4);
entity.Property(e => e.Tax).HasPrecision(19, 4);
entity.Property(e => e.ShippingFee).HasPrecision(19, 4);
entity.Property(e => e.Discount).HasPrecision(19, 4);
entity.Property(e => e.Total).HasPrecision(22, 4);
entity.Property(e => e.Currency).IsRequired().HasMaxLength(3).IsFixedLength();
entity.HasOne(d => d.User)
.WithMany(p => p.Orders)
.HasForeignKey(d => d.UserID)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Orders_Users");
});
modelBuilder.Entity<OrderDetail>(entity =>
{
entity.ToTable("OrderDetails", "dbo");
entity.HasKey(e => e.OrderDetailID);
entity.Property(e => e.OrderDetailID).ValueGeneratedNever();
entity.Property(e => e.UnitPrice).HasPrecision(19, 4);
entity.Property(e => e.UnitDiscount).HasPrecision(19, 4);
entity.Property(e => e.LineTotal).HasPrecision(38, 6);
entity.HasOne(d => d.Order)
.WithMany(p => p.OrderDetails)
.HasForeignKey(d => d.OrderID)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_OrderDetails_Orders");
entity.HasOne(d => d.Product)
.WithMany(p => p.OrderDetails)
.HasForeignKey(d => d.ProductID)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_OrderDetails_Products");
});
}
}
Userモデル
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyBlazorServerApp.Data.Entities;
[Table("Users", Schema = "dbo")]
public class User
{
[Key]
public int UserID { get; set; }
[Required, MaxLength(320)]
public string Email { get; set; } = string.Empty;
[Required]
public byte[] PasswordHash { get; set; } = Array.Empty<byte>();
[Required, MaxLength(50)]
public string FirstName { get; set; } = string.Empty;
[Required, MaxLength(50)]
public string LastName { get; set; } = string.Empty;
[MaxLength(30)]
public string? Phone { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public ICollection<Order> Orders { get; set; } = new List<Order>();
}
Productモデル
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyBlazorServerApp.Data.Entities;
[Table("Products", Schema = "dbo")]
public class Product
{
[Key]
public int ProductID { get; set; }
[Required, MaxLength(50)]
public string SKU { get; set; } = string.Empty;
[Required, MaxLength(200)]
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
[Column(TypeName = "decimal(19,4)")]
public decimal Price { get; set; }
[MaxLength(3)]
public string Currency { get; set; } = "JPY";
public int StockQuantity { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public ICollection<OrderDetail> OrderDetails { get; set; } = new List<OrderDetail>();
}
Orderモデル
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyBlazorServerApp.Data.Entities;
[Table("Orders", Schema = "dbo")]
public class Order
{
[Key]
public long OrderID { get; set; }
[Required, MaxLength(30)]
public string OrderNumber { get; set; } = string.Empty;
public int UserID { get; set; }
public DateTime OrderDate { get; set; }
[Required, MaxLength(20)]
public string Status { get; set; } = string.Empty;
[Column(TypeName = "decimal(19,4)")]
public decimal Subtotal { get; set; }
[Column(TypeName = "decimal(19,4)")]
public decimal Tax { get; set; }
[Column(TypeName = "decimal(19,4)")]
public decimal ShippingFee { get; set; }
[Column(TypeName = "decimal(19,4)")]
public decimal Discount { get; set; }
[Column(TypeName = "decimal(22,4)")]
public decimal? Total { get; set; }
[MaxLength(3)]
public string Currency { get; set; } = "JPY";
[MaxLength(500)]
public string? ShippingAddress { get; set; }
[MaxLength(500)]
public string? BillingAddress { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
[ForeignKey(nameof(UserID))]
public User? User { get; set; }
public ICollection<OrderDetail> OrderDetails { get; set; } = new List<OrderDetail>();
}
OrderDetailモデル
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MyBlazorServerApp.Data.Entities;
[Table("OrderDetails", Schema = "dbo")]
public class OrderDetail
{
[Key]
public long OrderDetailID { get; set; }
public long OrderID { get; set; }
public int LineNumber { get; set; }
public int ProductID { get; set; }
public int Quantity { get; set; }
[Column(TypeName = "decimal(19,4)")]
public decimal UnitPrice { get; set; }
[Column(TypeName = "decimal(19,4)")]
public decimal UnitDiscount { get; set; }
[Column(TypeName = "decimal(38,6)")]
public decimal? LineTotal { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
[ForeignKey(nameof(OrderID))]
public Order? Order { get; set; }
[ForeignKey(nameof(ProductID))]
public Product? Product { get; set; }
}
動作確認
GitHub Copilotが作成したコードの動作確認を行います。
VS Code 拡張機能 C# Dev Kit について
VS Code の実行とデバッグより「実行とデバッグ」を押下します。
デバッガは「C#」を選択します。(※VS Code 拡張機能 C# Dev Kitが必要)
スタートさせるプロジェクトは「BlazorSampleApp」となる選択をします。(画像では全部同じですね。。私は念のため、「スタートアッププロジェクトの起動」を選択しています。)
BlazorServerAppがビルドされ、デバッグ起動がスタートします。
無事、BlazorServerAppが起動しました。
以下は簡単な操作gifです。
データベース TEST_DB_2の各テーブル(Users, Products, Orders, OrderDetails)内のデータを表示できています。
また、CRUD操作時にトーストメッセージで操作結果を表示できてます。
残念ながら、「主キーや制約を考慮して」とcopilotに依頼をしましたが、どうにも考慮できておらず、実行するとエラーが出て、不親切なメッセージが表示されるだけになってますね。。
この辺はプロンプトの工夫で回避できそうです。
とはいえ、データベースの内容を踏まえてコーディングを行ってくれるだけで、涙ものです。なんなら泣いた。
まとめ
ということで、mcp-database-serverを使用して、真なるDatabase Firstな開発(?)を行いました。
アプリとしての作りこみはさらなるプロンプトの工夫が必要が必要と感じましたが、目標としていたデータベースの内容を参照して、コーディングに反映させる点はクリアできたので良しとしましょう。。良しとしてください。
冒頭でも記載の通りですが、アプリ開発といえどもデータベースの知識は必要であり、経験上、アプリ側開発者とデータベース側開発者の設計意図の食い違いが発生しやすいので、mcp-database-serverの活用で両者の距離を縮めることができればと思う次第です。
本記事が少しでも何かのお役に立てれば幸いです。
参考
- GitHub mcp-database-server
Discussion