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者います。

やり取りするメッセージはすべてJSON-RPC 2.0です。idを持つ「要求」には必ず同じidの「応答」(resultかerror)を返し、idを持たない「通知」には応答しません。

stdioトランスポート:標準出力はプロトコル専用

MCPにはメッセージの運び方(トランスポート)が2種類あり、このサーバーはstdioを使います。クライアントがサーバーを子プロセスとして起動し、標準入力に要求を書き込み、標準出力から応答を読みます。1行が1メッセージです。

MCPクライアント Claude Code など MCPサーバー(子プロセス) java -jar dbxray-mcp.jar --snapshot=…/snapshot McpServerMain.main() stdin:要求・通知(1行に1件) stdout:応答だけ(プロトコル専用) stderr:[info]・[error]・SDKのWARN(ログ扱い) snapshot/ ・ insights/ 起動時に1回だけ全件読む
図1 stdioの3本の流れ。標準出力にプロトコル以外の文字が1つでも混ざると、クライアントはJSONとして読めず接続が壊れます。そのためSystem.outへの出力は禁止で、利用者向けの表示とSDKのログ(slf4j-simple)はすべて標準エラーへ出します。終了はクライアントが標準入力を閉じたときです。

1回の接続の流れ

接続は「初期化 → 発見 → 実行 → 終了」の順に進みます。サーバーが能動的に何かを送ることはなく、すべてクライアントからの要求への応答です。

MCPクライアント MCPサーバー 初期化 発見 実行(繰り返し) 終了 initialize(protocolVersion・clientInfo) result:serverInfo・capabilities・instructions notifications/initialized(idが無い通知。応答しない) tools/list(resources/list) 19個のツール定義(リソースならテーブルの数だけのURI・name) tools/call {name, arguments}(resources/read {uri}) CallToolResult {content: [text], isError}(ReadResourceResult) stdinを閉じる(EOF)→ サーバーのプロセスが終わる
図2 接続のライフサイクル。実線が要求、破線が応答・通知です。括弧内はリソースの場合で、流れは同じです。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 Schema
  • annotations: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"}}}

MCPの概念と、このリポジトリでの実体

MCPの概念このリポジトリでの実体
トランスポート(stdio)SDKのStdioServerTransportProvider。McpServerMain.startで生成
serverInfo・instructionsMcpServerMainの定数SERVER_NAME・INSTRUCTIONSとversion()
capabilities.tools・resourcesServerCapabilities.builder().tools(false).resources(false, false)(falseはlistChanged、リソースはsubscribe・listChanged)
ツールの定義ToolSpecifications.readOnlyTool(name, title, description, inputSchema, handler)。全ツールがこれを通る
inputSchemaToolSpecifications.objectSchemaとstringProperty等の部品で、Map<String,Object>として組み立てる
annotations全ツール共通でreadOnlyHint=true・destructiveHint=false・idempotentHint=true・openWorldHint=false
tools/callの処理各*ToolsクラスのprivateメソッドCallToolResult xxx(ToolArguments)
結果のcontentToolResults.json(出力record)、またはスナップショットの1行そのもの
isError: trueInvalidToolArgumentExceptionを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-jackson2Jackson 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の中で起きること

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と同じ考え方で、中心のロジックを単体テストしやすくするためです。

