
Microsoft Graph Gateway
- 6 installs
- 23 repo stars
- Updated August 4, 2026
- aktsmm/agent-skills
microsoft-graph-gateway is a Claude skill that routes Microsoft Graph work in a workspace, using workIQ for common reads and Microsoft Graph for writes and precise reads.
About
Routes Microsoft Graph work in a workspace, preferring workIQ for common reads and Microsoft Graph for writes and precise gap-reads. A developer uses it to read or write Outlook mail, calendar, contacts, OneDrive/SharePoint files, Teams, Planner, To Do, users, groups and directory data from VS Code. It classifies each request as read, gap-read or write, enforces explicit confirmation before writes, and executes through a Graph CLI, MCP server or other gateway substrate.
- Routes M365 requests: workIQ for common reads, Microsoft Graph for writes and precise reads
- Requires explicit confirmation before any Graph write; deletes disabled by default
- Executes through a pluggable substrate (Graph CLI, MCP server, or gateway)
Microsoft Graph Gateway by the numbers
- 6 all-time installs (skills.sh)
- Ranked #1,691 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
microsoft-graph-gateway capabilities & compatibility
Free skill; requires Microsoft 365 tenant access and a Graph substrate/app registration
- Capabilities
- biz ops setup · email automation · calendar management
- Works with
- outlook · sharepoint · teams · gmail · google drive
- Use cases
- email · api development · orchestration
- IDEs
- vscode
- Pricing
- Bring your own API key
What microsoft-graph-gateway says it does
Route Microsoft Graph work in this workspace.
Use Microsoft Graph for all writes.
npx skills add https://github.com/aktsmm/agent-skills --skill microsoft-graph-gatewayAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 23 |
| Last updated | August 4, 2026 |
| Repository | aktsmm/agent-skills ↗ |
What it does
Read or write Microsoft 365 data (mail, calendar, files, Teams) from VS Code via workIQ or Microsoft Graph.
Who is it for?
Routing and safely executing Microsoft 365 reads and writes across mail, calendar, files, Teams and directory
Skip if: Non-Graph tasks, or when a purely common read is fully answered by workIQ alone
When should I use this skill?
Reading or writing M365 data from VS Code, or needing an exact Graph endpoint, permission or payload
What you get
Correctly routed Graph operations with least-privilege scopes and confirmed writes
- Routed Graph reads/writes
- Write confirmation summaries
- Least-privilege permission profiles
By the numbers
- 8-step procedure
- 3 request classes (common read, gap-read, write)
Files
Microsoft Graph Gateway
Use this skill as the orchestration shell for Microsoft Graph work in this workspace.
This skill does not try to abstract all of Microsoft Graph into a fixed checklist. Instead, it does two things:
1. Route common read requests to the right source. 2. Enforce safe execution rules before Graph writes or advanced reads.
When To Use
- The user wants to read or write Microsoft 365 data from VS Code.
- The request mentions mail, email, inbox, Outlook, meetings, calendar, contacts, OneDrive, SharePoint, Teams, Planner, To Do, users, groups, or Microsoft Graph.
- The user wants an exact Microsoft Graph endpoint, permission, query option, or payload.
- WorkIQ can likely answer a common read request, but Graph may be needed as a fallback.
Routing Rules
Read Routing
- Prefer WorkIQ first for common read scenarios such as inbox checks, meeting lookups, file discovery, and lightweight summaries.
- Use Microsoft Graph for gap-read scenarios when WorkIQ cannot answer, returns insufficient detail, or the user asks for exact Graph semantics.
- Go straight to Microsoft Graph when the user asks for:
- exact endpoint names or raw REST calls
- precise properties,
$select,$filter,$expand, or API version control - Teams, Planner, To Do, directory, or permission-oriented data
- schema, permission, throttling, batching, delta, or webhook behavior
Write Routing
- Use Microsoft Graph for all writes.
- Treat send, create, update, move, upload, reply, assign, and respond operations as writes.
- Require explicit confirmation before any write.
- Keep delete actions disabled by default or behind a stronger confirmation step.
Operating Model
- This skill is a thin shell.
- The execution substrate can be a Graph CLI, a self-hosted MCP server, or another Graph gateway.
- Do not invent endpoints or permissions from memory when an authoritative lookup is available.
- Keep delegated and application permission flows separate.
- Prefer least-privileged scopes and minimal projections.
Procedure
1. Classify the request as common read, gap-read, or write. 2. For common read, try the WorkIQ route first. 3. For gap-read or write, identify the target Microsoft Graph surface. 4. Check the capability, routing, and substrate references before choosing the execution path.
- raw execution contract
- permission profiles
- curated tool catalog
5. For Microsoft Graph execution, determine:
- resource area
- endpoint or tool
- API version
- least-privileged permission profile
- minimal response shape
6. Before any write, present a concise confirmation summary covering target, action, and payload intent. 7. Execute through the chosen substrate. 8. Report the result, including any permission, throttling, or follow-up concerns.
Script Entry Points
- Use Invoke-GraphGateway.ps1 for raw Graph execution through the selected substrate.
- Use invoke-graph-gateway.sh on macOS or Linux when
pwshis installed. - Use New-GraphWriteConfirmation.ps1 to generate a concise write confirmation summary before execution.
- Use Test-GraphGatewayScaffold.ps1 to smoke-test the current scaffold without live Graph execution.
- Use test-graph-gateway-scaffold.sh on macOS or Linux when
pwshis installed. - Use Get-GraphGatewayRunnerStatus.ps1 to inspect runner discovery and auth readiness.
- Use get-graph-gateway-runner-status.sh on macOS or Linux when
pwshis installed. - Use Install-MsGraphRunner.ps1 to bootstrap the preferred
merill/msgraphrunner into the workspace when you explicitly want local setup. - Use New-GraphEventResponseRequest.ps1 to generate accept, decline, and tentative meeting-response requests.
- Use New-GraphGatewayAppConfig.ps1 to generate environment snippets for a tenant-approved custom app registration.
Example Assets
- Read Mail Request
- Send Mail Request
- List Events Request
- Create Event Request
- Accept Event Request
- Decline Event Request
- Tentative Event Request
- Custom App Env Example
Done Criteria
- The request was routed to WorkIQ or Microsoft Graph for an explicit reason.
- Graph calls used least-privileged intent and minimal response shape where practical.
- Writes were confirmed before execution.
- Deletes were blocked or separately escalated.
- The response states what happened and any remaining risk or next action.
References
- Capability Matrix
- Routing And Safety
- Substrate Selection
- Raw Execution Contract
- Permission Profiles
- Curated Tools First Wave
- Environment Setup
- Prerequisites
- Troubleshooting
{
"method": "POST",
"path": "/me/events/{event-id}/accept",
"apiVersion": "v1.0",
"permissionProfile": "calendar-write",
"intentSummary": "Accept a meeting request",
"body": {
"comment": "Accepted.",
"sendResponse": true
}
}
{
"method": "POST",
"path": "/me/events",
"apiVersion": "v1.0",
"permissionProfile": "calendar-write",
"intentSummary": "Create a short calendar smoke-test event",
"body": {
"subject": "[Graph Gateway Test] Calendar smoke test",
"body": {
"contentType": "Text",
"content": "Temporary calendar smoke test created by the Microsoft Graph gateway."
},
"start": {
"dateTime": "2026-05-21T18:00:00",
"timeZone": "Tokyo Standard Time"
},
"end": {
"dateTime": "2026-05-21T18:15:00",
"timeZone": "Tokyo Standard Time"
}
}
}
MSGRAPH_CLIENT_ID=<your-entra-app-client-id>
MSGRAPH_TENANT_ID=<your-tenant-id-or-domain>
GRAPH_GATEWAY_RUNNER=<path-to-msgraph-runner>{
"method": "POST",
"path": "/me/events/{event-id}/decline",
"apiVersion": "v1.0",
"permissionProfile": "calendar-write",
"intentSummary": "Decline a meeting request",
"body": {
"comment": "Declined.",
"sendResponse": true
}
}
{
"method": "GET",
"path": "/me/events",
"apiVersion": "v1.0",
"query": {
"select": "subject,start,end,organizer,responseStatus",
"top": 10,
"orderby": "start/dateTime"
},
"permissionProfile": "calendar-read-basic",
"intentSummary": "List upcoming events with a minimal useful projection"
}
{
"method": "GET",
"path": "/me/messages",
"apiVersion": "v1.0",
"query": {
"select": "subject,from,receivedDateTime",
"top": 5
},
"permissionProfile": "mail-read-basic",
"intentSummary": "Read the latest messages with a minimal projection"
}{
"method": "POST",
"path": "/me/sendMail",
"apiVersion": "v1.0",
"permissionProfile": "mail-write",
"intentSummary": "Send a short status email to the project group",
"body": {
"message": {
"subject": "Status update",
"body": {
"contentType": "Text",
"content": "All checks passed."
}
}
}
}
{
"method": "POST",
"path": "/me/events/{event-id}/tentativelyAccept",
"apiVersion": "v1.0",
"permissionProfile": "calendar-write",
"intentSummary": "Tentatively accept a meeting request",
"body": {
"comment": "Tentative.",
"sendResponse": true
}
}
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
## English
Copyright (c) 2026 yamapan (aktsmm)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
*(Please contact the author if you wish to use this material for commercial purposes.)*
- **ShareAlike** — If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
**AI/ML Training Restriction** — Use of this content for AI/ML training, data
mining, or other analytical purposes is prohibited without explicit permission.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
## 日本語
Copyright (c) 2026 yamapan (aktsmm)
この作品はクリエイティブ・コモンズ 表示-非営利-継承 4.0 国際ライセンスの下に提供されています。
あなたは以下の条件に従う限り、自由に:
- **共有** — どのようなメディアやフォーマットでも資料を複製・再配布できます
- **翻案** — 資料をリミックス、変形、および加工することができます
以下の条件に従ってください:
- **表示** — あなたは適切なクレジットを表示し、ライセンスへのリンクを提供し、
変更があったらその旨を示さなければなりません。これらは合理的であればどのような方法で
行っても構いませんが、許諾者があなたやあなたの利用行為を支持していると示唆するような
方法は除きます。
- **非営利** — あなたは営利目的でこの資料を利用してはなりません。
(※商用利用をご希望の場合は、別途ご連絡ください。)
- **継承** — もしあなたがこの資料をリミックス、変形、または加工した場合、
あなたはあなたの貢献部分を元の作品と同じライセンスの下で配布しなければなりません。
追加的な制約は課せません — あなたは、このライセンスが他の者に許諾することを法的に
制限するような法的条項や技術的手段を適用してはなりません。
**AI/MLトレーニング制限** — 本コンテンツをAI/MLモデルのトレーニング、データマイニング、
その他の解析目的での使用は明示的な許可なく禁止されています。
ライセンス全文: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ja
---
## Special Permission for Microsoft Employees / Microsoft 社員向け特別許諾
### English
Microsoft Corporation employees are granted permission to use, copy, modify, and
distribute this material for any purpose within the scope of their employment
duties at Microsoft, including internal business use and customer-facing
activities, without the NonCommercial restriction of this license.
This special permission applies only to work performed as part of official
Microsoft business activities.
### 日本語
Microsoft Corporation の社員は、Microsoft での業務の範疇において、本資料を社内業務
および顧客対応を含むあらゆる目的で使用、複製、改変、配布することが許諾されます。
この場合、本ライセンスの「非営利」制限は適用されません。
この特別許諾は、Microsoft の公式な業務活動の一環として行われる作業にのみ適用されます。
---
## Disclaimer / 免責事項
### English
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 日本語
本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、商品性、
特定目的への適合性、および権利非侵害についての保証を含むがこれに限定されない、
いかなる種類の保証も伴いません。作者または著作権者は、契約行為、不法行為、
またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの
使用またはその他の扱いによって生じる一切の請求、損害、その他の責任について
責任を負いません。
Capability Matrix
This matrix defines which surfaces belong in the gateway and what kind of support exists today.
| Area | Microsoft Graph Surface | Current Best Fit | Notes |
|---|---|---|---|
| Outlook mail | Raw Graph + curated mail tools | Read, search, send, reply, move, and folder operations are all available in Graph. | |
| Calendar | Outlook calendar | Raw Graph + curated calendar tools | Event creation, update, response, and availability checks are first-class Graph scenarios. |
| Contacts / People | Outlook contacts, People | Raw Graph, partial curated support | Contacts are common; People and relevance-based scenarios usually need exact Graph handling. |
| Files / OneDrive | OneDrive driveItem APIs | Raw Graph + curated file tools | Upload, download, search, browse, and content operations are common. |
| SharePoint | Sites, drives, lists | Raw Graph, targeted curated support | SharePoint often needs exact IDs, site paths, permission inspection, or sharing semantics. |
| Teams | Teams, chats, channels, meetings | Raw Graph first | Broad support exists, but current candidate substrates are thinner here. |
| Directory | Users, groups, apps, devices | Raw Graph first | Exact permissions, filters, and tenant-specific policy handling matter here. |
| Planner / Tasks / To Do | Planner and To Do APIs | Raw Graph first | Useful for later convenience tools, but raw access should exist first. |
| Search / Reports / Insights | Search, reports, people, insights | Raw Graph first | Often require exact endpoint knowledge and careful permission handling. |
Design Consequence
- Do not try to represent the full Graph surface as only curated commands.
- Preserve a generic raw execution path so the skill can reach arbitrary endpoints.
- Add convenience tools only for high-frequency workflows after the raw path exists.
Curated Tools First Wave
This document defines the first convenience tools that sit above the raw Graph executor.
Goal
- Cover high-frequency productivity actions with short, predictable language.
- Keep the list intentionally small so the gateway does not explode into hundreds of brittle wrappers.
- Preserve the raw executor as the escape hatch for everything else.
First-Wave Tool Set
| Tool | Purpose | Notes |
|---|---|---|
list-messages | Read recent messages with minimal projection | Prefer WorkIQ first for simple inbox reads |
get-message | Read a specific message in detail | Use Graph when exact properties or body content matter |
send-message | Send a new email | Write confirmation required |
reply-message | Reply or reply-all | Write confirmation required |
list-events | Read upcoming events | Prefer WorkIQ first for simple schedule reads |
create-event | Create a meeting or appointment | Write confirmation required |
update-event | Reschedule or modify an event | Write confirmation required |
accept-event | Accept a meeting request | Write confirmation required |
decline-event | Decline a meeting request | Write confirmation required |
tentative-event | Tentatively accept a meeting request | Write confirmation required |
check-availability | Inspect free/busy windows | Usually Graph directly |
search-files | Search OneDrive or SharePoint files | Prefer WorkIQ first for broad discovery |
get-file | Read or download a file reference | Decide later whether content stays inline or external |
upload-file | Upload or replace content | Write confirmation required |
list-contacts | Read contacts | Convenience wrapper over common contact views |
get-user-or-group | Read directory targets by exact identifier | Helps avoid broad raw directory calls for common cases |
raw-graph-call | Escape hatch for arbitrary Graph execution | Always available for unsupported surfaces |
Not In The First Wave
- full Teams surface
- Planner plan and task management wrappers
- To Do wrappers
- lifecycle and webhook orchestration
- broad SharePoint administration wrappers
- delete wrappers
These should remain raw-Graph-first until stable usage patterns emerge.
Meeting responses are now important enough to treat as first-wave conveniences because they are common, semantically stable, and easy to confirm safely.
Selection Criteria
A workflow belongs in the first wave only if it is:
1. common 2. high-value 3. semantically stable 4. easy to confirm safely 5. likely to benefit from shorter language than a raw endpoint call
Naming Principle
- Use action-oriented names.
- Prefer resource-neutral verbs only when the scope is obvious.
- Avoid adding multiple aliases for the same underlying action.
Environment Setup
This skill currently assumes a Microsoft Graph execution substrate is available on the machine.
Current Runner Preference
1. -RunnerPath argument on the gateway script 2. GRAPH_GATEWAY_RUNNER environment variable 3. MSGRAPH_RUNNER environment variable 4. msgraph on PATH
Current Expectation
The current scaffold is optimized around the merill/msgraph runner.
PowerShell 7 is the common execution layer across Windows, macOS, and Linux for the current scaffold.
Helper Script
- Use Get-GraphGatewayRunnerStatus.ps1 to inspect runner discovery and current auth status.
- Use Install-MsGraphRunner.ps1 to download and unpack the preferred runner when you intentionally want a local substrate.
Recommended Setup Pattern
If msgraph is already on PATH
- No additional configuration is required for runner discovery.
If the runner is not on PATH
- Set
GRAPH_GATEWAY_RUNNERto the full executable path. - Or pass
-RunnerPathexplicitly during execution. - Or bootstrap a local copy with
Install-MsGraphRunner.ps1and then setGRAPH_GATEWAY_RUNNER.
If the default client app is blocked by tenant consent policy
- Set
MSGRAPH_CLIENT_IDto a tenant-approved custom app registration. - Set
MSGRAPH_TENANT_IDto the target tenant. - Re-run sign-in with the new client.
- Use New-GraphGatewayAppConfig.ps1 to generate the environment snippet.
Live Execution Readiness Checklist
1. Runner command is discoverable. 2. Authentication status can be read. 3. Dry-run works without a runner. 4. Write confirmation is generated before any live write. 5. Least-privileged permission profile is chosen before execution. 6. The active client app and tenant policy actually permit the intended write scopes.
Scope Boundary
This skill currently documents and scaffolds the runner contract. It does not yet provision runner binaries, app registrations, or tenant consent flows automatically.
The installer helper downloads the preferred runner, but it still leaves authentication and tenant consent as explicit operator steps.
If a live /me read succeeds but live mail or calendar writes return 403 ErrorAccessDenied, treat tenant consent and app registration as a real prerequisite for write validation.
Permission Profiles
This catalog defines initial permission profiles for interactive delegated use.
Design Rules
- Default to delegated permissions.
- Use least privilege first.
- Do not mix application permissions into the same interactive profile.
- If a request needs more privilege than the current profile, surface that escalation explicitly.
Initial Profiles
| Profile | Typical Areas | Typical Delegated Scopes | Notes |
|---|---|---|---|
mail-read-basic | inbox checks, light summaries | Mail.Read | Common read fallback when WorkIQ is insufficient |
mail-write | send, reply, move, update mail | Mail.ReadWrite, Mail.Send | Needed for most mail mutation flows |
calendar-read-basic | event lookups, availability checks | Calendars.Read | Common meeting and event reads |
calendar-write | create, update, or respond to events | Calendars.ReadWrite | Use for scheduling, updates, and accept or decline workflows |
contacts-basic | contact lookup and update | Contacts.Read, Contacts.ReadWrite | Split later if needed |
files-read-basic | OneDrive and simple file reads | Files.Read | Escalate only when user intent truly needs broader file reach |
files-write | upload, update, move personal files | Files.ReadWrite | Personal or signed-in user scope first |
sharepoint-read-broad | site, drive, or document reads beyond personal scope | Sites.Read.All, optionally Files.Read.All | Higher-risk profile; call out explicitly |
sharepoint-write-broad | site-level file mutation | Sites.ReadWrite.All, optionally Files.ReadWrite.All | Higher-risk profile; require stronger explanation |
directory-read | users, groups, apps, devices | resource-specific least privilege based on endpoint | Often tenant-sensitive; avoid broad fallback guesses |
planner-tasks-write | Planner, To Do, tasks | endpoint-specific least privilege | Keep separate because support patterns vary |
Escalation Guidance
- If the requested operation crosses from user-owned data into tenant-wide or site-wide data, call out the escalation before execution.
- If the operation needs a broad directory or SharePoint scope, say so explicitly in the confirmation summary.
- If the exact least-privileged scope is unclear, perform metadata lookup before attempting execution.
Future Split
This first version intentionally keeps the profile catalog compact. Split profiles further only when:
- user-owned and tenant-wide operations frequently diverge
- consent friction becomes high
- the same profile is being used for too many unrelated operations
Prerequisites
Keep prerequisites short and explicit.
Required
1. VS Code with GitHub Copilot Chat agent mode enabled 2. PowerShell 7 or later for the current scaffold 3. Network access to Microsoft Graph and Microsoft Entra sign-in endpoints 4. A Microsoft Graph execution substrate such as merill/msgraph 5. A signed-in Microsoft account with consent for the required delegated scopes 6. For live write tests, the current tenant and client app must allow the required write scopes such as Mail.Send and Calendars.ReadWrite
Recommended
1. msgraph available on PATH, or GRAPH_GATEWAY_RUNNER set 2. A workspace-local runner install if you do not want to depend on global tools 3. WorkIQ available for common read routing
Current Platform Note
- Windows is supported through the PowerShell scripts directly.
- macOS and Linux are supported through the
pwsh-backed.shwrappers inscripts/. - The current scaffold is cross-platform at the entrypoint level, but live behavior still depends on the installed runner and auth flow.
Light Setup Checklist
1. Run the scaffold smoke test 2. Install or point to a runner 3. Check runner status 4. Sign in to Graph 5. Perform a read test before a write test
Write Test Note
- A successful delegated sign-in does not guarantee write access.
- Live mail and calendar writes can still fail with
403 ErrorAccessDeniedif the current client app or tenant policy does not allow the requested write scopes. - If that happens, use a custom Entra ID app registration or a tenant-approved client configuration for live write validation.
Admin Approval Note
- If the sign-in screen says that administrator approval is required for Microsoft Graph Command Line Tools, the tenant is blocking consent for the current client app.
- In that case, re-signing alone is not enough.
- Use one of these paths:
- ask a tenant admin to approve the app and required delegated scopes
- switch the runner to a tenant-approved custom app registration
- Use New-GraphGatewayAppConfig.ps1 or custom-app.env.example to prepare the environment for a custom app quickly.
Raw Execution Contract
This document defines the generic Graph executor that preserves full reach across Microsoft Graph.
Purpose
- Reach arbitrary Microsoft Graph endpoints without waiting for a curated tool.
- Keep a stable execution contract even if the backend substrate changes.
- Preserve enough structure for routing, confirmation, auditing, and future UI work.
Input Shape
The raw executor should collect the following logical fields before execution.
| Field | Required | Description |
|---|---|---|
method | Yes | GET, POST, PATCH, or PUT in the first implementation |
path | Yes | Relative Graph path such as /me/messages or /users/{id}/calendar/events |
apiVersion | No | v1.0 by default, beta only when justified |
query | No | Structured query options such as $select, $filter, $expand, $top, $orderby |
headers | No | Explicit headers such as Prefer or ConsistencyLevel |
body | No | JSON payload for write operations |
permissionProfile | Yes | Named profile chosen from the permission catalog |
intentSummary | Yes | Short plain-language description of what the call is meant to do |
isWrite | Yes | Derived from method and semantic action |
Output Shape
The raw executor should normalize its result to the following logical shape.
| Field | Description |
|---|---|
statusCode | HTTP status code |
requestSummary | Method, version, and path summary |
responseBody | Parsed JSON body or raw content summary |
nextAction | Suggested follow-up such as pagination, consent, retry, or none |
warnings | Throttling, beta usage, broad permission use, large response, or unsupported semantics |
Execution Rules
1. Default to v1.0. 2. Use beta only when the required feature does not exist in v1.0. 3. Treat POST, PATCH, and PUT as writes. 4. Reject DELETE in the first implementation. 5. Require an explicit permission profile before execution. 6. Encourage $select for entity reads when practical. 7. Respect Retry-After and surface throttling guidance. 8. Preserve the exact path and query semantics instead of silently rewriting them.
Confirmation Rules For Raw Writes
Before a raw write executes, the confirmation summary should include:
1. the target resource path 2. the action in plain language 3. the important body fields being changed or created 4. whether beta is in use 5. whether the permission profile is broader than usual
Why This Matters
- Curated tools will never cover the full Graph surface.
- The raw executor is what makes the gateway truly "all reachable" instead of just "many convenience commands."
Current Script Mapping
- Invoke-GraphGateway.ps1 is the first implementation scaffold of this contract.
- New-GraphWriteConfirmation.ps1 generates the confirmation summary for write operations.
- Test-GraphGatewayScaffold.ps1 verifies the scaffold against example request files.
- read-mail.request.json and send-mail.request.json provide reusable request examples.
- New-GraphEventResponseRequest.ps1 generates request objects for accept, decline, and tentative meeting responses.
Routing And Safety
Read Responsibility
- Prefer WorkIQ for common read requests that map naturally to inbox, meetings, and file discovery.
- Use Microsoft Graph when WorkIQ cannot answer, lacks detail, or the user needs exact Graph semantics.
- Go directly to Graph for:
- endpoint discovery
- permission lookup
- exact filters or projections
- directory, Teams, Planner, To Do, reports, or schema-heavy requests
Write Responsibility
- All writes go through Microsoft Graph.
- Treat the following as writes even if they feel lightweight:
- send
- create
- update
- reply
- forward
- move
- upload
- respond
- assign
Confirmation Policy
Before any write, provide a short confirmation summary with:
1. target resource 2. intended action 3. payload intent in plain language 4. noteworthy risk, if any
Do not execute until the user confirms.
Delete Policy
- Block delete by default in the first implementation.
- If delete is later enabled, require a stronger confirmation step than standard writes.
Permission Policy
- Default to delegated permissions for interactive use.
- Keep application permissions in a separate future profile.
- Use least privilege and avoid broad directory or file scopes unless the operation requires them.
Performance Policy
- Use
$selectwhenever practical. - Use minimal response handling for write operations when supported.
- Respect
Retry-Afteron throttling. - Prefer delta query and change notifications over polling for sync scenarios.
- Keep JSON batching within the platform limit of 20 requests.
Substrate Selection
The gateway skill needs an execution substrate. This reference captures the current recommendation.
Recommended Core
merill/msgraph
Use as the primary generic Graph substrate when you need:
- arbitrary endpoint reach
- permission and schema lookup
- direct Graph execution
- strong write guardrails
Why it fits:
- broad Graph coverage
- explicit safety model for writes
- strong discovery story for endpoints and permissions
Domain Helpers
elyxlz/microsoft-mcp
Best when you need curated productivity tools for:
- calendar
- OneDrive
- contacts
- multi-account flows
XenoXilus/outlook-mcp
Best when you need curated productivity tools for:
- Outlook mail
- calendar
- SharePoint links and files
- attachment and document processing
Not A Primary Substrate
Microsoft MCP Server for Enterprise
Useful as a read-only supplement for directory and Entra questions, but not as the main execution substrate for Outlook and productivity writes.
VS Code Marketplace Extensions
Current marketplace candidates are mainly:
- development helpers
- autocomplete helpers
- read-only viewers
They are not the recommended foundation for a full Graph gateway.
Current Recommendation
1. Use merill/msgraph as the generic Graph core. 2. Add a thin skill shell for routing, confirmation, and policy. 3. Evaluate a productivity-focused MCP helper only where curated tools clearly improve operator experience. 4. Consider a VS Code extension only after the gateway behavior is stable.
Operational Follow-Up
- Pair this reference with Environment Setup before attempting live execution.
Troubleshooting
Admin Approval Required
Symptom:
- The sign-in page says administrator approval is required for Microsoft Graph Command Line Tools.
Meaning:
- The tenant does not allow user consent for the current client app and requested delegated scopes.
What to do:
1. Ask a tenant admin to approve the app. 2. Or switch to a tenant-approved custom app registration. 3. Re-sign in after switching the client ID and tenant ID.
403 ErrorAccessDenied On Writes
Symptom:
GET /meworks, butPOST /me/sendMailorPOST /me/eventsreturns403 ErrorAccessDenied.
Meaning:
- Authentication succeeded, but the token or app policy does not allow the requested write scopes.
What to do:
1. Check whether the tenant approved Mail.Send or Calendars.ReadWrite for the active client app. 2. Re-sign in with a tenant-approved app registration. 3. Retry a read first, then retry the write.
Useful Helpers
- Get-GraphGatewayRunnerStatus.ps1
- New-GraphGatewayAppConfig.ps1
- custom-app.env.example
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v pwsh >/dev/null 2>&1; then
echo "pwsh was not found. Install PowerShell 7+ to use the Microsoft Graph gateway on macOS or Linux." >&2
exit 1
fi
pwsh -NoLogo -NoProfile -File "$SCRIPT_DIR/Get-GraphGatewayRunnerStatus.ps1" "$@"param(
[string]$RunnerPath
)
$ErrorActionPreference = 'Stop'
function Resolve-RunnerPath {
param(
[string]$ExplicitRunnerPath
)
if ($ExplicitRunnerPath) {
return $ExplicitRunnerPath
}
if ($env:GRAPH_GATEWAY_RUNNER) {
return $env:GRAPH_GATEWAY_RUNNER
}
if ($env:MSGRAPH_RUNNER) {
return $env:MSGRAPH_RUNNER
}
$runner = Get-Command 'msgraph' -ErrorAction SilentlyContinue
if ($runner) {
return $runner.Source
}
return $null
}
$resolvedRunner = Resolve-RunnerPath -ExplicitRunnerPath $RunnerPath
$authStatus = $null
$authError = $null
if ($resolvedRunner) {
try {
$authStatus = & $resolvedRunner auth status 2>&1
} catch {
$authError = $_.Exception.Message
}
}
[pscustomobject]@{
runnerPath = $resolvedRunner
runnerDetected = [bool]$resolvedRunner
graphGatewayRunnerEnv = $env:GRAPH_GATEWAY_RUNNER
msgraphRunnerEnv = $env:MSGRAPH_RUNNER
authStatus = $authStatus
authError = $authError
}param(
[string]$InstallRoot = '.tools\msgraph-runner',
[switch]$AddToProcessEnv,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$resolvedInstallRoot = if ([System.IO.Path]::IsPathRooted($InstallRoot)) {
$InstallRoot
} else {
Join-Path (Get-Location) $InstallRoot
}
$zipPath = Join-Path $resolvedInstallRoot 'msgraph.zip'
$extractRoot = Join-Path $resolvedInstallRoot 'msgraph'
$releaseUrl = 'https://github.com/merill/msgraph/releases/latest/download/msgraph.zip'
if ((Test-Path -LiteralPath $extractRoot) -and -not $Force) {
throw "Install root already contains an extracted runner. Use -Force to overwrite: $extractRoot"
}
New-Item -ItemType Directory -Force -Path $resolvedInstallRoot | Out-Null
Invoke-WebRequest -Uri $releaseUrl -OutFile $zipPath
if (Test-Path -LiteralPath $extractRoot) {
Remove-Item -LiteralPath $extractRoot -Recurse -Force
}
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractRoot -Force
$packageRoot = Get-ChildItem -LiteralPath $extractRoot -Recurse -Directory |
Where-Object { $_.Name -eq 'msgraph' } |
Select-Object -First 1
if (-not $packageRoot) {
throw 'The extracted package root was not found.'
}
$runnerCandidate = if ($IsWindows) {
Get-ChildItem -LiteralPath $packageRoot.FullName -Recurse -File |
Where-Object { $_.Name -eq 'run.ps1' } |
Select-Object -First 1
} else {
Get-ChildItem -LiteralPath $packageRoot.FullName -Recurse -File |
Where-Object { $_.Name -eq 'run.sh' } |
Select-Object -First 1
}
if (-not $runnerCandidate) {
throw 'The platform-specific msgraph runner wrapper was not found after extraction.'
}
if ($AddToProcessEnv) {
$env:GRAPH_GATEWAY_RUNNER = $runnerCandidate.FullName
}
[pscustomobject]@{
installRoot = $resolvedInstallRoot
extractedRoot = $extractRoot
runnerPath = $runnerCandidate.FullName
graphGatewayRunnerEnv = if ($AddToProcessEnv) { $env:GRAPH_GATEWAY_RUNNER } else { $null }
}#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v pwsh >/dev/null 2>&1; then
echo "pwsh was not found. Install PowerShell 7+ to use the Microsoft Graph gateway on macOS or Linux." >&2
exit 1
fi
pwsh -NoLogo -NoProfile -File "$SCRIPT_DIR/Invoke-GraphGateway.ps1" "$@"param(
[ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
[string]$Method,
[string]$Path,
[ValidateSet('v1.0', 'beta')]
[string]$ApiVersion = 'v1.0',
[string]$Select,
[string]$Filter,
[int]$Top,
[string]$Expand,
[string]$OrderBy,
[string[]]$Headers,
[string]$BodyJson,
[string]$PermissionProfile,
[string]$IntentSummary,
[switch]$AllowWrites,
[switch]$DryRun,
[string]$RunnerPath,
[string]$RequestFile
)
$ErrorActionPreference = 'Stop'
function Get-RequestValue {
param(
[object]$ExplicitValue,
[object]$FileValue,
[switch]$AllowEmptyString
)
if ($null -ne $ExplicitValue) {
if ($AllowEmptyString -or $ExplicitValue -ne '') {
return $ExplicitValue
}
}
return $FileValue
}
function Resolve-RunnerCommand {
param(
[string]$ExplicitRunnerPath
)
if ($ExplicitRunnerPath) {
return $ExplicitRunnerPath
}
if ($env:GRAPH_GATEWAY_RUNNER) {
return $env:GRAPH_GATEWAY_RUNNER
}
if ($env:MSGRAPH_RUNNER) {
return $env:MSGRAPH_RUNNER
}
$runner = Get-Command 'msgraph' -ErrorAction SilentlyContinue
if ($runner) {
return $runner.Source
}
throw 'Microsoft Graph runner was not found. Set -RunnerPath, GRAPH_GATEWAY_RUNNER, or MSGRAPH_RUNNER, or install a runner that exposes the msgraph command.'
}
function Assert-Request {
param(
[string]$ResolvedMethod,
[string]$ResolvedPath,
[bool]$IsWrite,
[bool]$AllowWriteExecution
)
if ([string]::IsNullOrWhiteSpace($ResolvedMethod)) {
throw 'Method is required.'
}
if ([string]::IsNullOrWhiteSpace($ResolvedPath)) {
throw 'Path is required.'
}
if (-not $ResolvedPath.StartsWith('/')) {
throw 'Path must start with /. Example: /me/messages'
}
if ($ResolvedMethod -eq 'DELETE') {
throw 'DELETE is blocked by the current Microsoft Graph gateway policy.'
}
if ($IsWrite -and -not $AllowWriteExecution) {
throw 'Write execution requires -AllowWrites.'
}
}
function Get-ImportantBodyFields {
param(
[object]$ResolvedBody
)
if ($null -eq $ResolvedBody) {
return @()
}
if ($ResolvedBody -is [string]) {
if ([string]::IsNullOrWhiteSpace($ResolvedBody)) {
return @()
}
try {
$parsedBody = $ResolvedBody | ConvertFrom-Json -Depth 20
} catch {
return @('Body JSON could not be parsed for summary generation')
}
} else {
$parsedBody = $ResolvedBody
}
if ($parsedBody -isnot [psobject]) {
return @()
}
return @($parsedBody.PSObject.Properties.Name | Select-Object -First 8)
}
function New-ConfirmationPreview {
param(
[string]$ResolvedMethod,
[string]$ResolvedPath,
[string]$ResolvedApiVersion,
[string]$ResolvedPermissionProfile,
[string]$ResolvedIntentSummary,
[object]$ResolvedBody
)
$isWrite = $ResolvedMethod -ne 'GET'
$importantFields = Get-ImportantBodyFields -ResolvedBody $ResolvedBody
[pscustomobject]@{
isWrite = $isWrite
target = $ResolvedPath
method = $ResolvedMethod
apiVersion = $ResolvedApiVersion
permissionProfile = $ResolvedPermissionProfile
intentSummary = $ResolvedIntentSummary
importantBodyFields = $importantFields
}
}
$requestFromFile = $null
if ($RequestFile) {
if (-not (Test-Path -LiteralPath $RequestFile)) {
throw "Request file was not found: $RequestFile"
}
$requestFromFile = Get-Content -LiteralPath $RequestFile -Raw -Encoding UTF8 | ConvertFrom-Json -Depth 20
}
$resolvedMethod = Get-RequestValue -ExplicitValue $Method -FileValue $requestFromFile.method
$resolvedPath = Get-RequestValue -ExplicitValue $Path -FileValue $requestFromFile.path
$resolvedApiVersion = Get-RequestValue -ExplicitValue $ApiVersion -FileValue $requestFromFile.apiVersion
$resolvedSelect = Get-RequestValue -ExplicitValue $Select -FileValue $requestFromFile.query.select -AllowEmptyString
$resolvedFilter = Get-RequestValue -ExplicitValue $Filter -FileValue $requestFromFile.query.filter -AllowEmptyString
$resolvedTop = Get-RequestValue -ExplicitValue $Top -FileValue $requestFromFile.query.top
$resolvedExpand = Get-RequestValue -ExplicitValue $Expand -FileValue $requestFromFile.query.expand -AllowEmptyString
$resolvedOrderBy = Get-RequestValue -ExplicitValue $OrderBy -FileValue $requestFromFile.query.orderBy -AllowEmptyString
$resolvedHeaders = Get-RequestValue -ExplicitValue $Headers -FileValue $requestFromFile.headers
$resolvedBody = Get-RequestValue -ExplicitValue $BodyJson -FileValue $requestFromFile.body
$resolvedPermissionProfile = Get-RequestValue -ExplicitValue $PermissionProfile -FileValue $requestFromFile.permissionProfile
$resolvedIntentSummary = Get-RequestValue -ExplicitValue $IntentSummary -FileValue $requestFromFile.intentSummary
$isWrite = $resolvedMethod -ne 'GET'
Assert-Request -ResolvedMethod $resolvedMethod -ResolvedPath $resolvedPath -IsWrite $isWrite -AllowWriteExecution $AllowWrites.IsPresent
$argumentList = [System.Collections.Generic.List[string]]::new()
$argumentList.Add('graph-call')
$argumentList.Add($resolvedMethod)
$argumentList.Add($resolvedPath)
if ($resolvedApiVersion) {
$argumentList.Add('--api-version')
$argumentList.Add($resolvedApiVersion)
}
if ($resolvedSelect) {
$argumentList.Add('--select')
$argumentList.Add($resolvedSelect)
}
if ($resolvedFilter) {
$argumentList.Add('--filter')
$argumentList.Add($resolvedFilter)
}
if ($resolvedTop) {
$argumentList.Add('--top')
$argumentList.Add([string]$resolvedTop)
}
if ($resolvedExpand) {
$argumentList.Add('--expand')
$argumentList.Add($resolvedExpand)
}
if ($resolvedOrderBy) {
$argumentList.Add('--orderby')
$argumentList.Add($resolvedOrderBy)
}
if ($resolvedHeaders) {
foreach ($header in @($resolvedHeaders)) {
$argumentList.Add('--headers')
$argumentList.Add([string]$header)
}
}
if ($null -ne $resolvedBody -and ($resolvedBody -ne '')) {
$bodyArgument = if ($resolvedBody -is [string]) { $resolvedBody } else { $resolvedBody | ConvertTo-Json -Depth 20 -Compress }
$argumentList.Add('--body')
$argumentList.Add($bodyArgument)
}
if ($isWrite) {
$argumentList.Add('--allow-writes')
}
$preview = New-ConfirmationPreview `
-ResolvedMethod $resolvedMethod `
-ResolvedPath $resolvedPath `
-ResolvedApiVersion $resolvedApiVersion `
-ResolvedPermissionProfile $resolvedPermissionProfile `
-ResolvedIntentSummary $resolvedIntentSummary `
-ResolvedBody $resolvedBody
if ($DryRun) {
[pscustomobject]@{
runnerCommand = if ($RunnerPath) { $RunnerPath } elseif ($env:GRAPH_GATEWAY_RUNNER) { $env:GRAPH_GATEWAY_RUNNER } elseif ($env:MSGRAPH_RUNNER) { $env:MSGRAPH_RUNNER } else { $null }
arguments = @($argumentList)
preview = $preview
}
return
}
$runnerCommand = Resolve-RunnerCommand -ExplicitRunnerPath $RunnerPath
& $runnerCommand @argumentListparam(
[Parameter(Mandatory = $true)]
[ValidateSet('accept', 'decline', 'tentativelyAccept')]
[string]$ResponseType,
[Parameter(Mandatory = $true)]
[string]$EventId,
[string]$ScopePath = '/me/events',
[string]$Comment,
[bool]$SendResponse = $true,
[switch]$AsJson
)
$ErrorActionPreference = 'Stop'
if (-not $ScopePath.StartsWith('/')) {
throw 'ScopePath must start with /. Example: /me/events or /users/user@example.com/events'
}
$normalizedScopePath = $ScopePath.TrimEnd('/')
$requestObject = [ordered]@{
method = 'POST'
path = "$normalizedScopePath/$EventId/$ResponseType"
apiVersion = 'v1.0'
permissionProfile = 'calendar-write'
intentSummary = switch ($ResponseType) {
'accept' { 'Accept a meeting request' }
'decline' { 'Decline a meeting request' }
'tentativelyAccept' { 'Tentatively accept a meeting request' }
}
body = [ordered]@{
comment = $Comment
sendResponse = $SendResponse
}
}
if ($AsJson) {
$requestObject | ConvertTo-Json -Depth 20
return
}
[pscustomobject]$requestObjectparam(
[Parameter(Mandatory = $true)]
[string]$ClientId,
[Parameter(Mandatory = $true)]
[string]$TenantId,
[ValidateSet('powershell', 'bash', 'json')]
[string]$Format = 'powershell',
[string]$RunnerPath
)
$ErrorActionPreference = 'Stop'
$resolvedRunnerPath = if ($RunnerPath) {
$RunnerPath
} elseif ($env:GRAPH_GATEWAY_RUNNER) {
$env:GRAPH_GATEWAY_RUNNER
} elseif ($env:MSGRAPH_RUNNER) {
$env:MSGRAPH_RUNNER
} else {
'$GRAPH_GATEWAY_RUNNER'
}
switch ($Format) {
'powershell' {
@(
"$env:MSGRAPH_CLIENT_ID = '$ClientId'"
"$env:MSGRAPH_TENANT_ID = '$TenantId'"
"$env:GRAPH_GATEWAY_RUNNER = '$resolvedRunnerPath'"
) -join [Environment]::NewLine
}
'bash' {
@(
"export MSGRAPH_CLIENT_ID='$ClientId'"
"export MSGRAPH_TENANT_ID='$TenantId'"
"export GRAPH_GATEWAY_RUNNER='$resolvedRunnerPath'"
) -join [Environment]::NewLine
}
'json' {
[pscustomobject]@{
MSGRAPH_CLIENT_ID = $ClientId
MSGRAPH_TENANT_ID = $TenantId
GRAPH_GATEWAY_RUNNER = $resolvedRunnerPath
} | ConvertTo-Json -Depth 5
}
}param(
[Parameter(Mandatory = $true)]
[ValidateSet('POST', 'PATCH', 'PUT')]
[string]$Method,
[Parameter(Mandatory = $true)]
[string]$Path,
[ValidateSet('v1.0', 'beta')]
[string]$ApiVersion = 'v1.0',
[string]$PermissionProfile,
[string]$IntentSummary,
[string]$BodyJson
)
$ErrorActionPreference = 'Stop'
function Get-BodyPreview {
param(
[object]$ResolvedBody
)
if ($null -eq $ResolvedBody) {
return @()
}
if ($ResolvedBody -is [string]) {
if ([string]::IsNullOrWhiteSpace($ResolvedBody)) {
return @()
}
try {
$parsedBody = $ResolvedBody | ConvertFrom-Json -Depth 20
} catch {
return @('Body JSON could not be parsed')
}
} else {
$parsedBody = $ResolvedBody
}
if ($parsedBody -isnot [psobject]) {
return @('Body content is not an object payload')
}
$lines = [System.Collections.Generic.List[string]]::new()
foreach ($property in $parsedBody.PSObject.Properties | Select-Object -First 10) {
$valuePreview = $property.Value
if ($valuePreview -is [System.Array]) {
$valuePreview = "Array[$($valuePreview.Count)]"
} elseif ($valuePreview -is [psobject]) {
$valuePreview = 'Object'
}
$lines.Add("- $($property.Name): $valuePreview")
}
return @($lines)
}
$summaryLines = [System.Collections.Generic.List[string]]::new()
$summaryLines.Add("Target: $Path")
$summaryLines.Add("Action: $Method")
$summaryLines.Add("API version: $ApiVersion")
if ($PermissionProfile) {
$summaryLines.Add("Permission profile: $PermissionProfile")
}
if ($IntentSummary) {
$summaryLines.Add("Intent: $IntentSummary")
}
if ($ApiVersion -eq 'beta') {
$summaryLines.Add('Risk: beta API requested')
}
$summaryLines.Add('Body preview:')
foreach ($line in Get-BodyPreview -ResolvedBody $BodyJson) {
$summaryLines.Add($line)
}
$summaryLines -join [Environment]::NewLine#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v pwsh >/dev/null 2>&1; then
echo "pwsh was not found. Install PowerShell 7+ to use the Microsoft Graph gateway on macOS or Linux." >&2
exit 1
fi
pwsh -NoLogo -NoProfile -File "$SCRIPT_DIR/Test-GraphGatewayScaffold.ps1" "$@"param()
$ErrorActionPreference = 'Stop'
$skillRoot = Split-Path -Parent $PSScriptRoot
$invokeScript = Join-Path $PSScriptRoot 'Invoke-GraphGateway.ps1'
$confirmScript = Join-Path $PSScriptRoot 'New-GraphWriteConfirmation.ps1'
$eventResponseScript = Join-Path $PSScriptRoot 'New-GraphEventResponseRequest.ps1'
$appConfigScript = Join-Path $PSScriptRoot 'New-GraphGatewayAppConfig.ps1'
$readRequest = Join-Path $skillRoot 'assets\read-mail.request.json'
$writeRequest = Join-Path $skillRoot 'assets\send-mail.request.json'
$dryRunResult = & $invokeScript -RequestFile $readRequest -DryRun
if (-not $dryRunResult) {
throw 'Dry-run result was empty.'
}
if ($dryRunResult.preview.target -ne '/me/messages') {
throw 'Dry-run preview target did not match the expected path.'
}
if ($dryRunResult.preview.permissionProfile -ne 'mail-read-basic') {
throw 'Dry-run preview permission profile did not match the expected value.'
}
$writeRequestObject = Get-Content -LiteralPath $writeRequest -Raw -Encoding UTF8 | ConvertFrom-Json -Depth 20
$confirmationSummary = & $confirmScript `
-Method $writeRequestObject.method `
-Path $writeRequestObject.path `
-ApiVersion $writeRequestObject.apiVersion `
-PermissionProfile $writeRequestObject.permissionProfile `
-IntentSummary $writeRequestObject.intentSummary `
-BodyJson ($writeRequestObject.body | ConvertTo-Json -Depth 20 -Compress)
if ($confirmationSummary -notmatch 'Target: /me/sendMail') {
throw 'Confirmation summary did not include the expected target.'
}
if ($confirmationSummary -notmatch 'Permission profile: mail-write') {
throw 'Confirmation summary did not include the expected permission profile.'
}
$acceptRequest = & $eventResponseScript -ResponseType accept -EventId 'sample-event-id'
if ($acceptRequest.path -ne '/me/events/sample-event-id/accept') {
throw 'Accept event request path did not match the expected value.'
}
if ($acceptRequest.permissionProfile -ne 'calendar-write') {
throw 'Accept event request permission profile did not match the expected value.'
}
$appConfig = & $appConfigScript -ClientId '11111111-1111-1111-1111-111111111111' -TenantId 'contoso.onmicrosoft.com' -Format json | ConvertFrom-Json
if ($appConfig.MSGRAPH_CLIENT_ID -ne '11111111-1111-1111-1111-111111111111') {
throw 'Custom app config did not preserve the expected client ID.'
}
if ($appConfig.MSGRAPH_TENANT_ID -ne 'contoso.onmicrosoft.com') {
throw 'Custom app config did not preserve the expected tenant ID.'
}
[pscustomobject]@{
dryRunVerified = $true
confirmationVerified = $true
eventResponseVerified = $true
appConfigVerified = $true
readRequest = $readRequest
writeRequest = $writeRequest
}Related skills
FAQ
When does it use Graph vs workIQ?
workIQ for common reads (inbox, meetings, file discovery); Microsoft Graph for all writes and gap-reads needing exact endpoints, $select/$filter or directory data.
How are writes handled?
Send, create, update, move, upload, reply and assign are treated as writes and require explicit confirmation; deletes are disabled by default or behind stronger confirmation.