v0.4.2 の時点のガイド(ソース)
dbxray / mcp-server
mcp-server 読解ガイド
cliが出力したスキーマのスナップショット(JSON Lines)を起動時にメモリへ読み込み、AIから呼べる19個の読み取り専用ツールと、利用者が会話に添付できるテーブル定義のリソースとして、標準入出力で公開するサーバーです。DBには接続しません。このガイドは、MCPとSDKの事前知識から始めて、ソースごとの役割と繋がりまでを順に説明します。
- 約8.0k 行main配下のJava(空行・コメントを含む目安)
- 19 ツールすべて読み取り専用。ほかに全テーブルをリソースとして公開
- mcp-core 2.0.1MCP Java SDK。JSONはJackson 2系
- stdioMCPクライアントが子プロセスとして起動
mainブランチ(0bb6bdd。PR #114までを含む)を読んで作成し、MCPサーバーのSchemaCatalogの窓口への分割とArchitectureTestの追加に合わせて更新。さらに、cliの--previewが出力する関数の利用テーブル(Issue #134・PR #137)に合わせて、get_functionのtableUsageの分を追記しています(7.10節)。その後、中間テーブルと多対多の関係(erRole・manyToMany。PR #155)と、構造の似たテーブルを探すfind_similar_tables(PR #156)を追記し、サンプルのDBに足した履歴テーブルemployee_historyに合わせて例を採り直しています(7.11節・7.12節)。JSONの例は、ビルドしたjarにdocs/sample/postgres/output/snapshotを読ませて実際に採取した応答です(長いものは省略)。
1事前知識:MCPのしくみ
MCP(Model Context Protocol)は、AIアプリケーション(Claude Code、Claude Desktop等)が外部の道具やデータに接続するための通信規約です。登場人物は3者います。
- ホスト:利用者が操作するAIアプリ。Claude Codeそのもの。
- クライアント:ホストの中で、1つのサーバーとの接続を受け持つ部品。サーバー1つにつき1つ。
- サーバー:道具を提供する側。このリポジトリの
mcp-serverはこれにあたります。
やり取りするメッセージはすべてJSON-RPC 2.0です。idを持つ「要求」には必ず同じidの「応答」(resultかerror)を返し、idを持たない「通知」には応答しません。
stdioトランスポート:標準出力はプロトコル専用
MCPにはメッセージの運び方(トランスポート)が2種類あり、このサーバーはstdioを使います。クライアントがサーバーを子プロセスとして起動し、標準入力に要求を書き込み、標準出力から応答を読みます。1行が1メッセージです。
System.outへの出力は禁止で、利用者向けの表示とSDKのログ(slf4j-simple)はすべて標準エラーへ出します。終了はクライアントが標準入力を閉じたときです。1回の接続の流れ
接続は「初期化 → 発見 → 実行 → 終了」の順に進みます。サーバーが能動的に何かを送ることはなく、すべてクライアントからの要求への応答です。
instructionsは「どのツールから呼ぶか」をAIへ伝える文章で、McpServerMain.INSTRUCTIONSに書かれています。実際の初期化のやり取りです(instructionsは途中で省略)。
→ stdin
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}
← stdout
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18",
"capabilities":{"logging":{},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},
"serverInfo":{"name":"dbxray","version":"0.2.0"},
"instructions":"DBのテーブル定義(dbxrayが出力したスキーマのスナップショット)を調べるサーバー。全体像はlist_schemas、…"}}
tools.listChanged:falseは「起動後にツールの一覧は変わらない」という宣言です。resourcesも同じく一覧は変わらず、購読(subscribe)にも対応しません。スナップショットは起動時に読んだまま変わらないためです。loggingはSDKが自動で付け加える能力で、このサーバーは使っていません。versionはjarのマニフェストのImplementation-Versionで、jar以外から起動するとdevになります。
ツール:定義・結果・2種類のエラー
MCPのサーバーが提供できるものは3種類(ツール・リソース・プロンプト)あり、このサーバーはツールとリソースを提供します(プロンプトは無し)。中心はツールで、「AIが自分の判断で呼ぶ関数」です。リソースは次の節で扱います。
ツールの定義(tools/listで返る)
name・title:呼び出し名と表示名description:AIが使い分けを判断するための説明文inputSchema:引数のJSON Schemaannotations:readOnlyHint等。クライアントが許可の確認画面などで使うヒントで、保証ではありません
ツールの結果(tools/callで返る)
content:結果の配列。このサーバーは常にテキスト1件で、中身はJSONの文字列ですisError:ツールとして失敗したか
JSONの中にJSONの文字列が入る二重構造になるのは、このためです。
エラーには2つの経路がある
ツールのエラー(resultの中でisError: true)は、AI自身が読みます。「テーブルemployeが見つかりません。名前の似たテーブル: …」のように直し方を書いておけば、AIは引数を直して呼び直せます。このサーバーは、引数の誤りをすべてこちらで返します。
プロトコルのエラー(JSON-RPCのerror)は、存在しないツール名や不具合のときに返ります。AIに届くとは限りません。
→ 見つからない名前(ツールのエラー。ObjectResolverが文言を作る)
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_table","arguments":{"table":"employe"}}}
←
{"jsonrpc":"2.0","id":4,"result":{"isError":true,"content":[{"type":"text",
"text":"テーブルemployeが見つかりません。名前の似たテーブル: testdb:sample.employee, testdb:sample.employee_directory_view, …"}]}}
→ 存在しないツール(プロトコルのエラー。SDKが返す)
{"jsonrpc":"2.0","id":22,"method":"tools/call","params":{"name":"no_such_tool","arguments":{}}}
←
{"jsonrpc":"2.0","id":22,"error":{"code":-32602,"message":"Unknown tool: invalid_tool_name","data":"Tool not found: no_such_tool"}}
リソース:利用者が会話に添付するもの
ツールが「AIが呼ぶもの」なのに対し、リソースは利用者(またはクライアント)が選んで会話に載せるデータです。Claude Codeのターミナル版なら@、VS Codeなら「コンテキストの追加…」→「MCP リソース」の候補に並び、利用者が選ぶと中身がそのまま会話に添付されます。AIの判断に依らず、利用者が指名したテーブルの定義を確実に載せられるのが利点です。
このサーバーは、スナップショットの全テーブル(ビューを含む)を1つずつ具体的なリソースとして列挙します(URIテンプレートは使いません。理由はdocs/architecture/mcp-server.mdの「URIテンプレートと補完を出さない理由」)。読むと、get_tableを絞り込み無しで呼んだ場合と同じJSONが返ります。
→ 一覧(テーブルの数だけ並ぶ。先頭の2件)
{"jsonrpc":"2.0","id":3,"method":"resources/list"}
←
{"jsonrpc":"2.0","id":3,"result":{"resources":[
{"uri":"dbxray://testdb/sample/department","name":"sample.department","title":"sample.department",
"description":"sample.department / testdb / table / 社内の組織単位を管理するマスタテーブル。 従業員(employee)は必ずいずれか1つの部署に所属する。",
"mimeType":"application/json"},
{"uri":"dbxray://testdb/sample/employee","name":"従業員 (sample.employee)","title":"従業員 (sample.employee)",
"description":"従業員 (sample.employee) / testdb / table","mimeType":"application/json"}, …]}}
→ 読み出し(textはget_tableと同じJSONの文字列)
{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"dbxray://testdb/sample/employee"}}
←
{"jsonrpc":"2.0","id":4,"result":{"contents":[{"uri":"dbxray://testdb/sample/employee","mimeType":"application/json",
"text":"{\"schema\":\"sample\",\"name\":\"employee\",\"logicalName\":\"従業員\",\"type\":\"table\",\"columns\":[…]…}"}]}}
→ 一覧に無いURI(プロトコルのエラー。SDKが返す)
{"jsonrpc":"2.0","id":14,"method":"resources/read","params":{"uri":"dbxray://testdb/sample/nope"}}
←
{"jsonrpc":"2.0","id":14,"error":{"code":-32002,"message":"Resource not found","data":{"uri":"dbxray://testdb/sample/nope"}}}
nameは論理名 (スキーマ名.物理名)(論理名が無ければスキーマ名.物理名)。クライアントの絞り込みはname・description・URIが対象で、titleは対象外のため、論理名・物理名のどちらでも探せるようnameに両方を入れています。descriptionの先頭にもnameを入れるのは、Claude Codeの候補がURIを途中で切り、nameよりdescriptionを優先して表示するためです(120文字で切り詰め)。- URIの各区間(DB名・スキーマ名・テーブル名)はパーセントエンコードします。DB由来の名前は
/・空白・日本語を含み得るためです。name・descriptionも、制御文字・改行・書字方向を変える制御文字を空白に置き換えて1行にします。
MCPの概念と、このリポジトリでの実体
| MCPの概念 | このリポジトリでの実体 |
|---|---|
| トランスポート(stdio) | SDKのStdioServerTransportProvider。McpServerMain.startで生成 |
serverInfo・instructions | McpServerMainの定数SERVER_NAME・INSTRUCTIONSとversion() |
capabilities.tools・resources | ServerCapabilities.builder().tools(false).resources(false, false)(falseはlistChanged、リソースはsubscribe・listChanged) |
| ツールの定義 | ToolSpecifications.readOnlyTool(name, title, description, inputSchema, handler)。全ツールがこれを通る |
inputSchema | ToolSpecifications.objectSchemaとstringProperty等の部品で、Map<String,Object>として組み立てる |
annotations | 全ツール共通でreadOnlyHint=true・destructiveHint=false・idempotentHint=true・openWorldHint=false |
| tools/callの処理 | 各*ToolsクラスのprivateメソッドCallToolResult xxx(ToolArguments) |
結果のcontent | ToolResults.json(出力record)、またはスナップショットの1行そのもの |
isError: true | InvalidToolArgumentExceptionをreadOnlyToolが捕まえて変換 |
| リソースの定義と読み出し | TableDefinitionResources.specifications()がテーブルごとにSyncResourceSpecification(Resourceと読み出しのハンドラの組)を作る。URIはTableResourceUri.of、中身はTableOutputBuilder(get_tableと共通) |
2事前知識:MCP Java SDK(mcp-core 2.0.1)
使っているモジュールと、選んだ理由
| 依存 | 役割 | 補足 |
|---|---|---|
mcp-core | プロトコルの型(McpSchema)、サーバー・クライアント、トランスポート | 内部はProject Reactor(Mono・Flux)で書かれた非同期実装 |
mcp-json-jackson2 | Jackson 2によるJSONの変換(JacksonMcpJsonMapper)と、JSON Schemaの検証器 | SDKの一括版mcpはJackson 3を同梱するため、cliと同じJackson 2系に揃えるこの組み合わせを選んでいる(modules/mcp-server/build.gradleのコメント) |
jackson-databind | スナップショットの読み込み、ツールの結果のJSON化 | SDKとは別に、自前のコードが直接使う |
slf4j-simple(runtimeOnly) | SDKのログの出力先 | simplelogger.propertiesでWARN以上を標準エラーへ |
サーバーの組み立て方
SDKのサーバーはBuilderで組み立てます。このリポジトリでSDKのサーバーAPIに触るのは、McpServerMain.startのこの部分だけです。
final McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
McpServer.sync(new StdioServerTransportProvider(jsonMapper)) // 同期版のBuilder。stdioで1セッション
.serverInfo(SERVER_NAME, version())
.instructions(INSTRUCTIONS)
.capabilities(ServerCapabilities.builder().tools(false).resources(false, false).build())
.jsonMapper(jsonMapper)
.tools(new TableDefinitionTools(catalog).specifications()) // List<SyncToolSpecification>
.resources(new TableDefinitionResources(catalog).specifications()) // List<SyncResourceSpecification>
.immediateExecution(true) // 2.4節
.build(); // ここで標準入力の読み取りが始まる
ツール1つはSyncToolSpecificationで表します。中身は「ツールの定義(Tool)」と「呼ばれたときの処理(callHandler)」の組です。callHandlerの型は(McpSyncServerExchange, CallToolRequest) → CallToolResultで、exchangeはクライアントへ逆に問い合わせるための窓口ですが、このサーバーは使いません。リソース1つも同じ形のSyncResourceSpecificationで、「リソースの定義(Resource)」と「読まれたときの処理((exchange, ReadResourceRequest) → ReadResourceResult)」の組です。
SDKの中で起きること
- 同期APIは非同期実装の包みです。
McpServer.sync(...)で作るMcpSyncServerは内部でMcpAsyncServerを持ち、同期のハンドラをMono.fromCallable(...)で包んで登録します。 - stdioは専用スレッド2本で動きます。
StdioServerTransportProviderはbuild()の時点でセッションを1つ作り、標準入力を1行ずつ読むstdio-inboundスレッドと、標準出力へ書くstdio-outboundスレッド(それぞれ単一スレッドのExecutor)を起動します。 main()はすぐ戻ります。build()の後にmain()は終わりますが、上の2本はデーモンでないスレッドなのでJVMは動き続けます。標準入力がEOFになると読み取りが終わり、スレッドが片付いてプロセスが終了します。- 要求の振り分けはSDKの
McpServerSessionがmethod名で行います。tools/callなら、名前でツールを探し、2.5節の入力検証をしてから、登録したcallHandlerを呼びます。resources/readなら、URIが完全一致するリソースを探して読み出しのハンドラを呼び、無ければ-32002 Resource not foundを返します。
immediateExecution(true) の理由
既定(false)のとき
ツールのハンドラとリソースの読み出しをboundedElasticの複数スレッドで並行に実行します。並行に終わった複数の応答が、別々のスレッドから同時に出力用のシンク(Reactorのunicastシンク)へ書き込まれます。unicastシンクは同時書き込みを受け付けずtryEmitNextが失敗し、その応答は送られません。クライアントは応答を待ち続けて止まります(java-sdk#686)。
true にしたとき(このサーバー)
ハンドラをstdio-inboundスレッドでそのまま1つずつ実行します。書き込みが重ならないため止まりません。ツールもリソースの読み出しもメモリ上の検索だけで速いので、直列でも実用上の影響は小さい、という判断です。回帰はMcpServerProcessTest#respondsToPipelinedToolCalls(ツール)と#respondsToPipelinedResourceReads(リソース)が、応答を待たずに続けて呼んで確かめています。
修正はSDKのmainに入っていて未リリース(docs/architecture/mcp-server.mdの記述)。修正版が出たらimmediateExecutionを残すかを判断する、とされています。
SDKが先に行う入力検証
SDKはcallHandlerを呼ぶ前に、argumentsをinputSchema(JSON Schema)で検証します(ToolInputValidator。validateToolInputsの既定はtrue)。型・enum・minimum/maximum・required・additionalProperties:falseに反すると、ハンドラは呼ばれず、SDKが英語の文言でisError: trueを返します。
→ get_related_tables の depth は inputSchema で maximum 3
{"name":"get_related_tables","arguments":{"table":"employee","depth":4}}
← SDKが返す(ToolArgumentsの「1〜3の整数を指定してください」には届かない)
{"isError":true,"content":[{"type":"text","text":"Tool (get_related_tables) input validation failed: Validation failed: JSON schema validation errors: [/depth: must have a maximum value of 3]"}]}
引数の検証は役割分担です。型・範囲・enum・未知の引数はSDKが入力スキーマで検証し、これを正とします。自前のToolArgumentsは、スキーマで表せない検証(空白を除いた後の空、件数の上限、大文字小文字を区別しない選択肢の照合など)だけを行います。
ServiceLoader と shadowJar
SDKは既定のJSON実装(McpJsonMapperとJsonSchemaValidator)をjava.util.ServiceLoaderで探します。マッパーはMcpServerMainが明示的に渡していますが、入力検証器は渡していないため、mcp-json-jackson2のjarのMETA-INF/services/…JsonSchemaValidatorSupplierから見つけています。見つからなければ起動時にServiceConfigurationErrorで失敗します。
配布用の1つのjar(shadowJar)に依存を詰めると、同名のMETA-INF/servicesファイルが上書きされて登録が消えることがあります。build.gradleのmergeServiceFiles()はそれを防ぐ設定で、McpServerProcessTestが実物のjarを子プロセスで起動して確かめています。
3全体像と読む順
パッケージは5つです。依存の向きはtool → catalog ← snapshot・catalog ← insightで、検索と関連のたどりを持つcatalogはSDKにもJSONライブラリにも依存しません。cliのpresentation → application → domain ← infrastructureと同じ考え方で、中心のロジックを単体テストしやすくするためです。
catalogには外へ向かう矢印がありません。McpServerMainは2つの読み込み部品・withViewpoints・withFunctionTableUsages・TableDefinitionTools・TableDefinitionResources・SDKを順に呼んで配線します。snapshotとinsightは互いを知りません。| パッケージ | ひとことで |
|---|---|
mcp(直下) | 入口。起動引数の検証、読み込みの呼び出し、SDKの起動 |
mcp.snapshot | snapshot/のJSON Linesを読み、SchemaCatalogを組み立てる |
mcp.insight | insights/の観点(viewpoints.json)と、関数が利用しているテーブル({スキーマ}/functionTableUsages.json。cliの--preview)を読む |
mcp.catalog | メモリ上の全オブジェクトと、検索・名前の解決・関連のたどり・相互参照・関連のまとまり・ER図の範囲・中間テーブルの判定・構造の似たテーブルの探索 |
mcp.tool | MCPのツールとリソースの定義。引数を読み、catalogに聞き、結果をJSON(ER図はMermaid)にする |
おすすめの読む順
上から順に読むと、外側(起動とMCP)から内側(ルール)へ進めます。
McpServerMain:何をどの順で組み立てるか。INSTRUCTIONSでツールの全体像もつかめます。TableDefinitionTools:ツールクラス12の並び=クライアントに見える順。ToolSpecifications.readOnlyTool:全ツール共通の包み。引数の誤りがisErrorになる場所です。SchemaTools:小さなツールクラスで「型」をつかみ、続けて一番大きいTableTools。get_tableの結果の加工はTableOutputBuilderに分かれています。ToolArguments・ObjectResolver・Page:ツールクラスが共通で使う部品。SchemaCatalog:組み立てと、関心ごとの窓口(tables()・functions()・sequences()・types()・viewpoints()・functionTableUsages()・relations()・junctions())の一覧です。続けてTableCatalog・ObjectCatalog・ViewpointCatalogの公開メソッドを見ると、ツールから見たcatalogの全機能がつかめます。TableFilter・SearchQuery・RelationGraph:テーブルの絞り込み、点数づけ、幅優先探索。7章の図と合わせて読むと速いです。SnapshotDirectoryReader:ファイルからEntryができるまで。6章参照。DiagramTools・MermaidErDiagram、TableDefinitionResources:ツールの型から外れる2つ。結果にMermaidの記述を入れるER図(7.9節)と、AIではなく利用者が選ぶリソース(1章)です。JunctionDetector・SimilarTableFinder・ImpactAnalyzer:スナップショットに無い値を導く・既存の窓口をまとめる、後から足された3つ。中間テーブルの判定(7.11節)、構造の似たテーブルの探索(7.12節)、テーブルの影響範囲の集約(7.13節)です。
4起動の流れ
McpServerMain.mainは次の順に進みます。利用者が直せる誤りはすべてUserCorrectableExceptionで表し、mainの1箇所で捕まえて終了コード2で終わります(cliと同じ方針。ただしcliのクラスとは別物で、mcpパッケージに自前で持っています)。
- 標準エラーをUTF-8に固定:
System.setErr(new PrintStream(…, UTF_8))。実行環境のロケールで日本語が化けないようにするためです。 - 起動引数の解釈:
ServerArguments.parse。受け付けるのは--snapshot=<ディレクトリ>1つだけで、未知の引数・重複・空・未指定はすべて誤りです。 - 参考情報の読み込み:
InsightsDirectoryReaderのreadViewpoints(観点)とreadFunctionTableUsages(関数の利用テーブル。cliを--previewで実行した場合だけある。7.10節)。--snapshotの親の兄弟のinsights/を自分で求めて読みます。ディレクトリ・ファイルが無ければ0件で、誤りにはしません。 - スナップショットの読み込み:
SnapshotDirectoryReader.readがSchemaCatalogを返します。ディレクトリが無い、database.jsonが1つも無い、新しすぎるformatVersion、JSONとして読めない行、schema・nameの無い行は誤りです(メッセージにファイル名と行番号が入ります)。 - 参考情報の合成:
catalog.withViewpoints(viewpoints).withFunctionTableUsages(functionTableUsages)。SchemaCatalogは不変なので、観点・関数の利用テーブルを加えた新しいインスタンスを作ります。 - 読み込み件数の表示:標準エラーへ
[info]:15テーブルを読み込みました。 [snapshot=…]。 - SDKの起動:
start(catalog)。2章のBuilderでツールとリソース(全テーブル分)を登録して組み立て、build()した時点で標準入力の待ち受けが始まり、mainは戻ります。
I/Oの失敗(UncheckedIOException)は利用者が直せる誤りではないため捕まえません。スタックトレースが標準エラーに出て、終了コードは1になります。
51回のツール呼び出しの流れ
get_tableを例に、標準入力の1行が標準出力の1行になるまでをたどります。灰色の枠がSDK、色の付いた枠がこのリポジトリのコードです。右側は途中で抜ける経路です。
immediateExecution(true)のため、最後の書き込み以外はすべてstdio-inboundスレッドで直列に動きます。橙の2つはAIが読んで直せる「ツールのエラー」、灰色の破線の2つはプロトコルのエラーです。想定外の例外は自前のどの段からでも起こり得ます。全ツールを包むreadOnlyToolの核心はこの部分です。InvalidToolArgumentExceptionだけを捕まえてisErrorに変え、それ以外の例外はSDKへそのまま投げます。
.callHandler((exchange, request) -> {
try {
return handler.apply(new ToolArguments(request.arguments()));
} catch (InvalidToolArgumentException e) {
return CallToolResult.builder().addTextContent(e.getMessage()).isError(true).build();
}
})
どこで止まり、何が返るか
| 入力の例 | 止める場所 | 返り方と文言 |
|---|---|---|
name: "no_such_tool" | SDK 振り分け | error -32602 Unknown tool: invalid_tool_name |
sections: "foreignKeys,triggers"(型はarray) | SDK 型 | isError … /sections: string found, array expected |
get_tableにfoo: 1 | SDK additionalProperties:false | isError … property 'foo' is not defined in the schema … |
get_related_tablesにdepth: 4 | SDK maximum: 3 | isError … /depth: must have a maximum value of 3 |
list_tablesにerRole: "subtype" | SDK enum | isError … /erRole: does not have a value in the enumeration ["junction"] |
table: "employe" | 自前 ObjectResolver | isError テーブルemployeが見つかりません。名前の似たテーブル: … |
| 同名テーブルが複数スキーマにある | 自前 ObjectResolver | isError テーブルxが複数あります。schema(DBが異なる場合はdatabase)を指定してください。候補: … |
tableを2件+columns | 自前 TableTools.getTable | isError 引数columnsは、tableを1件指定した場合だけ使えます。 |
columnsに存在しないカラム | 自前 TableOutputBuilder | isError テーブルsample.employeeにカラムxがありません。カラム: … |
viewpointに未知のid | 自前 ViewpointResolver | isError 観点xが見つかりません。観点: personnel, project, … |
find_similar_tablesにtableとcolumnsの両方(どちらも無い場合も) | 自前 SimilarTableTools | isError 引数tableとcolumnsは、どちらか一方を指定してください。 |
find_similar_tablesのtableが複数スキーマにある | 自前 ObjectResolver(案内を差し替え) | isError テーブルxが複数あります。tableをスキーマ名.テーブル名の形(DBが異なる場合はdatabaseも)で指定してください。候補: … |
get_er_diagramにtableとviewpointの両方(どちらも無い場合も) | 自前 DiagramTools | isError 引数tableとviewpointは、どちらか一方を指定してください。 |
get_er_diagramにviewpointとdepth | 自前 DiagramTools | isError 引数depthは、tableを指定した場合だけ使えます。 |
get_er_diagramで描くテーブルが80件超 | 自前 DiagramTools.render | isError 図に描くテーブルがn件となり、上限(80件)を超えます。… |
resources/readに一覧に無いURI | SDK リソースの振り分け | error -32002 Resource not found |
| 不具合・I/Oの失敗 | SDK セッション | error -32603(例外のメッセージ) |
6データの持ち方
1行を2通りに持つ
このサーバーを理解するうえで一番効く事実です。スナップショットの1行は、読み込み時に「検索に使う型付きの項目」と「元の1行の文字列(json)」の両方として保持されます。
TableLine等の読み込み用recordはSnapshotDirectoryReaderの中だけにあり、読む項目を絞っています。get_table・get_sequence・get_type・get_functionとテーブルのリソースは元の1行を返すため、cliがスナップショットに項目を足すと、mcp-serverを変えなくてもAIに届きます。この設計から、次のことが言えます。
- 検索に新しい項目を使いたいときだけ、
*Lineと*Entryの両方にフィールドを足します。 get_tableのsectionsで削れるのはTableSectionのenumに並ぶ9項目(columns・indexes・constraints・foreignKeys・logicalRelations・triggers・definition・referencedTables・referencedByViews)だけです。それ以外(name・logicalName・partitionKeyや将来の項目)は常に残ります。- 観点(
viewpoints)はスナップショットの項目ではないため、get_tableの結果に後から足されます。sectionsでの絞り込みの対象外です。 - 関数が利用しているテーブル(
tableUsage)も、スナップショットの項目ではなく、参考情報からget_functionの結果に後から足されます。cliを--previewで実行して参考情報がある場合だけで、定義本体のハッシュが一致するときに限ります(7.10節)。 - 中間テーブルの区分(
erRole)・つなぐテーブル(junctionOf)・多対多の相手(manyToMany)も、スナップショットの項目ではなく、主キー・外部キーの構成から組み立て時に導いた値をget_tableの結果に後から足します(7.11節)。観点と同じくsectionsでの絞り込みの対象外で、該当すれば常に返します。 - テーブルのリソースの中身は、
get_tableと同じTableOutputBuilderで組み立てます(sections・columnsの絞り込み無し)。get_tableの結果に項目を足せば、リソースにも同じ項目が載ります。 - テーブルを参照しているビュー(
referencedByViews)もスナップショットの項目ではなく、他のビューのreferencedTablesから逆引きして後から足します(7.5節)。こちらはTableSectionに入っているため、sectionsで選べます。
catalogのrecord(Entry)一覧
| record | 元のファイル | 持つもの | SchemaObject |
|---|---|---|---|
DatabaseEntry | database.json | DB名、DBMS種別、メジャーバージョン(古いcliのスナップショットではnull) | — |
TableEntry | tables.jsonl | ObjectKey、論理名・区分・説明・備考、カラム・外部キー・論理リレーション・トリガー、ビューが参照するテーブル(ObjectKeyのリスト)、json | ○ |
ColumnEntry / RelationEntry / TriggerEntry | テーブルの中 | カラム、関連(自テーブル→参照先)、トリガー | — |
FunctionEntry | functions.jsonl | 種別・引数・戻り値・言語、definitionHash(定義本体のSHA-256。参考情報と照らす)、json(定義本体を含む) | — |
FunctionOverloads | (組み立て時に生成) | 同じキーのFunctionEntryの束。名前の解決の単位 | ○ |
SequenceEntry | sequences.jsonl | 所有カラム(テーブル名.カラム名)、json | ○ |
TypeEntry | types.jsonl | 種別(ENUM等)、json | ○ |
ViewpointEntry | insights/{DB}/viewpoints.json | id・表示名・説明・所属テーブル(ObjectKeyのリスト) | —(スキーマを持たない) |
FunctionTableUsageEntry | insights/{DB}/{schema}/functionTableUsages.jsonの1件 | ObjectKey・引数・definitionHash・状態・利用しているテーブル(UsedTable:スキーマ・名前・スキーマの決まり方・候補・操作)・動的SQL・incomplete・overloadsMerged | —(FunctionEntryに対応付ける) |
nullを持たない約束
すべてのEntryは、コンパクトコンストラクタでnullを空文字・空リストへ揃えます(TextValues.orEmpty)。結果をJSON化するToolResultsはJsonInclude.Include.NON_EMPTYで空の値を出力しないため、「値の無い項目は結果に出ない」という仕様は、この2つの組み合わせで成り立っています。nextOffsetやhasMoreをInteger・Booleanにしてnullを入れているのも、同じ理由で「無ければ出さない」ためです。
出力のrecordはそのままAPI
各ツールクラスの中にあるListTablesOutput・RelatedTablesOutput等のrecordは、Jacksonがrecordのコンポーネント名をキーにしてJSON化します。コンポーネント名を変えると、AIから見える結果の項目名が変わります。リファクタリングで改名するときは、ツールのAPIを変えることと同じです。
7catalogの中心ロジック
catalogの各窓口(TableCatalog・ObjectCatalog・ViewpointCatalog)は全オブジェクトをListで持ち、要求のたびに先頭から絞り込みます。あらかじめ作る索引は、RelationGraphの「参照先・参照元」の2つのMapとそこから求めたテーブルごとの関連の数(RelationCounts)、JunctionTablesの中間テーブルの判定結果と多対多の相手(7.11節)、SimilarTableFinderの正規化したカラム名とDBごとのカラム名の数(7.12節)だけです。どれも呼び出しのたびに求め直すと重い、またはスナップショットだけで決まる値です。スナップショットは数百テーブル規模なので、線形の走査で十分という割り切りです。
7.1 名前の解決(get_* と関連ツールの入口)
ObjectReference.of(database, schema, name):schemaが未指定で名前がsample.employeeの形なら、最初の.で分けてスキーマ修飾とみなします。ObjectCatalog.lookup<E extends SchemaObject>:DB・スキーマ・名前を大文字小文字を区別せず完全一致で比べます。テーブル・関数・シーケンス・型の4種類がSchemaObjectを実装しているので、この1つの汎用メソッドを共有できます(関数・シーケンス・型はObjectCatalogの窓口から、テーブルは候補の求め方だけを差し替えてTableCatalog.lookupから呼びます)。- 結果は封印インタフェース
Lookupの3通り:Found(1件)・Ambiguous(複数。キー順)・NotFound(候補付き)。 ObjectResolver.resolveがswitchのパターンマッチで分岐し、Found以外は候補を含む文言でInvalidToolArgumentExceptionを投げます。
見つからないときの候補の求め方は、種類で違います。テーブルは「その名前でsearch_tablesした上位5件」なので、カラム名の一致も候補に入ります(employeにattendanceが出るのはemployee_idカラムがあるため)。関数・シーケンス・型は「名前の部分一致の先頭5件」です。どちらもDB・スキーマの絞り込みを外して探します。
7.2 テーブル検索の点数(SearchQuery)
検索語をNFKC正規化と小文字化(TextValues.normalize)してから空白で区切ります。全角空白もNFKCで半角になるため区切りとして効きます。形態素解析はしません。
- AND:どの項目にも含まれない語が1つでもあれば、そのテーブルは一致しません。
- 語ごとに最大点:1語について、一致した項目の点数の最大値を取り、語ごとの最大値を合計します。
- 同点:DB名・スキーマ名・テーブル名の順。
| 一致した項目 | 完全一致 | 前方一致 | 部分一致 | matchedInの表記 |
|---|---|---|---|---|
| テーブル名 | 100 | 60 | 40 | name |
| 論理名 | 80 | — | 30 | logicalName |
| カラム名 | 25 | — | 12 | column:カラム名 |
| カラムの論理名 | — | — | 12 | column:カラム名 |
| 説明 | — | — | 8 | description |
| 備考 | — | — | 5 | remarks |
| カラムの備考 | — | — | 4 | column:カラム名 |
サンプルで従業員を検索すると、employee(論理名「従業員」の完全一致で80点)、employee_history(論理名「従業員履歴」の部分一致で30点)、department(説明に「従業員」を含み8点)の順になります。
{"total":3,"tables":[
{"database":"testdb","schema":"sample","name":"employee","logicalName":"従業員","type":"table",
"matchedIn":["logicalName","column:employee_id","column:employee_code","column:employee_name","column:manager_id"]},
{"database":"testdb","schema":"sample","name":"employee_history","logicalName":"従業員履歴","type":"table","matchedIn":["logicalName"]},
{"database":"testdb","schema":"sample","name":"department","type":"table","description":"社内の組織単位を…","matchedIn":["description"]}]}
totalはlimitで切る前の件数です。
一覧・検索の対象の絞り込み(DB・スキーマ・区分・観点・ER上の区分)は、ツール側でTableFilterとして1回組み立ててからTableCatalogへ渡し、catalogは各テーブルをmatchesで問い合わせるだけです。絞り込みは検索と並べ替えより前に行うため、観点・中間テーブルを指定しても総数とlimitが正しくなります。中間テーブルだけに絞る条件(withJunctionsOnly)は、判定結果の窓口JunctionTablesを受け取って問い合わせます。区分の値(table・view・materialized_view)はTableTypeの1か所にあり、list_tablesの入力スキーマのenumもここから作ります。
7.3 カラムの逆引き(ColumnQuery・find_columns)
強さは「物理名の完全一致 4 > 論理名の完全一致 3 > 物理名の前方一致 2 > 物理名か論理名の部分一致 1」です。match: exact(既定)では3以上だけが残ります。各カラムには、そのカラムが外部キー・論理リレーションで指している先(複合キーなら同じ位置の相手カラム)をreferencesとして添えます。
7.4 関連のグラフ(RelationGraph)
テーブルを頂点、外部キーと論理リレーションを辺とするグラフです。スナップショットには「自分が参照している先」しか書かれていないため、組み立て時に全テーブルを1周して「自分を参照している元」(incoming)の索引を作ります。辺の向きは常に参照元(外部キーを持つ側)→ 参照先です。
attendance_noteからの距離(幅優先探索の段)。find_join_pathは向きを問わずたどるため、3段目のemployee ← project_assignmentは矢印と逆に進みます。3つのビュー(attendance_monthly_view・employee_directory_view・project_summary_mv)は関連を持たないので描いていません。relatedTables(get_related_tables)
- 幅優先で
depth段まで広げる。directionで参照先だけ・参照元だけ・両方を選ぶ。 - 同じ関連は最初に見つけた段で1回だけ記録する(
putIfAbsent)。 - スナップショットに無い参照先は
missingTablesに分け、その先はたどらない。 - 自己参照(
manager_id)は関連として返るが、訪問済みなので再訪しない。
joinPaths(find_join_path)
- 向きを問わず幅優先。各頂点への「最短で来た手前の段」をすべて記録する(同じ段からの到達は複数残す)。
- 終点に届いた段を最後まで処理したら止め、終点から始点へ手前の段をたどり直して全最短経路を集める。
limit + 1件まで集め、超えた分があればhasMore: true。- 自己参照とスナップショットに無いテーブルは経由しない。
← find_join_path(from: attendance_note, to: project) の text(joinsは省略)
{"from":"sample.attendance_note","to":"sample.project","paths":[{
"tables":["sample.attendance_note","sample.attendance","sample.employee","sample.project_assignment","sample.project"],
"joins":[ {"from":"sample.attendance_note","fromColumns":["attendance_id","work_date"],"to":"sample.attendance",…}, … ]}]}
組み立て時には、テーブルごとの関連の数(RelationCounts)も1回だけ求めます。list_tablesが各行に添え、orderByでの並べ替えに使います。どれも自己参照とスナップショットに無いテーブルを除き、同じテーブルとの複数の関連は1つと数えます。
incoming:自テーブルを参照しているテーブルの数。多いほど中心のマスタ(サンプルのemployeeは5)。outgoing:自テーブルが参照しているテーブルの数。多いほどトランザクションや中間テーブル(project_assignmentは2)。impact:参照元をRelationCounts.IMPACT_DEPTH(3)段まで幅優先でたどって届くテーブルの数(変更の影響範囲)。被参照の少ない根っこのマスタも拾える(departmentは被参照1だがemployee経由で7)。段数を無制限にすると、大きなDBでは共通のマスタ経由で大半のテーブルに届いて差が出なくなるため打ち切る。
joinsの各要素のfrom・toは関連そのものの参照元・参照先で、たどる向きとは限りません。AIがJOINの条件を書くときはfromColumnsとtoColumnsを同じ位置同士で結びます。グラフの頂点の照合はObjectKeyのequals(大文字小文字を区別する)で、cliが同じ表記で出力する前提に立っています。
7.5 オブジェクト間の相互参照(SqlNames)
スナップショットには「このシーケンスをどのカラムが使うか」のような逆向きの情報がありません。そこで呼び出しのたびに全カラム・全トリガーを走査し、文字列からオブジェクトの名前を取り出して比べます。手順はどれも同じ3段です。
- 切り出す:カラムのデフォルト値・型、トリガーの関数名から、名前の部分だけを取り出す(種類ごとに方法が違う。下の表)。
- キーにする:
qualifyでObjectKeyにする。二重引用符を外し、スキーマ修飾が無ければ参照元テーブルのスキーマを補う。 - 比べる:
sameObjectで、調べたいオブジェクトのキーと大文字小文字を区別せずに比べる。
| ツールの項目 | 見る文字列 | サンプルの実データ | 切り出した名前 |
|---|---|---|---|
get_sequenceのusedByColumns | カラムのデフォルト値。nextval('…'の引用符の中を正規表現で取る | employee.employee_id:nextval('sample.employee_employee_id_seq'::regclass) | sample.employee_employee_id_seq |
get_typeのusedByColumns | カラムの型。末尾の配列の印[]を除く(下で詳しく) | employee.status:sample.employee_status_enum | sample.employee_status_enum |
get_functionのcalledByTriggers | トリガーの関数名。(があればそれ以降を除く | attendanceのトリガー:sample.check_work_minutes | sample.check_work_minutes |
型の切り出しの例。スナップショットのカラムの型(type)は、cliのmapperがPostgreSQLのformat_typeで取得した文字列です。ユーザー定義型を使うカラムには型名がそのまま入り、その型の配列を持つカラムには末尾に[]が付きます。typeOfColumnは末尾の[…](正規表現(\[\d*])+$)を除いてから比べるので、配列のカラムも「その型を使うカラム」としてusedByColumnsに入ります。
カラムの型(type) | 切り出した名前 | 結果 |
|---|---|---|
sample.employee_status_enumemployee.status | sample.employee_status_enum | ENUM employee_status_enumに一致 |
sample.positive_numericproject.budget | sample.positive_numeric | DOMAIN positive_numericに一致 |
sample.employee_status_enum[]仮の例:過去のステータスを配列で持つカラム | []を除いてsample.employee_status_enum | ENUMに一致。配列のカラムも数える |
employee_status_enum仮の例:型のスキーマが接続のsearch_pathに入っていて、修飾されずに出力された場合 | テーブルのスキーマを補ってsample.employee_status_enum | ENUMに一致 |
character varying(50)employee.employee_name | sample.character varying(50) | 一致しない。組み込み型はtypes.jsonlに無いため、何もしなくても候補から外れる |
integer[] | sample.integer | 一致しない(同上) |
PostgreSQLは配列の宣言時の大きさ(integer[3])を保持せず、format_typeもinteger[]と表すため、数字入りの[3]は実際の出力には現れません。正規表現は[3]や多次元の[][]も除ける形で書かれています。サンプル(docs/sample/postgres)には配列のカラムが無いため、上の表の配列の行は仮の例です。
サンプルでget_typeを呼んだ実際の結果です。ビューの同名カラムも数えられています。
{"schema":"sample","name":"employee_status_enum","category":"ENUM","definition":"ACTIVE, ON_LEAVE, RETIRED",
"usedByColumns":["sample.employee.status","sample.employee_directory_view.status"]}
参照しているビューの逆引き。get_tableのreferencedByViewsも、逆向きの情報を呼び出しのたびに求める相互参照です。ただし、cliがDBの依存関係から取得した参照先(ビューのreferencedTables。スキーマ名・名前)が構造化されて届くため、文字列の切り出しは要りません。viewsReferencingは、全テーブルのうちreferencedTablesに調べたいテーブルのObjectKeyを含むものをキー順に返します。比較はObjectKeyのequals(大文字小文字を区別する)で、関連のグラフと同じくcliが同じ表記で出力する前提です。サンプルでemployeeを引いた結果です(sectionsで絞っています)。
{"schema":"sample","name":"employee","logicalName":"従業員","type":"table",
"referencedByViews":[{"schema":"sample","name":"employee_directory_view","type":"view"}],
"viewpoints":[{"id":"personnel","name":"人事管理"},{"id":"project","name":"プロジェクト管理"}],
"manyToMany":[{"table":"sample.project","via":"sample.project_assignment"}]}
末尾のmanyToManyは、sectionsに関わらず足される多対多の相手です(7.11節)。
ビューの側のreferencedTablesには、出力対象外のスキーマのテーブルも事実として残ります(サンプルのattendance_monthly_viewはsample_archive.departmentを参照)。スナップショットに無いテーブルはget_tableで引けないため、逆引きの結果にも現れません。
7.6 関数のオーバーロードと定義本体
SchemaCatalogの組み立て時に、同じObjectKeyのFunctionEntryをFunctionOverloadsへ束ねます(出現順を保つLinkedHashMap)。名前の解決はこの束が単位です。get_functionは既定で定義本体(definition)を除きます。includeDefinition: trueのとき、全オーバーロードの本体が同じなら(Oracleのパッケージ内サブプログラム等)1つにまとめて上位に置き、違えば各オーバーロードに残します。どちらも4,000文字を超えると切り詰め、全体の文字数を書き添えます。getFunctionの中で、共通の本体を求めるsharedDefinitionと、各オーバーロードを切り詰めるtruncateDefinitionOfを順に呼んでいます。- オーバーロードごとに、参考情報があれば利用しているテーブル(
tableUsage)を足します(catalog.functionTableUsages().find(entry)。7.10節)。定義本体を除いても、FunctionEntry.definitionHashは読み込み時に求めてあるため、照合できます。
7.7 一覧の範囲(Page)
一覧系のツールはまず全件を求めてから、Page.applyでoffsetからlimit件を切り出します。totalは全件数、続きがあればnextOffset = offset + limitを返し、無ければnull(出力されない)です。offsetが全件より大きくても例外にはならず、空のリストになります。件数の既定値100・上限500はPage.DEFAULT_LIMIT・Page.MAX_LIMITにあり、固有の値を持つツール(search_tables・find_columns)以外はこれを使います。
7.8 関連のまとまり(ClusterDetector・list_table_clusters)
観点が宣言されていない範囲で、業務のまとまりの見当を付けるための推測です。使うのは外部キー・論理リレーションのつながりだけで、行数等の統計やテーブル名の接頭辞は使いません。
- 範囲を決める:
database・schemaで絞り、excludeViewpointTables: trueならいずれかの観点に所属するテーブルを除きます。範囲外のテーブルとの関連は無いものとみなします(RelationGraph.adjacentTablesの結果を範囲で切る)。 - 関連の無いテーブルを外す:範囲内に隣が1つも無いテーブルは、まとまりに含めず
unrelatedTablesの数だけを返します。 - 連結成分に分ける:向きを問わずつながるテーブルの組を1つのまとまりとします。
- 大きすぎれば分け直す:
maxClusterSize(既定30)を超えるまとまりは、被参照の最も多いテーブルをハブとして除き、残りを連結成分に分け直します。これを繰り返します。ただし、そのテーブルの被参照がTableClusters.HUB_MIN_INCOMING(3)未満なら、一続きの関連を切るだけになるため分けずに残します。 - ハブだけとつながるテーブルを寄せる:ハブを除いて1テーブルだけになったものは、被参照の最も多い隣のハブごとに1つのまとまりへ集めます。
まとまりはテーブルの多い順、同数なら代表(被参照の最も多いテーブル)の順に並べます。サンプルでmaxClusterSize: 4にすると、employee(被参照5)がハブとして除かれ、10テーブルのまとまりが3つに分かれます。
{"total":4,"unrelatedTables":3,
"hubs":[{"database":"testdb","name":"sample.employee","logicalName":"従業員","incoming":5}],
"clusters":[
{"database":"testdb","representative":"sample.department","size":5,
"tables":[{"name":"sample.department"},{"name":"sample.parking_spot"},{"name":"sample.audit_log"},
{"name":"sample.employee_history","logicalName":"従業員履歴"},{"name":"sample.employee_profile"}],
"hubs":["sample.employee"],"viewpoints":["personnel"]},
{"database":"testdb","representative":"sample.attendance","size":2,…,"hubs":["sample.employee"]},
{"database":"testdb","representative":"sample.project","size":2,…,"hubs":["sample.employee"],"viewpoints":["project"]},
{"database":"testdb","representative":"sample.warehouse_zone","size":2,…,"viewpoints":["logistics"]}]}
最初のまとまりの5テーブルは互いに関連を持たず、どれもハブのemployeeとだけつながっているため、5の手順で寄せられたものです。既定のmaxClusterSize(30)では分け直さず、employeeを代表とする10テーブルと、warehouse_zone・shipmentの2つになります。結果のviewpointsは、まとまりのテーブルを1つでも含む観点です。
7.9 ER図の範囲と記法(DiagramScope・MermaidErDiagram・get_er_diagram)
get_er_diagramは2段に分かれます。catalogが何を描くか(DiagramScope:テーブル・関連・スナップショットに無いテーブル)を決め、toolのMermaidErDiagramがどう書くか(MermaidのerDiagramの文字列)を受け持ちます。catalogにMermaidの知識を持ち込まないための分け方です。
tableを指定したとき
relatedTables(get_related_tablesと同じ幅優先)の結果を、DiagramScope.ofでそのまま範囲にする。depth(既定1、最大3)・direction(既定both)が使える。
viewpointを指定したとき
RelationGraph.among(観点の所属テーブルを渡す):所属テーブルのoutgoingのうち、参照先も所属テーブルのものだけを描く。観点の外との関連は描かない。- 関連の無い所属テーブルも箱だけ描く。スナップショットに無い所属テーブルは
missingTablesへ。
箱として描くのはDiagramScope.nodes()で、範囲のテーブルの後に、関連の参照先のうちスナップショットに無いテーブルを足したものです(カラムの無い箱になります)。これがDiagramTools.MAX_NODES(80。cliのerDiagramMaxNodesの既定値)を超えるとツールのエラーにします。
MermaidErDiagramの表記は、cliのER図(MermaidSupport)に揃えています。mcp-serverはcliのコードに依存できないため同じ規則を別に持ち、ずれはSampleErDiagramContractTestがcliの観点ページのER図(ベースライン)と突き合わせて検知します。
| 要素 | 書き方 |
|---|---|
| 関連の線 | 参照先(親)→ 参照元(子)の順に書く。外部キーは実線--、論理リレーションは破線..。ラベルは関連の名前 |
| 多重度の端点 | ONE_TO_MANY ||--o{、OPTIONAL_ONE_TO_MANY |o--o{、ONE_TO_ONE ||--o|、OPTIONAL_ONE_TO_ONE |o--o|。未設定・未知ならONE_TO_MANY |
| 箱の識別子 | スキーマ名_テーブル名の英数字・_以外を_に置き換える。置き換えで重なったら_2等の連番 |
| 箱の表示名 | テーブル名(論理名)。図の中に同名のテーブルが複数スキーマにあるときだけスキーマ名.テーブル名 |
| 箱のカラム | 図に描いた関連をつなぐカラムだけ。型 カラム名 PK, FK "論理名"。型は括弧(桁数)を除き記号を_に、空ならunknown |
| DB由来の文字列 | 表示名・関連名・論理名の二重引用符は単一引用符に、改行は空白に置き換える(quotable)。改行の後ろをMermaidの次の行や、コードブロックの閉じ(```)と解釈させないため |
サンプルで観点personnelを描いた実際の結果です。employeeの自己参照(manager_id)も描かれ、employee_profile.employee_idは主キーかつ外部キーなのでPK, FKになります。
← get_er_diagram(viewpoint: personnel) の text(mermaidは改行を戻して表示)
{"viewpoint":"personnel","mermaid":"…",
"tables":["sample.department","sample.employee","sample.employee_history","sample.employee_profile","sample.parking_spot"]}
erDiagram
sample_department["department"]
sample_employee["employee(従業員)"]
sample_employee_history["employee_history(従業員履歴)"]
sample_employee_profile["employee_profile"]
sample_parking_spot["parking_spot"]
sample_department ||--o{ sample_employee : "employee_department_id_fkey"
sample_employee |o--o{ sample_employee : "employee_manager_id_fkey"
sample_parking_spot |o--o| sample_employee : "employee_parking_spot_id_fkey"
sample_employee ||--o{ sample_employee_history : "employee_history_employee_id_fkey"
sample_employee ||--o| sample_employee_profile : "employee_profile_employee_id_fkey"
sample_department {
integer department_id PK
}
sample_employee {
integer employee_id PK "従業員ID"
integer department_id FK "所属部署ID"
integer manager_id FK "上長の従業員ID"
integer parking_spot_id FK "駐車場ID"
}
sample_employee_history {
integer employee_id FK
}
sample_employee_profile {
integer employee_id PK, FK
}
sample_parking_spot {
integer parking_spot_id PK
}
関連が1つも無い範囲ではmessage(「図に描くテーブルの間に、外部キー・論理リレーションはありません。」)を添えます。AIには、mermaidの値を手を加えずに```mermaidのコードブロックに入れて示すよう、ツールの説明で指示しています。
7.10 関数が利用しているテーブル(FunctionTableUsages・get_function の tableUsage)
cliを--previewで実行すると、関数・プロシージャが利用しているテーブルと操作(C・R・U・D)を、定義本体から機械的に抽出した参考値として、参考情報の{DB名}/{スキーマ名}/functionTableUsages.jsonへ出力します(cli 読解ガイドの9.9節)。DBが持つ事実ではなく精度にリスクがあるため、プレビューの機能(互換性の対象外)です。mcp-serverはこれを読み、get_functionがオーバーロードごとにtableUsageとして返します。観点と同じく、スナップショットとは別のファイルを、読み込みの後に合成する情報です。
--rm-distを付けずに対象のスキーマを減らした場合等)。そのとき、定義が変わった後の古い抽出結果を最新のものとして返さないよう、キーと引数に加えて、抽出元の定義本体のハッシュが一致するものだけを返します。- 読み込み:
InsightsDirectoryReader.readFunctionTableUsagesが、insights/{DB}/{スキーマ}/functionTableUsages.jsonをすべて読みます。無ければ0件で、誤りにはしません。formatVersionの上限は観点と同じ1(SUPPORTED_FORMAT_VERSION)、未知の項目は無視します。DB名・スキーマ名は、ディレクトリ名(パスに使えない文字を置き換えたもの)ではなく、ファイルの中のdatabase・schemaを使います(ファイルにDB名が無い場合だけ、DBのディレクトリ名)。:や/を含むDB名でも、スナップショットの関数と対応付けられるようにするためです。 - 定義本体のハッシュ:
FunctionEntry.definitionHashは、読み込み時にSnapshotDirectoryReaderが求めます。求め方(UTF-8のSHA-256の小文字の16進)は、cliのFunctionTableUsagesInsightと同じで、片方だけを変えると、すべての関数で参考情報が返らなくなります。Oracleのパッケージのサブプログラムは、どれも定義にパッケージ全体を持つため、直前と同じ定義本体ならハッシュを使い回します。definitionHashを持たない参考情報を、定義本体のある関数に対応付けることはしません。 - 返す形:
FunctionTools.tableUsageが、AIが読みやすい形に整えます。テーブルはスキーマ名.テーブル名、操作はCRUDの順に1つの文字列(RD等)です。解析した結果は、利用しているテーブルが無くてもtablesを空の配列で返します(参考情報が無い場合の「tableUsageの項目ごと無い」と区別するため)。解析しなかった場合(unsupported_language等)はstatus(とlanguage)だけです。動的SQL・読み切れなかった・オーバーロードをまとめた場合だけ、dynamicSql・incomplete・overloadsMergedを足します。 - スキーマが決まらない名前:
tableにスキーマを付けず、schemaResolution(search_path・current_user・synonym)と、同じ名前のテーブルがあるスキーマschemaCandidatesを返します。ツールの説明に「候補が1つでもそのスキーマに決めつけないこと」「出力対象のテーブルだけを返すため、tablesが空でもテーブルに触れないとは限らない」と書き、参考値であることをAIに伝えます。 - 逆引きと参考情報の状態:
findは、参考情報が無い場合と、ハッシュが異なる古い場合の両方で空を返します。利用者がすべきことが違う(無ければcliを--preview付きで実行し、古ければ出力し直す)ため、freshnessOfがCURRENT・STALE・ABSENTを区別して返します(findとget_functionの振る舞いは変えません)。UsedTable.matchesは、利用しているテーブルが指定したテーブルを指すかをCONFIRMED(スキーマが決まった名前の一致)・CANDIDATE(スキーマが決まらない名前で、候補に指定したテーブルのスキーマが含まれる)・NONEで返します。参考情報はカタログ上の名前で出力されるため、大文字小文字は区別して比べます。これらを使った「テーブルを使う関数」の逆引きは7.13節です。
get_functionの結果(tableUsageの部分)です。サンプルのclose_project(スキーマが決まる例)と、count_rows(動的SQLを含み、スキーマが決まらない例)を、既定のベースラインのスナップショット(docs/sample/postgres/output/snapshot)に、--previewの差分のベースラインの参考情報(docs/sample/postgres/output-preview/insights)を重ねたディレクトリをビルドしたjarに読ませて採取しました。
{"database":"testdb","schema":"sample","name":"close_project",
"overloads":[{"kind":"PROCEDURE","arguments":"IN p_project_id integer","language":"plpgsql",
"tableUsage":{"tables":[
{"table":"sample.audit_log","operations":"CR"},
{"table":"sample.employee","operations":"U"},
{"table":"sample.project","operations":"R"},
{"table":"sample.project_assignment","operations":"RD"}]}}]}
{"database":"testdb","schema":"sample","name":"count_rows",
"overloads":[{"kind":"FUNCTION","arguments":"p_table_name text","result":"bigint","language":"plpgsql",
"tableUsage":{
"tables":[{"table":"employee","schemaResolution":"search_path",
"schemaCandidates":["sample"],"operations":"R"}],
"dynamicSql":["EXECUTE"]}}]}
7.11 中間テーブルと多対多(JunctionDetector・JunctionTables・erRole・manyToMany)
多対多の関係は、DBには「中間テーブルからの2本の外部キー」という形でしか現れません。AIがprojectとemployeeの関係を知るには、project_assignmentを見つけて2本の外部キーを読む必要があります。そこで、主キー・外部キーの構成から中間テーブルを機械的に判定し、テーブルのER上の区分(erRole: junction)と、中間テーブルを介した多対多の相手(manyToMany)として返します。ClusterDetector(7.8節)と同じく、DBの事実ではなく推測です。
SampleSnapshotContractTestが、サンプルで中間テーブルと判定されるのはproject_assignmentだけであることを確かめています。JunctionDetector.detectは1テーブルについて次の順に判定します。
- 区分が
tableのテーブルだけを対象にする(ビュー・マテリアライズドビューは外す)。 - 候補を集める:参照元の列がすべて主キーに含まれる関連(
RelationGraph.outgoing。外部キー、論理リレーションの順)を集める。同じ列の組の関連は1本とみなし、先に現れる外部キーを残す。そのうえで、多重度がONE_TO_ONE・OPTIONAL_ONE_TO_ONEの関連(Relation.isOneToOne)を外す。列名は大文字小文字を区別せずに比べる。 - 候補が多すぎれば諦める:候補が
MAX_CANDIDATES(12)を超えると判定しない。組み合わせを総当たりするため、2のべき乗で増えるのを抑える。 - 組み合わせを総当たりする:主キーの列を過不足なく覆い、かつどの関連にも他の関連に無い列がある(外すと覆えなくなる)組み合わせのうち、関連の数が最も多いものを採る。同数なら候補の並びで先のもの。
- 2本以上なら中間テーブル:採った組み合わせが
MIN_LINKS(2)本以上なら、JunctionTable(テーブルと構成する関連)を返す。
この「外すと覆えなくなる組み合わせのうち最も多いもの」という決め方で、次のような形も扱えます。
| 形 | 判定 | 理由 |
|---|---|---|
主キー以外の列(role・assigned_at等の属性)がある | 中間テーブル | 主キー以外の列は問わない |
テナントIDを含む複合キー((tenant_id, a_id)→A、(tenant_id, b_id)→B) | 中間テーブル | 関連が列を共有していても、それぞれに自分だけの列(a_id・b_id)がある |
| 主キーに含まれる余分な関連(テナントだけを指す近道の外部キー等) | 中間テーブル(余分な関連は構成に入らない) | 余分な関連を足すと「外すと覆えなくなる」を満たさない |
| 同じテーブルを2本の外部キーで参照する(社員の上司・部下の組等) | 中間テーブル(自己多対多) | manyToManyの相手は自分自身になる |
| 3つ以上の親を持つ | 中間テーブル | 親どうしがすべて互いの多対多の相手になる |
1対1の拡張テーブル(employee_profile) | 中間テーブルではない | 1対1の関連は、同じ列の組をまとめた後で数えない |
サロゲートキー+一意索引(idを主キーにし、(a_id, b_id)を一意索引で守る) | 中間テーブルではない | スナップショットの索引は列が構造化されていないため、主キーでしか判定しない |
| 主キーに収まる関連が12本を超える | 判定しない | MAX_CANDIDATES |
JunctionTablesは、SchemaCatalog.ofの組み立て時に全テーブルを1回だけ判定し、結果を2つのMapに持ちます。主キー・外部キーの構成から導ける値なので、スナップショットには入れません(RelationCountsと同じ考え方)。
find(table)・roleOf(table):中間テーブルの構成と区分。list_tables・search_tablesの各行のerRole、erRole引数での絞り込み(TableFilter.withJunctionsOnly)、get_tableのerRole・junctionOfに使います。junctionOf(JunctionTable.parents)には、スナップショットに無い親も残します。manyToMany(table):中間テーブルごとに、つなぐ親どうしを互いの相手として集めたもの(相手・中間テーブルの順)。スナップショットに無い親は、起点にも相手にもしません(get_tableで引けないため)。自己多対多は、テーブルではなく関連の位置で比べて、自分自身を相手にします。
TableOutputBuilderは、manyToManyをMAX_MANY_TO_MANY(20)件までにし、超えると全体の件数をmanyToManyTotalで示します。ほぼすべての中間テーブルの親になる共通のテーブル(テナント等)の結果が膨らむのを防ぐためです。サンプルの実際の結果です(sectionsで絞っています)。
← get_table(table: project_assignment, sections: [foreignKeys]) の text(foreignKeysは省略)
{"schema":"sample","name":"project_assignment","type":"table","foreignKeys":[…],
"viewpoints":[{"id":"project","name":"プロジェクト管理"}],
"erRole":"junction","junctionOf":["sample.employee","sample.project"]}
← get_table(table: project, sections: [columns], columns: [project_id]) の text(columnsは省略)
{"schema":"sample","name":"project","logicalName":"プロジェクト","type":"table","columns":[…],
"viewpoints":[{"id":"project","name":"プロジェクト管理"}],
"manyToMany":[{"table":"sample.employee","via":"sample.project_assignment"}]}
← list_tables(erRole: junction) の text
{"total":1,"tables":[{"database":"testdb","schema":"sample","name":"project_assignment","type":"table",
"erRole":"junction","incoming":0,"outgoing":2,"impact":0}]}
区分の値はErRoleのenumにあり、list_tables・search_tablesの入力スキーマのenumもここから作ります。今の値はjunctionだけで、通常のテーブルは区分を持ちません(erRoleの項目ごと出ません)。
7.12 構造の似たテーブル(SimilarTableFinder・find_similar_tables)
新しいテーブルを作る前に同じ目的の既存テーブルが無いか確かめる、履歴・バックアップ・複製のテーブルを見つける、といった用途のためのツールです。比べるのはカラム名の集合だけで、説明文の近さのような意味の類似は扱いません(呼び出したAIがget_tableで判断します)。基準は既存のテーブル(table)か、設計中のカラム名の並び(columns)のどちらか一方で、SimilarTableQuery.ofTable・ofColumnsが条件を組み立てます。
- 比べる相手:範囲(
TableFilter。DB・スキーマ・観点)内の、区分がtableのテーブル。基準自身は除く。tableを指定した場合は、基準と同じDBに限る。 - カラム名を正規化する:NFKC+小文字(
TextValues.normalize)。正規化で同じ名前になるカラムは先のものを使う。相手のテーブルの正規化は、TableCatalogの組み立て時にSimilarTableFinderが1回だけ行う。 - 重みを付ける:カラム名ごとに
ln((N+1) / そのカラム名を持つテーブルの数)。Nと数は、範囲のDBの区分がtableの全テーブル(基準自身を除く)で数える。範囲内のどのテーブルも持たないカラム名は、1テーブルだけが持つ場合と同じ重みにする。 - 重み付きJaccard係数:共通のカラムの重みの合計 ÷(共通+基準にだけある+相手にだけある カラムの重みの合計)を百分率にする。カラムが同じ場合(
identical)だけを100とし、それ以外は丸めても99にする。 - 下限で絞り、並べる:共通のカラム数が
minShared(既定3)以上、類似度がminSimilarity(既定50)以上のものを、類似度の高い順・共通のカラムの多い順・名前の順に並べ、limit件を返す。
なぜ重みを付け、なぜDB全体で数えるのか
全テーブルがid・created_at・updated_atを持つDBでは、重みを付けないと、汎用のカラムを共有しているだけのテーブルが似ていると判定されます。持つテーブルが多いカラム名ほど重みを下げて、類似の根拠にならないようにしています(N+1で割るのは、全テーブルが持つカラム名でも重みが0にならないようにするため)。
重みを探す範囲(スキーマ・観点)で数えると、範囲を絞るほどテーブルが減り、汎用のカラムの重みが下がらなくなります。そのため範囲によらずDB全体で数えます。基準自身を数えないのは、tableで指定した場合と、同じカラムをcolumnsで指定した場合の重みを揃えるためです。DBのテーブル数が少ないと効きが弱いのは避けられません。
サンプルでemployeeを基準にした場合の計算です。区分がtableのテーブルは12あり、基準を除くとN=11です。
| カラム名 | 持つテーブルの数 | 重み ln(12/数) | 基準(employee) | 相手(employee_history) |
|---|---|---|---|---|
employee_id | 4 | 1.10 | ○ | ○ |
department_id・parking_spot_id | 2 | 1.79 | ○ | ○ |
employee_code・employee_name・manager_id・status・salary・profile・hired_date・updated_at | 1 | 2.48 | ○ | ○ |
history_id・valid_from・valid_to | 1 | 2.48 | — | ○ |
共通の11カラムの重みは1.10+1.79×2+2.48×8≒24.56、相手にだけある3カラムは2.48×3≒7.46で、類似度は24.56÷(24.56+7.46)≒77%です。基準にだけあるカラムが無く、相手にだけあるカラムがあるので、種別はsuperset(相手が基準の全カラムを持ち、追加がある。履歴・拡張の疑い)になります。
← find_similar_tables(table: employee) の text
{"base":"sample.employee","total":1,"tables":[{"database":"testdb","name":"sample.employee_history","logicalName":"従業員履歴",
"similarity":77,"kind":"superset","sharedColumns":11,"onlyInOther":["history_id","valid_from","valid_to"],
"typeMismatches":[{"column":"employee_name","base":"character varying(50)","other":"character varying(100)","difference":"size"}],
"relatedToBase":true}]}
← find_similar_tables(columns: [attendance_id, work_date, note, employee_id]) の text
{"total":1,"tables":[{"database":"testdb","name":"sample.attendance_note","similarity":64,"kind":"overlap","sharedColumns":3,
"onlyInBase":["employee_id"],"onlyInOther":["note_id"]}]}
← find_similar_tables(columns: [attendance_id, work_date]) の text(基準のカラムがminSharedに満たない)
{"total":0,"message":"基準のカラムは2個で、minShared(3)に満たないため、条件を満たすテーブルはありません。minSharedを2以下にしてください。"}
| 結果の項目 | 求め方 |
|---|---|
kind | SimilarityKind.of。閾値を使わず、基準にだけある・相手にだけあるカラムの有無だけで決める。identical(どちらも無い)・superset(相手にだけある)・subset(基準にだけある)・overlap(どちらもある) |
onlyInBase・onlyInOther | 元のカラム名を、それぞれのテーブルでの並び順で。MAX_LISTED_COLUMNS(20)件までにし、省いた数をonlyInBaseOmitted・onlyInOtherOmittedで示す |
typeMismatches | tableを指定した場合だけ(columnsには型が無い)。ColumnType.parseで型の表記を括弧の外(名前)と内(長さ・精度)に分け、名前が違えばtype、長さ・精度だけが違えばsize。timestamp(6) without time zoneのように括弧が途中に入る表記も、括弧の外を名前とみなす。大文字小文字・全角半角・空白だけの違いは違いとしない |
relatedToBase | 基準と外部キー・論理リレーションで直接つながる(RelationGraph.adjacentTables)ならtrue。除かずに印を付けるのは、履歴・拡張のテーブルは基準を参照していることが多く、除くと見つけたいテーブルが消えるため。親子は互いのキーを持つため似て見えることもある |
message | 条件を満たすテーブルが無い場合だけ。基準のカラムがminSharedに満たなければ下げる値を、それ以外はminShared・minSimilarityを小さくする方法を示す |
tableを指定した場合、引数schemaは探す範囲の絞り込みに使い、基準の特定には使いません。そのため、同名のテーブルが複数のスキーマにあるときはスキーマ名.テーブル名の形で指定させます。ObjectResolver.resolveには、複数に当てはまる場合の案内(ambiguityHint)を差し替えられる形があり、SimilarTableToolsはそれを使ってこの形を案内します。
7.13 テーブルの影響範囲(ImpactAnalyzer・get_impact)
テーブルを変更・削除する前に「何が壊れうるか」を調べるためのツールです。参照元のテーブル・ビュー・トリガー・関数は別々の窓口が持つため、これまではget_related_tables・get_table・get_function・list_triggersを組み合わせて調べる必要がありました。しかも「このテーブルを使う関数」は、tableUsageが関数からテーブルの向きしか返さないため、引く手段がありませんでした。get_impactは、これらをテーブルを起点に1回で集めます。
集めるだけで、スナップショットに無い値は足しません。複数の窓口にまたがるため入口はSchemaCatalog.impactOfで、集め方の規則はImpactAnalyzer(package-private。ClusterDetectorと同じ)が持ち、結果はTableImpact(record)に入ります。呼び出しのたびに求めます。
affected)、破線の1つが「自テーブルが使うもの」(dependsOn)です。関数だけは、cliの--previewの参考情報に依存します(破線の枠)。集め方は5つに分かれ、それぞれ結果の1つのキーになります。
- 参照元(
affected.referencedBy):RelationGraph.relatedTables(table, 3, INCOMING)は、テーブルではなく関連を段数つきで返します。段1の関連をdirect(自己参照を含む。参照元のテーブル・種別・関連名の順)に、段1に現れないテーブルをindirect(最小の段数と、その段で見つけた関連の参照先via。段数・テーブルの順)に分けます。同じテーブルが段1と段2の両方に現れる場合はdirectだけに載せ、自テーブルはindirectに含めません。そのため、directの参照元(自テーブルを除く)とindirectを重複なく合わせた数は、list_tablesのimpactと一致します。 - ビュー(
affected.views):TableCatalog.viewsReferencingを、ビューを参照するビューまで段ごとに繰り返します。同じビューは1度しか数えないため、循環していても止まります。経由したビューをviaで示します。 - トリガー(
affected.triggers):自テーブルのtriggers()です。ビューを調べた場合は、INSTEAD OFトリガーが入ります。 - 関数(
affected.functions):同じDBの全関数を関数名(FunctionOverloads)ごとに見て、オーバーロードの参考情報(FunctionTableUsages.find)のUsedTableが、自テーブルまたは2.で集めたビューを指すかをUsedTable.matchesで判定します。一致しなかったオーバーロードのうち、参考情報はあるが確かめきれないもの(動的SQL・読み切れなかった・解析不可)は、functions.unverifiedに理由とともに挙げます。 - 依存先(
dependsOn):自テーブルのカラムについて、デフォルト値のnextvalが使うシーケンス(SqlNames.sequenceOfDefault)と、型に使われているスナップショットにあるユーザー定義型(SqlNames.typeOfColumn)を、使うカラムとともにまとめます。
なぜ affected と dependsOn を分けるのか
参照元・ビュー・トリガー・関数は「自テーブルを変えると影響を受けるもの」、シーケンス・型は「自テーブルが使っているもの」で、向きが逆です。同じ階層に並べると、AIはどれも影響範囲の一覧として読んでしまいます。キーの名前だけで向きが分かるよう、2つに分けています。
関数の逆引きは、次の規則で求めます。
- 完全一致:参考情報は出力対象のカタログ上の名前で出力されるため、大文字小文字を区別して比べます(PostgreSQLでは、二重引用符で囲んだ
"Employee"とemployeeが別のテーブルになり得ます)。ビューの逆引き(viewsReferencing)と揃えています。 - 確定と候補:スキーマが決まった名前は確定です。スキーマが決まらない名前(
search_path等)は、候補のスキーマに自テーブルのスキーマが含まれる場合だけ候補とし、結果ではmatch: possibleとschemaResolutionで示します。1つのオーバーロードの中で同じテーブルへの一致が確定と候補にまたがる場合は、確定を採ります。 - 出力の単位は関数と経路:1行は関数名と経路(自テーブルか、使っているビューか)の組で、自テーブルとビューの両方を使う関数は2行になります(
moreの総数もこの行数です)。同名のオーバーロードは1行にまとめ、一致したオーバーロードの引数をoverloadsに並べ、operationsは合算します。Oracleのパッケージで本体をまとめて抽出した場合(overloadsMerged)に、同じ内容が重複して並ぶのを防ぎます。候補になるのは、一致したすべてのオーバーロードが候補でしか一致しなかった場合です。確定のオーバーロードと候補のオーバーロードが混ざる場合はpossibleにならず、候補のオーバーロードの操作もoperationsに合算されるため、確定のように見えます(既知の制約で、FunctionUse.possibleのJavadocに書いてあります)。 - ビュー経由:参考情報には関数が使うビューも載るため、ビューだけを読む関数も拾います。その行には、使っているビューを
viaで付けます。 - 実行するトリガー:
TableCatalog.triggersCallingで、関数を実行するトリガーをcalledByTriggersに付けます。ビューのINSTEAD OFトリガー経由で書き込む関数が、ここで分かります。
参考情報は、動的SQL・呼び出している関数の中・出力対象外のテーブルを含みません。載っていないことが使われていないことを意味しないため、functions.coverageで調べがついた範囲を数えます。同じDBの関数・プロシージャを、参考情報の状態で分けます。
| 項目 | 意味 | 利用者がすべきこと |
|---|---|---|
noInsight | 参考情報が無い(freshnessOfがABSENT) | cliを--preview付きで実行して出力する |
stale | 参考情報はあるが定義本体のハッシュが異なる(STALE) | cliを--preview付きで出力し直す |
unsupported | 参考情報はあるが解析していない(対象外の言語・難読化等) | 手で確認する |
analyzed | 解析済み。内数としてwithDynamicSql(動的SQLを含む)・incomplete(読み切れなかった) | 動的SQLの中の参照は含まれない |
triggersWithoutFunction | 実行する関数名を持たず、本体を直接持つトリガーの数(Oracle) | 本体の中の参照は参考情報の対象外のため、トリガーが触るテーブルは分からない。手で確認する |
totalは、analyzed・unsupported・noInsight・staleの合計です。この区別は、結果のnotes(参考情報が無い・古い関数がある場合と、関数名を持たないトリガーがある場合に、対応を示す固定文)にも反映されます。
functions.unverifiedは、coverageのunsupportedと、analyzedの内数(withDynamicSql・incomplete)のうち、usagesに載らなかったオーバーロードを、function・arguments・reason(dynamicSql・incomplete・unsupported。複数当てはまる場合はこの順で先のものを採る)で返します。参考情報からは、自テーブルを使うかどうかを確かめきれない関数なので、AIはget_functionで定義本体を確かめます。テーブルとの関係は分からないため、同じDBの該当する関数をすべて挙げます。
なぜ noInsight・stale を unverified に挙げないのか
参考情報が無い・古い関数への対処は、関数を1つずつ読むことではなく、cliを--preview付きで出力し直すことです。--previewを付けずに出力したDBでは全関数が該当するため、挙げると概要が毎回50件で埋まり、coverage・notesと同じことを繰り返すだけになります。そのため件数(coverage)とnotesの案内だけで伝え、unverifiedは「定義本体を読めば確かめられる関数」に限ります。プレビューの機能が正規版になって参考情報が常に出力されれば、この2つは出力の取り違えを示す警告に近くなります。
サンプルのemployeeの結果です(既定のベースラインのスナップショットに、--previewの差分のベースラインの参考情報を合わせたもの。direct・triggers・usagesは一部を省略しています)。
← get_impact(table: employee) の text
{"table":"sample.employee","database":"testdb","type":"table",
"affected":{
"referencedBy":{
"direct":[{"table":"sample.attendance","kind":"foreignKey","columns":["employee_id"],"referencedColumns":["employee_id"],"cardinality":"ONE_TO_MANY"},
{"table":"sample.employee","kind":"foreignKey","columns":["manager_id"],…}, ← 自己参照
…],
"indirect":[{"table":"sample.attendance_note","depth":2,"via":"sample.attendance"}],"indirectTotal":1},
"views":[{"view":"sample.employee_directory_view","type":"view"},
{"view":"sample.attendance_monthly_view","type":"view","via":"sample.employee_directory_view"}],
"triggers":[{"name":"trg_employee_audit","timing":"AFTER","events":["INSERT","DELETE","UPDATE"],"function":"sample.log_employee_change"},…],
"functions":{"preview":true,
"coverage":{"total":11,"analyzed":11,"unsupported":0,"noInsight":0,"stale":0,"withDynamicSql":1,"incomplete":0,"triggersWithoutFunction":0},
"usages":[{"function":"sample.close_project","kind":"PROCEDURE","overloads":["IN p_project_id integer"],"operations":"U"},
{"function":"sample.count_rows",…,"operations":"R","match":"possible","schemaResolution":"search_path","dynamicSql":["EXECUTE"]},
{"function":"sample.employee_directory_insert",…,"operations":"C",
"calledByTriggers":["sample.employee_directory_view.trg_employee_directory_insert"]},…]}},
"dependsOn":{"sequences":[{"sequence":"sample.employee_employee_id_seq","columns":["employee_id"]}],
"types":[{"type":"sample.employee_status_enum","category":"ENUM","columns":["status"]}]},
"notes":[…]}
count_rowsは、動的SQL(EXECUTE)を含み、スキーマが決まらない名前(search_path)でしかemployeeに一致しないため、候補(possible)として示されます。Oracleのサンプルでは、EMPLOYEE_DIRECTORY_VIEWのINSTEAD OFトリガーがEMPLOYEEへINSERTしますが、トリガーの本体の中の参照は参考情報の対象外のため、functionsには現れません(SampleOracleImpactContractTestが確かめています)。PostgreSQLのサンプルのemployeeでは、確かめきれない関数が無いためunverifiedは出力されません。Oracleのサンプルでは、動的SQLを含むPROJECT_OPS.PURGE_AUDIT_LOGがreason: dynamicSqlとして挙がります。
sectionを指定しない概要では、各リスト(direct・indirect・views・triggers・usages・unverified)をImpactTools.MAX_LISTED(50)件までを決まった順で返します。超えたリストは、総数と続きの取り方をmore(section・total・nextOffset)だけで示します。共通のマスタ等では参照元がすぐに50件を超えるため、間接参照の総数indirectTotalは常に返します。
続きは、同じツールにsection(概要でそのリストを持つキーの名前。direct・indirect・views・triggers・usages・unverified)とoffset・limit(既定100、最大500)を指定して読みます。結果はsection・total・nextOffset・itemsで、itemsの形は概要の各リストと同じです。offset・limitだけを指定した場合は、無視せずエラーにします(sectionの指定漏れで、先頭のページを返してしまわないため)。
なぜツールを分けず、section の引数で続きを取るのか
リストごとにツール(get_impact_views等)を分けると、AIに見せるツールの数と説明の量が増え、影響範囲の調べ方が6つの入口に散らばります。sectionの引数なら、最初に概要を1回呼び、moreに示されたsectionとnextOffsetをそのまま次の呼び出しに渡せます。ほかの一覧のツールのoffset・nextOffsetと同じ作法です。
8ツールとソースの対応
ツールクラスの型
12のツールクラスはすべて同じ形をしています。1つ読めば残りは差分を見るだけです。
final class XxxTools {
static final String GET_XXX = "get_xxx"; // ツール名。他クラスの案内文からも参照される
private final SchemaCatalog catalog;
List<SyncToolSpecification> specifications() {
return List.of(readOnlyTool(GET_XXX, "表示名", "AIが読む説明",
objectSchema(Map.of(/* 引数名 → JSON Schema */), List.of(/* 必須の引数 */)),
this::getXxx));
}
private CallToolResult getXxx(ToolArguments arguments) {
// 1. arguments から読む(一覧なら Page.read(arguments, Page.DEFAULT_LIMIT)。誤りは InvalidToolArgumentException)
// 2. catalog に問い合わせる(名前なら ObjectResolver.resolve 経由)
// 3. 出力 record を ToolResults.json(...) で返す
}
record GetXxxOutput(…) {} // コンポーネント名がそのまま結果のJSONのキー
}
19ツールの対応表
| ツール | ハンドラ | catalogへの問い合わせ | 結果 |
|---|---|---|---|
list_schemas | SchemaTools#listSchemas | schemas() → SchemaSummary | ListSchemasOutput |
list_viewpoints | ViewpointTools#listViewpoints | viewpoints().list(scope) | ListViewpointsOutput |
list_table_clusters | ClusterTools#listTableClusters | tableClusters(scope, maxClusterSize, excludeViewpointTables)・viewpoints().list | ListTableClustersOutput |
search_tables | TableTools#searchTables | SearchQuery.of・TableFilter(viewpoints().find・junctions())→ tables().search・junctions().roleOf | SearchTablesOutput |
list_tables | TableTools#listTables | TableFilter(区分はTableType、ER上の区分はErRole)・TableOrder → tables().list・relations().counts・junctions().roleOf | ListTablesOutput |
get_table | TableTools#getTable | tables().lookup(件数分)→ TableOutputBuilder(tables().viewsReferencing・viewpoints().containing・junctions().find・junctions().manyToMany) | 元の1行を加工(複数ならGetTableOutput) |
find_columns | TableTools#findColumns | ColumnQuery.of → tables().findColumns | FindColumnsOutput |
find_similar_tables | SimilarTableTools#findSimilarTables | SimilarTableQuery.ofTable(tables().lookup)かofColumns・TableFilter → tables().findSimilar | FindSimilarTablesOutput |
get_related_tables | RelationTools#getRelatedTables | tables().lookup → relations().relatedTables | RelatedTablesOutput |
find_join_path | RelationTools#findJoinPath | tables().lookup×2 → relations().joinPaths | JoinPathOutput |
get_impact | ImpactTools#getImpact | tables().lookup → impactOf(ImpactAnalyzer: relations().relatedTables・tables().viewsReferencing・functionTableUsages().find・tables().triggersCalling・SqlNames) | GetImpactOutput(affected・dependsOn・more・notes)。section指定時はSectionOutput(Section.itemsの1ページ) |
get_er_diagram | DiagramTools#getErDiagram | tables().lookup → relations().relatedTables(table)か、viewpoints().find → relations().among(viewpoint)→ MermaidErDiagram.render | ErDiagramOutput |
list_functions | FunctionTools#listFunctions | functions().list(scope, NameFilter)(オーバーロードを展開) | ListFunctionsOutput |
get_function | FunctionTools#getFunction | functions().lookup → tables().triggersCalling・functionTableUsages().find | GetFunctionOutput |
list_sequences | SequenceTools#listSequences | sequences().list | ListSequencesOutput |
get_sequence | SequenceTools#getSequence | sequences().lookup → tables().columnsUsing | 元の1行+usedByColumns |
list_types | TypeTools#listTypes | types().list(scope, filter)+種別の絞り込み | ListTypesOutput |
get_type | TypeTools#getType | types().lookup → tables().columnsUsing | 元の1行+usedByColumns |
list_triggers | TriggerTools#listTriggers | tables().triggers(scope) | ListTriggersOutput |
件数・段数の上限
どれもAIのコンテキストを圧迫しないための上限です。上限はinputSchemaのmaximumだけに書かれ、範囲外はSDKが止めます(ToolArguments.optionalIntは範囲を検査しません)。
| ツール・引数 | 既定 | 上限 | 定数の場所 |
|---|---|---|---|
list_table_clustersのmaxClusterSize | 30 | 500 | ClusterTools(下限2) |
list_table_clustersのlimit | 20 | 100 | ClusterTools |
list_table_clustersの1まとまりのテーブル | — | 100 | ClusterTools.MAX_LISTED_TABLES(超えたらhasMoreTables) |
search_tablesのlimit | 20 | 100 | TableTools |
list_tablesのlimit | 100 | 500 | Page.DEFAULT_LIMIT・MAX_LIMIT |
find_columnsのlimit | 50 | 500 | TableTools |
get_tableのtableの件数 | — | 10 | TableTools.MAX_TABLES |
get_tableのmanyToMany | — | 20 | TableOutputBuilder.MAX_MANY_TO_MANY(超えたらmanyToManyTotal) |
find_similar_tablesのlimit | 10 | 50 | SimilarTableTools |
find_similar_tablesのcolumnsの件数 | — | 100 | SimilarTableTools.MAX_COLUMNS |
find_similar_tablesのminShared・minSimilarity | 3・50 | 20・100 | SimilarTableTools(下限1・10) |
find_similar_tablesのonlyInBase・onlyInOther | — | 20 | SimilarTableTools.MAX_LISTED_COLUMNS(超えたら…Omitted) |
get_impactの概要の各リスト(direct・indirect・views・triggers・usages・unverified) | — | 50 | ImpactTools.MAX_LISTED(超えたらmore。indirectTotalは常に) |
get_impactのsection指定時の1ページ | 100 | 500 | Page.DEFAULT_LIMIT・Page.MAX_LIMIT(ほかの一覧のツールと同じ) |
get_impactの間接参照の段数 | — | 3 | RelationCounts.IMPACT_DEPTH(list_tablesのimpactと同じ) |
get_related_tablesのdepth | 1 | 3 | RelationTools.MAX_DEPTH |
find_join_pathのmaxLength | 4 | 6 | RelationTools |
find_join_pathのlimit | 5 | 20 | RelationTools |
get_er_diagramのdepth | 1 | 3 | DiagramTools |
get_er_diagramの図に描くテーブル | — | 80 | DiagramTools.MAX_NODES(超えたらツールのエラー) |
list_functions・list_sequences・list_types・list_triggersのlimit | 100 | 500 | Page.DEFAULT_LIMIT・MAX_LIMIT |
get_functionの定義本体 | — | 4,000字 | FunctionTools.MAX_DEFINITION_LENGTH |
| 見つからないときの候補 | — | 5 | ObjectCatalog.MAX_SUGGESTIONS |
| 中間テーブルの判定で主キーに収まる関連 | — | 12 | JunctionDetector.MAX_CANDIDATES(超えたら判定しない) |
9ソースごとの役割
main配下のクラスと、テストの一覧です。
| ファイル | 役割 | 使う相手・使われる相手 |
|---|---|---|
| mcp(直下):入口 | ||
| McpServerMain | エントリーポイント。起動引数の検証→観点・関数の利用テーブル・スナップショットの読み込み→SDKの起動(ツールとリソースの登録)。標準エラーをUTF-8に固定し、UserCorrectableExceptionなら終了コード2 | → ServerArguments・2つのReader・TableDefinitionTools・TableDefinitionResources・SDK |
| ServerArguments | 起動引数(--snapshot=のみ)の解釈と検証 | ← McpServerMain |
| UserCorrectableException | 利用者が起動引数・スナップショットを直せば解消する失敗。メッセージに直し方を書く | ← ServerArguments・2つのReader |
| package-info | 依存の向き(tool → catalog ← snapshot・catalog ← insight)の宣言 | — |
| mcp.snapshot / mcp.insight:ファイルの読み込み | ||
| SnapshotDirectoryReader | snapshot/{DB}/database.jsonと{schema}/{tables,functions,sequences,types}.jsonlを読みSchemaCatalogを作る。無いファイルは0件、未知の項目は無視、formatVersionの上限は1。関数は定義本体のSHA-256(definitionHash)も求める。読み込み用のrecord(TableLine等)を内部に持つ | ← McpServerMain/→ SchemaCatalog.of・各Entry |
| InsightsDirectoryReader | --snapshotの親の兄弟insights/{DB}/viewpoints.jsonと{DB}/{スキーマ}/functionTableUsages.jsonを読む。観点のDB名はディレクトリ名、関数の利用テーブルのDB名・スキーマ名はファイルの中の値(ファイルにDB名が無ければDBのディレクトリ名)。formatVersionの上限は1。無ければ0件 | ← McpServerMain/→ ViewpointEntry・FunctionTableUsageEntry・ObjectKey |
| package-info ×2 | 各パッケージの要旨 | — |
| mcp.catalog:中心のロジック | ||
| SchemaCatalog | 全オブジェクトを保持する不変クラス。関心ごとの窓口(tables()・functions()・sequences()・types()・viewpoints()・functionTableUsages()・relations()・junctions())を返し、自身は複数の窓口にまたがる処理(schemas・tableClusters・impactOf)だけを持つ。withViewpoints・withFunctionTableUsagesで観点・関数の利用テーブルを差し替えた新インスタンスを返す | ← 全ツールクラス・Reader/→ 各窓口・ClusterDetector・ImpactAnalyzer・SchemaSummary.summarize(組み立て時にJunctionTablesも作る) |
| TableCatalog | テーブル(ビューを含む)の一覧(関連の数での並べ替えを含む)・検索・名前の解決・カラムの逆引き・構造の似たテーブルの探索(findSimilar)・トリガーの一覧と、テーブルを走査して求める相互参照(参照しているビュー・関数を実行するトリガー・シーケンスや型を使うカラム) | ← SchemaCatalog.tables()/→ RelationGraph・SearchQuery・ColumnQuery・SimilarTableFinder・SqlNames |
| ObjectCatalog | 関数(FunctionOverloads)・シーケンス・型に共通の、名前の順の一覧(list)と名前の解決(lookup。見つからなければ部分一致の候補)。テーブルの解決もここの汎用メソッドを使う。種類に固有の条件(型の種別)はツール側で絞る | ← SchemaCatalogのfunctions()等・TableCatalog |
| ViewpointCatalog | 観点の一覧・idでの解決と、テーブルが所属する観点の逆引き(containing) | ← SchemaCatalog.viewpoints() |
| FunctionTableUsages | 関数・プロシージャが利用しているテーブルの参考情報を、スナップショットの関数(FunctionEntry)から引く窓口。キー(DB名・スキーマ名・名前)と引数で識別し、定義本体のハッシュが異なる古い参考情報は返さない(7.10節)。freshnessOfは、参考情報が無い/古い/有効を区別して返す | ← SchemaCatalog.functionTableUsages()・FunctionTools・ImpactAnalyzer |
| RelationGraph | 関連のグラフ。outgoing・incomingの索引、テーブルごとの関連の数(counts)、向きを問わず隣り合うテーブル(adjacentTables)、幅優先のrelatedTables、指定したテーブル同士の関連(among)、全最短経路のjoinPaths。公開クラスで、ツールはSchemaCatalog.relations()から直接呼ぶ | ← 関連を扱うツール・TableCatalog・ClusterDetector・JunctionTables |
| JunctionTables | 中間テーブルの判定結果と、中間テーブルを介した多対多の関係の窓口。組み立て時に全テーブルを1回だけ判定し、find・roleOf・manyToManyで引く。スナップショットに無い親は多対多の相手にしない(7.11節) | ← SchemaCatalog.junctions()・TableFilter・TableTools・TableOutputBuilder/→ JunctionDetector・RelationGraph.outgoing |
| JunctionDetector | 1テーブルが中間テーブルか判定する規則だけを持つ。主キーに収まる関連を候補にし(同じ列の組は外部キーを優先して1本、1対1は除く)、主キーを過不足なく覆い、外すと覆えなくなる組み合わせのうち最も関連の多いものを総当たりで探す。候補の上限MAX_CANDIDATES(12) | ← JunctionTables(パッケージプライベート) |
| JunctionTable ManyToMany ErRole | 中間テーブルと構成する関連(parents()でつなぐテーブル)/多対多の相手と中間テーブルの組/ER上の区分のenum(junction。入力スキーマのenumの元) | ← JunctionTables・TableOutputBuilder・TableTools |
| SimilarTableFinder | 構造(カラム名)の似たテーブルの探索。組み立て時に比べる相手(区分がtable)のカラム名を正規化し、DBごとのカラム名の数を求めておく。重みln((N+1)/数)の重み付きJaccard係数で比べ、類似度の高い順に並べる(7.12節) | ← TableCatalog.findSimilar(パッケージプライベート) |
| SimilarTableQuery | 探す条件(基準のカラム・基準のテーブル・minShared・minSimilarity)。ofTable・ofColumnsで組み立てる。正規化した名前が重なるカラムは先のものだけを残す | ← SimilarTableTools/→ SimilarTableFinder |
| SimilarTables SimilarTable | 探した結果(総数と、件数の上限までのテーブル)と、1テーブル分(類似度・種別・共通のカラム数・片方にだけあるカラム・型の違いTypeMismatch・relatedToBase)。型の違いの程度はTypeDifference(type・size) | ← SimilarTableFinder/→ SimilarTableTools |
| SimilarityKind | 基準と相手のカラム名の集合の関係(identical・superset・subset・overlap)。閾値を使わず、片方にだけあるカラムの有無で決める | ← SimilarTableFinder |
| ImpactAnalyzer | テーブルの影響範囲を、各窓口の情報から集める規則。参照元(直接と間接の分け方)・ビュー(ビューのビュー)・関数の逆引き(完全一致・確定と候補・オーバーロードの合算・ビュー経由)・参考情報の範囲と、確かめきれない関数(unverified)・シーケンスと型。DB名は完全一致で比べる。package-private(7.13節) | ← SchemaCatalog.impactOf/→ TableCatalog・RelationGraph・FunctionTableUsages・SqlNames |
| TableImpact FunctionUse FunctionCoverage | 影響範囲の結果。TableImpactは参照元(IndirectReference)・ビュー(ViewReference)・トリガー・関数(FunctionImpact)・シーケンス・型を持つ。FunctionUseは関数名と経路(自テーブルかビューか)ごとの1行、FunctionCoverageは参考情報の状態の数、UnverifiedFunctionは確かめきれないオーバーロード1つ(理由はReason) | ← ImpactAnalyzer・ImpactTools |
| ClusterDetector | 関連のまとまりの推測。連結成分に分け、上限を超えるまとまりは被参照の最も多いテーブル(HUB_MIN_INCOMING以上)をハブとして除いて分け直す。ハブとの間にしか関連の無いテーブルはハブごとに寄せる | ← SchemaCatalog.tableClusters(パッケージプライベート)/→ RelationGraph |
| TableClusters TableCluster | まとまりを求めた結果(まとまり・ハブ・関連の無いテーブルの数、ハブの下限HUB_MIN_INCOMING)と、1つのまとまり(先頭が代表のテーブル) | ← ClusterDetector/→ ClusterTools |
| DiagramScope | ER図に描くテーブル・関連・スナップショットに無いテーブル。nodes()で箱として描くテーブル(関連の参照先で無いものを含む)を返す。of(RelatedTables)で関連をたどった結果から作る(7.9節) | ← RelationGraph.among・DiagramTools/→ MermaidErDiagram |
| SearchQuery | テーブル検索の検索語。空白区切りのAND、項目ごとの点数(7.2節) | ← TableTools・TableCatalog |
| ColumnQuery | カラムの逆引きの条件と強さ(7.3節) | ← TableTools・TableCatalog |
| SqlNames | nextval・型名・トリガーの関数名からObjectKeyを取り出し、大文字小文字を区別せず比べる(7.5節) | ← TableCatalog |
| TextValues | null→空文字、検索用の正規化(NFKC+小文字) | ← Entry・Query類 |
| SchemaObject | 名前で指定できるオブジェクトの共通インタフェース(key()) | ← TableEntry・FunctionOverloads・SequenceEntry・TypeEntry |
| ObjectKey | DB名・スキーマ名・名前の組。qualifiedName()でスキーマ.名前。一覧・候補を並べる順ORDERも持つ | どこでも |
| ObjectReference | AIが渡した名前の指定。スキーマ.名前の分割 | ← ObjectResolver・TableTools |
| Lookup | 名前の解決結果の封印インタフェース(Found・Ambiguous・NotFound) | ← ObjectCatalog・TableCatalog/→ ObjectResolver |
| SearchScope | DB・スキーマでの絞り込み(空文字=絞らない、大文字小文字を区別しない)。観点用にDB名だけで比べるmatchesDatabaseも持つ | ← ToolArguments.scope() |
| TableFilter | テーブルの一覧・検索の絞り込み(DB・スキーマ・区分・観点・中間テーブル)。1回組み立ててmatchesで問い合わせる。重みをDB全体で数えるためのmatchesDatabaseも持つ | ← TableTools・SimilarTableTools/→ TableCatalog.list・search・findSimilar |
| TableType | テーブルの区分(table・view・materialized_view)。未知の値は空として扱う | ← TableFilter・SchemaSummary.summarize・TableToolsの入力スキーマ |
| NameFilter | 関数・シーケンス・型の一覧を名前の部分一致で絞る | ← 関数・シーケンス・型のツール |
| MatchMode | EXACT・PARTIAL(find_columnsのmatch) | ← ColumnQuery |
| DatabaseEntry | database.json(DB名・DBMS種別・メジャーバージョン) | ← SnapshotDirectoryReader |
| TableEntry | tables.jsonlの1行。型付きの項目(ビューが参照するテーブルを含む)と元のjson | どこでも |
| ColumnEntry | カラム(名前・論理名・型・PK・NOT NULL・デフォルト値・備考) | ← TableEntry |
| RelationEntry | 外部キー・論理リレーション(自テーブル→参照先)。参照先スキーマが空なら自スキーマ | ← TableEntry/→ Relation.of |
| TriggerEntry | トリガー(タイミング・イベント・実行単位・関数名) | ← TableEntry |
| FunctionEntry | functions.jsonlの1行(定義本体を含むjson)と、定義本体のハッシュ(definitionHash) | ← FunctionOverloads・FunctionTableUsages |
| FunctionOverloads | 同名の関数の束。名前の解決の単位 | ← ObjectCatalog・TableCatalog.triggersCalling・FunctionTools |
| SequenceEntry | sequences.jsonlの1行(所有カラム) | ← SequenceTools |
| TypeEntry | types.jsonlの1行(種別) | ← TypeTools |
| ViewpointEntry | 観点(id・表示名・説明・所属テーブル)。SchemaObjectではない。所属の判定はcontains | ← InsightsDirectoryReader・ViewpointCatalog・ViewpointResolver・ViewpointTools |
| FunctionTableUsageEntry | 関数1つ分の参考情報(キー・引数・definitionHash・状態・UsedTableの列・動的SQL・incomplete・overloadsMerged)。SchemaObjectではない。UsedTable.matchesが、利用しているテーブルが指定したテーブルを指すか(確定/候補)を判定する | ← InsightsDirectoryReader・FunctionTableUsages・FunctionTools・ImpactAnalyzer |
| Relation | グラフの辺(参照元・参照先・カラム・由来・多重度・名前)。otherSideで反対側、isOneToOneで1対1か | ← RelationGraph/→ 結果のRelationOutput |
| RelationKind | FOREIGN_KEY・LOGICAL_RELATION | ← Relation・ColumnHit |
| Direction | たどる向き(OUTGOING・INCOMING・BOTH) | ← RelationTools・RelationGraph |
| RelationCounts | テーブルの関連の数(incoming・outgoing・impact)と、影響範囲をたどる段数の上限IMPACT_DEPTH | ← RelationGraph/→ TableToolsのListTablesOutput |
| TableOrder | list_tablesの並べ方(NAME・INCOMING・OUTGOING・IMPACT) | ← TableTools/→ TableCatalog.list |
| RelatedTables | 関連をたどった結果(段数付きの関連・現れたテーブル・無いテーブル) | ← RelationGraph |
| JoinPath JoinPaths | 1本の経路と、最短経路の集まり(hasMore付き) | ← RelationGraph |
| SearchResult TableHit | 検索の総数と、一致した1テーブル(点数・matchedIn) | ← SearchQuery・TableCatalog |
| ColumnHit | 逆引きで当たったカラムと参照先(ColumnReference) | ← TableCatalog.findColumns |
| TableColumn TableTrigger | カラム・トリガーと、それを持つテーブルの組 | ← TableCatalogの相互参照・triggers |
| SchemaSummary | スキーマごとのオブジェクト数と、DBのDBMS種別・メジャーバージョン(list_schemasの元)。集計はsummarizeが行う | ← SchemaCatalog.schemas |
| package-info | パッケージの要旨 | — |
| mcp.tool:MCPのツール | ||
| TableDefinitionTools | 12のツールクラスのspecifications()を並べてSDKへ渡す。並び順がクライアントに見える順 | ← McpServerMain/→ 各*Tools |
| ToolSpecifications | readOnlyTool(全ツールの包み・注釈・isErrorへの変換)と、inputSchemaの部品(objectSchema・stringProperty等) | ← 全*Tools |
| ToolArguments | 引数の読み取りと、入力スキーマで表せない検証。文字列は前後の空白を除く、リストは配列を受ける(文字列も許すtableではカンマ区切りも)、選択肢は完全一致で照合する。型・範囲・enum・未知の引数はSDKに任せる | ← 全*Tools・Page・ObjectResolver |
| InvalidToolArgumentException | 入力スキーマで表せない引数の誤り(空・件数・見つからない名前等)。メッセージはAIへの直し方の指示 | → readOnlyToolがisErrorに変換 |
| ObjectResolver | 名前→オブジェクト1つ。Lookupの3通りを文言に変える。複数に当てはまる場合の案内を差し替えられる形もある(find_similar_tablesが使う) | ← 名前を受けるツール/→ 各窓口のlookup |
| ViewpointResolver | 引数viewpoint(観点のid)→観点1つ。見つからなければ宣言済みのidを並べた文言でInvalidToolArgumentException | ← TableTools・DiagramTools/→ viewpoints().find・viewpoints().list |
| ToolResults | 結果のJSON化(NON_EMPTY)、元の1行の読み込み、usedByColumnsの追加 | ← 全*Tools |
| Page | offset・limitの読み取り・スキーマ(上限はmaximum)・切り出し・nextOffset。一覧の件数の既定値・上限の定数 | ← 一覧系のツール |
| TableSection | get_tableのsectionsで選べる9項目。referencedByViewsだけはスナップショットに無い、逆引きで足す項目 | ← TableTools・TableOutputBuilder |
| SchemaTools | list_schemas。DBごとにDBMS種別・メジャーバージョン(不明なら出さない)を添える | → SchemaCatalog.schemas |
| ViewpointTools | list_viewpoints | → viewpoints().list |
| ClusterTools | list_table_clusters。まとまりごとに、テーブル(100件まで)・ハブ・まとまりのテーブルを含む観点を添える | → tableClusters・viewpoints().list・relations().counts |
| TableTools | search_tables・list_tables・get_table・find_columns。引数からTableFilter(erRoleを含む)・TableOrderを組み立てる、一覧・検索の各行にerRoleを添える、get_tableの複数件の名前をまとめて解決する | → 検索・一覧・解決のメソッド、ViewpointResolver・TableOutputBuilder |
| TableOutputBuilder | get_tableの1テーブル分の結果の組み立て。元の1行にsections・columnsの絞り込み、参照しているビュー、所属する観点、ER上の区分(erRole・junctionOf)と多対多の相手(manyToMany。上限MAX_MANY_TO_MANY)を反映する | ← TableTools・TableDefinitionResources/→ tables().viewsReferencing・viewpoints().containing・junctions() |
| SimilarTableTools | find_similar_tables。tableとcolumnsのどちらか一方を受け、基準を解決して範囲(tableなら基準と同じDB)を組み立てる。片方にだけあるカラムは20件までにし、該当なしのときは条件のゆるめ方をmessageで示す | → tables().lookup・tables().findSimilar・ObjectResolver・ViewpointResolver |
| ImpactTools | get_impact。tableを解決してimpactOfを呼び、affected(参照元・ビュー・トリガー・関数)とdependsOn(シーケンス・型)に分けて返す。概要の各リストは50件まで(MAX_LISTED)で、超えた分はsection・offset・limitで読む(Section列挙型)。functions.preview・coverage・unverifiedと固定文のnotesで、参考値であることと読み方を伝える | ← TableDefinitionTools/→ SchemaCatalog.impactOf |
| RelationTools | get_related_tables・find_join_path。kindNameで由来をforeignKey/logicalRelationに | → relations().relatedTables・relations().joinPaths |
| DiagramTools | get_er_diagram。tableとviewpointのどちらか一方を受け、範囲を求めてMermaidErDiagramで書く。描くテーブルの上限MAX_NODES(80) | → relations().relatedTables・relations().among・ViewpointResolver・MermaidErDiagram |
| MermaidErDiagram | DiagramScopeからMermaidのerDiagramを組み立てる。表記はcliのMermaidSupportに揃え、DB由来の名前の識別子・表示名・型を無害化する(7.9節) | ← DiagramTools |
| FunctionTools | list_functions・get_function。定義本体の除去・集約(sharedDefinition)・切り詰め(truncateDefinitionOf)と、オーバーロードごとのtableUsageの組み立て(テーブルはスキーマ.テーブル、操作はCRUDの文字列。7.10節) | → functions().list・functions().lookup・tables().triggersCalling・functionTableUsages().find |
| SequenceTools | list_sequences・get_sequence | → sequences().list・tables().columnsUsing |
| TypeTools | list_types・get_type。種別はPostgreSQLとOracleの7つ | → types().list・tables().columnsUsing |
| TriggerTools | list_triggers(テーブルをまたぐ一覧) | → tables().triggers |
| TableDefinitionResources | 全テーブル(ビューを含む)をリソースとして列挙する。nameは論理名 (スキーマ名.物理名)、descriptionの先頭にも同じ名前(120文字まで)。表示用の文字列から制御文字・書字方向の制御文字を除き1行にする。読まれたらget_tableと同じJSONを組み立てる | ← McpServerMain/→ TableResourceUri・TableOutputBuilder |
| TableResourceUri | リソースのURIdbxray://{DB名}/{スキーマ名}/{テーブル名}。各区間をUTF-8でパーセントエンコード(英数字と-._~以外) | ← TableDefinitionResources |
| package-info | パッケージの要旨 | — |
| テスト(src/test) | ||
| McpServerProcessTest | E2E。配布するjarを子プロセスで起動し、MCPクライアントから呼ぶ。ツールの並び、標準出力の汚れ、多対多・中間テーブル・構造の似たテーブルの結果とerRoleのenumの検証、immediateExecutionの回帰(ツール・リソースの両方)、SDKの入力スキーマの検証(型・範囲・未知の引数の拒否、tableのカンマ区切り)、リソースの一覧・読み出し、起動失敗時の終了コード2 | → shadowJar・サンプルのスナップショット |
| ServerArgumentsTest | 起動引数の解釈と誤り | → ServerArguments |
| ArchitectureTest | ArchUnitで構造の規約を検査する。パッケージの依存の向き(tool → catalog ← snapshot・catalog ← insight)、catalogがJDK以外に依存しないこと、標準出力(System.out等)へ書き込まないこと、cliのコードに依存しないこと | → main配下の全クラス |
| TableCatalogTest | テーブルの検索・逆引き・解決・相互参照(参照しているビューを含む) | → TableCatalog |
| ObjectCatalogTest | 関数・シーケンス・型の一覧と解決(同じ組み立てでスキーマごとの数・トリガーの一覧も) | → ObjectCatalog |
| RelationGraphTest | 関連のたどり・経路・関連の数・観点のER図の範囲 | → RelationGraph |
| ViewpointCatalogTest | 観点の一覧・解決・逆引きと、観点による絞り込み | → ViewpointCatalog |
| SchemaCatalogTest | スキーマごとの数・テーブルの一覧・関連のまとまり | → SchemaCatalog |
| JunctionDetectorTest JunctionTablesTest JunctionFixtures | 中間テーブルの判定(複合主キー・論理リレーション・同じ列の関連の統合・列を共有する関連・余分な関連・組み合わせの選び方・1対1の除外・候補数の上限)/多対多の相手(自己多対多・3つ以上の親・スナップショットに無い親)/テスト用の関連・テーブルの補助 | → JunctionDetector・JunctionTables |
| SimilarTableFinderTest ColumnTypeTest | 構造の似たテーブル(種別・汎用のカラムの重みとDB全体での数え方・基準自身を数えないこと・類似度の上限・下限・relatedToBase・ビューの除外・範囲・正規化・型の違い・並びと件数)/型の表記の分解 | → 各クラス |
| ImpactAnalyzerTest FunctionTableUsageEntryTest | 影響範囲の集約(直接と間接の分け方・自己参照・impactとの整合、ビューのビューと循環、関数の逆引きの完全一致・確定と候補・ビュー経由・オーバーロードの合算・別DBの除外、参考情報の状態と範囲の数え方、確かめきれない関数の理由と並び、確定と候補が混ざるオーバーロード、シーケンスと型)/UsedTable.matchesの確定・候補・大文字小文字 | → ImpactAnalyzer・FunctionTableUsageEntry |
| SearchQueryTest SqlNamesTest | 点数・AND・正規化/名前の取り出し | → 各クラス |
| TestTables TestCatalogs Lookups | テスト用のテーブルのビルダー/テーブルだけからSchemaCatalogを組み立てる補助/Lookupから値を取り出す補助 | ← catalogのテスト |
| SnapshotDirectoryReaderTest | 読み込みと起動時の誤り(ファイル名・行番号)、関数の定義本体のハッシュ | → SnapshotDirectoryReader |
| SampleSnapshotContractTest | cliのベースラインdocs/sample/postgres/output/snapshotを読む契約テスト。cliの形式変更への追従漏れを検知。サンプルで中間テーブルと判定されるのがproject_assignmentだけであること、employee_historyがemployeeのsuperset(型の違いはsize)として見つかることも確かめる | → ベースライン |
| InsightsDirectoryReaderTest | 観点・関数の利用テーブルの読み込みと兄弟ディレクトリの解決(DB名・スキーマ名はファイルの中の値) | → InsightsDirectoryReader |
| SampleInsightsContractTest | ベースラインの観点と、スナップショットのテーブルのキーが一致すること | → ベースライン |
| FunctionTableUsagesTest | 参考情報と関数の対応付け(キー・引数・定義本体のハッシュ。ハッシュが異なる古い参考情報・ハッシュを持たない参考情報は返さない)と、参考情報の状態(有効/無い/古い) | → FunctionTableUsages |
| SampleFunctionTableUsagesContractTest | cliの--previewの差分のベースライン(docs/sample/postgres/output-preview)の参考情報が読めて、スナップショットの関数(既定のベースライン)と対応付くことの契約テスト。cliのハッシュの求め方・形式の変更への追従漏れを検知。サンプルのemployeeの影響範囲(自己参照・ビューのビュー・関数5つ)も確かめる | → ベースライン |
| SampleOracleImpactContractTest | Oracleのサンプル(docs/sample/oracle/outputとoutput-preview)で、EMPLOYEEの影響範囲を求められること。トリガーが関数名を持たず、triggersWithoutFunctionに数が出て、トリガーが触るテーブルは含まれないことを確かめる | → ベースライン |
| TableDefinitionToolsTest | 全ツールの結果のJSON・エラー。callHandler().apply(null, …)で直接呼ぶため、SDKの入力検証は通らない | → TableDefinitionTools |
| ErRoleToolsTest SimilarTableToolsTest | erRole・junctionOf・manyToMany(上限とmanyToManyTotalを含む)とerRoleでの絞り込み/find_similar_tablesの結果・引数の誤り・message | → TableDefinitionTools |
| ImpactToolsTest | get_impactの結果のJSON(affected・dependsOn・functions.preview)、上限と総数・more、section・offset・limitのページング、sectionなしのoffsetのエラー、値の無い項目の省略、notesの出し分け、存在しないテーブルのエラー | → TableDefinitionTools |
| ToolArgumentsTest | 引数の読み取り(空白・型・カンマ区切り・列挙) | → ToolArguments |
| MermaidErDiagramTest | DB由来の名前(記号・二重引用符・改行)の無害化と、識別子が重なったときの連番 | → MermaidErDiagram |
| SampleErDiagramContractTest | 観点のER図が、cliの観点ページ(ベースライン)のER図と同じ記述になることの契約テスト。表記のずれを検知 | → ベースライン |
| TableDefinitionResourcesTest TableResourceUriTest | リソースの名前・説明(先頭の表示名・切り詰め・制御文字の除去)・中身/URIのパーセントエンコード | → 各クラス |
| 一致するファイルはありません。 | ||
catalogには単体テストのカバレッジの最低基準(line 95%・branch 85%)があり、./gradlew buildで検査されます。E2Eは子プロセスで動くため計測の対象外です。
10変更するときの道しるべ
ツールを1つ足すとき
- 関心が合う
*ToolsクラスにreadOnlyTool(…)を足す。無ければクラスを作り、TableDefinitionTools.specifications()に並べる(並び順はクライアントに見える順)。 - 検索・たどりのルールは
catalogの該当する窓口(TableCatalog・ObjectCatalog・ViewpointCatalog・RelationGraph)に置き、その窓口のテスト(TableCatalogTest等)で確かめる(カバレッジ基準の対象)。オブジェクトの種類を足すときはObjectCatalogを使い、SchemaCatalogには窓口を返すメソッドだけを足す。catalogにSDKやJacksonの型を持ち込まない(ArchitectureTestが検出する)。 - 型・範囲(
minimum/maximum)・enumはinputSchemaに書く。SDKがそれを検証するので、ToolArgumentsでは同じ検査を重ねず、スキーマで表せない検証だけを書く。検証の挙動はMcpServerProcessTestで確かめる(ハンドラを直接呼ぶテストではSDKの検証を通らない)。 - 結果は
*Outputのrecordにする。コンポーネント名がそのまま結果のキーになる。 McpServerMain.INSTRUCTIONSに使い分けを書き足す。McpServerProcessTestのツール一覧、TableDefinitionToolsTest、docs/architecture/mcp-server.mdとdocs/usage/mcp-server.mdのツールの表を更新する。
スナップショットの形式が変わるとき
- 項目の追加は互換です。未知の項目は読み込みで無視され、
get_*は元の1行を返すので、何もしなくてもAIに届きます。検索・逆引きに使うときだけ*Lineと*Entryに足します(例:referencedByViewsの逆引きのため、ビューのreferencedTablesをViewReferenceLine・TableEntryに足した)。古いcliのスナップショットには無い項目なので、nullや空を許して読みます。 - 互換性の無い変更は、cliの
DatabaseSnapshot.FORMAT_VERSIONを上げ、SnapshotDirectoryReader.SUPPORTED_FORMAT_VERSIONを追従させます。観点ならViewpointsInsight.FORMAT_VERSIONとInsightsDirectoryReader.SUPPORTED_FORMAT_VERSIONです。 - cliのベースラインを出し直すと、
SampleSnapshotContractTest・SampleInsightsContractTestが追従漏れを検知します(./gradlew :mcp-server:test)。
関数の利用テーブル(プレビュー)の参考情報が変わるとき
- 項目の追加は互換です。
InsightsDirectoryReaderは未知の項目を無視します。get_functionの結果に出したいときだけ、FunctionTableUsageLine・FunctionTableUsageEntryに足し、FunctionTools.tableUsageで組み立てます。互換性の無い変更は、cliのFunctionTableUsagesInsight.FORMAT_VERSIONを上げ、InsightsDirectoryReader.SUPPORTED_FORMAT_VERSION(観点と共通)を追従させます。 - 定義本体のハッシュの求め方(UTF-8のSHA-256の小文字の16進)は、cliの
FunctionTableUsagesInsightとmcp-serverのSnapshotDirectoryReaderに、同じものが1つずつあります。片方だけを変えると、すべての関数でtableUsageが返らなくなります。 - cliの
--previewの差分のベースライン(docs/sample/postgres/output-preview)を出し直すと、SampleFunctionTableUsagesContractTestが追従漏れを検知します(./gradlew :mcp-server:test)。ベースラインのスナップショットは既定のoutput/snapshotから読みます。 - プレビューの機能なので、互換性の対象外です。
tableUsageの説明(ツールの説明)に、参考値であることと、候補のスキーマに決めつけないことを書き続けます。
中間テーブルの判定・構造の類似の規則を変えるとき
- どちらも判定・計算の規則は
catalogの1クラス(JunctionDetector・SimilarTableFinder)にあり、結果を表すrecord(JunctionTable・SimilarTable)は規則を持ちません。規則を変えたらJunctionDetectorTest・SimilarTableFinderTestに場合を足します(カバレッジ基準の対象)。 - サンプルでの判定・結果は
SampleSnapshotContractTestが確かめています。サンプルのDDLにテーブルを足すと、中間テーブルの判定・多対多の相手・類似の結果が変わることがあるため、このテストとガイドの例を見直します(employee_historyを足したときは、検索・関連の数・まとまり・ER図の例が変わった)。 - 判定の条件・結果の項目はツールの説明(
TableTools・SimilarTableTools)とdocs/usage/mcp-server.mdにも書いてあるため、合わせて直します。どちらもスナップショットに入れない導出した値なので、cliの形式は変わりません。 - 中間テーブル以外のER上の区分を足すときは、
ErRoleに値を足します。入力スキーマのenumはここから作られ、TableTools.tableFilterのswitchが網羅していないとコンパイルが通りません。
影響範囲の集め方を変えるとき
- 集め方の規則は
catalogのImpactAnalyzerにあり、結果を表すrecord(TableImpact・FunctionUse・FunctionCoverage)は規則を持ちません。ツール側(ImpactTools)は、record・JSONの形と上限、固定文のnotesだけを持ちます。 affected.functionsは、cliの--previewの参考情報に依存するため、互換性の約束の対象外です。ツールの説明・結果のfunctions.preview・notes・docs/usage/mcp-server.mdの4か所に「参考値であり、載っていなくても使われていないとは限らない」ことを書き続けます。- サンプルでの結果は
SampleFunctionTableUsagesContractTest(PostgreSQL)とSampleOracleImpactContractTest(Oracle)が確かめています。サンプルのDDLに関数・トリガー・ビューを足すと、employeeの影響範囲が変わることがあるため、ベースラインを出し直したら、これらのテストを見直します(./gradlew :mcp-server:test)。 direct・indirectの分け方を変えるときは、list_tablesのimpact(RelationGraph)との整合を保ちます。ImpactAnalyzerTestと契約テストが、自己参照を含めて確かめています。- 関数の参考情報の状態を足すときは、
FunctionTableUsages.FreshnessとFunctionCoverageの項目、ImpactToolsのCoverage・notesを合わせて直します。定義本体を読めば確かめられる状態なら、UnverifiedFunction.ReasonとUnverifiedRefにも足します。 - リストを足す・分けるときは、
ImpactTools.Section(sectionの値とitems)、概要の上限(Affected・Functionsのhead)、moreの順を揃えます。sectionの値は、概要でそのリストを持つキーの名前と同じにします。sectionの値は、ツールの入力スキーマのenumに使われるため、変えると呼び出し側が壊れます。
ER図の表記・リソースを変えるとき
- ER図の表記(多重度の端点・線種・箱のカラム・エスケープ)は、cliの
MermaidSupportとmcp-serverのMermaidErDiagramの両方にあります。片方だけを変えるとSampleErDiagramContractTestが失敗します。cliを変えたらベースラインを出し直してから、こちらを追従させます。 - リソースの中身は
TableOutputBuilderをget_tableと共有しているため、get_tableの結果を変えるとリソースも変わります。リソースだけを変えたいときはTableDefinitionResourcesで分けます。 - リソースの
name・description・URIの形は、MCPクライアントの絞り込み・表示の挙動に合わせて決めています。変えるときはdocs/architecture/mcp-server.mdの「リソース」の確認結果を読み、実機で確かめます。
やってはいけないこと
System.outへの出力や、標準出力へ出すログ設定の追加。プロトコルが壊れます(System.out等への書き込みはArchitectureTestが検出しますが、ログの設定は検出しません)。immediateExecution(true)の削除。SDKの修正版が出るまでは、並行呼び出しで応答が止まります。- cliのコードへの依存。両者の接点は、スナップショットと参考情報(
insights/)の形式だけです。 - 関数の参考情報を、定義本体のハッシュで照らさずに返す。片方だけが出力し直された後の、古い抽出結果を最新のものとして答えてしまいます。
11用語集
- スナップショット
- cliがDBから取得した定義を、機械可読なJSON Linesで出力したもの。
{output.path}/snapshot/{DB名}/に置かれる。mcp-serverの唯一の主な入力。 - 参考情報(insights)
- スナップショットとは別に
{output.path}/insights/へ出力される補助情報。観点と、関数が利用しているテーブル(cliを--previewで実行した場合だけ)。 - 観点(viewpoint)
- 業務ドメイン別にテーブルをまとめる切り口(例:人事管理)。1つのテーブルが複数の観点に所属できる。DB単位でスキーマを持たない。
- 利用しているテーブル(tableUsage)
- 関数・プロシージャの定義本体から、cliが機械的に抽出した、利用している出力対象のテーブルと操作(C・R・U・D)。
get_functionがオーバーロードごとに返す参考値で、呼び出している関数・動的SQLの中の参照は含まない。 - 定義本体のハッシュ(definitionHash)
- 定義本体のSHA-256(小文字の16進)。参考情報が、今のスナップショットと同じ定義本体から抽出したものかを照らすために使う。
- プレビューの機能
- cliの
--previewを付けたときだけ動く、精度・出力の形を固めている途中の機能。出力は互換性の対象外。 - スキーマが決まらない名前
- スキーマ修飾が無く、定義からは決まらないテーブル名。
schemaResolution(search_path・current_user・synonym)と、候補のスキーマ(schemaCandidates)で返す。 - 中間テーブル(junction)・ER上の区分(erRole)
- 多対多をつなぐテーブル。主キーの列を、外すと覆えなくなる2本以上の外部キー・論理リレーションで過不足なく覆えるテーブルを、mcp-serverが機械的に判定する(推測)。
erRole: junctionで表し、通常のテーブルは区分を持たない。 - 多対多(manyToMany)
- 中間テーブルを介してつながる2つのテーブルの関係。
get_tableが相手のテーブルと中間テーブル(via)の組で返す。 - 構造の似たテーブル・類似度
- カラム名の集合が似ているテーブル(
find_similar_tables)。類似度は、汎用のカラムほど重みを下げた重み付きJaccard係数(%)で、100はカラムが同じ場合だけ。意味の類似ではない。 - 関連のまとまり(cluster)・ハブ
- 関連のつながりだけから推測したテーブルの組(
list_table_clusters)。人が宣言する観点とは別物。ハブは、大きなまとまりを分けるために除いた、多くのテーブルから参照される共通のテーブル。 - 外部キー(foreignKey)
- DBに実在する制約。関連の由来の1つ。
- 論理リレーション(logicalRelation)
- DBに制約は無いが、サイドカーYAMLで宣言された関連。グラフでは外部キーと同じ辺として扱う。
- 参照するテーブル(referencedTables)
- ビュー・マテリアライズドビューが参照するテーブル(ビューを含む)。cliがDBの依存関係から取得し、ビューの行にだけ入る。逆向きの「参照しているビュー」(
referencedByViews)はmcp-serverが逆引きして求める。 - メジャーバージョン(majorVersion)
- DBMSの版の先頭の数字(例:PostgreSQLの16)。
list_schemasが返し、AIが書けるSQLを判断する手がかりにする。古いcliのスナップショットには無い。 - 多重度(cardinality)
ONE_TO_MANY等。参照先(親)1行に対する参照元(子)の行数を表す。- オーバーロード
- 同じスキーマ・同じ名前で引数が異なる関数。
FunctionOverloadsに束ねる。 ObjectKey・修飾名- DB名・スキーマ名・名前の組。結果では
スキーマ名.名前(qualifiedName)、候補の文言ではDB名:スキーマ名.名前で表す。 - ホスト/クライアント/サーバー
- MCPの3者。AIアプリ、その中の接続担当、道具の提供側。
- トランスポート
- メッセージの運び方。このサーバーはstdio(標準入出力)。
inputSchema- ツールの引数のJSON Schema。AIが引数を組み立てる手がかりであり、SDKの入力検証の基準でもある。
- リソース
- MCPでサーバーが出すデータ。このサーバーでは1テーブルが1リソース(
dbxray://…)。AIが呼ぶツールと違い、利用者が@等で選んで会話に添付する。 - Mermaid(
erDiagram) - テキストで図を書く記法。cliの定義書のER図と
get_er_diagramが同じ表記で出力する。 isError- ツールとしての失敗。AIが読んで直せる。JSON-RPCの
errorとは別物。 instructions- 初期化の応答でサーバーがAIへ渡す使い方の説明。
immediateExecution- SDKのBuilderの設定。
trueでツールを受信スレッド上で直列に実行する。