Claude CodeでTeams Botを作る — Teams SDKの雛形とローカル検証
Claude CodeでMicrosoft Teams Botを作る手順。Teams SDKの雛形生成、CLAUDE.mdへの構成規約、Agents Playgroundでの認証なしローカル検証、Teams実機で試す前の確認点までを扱う。
Claude CodeでTeams Botを作るときの分担
Teams Botは、雛形の生成、メッセージハンドラーの実装、ローカルでの疎通確認までをClaude Codeに任せやすい題材です。一方でMicrosoft 365側の登録や実機のTeamsでの動作確認は、人の手が要ります。
この記事では、MicrosoftのTeams SDKクイックスタートに沿って、echoテンプレートから始めてローカルで応答を確認するまでを扱います。あわせて、Teams Botの構成ルールをClaude CodeのCLAUDE.mdに書いておき、Claude Codeが毎回同じ前提で実装する形にします。
ひとつ先に注意があります。クイックスタートの書き方と、いま npm install で入るSDKの挙動が食い違う箇所があります。認証を無効にするオプション名がその例で、手順3で実際の出力を載せます。
Teams Botの構成要素と作る前の選択
Teams Botは、ユーザーのメッセージを受けて、ルールまたはAIで判断し、応答や処理を返すアプリです。Microsoft Learnの概要ページは、Botの要素として次の5つを挙げています。
Teams Botの5つの要素
アクティビティハンドラー
ユーザー操作から生じるイベントを処理する部品です。
イベント
メッセージ送信やボタン操作などで発火するものです。
会話
ユーザーとBotのやり取りで、履歴や状態を含みます。
Botロジック
どう応答するかを決める判断部分です。
Botのスコープ
個人チャット、グループチャット、チャネルの3つです。
Botの種類は、AIを使うカスタムエンジンエージェント、会話型Bot、通知Bot、ワークフローBot、コマンドBotに分かれます。作りたいものがどれかを先に決めると、Claude Codeへの指示が具体的になります。
開発の道具立ては、同じ概要ページによるとBot Framework SDKまたはTeams SDKに、Microsoft 365 Agents Toolkit(旧Teams Toolkit)を組み合わせる形です。Teams SDKはJavaScriptとC#で一般提供され、Pythonは開発者プレビューとされています。旧称のTeams AIライブラリv1は非推奨です。
手順1: CLAUDE.mdにTeams Botの構成規約を書く
コードを書かせる前に、プロジェクトの前提をCLAUDE.mdに置きます。CLAUDE.mdは毎回のセッション開始時にコンテキストへ読み込まれる指示ファイルです。Claude Codeのドキュメントは、1ファイル200行未満を目安に、確認できる具体的な書き方を勧めています。
Teams Bot向けの断片は、たとえば次のような形です(構成規約の一例で、Microsoftの雛形そのものではありません)。
# Teams Bot 規約
- SDK: Teams SDK(TypeScript)。TeamsFxは新規に使わない
- SDKのバージョンは `package.json` で固定する(雛形は `latest` 指定)
- 受信エンドポイントは `/api/messages`、ポートは 3978
- Botの応答ロジックは `src/handlers/` に置き、`src/index.ts` は起動と登録だけにする
- 認証の無効化(`dangerouslyAllowUnauthenticatedRequests`、旧名 `skipAuth`)はローカル検証専用。本番用の設定に書かない
- 資格情報(CLIENT_ID・CLIENT_SECRET・TENANT_ID)は `.env` のみ。`.gitignore` に `.env` を入れてからコミットする
- 変更後は `npm run dev` で起動し、Agents Playgroundで応答を確認してから完了とするポート3978、/api/messages、npm run dev は、公式のクイックスタートに出てくる値に合わせています。曖昧な指示より、検証できる指示のほうが従われやすいとClaude Codeのドキュメントにあります。「ハンドラーは src/handlers/ に置く」のような具体的な指示が、その例です。
規約が増えたら、ファイル種別ごとに .claude/rules/ へ分けます。paths フロントマターで対象を絞ると、該当ファイルを触るときだけ読み込まれます。
---
paths:
- "appPackage/**"
- "manifest*.json"
---
# Teamsアプリマニフェストの規約
- マニフェストはJSON。アイコンと一緒にzipのアプリパッケージにまとめる
- `bots` のスコープは、対象に合わせて personal / team / groupChat のうち必要なものだけを指定する
- マニフェストを変えたら、変更内容と再アップロードが必要な点をコミットメッセージに書くTeamsのマニフェストは、アプリの機能と設定を記述するJSONで、アイコンなどとともにzip形式のアプリパッケージになります。マニフェストのスキーマ参照には、bots.scopes に copilot が加わった版の記載もあります。値を書かせる前に最新のスキーマを見るよう、規約に一行足しておくと安全です。
手順2: Teams Developer CLIで雛形を作る
クイックスタートの前提は、Node.js v20以上(C#はv10以上の.NET、Pythonはv3.12以上)です。まずTeams Developer CLIを入れます。クイックスタートは、このCLIをプレビュー段階と説明しています。
npm install -g @microsoft/teams.cli
teams --version
teams project new typescript quote-agent --template echoechoテンプレートは、受け取ったメッセージをそのまま返すだけの基本的なエージェントです。仕組みの学習にちょうどよい最小構成で、ここからClaude Codeに拡張させます。
teams.cli 3.0.3でこのコマンドを実行すると、生成されるのは package.json・tsconfig.json・tsup.config.js・src/index.ts の4ファイルだけです。.gitignore も .env もありません。src/index.ts の中身は次のとおりです。
import { App } from '@microsoft/teams.apps';
const app = new App();
app.on('message', async ({ send, activity }) => {
await send({ type: 'typing' });
await send(`you said "${activity.text}"`);
});
app.start(process.env.PORT || 3978).catch(console.error);クイックスタートの既存プロジェクト向けコード例は、You said: で始まる文を返す1行です。typingの送信も PORT 環境変数もありません。ドキュメントのコードをそのまま写した雛形ではないので、Claude Codeには生成された src/index.ts を読ませてから拡張を頼みます。
dev スクリプトは tsx watch -r dotenv/config src/index.ts で、.env を読み込みます。一方で .gitignore がないため、資格情報を .env に置くと追跡対象になりえます。手順1の規約に .gitignore の一行を入れたのは、このためです。依存の @microsoft/teams.apps は latest 指定になっているので、再現性が要るときは版を固定します。
cd quote-agent
npm install
claudeclaude を起動したら、手順1のCLAUDE.mdを置いた状態で、たとえば「echoの応答を、/quote というコマンドにだけ反応するハンドラーに分けて。ハンドラーは src/handlers/ に置く」と頼みます。app.on('message', ...) の登録の形を保ったまま、ハンドラー関数だけを別ファイルに切り出す指示にすると、差分が小さく、レビューしやすくなります。
手順3: 起動してローカルで応答を確認する
雛形を起動します。
npm run devteams.apps 2.1.0の雛形で、資格情報を設定せずに起動したときの出力は次のとおりです。
> quote-agent@0.0.0 dev
> tsx watch -r dotenv/config src/index.ts
[WARN] @teams/app No credentials configured and dangerouslyAllowUnauthenticatedRequests is not enabled. All incoming requests will be rejected. Configure client authentication to securely receive messages, or set dangerouslyAllowUnauthenticatedRequests: true for local development.
[INFO] @teams/app listening on port 3978 🚀この状態で /api/messages に認証なしのPOSTを送ると、401 Unauthorized と {"error":"Authentication not configured"} が返ります。サーバーはポート3978で待ち受けていても、受信リクエストはすべて拒否されます。
ローカルで試すには、Microsoft 365 Agents Playgroundを使います。Teamsにサイドロードせずにエージェントを試せるツールです。Playgroundは認証なしでリクエストを送るため、ローカル専用で認証を無効にします。
認証を無効にするオプション名
クイックスタートの記載
const app = new App({ skipAuth: true });TypeScriptの例にこの名前で載っています。
teams.apps 2.1.0 の型定義
const app = new App({
dangerouslyAllowUnauthenticatedRequests: true,
});skipAuth は @deprecated で、新しい名前への置き換えが指示されています。
旧名の skipAuth: true でも動きますが、起動時に [DEPRECATED] skipAuth is deprecated. Use dangerouslyAllowUnauthenticatedRequests instead. という警告が出ます。新しい名前に切り替えると、警告は Bot will accept unauthenticated requests on /api/messages. に変わり、先ほどの401は返らなくなります。型定義のコメントによると、オプションを渡さない場合は環境変数 DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS が参照されます。ローカルの .env だけに書いておけば、コードを本番と共通にできます。
クイックスタートは、この種のオプションを本番では絶対に使わないよう警告しています。Claude Codeが生成したコードにこの名前がコミットされていないかは、手順1の規約でも伝えますが、確実に止めたいならあとで触れるhookが向いています。
別のターミナルでPlaygroundを起動します。
npm install -g @microsoft/m365agentsplayground
agentsplayground -e http://localhost:3978/api/messages -c emulatorPlaygroundは http://localhost:56150 で開き、送ったメッセージへの応答が画面に表示されます。この確認まではClaude Codeにサーバー起動とログの読み取りをさせられます。Playgroundの操作は自分で行い、結果をClaude Codeへ伝えて次の修正に進めるのが現実的な分担です。
手順4: Teams実機で試す前に確認すること
Playgroundで応答を確認できたら、実際のTeamsで試す段階です。ここからは環境側の設定が絡むので、Claude Codeに任せる範囲を絞ります。
実機のTeamsには、Teams SDKのドキュメントにある teams CLIの流れで入れます。ローカルのポートを公開するHTTPSのトンネル(DevTunnelsやngrokなど)と、テナントでのサイドロードの許可が前提です。dev tunnelなら、次の2コマンドでポート3978を公開できます。
devtunnel user login
devtunnel host -p 3978 --protocol http --allow-anonymous別のターミナルで、トンネルのURLをエンドポイントに指定してアプリを登録し、Botを起動します。
teams login
teams app create --name <your-bot-name> --endpoint https://<tunnel-host>/api/messages --env .env
npm run devteams app create は、CLIENT_ID・CLIENT_SECRET・TENANT_ID を .env に書き込みます。手順1の規約で .env を .gitignore に入れておく理由は、ここにもあります。コマンドの出力にある「Install in Teams」のリンクから、実機のTeamsにBotを追加します。
Microsoft 365 Agents Toolkit(atk)を使う手順もあります。こちらはVS CodeのAgents Toolkitプロジェクトの流れで、env/.env.local の BOT_DOMAIN と BOT_ENDPOINT を更新してから atk provision --env local などを実行します。手順2の雛形には env/.env.local がないので、そのまま当てはめることはできません。
実機で確認する前のチェックリストは次のとおりです。
| 確認項目 | 内容 | Claude Codeに任せられるか |
|---|---|---|
| 認証無効化の残存 | 内容skipAuth / dangerouslyAllowUnauthenticatedRequests が本番設定に入っていないか | Claude Codeに任せられるか◎ grepで検索できる |
.env のコミット | 内容資格情報が追跡対象になっていないか | Claude Codeに任せられるか◎ git status で確認できる |
| メッセージサイズ | 内容1メッセージは約100KB上限、80KB以内が目安 | Claude Codeに任せられるか○ 長文応答の分割を実装できる |
| @メンションの除去 | 内容チャネルでは本文に <at> のマークアップが入る | Claude Codeに任せられるか○ 除去処理を実装できる |
| M365アカウント・テナント | 内容サインインとテスト用テナントの準備 | Claude Codeに任せられるか✕ 自分の手で行う |
メッセージサイズの上限を超えると、Botは413(RequestEntityTooLarge)とエラーコード MessageSizeTooBig を受け取ります。上限にはメッセージ本文、画像リンク、@メンション、リアクションがUTF-16で含まれ、base64画像は含まれません。
グループとチャネルで反応しないとき
個人チャットで動いたBotが、グループチャットやチャネルで反応しないことがあります。
Microsoft Learnのチャネル・グループ会話のページによると、既定ではBotが直接@メンションされたメッセージだけを受け取ります。チームやチャネルへのメンションや、Botの投稿へのメンションなしの返信は届きません。メンションなしでも受け取りたい場合は、RSC(リソース固有の同意)を設定します。
つまり「メンションされたときだけ処理する条件分岐」をコードに書く必要はありません。コードで必要になるのは、本文に残るメンションの除去です。チャネルの本文には <at>@agentname</at> のようなマークアップが入るので、そのままコマンド判定に渡すと一致しなくなります。TypeScriptのSDKには除去用の関数があります。
app.on('message', async ({ activity, send }) => {
const clean = activity.stripMentionsText().text;
await send(`You said: ${clean}`);
});この関数は、既定では entities にあるすべてのメンションを取り除きます。Bot以外のメンションも消えるので、ほかの人へのメンションが依頼内容の一部になる場合は、stripMentionsText({ accountId: <BotのID> }) のように対象のアカウントを指定します。呼び出すと activity.text も除去後の文字列に書き換わります。
メンションされた人の情報は、本文でなく entities から取ります。送信者が本文を書き換えられるためです。Botがメンションされたかどうかは activity.isRecipientMentioned() でも判定できます。
プライベートチャネルにはもうひとつ制限があります。Botを追加できる環境でも、プライベートチャネルでは投稿やAdaptive Cardの送信ができません。
Botがチームに追加されたときの歓迎メッセージは、app.on('install.add', ...) のハンドラーで送れます。ただしメンバーが100人を超えるような大きなチームでは、歓迎メッセージを送らないよう案内されています。
Claude Codeへの指示と検証のサイクル
雛形以降の実装は、次の順で回すと手戻りが減ります。
1つの応答を足すサイクル
- 1
応答を1つ決める
追加したい応答を1つに絞り、ハンドラーの追加を頼みます。
- 2
起動してログを読ませる
npm run devで起動し、エラーが出ていないかClaude Codeにログを読ませます。 - 3
Playgroundで確かめる
応答を確認し、表示された文言や、出ていない場合はその旨を伝えます。
- 4
差分を貼って直す
期待と違う場合は、その差分をそのまま貼って修正を頼みます。
「このルールだけは必ず守らせたい」ものは、CLAUDE.mdではなくhookに置きます。Claude Codeのドキュメントも、CLAUDE.mdはコンテキストであって強制設定ではなく、特定の操作を確実に止めたいならPreToolUseフックを使うよう案内しています。認証無効化のオプションを本番ファイルに書かせない、といった禁止事項はこちらが向いています。
よくあるつまずき
起動しても応答が返ってこない
資格情報がなく認証の無効化も有効でないときは、すべてのリクエストが401で拒否されます。起動時のログに「No credentials configured」の警告が出ていないかを確認します。
Playgroundは動くのにTeamsでは反応しない
Playgroundは認証なしのテスト用です。実機のTeamsでは、アプリの登録・マニフェスト・公開エンドポイントが揃っている必要があります。トンネルを起動し忘れたり、teams app create に渡したエンドポイントが古いトンネルのURLのままだったりするのが原因になりがちです。
Claude Codeが旧式の手順で実装する
TeamsFxやTeams AIライブラリv1を前提にした古い手順が出てきたら、CLAUDE.mdの規約(手順1)を見直します。新規にTeamsFxを使わない旨と、使うSDKを明記しておきます。
CLAUDE.mdが長くなりすぎた
200行を超えると、コンテキストを圧迫し、従われにくくなります。マニフェストやテストなど、ファイル種別が限られる規約は .claude/rules/ に分けます。
まとめ
雛形の生成と応答ロジックの実装はClaude Codeに任せ、登録とテナント設定は自分で行う分担が扱いやすい構成です。公式ドキュメントと実際のSDKでは細部がずれることがあるので、生成された雛形と起動ログを先にClaude Codeへ読ませ、規約もその実物に合わせて書いておくと手戻りが減ります。
同じ「Claude Codeでチャット系Botを作る」型の記事として、HTTPのInteractionsを受けるDiscord Bot、Webhook受信からSDK応答まで進めるLINE Botがあります。拡張機能の雛形から公開まで進める例はVS Code拡張の自作にあります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots
- learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/getting-started/qu…
- learn.microsoft.com/en-us/microsoftteams/platform/toolkit/debug-local
- learn.microsoft.com/en-us/microsoftteams/platform/toolkit/agents-toolkit-funda…
- learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/ch…
- learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/co…
- code.claude.com/docs/en/memory