本文へスキップ
Edition · Tokyo
実装・公開事例 · 定期更新

e-Gov法令XMLをMCP向けJSONへ変換する:条・項・号と出典を壊さない設計

e-Gov法令APIのXMLを、AIが引用しやすいMCPレスポンスへ変換する設計を解説。条・項・号の順序、施行時点、改正履歴、原文参照を失わない型と実装手順を示します。

codeagent.jp編集部 情報確認 約6分
Tags
情報確認
参考リンク
5件
更新性
定期更新
読了目安
約6分
更新管理

仕様・料金・提供範囲が変わりやすいテーマは、公開日・更新日・情報確認日を分けて管理します。 導入前には必ず記事末尾の一次情報と公式ドキュメントで最新状況を確認してください。

e-Gov法令XMLをMCP向けJSONへ変換する:条・項・号と出典を壊さない設計 の16:9共有用サマリー画像。 XMLを平坦な文字列にせず、法令の階層・時点・出典をMCPのJSONへ残す 1. 入力で固定する: lawId・基準日・取得URLを先に確定、XML要素の順序と属性を保持して解析、本文とAPIメタデータを別々に扱う 2. 変換で守る: Article・Paragraph・Itemを階層のまま写す、表示番号と機械識別子を混同しない、欠落値をAIに補完させない 3. 出力で検証する: 引用単位ごとに原文参照を付与、revisionIdと基準日を毎回答へ含める、代表・枝番・附則を回帰テストする
e-Gov法令XMLをMCP向けJSONへ変換する:条・項・号と出典を壊さない設計 資料 26-DIT1 2026.08.13 実装・公開事例

結論:XMLを「本文1本」に潰さず、引用できる小さなJSONへ変換する

e-Gov法令XMLをMCPで扱うときの要点は、条・項・号の階層、取得時点、改正リビジョン、原文URLを同じレスポンスに残すことです。AIに渡す文字数を減らすために本文を平坦化しても、検証に必要な位置情報まで捨ててはいけません。

この記事では、e-Govの法令標準XMLを入力にして、MCPツールが返す安定したJSONを設計します。法令の解釈や個別事案への当てはめは扱いません。最終判断には、対象時点の原文と必要に応じて専門家の確認が必要です。

対象読者

  • e-Gov法令APIをTypeScriptから利用したい人
  • MCPサーバーに法令検索・条文取得ツールを追加したい人
  • AIの回答から「どの法令の何条何項か」を追跡できるようにしたい人
  • XMLをJSONへ変換した後の欠落や順序崩れをテストしたい人

まず固定する4つの識別情報

変換コードを書く前に、MCPレスポンスで必ず返す値を決めます。

役割欠けたときの問題
lawId法令を機械的に識別同名・改題・略称に弱くなる
revisionId取得した改正状態を識別後日、同じ内容を再現できない
asOfどの時点を求めたかを記録現行条文と過去条文を取り違える
sourceUrl人間が原文へ戻る入口AIの引用を照合できない

法令IDは法令番号と一対一になるよう設計された15文字の識別子です。改題後も名称だけで追わず、IDを軸にする理由がここにあります(e-Gov「法令種別と法令ID」)。

法令API v2の law_data は、法令IDまたは法令番号を指定した場合は該当する版を返し、asof で指定日以前の改正履歴を選べます。リビジョンIDを直接指定した場合は asof が使われないため、呼び出し側で両者を混同しない設計が必要です(法令API v2仕様:law_data)。

法令XMLのどこをJSONへ写すか

法令標準XMLのルート LawLawNumLawBody を持ち、LawBody の下に法令名、本文、附則、別表などが並びます(法令標準XMLスキーマの解説)。MCPで条文を答えるだけでも、次の要素は残します。

