本文へスキップ

EDINET APIをMCP化する設計:書類検索・取得・XBRL抽出の3ツールとキー管理・出典保持

金融庁EDINET API v2をMCP化する設計案。書類検索→取得→XBRL/CSV抽出の3ツール分割、Subscription-Keyの環境変数管理とマスク、日次一覧と書類本体で分けるキャッシュ、docID・提出日時を保持する出典スキーマを型定義とスケルトンで示します(2026-09-14、キー未取得で実行未検証)。

SHAYOUWORLD 更新 約6分

EDINET API を MCP にするとき、やってはいけないのは「1つの検索ツールが ZIP をそのままモデルに返す」設計です。 書類一覧は日付単位でしか取れず、書類本体は ZIP/PDF で、API キーは URL に載ります。この3つの制約をそれぞれ別のツールと別の層で受け止めるのが、この記事の設計です。

同じ SDK(@modelcontextprotocol/server 2.0.0)で実際に動かした例は国会会議録APIをMCPサーバー化するにあります。出典オブジェクトの形はMCPのprovenance用outputSchema設計に合わせました。

先に押さえること

  1. 3ツールに分ける。 search_documents(候補と状態)→ fetch_document(保存とハッシュ)→ extract_facts(許可項目の抽出)。
  2. キーは環境変数、URL はマスク。 Subscription-Key はクエリに載るので、そのままログに出すと漏れます。
  3. キャッシュは3種類。 当日一覧は短命、過去一覧は日次、書類本体は不変扱い。
  4. 出典は docIDsubmitDateTime 訂正は parentDocID で原本と結び、warnings に未検証や延長期間中を残します。

なぜ3ツールなのか

EDINET API 自体は「日付の一覧」と「docID の本体」の2本しかありません。それをそのまま2ツールにすると、モデルが日付を総当たりし、ZIP を受け取って途方に暮れます。API の形ではなく、エージェントが答えるまでに必要な状態遷移でツールを切ります。

ツール入力返すものAPI 呼び出し
search_documentsedinetCode または secCodedocTypeCode(既定 120)、periodEndFrom/ToincludeAmendments候補の docID・提出者・期間・提出日時・各フラグ・legalStatusparentDocIDなし(ローカル索引。索引の更新は別バッチ)
fetch_documentdocIDtype(1〜5)保存先パス、Content-Type、バイト数、SHA-256、出典書類取得 API を1回
extract_factsdocIDconcepts(許可リスト内の項目名)、consolidated項目ごとの値・単位・期間・コンテキスト、出典と変換履歴なし(保存済み ZIP を読む)

search_documents が API を直接呼ばないのがポイントです。書類一覧 API は date 必須で企業名検索がないため、日次バッチで type=2 の一覧を取り込み、docID をキーにした索引をサーバーが持ちます。当日分の追加は seqNumber の差分(前回の最後の連番より大きいもの)で拾えます。

APIをそのまま2ツールにする
状態遷移で3ツールにする
企業から有報を探す
モデルが日付を推測して一覧APIを回す
ローカル索引を edinetCode と docTypeCode で引く
ZIP の扱い
ツール結果に base64 や本文を詰める
ファイルに保存してパス・ハッシュだけ返す
数値の取り出し
モデルが CSV 全文を読む
許可した項目だけを構造化して返す
訂正・取下げ
名前が新しい1件を選びがち
parentDocID と withdrawalStatus を候補に含めて返す
API 負荷
探索のたびに一覧APIを叩く
一覧は日次バッチ、本体は docID ごとに1回
EDINET API仕様書(Version 2)の制約(日付単位の一覧、ZIP/PDF 応答、429)を前提にした比較

キーの管理:環境変数とマスク

EDINET API はキーをクエリパラメータ Subscription-Key で渡す仕様です。つまりリクエスト URL そのものが秘密情報になります。決めごとは3つです。

  1. キーは EDINET_API_KEY 環境変数からだけ読む。 ツールの入力に含めない。起動時に未設定なら stderr に出して終了する
  2. URL を出典・エラー・ログに載せる前にマスクする。 Subscription-Key=...Subscription-Key=*** に置換する関数を通す
  3. 出典 uri には閲覧サイトの書類 URL か API のパス部分だけを入れる。 キー付きの完全 URL は provenance に入れない

Claude Code への登録は、公式ドキュメントの --env オプションか .mcp.jsonenv で渡します。

Terminal window
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 に写します。

60秒
当日分の一覧
8:30過ぎから原則1分毎に更新される
翌日24時過ぎまで
過去分の一覧
日次更新処理で全ファイル日付が差し替わる
不変
書類本体(docID + type)
同じ docID の内容は変わらない。訂正は別の docID
10年
保持の上限
閲覧期間満了で legalStatus=0、取得不可
EDINET API仕様書(Version 2)3-1-3、1-2-2、3-1-4 の記載を TTL に対応させた設計値

書類本体を不変扱いにできるのは、訂正報告書が親書類から独立した docID で採番されるためです。ただし、過去分の一覧側は「閲覧期間満了で docID 以外が null になる」「取下書の提出で取下区分が設定される」という更新を受けるので、索引の日次更新を止めると取得不可の書類を候補に出し続けることになります。

出典スキーマ:docID と提出日時を必須にする

