add-workiq について
Add Work IQは、Power AppsのコードアプリにWork IQ Copilot MCPを接続する開発スキルです。コネクター設定や生成サービスの確認に加え、JSON-RPCのハンドシェイク、複数ターンの会話、ネストされた応答を扱うMCPセッションラッパーを作成します。
主な機能
- 01Work IQ Copilot MCP接続を作成または再利用
- 02データソースを追加し生成サービスを確認
- 03MCP初期化とJSON-RPCリクエストをラップ
- 04呼び出し間で会話IDを保持
- 05CopilotChatのネストされた応答を解析
活用シーン
- 01Power AppsコードアプリからWork IQを呼び出す
- 02Microsoft 365コンテンツに基づく複数ターンの検索を構築
- 03MCPセッションの初期化や呼び出しエラーに対処
MCP 対応
Work IQ Copilot MCPコネクターを通じてMCPエンドポイントに接続し、CopilotChatツールを呼び出します。McpSessionラッパーがハンドシェイク、リクエストID、セッション復旧、応答解析を担います。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill add-workiqインストール後、Claude Code で/add-workiqを入力して読み込まれたか確認します。
詳細
📋 共通の指示:shared-instructions.md - 横断的な注意事項です。
Work IQ(M365 Copilot Search)を追加する
Work IQ は、専用の Work IQ Copilot MCP コネクタ(shared_a365copilotchatmcp。メーカー ポータルでは「Work IQ Copilot MCP (Preview)」と表示されます)を通じて利用します。このコネクタは、MCP(Model Context Protocol)エンドポイントを公開し、その CopilotChat ツールによって、Microsoft 365 コンテンツを対象に、AI を活用したナレッジに基づく検索と会話を行います。
これは Work IQ 専用に構築されたコネクタで、単一の
mcp_m365copilotオペレーションを含むWorkIQCopilotMCPServiceが生成されます。このスキルに記載されているすべてのコマンドとコード参照は、shared_a365copilotchatmcp専用です(接続コマンド、および生成されるWorkIQCopilotMCPService/WorkIQCopilotMCPModelファイル)。別のコネクタにはこのスキルを使用しないでください。より広範なshared_a365mcpserversの「Microsoft 365 MCP Servers」バンドル(Mail/Teams/SharePoint MCP サーバー)が必要な場合は、/add-connectorで追加してください。この場合、生成されるサービスはMicrosoftMCPServersServiceなので、Step 2 のコマンドとラッパーのインポートを適宜変更する必要があります。
⚠️ Work IQ は MCP を使用します。 最初の
tools/callの前に、JSON-RPC のinitializeハンドシェイクが実行されます。このコネクタのサーバー(Microsoft.MCPPlatform.WebApi)はステートレスに対応しているため、initializeにMcp-Session-Idを送信しないでください(サーバーはこれをセッション検索として扱い、-32001 Session not foundを返します)。以下のMcpSessionラッパーを通じてコネクタを操作してください。このラッパーはハンドシェイクを実行し、デフォルトではセッション ID を送信せず、JSON-RPC ID の順序を管理し、Session not foundの場合は自動で再試行し、ネストされたレスポンスを解析します。
ワークフロー
- Memory Bank を確認 → 2. コネクタを追加 → 3. 生成されたサービスを調査 → 4. McpSession ラッパーを作成 → 5. Work IQ を使用 → 6. ビルド → 7. Memory Bank を更新
Step 1: Memory Bank を確認する
shared-instructions.md に従って memory-bank.md を確認してください。
Step 2: コネクタを追加する
Power Apps code-app CLI(@microsoft/power-apps-cli。pa または power-apps として実行します。cli-binary.md を参照して解決してください)が、接続を作成し、型付きサービスを生成します。CLI がインストールされていること(npm install)と、サインイン済みであることを確認してください(pa auth status、または power-apps のみを使用するプロジェクトでは power-apps auth-status を実行します。ほかの code-app スキルと同じ認証情報を共有します)。
接続を検索または作成する
まず既存の接続を確認します:
pa connection list
出力内で Work IQ Copilot MCP (Preview) 接続(api id shared_a365copilotchatmcp)を探します。一覧にある場合は、その Connection ID を控えて、下の「データソースを追加する」に進みます。
見つからない場合は、 ネイティブの create-connection 動詞を使って作成します:
pa connection create --connector shared_a365copilotchatmcp
- 環境はアプリの
power.config.jsonから自動的に読み込まれるため、環境 ID を渡す必要はありません。 - このコネクタでは OAuth が必要です。そのため、CLI はサインインと同意を完了するためにブラウザーを開きます(SSO のみのコネクタはブラウザーを開かずにサイレントで完了します)。
- 成功すると Connection ID が出力されます。次の手順のために保存してください。
ここで停止してください — 対話形式でのサインインが必要です:
- ブラウザーが開いたことをユーザーに伝えます(または CLI が出力した URL を共有します)。
- Microsoft 365 アカウントでサインインし、Work IQ Copilot MCP コネクタへの同意を付与するよう依頼します。
- ブラウザーに成功が表示されたことをユーザーが確認するまで、待ってください。
create-connection が失敗する場合:
- 「not signed in」/認証エラー →
pa auth statusを実行します(power-appsのみを使うプロジェクトではpower-apps auth-status)。必要に応じてサインインしてから再試行します。 - 「Connection creation was cancelled.」 → ブラウザーのフローが途中で閉じられています。もう一度実行して、最後まで完了してください。
- その他のゼロ以外の終了コード → 正確なエラーを報告して、停止します。
代替手段として、ユーザーは maker portal で接続を手動作成できます:https://make.powerapps.com/environments/<environment-id>/connections → + New connection → 「Work IQ Copilot MCP」を検索 → Create。その後、list-connections を再実行します。
データソースを追加する
接続が作成されたら、コードアプリに追加します(これにより、型付きサービスとモデルが生成されます):
pa app add data-source --connector shared_a365copilotchatmcp -c <connection-id>
これは非表形式のコネクタのため、必要なのは --connector(api id)と -c(connection id)のみです。
ステップ 3:生成されたサービスを確認する
コネクタを追加した後、生成されたサービスが存在することを確認します。これは単一の操作だけを持つ小さなサービスなので、直接読むか、grep で検索できます。
Grep pattern="async \w+" path="src/generated/services/WorkIQCopilotMCPService.ts"
WorkIQCopilotMCPService が公開する操作は、mcp_m365copilot(「Work IQ Copilot(プレビュー)」)の 1 つだけです。また、接続確認専用の Getmcp_m365copilot GET バリアントもあります。Work IQ/CopilotChat は mcp_m365copilot を通じて動作します。
生成されたシグネチャは次のとおりです。
public static async mcp_m365copilot(
Mcp_Session_Id?: string,
queryRequest?: QueryRequest
): Promise<IOperationResult<void>>
- 第 1 引数は MCP セッション ID(コネクタの
Mcp-Session-Idパラメーター)です。undefinedのままにしてください。このサーバーではクライアントセッション ID は割り当てられず、必要もありません。また、initializeで送信すると-32001が返されます(以下を参照)。 - 第 2 引数は JSON-RPC 本文で、
QueryRequest({ jsonrpc?, id?, method?, params?, result?, error? })として型付けされています。src/generated/models/WorkIQCopilotMCPModel.tsからエクスポートされています。 - 戻り値の型は
IOperationResult<void>です。実際の JSON-RPC/SSE 本文は、実行時にresult.dataに格納されます。
前提としてよい事実(実行時に調べようとしないでください):
- ツール名は
CopilotChat(大文字と小文字を区別)です。query、search、findに置き換えないでください。 - 引数のキーは
messageです。query、prompt、questionではありません。 initializeのハンドシェイクではMcp-Session-Idを送信しないでください。 このコネクタのサーバーは、受信した ID を既存セッションの検索に使い、-32001 Session not foundを返します。このサーバーは ステートレスにも対応しており、セッション ID がなくてもinitializeとtools/callの両方が成功するため、以下のMcpSessionラッパーはデフォルトでセッション ID を追跡しません。
セッション ID の処理(テストで確認済み)。 このコネクタの MCP サーバーは ステートレスにも対応しています。
Mcp-Session-Idを指定せずにinitializeすると、サーバー機能とともに200が返されます。その後、ID を指定せずにtools/callを実行すると Copilot の応答が返されます。追跡またはエコーするセッション ID は不要です。クライアント ID を生成してinitializeで送信しないでください。サーバーはそれを検索用 ID として扱い、404 / -32001 Session not foundを返します(これはこのラッパーの以前のバージョンに実在したバグです)。なお、code-apps のデータレイヤーにあるIOperationResult<TResponse>が公開するのは{ success, data, error, skipToken, count, fileName }のみで、レスポンスヘッダーは公開されません。そのため、今後の MCP サーバーがMcp-Session-Idレスポンスヘッダーだけでセッション ID を返す場合、ここでは読み取れません。ラッパーは引き続き、result.data内にサーバー ID が現れた場合には防御的にそれを採用します。
Step 4: McpSession ラッパーを作成する
⚠️ 重要: MCP セッションの処理とレスポンスの解析は複雑です。以下の本番環境対応済みの McpSession クラスをそのままコピーしてください。initialize ハンドシェイクを実行し(セッション ID は送信しません)、JSON-RPC ID を順序付け、「Session not found」発生時に自動で再試行し、会話 ID を保持して、深くネストされたレスポンスを解析します。
src/connectors/mcpClient.ts を作成します。
// 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<{
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') {
// Handle SSE framing: "event: message\ndata: {JSON}"
const dataLines = data
.split(/\r?\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trim())
// Per the SSE spec, multiple `data:` lines are joined with newlines — a single
// JSON object can be split across lines, so joining with '' would corrupt it.
const payload = dataLines.length ? dataLines.join('\n') : data
try {
return JSON.parse(payload) as JsonRpcResponse
} catch {
return { result: { raw: data } }
}
}
return { result: { raw: data } }
}
export class McpSession {
private nextId = 1
// MCP Streamable HTTP: the client must NOT send a session id on `initialize` —
// the server assigns one. Sending a client-generated id makes this connector's
// server return `404 / -32001 Session not found`. This server is stateless-
// tolerant, so we send no id at all; `extractSessionId` still adopts a server
// id if one ever surfaces in the response body.
private sessionId: string | undefined = 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 {
// Drop any session id and re-handshake from scratch (no id on `initialize`).
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 }
// Send no session id on `initialize` (server treats any id as existing-session lookup).
const sessionId = method === 'initialize' ? undefined : this.sessionId
const raw = (await WorkIQCopilotMCPService.mcp_m365copilot(
sessionId,
req as unknown 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: 'Power Apps Code App', version: '1.0.0' },
})
// Fail fast: surface a failed handshake instead of continuing to `tools/call`.
if (res.error) {
throw new Error(`MCP initialize failed: ${res.error.message ?? JSON.stringify(res.error)}`)
}
this.initialized = true
return res
}
/** Discover the available MCP tools (optional; the tool name is fixed as `CopilotChat`). */
async listTools(): Promise<JsonRpcResponse> {
if (!this.initialized) await this.initialize()
return this.send('tools/list', {})
}
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 } : {}),
},
})
// Surface MCP/connector errors as thrown exceptions so callers can `try/catch`
// instead of string-matching the returned text.
if (raw.error) {
throw new Error(raw.error.message ?? JSON.stringify(raw.error))
}
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
} {
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 : []
// messages[0] echoes the user query; the AI reply is the one carrying
// attributions (citations). Fall back to index 1, then the last message.
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 }
}
}
セッション ID に関する注記(テストで確認済み)。
McpSessionはセッション ID を持たない状態で開始し、initializeハンドシェイクでは ID を送信しません。shared_a365copilotchatmcp(サーバーMicrosoft.MCPPlatform.WebApi)に対するテストで、このサーバーはステートレスなクライアントにも対応していることが確認されました。ID を指定しないinitializeは200とケイパビリティを返し、ID を指定しないtools/callは Copilot の応答を返します。initializeでクライアントが生成した ID を送信すると、サーバーは404 / -32001 Session not foundを返します。これが元のバグでした。IOperationResultが公開するのは{ success, data, error, skipToken, count, fileName }のみ(レスポンスヘッダーは含まれません)のため、ヘッダーにしかないセッション ID はここでは読み取れません。それでも、extractSessionIdはresult.data内にサーバー ID が現れた場合はそれを引き継ぎ、ハンドシェイクは堅牢性を高めるため「Session not found」発生時に自動で再試行します。ステートフルで、セッション ID を
Mcp-Session-Idレスポンスヘッダーでのみ返す別の MCP コネクターを接続する場合、この SDK ではその ID を読み取れません。デバッガーでmcp_m365copilotの生の結果を 1 件確認し、ID がどこに現れるかを確かめてください。result.data内に現れる場合は、そのパスをextractSessionIdに追加します。
Step 5: Work IQ を使用する
アプリごとに 1 回だけ初期化し(例:起動時の React useEffect 内、またはモジュールシングルトン内)、すべての Work IQ 呼び出しで再利用します。
import { McpSession } from './connectors/mcpClient'
// Initialize once per app session and reuse across calls.
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
}
}
主なパターン:
- ✅ 1 回だけ初期化し、複数の呼び出しで再利用します(リクエストごとに
new McpSession()を実行しないでください) - ✅
callCopilotChat()にコンテキストに応じたプロンプトを渡します - ✅ 「Session not found」エラーが発生した場合、セッションは自動で再初期化されます
- ✅ 複数ターンのチャットに備え、
conversationIdは呼び出し間で自動的に保持されます
プロンプトの構成
Work IQ はコンテキストが豊富で、構造化されたプロンプトに適切に応答します。このパターンをシナリオに合わせて調整してください。
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)
応用できるシナリオ:アクション項目を含む会議の要約、メールから優先順位付けした日々のアクション項目、ドキュメントに基づくプロジェクトのリスク分析、チームのパフォーマンスに関する洞察、その他 M365 データに基づくあらゆる分析です。
レスポンスの解析
callCopilotChat() は、ネストされた JSON-RPC/SSE レスポンスをすでに展開し、整形済みの text を返します。要求した形式に従って、そのテキストを解析してください。
構造化された 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 { text } = await workIqSession.callCopilotChat(prompt)
const summary = extractSection(text, 'Summary')
const actionItems = extractSection(text, 'Action Items')
構造化されていない出力の場合 — text をそのまま使用します(そのまま表示するか、UI 用に整形します)。
McpSession パターンが必要な理由
Work IQ は**MCP(Model Context Protocol)**を使用します。正しく利用するには、次の処理が必要です。
- 初回呼び出し前のセッション初期化(ケイパビリティを交換するハンドシェイク)
- セッション処理 — このコネクターはステートレスなクライアントにも対応しているため、ラッパーはセッション ID を送信しません(
initializeで送信すると-32001 Session not foundが発生します)。レスポンス本文にサーバー ID が現れた場合に限り、それを安全に引き継ぎます(上記のセッション ID に関する注記を参照) - JSON-RPC ID の順序付け(各リクエストには一意で、順に増加する ID が必要です)
- 会話 ID の保持による複数ターンの会話のサポート
- ネストされたレスポンスの解析(JSON-RPC → テキストコンテンツ → 内部 JSON → Graph の会話)
- セッションタイムアウト時の自動復旧(
-32001/「Session not found」)
McpSession はこれらすべてを処理します。これを迂回すると(呼び出しごとに独自の ID を使う、またはハンドシェイクなしで直接呼び出すなど)、「Session not found」エラーが発生し、連携に失敗します。
Step 6: ビルドする
npm run build
先に進む前に、TypeScript のエラーを修正してください。まだデプロイしないでください。
ステップ 7:メモリバンクを更新する
memory-bank.md を更新し、以下の内容を記録します。コネクター(shared_a365copilotchatmcp)を追加したこと、src/connectors/mcpClient.ts に McpSession ラッパーを作成したこと、Work IQ の使用を組み込んだこと、ビルドのステータス。
重要 — 機密情報は保存しないでください:
- ❌ OAuth の URL や同意リンク
- ❌ 接続 ID
- ❌ セッション ID やトークン
- ❌ 環境 ID
- ❌ あらゆる認証情報
「Work IQ Copilot MCP コネクターを設定済み」や「McpSession を介して Work IQ CopilotChat の統合を実装済み」のような、概要レベルの進捗のみを保存してください。