Ms Teams Apps logo

Ms Teams Apps

Community
alinaqi
ms-teams-apps

Microsoft Teams bots and AI agents - Claude/OpenAI, Adaptive Cards, Graph API

Overview

Publisheralinaqi
Repositorymaggy
Skill namems-teams-apps
Stars
707
Forks
57
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by alinaqi on GitHub. Read the source before you install it.

Installation

Install the Ms Teams Apps AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/alinaqi/maggy/tree/main/skills/ms-teams-apps
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/alinaqi/maggy.git /tmp/maggy
mkdir -p .claude/skills
cp -r /tmp/maggy/skills/ms-teams-apps .claude/skills/ms-teams-apps
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Ms Teams Apps in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Ms Teams Apps on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Ms Teams Apps is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Microsoft Teams Apps Skill

Purpose: Build AI-powered agents and apps for Microsoft Teams. Create conversational bots, message extensions, and intelligent assistants that integrate with LLMs like OpenAI and Claude.


Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│  TEAMS APP TYPES                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  1. AI AGENTS (Bots)                                            │
│     Conversational apps powered by LLMs                         │
│     Handle messages, commands, and actions                      │
│                                                                 │
│  2. MESSAGE EXTENSIONS                                          │
│     Search external systems, insert cards into messages         │
│     Action commands with modal dialogs                          │
│                                                                 │
│  3. TABS                                                        │
│     Embedded web applications inside Teams                      │
│     Personal, channel, or meeting tabs                          │
│                                                                 │
│  4. WEBHOOKS & CONNECTORS                                       │
│     Incoming: Post messages to channels                         │
│     Outgoing: Respond to @mentions                              │
├─────────────────────────────────────────────────────────────────┤
│  SDK LANDSCAPE (2025)                                           │
│  ─────────────────────────────────────────────────────────────  │
│  Teams SDK v2: Primary SDK for Teams-only apps                  │
│  M365 Agents SDK: Multi-channel (Teams, Outlook, Copilot)       │
│  Teams Toolkit: VS Code extension for development               │
└─────────────────────────────────────────────────────────────────┘

Quick Start

Install Teams CLI

bash
npm install -g @microsoft/teams.cli

Create New Project

bash
# TypeScript (Recommended)
npx @microsoft/teams.cli new typescript my-agent --template echo

# Python
npx @microsoft/teams.cli new python my-agent --template echo

# C#
npx @microsoft/teams.cli new csharp my-agent --template echo

Project Structure

my-agent/
├── src/
│   ├── index.ts              # Entry point
│   ├── app.ts                # App configuration
│   └── handlers/
│       ├── message.ts        # Message handlers
│       └── commands.ts       # Command handlers
├── appPackage/
│   ├── manifest.json         # App manifest
│   ├── color.png             # App icon (192x192)
│   └── outline.png           # Outline icon (32x32)
├── .env                      # Environment variables
├── teamsapp.yml              # Teams Toolkit config
└── package.json

App Manifest

Basic Manifest Structure

json
{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
  "manifestVersion": "1.17",
  "version": "1.0.0",
  "id": "{{APP_ID}}",
  "developer": {
    "name": "Your Company",
    "websiteUrl": "https://yourcompany.com",
    "privacyUrl": "https://yourcompany.com/privacy",
    "termsOfUseUrl": "https://yourcompany.com/terms"
  },
  "name": {
    "short": "AI Assistant",
    "full": "AI Assistant for Teams"
  },
  "description": {
    "short": "Your AI-powered assistant",
    "full": "An intelligent assistant that helps you with tasks using AI."
  },
  "icons": {
    "color": "color.png",
    "outline": "outline.png"
  },
  "accentColor": "#5558AF",
  "bots": [
    {
      "botId": "{{BOT_ID}}",
      "scopes": ["personal", "team", "groupChat"],
      "supportsFiles": false,
      "isNotificationOnly": false,
      "commandLists": [
        {
          "scopes": ["personal", "team", "groupChat"],
          "commands": [
            {
              "title": "help",
              "description": "Show available commands"
            },
            {
              "title": "ask",
              "description": "Ask the AI a question"
            }
          ]
        }
      ]
    }
  ],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": ["*.azurewebsites.net"]
}

