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?

【Chatworkシリーズ #17】【世界初かもしれない】ChatworkでClaude Code Channelsを実装してみた

0
Last updated at Posted at 2026-03-23

3月20日、Anthropicが「Claude Code Channels」をresearch previewとして発表した。TelegramやDiscordからClaude Codeに指示を飛ばせる、いわばチャットアプリとコーディングエージェントの直結パイプ。

で、思った。Telegramなんて仕事で使わないんだけど。

日本のビジネスチャットはChatworkかSlack。うちのチームはChatworkを6年使い倒している。「Chatworkからスマホで指示を飛ばせたら、外出中でもコードが書ける」——これ、やるしかない。

公式のソースコードが公開されていたので、分解して、Chatwork版を作った。探した限り、Chatwork版のChannel実装は見当たらない。たぶん世界初。

Claude Code Channelsとは?

30秒で説明する。

従来のClaude Code × Chatwork連携はMCPサーバー経由だった。Claude Codeが「Chatworkのメッセージ取ってきて」と主動的にAPIを叩く方式。つまりClaude Codeが動いていないと何も始まらない。

Channelsは逆。Chatwork側からClaude Codeにメッセージをpushする。 外出先のスマホでChatworkに「あのバグ直して」と書くと、Mac上のClaude Codeに届いて勝手にコーディングが始まる。

従来のMCP:
  Claude Code → (pull) → Chatwork API
  「取りに行く」

Channels:
  Chatwork → (push) → Claude Code
  「届く」

地味な違いに見えるけど、使い心地がまるで違う。

公式Telegram/Discordの中身を分解する

Anthropicは公式プラグインのソースコードを丸ごとGitHubに公開している。anthropics/claude-plugins-officialというリポジトリに、Telegram・Discord・Fakechat(テスト用)の3つが入っている。TypeScript/Bun。

読んでみたら、仕組みはシンプルだった。

Channelの正体 = 特殊なMCPサーバー

ChannelはMCPサーバーの拡張版だ。通常のMCPサーバーに claude/channel というケイパビリティを追加すると、Claude Codeが「このサーバーからのpush通知を受け取りますよ」と認識する。

const mcp = new Server(
  { name: 'chatwork-channel', version: '0.1.0' },
  {
    capabilities: {
      experimental: {
        'claude/channel': {},              // ← これがChannel化の鍵
        'claude/channel/permission': {},   // ← Permission Relay(後述)
      },
      tools: {},
    },
  }
);

メッセージのpushは notifications/claude/channel という通知メソッドで行う。

await mcp.notification({
  method: 'notifications/claude/channel',
  params: {
    content: 'ここにメッセージ本体',
    meta: {
      chat_id: 'ルームID',
      sender: '送信者名',
    }
  }
});

Claude Code側では <channel source="chatwork-channel" sender="太郎">ここにメッセージ本体</channel> というタグとして届く。

Telegram公式のツール構成

ツール 機能
reply テキスト・画像の送信。長文自動分割
react 絵文字リアクション
edit_message 送信済みメッセージの編集

Discord公式のツール構成

Telegramに加えて fetch_messages(履歴取得)と download_attachment(ファイルDL)がある。5ツール体制。

セキュリティ設計

ここが一番重要。公式はSender Gating(送信者フィルタリング)をルームIDではなく送信者IDで行っている。

❌ message.chat.id でフィルタ → グループ内の誰でも注入できる
✅ message.from.id でフィルタ → 許可ユーザーのみ

チャットルームのIDでフィルタすると、そのルームにいる全員がClaude Codeに指示を飛ばせてしまう。プロンプトインジェクションの温床になる。Telegram公式はAllowlist方式で、ペアリングフローまで実装している。

Permission Relay(権限リレー)も面白い。Claude Codeがファイル書き込みなどの許可を求めるとき、ローカルのダイアログと同時にチャットにも通知が飛ぶ。外出先から yes abcde と返すだけで承認できる。l(小文字エル)を除いた5文字コードで、スマホでも打ち間違えない配慮がされている。

Chatwork Channelを作る

公式の設計がわかったので、Chatwork版を実装する。

必要なもの

  • Node.js v20以上(Bunでも可)
  • @modelcontextprotocol/sdk パッケージ
  • Chatwork APIトークン
  • Claude Code v2.1.80以上(Permission Relayはv2.1.81以上)

プロジェクト初期化

mkdir chatwork-channel && cd chatwork-channel
npm init -y
npm install @modelcontextprotocol/sdk zod

package.json"type": "module" を追加する。MCP SDKはESMだけ。

基本構造(chatwork-channel.mjs)

全体で約270行。核心部分だけ抜き出す。

1. Chatwork APIヘルパー

const CW_BASE = 'https://api.chatwork.com/v2';

