| name | add-workiq |
| description | Adds Work IQ (M365 Copilot Search) to a Power Apps code app via the Work IQ Copilot MCP connector (shared_a365copilotchatmcp), then wires up a production-ready McpSession wrapper for AI-powered, knowledge-grounded search and chat. Use when integrating Microsoft 365 Copilot search/chat. The CopilotChat tool searches internal Microsoft 365 content (documents, emails, chats, sites, files) across your organization — prefer workload-specific tools (SharePoint, OneDrive, Teams, Mail) when the workload is explicit; do not use it for general knowledge, news, public web, or external information. |
| user-invocable | true |
| allowed-tools | Read, Edit, Write, Grep, Glob, Bash, LSP, TaskCreate, TaskUpdate, TaskList, TaskGet, AskUserQuestion, Skill |
| model | sonnet |
📋 Shared Instructions: shared-instructions.md - Cross-cutting concerns.
Add Work IQ (M365 Copilot Search)
Work IQ is accessed through the dedicated Work IQ Copilot MCP connector (shared_a365copilotchatmcp; shown as "Work IQ Copilot MCP (Preview)" in the maker portal). It exposes an MCP (Model Context Protocol) endpoint whose CopilotChat tool performs AI-powered, knowledge-grounded search and conversation over Microsoft 365 content.
This is the purpose-built Work IQ connector and generates a WorkIQCopilotMCPService with a single mcp_m365copilot operation. Every command and code reference in this skill is specific to shared_a365copilotchatmcp (connection commands, and the generated WorkIQCopilotMCPService / WorkIQCopilotMCPModel files). Do not run this skill for a different connector. If you instead need the broader shared_a365mcpservers "Microsoft 365 MCP Servers" bundle (Mail/Teams/SharePoint MCP servers), add it via /add-connector — its generated service is MicrosoftMCPServersService, so you must adjust the Step 2 commands and the wrapper imports accordingly.
⚠️ Work IQ uses MCP. A JSON-RPC initialize handshake runs before the first tools/call. This connector's server (Microsoft.MCPPlatform.WebApi) is stateless-tolerant — do NOT send an Mcp-Session-Id on initialize (the server treats it as a session lookup and returns -32001 Session not found). Drive the connector through the McpSession wrapper below, which runs the handshake, sends no session id by default, sequences JSON-RPC ids, auto-retries on Session not found, and parses the nested response for you.
Workflow
- Check Memory Bank → 2. Add Connector → 3. Inspect Generated Service → 4. Create McpSession Wrapper → 5. Use Work IQ → 6. Build → 7. Update Memory Bank
Step 1: Check Memory Bank
Check for memory-bank.md per shared-instructions.md.
Step 2: Add Connector
The Power Apps code-app CLI (@microsoft/power-apps-cli, invoked as pa or power-apps — resolve via cli-binary.md) creates the connection and generates the typed service itself. Make sure the CLI is installed (npm install) and you are signed in (pa auth status, or power-apps auth-status on power-apps-only projects; it shares the same auth as the rest of the code-app skills).
Find or Create the Connection
Check for an existing connection first:
pa connection list
Look for a Work IQ Copilot MCP (Preview) connection (api id shared_a365copilotchatmcp) in the output. If one is listed, note its Connection ID and skip to "Add the Data Source" below.
Otherwise create one with the native create-connection verb:
pa connection create --connector shared_a365copilotchatmcp
- The environment is read automatically from the app's
power.config.json — you do not need to pass an environment id.
- This connector requires OAuth, so the CLI opens a browser to complete sign-in/consent (SSO-only connectors complete silently with no browser).
- On success it prints the Connection ID — save it for the next step.
STOP HERE — interactive sign-in required:
- Tell the user the browser has opened (or share the URL the CLI prints).
- Ask them to sign in with their Microsoft 365 account and grant consent to the Work IQ Copilot MCP connector.
- Wait for the user to confirm the browser shows success before continuing.
If create-connection fails:
- "not signed in" / auth error → run
pa auth status (or power-apps auth-status on power-apps-only projects), sign in if needed, and retry.
- "Connection creation was cancelled." → the browser flow was closed early; re-run and complete it.
- Any other non-zero exit → report the exact error and STOP.
As a fallback, the user can create the connection manually in the maker portal: https://make.powerapps.com/environments/<environment-id>/connections → + New connection → search for "Work IQ Copilot MCP" → Create, then re-run list-connections.
Add the Data Source
Once the connection exists, add it to the code app (this is what generates the typed service + model):
pa app add data-source --connector shared_a365copilotchatmcp -c <connection-id>
This is a non-tabular connector — only --connector (api id) and -c (connection id) are needed.
Step 3: Inspect Generated Service
After adding the connector, confirm the generated service is present. This is a small, single-operation service, so you can read it directly or grep it:
Grep pattern="async \w+" path="src/generated/services/WorkIQCopilotMCPService.ts"
The WorkIQCopilotMCPService exposes exactly one operation — mcp_m365copilot ("Work IQ Copilot (Preview)"), plus a Getmcp_m365copilot GET variant used only for connection verification. Work IQ / CopilotChat is driven through mcp_m365copilot.
Its generated signature is:
public static async mcp_m365copilot(
Mcp_Session_Id?: string,
queryRequest?: QueryRequest
): Promise<IOperationResult<void>>
- First argument is the MCP session id (the connector's
Mcp-Session-Id parameter). Leave it undefined — this server assigns/needs no client session id, and sending one on initialize returns -32001 (see below).
- Second argument is the JSON-RPC body, typed as
QueryRequest ({ jsonrpc?, id?, method?, params?, result?, error? }) — exported from src/generated/models/WorkIQCopilotMCPModel.ts.
- Return type is
IOperationResult<void>; the actual JSON-RPC / SSE body arrives in result.data at runtime.
Facts you can rely on (do not try to discover them at runtime):
- The tool name is
CopilotChat (case-sensitive). Do not substitute query, search, or find.
- The argument key is
message — not query, prompt, or question.
- Do NOT send an
Mcp-Session-Id on the initialize handshake. This connector's server treats an incoming id as an existing-session lookup and returns -32001 Session not found. The server is stateless-tolerant: initialize and tools/call both succeed with no session id, so the McpSession wrapper below tracks none by default.
Session-id handling (verified during testing). This connector's MCP server is stateless-tolerant. initialize with no Mcp-Session-Id returns 200 with server capabilities, and a subsequent tools/call with no id returns the Copilot reply — no session id needs to be tracked or echoed. Do not generate a client id and send it on initialize: the server treats it as a lookup and returns 404 / -32001 Session not found (this was a real bug in an earlier version of this wrapper). Note that the code-apps data layer's IOperationResult<TResponse> exposes only { success, data, error, skipToken, count, fileName } — it does not surface response headers — so if a future MCP server returns a session id only in the Mcp-Session-Id response header, it would be unreadable here. The wrapper still defensively adopts a server id if one ever appears inside result.data.
Step 4: Create McpSession Wrapper
⚠️ CRITICAL: MCP session handling and response parsing are intricate. Copy the production-ready McpSession class below exactly. It runs the initialize handshake (sending no session id), sequences JSON-RPC ids, auto-retries on "Session not found", persists the conversation id, and parses the deeply nested response.
Create src/connectors/mcpClient.ts:
import type { IOperationResult } from '@microsoft/power-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<{
?:
?:
?:
}>
}
= {
?: []
}
(): {
(!result. && result.) {
{ : { : result.. } }
}
: = result.
(data == ) {}
( data === ) data
( data === ) {
dataLines = data
.()
.( line.())
.( line.().())
payload = dataLines. ? dataLines.() : data
{
.(payload)
} {
{ : { : data } }
}
}
{ : { : data } }
}
{
nextId =
: | =
: |
initialized =
(: <>): | {
container = raw <, >
dataObj =
raw. && raw. ===
? (raw. <, >)
:
resultObj =
dataObj?. && dataObj. ===
? (dataObj. <, >)
:
: <> = [
dataObj?.[],
dataObj?.,
dataObj?.,
resultObj?.[],
resultObj?.,
resultObj?.,
container[],
container.,
container.,
]
found = candidates.( value === && value. > )
found === ? found :
}
(: ): {
message = (res.?. ?? ).()
message.() || res.?. === -
}
(): {
. =
. =
}
(
: ,
?: <, >,
allowRetry =
): <> {
: = { : , : (.++), method, params }
sessionId = method === ? : .
raw = ( .(
sessionId,
req
)) <>
negotiatedSessionId = .(raw)
(negotiatedSessionId) {
. = negotiatedSessionId
}
parsed = (raw)
(allowRetry && method !== && .(parsed)) {
.()
.()
.(method, params, )
}
parsed
}
(): <> {
res = .(, {
: ,
: {},
: { : , : },
})
(res.) {
()
}
. =
res
}
(): <> {
(!.) .()
.(, {})
}
(: ): <{ : ; ?: }> {
(!.) .()
raw = .(, {
: ,
: {
message,
...(. ? { : . } : {}),
},
})
(raw.) {
(raw.. ?? .(raw.))
}
parsed = (raw)
(parsed.) {
. = parsed.
}
{ : parsed., : parsed. }
}
}
(): | {
content = res.?. <{ ?: ; ?: }> |
(!.(content)) {
}
textBlocks = content
.( c. === && c. === )
.( c.!.())
.( value. > )
(textBlocks. === ) {
}
jsonBlock = textBlocks.(
block.() && .(block)
)
(jsonBlock) {
jsonBlock
}
nonMetadata = textBlocks.( !.(block))
nonMetadata ?? textBlocks[]
}
(): {
:
?:
} {
rawText = (res)
(!rawText) {
{ : res. ? .(res., , ) : }
}
{
inner = .(rawText) {
?:
?:
?:
?:
}
( inner. === ) {
{
convo = .(inner.)
messages = .(convo.) ? convo. : []
attributed = messages.(
.(m.) && m.. >
)
selected = attributed ?? messages[] ?? messages[messages. - ]
replyText = selected?.?.()
(replyText) {
{ : replyText, : inner. }
}
} {
}
}
fallbackText = inner.?.() || inner.?.() || rawText
{ : fallbackText, : inner. }
} {
{ : rawText }
}
}
Session-id note (verified during testing). McpSession starts with no session id and sends none on the initialize handshake. Testing against shared_a365copilotchatmcp (server Microsoft.MCPPlatform.WebApi) confirmed it is stateless-tolerant: initialize with no id returns 200 + capabilities, and tools/call with no id returns the Copilot reply. Sending a client-generated id on initialize instead makes the server return 404 / -32001 Session not found — that was the original bug. IOperationResult exposes only { success, data, error, skipToken, count, fileName } (no response headers), so a header-only session id would be unreadable here; extractSessionId still adopts a server id if one ever appears inside result.data, and the handshake auto-retries on Session not found for resilience.
If you ever bind a different MCP connector that is genuinely stateful and returns its session id only in the Mcp-Session-Id response header, this SDK cannot read it — capture one raw mcp_m365copilot result in the debugger to confirm where the id surfaces, and if it appears inside result.data, add that path to extractSessionId.
Step 5: Use Work IQ
Initialize once per app (e.g., in a React useEffect on boot or a module singleton) and reuse for all Work IQ calls:
import { McpSession } from './connectors/mcpClient'
const workIqSession = new McpSession()
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 (never
new McpSession() per request)
- ✅ Pass context-specific prompts to
callCopilotChat()
- ✅ The session auto-reinitializes if a "Session not found" error occurs
- ✅
conversationId is automatically persisted across calls for multi-turn chats
Prompt Structure
Work IQ responds well to context-rich, structured prompts. Adapt this pattern for your scenario:
const prompt = `
You are [role/expert description].
**Context:**
- [Relevant data or background information]
- [Additional context as needed]
**Task:** [Clear, specific instruction]
**Format:**
- Use markdown with clear section headings (## Summary, ## Action Items, etc.)
- Specify limits (word count, number of items, etc.)
`.trim()
const { text } = await workIqSession.callCopilotChat(prompt)
Adaptable scenarios: meeting summaries with action items, prioritized daily action items from email, project risk analysis from documents, team performance insights, or any knowledge-grounded analysis over M365 data.
Response Parsing
callCopilotChat() already unwraps the nested JSON-RPC / SSE response and returns clean text. Parse that text according to the format you requested.
For structured markdown output (when you asked for ## 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 { text } = await workIqSession.callCopilotChat(prompt)
const summary = extractSection(text, 'Summary')
const actionItems = extractSection(text, 'Action Items')
For unstructured output — use text directly (display as-is or format for your UI).
Why the McpSession Pattern Is Required
Work IQ uses MCP (Model Context Protocol). Driving it correctly requires:
- Session initialization before the first call (handshake to exchange capabilities)
- Session handling — this connector is stateless-tolerant, so the wrapper sends no session id (sending one on
initialize triggers -32001 Session not found); it defensively adopts a server id only if one ever surfaces in the response body (see the session-id note above)
- JSON-RPC id sequencing (each request needs a unique incrementing id)
- Multi-turn conversation support via conversation-id persistence
- Nested response parsing (JSON-RPC → text content → inner JSON → Graph conversation)
- Automatic recovery on session timeouts (
-32001 / "Session not found")
McpSession handles all of this. Bypassing it (ad-hoc per-call ids or direct calls without the handshake) leads to "Session not found" errors and failed integrations.
Step 6: Build
npm run build
Fix TypeScript errors before proceeding. Do NOT deploy yet.
Step 7: Update Memory Bank
Update memory-bank.md with: connector added (shared_a365copilotchatmcp), McpSession wrapper created at src/connectors/mcpClient.ts, Work IQ usage wired up, build status.
IMPORTANT — Do NOT save sensitive information:
- ❌ OAuth URLs or consent links
- ❌ Connection IDs
- ❌ Session IDs or tokens
- ❌ Environment IDs
- ❌ Any authentication credentials
Only save high-level progress like "Work IQ Copilot MCP connector configured" or "Work IQ CopilotChat integration implemented via McpSession".