Manifest with Message Extensions

json
{
  "composeExtensions": [
    {
      "botId": "{{BOT_ID}}",
      "commands": [
        {
          "id": "searchQuery",
          "type": "query",
          "title": "Search",
          "description": "Search for information",
          "initialRun": true,
          "parameters": [
            {
              "name": "query",
              "title": "Search query",
              "description": "Enter your search terms",
              "inputType": "text"
            }
          ]
        },
        {
          "id": "createTask",
          "type": "action",
          "title": "Create Task",
          "description": "Create a new task",
          "fetchTask": true,
          "context": ["compose", "commandBox", "message"]
        }
      ]
    }
  ]
}

AI Agent Development

Basic Bot with Teams SDK v2

typescript
// src/app.ts
import { App, HttpPlugin, DevtoolsPlugin } from '@microsoft/teams.ai';
import { OpenAIModel, ActionPlanner, PromptManager } from '@microsoft/teams.ai';

// Configure the AI model
const model = new OpenAIModel({
  azureApiKey: process.env.AZURE_OPENAI_API_KEY!,
  azureDefaultDeployment: process.env.AZURE_OPENAI_DEPLOYMENT!,
  azureEndpoint: process.env.AZURE_OPENAI_ENDPOINT!,
  // Or use OpenAI directly:
  // apiKey: process.env.OPENAI_API_KEY!,
  // defaultModel: 'gpt-4'
});

// Configure prompts
const prompts = new PromptManager({
  promptsFolder: './src/prompts'
});

// Create action planner
const planner = new ActionPlanner({
  model,
  prompts,
  defaultPrompt: 'chat'
});

// Create the app
const app = new App({
  plugins: [
    new HttpPlugin(),
    new DevtoolsPlugin()
  ],
  ai: {
    planner
  }
});

// Handle messages
app.on('message', async (context, state) => {
  // AI automatically handles the conversation
  // The planner uses the 'chat' prompt to generate responses
});

// Handle specific commands
app.message('/help', async (context, state) => {
  await context.sendActivity({
    type: 'message',
    text: 'Available commands:\n- /help - Show this message\n- /ask [question] - Ask me anything'
  });
});

// Start the app
app.start();

Prompt Configuration

yaml
# src/prompts/chat/config.json
{
  "schema": 1.1,
  "description": "AI Assistant for Teams",
  "type": "completion",
  "completion": {
    "model": "gpt-4",
    "max_tokens": 1000,
    "temperature": 0.7,
    "top_p": 1
  }
}
text
# src/prompts/chat/skprompt.txt
You are an AI assistant for Microsoft Teams. You help users with their questions and tasks.

Current conversation:
{{$history}}

User: {{$input}}
Assistant:

Integrating Claude/Anthropic

Claude-Powered Teams Bot

typescript
// src/claude-bot.ts
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!
});

const app = new App({
  plugins: [new HttpPlugin()]
});

// Conversation history store
const conversations = new Map<string, Anthropic.MessageParam[]>();

app.on('message', async (context, state) => {
  const userId = context.activity.from.id;
  const userMessage = context.activity.text;

  // Get or initialize conversation history
  if (!conversations.has(userId)) {
    conversations.set(userId, []);
  }
  const history = conversations.get(userId)!;

  // Add user message to history
  history.push({ role: 'user', content: userMessage });

  // Show typing indicator
  await context.sendActivity({ type: 'typing' });

  try {
    // Call Claude API
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 1024,
      system: `You are an AI assistant integrated into Microsoft Teams.
        Help users with their questions and tasks.
        Be concise and helpful. Use markdown formatting when appropriate.
        Current user: ${context.activity.from.name}`,
      messages: history
    });

    const assistantMessage = response.content[0].type === 'text'
      ? response.content[0].text
      : '';

    // Add assistant response to history
    history.push({ role: 'assistant', content: assistantMessage });

    // Keep history manageable (last 20 messages)
    if (history.length > 20) {
      history.splice(0, history.length - 20);
    }

    // Send response
    await context.sendActivity({
      type: 'message',
      text: assistantMessage
    });

  } catch (error) {
    console.error('Claude API error:', error);
    await context.sendActivity({
      type: 'message',
      text: 'Sorry, I encountered an error processing your request.'
    });
  }
});

