microsoft/ngc-readiness
> Scans the Teams platform based app code base. Evaluates the code based on the rules described in the skill. Generates a report on the NGC readiness of the app based on the rules. Trigger phrases include "Generate a report for NGC readiness", "NGC readiness report", "Scan for issues related to NGC" etc.
npx skills add https://github.com/microsoft/teams-platform-skills --skill ngc-readiness
You are an expert Microsoft Teams platform engineer. Your job is to scan this codebase and produce a NGC Channel Readiness Report — a structured assessment of whether this Teams app is ready to work in shared and private channels (New Gen Channels).
Parse $ARGUMENTS before starting:
$ARGUMENTS as a space separated list of folders. If no folder is given, scan the current directory.> NGC = New Gen Channels (Shared and Private channels in Microsoft Teams)
> These rules define what every Teams app must comply with to work correctly in shared and private channels.
> References:
> - Build apps for shared and private channels
> - Graph API: channel resource
> - Graph API: channel membership change notifications
> - Teams SDK (.NET)
| ID | Category | Description |
|----|----------|-------------|
| MAN | Manifest | App manifest declarations required for NGC |
| MEM | Membership | Correct use of channel membership APIs |
| FS | File & SharePoint | Channel-scoped file and drive access |
| BOT | Bot & Messaging | Bot installation, events, and proactive messaging |
| AUTH | Auth & External Users | Cross-tenant auth and external user handling |
| DATA | Data Scoping | Preventing cross-channel data leaks |
| SUB | Graph Subscriptions | Correct subscription configuration for membership changes |
| CTX | Context API | Using getContext() correctly for NGC |
supportsChannelFeaturesSeverity: CRITICAL
Add supportsChannelFeatures with value tier1 to your app manifest (v1.25+) to make the app available in shared and private channels.
{
"manifestVersion": "1.25",
"supportsChannelFeatures": "tier1"
}
If missing: The app will not appear in the channel app picker. Users cannot install it in shared or private channels.
Notes:
tier1 has no dependency on classic or admin-level access controls.Severity: HIGH
RSC permissions are conditional — only required if the app actually calls the relevant Graph APIs. Do not flag these as violations unless the code scan (Step 3) confirms the app uses the corresponding APIs.
ChannelMember.Read.Group — required only if the app calls allMembers, members, or any channel membership Graph API using RSC (not service auth with delegated permissions).File.Read.Group — required only if the app calls filesFolder or accesses channel files via Graph API using RSC.{
"authorization": {
"permissions": {
"resourceSpecific": [
{
"name": "ChannelMember.Read.Group",
"type": "Application"
},
{
"name": "File.Read.Group",
"type": "Application"
}
]
}
}
}
If missing when required: Channel membership API calls return 403. File API calls return 403 for RSC-only apps.
If the app uses service-level auth (client credentials, OBO with delegated Graph permissions) instead of RSC: These manifest entries are not required.
Severity: CRITICAL
Do not use /teams/{team-id}/members or AAD group membership (/groups/{group-id}/members) to infer who is in a channel.
What breaks:
Correct approach: Always use the channel-scoped allMembers API:
GET /teams/{team-id}/channels/{channel-id}/allMembers
allMembers API — not members — for channel rosterSeverity: CRITICAL
/channels/{id}/members returns only directly added members. Use allMembers to include both direct and indirect members (users who joined via a shared team).
GET /teams/{team-id}/channels/{channel-id}/allMembers
Direct vs. Indirect members:
@microsoft.graph.originalSourceMembershipUrl annotation pointing to their source team.SDK equivalents — scan for VIOLATIONS (.Members instead of .AllMembers):
| Language | Violation (direct only) | Correct (all members) |
|----------|------------------------|----------------------|
| C# (Graph SDK) | graphClient.Teams[id].Channels[id].Members.GetAsync() | graphClient.Teams[id].Channels[id].AllMembers.GetAsync() |
| JavaScript | client.api('/teams/.../channels/.../members').get() | client.api('/teams/.../channels/.../allMembers').get() |
| Java | graphClient.teams().byTeamId(id).channels().byChannelId(id).members().get() | ...allMembers().get() |
| Python | graph_client.teams.by_team_id(id).channels.by_channel_id(id).members.get() | ...all_members.get() |
| Teams SDK (.NET new) | context.Api.Teams[id].Channels[id].Members.GetAsync() | ...AllMembers.GetAsync() |
| Teams SDK (TS new) | app.api.teams(id).channels(id).members.get() | ...allMembers.get() |
doesUserHaveAccess before acting on member-removed eventsSeverity: HIGH
When a member removed notification arrives for an indirect member, the user may still have access via a direct membership path. Always verify before revoking:
GET /teams/{team-id}/channels/{channel-id}/doesUserHaveAccess(userId='@userid',tenantId='@TenantID',userPrincipalName='@UserPrincipalName')
If skipped: Users are incorrectly treated as removed when they still have channel access via a different path.
Severity: HIGH
Private channels are restricted subsets of the team. Shared channels may include members from entirely different teams or tenants. Team-wide "add all" or "notify all" flows must not use team membership as a proxy for channel visibility. Always rely on allMembers API.
GET /teams/{team-id}/channels/{channel-id}/allMembers
hostTeamGroupID (not groupId) as team-id in membership API callsSeverity: CRITICAL
In a shared or private channel, getContext() returns two channel-specific properties:
hostTeamGroupID — the AAD group ID of the host team that owns the channelhostTenantID — the tenant ID of the host teamThe plain groupId from getContext() is the group ID of the team the user is currently navigated under, which in a shared channel may be a different team than the one hosting the channel. Using groupId as the team-id in Graph calls targets the wrong team and returns incorrect or empty membership.
Incorrect (shared/private channel context):
const context = await microsoftTeams.app.getContext();
const teamId = context.team.groupId; // ❌ may be the wrong team in shared channels
Correct:
const context = await microsoftTeams.app.getContext();
const teamId = context.hostTeamGroupID; // ✅ always the channel's host team
const channelId = context.channel.id;
// GET /teams/{teamId}/channels/{channelId}/allMembers
For bots: Use turnContext.Activity.GetChannelData<TeamsChannelData>()?.Team?.Id or turnContext.Activity.TeamsGetTeamInfo()?.AadGroupId as the team-id.
If groupId is used instead:
Note that hostTeamGroupID and hostTenantID are not applicable for all URLs or URL templates. They are applicable only for Graph API calls, as values read from getContext() to be passed in the URL. There are no query parameters or placeholders that app developers need to use with these names.
@microsoft.graph.originalSourceMembershipUrl to classify direct vs indirect membersSeverity: MEDIUM
The allMembers API and indirect membership change notifications include the @microsoft.graph.originalSourceMembershipUrl annotation on each member object. Apps must read this annotation to correctly classify members:
{
"@microsoft.graph.originalSourceMembershipUrl": "https://graph.microsoft.com/v1.0/tenants/{tenant}/teams/{sourceTeamId}/channels/{channelId}/members/{memberId}",
"id": "...",
"roles": ["owner"],
"displayName": "Jane Doe",
"tenantId": "..."
}
If not handled:
Severity: CRITICAL
Standard channels share the team's SharePoint site. Shared and private channels each have their own dedicated SharePoint site and driveId.
If assumed:
driveId and itemId per channel via filesFolderSeverity: CRITICAL
Use this API to get the correct drive root for any channel type:
GET /teams/{teamId}/channels/{channelId}/filesFolder
Response properties to store and reuse:
parentReference.driveId — the channel's SharePoint driveIdid — the folderId for the channel's rootNever hardcode SharePoint library names or paths (e.g., /Shared Documents/{ChannelName}). Use this API for all channel types, including standard channels.
SDK equivalents:
| Language | Correct usage |
|----------|--------------|
| C# (Graph SDK) | graphClient.Teams[teamId].Channels[channelId].FilesFolder.GetAsync() |
| JavaScript | client.api('/teams/{id}/channels/{id}/filesFolder').get() |
| Python | graph_client.teams.by_team_id(id).channels.by_channel_id(id).files_folder.get() |
| Java | graphClient.teams().byTeamId(id).channels().byChannelId(id).filesFolder().get() |
> Private channel note: After the private channel SharePoint migration, filesFolder returns the root folder's driveItem, not a named subfolder. Newly created private channels no longer create a dedicated subfolder — all files go to the root. Do not rely on a named subfolder matching the channel name.
driveId for all channelsSeverity: CRITICAL
Each shared/private channel has a distinct driveId. A cached team-level driveId will cause all file operations in shared/private channels to target the wrong drive.
Correct pattern: Cache driveId per channelId, and refresh via filesFolder when a new channel is encountered.
See FS-2 section for calling patterns.
Severity: HIGH
Organization-wide or anonymous sharing links exclude external members even if they are channel members.
Do not use:
Use instead:
/invite API)POST /drives/{driveId}/items/{itemId}/invite
File.Read.Group RSC permission for RSC apps accessing channel filesSeverity: HIGH *(conditional — only applies if the app uses RSC auth for file APIs)*
For apps using Resource-Specific Consent (RSC) that access channel files via the Graph API, declare the File.Read.Group permission. This is the least-privileged RSC application permission for the filesFolder API.
This permission is NOT required if the app uses service-level auth (client credentials or OBO with delegated Graph permissions) instead of RSC.
{
"authorization": {
"permissions": {
"resourceSpecific": [
{
"name": "File.Read.Group",
"type": "Application"
}
]
}
}
}
If missing (RSC apps only): GET /teams/{id}/channels/{id}/filesFolder returns 403.
Severity: CRITICAL
Apps installed at the team level are not automatically available in shared or private channels. The app must be explicitly added to each channel.
If assumed:
Correct approach: Use the channelMemberAdded event to detect when the bot is added to a channel and trigger per-channel setup logic.
channelMemberAdded for per-channel bot setupSeverity: HIGH
When the bot receives a channelMemberAdded event inside OnConversationUpdateActivityAsync, use it to:
Do not rely solely on team-level install events (teamMemberAdded / uninstall) for per-channel logic.
channelshared and channelunshared eventsSeverity: HIGH
When a shared channel is added to or removed from another team, your bot receives a conversationUpdate activity with eventType = channelshared or channelunshared.
Use these events to:
allMembers.var tcd = turnContext.Activity.GetChannelData<TeamsChannelData>();
var eventType = tcd?.EventType?.ToLowerInvariant();
switch (eventType)
{
case "channelshared":
// Refresh membership, update state
break;
case "channelunshared":
// Remove channel from active list
break;
}
If not handled: Bot keeps sending to channels it's no longer part of, generating repeated 403 errors and stale state.
channelMemberAdded, not only on team installSeverity: MEDIUM
If welcome/setup logic only fires on team install events, shared and private channels never receive it when the bot is first enabled there.
GetChannelData<SharedChannelChannelData>() for shared channel event payloadsSeverity: HIGH
For channelshared and channelunshared events, the full payload (including the list of teams the channel was shared with or unshared from) is only available via SharedChannelChannelData, not the base TeamsChannelData.
// ❌ Insufficient — misses SharedWithTeams / UnsharedFromTeams lists
var tcd = turnContext.Activity.GetChannelData<TeamsChannelData>();
// ✅ Correct — gives access to SharedWithTeams and UnsharedFromTeams
var extended = turnContext.Activity.GetChannelData<SharedChannelChannelData>();
var sharedWith = extended?.SharedWithTeams ?? new List<TeamInfoEx>();
var unsharedFrom = extended?.UnsharedFromTeams ?? new List<TeamInfoEx>();
Each TeamInfoEx in the list contains Id, Name, AadGroupId, and TenantId — use these to update per-team state and handle cross-tenant scenarios.
If only TeamsChannelData is used: The bot cannot determine which teams the channel was shared with or unshared from, making it impossible to update membership tracking or routing state accurately.
Microsoft.Teams.SDK / @microsoft/teams-js v2)Severity: CRITICAL
NGC-aware APIs — SharedChannelChannelData, AllMembers, hostTeamGroupID, channel.ownerTenantId, and the channelshared/channelunshared event types — were introduced in version 2.0 of both the .NET and JavaScript Teams SDKs. Any version below 2.0 is missing these APIs entirely; calling them against an older SDK silently falls back to incomplete or incorrect behavior.
For .NET projects — check *.csproj / packages.config / Directory.Packages.props:
<!-- ❌ Too old — NGC APIs unavailable -->
<PackageReference Include="Microsoft.Teams.SDK" Version="1.*" />
<!-- ✅ Minimum required -->
<PackageReference Include="Microsoft.Teams.SDK" Version="2.0.0" />
For JavaScript / TypeScript projects — check package.json:
// ❌ Too old
"@microsoft/teams-js": "^1.x.x"
// ✅ Minimum required
"@microsoft/teams-js": "^2.0.0"
If below 2.0.0:
SharedChannelChannelData type is absent; channelshared/channelunshared events cannot be parsed.hostTeamGroupID and channel.ownerTenantId are not present in the context object.AllMembers navigation property does not exist on the Graph SDK fluent client.hostTenantId / ownerTenantId for SharePoint token requestsSeverity: CRITICAL
External (cross-tenant) users reside in their home tenant. When they access SharePoint content in the host channel's site, the getAuthToken call must use the host tenant ID, not the user's own tenant ID.
For tabs (JS SDK v2):
const context = await microsoftTeams.app.getContext();
const hostTenantId = context.channel.ownerTenantId;
For bots (Agent SDK / Bot Framework):
var hostTenantId = turnContext.Activity.Conversation.TenantId;
Pass hostTenantId in the tenantId parameter of getAuthToken so SPO grants the correct cross-tenant access.
If skipped: External users get token errors or access-denied when opening tab content backed by SharePoint.
Severity: HIGH
Shared channels support cross-tenant members. Their tenantId will not match the host's ownerTenantId. Treating unknown tenant IDs as "invalid" accidentally blocks legitimate external collaborators.
Do not assume all users are from the host tenant. Compare user.tenant.id (the current user's tenant) with channel.ownerTenantId (the host tenant) to detect external users. If they differ, the user is external — not invalid.
const context = await microsoftTeams.app.getContext();
const isExternal = context.user.tenant.id !== context.channel.ownerTenantId;
roles property, not heuristicsSeverity: MEDIUM
To reliably identify guests, check the roles field in the allMembers response:
GET /teams/{team-id}/channels/{channel-id}/allMembers
For guests: "roles": ["guest"]
Do not rely on display name patterns, email domain heuristics, or upstream session flags.
Severity: CRITICAL
Private channel content is restricted to channel members. Aggregating it into a team-wide view exposes sensitive data to users who do not have access.
Rule: Scope all data reads, reports, and analytics to the channel context in which the app is running.
Severity: HIGH
Broadcasting content from one channel to others, or generating links that span channels, assumes all team members see all channels — which is false for shared and private channels.
Rule: Cross-channel operations require explicit permission checks per channel.
channelType or membershipType for app behavior logicSeverity: CRITICAL
Channel type values are subject to change as the Teams platform evolves. App logic that branches on membershipType or channelType will become brittle.
Correct approach: Use capability-based APIs (allMembers, filesFolder, doesUserHaveAccess) to determine behavior, not channel-type discrimination.
> Note: Reading membershipType for display-only purposes (e.g., showing a label) is acceptable. The rule applies to branching app logic on these values.
notifyOnIndirectMembershipUpdate=trueSeverity: HIGH
The correct resource path for subscribing to ALL private and shared channel membership changes in a team is:
/teams/{team-id}/channels/getAllMembers
To also receive indirect membership updates (from shared teams), add both query parameters:
/teams/{team-id}/channels/getAllMembers?notifyOnIndirectMembershipUpdate=true&suppressNotificationWhenSharedUnsharedWithTeam=true
Full subscription request:
{
"changeType": "created,deleted,updated",
"notificationUrl": "https://your-webhook/api/notifications",
"resource": "/teams/{team-id}/channels/getAllMembers?notifyOnIndirectMembershipUpdate=true&suppressNotificationWhenSharedUnsharedWithTeam=true",
"includeResourceData": true,
"expirationDateTime": "2024-09-19T11:00:00.0000000Z"
}
Without notifyOnIndirectMembershipUpdate=true: Only direct membership changes fire. Indirect changes (from shared teams) are silently missed, causing roster drift.
Without suppressNotificationWhenSharedUnsharedWithTeam=true: Individual per-user notifications fire when a channel is shared/unshared with a large team, causing thousands of events. Always pair this with SUB-2.
sharedWithTeams subscription for bulk share/unshare eventsSeverity: HIGH
When a shared channel is added to or removed from a large team, per-user notifications can number in the thousands. Subscribe to sharedWithTeams for a single share/unshare event, then refresh the full member list via allMembers.
{
"changeType": "created,deleted",
"notificationUrl": "https://your-webhook/api/notifications",
"resource": "/teams/{team-id}/channels/{channel-id}/sharedWithTeams",
"includeResourceData": true,
"expirationDateTime": "2024-09-19T11:00:00.0000000Z"
}
After receiving this notification, call allMembers to get the current full roster.
Severity: MEDIUM
A user who is both a direct and indirect member of a shared channel may trigger duplicate memberAdded notifications. Before processing, check whether the user already exists in your roster using allMembers and deduplicate using the @microsoft.graph.originalSourceMembershipUrl annotation to distinguish the source.
lifecycleNotificationUrl for subscriptions with expiry > 1 hourSeverity: HIGH
If the expirationDateTime in a subscription request is more than one hour in the future, you must include a lifecycleNotificationUrl property. Without it, subscription creation fails with an error.
{
"changeType": "created,deleted,updated",
"notificationUrl": "https://your-webhook/api/notifications",
"lifecycleNotificationUrl": "https://your-webhook/api/lifecycle",
"resource": "/teams/{team-id}/channels/getAllMembers?notifyOnIndirectMembershipUpdate=true",
"expirationDateTime": "2024-09-19T11:00:00.0000000Z"
}
If missing: Subscription creation requests with expiry > 1 hour will be rejected by the platform.
hostTeamGroupID and hostTenantID from getContext() for Graph callsSeverity: HIGH
For shared and private channels, getContext() returns two new properties:
hostTeamGroupID — the group ID of the team that hosts the channelhostTenantID — the tenant ID of the host teamUse these (not the bare groupId) when calling Graph APIs for channel membership, file access, or tenant comparison.
> See also MEM-5 for the specific membership API implication: hostTeamGroupID must be used as the {team-id} parameter in all /teams/{team-id}/channels/{channel-id}/... Graph API calls.
channel.ownerTenantId for cross-tenant SharePoint accessSeverity: HIGH
When calling getAuthToken for SharePoint resources from a tab, use context.channel.ownerTenantId as the tenantId parameter. This ensures tokens are scoped to the host tenant where the SharePoint site lives.
Before doing any deep scan, locate manifest.json (or files under appPackage/).
Check whether any element in the manifest has a "scopes" array that contains "team". This includes configurableTabs, staticTabs, bots, composeExtensions, etc.
"team" in its scopes: The app has no channel installation surface (personal-only or groupChat-only). NGC rules do not apply. Stop here and report: ✅ Channel Ready — NGC Not Applicable with a note that the app has no team/channel scope and therefore does not need NGC compliance."team" in its scopes: The app can be installed in channels. Continue to Step 2.manifest.json is not found: Issue a warning that manifest is not available (may be hosted on CDN). Proceed with code scanning and note that manifest must be verified separately.Read manifest.json and check only the items below. Do not evaluate any other manifest section. Ignore all URLs in the manifest.
manifestVersion — must be "1.25" or higher. Flag as CRITICAL if lower or missing.supportsChannelFeatures — must be present with value "tier1". Flag as CRITICAL if missing or wrong value.RSC permissions (ChannelMember.Read.Group, File.Read.Group) are NOT checked here. They are conditional on what the code actually does and are evaluated during the code scan in Step 3:
ChannelMember.Read.Group is checked inside the Membership APIs scan section.File.Read.Group is checked inside the File & SharePoint scan section.Do not flag RSC permissions as violations in this step.
Do not flag any other manifest fields as violations.
Using the rules above as evaluation criteria, systematically search the codebase for evidence of compliance or violation. Use targeted searches across all relevant file types.
> Construct applicability principle: Every rule category below is conditional on the app actually using that construct. Before scanning a category, first confirm the construct is present. If it is absent, skip the entire category and note it as "not applicable" in the report — do not flag phantom violations for code that doesn't exist. The applicability check for each category is listed under its "Skip if" line.
Skip if: No Graph API calls to teams, channels, groups, or membership endpoints exist anywhere in the codebase (search for graph, teams, channels, members). If absent, skip all MEM rules.
Flag as violations:
GetGroupMembersAsync / /groups/{id}/members used for channel access decisions.Members.GetAsync() — C# Graph SDK, returns direct only.members().get() — Java Graph SDK, returns direct only.members.get() — Python Graph SDK, returns direct onlyclient.api('/teams/.../channels/.../members').get() — JavaScript, returns direct only/teams/{id}/members — team-level member list, not channel-scopedLook for correct usage:
.AllMembers.GetAsync() — C# Graph SDK ✅.allMembers().get() — Java Graph SDK ✅.all_members.get() — Python Graph SDK ✅client.api('/teams/.../channels/.../allMembers').get() — JavaScript ✅/teams/{id}/channels/{id}/allMembers in raw HTTP calls ✅Also search for:
doesUserHaveAccess — note if used or absent on member-removed events@microsoft.graph.originalSourceMembershipUrl — note if absent (Bucket 3 item)groupId used as team-id in Graph calls — flag if hostTeamGroupID is not used insteadhostTeamGroupID — confirm it's read from context and passed to Graph APIsRSC manifest check (MAN-2 — only if membership APIs are in use):
If the app calls allMembers, members, or any channel membership Graph API, check whether the app uses RSC (Resource-Specific Consent) or service-level auth (client credentials / OBO with delegated Graph permissions):
ChannelMember.Read.Group is absent from authorization.permissions.resourceSpecific in the manifest.Skip if: No SharePoint, OneDrive, or drive-related code exists (search for drive, sharepoint, filesFolder, Shared Documents, driveId). If absent, skip all FS rules.
Flag as violations:
/Shared Documents/ paths in SPO URLsdriveId cached at team level without channelId as keyLook for correct usage:
filesFolder in API calls — confirms FS-2 compliance ✅.FilesFolder.GetAsync() — C# SDK ✅parentReference.driveId stored per-channel ✅/drives/{driveId}/items/{itemId}/invite — membership-based sharing ✅RSC manifest check (MAN-2 / FS-5 — only if file APIs are in use):
If the app calls filesFolder or accesses channel files via Graph API, check whether it uses RSC or service-level auth:
File.Read.Group is absent from authorization.permissions.resourceSpecific in the manifest.Skip if: The manifest has no bots element. If no bot is registered, skip all BOT rules including BOT-6.
SDK version check (BOT-6 — applies to all bot projects):
*.csproj, Directory.Packages.props, and packages.config for Microsoft.Teams.SDK. Flag as CRITICAL if the resolved version is below 2.0.0 (e.g., 1.*, 0.*, or any pre-release below 2.0).package.json for @microsoft/teams-js. Flag as CRITICAL if the version is below 2.0.0 (e.g., ^1.x.x).Flag as violations (only if bot does proactive messaging or has per-channel state):
OnConversationUpdateActivityAsync present but no handling for channelshared/channelunshared eventTypesGetChannelData<TeamsChannelData>() used alone for shared channel events without SharedChannelChannelDatateamMemberAdded with no channelMemberAdded equivalentNote: Messaging-extension-only bots (link unfurling, search commands with no proactive messaging or per-channel state) are not subject to BOT-1 through BOT-5 — these rules only apply to bots that send proactive messages or maintain per-channel install state. BOT-6 (SDK version) applies to all bot projects regardless.
Skip if: No authentication flows exist — no getAuthToken, AcquireToken, MSAL, OBO, or similar token-acquisition code found anywhere in the codebase. If absent, skip all AUTH rules.
Flag as violations:
IsEnterpriseGuest in shared channel contexts)getAuthToken called without passing tenantId when SharePoint is involvedNote for Bucket 2 (suggestions, not violations):
roles — the app may intentionally not support external users, but it's worth suggesting they evaluate this per channel typeSkip if: No multi-channel aggregation, reporting, broadcast, or cross-channel post patterns exist, AND no membershipType/channelType/MembershipType/ChannelType identifiers appear anywhere in the codebase. If absent, skip DATA rules.
membershipType or channelType used in if/switch/case branching to select different code paths, different API calls, or different access decisions. Quote the switch/case block. The fix is to replace all branches with a single allMembers (or filesFolder) call that works for every channel type uniformly.Skip if: No Graph change notification or subscription code exists (search for subscriptionUrl, notificationUrl, changeType, /subscriptions). If absent, skip all SUB rules.
Flag as violations:
/channels/{id}/members — wrong endpointnotifyOnIndirectMembershipUpdate=true in membership subscription resourcelifecycleNotificationUrl when expirationDateTime is set to more than 1 hour outsharedWithTeams subscription when the app handles bulk membership eventsSkip if: No getContext, microsoftTeams.app.getContext, or Teams JS SDK context calls exist in the frontend code. If absent, skip all CTX rules.
hostTeamGroupID — confirm it's read from context and passed to Graph APIshostTenantID — confirm it's read for tenant comparisonschannel.ownerTenantId — confirm it's used for SPO token scopingProduce the following structured report as an MD file in the root of the repo.
App: [name or root directory of the scanned codebase]
Scanned: [today's date]
State one of the following verdicts, bold and prominent:
| Verdict | Criteria |
|---------|----------|
| ✅ Channel Ready | Zero confirmed violations. Manifest has supportsChannelFeatures: tier1 and correct manifest version. |
| ✅ Channel Ready — NGC Not Applicable | App has no "team" scope in any manifest element. NGC rules do not apply. |
| ⚠️ Needs Minor Changes | No CRITICAL violations. A few HIGH rules need attention. Estimated effort of 2 to 3 days. |
| 🔴 Needs Major Changes | One or more CRITICAL violations. Core APIs or manifest need rework. Estimated effort 1+ weeks. |
Follow the verdict with 2–4 sentences explaining *why* the app is in this state, naming the dominant failure categories.
> These are blatant API misuse or missing declarations that will certainly break the app in shared or private channels. Each item must be fixed before the app is NGC-ready.
Grouping rule — organize by code path, not by rule ID.
Group all violations that touch the same file or the same logical code area into a single entry. List every applicable rule ID in the header. This makes it easy for a developer to open one file and see all the issues they need to fix in one place. Ordering: CRITICAL entries first, then HIGH.
When multiple membership API calls are wrong in the same file (e.g., GetGroupMembersAsync, GetChannelMembersAsync, and GetChannelMemberAsync all need to become allMembers), describe them all together in one entry rather than creating a separate entry for each call site.
For each entry:
### [N]. [File or Logical Code Area] — [Short Violation Title]
**Severity:** CRITICAL | HIGH (use the highest severity among all violations in this group)
**Rules violated:** [e.g., MEM-1, MEM-2, DATA-3]
**Files:** `path/to/file:line` (list multiple lines if violations span several locations in the same file)
**What's wrong:**
[Describe all violations in this code area together. Quote each specific call or pattern. Explain why each fails in shared/private channels. If multiple similar calls have the same root cause (e.g., all need allMembers), state the root cause once and enumerate the affected call sites.]
**Fix:**
[Consolidated fix addressing all violations in this entry. Correct API, code pattern, manifest field, or SDK method. Include a short snippet if it clarifies the fix.]
**Impact if unresolved:**
[One sentence on the concrete user-facing or security consequence in shared/private channels.]
If there are zero violations total, state: *No confirmed violations found.*
> These are patterns that may or may not be issues depending on the app's intended behavior. The app could have valid reasons for these patterns — they are flagged here for review, not as required fixes.
Use a concise list format. For each item:
**[Rule ID] — [Short title]**
`path/to/file:line`
[One sentence describing the pattern and why it's worth reviewing. End with a specific suggestion for what to check or consider.]
Examples of what belongs here:
groupId from context and passes to a backend — may be correct if the backend remaps it; verify the backend uses host team ID for channel-scoped Graph calls> These are not current violations — the code does not use these APIs yet. They are changes that will be required when migrating to NGC-compliant patterns, or that would improve correctness and resilience once the Bucket 1 fixes are in place.
Use a concise list format. For each item:
**[Rule ID] — [Short title]**
[One sentence describing what needs to be added or changed, and when it becomes relevant (e.g., "Required when adopting allMembers API").]
Examples of what belongs here:
ChannelMember.Read.Group RSC permission to manifest — required when moving from service-auth Graph calls to RSC-based channel membership APIshostTeamGroupID from getContext() and pipe to backend — required alongside any allMembers migration@microsoft.graph.originalSourceMembershipUrl on allMembers responses — needed after allMembers adoption to correctly classify direct vs indirect members and avoid double-counting| Area | Violations | Effort |
|------|-----------|--------|
| Manifest | | |
| Membership APIs | | |
| File / SharePoint | | |
| Bot Events | | |
| Auth | | |
| Data Scoping | | |
| Graph Subscriptions | | |
| Context API | | |
| Total | | [range] |
Complexity driver: [The single biggest source of effort in one sentence]
Generate the report as an MD file in the root folder.
Take microsoft/ngc-readiness from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.