XML要素JSON側変換上の注意
LawTitlelawTitle略称ではなく取得本文の題名
Articlearticles[]MainProvision、章・節などの階層、附則を区別して列挙する
Paragraphparagraphs[]表示上番号がない第1項も要素として存在する
Itemitems[]号の下にイ・ロ等の下位要素があり得る
Sentencesentences[]本文・ただし書を Function で区別できる
SupplProvisionsupplementaryProvisions[]本則と混ぜず、施行関係の文脈を残す

法令の表示では番号が省略される1項だけの条でも、XML上は Paragraph が置かれます。また「第二十一条の二」のような枝番号付きの条は第二十一条の子要素ではなく、独立した Article です(e-Gov「法令の構造と法令標準XML」)。DOMの深さだけで条番号を推測せず、要素名と属性を読みます。

MCPレスポンスの型を先に決める

XMLパーサー固有の形をそのまま公開すると、ライブラリ交換でMCPの出力が変わります。公開境界には、自分で管理できる小さなDTOを置きます。

type SourceRef = {
provider: "e-Gov法令検索";
lawId: string;
revisionId: string | null;
asOf: string | null; // YYYY-MM-DD
retrievedAt: string; // ISO 8601
sourceUrl: string;
};
type SentenceDto = {
text: string;
function: "main" | "proviso" | null;
};
type ItemDto = {
kind: "Item" | `Subitem${number}`;
number: string | null;
title: string | null;
sentences: SentenceDto[];
children: ItemDto[];
warnings: string[];
};
type ParagraphDto = {
number: number | null;
caption: string | null;
sentences: SentenceDto[];
items: ItemDto[];
};
type ArticleDto = {
number: string;
caption: string | null;
title: string | null;
paragraphs: ParagraphDto[];
};
type LawExcerptDto = {
schemaVersion: "law-excerpt/1";
lawTitle: string;
lawNumber: string;
articles: ArticleDto[];
supplementaryProvisions: ArticleDto[][];
source: SourceRef;
warnings: string[];
};

schemaVersion はe-Gov側の版ではなく、MCPが公開するJSON契約の版です。null と空配列を使い分け、存在しない番号をAIに推測させないことも重要です。

順序を壊さない変換手順

XMLからの変換は、次の順番にすると責務を分けられます。

  1. APIレスポンスから law_inforevision_info、法令本文を分離する。
  2. XMLを名前空間対応のパーサーで解析し、要素順を保持する。
  3. MainProvision から章・節などの構造要素をたどり、直下の出現順を保って Article を列挙する。
  4. 各条の Paragraph、各項の Item、各号の Subitem1Subitem10 を子要素順に写す。
  5. Sentence の文字列と Function 属性を保持する。
  6. SupplProvision は本則と別配列へ変換する。
  7. APIメタデータから SourceRef を作り、本文DTOへ付加する。
  8. 要求された条が見つからない場合は空の成功値ではなく、機械可読なエラーを返す。

DOM風のAPIを持つXMLパーサーなら、核になる変換は次の形です。実運用では実行環境に合うXMLパーサーを選び、外部エンティティを解決しない設定にしてください。