// Clear conversation command
app.message('/clear', async (context, state) => {
  const userId = context.activity.from.id;
  conversations.delete(userId);
  await context.sendActivity('Conversation cleared. Starting fresh!');
});

app.start();

Claude with Tools/Function Calling

typescript
// src/claude-agent.ts
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic();

// Define tools the agent can use
const tools: Anthropic.Tool[] = [
  {
    name: 'search_knowledge_base',
    description: 'Search the company knowledge base for information',
    input_schema: {
      type: 'object' as const,
      properties: {
        query: {
          type: 'string',
          description: 'The search query'
        }
      },
      required: ['query']
    }
  },
  {
    name: 'create_task',
    description: 'Create a new task in the task management system',
    input_schema: {
      type: 'object' as const,
      properties: {
        title: { type: 'string', description: 'Task title' },
        description: { type: 'string', description: 'Task description' },
        assignee: { type: 'string', description: 'Person to assign the task to' },
        due_date: { type: 'string', description: 'Due date in YYYY-MM-DD format' }
      },
      required: ['title']
    }
  },
  {
    name: 'get_calendar',
    description: 'Get calendar events for a user',
    input_schema: {
      type: 'object' as const,
      properties: {
        user: { type: 'string', description: 'User email or name' },
        days: { type: 'number', description: 'Number of days to look ahead' }
      },
      required: ['user']
    }
  }
];

// Tool implementations
async function executeTools(toolName: string, toolInput: any): Promise<string> {
  switch (toolName) {
    case 'search_knowledge_base':
      // Implement your search logic
      return `Found 3 results for "${toolInput.query}":\n1. Document A\n2. Document B\n3. Document C`;

    case 'create_task':
      // Implement task creation (e.g., call Microsoft Graph API)
      return `Task created: "${toolInput.title}"`;

    case 'get_calendar':
      // Implement calendar lookup
      return `Calendar for ${toolInput.user}: 2 meetings today`;

    default:
      return 'Unknown tool';
  }
}

// Agent loop with tool use
async function runAgent(userMessage: string): Promise<string> {
  let messages: Anthropic.MessageParam[] = [
    { role: 'user', content: userMessage }
  ];

  while (true) {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 1024,
      system: 'You are a helpful Teams assistant. Use tools when needed to help users.',
      tools,
      messages
    });

    // Check if we need to use tools
    if (response.stop_reason === 'tool_use') {
      const toolResults: Anthropic.MessageParam[] = [];

      for (const content of response.content) {
        if (content.type === 'tool_use') {
          const result = await executeTools(content.name, content.input);
          toolResults.push({
            role: 'user',
            content: [{
              type: 'tool_result',
              tool_use_id: content.id,
              content: result
            }]
          });
        }
      }

      messages.push({ role: 'assistant', content: response.content });
      messages.push(...toolResults);
      continue;
    }

    // Return final text response
    const textContent = response.content.find(c => c.type === 'text');
    return textContent?.text || 'No response';
  }
}

Adaptive Cards

Basic Adaptive Card

typescript
// src/cards/welcome-card.ts
import { CardFactory } from 'botbuilder';

export function createWelcomeCard(userName: string) {
  return CardFactory.adaptiveCard({
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'TextBlock',
        text: `Welcome, ${userName}!`,
        size: 'Large',
        weight: 'Bolder'
      },
      {
        type: 'TextBlock',
        text: 'I\'m your AI assistant. How can I help you today?',
        wrap: true
      },
      {
        type: 'ActionSet',
        actions: [
          {
            type: 'Action.Submit',
            title: 'Get Started',
            data: { action: 'getStarted' }
          },
          {
            type: 'Action.Submit',
            title: 'View Help',
            data: { action: 'help' }
          }
        ]
      }
    ]
  });
}

