Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: add-office365-users description: Adds Office 365 Users by delegating to /add-data-source with api-id=shared_office365users and action mode. Use for user profiles, managers, direct reports, people search, and profile photos. user-invocable: true allowed-tools: Read, AskUserQuestion, Skill model: sonnet
📋 Shared Instructions: [shared-instructions.md](${CLAUDE_PLUGIN_ROOT}/shared/shared-instructions.md) — Cross-cutting concerns.
Add Office 365 Users (Wrapper)
This skill is a thin wrapper. Use /add-data-source as the single implementation path.
Delegation contract
Invoke /add-data-source with:
api-id:shared_office365usersmode:action
Office 365 Users: Method Selection Guide
After the connector is added, import the generated service:
import { Office365UsersService } from '../../generated/services/Office365UsersService'
Use the V2 methods for profiles and reporting relationships:
| Scenario | Method | Result | |
|---|---|---|---|
| Current signed-in user | MyProfile_V2($select?) | One GraphUser_V1 | |
| Specific user by ID or UPN | UserProfile_V2(id, $select?) | One GraphUser_V1 | |
| User's manager | Manager_V2(id, $select?) | One GraphUser_V1 | |
| Direct reports | DirectReports_V2(id, $select?, $top?) | Profiles in result.data.value | |
| Search people | SearchUserV2(searchTerm, top?, ...) | Search response in result.data.value | |
| Profile photo | UserPhotoMetadata(userId), then UserPhoto_V2(id) | Metadata, then binary image data |
DirectReports_V2 requires the target manager's Microsoft Entra directory ID or user principal name (UPN). For the signed-in user, call MyProfile_V2('id,userPrincipalName'), then pass the returned id or fall back to userPrincipalName. For a known person, pass their directory ID or UPN directly.
Profile and direct-report pattern
Request only the fields the UI needs:
const fields = ['id','displayName','jobTitle','department','mail','userPrincipalName','officeLocation',].join(',')const myProfile = await Office365UsersService.MyProfile_V2('id,userPrincipalName',)if (!myProfile.success) {throw new Error(myProfile.error?.message ?? 'Failed to resolve current user')}const managerId =myProfile.data?.id ?? myProfile.data?.userPrincipalNameif (!managerId) {throw new Error('Current user profile did not include an ID or UPN')}const result = await Office365UsersService.DirectReports_V2(managerId,fields,)if (!result.success) {throw new Error(result.error?.message ?? 'Failed to load direct reports')}const people = result.data?.value ?? []
An empty value array is valid and means the user has no direct reports.
Profile photos: binary response and CSP-safe rendering
Call UserPhotoMetadata before downloading the photo. HasPhoto: false is a valid no-photo state; render initials or another accessible fallback.
Image responses are a special runtime contract: the generated UserPhoto_V2 signature may declare IOperationResult<string>, but the Managed Apps SDK decodes an image/* response into a Uint8Array. Do not interpolate that byte array directly into a base64 URL, and do not use a blob: URL—the deployed App Player content security policy may block blob: under img-src.
Convert the bytes to a data: URL:
function bytesToBase64(bytes: Uint8Array) {const chunkSize = 0x8000let binary = ''for (let offset = 0; offset < bytes.length; offset += chunkSize) {binary += String.fromCharCode(...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)),)}return btoa(binary)}function toImageDataUrl(value: unknown, contentType = 'image/jpeg') {if (value instanceof Uint8Array) {return `data:${contentType};base64,${bytesToBase64(value)}`}if (value instanceof ArrayBuffer) {return `data:${contentType};base64,${bytesToBase64(new Uint8Array(value))}`}if (typeof value === 'string') {if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(value)) {return value}if (/^https?:/i.test(value)) {throw new Error('Profile photo returned an HTTP URL instead of binary image data.',)}return `data:${contentType};base64,${value}`}throw new Error('The profile photo response used an unsupported format.')}
UserPhoto_V2 has a binary response contract and does not return an external image URL. An HTTP value indicates an unexpected response shape, so the helper rejects it rather than passing a potentially CSP-blocked or authenticated URL to <img src>.
Complete photo flow:
const metadata = await Office365UsersService.UserPhotoMetadata(userId)if (!metadata.success) {throw new Error(metadata.error?.message ?? 'Failed to check profile photo')}if (!metadata.data?.HasPhoto) {return undefined}const photo = await Office365UsersService.UserPhoto_V2(userId)if (!photo.success) {throw new Error(photo.error?.message ?? 'Failed to load profile photo')}return photo.data? toImageDataUrl(photo.data, metadata.data.ContentType): undefined
Always fetch photos through the generated connector service. Do not call Microsoft Graph directly and do not assign the authenticated connector runtime URL to <img src>.