
Description
Adds Work IQ Copilot MCP by delegating to /add-connector with api-id=shared_a365copilotchatmcp and action mode. Use when users need Microsoft 365 knowledge-grounded Work IQ search/chat.
SKILL.md
📋 Shared Instructions: shared-instructions.md — Cross-cutting concerns.
Add Work IQ Copilot MCP (Wrapper)
This skill is a thin wrapper. Use /add-connector as the single implementation path.
Delegation contract
Invoke /add-connector with:
api-id:shared_a365copilotchatmcpmode:action
Work IQ Integration: MCP Session Pattern
After the connector is added, you'll have access to WorkIQCopilotMCPService. Work IQ uses MCP (Model Context Protocol), a stateful protocol. Use the McpSession wrapper class to manage this properly.
Setup: Create McpSession Wrapper
⚠️ CRITICAL: The McpSession implementation is complex. Copy the production-ready code below exactly. It handles session negotiation, auto-retry on errors, proper JSON-RPC ID sequencing, and response parsing.
Create src/connectors/mcpClient.ts:
import type { IOperationResult } from '@microsoft/managed-apps/data'
import { WorkIQCopilotMCPService } from '../../generated/services/WorkIQCopilotMCPService'
import type { QueryRequest } from '../../generated/models/WorkIQCopilotMCPModel'
export interface JsonRpcRequest {
jsonrpc: '2.0'
id?: string
method: string
params?: Record<string, unknown>
}
export interface JsonRpcResponse {
jsonrpc?: string
id?: string
result?: Record<string, unknown>
error?: { code?: number; message?: string; data?: unknown }
}
type CopilotConversationMessage = {
text?: string
attributions?: Array<{ attributionType?: string; providerDisplayName?: string; seeMoreWebUrl?: string }>
}
type CopilotConversation = {
messages?: CopilotConversationMessage[]
}
function parseRpc(result: IOperationResult<unknown>): JsonRpcResponse {
if (!result.success && result.error) {
return { error: { message: result.error.message } }
}
const data: unknown = result.data
if (data == null) return {}
if (typeof data === 'object') return data as JsonRpcResponse
if (typeof data === 'string') {
const dataLines = data
.split(/\r?\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trim())
const payload = dataLines.length ? dataLines.join('') : data
try {
return JSON.parse(payload) as JsonRpcResponse
} catch {
return { result: { raw: data } }
}
}
return { result: { raw: data } }
}
export class McpSession {
private nextId = 1
private sessionId: string | undefined
private conversationId: string | undefined
private initialized = false
private extractSessionId(raw: IOperationResult<unknown>): string | undefined {
const container = raw as unknown as Record<string, unknown>
const dataObj =
raw.data && typeof raw.data === 'object' ? (raw.data as Record<string, unknown>) : undefined
const resultObj =
dataObj?.result && typeof dataObj.result === 'object'
? (dataObj.result as Record<string, unknown>)
: undefined
const candidates: Array<unknown> = [
dataObj?.['Mcp-Session-Id'],
dataObj?.mcpSessionId,
dataObj?.sessionId,
resultObj?.['Mcp-Session-Id'],
resultObj?.mcpSessionId,
resultObj?.sessionId,
container['Mcp-Session-Id'],
container.mcpSessionId,
container.sessionId,
]
const found = candidates.find((value) => typeof value === 'string' && value.length > 0)
return typeof found === 'string' ? found : undefined
}
private isSessionNotFound(res: JsonRpcResponse): boolean {
const message = (res.error?.message ?? '').toLowerCase()
return message.includes('session not found') || res.error?.code === -32001
}
private resetSession(): void {
this.sessionId = undefined
this.initialized = false
}
private async send(
method: string,
params?: Record<string, unknown>,
allowRetry = true
): Promise<JsonRpcResponse> {
const req: JsonRpcRequest = { jsonrpc: '2.0', id: String(this.nextId++), method, params }
const raw = (await WorkIQCopilotMCPService.mcp_m365copilot(
this.sessionId,
req as QueryRequest
)) as unknown as IOperationResult<unknown>
const negotiatedSessionId = this.extractSessionId(raw)
if (negotiatedSessionId) {
this.sessionId = negotiatedSessionId
}
const parsed = parseRpc(raw)
if (allowRetry && method !== 'initialize' && this.isSessionNotFound(parsed)) {
this.resetSession()
await this.initialize()
return this.send(method, params, false)
}
return parsed
}
async initialize(): Promise<JsonRpcResponse> {
const res = await this.send('initialize', {
protocolVersion: '2025-06-18',
capabilities: {},
clientInfo: { name: 'Custom App', version: '1.0.0' },
})
this.initialized = !res.error
return res
}
async callCopilotChat(message: string): Promise<{ text: string; conversationId?: string }> {
if (!this.initialized) await this.initialize()
const raw = await this.send('tools/call', {
name: 'CopilotChat',
arguments: {
message,
...(this.conversationId ? { conversationId: this.conversationId } : {}),
},
})
const parsed = extractCopilotText(raw)
if (parsed.conversationId) {
this.conversationId = parsed.conversationId
}
return { text: parsed.text, conversationId: parsed.conversationId }
}
}
function extractContentText(res: JsonRpcResponse): string | undefined {
const content = res.result?.content as Array<{ type?: string; text?: string }> | undefined
if (!Array.isArray(content)) {
return undefined
}
const textBlocks = content
.filter((c) => c.type === 'text' && typeof c.text === 'string')
.map((c) => c.text!.trim())
.filter((value) => value.length > 0)
if (textBlocks.length === 0) {
return undefined
}
// Prefer the JSON payload block. Some responses append metadata text blocks
// such as "CorrelationId: ..." that should not be concatenated.
const jsonBlock = textBlocks.find(
(block) => block.startsWith('{') && /"conversationId"|"rawResponse"|"reply"/.test(block)
)
if (jsonBlock) {
return jsonBlock
}
const nonMetadata = textBlocks.find((block) => !/^CorrelationId\s*:/i.test(block))
return nonMetadata ?? textBlocks[0]
}
export function extractCopilotText(res: JsonRpcResponse): { text: string; conversationId?: string } {
if (res.error) {
return { text: `Error: ${res.error.message ?? JSON.stringify(res.error)}` }
}
const rawText = extractContentText(res)
if (!rawText) {
return { text: res.result ? JSON.stringify(res.result, null, 2) : '(no content returned)' }
}
try {
const inner = JSON.parse(rawText) as {
conversationId?: string
reply?: string
message?: string
rawResponse?: string
}
if (typeof inner.rawResponse === 'string') {
try {
const convo = JSON.parse(inner.rawResponse) as CopilotConversation
const messages = Array.isArray(convo.messages) ? convo.messages : []
const attributed = messages.find(
(m) => Array.isArray(m.attributions) && m.attributions.length > 0
)
const selected = attributed ?? messages[1] ?? messages[messages.length - 1]
const replyText = selected?.text?.trim()
if (replyText) {
return { text: replyText, conversationId: inner.conversationId }
}
} catch {
// Fall through to simple reply extraction.
}
}
const fallbackText = inner.reply?.trim() || inner.message?.trim() || rawText
return { text: fallbackText, conversationId: inner.conversationId }
} catch {
return { text: rawText }
}
}
Usage: Generic Work IQ Integration Pattern
Initialize once per app (typically on component mount or app boot) and reuse for all Work IQ calls:
import { McpSession } from './connectors/mcpClient'
// Initialize once per app session (e.g., in React useEffect on app boot)
const workIqSession = new McpSession()
// Call with any knowledge-grounded prompt
export async function queryWorkIQ(userPrompt: string): Promise<string> {
try {
const { text } = await workIqSession.callCopilotChat(userPrompt)
return text
} catch (error) {
const msg = error instanceof Error ? error.message : 'Work IQ query failed'
console.error('Work IQ Error:', msg)
throw error
}
}
Key Patterns:
- ✅ Initialize once, reuse across multiple calls
- ✅ Pass context-specific prompts to
callCopilotChat() - ✅ Adapt prompts for your specific scenario (meetings, priorities, analysis, etc.)
- ✅ Session automatically reinitializes if "Session not found" error occurs
- ✅ Conversation ID is automatically persisted across calls for multi-turn chats
Prompt Structure
Work IQ responds well to context-rich, structured prompts. Use this pattern and adapt it for your specific scenario:
// Generic pattern to adapt for your use case:
const prompt = `
You are [role/expert description].
**Context:**
- [Relevant data or background information]
- [Additional context as needed]
**Task:** [Clear, specific instruction]
**Format:** [If you need structured output, specify the format]
- Use markdown with clear section headings (## Section, ## Action Items, etc.)
- Specify limits (word count, number of items, etc.)
`.trim()
const { text } = await workIqSession.callCopilotChat(prompt)
How to adapt this pattern:
- ✅ Customize the role and context for your scenario (e.g., "meeting summarizer", "action item prioritizer", "project analyst")
- ✅ Add domain-specific information from your app
- ✅ Define the exact format you need (structured markdown, JSON, bullets, etc.)
- ✅ Adjust word limits and output expectations for your use case
Examples of adaptable scenarios:
- Meeting summaries with action items
- Prioritized daily action items from emails
- Project risk analysis from documents
- Team performance insights from communications
- Or any other knowledge-grounded analysis task
Response Parsing
Work IQ returns text that you parse based on your use case.
For structured markdown output (when you asked for ## sections):
const { text } = await workIqSession.callCopilotChat(prompt)
// Extract markdown sections
function extractSection(text: string, sectionName: string): string[] {
const regex = new RegExp(`##\\s*${sectionName}\\s*([\\s\\S]*?)(?=##|$)`)
const match = text.match(regex)
if (!match) return []
return match[1]
.split('\n')
.filter(line => line.trim().startsWith('-'))
.map(line => line.replace(/^-\s*/, '').trim())
.filter(Boolean)
}
const summary = extractSection(text, 'Summary')
const actionItems = extractSection(text, 'Action Items')
For unstructured output (when format is not critical):
const { text } = await workIqSession.callCopilotChat(prompt)
// Use text directly - display as-is or format as needed for your UI
Adjust parsing based on:
- ✅ The format you specified in the prompt
- ✅ Your response structure (markdown sections, JSON, numbered lists, etc.)
- ✅ Both structured (markdown, JSON) and unstructured (text) outputs
Error Handling & Auto-Recovery
McpSession automatically handles "Session not found" errors by reinitializing. Surface errors appropriately:
try {
const { text } = await workIqSession.callCopilotChat(prompt)
if (text.includes('Error:')) {
throw new Error(text)
}
return text
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Query failed'
console.error('Work IQ Error:', errorMsg)
throw error
}
What McpSession handles automatically:
- ✅ Detects "Session not found" (-32001) errors
- ✅ Closes the session and auto-reinitializes
- ✅ Retries the failed request with the new session
- ✅ No manual retry logic needed
You only need to:
- ✅ Wrap calls in try/catch
- ✅ Surface errors to users appropriately
- ✅ Let exceptions propagate for app-level error handling
Implementation Guidance
Step 1: Copy mcpClient.ts Use the provided McpSession implementation as-is. It handles all MCP protocol complexity.
Step 2: Initialize once Create the McpSession instance once per app session (in useEffect or on boot).
Step 3: Craft your prompts For your specific use case, provide context-rich prompts that include:
- Your domain context (meetings, priorities, risks, etc.)
- Specific data from your app
- Expected output format
Step 4: Parse and display Extract the relevant data from Work IQ's response based on the format you requested.
Common Use Cases
Meeting Summaries:
- Prompt Work IQ with meeting context (date, organizer, attendees, description)
- Request ## Summary and ## Action Items sections
- Parse markdown sections and display in modal
Prioritized Daily Action Items:
- Query Work IQ to extract high-priority tasks from emails/messages
- Request ranked list format with due dates and owners
- Parse and display as prioritized task list
Project Risk Analysis:
- Prompt Work IQ to analyze project communications
- Request structured risk assessment with mitigation recommendations
- Parse JSON and display risk dashboard
Team Performance Insights:
- Query collaboration patterns from Teams/emails
- Request insights on productivity and blockers
- Parse and display performance dashboard
Any Knowledge-Grounded Analysis:
- Adapt the pattern for your domain
- Use Work IQ's access to M365 data (emails, Teams, calendar, documents)
- Return structured or unstructured output as needed
Why This Pattern is Required
Work IQ uses MCP (Model Context Protocol), a stateful protocol that requires:
- Session initialization before first call (handshake to exchange capabilities)
- Session ID tracking across all requests
- Proper JSON-RPC ID sequencing (each request must have a unique numeric ID)
- Multi-turn conversation support with conversation ID persistence
- Complex response parsing (nested JSON-RPC + optional streaming)
- Automatic error recovery on session timeouts
The McpSession class handles all of this. Attempting to bypass it (using random session IDs, hardcoded IDs, or direct API calls) will result in "Session not found" errors and failed integrations.
More skills from the Managed-Apps repository
View all 6 skillsadd-azuredevops
integrate Azure DevOps into Microsoft Apps
Jul 7Azure DevOpsIntegrationsMicrosoftadd-office365
integrate Office 365 Outlook
Jul 3MicrosoftMicrosoft 365Outlook CalendarOutlook Emaildelete-app
delete Microsoft Apps
Jul 7MicrosoftOperationslist-apps
discover Microsoft Apps in the environment
Jul 3CLIMicrosoftOperationslist-connectors
discover Microsoft App connectors and data sources
Jul 7API DevelopmentIntegrationsMicrosoft
More from Microsoft
View publisherrushstack-best-practices
manage Rush monorepos with best practices
rushstack
Apr 6EngineeringLocal DevelopmentMicrosoftProject Management +1azure-ai-agents-persistent-dotnet
build AI agents with Azure .NET SDK
skills
Jul 3.NETAgentsAzureLLMazure-ai-anomalydetector-java
build anomaly detection applications with Java
skills
May 13AnalyticsAzureData AnalysisJava +2azure-ai-contentsafety-java
build content moderation applications with Azure AI
skills
Jul 7AI InfrastructureAzureJavaSecurityazure-ai-contentsafety-py
detect harmful content with Azure AI Content Safety
skills
Jul 18AzureComplianceLLMMicrosoft +2azure-ai-language-conversations-py
implement conversational language understanding with Python
skills
Jul 31AnalyticsAzureLLMPython