AI Response Card with Actions

typescript
// src/cards/ai-response-card.ts
export function createAIResponseCard(
  question: string,
  answer: string,
  sources?: string[]
) {
  return {
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'Container',
        style: 'emphasis',
        items: [
          {
            type: 'TextBlock',
            text: 'Your Question',
            size: 'Small',
            weight: 'Bolder'
          },
          {
            type: 'TextBlock',
            text: question,
            wrap: true
          }
        ]
      },
      {
        type: 'Container',
        items: [
          {
            type: 'TextBlock',
            text: 'AI Response',
            size: 'Small',
            weight: 'Bolder'
          },
          {
            type: 'TextBlock',
            text: answer,
            wrap: true
          }
        ]
      },
      ...(sources && sources.length > 0 ? [{
        type: 'Container',
        items: [
          {
            type: 'TextBlock',
            text: 'Sources',
            size: 'Small',
            weight: 'Bolder'
          },
          ...sources.map(source => ({
            type: 'TextBlock',
            text: `• ${source}`,
            size: 'Small'
          }))
        ]
      }] : [])
    ],
    actions: [
      {
        type: 'Action.Submit',
        title: '👍 Helpful',
        data: { action: 'feedback', value: 'positive' }
      },
      {
        type: 'Action.Submit',
        title: '👎 Not Helpful',
        data: { action: 'feedback', value: 'negative' }
      },
      {
        type: 'Action.Submit',
        title: 'Ask Follow-up',
        data: { action: 'followUp' }
      }
    ]
  };
}

Form Card for User Input

typescript
// src/cards/task-form-card.ts
export function createTaskFormCard() {
  return {
    type: 'AdaptiveCard',
    $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
    version: '1.5',
    body: [
      {
        type: 'TextBlock',
        text: 'Create New Task',
        size: 'Large',
        weight: 'Bolder'
      },
      {
        type: 'Input.Text',
        id: 'taskTitle',
        label: 'Task Title',
        isRequired: true,
        placeholder: 'Enter task title'
      },
      {
        type: 'Input.Text',
        id: 'taskDescription',
        label: 'Description',
        isMultiline: true,
        placeholder: 'Enter task description'
      },
      {
        type: 'Input.ChoiceSet',
        id: 'priority',
        label: 'Priority',
        choices: [
          { title: 'High', value: 'high' },
          { title: 'Medium', value: 'medium' },
          { title: 'Low', value: 'low' }
        ],
        value: 'medium'
      },
      {
        type: 'Input.Date',
        id: 'dueDate',
        label: 'Due Date'
      }
    ],
    actions: [
      {
        type: 'Action.Submit',
        title: 'Create Task',
        data: { action: 'createTask' }
      },
      {
        type: 'Action.Submit',
        title: 'Cancel',
        data: { action: 'cancel' }
      }
    ]
  };
}

Microsoft Graph Integration

Setup Graph Client

typescript
// src/graph/client.ts
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';
import { ClientSecretCredential } from '@azure/identity';

export function createGraphClient() {
  const credential = new ClientSecretCredential(
    process.env.AZURE_TENANT_ID!,
    process.env.AZURE_CLIENT_ID!,
    process.env.AZURE_CLIENT_SECRET!
  );

  const authProvider = new TokenCredentialAuthenticationProvider(credential, {
    scopes: ['https://graph.microsoft.com/.default']
  });

  return Client.initWithMiddleware({ authProvider });
}

Common Graph Operations

typescript
// src/graph/operations.ts
import { Client } from '@microsoft/microsoft-graph-client';

export class GraphOperations {
  constructor(private client: Client) {}

  // Get user profile
  async getUserProfile(userId: string) {
    return this.client.api(`/users/${userId}`).get();
  }

  // Get user's calendar events
  async getCalendarEvents(userId: string, days: number = 7) {
    const startDate = new Date().toISOString();
    const endDate = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();

    return this.client
      .api(`/users/${userId}/calendarView`)
      .query({
        startDateTime: startDate,
        endDateTime: endDate
      })
      .select('subject,start,end,location')
      .orderby('start/dateTime')
      .get();
  }

