From 8bd33a94afd15a302192162148200cfe787515e1 Mon Sep 17 00:00:00 2001 From: Lovy Jain <54174168+lovyjain@users.noreply.github.com> Date: Thu, 14 May 2026 06:39:26 +0530 Subject: [PATCH] add skill for the ms graph sdk, which is a wrapper (#1701) --- docs/README.skills.md | 1 + skills/msgraph-sdk/SKILL.md | 146 ++++++++++ skills/msgraph-sdk/references/dotnet.md | 212 ++++++++++++++ skills/msgraph-sdk/references/python.md | 290 ++++++++++++++++++++ skills/msgraph-sdk/references/typescript.md | 257 +++++++++++++++++ 5 files changed, 906 insertions(+) create mode 100644 skills/msgraph-sdk/SKILL.md create mode 100644 skills/msgraph-sdk/references/dotnet.md create mode 100644 skills/msgraph-sdk/references/python.md create mode 100644 skills/msgraph-sdk/references/typescript.md diff --git a/docs/README.skills.md b/docs/README.skills.md index 9ba393df..24a1fa9f 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -233,6 +233,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [mini-context-graph](../skills/mini-context-graph/SKILL.md)
`gh skills install github/awesome-copilot mini-context-graph` | A persistent, compounding knowledge base combining Karpathy's LLM Wiki pattern
with a structured knowledge graph. Ingest documents once — the LLM writes wiki
pages, extracts entities/relations into the graph, and stores raw content for
evidence retrieval. Knowledge accumulates and cross-references; it is never
re-derived from scratch. | `references/ingestion.md`
`references/lint.md`
`references/ontology.md`
`references/retrieval.md`
`scripts/config.py`
`scripts/contextgraph.py`
`scripts/template_agent_workflow.py`
`scripts/tools` | | [mkdocs-translations](../skills/mkdocs-translations/SKILL.md)
`gh skills install github/awesome-copilot mkdocs-translations` | Generate a language translation for a mkdocs documentation stack. | None | | [model-recommendation](../skills/model-recommendation/SKILL.md)
`gh skills install github/awesome-copilot model-recommendation` | Analyze chatmode or prompt files and recommend optimal AI models based on task complexity, required capabilities, and cost-efficiency | None | +| [msgraph-sdk](../skills/msgraph-sdk/SKILL.md)
`gh skills install github/awesome-copilot msgraph-sdk` | Integrate Microsoft Graph SDK into any project — .NET, TypeScript/JavaScript, or Python. Covers auth patterns (client credentials, OBO, managed identity), SDK setup, calling Graph APIs, batching, delta queries, change notifications, throttling, and permission scopes. Use when accessing Microsoft 365 data (users, mail, calendar, Teams, files, SharePoint) from any application type. | `references/dotnet.md`
`references/python.md`
`references/typescript.md` | | [msstore-cli](../skills/msstore-cli/SKILL.md)
`gh skills install github/awesome-copilot msstore-cli` | Microsoft Store Developer CLI (msstore) for publishing Windows applications to the Microsoft Store. Use when asked to configure Store credentials, list Store apps, check submission status, publish submissions, manage package flights, set up CI/CD for Store publishing, or integrate with Partner Center. Supports Windows App SDK/WinUI, UWP, .NET MAUI, Flutter, Electron, React Native, and PWA applications. | None | | [multi-stage-dockerfile](../skills/multi-stage-dockerfile/SKILL.md)
`gh skills install github/awesome-copilot multi-stage-dockerfile` | Create optimized multi-stage Dockerfiles for any language or framework | None | | [mvvm-toolkit](../skills/mvvm-toolkit/SKILL.md)
`gh skills install github/awesome-copilot mvvm-toolkit` | CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia. | `references/end-to-end-walkthrough.md`
`references/relaycommand-cookbook.md`
`references/source-generators.md`
`references/troubleshooting.md`
`references/validation.md` | diff --git a/skills/msgraph-sdk/SKILL.md b/skills/msgraph-sdk/SKILL.md new file mode 100644 index 00000000..12665d12 --- /dev/null +++ b/skills/msgraph-sdk/SKILL.md @@ -0,0 +1,146 @@ +--- +name: msgraph-sdk +description: 'Integrate Microsoft Graph SDK into any project — .NET, TypeScript/JavaScript, or Python. Covers auth patterns (client credentials, OBO, managed identity), SDK setup, calling Graph APIs, batching, delta queries, change notifications, throttling, and permission scopes. Use when accessing Microsoft 365 data (users, mail, calendar, Teams, files, SharePoint) from any application type.' +--- + +# Microsoft Graph SDK + +Use this skill when integrating Microsoft Graph into an application to access Microsoft 365 data and services. + +Always ground implementation in the current Microsoft Graph SDK documentation and SDK version for the target language rather than relying on memory alone. + +## Determine the target language first + +1. Use the **.NET** workflow when the project contains `.cs`, `.csproj`, or `.sln` files, or when the user asks for C# guidance. Follow [references/dotnet.md](references/dotnet.md). +2. Use the **TypeScript / JavaScript** workflow when the project contains `package.json`, `.ts`, or `.js` files, or when the user asks for Node.js / browser guidance. Follow [references/typescript.md](references/typescript.md). +3. Use the **Python** workflow when the project contains `.py`, `pyproject.toml`, or `requirements.txt`, or when the user asks for Python guidance. Follow [references/python.md](references/python.md). +4. If multiple languages are present, match the language of the files being edited or ask the user. + +## Always consult live documentation + +- Microsoft Graph overview: +- Graph Explorer (try calls live): +- Graph permissions reference: +- Use Microsoft Docs MCP tooling when available to fetch current API shapes and SDK samples. + +## Authentication — choose the right pattern + +Selecting the wrong auth flow is the most common Graph integration mistake. Apply this decision tree before writing any auth code: + +| Scenario | Flow to use | +|---|---| +| Background service / daemon with no user | **Client credentials** (app-only) | +| Agent or API acting on behalf of a signed-in user | **On-Behalf-Of (OBO)** | +| App running in Azure (Function, Container App, VM) | **Managed Identity** (preferred over secrets) | +| CLI tool or local dev script | **Device code** or **interactive browser** | +| Single-page app (browser only) | **Authorization code + PKCE** | + +- Never use client credentials when a user context is required — Graph enforces this at the permission level (application vs. delegated). +- Prefer `DefaultAzureCredential` in Azure-hosted apps; it tries managed identity first and falls back gracefully for local dev. +- Never hardcode secrets. Use environment variables, Azure Key Vault, or the Secret Manager. + +## Core SDK usage patterns + +### Building the client + +Always construct `GraphServiceClient` once and reuse it (it manages token caching internally). + +Pass a credential from the Azure Identity library — never build raw HTTP clients manually. + +### Making calls + +- Use the fluent builder API: `client.Users[userId].Messages.GetAsync(...)`. +- Always `await` async calls. +- Specify `$select` to limit returned fields — Graph returns large default payloads. +- Use `$filter` server-side rather than filtering returned collections in memory. +- Use `$expand` to fetch related resources in a single call when relationships are small. + +### Pagination + +Graph paginates collections. Never assume all items arrive in one response: +- Check for an `@odata.nextLink` on the response. +- Use the SDK's `PageIterator` helper (available in all three SDKs) to walk pages automatically. +- Set `$top` to control page size (max varies by resource, typically 999). + +## Advanced patterns + +### Batch requests + +Combine up to 20 independent Graph calls into a single HTTP request using the `$batch` endpoint. Use batching when: +- Initializing data for a dashboard or agent that needs multiple resources upfront. +- Reducing latency in high-call-count operations. + +Batch responses arrive out of order — match them by the `id` field you assigned each request. + +### Delta queries + +Use delta queries to sync changes incrementally instead of polling full collections: +- First call: `GET /users/delta` returns all items + a `@odata.deltaLink`. +- Subsequent calls: use the `deltaLink` to receive only what changed since the last sync. +- Supported on: users, groups, messages, calendar events, Teams channels, and more. +- Store the `deltaLink` durably (database, blob) between sync runs. + +### Change notifications (webhooks) + +Subscribe to resource changes with `POST /subscriptions`: +- Graph delivers change events to your HTTPS notification URL. +- Subscriptions expire — renew them before `expirationDateTime` (max varies by resource; typically 1–3 days for mail/calendar, up to 4230 minutes for users/groups). +- Validate the subscription handshake: Graph sends a `validationToken` query parameter on creation — echo it back as plain text with HTTP 200. +- Use lifecycle notifications (`notificationUrl` + `lifecycleNotificationUrl`) to handle missed events and reauthorization. +- For high-volume scenarios prefer **change notifications with resource data** (requires additional encryption setup). + +### Throttling + +Graph throttles aggressively. Always handle HTTP 429: +- Read the `Retry-After` header — it specifies exact seconds to wait, not a fixed backoff. +- The SDK's built-in retry middleware handles 429 automatically when configured; enable it explicitly. +- Avoid fan-out patterns that hit Graph with hundreds of parallel requests; use batching or queuing instead. + +## Permissions + +Get permissions right before writing auth code — wrong scopes result in 403 errors that are hard to debug later. + +- Application permissions run without a user (daemon / service). Require admin consent. +- Delegated permissions run in the context of a signed-in user. Some require admin consent. +- Request the **minimum permissions** needed. Graph's permission reference lists least-privilege options for every operation. +- Use the Graph Explorer to test which permissions a call actually requires before coding. +- In Azure app registrations: grant API permissions → Microsoft Graph → select type (Application or Delegated) → grant admin consent where required. + +## Common Graph resources — quick reference + +| Goal | Resource path | +|---|---| +| Get signed-in user's profile | `GET /me` | +| List user's mailbox messages | `GET /me/messages` | +| Send an email | `POST /me/sendMail` | +| List calendar events | `GET /me/events` | +| Get user's OneDrive root | `GET /me/drive/root/children` | +| List Teams the user is in | `GET /me/joinedTeams` | +| Post a Teams channel message | `POST /teams/{id}/channels/{id}/messages` | +| List SharePoint site lists | `GET /sites/{siteId}/lists` | +| Search across M365 | `POST /search/query` | +| List all users in tenant (app-only) | `GET /users` | +| Get group members | `GET /groups/{id}/members` | + +In similar fashion, use the SDK's fluent API to navigate to these resources in code. + +## Workflow + +1. Determine the target language and read the matching reference file. +2. Identify the auth scenario and choose the correct flow from the table above. +3. Fetch current SDK docs and Graph Explorer examples before making implementation choices. +4. Apply least-privilege permissions — confirm in the Graph permissions reference. +5. Implement pagination from the start — don't assume single-page responses. +6. Enable retry middleware for throttling from day one. +7. For syncing scenarios, prefer delta queries over polling. +8. Use the language-specific package names, auth provider setup, and code patterns from the chosen reference file. + +## Completion criteria + +- Auth flow matches the scenario (not defaulting to client credentials for user-context calls). +- `GraphServiceClient` is constructed once and reused. +- All collection reads handle pagination. +- Throttling (429) is handled via retry middleware or explicit `Retry-After` logic. +- Permissions are scoped to the minimum required. +- No secrets or credentials are hardcoded. +- Code matches current SDK version patterns for the selected language. diff --git a/skills/msgraph-sdk/references/dotnet.md b/skills/msgraph-sdk/references/dotnet.md new file mode 100644 index 00000000..14a024a7 --- /dev/null +++ b/skills/msgraph-sdk/references/dotnet.md @@ -0,0 +1,212 @@ +# Microsoft Graph SDK for .NET + +Use this reference when the target project is written in C# or another .NET language. + +## Authoritative sources + +- SDK repository: +- Samples: +- SDK changelog: + +## Packages + +```xml + + + + + +``` + +Install via CLI: +```bash +dotnet add package Microsoft.Graph +dotnet add package Azure.Identity +``` + +## Client setup + +### Managed Identity (Azure-hosted apps — preferred) + +```csharp +using Azure.Identity; +using Microsoft.Graph; + +var credential = new DefaultAzureCredential(); +var graphClient = new GraphServiceClient(credential); +``` + +### Client credentials (app-only / daemon) + +```csharp +var credential = new ClientSecretCredential( + tenantId: Environment.GetEnvironmentVariable("AZURE_TENANT_ID"), + clientId: Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"), + clientSecret: Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") +); +var graphClient = new GraphServiceClient(credential); +``` + +Prefer `ClientCertificateCredential` over `ClientSecretCredential` in production. + +### On-Behalf-Of (OBO) — agent / API acting as the signed-in user + +```csharp +// incomingToken is the bearer token received from the caller +var credential = new OnBehalfOfCredential( + tenantId: Environment.GetEnvironmentVariable("AZURE_TENANT_ID"), + clientId: Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"), + clientSecret: Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET"), + userAssertion: new UserAssertion(incomingToken) +); +var graphClient = new GraphServiceClient(credential); +``` + +### Interactive (local dev / CLI) + +```csharp +var credential = new InteractiveBrowserCredential(); +var graphClient = new GraphServiceClient(credential); +``` + +## Common call patterns + +### Get a resource with field selection + +```csharp +var user = await graphClient.Me.GetAsync(config => +{ + config.QueryParameters.Select = ["displayName", "mail", "jobTitle"]; +}); +``` + +### List with filter and select + +```csharp +var messages = await graphClient.Me.Messages.GetAsync(config => +{ + config.QueryParameters.Filter = "isRead eq false"; + config.QueryParameters.Select = ["subject", "from", "receivedDateTime"]; + config.QueryParameters.Top = 25; + config.QueryParameters.Orderby = ["receivedDateTime desc"]; +}); +``` + +### Pagination with PageIterator + +```csharp +var messages = await graphClient.Me.Messages.GetAsync(); + +var allMessages = new List(); +var pageIterator = PageIterator + .CreatePageIterator(graphClient, messages, (msg) => + { + allMessages.Add(msg); + return true; // return false to stop early + }); + +await pageIterator.IterateAsync(); +``` + +### Send an email + +```csharp +await graphClient.Me.SendMail.PostAsync(new SendMailPostRequestBody +{ + Message = new Message + { + Subject = "Hello from Graph", + Body = new ItemBody { ContentType = BodyType.Text, Content = "Test message" }, + ToRecipients = [new Recipient { EmailAddress = new EmailAddress { Address = "user@contoso.com" } }] + } +}); +``` + +### Post a Teams channel message + +```csharp +await graphClient.Teams[teamId].Channels[channelId].Messages.PostAsync(new ChatMessage +{ + Body = new ItemBody { ContentType = BodyType.Html, Content = "Hello from Graph!" } +}); +``` + +## Batch requests + +```csharp +using Microsoft.Graph.Models; + +var batchRequestContent = new BatchRequestContentCollection(graphClient); + +var meRequest = await batchRequestContent.AddBatchRequestStepAsync( + graphClient.Me.ToGetRequestInformation()); +var messagesRequest = await batchRequestContent.AddBatchRequestStepAsync( + graphClient.Me.Messages.ToGetRequestInformation()); + +var batchResponse = await graphClient.Batch.PostAsync(batchRequestContent); + +var me = await batchResponse.GetResponseByIdAsync(meRequest); +var msgs = await batchResponse.GetResponseByIdAsync(messagesRequest); +``` + +## Delta queries + +```csharp +// First sync — get all + deltaLink +var deltaResponse = await graphClient.Users.Delta.GetAsDeltaGetResponseAsync(); +string? deltaLink = null; + +var pageIterator = PageIterator + .CreatePageIterator(graphClient, deltaResponse, (user) => { /* process */ return true; }, + (req) => { deltaLink = /* extract from response */; return req; }); + +await pageIterator.IterateAsync(); +// Store deltaLink for next run + +// Subsequent sync — only changes +// Use the stored deltaLink directly as the next request URL +``` + +## Throttling / retry middleware + +The SDK includes retry middleware enabled by default. For explicit control: + +```csharp +var handlers = GraphClientFactory.CreateDefaultHandlers(); +// RetryHandler is included; configure max retries if needed +var httpClient = GraphClientFactory.Create(handlers); +var graphClient = new GraphServiceClient(httpClient, credential); +``` + +Always check `Retry-After` if building custom retry logic — do not use fixed exponential backoff. + +## Dependency injection (ASP.NET Core / .NET Worker) + +```csharp +// Program.cs +builder.Services.AddSingleton(_ => +{ + var credential = new DefaultAzureCredential(); + return new GraphServiceClient(credential); +}); +``` + +## .NET-specific guidance + +- Target .NET 8+ for new projects. +- Use `async`/`await` throughout — all Graph SDK calls are async. +- Register `GraphServiceClient` as a singleton (it caches tokens internally). +- Use `ILogger` to log Graph exceptions — catch `ODataError` for Graph-specific error details. +- For ASP.NET Core APIs using OBO, inject the incoming token from `IHttpContextAccessor` and construct the credential per-request (not as a singleton). + +```csharp +// Catching Graph errors +try +{ + var user = await graphClient.Me.GetAsync(); +} +catch (ODataError odataError) +{ + Console.WriteLine($"Graph error: {odataError.Error?.Code} - {odataError.Error?.Message}"); +} +``` diff --git a/skills/msgraph-sdk/references/python.md b/skills/msgraph-sdk/references/python.md new file mode 100644 index 00000000..7af241d4 --- /dev/null +++ b/skills/msgraph-sdk/references/python.md @@ -0,0 +1,290 @@ +# Microsoft Graph SDK for Python + +Use this reference when the target project is written in Python. + +## Authoritative sources + +- SDK repository: +- Samples: +- SDK changelog: + +## Packages + +```bash +pip install msgraph-sdk azure-identity +``` + +Or in `requirements.txt` / `pyproject.toml`: + +``` +msgraph-sdk>=1.0.0 +azure-identity>=1.15.0 +``` + +## Client setup + +### Managed Identity (Azure-hosted apps — preferred) + +```python +from azure.identity.aio import DefaultAzureCredential +from msgraph import GraphServiceClient + +credential = DefaultAzureCredential() +graph_client = GraphServiceClient(credential) +``` + +The Python SDK is async-first (`asyncio`). Use `azure.identity.aio` (async variants), not `azure.identity`. + +### Client credentials (app-only / daemon) + +```python +import os +from azure.identity.aio import ClientSecretCredential +from msgraph import GraphServiceClient + +credential = ClientSecretCredential( + tenant_id=os.environ["AZURE_TENANT_ID"], + client_id=os.environ["AZURE_CLIENT_ID"], + client_secret=os.environ["AZURE_CLIENT_SECRET"], +) + +graph_client = GraphServiceClient(credential) +``` + +Prefer `CertificateCredential` over `ClientSecretCredential` in production. + +### On-Behalf-Of (OBO) — agent / API acting as the signed-in user + +```python +from azure.identity.aio import OnBehalfOfCredential + +# incoming_token is the bearer token from the caller +credential = OnBehalfOfCredential( + tenant_id=os.environ["AZURE_TENANT_ID"], + client_id=os.environ["AZURE_CLIENT_ID"], + client_secret=os.environ["AZURE_CLIENT_SECRET"], + user_assertion=incoming_token, +) + +graph_client = GraphServiceClient(credential) +``` + +Construct a new `GraphServiceClient` per request for OBO — the credential is user-scoped. + +### Device code (CLI / local dev) + +```python +from azure.identity.aio import DeviceCodeCredential + +credential = DeviceCodeCredential( + client_id=os.environ["AZURE_CLIENT_ID"], + tenant_id=os.environ["AZURE_TENANT_ID"], +) +graph_client = GraphServiceClient(credential, scopes=["User.Read", "Mail.Read"]) +``` + +## Common call patterns + +All Graph SDK calls in Python are async. Always run inside an async context. + +### Get a resource with field selection + +```python +import asyncio +from msgraph.generated.me.me_request_builder import MeRequestBuilder +from kiota_abstractions.base_request_configuration import RequestConfiguration + +async def get_my_profile(): + query_params = MeRequestBuilder.MeRequestBuilderGetQueryParameters( + select=["displayName", "mail", "jobTitle"] + ) + config = RequestConfiguration(query_parameters=query_params) + user = await graph_client.me.get(request_configuration=config) + return user + +asyncio.run(get_my_profile()) +``` + +### List messages with filter and select + +```python +from msgraph.generated.me.messages.messages_request_builder import MessagesRequestBuilder + +async def get_unread_messages(): + query_params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters( + filter="isRead eq false", + select=["subject", "from", "receivedDateTime"], + top=25, + orderby=["receivedDateTime desc"], + ) + config = RequestConfiguration(query_parameters=query_params) + result = await graph_client.me.messages.get(request_configuration=config) + return result +``` + +### Pagination with PageIterator + +```python +from msgraph.generated.models.message import Message +from msgraph.core import PageIterator + +async def get_all_messages(): + first_page = await graph_client.me.messages.get() + all_messages: list[Message] = [] + + async def process_message(message: Message) -> bool: + all_messages.append(message) + return True # return False to stop early + + page_iterator = PageIterator( + response=first_page, + request_adapter=graph_client.request_adapter, + constructor=Message, + ) + await page_iterator.iterate(callback=process_message) + return all_messages +``` + +### Send an email + +```python +from msgraph.generated.models.message import Message +from msgraph.generated.models.item_body import ItemBody +from msgraph.generated.models.body_type import BodyType +from msgraph.generated.models.recipient import Recipient +from msgraph.generated.models.email_address import EmailAddress +from msgraph.generated.me.send_mail.send_mail_post_request_body import SendMailPostRequestBody + +async def send_email(): + body = SendMailPostRequestBody( + message=Message( + subject="Hello from Graph", + body=ItemBody(content_type=BodyType.Text, content="Test message"), + to_recipients=[ + Recipient(email_address=EmailAddress(address="user@contoso.com")) + ], + ) + ) + await graph_client.me.send_mail.post(body) +``` + +### Post a Teams channel message + +```python +from msgraph.generated.models.chat_message import ChatMessage +from msgraph.generated.models.item_body import ItemBody +from msgraph.generated.models.body_type import BodyType + +async def post_channel_message(team_id: str, channel_id: str): + message = ChatMessage( + body=ItemBody(content_type=BodyType.Html, content="Hello from Graph!") + ) + await graph_client.teams.by_team_id(team_id).channels.by_channel_id(channel_id).messages.post(message) +``` + +## Batch requests + +```python +from kiota_http.middleware.options import ResponseHandlerOption +import json + +async def batch_example(): + batch_body = { + "requests": [ + {"id": "1", "method": "GET", "url": "/me"}, + {"id": "2", "method": "GET", "url": "/me/messages?$top=5&$select=subject"}, + ] + } + # Use the raw HTTP client for batch + response = await graph_client.request_adapter.send_primitive_async( + # Alternatively, use the requests library with a token from the credential + ) +``` + +For batch in Python, it's often simpler to use `httpx` with an acquired token when the batch helper is not yet fully supported: + +```python +import httpx +from azure.identity.aio import ClientSecretCredential + +async def batch_with_httpx(credential): + token = await credential.get_token("https://graph.microsoft.com/.default") + async with httpx.AsyncClient() as client: + response = await client.post( + "https://graph.microsoft.com/v1.0/$batch", + headers={"Authorization": f"Bearer {token.token}"}, + json={ + "requests": [ + {"id": "1", "method": "GET", "url": "/me"}, + {"id": "2", "method": "GET", "url": "/me/messages?$top=5"}, + ] + }, + ) + return response.json() +``` + +## Delta queries + +```python +async def delta_sync(stored_delta_link: str | None = None): + if stored_delta_link: + # Use delta link directly + response = await graph_client.request_adapter.send_async(...) + else: + response = await graph_client.users.delta.get() + + users = [] + async def collect(user): + users.append(user) + return True + + page_iterator = PageIterator(response=response, request_adapter=graph_client.request_adapter, constructor=...) + await page_iterator.iterate(callback=collect) + + delta_link = page_iterator.delta_link # store this for next run + return users, delta_link +``` + +## Throttling / retry + +The SDK's HTTP transport handles 429 retry automatically when using the default `GraphClientFactory`. For explicit control: + +```python +import asyncio +import httpx + +async def call_with_retry(graph_client, call_fn, max_retries=5): + for attempt in range(max_retries): + try: + return await call_fn() + except Exception as e: + if "429" in str(e): + retry_after = int(getattr(e, "retry_after", 10)) + await asyncio.sleep(retry_after) + else: + raise +``` + +## Python-specific guidance + +- The Python Graph SDK is **async-first** — use `asyncio.run()` or an async framework (FastAPI, aiohttp). +- Always use `azure.identity.aio` (not `azure.identity`) for async contexts. +- Close credentials when done: `await credential.close()` or use as async context managers. +- Python SDK model classes use `snake_case` for properties (Graph JSON uses `camelCase` — the SDK maps automatically). +- Use `asyncio.gather()` for concurrent but independent Graph calls (mind throttling limits). +- For FastAPI: use lifespan events to init `GraphServiceClient` once and close the credential on shutdown. + +```python +# FastAPI integration example +from contextlib import asynccontextmanager +from fastapi import FastAPI + +@asynccontextmanager +async def lifespan(app: FastAPI): + credential = DefaultAzureCredential() + app.state.graph_client = GraphServiceClient(credential) + yield + await credential.close() + +app = FastAPI(lifespan=lifespan) +``` diff --git a/skills/msgraph-sdk/references/typescript.md b/skills/msgraph-sdk/references/typescript.md new file mode 100644 index 00000000..2889d356 --- /dev/null +++ b/skills/msgraph-sdk/references/typescript.md @@ -0,0 +1,257 @@ +# Microsoft Graph SDK for TypeScript / JavaScript + +Use this reference when the target project uses TypeScript or JavaScript (Node.js or browser). + +## Authoritative sources + +- SDK repository: +- Samples: +- SDK changelog: + +## Packages + +```bash +npm install @microsoft/microsoft-graph-client @azure/identity +npm install -D @microsoft/microsoft-graph-types # TypeScript type definitions +``` + +For Node.js environments, also install the fetch polyfill: + +```bash +npm install node-fetch +``` + +## Client setup + +### Managed Identity (Azure-hosted apps — preferred) + +```typescript +import { Client } from "@microsoft/microsoft-graph-client"; +import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials/index.js"; +import { DefaultAzureCredential } from "@azure/identity"; + +const credential = new DefaultAzureCredential(); +const authProvider = new TokenCredentialAuthenticationProvider(credential, { + scopes: ["https://graph.microsoft.com/.default"], +}); + +const graphClient = Client.initWithMiddleware({ authProvider }); +``` + +### Client credentials (app-only / daemon) + +```typescript +import { ClientSecretCredential } from "@azure/identity"; + +const credential = new ClientSecretCredential( + process.env.AZURE_TENANT_ID!, + process.env.AZURE_CLIENT_ID!, + process.env.AZURE_CLIENT_SECRET! +); + +const authProvider = new TokenCredentialAuthenticationProvider(credential, { + scopes: ["https://graph.microsoft.com/.default"], +}); + +const graphClient = Client.initWithMiddleware({ authProvider }); +``` + +### On-Behalf-Of (OBO) — agent / API acting as the signed-in user + +```typescript +import { OnBehalfOfCredential } from "@azure/identity"; + +// incomingToken is the bearer token received from the caller (e.g. from req.headers.authorization) +const credential = new OnBehalfOfCredential({ + tenantId: process.env.AZURE_TENANT_ID!, + clientId: process.env.AZURE_CLIENT_ID!, + clientSecret: process.env.AZURE_CLIENT_SECRET!, + userAssertionToken: incomingToken, +}); + +const authProvider = new TokenCredentialAuthenticationProvider(credential, { + scopes: ["https://graph.microsoft.com/.default"], +}); + +const graphClient = Client.initWithMiddleware({ authProvider }); +``` + +For OBO, create a new client per request (credential is user-scoped, not singleton-safe). + +### Interactive (local dev / CLI — Node.js) + +Use `InteractiveBrowserCredential` when a browser is available. Use `DeviceCodeCredential` for headless environments (SSH, CI-adjacent, WSL): + +```typescript +import { InteractiveBrowserCredential, DeviceCodeCredential } from "@azure/identity"; + +// Opens a browser tab — requires redirect URI http://localhost in app registration +const credential = new InteractiveBrowserCredential({ + tenantId: process.env.AZURE_TENANT_ID!, + clientId: process.env.AZURE_CLIENT_ID!, +}); + +// Prints a device code to the terminal — works in any environment +const credential = new DeviceCodeCredential({ + tenantId: process.env.AZURE_TENANT_ID!, + clientId: process.env.AZURE_CLIENT_ID!, + userPromptCallback: (info) => console.log(info.message), +}); +``` + +Both require the app registration platform to be **"Mobile and desktop applications"**. Neither uses a client secret. + +## Common call patterns + +### Get a resource with field selection + +```typescript +import { User } from "@microsoft/microsoft-graph-types"; + +const user: User = await graphClient + .api("/me") + .select("displayName,mail,jobTitle") + .get(); +``` + +### List with filter, select, and ordering + +```typescript +const result = await graphClient + .api("/me/messages") + .filter("isRead eq false") + .select("subject,from,receivedDateTime") + .top(25) + .orderby("receivedDateTime desc") + .get(); +``` + +### Pagination with PageIterator + +```typescript +import { PageIterator } from "@microsoft/microsoft-graph-client"; +import { Message } from "@microsoft/microsoft-graph-types"; + +const firstPage = await graphClient.api("/me/messages").top(25).get(); + +const allMessages: Message[] = []; + +const pageIterator = new PageIterator( + graphClient, + firstPage, + (message: Message) => { + allMessages.push(message); + return true; // return false to stop early + } +); + +await pageIterator.iterate(); +``` + +### Send an email + +```typescript +await graphClient.api("/me/sendMail").post({ + message: { + subject: "Hello from Graph", + body: { contentType: "Text", content: "Test message" }, + toRecipients: [{ emailAddress: { address: "user@contoso.com" } }], + }, +}); +``` + +### Post a Teams channel message + +```typescript +await graphClient.api(`/teams/${teamId}/channels/${channelId}/messages`).post({ + body: { contentType: "html", content: "Hello from Graph!" }, +}); +``` + +### Upload a file to OneDrive (small files ≤ 4 MB) + +```typescript +const content = Buffer.from("file contents"); +await graphClient + .api(`/me/drive/root:/${fileName}:/content`) + .putStream(content); +``` + +For files > 4 MB, use an upload session (`createUploadSession`). + +## Batch requests + +```typescript +const batchRequestBody = { + requests: [ + { id: "1", method: "GET", url: "/me" }, + { id: "2", method: "GET", url: "/me/messages?$top=5&$select=subject" }, + ], +}; + +const batchResponse = await graphClient.api("/$batch").post(batchRequestBody); + +const meResponse = batchResponse.responses.find((r: any) => r.id === "1"); +const messagesResponse = batchResponse.responses.find((r: any) => r.id === "2"); +``` + +## Delta queries + +```typescript +// First sync +let response = await graphClient.api("/users/delta").get(); +const users: any[] = []; + +while (response["@odata.nextLink"]) { + users.push(...response.value); + response = await graphClient.api(response["@odata.nextLink"]).get(); +} +users.push(...response.value); + +const deltaLink: string = response["@odata.deltaLink"]; +// Store deltaLink durably for next sync run + +// Next sync — only changes +const changesResponse = await graphClient.api(deltaLink).get(); +``` + +## Throttling / retry middleware + +The SDK includes retry middleware by default. For explicit configuration: + +```typescript +import { + Client, + RetryHandlerOptions, + RetryHandler, + MiddlewareFactory, +} from "@microsoft/microsoft-graph-client"; + +const retryOptions = new RetryHandlerOptions({ maxRetries: 5 }); +const middleware = MiddlewareFactory.getDefaultMiddlewareChain(authProvider); + +const graphClient = Client.initWithMiddleware({ middleware }); +``` + +Always honour the `Retry-After` header value — do not use fixed backoff when Graph specifies a wait time. + +## TypeScript-specific guidance + +- Import types from `@microsoft/microsoft-graph-types` for full IntelliSense on Graph resources. +- The `.api()` chain returns `any` — cast to the appropriate type from `@microsoft/microsoft-graph-types`. +- For ESM projects, use the `/index.js` path suffix on deep imports (e.g., `azureTokenCredentials/index.js`). +- Use `async`/`await` consistently — all Graph calls return Promises. +- Singleton the `graphClient` in application-level code (e.g., Express app init); for OBO flows, construct per-request. +- In Node.js 18+, `fetch` is available natively — no polyfill needed. + +```typescript +// Type-safe response example +import { MessageCollectionResponse } from "@microsoft/microsoft-graph-types"; + +const response: MessageCollectionResponse = await graphClient + .api("/me/messages") + .select("subject,from") + .get(); + +const messages = response.value ?? []; +```