const directChildren = (parent: Element, tag: string): Element[] =>
Array.from(parent.children).filter((node) => node.localName === tag);
const directText = (parent: Element, tag: string): string | null => {
const node = directChildren(parent, tag)[0];
const value = node?.textContent?.trim();
return value ? value : null;
};
const sentence = (node: Element): SentenceDto => {
const raw = node.getAttribute("Function");
return {
text: node.textContent?.replace(/\s+/g, " ").trim() ?? "",
function:
raw === "main" ? "main" :
raw === "proviso" ? "proviso" :
null,
};
};
const item = (node: Element): ItemDto => ({
kind: node.localName as ItemDto["kind"],
number: node.getAttribute("Num"),
title: directText(node, node.localName === "Item" ? "ItemTitle" : `${node.localName}Title`),
sentences: directChildren(node, node.localName === "Item" ? "ItemSentence" : `${node.localName}Sentence`)
.flatMap((group) => directChildren(group, "Sentence"))
.map(sentence),
children: Array.from(node.children)
.filter((child) => /^Subitem(?:10|[1-9])$/.test(child.localName))
.map(item),
warnings: Array.from(node.children)
.filter((child) => ["Column", "Table", "List", "TableStruct"].includes(child.localName))
.map((child) => `未対応要素: ${child.localName}`),
});
const paragraph = (node: Element): ParagraphDto => {
const raw = node.getAttribute("Num");
const parsed = raw === null ? null : Number.parseInt(raw, 10);
return {
number: Number.isNaN(parsed) ? null : parsed,
caption: directText(node, "ParagraphCaption"),
sentences: directChildren(node, "ParagraphSentence")
.flatMap((group) => directChildren(group, "Sentence"))
.map(sentence),
items: directChildren(node, "Item").map(item),
};
};
const article = (node: Element): ArticleDto => ({
number: node.getAttribute("Num") ?? "",
caption: directText(node, "ArticleCaption"),
title: directText(node, "ArticleTitle"),
paragraphs: directChildren(node, "Paragraph").map(paragraph),
});
const articleContainers = new Set([
"MainProvision", "Part", "Chapter", "Section", "Subsection", "Division",
]);
const collectArticles = (node: Element): ArticleDto[] =>
Array.from(node.children).flatMap((child) => {
if (child.localName === "Article") return [article(child)];
return articleContainers.has(child.localName) ? collectArticles(child) : [];
});

この抜粋は条・項・号の変換核です。実際の入口では MainProvisionPartChapterSectionSubsectionDivision を再帰的にたどって Article を出現順に集め、SupplProvisionsupplementaryProvisions へ分離します。querySelectorAll("Article") で全子孫を一括取得すると本則と附則の境界を失うため、どのコンテナから読んだかを必ず保持します。表、様式、別表まで扱う場合は、別DTOとして段階的に追加してください。

APIの形式指定と変換境界

法令API v2には、外側のレスポンス形式を選ぶ response_format と、法令本文形式を選ぶ law_full_text_format があります。組み合わせによって本文がBase64表現になる場合もあるため、形式を暗黙の既定値に任せず固定します(法令API v2仕様)。

また、v2の法令本文JSONや法令ファイルJSONなどには試行版で仕様変更の可能性がある旨が表示されています(法令API v2仕様の注意事項)。そのため、MCPの出力をe-GovのJSONと同型にせず、次の境界を置きます。

e-Gov API
├─ APIメタデータ ──> 出典・時点の検証
└─ 法令標準XML ────> XMLパーサー ──> LawExcerptDto
└─ MCP tool result

法令ファイル取得エンドポイントはXML、JSON、HTML、RTF、DOCXを選べます(法令API v2:law_file)。XMLを正規入力とするなら、取得層でメディア型とサイズ上限を検証してからパーサーへ渡します。

MCPツールは用途別に小さくする

全文を毎回返す1ツールより、検索と取得を分けた方が、コンテキスト消費と誤引用を抑えられます。

ツール入力返すもの返さないもの
search_laws正式名・法令番号候補のID、題名、番号全文
get_law_articlelawId、条番号、asOf指定条のDTOと出典他の条
get_law_revisionslawId改正履歴と施行日法的評価
get_law_sourcerevisionId原文URLと取得情報AI要約

既存MCPへ新しい取得層を組み込む前に、e-Gov法令MCPの構成と改善方針で責務分割を確認できます。時点指定の挙動は法令API v2のasof実測も参考になります。ただし、どちらの記事も特定パッケージの未統合変更が公開済みであることを意味しません。

エラーを「空の回答」にしない

AIは空配列を見ると、名称違いや通信失敗を内容不存在と解釈しがちです。ツール呼び出しがプロトコル上は完了したものの指定条が見つからない場合は、たとえば次のToolResultを返します。

{
"resultType": "complete",
"content": [
{
"type": "text",
"text": "指定した条番号は取得したリビジョンにありません"
}
],
"structuredContent": {
"errorCode": "ARTICLE_NOT_FOUND",
"message": "指定した条番号は取得したリビジョンにありません",
"lawId": "000000000000000",
"revisionId": "取得できた場合のみ入れる",
"requestedArticle": "21_2",
"retryable": false
},
"isError": true
}

