diff --git a/README.md b/README.md
index 9d32100497..5b90f64a58 100644
--- a/README.md
+++ b/README.md
@@ -586,7 +586,7 @@ The following sets of tools are available:
|
| `discussions` | GitHub Discussions related tools |
|
| `gists` | GitHub Gist related tools |
|
| `git` | GitHub Git API related tools for low-level Git operations |
-|
| `governance` | Repository governance tools for managing rulesets at the repository, organization, and enterprise levels |
+|
| `governance` | Repository governance tools for managing rulesets and custom properties at the repository, organization, and enterprise levels |
|
| `issues` | GitHub Issues related tools |
|
| `labels` | GitHub Labels related tools |
|
| `notifications` | GitHub Notifications related tools |
@@ -892,6 +892,29 @@ The following sets of tools are available:
- `rules`: An array of rules within the ruleset. Each rule is an object with a 'type' (e.g. 'creation', 'deletion', 'non_fast_forward', 'required_signatures', 'pull_request', 'required_status_checks') and, for rules that need configuration, a 'parameters' object (object[], required)
- `target`: The target of the ruleset. Defaults to 'branch'. 'repository' is only valid for 'organization' and 'enterprise' level rulesets. (string, optional)
+- **custom_properties_read** - Read custom properties
+ - **OAuth Challenge Scopes**: `repo`, `read:org`, `read:enterprise`
+ - `enterprise`: Enterprise slug. Required when level is 'enterprise'. (string, optional)
+ - `level`: The level at which custom properties are managed:
+ - 'repository': The custom property VALUES assigned to a repository (requires 'owner' and 'repo').
+ - 'organization': The custom property DEFINITIONS (schema) for an organization (requires 'org').
+ - 'enterprise': The custom property DEFINITIONS (schema) for an enterprise (requires 'enterprise'). (string, required)
+ - `org`: Organization name. Required when level is 'organization'. (string, optional)
+ - `owner`: Repository owner. Required when level is 'repository'. (string, optional)
+ - `repo`: Repository name. Required when level is 'repository'. (string, optional)
+
+- **custom_properties_write** - Set custom properties
+ - **OAuth Challenge Scopes**: `repo`, `admin:org`, `admin:enterprise`
+ - `enterprise`: Enterprise slug. Required when level is 'enterprise'. (string, optional)
+ - `level`: The level at which custom properties are managed:
+ - 'repository': The custom property VALUES assigned to a repository (requires 'owner' and 'repo').
+ - 'organization': The custom property DEFINITIONS (schema) for an organization (requires 'org').
+ - 'enterprise': The custom property DEFINITIONS (schema) for an enterprise (requires 'enterprise'). (string, required)
+ - `org`: Organization name. Required when level is 'organization'. (string, optional)
+ - `owner`: Repository owner. Required when level is 'repository'. (string, optional)
+ - `properties`: The custom properties to create or update. At the repository level each item assigns a value ('property_name' and 'value'); at the organization and enterprise levels each item defines the schema ('property_name' and 'value_type', plus optional definition fields). (object[], required)
+ - `repo`: Repository name. Required when level is 'repository'. (string, optional)
+
- **repository_ruleset_read** - Read repository rulesets
- **OAuth Challenge Scopes**: `repo`, `read:org`, `read:enterprise`
- `actor_name`: The handle for the GitHub user account to filter rule suites on. Used by the 'list_rule_suites' method. (string, optional)
diff --git a/docs/remote-server.md b/docs/remote-server.md
index 78b5c906ac..be91009148 100644
--- a/docs/remote-server.md
+++ b/docs/remote-server.md
@@ -30,7 +30,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
| 
`discussions` | GitHub Discussions related tools | https://api.githubcopilot.com/mcp/x/discussions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/discussions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%2Freadonly%22%7D) |
| 
`gists` | GitHub Gist related tools | https://api.githubcopilot.com/mcp/x/gists | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/gists/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%2Freadonly%22%7D) |
| 
`git` | GitHub Git API related tools for low-level Git operations | https://api.githubcopilot.com/mcp/x/git | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-git&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgit%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/git/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-git&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgit%2Freadonly%22%7D) |
-| 
`governance` | Repository governance tools for managing rulesets at the repository, organization, and enterprise levels | https://api.githubcopilot.com/mcp/x/governance | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-governance&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgovernance%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/governance/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-governance&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgovernance%2Freadonly%22%7D) |
+| 
`governance` | Repository governance tools for managing rulesets and custom properties at the repository, organization, and enterprise levels | https://api.githubcopilot.com/mcp/x/governance | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-governance&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgovernance%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/governance/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-governance&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgovernance%2Freadonly%22%7D) |
| 
`issues` | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) |
| 
`labels` | GitHub Labels related tools | https://api.githubcopilot.com/mcp/x/labels | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-labels&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Flabels%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/labels/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-labels&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Flabels%2Freadonly%22%7D) |
| 
`notifications` | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) |
diff --git a/pkg/github/__toolsnaps__/custom_properties_read.snap b/pkg/github/__toolsnaps__/custom_properties_read.snap
new file mode 100644
index 0000000000..9a7ff10e9b
--- /dev/null
+++ b/pkg/github/__toolsnaps__/custom_properties_read.snap
@@ -0,0 +1,42 @@
+{
+ "annotations": {
+ "idempotentHint": false,
+ "readOnlyHint": true,
+ "title": "Read custom properties"
+ },
+ "description": "Read custom properties at the repository, organization, or enterprise level. At the repository level this returns the property values assigned to a repository; at the organization and enterprise levels it returns the property definitions (schema). Select the level with the 'level' parameter.",
+ "inputSchema": {
+ "properties": {
+ "enterprise": {
+ "description": "Enterprise slug. Required when level is 'enterprise'.",
+ "type": "string"
+ },
+ "level": {
+ "description": "The level at which custom properties are managed:\n- 'repository': The custom property VALUES assigned to a repository (requires 'owner' and 'repo').\n- 'organization': The custom property DEFINITIONS (schema) for an organization (requires 'org').\n- 'enterprise': The custom property DEFINITIONS (schema) for an enterprise (requires 'enterprise').",
+ "enum": [
+ "repository",
+ "organization",
+ "enterprise"
+ ],
+ "type": "string"
+ },
+ "org": {
+ "description": "Organization name. Required when level is 'organization'.",
+ "type": "string"
+ },
+ "owner": {
+ "description": "Repository owner. Required when level is 'repository'.",
+ "type": "string"
+ },
+ "repo": {
+ "description": "Repository name. Required when level is 'repository'.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "level"
+ ],
+ "type": "object"
+ },
+ "name": "custom_properties_read"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/custom_properties_write.snap b/pkg/github/__toolsnaps__/custom_properties_write.snap
new file mode 100644
index 0000000000..9c9df1f6ca
--- /dev/null
+++ b/pkg/github/__toolsnaps__/custom_properties_write.snap
@@ -0,0 +1,174 @@
+{
+ "annotations": {
+ "idempotentHint": false,
+ "readOnlyHint": false,
+ "title": "Set custom properties"
+ },
+ "description": "Create or update custom properties at the repository, organization, or enterprise level. At the repository level this sets the property values on a repository (the properties must already be defined for the organization). Organization and enterprise definition writes preserve omitted writable fields by reading current definitions immediately before updating; concurrent definition updates remain last-write-wins. Select the level with the 'level' parameter.",
+ "inputSchema": {
+ "properties": {
+ "enterprise": {
+ "description": "Enterprise slug. Required when level is 'enterprise'.",
+ "type": "string"
+ },
+ "level": {
+ "description": "The level at which custom properties are managed:\n- 'repository': The custom property VALUES assigned to a repository (requires 'owner' and 'repo').\n- 'organization': The custom property DEFINITIONS (schema) for an organization (requires 'org').\n- 'enterprise': The custom property DEFINITIONS (schema) for an enterprise (requires 'enterprise').",
+ "enum": [
+ "repository",
+ "organization",
+ "enterprise"
+ ],
+ "type": "string"
+ },
+ "org": {
+ "description": "Organization name. Required when level is 'organization'.",
+ "type": "string"
+ },
+ "owner": {
+ "description": "Repository owner. Required when level is 'repository'.",
+ "type": "string"
+ },
+ "properties": {
+ "description": "The custom properties to create or update. At the repository level each item assigns a value ('property_name' and 'value'); at the organization and enterprise levels each item defines the schema ('property_name' and 'value_type', plus optional definition fields).",
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "description": "A repository-level custom property value.",
+ "properties": {
+ "property_name": {
+ "description": "The name of the custom property.",
+ "type": "string"
+ },
+ "value": {
+ "description": "Repository level only: the value to assign. A string, an array of strings, or null to clear the value.",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "property_name",
+ "value"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "description": "An organization- or enterprise-level custom property definition.",
+ "properties": {
+ "allowed_values": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The ordered list of allowed values for single_select and multi_select properties. Omit when updating to preserve the current list; use null or an empty array to clear it."
+ },
+ "default_value": {
+ "description": "The value applied when a repository does not set the property. Omit when updating to preserve the current value; use null to clear it.",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "A short description of the property. Omit when updating to preserve the current description; use null to clear it."
+ },
+ "property_name": {
+ "description": "The name of the custom property.",
+ "type": "string"
+ },
+ "require_explicit_values": {
+ "description": "Whether repositories must explicitly set a value for the property. Omit when updating to preserve the current setting.",
+ "type": "boolean"
+ },
+ "required": {
+ "description": "Whether the property must be set on every repository. Omit when updating to preserve the current setting.",
+ "type": "boolean"
+ },
+ "value_type": {
+ "description": "The data type of the property. Required for new definitions; omit when updating to preserve the current type.",
+ "enum": [
+ "string",
+ "single_select",
+ "multi_select",
+ "true_false",
+ "url"
+ ],
+ "type": "string"
+ },
+ "values_editable_by": {
+ "anyOf": [
+ {
+ "enum": [
+ "org_actors",
+ "org_and_repo_actors"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Who can edit the values of the property. Omit when updating to preserve the current setting; use null to restore the default."
+ }
+ },
+ "required": [
+ "property_name"
+ ],
+ "type": "object"
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "repo": {
+ "description": "Repository name. Required when level is 'repository'.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "level",
+ "properties"
+ ],
+ "type": "object"
+ },
+ "name": "custom_properties_write"
+}
\ No newline at end of file
diff --git a/pkg/github/custom_properties.go b/pkg/github/custom_properties.go
new file mode 100644
index 0000000000..0e71aa8ecc
--- /dev/null
+++ b/pkg/github/custom_properties.go
@@ -0,0 +1,492 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ ghErrors "github.com/github/github-mcp-server/pkg/errors"
+ "github.com/github/github-mcp-server/pkg/inventory"
+ "github.com/github/github-mcp-server/pkg/translations"
+ "github.com/github/github-mcp-server/pkg/utils"
+ "github.com/google/go-github/v89/github"
+ "github.com/google/jsonschema-go/jsonschema"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+const customPropertiesLevelDescription = "The level at which custom properties are managed:\n" +
+ "- 'repository': The custom property VALUES assigned to a repository (requires 'owner' and 'repo').\n" +
+ "- 'organization': The custom property DEFINITIONS (schema) for an organization (requires 'org').\n" +
+ "- 'enterprise': The custom property DEFINITIONS (schema) for an enterprise (requires 'enterprise')."
+
+// CustomPropertiesRead creates the custom properties read tool.
+func CustomPropertiesRead(t translations.TranslationHelperFunc) inventory.ServerTool {
+ return NewTool(
+ ToolsetMetadataGovernance,
+ mcp.Tool{
+ Name: "custom_properties_read",
+ Description: t("TOOL_CUSTOM_PROPERTIES_READ_DESCRIPTION", "Read custom properties at the repository, organization, or enterprise level. At the repository level this returns the property values assigned to a repository; at the organization and enterprise levels it returns the property definitions (schema). Select the level with the 'level' parameter."),
+ Annotations: &mcp.ToolAnnotations{
+ Title: t("TOOL_CUSTOM_PROPERTIES_READ_USER_TITLE", "Read custom properties"),
+ ReadOnlyHint: true,
+ },
+ InputSchema: &jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "level": {
+ Type: "string",
+ Enum: []any{"repository", "organization", "enterprise"},
+ Description: customPropertiesLevelDescription,
+ },
+ "owner": {
+ Type: "string",
+ Description: "Repository owner. Required when level is 'repository'.",
+ },
+ "repo": {
+ Type: "string",
+ Description: "Repository name. Required when level is 'repository'.",
+ },
+ "org": {
+ Type: "string",
+ Description: "Organization name. Required when level is 'organization'.",
+ },
+ "enterprise": {
+ Type: "string",
+ Description: "Enterprise slug. Required when level is 'enterprise'.",
+ },
+ },
+ Required: []string{"level"},
+ },
+ },
+ rulesetReadScopeAccess(),
+ func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
+ level, err := RequiredParam[string](args, "level")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ client, err := deps.GetClient(ctx)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ switch level {
+ case "repository":
+ return customPropertiesReadRepository(ctx, client, args)
+ case "organization":
+ return customPropertiesReadOrganization(ctx, client, args)
+ case "enterprise":
+ return customPropertiesReadEnterprise(ctx, client, args)
+ default:
+ return utils.NewToolResultError(fmt.Sprintf("unknown level: %q (expected 'repository', 'organization', or 'enterprise')", level)), nil, nil
+ }
+ },
+ )
+}
+
+func customPropertiesReadRepository(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ owner, err := RequiredParam[string](args, "owner")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ repo, err := RequiredParam[string](args, "repo")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ properties, resp, err := client.Repositories.GetAllCustomPropertyValues(ctx, owner, repo)
+ if resp != nil {
+ defer func() { _ = resp.Body.Close() }()
+ }
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get repository custom property values", resp, err), nil, nil
+ }
+
+ return MarshalledTextResult(properties), nil, nil
+}
+
+func customPropertiesReadOrganization(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ org, err := RequiredParam[string](args, "org")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ properties, errResult := getCustomPropertyDefinitions(ctx, client, fmt.Sprintf("orgs/%s/properties/schema", url.PathEscape(org)), "failed to get organization custom properties")
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+
+ return MarshalledTextResult(properties), nil, nil
+}
+
+func customPropertiesReadEnterprise(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ enterprise, err := RequiredParam[string](args, "enterprise")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ properties, errResult := getCustomPropertyDefinitions(ctx, client, fmt.Sprintf("enterprises/%s/properties/schema", url.PathEscape(enterprise)), "failed to get enterprise custom properties")
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+
+ return MarshalledTextResult(properties), nil, nil
+}
+
+// CustomPropertiesWrite creates the custom properties write tool.
+func CustomPropertiesWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
+ return NewTool(
+ ToolsetMetadataGovernance,
+ mcp.Tool{
+ Name: "custom_properties_write",
+ Description: t("TOOL_CUSTOM_PROPERTIES_WRITE_DESCRIPTION", "Create or update custom properties at the repository, organization, or enterprise level. At the repository level this sets the property values on a repository (the properties must already be defined for the organization). Organization and enterprise definition writes preserve omitted writable fields by reading current definitions immediately before updating; concurrent definition updates remain last-write-wins. Select the level with the 'level' parameter."),
+ Annotations: &mcp.ToolAnnotations{
+ Title: t("TOOL_CUSTOM_PROPERTIES_WRITE_USER_TITLE", "Set custom properties"),
+ ReadOnlyHint: false,
+ },
+ InputSchema: &jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "level": {
+ Type: "string",
+ Enum: []any{"repository", "organization", "enterprise"},
+ Description: customPropertiesLevelDescription,
+ },
+ "owner": {
+ Type: "string",
+ Description: "Repository owner. Required when level is 'repository'.",
+ },
+ "repo": {
+ Type: "string",
+ Description: "Repository name. Required when level is 'repository'.",
+ },
+ "org": {
+ Type: "string",
+ Description: "Organization name. Required when level is 'organization'.",
+ },
+ "enterprise": {
+ Type: "string",
+ Description: "Enterprise slug. Required when level is 'enterprise'.",
+ },
+ "properties": {
+ Type: "array",
+ Description: "The custom properties to create or update. At the repository level each item assigns a value ('property_name' and 'value'); at the organization and enterprise levels each item defines the schema ('property_name' and 'value_type', plus optional definition fields).",
+ Items: &jsonschema.Schema{
+ Type: "object",
+ OneOf: []*jsonschema.Schema{
+ customPropertyValueSchema(),
+ customPropertyDefinitionSchema(),
+ },
+ },
+ },
+ },
+ Required: []string{"level", "properties"},
+ },
+ },
+ rulesetWriteScopeAccess(),
+ func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
+ level, err := RequiredParam[string](args, "level")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ client, err := deps.GetClient(ctx)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ switch level {
+ case "repository":
+ return customPropertiesWriteRepository(ctx, client, args)
+ case "organization":
+ return customPropertiesWriteOrganization(ctx, client, args)
+ case "enterprise":
+ return customPropertiesWriteEnterprise(ctx, client, args)
+ default:
+ return utils.NewToolResultError(fmt.Sprintf("unknown level: %q (expected 'repository', 'organization', or 'enterprise')", level)), nil, nil
+ }
+ },
+ )
+}
+
+func customPropertiesWriteRepository(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ owner, err := RequiredParam[string](args, "owner")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ repo, err := RequiredParam[string](args, "repo")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ values, errResult := parseCustomProperties[*github.CustomPropertyValue](args, customPropertyValueSchema())
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+
+ resp, err := client.Repositories.CreateOrUpdateCustomProperties(ctx, owner, repo, values)
+ if resp != nil {
+ defer func() { _ = resp.Body.Close() }()
+ }
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update repository custom property values", resp, err), nil, nil
+ }
+
+ return utils.NewToolResultText("Repository custom property values updated successfully"), nil, nil
+}
+
+func customPropertiesWriteOrganization(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ org, err := RequiredParam[string](args, "org")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ properties, errResult := parseCustomProperties[map[string]any](args, customPropertyDefinitionSchema())
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+
+ return customPropertiesWriteDefinitions(ctx, client, fmt.Sprintf("orgs/%s/properties/schema", url.PathEscape(org)), "organization", properties)
+}
+
+func customPropertiesWriteEnterprise(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ enterprise, err := RequiredParam[string](args, "enterprise")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ properties, errResult := parseCustomProperties[map[string]any](args, customPropertyDefinitionSchema())
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+
+ return customPropertiesWriteDefinitions(ctx, client, fmt.Sprintf("enterprises/%s/properties/schema", url.PathEscape(enterprise)), "enterprise", properties)
+}
+
+func customPropertyValueSchema() *jsonschema.Schema {
+ return &jsonschema.Schema{
+ Type: "object",
+ Description: "A repository-level custom property value.",
+ AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
+ Properties: map[string]*jsonschema.Schema{
+ "property_name": {
+ Type: "string",
+ Description: "The name of the custom property.",
+ },
+ "value": {
+ Description: "Repository level only: the value to assign. A string, an array of strings, or null to clear the value.",
+ OneOf: []*jsonschema.Schema{
+ {Type: "string"},
+ {
+ Type: "array",
+ Items: &jsonschema.Schema{Type: "string"},
+ },
+ {Type: "null"},
+ },
+ },
+ },
+ Required: []string{"property_name", "value"},
+ }
+}
+
+func customPropertyDefinitionSchema() *jsonschema.Schema {
+ return &jsonschema.Schema{
+ Type: "object",
+ Description: "An organization- or enterprise-level custom property definition.",
+ AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
+ Properties: map[string]*jsonschema.Schema{
+ "property_name": {
+ Type: "string",
+ Description: "The name of the custom property.",
+ },
+ "value_type": {
+ Type: "string",
+ Enum: []any{"string", "single_select", "multi_select", "true_false", "url"},
+ Description: "The data type of the property. Required for new definitions; omit when updating to preserve the current type.",
+ },
+ "required": {
+ Type: "boolean",
+ Description: "Whether the property must be set on every repository. Omit when updating to preserve the current setting.",
+ },
+ "default_value": {
+ Description: "The value applied when a repository does not set the property. Omit when updating to preserve the current value; use null to clear it.",
+ OneOf: []*jsonschema.Schema{
+ {Type: "string"},
+ {
+ Type: "array",
+ Items: &jsonschema.Schema{Type: "string"},
+ },
+ {Type: "null"},
+ },
+ },
+ "description": {
+ Description: "A short description of the property. Omit when updating to preserve the current description; use null to clear it.",
+ AnyOf: []*jsonschema.Schema{
+ {Type: "string"},
+ {Type: "null"},
+ },
+ },
+ "allowed_values": {
+ Description: "The ordered list of allowed values for single_select and multi_select properties. Omit when updating to preserve the current list; use null or an empty array to clear it.",
+ AnyOf: []*jsonschema.Schema{
+ {
+ Type: "array",
+ Items: &jsonschema.Schema{Type: "string"},
+ },
+ {Type: "null"},
+ },
+ },
+ "values_editable_by": {
+ Description: "Who can edit the values of the property. Omit when updating to preserve the current setting; use null to restore the default.",
+ AnyOf: []*jsonschema.Schema{
+ {
+ Type: "string",
+ Enum: []any{"org_actors", "org_and_repo_actors"},
+ },
+ {Type: "null"},
+ },
+ },
+ "require_explicit_values": {
+ Type: "boolean",
+ Description: "Whether repositories must explicitly set a value for the property. Omit when updating to preserve the current setting.",
+ },
+ },
+ Required: []string{"property_name"},
+ }
+}
+
+var customPropertyDefinitionFields = []string{
+ "value_type",
+ "required",
+ "default_value",
+ "description",
+ "allowed_values",
+ "values_editable_by",
+ "require_explicit_values",
+}
+
+func customPropertiesWriteDefinitions(ctx context.Context, client *github.Client, apiURL, sourceType string, requested []map[string]any) (*mcp.CallToolResult, any, error) {
+ if errResult := validateUniqueCustomPropertyNames(requested); errResult != nil {
+ return errResult, nil, nil
+ }
+
+ current, errResult := getCustomPropertyDefinitions(ctx, client, apiURL, fmt.Sprintf("failed to get %s custom properties before updating", sourceType))
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+ merged, errResult := mergeCustomPropertyDefinitions(current, requested, sourceType)
+ if errResult != nil {
+ return errResult, nil, nil
+ }
+
+ req, err := client.NewRequest(ctx, http.MethodPatch, apiURL, map[string]any{"properties": merged})
+ if err != nil {
+ return utils.NewToolResultErrorFromErr("failed to create custom properties update request", err), nil, nil
+ }
+
+ var updated []map[string]any
+ resp, err := client.Do(req, &updated)
+ if resp != nil {
+ defer func() { _ = resp.Body.Close() }()
+ }
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, fmt.Sprintf("failed to update %s custom properties", sourceType), resp, err), nil, nil
+ }
+ return MarshalledTextResult(updated), nil, nil
+}
+
+func getCustomPropertyDefinitions(ctx context.Context, client *github.Client, apiURL, errorMessage string) ([]map[string]any, *mcp.CallToolResult) {
+ req, err := client.NewRequest(ctx, http.MethodGet, apiURL, nil)
+ if err != nil {
+ return nil, utils.NewToolResultErrorFromErr("failed to create custom properties request", err)
+ }
+
+ var properties []map[string]any
+ resp, err := client.Do(req, &properties)
+ if resp != nil {
+ defer func() { _ = resp.Body.Close() }()
+ }
+ if err != nil {
+ return nil, ghErrors.NewGitHubAPIErrorResponse(ctx, errorMessage, resp, err)
+ }
+ return properties, nil
+}
+
+func validateUniqueCustomPropertyNames(properties []map[string]any) *mcp.CallToolResult {
+ seen := make(map[string]struct{}, len(properties))
+ for i, property := range properties {
+ name := property["property_name"].(string)
+ if _, ok := seen[name]; ok {
+ return utils.NewToolResultError(fmt.Sprintf("properties[%d].property_name duplicates %q", i, name))
+ }
+ seen[name] = struct{}{}
+ }
+ return nil
+}
+
+func mergeCustomPropertyDefinitions(current, requested []map[string]any, sourceType string) ([]map[string]any, *mcp.CallToolResult) {
+ currentByName := make(map[string]map[string]any, len(current))
+ for _, property := range current {
+ name, _ := property["property_name"].(string)
+ if name != "" {
+ currentByName[name] = property
+ }
+ }
+
+ merged := make([]map[string]any, 0, len(requested))
+ for i, update := range requested {
+ name := update["property_name"].(string)
+ existing, exists := currentByName[name]
+ if !exists {
+ if _, ok := update["value_type"]; !ok {
+ return nil, utils.NewToolResultError(fmt.Sprintf("properties[%d].value_type is required for new property %q", i, name))
+ }
+ merged = append(merged, update)
+ continue
+ }
+ if existingSource, _ := existing["source_type"].(string); existingSource != "" && existingSource != sourceType {
+ return nil, utils.NewToolResultError(fmt.Sprintf("property %q is inherited from %s and cannot be updated at the %s level", name, existingSource, sourceType))
+ }
+
+ property := map[string]any{"property_name": name}
+ for _, field := range customPropertyDefinitionFields {
+ if value, ok := existing[field]; ok {
+ property[field] = value
+ }
+ if value, ok := update[field]; ok {
+ property[field] = value
+ }
+ }
+ merged = append(merged, property)
+ }
+ return merged, nil
+}
+
+func parseCustomProperties[T any](args map[string]any, itemSchema *jsonschema.Schema) ([]T, *mcp.CallToolResult) {
+ raw, ok := args["properties"]
+ if !ok || raw == nil {
+ return nil, utils.NewToolResultError("properties parameter is required")
+ }
+ arr, ok := raw.([]any)
+ if !ok {
+ return nil, utils.NewToolResultError("properties parameter must be an array")
+ }
+ resolved, err := itemSchema.Resolve(nil)
+ if err != nil {
+ return nil, utils.NewToolResultErrorFromErr("failed to resolve properties schema", err)
+ }
+ for i, property := range arr {
+ if err := resolved.Validate(property); err != nil {
+ return nil, utils.NewToolResultErrorFromErr(fmt.Sprintf("properties[%d] is invalid", i), err)
+ }
+ }
+
+ encoded, err := json.Marshal(arr)
+ if err != nil {
+ return nil, utils.NewToolResultErrorFromErr("failed to encode properties", err)
+ }
+ var out []T
+ if err := json.Unmarshal(encoded, &out); err != nil {
+ return nil, utils.NewToolResultErrorFromErr("failed to parse properties", err)
+ }
+ return out, nil
+}
diff --git a/pkg/github/custom_properties_test.go b/pkg/github/custom_properties_test.go
new file mode 100644
index 0000000000..be04872038
--- /dev/null
+++ b/pkg/github/custom_properties_test.go
@@ -0,0 +1,550 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/google/go-github/v89/github"
+ "github.com/google/jsonschema-go/jsonschema"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/github/github-mcp-server/internal/toolsnaps"
+ "github.com/github/github-mcp-server/pkg/translations"
+)
+
+func Test_CustomPropertiesRead(t *testing.T) {
+ toolDef := CustomPropertiesRead(translations.NullTranslationHelper)
+ require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool))
+
+ assert.Equal(t, "custom_properties_read", toolDef.Tool.Name)
+ assert.NotEmpty(t, toolDef.Tool.Description)
+ assert.True(t, toolDef.Tool.Annotations.ReadOnlyHint)
+
+ schema, ok := toolDef.Tool.InputSchema.(*jsonschema.Schema)
+ require.True(t, ok, "InputSchema should be *jsonschema.Schema")
+ assert.ElementsMatch(t, schema.Required, []string{"level"})
+
+ t.Run("repository level: returns property values", func(t *testing.T) {
+ mockValues := []*github.CustomPropertyValue{{PropertyName: "environment", Value: "production"}}
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "GET /repos/{owner}/{repo}/properties/values": mockResponse(t, http.StatusOK, mockValues),
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "repository", "owner": "owner", "repo": "repo"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ var returned []*github.CustomPropertyValue
+ require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returned))
+ require.Len(t, returned, 1)
+ assert.Equal(t, "environment", returned[0].PropertyName)
+ })
+
+ t.Run("repository level: requires owner and repo", func(t *testing.T) {
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "repository", "owner": "owner"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.True(t, result.IsError)
+ assert.Contains(t, getErrorResult(t, result).Text, "repo")
+ })
+
+ t.Run("organization level: returns property definitions", func(t *testing.T) {
+ mockProps := []map[string]any{{
+ "property_name": "environment",
+ "value_type": "single_select",
+ "require_explicit_values": true,
+ }}
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "GET /orgs/{org}/properties/schema": mockResponse(t, http.StatusOK, mockProps),
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "organization", "org": "octo"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ var returned []map[string]any
+ require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returned))
+ require.Len(t, returned, 1)
+ assert.Equal(t, "environment", returned[0]["property_name"])
+ assert.Equal(t, true, returned[0]["require_explicit_values"])
+ })
+
+ t.Run("organization level: requires org", func(t *testing.T) {
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "organization"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.True(t, result.IsError)
+ assert.Contains(t, getErrorResult(t, result).Text, "org")
+ })
+
+ t.Run("enterprise level: returns property definitions", func(t *testing.T) {
+ mockProps := []*github.CustomProperty{{PropertyName: github.Ptr("compliance"), ValueType: "true_false"}}
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "GET /enterprises/{enterprise}/properties/schema": mockResponse(t, http.StatusOK, mockProps),
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "enterprise", "enterprise": "acme"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ var returned []*github.CustomProperty
+ require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returned))
+ require.Len(t, returned, 1)
+ assert.Equal(t, "compliance", returned[0].GetPropertyName())
+ })
+
+ t.Run("unknown level returns an error", func(t *testing.T) {
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "team"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.True(t, result.IsError)
+ assert.Contains(t, getErrorResult(t, result).Text, "unknown level")
+ })
+}
+
+func Test_CustomPropertiesWrite(t *testing.T) {
+ toolDef := CustomPropertiesWrite(translations.NullTranslationHelper)
+ require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool))
+
+ assert.Equal(t, "custom_properties_write", toolDef.Tool.Name)
+ assert.NotEmpty(t, toolDef.Tool.Description)
+ assert.False(t, toolDef.Tool.Annotations.ReadOnlyHint)
+
+ schema, ok := toolDef.Tool.InputSchema.(*jsonschema.Schema)
+ require.True(t, ok, "InputSchema should be *jsonschema.Schema")
+ assert.ElementsMatch(t, schema.Required, []string{"level", "properties"})
+
+ t.Run("property items use level-specific schemas", func(t *testing.T) {
+ itemSchema := schema.Properties["properties"].Items
+ assert.Equal(t, "object", itemSchema.Type)
+ require.Len(t, itemSchema.OneOf, 2)
+ valueItemSchema := itemSchema.OneOf[0]
+ definitionItemSchema := itemSchema.OneOf[1]
+ require.NotNil(t, valueItemSchema.AdditionalProperties.Not)
+ require.NotNil(t, definitionItemSchema.AdditionalProperties.Not)
+ assert.ElementsMatch(t, []string{"property_name", "value"}, valueItemSchema.Required)
+ assert.ElementsMatch(t, []string{"property_name"}, definitionItemSchema.Required)
+ assert.Equal(t, "boolean", definitionItemSchema.Properties["require_explicit_values"].Type)
+
+ resolvedValue, err := valueItemSchema.Resolve(nil)
+ require.NoError(t, err)
+ resolvedDefinition, err := definitionItemSchema.Resolve(nil)
+ require.NoError(t, err)
+
+ valueSchema := valueItemSchema.Properties["value"]
+ require.Len(t, valueSchema.OneOf, 3)
+ assert.Equal(t, "string", valueSchema.OneOf[0].Type)
+ assert.Equal(t, "array", valueSchema.OneOf[1].Type)
+ assert.Equal(t, "string", valueSchema.OneOf[1].Items.Type)
+ assert.Equal(t, "null", valueSchema.OneOf[2].Type)
+
+ defaultValueSchema := definitionItemSchema.Properties["default_value"]
+ require.Len(t, defaultValueSchema.OneOf, 3)
+ assert.Equal(t, "string", defaultValueSchema.OneOf[0].Type)
+ assert.Equal(t, "array", defaultValueSchema.OneOf[1].Type)
+ assert.Equal(t, "string", defaultValueSchema.OneOf[1].Items.Type)
+ assert.Equal(t, "null", defaultValueSchema.OneOf[2].Type)
+
+ tests := []struct {
+ name string
+ definition bool
+ property map[string]any
+ shouldPass bool
+ }{
+ {name: "repository string", property: map[string]any{"property_name": "environment", "value": "production"}, shouldPass: true},
+ {name: "repository string array", property: map[string]any{"property_name": "environment", "value": []any{"production", "staging"}}, shouldPass: true},
+ {name: "repository null", property: map[string]any{"property_name": "environment", "value": nil}, shouldPass: true},
+ {name: "repository number", property: map[string]any{"property_name": "environment", "value": 1}},
+ {name: "repository boolean", property: map[string]any{"property_name": "environment", "value": true}},
+ {name: "repository object", property: map[string]any{"property_name": "environment", "value": map[string]any{"name": "production"}}},
+ {name: "repository definition field", property: map[string]any{"property_name": "environment", "value": "production", "required": true}},
+ {
+ name: "definition all fields",
+ definition: true,
+ property: map[string]any{
+ "property_name": "environment",
+ "value_type": "single_select",
+ "required": true,
+ "default_value": "production",
+ "description": "Deployment environment",
+ "allowed_values": []any{"production", "staging"},
+ "values_editable_by": "org_and_repo_actors",
+ "require_explicit_values": true,
+ },
+ shouldPass: true,
+ },
+ {name: "partial definition", definition: true, property: map[string]any{"property_name": "environment", "required": false}, shouldPass: true},
+ {name: "definition array default", definition: true, property: map[string]any{"property_name": "compliance", "value_type": "multi_select", "default_value": []any{"soc2", "fedramp"}}, shouldPass: true},
+ {name: "definition explicit nulls", definition: true, property: map[string]any{"property_name": "environment", "default_value": nil, "description": nil, "allowed_values": nil, "values_editable_by": nil}, shouldPass: true},
+ {name: "definition number default", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "default_value": 1}},
+ {name: "definition boolean default", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "default_value": true}},
+ {name: "definition object default", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "default_value": map[string]any{"name": "production"}}},
+ {name: "definition repository field", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "value": "production"}},
+ {name: "definition misspelled field", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "require": true}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ resolved := resolvedValue
+ if tt.definition {
+ resolved = resolvedDefinition
+ }
+ err := resolved.Validate(tt.property)
+ if tt.shouldPass {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ }
+ })
+ }
+ })
+
+ t.Run("repository level: sets property values with one request", func(t *testing.T) {
+ requests := 0
+ var captured struct {
+ Properties []*github.CustomPropertyValue `json:"properties"`
+ }
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "PATCH /repos/{owner}/{repo}/properties/values": func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ body, _ := io.ReadAll(r.Body)
+ _ = json.Unmarshal(body, &captured)
+ w.WriteHeader(http.StatusNoContent)
+ },
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{
+ "level": "repository",
+ "owner": "owner",
+ "repo": "repo",
+ "properties": []any{
+ map[string]any{"property_name": "environment", "value": "production"},
+ map[string]any{"property_name": "deprecated", "value": nil},
+ },
+ })
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+ assert.Contains(t, getTextResult(t, result).Text, "updated successfully")
+ assert.Equal(t, 1, requests)
+
+ require.Len(t, captured.Properties, 2)
+ assert.Equal(t, "environment", captured.Properties[0].PropertyName)
+ assert.Equal(t, "production", captured.Properties[0].Value)
+ assert.Equal(t, "deprecated", captured.Properties[1].PropertyName)
+ assert.Nil(t, captured.Properties[1].Value)
+ })
+
+ t.Run("repository level: requires properties", func(t *testing.T) {
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{"level": "repository", "owner": "owner", "repo": "repo"})
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.True(t, result.IsError)
+ assert.Contains(t, getErrorResult(t, result).Text, "properties parameter is required")
+ })
+
+ t.Run("organization level: merges mixed create and update in two requests", func(t *testing.T) {
+ requests := 0
+ var captured struct {
+ Properties []map[string]any `json:"properties"`
+ }
+ current := []map[string]any{
+ {
+ "property_name": "environment",
+ "source_type": "organization",
+ "url": "https://api.github.com/orgs/octo/properties/schema/environment",
+ "value_type": "single_select",
+ "required": true,
+ "default_value": "production",
+ "description": "Deployment environment",
+ "allowed_values": []string{"production", "staging"},
+ "values_editable_by": "org_and_repo_actors",
+ "require_explicit_values": true,
+ },
+ {
+ "property_name": "legacy",
+ "source_type": "organization",
+ "value_type": "string",
+ "required": false,
+ "default_value": nil,
+ "description": "",
+ "allowed_values": []string{},
+ "values_editable_by": nil,
+ "require_explicit_values": false,
+ },
+ {
+ "property_name": "untouched",
+ "source_type": "organization",
+ "value_type": "string",
+ },
+ }
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "GET /orgs/{org}/properties/schema": func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ mockResponse(t, http.StatusOK, current)(w, r)
+ },
+ "PATCH /orgs/{org}/properties/schema": func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ body, _ := io.ReadAll(r.Body)
+ require.NoError(t, json.Unmarshal(body, &captured))
+ mockResponse(t, http.StatusOK, captured.Properties)(w, r)
+ },
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{
+ "level": "organization",
+ "org": "octo",
+ "properties": []any{
+ map[string]any{
+ "property_name": "environment",
+ "required": false,
+ },
+ map[string]any{
+ "property_name": "legacy",
+ "description": nil,
+ "allowed_values": []any{},
+ "require_explicit_values": false,
+ },
+ map[string]any{
+ "property_name": "service",
+ "value_type": "string",
+ "required": false,
+ "default_value": nil,
+ "description": "",
+ "allowed_values": nil,
+ "values_editable_by": nil,
+ "require_explicit_values": false,
+ },
+ },
+ })
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+ assert.Equal(t, 2, requests)
+ require.Len(t, captured.Properties, 3)
+
+ environment := captured.Properties[0]
+ assert.Equal(t, "environment", environment["property_name"])
+ assert.Equal(t, "single_select", environment["value_type"])
+ assert.Equal(t, false, environment["required"])
+ assert.Equal(t, "production", environment["default_value"])
+ assert.Equal(t, "Deployment environment", environment["description"])
+ assert.Equal(t, []any{"production", "staging"}, environment["allowed_values"])
+ assert.Equal(t, "org_and_repo_actors", environment["values_editable_by"])
+ assert.Equal(t, true, environment["require_explicit_values"])
+ assert.NotContains(t, environment, "source_type")
+ assert.NotContains(t, environment, "url")
+
+ legacy := captured.Properties[1]
+ assert.Equal(t, "string", legacy["value_type"])
+ assert.Equal(t, false, legacy["required"])
+ assert.Contains(t, legacy, "default_value")
+ assert.Nil(t, legacy["default_value"])
+ assert.Contains(t, legacy, "description")
+ assert.Nil(t, legacy["description"])
+ assert.Contains(t, legacy, "allowed_values")
+ assert.Empty(t, legacy["allowed_values"])
+ assert.Contains(t, legacy, "values_editable_by")
+ assert.Nil(t, legacy["values_editable_by"])
+ assert.Equal(t, false, legacy["require_explicit_values"])
+
+ service := captured.Properties[2]
+ assert.Equal(t, "string", service["value_type"])
+ assert.Equal(t, false, service["required"])
+ assert.Contains(t, service, "default_value")
+ assert.Nil(t, service["default_value"])
+ assert.Equal(t, "", service["description"])
+ assert.Contains(t, service, "allowed_values")
+ assert.Nil(t, service["allowed_values"])
+ assert.Contains(t, service, "values_editable_by")
+ assert.Nil(t, service["values_editable_by"])
+ assert.Equal(t, false, service["require_explicit_values"])
+ })
+
+ t.Run("enterprise level: merges an existing definition", func(t *testing.T) {
+ requests := 0
+ var captured struct {
+ Properties []map[string]any `json:"properties"`
+ }
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "GET /enterprises/{enterprise}/properties/schema": func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ mockResponse(t, http.StatusOK, []map[string]any{{
+ "property_name": "compliance",
+ "source_type": "enterprise",
+ "value_type": "multi_select",
+ "required": true,
+ "default_value": []string{"soc2"},
+ "description": "Compliance frameworks",
+ "allowed_values": []string{"soc2", "fedramp"},
+ "values_editable_by": "org_actors",
+ "require_explicit_values": true,
+ }})(w, r)
+ },
+ "PATCH /enterprises/{enterprise}/properties/schema": func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ body, _ := io.ReadAll(r.Body)
+ require.NoError(t, json.Unmarshal(body, &captured))
+ mockResponse(t, http.StatusOK, captured.Properties)(w, r)
+ },
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{
+ "level": "enterprise",
+ "enterprise": "acme",
+ "properties": []any{
+ map[string]any{
+ "property_name": "compliance",
+ "default_value": []any{"soc2", "fedramp"},
+ },
+ },
+ })
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+ assert.Equal(t, 2, requests)
+ require.Len(t, captured.Properties, 1)
+ assert.Equal(t, "multi_select", captured.Properties[0]["value_type"])
+ assert.Equal(t, []any{"soc2", "fedramp"}, captured.Properties[0]["default_value"])
+ assert.Equal(t, true, captured.Properties[0]["require_explicit_values"])
+ })
+
+ t.Run("rejects invalid definition writes before patch", func(t *testing.T) {
+ tests := []struct {
+ name string
+ args map[string]any
+ current []map[string]any
+ getStatus int
+ expectedError string
+ expectedRequests int
+ }{
+ {
+ name: "organization definition without value_type",
+ args: map[string]any{
+ "level": "organization",
+ "org": "octo",
+ "properties": []any{map[string]any{"property_name": "environment"}},
+ },
+ expectedError: "value_type is required for new property",
+ expectedRequests: 1,
+ },
+ {
+ name: "duplicate property names",
+ args: map[string]any{
+ "level": "organization",
+ "org": "octo",
+ "properties": []any{
+ map[string]any{"property_name": "environment"},
+ map[string]any{"property_name": "environment"},
+ },
+ },
+ expectedError: "duplicates",
+ },
+ {
+ name: "inherited enterprise definition",
+ args: map[string]any{
+ "level": "organization",
+ "org": "octo",
+ "properties": []any{map[string]any{"property_name": "environment"}},
+ },
+ current: []map[string]any{{
+ "property_name": "environment",
+ "source_type": "enterprise",
+ "value_type": "string",
+ }},
+ expectedError: "inherited from enterprise",
+ expectedRequests: 1,
+ },
+ {
+ name: "GET failure",
+ args: map[string]any{
+ "level": "organization",
+ "org": "octo",
+ "properties": []any{map[string]any{"property_name": "environment"}},
+ },
+ getStatus: http.StatusInternalServerError,
+ expectedError: "failed to get organization custom properties before updating",
+ expectedRequests: 1,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ requests := 0
+ status := tt.getStatus
+ if status == 0 {
+ status = http.StatusOK
+ }
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ "GET /orgs/{org}/properties/schema": func(w http.ResponseWriter, r *http.Request) {
+ requests++
+ mockResponse(t, status, tt.current)(w, r)
+ },
+ "PATCH /orgs/{org}/properties/schema": func(w http.ResponseWriter, _ *http.Request) {
+ requests++
+ w.WriteHeader(http.StatusOK)
+ },
+ }))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(tt.args)
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.True(t, result.IsError)
+ assert.Contains(t, getErrorResult(t, result).Text, tt.expectedError)
+ assert.Equal(t, tt.expectedRequests, requests)
+ })
+ }
+ })
+
+ t.Run("unknown level returns an error", func(t *testing.T) {
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}))
+ deps := BaseDeps{Client: client}
+ handler := toolDef.Handler(deps)
+ request := createMCPRequest(map[string]any{
+ "level": "team",
+ "properties": []any{map[string]any{"property_name": "x"}},
+ })
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.True(t, result.IsError)
+ assert.Contains(t, getErrorResult(t, result).Text, "unknown level")
+ })
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index a6ffbecfee..6764edfc26 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -75,7 +75,7 @@ var (
}
ToolsetMetadataGovernance = inventory.ToolsetMetadata{
ID: "governance",
- Description: "Repository governance tools for managing rulesets at the repository, organization, and enterprise levels",
+ Description: "Repository governance tools for managing rulesets and custom properties at the repository, organization, and enterprise levels",
Icon: "law",
}
ToolsetMetadataActions = inventory.ToolsetMetadata{
@@ -270,9 +270,11 @@ func AllTools(t translations.TranslationHelperFunc, opts ...ToolOption) []invent
// Organization tools
SearchOrgs(t),
- // Governance tools (rulesets)
+ // Governance tools
RepositoryRulesetRead(t),
CreateRepositoryRuleset(t),
+ CustomPropertiesRead(t),
+ CustomPropertiesWrite(t),
// Pull request tools
PullRequestRead(t),
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 9e8cd0b794..f94ecc3ed4 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -42,11 +42,11 @@ The following packages are included for the amd64, arm64 architectures.
- [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
- [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE))
- [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE))
- - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.55.0:LICENSE))
- - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.36.0:LICENSE))
- - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.20.0:LICENSE))
- - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.45.0:LICENSE))
- - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.37.0:LICENSE))
- - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.15.0:LICENSE))
+ - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](Unknown))
[github/github-mcp-server]: https://github.com/github/github-mcp-server
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index f5b267f28d..d8f98770b3 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -4,13 +4,56 @@ The following open source dependencies are used to build the [github/github-mcp-
## Table of Contents
-- [386, amd64, arm64](#386-amd64-arm64)
+- [amd64](#amd64)
+- [386](#386)
+- [arm64](#arm64)
---
-## 386, amd64, arm64
+## amd64
-The following packages are included for the 386, amd64, arm64 architectures.
+The following packages are included for the amd64 architectures.
+
+ - [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
+ - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
+ - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE))
+ - [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.3.2/LICENSE))
+ - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.5.0/LICENSE))
+ - [github.com/google/go-github/v89/github](https://pkg.go.dev/github.com/google/go-github/v89/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/34349a88bac3/LICENSE))
+ - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE))
+ - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE))
+ - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
+ - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE))
+ - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE))
+ - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt))
+ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
+ - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE))
+ - [github.com/segmentio/asm](https://pkg.go.dev/github.com/segmentio/asm) ([MIT](https://github.com/segmentio/asm/blob/v1.1.3/LICENSE))
+ - [github.com/segmentio/encoding](https://pkg.go.dev/github.com/segmentio/encoding) ([MIT](https://github.com/segmentio/encoding/blob/v0.5.4/LICENSE))
+ - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
+ - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
+ - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/5f936abd7ae8/LICENSE))
+ - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.15.0/LICENSE.txt))
+ - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.10.0/LICENSE))
+ - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.10.2/LICENSE.txt))
+ - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.10/LICENSE))
+ - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.21.0/LICENSE))
+ - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
+ - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE))
+ - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE))
+ - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](Unknown))
+
+## 386
+
+The following packages are included for the 386 architectures.
- [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
@@ -43,10 +86,51 @@ The following packages are included for the 386, amd64, arm64 architectures.
- [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE))
- [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.55.0:LICENSE))
- - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.36.0:LICENSE))
- - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.20.0:LICENSE))
- - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.45.0:LICENSE))
- - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.37.0:LICENSE))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](Unknown))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.15.0:LICENSE))
+## arm64
+
+The following packages are included for the arm64 architectures.
+
+ - [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
+ - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
+ - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE))
+ - [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.3.2/LICENSE))
+ - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.5.0/LICENSE))
+ - [github.com/google/go-github/v89/github](https://pkg.go.dev/github.com/google/go-github/v89/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/34349a88bac3/LICENSE))
+ - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE))
+ - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE))
+ - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
+ - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE))
+ - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE))
+ - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt))
+ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
+ - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE))
+ - [github.com/segmentio/asm](https://pkg.go.dev/github.com/segmentio/asm) ([MIT](https://github.com/segmentio/asm/blob/v1.1.3/LICENSE))
+ - [github.com/segmentio/encoding](https://pkg.go.dev/github.com/segmentio/encoding) ([MIT](https://github.com/segmentio/encoding/blob/v0.5.4/LICENSE))
+ - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
+ - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
+ - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/5f936abd7ae8/LICENSE))
+ - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.15.0/LICENSE.txt))
+ - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.10.0/LICENSE))
+ - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.10.2/LICENSE.txt))
+ - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.10/LICENSE))
+ - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.21.0/LICENSE))
+ - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
+ - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE))
+ - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE))
+ - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.55.0:LICENSE))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](Unknown))
+
[github/github-mcp-server]: https://github.com/github/github-mcp-server
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index 7e834bc36f..dca13b7c2a 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -4,13 +4,99 @@ The following open source dependencies are used to build the [github/github-mcp-
## Table of Contents
-- [386, amd64, arm64](#386-amd64-arm64)
+- [arm64](#arm64)
+- [386](#386)
+- [amd64](#amd64)
---
-## 386, amd64, arm64
+## arm64
-The following packages are included for the 386, amd64, arm64 architectures.
+The following packages are included for the arm64 architectures.
+
+ - [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
+ - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
+ - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE))
+ - [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.3.2/LICENSE))
+ - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.5.0/LICENSE))
+ - [github.com/google/go-github/v89/github](https://pkg.go.dev/github.com/google/go-github/v89/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/34349a88bac3/LICENSE))
+ - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE))
+ - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE))
+ - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
+ - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE))
+ - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE))
+ - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE))
+ - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt))
+ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
+ - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE))
+ - [github.com/segmentio/asm](https://pkg.go.dev/github.com/segmentio/asm) ([MIT](https://github.com/segmentio/asm/blob/v1.1.3/LICENSE))
+ - [github.com/segmentio/encoding](https://pkg.go.dev/github.com/segmentio/encoding) ([MIT](https://github.com/segmentio/encoding/blob/v0.5.4/LICENSE))
+ - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
+ - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
+ - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/5f936abd7ae8/LICENSE))
+ - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.15.0/LICENSE.txt))
+ - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.10.0/LICENSE))
+ - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.10.2/LICENSE.txt))
+ - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.10/LICENSE))
+ - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.21.0/LICENSE))
+ - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
+ - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE))
+ - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE))
+ - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.55.0:LICENSE))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.36.0:LICENSE))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.20.0:LICENSE))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.37.0:LICENSE))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](Unknown))
+
+## 386
+
+The following packages are included for the 386 architectures.
+
+ - [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
+ - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
+ - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE))
+ - [github.com/go-chi/chi/v5](https://pkg.go.dev/github.com/go-chi/chi/v5) ([MIT](https://github.com/go-chi/chi/blob/v5.3.2/LICENSE))
+ - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.5.0/LICENSE))
+ - [github.com/google/go-github/v89/github](https://pkg.go.dev/github.com/google/go-github/v89/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/34349a88bac3/LICENSE))
+ - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE))
+ - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE))
+ - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
+ - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE))
+ - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE))
+ - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE))
+ - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/v1.7.0/LICENSE))
+ - [github.com/muesli/cache2go](https://pkg.go.dev/github.com/muesli/cache2go) ([BSD-3-Clause](https://github.com/muesli/cache2go/blob/518229cd8021/LICENSE.txt))
+ - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
+ - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE))
+ - [github.com/segmentio/asm](https://pkg.go.dev/github.com/segmentio/asm) ([MIT](https://github.com/segmentio/asm/blob/v1.1.3/LICENSE))
+ - [github.com/segmentio/encoding](https://pkg.go.dev/github.com/segmentio/encoding) ([MIT](https://github.com/segmentio/encoding/blob/v0.5.4/LICENSE))
+ - [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
+ - [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
+ - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/5f936abd7ae8/LICENSE))
+ - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.15.0/LICENSE.txt))
+ - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.10.0/LICENSE))
+ - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.10.2/LICENSE.txt))
+ - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.10/LICENSE))
+ - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.21.0/LICENSE))
+ - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
+ - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE))
+ - [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.5/LICENSE))
+ - [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.20.0:LICENSE))
+ - [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](Unknown))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](Unknown))
+
+## amd64
+
+The following packages are included for the amd64 architectures.
- [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))