はじめに
当社(合同会社ジョインクラス)では、営業リード収集・SNS監視・記事補充などの業務を、launchd の定期ジョブ17個から Claude を無人実行して回しています。自動化率はおよそ98%です。
この記事では、本番で動かしている claude -p のシェルラッパー を出発点にして、Claude Agent SDK(TypeScript)+ MCP で同じ業務エージェントを組む最小構成を紹介します。読み終わったら、その日のうちに「MCPツールを1つ持ち、予算上限つきで無人実行できるエージェント」を手元で動かせるはずです。
前提
- Node.js 20 以上
- Claude Code にログイン済み、または
ANTHROPIC_API_KEYを設定済み - macOS / Linux(定期実行は launchd か cron を使う)
Step 0: いま本番で動いているシェル版
まずは出発点です。当社の全ジョブは、次の共通ラッパーを通して claude を呼んでいます(実ファイル .company/scripts/lib/claude-run.sh から抜粋)。
#!/bin/bash
# claude を無人実行するときの共通ラッパー。実コストを記録する。
# 注意: --bare は認証を読まないため "Not logged in" で失敗する。使わない。
_CLAUDE_RUN_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
claude_run() {
local label="$1"; shift
claude --output-format json \
--fallback-model claude-sonnet-5 \
--max-budget-usd 3 \
"$@" 2>/dev/null \
| python3 "$_CLAUDE_RUN_LIB/record-cost.py" "$label"
}
呼び出し側はこうです(auto-lead-collect.sh より)。
source "$(dirname "$0")/lib/claude-run.sh"
claude_run lead-collect --effort low --allowedTools "WebSearch,WebFetch,Write" -p "
(リード収集の指示)
"
ポイントは3つだけです。
-
--allowedToolsで 使えるツールを列挙(許可リスト方式) -
--max-budget-usdで 1回あたりの予算上限 -
--output-format jsonで結果とコストを 機械可読で受け取り記録
この3点をそのまま SDK に移植します。
Step 1: プロジェクト作成
mkdir lead-agent && cd lead-agent
npm init -y
npm i @anthropic-ai/claude-agent-sdk zod
npm i -D tsx typescript @types/node
package.json に "type": "module" を追加しておきます。
Step 2: 自前のMCPツールを1つ定義する
業務エージェントで一番効くのは「社内データへの安全な窓口」をMCPツールとして渡すことです。ここではリード候補をTSVに追記するだけのツールを作ります。Write ツールを丸ごと渡すより、書ける場所と形式をツール側で固定できる のが利点です。
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
import { appendFile } from "node:fs/promises";
import { z } from "zod";
const LEADS_FILE = "./leads.tsv";
export const crmServer = createSdkMcpServer({
name: "crm",
version: "1.0.0",
tools: [
tool(
"add_lead",
"リード候補を1件追記する。重複チェックは呼び出し側で行うこと",
{
company: z.string().min(1),
url: z.string().url(),
reason: z.string().max(200),
},
async ({ company, url, reason }) => {
const line = [new Date().toISOString(), company, url, reason].join("\t");
await appendFile(LEADS_FILE, line + "\n");
return { content: [{ type: "text", text: `added: ${company}` }] };
},
),
],
});
zod スキーマがそのままツールの入力定義になるので、モデルが URL でない値を渡すとツール実行前に弾かれます。
Step 3: エージェント本体
import { query } from "@anthropic-ai/claude-agent-sdk";
import { crmServer } from "./tools.js";
const prompt = `
中小企業向けAI業務自動化に関心がありそうな企業を3社探し、
crm の add_lead ツールで1社ずつ登録してください。
`;
for await (const message of query({
prompt,
options: {
mcpServers: { crm: crmServer },
// シェル版の --allowedTools に相当。MCPツールは mcp__<server>__<tool> 形式
allowedTools: ["WebSearch", "WebFetch", "mcp__crm__add_lead"],
maxTurns: 20,
maxBudgetUsd: 3, // シェル版の --max-budget-usd 3
fallbackModel: "claude-sonnet-5",
},
})) {
if (message.type !== "result") continue;
// シェル版の record-cost.py に相当
console.log(JSON.stringify({
label: "lead-collect",
subtype: message.subtype,
cost_usd: message.total_cost_usd,
turns: message.num_turns,
}));
if (message.subtype !== "success") process.exit(1);
}
実行します。
npx tsx src/agent.ts
cat leads.tsv
leads.tsv に3行入り、最後に {"label":"lead-collect","subtype":"success","cost_usd":...} が出れば完成です。
Step 4: 外部のMCPサーバーをつなぐ
自前ツールと同じ mcpServers に、stdio の外部サーバーも並べられます。当社は note の操作を MCP サーバー経由で行っているので、例えば次のようになります。
mcpServers: {
crm: crmServer,
"note-api": {
command: "node",
args: ["/path/to/note-mcp-server/build/index.js"],
},
},
allowedTools: ["mcp__crm__add_lead", "mcp__note-api__search-notes"],
読み取り系のツールだけを列挙する のがコツです。サーバーが投稿系ツールを持っていても、allowedTools に書かなければ使われません。
落とし穴(全部踏みました)
1. 「確認なし送信」は必ず事故る
以前、AIが生成した営業メールを確認なしで自動送信し、取引先に敬語のおかしいメールが届いたことがあります。それ以来、対外送信するツールはエージェントに渡さず、ドラフトをファイルに書かせて承認キューに積む構成にしています。上の add_lead が「追記だけ」なのはこのためです。
2. 危険なコマンドは許可リストで閉じる
AIが git push --force を実行してブランチが消えたこともありました。Bash を渡すなら、allowedTools: ["Bash(git status)"] のようにコマンド単位で絞ってください。業務エージェントなら、そもそも Bash を渡さずMCPツールに寄せるほうが安全です。
3. コストは「呼び出し回数」でしか削れない
実測すると、「2+2」を聞くだけの1回でも $0.27〜0.47 かかっていました。内訳の大半はキャッシュ作成の約41,000トークンで、自社の CLAUDE.md・エージェント・スキルは5,800トークンしかありません。プロンプトを削るより、1回の実行でまとめて処理させるほうが効きます。maxBudgetUsd は暴走時の保険として必ず付けましょう。
4. cron は macOS で黙って死ぬ
フルディスクアクセスの問題で cron ジョブが全滅したことがあり、launchd に全面移行しました。macOS で定期実行するなら最初から launchd を使ってください。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>jp.example.lead-agent</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string><string>-lc</string>
<string>cd ~/lead-agent && npx tsx src/agent.ts >> agent.log 2>&1</string>
</array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/jp.example.lead-agent.plist
launchctl start jp.example.lead-agent # 即時に1回実行して動作確認
tail -f ~/lead-agent/agent.log
検証チェックリスト
-
allowedToolsに無いツールを指示に書いても使われない(例: 「Bashでlsして」と頼んで拒否されるか) -
maxBudgetUsd: 0.01にして、subtypeが成功以外で終了し exit code 1 になる -
add_leadに不正なURLを渡させると、ツールが実行されずエラーが返る - launchd 経由の実行でもログに結果JSONが出る(ログイン環境の差で認証が切れていないか)
シェル版とSDK版、どちらを使うか
当社の実感では、1プロンプト完結のジョブはシェル版で十分 です。SDK版に移す価値があるのは、
- 社内データへの窓口をMCPツールとして型付きで固定したいとき
- 結果メッセージを見て、次の処理を分岐させたいとき
の2つです。いきなり全部をSDKにせず、事故りやすい「書き込み」を持つジョブから移すのがおすすめです。
おわりに
業務エージェントの本質は、AIの能力を制限することではなく「安心して委任するための仕組み」を作ることだと考えています。許可リスト・予算上限・コスト記録の3点さえ押さえれば、最小構成でも十分に本番で回ります。
MCPサーバーを自作して業務エージェントに組み込む手順は、書籍『Claude Code × MCP サーバー開発入門』で、stdio/HTTP サーバーの実装から権限設計まで体系的にまとめています。