0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kSQL MCPサーバーのコード解説8:esbuildで1ファイルに固めて配布する(最終回)

0
Last updated at Posted at 2026-07-03

実装がどれほど安全でも、配布物に鍵が混ざったり依存解決が壊れたりすれば台無しです。MCP サーバーでは、とくにこの問題が表面化しやすくなります。ローカル実行、Desktop 統合、LLM クライアントからのツール発見といった文脈では、「動くコード」だけでなく、「どう配るか」がそのまま信頼境界の一部になるためです。

本稿では、build-mcp.mjsbuild-mcpb.mjspackage.json、スモークテスト群を読みながら、kSQL MCP サーバーがどう配布されるのかを追います。焦点は ksql ではなく、MCP サーバー用の ksql-mcp 側です。

シリーズ最終回ですが、ここだけでも読めるように構成します。そのうえで、第1〜7回で積み上げてきた次の3本柱が、ビルドとパッケージにどう反映されているかを接続していきます。

  • 能力の最小化
  • 検証先行
  • 多層防御

導入:配布形態は実装の外側にある信頼境界

MCP サーバーの安全性は、実装だけで閉じません。少なくとも次の3層をまとめて見る必要があります。

  • 実装:どのツールを持ち、何を許可し、何を止めるか
  • 設定:接続先、権限、秘密情報をどう注入するか
  • 配布:依存関係、起動方法、クライアントへの受け渡しをどう固定するか

このシリーズでは、read-only 既定、変更系(DML)ガード、件数上限、validate を前提にした実行フローなどを見てきました。それらはサーバー内部の安全制御です。一方で、配布物が壊れていればその制御はそもそも起動しませんし、秘密情報が配布物に混ざれば設計全体が崩れます。

つまり、配布は実装の後工程ではなく、同じ信頼境界の延長です。

本稿の軸は、src/mcp/index.ts の実装詳細を増やすことではなく、「それをどういう形で閉じ込めて外へ渡すか」を見ることです。

本論1:build-mcp.mjs は Node 18 向け CJS バンドルを作る

まず build-mcp.mjs の責務は明快です。src/mcp/index.ts をエントリに、MCP サーバーを dist-mcp/ksql-mcp.js へバンドルします。ここでは架空の簡略例ではなく、実ソースの形に沿って要点を抜き出します。

// build-mcp.mjs
import * as esbuild from "esbuild";
import {
  existsSync,
  mkdirSync,
  readFileSync,
  writeFileSync,
} from "fs";
import { resolve } from "path";

if (!existsSync("dist-mcp")) mkdirSync("dist-mcp");

await esbuild.build({
  entryPoints: [resolve("src/mcp/index.ts")],
  outfile: resolve("dist-mcp/ksql-mcp.js"),
  bundle: true,
  platform: "node",
  target: ["node18"],
  format: "cjs",
});

ここで押さえるべき点は、設定がかなり素直だということです。

  • エントリは src/mcp/index.ts
  • 出力は dist-mcp/ksql-mcp.js
  • bundle: true
  • platform: "node"
  • target: ["node18"]
  • format: "cjs"

逆に、実コードには次の設定はありません。

  • banner
  • metafile
  • sourcemap
  • minify
  • define
  • external

つまり、このビルドは esbuild の機能を盛り込んだ「多機能バンドル設定」ではなく、Node 18 / CJS 向けの単一配布物を作るための、かなり絞った設定です。ここで重要なのは設定項目の多さではなく、「配布形態をどこまで固定しているか」です。

shebang は build オプションではなく、ビルド後に付与する

現行実装で特徴的なのは shebang の入れ方です。banner ではなく、ビルド後に出力ファイルを読み直して、先頭に無ければ付与します。

const outPath = "dist-mcp/ksql-mcp.js";
const shebang = "#!/usr/bin/env node\n";
const current = readFileSync(outPath, "utf8");

if (!current.startsWith(shebang)) {
  writeFileSync(outPath, shebang + current, "utf8");
}

この実装が意味しているのは、「バンドル結果を最終配布物として整える工程」を esbuild 呼び出しの外側にも明示している、ということです。banner で入れる手もありますが、実コードは後付け方式を採っています。本稿ではその事実を主に読みます。

この方式の利点は、生成済みファイルに対して「先頭に shebang があるか」を直接確認しながら補正できることです。ビルド設定だけで完結させるより、最終成果物に対する加工として見通しがよい、という判断だと読めます。