これはSDKから返す内側のToolResult相当で、MCPのJSON-RPC error オブジェクトではありません。ツール名不明やプロトコル不正などJSON-RPC層の失敗とは分けます。区別したいアプリケーションエラーは、LAW_NOT_FOUNDREVISION_NOT_FOUNDARTICLE_NOT_FOUNDUPSTREAM_TIMEOUTINVALID_XMLUNSUPPORTED_SCHEMA です。上流レスポンスの一部をそのままモデルへ見せず、ログには機密情報を除いた相関IDを残します。

回帰テストに入れる法令構造

「有名な第1条が取れた」だけでは変換の正しさを証明できません。次の構造を、固定したXMLフィクスチャでテストします。

  • 1項だけで表示番号が省略される条
  • 2項以上を持つ条
  • 号、イ・ロ、(1)など多段の列挙
  • 本文とただし書が同じ項にある条
  • 「第二十一条の二」のような枝番号
  • 附則内の条
  • 表、別表、様式、ルビや画像参照を含む文
  • 改正前後で題名または条番号が変わる法令

検証では、XMLへ戻したときの完全一致より、入力要素数、順序、番号、文文字列、出典識別子が期待値と一致することを確かめます。スキーマ自体は公開XSDで機械検証できます(XMLSchemaForJapaneseLaw v3)。

公開前チェックリスト

  • lawIdrevisionIdasOfsourceUrlを出力する
  • 本則・章節構造・附則を区別し、ArticleParagraphItemSubitemを出現順に読む
  • Sentence Functionの本文・ただし書を区別する
  • 枝番号を数値化して欠落させない
  • 附則と本則を同じ配列へ無条件に混ぜない
  • 不明な要素を黙って捨てず、警告または未対応エラーにする
  • XMLサイズ、タイムアウト、メディア型を取得層で制限する
  • AI要約とは別に、原文の引用位置を返す
  • 対象時点が重要な回答は人間確認へ送る

よくある質問

e-Gov法令API v2がJSONを返すなら、XML変換は不要ですか?

用途次第です。v2にはJSON形式がありますが、法令本文JSONの一部は試行版として仕様変更の可能性が明記されています。MCPの公開契約を安定させたい場合は、法令標準XMLを入力として自前の小さなDTOへ変換する選択肢があります。

条文を単純なプレーンテキストにしてはいけませんか?

全文検索だけなら使えますが、引用・差分・条項指定では条、項、号、ただし書、附則の境界が必要です。本文テキストと構造化位置情報を併存させると、AIの回答を原文へ戻して検証できます。

この記事のコードは公開中のegov-law-mcpがAPI v2対応済みであることを示しますか?

示しません。この記事は変換境界の設計例です。特定パッケージの実装状況や公開状態は、対象バージョンのソースと配布情報を別途確認してください。

まとめ

e-Gov法令XMLをMCP向けJSONへ変換するときは、本文の短縮より先に検証可能性を設計します。

  • 法令ID、改正リビジョン、基準日、原文URLを固定する
  • 条・項・号を階層のままDTOへ写し、表示省略や枝番号を推測しない
  • e-Govのレスポンス形式とMCPの公開JSON契約を分離する
  • 不存在、通信失敗、未対応構造を別エラーとして返す
  • 代表的な条だけでなく、附則・枝番・ただし書まで回帰テストする

変換後のJSONは「AIが読みやすい」だけでなく、人間が原文へ戻って確かめられることが完成条件です。接続後の切り分けにはMCPサーバー接続トラブルシューティングも利用できます。

Primary sources

一次情報・参考リンク

About the author
codeagent.jp編集部

Claude Code / Codex / MCP を個人開発サイト運用と公開MCPサーバー開発で試し、一次情報・検証ログ・失敗例をもとに整理します。

関連して読む