소스 정보
- 저장소
- alinaqi/maggy
- 최근 소스 활동
- 2026년 4월 7일 05:21
- 감지된 SKILL.md 언어
- 영어
- 스타
- 705
- 포크
- 56
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
SOC 직업 분류 기준
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/alinaqi/maggy --skill ms-teams-apps명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
Universal coding patterns, constraints, TDD workflow, atomic todos
Multi-model validation council — auto-validate plans, architecture changes, and PRs via validate-plan/review before executing
Task-scoped memory lifecycle — typed MnemoGraph prevents lossy context compaction by treating facts/decisions/code-refs/handoffs as distinct node types with per-type eviction policies
| name | ms-teams-apps |
| description | Microsoft Teams bots and AI agents - Claude/OpenAI, Adaptive Cards, Graph API |
| when-to-use | When building Microsoft Teams bots, tabs, or message extensions |
| user-invocable | false |
| effort | medium |
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.
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
npm install -g @microsoft/teams.cli
# 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
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
{
"$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"
{
"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"
}
]
// 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 ()
],
: {
planner
}
});
app.(, (context, state) => {
});
app.(, (context, state) => {
context.({
: ,
:
});
});
app.();
# 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
}
}
# 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:
// 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', : userMessage });
context.({ : });
{
response = anthropic..({
: ,
: ,
: ,
: history
});
assistantMessage = response.[]. ===
? response.[].
: ;
history.({ : , : assistantMessage });
(history. > ) {
history.(, history. - );
}
context.({
: ,
: assistantMessage
});
} (error) {
.(, error);
context.({
: ,
:
});
}
});
app.(, (context, state) => {
userId = context...;
conversations.(userId);
context.();
});
app.();
// 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' },
: { : , : },
: { : , : }
},
: []
}
},
{
: ,
: ,
: {
: ,
: {
: { : , : },
: { : , : }
},
: []
}
}
];
(): <> {
(toolName) {
:
;
:
;
:
;
:
;
}
}
(): <> {
: .[] = [
{ : , : userMessage }
];
() {
response = anthropic..({
: ,
: ,
: ,
tools,
messages
});
(response. === ) {
: .[] = [];
( content response.) {
(content. === ) {
result = (content., content.);
toolResults.({
: ,
: [{
: ,
: content.,
: result
}]
});
}
}
messages.({ : , : response. });
messages.(...toolResults);
;
}
textContent = response..( c. === );
textContent?. || ;
}
}
// 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',
: { : }
}
]
}
]
});
}
// 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',
: answer,
:
}
]
},
...(sources && sources. > ? [{
: ,
: [
{
: ,
: ,
: ,
:
},
...sources.( ({
: ,
: ,
:
}))
]
}] : [])
],
: [
{
: ,
: ,
: { : , : }
},
{
: ,
: ,
: { : , : }
},
{
: ,
: ,
: { : }
}
]
};
}
// 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: },
{ : , : },
{ : , : }
],
:
},
{
: ,
: ,
:
}
],
: [
{
: ,
: ,
: { : }
},
{
: ,
: ,
: { : }
}
]
};
}
// 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 });
}
// 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()
.({
: startDate,
: endDate
})
.()
.()
.();
}
() {
..().({
: {
subject,
: { : , : body },
: [{ : { : to } }]
}
});
}
() {
..().({
subject,
: startTime,
: endTime,
: {
: attendees.( ({
: email,
:
}))
}
});
}
() {
.
.()
.({
: { : message }
});
}
}
// 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}!`);
});
// 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;
}
// 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..title}: `);
}
documents;
}
// 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-20250514',
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}`
}
]
});
response.[]. === ? response.[]. : ;
}
# 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
# .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
# 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"]
# 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
# 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
# Start ngrok tunnel
ngrok http 3978
# Update manifest with ngrok URL
# Bot endpoint: https://xxxx.ngrok.io/api/messages
# Start local debugging (opens Teams with your app)
npx teamsfx preview --local
// 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');
});
});
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
| Tip | Description |
|---|---|
| Cache Graph tokens | Token refresh is expensive |
| Stream long responses | Use typing indicator + chunked responses |
| Index knowledge base | Pre-embed documents for RAG |
| Use connection pooling | Reuse HTTP connections |
| Compress payloads | Gzip large card responses |
// 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 || )]
});
}
}
});
app.(, (context) => {
userId = context...;
userMessage = context..;
(!conversations.(userId)) {
conversations.(userId, []);
}
history = conversations.(userId)!;
history.({ : , : userMessage });
context.({ : });
response = anthropic..({
: ,
: ,
: ,
: history
});
answer = response.[]. ===
? response.[].
: ;
history.({ : , : answer });
context.({
: [{
: ,
: (userMessage, answer)
}]
});
});
app.(, (context) => {
action = context..?.;
(action) {
:
.(, context..);
context.();
;
:
context.();
;
}
});
app.();
| Issue | Cause | Fix |
|---|---|---|
| Bot not responding | Endpoint unreachable | Check ngrok/Azure URL in manifest |
| Auth failures | Token expired/invalid | Refresh OAuth connection |
| Cards not rendering | Invalid schema | Validate at adaptivecards.io/designer |
| Graph 403 errors | Missing permissions | Check app registration permissions |
| Slow responses | API latency | Add typing indicator, consider streaming |