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: | comment-discussion | `discussions` | GitHub Discussions related tools | | logo-gist | `gists` | GitHub Gist related tools | | git-branch | `git` | GitHub Git API related tools for low-level Git operations | -| law | `governance` | Repository governance tools for managing rulesets at the repository, organization, and enterprise levels | +| law | `governance` | Repository governance tools for managing rulesets and custom properties at the repository, organization, and enterprise levels | | issue-opened | `issues` | GitHub Issues related tools | | tag | `labels` | GitHub Labels related tools | | bell | `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 | comment-discussion
`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) | | logo-gist
`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-branch
`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) | -| law
`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) | +| law
`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) | | issue-opened
`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) | | tag
`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) | | bell
`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))