formattarget は実コードでは固定値、設計としては見直し対象

実コードが採用しているのは format: "cjs"target: ["node18"] です。ここは説明上の仮値ではなく、現時点のリポジトリが選んでいる配布前提です。

本コードは target: ["node18"]format: "cjs" を採用しています。これは「Node 18 以上を前提に CJS で配る」という現在の判断です。将来の Node LTS 更新や依存関係の変化に応じて見直しうる値ですが、少なくとも本記事では実ソースの選定をそのまま読みます。

ビルドパイプラインは「バンドルして終わり」ではない

build-mcp.mjs 全体の流れを先に図にすると、責務の切れ目が見えやすくなります。

この図の読みどころは、esbuild 完了時点が最終成果物ではないことです。バンドル後に shebang を付け、さらに生成物自体を検査して、はじめて「配布可能な ksql-mcp.js」になります。

本論2:依存の封じ込めは、バンドル結果への正規表現ガードで固定する

このパートは現行実装の肝です。build-mcp.mjs は esbuild の構造化メタ情報を見ていません。実際にやっているのは、バンドル後の出力ファイル文字列を読んで、残ってはいけない require(...) を検出することです。

実コードの要点は次の形です。

const bundled = readFileSync(outPath, "utf8");

const forbiddenRuntimeImports = [
  /require\((["'])@modelcontextprotocol\/sdk(?:\/[^"']*)?\1\)/,
  /require\((["'])zod(?:\/[^"']*)?\1\)/,
];

if (forbiddenRuntimeImports.some((pattern) => pattern.test(bundled))) {
  throw new Error(
    "[kSQL] mcp bundle contains external MCP SDK or zod imports."
  );
}

何を検査しているのか

検査対象は2系統です。

  • require("@modelcontextprotocol/sdk...")
  • require("zod")require("zod/サブパス")

つまり、MCP SDK と Zod がバンドル後の dist-mcp/ksql-mcp.js に「実行時 require として残っていないか」を見ています。

狙いは明確です。これらは MCP サーバーの中核依存であり、単一配布物の中に取り込まれているべき依存です。もしバンドル結果に require("@modelcontextprotocol/sdk")require("zod") が残っていれば、配布先で追加の node_modules 解決を期待することになります。Desktop 統合のように配布物単体で動かしたい文脈では、これは封じ込め漏れのサインです。

なぜ MCP SDK と Zod を名指しするのか

ここで全依存を一般化して調べているわけではありません。実コードは、MCP SDK と Zod をピンポイントで検査しています。これは雑な実装ではなく、「何が外に漏れるとまず壊れるか」を名指しで固定している実装です。

  • MCP SDK が外部解決に逃げると、サーバー本体の起動に直結して破綻しやすい
  • Zod が外部解決に逃げると、ツール入出力のスキーマ検証が配布先依存になる
  • どちらも「MCP サーバーとして成立するための中核依存」で、単一配布物に残す理由が薄い

加えて、実コードは format: "cjs" で出力しているため、検査対象もその生成形に合わせた require(...) です。ここは抽象論ではなく、ビルド設定と検査方法が噛み合っています。

これは「ビルド設定に加えて、ビルド結果も検査する」多層防御

シリーズ全体の文脈に戻すと、この設計は一貫しています。

  • スキーマ検証だけでなく実行時アサートを置く
  • validate だけでなく実行フロー側にも前提を置く
  • read-only 既定に加えて変更系(DML)側にもガードを置く
  • バンドル設定に加えて、バンドル結果そのものも検査する

ここでも同じです。esbuild に bundle: true を渡しただけでは、「配布先で本当に外部依存なしに動くか」は固定しきれません。そこで、生成物を読み直して禁則に当たる require(...) が残っていないかを検査しています。

これは冗長ではなく、配布に対する実行時アサートに近い役割です。

バンドル前提の配布物では、「何を内部化し、何を外部解決に残さないか」を検査コードで固定しておくと、依存追加や設定変更で方針がぶれにくくなります。ここではその対象を MCP SDK と Zod に絞って明文化しています。

この検査のトレードオフ

実装事実として、このガードは正規表現による文字列検査です。そのため、将来 esbuild の生成形が大きく変われば、追従が必要になる可能性はあります。さらに、文字列マッチである以上、動的に組み立てた require、コメントや文字列リテラル中に現れる require("zod") 様の文字列、あるいはミニファイ等で表記が崩れたケースには、原理的に取りこぼしや誤検出の余地があります。

ただし、本論1で見た通り本コードの bundle 設定は素直で、minify も使っていません。検査対象は bundle 直後の生成物なので、現実的には表記揺れは小さく、この軽量ガードで実用上は足りる、と見積もるのが妥当です。

大事なのは、ここで実ソースにない別案を持ち込んで置き換えることではなく、このリポジトリが採っている安全網の形を正しく読むことです。

本論3:build-mcpb.mjs は Desktop 向け配布物を組み立てる

単一ファイル化だけでは、Desktop 統合に必要なメタデータと入口が足りません。そこで build-mcpb.mjs は、MCP サーバーを .mcpb として束ねます。

生成物の構成は次の5ファイルです。

  • manifest.json
  • server/index.js
  • server/ksql-mcp.js
  • README.md
  • LICENSE

この図の読みどころは、MCPB が単なる zip ではなく、「Desktop が起動する入口」と「配布物のメタデータ」をひとまとめにした単位だということです。

manifest は実値で読む

build-mcpb.mjs の manifest はプレースホルダではなく、実際の値を持っています。要点を抜き出すと次の形です。

const manifest = {
  manifest_version: "0.3",
  name: "ksql-mcp",
  display_name: "kSQL MCP",
  version: pkg.version,
  description: "Run kSQL against kintone apps through MCP.",
  server: {
    type: "node",
    entry_point: "server/index.js",
    mcp_config: {
      command: "node",
      args: [
        "${__dirname}/server/index.js",
        "--config",
        "${user_config.configPath}",
      ],
      env: {},
    },
  },
  tools: [
    { name: "ksql_validate", description: "..." },
    { name: "ksql_explain", description: "..." },
    { name: "ksql_query", description: "Execute read-only kSQL." },
    { name: "ksql_mutate", description: "Execute DML kSQL with explicit safety approvals." },
    { name: "ksql_describe_app", description: "..." },
    { name: "ksql_show_apps", description: "..." },
    { name: "ksql_save_query", description: "..." },
    { name: "ksql_list_queries", description: "..." },
    { name: "ksql_get_query", description: "..." },
    { name: "ksql_run_saved_query", description: "..." },
    { name: "ksql_delete_query", description: "..." },
  ],
  tools_generated: false,
  compatibility: {
    claude_desktop: ">=0.10.0",
    platforms: ["darwin", "win32"],
    runtimes: { node: ">=18.0.0" },
  },
  user_config: {
    configPath: {
      type: "file",
      title: "ksql.config.json",
      description: "Absolute path to your ksql.config.json file.",
      required: true,
    },
  },
};

ここで重要なのは次の4点です。

  1. manifest_version は実コードでは "0.3"
  2. tools は 11 件あり、問い合わせ系だけでなく保存 SQL 関連も含む
  3. server.entry_pointserver/index.js
  4. user_config.configPath で設定ファイルの絶対パスを外から受け取る

sensitive: false のような追加フィールドは、実コードにはありません。ここを盛って説明すると、配布設計そのものを読み違えます。加えて、ksql_queryksql_mutatedescription は、後段の Inspector 節で確認する「ツール一覧としてどう露出するか」にそのまま効く文言です。

manifest_versionuser_config の詳細スキーマは MCPB / DXT 側の仕様に依存します。本稿では、このリポジトリが現時点で採用している値を示しています。仕様の正本は公式ドキュメントを確認してください。

launcher は薄く、実体は server/ksql-mcp.js

server/index.js は launcher です。実コードの役割は薄く、./ksql-mcp.js から main を読み込んで実行する入口になっています。

const { main } = require("./ksql-mcp.js");

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

構造としては、MCPB 内の server/index.js が Desktop からの入口で、その隣に置かれた server/ksql-mcp.js がバンドル済みサーバー本体です。起動導線だけを launcher に持たせ、本体ロジックはバンドル済みファイルへ寄せる責務分離になっています。

この分け方は、配布のための入口と、実装本体の責務を混ぜないために効いています。

設定は user_config から受け取る

manifest の mcp_config.args には --config${user_config.configPath} が入っています。つまり、配布物は ksql.config.json の位置を外から受け取り、その設定で起動します。

ここで一貫しているのは、「鍵や接続情報を成果物へ焼き込まない」方針です。

  • 配布物はロジックとメタデータを持つ
  • 実環境ごとの設定は user_config で外から入れる
  • さらに必要な秘密は設定ファイル側から env: 参照へ逃がせる

これは第6回(設定と秘密情報の注入)で見た設定設計と同じ線上にあります。配布物は再配布されうるので、そこへ秘密を閉じ込めない。この原則が、実装・設定・配布で揃っています。

dist-mcpb は毎回クリーン生成し、.gitignore 未設定なら止める

build-mcpb.mjs には、運用上のガードもあります。

  • 出力前に dist-mcpbrmSync で消してから作り直す
  • .gitignore/dist-mcpb が含まれない場合は throw する

後者は任意チェックではありません。isDistMcpbIgnored()false なら、.gitignore must include /dist-mcpb before building MCPB artifacts でビルドを止めます。

これは小さな実装に見えて、配布設計としてはかなり重要です。生成物ディレクトリを誤ってコミット対象にしないことを、人手運用ではなくビルド時ガードに寄せています。ランタイムの安全制御ではありませんが、「不要なものを外へ出さない」という意味では同じ発想です。

zip 生成は自前実装だが、責務は限定している

build-mcpb.mjs は zip 生成を外部依存に頼らず、自前で組み立てています。要点は、ローカルヘッダ、セントラルディレクトリ、CRC32 を自前で扱う最小実装だということです。

ここでの判断は「配布工程のためだけに依存を増やさない」方向です。これは、MCP SDK や Zod を bundle に内包し、配布物に外部解決を残さない本記事全体の方針と同じ線上にあります。つまり、パッケージ生成のためだけに新たな依存を増やさず、依存ゼロに寄せた配布の一貫性として読めます。

ただし、その代わりに ZIP 形式の整合性責任を自分たちで持つことになります。実コードはその責任を引き受けたうえで、MCPB の生成をツールチェーン内に閉じています。

本稿の主眼は ZIP 実装の細部ではないので、全文転載はしません。重要なのは、外部依存を避ける代わりに、パッケージ形式の構築責任をビルドスクリプトへ引き取っていることです。

本論4:package.json はビルド・配布・検証の接着点

配布設計は build-mcp.mjsbuild-mcpb.mjs だけで完結しません。package.json が、実際の公開物・コマンド入口・スモークテストを束ねます。ここも実体に合わせて見る必要があります。

まず binfiles は次の通りです。

{
  "bin": {
    "ksql": "dist-cli/ksql.js",
    "ksql-mcp": "dist-mcp/ksql-mcp.js"
  },
  "files": [
    "dist-cli/",
    "dist-mcp/",
    "dist-mcpb/",
    "README.md",
    "LICENSE",
    "package.json"
  ]
}

ここで大事なのは、CLI 本体の ksqldist-cli/ksql.js を指していることです。dist/ksql.js ではありません。MCP サーバー用の ksql-mcp と、CLI 側の成果物ディレクトリが分かれています。

関連スクリプトは prepack と verify 系で束ねる

実コードの関連スクリプトは次の通りです。

{
  "scripts": {
    "build": "npm run build:plugin && npm run build:cli && npm run build:mcp && npm run build:mcpb",
    "build:mcp": "node build-mcp.mjs",
    "build:mcpb": "node build-mcpb.mjs",
    "mcp:smoke": "node scripts/mcp-smoke.mjs",
    "mcp:pack-smoke": "node scripts/mcp-pack-smoke.mjs",
    "mcp:kintone-smoke": "node scripts/mcp-kintone-smoke.mjs",
    "mcp:verify": "npm run build:mcp && npm run mcp:smoke && npm run mcp:pack-smoke",
    "mcpb:verify": "npm run build:mcp && npm run build:mcpb && node scripts/mcpb-verify.mjs",
    "mcp:inspector": "npx -y @modelcontextprotocol/inspector node dist-mcp/ksql-mcp.js",
    "prepack": "npm run build:cli && npm run build:mcp && npm run build:mcpb",
    "test": "jest"
  }
}

ここで確認すべきは、実コードに prepareprepublishOnly は無いことです。公開前のビルド束ねは prepack で行っています。したがって、「publish 前にビルドを走らせる」話を書くなら、prepack を中心に読むのが正確です。

また、mcpb:verify が呼ぶのは scripts/mcpb-verify.mjs です。verify-mcpb.mjs ではありません。こうした名前のずれは、そのまま実行手順の誤読になります。

スモークと手動確認の役割分担

配布側の確認は次のように分けられます。

スクリプト 役割
mcp:smoke バンドル済み MCP サーバーの最小起動確認
mcp:pack-smoke パッケージ化の文脈を含めた確認
mcp:kintone-smoke kintone 実連携を伴うスモーク確認
mcp:verify build:mcp + mcp:smoke + mcp:pack-smoke の合成
mcpb:verify build:mcp + build:mcpb + mcpb-verify.mjs の合成
mcp:inspector Inspector でツール可視性を手動確認

mcp:kintone-smoke という名前は唐突ではありません。kSQL 自体が kintone 連携ツールなので、これは「kintone との実連携を伴うスモーク」をそのまま表しています。

実装側テストと配布側スモークの二段構え

このリポジトリの検証は、実装テストと配布スモークを分けています。

  • src/mcp/__tests__/tools.test.ts
  • src/mcp/__tests__/savedQueries.test.ts

のような実装側テストは、ツール定義や保存 SQL の振る舞いといった内部不変条件を守る層です。一方で、mcp:smokemcpb:verify は、成果物として起動できるか、配布単位として破綻していないかを守る層です。

この分離は重要です。TypeScript と unit test だけでは、「配ったファイルが本当に立ち上がるか」は保証できません。だからこそ、配布物を対象にしたスモークが別にあります。ここでも「検証先行」と「多層防御」がそのまま出ています。

Inspector は補助線として使う

mcp:inspector も実コードにあります。

{
  "mcp:inspector": "npx -y @modelcontextprotocol/inspector node dist-mcp/ksql-mcp.js"
}

これは Inspector 自体を起動するだけではなく、対象として dist-mcp/ksql-mcp.js を明示しています。役割は自動テストの代替ではなく、ツール一覧や見え方を人間が確認する補助線です。

  • ツールが期待通り列挙されるか
  • 説明文がどう露出するか
  • 手動観察で違和感がないか

ここで見る説明文は、manifest の ksql_query にある Execute read-only kSQL. や、ksql_mutate にある Execute DML kSQL with explicit safety approvals. のような文言です。自動化できる検証と、人間が UI / 挙動として見る検証を分けている点も、このシリーズの設計姿勢と整合しています。

まとめ:実装・設定・配布を貫く一つの信頼境界

最後に、本稿で見た配布設計の要点を回収します。

  • build-mcp.mjssrc/mcp/index.tsdist-mcp/ksql-mcp.js へ Node 18 / CJS でバンドルする
  • shebang は banner ではなく、ビルド後に出力ファイルへ後付けする
  • バンドル結果を読み直し、@modelcontextprotocol/sdkzod の実行時 require(...) が残っていれば失敗させる
  • build-mcpb.mjsmanifest.json、launcher、バンドル済みサーバー、README.mdLICENSE.mcpb に束ねる
  • manifest は user_config.configPath を通じて設定を外から受け取り、秘密を成果物へ焼き込まない
  • dist-mcpb は毎回クリーン生成し、.gitignore に未登録ならビルドを止める
  • package.jsonprepack、verify 系スクリプト、Inspector 起動を通じて、配布前提を npm 側に接着する

これはシリーズを通しての3本柱と、そのままつながっています。

1. 能力の最小化

  • read-only と変更系(DML)をツール分離する
  • 配布物にも不要な依存や秘密を持ち込まない

2. 検証先行

  • validate を実行の前提にする
  • バンドル結果や MCPB 生成結果も配布前に検証する

3. 多層防御

  • 実行時アサート
  • 件数上限
  • DML ガード
  • バンドル後検査
  • スモークテスト
  • パッケージ検証

本シリーズの各回は、ツール定義、スキーマ、実行フロー、設定、保存 SQL、ビルド、配布とテーマが分かれていました。しかし通底している原理は一つです。AI に業務システムへの入口を渡すなら、実装・設定・配布を別々の都合で設計しないことです。

kSQL MCP サーバーでは、その原理が最後の配布工程まで崩れていません。単に「動くサーバーを作る」のではなく、「どう閉じ込め、どう渡し、どこで壊れる前に止めるか」を一つの信頼境界として揃えています。シリーズ全体を通して見るべきなのは、個別の小技ではなく、この境界の引き方です。

連載ナビ

これがシリーズ最終回です。読んでいただきありがとうございました。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?