async function cwFetch(path, options = {}) {
  const res = await fetch(`${CW_BASE}${path}`, {
    ...options,
    headers: {
      'X-ChatWorkToken': process.env.CHATWORK_API_TOKEN,
      ...options.headers,
    },
  });
  if (res.status === 204) return null;  // No Content(正常)
  if (!res.ok) throw new Error(`Chatwork API ${res.status}`);
  return res.json();
}

Chatwork APIは新着メッセージなしのとき204を返す。最初これを忘れて Unexpected end of JSON input で10分溶かした。

2. ポーリングループ

Chatwork APIにはWebhookがあるが、Channelsはstdioトランスポートで動くのでHTTPサーバーが立てられない。ポーリング一択。

const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;

async function poll() {
  const messages = await getMessages(ROOM_ID, isFirstPoll);
  if (!messages?.length) return;

  for (const msg of messages) {
    if (isFirstPoll) { lastMessageId = msg.message_id; continue; }
    if (String(msg.account.account_id) === MY_ACCOUNT_ID) continue;
    if (lastMessageId && msg.message_id <= lastMessageId) continue;

    lastMessageId = msg.message_id;
    const body = (msg.body || '').trim();
    if (!body) continue;

    // Permission Relay応答チェック
    const m = PERMISSION_REPLY_RE.exec(body);
    if (m) {
      await mcp.notification({
        method: 'notifications/claude/channel/permission',
        params: {
          request_id: m[2].toLowerCase(),
          behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
        },
      });
      continue;
    }

    // 通常メッセージ → Claude Codeにpush
    await mcp.notification({
      method: 'notifications/claude/channel',
      params: {
        content: body,
        meta: {
          chat_id: ROOM_ID,
          sender: msg.account.name || 'unknown',
          account_id: String(msg.account.account_id),
        },
      },
    });
  }
  isFirstPoll = false;
}

初回ポーリングは lastMessageId を記録するだけで、過去メッセージはpushしない。これをやらないと、セッション開始時に過去のメッセージが全部Claude Codeに流れ込んで大惨事になる。

3. ツール定義(公式準拠)

公式Telegram/Discordに合わせて4ツール実装した。

mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'reply',
      description: 'ルームにメッセージを返信する',
      inputSchema: { /* chat_id, text, reply_to */ },
    },
    {
      name: 'edit_message',
      description: '送信済みメッセージを編集する',
      inputSchema: { /* chat_id, message_id, text */ },
    },
    {
      name: 'fetch_messages',
      description: 'メッセージ履歴を取得する',
      inputSchema: { /* chat_id, limit */ },
    },
    {
      name: 'download_file',
      description: 'ファイル一覧・ダウンロードURLを取得する',
      inputSchema: { /* chat_id, file_id */ },
    },
  ],
}));

Chatwork APIにはリアクション機能がないので react ツールは見送った。公式Telegramの3ツール(reply, react, edit_message)に対して、うちは4ツール(reply, edit_message, fetch_messages, download_file)。履歴取得とファイルDLはDiscord公式と同等。

.mcp.jsonに登録

{
  "mcpServers": {
    "chatwork-channel": {
      "command": "node",
      "args": ["tools/chatwork-channel/chatwork-channel.mjs"],
      "env": {
        "CHATWORK_API_TOKEN": "あなたのトークン",
        "CHATWORK_ROOM_ID": "対象ルームID",
        "CHATWORK_MY_ACCOUNT": "自分のaccount_id",
        "CHATWORK_POLL_SEC": "10"
      }
    }
  }
}

動かしてみた

起動

claude --dangerously-load-development-channels server:chatwork-channel

最初 --channels server:chatwork-channel で起動したら、黄色い警告が出た。

server:chatwork-channel · server: entries need
--dangerously-load-development-channels

公式Allowlistに載っていないサードパーティ実装は --dangerously-load-development-channels が必要。名前に dangerously が入ってるのは「開発中なので許して」という意味。research previewだから仕方ない。

起動すると緑色で表示される。

Listening for channel messages from: server:chatwork-channel

これが出ればChannel待機状態。ポーリングが回り始めている。

テスト1: 送信(Claude Code → Chatwork)

Claude Codeから reply ツールでChatworkに送信。Chatworkに [info][title]Claude Code[/title] タグ付きで届く。タイトル付き枠線で表示されるので、普通の投稿と区別がつく。

テスト2: 履歴取得

fetch_messages で直近のメッセージを取得。自動通知も他メンバーの投稿も取れる。

テスト3: メッセージ編集

edit_message で送信済みメッセージを編集。タイトルが「Claude Code(編集済み)」に変わる。

テスト4: push通知(Chatwork → Claude Code)★本丸

これがChannelsの真骨頂。スマホのChatworkアプリから「聞こえますか?」と送ったら、ターミナルのClaude Codeに届いた。

← chatwork-channel: [To:11166065]AI-managementさん 聞こえますか?

