From c0fe969f9461dbf5c8ad0f73d0aa78f61b91f500 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Thu, 27 Aug 2026 16:22:19 +0200 Subject: [PATCH 1/8] feat(governance): add custom properties tools with multi-level scope challenge Add custom properties support to the non-default governance toolset, completing the second half of the rulesets + custom properties work requested in #820. Rather than porting the original six single-level tools verbatim, this consolidates them into two level-parameterized tools: - custom_properties_read (level: repository | organization | enterprise) - custom_properties_write (level: repository | organization | enterprise) The `level` argument dispatches to the correct GitHub API. Repository level reads and writes property VALUES (property_name + value), while organization and enterprise levels read and write property DEFINITIONS/schema (value_type, required, allowed_values, default_value, description, values_editable_by). This distinction is documented in the tool and field descriptions. Both tools reuse the shared governanceReadScopeAccess/governanceWriteScopeAccess helpers (renamed from the ruleset-specific names) so rulesets and custom properties present one consistent, exhaustive scope-challenge policy that up-scopes based on the requested level. Co-authored-by: Patrick Knight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8 --- README.md | 25 +- docs/remote-server.md | 2 +- .../__toolsnaps__/custom_properties_read.snap | 42 ++ .../custom_properties_write.snap | 99 +++++ pkg/github/custom_properties.go | 363 ++++++++++++++++++ pkg/github/custom_properties_test.go | 262 +++++++++++++ pkg/github/tools.go | 6 +- 7 files changed, 795 insertions(+), 4 deletions(-) create mode 100644 pkg/github/__toolsnaps__/custom_properties_read.snap create mode 100644 pkg/github/__toolsnaps__/custom_properties_write.snap create mode 100644 pkg/github/custom_properties.go create mode 100644 pkg/github/custom_properties_test.go 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..e5b63b12a9 --- /dev/null +++ b/pkg/github/__toolsnaps__/custom_properties_write.snap @@ -0,0 +1,99 @@ +{ + "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); at the organization and enterprise levels it creates or updates 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" + }, + "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": { + "properties": { + "allowed_values": { + "description": "Organization and enterprise levels only: the ordered list of allowed values for single_select and multi_select properties.", + "items": { + "type": "string" + }, + "type": "array" + }, + "default_value": { + "description": "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings." + }, + "description": { + "description": "Organization and enterprise levels only: a short description of the property.", + "type": "string" + }, + "property_name": { + "description": "The name of the custom property.", + "type": "string" + }, + "required": { + "description": "Organization and enterprise levels only: whether the property must be set on every repository.", + "type": "boolean" + }, + "value": { + "description": "Repository level only: the value to assign. A string, an array of strings, or null to clear the value." + }, + "value_type": { + "description": "Organization and enterprise levels only: the data type of the property. Required when defining a property.", + "enum": [ + "string", + "single_select", + "multi_select", + "true_false", + "url" + ], + "type": "string" + }, + "values_editable_by": { + "description": "Organization and enterprise levels only: who can edit the values of the property.", + "enum": [ + "org_actors", + "org_and_repo_actors" + ], + "type": "string" + } + }, + "required": [ + "property_name" + ], + "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..7091cbf75d --- /dev/null +++ b/pkg/github/custom_properties.go @@ -0,0 +1,363 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + + 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" +) + +// customPropertiesLevelDescription documents the "level" parameter shared by +// the custom properties read and write tools. The repository level operates on +// the property VALUES assigned to a repository, while the organization and +// enterprise levels operate on the property DEFINITIONS (schema). +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 a tool for read operations on custom properties +// at the repository, organization, or enterprise level. The level is selected +// with the "level" parameter. Repository reads return property values; +// organization and enterprise reads return property definitions. +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 + } + }, + ) +} + +// customPropertiesReadRepository handles custom_properties_read calls with level="repository". +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 +} + +// customPropertiesReadOrganization handles custom_properties_read calls with level="organization". +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, resp, err := client.Organizations.GetAllCustomProperties(ctx, org) + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get organization custom properties", resp, err), nil, nil + } + + return MarshalledTextResult(properties), nil, nil +} + +// customPropertiesReadEnterprise handles custom_properties_read calls with level="enterprise". +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, resp, err := client.Enterprise.GetAllCustomProperties(ctx, enterprise) + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get enterprise custom properties", resp, err), nil, nil + } + + return MarshalledTextResult(properties), nil, nil +} + +// CustomPropertiesWrite creates a tool for create-or-update operations on +// custom properties at the repository, organization, or enterprise level. The +// level is selected with the "level" parameter. Repository writes set property +// values; organization and enterprise writes define property schemas. +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); at the organization and enterprise levels it creates or updates the property definitions (schema). 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: customPropertyItemSchema(), + }, + }, + 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 + } + }, + ) +} + +// customPropertiesWriteRepository handles custom_properties_write calls with level="repository". +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) + 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 +} + +// customPropertiesWriteOrganization handles custom_properties_write calls with level="organization". +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[*github.CustomProperty](args) + if errResult != nil { + return errResult, nil, nil + } + + updated, resp, err := client.Organizations.CreateOrUpdateCustomProperties(ctx, org, properties) + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update organization custom properties", resp, err), nil, nil + } + + return MarshalledTextResult(updated), nil, nil +} + +// customPropertiesWriteEnterprise handles custom_properties_write calls with level="enterprise". +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[*github.CustomProperty](args) + if errResult != nil { + return errResult, nil, nil + } + + updated, resp, err := client.Enterprise.CreateOrUpdateCustomProperties(ctx, enterprise, properties) + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update enterprise custom properties", resp, err), nil, nil + } + + return MarshalledTextResult(updated), nil, nil +} + +// customPropertyItemSchema describes a single item of the "properties" array +// for custom_properties_write. It covers both a repository property value and +// an organization or enterprise property definition; which fields apply +// depends on the "level" argument. +func customPropertyItemSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "object", + 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.", + }, + "value_type": { + Type: "string", + Enum: []any{"string", "single_select", "multi_select", "true_false", "url"}, + Description: "Organization and enterprise levels only: the data type of the property. Required when defining a property.", + }, + "required": { + Type: "boolean", + Description: "Organization and enterprise levels only: whether the property must be set on every repository.", + }, + "default_value": { + Description: "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings.", + }, + "description": { + Type: "string", + Description: "Organization and enterprise levels only: a short description of the property.", + }, + "allowed_values": { + Type: "array", + Description: "Organization and enterprise levels only: the ordered list of allowed values for single_select and multi_select properties.", + Items: &jsonschema.Schema{Type: "string"}, + }, + "values_editable_by": { + Type: "string", + Enum: []any{"org_actors", "org_and_repo_actors"}, + Description: "Organization and enterprise levels only: who can edit the values of the property.", + }, + }, + Required: []string{"property_name"}, + } +} + +// parseCustomProperties reads the "properties" array argument and decodes it into +// the requested go-github type. It returns a non-nil *mcp.CallToolResult +// describing the problem when the argument is missing or malformed. +func parseCustomProperties[T any](args map[string]any) ([]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") + } + + 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..505c15402f --- /dev/null +++ b/pkg/github/custom_properties_test.go @@ -0,0 +1,262 @@ +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 := []*github.CustomProperty{{PropertyName: github.Ptr("environment"), ValueType: "single_select"}} + 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 []*github.CustomProperty + require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returned)) + require.Len(t, returned, 1) + assert.Equal(t, "environment", returned[0].GetPropertyName()) + }) + + 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("repository level: sets property values", func(t *testing.T) { + 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) { + 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"}, + }, + }) + + 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") + + require.Len(t, captured.Properties, 1) + assert.Equal(t, "environment", captured.Properties[0].PropertyName) + assert.Equal(t, "production", captured.Properties[0].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: defines property schema", func(t *testing.T) { + var captured struct { + Properties []*github.CustomProperty `json:"properties"` + } + client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + "PATCH /orgs/{org}/properties/schema": func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &captured) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[{"property_name":"environment","value_type":"single_select"}]`)) + }, + })) + 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", + "value_type": "single_select", + "required": true, + "allowed_values": []any{"production", "staging"}, + }, + }, + }) + + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError) + + require.Len(t, captured.Properties, 1) + assert.Equal(t, "environment", captured.Properties[0].GetPropertyName()) + assert.Equal(t, github.PropertyValueType("single_select"), captured.Properties[0].ValueType) + assert.ElementsMatch(t, []string{"production", "staging"}, captured.Properties[0].AllowedValues) + }) + + t.Run("enterprise level: defines property schema", func(t *testing.T) { + var captured struct { + Properties []*github.CustomProperty `json:"properties"` + } + client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + "PATCH /enterprises/{enterprise}/properties/schema": func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &captured) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[{"property_name":"compliance","value_type":"true_false"}]`)) + }, + })) + 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", "value_type": "true_false"}, + }, + }) + + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError) + + require.Len(t, captured.Properties, 1) + assert.Equal(t, "compliance", captured.Properties[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", + "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), From cd6347cc94431bf5d19cd42173c5ef0fb2c0ab41 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 1 Sep 2026 12:55:52 +0200 Subject: [PATCH 2/8] fix(governance): validate custom property values Co-authored-by: Patrick Knight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8 --- .../custom_properties_write.snap | 29 ++++- pkg/github/custom_properties.go | 60 ++++----- pkg/github/custom_properties_test.go | 119 +++++++++++++++++- 3 files changed, 175 insertions(+), 33 deletions(-) diff --git a/pkg/github/__toolsnaps__/custom_properties_write.snap b/pkg/github/__toolsnaps__/custom_properties_write.snap index e5b63b12a9..5fb41be369 100644 --- a/pkg/github/__toolsnaps__/custom_properties_write.snap +++ b/pkg/github/__toolsnaps__/custom_properties_write.snap @@ -40,7 +40,18 @@ "type": "array" }, "default_value": { - "description": "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings." + "description": "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings.", + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] }, "description": { "description": "Organization and enterprise levels only: a short description of the property.", @@ -55,7 +66,21 @@ "type": "boolean" }, "value": { - "description": "Repository level only: the value to assign. A string, an array of strings, or null to clear the 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" + } + ] }, "value_type": { "description": "Organization and enterprise levels only: the data type of the property. Required when defining a property.", diff --git a/pkg/github/custom_properties.go b/pkg/github/custom_properties.go index 7091cbf75d..e5835f635a 100644 --- a/pkg/github/custom_properties.go +++ b/pkg/github/custom_properties.go @@ -14,19 +14,12 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -// customPropertiesLevelDescription documents the "level" parameter shared by -// the custom properties read and write tools. The repository level operates on -// the property VALUES assigned to a repository, while the organization and -// enterprise levels operate on the property DEFINITIONS (schema). 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 a tool for read operations on custom properties -// at the repository, organization, or enterprise level. The level is selected -// with the "level" parameter. Repository reads return property values; -// organization and enterprise reads return property definitions. +// CustomPropertiesRead creates the custom properties read tool. func CustomPropertiesRead(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( ToolsetMetadataGovernance, @@ -91,7 +84,6 @@ func CustomPropertiesRead(t translations.TranslationHelperFunc) inventory.Server ) } -// customPropertiesReadRepository handles custom_properties_read calls with level="repository". 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 { @@ -113,7 +105,6 @@ func customPropertiesReadRepository(ctx context.Context, client *github.Client, return MarshalledTextResult(properties), nil, nil } -// customPropertiesReadOrganization handles custom_properties_read calls with level="organization". 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 { @@ -131,7 +122,6 @@ func customPropertiesReadOrganization(ctx context.Context, client *github.Client return MarshalledTextResult(properties), nil, nil } -// customPropertiesReadEnterprise handles custom_properties_read calls with level="enterprise". 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 { @@ -149,10 +139,7 @@ func customPropertiesReadEnterprise(ctx context.Context, client *github.Client, return MarshalledTextResult(properties), nil, nil } -// CustomPropertiesWrite creates a tool for create-or-update operations on -// custom properties at the repository, organization, or enterprise level. The -// level is selected with the "level" parameter. Repository writes set property -// values; organization and enterprise writes define property schemas. +// CustomPropertiesWrite creates the custom properties write tool. func CustomPropertiesWrite(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( ToolsetMetadataGovernance, @@ -222,7 +209,6 @@ func CustomPropertiesWrite(t translations.TranslationHelperFunc) inventory.Serve ) } -// customPropertiesWriteRepository handles custom_properties_write calls with level="repository". 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 { @@ -232,7 +218,7 @@ func customPropertiesWriteRepository(ctx context.Context, client *github.Client, if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - values, errResult := parseCustomProperties[*github.CustomPropertyValue](args) + values, errResult := parseCustomProperties[*github.CustomPropertyValue](args, "value") if errResult != nil { return errResult, nil, nil } @@ -248,13 +234,12 @@ func customPropertiesWriteRepository(ctx context.Context, client *github.Client, return utils.NewToolResultText("Repository custom property values updated successfully"), nil, nil } -// customPropertiesWriteOrganization handles custom_properties_write calls with level="organization". 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[*github.CustomProperty](args) + properties, errResult := parseCustomProperties[*github.CustomProperty](args, "value_type") if errResult != nil { return errResult, nil, nil } @@ -270,13 +255,12 @@ func customPropertiesWriteOrganization(ctx context.Context, client *github.Clien return MarshalledTextResult(updated), nil, nil } -// customPropertiesWriteEnterprise handles custom_properties_write calls with level="enterprise". 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[*github.CustomProperty](args) + properties, errResult := parseCustomProperties[*github.CustomProperty](args, "value_type") if errResult != nil { return errResult, nil, nil } @@ -292,10 +276,7 @@ func customPropertiesWriteEnterprise(ctx context.Context, client *github.Client, return MarshalledTextResult(updated), nil, nil } -// customPropertyItemSchema describes a single item of the "properties" array -// for custom_properties_write. It covers both a repository property value and -// an organization or enterprise property definition; which fields apply -// depends on the "level" argument. +// customPropertyItemSchema combines repository values with organization and enterprise definitions. func customPropertyItemSchema() *jsonschema.Schema { return &jsonschema.Schema{ Type: "object", @@ -306,6 +287,14 @@ func customPropertyItemSchema() *jsonschema.Schema { }, "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"}, + }, }, "value_type": { Type: "string", @@ -318,6 +307,13 @@ func customPropertyItemSchema() *jsonschema.Schema { }, "default_value": { Description: "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings.", + OneOf: []*jsonschema.Schema{ + {Type: "string"}, + { + Type: "array", + Items: &jsonschema.Schema{Type: "string"}, + }, + }, }, "description": { Type: "string", @@ -338,10 +334,7 @@ func customPropertyItemSchema() *jsonschema.Schema { } } -// parseCustomProperties reads the "properties" array argument and decodes it into -// the requested go-github type. It returns a non-nil *mcp.CallToolResult -// describing the problem when the argument is missing or malformed. -func parseCustomProperties[T any](args map[string]any) ([]T, *mcp.CallToolResult) { +func parseCustomProperties[T any](args map[string]any, requiredItemField string) ([]T, *mcp.CallToolResult) { raw, ok := args["properties"] if !ok || raw == nil { return nil, utils.NewToolResultError("properties parameter is required") @@ -350,6 +343,15 @@ func parseCustomProperties[T any](args map[string]any) ([]T, *mcp.CallToolResult if !ok { return nil, utils.NewToolResultError("properties parameter must be an array") } + for i, rawProperty := range arr { + property, ok := rawProperty.(map[string]any) + if !ok { + return nil, utils.NewToolResultError(fmt.Sprintf("properties[%d] must be an object", i)) + } + if _, ok := property[requiredItemField]; !ok { + return nil, utils.NewToolResultError(fmt.Sprintf("properties[%d].%s is required", i, requiredItemField)) + } + } encoded, err := json.Marshal(arr) if err != nil { diff --git a/pkg/github/custom_properties_test.go b/pkg/github/custom_properties_test.go index 505c15402f..afb9278dd1 100644 --- a/pkg/github/custom_properties_test.go +++ b/pkg/github/custom_properties_test.go @@ -134,6 +134,65 @@ func Test_CustomPropertiesWrite(t *testing.T) { require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.ElementsMatch(t, schema.Required, []string{"level", "properties"}) + t.Run("value schemas enforce documented JSON types", func(t *testing.T) { + defaultValueSchema := schema.Properties["properties"].Items.Properties["default_value"] + require.Len(t, defaultValueSchema.OneOf, 2) + 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) + + valueSchema := schema.Properties["properties"].Items.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) + + resolved, err := schema.Resolve(nil) + require.NoError(t, err) + + tests := []struct { + name string + field string + value any + shouldPass bool + }{ + {name: "default string", field: "default_value", value: "production", shouldPass: true}, + {name: "default string array", field: "default_value", value: []any{"production", "staging"}, shouldPass: true}, + {name: "default number", field: "default_value", value: 1}, + {name: "default boolean", field: "default_value", value: true}, + {name: "default object", field: "default_value", value: map[string]any{"environment": "production"}}, + {name: "default null", field: "default_value", value: nil}, + {name: "value string", field: "value", value: "production", shouldPass: true}, + {name: "value string array", field: "value", value: []any{"production", "staging"}, shouldPass: true}, + {name: "value null", field: "value", value: nil, shouldPass: true}, + {name: "value number", field: "value", value: 1}, + {name: "value boolean", field: "value", value: true}, + {name: "value object", field: "value", value: map[string]any{"environment": "production"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + level := "repository" + property := map[string]any{"property_name": "environment"} + if tt.field == "default_value" { + level = "organization" + property["value_type"] = "single_select" + } + property[tt.field] = tt.value + err := resolved.Validate(map[string]any{ + "level": level, + "properties": []any{property}, + }) + if tt.shouldPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + }) + t.Run("repository level: sets property values", func(t *testing.T) { var captured struct { Properties []*github.CustomPropertyValue `json:"properties"` @@ -153,6 +212,7 @@ func Test_CustomPropertiesWrite(t *testing.T) { "repo": "repo", "properties": []any{ map[string]any{"property_name": "environment", "value": "production"}, + map[string]any{"property_name": "deprecated", "value": nil}, }, }) @@ -161,9 +221,11 @@ func Test_CustomPropertiesWrite(t *testing.T) { require.False(t, result.IsError) assert.Contains(t, getTextResult(t, result).Text, "updated successfully") - require.Len(t, captured.Properties, 1) + 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) { @@ -178,6 +240,48 @@ func Test_CustomPropertiesWrite(t *testing.T) { assert.Contains(t, getErrorResult(t, result).Text, "properties parameter is required") }) + t.Run("requires level-specific property fields", func(t *testing.T) { + tests := []struct { + name string + args map[string]any + requiredPath string + }{ + { + name: "repository value", + args: map[string]any{ + "level": "repository", + "owner": "owner", + "repo": "repo", + "properties": []any{map[string]any{"property_name": "environment"}}, + }, + requiredPath: "properties[0].value", + }, + { + name: "organization value_type", + args: map[string]any{ + "level": "organization", + "org": "octo", + "properties": []any{map[string]any{"property_name": "environment"}}, + }, + requiredPath: "properties[0].value_type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})) + 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.requiredPath) + }) + } + }) + t.Run("organization level: defines property schema", func(t *testing.T) { var captured struct { Properties []*github.CustomProperty `json:"properties"` @@ -200,6 +304,7 @@ func Test_CustomPropertiesWrite(t *testing.T) { "property_name": "environment", "value_type": "single_select", "required": true, + "default_value": "production", "allowed_values": []any{"production", "staging"}, }, }, @@ -213,6 +318,9 @@ func Test_CustomPropertiesWrite(t *testing.T) { assert.Equal(t, "environment", captured.Properties[0].GetPropertyName()) assert.Equal(t, github.PropertyValueType("single_select"), captured.Properties[0].ValueType) assert.ElementsMatch(t, []string{"production", "staging"}, captured.Properties[0].AllowedValues) + defaultValue, ok := captured.Properties[0].DefaultValueString() + require.True(t, ok) + assert.Equal(t, "production", defaultValue) }) t.Run("enterprise level: defines property schema", func(t *testing.T) { @@ -233,7 +341,11 @@ func Test_CustomPropertiesWrite(t *testing.T) { "level": "enterprise", "enterprise": "acme", "properties": []any{ - map[string]any{"property_name": "compliance", "value_type": "true_false"}, + map[string]any{ + "property_name": "compliance", + "value_type": "multi_select", + "default_value": []any{"soc2", "fedramp"}, + }, }, }) @@ -243,6 +355,9 @@ func Test_CustomPropertiesWrite(t *testing.T) { require.Len(t, captured.Properties, 1) assert.Equal(t, "compliance", captured.Properties[0].GetPropertyName()) + defaultValues, ok := captured.Properties[0].DefaultValueStrings() + require.True(t, ok) + assert.Equal(t, []string{"soc2", "fedramp"}, defaultValues) }) t.Run("unknown level returns an error", func(t *testing.T) { From c9ade1f89dbfb1140fbd51946df102896fe463be Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Sep 2026 12:09:26 +0200 Subject: [PATCH 3/8] fix(governance): reject unknown custom property fields Co-authored-by: Patrick Knight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8 --- .../custom_properties_write.snap | 1 + pkg/github/custom_properties.go | 3 ++- pkg/github/custom_properties_test.go | 27 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/github/__toolsnaps__/custom_properties_write.snap b/pkg/github/__toolsnaps__/custom_properties_write.snap index 5fb41be369..921f196567 100644 --- a/pkg/github/__toolsnaps__/custom_properties_write.snap +++ b/pkg/github/__toolsnaps__/custom_properties_write.snap @@ -31,6 +31,7 @@ "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": { + "additionalProperties": false, "properties": { "allowed_values": { "description": "Organization and enterprise levels only: the ordered list of allowed values for single_select and multi_select properties.", diff --git a/pkg/github/custom_properties.go b/pkg/github/custom_properties.go index e5835f635a..b77bba6287 100644 --- a/pkg/github/custom_properties.go +++ b/pkg/github/custom_properties.go @@ -279,7 +279,8 @@ func customPropertiesWriteEnterprise(ctx context.Context, client *github.Client, // customPropertyItemSchema combines repository values with organization and enterprise definitions. func customPropertyItemSchema() *jsonschema.Schema { return &jsonschema.Schema{ - Type: "object", + Type: "object", + AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}}, Properties: map[string]*jsonschema.Schema{ "property_name": { Type: "string", diff --git a/pkg/github/custom_properties_test.go b/pkg/github/custom_properties_test.go index afb9278dd1..d63ebf87f2 100644 --- a/pkg/github/custom_properties_test.go +++ b/pkg/github/custom_properties_test.go @@ -134,6 +134,33 @@ func Test_CustomPropertiesWrite(t *testing.T) { require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.ElementsMatch(t, schema.Required, []string{"level", "properties"}) + t.Run("property items reject unknown fields", func(t *testing.T) { + itemSchema := schema.Properties["properties"].Items + require.NotNil(t, itemSchema.AdditionalProperties) + require.NotNil(t, itemSchema.AdditionalProperties.Not) + + resolved, err := itemSchema.Resolve(nil) + require.NoError(t, err) + require.NoError(t, resolved.Validate(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.NoError(t, resolved.Validate(map[string]any{ + "property_name": "environment", + "value": []any{"production", "staging"}, + })) + require.Error(t, resolved.Validate(map[string]any{ + "property_name": "environment", + "value_type": "string", + "require": true, + })) + }) + t.Run("value schemas enforce documented JSON types", func(t *testing.T) { defaultValueSchema := schema.Properties["properties"].Items.Properties["default_value"] require.Len(t, defaultValueSchema.OneOf, 2) From 80e310ca433615a336dd873be7f63a23eed16bfb Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Sep 2026 12:25:12 +0200 Subject: [PATCH 4/8] fix(governance): validate custom properties by level Co-authored-by: Patrick Knight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8 --- .../custom_properties_write.snap | 149 ++++++++------- pkg/github/custom_properties.go | 62 ++++--- pkg/github/custom_properties_test.go | 173 +++++++++++------- 3 files changed, 229 insertions(+), 155 deletions(-) diff --git a/pkg/github/__toolsnaps__/custom_properties_write.snap b/pkg/github/__toolsnaps__/custom_properties_write.snap index 921f196567..c502f70b26 100644 --- a/pkg/github/__toolsnaps__/custom_properties_write.snap +++ b/pkg/github/__toolsnaps__/custom_properties_write.snap @@ -31,82 +31,103 @@ "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": { - "additionalProperties": false, - "properties": { - "allowed_values": { - "description": "Organization and enterprise levels only: the ordered list of allowed values for single_select and multi_select properties.", - "items": { - "type": "string" - }, - "type": "array" - }, - "default_value": { - "description": "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings.", - "oneOf": [ - { + "oneOf": [ + { + "additionalProperties": false, + "description": "A repository-level custom property value.", + "properties": { + "property_name": { + "description": "The name of the custom property.", "type": "string" }, - { - "items": { - "type": "string" - }, - "type": "array" + "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" + } + ] } - ] - }, - "description": { - "description": "Organization and enterprise levels only: a short description of the property.", - "type": "string" - }, - "property_name": { - "description": "The name of the custom property.", - "type": "string" - }, - "required": { - "description": "Organization and enterprise levels only: whether the property must be set on every repository.", - "type": "boolean" + }, + "required": [ + "property_name", + "value" + ], + "type": "object" }, - "value": { - "description": "Repository level only: the value to assign. A string, an array of strings, or null to clear the value.", - "oneOf": [ - { - "type": "string" - }, - { + { + "additionalProperties": false, + "description": "An organization- or enterprise-level custom property definition.", + "properties": { + "allowed_values": { + "description": "The ordered list of allowed values for single_select and multi_select properties.", "items": { "type": "string" }, "type": "array" }, - { - "type": "null" + "default_value": { + "description": "The value applied when a repository does not set the property. A string or an array of strings.", + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "description": { + "description": "A short description of the property.", + "type": "string" + }, + "property_name": { + "description": "The name of the custom property.", + "type": "string" + }, + "required": { + "description": "Whether the property must be set on every repository.", + "type": "boolean" + }, + "value_type": { + "description": "The data type of the property.", + "enum": [ + "string", + "single_select", + "multi_select", + "true_false", + "url" + ], + "type": "string" + }, + "values_editable_by": { + "description": "Who can edit the values of the property.", + "enum": [ + "org_actors", + "org_and_repo_actors" + ], + "type": "string" } - ] - }, - "value_type": { - "description": "Organization and enterprise levels only: the data type of the property. Required when defining a property.", - "enum": [ - "string", - "single_select", - "multi_select", - "true_false", - "url" - ], - "type": "string" - }, - "values_editable_by": { - "description": "Organization and enterprise levels only: who can edit the values of the property.", - "enum": [ - "org_actors", - "org_and_repo_actors" + }, + "required": [ + "property_name", + "value_type" ], - "type": "string" + "type": "object" } - }, - "required": [ - "property_name" - ], - "type": "object" + ] }, "type": "array" }, diff --git a/pkg/github/custom_properties.go b/pkg/github/custom_properties.go index b77bba6287..44e8a57a10 100644 --- a/pkg/github/custom_properties.go +++ b/pkg/github/custom_properties.go @@ -177,7 +177,12 @@ func CustomPropertiesWrite(t translations.TranslationHelperFunc) inventory.Serve "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: customPropertyItemSchema(), + Items: &jsonschema.Schema{ + OneOf: []*jsonschema.Schema{ + customPropertyValueSchema(), + customPropertyDefinitionSchema(), + }, + }, }, }, Required: []string{"level", "properties"}, @@ -218,7 +223,7 @@ func customPropertiesWriteRepository(ctx context.Context, client *github.Client, if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - values, errResult := parseCustomProperties[*github.CustomPropertyValue](args, "value") + values, errResult := parseCustomProperties[*github.CustomPropertyValue](args, customPropertyValueSchema()) if errResult != nil { return errResult, nil, nil } @@ -239,7 +244,7 @@ func customPropertiesWriteOrganization(ctx context.Context, client *github.Clien if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - properties, errResult := parseCustomProperties[*github.CustomProperty](args, "value_type") + properties, errResult := parseCustomProperties[*github.CustomProperty](args, customPropertyDefinitionSchema()) if errResult != nil { return errResult, nil, nil } @@ -260,7 +265,7 @@ func customPropertiesWriteEnterprise(ctx context.Context, client *github.Client, if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - properties, errResult := parseCustomProperties[*github.CustomProperty](args, "value_type") + properties, errResult := parseCustomProperties[*github.CustomProperty](args, customPropertyDefinitionSchema()) if errResult != nil { return errResult, nil, nil } @@ -276,10 +281,10 @@ func customPropertiesWriteEnterprise(ctx context.Context, client *github.Client, return MarshalledTextResult(updated), nil, nil } -// customPropertyItemSchema combines repository values with organization and enterprise definitions. -func customPropertyItemSchema() *jsonschema.Schema { +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": { @@ -297,17 +302,32 @@ func customPropertyItemSchema() *jsonschema.Schema { {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: "Organization and enterprise levels only: the data type of the property. Required when defining a property.", + Description: "The data type of the property.", }, "required": { Type: "boolean", - Description: "Organization and enterprise levels only: whether the property must be set on every repository.", + Description: "Whether the property must be set on every repository.", }, "default_value": { - Description: "Organization and enterprise levels only: the value applied when a repository does not set the property. A string or an array of strings.", + Description: "The value applied when a repository does not set the property. A string or an array of strings.", OneOf: []*jsonschema.Schema{ {Type: "string"}, { @@ -318,24 +338,24 @@ func customPropertyItemSchema() *jsonschema.Schema { }, "description": { Type: "string", - Description: "Organization and enterprise levels only: a short description of the property.", + Description: "A short description of the property.", }, "allowed_values": { Type: "array", - Description: "Organization and enterprise levels only: the ordered list of allowed values for single_select and multi_select properties.", + Description: "The ordered list of allowed values for single_select and multi_select properties.", Items: &jsonschema.Schema{Type: "string"}, }, "values_editable_by": { Type: "string", Enum: []any{"org_actors", "org_and_repo_actors"}, - Description: "Organization and enterprise levels only: who can edit the values of the property.", + Description: "Who can edit the values of the property.", }, }, - Required: []string{"property_name"}, + Required: []string{"property_name", "value_type"}, } } -func parseCustomProperties[T any](args map[string]any, requiredItemField string) ([]T, *mcp.CallToolResult) { +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") @@ -344,13 +364,13 @@ func parseCustomProperties[T any](args map[string]any, requiredItemField string) if !ok { return nil, utils.NewToolResultError("properties parameter must be an array") } - for i, rawProperty := range arr { - property, ok := rawProperty.(map[string]any) - if !ok { - return nil, utils.NewToolResultError(fmt.Sprintf("properties[%d] must be an object", i)) - } - if _, ok := property[requiredItemField]; !ok { - return nil, utils.NewToolResultError(fmt.Sprintf("properties[%d].%s is required", i, requiredItemField)) + 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) } } diff --git a/pkg/github/custom_properties_test.go b/pkg/github/custom_properties_test.go index d63ebf87f2..7c66c72b58 100644 --- a/pkg/github/custom_properties_test.go +++ b/pkg/github/custom_properties_test.go @@ -134,83 +134,77 @@ func Test_CustomPropertiesWrite(t *testing.T) { require.True(t, ok, "InputSchema should be *jsonschema.Schema") assert.ElementsMatch(t, schema.Required, []string{"level", "properties"}) - t.Run("property items reject unknown fields", func(t *testing.T) { + t.Run("property items use level-specific schemas", func(t *testing.T) { itemSchema := schema.Properties["properties"].Items - require.NotNil(t, itemSchema.AdditionalProperties) - require.NotNil(t, itemSchema.AdditionalProperties.Not) - - resolved, err := itemSchema.Resolve(nil) + 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", "value_type"}, definitionItemSchema.Required) + + resolvedValue, err := valueItemSchema.Resolve(nil) + require.NoError(t, err) + resolvedDefinition, err := definitionItemSchema.Resolve(nil) require.NoError(t, err) - require.NoError(t, resolved.Validate(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.NoError(t, resolved.Validate(map[string]any{ - "property_name": "environment", - "value": []any{"production", "staging"}, - })) - require.Error(t, resolved.Validate(map[string]any{ - "property_name": "environment", - "value_type": "string", - "require": true, - })) - }) - - t.Run("value schemas enforce documented JSON types", func(t *testing.T) { - defaultValueSchema := schema.Properties["properties"].Items.Properties["default_value"] - require.Len(t, defaultValueSchema.OneOf, 2) - 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) - valueSchema := schema.Properties["properties"].Items.Properties["value"] + 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) - resolved, err := schema.Resolve(nil) - require.NoError(t, err) + defaultValueSchema := definitionItemSchema.Properties["default_value"] + require.Len(t, defaultValueSchema.OneOf, 2) + 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) tests := []struct { name string - field string - value any + definition bool + property map[string]any shouldPass bool }{ - {name: "default string", field: "default_value", value: "production", shouldPass: true}, - {name: "default string array", field: "default_value", value: []any{"production", "staging"}, shouldPass: true}, - {name: "default number", field: "default_value", value: 1}, - {name: "default boolean", field: "default_value", value: true}, - {name: "default object", field: "default_value", value: map[string]any{"environment": "production"}}, - {name: "default null", field: "default_value", value: nil}, - {name: "value string", field: "value", value: "production", shouldPass: true}, - {name: "value string array", field: "value", value: []any{"production", "staging"}, shouldPass: true}, - {name: "value null", field: "value", value: nil, shouldPass: true}, - {name: "value number", field: "value", value: 1}, - {name: "value boolean", field: "value", value: true}, - {name: "value object", field: "value", value: map[string]any{"environment": "production"}}, + {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", + }, + 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 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 null default", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "default_value": nil}}, + {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) { - level := "repository" - property := map[string]any{"property_name": "environment"} - if tt.field == "default_value" { - level = "organization" - property["value_type"] = "single_select" + resolved := resolvedValue + if tt.definition { + resolved = resolvedDefinition } - property[tt.field] = tt.value - err := resolved.Validate(map[string]any{ - "level": level, - "properties": []any{property}, - }) + err := resolved.Validate(tt.property) if tt.shouldPass { require.NoError(t, err) } else { @@ -267,11 +261,10 @@ func Test_CustomPropertiesWrite(t *testing.T) { assert.Contains(t, getErrorResult(t, result).Text, "properties parameter is required") }) - t.Run("requires level-specific property fields", func(t *testing.T) { + t.Run("rejects missing and level-incompatible fields", func(t *testing.T) { tests := []struct { - name string - args map[string]any - requiredPath string + name string + args map[string]any }{ { name: "repository value", @@ -281,7 +274,6 @@ func Test_CustomPropertiesWrite(t *testing.T) { "repo": "repo", "properties": []any{map[string]any{"property_name": "environment"}}, }, - requiredPath: "properties[0].value", }, { name: "organization value_type", @@ -290,7 +282,43 @@ func Test_CustomPropertiesWrite(t *testing.T) { "org": "octo", "properties": []any{map[string]any{"property_name": "environment"}}, }, - requiredPath: "properties[0].value_type", + }, + { + name: "repository rejects definition field", + args: map[string]any{ + "level": "repository", + "owner": "owner", + "repo": "repo", + "properties": []any{map[string]any{ + "property_name": "environment", + "value": "production", + "required": true, + }}, + }, + }, + { + name: "organization rejects value", + args: map[string]any{ + "level": "organization", + "org": "octo", + "properties": []any{map[string]any{ + "property_name": "environment", + "value_type": "string", + "value": "production", + }}, + }, + }, + { + name: "enterprise rejects value", + args: map[string]any{ + "level": "enterprise", + "enterprise": "acme", + "properties": []any{map[string]any{ + "property_name": "environment", + "value_type": "string", + "value": "production", + }}, + }, }, } @@ -304,7 +332,7 @@ func Test_CustomPropertiesWrite(t *testing.T) { 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.requiredPath) + assert.Contains(t, getErrorResult(t, result).Text, "properties[0] is invalid") }) } }) @@ -328,11 +356,13 @@ func Test_CustomPropertiesWrite(t *testing.T) { "org": "octo", "properties": []any{ map[string]any{ - "property_name": "environment", - "value_type": "single_select", - "required": true, - "default_value": "production", - "allowed_values": []any{"production", "staging"}, + "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", }, }, }) @@ -344,7 +374,10 @@ func Test_CustomPropertiesWrite(t *testing.T) { require.Len(t, captured.Properties, 1) assert.Equal(t, "environment", captured.Properties[0].GetPropertyName()) assert.Equal(t, github.PropertyValueType("single_select"), captured.Properties[0].ValueType) + assert.True(t, captured.Properties[0].GetRequired()) + assert.Equal(t, "Deployment environment", captured.Properties[0].GetDescription()) assert.ElementsMatch(t, []string{"production", "staging"}, captured.Properties[0].AllowedValues) + assert.Equal(t, "org_and_repo_actors", captured.Properties[0].GetValuesEditableBy()) defaultValue, ok := captured.Properties[0].DefaultValueString() require.True(t, ok) assert.Equal(t, "production", defaultValue) From 8e921ba08be29e2272217f0a828fd2e0772555e8 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Sep 2026 12:32:35 +0200 Subject: [PATCH 5/8] fix(governance): preserve custom property item type Co-authored-by: Patrick Knight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8 --- pkg/github/__toolsnaps__/custom_properties_write.snap | 3 ++- pkg/github/custom_properties.go | 1 + pkg/github/custom_properties_test.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/github/__toolsnaps__/custom_properties_write.snap b/pkg/github/__toolsnaps__/custom_properties_write.snap index c502f70b26..7ff80bf505 100644 --- a/pkg/github/__toolsnaps__/custom_properties_write.snap +++ b/pkg/github/__toolsnaps__/custom_properties_write.snap @@ -127,7 +127,8 @@ ], "type": "object" } - ] + ], + "type": "object" }, "type": "array" }, diff --git a/pkg/github/custom_properties.go b/pkg/github/custom_properties.go index 44e8a57a10..e74bd82c1b 100644 --- a/pkg/github/custom_properties.go +++ b/pkg/github/custom_properties.go @@ -178,6 +178,7 @@ func CustomPropertiesWrite(t translations.TranslationHelperFunc) inventory.Serve 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(), diff --git a/pkg/github/custom_properties_test.go b/pkg/github/custom_properties_test.go index 7c66c72b58..e44ebb2e5e 100644 --- a/pkg/github/custom_properties_test.go +++ b/pkg/github/custom_properties_test.go @@ -136,6 +136,7 @@ func Test_CustomPropertiesWrite(t *testing.T) { 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] From 3463a42ba785962b2cb7ffd988dd7fac59f65ce5 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 2 Sep 2026 18:12:20 +0200 Subject: [PATCH 6/8] fix(governance): preserve partial property updates Co-authored-by: Patrick Knight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8 --- .../custom_properties_write.snap | 63 +++- pkg/github/custom_properties.go | 195 +++++++--- pkg/github/custom_properties_test.go | 350 ++++++++++++------ 3 files changed, 426 insertions(+), 182 deletions(-) diff --git a/pkg/github/__toolsnaps__/custom_properties_write.snap b/pkg/github/__toolsnaps__/custom_properties_write.snap index 7ff80bf505..9c9df1f6ca 100644 --- a/pkg/github/__toolsnaps__/custom_properties_write.snap +++ b/pkg/github/__toolsnaps__/custom_properties_write.snap @@ -4,7 +4,7 @@ "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); at the organization and enterprise levels it creates or updates the property definitions (schema). Select the level with the 'level' parameter.", + "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": { @@ -69,14 +69,21 @@ "description": "An organization- or enterprise-level custom property definition.", "properties": { "allowed_values": { - "description": "The ordered list of allowed values for single_select and multi_select properties.", - "items": { - "type": "string" - }, - "type": "array" + "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. A string or an array of strings.", + "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" @@ -86,23 +93,37 @@ "type": "string" }, "type": "array" + }, + { + "type": "null" } ] }, "description": { - "description": "A short description of the property.", - "type": "string" + "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.", + "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.", + "description": "The data type of the property. Required for new definitions; omit when updating to preserve the current type.", "enum": [ "string", "single_select", @@ -113,17 +134,23 @@ "type": "string" }, "values_editable_by": { - "description": "Who can edit the values of the property.", - "enum": [ - "org_actors", - "org_and_repo_actors" + "anyOf": [ + { + "enum": [ + "org_actors", + "org_and_repo_actors" + ], + "type": "string" + }, + { + "type": "null" + } ], - "type": "string" + "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", - "value_type" + "property_name" ], "type": "object" } diff --git a/pkg/github/custom_properties.go b/pkg/github/custom_properties.go index e74bd82c1b..0e71aa8ecc 100644 --- a/pkg/github/custom_properties.go +++ b/pkg/github/custom_properties.go @@ -4,6 +4,8 @@ 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" @@ -111,12 +113,9 @@ func customPropertiesReadOrganization(ctx context.Context, client *github.Client return utils.NewToolResultError(err.Error()), nil, nil } - properties, resp, err := client.Organizations.GetAllCustomProperties(ctx, org) - if resp != nil { - defer func() { _ = resp.Body.Close() }() - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get organization custom properties", resp, err), 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 @@ -128,12 +127,9 @@ func customPropertiesReadEnterprise(ctx context.Context, client *github.Client, return utils.NewToolResultError(err.Error()), nil, nil } - properties, resp, err := client.Enterprise.GetAllCustomProperties(ctx, enterprise) - if resp != nil { - defer func() { _ = resp.Body.Close() }() - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get enterprise custom properties", resp, err), 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 @@ -145,7 +141,7 @@ func CustomPropertiesWrite(t translations.TranslationHelperFunc) inventory.Serve 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); at the organization and enterprise levels it creates or updates the property definitions (schema). Select the level with the 'level' parameter."), + 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, @@ -245,20 +241,12 @@ func customPropertiesWriteOrganization(ctx context.Context, client *github.Clien if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - properties, errResult := parseCustomProperties[*github.CustomProperty](args, customPropertyDefinitionSchema()) + properties, errResult := parseCustomProperties[map[string]any](args, customPropertyDefinitionSchema()) if errResult != nil { return errResult, nil, nil } - updated, resp, err := client.Organizations.CreateOrUpdateCustomProperties(ctx, org, properties) - if resp != nil { - defer func() { _ = resp.Body.Close() }() - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update organization custom properties", resp, err), nil, nil - } - - return MarshalledTextResult(updated), 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) { @@ -266,20 +254,12 @@ func customPropertiesWriteEnterprise(ctx context.Context, client *github.Client, if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - properties, errResult := parseCustomProperties[*github.CustomProperty](args, customPropertyDefinitionSchema()) + properties, errResult := parseCustomProperties[map[string]any](args, customPropertyDefinitionSchema()) if errResult != nil { return errResult, nil, nil } - updated, resp, err := client.Enterprise.CreateOrUpdateCustomProperties(ctx, enterprise, properties) - if resp != nil { - defer func() { _ = resp.Body.Close() }() - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update enterprise custom properties", resp, err), nil, nil - } - - return MarshalledTextResult(updated), nil, nil + return customPropertiesWriteDefinitions(ctx, client, fmt.Sprintf("enterprises/%s/properties/schema", url.PathEscape(enterprise)), "enterprise", properties) } func customPropertyValueSchema() *jsonschema.Schema { @@ -321,39 +301,164 @@ func customPropertyDefinitionSchema() *jsonschema.Schema { "value_type": { Type: "string", Enum: []any{"string", "single_select", "multi_select", "true_false", "url"}, - Description: "The data type of the property.", + 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.", + 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. A string or an array of strings.", + 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": { - Type: "string", - Description: "A short description of the property.", + 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": { - Type: "array", - Description: "The ordered list of allowed values for single_select and multi_select properties.", - Items: &jsonschema.Schema{Type: "string"}, + 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": { - Type: "string", - Enum: []any{"org_actors", "org_and_repo_actors"}, - Description: "Who can edit the values of the property.", + 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", "value_type"}, + 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) { diff --git a/pkg/github/custom_properties_test.go b/pkg/github/custom_properties_test.go index e44ebb2e5e..be04872038 100644 --- a/pkg/github/custom_properties_test.go +++ b/pkg/github/custom_properties_test.go @@ -60,7 +60,11 @@ func Test_CustomPropertiesRead(t *testing.T) { }) t.Run("organization level: returns property definitions", func(t *testing.T) { - mockProps := []*github.CustomProperty{{PropertyName: github.Ptr("environment"), ValueType: "single_select"}} + 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), })) @@ -72,10 +76,11 @@ func Test_CustomPropertiesRead(t *testing.T) { require.NoError(t, err) require.False(t, result.IsError) - var returned []*github.CustomProperty + 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].GetPropertyName()) + 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) { @@ -143,7 +148,8 @@ func Test_CustomPropertiesWrite(t *testing.T) { 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", "value_type"}, definitionItemSchema.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) @@ -158,10 +164,11 @@ func Test_CustomPropertiesWrite(t *testing.T) { assert.Equal(t, "null", valueSchema.OneOf[2].Type) defaultValueSchema := definitionItemSchema.Properties["default_value"] - require.Len(t, defaultValueSchema.OneOf, 2) + 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 @@ -180,21 +187,23 @@ func Test_CustomPropertiesWrite(t *testing.T) { 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", + "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 null default", definition: true, property: map[string]any{"property_name": "environment", "value_type": "string", "default_value": nil}}, {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}}, } @@ -215,12 +224,14 @@ func Test_CustomPropertiesWrite(t *testing.T) { } }) - t.Run("repository level: sets property values", func(t *testing.T) { + 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) @@ -242,6 +253,7 @@ func Test_CustomPropertiesWrite(t *testing.T) { 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) @@ -262,92 +274,51 @@ func Test_CustomPropertiesWrite(t *testing.T) { assert.Contains(t, getErrorResult(t, result).Text, "properties parameter is required") }) - t.Run("rejects missing and level-incompatible fields", func(t *testing.T) { - tests := []struct { - name string - args map[string]any - }{ - { - name: "repository value", - args: map[string]any{ - "level": "repository", - "owner": "owner", - "repo": "repo", - "properties": []any{map[string]any{"property_name": "environment"}}, - }, - }, - { - name: "organization value_type", - args: map[string]any{ - "level": "organization", - "org": "octo", - "properties": []any{map[string]any{"property_name": "environment"}}, - }, - }, + 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{ { - name: "repository rejects definition field", - args: map[string]any{ - "level": "repository", - "owner": "owner", - "repo": "repo", - "properties": []any{map[string]any{ - "property_name": "environment", - "value": "production", - "required": true, - }}, - }, + "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, }, { - name: "organization rejects value", - args: map[string]any{ - "level": "organization", - "org": "octo", - "properties": []any{map[string]any{ - "property_name": "environment", - "value_type": "string", - "value": "production", - }}, - }, + "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, }, { - name: "enterprise rejects value", - args: map[string]any{ - "level": "enterprise", - "enterprise": "acme", - "properties": []any{map[string]any{ - "property_name": "environment", - "value_type": "string", - "value": "production", - }}, - }, + "property_name": "untouched", + "source_type": "organization", + "value_type": "string", }, } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})) - 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, "properties[0] is invalid") - }) - } - }) - - t.Run("organization level: defines property schema", func(t *testing.T) { - var captured struct { - Properties []*github.CustomProperty `json:"properties"` - } 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) - _ = json.Unmarshal(body, &captured) - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`[{"property_name":"environment","value_type":"single_select"}]`)) + require.NoError(t, json.Unmarshal(body, &captured)) + mockResponse(t, http.StatusOK, captured.Properties)(w, r) }, })) deps := BaseDeps{Client: client} @@ -357,13 +328,24 @@ func Test_CustomPropertiesWrite(t *testing.T) { "org": "octo", "properties": []any{ 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", + "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, }, }, }) @@ -371,29 +353,72 @@ func Test_CustomPropertiesWrite(t *testing.T) { result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) - - require.Len(t, captured.Properties, 1) - assert.Equal(t, "environment", captured.Properties[0].GetPropertyName()) - assert.Equal(t, github.PropertyValueType("single_select"), captured.Properties[0].ValueType) - assert.True(t, captured.Properties[0].GetRequired()) - assert.Equal(t, "Deployment environment", captured.Properties[0].GetDescription()) - assert.ElementsMatch(t, []string{"production", "staging"}, captured.Properties[0].AllowedValues) - assert.Equal(t, "org_and_repo_actors", captured.Properties[0].GetValuesEditableBy()) - defaultValue, ok := captured.Properties[0].DefaultValueString() - require.True(t, ok) - assert.Equal(t, "production", defaultValue) + 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: defines property schema", func(t *testing.T) { + t.Run("enterprise level: merges an existing definition", func(t *testing.T) { + requests := 0 var captured struct { - Properties []*github.CustomProperty `json:"properties"` + 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) - _ = json.Unmarshal(body, &captured) - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`[{"property_name":"compliance","value_type":"true_false"}]`)) + require.NoError(t, json.Unmarshal(body, &captured)) + mockResponse(t, http.StatusOK, captured.Properties)(w, r) }, })) deps := BaseDeps{Client: client} @@ -404,7 +429,6 @@ func Test_CustomPropertiesWrite(t *testing.T) { "properties": []any{ map[string]any{ "property_name": "compliance", - "value_type": "multi_select", "default_value": []any{"soc2", "fedramp"}, }, }, @@ -413,12 +437,100 @@ func Test_CustomPropertiesWrite(t *testing.T) { 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, "compliance", captured.Properties[0].GetPropertyName()) - defaultValues, ok := captured.Properties[0].DefaultValueStrings() - require.True(t, ok) - assert.Equal(t, []string{"soc2", "fedramp"}, defaultValues) + 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) { From 1453e3c97107cff76a1d0429feaf1b1acf885d8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Sep 2026 13:10:33 +0000 Subject: [PATCH 7/8] chore: regenerate license files Auto-generated by license-check workflow --- third-party-licenses.darwin.md | 12 ++-- third-party-licenses.linux.md | 98 ++++++++++++++++++++++++++++++--- third-party-licenses.windows.md | 92 ++++++++++++++++++++++++++++++- 3 files changed, 186 insertions(+), 16 deletions(-) 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)) From 725edf3c28abcf5914454ea5a80fb283b437686b Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Thu, 3 Sep 2026 15:12:55 +0200 Subject: [PATCH 8/8] chore: rerun checks after license regeneration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1e886867-a922-419a-b02c-ac643716aea8