MODEL CONTEXT PROTOCOL — PORTFOLIO
MCP Portfolio
自作 MCP サーバー 8本 — 設計と実装の記録
非エンジニアのビジネス職が
自分の業務の不便を
自分で MCP サーバーを書いて解いた記録
石塚 一樹 / Kazuki Ishizuka
QuackShift インサイドセールス(ビジネス職)
2026
Overview
これは何か
私はエンジニアではなく/AIスタートアップのインサイドセールス(営業)として展示会の運営や追客を担当している
日々の業務を Claude Code(AIエージェント)に任せる中で/既製のコネクタでは届かない不便がいくつも出てきた
そのたびに「ならば自分で MCP サーバーを書く」という形で解いてきた成果物が この8本
MCP(Model Context Protocol)= AIエージェントに外部のツールやデータを安全につなぐためのプロトコル
各サーバーは stdio 経由で Claude Code 本体と通信し/エージェントから呼べる「道具」を公開する
Design Principles
8本に共通する設計思想
- 完全ローカル / API コストゼロを優先埋め込みも索引も外部に出さない(qs-rag)/ クラウドDBを挟まない
- 依存は極小中核は MCP SDK + zod +
node:sqlite のみ / 外部DBドライバを入れない
- 秘密はコードにもリポジトリにも置かないトークンは環境変数か リポジトリ外のファイル / 鍵をコミットしない
- 認証基盤は1つを横に使い回す1つの OAuth クライアントを Gmail / Docs / Drive / Slides / Forms の5サーバーで共有 / トークンDB だけスコープごとに分け 互いの認証を壊さない
- 副作用を明示分離検索・読取は安全側 / 送信・更新は別ツールに切り出し誤爆を防ぐ
- Progressive Disclosureまず要約を返し 必要なら詳細を引く / エージェントが文脈を横に広げられる
- 取り返しのつかない操作は道具にしない削除系ツールは作らない / 変更系は dryRun 既定にし 消える中身を見せてから実行する(qs-drive・qs-docs)
- 正直さを設計に埋める推定を確定として返さない / 未確認を未確認と書いて返す(qs-firm)
Lineup
8本の一覧
| サーバー | 役割 | 状態 | 規模 | 外部API / 基盤 |
| qs-rag | ローカル知識ベースの意味検索(grep の代替) | 日次運用 | 753行 | Ollama(ローカル埋め込み) |
| qs-gmail | 複数 Gmail を識別子で一元操作 | 日次運用 | 673行 | Gmail API |
| qs-slides | ブランド適用済み Google Slides を自動生成 | 日次運用 | 625行 | Slides / Drive API |
| qs-crm-gas | スプレッドシート CRM を安全に読み書き | 日次運用 | 570行 | Apps Script Web App |
| qs-docs | Google Docs 本文の構造的な読み書き | 日次運用 | 542行 | Docs / Drive API |
| qs-forms | フォームの構造と回答を読む(read only) | 運用中 | 308行 | Forms / Drive API |
| qs-drive | Drive のファイル操作(探す・作る・移す・上げる) | 日次運用 | 192行 | Drive API |
| qs-firm | 会社名 → 売上 → 企業ランク自動判定 | 実装完了 / 投入準備中 | 152行 | なし(Web調査はホスト側) |
規模 = index.js + src/(+ bin / hooks / authorize / GAS)の実装行数 / node_modules・生成物・lock・使い捨てスクリプトは除外 / 合計 3,815行
qs-rag
ローカル知識ベースの意味検索サーバー
日次運用
課題
数百件の Markdown メモ・同期ドキュメント・過去セッションログを エージェントに参照させたい
だが grep はキーワード一致しか拾えず「あれ何だっけ」系の曖昧な問いに弱い / かといってクラウドのベクトルDBはコストと秘匿性が問題
→ 埋め込み・ストア・検索を全部ローカルに閉じた RAG を MCP として実装した
Design Decisions
設計上の判断(なぜ そうしたか)
- 埋め込み・ストアを完全ローカルに閉じた埋め込み = Ollama の
snowflake-arctic-embed2(1024次元・多言語)/ 外部送信ゼロ・API コストゼロ・秘匿性を担保
- ネイティブビルド不要のベクトルストア
node:sqlite にベクトルを置き メモリ上のブルートフォース cos 類似で全件スキャン / 数千チャンク規模ではこれで十分速く 近似索引(HNSW等)の依存を増やさない
- ベクトル + 全文検索のハイブリッド(RRF 融合)SQLite FTS5 の trigram tokenizer で日本語キーワード検索を併用し Reciprocal Rank Fusion(K=60)で融合 / FTS 失敗時は純ベクトルにフォールバック
- 2段階の差分インデックス
mtime で粗くふるい 変化したものだけ sha256 で確定判定 / 内容が同じなら埋め込み再計算をスキップし Ollama 推論コストを抑える
- 索引を3ドメインに分けたknowledge(自分で書いたメモ)/ context(Slack・Notion から同期した業務ログ)/ cc-log(過去の作業セッション)を分離し 検索時に指定・除外できる / 「決めたこと」と「その時の会話」を混ぜて引かないため
- 索引の中身を目で見られるようにした1024次元ベクトルを UMAP で2次元に落とし 単体 HTML の意味マップとして出力(ドメイン別に色分け)/ どの知識が孤立しているか・重複しているかを目視で点検する
- Progressive Disclosure + 文書要約チャンク見出し単位でチャンク化し 先頭に「タイトル+見出しツリー+要約」の
ord=0 チャンクを必ず足す / まず search_knowledge で当て get_detail で隣接チャンクを開く
Tools
公開している MCP ツール
| ツール | 引数 | 戻り値 |
| search_knowledge | query / limit? / exclude? / domain? | domain・見出し・score・id・抜粋の一覧 |
| get_detail | id | チャンク本文 + 同一ファイル内の隣接チャンク一覧 |
| reindex_now | — | 差分 reindex の結果(scanned / changed / total) |
+ 常駐運用 Claude Code の UserPromptSubmit フックとして毎ターン自動検索しコンテキストに注入 / Ollama 不通やタイムアウト(2.5s)でも無言終了し エージェントを絶対にブロックしない設計 / セッション終了フックで差分 reindex を走らせ 次の起動時には最新の索引になっている
スタック Node.js (ESM) / @modelcontextprotocol/sdk / zod / node:sqlite / Ollama / SQLite FTS5
実装規模 753行
外部npm依存 2個のみ(sdk / zod)
qs-gmail
複数 Gmail アカウントを 1サーバーで識別子管理
日次運用
課題
既製の Gmail コネクタは基本「1ユーザー = 1アカウント」前提
業務では用途別の共有メールボックス(展示会用 exhibition@ など)を複数扱いたいが それを1つのエージェントから区別して操作する手段がなかった
→ 複数アカウントを 1DB・1サーバーで識別子管理する Gmail MCP を実装した
Design Decisions
設計上の判断(なぜ そうしたか)
- アカウントを識別子で抽象化
node:sqlite の tokens(account PRIMARY KEY, email, tokens_json, ...) に集約 / 全ツールの第一引数が account 文字列で authedClient(account) に統一
- loopback OAuth を別CLIに切り出しstdio サーバー内ではブラウザ認証を扱えないため 初期登録だけ
authorize.js に分離 / 127.0.0.1 にローカルサーバーを立て承認コードを受け取る / access_type=offline + prompt=consent で必ず refresh_token を確保
- トークンの自動再永続化googleapis の
client.on('tokens') でリフレッシュを捕捉し即DB上書き / 旧値とマージするため refresh_token が返らない更新でも鍵が消えない
- 認証情報は env 優先 → ファイル → アカウント別マップ
QS_GMAIL_CLIENT_ID/SECRET → リポジトリ外の credentials JSON の順 / アカウント別マップで Internal と External の OAuth を 1サーバーに混在できる
- 副作用ツールを分離
create_draft(送信なし)と send_message(即送信)をツールレベルで分け 送信系は説明文で実行前確認を明示
- 添付・インライン画像を multipart の入れ子で組む添付は
multipart/mixed / 本文に埋める画像は cid: 参照の multipart/related / 両方あるときは mixed[ related[ HTML, 画像 ], 添付 ] の入れ子を自前で構築 / 拡張子から MIME を引く辞書を持ち 日本語ファイル名は RFC5987 + RFC2047 で符号化する
Tools
公開している MCP ツール
- list_accounts — 登録アカウント一覧
- search — 検索(query/差出人ドメイン/期間/未読のみ)
- read_message — 本文取得(MIME 再帰探索)
- list_labels — ラベル一覧
- archive — アーカイブ
- mark_read — 既読 / 未読
- create_draft — 下書き作成(返信連結対応)
- send_message — 送信(副作用・要確認)
- list_drafts — 下書き一覧
- delete_draft — 下書き削除
- remove_account — トークン削除
地味だが効く実装 外部メールライブラリを使わず RFC822 を手組み / 件名は RFC2047 Base64・本文は base64url に自前変換 / In-Reply-To と References を付与して Gmail のスレッドに正しく連結
スタック Node.js (ESM) / @modelcontextprotocol/sdk / googleapis / zod / node:sqlite
実装規模 673行
qs-slides
ブランド適用済み Google Slides を自動生成
日次運用
課題
提案資料や振り返りスライドを エージェントから直接 作りたい
PowerPoint 変換を挟むと Drive 上で即共同編集できない / Slides API は座標を EMU で手指定する必要があり 生では扱いにくい
→ スライド「型」を宣言するだけで ブランド適用済みのネイティブ Slides が Drive 直下に出る MCP を実装した
Design Decisions
設計上の判断(なぜ そうしたか)
- スライド型の抽象化レイヤーtitle / section / bullets / two_col / stat / image / closing / agenda の型 → ビルダー関数の辞書を持ち 呼び出し側は型を知らなくてよい / 未知の型は bullets にフォールバック
- 全レイアウトを EMU 座標の算数で手組みSlides API はテンプレ変数を持たないため ページ幅・マージンを定数化し全要素を計算配置 / マスタに依存せず再現性を担保しマジックナンバーをゼロに
- batchUpdate を 400 リクエスト単位で分割投げ1スライドが 10〜20 リクエストになるため 大量スライドで API 上限に当たらないよう
batchInChunks でループ
- オブジェクトID 衝突をネーミング規則で防ぐページカウンタ + 要素内連番で
e{n}_{kind}_{i} を生成 / プレゼン内グローバル一意の制約を静的に満たす
- 認証は qs-gmail の資産を流用同じ GCP クライアント JSON を環境変数チェーンで読み スコープだけ presentations + drive に差し替え / サービスアカウント不要で運用コストを増やさない
Tools
公開している MCP ツール
- create_deck — spec(型の配列)からデッキ生成
- get_presentation — 構造サマリ取得
- get_thumbnails — PNG をローカル保存(視覚QA)
- export_pdf — PDF 書き出し
- raw_batch_update — 生 API リクエストの直投げ
- list_accounts — アカウント一覧
地味だが効く実装 raw_batch_update を逃げ道として並置し 宣言型で届かない細工も同じ分割ロジックを通す(二重実装なし)/ get_thumbnails は生成直後の視覚確認を 1コールで完結
スタック Node.js (ESM) / @modelcontextprotocol/sdk / googleapis (slides v1 / drive v3) / zod / node:sqlite
実装規模 625行(うち約45%が型→APIリクエスト変換)
qs-crm-gas
スプレッドシート CRM を安全に読み書き
日次運用
課題
CRM は Google スプレッドシート上にある / エージェントから Sheets API を直叩きすると認証も権限管理も重い
一方「任意コードを実行できる GAS Web App」はスプレッドシートに対して危険すぎる
→ 呼べる関数を許可リストに限定し 共有トークンで認証する GAS Web App を立て その薄い RPC クライアントを MCP 化した
Design Decisions
設計上の判断(なぜ そうしたか)— セキュリティが主題
- 許可リスト関数のみの RPCGAS 側
ALLOWED_FNS(現在20関数)に無い関数名はブロック / dispatch は named function 呼び出しだけで 任意コードの実行経路を持たない / 攻撃面を「20の特定操作」まで圧縮
- 集計だけは式を渡す — ただし箱に入れて渡す集計のたびに GAS を書き直さないよう 行配列を受け取って答えを返す関数本体を文字列で渡せる読み取り口(
read / readSheet)を用意 / ただし SpreadsheetApp DriveApp GmailApp UrlFetchApp など16のグローバルを未定義の仮引数で遮蔽し strict モードで実行 / constructor __proto__ eval 等を含む式は実行前に静的拒否 / 読み取り専用の便利さと 書き込み経路の遮断を両立させた
- 共有トークン認証・秘密はコードに埋めないGAS の Script Property と MCP 側の環境変数を突き合わせ / Web App は匿名公開にした上で このトークン検証を唯一のゲートに / 設定漏れは unauthorized と区別してエラー分類
- 数式列を尊重した書き込み
updateCells はヘッダ名で列を特定し 渡された fields のセルだけ更新 / 追客ランク等の自動計算列は引数に含めない限り絶対に触らない
- 第1引数 = spreadsheetId で複数CRM を使い回す全公開関数の先頭引数が
spreadsheetId / 展示会ごとに作る寿命つきシートを 1サーバー・1デプロイで共用
- GET の URL 長制限に POST を併設CSV 流し込みのような大きなペイロードは GET のクエリ長で落ちるため
doPost を追加 / トークン認証と許可リストの検証は doGet と同じ経路を共有し 抜け道を作らない
- タブ・列の操作は冪等に倒すタブ作成は既にあれば作らない / 列挿入はヘッダ名指定なら同名列がある限り挿入しない / 同じ手順を2回流しても壊れないので エージェントのリトライを安全にする
- ヘッダ名の動的解決で列追加に強い
resolveCols_ が表記揺れを indexOf で吸収 / CRM のヘッダを変えてもコード修正不要
Tools
公開している MCP ツール
ツールは gas_call_function(fn, args) の 1本のみ / 呼べるのは許可リストの関数だけ(先頭引数は常に spreadsheetId)
- getByRankMemo — ランク・モードで行を絞る
- getStatusBreakdown — ランク×ステータス件数
- updateCells — ヘッダ名指定で列更新
- setConditionalFormat — 条件付き書式(冪等)
- getFormulas / setFormulas — 数式の読み書き
- setFormats — 背景色・太字・罫線
- clearCache — キャッシュ破棄
- read / readSheet — 式を渡して集計(サンドボックス実行)
- ほか 計20関数
スタック MCP側: Node.js (ESM) / sdk / zod / fetch ・ GAS側: Apps Script (V8) / SpreadsheetApp / PropertiesService
実装規模 570行(MCP 39 + GAS 531)
qs-docs
Google Docs 本文の構造的な読み書き
日次運用
課題
既製の Drive コネクタは 読み取りとメタデータ更新まで / 本文そのものを直す経路が無い
かといって Docs API の編集は「文書先頭からの文字位置(index)」で範囲を指定する方式で 人にもエージェントにも生では扱えない / しかも1回編集するたび後続の index がズレる
→ 構造を index 付きで返し 名前で操作する(この見出しの節を消す 等)層をかぶせた MCP を実装した
Design Decisions
設計上の判断(なぜ そうしたか)
- index の脆さを名前付きツールで隠したまず
get_structure / get_outline がブロック(見出し・段落・表)ごとの startIndex / endIndex を返し 実際の操作は「見出し名」「列番号」で指定する / 呼び出し側が数値を組み立てなくてよい
- 破壊的操作は必ず dryRun で実テキストを返す
delete_section / delete_ranges / delete_table_column は全て dryRun を持ち 消える範囲の本文をそのまま返して実行はしない / 「何が消えるか」を見てから承認する運用に固定した
- 複数レンジ削除は降順で1バッチに送る前から消すと後続の index がズレるため
startIndex 降順にソートしてから batchUpdate に積む / 末尾は docEnd - 1 にクランプ(Docs は最終セグメントの改行を削除できない仕様)
- 認証は qs-gmail の資産を流用しトークンだけ分離OAuth クライアントは project 単位・スコープは認証時指定という性質を使い 既存の credentials をそのまま再利用 / GCP プロジェクトを増やさず スコープは
documents + drive のみに絞る / トークンDB は別ファイルにして他サーバーの認証を壊さない
- API エラーを対処法込みに意訳するAPI 未有効化なら有効化 URL を抽出して埋めた日本語メッセージに スコープ不足なら再認証コマンドの案内に変換 / エージェントが自力で復帰できるエラー文にする
Tools
公開している MCP ツール(12本)
- get_structure — ブロック一覧(種別・index範囲・要約)
- get_outline — 見出しだけを階層で
- read_text — 全文プレーンテキスト(表はタブ区切り)
- export_markdown — Markdown 書き出し
- create_document — Markdown から新規作成
- list_accounts — アカウント一覧
- delete_section — 見出し単位で節を削除(dryRun可)
- delete_ranges — index 範囲を複数削除(dryRun可)
- delete_table_column — 表の列を削除(dryRun可)
- replace_text — 一括置換
- insert_text — 位置指定の挿入
- raw_batch_update — 生 API リクエストの直投げ
スタック Node.js (ESM) / @modelcontextprotocol/sdk / googleapis (docs v1 / drive v3) / zod / node:sqlite
実装規模 542行
外部npm依存 3個(sdk / googleapis / zod)
qs-forms
フォームの構造と回答を読む(read only)
運用中
課題
展示会・ウェビナーの申し込みフォームの回答を CRM と突き合わせたい
だが Forms API の回答は設問IDがキーで返るため 生では人もエージェントも読めない / 回答者メールも別枠にあり 突合の前に毎回整形が要る
→ 設問タイトルと回答者メールを join した形で返す 読み取り専用の MCP を実装した
Design Decisions
設計上の判断(なぜ そうしたか)
- スコープを readonly 3つに限定した
forms.body.readonly / forms.responses.readonly / drive.readonly のみ要求 / 後からツールを足しても書き込めない=副作用を運用ルールでなく権限で塞ぐ
- 設問と回答の join をサーバー側でやるフォーム構造から
設問ID → 設問タイトル のマップを作り 回答をタイトルキーに変換して返す / 呼び出し側に整形コードを書かせない
- 回答一覧は既定で全件を返す件数指定が無ければ
nextPageToken を内部で回して全件取得 / 「取りこぼしがないか」をエージェントに判断させない
- トークンDB を他サーバーと分けた認証情報(クライアントJSON)は共有するが スコープが違うサーバーと同じDBに置くと互いの認証を壊すため トークンだけ別ファイルに分離
- API に無いものはコードで作らず 手順で埋めるForms API には「フォーム一覧」を返す口が無い / 憶測の一覧取得を実装せず Drive 検索で formId を特定する手順を README に落とした
Tools
公開している MCP ツール(4本)
| ツール | 引数 | 戻り値 |
| get_form | formId | タイトル・設定・設問一覧(構造) |
| list_responses | formId / pageSize? / filter? | 回答一覧(回答者メール + 設問タイトルで join 済み) |
| get_response | formId / responseId | 回答1件(同上) |
| list_accounts | — | 認証済みアカウント一覧 |
スタック Node.js (ESM) / @modelcontextprotocol/sdk / googleapis (forms v1 / drive v3) / zod / node:sqlite
実装規模 308行
qs-drive
Drive のファイル操作(探す・作る・直す・移す・上げる)
日次運用
課題
qs-docs で本文は触れるようになったが 置き場所(フォルダを作る・名前を直す・移す・アップロードする)はエージェントから触れないままだった
ただし Drive はファイルが親フォルダを複数持てる仕様で 移動を雑に実装すると 所在不明のファイルを量産する
→ 変更系を全部 dryRun 既定にし 削除を最初から持たない 薄いサーバーとして実装した
Design Decisions
設計上の判断(なぜ そうしたか)
- 認証を qs-docs から直接 import した隣のサーバーの
auth.js をそのまま参照し トークンDB も共有 / Drive 用に OAuth を作り直さないので このサーバー自体は「Drive 操作の記述」だけで済み 192行に収まった
- 変更系ツールは dryRun 既定 true
create_folder / rename_item / move_item / upload_file は 明示的に dryRun:false と言われた時だけ実行 / 判定を1つの関数に集約し ツールごとの書き忘れを構造的に防ぐ
- 削除機能を持たせないゴミ箱送りを含め delete 系のツールを一切公開しない / 取り返しのつかない操作は「気をつける」でなく そもそも道具を作らないことで防ぐ
- 移動は「外す親」を明示させ 実際の親かを検証する
move_item は取り外す親フォルダIDを必須引数にし 現在の親に含まれなければエラーで止める / 複数親のファイルから意図しない親を剥がす事故を防ぐ
- 共有ドライブを最初から前提にする全ての API 呼び出しに
supportsAllDrives / 一覧系に includeItemsFromAllDrives を付与 / マイドライブ限定の実装にして後から作り直すことを避けた
Tools
公開している MCP ツール(8本)
- list_folder — フォルダ直下の一覧
- search_files — 名前で検索
- get_metadata — ファイル・フォルダ情報
- list_accounts — アカウント一覧
- create_folder — フォルダ作成(dryRun既定)
- rename_item — 改名(dryRun既定)
- move_item — 移動(dryRun既定・親の検証あり)
- upload_file — 新規アップロード(上書きなし)
スタック Node.js (ESM) / @modelcontextprotocol/sdk / googleapis (drive v3) / zod
実装規模 192行(8本で最小)
qs-firm
会社名 → 売上 → 企業ランクを自動判定
実装完了 / 投入準備中
※ 正直な状態表示 — コードは完成しユニットテストも通っているが 追客リストへの本番投入はこれから(8本のうちこれだけが日次で動いていない)
課題
展示会のリードリストには「会社名の生テキスト」しか無いことが多く 株式会社 / (株) / 全角 / 支店名 の表記ゆれで名寄せが難しい / 売上を手で調べると数百件で破綻する
最初は公的DB(gBizINFO)に問い合わせる実装にしたが API 申請とトークン管理が要る上に 非上場企業の売上はそもそも載っていない
→ 2026年8月に方針を変えた: 探すのはホスト側のWeb検索に任せ MCP は「調べ方の設計」と「根拠の解釈・判定」だけを持つ / 外部APIキーがゼロになった
Design Decisions
設計上の判断(なぜ そうしたか)— 役割分割が主題
- 探すのはホスト・決めるのは MCP に割った
plan_firm_research が 正規化した社名・検索クエリ・確認順(公式 → 有価証券報告書 → 官公庁 → 報道 → 企業DB)・完了条件を返す / エージェントが集めた根拠を classify_firm_from_web が判定する / 検索の性能はホストの進化に乗せ 判定基準だけを自分で持つ
- 根拠をスコアで比べて一次情報を優先するソース種別(公式60 / 有報55 / 官公庁50 / 報道35 / 企業DB25)+ 年度の新しさ + 連結+5 + 数値が明示+5 で採点し 最上位を採用 / 残りは「代替根拠」として一緒に返し 後から検証できるようにする
- 売上表記の揺れをパースで吸収する「売上高」「営業収益」「年商」を同義に均し 桁区切りや「約」を落として 兆 / 億 / 万 の乗数で円に変換 / 表記の違いを人が直す作業を残さない
- 確定と推定を必ず区別し 未確認を未確認と書く公式・有報・官公庁が出所なら
high それ以外は mid / 売上が最後まで出なければ「未確認・低ランク仮置き」と根拠文を明示して返す / 推定値を確定値の顔で返さない
- 上場企業だけは自動フォールバックさせない非上場で売上不明なら 50億円未満(ランク0)へ仮置きするが 上場企業はランクを付けずに「追加調査」を返す / 大企業を低ランクに落とす誤りは 取りこぼしのコストが非対称に大きいため
- 判定ロジックを純関数に切り出しテストを置いた正規化・スコアリング・ランク判定は I/O を持たない関数に分離し
node --test で 正規化 / 判定 / フォールバックを検証 / 8本の中で唯一 自動テストのあるサーバー
Tools
公開している MCP ツール(3本)
| ツール | 引数 | 戻り値 |
| plan_firm_research | name | 正規化名・検索クエリ・確認順・完了条件 |
| classify_firm_from_web | name / listed? / evidence[] | 売上(値・年度・連結単体・ソース・根拠)/ 企業ランク / 信頼度 / 代替根拠 |
| classify_firm_batch_from_web | companies[] | 調査済み企業を一括判定(件数 + 結果配列) |
企業ランクは売上規模で4段階(300億+ / 100–300億 / 50–100億 / 50億未満)/ 円で受け 1億で割って億換算してから閾値判定
スタック Node.js (ESM) / @modelcontextprotocol/sdk / zod / node:test
実装規模 152行(外部API依存を外し 判定に専念して 251行から縮小)
外部API なし
Summary
技術スタックと姿勢
| 観点 | 共通の選択 |
| 言語 / ランタイム | Node.js(ES Modules)— 全8本 |
| MCP 実装 | @modelcontextprotocol/sdk + StdioServerTransport / 引数は zod でスキーマ定義 |
| 永続化 | node:sqlite(Node 22+ 組み込み)— 外部DBドライバ依存ゼロ |
| 外部 API | Ollama(ローカル)/ Gmail / Slides / Docs / Drive / Forms / Apps Script Web App |
| 認証 | OAuth クライアント1つを5サーバーで共有 / トークンDB はスコープ単位で分離 / 最小権限(qs-forms は readonly のみ) |
| 秘密情報 | 環境変数 または リポジトリ外ファイル — コードにもリポジトリにも置かない |
| 配布 | 各リポジトリに README / LICENSE(MIT) を整備 |
この8本から伝えたいこと
- 道具がなければ自分で書くエンジニアでなくとも 業務の不便を 既製品待ちにせず コードで解いてきた / 7本は実際に業務で動いている
- 作り足すより 使い回す認証は1つを5サーバーで共有し qs-drive は隣のサーバーの認証をそのまま import して192行に収めた / サーバーが増えても運用の手間を増やさない
- 方針を変えた判断も残すqs-firm は公的DB 依存をやめ「探すのはホスト・決めるのは MCP」に作り替えた / 動いていた実装でも 前提が合わなければ捨てる
- 誇張しないqs-firm は「運用中」と書けば見栄えするが 実態は投入準備中 / 事実と差があるものは正直に状態表示する
- 設計判断には理由があるローカル優先・依存極小・許可リスト・副作用分離 — 各選択は コストと安全と保守性のトレードオフを意識した結果
石塚 一樹 / Kazuki Ishizuka — kazukiishizuka.marinos@gmail.com
各サーバーのソースは個別リポジトリ(README / MIT License 付き)として整備済み
本編ポートフォリオ — kazuki-ishizuka.pages.dev