  // Send email
  async sendEmail(
    fromUserId: string,
    to: string,
    subject: string,
    body: string
  ) {
    return this.client.api(`/users/${fromUserId}/sendMail`).post({
      message: {
        subject,
        body: { contentType: 'HTML', content: body },
        toRecipients: [{ emailAddress: { address: to } }]
      }
    });
  }

  // Create Teams meeting
  async createMeeting(
    userId: string,
    subject: string,
    startTime: string,
    endTime: string,
    attendees: string[]
  ) {
    return this.client.api(`/users/${userId}/onlineMeetings`).post({
      subject,
      startDateTime: startTime,
      endDateTime: endTime,
      participants: {
        attendees: attendees.map(email => ({
          upn: email,
          role: 'attendee'
        }))
      }
    });
  }

  // Post message to channel
  async postToChannel(teamId: string, channelId: string, message: string) {
    return this.client
      .api(`/teams/${teamId}/channels/${channelId}/messages`)
      .post({
        body: { content: message }
      });
  }
}

Authentication

SSO with Teams SDK

typescript
// src/auth.ts
import { App } from '@microsoft/teams.ai';

const app = new App({
  // ... other config
});

app.on('message', async ({ userGraph, isSignedIn, send, signin }) => {
  // Check if user is signed in
  if (!isSignedIn) {
    // Initiate sign-in flow
    await signin();
    return;
  }

  // User is signed in, access Graph API
  const me = await userGraph.call({
    method: 'GET',
    path: '/me'
  });

  await send(`Hello, ${me.displayName}!`);
});

Manual OAuth Flow

typescript
// src/auth/oauth.ts
import { OAuthPrompt, OAuthPromptSettings } from 'botbuilder-dialogs';

const oauthSettings: OAuthPromptSettings = {
  connectionName: process.env.OAUTH_CONNECTION_NAME!,
  text: 'Please sign in to continue',
  title: 'Sign In',
  timeout: 300000 // 5 minutes
};

// In your dialog
async function handleAuth(context, state) {
  const tokenResponse = await context.adapter.getUserToken(
    context,
    oauthSettings.connectionName
  );

  if (!tokenResponse?.token) {
    // No token, show sign-in card
    await context.sendActivity({
      attachments: [
        CardFactory.oauthCard(
          oauthSettings.connectionName,
          oauthSettings.title,
          oauthSettings.text
        )
      ]
    });
    return null;
  }

  return tokenResponse.token;
}

RAG (Retrieval-Augmented Generation)

Vector Search with Azure AI Search

typescript
// src/rag/azure-search.ts
import { SearchClient, AzureKeyCredential } from '@azure/search-documents';

const searchClient = new SearchClient(
  process.env.AZURE_SEARCH_ENDPOINT!,
  process.env.AZURE_SEARCH_INDEX!,
  new AzureKeyCredential(process.env.AZURE_SEARCH_KEY!)
);

export async function searchKnowledgeBase(
  query: string,
  topK: number = 5
): Promise<string[]> {
  const results = await searchClient.search(query, {
    top: topK,
    select: ['content', 'title', 'source'],
    queryType: 'semantic',
    semanticConfiguration: 'default'
  });

  const documents: string[] = [];
  for await (const result of results.results) {
    documents.push(`${result.document.title}: ${result.document.content}`);
  }

  return documents;
}

RAG-Enhanced Claude Response

typescript
// src/rag/claude-rag.ts
import Anthropic from '@anthropic-ai/sdk';
import { searchKnowledgeBase } from './azure-search';

const anthropic = new Anthropic();

export async function getRAGResponse(userQuery: string): Promise<string> {
  // 1. Search knowledge base
  const relevantDocs = await searchKnowledgeBase(userQuery);

  // 2. Build context
  const context = relevantDocs.join('\n\n---\n\n');

  // 3. Generate response with context
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    system: `You are a helpful assistant for Teams. Answer questions based on the provided context.
If the context doesn't contain relevant information, say so and provide a general response.
Always cite your sources when using information from the context.`,
    messages: [
      {
        role: 'user',
        content: `Context:\n${context}\n\nQuestion: ${userQuery}`
      }
    ]
  });

  return response.content[0].type === 'text' ? response.content[0].text : '';
}

