blastengine MCPサーバーは、blastengine のメール配信APIの一部を MCP(Model Context Protocol)ツールとして公開する公式の stdio サーバーです。配信検索・ログ取得・使用量確認といった参照系から、トランザクション送信・一斉配信までを、Claude Code や Claude Desktop などの MCP 対応クライアントから自然言語で扱えます。
ただし、公開されているのは blastengine API の一部の操作です。実運用では「配信詳細とそのメールログをまとめて1回で取りたい」「自社の運用に合わせた集計を返してほしい」といった、標準ツールにはない操作がほしくなることがあります。
本記事では、この blastengine MCPサーバーのソースを読み解き、自分用の独自ツールを1つ追加する手順を通しで解説します。題材として、配信詳細と宛先ごとのメールログをまとめて返す参照系ツール blastengine_delivery_report を実装します。
前提:これは「自分のフォーク」を拡張する話です
先に運用面の前提を共有します。blastengine MCPサーバーのリポジトリは、CONTRIBUTING に明記されているとおり外部からの Pull Request を受け付けていません(要望・不具合は GitHub Issue へ、という運用です)。
したがって本記事の「独自ツール追加」は、本家に取り込んでもらう話ではなく、MIT ライセンスの下で自分のフォーク/ローカルビルドを拡張して使う話になります。本体のバージョンが上がったら、自分の変更を追従(リベース)する運用になる点だけ、あらかじめ押さえておいてください。汎用的に役立ちそうな機能なら、自分のフォークで抱え込むより、Issue で提案するか、下のアンケートフォームから送ると開発チームに届くみたいなので、そちらに投げてしまうのが早いかもしれません。
https://docs.google.com/forms/d/e/1FAIpQLSdNZ9TswUT3JEv3pMHkJGqOvgmsXuKP1smkJiNEsqbTVDwRyg/viewform
対象読者は、TypeScript が読め、MCP サーバーの中身を触ってみたい方です。
まずアーキテクチャを掴む
src/ の構成はシンプルで、責務がきれいに分かれています。ツールを実行時に動かすだけなら、触るのは schemas / operations / server の3ファイルです。ただし、リポジトリのテスト(npm test)を通し、MCPB パッケージを正しく作るところまで含めると、ルートの manifest.json も更新が必要です(後述)。合わせて次の4ファイルを触ります。
src/schemas.tssrc/operations.tssrc/server.tsmanifest.json
| ファイル | 役割 |
|---|---|
src/index.ts |
stdio のエントリポイント。loadConfig → createBlastengineMcpServer → StdioServerTransport で接続するだけ |
src/config.ts |
環境変数を Config に変換。認証情報の読み込みと Bearer トークン生成(login_id + api_key の SHA-256 を hex 化して Base64)、送信系の有効化フラグを持つ |
src/blastengine-client.ts |
HTTP クライアント。request / multipart / download の3メソッド。認証ヘッダ付与・タイムアウト・エラーの構造化を一手に引き受ける |
src/schemas.ts |
zod による入力スキーマ。ツールごとに「shape(オブジェクトの形)」と「schema(z.object(shape))」をペアで export する |
src/operations.ts |
ツールの実処理。OperationContext(config と client)を受け取り、client を叩いて結果を返す |
src/server.ts |
ツールの登録。McpServer.registerTool を薄くラップした registerTool ヘルパーで、名前・説明・アノテーション・スキーマ・ハンドラを紐付ける |
src/errors.ts |
ToolError(コード付き例外)と、例外を構造化レスポンスに変換する errorToStructured
|
ツール1つは、次の3点セットで構成されています。この対応関係さえ掴めば、追加は機械的な作業です。
- 入力スキーマ(
schemas.ts)… 何を受け取るか - オペレーション関数(
operations.ts)… 何をするか - 登録(
server.ts)… どんな名前・説明・安全属性で公開するか
登録まわりの仕組み(server.ts)
server.ts には registerTool というヘルパーがあり、内部で MCP SDK の server.registerTool を呼びつつ、ハンドラの戻り値を toToolResult で包んでいます。ここがこのサーバーの肝で、オペレーション関数は「素の JavaScript オブジェクトを返す」か「ToolError を投げる」だけでよく、MCP のレスポンス整形(content テキスト化・structuredContent・isError)は共通処理が面倒を見てくれます。
// src/server.ts(抜粋)
async function toToolResult(run: () => Promise<unknown>): Promise<CallToolResult> {
try {
const data = await run();
return {
content: [{ type: "text", text: JSON.stringify(data ?? {}, null, 2) }],
structuredContent: toStructured(data)
};
} catch (error) {
const structured = errorToStructured(error); // ToolError の code/message/retryable を構造化
return {
isError: true,
content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
structuredContent: structured
};
}
}
つまり、成功時は返したオブジェクトがそのまま JSON になり、失敗時は throw new ToolError("code", "message") すれば整った構造化エラーになります。この規約に乗るのが、独自ツール実装のいちばんの近道です。
作るもの:blastengine_delivery_report
標準ツールには、配信の詳細を取る blastengine_delivery_get(GET /deliveries/{id})と、メールログを検索する blastengine_mail_results_list(GET /logs/mails/results)が別々にあります。運用では「ある配信について、サマリと宛先ごとの結果をまとめて1回で見たい」ことが多いので、この2つを内部で合成する参照系(読み取り専用)ツールを作ります。
新しい API エンドポイントを叩くわけではなく、既存クライアントの request を2回呼んで結果を束ねるだけなので、副作用がなく安全に検証できます。まずはこの「合成型」で全体の流れを体験するのがおすすめです。
手順
0. フォークして作業ブランチを切る
git clone https://github.com/<your-account>/blastengine-mcp.git
cd blastengine-mcp
git switch -c feat/delivery-report
npm install
Node.js は 22.15 以上が前提です(package.json の engines で >=22.15.0)。
1. 入力スキーマを追加する(src/schemas.ts)
schemas.ts の末尾(export type ...Input 群のあたり)に、次を追記します。ファイル内にはすでに const id = z.number().int().positive(); という部品が定義されているので、これを再利用します。
// src/schemas.ts に追記
export const deliveryReportShape = {
delivery_id: id,
results_count: z.number().int().min(1).max(1000).default(100)
};
export const deliveryReportSchema = z.object(deliveryReportShape);
export type DeliveryReportInput = z.infer<typeof deliveryReportSchema>;
ポイントは、他ツールと同じく shape と schema をペアで export することです。server.ts の registerTool は、shape をそのまま inputSchema(MCP のツール定義に載る JSON Schema の素)として使い、schema を実行時の schema.parse(args) に使う――という二段構えになっているためです。results_count は .default(100) を付けておくと、AI が省略しても既定値で動きます。
2. オペレーション関数を追加する(src/operations.ts)
operations.ts に処理本体を追記します。DeliveryDetail 型はこのファイルで既に import 済みなので、そのまま使えます。DeliveryReportInput の import 行だけ足してください。
// src/operations.ts の import に追加
import type { DeliveryReportInput } from "./schemas.js";
// 関数本体を追記
export async function deliveryReport(
input: DeliveryReportInput,
ctx: OperationContext
): Promise<unknown> {
// 1) 配信の詳細(件名・ステータス・宛先数など)
const detail = await ctx.client.request<DeliveryDetail>({
method: "GET",
path: `/deliveries/${input.delivery_id}`,
toolName: "blastengine_delivery_report"
});
// 2) この配信のメールログ(宛先ごとの結果)
const results = await ctx.client.request({
method: "GET",
path: "/logs/mails/results",
toolName: "blastengine_delivery_report",
query: {
delivery_id: input.delivery_id,
count: input.results_count
}
});
// 3) 束ねて返す(あとは共通処理が JSON 化してくれる)
return {
delivery_id: input.delivery_id,
subject: detail.subject,
status: detail.status,
delivery_type: detail.delivery_type,
total_count: detail.total_count,
results
};
}
ctx.client.request に渡す toolName は、X-Blastengine-MCP-Tool ヘッダに載って blastengine 側に届きます(付与されるのは既定の BLASTENGINE_CLIENT_HEADERS=true のときで、false にすると X-Blastengine-* ヘッダは付きません)。独自ツールでも自分のツール名を渡しておくと、利用状況の識別が正しくなります。query に渡した値のうち null や空文字はクライアント側の buildUrl が自動でスキップしてくれるので、未指定の扱いを自前で書く必要はありません。
3. ツールを登録する(src/server.ts)
最後に server.ts で配線します。import を2箇所足し、createBlastengineMcpServer の中(他の registerTool(...) が並んでいる場所)に登録を1つ追加します。
// operations の import に追加
import { deliveryReport } from "./operations.js";
// schemas の import に追加
import { deliveryReportShape, deliveryReportSchema } from "./schemas.js";
// createBlastengineMcpServer 内、他の registerTool(...) の並びに追加
registerTool(
server,
"blastengine_delivery_report",
"Summarize a single delivery: fetch delivery detail and its per-recipient mail results in one call. Read-only.",
{ title: "配信レポート取得", readOnlyHint: true },
deliveryReportShape,
deliveryReportSchema,
(input) => deliveryReport(input, ctx)
);
registerTool の引数は「名前・英語の説明・アノテーション・shape・schema・ハンドラ」の順です。ここで渡す description は AI がツールを選ぶ判断材料になるので、英語で簡潔に「何をするか」「読み取り専用か」を書きます。readOnlyHint: true は副作用がないことを示すアノテーションで、クライアントによっては許可プロンプトの表示や自動実行の扱いに影響します。
4. manifest.json のツール一覧を同期する
見落としやすいのがここです。ルートの manifest.json には、MCPB(Claude Desktop 向けパッケージ)用のツール一覧 tools[] が定義されています。実行時のツール登録は server.ts だけで完結するため、manifest.json を更新しなくても Claude Code や MCP Inspector では動きます。しかし、このリポジトリの stdio スモークテスト(tests/verify-stdio-output.mjs)は、サーバーが実際に返すツール名の一覧と manifest.json の tools[] が完全一致するかを検証しています。ここがズレていると npm test が落ちます(TypeScript ビルドと単体テストは通っても、スモークテストで失敗します)。
manifest.json の tools 配列に、追加したツールを1件足します。
{
"name": "blastengine_delivery_report",
"description": "配信詳細と宛先ごとのメールログをまとめて取得します"
}
MCPB を作らず、自分のローカルビルドを Claude Code から使うだけなら必須ではありませんが、テストを緑に保つ・配布物を正しくするために同期しておくのが無難です。
5. ビルドして MCP Inspector で確認する
npm run build
npx @modelcontextprotocol/inspector node dist/index.js
MCP Inspector の Tools 一覧に blastengine_delivery_report が現れ、delivery_id を渡すと配信サマリ+ログがまとめて返れば成功です。参照系なので BLASTENGINE_ENABLE_* フラグは不要で、認証情報(BLASTENGINE_LOGIN_ID / BLASTENGINE_API_KEY)さえあれば動きます。
その後、Claude Code や Claude Desktop の設定が指す dist/index.js を、いま npm run build で更新したものに向けておけば(同じパスなら再ビルドだけでOK)、クライアントを再起動して独自ツールが使えます。
6. テストを足す(任意だが推奨)
このリポジトリは vitest で npm test(npm run build && vitest run + stdio スモークテスト)が回るようになっています。BlastengineClient はコンストラクタで fetch 実装(FetchLike)を差し替えられるので、テストではダミーの fetch を注入して、期待どおりのパスにリクエストが飛び、結果が束ねられるかを検証できます。既存の tests/operations.test.ts が手本になります。
書き込み系ツールを足すときの作法
今回は参照系でしたが、送信・更新を伴うツールを足す場合は、このサーバーの安全設計に必ず合わせてください。標準の送信系ツールは、既定で無効・明示フラグでのみ有効化、という設計になっています。独自ツールでこれを迂回してはいけません。
- 冒頭で
requireEnabled(...)を呼び、対応するBLASTENGINE_ENABLE_*がtrueのときだけ実行する(未有効ならsend_disabledのようなToolErrorを投げる) - 登録時のアノテーションに
destructiveHint: trueを付ける - 誤送信リスクのある挙動は、戻り値に
warningを含めて明示する(標準の一斉配信ツールがそうしています)
// 書き込み系オペレーションの冒頭で有効化を要求する例
export async function myWriteTool(input: MyInput, ctx: OperationContext): Promise<unknown> {
requireEnabled(
ctx.config.enableSend,
"send_disabled",
"BLASTENGINE_ENABLE_SEND=true is required for this tool"
);
// ...ctx.client.request({ method: "POST", ... })
}
エラーは throw new ToolError("code", "message", { status, details, retryable }) の形で投げれば、toToolResult が構造化して返してくれます。独自の失敗理由には、既存と重複しない一意なコードを付けておくと、クライアント側でのハンドリングが楽になります。
つまずきやすいポイント
- shape と schema の二重管理:
inputSchemaには shape(z.objectで包む前のオブジェクト)を、schema.parseには schema を渡す、という使い分けです。片方だけ足すと型か実行時のどちらかで落ちます。 - import 拡張子は
.js:ソースは.tsですが、ESM 出力に合わせて相対 import は./schemas.jsのように.jsで書きます(既存コードも全部そうなっています)。 -
distを指しているか:クライアントが読むのはビルド後のdist/index.jsです。srcを直接指していないか、再ビルドを忘れていないかを確認します。 - 本体追従:フォーク運用なので、本体のバージョンが上がったら自分の変更(
schemas/operations/server+manifest.json)を追従させます。差分を小さく・局所的に保つと楽になります。
まとめ
blastengine MCPサーバーへの独自ツール追加は、「schemas に入力を定義」→「operations に処理を書く」→「server で登録」の3ステップが核で、テストと配布まで含めるなら manifest.json の同期を加えた4手です。HTTP・認証・タイムアウト・エラー整形・MCP レスポンス整形は共通レイヤーが引き受けてくれるので、自分が書くのは「入力の形」と「クライアントを叩いて結果を束ねる数行」だけです。
まずは今回のような副作用のない参照系・合成系から始め、感触を掴んでから、必要に応じて安全設計(有効化フラグ+destructiveHint)に沿った書き込み系へ広げていくのがおすすめです。実装したらMCP Inspectorで検証してから、クライアントに繋ぎましょう。
参考:blastengine MCPサーバーの全15ツールをMCP Inspectorで検証する
※本記事は執筆時点の blastengine MCPサーバー(MIT ライセンス)のソース構成に基づきます。本体は公式サポートの対象外で、仕様はバージョンにより変わり得ます。最新の構成は各自リポジトリでご確認ください。