EDINET APIをMCP化する設計:書類検索・取得・XBRL抽出の3ツールとキー管理・出典保持
金融庁EDINET API v2をMCP化する設計案。書類検索→取得→XBRL/CSV抽出の3ツール分割、Subscription-Keyの環境変数管理とマスク、日次一覧と書類本体で分けるキャッシュ、docID・提出日時を保持する出典スキーマを型定義とスケルトンで示します(2026-09-14、キー未取得で実行未検証)。
EDINET API を MCP にするとき、やってはいけないのは「1つの検索ツールが ZIP をそのままモデルに返す」設計です。 書類一覧は日付単位でしか取れず、書類本体は ZIP/PDF で、API キーは URL に載ります。この3つの制約をそれぞれ別のツールと別の層で受け止めるのが、この記事の設計です。
同じ SDK(@modelcontextprotocol/server 2.0.0)で実際に動かした例は国会会議録APIをMCPサーバー化するにあります。出典オブジェクトの形はMCPのprovenance用outputSchema設計に合わせました。
先に押さえること
- 3ツールに分ける。
search_documents(候補と状態)→fetch_document(保存とハッシュ)→extract_facts(許可項目の抽出)。 - キーは環境変数、URL はマスク。
Subscription-Keyはクエリに載るので、そのままログに出すと漏れます。 - キャッシュは3種類。 当日一覧は短命、過去一覧は日次、書類本体は不変扱い。
- 出典は
docIDとsubmitDateTime。 訂正はparentDocIDで原本と結び、warningsに未検証や延長期間中を残します。
なぜ3ツールなのか
EDINET API 自体は「日付の一覧」と「docID の本体」の2本しかありません。それをそのまま2ツールにすると、モデルが日付を総当たりし、ZIP を受け取って途方に暮れます。API の形ではなく、エージェントが答えるまでに必要な状態遷移でツールを切ります。
| ツール | 入力 | 返すもの | API 呼び出し |
|---|---|---|---|
search_documents | edinetCode または secCode、docTypeCode(既定 120)、periodEndFrom/To、includeAmendments | 候補の docID・提出者・期間・提出日時・各フラグ・legalStatus・parentDocID | なし(ローカル索引。索引の更新は別バッチ) |
fetch_document | docID、type(1〜5) | 保存先パス、Content-Type、バイト数、SHA-256、出典 | 書類取得 API を1回 |
extract_facts | docID、concepts(許可リスト内の項目名)、consolidated | 項目ごとの値・単位・期間・コンテキスト、出典と変換履歴 | なし(保存済み ZIP を読む) |
search_documents が API を直接呼ばないのがポイントです。書類一覧 API は date 必須で企業名検索がないため、日次バッチで type=2 の一覧を取り込み、docID をキーにした索引をサーバーが持ちます。当日分の追加は seqNumber の差分(前回の最後の連番より大きいもの)で拾えます。
キーの管理:環境変数とマスク
EDINET API はキーをクエリパラメータ Subscription-Key で渡す仕様です。つまりリクエスト URL そのものが秘密情報になります。決めごとは3つです。
- キーは
EDINET_API_KEY環境変数からだけ読む。 ツールの入力に含めない。起動時に未設定なら stderr に出して終了する - URL を出典・エラー・ログに載せる前にマスクする。
Subscription-Key=...をSubscription-Key=***に置換する関数を通す - 出典
uriには閲覧サイトの書類 URL か API のパス部分だけを入れる。 キー付きの完全 URL は provenance に入れない
Claude Code への登録は、公式ドキュメントの --env オプションか .mcp.json の env で渡します。
claude mcp add --transport stdio edinet \ --env EDINET_API_KEY=YOUR_KEY \ -- node /absolute/path/edinet-mcp/dist/server.jsプロジェクト共有の .mcp.json に書く場合は、値を直接書かず環境変数展開を使います。
{ "mcpServers": { "edinet": { "command": "node", "args": ["/absolute/path/edinet-mcp/dist/server.js"], "env": { "EDINET_API_KEY": "${EDINET_API_KEY}" } } }}秘密情報をエージェントから隔離する一般論はAIエージェントの秘密情報保護で扱っています。
キャッシュ:3種類の TTL
仕様書3-1-3 の更新タイミングをそのまま TTL に写します。
書類本体を不変扱いにできるのは、訂正報告書が親書類から独立した docID で採番されるためです。ただし、過去分の一覧側は「閲覧期間満了で docID 以外が null になる」「取下書の提出で取下区分が設定される」という更新を受けるので、索引の日次更新を止めると取得不可の書類を候補に出し続けることになります。
出典スキーマ:docID と提出日時を必須にする
出典は data とは別の provenance に置き、sources[] に docID を id として持たせます。EDINET 固有の時点情報が3つあるので、混ぜずに別フィールドにします。
| フィールド | 中身 | 由来 |
|---|---|---|
submitDateTime | 提出日時 | 書類一覧 API の submitDateTime |
periodStart / periodEnd | 対象期間(有報なら事業年度) | 同 periodStart / periodEnd |
retrievedAt | サーバーが取得した時刻 | サーバー側で生成 |
contentHash | 保存した ZIP/PDF の SHA-256 | fetch_document で計算 |
parentDocID | 訂正前の書類の docID | 同 parentDocID(設定されている場合のみ) |
contentHash は「後から同じバイト列か」を照合するためのもので、発行者の真正性を証明するものではありません(この点は provenance 記事と同じです)。
スケルトン:型定義とツール登録
以下は @modelcontextprotocol/server 2.0.0 と zod/v4 を前提にした骨組みです。動作確認していません。 TODO の部分は実装時に埋めます。
import { McpServer } from '@modelcontextprotocol/server';import { serveStdio } from '@modelcontextprotocol/server/stdio';import * as z from 'zod/v4';
// ---- 環境変数からキーを読む。ツール入出力には出さない ----const API_KEY = process.env.EDINET_API_KEY;if (!API_KEY) { console.error('EDINET_API_KEY が未設定です'); process.exit(1);}const API_BASE = 'https://api.edinet-fsa.go.jp/api/v2';const maskKey = (url: string) => url.replace(/Subscription-Key=[^&]*/g, 'Subscription-Key=***');
// ---- 書類一覧 API の results 1件(仕様書 3-1-2-2、40項目のうち設計で使うもの) ----type EdinetDocument = { seqNumber: number; docID: string; edinetCode: string | null; secCode: string | null; JCN: string | null; filerName: string | null; ordinanceCode: string | null; formCode: string | null; docTypeCode: string | null; periodStart: string | null; periodEnd: string | null; submitDateTime: string | null; docDescription: string | null; parentDocID: string | null; withdrawalStatus: '0' | '1' | '2'; docInfoEditStatus: '0' | '1' | '2'; disclosureStatus: '0' | '1' | '2' | '3'; xbrlFlag: '0' | '1'; pdfFlag: '0' | '1'; attachDocFlag: '0' | '1'; englishDocFlag: '0' | '1'; csvFlag: '0' | '1'; legalStatus: '0' | '1' | '2';};
// ---- 出典(mcp-source-provenance-schema の形 + EDINET 固有の時点情報) ----const SourceSchema = z.object({ id: z.string().length(8).describe('docID'), uri: z.string().describe('キーを含まない URL'), title: z.string(), publisher: z.literal('金融庁 EDINET'), retrievedAt: z.string(), sourceType: z.enum(['official-api', 'document']), submitDateTime: z.string().nullable(), periodStart: z.string().nullable(), periodEnd: z.string().nullable(), parentDocID: z.string().nullable(), contentHash: z.string().regex(/^sha256:[0-9a-f]{64}$/).optional(),});const ProvenanceSchema = z.object({ generatedAt: z.string(), sources: z.array(SourceSchema).min(1), transformations: z.array(z.object({ type: z.enum(['extract', 'filter', 'normalize']), description: z.string() })), warnings: z.array(z.string()),});
const DocumentSummarySchema = z.object({ docID: z.string(), filerName: z.string().nullable(), edinetCode: z.string().nullable(), secCode: z.string().nullable(), docTypeCode: z.string().nullable(), docDescription: z.string().nullable(), periodStart: z.string().nullable(), periodEnd: z.string().nullable(), submitDateTime: z.string().nullable(), parentDocID: z.string().nullable(), withdrawalStatus: z.string(), disclosureStatus: z.string(), legalStatus: z.string(), available: z.object({ xbrl: z.boolean(), pdf: z.boolean(), csv: z.boolean(), english: z.boolean() }),});
// ---- 索引・取得・抽出は別モジュール(未実装) ----interface DocumentIndex { search(q: { edinetCode?: string; secCode?: string; docTypeCode: string; periodEndFrom?: string; periodEndTo?: string; includeAmendments: boolean }): Promise<EdinetDocument[]>;}declare const index: DocumentIndex; // TODO: 日次で documents.json?type=2 を取り込む索引
serveStdio(() => { const server = new McpServer({ name: 'edinet', version: '0.0.1' });
server.registerTool( 'search_documents', { title: 'EDINET 書類検索(ローカル索引)', description: '日次取り込み済みの提出書類一覧から候補を返す。既定は有価証券報告書(docTypeCode=120)。' + '訂正報告書(130)は includeAmendments=true で含める。書類本体は fetch_document で取得する。', inputSchema: z.object({ edinetCode: z.string().length(6).optional(), secCode: z.string().length(5).optional(), docTypeCode: z.string().length(3).default('120'), periodEndFrom: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), periodEndTo: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), includeAmendments: z.boolean().default(false), limit: z.number().int().min(1).max(50).default(10), }), outputSchema: z.object({ data: z.object({ items: z.array(DocumentSummarySchema), indexedThrough: z.string().describe('索引が反映済みのファイル日付') }), provenance: ProvenanceSchema, }), annotations: { readOnlyHint: true }, }, async (input) => { // TODO: index.search() を呼び、withdrawalStatus=2 / disclosureStatus=2 / legalStatus=0 を warnings 付きで区別して返す throw new Error('not implemented'); }, );
server.registerTool( 'fetch_document', { title: 'EDINET 書類取得', description: 'docID と type(1: 本文+監査報告書 ZIP, 2: PDF, 3: 添付 ZIP, 4: 英文 ZIP, 5: CSV ZIP)で書類本体を保存し、' + 'パス・Content-Type・サイズ・SHA-256 を返す。本文そのものは返さない。', inputSchema: z.object({ docID: z.string().length(8), type: z.enum(['1', '2', '3', '4', '5']).default('1'), }), outputSchema: z.object({ data: z.object({ docID: z.string(), type: z.string(), savedPath: z.string(), contentType: z.string(), bytes: z.number(), sha256: z.string(), }).nullable(), provenance: ProvenanceSchema, }), annotations: { readOnlyHint: true, openWorldHint: true }, }, async ({ docID, type }) => { const url = `${API_BASE}/documents/${docID}?type=${type}&Subscription-Key=${API_KEY}`; // TODO: fetch(url) → Content-Type が application/json なら失敗(本文の StatusCode/metadata.status を読む) // application/octet-stream / application/pdf なら保存し SHA-256 を計算 // provenance.sources[].uri には maskKey(url) ではなく閲覧サイトの書類URLかパス部分だけを入れる // 429 は再試行せず warnings に載せて返す void maskKey(url); throw new Error('not implemented'); }, );
server.registerTool( 'extract_facts', { title: 'EDINET 財務項目抽出', description: '保存済みの CSV(type=5)または XBRL(type=1)から、許可リスト内の項目だけを値・単位・期間付きで返す。', inputSchema: z.object({ docID: z.string().length(8), concepts: z.array(z.string()).min(1).max(50).describe('許可リスト内の項目名'), consolidated: z.boolean().default(true), }), outputSchema: z.object({ data: z.object({ facts: z.array(z.object({ concept: z.string(), value: z.string(), unit: z.string().nullable(), periodStart: z.string().nullable(), periodEnd: z.string().nullable(), consolidated: z.boolean(), sourceRefs: z.array(z.string()), })), notFound: z.array(z.string()), }), provenance: ProvenanceSchema, }), annotations: { readOnlyHint: true }, }, async () => { // TODO: CSV レイアウトは「書類閲覧操作ガイド」で確認してから実装する(本記事では未確認) throw new Error('not implemented'); }, );
console.error('edinet MCP server listening on stdio'); return server;});スケルトンで意図的に決めていることを3つ挙げます。
fetch_documentのdataに本文フィールドがない。 返すのはパス・サイズ・ハッシュだけです。モデルが「中身を見せて」と頼んでも、このツールは応えませんsearch_documentsはwithdrawalStatus・disclosureStatus・legalStatusを隠さず返す。 取り下げ済みや不開示中を候補から黙って消すと、「見つからない」と「存在しない」の区別がつかなくなります。候補には残し、warningsで状態を明示しますextract_factsはconceptsを許可リストに限定する。 任意の項目名を受け付けると、モデルが存在しない項目を作って問い合わせ、空振りを埋めようとします。notFoundを別に返すのもそのためです
訂正・取下げ・満了の扱い
書類の状態は search_documents の結果に含めて、判断はエージェントと人間に残します。
| 状態 | 一覧上の値 | ツールの振る舞い |
|---|---|---|
| 訂正報告書がある | 訂正側の parentDocID に原本の docID | 原本と訂正を両方候補に出し、warnings に「訂正あり」を載せる |
| 取り下げられた | withdrawalStatus=2 | 候補に残すが fetch_document は拒否、warnings に明示 |
| 不開示中 | disclosureStatus=2 | 同上 |
| 延長期間中 | legalStatus=2 | 取得は可。仕様書の「法定縦覧期間内と同様には訂正されないことがある」を warnings に載せる |
| 閲覧期間満了 | legalStatus=0、docID 以外 null | 取得不可として返す |
これは法案から現行法までの調査フローで「法案名の類似で現行法に飛ばない」と書いたのと同じ発想で、書類名の新しさで最新版を選ばせないためのものです。
実装に進む前のチェックリスト
- EDINET のアカウントと API キーを取得し、キーなし・無効キーの401応答と、正しいキーでの
type=1応答を実測した -
EDINET_API_KEYを環境変数からだけ読み、入出力・ログ・出典に載らないことをテストした - 書類一覧の日次取り込みと、
seqNumber差分による当日分の追加を実装した -
Content-Typeで ZIP/PDF と JSON エラーを判定し、429 は再試行せず返す -
parentDocID・withdrawalStatus・disclosureStatus・legalStatusを候補に含めた - CSV のレイアウトを書類閲覧操作ガイドで確認し、
extract_factsの許可リストを作った -
outputSchemaにprovenance.sources[].submitDateTimeとperiodEndを必須として入れた - 生の JSON-RPC で
tools/callのstructuredContentを検証した(stdio 最小ハーネス)
まとめ
- EDINET API の「日付単位の一覧」「ZIP/PDF の本体」「URL に載るキー」を、
search_documents・fetch_document・extract_factsの3ツールと索引・保存・抽出の3層で受け止める - キーは
EDINET_API_KEY環境変数からだけ読み、URL はマスクしてから記録する。出典uriにキー付き URL を入れない - キャッシュは当日一覧60秒、過去一覧は日次、書類本体は不変。閲覧期間満了は取得不可として返す
- 出典は
docID・submitDateTime・periodEnd・retrievedAt・contentHashを分けて持ち、訂正はparentDocIDで結ぶ - コードは骨組みのみで実行未検証。キー取得後に実測してから更新する
MCP の価値は API の薄いラッパーではなく、モデルに渡す前に「どの書類の、いつ時点の、どの項目か」を確定させる層にあります。EDINET のように一覧と本体が離れている API ほど、その層の設計が効きます。
関連して読む
mcp・ai-agentを続けて読む
· 参考リンク 9件公共データAPIをMCP化する設計パターン:認証キー・識別子・出典・レート制限の共通解
e-Gov法令・国会会議録・EDINET・e-Stat・法人番号の公共APIをMCPにする共通設計。認証キーの環境変数化、識別子の正規化、出典と時点の保持、レート制限とキャッシュ、見つからない時に止まる設計、stdio/HTTPの選択をegov-law-mcpの実装(2026-09-14確認)で例示。
ai-agent・mcpを続けて読む
· 参考リンク 3件EDINET API v2の使い方:APIキー取得・書類一覧・書類取得の仕様と有報をAIで読む設計
金融庁EDINET API(Version 2)の書類一覧API・書類取得API・APIキー発行手順・ステータスコード・更新タイミングを、2026年6月版の公式仕様書で確認して整理。キー未取得のため実行は未検証ですが、キーなしで返る401応答は実測しました。有価証券報告書をAIエージェントで読む設計まで。
この記事の情報・検証メモ
- mcp
- ai-agent
- legal-tech
- edinet
- security
- workflow
- 公開日
- 情報確認
- 参考リンク
- 4件
- 更新性
- 定期更新
仕様・料金・提供範囲が変わりやすいテーマは、公開日・更新日・情報確認日を分けて管理します。 導入前には必ず記事末尾の一次情報と公式ドキュメントで最新状況を確認してください。
一次情報・参考リンク
- EDINET API仕様書(Version 2)2026年6月 金融庁 企画市場局 企業開示課 https://disclosure2dl.edinet-fsa.go.jp/guide/static/disclosure/download/ESE140206.pdf
- EDINET 利用規約 https://disclosure2dl.edinet-fsa.go.jp/guide/static/disclosure/WZEK0030.html
- MCP TypeScript SDK v2: Tools https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.html
- Claude Code Docs: Connect Claude Code to tools via MCP https://code.claude.com/docs/en/mcp