Claude Codeが自動で反応し、reply ツールでChatworkに返信を返した。外出先のスマホから送ったメッセージが、自宅のMac上のClaude Codeに到達して、勝手にコーディングが始まる。 これがpull型のMCPとの決定的な違い。

テスト5: Permission Relay(遠隔許可)

Claude Codeがファイル書き込みの許可を求めたとき、Chatworkに通知が飛んだ。

[Claude Code パーミッション確認]
ツール: reply
内容: Chatworkルームにメッセージを返信する

許可する場合: yes cujoh
拒否する場合: no cujoh

スマホで yes cujoh と返すだけで承認完了。Claude Codeは承認を受け取って処理を続行した。

5文字コードは l(小文字エル)を除外した [a-km-z] で生成される。スマホのフリック入力でも打ち間違えない配慮。

テスト6: リモートコーディング(フルワークフロー)

最後にフルワークフローを試した。スマホのChatworkから「README.mdを作って」と送ったら、こうなった。

  1. Chatwork → Channel push — メッセージがClaude Codeに到着
  2. Claude Codeが作業開始 — ファイルを読み、README.mdを生成
  3. Permission Relay — ファイル書き込みの許可をChatworkに通知
  4. スマホで yes xxxxx — 承認
  5. ファイル作成完了 — Chatworkに完了報告が届く

外出先のスマホで「あのバグ直して」「テスト書いて」と送るだけで、自宅のMacでコードが書かれ、許可もスマホから出せる。これはもうリモートコーディングの完成形に近い。

公式との比較

機能 Telegram公式 Discord公式 Chatwork版
reply
react — (APIなし)
edit_message
fetch_messages
download_file
Sender Gating
Permission Relay
Pairing Flow — (静的設定)
画像送受信 — (未実装)

Pairing Flowは未実装だが、業務利用では固定メンバーなので静的Allowlistで十分だと思っている。画像送受信はChatwork APIで対応可能なので、次のバージョンで入れたい。

ハマったポイント

1. MCP SDKはESMオンリー

"type": "module"package.json に書かないと ERR_MODULE_NOT_FOUND で死ぬ。地味だけど最初のハードル。

2. Chatwork APIの204レスポンス

新着メッセージなしのとき、Chatworkは 204 No Content を返す。bodyが空なので res.json() するとパースエラー。ステータスコードを先にチェックして null を返す処理が必要だった。

3. 初回ポーリングの罠

ポーリング開始時に過去メッセージを全部pushすると、Claude Codeが過去のメッセージに反応し始めて収拾がつかなくなる。初回は lastMessageId を記録するだけにして、2回目以降から新着のみpushする設計にした。

4. --channels--dangerously-load-development-channels の違い

--channels server:chatwork-channel で起動しても、公式Allowlistに入っていないと黄色い警告が出てChannelが有効にならない。開発中は --dangerously-load-development-channels が必要。claude --help に出てこない隠しフラグなので、最初は「フラグが存在しない」と思った。

5. 複数インスタンスの未読メッセージ競合

.mcp.json にChannelを登録すると、VS Codeのセッションでもターミナルのセッションでも同じMCPサーバーが起動する。Chatwork APIは GET /messages でメッセージを既読にするので、一方が先に読むともう一方に届かない。force=1 パラメータで常に全メッセージを取得し、lastMessageId で重複排除する設計に変更して解決した。

MCPとChannelsの使い分け

同じChatworkなのに、MCPとChannelsの2つの接続方式がある。混乱しそうなので整理しておく。

MCP(既存) Channels(今回)
方向 Claude Code → Chatwork Chatwork → Claude Code
トリガー Claude Codeが能動的に取得 Chatworkの新着がpushされる
用途 「未読確認して」「あのルームのメッセージ取って」 「外出先からスマホで指示を飛ばす」
セッション Claude Code起動中のみ Claude Code起動中のみ(同じ)

両方使うのが正解。MCPで「過去メッセージの検索」、Channelsで「リアルタイムの指示受け」と使い分ける。

まとめ

  • Claude Code Channelsは「特殊なMCPサーバー」。claude/channel ケイパビリティを足すだけ
  • 公式Telegram/Discordのソースコードが全公開されているので、設計パターンが丸わかり
  • Chatwork版は270行で実装できた。4ツール + Permission Relay + Sender Gating
  • 204エラーと初回ポーリングの罠に気をつければ、半日で動くものができる
  • MCPとChannelsは併用する。pull型とpush型で使い分ける

非公式のChannel実装(Slack、LINE、WeChat等)もコミュニティで出始めている。APIがあるチャットサービスなら、同じ設計パターンで作れる。Channelsはresearch previewだからAPIが変わるリスクはあるけど、MCPの拡張なので大きな断絶はないと思ってる。

Chatworkユーザーで「外出先からコードを書かせたい」人がいたら、試してみてほしい。


Chatworkシリーズ

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?