Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 73 additions & 62 deletions examples/ai-mcp/README.md
Original file line number Diff line number Diff line change
@@ -1,111 +1,122 @@
# ai-mcp — Azure OpenAI + MCP sample
# ai-mcp — multi-provider AI + MCP sample

A Teams bot powered by **Azure OpenAI** (via the `openai` SDK's `AzureOpenAI` client) and the **Model Context Protocol** SDK. Demonstrates streaming responses, per-conversation memory, a local clarification-card tool, remote MCP server tools, inline citations, follow-up suggestions, and custom feedback.
A Teams bot powered by either **Azure OpenAI** or **Anthropic Claude**, plus the **Model Context Protocol** SDK. It demonstrates streaming responses, per-conversation memory, a local clarification-card tool, remote MCP tools, inline citations, follow-up suggestions, and custom feedback.

This is the TypeScript counterpart to the .NET [`ExtAIBot`](https://github.com/microsoft/teams.net/pull/486) sample. The structural shape matches: a single bot process owns the AI plumbing, runs the tool-call loop in-process, and connects to MCP servers directly. The auto-loop comes from `openai.chat.completions.runTools()` — the OpenAI SDK's helper that auto-executes each tool's `function` callback and feeds the result back to the model until it produces final text.

> **Provider scope.** This sample is bound to the OpenAI chat-completions wire protocol — Azure OpenAI works; vanilla OpenAI works; non-OpenAI providers do not. (See the .NET sample for an `IChatClient` abstraction that's provider-agnostic — TS has no equivalent today.)
The Teams layer is provider-neutral: activity handlers call `agent.run(conversationId, text, stream)`. Each provider implementation owns its message history and tool loop.

## Features

- **Streaming** — `runner.on('content', delta => stream.emit(delta))` forwards text token-by-token
- **Conversation memory** — each conversation keeps its own `Map<conv, ChatCompletionMessageParam[]>` in process
- **Local tool** — `request_clarification`: a `RunnableToolFunction` whose `function` callback pushes an Adaptive Card into a per-turn bucket and returns a placeholder string. The agent discards its wrap-up text and sends only the card.
- **MCP client** — connects to the [Microsoft Learn docs MCP server](https://learn.microsoft.com/api/mcp) at startup. Each MCP tool is wrapped as a `RunnableToolFunction` whose `function` callback invokes the server and feeds the raw result into the citation collector before returning it to the model.
- **Inline citations** — `CitationCollector` parses MCP tool results for `{ contentUrl, title, content }` records; `[N]` markers in the final reply become clickable Teams citation entities.
- **Follow-up suggestions** — a separate non-streaming `chat.completions.create({ response_format: { type: 'json_schema', ... }})` call produces two short prompts shown as suggested-action chips.
- **Custom feedback** — every text reply enables `addFeedback('custom')`; clicking thumbs up/down opens a bot-rendered task module, and submissions hit `message.submit.feedback`.
- **Provider selection** — set `AI_PROVIDER=azure-openai` or `AI_PROVIDER=anthropic`.
- **Streaming** — forwards model text into the Teams `IStreamer`.
- **Conversation memory** — keeps provider-native history per Teams conversation.
- **Local tool** — shows an Adaptive Card when the request needs clarification.
- **MCP client** — connects to the Microsoft Learn MCP server and exposes its tools to either provider.
- **Inline citations** — turns `[N]` references from grounded answers into Teams citation entities.
- **Follow-up suggestions** — generates two suggested-action prompts after a text response.
- **Custom feedback** — enables Teams feedback controls and handles the feedback dialog.

## Prerequisites

- Node.js 20+
- An **Azure OpenAI resource** with a deployed model (e.g. `gpt-4o`) and an API key. No Foundry project required.
- A Teams bot registration (App ID + secret).
- A Teams bot registration with an App ID and secret
- One model provider:
- An Azure OpenAI resource, deployment, and API key
- An Anthropic API key and supported Claude model

## Teams CLI

Use the official Teams CLI (`@microsoft/teams.cli`) to create and manage the Teams app for this sample:
Install the Teams CLI and create the app registration:

```bash
npm install -g @microsoft/teams.cli
teams --version
teams login
```

Expose this sample's local `/api/messages` endpoint with a tunnel, then create the Teams app:

```bash
teams app create --name "ai-mcp" --endpoint "https://<your-tunnel>/api/messages" --env .env --json
```

The CLI writes `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID` to your `.env` file and prints an install link for Teams.
The CLI writes `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID` to `.env`.

## Setup
## Configure Azure OpenAI

Add the Azure OpenAI settings to the `.env` created by the CLI:
Azure OpenAI remains the default provider:

```env
AI_PROVIDER=azure-openai
AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com
AZURE_OPENAI_API_KEY=<your-api-key>
AZURE_OPENAI_MODEL_DEPLOYMENT_NAME=<deployment-name>
AZURE_OPENAI_API_VERSION=2024-10-21
```

# Optional — defaults to the public MS Learn MCP endpoint.
`AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` is the deployment name, not the underlying model name.

## Configure Anthropic

```env
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=<your-api-key>
ANTHROPIC_MODEL=<supported-claude-model>

# Optional; defaults to 4096.
ANTHROPIC_MAX_TOKENS=4096
```

The Anthropic implementation uses the Messages API directly. It streams text with `messages.stream()`, executes returned `tool_use` blocks, appends matching `tool_result` blocks, and continues until Claude returns a final response.

## Optional MCP configuration

Both providers use the same MCP client and tool dispatcher:

```env
MCP_SERVER_URL=https://learn.microsoft.com/api/mcp
```

`AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` is the **deployment name** on your Azure OpenAI resource, not the base model name.
The Microsoft Learn endpoint is the default. Startup fails if the configured MCP server cannot be reached.

## Running
## Run

```bash
npm install
npm run dev --workspace=@examples/ai-mcp
```

The bot connects to the MS Learn MCP server at startup and lists its tools before accepting messages. If the MCP server is unreachable, startup fails — by design, since the sample is meant to demonstrate the MCP path.

## Example interactions

- `Tell me about streaming` — ambiguous: the agent calls `request_clarification`; the bot replies with a clarification card. Pick an option → that choice arrives as the next user turn and the agent answers based on it.
- `How do I stream in teams.ts?` — agent calls an MS Learn search tool, replies with a docs-grounded answer and inline citations, plus two follow-up chips.
- `How do I list users with Microsoft Graph?` — same MCP search path, lands on Graph docs.

## How the pieces fit

```
src/index.ts — AzureOpenAI client + MCP init + Agent + handler registration
src/agent.ts — per-conv chat history, runTools auto-loop, follow-ups
src/local-tools.ts — request_clarification RunnableToolFunction + card builder
src/mcp-tools.ts — MCP client lifecycle, tool listing, wraps each tool
as a RunnableToolFunction
src/citation-collector.ts — parses MCP results, attaches Teams citations to the reply
src/handlers.ts — message, card.action.clarification, message.fetch-task,
message.submit.feedback
- `Tell me about streaming` — asks for clarification with an Adaptive Card.
- `How do I stream in teams.ts?` — searches Microsoft Learn and returns a cited answer.
- `How do I list users with Microsoft Graph?` — invokes an MCP search tool and suggests follow-up prompts.

## Project structure

```text
src/index.ts Provider selection, MCP initialization, Teams app startup
src/agent.ts Azure OpenAI agent and shared agent contract
src/anthropic-agent.ts Anthropic streaming and tool-use loop
src/prompts.ts Shared system and follow-up prompts
src/local-tools.ts Provider-neutral clarification behavior plus OpenAI adapter
src/mcp-tools.ts Provider-neutral MCP execution plus OpenAI adapter
src/citation-collector.ts MCP citation parsing and Teams citation attachment
src/handlers.ts Provider-neutral Teams activity handlers
```

### Tool loop
## Anthropic tool loop

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewrite drops the tool-loop walkthrough, the clarification-flow steps, and the "how the pieces fit" section, and replaces them with Anthropic-specific equivalents. Azure OpenAI is still the default provider, so the default path loses its explanation while the opt-in path gains one.

Could the Azure OpenAI walkthrough come back alongside the Anthropic one? The provider-neutral framing reads well otherwise.


`openai.chat.completions.runTools({ stream: true, tools })` does the heavy lifting:
For each model round, `AnthropicAgent`:

1. Sends the request with our tool definitions to Azure OpenAI.
2. If the model emits a `tool_calls` choice, the runner invokes the matching tool's `function` callback (passing parsed args).
3. The tool's return value is appended as a `role: 'tool'` message and the model is re-prompted.
4. Steps 2-3 repeat until the model produces a `content` message instead of a tool call.
5. Throughout, `content` events fire for each text delta — we forward them straight to the Teams stream.
1. Sends the provider-native conversation history, system prompt, and tools.
2. Streams text deltas into Teams.
3. Appends Claude's complete assistant content to history.
4. Executes every returned `tool_use` block.
5. Appends corresponding `tool_result` blocks as the next user message.
6. Repeats until no tool calls remain.

Each tool function is responsible for its own side effects:
Tool failures are returned to Claude with `is_error: true`. A maximum-round guard prevents an unbounded tool loop.

- `request_clarification` pushes the card into `pendingCards[]`.
- Each MCP tool invokes the server, feeds the raw result into `CitationCollector.tryExtract`, and returns the text to the model.
## Provider-neutral Teams boundary

After `runner.done()`, the agent inspects `pendingCards` and the citation collector to assemble the final Teams activity.
`handlers.ts` depends only on `IAgentRunner`:

### Clarification flow
```typescript
agent.run(activity.conversation.id, userText, stream);
```

1. User: ambiguous question.
2. Model calls `request_clarification`. The callback builds the card, pushes it onto `pendingCards`, returns `"Clarification card attached."`.
3. The model is re-prompted with that tool result and produces a brief wrap-up (e.g. "I've asked for clarification — please pick an option"). We discard this — `pendingCard` is set, so `replyText` is forced to `''`.
4. Bot sends an attachment-only message containing only the card.
5. User picks an option, submits → `card.action.clarification` invoke → handler calls `agent.run(conv, choice, stream)` exactly as if the choice were a new user message.
6. Model now has full context (its own previous tool call + the user's choice) and answers based on it.
Switching providers does not change Teams routing, card actions, citations, feedback, or final activity construction.
2 changes: 1 addition & 1 deletion examples/ai-mcp/appPackage/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"termsOfUseUrl": "https://www.microsoft.com/legal/terms-of-use"
},
"description": {
"short": "Teams bot powered by Azure OpenAI + MS Learn MCP",
"short": "Teams bot powered by AI + MS Learn MCP",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: AI is broad. Consider narrowing it to LLM. For example, this won't run with Supervised learning or Boosted Trees or Reinforcement Learning ... etc.

"full": "A Teams bot that streams answers grounded in MS Learn docs via the MCP protocol, with inline citations, follow-up chips, a clarification card tool, and custom feedback."
},
"icons": {
Expand Down
1 change: 1 addition & 0 deletions examples/ai-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dev:teamsfx:launch-testtool": "npx env-cmd --silent -f env/.env.testtool teamsapptester start"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.71.2",
"@microsoft/teams.api": "*",
"@microsoft/teams.apps": "*",
"@microsoft/teams.cards": "*",
Expand Down
35 changes: 12 additions & 23 deletions examples/ai-mcp/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,7 @@ import { ILogger } from '@microsoft/teams.common';
import { CitationCollector } from './citation-collector';
import { buildClarificationTool } from './local-tools';
import { McpToolSet } from './mcp-tools';


const SYSTEM_PROMPT = `\
You are a Teams docs assistant that can search Microsoft Learn (Teams, .NET, TypeScript, Microsoft Graph, Azure)
and explain bot concepts (streaming, Adaptive Cards, citations, feedback).

When you use information from a search tool, cite your sources inline using a 1-based numeric marker for each result
you reference (e.g. [1], [2]). Use the same number consistently for the same source within a reply.
Do not add a references or sources list at the end of your response — citations are displayed separately in the UI.

If the user's request is ambiguous or could mean two or more things, call the request_clarification tool with a short
question and 2-4 candidate interpretations rather than guessing.`;

const FOLLOW_UPS_PROMPT = `\
Produce 2 specific prompts the user might want to ask next, based on the conversation so far.

Each prompt MUST:
- Be phrased in the first person, as the user would type.
- Stay under 8 words.

Drill into a concrete topic, API, or concept that just came up — or, if the conversation just started, suggest
prompts that showcase what you can help with.`;
import { FOLLOW_UPS_PROMPT, SYSTEM_PROMPT } from './prompts';

const FOLLOW_UPS_SCHEMA = {
type: 'object',
Expand All @@ -50,6 +29,16 @@ export type AgentRunResult = {
followUps: string[];
};

/**
* Provider-neutral contract consumed by the Teams activity handlers.
*/
export interface IAgentRunner {
/**
* Runs one serialized conversation turn and streams its text into Teams.
*/
run(teamsConvId: string, userText: string, stream: IStreamer): Promise<AgentRunResult>;
}

export type AgentOptions = {
client: AzureOpenAI;
deploymentName: string;
Expand All @@ -72,7 +61,7 @@ export type AgentOptions = {
* onto the previous turn's promise — concurrent submits (e.g. clarification
* race) would otherwise interleave assistant/tool messages.
*/
export class Agent {
export class Agent implements IAgentRunner {
private readonly _client: AzureOpenAI;
private readonly _deployment: string;
private readonly _mcpTools: McpToolSet;
Expand Down
Loading
Loading