Deployment

Azure Bot Service Setup

bash
# Create resource group
az group create --name rg-teams-bot --location eastus

# Create App Service plan
az appservice plan create \
  --name asp-teams-bot \
  --resource-group rg-teams-bot \
  --sku B1 \
  --is-linux

# Create Web App
az webapp create \
  --name my-teams-bot \
  --resource-group rg-teams-bot \
  --plan asp-teams-bot \
  --runtime "NODE:18-lts"

# Create Bot Channels Registration
az bot create \
  --resource-group rg-teams-bot \
  --name my-teams-bot \
  --kind registration \
  --endpoint https://my-teams-bot.azurewebsites.net/api/messages \
  --sku F0

# Enable Teams channel
az bot msteams create \
  --name my-teams-bot \
  --resource-group rg-teams-bot

Environment Variables

bash
# .env
# Azure Bot
BOT_ID=your-bot-id
BOT_PASSWORD=your-bot-password
BOT_TENANT_ID=your-tenant-id

# Azure OpenAI
AZURE_OPENAI_API_KEY=your-key
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_DEPLOYMENT=gpt-4

# Or OpenAI
OPENAI_API_KEY=sk-xxx

# Or Anthropic
ANTHROPIC_API_KEY=sk-ant-xxx

# Microsoft Graph
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_TENANT_ID=your-tenant-id

# Azure AI Search (for RAG)
AZURE_SEARCH_ENDPOINT=https://your-search.search.windows.net
AZURE_SEARCH_KEY=your-key
AZURE_SEARCH_INDEX=knowledge-base

Docker Deployment

dockerfile
# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3978

CMD ["node", "dist/index.js"]
yaml
# docker-compose.yml
version: '3.8'

services:
  teams-bot:
    build: .
    ports:
      - "3978:3978"
    environment:
      - BOT_ID=${BOT_ID}
      - BOT_PASSWORD=${BOT_PASSWORD}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    restart: unless-stopped

Teams Toolkit Deployment

bash
# Login to Azure
npx teamsfx account login azure

# Provision resources
npx teamsfx provision --env dev

# Deploy
npx teamsfx deploy --env dev

# Publish to Teams
npx teamsfx publish --env dev

Testing

Local Testing with ngrok

bash
# Start ngrok tunnel
ngrok http 3978

# Update manifest with ngrok URL
# Bot endpoint: https://xxxx.ngrok.io/api/messages

Teams Toolkit Local Debug

bash
# Start local debugging (opens Teams with your app)
npx teamsfx preview --local

Unit Testing

typescript
// tests/bot.test.ts
import { TestAdapter, TurnContext } from 'botbuilder';
import { createWelcomeCard } from '../src/cards/welcome-card';

describe('Bot Tests', () => {
  let adapter: TestAdapter;

  beforeEach(() => {
    adapter = new TestAdapter();
  });

  test('should respond to hello', async () => {
    await adapter
      .send('hello')
      .assertReply((activity) => {
        expect(activity.text).toContain('Hello');
      });
  });

  test('should create welcome card', () => {
    const card = createWelcomeCard('John');
    expect(card.content.body[0].text).toContain('John');
  });
});

Best Practices

Conversation Design

┌─────────────────────────────────────────────────────────────────┐
│  CONVERSATION UX GUIDELINES                                     │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  1. GREET INTELLIGENTLY                                         │
│     - Welcome new users with onboarding card                    │
│     - Return users get quick access to recent actions           │
│                                                                 │
│  2. HANDLE ERRORS GRACEFULLY                                    │
│     - Never show stack traces to users                          │
│     - Provide clear recovery options                            │
│     - Log errors for debugging                                  │
│                                                                 │
│  3. USE CARDS FOR RICH CONTENT                                  │
│     - Adaptive Cards for forms and structured data              │
│     - Hero Cards for simple actions                             │
│     - Keep cards concise and actionable                         │
│                                                                 │
│  4. TYPING INDICATORS                                           │
│     - Show typing for long operations                           │
│     - Provide progress updates for very long tasks              │
│                                                                 │
│  5. CONTEXT AWARENESS                                           │
│     - Remember conversation history                             │
│     - Personalize based on user preferences                     │
│     - Respect team/channel context                              │
└─────────────────────────────────────────────────────────────────┘