出典は data とは別の provenance に置き、sources[]docIDid として持たせます。EDINET 固有の時点情報が3つあるので、混ぜずに別フィールドにします。

フィールド中身由来
submitDateTime提出日時書類一覧 API の submitDateTime
periodStart / periodEnd対象期間(有報なら事業年度)periodStart / periodEnd
retrievedAtサーバーが取得した時刻サーバー側で生成
contentHash保存した ZIP/PDF の SHA-256fetch_document で計算
parentDocID訂正前の書類の docIDparentDocID(設定されている場合のみ)

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_documentdata に本文フィールドがない。 返すのはパス・サイズ・ハッシュだけです。モデルが「中身を見せて」と頼んでも、このツールは応えません
  • search_documentswithdrawalStatusdisclosureStatuslegalStatus を隠さず返す。 取り下げ済みや不開示中を候補から黙って消すと、「見つからない」と「存在しない」の区別がつかなくなります。候補には残し、warnings で状態を明示します
  • extract_factsconcepts を許可リストに限定する。 任意の項目名を受け付けると、モデルが存在しない項目を作って問い合わせ、空振りを埋めようとします。notFound を別に返すのもそのためです

訂正・取下げ・満了の扱い

書類の状態は search_documents の結果に含めて、判断はエージェントと人間に残します。

状態一覧上の値ツールの振る舞い
訂正報告書がある訂正側の parentDocID に原本の docID原本と訂正を両方候補に出し、warnings に「訂正あり」を載せる
取り下げられたwithdrawalStatus=2候補に残すが fetch_document は拒否、warnings に明示
不開示中disclosureStatus=2同上
延長期間中legalStatus=2取得は可。仕様書の「法定縦覧期間内と同様には訂正されないことがある」を warnings に載せる
閲覧期間満了legalStatus=0docID 以外 null取得不可として返す

これは法案から現行法までの調査フローで「法案名の類似で現行法に飛ばない」と書いたのと同じ発想で、書類名の新しさで最新版を選ばせないためのものです。

実装に進む前のチェックリスト

  • EDINET のアカウントと API キーを取得し、キーなし・無効キーの401応答と、正しいキーでの type=1 応答を実測した
  • EDINET_API_KEY を環境変数からだけ読み、入出力・ログ・出典に載らないことをテストした
  • 書類一覧の日次取り込みと、seqNumber 差分による当日分の追加を実装した
  • Content-Type で ZIP/PDF と JSON エラーを判定し、429 は再試行せず返す
  • parentDocIDwithdrawalStatusdisclosureStatuslegalStatus を候補に含めた
  • CSV のレイアウトを書類閲覧操作ガイドで確認し、extract_facts の許可リストを作った
  • outputSchemaprovenance.sources[].submitDateTimeperiodEnd を必須として入れた
  • 生の JSON-RPC で tools/callstructuredContent を検証した(stdio 最小ハーネス

まとめ

  • EDINET API の「日付単位の一覧」「ZIP/PDF の本体」「URL に載るキー」を、search_documentsfetch_documentextract_facts の3ツールと索引・保存・抽出の3層で受け止める
  • キーは EDINET_API_KEY 環境変数からだけ読み、URL はマスクしてから記録する。出典 uri にキー付き URL を入れない
  • キャッシュは当日一覧60秒、過去一覧は日次、書類本体は不変。閲覧期間満了は取得不可として返す
  • 出典は docIDsubmitDateTimeperiodEndretrievedAtcontentHash を分けて持ち、訂正は parentDocID で結ぶ
  • コードは骨組みのみで実行未検証。キー取得後に実測してから更新する

MCP の価値は API の薄いラッパーではなく、モデルに渡す前に「どの書類の、いつ時点の、どの項目か」を確定させる層にあります。EDINET のように一覧と本体が離れている API ほど、その層の設計が効きます。

関連して読む

この記事の情報・検証メモ
公開日
情報確認
参考リンク
4件
更新性
定期更新
更新管理

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

図解を保存・共有

記事の要点を1枚にまとめました。画像は新しいタブで開いて保存できます。

EDINET APIをMCP化する設計:書類検索・取得・XBRL抽出の3ツールとキー管理・出典保持 検索・取得・抽出を分け、キーは環境変数に隔離し、docIDと提出日時を出典として必ず返す 3ツールに分ける:search_documents: ローカル索引から候補と状態を返す。fetch_document: docIDとtypeでZIP/PDFを保存し、ハッシュを返す。extract_facts: 許可した項目だけをCSV/XBRLから取り出す。 キーとキャッシュ:EDINET_API_KEY を環境変数で受け、URLはログ前にマスク。当日一覧は短命、過去一覧は日次、書類本体は不変扱い。閲覧期間満了は取得不可として返す。 出典を落とさない:docID・submitDateTime・periodEnd・取得日時を必須化。訂正は parentDocID で原本と結ぶ。warnings に未検証・延長期間中を明示。
EDINET APIをMCP化する設計:書類検索・取得・XBRL抽出の3ツールとキー管理・出典保持 記事の要約 2026.09.14 設計・ワークフロー
Primary sources

一次情報・参考リンク

About the author
SHAYOUWORLD

日本の公共データAPIを使うMCPサーバーを作って公開し、ローカルLLMを自分のGPUで測った記録を、一次情報・検証ログ・失敗例とあわせて整理します。