McpServerMain main():検証・読み込み・起動 生成して配線 snapshot/{DB}/ database.json {schema}/*.jsonl cliが出力 insights/{DB}/ viewpoints.json {schema}/*.json 関数の利用テーブル mcp.snapshot SnapshotDirectory Reader mcp.insight InsightsDirectory Reader mcp.catalog SchemaCatalog *Catalog・RelationGraph SearchQuery・Lookup 各種Entry(record) JunctionTables ほか SDK・JSONに依存しない mcp.tool TableDefinitionTools 11の *Tools ToolArguments ObjectResolver ほか …Resources リソース(全テーブル) MCP SDK mcp-core 2.0.1 stdio 読む 読む of(…) 観点 +関数 照会 登録
図3 パッケージと依存の向き。矢印は「相手を知っている(importしている)」向きです。catalogには外へ向かう矢印がありません。McpServerMainは2つの読み込み部品・withViewpoints・withFunctionTableUsages・TableDefinitionTools・TableDefinitionResources・SDKを順に呼んで配線します。snapshotとinsightは互いを知りません。
パッケージひとことで
mcp(直下)入口。起動引数の検証、読み込みの呼び出し、SDKの起動
mcp.snapshotsnapshot/のJSON Linesを読み、SchemaCatalogを組み立てる
mcp.insightinsights/の観点(viewpoints.json)と、関数が利用しているテーブル({スキーマ}/functionTableUsages.json。cliの--preview)を読む
mcp.catalogメモリ上の全オブジェクトと、検索・名前の解決・関連のたどり・相互参照・関連のまとまり・ER図の範囲・中間テーブルの判定・構造の似たテーブルの探索
mcp.toolMCPのツールとリソースの定義。引数を読み、catalogに聞き、結果をJSON(ER図はMermaid)にする

おすすめの読む順

上から順に読むと、外側(起動とMCP)から内側(ルール)へ進めます。

  1. McpServerMain:何をどの順で組み立てるか。INSTRUCTIONSでツールの全体像もつかめます。
  2. TableDefinitionTools:ツールクラス12の並び=クライアントに見える順。
  3. ToolSpecifications.readOnlyTool:全ツール共通の包み。引数の誤りがisErrorになる場所です。
  4. SchemaTools:小さなツールクラスで「型」をつかみ、続けて一番大きいTableTools。get_tableの結果の加工はTableOutputBuilderに分かれています。
  5. ToolArguments・ObjectResolver・Page:ツールクラスが共通で使う部品。
  6. SchemaCatalog:組み立てと、関心ごとの窓口(tables()・functions()・sequences()・types()・viewpoints()・functionTableUsages()・relations()・junctions())の一覧です。続けてTableCatalog・ObjectCatalog・ViewpointCatalogの公開メソッドを見ると、ツールから見たcatalogの全機能がつかめます。
  7. TableFilter・SearchQuery・RelationGraph:テーブルの絞り込み、点数づけ、幅優先探索。7章の図と合わせて読むと速いです。
  8. SnapshotDirectoryReader:ファイルからEntryができるまで。6章参照。
  9. DiagramTools・MermaidErDiagram、TableDefinitionResources:ツールの型から外れる2つ。結果にMermaidの記述を入れるER図(7.9節)と、AIではなく利用者が選ぶリソース(1章)です。
  10. JunctionDetector・SimilarTableFinder・ImpactAnalyzer:スナップショットに無い値を導く・既存の窓口をまとめる、後から足された3つ。中間テーブルの判定(7.11節)、構造の似たテーブルの探索(7.12節)、テーブルの影響範囲の集約(7.13節)です。

4起動の流れ

McpServerMain.mainは次の順に進みます。利用者が直せる誤りはすべてUserCorrectableExceptionで表し、mainの1箇所で捕まえて終了コード2で終わります(cliと同じ方針。ただしcliのクラスとは別物で、mcpパッケージに自前で持っています)。

  1. 標準エラーをUTF-8に固定:System.setErr(new PrintStream(…, UTF_8))。実行環境のロケールで日本語が化けないようにするためです。
  2. 起動引数の解釈:ServerArguments.parse。受け付けるのは--snapshot=<ディレクトリ>1つだけで、未知の引数・重複・空・未指定はすべて誤りです。
  3. 参考情報の読み込み:InsightsDirectoryReaderのreadViewpoints(観点)とreadFunctionTableUsages(関数の利用テーブル。cliを--previewで実行した場合だけある。7.10節)。--snapshotの親の兄弟のinsights/を自分で求めて読みます。ディレクトリ・ファイルが無ければ0件で、誤りにはしません。
  4. スナップショットの読み込み:SnapshotDirectoryReader.readがSchemaCatalogを返します。ディレクトリが無い、database.jsonが1つも無い、新しすぎるformatVersion、JSONとして読めない行、schema・nameの無い行は誤りです(メッセージにファイル名と行番号が入ります)。
  5. 参考情報の合成:catalog.withViewpoints(viewpoints).withFunctionTableUsages(functionTableUsages)。SchemaCatalogは不変なので、観点・関数の利用テーブルを加えた新しいインスタンスを作ります。
  6. 読み込み件数の表示:標準エラーへ[info]:15テーブルを読み込みました。 [snapshot=…]。
  7. SDKの起動:start(catalog)。2章のBuilderでツールとリソース(全テーブル分)を登録して組み立て、build()した時点で標準入力の待ち受けが始まり、mainは戻ります。

I/Oの失敗(UncheckedIOException)は利用者が直せる誤りではないため捕まえません。スタックトレースが標準エラーに出て、終了コードは1になります。

51回のツール呼び出しの流れ

get_tableを例に、標準入力の1行が標準出力の1行になるまでをたどります。灰色の枠がSDK、色の付いた枠がこのリポジトリのコードです。右側は途中で抜ける経路です。

SDK | StdioServerTransportProvider(stdio-inbound) 標準入力を1行読み、JSON-RPCのメッセージにする SDK | McpServerSession → tools/call method名で振り分け、nameで登録済みのツールを探す SDK | ToolInputValidator argumentsをinputSchema(JSON Schema)で検証 ToolSpecifications.readOnlyTool のラムダ new ToolArguments(…)で包み、ハンドラを呼ぶ TableTools.getTable → ObjectResolver.resolve スキーマで表せない検証をし、名前を1つに解決する TableCatalog.lookup → TableOutputBuilder 元の1行をObjectNodeにし、sections・columns・観点を反映 SDK | CallToolResult → stdio-outbound id付きの応答を1行にして標準出力へ書く JSON-RPCのerror -32602 未知のツール名(Unknown tool) isError: true(英語の文言) 型・範囲・enum・未知の引数の違反 isError: true(日本語の文言) InvalidToolArgumentException JSON-RPCのerror -32603 想定外の例外(不具合・I/O)
図4 tools/call の通り道と4つの抜け道。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: 1SDK additionalProperties:falseisError … property 'foo' is not defined in the schema …
get_related_tablesにdepth: 4SDK maximum: 3isError … /depth: must have a maximum value of 3
list_tablesにerRole: "subtype"SDK enumisError … /erRole: does not have a value in the enumeration ["junction"]
table: "employe"自前 ObjectResolverisError テーブルemployeが見つかりません。名前の似たテーブル: …
同名テーブルが複数スキーマにある自前 ObjectResolverisError テーブルxが複数あります。schema(DBが異なる場合はdatabase)を指定してください。候補: …
tableを2件+columns自前 TableTools.getTableisError 引数columnsは、tableを1件指定した場合だけ使えます。
columnsに存在しないカラム自前 TableOutputBuilderisError テーブルsample.employeeにカラムxがありません。カラム: …
viewpointに未知のid自前 ViewpointResolverisError 観点xが見つかりません。観点: personnel, project, …
find_similar_tablesにtableとcolumnsの両方(どちらも無い場合も)自前 SimilarTableToolsisError 引数tableとcolumnsは、どちらか一方を指定してください。
find_similar_tablesのtableが複数スキーマにある自前 ObjectResolver(案内を差し替え)isError テーブルxが複数あります。tableをスキーマ名.テーブル名の形(DBが異なる場合はdatabaseも)で指定してください。候補: …
get_er_diagramにtableとviewpointの両方(どちらも無い場合も)自前 DiagramToolsisError 引数tableとviewpointは、どちらか一方を指定してください。
get_er_diagramにviewpointとdepth自前 DiagramToolsisError 引数depthは、tableを指定した場合だけ使えます。
get_er_diagramで描くテーブルが80件超自前 DiagramTools.renderisError 図に描くテーブルがn件となり、上限(80件)を超えます。…
resources/readに一覧に無いURISDK リソースの振り分けerror -32002 Resource not found
不具合・I/Oの失敗SDK セッションerror -32603(例外のメッセージ)

6データの持ち方

1行を2通りに持つ

このサーバーを理解するうえで一番効く事実です。スナップショットの1行は、読み込み時に「検索に使う型付きの項目」と「元の1行の文字列(json)」の両方として保持されます。

tables.jsonl の1行 {"schema":…,"name":…} TableLine Jackson。未知の項目は無視 TableEntry key・logicalName・type columns・foreignKeys … json 元の1行(文字列) 検索・一覧・たどり search_tables find_columns・関連 get_* の結果 ObjectNodeにして 削る・足すだけ toEntry 文字列のまま
図5 型付きの項目と元の1行。TableLine等の読み込み用recordはSnapshotDirectoryReaderの中だけにあり、読む項目を絞っています。get_table・get_sequence・get_type・get_functionとテーブルのリソースは元の1行を返すため、cliがスナップショットに項目を足すと、mcp-serverを変えなくてもAIに届きます。

この設計から、次のことが言えます。

catalogのrecord(Entry)一覧

record元のファイル持つものSchemaObject
DatabaseEntrydatabase.jsonDB名、DBMS種別、メジャーバージョン(古いcliのスナップショットではnull)—
TableEntrytables.jsonlObjectKey、論理名・区分・説明・備考、カラム・外部キー・論理リレーション・トリガー、ビューが参照するテーブル(ObjectKeyのリスト)、json○
ColumnEntry / RelationEntry / TriggerEntryテーブルの中カラム、関連(自テーブル→参照先)、トリガー—
FunctionEntryfunctions.jsonl種別・引数・戻り値・言語、definitionHash(定義本体のSHA-256。参考情報と照らす)、json(定義本体を含む)—
FunctionOverloads(組み立て時に生成)同じキーのFunctionEntryの束。名前の解決の単位○
SequenceEntrysequences.jsonl所有カラム(テーブル名.カラム名)、json○
TypeEntrytypes.jsonl種別(ENUM等)、json○
ViewpointEntryinsights/{DB}/viewpoints.jsonid・表示名・説明・所属テーブル(ObjectKeyのリスト)—(スキーマを持たない)
FunctionTableUsageEntryinsights/{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_* と関連ツールの入口)

  1. ObjectReference.of(database, schema, name):schemaが未指定で名前がsample.employeeの形なら、最初の.で分けてスキーマ修飾とみなします。
  2. ObjectCatalog.lookup<E extends SchemaObject>:DB・スキーマ・名前を大文字小文字を区別せず完全一致で比べます。テーブル・関数・シーケンス・型の4種類がSchemaObjectを実装しているので、この1つの汎用メソッドを共有できます(関数・シーケンス・型はObjectCatalogの窓口から、テーブルは候補の求め方だけを差し替えてTableCatalog.lookupから呼びます)。
  3. 結果は封印インタフェースLookupの3通り:Found(1件)・Ambiguous(複数。キー順)・NotFound(候補付き)。
  4. ObjectResolver.resolveがswitchのパターンマッチで分岐し、Found以外は候補を含む文言でInvalidToolArgumentExceptionを投げます。

見つからないときの候補の求め方は、種類で違います。テーブルは「その名前でsearch_tablesした上位5件」なので、カラム名の一致も候補に入ります(employeにattendanceが出るのはemployee_idカラムがあるため)。関数・シーケンス・型は「名前の部分一致の先頭5件」です。どちらもDB・スキーマの絞り込みを外して探します。

7.2 テーブル検索の点数(SearchQuery)

検索語をNFKC正規化と小文字化(TextValues.normalize)してから空白で区切ります。全角空白もNFKCで半角になるため区切りとして効きます。形態素解析はしません。

一致した項目完全一致前方一致部分一致matchedInの表記
テーブル名1006040name
論理名80—30logicalName
カラム名25—12column:カラム名
カラムの論理名——12column:カラム名
説明——8description
備考——5remarks
カラムの備考——4column:カラム名

サンプルで従業員を検索すると、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)の索引を作ります。辺の向きは常に参照元(外部キーを持つ側)→ 参照先です。

manager_id 論理リレーション 逆向き attendance_note attendance employee project_assignment project department parking_spot audit_log employee_profile employee_history shipment warehouse_zone 別の連結成分(employeeからは届かない) 0 1 2 3 4 3 3 3 3 3
図6 サンプル(docs/sample/postgres)の関連と、find_join_path(attendance_note → project) の結果。矢印は参照元→参照先、破線は論理リレーションです。丸の数字は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つと数えます。

joinsの各要素のfrom・toは関連そのものの参照元・参照先で、たどる向きとは限りません。AIがJOINの条件を書くときはfromColumnsとtoColumnsを同じ位置同士で結びます。グラフの頂点の照合はObjectKeyのequals(大文字小文字を区別する)で、cliが同じ表記で出力する前提に立っています。

7.5 オブジェクト間の相互参照(SqlNames)

スナップショットには「このシーケンスをどのカラムが使うか」のような逆向きの情報がありません。そこで呼び出しのたびに全カラム・全トリガーを走査し、文字列からオブジェクトの名前を取り出して比べます。手順はどれも同じ3段です。

  1. 切り出す:カラムのデフォルト値・型、トリガーの関数名から、名前の部分だけを取り出す(種類ごとに方法が違う。下の表)。
  2. キーにする:qualifyでObjectKeyにする。二重引用符を外し、スキーマ修飾が無ければ参照元テーブルのスキーマを補う。
  3. 比べる: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_enumsample.employee_status_enum
get_functionのcalledByTriggersトリガーの関数名。(があればそれ以降を除くattendanceのトリガー:sample.check_work_minutessample.check_work_minutes

型の切り出しの例。スナップショットのカラムの型(type)は、cliのmapperがPostgreSQLのformat_typeで取得した文字列です。ユーザー定義型を使うカラムには型名がそのまま入り、その型の配列を持つカラムには末尾に[]が付きます。typeOfColumnは末尾の[…](正規表現(\[\d*])+$)を除いてから比べるので、配列のカラムも「その型を使うカラム」としてusedByColumnsに入ります。

カラムの型(type)切り出した名前結果
sample.employee_status_enum
employee.status
sample.employee_status_enumENUM employee_status_enumに一致
sample.positive_numeric
project.budget
sample.positive_numericDOMAIN positive_numericに一致
sample.employee_status_enum[]
仮の例:過去のステータスを配列で持つカラム
[]を除いてsample.employee_status_enumENUMに一致。配列のカラムも数える
employee_status_enum
仮の例:型のスキーマが接続のsearch_pathに入っていて、修飾されずに出力された場合
テーブルのスキーマを補ってsample.employee_status_enumENUMに一致
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 関数のオーバーロードと定義本体

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)

観点が宣言されていない範囲で、業務のまとまりの見当を付けるための推測です。使うのは外部キー・論理リレーションのつながりだけで、行数等の統計やテーブル名の接頭辞は使いません。

  1. 範囲を決める:database・schemaで絞り、excludeViewpointTables: trueならいずれかの観点に所属するテーブルを除きます。範囲外のテーブルとの関連は無いものとみなします(RelationGraph.adjacentTablesの結果を範囲で切る)。
  2. 関連の無いテーブルを外す:範囲内に隣が1つも無いテーブルは、まとまりに含めずunrelatedTablesの数だけを返します。
  3. 連結成分に分ける:向きを問わずつながるテーブルの組を1つのまとまりとします。
  4. 大きすぎれば分け直す:maxClusterSize(既定30)を超えるまとまりは、被参照の最も多いテーブルをハブとして除き、残りを連結成分に分け直します。これを繰り返します。ただし、そのテーブルの被参照がTableClusters.HUB_MIN_INCOMING(3)未満なら、一続きの関連を切るだけになるため分けずに残します。
  5. ハブだけとつながるテーブルを寄せる:ハブを除いて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として返します。観点と同じく、スナップショットとは別のファイルを、読み込みの後に合成する情報です。

スナップショットの関数(functions.jsonl の1行) "arguments":"IN p_project_id integer" "definition":"CREATE OR REPLACE PROCEDURE …" cliが出力(定義本体を含む) SnapshotDirectoryReader(DefinitionHasher) definition をUTF-8でSHA-256にする(小文字の16進) → FunctionEntry.definitionHash(定義本体が無ければ空文字) 参考情報の関数(functionTableUsages.json の1件) "arguments":"IN p_project_id integer" "definitionHash":"161314b9…","status":"analyzed" cliを --preview で実行したときだけ出力 InsightsDirectoryReader ファイルの中の database・schema と関数の name で ObjectKey を作る → FunctionTableUsageEntry(ディレクトリ名は使わない) FunctionTableUsages.find(function) ① キー(DB名・スキーマ名・名前)と引数が同じ参考情報を引く ② definitionHash が FunctionEntry.definitionHash と等しいときだけ返す (定義本体が無い関数は、どちらも空文字で一致する) 一致:get_function の overloads[].tableUsage オーバーロードごとに、利用しているテーブルと操作を返す 不一致・参考情報なし tableUsage は項目ごと付けない
図7 参考情報との対応付け。参考情報はスナップショットとは別のファイルのため、片方だけが出力し直されることがあります(--rm-distを付けずに対象のスキーマを減らした場合等)。そのとき、定義が変わった後の古い抽出結果を最新のものとして返さないよう、キーと引数に加えて、抽出元の定義本体のハッシュが一致するものだけを返します。

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の事実ではなく推測です。

project_assignment 主キー(PK)の列 project_id employee_id project employee 中間テーブル(erRole: junction) 1対多の外部キー2本でPKを過不足なく覆い、 どちらを外しても覆えなくなる employee_profile 主キー(PK)の列 employee_id ONE_TO_ONE employee 中間テーブルではない 1対1の関連は数えないため、残る関連は0本 (1対1の拡張テーブル) shipment 主キー(PK)の列 / PK以外の列 shipment_id warehouse_code zone_code warehouse_zone 中間テーブルではない 複合外部キーの列がPKに含まれず、 PKを覆う関連の候補にならない
図8 サンプルの3つのテーブルの判定。候補になるのは、参照元の列がすべて主キーに含まれる関連だけです。SampleSnapshotContractTestが、サンプルで中間テーブルと判定されるのはproject_assignmentだけであることを確かめています。

JunctionDetector.detectは1テーブルについて次の順に判定します。

  1. 区分がtableのテーブルだけを対象にする(ビュー・マテリアライズドビューは外す)。
  2. 候補を集める:参照元の列がすべて主キーに含まれる関連(RelationGraph.outgoing。外部キー、論理リレーションの順)を集める。同じ列の組の関連は1本とみなし、先に現れる外部キーを残す。そのうえで、多重度がONE_TO_ONE・OPTIONAL_ONE_TO_ONEの関連(Relation.isOneToOne)を外す。列名は大文字小文字を区別せずに比べる。
  3. 候補が多すぎれば諦める:候補がMAX_CANDIDATES(12)を超えると判定しない。組み合わせを総当たりするため、2のべき乗で増えるのを抑える。
  4. 組み合わせを総当たりする:主キーの列を過不足なく覆い、かつどの関連にも他の関連に無い列がある(外すと覆えなくなる)組み合わせのうち、関連の数が最も多いものを採る。同数なら候補の並びで先のもの。
  5. 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と同じ考え方)。

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が条件を組み立てます。

  1. 比べる相手:範囲(TableFilter。DB・スキーマ・観点)内の、区分がtableのテーブル。基準自身は除く。tableを指定した場合は、基準と同じDBに限る。
  2. カラム名を正規化する:NFKC+小文字(TextValues.normalize)。正規化で同じ名前になるカラムは先のものを使う。相手のテーブルの正規化は、TableCatalogの組み立て時にSimilarTableFinderが1回だけ行う。
  3. 重みを付ける:カラム名ごとにln((N+1) / そのカラム名を持つテーブルの数)。Nと数は、範囲のDBの区分がtableの全テーブル(基準自身を除く)で数える。範囲内のどのテーブルも持たないカラム名は、1テーブルだけが持つ場合と同じ重みにする。
  4. 重み付きJaccard係数:共通のカラムの重みの合計 ÷(共通+基準にだけある+相手にだけある カラムの重みの合計)を百分率にする。カラムが同じ場合(identical)だけを100とし、それ以外は丸めても99にする。
  5. 下限で絞り、並べる:共通のカラム数が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_id41.10○○
department_id・parking_spot_id21.79○○
employee_code・employee_name・manager_id・status・salary・profile・hired_date・updated_at12.48○○
history_id・valid_from・valid_to12.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以下にしてください。"}
結果の項目求め方
kindSimilarityKind.of。閾値を使わず、基準にだけある・相手にだけあるカラムの有無だけで決める。identical(どちらも無い)・superset(相手にだけある)・subset(基準にだけある)・overlap(どちらもある)
onlyInBase・onlyInOther元のカラム名を、それぞれのテーブルでの並び順で。MAX_LISTED_COLUMNS(20)件までにし、省いた数をonlyInBaseOmitted・onlyInOtherOmittedで示す
typeMismatchestableを指定した場合だけ(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)に入ります。呼び出しのたびに求めます。

調べるテーブル employee 関連(参照元) relatedTables(INCOMING, 3段) ビュー viewsReferencing を段ごとに繰り返す トリガー table.triggers() 関数(プレビューの参考情報) 全関数 × functionTableUsages.find → UsedTable.matches 依存先 SqlNames(nextval のシーケンス・カラムの型) affected.referencedBy affected.views affected.triggers affected.functions dependsOn
図9 影響範囲の集め方。実線の4つが「自テーブルを変えると影響を受けるもの」(affected)、破線の1つが「自テーブルが使うもの」(dependsOn)です。関数だけは、cliの--previewの参考情報に依存します(破線の枠)。

集め方は5つに分かれ、それぞれ結果の1つのキーになります。

  1. 参照元(affected.referencedBy):RelationGraph.relatedTables(table, 3, INCOMING)は、テーブルではなく関連を段数つきで返します。段1の関連をdirect(自己参照を含む。参照元のテーブル・種別・関連名の順)に、段1に現れないテーブルをindirect(最小の段数と、その段で見つけた関連の参照先via。段数・テーブルの順)に分けます。同じテーブルが段1と段2の両方に現れる場合はdirectだけに載せ、自テーブルはindirectに含めません。そのため、directの参照元(自テーブルを除く)とindirectを重複なく合わせた数は、list_tablesのimpactと一致します。
  2. ビュー(affected.views):TableCatalog.viewsReferencingを、ビューを参照するビューまで段ごとに繰り返します。同じビューは1度しか数えないため、循環していても止まります。経由したビューをviaで示します。
  3. トリガー(affected.triggers):自テーブルのtriggers()です。ビューを調べた場合は、INSTEAD OFトリガーが入ります。
  4. 関数(affected.functions):同じDBの全関数を関数名(FunctionOverloads)ごとに見て、オーバーロードの参考情報(FunctionTableUsages.find)のUsedTableが、自テーブルまたは2.で集めたビューを指すかをUsedTable.matchesで判定します。一致しなかったオーバーロードのうち、参考情報はあるが確かめきれないもの(動的SQL・読み切れなかった・解析不可)は、functions.unverifiedに理由とともに挙げます。
  5. 依存先(dependsOn):自テーブルのカラムについて、デフォルト値のnextvalが使うシーケンス(SqlNames.sequenceOfDefault)と、型に使われているスナップショットにあるユーザー定義型(SqlNames.typeOfColumn)を、使うカラムとともにまとめます。

なぜ affected と dependsOn を分けるのか

参照元・ビュー・トリガー・関数は「自テーブルを変えると影響を受けるもの」、シーケンス・型は「自テーブルが使っているもの」で、向きが逆です。同じ階層に並べると、AIはどれも影響範囲の一覧として読んでしまいます。キーの名前だけで向きが分かるよう、2つに分けています。

関数の逆引きは、次の規則で求めます。

参考情報は、動的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_schemasSchemaTools#listSchemasschemas() → SchemaSummaryListSchemasOutput
list_viewpointsViewpointTools#listViewpointsviewpoints().list(scope)ListViewpointsOutput
list_table_clustersClusterTools#listTableClusterstableClusters(scope, maxClusterSize, excludeViewpointTables)・viewpoints().listListTableClustersOutput
search_tablesTableTools#searchTablesSearchQuery.of・TableFilter(viewpoints().find・junctions())→ tables().search・junctions().roleOfSearchTablesOutput
list_tablesTableTools#listTablesTableFilter(区分はTableType、ER上の区分はErRole)・TableOrder → tables().list・relations().counts・junctions().roleOfListTablesOutput
get_tableTableTools#getTabletables().lookup(件数分)→ TableOutputBuilder(tables().viewsReferencing・viewpoints().containing・junctions().find・junctions().manyToMany)元の1行を加工(複数ならGetTableOutput)
find_columnsTableTools#findColumnsColumnQuery.of → tables().findColumnsFindColumnsOutput
find_similar_tablesSimilarTableTools#findSimilarTablesSimilarTableQuery.ofTable(tables().lookup)かofColumns・TableFilter → tables().findSimilarFindSimilarTablesOutput
get_related_tablesRelationTools#getRelatedTablestables().lookup → relations().relatedTablesRelatedTablesOutput
find_join_pathRelationTools#findJoinPathtables().lookup×2 → relations().joinPathsJoinPathOutput
get_impactImpactTools#getImpacttables().lookup → impactOf(ImpactAnalyzer: relations().relatedTables・tables().viewsReferencing・functionTableUsages().find・tables().triggersCalling・SqlNames)GetImpactOutput(affected・dependsOn・more・notes)。section指定時はSectionOutput(Section.itemsの1ページ)
get_er_diagramDiagramTools#getErDiagramtables().lookup → relations().relatedTables(table)か、viewpoints().find → relations().among(viewpoint)→ MermaidErDiagram.renderErDiagramOutput
list_functionsFunctionTools#listFunctionsfunctions().list(scope, NameFilter)(オーバーロードを展開)ListFunctionsOutput
get_functionFunctionTools#getFunctionfunctions().lookup → tables().triggersCalling・functionTableUsages().findGetFunctionOutput
list_sequencesSequenceTools#listSequencessequences().listListSequencesOutput
get_sequenceSequenceTools#getSequencesequences().lookup → tables().columnsUsing元の1行+usedByColumns
list_typesTypeTools#listTypestypes().list(scope, filter)+種別の絞り込みListTypesOutput
get_typeTypeTools#getTypetypes().lookup → tables().columnsUsing元の1行+usedByColumns
list_triggersTriggerTools#listTriggerstables().triggers(scope)ListTriggersOutput

件数・段数の上限

どれもAIのコンテキストを圧迫しないための上限です。上限はinputSchemaのmaximumだけに書かれ、範囲外はSDKが止めます(ToolArguments.optionalIntは範囲を検査しません)。

ツール・引数既定上限定数の場所
list_table_clustersのmaxClusterSize30500ClusterTools(下限2)
list_table_clustersのlimit20100ClusterTools
list_table_clustersの1まとまりのテーブル—100ClusterTools.MAX_LISTED_TABLES(超えたらhasMoreTables)
search_tablesのlimit20100TableTools
list_tablesのlimit100500Page.DEFAULT_LIMIT・MAX_LIMIT
find_columnsのlimit50500TableTools
get_tableのtableの件数—10TableTools.MAX_TABLES
get_tableのmanyToMany—20TableOutputBuilder.MAX_MANY_TO_MANY(超えたらmanyToManyTotal)
find_similar_tablesのlimit1050SimilarTableTools
find_similar_tablesのcolumnsの件数—100SimilarTableTools.MAX_COLUMNS
find_similar_tablesのminShared・minSimilarity3・5020・100SimilarTableTools(下限1・10)
find_similar_tablesのonlyInBase・onlyInOther—20SimilarTableTools.MAX_LISTED_COLUMNS(超えたら…Omitted)
get_impactの概要の各リスト(direct・indirect・views・triggers・usages・unverified)—50ImpactTools.MAX_LISTED(超えたらmore。indirectTotalは常に)
get_impactのsection指定時の1ページ100500Page.DEFAULT_LIMIT・Page.MAX_LIMIT(ほかの一覧のツールと同じ)
get_impactの間接参照の段数—3RelationCounts.IMPACT_DEPTH(list_tablesのimpactと同じ)
get_related_tablesのdepth13RelationTools.MAX_DEPTH
find_join_pathのmaxLength46RelationTools
find_join_pathのlimit520RelationTools
get_er_diagramのdepth13DiagramTools
get_er_diagramの図に描くテーブル—80DiagramTools.MAX_NODES(超えたらツールのエラー)
list_functions・list_sequences・list_types・list_triggersのlimit100500Page.DEFAULT_LIMIT・MAX_LIMIT
get_functionの定義本体—4,000字FunctionTools.MAX_DEFINITION_LENGTH
見つからないときの候補—5ObjectCatalog.MAX_SUGGESTIONS
中間テーブルの判定で主キーに収まる関連—12JunctionDetector.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:ファイルの読み込み
SnapshotDirectoryReadersnapshot/{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
JunctionDetector1テーブルが中間テーブルか判定する規則だけを持つ。主キーに収まる関連を候補にし(同じ列の組は外部キーを優先して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
DiagramScopeER図に描くテーブル・関連・スナップショットに無いテーブル。nodes()で箱として描くテーブル(関連の参照先で無いものを含む)を返す。of(RelatedTables)で関連をたどった結果から作る(7.9節)← RelationGraph.among・DiagramTools/→ MermaidErDiagram
SearchQueryテーブル検索の検索語。空白区切りのAND、項目ごとの点数(7.2節)← TableTools・TableCatalog
ColumnQueryカラムの逆引きの条件と強さ(7.3節)← TableTools・TableCatalog
SqlNamesnextval・型名・トリガーの関数名からObjectKeyを取り出し、大文字小文字を区別せず比べる(7.5節)← TableCatalog
TextValuesnull→空文字、検索用の正規化(NFKC+小文字)← Entry・Query類
SchemaObject名前で指定できるオブジェクトの共通インタフェース(key())← TableEntry・FunctionOverloads・SequenceEntry・TypeEntry
ObjectKeyDB名・スキーマ名・名前の組。qualifiedName()でスキーマ.名前。一覧・候補を並べる順ORDERも持つどこでも
ObjectReferenceAIが渡した名前の指定。スキーマ.名前の分割← ObjectResolver・TableTools
Lookup名前の解決結果の封印インタフェース(Found・Ambiguous・NotFound)← ObjectCatalog・TableCatalog/→ ObjectResolver
SearchScopeDB・スキーマでの絞り込み(空文字=絞らない、大文字小文字を区別しない)。観点用に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関数・シーケンス・型の一覧を名前の部分一致で絞る← 関数・シーケンス・型のツール
MatchModeEXACT・PARTIAL(find_columnsのmatch)← ColumnQuery
DatabaseEntrydatabase.json(DB名・DBMS種別・メジャーバージョン)← SnapshotDirectoryReader
TableEntrytables.jsonlの1行。型付きの項目(ビューが参照するテーブルを含む)と元のjsonどこでも
ColumnEntryカラム(名前・論理名・型・PK・NOT NULL・デフォルト値・備考)← TableEntry
RelationEntry外部キー・論理リレーション(自テーブル→参照先)。参照先スキーマが空なら自スキーマ← TableEntry/→ Relation.of
TriggerEntryトリガー(タイミング・イベント・実行単位・関数名)← TableEntry
FunctionEntryfunctions.jsonlの1行(定義本体を含むjson)と、定義本体のハッシュ(definitionHash)← FunctionOverloads・FunctionTableUsages
FunctionOverloads同名の関数の束。名前の解決の単位← ObjectCatalog・TableCatalog.triggersCalling・FunctionTools
SequenceEntrysequences.jsonlの1行(所有カラム)← SequenceTools
TypeEntrytypes.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
RelationKindFOREIGN_KEY・LOGICAL_RELATION← Relation・ColumnHit
Directionたどる向き(OUTGOING・INCOMING・BOTH)← RelationTools・RelationGraph
RelationCountsテーブルの関連の数(incoming・outgoing・impact)と、影響範囲をたどる段数の上限IMPACT_DEPTH← RelationGraph/→ TableToolsのListTablesOutput
TableOrderlist_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のツール
TableDefinitionTools12のツールクラスのspecifications()を並べてSDKへ渡す。並び順がクライアントに見える順← McpServerMain/→ 各*Tools
ToolSpecificationsreadOnlyTool(全ツールの包み・注釈・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
Pageoffset・limitの読み取り・スキーマ(上限はmaximum)・切り出し・nextOffset。一覧の件数の既定値・上限の定数← 一覧系のツール
TableSectionget_tableのsectionsで選べる9項目。referencedByViewsだけはスナップショットに無い、逆引きで足す項目← TableTools・TableOutputBuilder
SchemaToolslist_schemas。DBごとにDBMS種別・メジャーバージョン(不明なら出さない)を添える→ SchemaCatalog.schemas
ViewpointToolslist_viewpoints→ viewpoints().list
ClusterToolslist_table_clusters。まとまりごとに、テーブル(100件まで)・ハブ・まとまりのテーブルを含む観点を添える→ tableClusters・viewpoints().list・relations().counts
TableToolssearch_tables・list_tables・get_table・find_columns。引数からTableFilter(erRoleを含む)・TableOrderを組み立てる、一覧・検索の各行にerRoleを添える、get_tableの複数件の名前をまとめて解決する→ 検索・一覧・解決のメソッド、ViewpointResolver・TableOutputBuilder
TableOutputBuilderget_tableの1テーブル分の結果の組み立て。元の1行にsections・columnsの絞り込み、参照しているビュー、所属する観点、ER上の区分(erRole・junctionOf)と多対多の相手(manyToMany。上限MAX_MANY_TO_MANY)を反映する← TableTools・TableDefinitionResources/→ tables().viewsReferencing・viewpoints().containing・junctions()
SimilarTableToolsfind_similar_tables。tableとcolumnsのどちらか一方を受け、基準を解決して範囲(tableなら基準と同じDB)を組み立てる。片方にだけあるカラムは20件までにし、該当なしのときは条件のゆるめ方をmessageで示す→ tables().lookup・tables().findSimilar・ObjectResolver・ViewpointResolver
ImpactToolsget_impact。tableを解決してimpactOfを呼び、affected(参照元・ビュー・トリガー・関数)とdependsOn(シーケンス・型)に分けて返す。概要の各リストは50件まで(MAX_LISTED)で、超えた分はsection・offset・limitで読む(Section列挙型)。functions.preview・coverage・unverifiedと固定文のnotesで、参考値であることと読み方を伝える← TableDefinitionTools/→ SchemaCatalog.impactOf
RelationToolsget_related_tables・find_join_path。kindNameで由来をforeignKey/logicalRelationに→ relations().relatedTables・relations().joinPaths
DiagramToolsget_er_diagram。tableとviewpointのどちらか一方を受け、範囲を求めてMermaidErDiagramで書く。描くテーブルの上限MAX_NODES(80)→ relations().relatedTables・relations().among・ViewpointResolver・MermaidErDiagram
MermaidErDiagramDiagramScopeからMermaidのerDiagramを組み立てる。表記はcliのMermaidSupportに揃え、DB由来の名前の識別子・表示名・型を無害化する(7.9節)← DiagramTools
FunctionToolslist_functions・get_function。定義本体の除去・集約(sharedDefinition)・切り詰め(truncateDefinitionOf)と、オーバーロードごとのtableUsageの組み立て(テーブルはスキーマ.テーブル、操作はCRUDの文字列。7.10節)→ functions().list・functions().lookup・tables().triggersCalling・functionTableUsages().find
SequenceToolslist_sequences・get_sequence→ sequences().list・tables().columnsUsing
TypeToolslist_types・get_type。種別はPostgreSQLとOracleの7つ→ types().list・tables().columnsUsing
TriggerToolslist_triggers(テーブルをまたぐ一覧)→ tables().triggers
TableDefinitionResources全テーブル(ビューを含む)をリソースとして列挙する。nameは論理名 (スキーマ名.物理名)、descriptionの先頭にも同じ名前(120文字まで)。表示用の文字列から制御文字・書字方向の制御文字を除き1行にする。読まれたらget_tableと同じJSONを組み立てる← McpServerMain/→ TableResourceUri・TableOutputBuilder
TableResourceUriリソースのURIdbxray://{DB名}/{スキーマ名}/{テーブル名}。各区間をUTF-8でパーセントエンコード(英数字と-._~以外)← TableDefinitionResources
package-infoパッケージの要旨—
テスト(src/test)
McpServerProcessTestE2E。配布するjarを子プロセスで起動し、MCPクライアントから呼ぶ。ツールの並び、標準出力の汚れ、多対多・中間テーブル・構造の似たテーブルの結果とerRoleのenumの検証、immediateExecutionの回帰(ツール・リソースの両方)、SDKの入力スキーマの検証(型・範囲・未知の引数の拒否、tableのカンマ区切り)、リソースの一覧・読み出し、起動失敗時の終了コード2→ shadowJar・サンプルのスナップショット
ServerArgumentsTest起動引数の解釈と誤り→ ServerArguments
ArchitectureTestArchUnitで構造の規約を検査する。パッケージの依存の向き(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
SampleSnapshotContractTestcliのベースラインdocs/sample/postgres/output/snapshotを読む契約テスト。cliの形式変更への追従漏れを検知。サンプルで中間テーブルと判定されるのがproject_assignmentだけであること、employee_historyがemployeeのsuperset(型の違いはsize)として見つかることも確かめる→ ベースライン
InsightsDirectoryReaderTest観点・関数の利用テーブルの読み込みと兄弟ディレクトリの解決(DB名・スキーマ名はファイルの中の値)→ InsightsDirectoryReader
SampleInsightsContractTestベースラインの観点と、スナップショットのテーブルのキーが一致すること→ ベースライン
FunctionTableUsagesTest参考情報と関数の対応付け(キー・引数・定義本体のハッシュ。ハッシュが異なる古い参考情報・ハッシュを持たない参考情報は返さない)と、参考情報の状態(有効/無い/古い)→ FunctionTableUsages
SampleFunctionTableUsagesContractTestcliの--previewの差分のベースライン(docs/sample/postgres/output-preview)の参考情報が読めて、スナップショットの関数(既定のベースライン)と対応付くことの契約テスト。cliのハッシュの求め方・形式の変更への追従漏れを検知。サンプルのemployeeの影響範囲(自己参照・ビューのビュー・関数5つ)も確かめる→ ベースライン
SampleOracleImpactContractTestOracleのサンプル(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
ImpactToolsTestget_impactの結果のJSON(affected・dependsOn・functions.preview)、上限と総数・more、section・offset・limitのページング、sectionなしのoffsetのエラー、値の無い項目の省略、notesの出し分け、存在しないテーブルのエラー→ TableDefinitionTools
ToolArgumentsTest引数の読み取り(空白・型・カンマ区切り・列挙)→ ToolArguments
MermaidErDiagramTestDB由来の名前(記号・二重引用符・改行)の無害化と、識別子が重なったときの連番→ MermaidErDiagram
SampleErDiagramContractTest観点のER図が、cliの観点ページ(ベースライン)のER図と同じ記述になることの契約テスト。表記のずれを検知→ ベースライン
TableDefinitionResourcesTest
TableResourceUriTest
リソースの名前・説明(先頭の表示名・切り詰め・制御文字の除去)・中身/URIのパーセントエンコード→ 各クラス

catalogには単体テストのカバレッジの最低基準(line 95%・branch 85%)があり、./gradlew buildで検査されます。E2Eは子プロセスで動くため計測の対象外です。

10変更するときの道しるべ

ツールを1つ足すとき

  1. 関心が合う*ToolsクラスにreadOnlyTool(…)を足す。無ければクラスを作り、TableDefinitionTools.specifications()に並べる(並び順はクライアントに見える順)。
  2. 検索・たどりのルールはcatalogの該当する窓口(TableCatalog・ObjectCatalog・ViewpointCatalog・RelationGraph)に置き、その窓口のテスト(TableCatalogTest等)で確かめる(カバレッジ基準の対象)。オブジェクトの種類を足すときはObjectCatalogを使い、SchemaCatalogには窓口を返すメソッドだけを足す。catalogにSDKやJacksonの型を持ち込まない(ArchitectureTestが検出する)。
  3. 型・範囲(minimum/maximum)・enumはinputSchemaに書く。SDKがそれを検証するので、ToolArgumentsでは同じ検査を重ねず、スキーマで表せない検証だけを書く。検証の挙動はMcpServerProcessTestで確かめる(ハンドラを直接呼ぶテストではSDKの検証を通らない)。
  4. 結果は*Outputのrecordにする。コンポーネント名がそのまま結果のキーになる。
  5. McpServerMain.INSTRUCTIONSに使い分けを書き足す。
  6. McpServerProcessTestのツール一覧、TableDefinitionToolsTest、docs/architecture/mcp-server.mdとdocs/usage/mcp-server.mdのツールの表を更新する。

スナップショットの形式が変わるとき

関数の利用テーブル(プレビュー)の参考情報が変わるとき

中間テーブルの判定・構造の類似の規則を変えるとき

影響範囲の集め方を変えるとき

ER図の表記・リソースを変えるとき

やってはいけないこと

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でツールを受信スレッド上で直列に実行する。