Security Checklist

  • Validate all incoming messages
  • Use App-Only auth for Graph API when possible
  • Never log sensitive user data
  • Implement rate limiting
  • Use managed identity in Azure
  • Rotate secrets regularly
  • Enable audit logging

Performance Tips

TipDescription
Cache Graph tokensToken refresh is expensive
Stream long responsesUse typing indicator + chunked responses
Index knowledge basePre-embed documents for RAG
Use connection poolingReuse HTTP connections
Compress payloadsGzip large card responses

Project Templates

AI Assistant Template

typescript
// Complete AI assistant with Claude
import { App, HttpPlugin } from '@microsoft/teams.ai';
import Anthropic from '@anthropic-ai/sdk';
import { createWelcomeCard } from './cards/welcome-card';
import { createAIResponseCard } from './cards/ai-response-card';

const anthropic = new Anthropic();
const app = new App({ plugins: [new HttpPlugin()] });
const conversations = new Map<string, Anthropic.MessageParam[]>();

// Welcome new users
app.conversationUpdate('membersAdded', async (context) => {
  for (const member of context.activity.membersAdded || []) {
    if (member.id !== context.activity.recipient.id) {
      await context.sendActivity({
        attachments: [createWelcomeCard(member.name || 'User')]
      });
    }
  }
});

// Handle messages
app.on('message', async (context) => {
  const userId = context.activity.from.id;
  const userMessage = context.activity.text;

  // Initialize or get conversation
  if (!conversations.has(userId)) {
    conversations.set(userId, []);
  }
  const history = conversations.get(userId)!;
  history.push({ role: 'user', content: userMessage });

  // Show typing
  await context.sendActivity({ type: 'typing' });

  // Get AI response
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    system: 'You are a helpful Teams assistant.',
    messages: history
  });

  const answer = response.content[0].type === 'text'
    ? response.content[0].text
    : '';

  history.push({ role: 'assistant', content: answer });

  // Send rich card response
  await context.sendActivity({
    attachments: [{
      contentType: 'application/vnd.microsoft.card.adaptive',
      content: createAIResponseCard(userMessage, answer)
    }]
  });
});

// Handle card actions
app.on('adaptiveCard/action', async (context) => {
  const action = context.activity.value?.action;

  switch (action) {
    case 'feedback':
      // Log feedback
      console.log('Feedback:', context.activity.value);
      await context.sendActivity('Thanks for your feedback!');
      break;
    case 'followUp':
      await context.sendActivity('What would you like to know more about?');
      break;
  }
});

app.start();

Troubleshooting

IssueCauseFix
Bot not respondingEndpoint unreachableCheck ngrok/Azure URL in manifest
Auth failuresToken expired/invalidRefresh OAuth connection
Cards not renderingInvalid schemaValidate at adaptivecards.io/designer
Graph 403 errorsMissing permissionsCheck app registration permissions
Slow responsesAPI latencyAdd typing indicator, consider streaming

Resources

Frequently asked questions

What does the Ms Teams Apps AI skill do?

Microsoft Teams bots and AI agents - Claude/OpenAI, Adaptive Cards, Graph API

Why use Ms Teams Apps on TypingMind?

Because you install it once and use it with any model. Ms Teams Apps is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Ms Teams Apps in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/alinaqi/maggy/tree/main/skills/ms-teams-apps. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Ms Teams Apps?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Ms Teams Apps?

As many as you like. As long as a model supports skills, you can use Ms Teams Apps with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Ms Teams Apps AI skill free?

Yes. It is published on GitHub by alinaqi under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