diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bcbd3dbe..92415c9d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,17 @@ jobs:
with:
pulumi-version: '3.197.0'
+ - name: Cache Pulumi plugins
+ uses: actions/cache@v4
+ with:
+ path: ~/.pulumi/plugins
+ key: pulumi-plugins-${{ hashFiles('Pulumi.yaml') }}
+ restore-keys: |
+ pulumi-plugins-
+
- name: Install Pulumi packages
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
run: pulumi install
- name: Install dependencies
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 951800f2..aea1d56d 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -34,7 +34,17 @@ jobs:
with:
pulumi-version: ${{ env.PULUMI_VERSION }}
+ - name: Cache Pulumi plugins
+ uses: actions/cache@v4
+ with:
+ path: ~/.pulumi/plugins
+ key: pulumi-plugins-${{ hashFiles('Pulumi.yaml') }}
+ restore-keys: |
+ pulumi-plugins-
+
- name: Install Pulumi packages
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
run: pulumi install
- name: Install dependencies
@@ -52,6 +62,10 @@ jobs:
env:
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
GITHUB_TOKEN: ${{ secrets.PULUMI_GITHUB_TOKEN }}
+ DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
+ DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }}
run: |
echo "$PULUMI_PASSPHRASE" > passphrase.prod.txt
+ pulumi config set discord:guildId "$DISCORD_GUILD_ID" --stack prod
+ pulumi config set discord:botToken "$DISCORD_BOT_TOKEN" --secret --stack prod
make up
\ No newline at end of file
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
new file mode 100644
index 00000000..8f5c8123
--- /dev/null
+++ b/.github/workflows/preview.yml
@@ -0,0 +1,153 @@
+name: Preview
+
+on:
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ PULUMI_VERSION: "3.197.0"
+
+jobs:
+ preview:
+ name: Preview Changes
+ runs-on: ubuntu-latest
+ environment: production
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
+
+ - name: Setup Pulumi
+ uses: pulumi/actions@v6
+ with:
+ pulumi-version: ${{ env.PULUMI_VERSION }}
+
+ - name: Cache Pulumi plugins
+ uses: actions/cache@v4
+ with:
+ path: ~/.pulumi/plugins
+ key: pulumi-plugins-${{ hashFiles('Pulumi.yaml') }}
+ restore-keys: |
+ pulumi-plugins-
+
+ - name: Install Pulumi packages
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: pulumi install
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run validation
+ run: npm run check
+
+ - name: Authenticate to Google Cloud
+ uses: google-github-actions/auth@v2
+ with:
+ credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}
+
+ - name: Preview changes
+ id: preview
+ env:
+ PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
+ GITHUB_TOKEN: ${{ secrets.PULUMI_GITHUB_TOKEN }}
+ DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
+ DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }}
+ run: |
+ echo "$PULUMI_PASSPHRASE" > passphrase.prod.txt
+ pulumi login gs://mcp-access-prod-pulumi-state
+
+ # Build config flags for Discord if secrets are available
+ CONFIG_FLAGS=""
+ if [ -n "$DISCORD_GUILD_ID" ]; then
+ CONFIG_FLAGS="$CONFIG_FLAGS --config discord:guildId=$DISCORD_GUILD_ID"
+ fi
+ if [ -n "$DISCORD_BOT_TOKEN" ]; then
+ CONFIG_FLAGS="$CONFIG_FLAGS --config discord:botToken=$DISCORD_BOT_TOKEN"
+ fi
+
+ # Run preview and capture output
+ set +e
+ PREVIEW_OUTPUT=$(PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi preview --stack prod --diff $CONFIG_FLAGS 2>&1)
+ PREVIEW_EXIT_CODE=$?
+ set -e
+
+ # Save output for comment
+ echo "exit_code=$PREVIEW_EXIT_CODE" >> $GITHUB_OUTPUT
+
+ # Write preview to file (handles multiline)
+ echo "$PREVIEW_OUTPUT" > preview_output.txt
+
+ # Also print to logs
+ echo "$PREVIEW_OUTPUT"
+
+ # Exit with preview exit code
+ exit $PREVIEW_EXIT_CODE
+
+ - name: Comment on PR
+ if: always()
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ let output = '';
+ try {
+ output = fs.readFileSync('preview_output.txt', 'utf8');
+ } catch (e) {
+ output = 'Failed to read preview output';
+ }
+
+ // Truncate if too long for GitHub comment
+ const maxLength = 60000;
+ if (output.length > maxLength) {
+ output = output.substring(0, maxLength) + '\n\n... (truncated)';
+ }
+
+ const body = `## Pulumi Preview
+
+
+ Click to expand preview output
+
+ \`\`\`
+ ${output}
+ \`\`\`
+
+
+ `;
+
+ // Find existing comment
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const botComment = comments.find(c =>
+ c.user.type === 'Bot' && c.body.includes('## Pulumi Preview')
+ );
+
+ if (botComment) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: body
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: body
+ });
+ }
diff --git a/package.json b/package.json
index 59c9dd72..e23b7083 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,8 @@
"scripts": {
"build": "tsc",
"validate": "npx ts-node scripts/validate-config.ts",
+ "test": "npx ts-node scripts/test-config.ts",
+ "check": "npm run validate && npm run test",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
diff --git a/scripts/test-config.ts b/scripts/test-config.ts
new file mode 100644
index 00000000..471bbf4d
--- /dev/null
+++ b/scripts/test-config.ts
@@ -0,0 +1,87 @@
+#!/usr/bin/env npx ts-node
+
+/**
+ * Tests the configuration structure without needing Pulumi credentials.
+ * Run with: npx ts-node scripts/test-config.ts
+ */
+
+import { ROLES, buildRoleLookup, getRolesForPlatform } from '../src/config/roles';
+import { ROLE_IDS, isValidRoleId } from '../src/config/roleIds';
+import { MEMBERS } from '../src/config/users';
+
+let passed = 0;
+let failed = 0;
+
+function test(name: string, fn: () => boolean) {
+ try {
+ if (fn()) {
+ console.log(`✓ ${name}`);
+ passed++;
+ } else {
+ console.log(`✗ ${name}`);
+ failed++;
+ }
+ } catch (e) {
+ console.log(`✗ ${name}: ${e}`);
+ failed++;
+ }
+}
+
+console.log('Testing role configuration...\n');
+
+// Test ROLE_IDS
+test('ROLE_IDS has entries', () => Object.keys(ROLE_IDS).length > 0);
+test('All ROLE_IDS values are valid', () =>
+ Object.values(ROLE_IDS).every((id) => isValidRoleId(id)));
+
+// Test ROLES
+test('ROLES array is not empty', () => ROLES.length > 0);
+test('All roles have id and description', () => ROLES.every((r) => r.id && r.description));
+test('All role IDs are unique', () => {
+ const ids = ROLES.map((r) => r.id);
+ return ids.length === new Set(ids).size;
+});
+
+// Test platform configs
+const githubRoles = getRolesForPlatform('github');
+const discordRoles = getRolesForPlatform('discord');
+const googleRoles = getRolesForPlatform('google');
+
+test('Has GitHub roles', () => githubRoles.length > 0);
+test('Has Discord roles', () => discordRoles.length > 0);
+test('Has Google roles', () => googleRoles.length > 0);
+
+test('GitHub roles have team names', () => githubRoles.every((r) => r.github?.team));
+test('Discord roles have role names', () => discordRoles.every((r) => r.discord?.role));
+test('Google roles have group names', () => googleRoles.every((r) => r.google?.group));
+
+// Test parent relationships
+const roleLookup = buildRoleLookup();
+test('All GitHub parent references are valid', () =>
+ githubRoles.every((r) => {
+ if (!r.github?.parent) return true;
+ const parent = roleLookup.get(r.github.parent);
+ return parent && parent.github;
+ }));
+
+// Test members
+test('MEMBERS array is not empty', () => MEMBERS.length > 0);
+test('All members have at least one identifier', () =>
+ MEMBERS.every((m) => m.github || m.email || m.discord));
+test('All member role references are valid', () =>
+ MEMBERS.every((m) => m.memberOf.every((id) => roleLookup.has(id))));
+
+// Test specific roles exist
+test('CORE_MAINTAINERS role exists', () => !!roleLookup.get(ROLE_IDS.CORE_MAINTAINERS));
+test('ADMINISTRATORS role exists (Discord-only)', () => {
+ const role = roleLookup.get(ROLE_IDS.ADMINISTRATORS);
+ return role !== undefined && role.discord !== undefined && role.github === undefined;
+});
+test('TYPESCRIPT_SDK_AUTH role exists (GitHub-only)', () => {
+ const role = roleLookup.get(ROLE_IDS.TYPESCRIPT_SDK_AUTH);
+ return role !== undefined && role.github !== undefined && role.discord === undefined;
+});
+
+// Summary
+console.log(`\n${passed} passed, ${failed} failed`);
+process.exit(failed > 0 ? 1 : 0);
diff --git a/scripts/validate-config.ts b/scripts/validate-config.ts
index d8132467..59d8a78d 100644
--- a/scripts/validate-config.ts
+++ b/scripts/validate-config.ts
@@ -1,27 +1,27 @@
#!/usr/bin/env npx ts-node
/**
- * Validates that all team references in repoAccess.ts exist in groups.ts
+ * Validates that all references in the config are valid.
* Run with: npx ts-node scripts/validate-config.ts
*/
-import { GROUPS } from '../src/config/groups';
+import { ROLES, buildRoleLookup } from '../src/config/roles';
import { REPOSITORY_ACCESS } from '../src/config/repoAccess';
import { MEMBERS } from '../src/config/users';
-import type { Group, Member } from '../src/config/utils';
+import type { RoleId } from '../src/config/roleIds';
-// Get all GitHub team names (groups not limited to google-only platforms)
+const roleLookup = buildRoleLookup();
+
+// Get all GitHub team names (roles that have GitHub config)
const githubTeamNames = new Set();
-// Get all group names (for member validation - members can be in any group)
-const allGroupNames = new Set();
+// Get all role IDs (for member validation)
+const allRoleIds = new Set();
-for (const g of GROUPS) {
- const group = g as Group;
- allGroupNames.add(group.name);
+for (const role of ROLES) {
+ allRoleIds.add(role.id);
- const platforms = group.onlyOnPlatforms;
- if (!platforms || platforms.includes('github')) {
- githubTeamNames.add(group.name);
+ if (role.github) {
+ githubTeamNames.add(role.github.team);
}
}
@@ -35,41 +35,42 @@ for (const repo of REPOSITORY_ACCESS) {
for (const teamRef of repo.teams) {
if (!githubTeamNames.has(teamRef.team)) {
console.error(
- `ERROR: Repository "${repo.repository}" references team "${teamRef.team}" which does not exist in groups.ts`
+ `ERROR: Repository "${repo.repository}" references team "${teamRef.team}" which does not exist in roles.ts`
);
hasErrors = true;
}
}
}
-// Validate team references in MEMBERS (memberOf)
-// Members can be in any group (GitHub or Google-only)
-console.log('Validating team references in users.ts...');
-for (const m of MEMBERS) {
- const member = m as Member;
- for (const teamKey of member.memberOf) {
- if (!allGroupNames.has(teamKey)) {
+// Validate role references in MEMBERS (memberOf)
+console.log('Validating role references in users.ts...');
+for (const member of MEMBERS) {
+ for (const roleId of member.memberOf) {
+ if (!allRoleIds.has(roleId)) {
console.error(
- `ERROR: Member "${member.github || member.email}" references team "${teamKey}" which does not exist in groups.ts`
+ `ERROR: Member "${member.github || member.email}" references role "${roleId}" which does not exist in roles.ts`
);
hasErrors = true;
}
}
}
-// Validate parent team references (memberOf in groups)
-console.log('Validating parent team references in groups.ts...');
-for (const g of GROUPS) {
- const group = g as Group;
- if (!group.memberOf) continue;
+// Validate parent role references in roles.ts
+console.log('Validating parent role references in roles.ts...');
+for (const role of ROLES) {
+ if (!role.github?.parent) continue;
- for (const parentTeam of group.memberOf) {
- if (!allGroupNames.has(parentTeam)) {
- console.error(
- `ERROR: Group "${group.name}" has parent "${parentTeam}" which does not exist in groups.ts`
- );
- hasErrors = true;
- }
+ const parentRole = roleLookup.get(role.github.parent);
+ if (!parentRole) {
+ console.error(
+ `ERROR: Role "${role.id}" has parent "${role.github.parent}" which does not exist in roles.ts`
+ );
+ hasErrors = true;
+ } else if (!parentRole.github) {
+ console.error(
+ `ERROR: Role "${role.id}" has parent "${role.github.parent}" which does not have GitHub config`
+ );
+ hasErrors = true;
}
}
@@ -77,6 +78,6 @@ if (hasErrors) {
console.error('\nValidation failed! Please fix the errors above.');
process.exit(1);
} else {
- console.log('\nAll team references are valid.');
+ console.log('\nAll references are valid.');
process.exit(0);
}
diff --git a/src/config/groups.ts b/src/config/groups.ts
deleted file mode 100644
index 72c15528..00000000
--- a/src/config/groups.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import { defineGroups } from './utils';
-
-// NOTE: For GitHub teams, only the first memberOf will be used as the parent team.
-// GitHub only supports one parent team per team.
-//
-// Email groups (isEmailGroup: true) accept emails from anyone (including external users)
-// and notify group members for each email.
-//
-// Groups are created on all platforms (GitHub and Google) by default.
-// To limit a group to specific platforms, set the onlyOnPlatforms array (e.g., onlyOnPlatforms: ['google']).
-export const GROUPS = defineGroups([
- // MCP Organization Structure
- {
- name: 'steering-committee',
- description: 'MCP Steering Committee',
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'core-maintainers',
- description: 'Core maintainers',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'moderators',
- description: 'Community moderators',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'docs-maintaners',
- description: 'MCP docs maintainers',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'inspector-maintainers',
- description: 'MCP Inspector maintainers',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'mcpb-maintainers',
- description: 'MCPB (Model Context Protocol Bundle) maintainers',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
-
- // SDK Maintainers
- {
- name: 'sdk-maintainers',
- description: 'Authors and maintainers of official MCP SDKs',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'csharp-sdk',
- description: 'Official C# SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'go-sdk',
- description: 'The Go SDK Team',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'java-sdk',
- description: 'Official Java SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'kotlin-sdk',
- description: 'Official Kotlin SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'php-sdk',
- description: 'Official PHP SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'python-sdk',
- description: 'Official Python SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'python-sdk-auth',
- description: 'Auth related owners',
- memberOf: ['python-sdk'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'ruby-sdk',
- description: 'Official Ruby SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'rust-sdk',
- description: 'Official Rust SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'swift-sdk',
- description: 'Official Swift SDK maintainers',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'typescript-sdk',
- description: 'Official TypeScript SDK',
- memberOf: ['sdk-maintainers'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'typescript-sdk-auth',
- description: 'Code owners for auth in Typescript SDK',
- memberOf: ['typescript-sdk'],
- onlyOnPlatforms: ['github'],
- },
-
- // Working Groups
- {
- name: 'working-groups',
- description: 'MCP Working Groups',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'auth-wg',
- description: 'Authentication Working Group',
- memberOf: ['working-groups'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'registry-wg',
- description: 'Official registry builders and maintainers',
- memberOf: ['working-groups'],
- },
- {
- name: 'security-wg',
- description: 'Security Working Group',
- memberOf: ['working-groups'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'transport-wg',
- description: 'Transport Working Group',
- memberOf: ['working-groups'],
- onlyOnPlatforms: ['github'],
- },
-
- // Interest Groups
- {
- name: 'interest-groups',
- description: 'Interest Groups',
- memberOf: ['steering-committee'],
- onlyOnPlatforms: ['github'],
- },
- {
- name: 'ig-financial-services',
- description: 'Financial Services Interest Group',
- memberOf: ['interest-groups'],
- onlyOnPlatforms: ['github'],
- },
-
- // Email-only groups
- {
- name: 'antitrust',
- description: 'Antitrust compliance contacts',
- isEmailGroup: true,
- onlyOnPlatforms: ['google'],
- },
- {
- name: 'catch-all',
- description: 'Catch-all email group',
- isEmailGroup: true,
- onlyOnPlatforms: ['google'],
- },
-] as const);
diff --git a/src/config/repoAccess.ts b/src/config/repoAccess.ts
index 054429f8..3434d954 100644
--- a/src/config/repoAccess.ts
+++ b/src/config/repoAccess.ts
@@ -21,7 +21,7 @@ export const REPOSITORY_ACCESS: RepositoryAccess[] = [
{ team: 'auth-wg', permission: 'push' },
{ team: 'core-maintainers', permission: 'maintain' },
{ team: 'csharp-sdk', permission: 'push' },
- { team: 'docs-maintaners', permission: 'push' },
+ { team: 'docs-maintainers', permission: 'push' },
{ team: 'go-sdk', permission: 'push' },
{ team: 'ig-financial-services', permission: 'push' },
{ team: 'interest-groups', permission: 'push' },
@@ -51,7 +51,7 @@ export const REPOSITORY_ACCESS: RepositoryAccess[] = [
{ team: 'auth-wg', permission: 'triage' },
{ team: 'core-maintainers', permission: 'maintain' },
{ team: 'csharp-sdk', permission: 'triage' },
- { team: 'docs-maintaners', permission: 'triage' },
+ { team: 'docs-maintainers', permission: 'triage' },
{ team: 'go-sdk', permission: 'triage' },
{ team: 'ig-financial-services', permission: 'triage' },
{ team: 'interest-groups', permission: 'triage' },
@@ -113,7 +113,7 @@ export const REPOSITORY_ACCESS: RepositoryAccess[] = [
{ team: 'auth-wg', permission: 'push' },
{ team: 'core-maintainers', permission: 'maintain' },
{ team: 'csharp-sdk', permission: 'triage' },
- { team: 'docs-maintaners', permission: 'push' },
+ { team: 'docs-maintainers', permission: 'push' },
{ team: 'go-sdk', permission: 'triage' },
{ team: 'ig-financial-services', permission: 'triage' },
{ team: 'interest-groups', permission: 'triage' },
@@ -143,7 +143,7 @@ export const REPOSITORY_ACCESS: RepositoryAccess[] = [
{ team: 'auth-wg', permission: 'push' },
{ team: 'core-maintainers', permission: 'maintain' },
{ team: 'csharp-sdk', permission: 'push' },
- { team: 'docs-maintaners', permission: 'push' },
+ { team: 'docs-maintainers', permission: 'push' },
{ team: 'go-sdk', permission: 'push' },
{ team: 'ig-financial-services', permission: 'push' },
{ team: 'interest-groups', permission: 'push' },
@@ -176,7 +176,7 @@ export const REPOSITORY_ACCESS: RepositoryAccess[] = [
{ team: 'auth-wg', permission: 'push' },
{ team: 'core-maintainers', permission: 'admin' },
{ team: 'csharp-sdk', permission: 'push' },
- { team: 'docs-maintaners', permission: 'push' },
+ { team: 'docs-maintainers', permission: 'push' },
{ team: 'go-sdk', permission: 'push' },
{ team: 'java-sdk', permission: 'push' },
{ team: 'kotlin-sdk', permission: 'push' },
diff --git a/src/config/roleIds.ts b/src/config/roleIds.ts
new file mode 100644
index 00000000..2170c636
--- /dev/null
+++ b/src/config/roleIds.ts
@@ -0,0 +1,75 @@
+/**
+ * Role ID constants for type-safe role references.
+ * Using constants prevents typos and enables autocomplete.
+ */
+export const ROLE_IDS = {
+ // ===================
+ // Organization Structure
+ // ===================
+ STEERING_COMMITTEE: 'steering-committee',
+ CORE_MAINTAINERS: 'core-maintainers',
+ LEAD_MAINTAINERS: 'lead-maintainers',
+ MODERATORS: 'moderators',
+ ADMINISTRATORS: 'administrators', // Discord only
+
+ // ===================
+ // Maintainer Groups
+ // ===================
+ MAINTAINERS: 'maintainers',
+ DOCS_MAINTAINERS: 'docs-maintainers',
+ INSPECTOR_MAINTAINERS: 'inspector-maintainers',
+ MCPB_MAINTAINERS: 'mcpb-maintainers',
+ REFERENCE_SERVERS_MAINTAINERS: 'reference-servers-maintainers',
+ REGISTRY_MAINTAINERS: 'registry-maintainers',
+ USE_MCP_MAINTAINERS: 'use-mcp-maintainers',
+
+ // ===================
+ // SDK Maintainers
+ // ===================
+ SDK_MAINTAINERS: 'sdk-maintainers',
+ CSHARP_SDK: 'csharp-sdk',
+ GO_SDK: 'go-sdk',
+ JAVA_SDK: 'java-sdk',
+ KOTLIN_SDK: 'kotlin-sdk',
+ PHP_SDK: 'php-sdk',
+ PYTHON_SDK: 'python-sdk',
+ PYTHON_SDK_AUTH: 'python-sdk-auth', // GitHub only (CODEOWNERS)
+ RUBY_SDK: 'ruby-sdk',
+ RUST_SDK: 'rust-sdk',
+ SWIFT_SDK: 'swift-sdk',
+ TYPESCRIPT_SDK: 'typescript-sdk',
+ TYPESCRIPT_SDK_AUTH: 'typescript-sdk-auth', // GitHub only (CODEOWNERS)
+
+ // ===================
+ // Working Groups
+ // ===================
+ WORKING_GROUPS: 'working-groups',
+ AUTH_WG: 'auth-wg',
+ SECURITY_WG: 'security-wg',
+ SERVER_IDENTITY_WG: 'server-identity-wg',
+ TRANSPORT_WG: 'transport-wg',
+
+ // ===================
+ // Interest Groups
+ // ===================
+ INTEREST_GROUPS: 'interest-groups',
+ AGENTS_IG: 'agents-ig',
+ AUTH_IG: 'auth-ig',
+ CLIENT_IMPLEMENTOR_IG: 'client-implementor-ig',
+ FINANCIAL_SERVICES_IG: 'financial-services-ig',
+
+ // ===================
+ // Email Groups (Google only)
+ // ===================
+ ANTITRUST: 'antitrust',
+ CATCH_ALL: 'catch-all',
+} as const;
+
+export type RoleId = (typeof ROLE_IDS)[keyof typeof ROLE_IDS];
+
+/**
+ * Helper to check if a string is a valid RoleId at runtime.
+ */
+export function isValidRoleId(id: string): id is RoleId {
+ return Object.values(ROLE_IDS).includes(id as RoleId);
+}
diff --git a/src/config/roles.ts b/src/config/roles.ts
new file mode 100644
index 00000000..42535d9b
--- /dev/null
+++ b/src/config/roles.ts
@@ -0,0 +1,315 @@
+import { ROLE_IDS, type RoleId } from './roleIds';
+
+/**
+ * GitHub team configuration
+ */
+export interface GitHubConfig {
+ /** Team name (usually matches role ID) */
+ team: string;
+ /** Parent team role ID */
+ parent?: RoleId;
+}
+
+/**
+ * Discord role configuration
+ */
+export interface DiscordConfig {
+ /** Display name in Discord (can have spaces) */
+ role: string;
+}
+
+/**
+ * Google Workspace group configuration
+ */
+export interface GoogleConfig {
+ /** Group name (used as prefix for @modelcontextprotocol.io email) */
+ group: string;
+ /** If true, accepts emails from anyone including external users */
+ isEmailGroup?: boolean;
+}
+
+/**
+ * Role definition with platform-specific configurations.
+ * A role only exists on platforms where it has a config key.
+ */
+export interface Role {
+ id: RoleId;
+ description: string;
+ github?: GitHubConfig;
+ discord?: DiscordConfig;
+ google?: GoogleConfig;
+}
+
+/**
+ * All roles in the MCP organization.
+ * Each role specifies which platforms it exists on via presence of config keys.
+ */
+export const ROLES: readonly Role[] = [
+ // ===================
+ // Organization Structure
+ // ===================
+ {
+ id: ROLE_IDS.STEERING_COMMITTEE,
+ description: 'MCP Steering Committee',
+ github: { team: 'steering-committee' },
+ // No discord - this is a GitHub-only organizational container
+ },
+ {
+ id: ROLE_IDS.ADMINISTRATORS,
+ description: 'Discord server administrators',
+ discord: { role: 'administrators' },
+ // Discord only - no GitHub equivalent
+ },
+ {
+ id: ROLE_IDS.LEAD_MAINTAINERS,
+ description: 'Lead maintainers with elevated responsibilities',
+ discord: { role: 'lead maintainers' },
+ // Discord only for now - could add GitHub if needed
+ },
+ {
+ id: ROLE_IDS.CORE_MAINTAINERS,
+ description: 'Core maintainers',
+ github: { team: 'core-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE },
+ discord: { role: 'core maintainers' },
+ },
+ {
+ id: ROLE_IDS.MODERATORS,
+ description: 'Community moderators',
+ github: { team: 'moderators', parent: ROLE_IDS.STEERING_COMMITTEE },
+ discord: { role: 'community moderators' },
+ },
+
+ // ===================
+ // Maintainer Groups
+ // ===================
+ {
+ id: ROLE_IDS.MAINTAINERS,
+ description: 'General maintainers',
+ discord: { role: 'maintainers' },
+ // Discord only - general maintainer role
+ },
+ {
+ id: ROLE_IDS.DOCS_MAINTAINERS,
+ description: 'MCP docs maintainers',
+ github: { team: 'docs-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE },
+ // No discord role for docs maintainers
+ },
+ {
+ id: ROLE_IDS.INSPECTOR_MAINTAINERS,
+ description: 'MCP Inspector maintainers',
+ github: { team: 'inspector-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE },
+ discord: { role: 'inspector maintainers' },
+ },
+ {
+ id: ROLE_IDS.MCPB_MAINTAINERS,
+ description: 'MCPB (Model Context Protocol Bundle) maintainers',
+ github: { team: 'mcpb-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE },
+ // No discord role
+ },
+ {
+ id: ROLE_IDS.REFERENCE_SERVERS_MAINTAINERS,
+ description: 'Reference servers maintainers',
+ discord: { role: 'reference servers maintainers' },
+ // Discord only for now
+ },
+ {
+ id: ROLE_IDS.REGISTRY_MAINTAINERS,
+ description: 'Official registry builders and maintainers',
+ github: { team: 'registry-wg', parent: ROLE_IDS.WORKING_GROUPS },
+ discord: { role: 'registry maintainers' },
+ google: { group: 'registry-wg' },
+ },
+ {
+ id: ROLE_IDS.USE_MCP_MAINTAINERS,
+ description: 'use-mcp maintainers',
+ discord: { role: 'use-mcp maintainers' },
+ // Discord only
+ },
+
+ // ===================
+ // SDK Maintainers
+ // ===================
+ {
+ id: ROLE_IDS.SDK_MAINTAINERS,
+ description: 'Authors and maintainers of official MCP SDKs',
+ github: { team: 'sdk-maintainers', parent: ROLE_IDS.STEERING_COMMITTEE },
+ discord: { role: 'sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.CSHARP_SDK,
+ description: 'Official C# SDK maintainers',
+ github: { team: 'csharp-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'c# sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.GO_SDK,
+ description: 'The Go SDK Team',
+ github: { team: 'go-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'go sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.JAVA_SDK,
+ description: 'Official Java SDK maintainers',
+ github: { team: 'java-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'java sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.KOTLIN_SDK,
+ description: 'Official Kotlin SDK maintainers',
+ github: { team: 'kotlin-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'kotlin sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.PHP_SDK,
+ description: 'Official PHP SDK maintainers',
+ github: { team: 'php-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'php sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.PYTHON_SDK,
+ description: 'Official Python SDK maintainers',
+ github: { team: 'python-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'python sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.PYTHON_SDK_AUTH,
+ description: 'Python SDK auth code owners',
+ github: { team: 'python-sdk-auth', parent: ROLE_IDS.PYTHON_SDK },
+ // GitHub only - for CODEOWNERS
+ },
+ {
+ id: ROLE_IDS.RUBY_SDK,
+ description: 'Official Ruby SDK maintainers',
+ github: { team: 'ruby-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'ruby sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.RUST_SDK,
+ description: 'Official Rust SDK maintainers',
+ github: { team: 'rust-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'rust sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.SWIFT_SDK,
+ description: 'Official Swift SDK maintainers',
+ github: { team: 'swift-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'swift sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.TYPESCRIPT_SDK,
+ description: 'Official TypeScript SDK',
+ github: { team: 'typescript-sdk', parent: ROLE_IDS.SDK_MAINTAINERS },
+ discord: { role: 'typescript sdk maintainers' },
+ },
+ {
+ id: ROLE_IDS.TYPESCRIPT_SDK_AUTH,
+ description: 'Code owners for auth in Typescript SDK',
+ github: { team: 'typescript-sdk-auth', parent: ROLE_IDS.TYPESCRIPT_SDK },
+ // GitHub only - for CODEOWNERS
+ },
+
+ // ===================
+ // Working Groups
+ // ===================
+ {
+ id: ROLE_IDS.WORKING_GROUPS,
+ description: 'MCP Working Groups',
+ github: { team: 'working-groups', parent: ROLE_IDS.STEERING_COMMITTEE },
+ // No discord - organizational container
+ },
+ {
+ id: ROLE_IDS.AUTH_WG,
+ description: 'Authentication Working Group',
+ github: { team: 'auth-wg', parent: ROLE_IDS.WORKING_GROUPS },
+ // See AUTH_IG for Discord role
+ },
+ {
+ id: ROLE_IDS.SECURITY_WG,
+ description: 'Security Working Group',
+ github: { team: 'security-wg', parent: ROLE_IDS.WORKING_GROUPS },
+ // See interest group for Discord role
+ },
+ {
+ id: ROLE_IDS.SERVER_IDENTITY_WG,
+ description: 'Server Identity Working Group',
+ discord: { role: 'server identity working group' },
+ // Discord only for now
+ },
+ {
+ id: ROLE_IDS.TRANSPORT_WG,
+ description: 'Transport Working Group',
+ github: { team: 'transport-wg', parent: ROLE_IDS.WORKING_GROUPS },
+ discord: { role: 'transports working group' },
+ },
+
+ // ===================
+ // Interest Groups
+ // ===================
+ {
+ id: ROLE_IDS.INTEREST_GROUPS,
+ description: 'Interest Groups',
+ github: { team: 'interest-groups', parent: ROLE_IDS.STEERING_COMMITTEE },
+ // No discord - organizational container
+ },
+ {
+ id: ROLE_IDS.AGENTS_IG,
+ description: 'Agents Interest Group',
+ discord: { role: 'agents interest group' },
+ // Discord only
+ },
+ {
+ id: ROLE_IDS.AUTH_IG,
+ description: 'Auth Interest Group',
+ discord: { role: 'auth interest group' },
+ // Discord only - separate from AUTH_WG which is GitHub
+ },
+ {
+ id: ROLE_IDS.CLIENT_IMPLEMENTOR_IG,
+ description: 'Client Implementor Interest Group',
+ discord: { role: 'client implementor interest group' },
+ // Discord only
+ },
+ {
+ id: ROLE_IDS.FINANCIAL_SERVICES_IG,
+ description: 'Financial Services Interest Group',
+ github: { team: 'ig-financial-services', parent: ROLE_IDS.INTEREST_GROUPS },
+ discord: { role: 'financial services interest group' },
+ },
+
+ // ===================
+ // Email Groups (Google only)
+ // ===================
+ {
+ id: ROLE_IDS.ANTITRUST,
+ description: 'Antitrust compliance contacts',
+ google: { group: 'antitrust', isEmailGroup: true },
+ // Google only
+ },
+ {
+ id: ROLE_IDS.CATCH_ALL,
+ description: 'Catch-all email group',
+ google: { group: 'catch-all', isEmailGroup: true },
+ // Google only
+ },
+] as const;
+
+/**
+ * Get a role by ID
+ */
+export function getRole(id: RoleId): Role | undefined {
+ return ROLES.find((r) => r.id === id);
+}
+
+/**
+ * Get all roles that exist on a specific platform
+ */
+export function getRolesForPlatform(platform: 'github' | 'discord' | 'google'): Role[] {
+ return ROLES.filter((r) => r[platform] !== undefined);
+}
+
+/**
+ * Build a lookup map of roles by ID
+ */
+export function buildRoleLookup(): Map {
+ return new Map(ROLES.map((r) => [r.id, r]));
+}
diff --git a/src/config/users.ts b/src/config/users.ts
index 169725a5..072613b3 100644
--- a/src/config/users.ts
+++ b/src/config/users.ts
@@ -1,85 +1,93 @@
import type { Member } from './utils';
+import { ROLE_IDS } from './roleIds';
export const MEMBERS: readonly Member[] = [
{
github: '000-000-000-000-000',
- memberOf: ['core-maintainers'],
+ memberOf: [ROLE_IDS.CORE_MAINTAINERS],
},
{
github: 'CodeWithKyrian',
- memberOf: ['php-sdk'],
+ memberOf: [ROLE_IDS.PHP_SDK],
},
{
github: 'D-McAdams',
- memberOf: ['auth-wg'],
+ memberOf: [ROLE_IDS.AUTH_WG],
},
{
github: 'Kehrlann',
- memberOf: ['java-sdk'],
+ memberOf: [ROLE_IDS.JAVA_SDK],
},
{
github: 'Kludex',
- memberOf: ['python-sdk'],
+ memberOf: [ROLE_IDS.PYTHON_SDK],
},
{
github: 'Nyholm',
- memberOf: ['php-sdk'],
+ memberOf: [ROLE_IDS.PHP_SDK],
},
{
github: 'Ololoshechkin',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'a-akimov',
- memberOf: ['docs-maintaners'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS],
},
{
github: 'aaronpk',
- memberOf: ['auth-wg'],
+ memberOf: [ROLE_IDS.AUTH_WG],
},
{
github: 'alexhancock',
- memberOf: ['rust-sdk'],
+ memberOf: [ROLE_IDS.RUST_SDK],
},
{
github: 'an-dustin',
- memberOf: ['security-wg'],
+ memberOf: [ROLE_IDS.SECURITY_WG],
},
{
github: 'ansaba',
- memberOf: ['go-sdk'],
+ memberOf: [ROLE_IDS.GO_SDK],
},
{
github: 'asklar',
- memberOf: ['mcpb-maintainers'],
+ memberOf: [ROLE_IDS.MCPB_MAINTAINERS],
},
{
github: 'atesgoral',
- memberOf: ['ruby-sdk'],
+ memberOf: [ROLE_IDS.RUBY_SDK],
},
{
github: 'baxen',
- memberOf: ['rust-sdk'],
+ memberOf: [ROLE_IDS.RUST_SDK],
},
{
github: 'bhosmer-ant',
- memberOf: ['core-maintainers', 'docs-maintaners', 'moderators', 'python-sdk', 'typescript-sdk'],
+ discord: '1272295077074567242',
+ memberOf: [
+ ROLE_IDS.CORE_MAINTAINERS,
+ ROLE_IDS.DOCS_MAINTAINERS,
+ ROLE_IDS.MODERATORS,
+ ROLE_IDS.PYTHON_SDK,
+ ROLE_IDS.TYPESCRIPT_SDK,
+ ],
},
{
github: 'carlpeaslee',
- memberOf: ['swift-sdk'],
+ memberOf: [ROLE_IDS.SWIFT_SDK],
},
{
github: 'chemicL',
- memberOf: ['java-sdk'],
+ memberOf: [ROLE_IDS.JAVA_SDK],
},
{
github: 'chr-hertel',
- memberOf: ['php-sdk'],
+ memberOf: [ROLE_IDS.PHP_SDK],
},
{
github: 'cliffhall',
- memberOf: ['docs-maintaners', 'inspector-maintainers', 'moderators'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.INSPECTOR_MAINTAINERS, ROLE_IDS.MODERATORS],
},
{
github: 'crondinini-ant',
@@ -87,271 +95,278 @@ export const MEMBERS: readonly Member[] = [
},
{
github: 'dend',
- memberOf: ['csharp-sdk'],
+ memberOf: [ROLE_IDS.CSHARP_SDK],
},
{
github: 'devcrocod',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'domdomegg',
email: 'adam@modelcontextprotocol.io',
- memberOf: ['mcpb-maintainers', 'registry-wg'],
+ memberOf: [ROLE_IDS.MCPB_MAINTAINERS, ROLE_IDS.REGISTRY_MAINTAINERS],
},
{
github: 'dsp-ant',
email: 'david@modelcontextprotocol.io',
+ discord: '166107790262272000', // Example Discord user ID - replace with real ID
memberOf: [
- 'auth-wg',
- 'core-maintainers',
- 'docs-maintaners',
- 'go-sdk',
- 'ig-financial-services',
- 'moderators',
- 'php-sdk',
- 'python-sdk',
- 'security-wg',
- 'transport-wg',
- 'typescript-sdk',
+ ROLE_IDS.AUTH_WG,
+ ROLE_IDS.CORE_MAINTAINERS,
+ ROLE_IDS.DOCS_MAINTAINERS,
+ ROLE_IDS.GO_SDK,
+ ROLE_IDS.FINANCIAL_SERVICES_IG,
+ ROLE_IDS.MODERATORS,
+ ROLE_IDS.PHP_SDK,
+ ROLE_IDS.PYTHON_SDK,
+ ROLE_IDS.SECURITY_WG,
+ ROLE_IDS.TRANSPORT_WG,
+ ROLE_IDS.TYPESCRIPT_SDK,
],
},
{
github: 'e5l',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'eiriktsarpalis',
- memberOf: ['csharp-sdk'],
+ memberOf: [ROLE_IDS.CSHARP_SDK],
},
{
github: 'evalstate',
- memberOf: ['docs-maintaners', 'moderators'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.MODERATORS],
},
{
github: 'fabpot',
- memberOf: ['php-sdk'],
+ memberOf: [ROLE_IDS.PHP_SDK],
},
{
github: 'felixrieseberg',
- memberOf: ['mcpb-maintainers'],
+ memberOf: [ROLE_IDS.MCPB_MAINTAINERS],
},
{
github: 'felixweinberger',
- memberOf: ['python-sdk', 'security-wg', 'typescript-sdk'],
+ memberOf: [ROLE_IDS.PYTHON_SDK, ROLE_IDS.SECURITY_WG, ROLE_IDS.TYPESCRIPT_SDK],
},
{
github: 'findleyr',
- memberOf: ['go-sdk'],
+ memberOf: [ROLE_IDS.GO_SDK],
},
{
github: 'halter73',
- memberOf: ['csharp-sdk'],
+ memberOf: [ROLE_IDS.CSHARP_SDK],
},
{
github: 'ignatov',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'ihrpr',
- memberOf: ['docs-maintaners', 'python-sdk', 'typescript-sdk'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.PYTHON_SDK, ROLE_IDS.TYPESCRIPT_SDK],
},
{
github: 'jamadeo',
- memberOf: ['rust-sdk'],
+ memberOf: [ROLE_IDS.RUST_SDK],
},
{
github: 'jba',
- memberOf: ['go-sdk'],
+ memberOf: [ROLE_IDS.GO_SDK],
},
{
github: 'jenn-newton',
- memberOf: ['security-wg'],
+ memberOf: [ROLE_IDS.SECURITY_WG],
},
{
github: 'jerome3o-anthropic',
- memberOf: ['moderators'],
+ memberOf: [ROLE_IDS.MODERATORS],
},
{
github: 'joan-anthropic',
- memberOf: ['mcpb-maintainers'],
+ memberOf: [ROLE_IDS.MCPB_MAINTAINERS],
},
{
github: 'jonathanhefner',
- memberOf: ['docs-maintaners', 'moderators', 'ruby-sdk'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.MODERATORS, ROLE_IDS.RUBY_SDK],
},
{
github: 'jspahrsummers',
email: 'justin@modelcontextprotocol.io',
- memberOf: ['core-maintainers'],
+ memberOf: [ROLE_IDS.CORE_MAINTAINERS],
},
{
github: 'KKonstantinov',
- memberOf: ['inspector-maintainers', 'typescript-sdk'],
+ memberOf: [ROLE_IDS.INSPECTOR_MAINTAINERS, ROLE_IDS.TYPESCRIPT_SDK],
},
{
github: 'koic',
- memberOf: ['ruby-sdk'],
+ memberOf: [ROLE_IDS.RUBY_SDK],
},
{
github: 'kpavlov',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'kurtisvg',
- memberOf: ['core-maintainers', 'transport-wg'],
+ memberOf: [ROLE_IDS.CORE_MAINTAINERS, ROLE_IDS.TRANSPORT_WG],
},
{
github: 'localden',
- memberOf: ['auth-wg', 'core-maintainers'],
+ memberOf: [ROLE_IDS.AUTH_WG, ROLE_IDS.CORE_MAINTAINERS],
},
{
github: 'maheshmurag',
- memberOf: ['moderators'],
+ memberOf: [ROLE_IDS.MODERATORS],
},
{
github: 'markpollack',
- memberOf: ['java-sdk'],
+ memberOf: [ROLE_IDS.JAVA_SDK],
},
{
github: 'marshallofsound',
- memberOf: ['mcpb-maintainers'],
+ memberOf: [ROLE_IDS.MCPB_MAINTAINERS],
},
{
github: 'mattt',
- memberOf: ['swift-sdk'],
+ memberOf: [ROLE_IDS.SWIFT_SDK],
},
{
github: 'maxisbey',
- memberOf: ['python-sdk'],
+ memberOf: [ROLE_IDS.PYTHON_SDK],
},
{
github: 'mattzcarey',
- memberOf: ['typescript-sdk'],
+ memberOf: [ROLE_IDS.TYPESCRIPT_SDK],
},
{
github: 'michaelneale',
- memberOf: ['rust-sdk'],
+ memberOf: [ROLE_IDS.RUST_SDK],
},
{
github: 'movetz',
- memberOf: ['swift-sdk'],
+ memberOf: [ROLE_IDS.SWIFT_SDK],
},
{
github: 'mikekistler',
- memberOf: ['csharp-sdk'],
+ memberOf: [ROLE_IDS.CSHARP_SDK],
},
{
github: 'nickcoai',
- memberOf: ['core-maintainers'],
+ memberOf: [ROLE_IDS.CORE_MAINTAINERS],
},
{
github: 'nicolas-grekas',
- memberOf: ['php-sdk'],
+ memberOf: [ROLE_IDS.PHP_SDK],
},
{
github: 'ochafik',
- memberOf: ['python-sdk', 'python-sdk-auth', 'typescript-sdk', 'typescript-sdk-auth'],
+ memberOf: [
+ ROLE_IDS.PYTHON_SDK,
+ ROLE_IDS.PYTHON_SDK_AUTH,
+ ROLE_IDS.TYPESCRIPT_SDK,
+ ROLE_IDS.TYPESCRIPT_SDK_AUTH,
+ ],
},
{
github: 'og-ant',
- memberOf: ['security-wg'],
+ memberOf: [ROLE_IDS.SECURITY_WG],
},
{
github: 'olaservo',
- memberOf: ['docs-maintaners', 'inspector-maintainers', 'moderators'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.INSPECTOR_MAINTAINERS, ROLE_IDS.MODERATORS],
},
{
github: 'pcarleton',
memberOf: [
- 'core-maintainers',
- 'python-sdk',
- 'python-sdk-auth',
- 'typescript-sdk',
- 'typescript-sdk-auth',
- 'auth-wg',
+ ROLE_IDS.CORE_MAINTAINERS,
+ ROLE_IDS.PYTHON_SDK,
+ ROLE_IDS.PYTHON_SDK_AUTH,
+ ROLE_IDS.TYPESCRIPT_SDK,
+ ROLE_IDS.TYPESCRIPT_SDK_AUTH,
+ ROLE_IDS.AUTH_WG,
],
},
{
github: 'pederhp',
- memberOf: ['ig-financial-services'],
+ memberOf: [ROLE_IDS.FINANCIAL_SERVICES_IG],
},
{
github: 'petery-ant',
- memberOf: ['security-wg'],
+ memberOf: [ROLE_IDS.SECURITY_WG],
},
{
github: 'pronskiy',
- memberOf: ['php-sdk'],
+ memberOf: [ROLE_IDS.PHP_SDK],
},
{
github: 'pwwpche',
- memberOf: ['core-maintainers'],
+ memberOf: [ROLE_IDS.CORE_MAINTAINERS],
},
{
github: 'rdimitrov',
email: 'radoslav@modelcontextprotocol.io',
- memberOf: ['registry-wg'],
+ memberOf: [ROLE_IDS.REGISTRY_MAINTAINERS],
},
{
github: 'sambhav',
- memberOf: ['ig-financial-services'],
+ memberOf: [ROLE_IDS.FINANCIAL_SERVICES_IG],
},
{
github: 'samthanawalla',
- memberOf: ['go-sdk'],
+ memberOf: [ROLE_IDS.GO_SDK],
},
{
github: 'sdubov',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'stephentoub',
- memberOf: ['csharp-sdk'],
+ memberOf: [ROLE_IDS.CSHARP_SDK],
},
{
github: 'stallent',
- memberOf: ['swift-sdk'],
+ memberOf: [ROLE_IDS.SWIFT_SDK],
},
{
github: 'tadasant',
email: 'tadas@modelcontextprotocol.io',
- memberOf: ['moderators', 'registry-wg'],
+ memberOf: [ROLE_IDS.MODERATORS, ROLE_IDS.REGISTRY_MAINTAINERS, ROLE_IDS.ADMINISTRATORS],
},
{
github: 'tiginamaria',
- memberOf: ['kotlin-sdk'],
+ memberOf: [ROLE_IDS.KOTLIN_SDK],
},
{
github: 'toby',
email: 'toby@modelcontextprotocol.io',
- memberOf: ['registry-wg'],
+ memberOf: [ROLE_IDS.REGISTRY_MAINTAINERS],
},
{
github: 'topherbullock',
- memberOf: ['ruby-sdk'],
+ memberOf: [ROLE_IDS.RUBY_SDK],
},
{
github: 'tzolov',
- memberOf: ['docs-maintaners', 'java-sdk'],
+ memberOf: [ROLE_IDS.DOCS_MAINTAINERS, ROLE_IDS.JAVA_SDK],
},
{
email: 'adamj@anthropic.com',
- memberOf: ['catch-all'],
+ memberOf: [ROLE_IDS.CATCH_ALL],
},
{
email: 'davidsp@anthropic.com',
- memberOf: ['antitrust'],
+ memberOf: [ROLE_IDS.ANTITRUST],
},
{
email: 'mattsamuels@anthropic.com',
- memberOf: ['antitrust'],
+ memberOf: [ROLE_IDS.ANTITRUST],
},
{
email: 'davideramian@anthropic.com',
- memberOf: ['antitrust'],
+ memberOf: [ROLE_IDS.ANTITRUST],
},
{
github: 'caitiem20',
- memberOf: ['core-maintainers'],
+ discord: '1425586366288494722',
+ memberOf: [ROLE_IDS.CORE_MAINTAINERS],
},
] as const;
diff --git a/src/config/utils.ts b/src/config/utils.ts
index fd0112a5..782c3e9c 100644
--- a/src/config/utils.ts
+++ b/src/config/utils.ts
@@ -1,43 +1,71 @@
-import type { GROUPS } from './groups';
+import type { RoleId } from './roleIds';
+import type { Role } from './roles';
-function isValidGroupName(name: string): boolean {
- return /^[a-z][a-z0-9-]*[a-z]$/.test(name);
+/**
+ * A member of the MCP organization.
+ * Members are assigned to roles via memberOf, and the role definitions
+ * determine which platforms (GitHub, Discord, Google) they get access to.
+ */
+export interface Member {
+ /** GitHub username */
+ github?: string;
+ /** Email address (for Google Workspace) */
+ email?: string;
+ /** Discord user ID (snowflake) */
+ discord?: string;
+ /** Roles this member belongs to */
+ memberOf: readonly RoleId[];
}
-export type Platform = 'github' | 'google';
-
-export function defineGroups<
- const T extends readonly {
- name: Lowercase;
- description: string;
- memberOf?: readonly T[number]['name'][];
- isEmailGroup?: boolean;
- onlyOnPlatforms?: readonly Platform[];
- }[],
->(groups: T) {
- for (const group of groups) {
- if (!isValidGroupName(group.name)) {
- throw new Error(
- `Invalid group name: ${group.name}. Must be lowercase alphanumeric with dashes, starting with a letter.`
- );
+/**
+ * Sort roles by GitHub parent dependency (topological sort).
+ * Ensures parent teams are created before child teams.
+ *
+ * This is necessary because when creating GitHub teams, we need the parent
+ * team's ID. If we process roles in arbitrary order, a child team might be
+ * processed before its parent, resulting in undefined parentTeamId.
+ */
+export function sortRolesByGitHubDependency(
+ roles: readonly Role[],
+ roleLookup: Map
+): Role[] {
+ const result: Role[] = [];
+ const visited = new Set();
+
+ function visit(role: Role): void {
+ if (visited.has(role.id)) return;
+
+ // Only process roles with GitHub config
+ if (!role.github) {
+ visited.add(role.id);
+ return;
}
- }
- return groups;
-}
+ // Visit parent first if it exists and has GitHub config
+ if (role.github.parent) {
+ const parentRole = roleLookup.get(role.github.parent);
+ if (parentRole) {
+ visit(parentRole);
+ }
+ }
-export type GroupKey = (typeof GROUPS)[number]['name'];
+ visited.add(role.id);
+ result.push(role);
+ }
-export interface Group {
- name: GroupKey;
- description: string;
- memberOf?: readonly GroupKey[];
- isEmailGroup?: boolean;
- onlyOnPlatforms?: readonly Platform[];
-}
+ for (const role of roles) {
+ visit(role);
+ }
-export interface Member {
- github?: string;
- email?: string;
- memberOf: readonly GroupKey[];
+ return result;
}
+
+// Re-export for convenience
+export { ROLE_IDS, type RoleId } from './roleIds';
+export {
+ ROLES,
+ type Role,
+ type GitHubConfig,
+ type DiscordConfig,
+ type GoogleConfig,
+} from './roles';
diff --git a/src/discord.ts b/src/discord.ts
new file mode 100644
index 00000000..a26f6ce4
--- /dev/null
+++ b/src/discord.ts
@@ -0,0 +1,418 @@
+import * as pulumi from '@pulumi/pulumi';
+import { ROLES, type Role, buildRoleLookup } from './config/roles';
+import { MEMBERS } from './config/users';
+import type { RoleId } from './config/roleIds';
+
+const config = new pulumi.Config('discord');
+// Discord integration is optional - only enabled if botToken and guildId are configured
+const DISCORD_BOT_TOKEN = config.getSecret('botToken');
+const DISCORD_GUILD_ID = config.get('guildId');
+const DISCORD_ENABLED = DISCORD_BOT_TOKEN !== undefined && DISCORD_GUILD_ID !== undefined;
+
+if (!DISCORD_ENABLED) {
+ pulumi.log.info('Discord integration disabled: botToken or guildId not configured');
+}
+
+const DISCORD_API_BASE = 'https://discord.com/api/v10';
+
+interface DiscordApiError {
+ code: number;
+ message: string;
+}
+
+async function discordFetch(
+ token: string,
+ endpoint: string,
+ options: RequestInit = {}
+): Promise {
+ const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, {
+ ...options,
+ headers: {
+ Authorization: `Bot ${token}`,
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const error = (await response.json()) as DiscordApiError;
+ throw new Error(`Discord API error: ${error.message} (code: ${error.code})`);
+ }
+
+ // Handle 204 No Content
+ if (response.status === 204) {
+ return undefined as T;
+ }
+
+ return response.json() as Promise;
+}
+
+// Discord API response types
+interface DiscordRoleApiResponse {
+ id: string;
+ name: string;
+ position: number;
+ permissions: string;
+ managed: boolean;
+}
+
+interface DiscordGuildMemberApiResponse {
+ roles: string[];
+}
+
+// Discord Role Dynamic Provider
+interface DiscordRoleInputs {
+ guildId: string;
+ roleName: string;
+ token: string;
+}
+
+interface DiscordRoleOutputs extends DiscordRoleInputs {
+ roleId: string;
+}
+
+const discordRoleProvider: pulumi.dynamic.ResourceProvider = {
+ async create(
+ inputs: DiscordRoleInputs
+ ): Promise> {
+ const role = await discordFetch(
+ inputs.token,
+ `/guilds/${inputs.guildId}/roles`,
+ {
+ method: 'POST',
+ body: JSON.stringify({
+ name: inputs.roleName,
+ permissions: '0', // No special permissions - roles are for organization only
+ mentionable: false,
+ hoist: false,
+ }),
+ }
+ );
+
+ return {
+ id: role.id,
+ outs: {
+ ...inputs,
+ roleId: role.id,
+ },
+ };
+ },
+
+ async read(
+ id: string,
+ props: DiscordRoleOutputs
+ ): Promise> {
+ try {
+ const roles = await discordFetch(
+ props.token,
+ `/guilds/${props.guildId}/roles`
+ );
+
+ const role = roles.find((r) => r.id === id);
+ if (!role) {
+ // Role was deleted externally
+ throw new Error(`Role ${id} not found`);
+ }
+
+ return {
+ id,
+ props: {
+ ...props,
+ roleName: role.name,
+ roleId: role.id,
+ },
+ };
+ } catch {
+ throw new Error(`Failed to read role ${id}`);
+ }
+ },
+
+ async update(
+ id: string,
+ _olds: DiscordRoleOutputs,
+ news: DiscordRoleInputs
+ ): Promise> {
+ await discordFetch(news.token, `/guilds/${news.guildId}/roles/${id}`, {
+ method: 'PATCH',
+ body: JSON.stringify({
+ name: news.roleName,
+ }),
+ });
+
+ return {
+ outs: {
+ ...news,
+ roleId: id,
+ },
+ };
+ },
+
+ async delete(id: string, props: DiscordRoleOutputs): Promise {
+ try {
+ await discordFetch(props.token, `/guilds/${props.guildId}/roles/${id}`, {
+ method: 'DELETE',
+ });
+ } catch (error) {
+ // Ignore errors if role is already deleted
+ console.warn(`Failed to delete role ${id}: ${error}`);
+ }
+ },
+};
+
+class DiscordRole extends pulumi.dynamic.Resource {
+ public readonly roleId!: pulumi.Output;
+ public readonly roleName!: pulumi.Output;
+ public readonly guildId!: pulumi.Output;
+
+ constructor(
+ name: string,
+ args: {
+ guildId: pulumi.Input;
+ roleName: pulumi.Input;
+ token: pulumi.Input;
+ },
+ opts?: pulumi.CustomResourceOptions
+ ) {
+ super(
+ discordRoleProvider,
+ name,
+ {
+ roleId: undefined,
+ ...args,
+ },
+ opts
+ );
+ }
+}
+
+// Discord Member Role Sync Dynamic Provider
+// This provider reconciles a user's roles to match exactly what's defined in config
+// It adds missing roles AND removes extra roles (only for roles we manage)
+interface DiscordMemberRoleSyncInputs {
+ guildId: string;
+ userId: string;
+ /** Role IDs that this user SHOULD have (managed roles only) */
+ expectedRoleIds: string[];
+ /** All role IDs that we manage (to know which ones to potentially remove) */
+ managedRoleIds: string[];
+ token: string;
+}
+
+interface DiscordMemberRoleSyncOutputs extends DiscordMemberRoleSyncInputs {
+ /** Roles that were added during last sync */
+ addedRoles: string[];
+ /** Roles that were removed during last sync */
+ removedRoles: string[];
+}
+
+async function syncMemberRoles(
+ inputs: DiscordMemberRoleSyncInputs
+): Promise<{ addedRoles: string[]; removedRoles: string[] }> {
+ // Get the user's current roles
+ const member = await discordFetch(
+ inputs.token,
+ `/guilds/${inputs.guildId}/members/${inputs.userId}`
+ );
+
+ const currentRoles = new Set(member.roles);
+ const expectedRoles = new Set(inputs.expectedRoleIds);
+ const managedRoles = new Set(inputs.managedRoleIds);
+
+ const addedRoles: string[] = [];
+ const removedRoles: string[] = [];
+
+ // Add missing roles
+ for (const roleId of Array.from(expectedRoles)) {
+ if (!currentRoles.has(roleId)) {
+ await discordFetch(
+ inputs.token,
+ `/guilds/${inputs.guildId}/members/${inputs.userId}/roles/${roleId}`,
+ { method: 'PUT' }
+ );
+ addedRoles.push(roleId);
+ }
+ }
+
+ // Remove roles that the user has but shouldn't (only managed roles)
+ for (const roleId of Array.from(currentRoles)) {
+ if (managedRoles.has(roleId) && !expectedRoles.has(roleId)) {
+ await discordFetch(
+ inputs.token,
+ `/guilds/${inputs.guildId}/members/${inputs.userId}/roles/${roleId}`,
+ { method: 'DELETE' }
+ );
+ removedRoles.push(roleId);
+ }
+ }
+
+ return { addedRoles, removedRoles };
+}
+
+const discordMemberRoleSyncProvider: pulumi.dynamic.ResourceProvider = {
+ async create(
+ inputs: DiscordMemberRoleSyncInputs
+ ): Promise> {
+ const { addedRoles, removedRoles } = await syncMemberRoles(inputs);
+
+ return {
+ id: inputs.userId,
+ outs: {
+ ...inputs,
+ addedRoles,
+ removedRoles,
+ },
+ };
+ },
+
+ async read(
+ id: string,
+ props: DiscordMemberRoleSyncOutputs
+ ): Promise> {
+ try {
+ const member = await discordFetch(
+ props.token,
+ `/guilds/${props.guildId}/members/${props.userId}`
+ );
+
+ const currentRoles = new Set(member.roles);
+ const expectedRoles = new Set(props.expectedRoleIds);
+ const managedRoles = new Set(props.managedRoleIds);
+
+ // Check if roles are in sync (only considering managed roles)
+ const outOfSync =
+ Array.from(expectedRoles).some((r) => !currentRoles.has(r)) ||
+ Array.from(currentRoles).some((r) => managedRoles.has(r) && !expectedRoles.has(r));
+
+ if (outOfSync) {
+ // Return current state but note it needs update
+ return {
+ id,
+ props: {
+ ...props,
+ addedRoles: [],
+ removedRoles: [],
+ },
+ };
+ }
+
+ return { id, props };
+ } catch {
+ throw new Error(`Failed to read member roles for ${id}`);
+ }
+ },
+
+ async update(
+ id: string,
+ _olds: DiscordMemberRoleSyncOutputs,
+ news: DiscordMemberRoleSyncInputs
+ ): Promise> {
+ const { addedRoles, removedRoles } = await syncMemberRoles(news);
+
+ return {
+ outs: {
+ ...news,
+ addedRoles,
+ removedRoles,
+ },
+ };
+ },
+
+ async delete(id: string, props: DiscordMemberRoleSyncOutputs): Promise {
+ // When a user is removed from config, remove all their managed roles
+ for (const roleId of props.expectedRoleIds) {
+ try {
+ await discordFetch(
+ props.token,
+ `/guilds/${props.guildId}/members/${props.userId}/roles/${roleId}`,
+ { method: 'DELETE' }
+ );
+ } catch (error) {
+ console.warn(`Failed to remove role ${roleId} from user ${id}: ${error}`);
+ }
+ }
+ },
+};
+
+class DiscordMemberRoleSync extends pulumi.dynamic.Resource {
+ public readonly addedRoles!: pulumi.Output;
+ public readonly removedRoles!: pulumi.Output;
+
+ constructor(
+ name: string,
+ args: {
+ guildId: pulumi.Input;
+ userId: pulumi.Input;
+ expectedRoleIds: pulumi.Input[]>;
+ managedRoleIds: pulumi.Input[]>;
+ token: pulumi.Input;
+ },
+ opts?: pulumi.CustomResourceOptions
+ ) {
+ super(
+ discordMemberRoleSyncProvider,
+ name,
+ {
+ addedRoles: undefined,
+ removedRoles: undefined,
+ ...args,
+ },
+ opts
+ );
+ }
+}
+
+const roleLookup = buildRoleLookup();
+// Discord roles keyed by Discord role name
+const roles: Record = {};
+
+// Only create Discord resources if Discord is enabled
+if (DISCORD_ENABLED) {
+ // These are guaranteed to be defined when DISCORD_ENABLED is true
+ const guildId = DISCORD_GUILD_ID!;
+ const botToken = DISCORD_BOT_TOKEN!;
+
+ // Create Discord roles for roles that have Discord config
+ ROLES.forEach((role: Role) => {
+ if (!role.discord) return;
+
+ roles[role.discord.role] = new DiscordRole(`discord-role-${role.id}`, {
+ guildId,
+ roleName: role.discord.role,
+ token: botToken,
+ });
+ });
+
+ // Collect all managed role IDs (roles that have Discord config)
+ const allManagedRoleIds = ROLES.filter((r) => r.discord).map(
+ (r) => roles[r.discord!.role].roleId
+ );
+
+ // Sync roles for each member
+ MEMBERS.forEach((member) => {
+ if (!member.discord) return;
+
+ // Get the Discord role IDs this member should have
+ const expectedRoleIds = member.memberOf
+ .map((roleId: RoleId) => {
+ const role = roleLookup.get(roleId);
+ if (!role?.discord) return null;
+ return roles[role.discord.role].roleId;
+ })
+ .filter((id): id is pulumi.Output => id !== null);
+
+ // Create a sync resource for this member
+ new DiscordMemberRoleSync(
+ `discord-member-sync-${member.discord}`,
+ {
+ guildId,
+ userId: member.discord!,
+ expectedRoleIds,
+ managedRoleIds: allManagedRoleIds,
+ token: botToken,
+ },
+ { dependsOn: Object.values(roles) }
+ );
+ });
+}
+
+export { roles as discordRoles };
diff --git a/src/github.ts b/src/github.ts
index d398bf63..3d6a2230 100644
--- a/src/github.ts
+++ b/src/github.ts
@@ -1,40 +1,61 @@
import * as github from '@pulumi/github';
-import { GROUPS } from './config/groups';
+import { ROLES, type Role, buildRoleLookup } from './config/roles';
import { REPOSITORY_ACCESS } from './config/repoAccess';
-import type { Group } from './config/utils';
import { MEMBERS } from './config/users';
+import { sortRolesByGitHubDependency } from './config/utils';
+import type { RoleId } from './config/roleIds';
+const roleLookup = buildRoleLookup();
+// Teams keyed by GitHub team name (matches repoAccess.ts references)
const teams: Record = {};
-GROUPS.forEach((group: Group) => {
- // Skip groups that don't include github in their platforms
- if (group.onlyOnPlatforms && !group.onlyOnPlatforms.includes('github')) return;
+// Sort roles so parent teams are created before child teams
+const sortedRoles = sortRolesByGitHubDependency(ROLES, roleLookup);
- teams[group.name] = new github.Team(group.name, {
- name: group.name,
- description: group.description + ' \n(Managed by github.com/modelcontextprotocol/access)',
+// Create GitHub teams for roles that have GitHub config
+sortedRoles.forEach((role: Role) => {
+ if (!role.github) return;
+
+ // Resolve parent team ID if specified
+ // Parent is guaranteed to exist in `teams` due to topological sort
+ let parentTeamId: github.Team['id'] | undefined;
+ if (role.github.parent) {
+ const parentRole = roleLookup.get(role.github.parent);
+ if (parentRole?.github) {
+ parentTeamId = teams[parentRole.github.team]?.id;
+ }
+ }
+
+ teams[role.github.team] = new github.Team(role.github.team, {
+ name: role.github.team,
+ description: role.description + ' \n(Managed by github.com/modelcontextprotocol/access)',
privacy: 'closed',
- parentTeamId: group.memberOf?.[0] ? teams[group.memberOf[0]].id : undefined,
+ parentTeamId,
});
});
+// Create team memberships
MEMBERS.forEach((member) => {
if (!member.github) return;
- member.memberOf.forEach((teamKey) => {
- new github.TeamMembership(`${member.github}-${teamKey}`, {
- teamId: teams[teamKey].id,
+ member.memberOf.forEach((roleId: RoleId) => {
+ const role = roleLookup.get(roleId);
+ if (!role?.github) return; // Role doesn't have GitHub config
+
+ new github.TeamMembership(`${member.github}-${role.github.team}`, {
+ teamId: teams[role.github.team].id,
username: member.github!,
role: 'member',
});
});
});
+// Configure repository access
REPOSITORY_ACCESS.forEach((repo) => {
new github.RepositoryCollaborators(`repo-${repo.repository}`, {
repository: repo.repository,
teams: repo.teams?.map((t) => ({
- teamId: teams[t.team].id,
+ teamId: teams[t.team]?.id,
permission: t.permission,
})),
users: repo.users?.map((u) => ({
@@ -43,3 +64,5 @@ REPOSITORY_ACCESS.forEach((repo) => {
})),
});
});
+
+export { teams as githubTeams };
diff --git a/src/google.ts b/src/google.ts
index 96c7d7c8..f52499b5 100644
--- a/src/google.ts
+++ b/src/google.ts
@@ -1,22 +1,24 @@
import * as gworkspace from '@pulumi/googleworkspace';
-import { GROUPS } from './config/groups';
-import type { Group } from './config/utils';
+import { ROLES, type Role, buildRoleLookup } from './config/roles';
import { MEMBERS } from './config/users';
+import type { RoleId } from './config/roleIds';
+const roleLookup = buildRoleLookup();
+// Groups keyed by Google group name
const groups: Record = {};
-GROUPS.forEach((group: Group) => {
- // Skip groups that don't include google in their platforms
- if (group.onlyOnPlatforms && !group.onlyOnPlatforms.includes('google')) return;
+// Create Google groups for roles that have Google config
+ROLES.forEach((role: Role) => {
+ if (!role.google) return;
- groups[group.name] = new gworkspace.Group(group.name, {
- email: `${group.name}@modelcontextprotocol.io`,
- name: group.name,
- description: group.description + ' \n(Managed by github.com/modelcontextprotocol/access)',
+ groups[role.google.group] = new gworkspace.Group(role.google.group, {
+ email: `${role.google.group}@modelcontextprotocol.io`,
+ name: role.google.group,
+ description: role.description + ' \n(Managed by github.com/modelcontextprotocol/access)',
});
- new gworkspace.GroupSettings(group.name, {
- email: groups[group.name].email,
+ new gworkspace.GroupSettings(role.google.group, {
+ email: groups[role.google.group].email,
// Maximise visibility of group. It's visible in GitHub anyway
whoCanViewMembership: 'ALL_IN_DOMAIN_CAN_VIEW',
@@ -29,7 +31,7 @@ GROUPS.forEach((group: Group) => {
// Email groups allow anyone (including externals) to post
// Non-email groups are not intended as mailing lists, so use the most restrictive settings
// whoCanViewGroup is badly named, but actually means 'Permissions to view group messages'. See https://developers.google.com/workspace/admin/groups-settings/v1/reference/groups
- ...(group.isEmailGroup
+ ...(role.google.isEmailGroup
? {
whoCanPostMessage: 'ANYONE_CAN_POST',
whoCanContactOwner: 'ALL_OWNERS_CAN_CONTACT',
@@ -41,30 +43,22 @@ GROUPS.forEach((group: Group) => {
whoCanViewGroup: 'ALL_OWNERS_CAN_VIEW',
}),
});
-
- group.memberOf?.forEach((parentGroupKey) => {
- // Skip if parent group doesn't exist on Google (e.g., onlyOnPlatforms: ['github'])
- if (!groups[parentGroupKey]) return;
-
- new gworkspace.GroupMember(`${group.name}-in-${parentGroupKey}`, {
- groupId: groups[parentGroupKey].id,
- email: groups[group.name].email,
- role: 'MEMBER',
- });
- });
});
+// Create group memberships for users
MEMBERS.forEach((member) => {
if (!member.email) return;
- member.memberOf.forEach((teamKey) => {
- // Skip if group doesn't exist on Google (e.g., onlyOnPlatforms: ['github'])
- if (!groups[teamKey]) return;
+ member.memberOf.forEach((roleId: RoleId) => {
+ const role = roleLookup.get(roleId);
+ if (!role?.google) return; // Role doesn't have Google config
- new gworkspace.GroupMember(`${member.email}-${teamKey}`, {
- groupId: groups[teamKey].id,
+ new gworkspace.GroupMember(`${member.email}-${role.google.group}`, {
+ groupId: groups[role.google.group].id,
email: member.email!,
role: 'MEMBER',
});
});
});
+
+export { groups as googleGroups };
diff --git a/src/index.ts b/src/index.ts
index 4fe06f18..c72d079b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,3 @@
export * from './github';
export * from './google';
+export * from './discord';