diff --git a/.github/actions/build-ui/action.yml b/.github/actions/build-ui/action.yml
new file mode 100644
index 0000000000..229057d5cb
--- /dev/null
+++ b/.github/actions/build-ui/action.yml
@@ -0,0 +1,38 @@
+name: Build UI
+description: Restore cached UI HTML artifacts, or set up Node and run script/build-ui on cache miss.
+
+runs:
+ using: composite
+ steps:
+ - name: Cache UI artifacts
+ id: cache-ui
+ uses: actions/cache@v5
+ with:
+ path: |
+ pkg/github/ui_dist/get-me.html
+ pkg/github/ui_dist/issue-write.html
+ pkg/github/ui_dist/pr-write.html
+ key: ui-dist-v1-${{ hashFiles('ui/package-lock.json', 'ui/package.json', 'ui/index.html', 'ui/tsconfig*.json', 'ui/vite.config.ts', 'ui/src/**', 'ui/scripts/**') }}
+ enableCrossOsArchive: true
+
+ - name: Set up Node.js
+ if: steps.cache-ui.outputs.cache-hit != 'true'
+ uses: actions/setup-node@v6
+ with:
+ node-version: "20"
+ cache: npm
+ cache-dependency-path: ui/package-lock.json
+
+ - name: Build UI
+ if: steps.cache-ui.outputs.cache-hit != 'true'
+ shell: bash
+ run: script/build-ui
+
+ - name: Report UI cache status
+ shell: bash
+ run: |
+ if [ "${{ steps.cache-ui.outputs.cache-hit }}" = "true" ]; then
+ echo "UI artifacts restored from cache (skipped build)."
+ else
+ echo "UI artifacts rebuilt from source."
+ fi
diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml
index e58a45e71e..ecbe9f0dcb 100644
--- a/.github/workflows/code-scanning.yml
+++ b/.github/workflows/code-scanning.yml
@@ -78,9 +78,9 @@ jobs:
go-version: ${{ fromJSON(steps.resolve-environment.outputs.environment).configuration.go.version }}
cache: false
- - name: Set up Node.js
- if: matrix.language == 'go' || matrix.language == 'javascript'
- uses: actions/setup-node@v4
+ - name: Set up Node.js (for JavaScript CodeQL)
+ if: matrix.language == 'javascript'
+ uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
@@ -88,7 +88,7 @@ jobs:
- name: Build UI
if: matrix.language == 'go'
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- name: Autobuild
uses: github/codeql-action/autobuild@v4
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 638713c700..f56d4f31a2 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -46,7 +46,7 @@ jobs:
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
- uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 #v4.1.0
+ uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 #v4.1.2
with:
cosign-release: "v2.2.4"
@@ -60,7 +60,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
- uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -93,7 +93,7 @@ jobs:
key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }}
- name: Inject go-build-cache
- uses: reproducible-containers/buildkit-cache-dance@1b8ab18fbda5ad3646e3fcc9ed9dd41ce2f297b4 # v3.3.2
+ uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0
with:
cache-map: |
{
diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml
index de62d6282c..309eddb38e 100644
--- a/.github/workflows/docs-check.yml
+++ b/.github/workflows/docs-check.yml
@@ -16,15 +16,8 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: ui/package-lock.json
-
- name: Build UI
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- name: Set up Go
uses: actions/setup-go@v6
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index f874b2b59d..1fea50114a 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -25,16 +25,8 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: ui/package-lock.json
-
- name: Build UI
- shell: bash
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- name: Set up Go
uses: actions/setup-go@v6
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
index f8eddc076c..1004fc2747 100644
--- a/.github/workflows/goreleaser.yml
+++ b/.github/workflows/goreleaser.yml
@@ -16,15 +16,8 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: ui/package-lock.json
-
- name: Build UI
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- name: Set up Go
uses: actions/setup-go@v6
@@ -35,7 +28,7 @@ jobs:
run: go mod download
- name: Run GoReleaser
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
+ uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89
with:
distribution: goreleaser
# GoReleaser version
@@ -47,7 +40,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate signed build provenance attestations for workflow artifacts
- uses: actions/attest-build-provenance@v3
+ uses: actions/attest-build-provenance@v4
with:
subject-path: |
dist/*.tar.gz
diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml
index 9e352c3f69..2f27353d83 100644
--- a/.github/workflows/license-check.yml
+++ b/.github/workflows/license-check.yml
@@ -32,15 +32,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: gh pr checkout ${{ github.event.pull_request.number }}
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: ui/package-lock.json
-
- name: Build UI
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- name: Set up Go
uses: actions/setup-go@v6
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 3676cb4103..5b912cea0f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,13 +14,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- - uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- cache-dependency-path: ui/package-lock.json
- name: Build UI
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- uses: actions/setup-go@v6
with:
go-version: '1.25'
diff --git a/.github/workflows/mcp-diff.yml b/.github/workflows/mcp-diff.yml
index 3c6c0149a8..56f3500811 100644
--- a/.github/workflows/mcp-diff.yml
+++ b/.github/workflows/mcp-diff.yml
@@ -19,13 +19,8 @@ jobs:
with:
fetch-depth: 0
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
-
- name: Build UI
- run: script/build-ui
+ uses: ./.github/actions/build-ui
- name: Run MCP Server Diff
uses: SamMorrowDrums/mcp-server-diff@v2.3.5
diff --git a/Dockerfile b/Dockerfile
index 5036ba8b9d..a4e8e8db75 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20-alpine@sha256:09e2b3d9726018aecf269bd35325f46bf75046a643a66d28360ec71132750ec8 AS ui-build
+FROM node:26-alpine@sha256:e71ac5e964b9201072425d59d2e876359efa25dc96bb1768cb73295728d6e4ea AS ui-build
WORKDIR /app
COPY ui/package*.json ./ui/
RUN cd ui && npm ci
@@ -7,7 +7,7 @@ COPY ui/ ./ui/
RUN mkdir -p ./pkg/github/ui_dist && \
cd ui && npm run build
-FROM golang:1.25.9-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f AS build
+FROM golang:1.25.10-alpine@sha256:8d22e29d960bc50cd025d93d5b7c7d220b1ee9aa7a239b3c8f55a57e987e8d45 AS build
ARG VERSION="dev"
# Set the working directory
@@ -30,7 +30,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
-o /bin/github-mcp-server ./cmd/github-mcp-server
# Make a stage to run the app
-FROM gcr.io/distroless/base-debian12@sha256:9dce90e688a57e59ce473ff7bc4c80bc8fe52d2303b4d99b44f297310bbd2210
+FROM gcr.io/distroless/base-debian12@sha256:58695f439f772a00009c8f6be4c183f824c1f556d74b313c30900f167e4772f8
# Add required MCP server annotation
LABEL io.modelcontextprotocol.server.name="io.github.github/github-mcp-server"
diff --git a/README.md b/README.md
index a12f1531ac..1030f83ca0 100644
--- a/README.md
+++ b/README.md
@@ -730,6 +730,23 @@ The following sets of tools are available:
Discussions
+- **discussion_comment_write** - Manage discussion comments
+ - **Required OAuth Scopes**: `repo`
+ - `body`: Comment content (required for 'add', 'reply', and 'update' methods) (string, optional)
+ - `commentNodeID`: The Node ID of the discussion comment (required for 'reply', 'update', 'delete', 'mark_answer', and 'unmark_answer' methods). For 'reply', this is the top-level comment to reply to; GitHub Discussions only support one level of nesting. (string, optional)
+ - `discussionNumber`: Discussion number (required for 'add' and 'reply' methods) (number, optional)
+ - `method`: Write operation to perform on a discussion comment.
+ Options are:
+ - 'add' - adds a new top-level comment to a discussion.
+ - 'reply' - replies to a top-level discussion comment (GitHub Discussions only support one level of nesting).
+ - 'update' - updates an existing discussion comment.
+ - 'delete' - deletes a discussion comment.
+ - 'mark_answer' - marks a discussion comment as the answer (Q&A only).
+ - 'unmark_answer' - unmarks a discussion comment as the answer (Q&A only).
+ (string, required)
+ - `owner`: Repository owner (required for 'add' and 'reply' methods) (string, optional)
+ - `repo`: Repository name (required for 'add' and 'reply' methods) (string, optional)
+
- **get_discussion** - Get discussion
- **Required OAuth Scopes**: `repo`
- `discussionNumber`: Discussion Number (number, required)
@@ -740,6 +757,7 @@ The following sets of tools are available:
- **Required OAuth Scopes**: `repo`
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
- `discussionNumber`: Discussion Number (number, required)
+ - `includeReplies`: When true, each top-level comment will include its replies nested within it (up to 100 replies per comment, which is the GitHub API maximum). Defaults to false. (boolean, optional)
- `owner`: Repository owner (string, required)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `repo`: Repository name (string, required)
@@ -1256,6 +1274,14 @@ The following sets of tools are available:
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `repo`: Repository name (string, required)
+- **list_repository_collaborators** - List repository collaborators
+ - **Required OAuth Scopes**: `repo`
+ - `affiliation`: Filter by affiliation. Can be one of: 'outside' (outside collaborators), 'direct' (all with permissions regardless of org membership), 'all' (all collaborators). Default: 'all' (string, optional)
+ - `owner`: Repository owner (string, required)
+ - `page`: Page number for pagination (default 1, min 1) (number, optional)
+ - `perPage`: Results per page for pagination (default 30, min 1, max 100) (number, optional)
+ - `repo`: Repository name (string, required)
+
- **list_tags** - List tags
- **Required OAuth Scopes**: `repo`
- `owner`: Repository owner (string, required)
@@ -1413,6 +1439,11 @@ The following sets of tools are available:
Copilot Spaces
+- **Authentication note**
+ - Fine-grained PATs are not hidden by classic PAT scope filtering, so these tools may still appear even when the token cannot use them.
+ - For org-owned spaces, fine-grained PATs must be installed on the owning organization and include `organization_copilot_spaces: read`.
+ - If an org-owned space contains repository-backed resources, the token must also have access to every referenced repository or the space may be treated as not found.
+
- **get_copilot_space** - Get Copilot Space
- `owner`: The owner of the space. (string, required)
- `name`: The name of the space. (string, required)
diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go
index ad40ecad02..73d5f271c9 100644
--- a/e2e/e2e_test.go
+++ b/e2e/e2e_test.go
@@ -18,7 +18,7 @@ import (
"github.com/github/github-mcp-server/internal/ghmcp"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/translations"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
)
diff --git a/go.mod b/go.mod
index 89cafc377d..b2a12f2577 100644
--- a/go.mod
+++ b/go.mod
@@ -5,12 +5,12 @@ go 1.25.0
require (
github.com/go-chi/chi/v5 v5.2.5
github.com/go-viper/mapstructure/v2 v2.5.0
- github.com/google/go-github/v82 v82.0.0
- github.com/google/jsonschema-go v0.4.2
+ github.com/google/go-github/v87 v87.0.0
+ github.com/google/jsonschema-go v0.4.3
github.com/josephburnett/jd/v2 v2.5.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/microcosm-cc/bluemonday v1.0.27
- github.com/modelcontextprotocol/go-sdk v1.5.1-0.20260403154220-27f29c1cef3b
+ github.com/modelcontextprotocol/go-sdk v1.6.0
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
diff --git a/go.sum b/go.sum
index 615b4e9c0c..c0e9f09552 100644
--- a/go.sum
+++ b/go.sum
@@ -16,12 +16,12 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk=
-github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM=
+github.com/google/go-github/v87 v87.0.0 h1:9Ck3dcOxWJyfsN8tzdah4YvmqB/7ZsstMglv/PkOsl0=
+github.com/google/go-github/v87 v87.0.0/go.mod h1:hGUoT5pwm/ck5uLL+wroSVQfg8mpe+buxllCcGV4VaM=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
-github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
-github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
+github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -39,8 +39,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
-github.com/modelcontextprotocol/go-sdk v1.5.1-0.20260403154220-27f29c1cef3b h1:mB8zdpP8SX1TEqnEZpV2hHD30EQXivsZl4AP9hgm7F8=
-github.com/modelcontextprotocol/go-sdk v1.5.1-0.20260403154220-27f29c1cef3b/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA=
+github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY=
+github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 h1:31Y+Yu373ymebRdJN1cWLLooHH8xAr0MhKTEJGV/87g=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go
index b1925bffd3..6c8c3934d5 100644
--- a/internal/ghmcp/server.go
+++ b/internal/ghmcp/server.go
@@ -24,18 +24,19 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
)
// githubClients holds all the GitHub API clients created for a server instance.
type githubClients struct {
- rest *gogithub.Client
- gql *githubv4.Client
- gqlHTTP *http.Client // retained for middleware to modify transport
- raw *raw.Client
- repoAccess *lockdown.RepoAccessCache
+ rest *gogithub.Client
+ restUATransp *transport.UserAgentTransport
+ gql *githubv4.Client
+ gqlHTTP *http.Client // retained for middleware to modify transport
+ raw *raw.Client
+ repoAccess *lockdown.RepoAccessCache
}
// createGitHubClients creates all the GitHub API clients needed by the server.
@@ -61,10 +62,18 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
}
// Construct REST client
- restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token)
- restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version)
- restClient.BaseURL = restURL
- restClient.UploadURL = uploadURL
+ restUATransport := &transport.UserAgentTransport{
+ Transport: http.DefaultTransport,
+ Agent: fmt.Sprintf("github-mcp-server/%s", cfg.Version),
+ }
+ restClient, err := gogithub.NewClient(
+ gogithub.WithHTTPClient(&http.Client{Transport: restUATransport}),
+ gogithub.WithAuthToken(cfg.Token),
+ gogithub.WithEnterpriseURLs(restURL.String(), uploadURL.String()),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create REST client: %w", err)
+ }
// Construct GraphQL client
// We use NewEnterpriseClient unconditionally since we already parsed the API host
@@ -80,7 +89,10 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
gqlClient := githubv4.NewEnterpriseClient(graphQLURL.String(), gqlHTTPClient)
// Create raw content client (shares REST client's HTTP transport)
- rawClient := raw.NewClient(restClient, rawURL)
+ rawClient, err := raw.NewClient(restClient, rawURL)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create raw client: %w", err)
+ }
// Set up repo access cache for lockdown mode
var repoAccessCache *lockdown.RepoAccessCache
@@ -95,11 +107,12 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolv
}
return &githubClients{
- rest: restClient,
- gql: gqlClient,
- gqlHTTP: gqlHTTPClient,
- raw: rawClient,
- repoAccess: repoAccessCache,
+ rest: restClient,
+ restUATransp: restUATransport,
+ gql: gqlClient,
+ gqlHTTP: gqlHTTPClient,
+ raw: rawClient,
+ repoAccess: repoAccessCache,
}, nil
}
@@ -170,7 +183,7 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
github.RegisterUIResources(ghServer)
}
- ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.rest, clients.gqlHTTP))
+ ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.restUATransp, clients.gqlHTTP))
return ghServer, nil
}
@@ -345,7 +358,7 @@ func createFeatureChecker(enabledFeatures []string, insidersMode bool) inventory
}
}
-func addUserAgentsMiddleware(cfg github.MCPServerConfig, restClient *gogithub.Client, gqlHTTPClient *http.Client) func(next mcp.MethodHandler) mcp.MethodHandler {
+func addUserAgentsMiddleware(cfg github.MCPServerConfig, restUATransp *transport.UserAgentTransport, gqlHTTPClient *http.Client) func(next mcp.MethodHandler) mcp.MethodHandler {
return func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, request mcp.Request) (result mcp.Result, err error) {
if method != "initialize" {
@@ -368,7 +381,7 @@ func addUserAgentsMiddleware(cfg github.MCPServerConfig, restClient *gogithub.Cl
userAgent += " (insiders)"
}
- restClient.UserAgent = userAgent
+ restUATransp.Agent = userAgent
gqlHTTPClient.Transport = &transport.UserAgentTransport{
Transport: gqlHTTPClient.Transport,
diff --git a/pkg/errors/error.go b/pkg/errors/error.go
index d757651592..7c1f28e660 100644
--- a/pkg/errors/error.go
+++ b/pkg/errors/error.go
@@ -6,7 +6,7 @@ import (
"net/http"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/errors/error_test.go b/pkg/errors/error_test.go
index e33d5bd39e..7459569f2a 100644
--- a/pkg/errors/error_test.go
+++ b/pkg/errors/error_test.go
@@ -6,7 +6,7 @@ import (
"net/http"
"testing"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/pkg/github/__toolsnaps__/discussion_comment_write.snap b/pkg/github/__toolsnaps__/discussion_comment_write.snap
new file mode 100644
index 0000000000..5edadfaeaa
--- /dev/null
+++ b/pkg/github/__toolsnaps__/discussion_comment_write.snap
@@ -0,0 +1,48 @@
+{
+ "annotations": {
+ "destructiveHint": true,
+ "title": "Manage discussion comments"
+ },
+ "description": "Write operations for discussion comments.\nSupports adding top-level comments, replying to existing comments, updating comment content, deleting comments, and marking or unmarking comments as the answer.",
+ "inputSchema": {
+ "properties": {
+ "body": {
+ "description": "Comment content (required for 'add', 'reply', and 'update' methods)",
+ "type": "string"
+ },
+ "commentNodeID": {
+ "description": "The Node ID of the discussion comment (required for 'reply', 'update', 'delete', 'mark_answer', and 'unmark_answer' methods). For 'reply', this is the top-level comment to reply to; GitHub Discussions only support one level of nesting.",
+ "type": "string"
+ },
+ "discussionNumber": {
+ "description": "Discussion number (required for 'add' and 'reply' methods)",
+ "type": "number"
+ },
+ "method": {
+ "description": "Write operation to perform on a discussion comment.\nOptions are:\n- 'add' - adds a new top-level comment to a discussion.\n- 'reply' - replies to a top-level discussion comment (GitHub Discussions only support one level of nesting).\n- 'update' - updates an existing discussion comment.\n- 'delete' - deletes a discussion comment.\n- 'mark_answer' - marks a discussion comment as the answer (Q\u0026A only).\n- 'unmark_answer' - unmarks a discussion comment as the answer (Q\u0026A only).\n",
+ "enum": [
+ "add",
+ "reply",
+ "update",
+ "delete",
+ "mark_answer",
+ "unmark_answer"
+ ],
+ "type": "string"
+ },
+ "owner": {
+ "description": "Repository owner (required for 'add' and 'reply' methods)",
+ "type": "string"
+ },
+ "repo": {
+ "description": "Repository name (required for 'add' and 'reply' methods)",
+ "type": "string"
+ }
+ },
+ "required": [
+ "method"
+ ],
+ "type": "object"
+ },
+ "name": "discussion_comment_write"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/get_discussion_comments.snap b/pkg/github/__toolsnaps__/get_discussion_comments.snap
index f9e6095650..422fc40bf7 100644
--- a/pkg/github/__toolsnaps__/get_discussion_comments.snap
+++ b/pkg/github/__toolsnaps__/get_discussion_comments.snap
@@ -14,6 +14,10 @@
"description": "Discussion Number",
"type": "number"
},
+ "includeReplies": {
+ "description": "When true, each top-level comment will include its replies nested within it (up to 100 replies per comment, which is the GitHub API maximum). Defaults to false.",
+ "type": "boolean"
+ },
"owner": {
"description": "Repository owner",
"type": "string"
diff --git a/pkg/github/__toolsnaps__/get_me.snap b/pkg/github/__toolsnaps__/get_me.snap
index b451b49de6..6f287df092 100644
--- a/pkg/github/__toolsnaps__/get_me.snap
+++ b/pkg/github/__toolsnaps__/get_me.snap
@@ -1,7 +1,11 @@
{
"_meta": {
"ui": {
- "resourceUri": "ui://github-mcp-server/get-me"
+ "resourceUri": "ui://github-mcp-server/get-me",
+ "visibility": [
+ "model",
+ "app"
+ ]
}
},
"annotations": {
diff --git a/pkg/github/__toolsnaps__/list_repository_collaborators.snap b/pkg/github/__toolsnaps__/list_repository_collaborators.snap
new file mode 100644
index 0000000000..629e4bdf1c
--- /dev/null
+++ b/pkg/github/__toolsnaps__/list_repository_collaborators.snap
@@ -0,0 +1,45 @@
+{
+ "annotations": {
+ "readOnlyHint": true,
+ "title": "List repository collaborators"
+ },
+ "description": "List collaborators of a GitHub repository. Results are paginated; the response includes `nextPage`, `prevPage`, `firstPage`, and `lastPage` fields. To get the next page, use the `nextPage` value as the `page` parameter.",
+ "inputSchema": {
+ "properties": {
+ "affiliation": {
+ "description": "Filter by affiliation. Can be one of: 'outside' (outside collaborators), 'direct' (all with permissions regardless of org membership), 'all' (all collaborators). Default: 'all'",
+ "enum": [
+ "outside",
+ "direct",
+ "all"
+ ],
+ "type": "string"
+ },
+ "owner": {
+ "description": "Repository owner",
+ "type": "string"
+ },
+ "page": {
+ "description": "Page number for pagination (default 1, min 1)",
+ "minimum": 1,
+ "type": "number"
+ },
+ "perPage": {
+ "description": "Results per page for pagination (default 30, min 1, max 100)",
+ "maximum": 100,
+ "minimum": 1,
+ "type": "number"
+ },
+ "repo": {
+ "description": "Repository name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "owner",
+ "repo"
+ ],
+ "type": "object"
+ },
+ "name": "list_repository_collaborators"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/update_issue_type.snap b/pkg/github/__toolsnaps__/update_issue_type.snap
index 6354a42e16..237603a6ed 100644
--- a/pkg/github/__toolsnaps__/update_issue_type.snap
+++ b/pkg/github/__toolsnaps__/update_issue_type.snap
@@ -20,6 +20,11 @@
"description": "Repository owner (username or organization)",
"type": "string"
},
+ "rationale": {
+ "description": "One concise sentence explaining what specifically about the issue led you to choose this type. State the concrete signal (e.g. 'Reports a crash when saving' → bug, 'Asks for dark mode support' → feature).",
+ "maxLength": 280,
+ "type": "string"
+ },
"repo": {
"description": "Repository name",
"type": "string"
diff --git a/pkg/github/actions.go b/pkg/github/actions.go
index 85afed6e1b..a7ce039d83 100644
--- a/pkg/github/actions.go
+++ b/pkg/github/actions.go
@@ -16,7 +16,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@@ -989,10 +989,10 @@ func runWorkflow(ctx context.Context, client *github.Client, owner, repo, workfl
var workflowType string
if workflowIDInt, parseErr := strconv.ParseInt(workflowID, 10, 64); parseErr == nil {
- resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event)
+ _, resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event)
workflowType = "workflow_id"
} else {
- resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event)
+ _, resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event)
workflowType = "workflow_file"
}
diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go
index 6eba71b8b3..371bbbe9dc 100644
--- a/pkg/github/actions_test.go
+++ b/pkg/github/actions_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -86,7 +86,7 @@ func Test_ActionsList_ListWorkflows(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -136,7 +136,7 @@ func Test_ActionsList_ListWorkflowRuns(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -185,7 +185,7 @@ func Test_ActionsList_ListWorkflowRuns(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -241,7 +241,7 @@ func Test_ActionsGet_GetWorkflow(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -284,7 +284,7 @@ func Test_ActionsGet_GetWorkflowRun(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -412,7 +412,7 @@ func Test_ActionsRunTrigger_RunWorkflow(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -449,7 +449,7 @@ func Test_ActionsRunTrigger_CancelWorkflowRun(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -480,7 +480,7 @@ func Test_ActionsRunTrigger_CancelWorkflowRun(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -504,7 +504,7 @@ func Test_ActionsRunTrigger_CancelWorkflowRun(t *testing.T) {
t.Run("missing run_id for non-run_workflow methods", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -556,7 +556,7 @@ func Test_ActionsGetJobLogs_SingleJob(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
ContentWindowSize: 5000,
@@ -618,7 +618,7 @@ func Test_ActionsGetJobLogs_FailedJobs(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
ContentWindowSize: 5000,
@@ -668,7 +668,7 @@ func Test_ActionsGetJobLogs_FailedJobs(t *testing.T) {
}),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
ContentWindowSize: 5000,
diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go
index 34249b2129..2deefd321c 100644
--- a/pkg/github/code_scanning.go
+++ b/pkg/github/code_scanning.go
@@ -11,7 +11,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go
index 7a3c16fd15..64c61766ed 100644
--- a/pkg/github/code_scanning_test.go
+++ b/pkg/github/code_scanning_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -80,7 +80,7 @@ func Test_GetCodeScanningAlert(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -206,7 +206,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go
index 9f84c02118..191e562793 100644
--- a/pkg/github/context_tools.go
+++ b/pkg/github/context_tools.go
@@ -58,6 +58,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
Meta: mcp.Meta{
"ui": map[string]any{
"resourceUri": GetMeUIResourceURI,
+ "visibility": []string{"model", "app"},
},
},
},
diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go
index 365a019ab6..2b17be86d1 100644
--- a/pkg/github/context_tools_test.go
+++ b/pkg/github/context_tools_test.go
@@ -10,7 +10,7 @@ import (
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -99,7 +99,7 @@ func Test_GetMe(t *testing.T) {
deps = stubDeps{clientFn: stubClientFnErr(tc.clientErr), obsv: stubExporters()}
} else {
obs := stubExporters()
- deps = BaseDeps{Client: github.NewClient(tc.mockedClient), Obsv: obs}
+ deps = BaseDeps{Client: mustNewGHClient(t, tc.mockedClient), Obsv: obs}
}
handler := serverTool.Handler(deps)
@@ -155,7 +155,7 @@ func Test_GetMe_IFC_InsidersMode(t *testing.T) {
t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) {
deps := BaseDeps{
- Client: github.NewClient(mockedHTTPClient),
+ Client: mustNewGHClient(t, mockedHTTPClient),
Flags: FeatureFlags{InsidersMode: false},
}
handler := serverTool.Handler(deps)
@@ -170,7 +170,7 @@ func Test_GetMe_IFC_InsidersMode(t *testing.T) {
t.Run("insiders mode enabled includes ifc label in result meta", func(t *testing.T) {
deps := BaseDeps{
- Client: github.NewClient(mockedHTTPClient),
+ Client: mustNewGHClient(t, mockedHTTPClient),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)
@@ -192,10 +192,7 @@ func Test_GetMe_IFC_InsidersMode(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "trusted", ifcMap["integrity"])
- confList, ok := ifcMap["confidentiality"].([]any)
- require.True(t, ok, "confidentiality should be a list")
- require.Len(t, confList, 1)
- assert.Equal(t, "public", confList[0])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
})
}
@@ -329,7 +326,7 @@ func Test_GetTeams(t *testing.T) {
name: "successful get teams",
makeDeps: func() ToolDependencies {
return BaseDeps{
- Client: github.NewClient(httpClientWithUser()),
+ Client: mustNewGHClient(t, httpClientWithUser()),
GQLClient: gqlClientForTestuser(),
}
},
@@ -354,7 +351,7 @@ func Test_GetTeams(t *testing.T) {
name: "no teams found",
makeDeps: func() ToolDependencies {
return BaseDeps{
- Client: github.NewClient(httpClientWithUser()),
+ Client: mustNewGHClient(t, httpClientWithUser()),
GQLClient: gqlClientNoTeams(),
}
},
@@ -375,7 +372,7 @@ func Test_GetTeams(t *testing.T) {
name: "get user fails",
makeDeps: func() ToolDependencies {
return BaseDeps{
- Client: github.NewClient(httpClientUserFails()),
+ Client: mustNewGHClient(t, httpClientUserFails()),
Obsv: stubExporters(),
}
},
@@ -387,7 +384,7 @@ func Test_GetTeams(t *testing.T) {
name: "getting GraphQL client fails",
makeDeps: func() ToolDependencies {
return stubDeps{
- clientFn: stubClientFnFromHTTP(httpClientWithUser()),
+ clientFn: stubClientFnFromHTTP(t, httpClientWithUser()),
gqlClientFn: stubGQLClientFnErr("GraphQL client error"),
obsv: stubExporters(),
}
diff --git a/pkg/github/copilot.go b/pkg/github/copilot.go
index d95357e738..017bb98bc9 100644
--- a/pkg/github/copilot.go
+++ b/pkg/github/copilot.go
@@ -17,7 +17,7 @@ import (
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/go-viper/mapstructure/v2"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
diff --git a/pkg/github/copilot_test.go b/pkg/github/copilot_test.go
index 0a1d5ef3b6..b86f26f474 100644
--- a/pkg/github/copilot_test.go
+++ b/pkg/github/copilot_test.go
@@ -10,7 +10,7 @@ import (
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -932,7 +932,7 @@ func Test_RequestCopilotReview(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := RequestCopilotReview(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go
index 541cc5c1e7..ccb36f4839 100644
--- a/pkg/github/dependabot.go
+++ b/pkg/github/dependabot.go
@@ -12,7 +12,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/dependabot_test.go b/pkg/github/dependabot_test.go
index 6c9b95ca36..2196b6b13f 100644
--- a/pkg/github/dependabot_test.go
+++ b/pkg/github/dependabot_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -89,7 +89,7 @@ func Test_GetDependabotAlert(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
handler := toolDef.Handler(deps)
@@ -243,7 +243,7 @@ func Test_ListDependabotAlerts(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
handler := toolDef.Handler(deps)
diff --git a/pkg/github/dependencies.go b/pkg/github/dependencies.go
index aad213e4e5..16be84efb4 100644
--- a/pkg/github/dependencies.go
+++ b/pkg/github/dependencies.go
@@ -18,7 +18,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
)
@@ -320,10 +320,14 @@ func (d *RequestDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {
}
// Construct REST client
- restClient := gogithub.NewClient(nil).WithAuthToken(token)
- restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", d.version)
- restClient.BaseURL = baseRestURL
- restClient.UploadURL = uploadURL
+ restClient, err := gogithub.NewClient(
+ gogithub.WithAuthToken(token),
+ gogithub.WithUserAgent(fmt.Sprintf("github-mcp-server/%s", d.version)),
+ gogithub.WithEnterpriseURLs(baseRestURL.String(), uploadURL.String()),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create REST client: %w", err)
+ }
return restClient, nil
}
@@ -370,7 +374,10 @@ func (d *RequestDeps) GetRawClient(ctx context.Context) (*raw.Client, error) {
return nil, fmt.Errorf("failed to get Raw URL: %w", err)
}
- rawClient := raw.NewClient(client, rawURL)
+ rawClient, err := raw.NewClient(client, rawURL)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create raw client: %w", err)
+ }
return rawClient, nil
}
diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go
index 700560b475..514a2d030d 100644
--- a/pkg/github/discussions.go
+++ b/pkg/github/discussions.go
@@ -4,13 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
+ "strings"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/go-viper/mapstructure/v2"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
@@ -405,6 +406,10 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
Type: "number",
Description: "Discussion Number",
},
+ "includeReplies": {
+ Type: "boolean",
+ Description: "When true, each top-level comment will include its replies nested within it (up to 100 replies per comment, which is the GitHub API maximum). Defaults to false.",
+ },
},
Required: []string{"owner", "repo", "discussionNumber"},
}),
@@ -421,6 +426,11 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
return utils.NewToolResultError(err.Error()), nil, nil
}
+ includeReplies, err := OptionalParam[bool](args, "includeReplies")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
// Get pagination parameters and convert to GraphQL format
pagination, err := OptionalCursorPaginationParams(args)
if err != nil {
@@ -447,24 +457,6 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil
}
- var q struct {
- Repository struct {
- Discussion struct {
- Comments struct {
- Nodes []struct {
- Body githubv4.String
- }
- PageInfo struct {
- HasNextPage githubv4.Boolean
- HasPreviousPage githubv4.Boolean
- StartCursor githubv4.String
- EndCursor githubv4.String
- }
- TotalCount int
- } `graphql:"comments(first: $first, after: $after)"`
- } `graphql:"discussion(number: $discussionNumber)"`
- } `graphql:"repository(owner: $owner, name: $repo)"`
- }
vars := map[string]any{
"owner": githubv4.String(params.Owner),
"repo": githubv4.String(params.Repo),
@@ -476,25 +468,111 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
} else {
vars["after"] = (*githubv4.String)(nil)
}
- if err := client.Query(ctx, &q, vars); err != nil {
- return utils.NewToolResultError(err.Error()), nil, nil
+
+ var comments []MinimalDiscussionComment
+ var pageInfo struct {
+ HasNextPage githubv4.Boolean
+ HasPreviousPage githubv4.Boolean
+ StartCursor githubv4.String
+ EndCursor githubv4.String
}
+ var totalCount int
- var comments []*github.IssueComment
- for _, c := range q.Repository.Discussion.Comments.Nodes {
- comments = append(comments, &github.IssueComment{Body: github.Ptr(string(c.Body))})
+ if includeReplies {
+ var q struct {
+ Repository struct {
+ Discussion struct {
+ Comments struct {
+ Nodes []struct {
+ ID githubv4.ID
+ Body githubv4.String
+ IsAnswer githubv4.Boolean
+ Replies struct {
+ Nodes []struct {
+ ID githubv4.ID
+ Body githubv4.String
+ IsAnswer githubv4.Boolean
+ }
+ TotalCount int
+ } `graphql:"replies(first: 100)"`
+ }
+ PageInfo struct {
+ HasNextPage githubv4.Boolean
+ HasPreviousPage githubv4.Boolean
+ StartCursor githubv4.String
+ EndCursor githubv4.String
+ }
+ TotalCount int
+ } `graphql:"comments(first: $first, after: $after)"`
+ } `graphql:"discussion(number: $discussionNumber)"`
+ } `graphql:"repository(owner: $owner, name: $repo)"`
+ }
+ if err := client.Query(ctx, &q, vars); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ for _, c := range q.Repository.Discussion.Comments.Nodes {
+ comment := MinimalDiscussionComment{
+ ID: fmt.Sprintf("%v", c.ID),
+ Body: string(c.Body),
+ IsAnswer: bool(c.IsAnswer),
+ ReplyTotalCount: c.Replies.TotalCount,
+ }
+ for _, r := range c.Replies.Nodes {
+ comment.Replies = append(comment.Replies, MinimalDiscussionComment{
+ ID: fmt.Sprintf("%v", r.ID),
+ Body: string(r.Body),
+ IsAnswer: bool(r.IsAnswer),
+ })
+ }
+ comments = append(comments, comment)
+ }
+ pageInfo = q.Repository.Discussion.Comments.PageInfo
+ totalCount = q.Repository.Discussion.Comments.TotalCount
+ } else {
+ var q struct {
+ Repository struct {
+ Discussion struct {
+ Comments struct {
+ Nodes []struct {
+ ID githubv4.ID
+ Body githubv4.String
+ IsAnswer githubv4.Boolean
+ }
+ PageInfo struct {
+ HasNextPage githubv4.Boolean
+ HasPreviousPage githubv4.Boolean
+ StartCursor githubv4.String
+ EndCursor githubv4.String
+ }
+ TotalCount int
+ } `graphql:"comments(first: $first, after: $after)"`
+ } `graphql:"discussion(number: $discussionNumber)"`
+ } `graphql:"repository(owner: $owner, name: $repo)"`
+ }
+ if err := client.Query(ctx, &q, vars); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ for _, c := range q.Repository.Discussion.Comments.Nodes {
+ comments = append(comments, MinimalDiscussionComment{
+ ID: fmt.Sprintf("%v", c.ID),
+ Body: string(c.Body),
+ IsAnswer: bool(c.IsAnswer),
+ })
+ }
+ pageInfo = q.Repository.Discussion.Comments.PageInfo
+ totalCount = q.Repository.Discussion.Comments.TotalCount
}
// Create response with pagination info
response := map[string]any{
"comments": comments,
"pageInfo": map[string]any{
- "hasNextPage": q.Repository.Discussion.Comments.PageInfo.HasNextPage,
- "hasPreviousPage": q.Repository.Discussion.Comments.PageInfo.HasPreviousPage,
- "startCursor": string(q.Repository.Discussion.Comments.PageInfo.StartCursor),
- "endCursor": string(q.Repository.Discussion.Comments.PageInfo.EndCursor),
+ "hasNextPage": pageInfo.HasNextPage,
+ "hasPreviousPage": pageInfo.HasPreviousPage,
+ "startCursor": string(pageInfo.StartCursor),
+ "endCursor": string(pageInfo.EndCursor),
},
- "totalCount": q.Repository.Discussion.Comments.TotalCount,
+ "totalCount": totalCount,
}
out, err := json.Marshal(response)
@@ -507,6 +585,409 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
)
}
+func DiscussionCommentWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
+ return NewTool(
+ ToolsetMetadataDiscussions,
+ mcp.Tool{
+ Name: "discussion_comment_write",
+ Description: t("TOOL_DISCUSSION_COMMENT_WRITE_DESCRIPTION", `Write operations for discussion comments.
+Supports adding top-level comments, replying to existing comments, updating comment content, deleting comments, and marking or unmarking comments as the answer.`),
+ Annotations: &mcp.ToolAnnotations{
+ Title: t("TOOL_DISCUSSION_COMMENT_WRITE_USER_TITLE", "Manage discussion comments"),
+ ReadOnlyHint: false,
+ DestructiveHint: jsonschema.Ptr(true),
+ },
+ InputSchema: &jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "method": {
+ Type: "string",
+ Description: `Write operation to perform on a discussion comment.
+Options are:
+- 'add' - adds a new top-level comment to a discussion.
+- 'reply' - replies to a top-level discussion comment (GitHub Discussions only support one level of nesting).
+- 'update' - updates an existing discussion comment.
+- 'delete' - deletes a discussion comment.
+- 'mark_answer' - marks a discussion comment as the answer (Q&A only).
+- 'unmark_answer' - unmarks a discussion comment as the answer (Q&A only).
+`,
+ Enum: []any{"add", "reply", "update", "delete", "mark_answer", "unmark_answer"},
+ },
+ "owner": {
+ Type: "string",
+ Description: "Repository owner (required for 'add' and 'reply' methods)",
+ },
+ "repo": {
+ Type: "string",
+ Description: "Repository name (required for 'add' and 'reply' methods)",
+ },
+ "discussionNumber": {
+ Type: "number",
+ Description: "Discussion number (required for 'add' and 'reply' methods)",
+ },
+ "body": {
+ Type: "string",
+ Description: "Comment content (required for 'add', 'reply', and 'update' methods)",
+ },
+ "commentNodeID": {
+ Type: "string",
+ Description: "The Node ID of the discussion comment (required for 'reply', 'update', 'delete', 'mark_answer', and 'unmark_answer' methods). For 'reply', this is the top-level comment to reply to; GitHub Discussions only support one level of nesting.",
+ },
+ },
+ Required: []string{"method"},
+ },
+ },
+ []scopes.Scope{scopes.Repo},
+ func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
+ method, err := RequiredParam[string](args, "method")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ client, err := deps.GetGQLClient(ctx)
+ if err != nil {
+ return utils.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil, nil
+ }
+
+ switch method {
+ case "add":
+ return addDiscussionComment(ctx, client, args)
+ case "reply":
+ return replyToDiscussionComment(ctx, client, args)
+ case "update":
+ return updateDiscussionComment(ctx, client, args)
+ case "delete":
+ return deleteDiscussionComment(ctx, client, args)
+ case "mark_answer":
+ return markDiscussionCommentAsAnswer(ctx, client, args)
+ case "unmark_answer":
+ return unmarkDiscussionCommentAsAnswer(ctx, client, args)
+ default:
+ return utils.NewToolResultError("invalid method, must be one of: 'add', 'reply', 'update', 'delete', 'mark_answer', 'unmark_answer'"), nil, nil
+ }
+ })
+}
+
+func addDiscussionComment(ctx context.Context, client *githubv4.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
+ }
+ discussionNumber, err := RequiredInt(args, "discussionNumber")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ body, err := RequiredParam[string](args, "body")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ // Get the discussion's node ID using its number
+ var q struct {
+ Repository struct {
+ Discussion struct {
+ ID githubv4.ID
+ } `graphql:"discussion(number: $discussionNumber)"`
+ } `graphql:"repository(owner: $owner, name: $repo)"`
+ }
+ vars := map[string]any{
+ "owner": githubv4.String(owner),
+ "repo": githubv4.String(repo),
+ "discussionNumber": githubv4.Int(discussionNumber), // #nosec G115 - discussion numbers are always small positive integers
+ }
+ if err := client.Query(ctx, &q, vars); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ input := githubv4.AddDiscussionCommentInput{
+ DiscussionID: q.Repository.Discussion.ID,
+ Body: githubv4.String(body),
+ }
+
+ var mutation struct {
+ AddDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"addDiscussionComment(input: $input)"`
+ }
+
+ if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ comment := mutation.AddDiscussionComment.Comment
+ out, err := json.Marshal(MinimalResponse{
+ ID: fmt.Sprintf("%v", comment.ID),
+ URL: string(comment.URL),
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal comment: %w", err)
+ }
+
+ return utils.NewToolResultText(string(out)), nil, nil
+}
+
+func requiredCommentNodeID(args map[string]any) (string, error) {
+ commentNodeID, err := RequiredParam[string](args, "commentNodeID")
+ if err != nil {
+ return "", err
+ }
+ if strings.TrimSpace(commentNodeID) == "" {
+ return "", fmt.Errorf("commentNodeID cannot be blank")
+ }
+ return commentNodeID, nil
+}
+
+func replyToDiscussionComment(ctx context.Context, client *githubv4.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ commentNodeID, err := requiredCommentNodeID(args)
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ 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
+ }
+ discussionNumber, err := RequiredInt(args, "discussionNumber")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ body, err := RequiredParam[string](args, "body")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ // The GitHub API silently ignores an invalid ReplyToID and creates a top-level
+ // comment instead of returning an error, so we validate upfront that the node
+ // exists and is a DiscussionComment to give callers a clear failure.
+ var nodeQuery struct {
+ Node struct {
+ DiscussionComment struct {
+ ID *githubv4.ID
+ Discussion struct {
+ ID githubv4.ID
+ } `graphql:"discussion"`
+ } `graphql:"... on DiscussionComment"`
+ } `graphql:"node(id: $replyToID)"`
+ }
+ if err := client.Query(ctx, &nodeQuery, map[string]any{
+ "replyToID": githubv4.ID(commentNodeID),
+ }); err != nil {
+ return utils.NewToolResultError(fmt.Sprintf("failed to validate commentNodeID: %v", err)), nil, nil
+ }
+ if nodeQuery.Node.DiscussionComment.ID == nil || *nodeQuery.Node.DiscussionComment.ID == "" {
+ return utils.NewToolResultError(fmt.Sprintf("commentNodeID %q does not resolve to a valid discussion comment", commentNodeID)), nil, nil
+ }
+
+ // Get the discussion's node ID using its number
+ var q struct {
+ Repository struct {
+ Discussion struct {
+ ID githubv4.ID
+ } `graphql:"discussion(number: $discussionNumber)"`
+ } `graphql:"repository(owner: $owner, name: $repo)"`
+ }
+ vars := map[string]any{
+ "owner": githubv4.String(owner),
+ "repo": githubv4.String(repo),
+ "discussionNumber": githubv4.Int(discussionNumber), // #nosec G115 - discussion numbers are always small positive integers
+ }
+ if err := client.Query(ctx, &q, vars); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ if nodeQuery.Node.DiscussionComment.Discussion.ID != q.Repository.Discussion.ID {
+ return utils.NewToolResultError(
+ fmt.Sprintf("commentNodeID %q does not belong to discussion #%d in %s/%s", commentNodeID, discussionNumber, owner, repo),
+ ), nil, nil
+ }
+
+ replyToID := githubv4.ID(commentNodeID)
+ input := githubv4.AddDiscussionCommentInput{
+ DiscussionID: nodeQuery.Node.DiscussionComment.Discussion.ID,
+ Body: githubv4.String(body),
+ ReplyToID: &replyToID,
+ }
+
+ var mutation struct {
+ AddDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"addDiscussionComment(input: $input)"`
+ }
+
+ if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ comment := mutation.AddDiscussionComment.Comment
+ out, err := json.Marshal(MinimalResponse{
+ ID: fmt.Sprintf("%v", comment.ID),
+ URL: string(comment.URL),
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal comment: %w", err)
+ }
+
+ return utils.NewToolResultText(string(out)), nil, nil
+}
+
+func updateDiscussionComment(ctx context.Context, client *githubv4.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ commentNodeID, err := requiredCommentNodeID(args)
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ body, err := RequiredParam[string](args, "body")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ input := githubv4.UpdateDiscussionCommentInput{
+ CommentID: githubv4.ID(commentNodeID),
+ Body: githubv4.String(body),
+ }
+
+ var mutation struct {
+ UpdateDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"updateDiscussionComment(input: $input)"`
+ }
+
+ if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ comment := mutation.UpdateDiscussionComment.Comment
+ out, err := json.Marshal(MinimalResponse{
+ ID: fmt.Sprintf("%v", comment.ID),
+ URL: string(comment.URL),
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal comment: %w", err)
+ }
+
+ return utils.NewToolResultText(string(out)), nil, nil
+}
+
+func deleteDiscussionComment(ctx context.Context, client *githubv4.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ commentNodeID, err := requiredCommentNodeID(args)
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ input := githubv4.DeleteDiscussionCommentInput{
+ ID: githubv4.ID(commentNodeID),
+ }
+
+ var mutation struct {
+ DeleteDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"deleteDiscussionComment(input: $input)"`
+ }
+
+ if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ comment := mutation.DeleteDiscussionComment.Comment
+ out, err := json.Marshal(MinimalResponse{
+ ID: fmt.Sprintf("%v", comment.ID),
+ URL: string(comment.URL),
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal comment: %w", err)
+ }
+
+ return utils.NewToolResultText(string(out)), nil, nil
+}
+
+func markDiscussionCommentAsAnswer(ctx context.Context, client *githubv4.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ commentNodeID, err := requiredCommentNodeID(args)
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ input := githubv4.MarkDiscussionCommentAsAnswerInput{
+ ID: githubv4.ID(commentNodeID),
+ }
+ var mutation struct {
+ MarkDiscussionCommentAsAnswer struct {
+ Discussion struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"markDiscussionCommentAsAnswer(input: $input)"`
+ }
+ if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ out, err := json.Marshal(struct {
+ DiscussionID string `json:"discussionID"`
+ DiscussionURL string `json:"discussionURL"`
+ }{
+ DiscussionID: fmt.Sprintf("%v", mutation.MarkDiscussionCommentAsAnswer.Discussion.ID),
+ DiscussionURL: string(mutation.MarkDiscussionCommentAsAnswer.Discussion.URL),
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal discussion: %w", err)
+ }
+
+ return utils.NewToolResultText(string(out)), nil, nil
+}
+
+func unmarkDiscussionCommentAsAnswer(ctx context.Context, client *githubv4.Client, args map[string]any) (*mcp.CallToolResult, any, error) {
+ commentNodeID, err := requiredCommentNodeID(args)
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ input := githubv4.UnmarkDiscussionCommentAsAnswerInput{
+ ID: githubv4.ID(commentNodeID),
+ }
+ var mutation struct {
+ UnmarkDiscussionCommentAsAnswer struct {
+ Discussion struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"unmarkDiscussionCommentAsAnswer(input: $input)"`
+ }
+ if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ out, err := json.Marshal(struct {
+ DiscussionID string `json:"discussionID"`
+ DiscussionURL string `json:"discussionURL"`
+ }{
+ DiscussionID: fmt.Sprintf("%v", mutation.UnmarkDiscussionCommentAsAnswer.Discussion.ID),
+ DiscussionURL: string(mutation.UnmarkDiscussionCommentAsAnswer.Discussion.URL),
+ })
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal discussion: %w", err)
+ }
+
+ return utils.NewToolResultText(string(out)), nil, nil
+}
+
func ListDiscussionCategories(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataDiscussions,
diff --git a/pkg/github/discussions_test.go b/pkg/github/discussions_test.go
index 692ef2ec83..36fdb6c43a 100644
--- a/pkg/github/discussions_test.go
+++ b/pkg/github/discussions_test.go
@@ -9,7 +9,7 @@ import (
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -647,10 +647,11 @@ func Test_GetDiscussionComments(t *testing.T) {
assert.Contains(t, schema.Properties, "owner")
assert.Contains(t, schema.Properties, "repo")
assert.Contains(t, schema.Properties, "discussionNumber")
+ assert.Contains(t, schema.Properties, "includeReplies")
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "discussionNumber"})
// Use exact string query that matches implementation output
- qGetComments := "query($after:String$discussionNumber:Int!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){comments(first: $first, after: $after){nodes{body},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}}"
+ qGetComments := "query($after:String$discussionNumber:Int!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){comments(first: $first, after: $after){nodes{id,body,isAnswer},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}}"
// Variables matching what GraphQL receives after JSON marshaling/unmarshaling
vars := map[string]any{
@@ -666,8 +667,8 @@ func Test_GetDiscussionComments(t *testing.T) {
"discussion": map[string]any{
"comments": map[string]any{
"nodes": []map[string]any{
- {"body": "This is the first comment"},
- {"body": "This is the second comment"},
+ {"id": "DC_id1", "body": "This is the first comment"},
+ {"id": "DC_id2", "body": "This is the second comment"},
},
"pageInfo": map[string]any{
"hasNextPage": false,
@@ -701,7 +702,10 @@ func Test_GetDiscussionComments(t *testing.T) {
// (Lines removed)
var response struct {
- Comments []*github.IssueComment `json:"comments"`
+ Comments []struct {
+ ID string `json:"id"`
+ Body string `json:"body"`
+ } `json:"comments"`
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
@@ -713,17 +717,17 @@ func Test_GetDiscussionComments(t *testing.T) {
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Len(t, response.Comments, 2)
- expectedBodies := []string{"This is the first comment", "This is the second comment"}
- for i, comment := range response.Comments {
- assert.Equal(t, expectedBodies[i], *comment.Body)
- }
+ assert.Equal(t, "DC_id1", response.Comments[0].ID)
+ assert.Equal(t, "This is the first comment", response.Comments[0].Body)
+ assert.Equal(t, "DC_id2", response.Comments[1].ID)
+ assert.Equal(t, "This is the second comment", response.Comments[1].Body)
}
func Test_GetDiscussionCommentsWithStringNumber(t *testing.T) {
// Test that WeakDecode handles string discussionNumber from MCP clients
toolDef := GetDiscussionComments(translations.NullTranslationHelper)
- qGetComments := "query($after:String$discussionNumber:Int!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){comments(first: $first, after: $after){nodes{body},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}}"
+ qGetComments := "query($after:String$discussionNumber:Int!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){comments(first: $first, after: $after){nodes{id,body,isAnswer},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}}"
vars := map[string]any{
"owner": "owner",
@@ -738,7 +742,7 @@ func Test_GetDiscussionCommentsWithStringNumber(t *testing.T) {
"discussion": map[string]any{
"comments": map[string]any{
"nodes": []map[string]any{
- {"body": "First comment"},
+ {"id": "DC_id3", "body": "First comment"},
},
"pageInfo": map[string]any{
"hasNextPage": false,
@@ -777,6 +781,7 @@ func Test_GetDiscussionCommentsWithStringNumber(t *testing.T) {
}
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &out))
assert.Len(t, out.Comments, 1)
+ assert.Equal(t, "DC_id3", out.Comments[0]["id"])
assert.Equal(t, "First comment", out.Comments[0]["body"])
}
@@ -924,3 +929,896 @@ func Test_ListDiscussionCategories(t *testing.T) {
})
}
}
+
+func Test_DiscussionCommentWrite(t *testing.T) {
+ t.Parallel()
+
+ toolDef := DiscussionCommentWrite(translations.NullTranslationHelper)
+ tool := toolDef.Tool
+ require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+ assert.Equal(t, "discussion_comment_write", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.False(t, tool.Annotations.ReadOnlyHint, "discussion_comment_write should not be read-only")
+ require.NotNil(t, tool.Annotations.DestructiveHint)
+ assert.True(t, *tool.Annotations.DestructiveHint, "discussion_comment_write should be destructive")
+ schema, ok := tool.InputSchema.(*jsonschema.Schema)
+ require.True(t, ok, "InputSchema should be *jsonschema.Schema")
+ assert.Contains(t, schema.Properties, "method")
+ assert.Contains(t, schema.Properties, "owner")
+ assert.Contains(t, schema.Properties, "repo")
+ assert.Contains(t, schema.Properties, "discussionNumber")
+ assert.Contains(t, schema.Properties, "body")
+ assert.Contains(t, schema.Properties, "commentNodeID")
+ assert.ElementsMatch(t, schema.Required, []string{"method"})
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "method: missing",
+ requestArgs: map[string]any{},
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "missing required parameter: method",
+ },
+ {
+ name: "invalid method",
+ requestArgs: map[string]any{
+ "method": "invalid",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "invalid method, must be one of: 'add', 'reply', 'update', 'delete', 'mark_answer', 'unmark_answer'",
+ },
+ })
+}
+
+func Test_DiscussionCommentWrite_Add(t *testing.T) {
+ t.Parallel()
+
+ discussionQueryMatcher := discussionCommentWriteDiscussionQueryMatcher(
+ 1,
+ githubv4mock.DataResponse(map[string]any{
+ "repository": map[string]any{
+ "discussion": map[string]any{
+ "id": "D_kwDOTest123",
+ },
+ },
+ }),
+ )
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "add: successful comment creation",
+ requestArgs: map[string]any{
+ "method": "add",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a test comment",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ discussionQueryMatcher,
+ githubv4mock.NewMutationMatcher(
+ struct {
+ AddDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"addDiscussionComment(input: $input)"`
+ }{},
+ githubv4.AddDiscussionCommentInput{
+ DiscussionID: githubv4.ID("D_kwDOTest123"),
+ Body: githubv4.String("This is a test comment"),
+ },
+ nil,
+ githubv4mock.DataResponse(map[string]any{
+ "addDiscussionComment": map[string]any{
+ "comment": map[string]any{
+ "id": "DC_kwDOComment456",
+ "url": "https://github.com/owner/repo/discussions/1#discussioncomment-456",
+ },
+ },
+ }),
+ ),
+ ),
+ expectedID: "DC_kwDOComment456",
+ expectedURL: "https://github.com/owner/repo/discussions/1#discussioncomment-456",
+ },
+ {
+ name: "add: discussion not found",
+ requestArgs: map[string]any{
+ "method": "add",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(999),
+ "body": "This is a comment",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewQueryMatcher(
+ struct {
+ Repository struct {
+ Discussion struct {
+ ID githubv4.ID
+ } `graphql:"discussion(number: $discussionNumber)"`
+ } `graphql:"repository(owner: $owner, name: $repo)"`
+ }{},
+ map[string]any{
+ "owner": githubv4.String("owner"),
+ "repo": githubv4.String("repo"),
+ "discussionNumber": githubv4.Int(999),
+ },
+ githubv4mock.ErrorResponse("Could not resolve to a Discussion with the number of 999."),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "Could not resolve to a Discussion with the number of 999.",
+ },
+ {
+ name: "add: mutation failure",
+ requestArgs: map[string]any{
+ "method": "add",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a comment",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ discussionQueryMatcher,
+ githubv4mock.NewMutationMatcher(
+ struct {
+ AddDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"addDiscussionComment(input: $input)"`
+ }{},
+ githubv4.AddDiscussionCommentInput{
+ DiscussionID: githubv4.ID("D_kwDOTest123"),
+ Body: githubv4.String("This is a comment"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("insufficient permissions to comment on this discussion"),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "insufficient permissions to comment on this discussion",
+ },
+ {
+ name: "add: missing body",
+ requestArgs: map[string]any{
+ "method": "add",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "missing required parameter: body",
+ },
+ })
+}
+
+func Test_DiscussionCommentWrite_Reply(t *testing.T) {
+ t.Parallel()
+
+ discussionQueryMatcher := discussionCommentWriteDiscussionQueryMatcher(
+ 1,
+ githubv4mock.DataResponse(map[string]any{
+ "repository": map[string]any{
+ "discussion": map[string]any{
+ "id": "D_kwDOTest123",
+ },
+ },
+ }),
+ )
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "reply: successful reply to comment",
+ requestArgs: map[string]any{
+ "method": "reply",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a reply",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ discussionCommentWriteReplyValidationQueryMatcher(
+ "DC_kwDOComment456",
+ githubv4mock.DataResponse(map[string]any{
+ "node": map[string]any{
+ "id": "DC_kwDOComment456",
+ "discussion": map[string]any{
+ "id": "D_kwDOTest123",
+ },
+ },
+ }),
+ ),
+ discussionQueryMatcher,
+ githubv4mock.NewMutationMatcher(
+ struct {
+ AddDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"addDiscussionComment(input: $input)"`
+ }{},
+ githubv4.AddDiscussionCommentInput{
+ DiscussionID: githubv4.ID("D_kwDOTest123"),
+ Body: githubv4.String("This is a reply"),
+ ReplyToID: githubv4ptr("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.DataResponse(map[string]any{
+ "addDiscussionComment": map[string]any{
+ "comment": map[string]any{
+ "id": "DC_kwDOReply789",
+ "url": "https://github.com/owner/repo/discussions/1#discussioncomment-789",
+ },
+ },
+ }),
+ ),
+ ),
+ expectedID: "DC_kwDOReply789",
+ expectedURL: "https://github.com/owner/repo/discussions/1#discussioncomment-789",
+ },
+ {
+ name: "reply: missing commentNodeID",
+ requestArgs: map[string]any{
+ "method": "reply",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a reply",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "missing required parameter: commentNodeID",
+ },
+ {
+ name: "reply: whitespace-only commentNodeID is rejected",
+ requestArgs: map[string]any{
+ "method": "reply",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a reply",
+ "commentNodeID": " ",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "commentNodeID cannot be blank",
+ },
+ {
+ name: "reply: invalid commentNodeID returns error",
+ requestArgs: map[string]any{
+ "method": "reply",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a reply",
+ "commentNodeID": "DC_kwDOInvalid",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ discussionCommentWriteReplyValidationQueryMatcher(
+ "DC_kwDOInvalid",
+ githubv4mock.DataResponse(map[string]any{
+ "node": nil,
+ }),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: `commentNodeID "DC_kwDOInvalid" does not resolve to a valid discussion comment`,
+ },
+ {
+ name: "reply: comment from another discussion is rejected",
+ requestArgs: map[string]any{
+ "method": "reply",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a reply",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ discussionCommentWriteReplyValidationQueryMatcher(
+ "DC_kwDOComment456",
+ githubv4mock.DataResponse(map[string]any{
+ "node": map[string]any{
+ "id": "DC_kwDOComment456",
+ "discussion": map[string]any{
+ "id": "D_kwDOOtherDiscussion456",
+ },
+ },
+ }),
+ ),
+ discussionQueryMatcher,
+ ),
+ expectToolError: true,
+ expectedErrMsg: `commentNodeID "DC_kwDOComment456" does not belong to discussion #1 in owner/repo`,
+ },
+ {
+ name: "reply: validation query failure",
+ requestArgs: map[string]any{
+ "method": "reply",
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "body": "This is a reply",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ discussionCommentWriteReplyValidationQueryMatcher(
+ "DC_kwDOComment456",
+ githubv4mock.ErrorResponse("Could not resolve to a node with the global id of 'DC_kwDOComment456'."),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "failed to validate commentNodeID: Could not resolve to a node with the global id of 'DC_kwDOComment456'.",
+ },
+ })
+}
+
+func Test_DiscussionCommentWrite_Update(t *testing.T) {
+ t.Parallel()
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "update: successful comment update",
+ requestArgs: map[string]any{
+ "method": "update",
+ "commentNodeID": "DC_kwDOComment456",
+ "body": "Updated comment text",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ UpdateDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"updateDiscussionComment(input: $input)"`
+ }{},
+ githubv4.UpdateDiscussionCommentInput{
+ CommentID: githubv4.ID("DC_kwDOComment456"),
+ Body: githubv4.String("Updated comment text"),
+ },
+ nil,
+ githubv4mock.DataResponse(map[string]any{
+ "updateDiscussionComment": map[string]any{
+ "comment": map[string]any{
+ "id": "DC_kwDOComment456",
+ "url": "https://github.com/owner/repo/discussions/1#discussioncomment-456",
+ },
+ },
+ }),
+ ),
+ ),
+ expectedID: "DC_kwDOComment456",
+ expectedURL: "https://github.com/owner/repo/discussions/1#discussioncomment-456",
+ },
+ {
+ name: "update: comment not found",
+ requestArgs: map[string]any{
+ "method": "update",
+ "commentNodeID": "DC_kwDOInvalid",
+ "body": "Updated comment text",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ UpdateDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"updateDiscussionComment(input: $input)"`
+ }{},
+ githubv4.UpdateDiscussionCommentInput{
+ CommentID: githubv4.ID("DC_kwDOInvalid"),
+ Body: githubv4.String("Updated comment text"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("Could not resolve to a node with the global id of 'DC_kwDOInvalid'."),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "Could not resolve to a node with the global id of 'DC_kwDOInvalid'.",
+ },
+ {
+ name: "update: insufficient permissions",
+ requestArgs: map[string]any{
+ "method": "update",
+ "commentNodeID": "DC_kwDOComment456",
+ "body": "Updated comment text",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ UpdateDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"updateDiscussionComment(input: $input)"`
+ }{},
+ githubv4.UpdateDiscussionCommentInput{
+ CommentID: githubv4.ID("DC_kwDOComment456"),
+ Body: githubv4.String("Updated comment text"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("insufficient permissions to update this discussion comment"),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "insufficient permissions to update this discussion comment",
+ },
+ {
+ name: "update: missing commentNodeID",
+ requestArgs: map[string]any{
+ "method": "update",
+ "body": "Updated comment text",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "missing required parameter: commentNodeID",
+ },
+ {
+ name: "update: whitespace-only commentNodeID is rejected",
+ requestArgs: map[string]any{
+ "method": "update",
+ "commentNodeID": " ",
+ "body": "Updated comment text",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "commentNodeID cannot be blank",
+ },
+ {
+ name: "update: missing body",
+ requestArgs: map[string]any{
+ "method": "update",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "missing required parameter: body",
+ },
+ })
+}
+
+func Test_DiscussionCommentWrite_Delete(t *testing.T) {
+ t.Parallel()
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "delete: successful comment delete",
+ requestArgs: map[string]any{
+ "method": "delete",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ DeleteDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"deleteDiscussionComment(input: $input)"`
+ }{},
+ githubv4.DeleteDiscussionCommentInput{
+ ID: githubv4.ID("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.DataResponse(map[string]any{
+ "deleteDiscussionComment": map[string]any{
+ "comment": map[string]any{
+ "id": "DC_kwDOComment456",
+ "url": "https://github.com/owner/repo/discussions/1#discussioncomment-456",
+ },
+ },
+ }),
+ ),
+ ),
+ expectedID: "DC_kwDOComment456",
+ expectedURL: "https://github.com/owner/repo/discussions/1#discussioncomment-456",
+ },
+ {
+ name: "delete: comment not found",
+ requestArgs: map[string]any{
+ "method": "delete",
+ "commentNodeID": "DC_kwDOInvalid",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ DeleteDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"deleteDiscussionComment(input: $input)"`
+ }{},
+ githubv4.DeleteDiscussionCommentInput{
+ ID: githubv4.ID("DC_kwDOInvalid"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("Could not resolve to a node with the global id of 'DC_kwDOInvalid'."),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "Could not resolve to a node with the global id of 'DC_kwDOInvalid'.",
+ },
+ {
+ name: "delete: insufficient permissions",
+ requestArgs: map[string]any{
+ "method": "delete",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ DeleteDiscussionComment struct {
+ Comment struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"deleteDiscussionComment(input: $input)"`
+ }{},
+ githubv4.DeleteDiscussionCommentInput{
+ ID: githubv4.ID("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("insufficient permissions to delete this discussion comment"),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "insufficient permissions to delete this discussion comment",
+ },
+ {
+ name: "delete: missing commentNodeID",
+ requestArgs: map[string]any{
+ "method": "delete",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "missing required parameter: commentNodeID",
+ },
+ })
+}
+
+func Test_DiscussionCommentWrite_MarkAnswer(t *testing.T) {
+ t.Parallel()
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "mark_answer: successful mark as answer",
+ requestArgs: map[string]any{
+ "method": "mark_answer",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ MarkDiscussionCommentAsAnswer struct {
+ Discussion struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"markDiscussionCommentAsAnswer(input: $input)"`
+ }{},
+ githubv4.MarkDiscussionCommentAsAnswerInput{
+ ID: githubv4.ID("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.DataResponse(map[string]any{
+ "markDiscussionCommentAsAnswer": map[string]any{
+ "discussion": map[string]any{
+ "id": "D_kwDOTest123",
+ "url": "https://github.com/owner/repo/discussions/1",
+ },
+ },
+ }),
+ ),
+ ),
+ expectedDiscussionID: "D_kwDOTest123",
+ expectedDiscussionURL: "https://github.com/owner/repo/discussions/1",
+ },
+ {
+ name: "mark_answer: mutation failure",
+ requestArgs: map[string]any{
+ "method": "mark_answer",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ MarkDiscussionCommentAsAnswer struct {
+ Discussion struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"markDiscussionCommentAsAnswer(input: $input)"`
+ }{},
+ githubv4.MarkDiscussionCommentAsAnswerInput{
+ ID: githubv4.ID("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("discussion is not a Q&A discussion"),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "discussion is not a Q&A discussion",
+ },
+ {
+ name: "mark_answer: whitespace-only commentNodeID is rejected",
+ requestArgs: map[string]any{
+ "method": "mark_answer",
+ "commentNodeID": " ",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(),
+ expectToolError: true,
+ expectedErrMsg: "commentNodeID cannot be blank",
+ },
+ })
+}
+
+func Test_DiscussionCommentWrite_UnmarkAnswer(t *testing.T) {
+ t.Parallel()
+
+ runDiscussionCommentWriteTests(t, []discussionCommentWriteTestCase{
+ {
+ name: "unmark_answer: successful unmark as answer",
+ requestArgs: map[string]any{
+ "method": "unmark_answer",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ UnmarkDiscussionCommentAsAnswer struct {
+ Discussion struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"unmarkDiscussionCommentAsAnswer(input: $input)"`
+ }{},
+ githubv4.UnmarkDiscussionCommentAsAnswerInput{
+ ID: githubv4.ID("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.DataResponse(map[string]any{
+ "unmarkDiscussionCommentAsAnswer": map[string]any{
+ "discussion": map[string]any{
+ "id": "D_kwDOTest123",
+ "url": "https://github.com/owner/repo/discussions/1",
+ },
+ },
+ }),
+ ),
+ ),
+ expectedDiscussionID: "D_kwDOTest123",
+ expectedDiscussionURL: "https://github.com/owner/repo/discussions/1",
+ },
+ {
+ name: "unmark_answer: mutation failure",
+ requestArgs: map[string]any{
+ "method": "unmark_answer",
+ "commentNodeID": "DC_kwDOComment456",
+ },
+ mockedClient: githubv4mock.NewMockedHTTPClient(
+ githubv4mock.NewMutationMatcher(
+ struct {
+ UnmarkDiscussionCommentAsAnswer struct {
+ Discussion struct {
+ ID githubv4.ID
+ URL githubv4.String `graphql:"url"`
+ }
+ } `graphql:"unmarkDiscussionCommentAsAnswer(input: $input)"`
+ }{},
+ githubv4.UnmarkDiscussionCommentAsAnswerInput{
+ ID: githubv4.ID("DC_kwDOComment456"),
+ },
+ nil,
+ githubv4mock.ErrorResponse("insufficient permissions"),
+ ),
+ ),
+ expectToolError: true,
+ expectedErrMsg: "insufficient permissions",
+ },
+ })
+}
+
+type discussionCommentWriteTestCase struct {
+ name string
+ requestArgs map[string]any
+ mockedClient *http.Client
+ expectToolError bool
+ expectedErrMsg string
+ expectedID string
+ expectedURL string
+ expectedDiscussionID string
+ expectedDiscussionURL string
+}
+
+func runDiscussionCommentWriteTests(t *testing.T, tests []discussionCommentWriteTestCase) {
+ t.Helper()
+
+ toolDef := DiscussionCommentWrite(translations.NullTranslationHelper)
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ gqlClient := githubv4.NewClient(tc.mockedClient)
+ deps := BaseDeps{GQLClient: gqlClient}
+ handler := toolDef.Handler(deps)
+
+ req := createMCPRequest(tc.requestArgs)
+ res, err := handler(ContextWithDeps(context.Background(), deps), &req)
+ require.NoError(t, err)
+
+ text := getTextResult(t, res).Text
+
+ if tc.expectToolError {
+ require.True(t, res.IsError)
+ assert.Contains(t, text, tc.expectedErrMsg)
+ return
+ }
+
+ require.False(t, res.IsError)
+
+ if tc.expectedDiscussionID != "" {
+ var response struct {
+ DiscussionID string `json:"discussionID"`
+ DiscussionURL string `json:"discussionURL"`
+ }
+ require.NoError(t, json.Unmarshal([]byte(text), &response))
+ assert.Equal(t, tc.expectedDiscussionID, response.DiscussionID)
+ assert.Equal(t, tc.expectedDiscussionURL, response.DiscussionURL)
+ } else {
+ var response MinimalResponse
+ require.NoError(t, json.Unmarshal([]byte(text), &response))
+ assert.Equal(t, tc.expectedID, response.ID)
+ assert.Equal(t, tc.expectedURL, response.URL)
+ }
+ })
+ }
+}
+
+func discussionCommentWriteDiscussionQueryMatcher(discussionNumber int32, response githubv4mock.GQLResponse) githubv4mock.Matcher {
+ return githubv4mock.NewQueryMatcher(
+ struct {
+ Repository struct {
+ Discussion struct {
+ ID githubv4.ID
+ } `graphql:"discussion(number: $discussionNumber)"`
+ } `graphql:"repository(owner: $owner, name: $repo)"`
+ }{},
+ map[string]any{
+ "owner": githubv4.String("owner"),
+ "repo": githubv4.String("repo"),
+ "discussionNumber": githubv4.Int(discussionNumber),
+ },
+ response,
+ )
+}
+
+func discussionCommentWriteReplyValidationQueryMatcher(commentNodeID string, response githubv4mock.GQLResponse) githubv4mock.Matcher {
+ return githubv4mock.NewQueryMatcher(
+ struct {
+ Node struct {
+ DiscussionComment struct {
+ ID *githubv4.ID
+ Discussion struct {
+ ID githubv4.ID
+ } `graphql:"discussion"`
+ } `graphql:"... on DiscussionComment"`
+ } `graphql:"node(id: $replyToID)"`
+ }{},
+ map[string]any{
+ "replyToID": githubv4.ID(commentNodeID),
+ },
+ response,
+ )
+}
+
+func githubv4ptr(id githubv4.ID) *githubv4.ID {
+ return &id
+}
+
+func Test_GetDiscussionCommentsWithReplies(t *testing.T) {
+ t.Parallel()
+
+ toolDef := GetDiscussionComments(translations.NullTranslationHelper)
+
+ qWithReplies := "query($after:String$discussionNumber:Int!$first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){comments(first: $first, after: $after){nodes{id,body,isAnswer,replies(first: 100){nodes{id,body,isAnswer},totalCount}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}}"
+
+ vars := map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": float64(1),
+ "first": float64(30),
+ "after": (*string)(nil),
+ }
+
+ mockResponse := githubv4mock.DataResponse(map[string]any{
+ "repository": map[string]any{
+ "discussion": map[string]any{
+ "comments": map[string]any{
+ "nodes": []map[string]any{
+ {
+ "id": "DC_id1",
+ "body": "Top-level comment",
+ "replies": map[string]any{
+ "nodes": []map[string]any{
+ {"id": "DC_reply1", "body": "Reply to first comment", "isAnswer": true},
+ },
+ "totalCount": 1,
+ },
+ },
+ {
+ "id": "DC_id2",
+ "body": "Another top-level comment",
+ "replies": map[string]any{
+ "nodes": []map[string]any{},
+ "totalCount": 0,
+ },
+ },
+ },
+ "pageInfo": map[string]any{
+ "hasNextPage": false,
+ "hasPreviousPage": false,
+ "startCursor": "",
+ "endCursor": "",
+ },
+ "totalCount": 2,
+ },
+ },
+ },
+ })
+
+ matcher := githubv4mock.NewQueryMatcher(qWithReplies, vars, mockResponse)
+ httpClient := githubv4mock.NewMockedHTTPClient(matcher)
+ gqlClient := githubv4.NewClient(httpClient)
+ deps := BaseDeps{GQLClient: gqlClient}
+ handler := toolDef.Handler(deps)
+
+ reqParams := map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "discussionNumber": int32(1),
+ "includeReplies": true,
+ }
+ req := createMCPRequest(reqParams)
+ res, err := handler(ContextWithDeps(context.Background(), deps), &req)
+ require.NoError(t, err)
+
+ text := getTextResult(t, res).Text
+ require.False(t, res.IsError, "expected no error, got: %s", text)
+
+ var response struct {
+ Comments []MinimalDiscussionComment `json:"comments"`
+ PageInfo struct {
+ HasNextPage bool `json:"hasNextPage"`
+ } `json:"pageInfo"`
+ TotalCount int `json:"totalCount"`
+ }
+ require.NoError(t, json.Unmarshal([]byte(text), &response))
+ assert.Len(t, response.Comments, 2)
+
+ assert.Equal(t, "DC_id1", response.Comments[0].ID)
+ assert.Equal(t, "Top-level comment", response.Comments[0].Body)
+ require.Len(t, response.Comments[0].Replies, 1)
+ assert.Equal(t, "DC_reply1", response.Comments[0].Replies[0].ID)
+ assert.Equal(t, "Reply to first comment", response.Comments[0].Replies[0].Body)
+ assert.True(t, response.Comments[0].Replies[0].IsAnswer)
+ assert.Equal(t, 1, response.Comments[0].ReplyTotalCount)
+
+ assert.Equal(t, "DC_id2", response.Comments[1].ID)
+ assert.Empty(t, response.Comments[1].Replies)
+ assert.Equal(t, 0, response.Comments[1].ReplyTotalCount)
+}
diff --git a/pkg/github/gists.go b/pkg/github/gists.go
index a0bc1b0855..de577af04d 100644
--- a/pkg/github/gists.go
+++ b/pkg/github/gists.go
@@ -12,7 +12,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/gists_test.go b/pkg/github/gists_test.go
index 74cd45d276..342cd0c8f5 100644
--- a/pkg/github/gists_test.go
+++ b/pkg/github/gists_test.go
@@ -9,7 +9,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -141,7 +141,7 @@ func Test_ListGists(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -252,7 +252,7 @@ func Test_GetGist(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -392,7 +392,7 @@ func Test_CreateGist(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -545,7 +545,7 @@ func Test_UpdateGist(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/git.go b/pkg/github/git.go
index 33a1f94efa..515d8b65f8 100644
--- a/pkg/github/git.go
+++ b/pkg/github/git.go
@@ -11,7 +11,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/git_test.go b/pkg/github/git_test.go
index cef65c9ef4..1ad7147507 100644
--- a/pkg/github/git_test.go
+++ b/pkg/github/git_test.go
@@ -9,7 +9,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -125,7 +125,7 @@ func Test_GetRepositoryTree(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go
index 6623894e43..72ed1939d5 100644
--- a/pkg/github/granular_tools_test.go
+++ b/pkg/github/granular_tools_test.go
@@ -3,13 +3,14 @@ package github
import (
"context"
"net/http"
+ "strings"
"testing"
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -175,7 +176,7 @@ func TestGranularCreateIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := gogithub.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := GranularCreateIssue(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
@@ -195,7 +196,7 @@ func TestGranularCreateIssue(t *testing.T) {
}
func TestGranularUpdateIssueTitle(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, &gogithub.Issue{
Number: gogithub.Ptr(42),
Title: gogithub.Ptr("New Title"),
@@ -217,7 +218,7 @@ func TestGranularUpdateIssueTitle(t *testing.T) {
}
func TestGranularUpdateIssueBody(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"body": "Updated body",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
@@ -241,7 +242,7 @@ func TestGranularUpdateIssueBody(t *testing.T) {
}
func TestGranularUpdateIssueAssignees(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"assignees": []any{"user1", "user2"},
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
@@ -262,7 +263,7 @@ func TestGranularUpdateIssueAssignees(t *testing.T) {
}
func TestGranularUpdateIssueLabels(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"labels": []any{"bug", "enhancement"},
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
@@ -283,7 +284,7 @@ func TestGranularUpdateIssueLabels(t *testing.T) {
}
func TestGranularUpdateIssueMilestone(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
"milestone": float64(5),
}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
@@ -304,24 +305,103 @@ func TestGranularUpdateIssueMilestone(t *testing.T) {
}
func TestGranularUpdateIssueType(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
- PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
- "type": "bug",
- }).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
- }))
- deps := BaseDeps{Client: client}
- serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
- handler := serverTool.Handler(deps)
+ tests := []struct {
+ name string
+ requestArgs map[string]any
+ expectedReq map[string]any
+ }{
+ {
+ name: "type only",
+ requestArgs: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "issue_number": float64(1),
+ "issue_type": "bug",
+ },
+ expectedReq: map[string]any{
+ "type": "bug",
+ },
+ },
+ {
+ name: "type with rationale",
+ requestArgs: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "issue_number": float64(1),
+ "issue_type": "feature",
+ "rationale": " This issue requests a new capability ",
+ },
+ expectedReq: map[string]any{
+ "type": map[string]any{
+ "value": "feature",
+ "rationale": "This issue requests a new capability",
+ },
+ },
+ },
+ }
- request := createMCPRequest(map[string]any{
- "owner": "owner",
- "repo": "repo",
- "issue_number": float64(1),
- "issue_type": "bug",
- })
- result, err := handler(ContextWithDeps(context.Background(), deps), &request)
- require.NoError(t, err)
- assert.False(t, result.IsError)
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
+ andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
+ }))
+ deps := BaseDeps{Client: client}
+ serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(tc.requestArgs)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ assert.False(t, result.IsError)
+ })
+ }
+}
+
+func TestGranularUpdateIssueTypeInvalidRationale(t *testing.T) {
+ tests := []struct {
+ name string
+ requestArgs map[string]any
+ expectedErrText string
+ }{
+ {
+ name: "rationale wrong type",
+ requestArgs: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "issue_number": float64(1),
+ "issue_type": "feature",
+ "rationale": float64(123),
+ },
+ expectedErrText: "parameter rationale is not of type string, is float64",
+ },
+ {
+ name: "rationale too long",
+ requestArgs: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "issue_number": float64(1),
+ "issue_type": "feature",
+ "rationale": strings.Repeat("a", 281),
+ },
+ expectedErrText: "parameter rationale must be 280 characters or less",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
+ serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(tc.requestArgs)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+
+ errorContent := getErrorResult(t, result)
+ assert.Contains(t, errorContent.Text, tc.expectedErrText)
+ })
+ }
}
func TestGranularUpdateIssueState(t *testing.T) {
@@ -360,7 +440,7 @@ func TestGranularUpdateIssueState(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
Number: gogithub.Ptr(1),
@@ -382,7 +462,7 @@ func TestGranularUpdateIssueState(t *testing.T) {
// --- Pull request granular tool handler tests ---
func TestGranularUpdatePullRequestTitle(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"title": "New PR Title",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
@@ -406,7 +486,7 @@ func TestGranularUpdatePullRequestTitle(t *testing.T) {
}
func TestGranularUpdatePullRequestBody(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"body": "Updated description",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
@@ -430,7 +510,7 @@ func TestGranularUpdatePullRequestBody(t *testing.T) {
}
func TestGranularUpdatePullRequestState(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
"state": "closed",
}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
@@ -454,7 +534,7 @@ func TestGranularUpdatePullRequestState(t *testing.T) {
}
func TestGranularRequestPullRequestReviewers(t *testing.T) {
- client := gogithub.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &gogithub.PullRequest{Number: gogithub.Ptr(1)}),
}))
deps := BaseDeps{Client: client}
diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go
index 67a05fd6c0..4181f102e4 100644
--- a/pkg/github/helper_test.go
+++ b/pkg/github/helper_test.go
@@ -10,6 +10,7 @@ import (
"strings"
"testing"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
testifymock "github.com/stretchr/testify/mock"
@@ -31,7 +32,6 @@ const (
GetReposByOwnerByRepo = "GET /repos/{owner}/{repo}"
GetReposBranchesByOwnerByRepo = "GET /repos/{owner}/{repo}/branches"
GetReposTagsByOwnerByRepo = "GET /repos/{owner}/{repo}/tags"
- GetReposCollaboratorsByOwnerByRepo = "GET /repos/{owner}/{repo}/collaborators"
GetReposCommitsByOwnerByRepo = "GET /repos/{owner}/{repo}/commits"
GetReposCommitsByOwnerByRepoByRef = "GET /repos/{owner}/{repo}/commits/{ref}"
GetReposContentsByOwnerByRepoByPath = "GET /repos/{owner}/{repo}/contents/{path}"
@@ -40,6 +40,7 @@ const (
GetReposSubscriptionByOwnerByRepo = "GET /repos/{owner}/{repo}/subscription"
PutReposSubscriptionByOwnerByRepo = "PUT /repos/{owner}/{repo}/subscription"
DeleteReposSubscriptionByOwnerByRepo = "DELETE /repos/{owner}/{repo}/subscription"
+ ListCollaborators = "GET /repos/{owner}/{repo}/collaborators"
// Git endpoints
GetReposGitTreesByOwnerByRepoByTree = "GET /repos/{owner}/{repo}/git/trees/{tree}"
@@ -179,6 +180,22 @@ type expectations struct {
requestBody any
}
+// mustNewGHClient creates a new GitHub client for testing.
+// If httpClient is nil, a client with no options is created.
+// The test fails immediately if client creation fails.
+func mustNewGHClient(t *testing.T, httpClient *http.Client) *gogithub.Client {
+ t.Helper()
+ var client *gogithub.Client
+ var err error
+ if httpClient == nil {
+ client, err = gogithub.NewClient()
+ } else {
+ client, err = gogithub.NewClient(gogithub.WithHTTPClient(httpClient))
+ }
+ require.NoError(t, err)
+ return client
+}
+
// expect is a helper function to create a partial mock that expects various
// request behaviors, such as path, query parameters, and request body.
func expect(t *testing.T, e expectations) *partialMock {
@@ -220,9 +237,15 @@ func expectRequestBody(t *testing.T, expectedRequestBody any) *partialMock {
type partialMock struct {
t *testing.T
- expectedPath string
- expectedQueryParams map[string]string
- expectedRequestBody any
+ expectedPath string
+ expectedQueryParams map[string]string
+ expectedRequestBody any
+ expectedHeaderContains map[string]string
+}
+
+func (p *partialMock) withHeaders(headers map[string]string) *partialMock {
+ p.expectedHeaderContains = headers
+ return p
}
func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc {
@@ -247,6 +270,12 @@ func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc
require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody)
}
+ if p.expectedHeaderContains != nil {
+ for k, v := range p.expectedHeaderContains {
+ require.Contains(p.t, r.Header.Get(k), v, "expected header %q to contain %q", k, v)
+ }
+ }
+
responseHandler(w, r)
}
}
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index 8ca9446e3f..b9dbdea3e6 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -16,7 +16,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
@@ -228,6 +228,32 @@ type ListIssuesQueryTypeWithLabelsWithSince struct {
} `graphql:"repository(owner: $owner, name: $repo)"`
}
+// SearchIssueResult wraps a REST search hit with its custom issue field values, fetched in a follow-up GraphQL nodes() query.
+type SearchIssueResult struct {
+ *github.Issue
+ FieldValues []MinimalIssueFieldValue `json:"field_values,omitempty"`
+}
+
+// SearchIssuesResponse mirrors the REST IssuesSearchResult JSON shape and adds field_values per item.
+type SearchIssuesResponse struct {
+ Total *int `json:"total_count,omitempty"`
+ IncompleteResults *bool `json:"incomplete_results,omitempty"`
+ Items []SearchIssueResult `json:"items"`
+}
+
+// searchIssuesNodesQuery batches a nodes(ids:) lookup over the REST search results to retrieve
+// each issue's custom field values in a single GraphQL request.
+type searchIssuesNodesQuery struct {
+ Nodes []struct {
+ Issue struct {
+ ID githubv4.ID
+ IssueFieldValues struct {
+ Nodes []IssueFieldValueFragment
+ } `graphql:"issueFieldValues(first: 25)"` // 25 exceeds the practical max of custom fields per issue in GitHub Projects
+ } `graphql:"... on Issue"`
+ } `graphql:"nodes(ids: $ids)"`
+}
+
// Implement the interface for all query types
func (q *ListIssuesQueryTypeWithLabels) GetIssueFragment() IssueQueryFragment {
return q.Repository.Issues
@@ -347,19 +373,37 @@ Options are:
return utils.NewToolResultErrorFromErr("failed to get GitHub graphql client", err), nil, nil
}
+ // attachIFC adds the IFC label to a successful tool result when
+ // InsidersMode is enabled. If the visibility lookup fails the
+ // label is omitted rather than misclassifying the result.
+ attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
+ if r == nil || r.IsError || !deps.GetFlags(ctx).InsidersMode {
+ return r
+ }
+ isPrivate, err := FetchRepoIsPrivate(ctx, client, owner, repo)
+ if err != nil {
+ return r
+ }
+ if r.Meta == nil {
+ r.Meta = mcp.Meta{}
+ }
+ r.Meta["ifc"] = ifc.LabelListIssues(isPrivate)
+ return r
+ }
+
switch method {
case "get":
result, err := GetIssue(ctx, client, deps, owner, repo, issueNumber)
- return result, nil, err
+ return attachIFC(result), nil, err
case "get_comments":
result, err := GetIssueComments(ctx, client, deps, owner, repo, issueNumber, pagination)
- return result, nil, err
+ return attachIFC(result), nil, err
case "get_sub_issues":
result, err := GetSubIssues(ctx, client, deps, owner, repo, issueNumber, pagination)
- return result, nil, err
+ return attachIFC(result), nil, err
case "get_labels":
result, err := GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber)
- return result, nil, err
+ return attachIFC(result), nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
@@ -485,11 +529,9 @@ func GetSubIssues(ctx context.Context, client *github.Client, deps ToolDependenc
}
featureFlags := deps.GetFlags(ctx)
- opts := &github.IssueListOptions{
- ListOptions: github.ListOptions{
- Page: pagination.Page,
- PerPage: pagination.PerPage,
- },
+ opts := &github.ListOptions{
+ Page: pagination.Page,
+ PerPage: pagination.PerPage,
}
subIssues, resp, err := client.SubIssue.ListByIssue(ctx, owner, repo, int64(issueNumber), opts)
@@ -594,7 +636,6 @@ func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string,
}
return utils.NewToolResultText(string(out)), nil
-
}
// ListIssueTypes creates a tool to list defined issue types for an organization. This can be used to understand supported issue type values for creating or updating issues.
@@ -889,7 +930,6 @@ func AddSubIssue(ctx context.Context, client *github.Client, owner string, repo
}
return utils.NewToolResultText(string(r)), nil
-
}
func RemoveSubIssue(ctx context.Context, client *github.Client, owner string, repo string, issueNumber int, subIssueID int) (*mcp.CallToolResult, error) {
@@ -972,6 +1012,114 @@ func ReprioritizeSubIssue(ctx context.Context, client *github.Client, owner stri
return utils.NewToolResultText(string(r)), nil
}
+// fetchIssueFieldValuesByNodeID runs one GraphQL nodes() query for the given REST issues and
+// returns a map of node_id -> flattened field values. Issues without a node_id are skipped, and
+// an empty result set short-circuits the round-trip.
+func fetchIssueFieldValuesByNodeID(ctx context.Context, gqlClient *githubv4.Client, issues []*github.Issue) (map[string][]MinimalIssueFieldValue, error) {
+ ids := make([]githubv4.ID, 0, len(issues))
+ for _, iss := range issues {
+ if iss == nil || iss.NodeID == nil || *iss.NodeID == "" {
+ continue
+ }
+ ids = append(ids, githubv4.ID(*iss.NodeID))
+ }
+ if len(ids) == 0 {
+ return nil, nil
+ }
+
+ var q searchIssuesNodesQuery
+ if err := gqlClient.Query(ctx, &q, map[string]any{"ids": ids}); err != nil {
+ return nil, err
+ }
+
+ result := make(map[string][]MinimalIssueFieldValue, len(q.Nodes))
+ for _, n := range q.Nodes {
+ if n.Issue.ID == nil {
+ continue
+ }
+ idStr := fmt.Sprintf("%v", n.Issue.ID)
+ if idStr == "" {
+ continue
+ }
+ vals := make([]MinimalIssueFieldValue, 0, len(n.Issue.IssueFieldValues.Nodes))
+ for _, fv := range n.Issue.IssueFieldValues.Nodes {
+ if m, ok := fragmentToMinimalIssueFieldValue(fv); ok {
+ vals = append(vals, m)
+ }
+ }
+ result[idStr] = vals
+ }
+ return result, nil
+}
+
+// searchIssuesHandler runs the REST issues search and enriches each hit with custom field values
+// fetched via a single follow-up GraphQL nodes() query.
+func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[string]any) (*mcp.CallToolResult, error) {
+ const errorPrefix = "failed to search issues"
+
+ query, opts, err := prepareSearchArgs(args, "issue")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := deps.GetClient(ctx)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr(errorPrefix+": failed to get GitHub client", err), nil
+ }
+ result, resp, err := client.Search.Issues(ctx, query, opts)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr(errorPrefix, err), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr(errorPrefix+": failed to read response body", err), nil
+ }
+ return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, errorPrefix, resp, body), nil
+ }
+
+ var fieldValuesByID map[string][]MinimalIssueFieldValue
+ if len(result.Issues) > 0 {
+ gqlClient, err := deps.GetGQLClient(ctx)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr(errorPrefix+": failed to get GitHub GraphQL client", err), nil
+ }
+ fieldValuesByID, err = fetchIssueFieldValuesByNodeID(ctx, gqlClient, result.Issues)
+ if err != nil {
+ return ghErrors.NewGitHubGraphQLErrorResponse(ctx, errorPrefix+": failed to fetch issue field values", err), nil
+ }
+ }
+
+ items := make([]SearchIssueResult, 0, len(result.Issues))
+ for _, iss := range result.Issues {
+ hit := SearchIssueResult{Issue: iss}
+ if iss != nil && iss.NodeID != nil {
+ hit.FieldValues = fieldValuesByID[*iss.NodeID]
+ }
+ items = append(items, hit)
+ }
+
+ response := SearchIssuesResponse{
+ Total: result.Total,
+ IncompleteResults: result.IncompleteResults,
+ Items: items,
+ }
+
+ r, err := json.Marshal(response)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr(errorPrefix+": failed to marshal response", err), nil
+ }
+
+ callResult := utils.NewToolResultText(string(r))
+ if deps.GetFlags(ctx).InsidersMode {
+ fn := searchIssuesIFCPostProcess(deps)
+ fn(ctx, result, callResult)
+ }
+ return callResult, nil
+}
+
// SearchIssues creates a tool to search for issues.
func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
@@ -1029,11 +1177,93 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
- result, err := searchHandler(ctx, deps.GetClient, args, "issue", "failed to search issues")
+ result, err := searchIssuesHandler(ctx, deps, args)
return result, nil, err
})
}
+// searchIssuesIFCPostProcess returns a searchPostProcessFn that attaches the
+// IFC label for a search_issues result. It looks up the visibility (and, for
+// private repos, collaborators) of every repository represented in the search
+// payload and joins the labels via ifc.LabelSearchIssues. If any per-repo
+// lookup fails the label is omitted to avoid misclassifying the result.
+func searchIssuesIFCPostProcess(deps ToolDependencies) searchPostProcessFn {
+ return func(ctx context.Context, result *github.IssuesSearchResult, callResult *mcp.CallToolResult) {
+ if callResult == nil || callResult.IsError || result == nil {
+ return
+ }
+
+ client, err := deps.GetClient(ctx)
+ if err != nil {
+ return
+ }
+
+ uniqueRepos := uniqueSearchIssuesRepos(result)
+ visibilities := make([]bool, 0, len(uniqueRepos))
+ for _, r := range uniqueRepos {
+ isPrivate, err := FetchRepoIsPrivate(ctx, client, r.owner, r.repo)
+ if err != nil {
+ return
+ }
+ visibilities = append(visibilities, isPrivate)
+ }
+
+ if callResult.Meta == nil {
+ callResult.Meta = mcp.Meta{}
+ }
+ callResult.Meta["ifc"] = ifc.LabelSearchIssues(visibilities)
+ }
+}
+
+type searchIssuesRepoRef struct {
+ owner string
+ repo string
+}
+
+// uniqueSearchIssuesRepos extracts the owner/repo pairs of every issue in the
+// search result, preserving order of first appearance and deduplicating.
+func uniqueSearchIssuesRepos(result *github.IssuesSearchResult) []searchIssuesRepoRef {
+ if result == nil {
+ return nil
+ }
+ seen := make(map[string]struct{})
+ var out []searchIssuesRepoRef
+ for _, issue := range result.Issues {
+ if issue == nil {
+ continue
+ }
+ owner, repo, ok := parseRepositoryURL(issue.GetRepositoryURL())
+ if !ok {
+ continue
+ }
+ key := owner + "/" + repo
+ if _, dup := seen[key]; dup {
+ continue
+ }
+ seen[key] = struct{}{}
+ out = append(out, searchIssuesRepoRef{owner: owner, repo: repo})
+ }
+ return out
+}
+
+// parseRepositoryURL extracts the owner and repo from a GitHub API repository
+// URL of the form https://api.github.com/repos/{owner}/{repo}.
+func parseRepositoryURL(repoURL string) (string, string, bool) {
+ if repoURL == "" {
+ return "", "", false
+ }
+ const marker = "/repos/"
+ idx := strings.LastIndex(repoURL, marker)
+ if idx < 0 {
+ return "", "", false
+ }
+ parts := strings.Split(strings.Trim(repoURL[idx+len(marker):], "/"), "/")
+ if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
+ return "", "", false
+ }
+ return parts[0], parts[1], true
+}
+
// IssueWrite creates a tool to create a new or update an existing issue in a GitHub repository.
// IssueWriteUIResourceURI is the URI for the issue_write tool's MCP App UI resource.
const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"
@@ -1646,22 +1876,7 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
if result.Meta == nil {
result.Meta = mcp.Meta{}
}
- var readers []string
- if isPrivate {
- restClient, err := deps.GetClient(ctx)
- if err == nil {
- if collaborators, err := FetchRepoCollaborators(ctx, restClient, owner, repo); err == nil {
- readers = collaborators
- }
- }
- // Fall back to the repository owner so the reader set is
- // never empty for a private repository even if the
- // collaborators lookup fails.
- if len(readers) == 0 {
- readers = []string{owner}
- }
- }
- result.Meta["ifc"] = ifc.LabelListIssues(isPrivate, readers)
+ result.Meta["ifc"] = ifc.LabelListIssues(isPrivate)
}
return result, nil, nil
})
diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go
index fe3b4bcc9b..5b335bd443 100644
--- a/pkg/github/issues_granular.go
+++ b/pkg/github/issues_granular.go
@@ -12,7 +12,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
@@ -309,27 +309,131 @@ func GranularUpdateIssueMilestone(t translations.TranslationHelperFunc) inventor
)
}
+// issueTypeWithRationale represents the object form of the issue type field,
+// allowing a rationale to be sent alongside the type name.
+type issueTypeWithRationale struct {
+ Value string `json:"value"`
+ Rationale string `json:"rationale"`
+}
+
+// issueTypeUpdateRequest is a custom request body for updating an issue type
+// with an optional rationale, using the object form that the REST API accepts.
+type issueTypeUpdateRequest struct {
+ Type issueTypeWithRationale `json:"type"`
+}
+
// GranularUpdateIssueType creates a tool to update an issue's type.
func GranularUpdateIssueType(t translations.TranslationHelperFunc) inventory.ServerTool {
- return issueUpdateTool(t,
- "update_issue_type",
- "Update the type of an existing issue (e.g. 'bug', 'feature').",
- "Update Issue Type",
- map[string]*jsonschema.Schema{
- "issue_type": {
- Type: "string",
- Description: "The issue type to set",
+ st := NewTool(
+ ToolsetMetadataIssues,
+ mcp.Tool{
+ Name: "update_issue_type",
+ Description: t("TOOL_UPDATE_ISSUE_TYPE_DESCRIPTION", "Update the type of an existing issue (e.g. 'bug', 'feature')."),
+ Annotations: &mcp.ToolAnnotations{
+ Title: t("TOOL_UPDATE_ISSUE_TYPE_USER_TITLE", "Update Issue Type"),
+ ReadOnlyHint: false,
+ DestructiveHint: jsonschema.Ptr(false),
+ OpenWorldHint: jsonschema.Ptr(true),
+ },
+ InputSchema: &jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "owner": {
+ Type: "string",
+ Description: "Repository owner (username or organization)",
+ },
+ "repo": {
+ Type: "string",
+ Description: "Repository name",
+ },
+ "issue_number": {
+ Type: "number",
+ Description: "The issue number to update",
+ Minimum: jsonschema.Ptr(1.0),
+ },
+ "issue_type": {
+ Type: "string",
+ Description: "The issue type to set",
+ },
+ "rationale": {
+ Type: "string",
+ Description: "One concise sentence explaining what specifically about the issue led you to choose this type. " +
+ "State the concrete signal (e.g. 'Reports a crash when saving' → bug, 'Asks for dark mode support' → feature).",
+ MaxLength: jsonschema.Ptr(280),
+ },
+ },
+ Required: []string{"owner", "repo", "issue_number", "issue_type"},
},
},
- []string{"issue_type"},
- func(args map[string]any) (*github.IssueRequest, error) {
+ []scopes.Scope{scopes.Repo},
+ func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, 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
+ }
+ issueNumber, err := RequiredInt(args, "issue_number")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
issueType, err := RequiredParam[string](args, "issue_type")
if err != nil {
- return nil, err
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ rationale, err := OptionalParam[string](args, "rationale")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ rationale = strings.TrimSpace(rationale)
+ if len([]rune(rationale)) > 280 {
+ return utils.NewToolResultError("parameter rationale must be 280 characters or less"), nil, nil
+ }
+
+ client, err := deps.GetClient(ctx)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
+ }
+
+ var body any
+ if rationale != "" {
+ body = &issueTypeUpdateRequest{
+ Type: issueTypeWithRationale{
+ Value: issueType,
+ Rationale: rationale,
+ },
+ }
+ } else {
+ body = &github.IssueRequest{Type: &issueType}
+ }
+
+ apiURL := fmt.Sprintf("repos/%s/%s/issues/%d", owner, repo, issueNumber)
+ req, err := client.NewRequest(ctx, "PATCH", apiURL, body)
+ if err != nil {
+ return utils.NewToolResultErrorFromErr("failed to create request", err), nil, nil
}
- return &github.IssueRequest{Type: &issueType}, nil
+
+ issue := &github.Issue{}
+ resp, err := client.Do(req, issue)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update issue", resp, err), nil, nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(MinimalResponse{
+ ID: fmt.Sprintf("%d", issue.GetID()),
+ URL: issue.GetHTMLURL(),
+ })
+ if err != nil {
+ return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
+ }
+ return utils.NewToolResultText(string(r)), nil, nil
},
)
+ st.FeatureFlagEnable = FeatureFlagIssuesGranular
+ return st
}
// GranularUpdateIssueState creates a tool to update an issue's state.
diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go
index c6ecdf99ea..e1d1f43948 100644
--- a/pkg/github/issues_test.go
+++ b/pkg/github/issues_test.go
@@ -14,7 +14,7 @@ import (
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -225,7 +225,7 @@ func Test_GetIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
var restClient *github.Client
if tc.restPermission != "" {
@@ -275,6 +275,123 @@ func Test_GetIssue(t *testing.T) {
}
}
+func Test_IssueRead_IFC_InsidersMode(t *testing.T) {
+ t.Parallel()
+
+ serverTool := IssueRead(translations.NullTranslationHelper)
+
+ mockIssue := &github.Issue{
+ Number: github.Ptr(1),
+ Title: github.Ptr("Test"),
+ Body: github.Ptr("body"),
+ State: github.Ptr("open"),
+ HTMLURL: github.Ptr("https://github.com/octocat/repo/issues/1"),
+ User: &github.User{Login: github.Ptr("u")},
+ }
+
+ mockComments := []*github.IssueComment{
+ {Body: github.Ptr("hello"), User: &github.User{Login: github.Ptr("u")}},
+ }
+
+ makeMockClient := func(isPrivate bool, repoStatus int) *http.Client {
+ handlers := map[string]http.HandlerFunc{
+ GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
+ GetReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockComments),
+ }
+ if repoStatus != 0 && repoStatus != http.StatusOK {
+ handlers[GetReposByOwnerByRepo] = mockResponse(t, repoStatus, "boom")
+ } else {
+ handlers[GetReposByOwnerByRepo] = mockResponse(t, http.StatusOK, map[string]any{
+ "name": "repo",
+ "private": isPrivate,
+ })
+ }
+ return MockHTTPClientWithHandlers(handlers)
+ }
+
+ getReq := map[string]any{
+ "method": "get",
+ "owner": "octocat",
+ "repo": "repo",
+ "issue_number": float64(1),
+ }
+ commentsReq := map[string]any{
+ "method": "get_comments",
+ "owner": "octocat",
+ "repo": "repo",
+ "issue_number": float64(1),
+ }
+
+ t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(false, 0)),
+ Flags: FeatureFlags{InsidersMode: false},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(getReq)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ assert.Nil(t, result.Meta)
+ })
+
+ t.Run("insiders mode enabled on public repo emits public untrusted", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(false, 0)),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(getReq)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
+ })
+
+ t.Run("insiders mode enabled on private repo with get_comments emits private untrusted", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(true, 0)),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(commentsReq)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "private", ifcMap["confidentiality"])
+ })
+
+ t.Run("insiders mode skips ifc label when visibility lookup fails", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(false, http.StatusInternalServerError)),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(getReq)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError, "tool call should still succeed when visibility lookup fails")
+
+ if result.Meta != nil {
+ _, hasIFC := result.Meta["ifc"]
+ assert.False(t, hasIFC, "ifc label should be omitted when visibility lookup fails")
+ }
+ })
+}
+
func Test_AddIssueComment(t *testing.T) {
// Verify tool definition once
serverTool := AddIssueComment(translations.NullTranslationHelper)
@@ -344,7 +461,7 @@ func Test_AddIssueComment(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -381,7 +498,6 @@ func Test_AddIssueComment(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("%d", tc.expectedComment.GetID()), minimalResponse.ID)
assert.Equal(t, tc.expectedComment.GetHTMLURL(), minimalResponse.URL)
-
})
}
}
@@ -647,7 +763,7 @@ func Test_SearchIssues(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -693,6 +809,266 @@ func Test_SearchIssues(t *testing.T) {
}
}
+func Test_SearchIssues_IFC_InsidersMode(t *testing.T) {
+ t.Parallel()
+
+ serverTool := SearchIssues(translations.NullTranslationHelper)
+
+ makeIssue := func(owner, repo string, number int) *github.Issue {
+ return &github.Issue{
+ Number: github.Ptr(number),
+ Title: github.Ptr("issue"),
+ State: github.Ptr("open"),
+ RepositoryURL: github.Ptr("https://api.github.com/repos/" + owner + "/" + repo),
+ User: &github.User{Login: github.Ptr("u")},
+ }
+ }
+
+ type repoFixture struct {
+ owner string
+ repo string
+ isPrivate bool
+ repoStatus int
+ }
+
+ repoHandlers := func(repos []repoFixture) map[string]http.HandlerFunc {
+ repoByPath := map[string]repoFixture{}
+ for _, r := range repos {
+ repoByPath["/repos/"+r.owner+"/"+r.repo] = r
+ }
+ return map[string]http.HandlerFunc{
+ GetReposByOwnerByRepo: func(w http.ResponseWriter, req *http.Request) {
+ r, ok := repoByPath[req.URL.Path]
+ if !ok {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ if r.repoStatus != 0 && r.repoStatus != http.StatusOK {
+ w.WriteHeader(r.repoStatus)
+ return
+ }
+ body, _ := json.Marshal(map[string]any{
+ "name": r.repo,
+ "private": r.isPrivate,
+ })
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(body)
+ },
+ }
+ }
+
+ makeMockClient := func(searchResult *github.IssuesSearchResult, repos []repoFixture) *http.Client {
+ handlers := repoHandlers(repos)
+ handlers[GetSearchIssues] = mockResponse(t, http.StatusOK, searchResult)
+ return MockHTTPClientWithHandlers(handlers)
+ }
+
+ reqParams := map[string]any{"query": "bug"}
+
+ t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
+ searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "public-repo", 1)}}
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{{owner: "octocat", repo: "public-repo"}})),
+ Flags: FeatureFlags{InsidersMode: false},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+ assert.Nil(t, result.Meta)
+ })
+
+ t.Run("insiders mode all public emits public untrusted", func(t *testing.T) {
+ searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "public-repo", 1)}}
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{{owner: "octocat", repo: "public-repo"}})),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
+ })
+
+ t.Run("insiders mode mixed public and private emits private untrusted", func(t *testing.T) {
+ searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{
+ makeIssue("octocat", "private-repo", 1),
+ makeIssue("octocat", "public-repo", 2),
+ }}
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{
+ {owner: "octocat", repo: "private-repo", isPrivate: true},
+ {owner: "octocat", repo: "public-repo"},
+ })),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "private", ifcMap["confidentiality"])
+ })
+
+ t.Run("insiders mode skips ifc label when visibility lookup fails", func(t *testing.T) {
+ searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "broken", 1)}}
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{
+ {owner: "octocat", repo: "broken", repoStatus: http.StatusInternalServerError},
+ })),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError, "tool call should still succeed when visibility lookup fails")
+
+ if result.Meta != nil {
+ _, hasIFC := result.Meta["ifc"]
+ assert.False(t, hasIFC, "ifc label should be omitted when visibility lookup fails")
+ }
+ })
+
+ t.Run("insiders mode empty results emits public untrusted", func(t *testing.T) {
+ searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{}}
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(searchResult, nil)),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
+ })
+}
+
+func unmarshalIFC(t *testing.T, ifcLabel any) map[string]any {
+ t.Helper()
+ require.NotNil(t, ifcLabel, "ifc label should be present")
+ ifcJSON, err := json.Marshal(ifcLabel)
+ require.NoError(t, err)
+ var ifcMap map[string]any
+ require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
+ return ifcMap
+}
+
+func Test_SearchIssues_FieldValuesEnrichment(t *testing.T) {
+ serverTool := SearchIssues(translations.NullTranslationHelper)
+
+ mockSearchResult := &github.IssuesSearchResult{
+ Total: github.Ptr(2),
+ IncompleteResults: github.Ptr(false),
+ Issues: []*github.Issue{
+ {
+ Number: github.Ptr(42),
+ Title: github.Ptr("Bug: Something is broken"),
+ State: github.Ptr("open"),
+ HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
+ NodeID: github.Ptr("I_node_42"),
+ User: &github.User{Login: github.Ptr("user1")},
+ },
+ {
+ Number: github.Ptr(43),
+ Title: github.Ptr("Feature request"),
+ State: github.Ptr("open"),
+ HTMLURL: github.Ptr("https://github.com/owner/repo/issues/43"),
+ NodeID: github.Ptr("I_node_43"),
+ User: &github.User{Login: github.Ptr("user2")},
+ },
+ },
+ }
+
+ restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ GetSearchIssues: mockResponse(t, http.StatusOK, mockSearchResult),
+ })
+
+ gqlVars := map[string]any{
+ "ids": []any{"I_node_42", "I_node_43"},
+ }
+ gqlResponse := githubv4mock.DataResponse(map[string]any{
+ "nodes": []map[string]any{
+ {
+ "id": "I_node_42",
+ "issueFieldValues": map[string]any{
+ "nodes": []map[string]any{
+ {
+ "__typename": "IssueFieldSingleSelectValue",
+ "field": map[string]any{"name": "priority"},
+ "value": "P1",
+ },
+ {
+ "__typename": "IssueFieldNumberValue",
+ "field": map[string]any{"name": "estimate"},
+ "valueNumber": 2.5,
+ },
+ },
+ },
+ },
+ {
+ "id": "I_node_43",
+ "issueFieldValues": map[string]any{
+ "nodes": []map[string]any{},
+ },
+ },
+ },
+ })
+
+ const nodesQueryString = "query($ids:[ID!]!){nodes(ids: $ids){... on Issue{id,issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name},... on IssueFieldNumber{name},... on IssueFieldSingleSelect{name},... on IssueFieldText{name}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name},... on IssueFieldNumber{name},... on IssueFieldSingleSelect{name},... on IssueFieldText{name}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name},... on IssueFieldNumber{name},... on IssueFieldSingleSelect{name},... on IssueFieldText{name}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name},... on IssueFieldNumber{name},... on IssueFieldSingleSelect{name},... on IssueFieldText{name}},value}}}}}}"
+ matcher := githubv4mock.NewQueryMatcher(nodesQueryString, gqlVars, gqlResponse)
+ gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
+
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, restClient),
+ GQLClient: gqlClient,
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(map[string]any{
+ "query": "repo:owner/repo is:open",
+ })
+
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError, "expected result to not be an error")
+
+ textContent := getTextResult(t, result)
+
+ var response SearchIssuesResponse
+ require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
+ require.Equal(t, 2, *response.Total)
+ require.Len(t, response.Items, 2)
+ assert.Equal(t, 42, *response.Items[0].Number)
+ assert.Equal(t, []MinimalIssueFieldValue{
+ {Field: "priority", Value: "P1"},
+ {Field: "estimate", Value: "2.5"},
+ }, response.Items[0].FieldValues)
+ assert.Equal(t, 43, *response.Items[1].Number)
+ assert.Empty(t, response.Items[1].FieldValues)
+}
+
func Test_CreateIssue(t *testing.T) {
// Verify tool definition once
serverTool := IssueWrite(translations.NullTranslationHelper)
@@ -808,7 +1184,7 @@ func Test_CreateIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
gqlClient := githubv4.NewClient(nil)
deps := BaseDeps{
Client: client,
@@ -862,7 +1238,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
serverTool := IssueWrite(translations.NullTranslationHelper)
- client := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesByOwnerByRepo: mockResponse(t, http.StatusCreated, mockIssue),
}))
@@ -944,7 +1320,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
})
completedReason := IssueClosedStateReasonCompleted
- closeClient := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ closeClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
}))
closeGQLClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(
@@ -1495,24 +1871,13 @@ func Test_ListIssues_IFC_InsidersMode(t *testing.T) {
require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
assert.Equal(t, "untrusted", ifcMap["integrity"])
- confList, ok := ifcMap["confidentiality"].([]any)
- require.True(t, ok, "confidentiality should be a list")
- require.Len(t, confList, 1)
- assert.Equal(t, "public", confList[0])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
})
- t.Run("insiders mode enabled on private repo emits private untrusted label with collaborators", func(t *testing.T) {
+ t.Run("insiders mode enabled on private repo emits private untrusted label", func(t *testing.T) {
matcher := githubv4mock.NewQueryMatcher(query, vars, makeResponse(true))
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
- restClient := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
- GetReposCollaboratorsByOwnerByRepo: mockResponse(t, http.StatusOK, []*github.User{
- {Login: github.Ptr("octocat")},
- {Login: github.Ptr("alice")},
- {Login: github.Ptr("bob")},
- }),
- }))
deps := BaseDeps{
- Client: restClient,
GQLClient: gqlClient,
Flags: FeatureFlags{InsidersMode: true},
}
@@ -1533,36 +1898,7 @@ func Test_ListIssues_IFC_InsidersMode(t *testing.T) {
require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
assert.Equal(t, "untrusted", ifcMap["integrity"])
- confList, ok := ifcMap["confidentiality"].([]any)
- require.True(t, ok, "confidentiality should be a list")
- assert.Equal(t, []any{"octocat", "alice", "bob"}, confList)
- })
-
- t.Run("insiders mode enabled on private repo falls back to owner when collaborators lookup fails", func(t *testing.T) {
- matcher := githubv4mock.NewQueryMatcher(query, vars, makeResponse(true))
- gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
- restClient := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
- GetReposCollaboratorsByOwnerByRepo: mockResponse(t, http.StatusInternalServerError, "boom"),
- }))
- deps := BaseDeps{
- Client: restClient,
- GQLClient: gqlClient,
- Flags: FeatureFlags{InsidersMode: true},
- }
- handler := serverTool.Handler(deps)
-
- request := createMCPRequest(reqParams)
- result, err := handler(ContextWithDeps(context.Background(), deps), &request)
- require.NoError(t, err)
- require.False(t, result.IsError)
-
- require.NotNil(t, result.Meta)
- ifcJSON, err := json.Marshal(result.Meta["ifc"])
- require.NoError(t, err)
- var ifcMap map[string]any
- require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
-
- assert.Equal(t, []any{"octocat"}, ifcMap["confidentiality"])
+ assert.Equal(t, "private", ifcMap["confidentiality"])
})
}
@@ -1997,7 +2333,7 @@ func Test_UpdateIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup clients with mocks
- restClient := github.NewClient(tc.mockedRESTClient)
+ restClient := mustNewGHClient(t, tc.mockedRESTClient)
gqlClient := githubv4.NewClient(tc.mockedGQLClient)
deps := BaseDeps{
Client: restClient,
@@ -2223,7 +2559,7 @@ func Test_GetIssueComments(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
var restClient *github.Client
if tc.lockdownEnabled {
restClient = mockRESTPermissionServer(t, "read", map[string]string{
@@ -2352,7 +2688,7 @@ func Test_GetIssueLabels(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
gqlClient := githubv4.NewClient(tc.mockedClient)
- client := github.NewClient(nil)
+ client := mustNewGHClient(t, nil)
deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
@@ -2559,7 +2895,7 @@ func Test_AddSubIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -2780,7 +3116,7 @@ func Test_GetSubIssues(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
gqlClient := githubv4.NewClient(nil)
deps := BaseDeps{
Client: client,
@@ -2999,7 +3335,7 @@ func Test_RemoveSubIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3259,7 +3595,7 @@ func Test_ReprioritizeSubIssue(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3375,7 +3711,7 @@ func Test_ListIssueTypes(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go
index e22fdae513..89d8a0199e 100644
--- a/pkg/github/minimal_types.go
+++ b/pkg/github/minimal_types.go
@@ -4,7 +4,7 @@ import (
"strconv"
"time"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/github/github-mcp-server/pkg/sanitize"
)
@@ -52,6 +52,31 @@ type MinimalSearchRepositoriesResult struct {
Items []MinimalRepository `json:"items"`
}
+// MinimalDiscussionComment is the trimmed output type for discussion comment objects.
+type MinimalDiscussionComment struct {
+ ID string `json:"id"`
+ Body string `json:"body"`
+ IsAnswer bool `json:"isAnswer,omitempty"`
+ Replies []MinimalDiscussionComment `json:"replies,omitempty"`
+ ReplyTotalCount int `json:"replyTotalCount,omitempty"`
+}
+
+// MinimalCodeSearchResult is the trimmed output type for code search results.
+type MinimalCodeSearchResult struct {
+ TotalCount int `json:"total_count"`
+ IncompleteResults bool `json:"incomplete_results"`
+ Items []MinimalCodeResult `json:"items"`
+}
+
+// MinimalCodeResult is the trimmed output type for a single code search hit.
+type MinimalCodeResult struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ SHA string `json:"sha"`
+ Repository string `json:"repository"`
+ TextMatches []*github.TextMatch `json:"text_matches,omitempty"`
+}
+
// MinimalCommitAuthor represents commit author information.
type MinimalCommitAuthor struct {
Name string `json:"name,omitempty"`
@@ -139,6 +164,13 @@ type MinimalResponse struct {
URL string `json:"url"`
}
+// MinimalCollaborator is the trimmed output type for repository collaborators.
+type MinimalCollaborator struct {
+ Login string `json:"login"`
+ ID int64 `json:"id"`
+ RoleName string `json:"role_name"`
+}
+
type MinimalProject struct {
ID *int64 `json:"id,omitempty"`
NodeID *string `json:"node_id,omitempty"`
diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go
index ddd3023932..61d8f40b2e 100644
--- a/pkg/github/notifications.go
+++ b/pkg/github/notifications.go
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
- "strconv"
"time"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
@@ -14,7 +13,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@@ -209,13 +208,7 @@ func DismissNotification(t translations.TranslationHelperFunc) inventory.ServerT
var resp *github.Response
switch state {
case "done":
- // for some inexplicable reason, the API seems to have threadID as int64 and string depending on the endpoint
- var threadIDInt int64
- threadIDInt, err = strconv.ParseInt(threadID, 10, 64)
- if err != nil {
- return utils.NewToolResultError(fmt.Sprintf("invalid threadID format: %v", err)), nil, nil
- }
- resp, err = client.Activity.MarkThreadDone(ctx, threadIDInt)
+ resp, err = client.Activity.MarkThreadDone(ctx, threadID)
case "read":
resp, err = client.Activity.MarkThreadRead(ctx, threadID)
default:
diff --git a/pkg/github/notifications_test.go b/pkg/github/notifications_test.go
index 030367d067..bcfc28abc2 100644
--- a/pkg/github/notifications_test.go
+++ b/pkg/github/notifications_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -108,7 +108,7 @@ func Test_ListNotifications(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -232,7 +232,7 @@ func Test_ManageNotificationSubscription(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -386,7 +386,7 @@ func Test_ManageRepositoryNotificationSubscription(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -456,7 +456,6 @@ func Test_DismissNotification(t *testing.T) {
expectError bool
expectRead bool
expectDone bool
- expectInvalid bool
expectedErrMsg string
}{
{
@@ -495,16 +494,6 @@ func Test_DismissNotification(t *testing.T) {
expectError: false,
expectDone: true,
},
- {
- name: "invalid threadID format",
- mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
- requestArgs: map[string]any{
- "threadID": "notanumber",
- "state": "done",
- },
- expectError: false,
- expectInvalid: true,
- },
{
name: "missing required threadID",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
@@ -534,7 +523,7 @@ func Test_DismissNotification(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -552,8 +541,6 @@ func Test_DismissNotification(t *testing.T) {
assert.Contains(t, text, "missing required parameter: threadID")
case tc.requestArgs["state"] == nil:
assert.Contains(t, text, "missing required parameter: state")
- case tc.name == "invalid threadID format":
- assert.Contains(t, text, "invalid threadID format")
case tc.name == "invalid state value":
assert.Contains(t, text, "Invalid state. Must be one of: read, done.")
default:
@@ -571,9 +558,6 @@ func Test_DismissNotification(t *testing.T) {
if tc.expectDone {
assert.Contains(t, textContent.Text, "Notification marked as done")
}
- if tc.expectInvalid {
- assert.Contains(t, textContent.Text, "invalid threadID format")
- }
})
}
}
@@ -647,7 +631,7 @@ func Test_MarkAllNotificationsRead(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -725,7 +709,7 @@ func Test_GetNotificationDetails(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/params.go b/pkg/github/params.go
index 1b45d61bd8..ecdc8c3549 100644
--- a/pkg/github/params.go
+++ b/pkg/github/params.go
@@ -6,7 +6,7 @@ import (
"math"
"strconv"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
)
diff --git a/pkg/github/params_test.go b/pkg/github/params_test.go
index 2254b737eb..b00efeb10c 100644
--- a/pkg/github/params_test.go
+++ b/pkg/github/params_test.go
@@ -5,7 +5,7 @@ import (
"math"
"testing"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/stretchr/testify/assert"
)
diff --git a/pkg/github/projects.go b/pkg/github/projects.go
index dcb9193eca..a5953f3be5 100644
--- a/pkg/github/projects.go
+++ b/pkg/github/projects.go
@@ -13,7 +13,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
@@ -618,16 +618,11 @@ func listProjects(ctx context.Context, client *github.Client, args map[string]an
var resp *github.Response
var projects []*github.ProjectV2
- var queryPtr *string
-
- if queryStr != "" {
- queryPtr = &queryStr
- }
minimalProjects := []MinimalProject{}
opts := &github.ListProjectsOptions{
ListProjectsPaginationOptions: pagination,
- Query: queryPtr,
+ Query: queryStr,
}
// If owner_type not provided, fetch from both user and org
@@ -801,17 +796,12 @@ func listProjectItems(ctx context.Context, client *github.Client, args map[strin
var resp *github.Response
var projectItems []*github.ProjectV2Item
- var queryPtr *string
-
- if queryStr != "" {
- queryPtr = &queryStr
- }
opts := &github.ListProjectItemsOptions{
Fields: fields,
ListProjectsOptions: github.ListProjectsOptions{
ListProjectsPaginationOptions: pagination,
- Query: queryPtr,
+ Query: queryStr,
},
}
@@ -1387,16 +1377,9 @@ func extractPaginationOptionsFromArgs(args map[string]any) (github.ListProjectsP
}
opts := github.ListProjectsPaginationOptions{
- PerPage: &perPage,
- }
-
- // Only set After/Before if they have non-empty values
- if after != "" {
- opts.After = &after
- }
-
- if before != "" {
- opts.Before = &before
+ PerPage: perPage,
+ After: after,
+ Before: before,
}
return opts, nil
diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go
index 9b0e07292f..512506476c 100644
--- a/pkg/github/projects_test.go
+++ b/pkg/github/projects_test.go
@@ -9,7 +9,6 @@ import (
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- gh "github.com/google/go-github/v82/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -100,7 +99,7 @@ func Test_ProjectsList_ListProjects(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := gh.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -140,7 +139,7 @@ func Test_ProjectsList_ListProjectFields(t *testing.T) {
GetOrgsProjectsV2FieldsByProject: mockResponse(t, http.StatusOK, fields),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -167,7 +166,7 @@ func Test_ProjectsList_ListProjectFields(t *testing.T) {
t.Run("missing project_number", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -196,7 +195,7 @@ func Test_ProjectsList_ListProjectItems(t *testing.T) {
GetOrgsProjectsV2ItemsByProject: mockResponse(t, http.StatusOK, items),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -249,7 +248,7 @@ func Test_ProjectsGet_GetProject(t *testing.T) {
GetOrgsProjectsV2ByProject: mockResponse(t, http.StatusOK, project),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -274,7 +273,7 @@ func Test_ProjectsGet_GetProject(t *testing.T) {
t.Run("unknown method", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -304,7 +303,7 @@ func Test_ProjectsGet_GetProjectField(t *testing.T) {
GetOrgsProjectsV2FieldsByProjectByFieldID: mockResponse(t, http.StatusOK, field),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -330,7 +329,7 @@ func Test_ProjectsGet_GetProjectField(t *testing.T) {
t.Run("missing field_id", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -360,7 +359,7 @@ func Test_ProjectsGet_GetProjectItem(t *testing.T) {
GetOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusOK, item),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -386,7 +385,7 @@ func Test_ProjectsGet_GetProjectItem(t *testing.T) {
t.Run("missing item_id", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -711,7 +710,7 @@ func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
PatchOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusOK, updatedItem),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -741,7 +740,7 @@ func Test_ProjectsWrite_UpdateProjectItem(t *testing.T) {
t.Run("missing updated_field", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -772,7 +771,7 @@ func Test_ProjectsWrite_DeleteProjectItem(t *testing.T) {
}),
})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -795,7 +794,7 @@ func Test_ProjectsWrite_DeleteProjectItem(t *testing.T) {
t.Run("missing item_id", func(t *testing.T) {
mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{})
- client := gh.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -864,7 +863,7 @@ func Test_ProjectsList_ListProjectStatusUpdates(t *testing.T) {
gqlClient := githubv4.NewClient(gqlMockedClient)
deps := BaseDeps{
- Client: gh.NewClient(restClient),
+ Client: mustNewGHClient(t, restClient),
GQLClient: gqlClient,
}
handler := toolDef.Handler(deps)
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index 0065b25a92..3653c906ba 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -8,7 +8,7 @@ import (
"net/http"
"github.com/go-viper/mapstructure/v2"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
diff --git a/pkg/github/pullrequests_granular.go b/pkg/github/pullrequests_granular.go
index 4a616f1b25..30d7f78d62 100644
--- a/pkg/github/pullrequests_granular.go
+++ b/pkg/github/pullrequests_granular.go
@@ -12,7 +12,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go
index 36a0207cc0..29339ee7db 100644
--- a/pkg/github/pullrequests_test.go
+++ b/pkg/github/pullrequests_test.go
@@ -10,7 +10,7 @@ import (
"github.com/github/github-mcp-server/internal/githubv4mock"
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -95,7 +95,7 @@ func Test_GetPullRequest(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient())
deps := BaseDeps{
Client: client,
@@ -327,7 +327,7 @@ func Test_UpdatePullRequest(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
gqlClient := githubv4.NewClient(nil)
deps := BaseDeps{
Client: client,
@@ -511,7 +511,7 @@ func Test_UpdatePullRequest_Draft(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// For draft-only tests, we need to mock both GraphQL and the final REST GET call
- restClient := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ restClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockUpdatedPR),
}))
gqlClient := githubv4.NewClient(tc.mockedClient)
@@ -641,7 +641,7 @@ func Test_ListPullRequests(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := ListPullRequests(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -759,7 +759,7 @@ func Test_MergePullRequest(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := MergePullRequest(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -1038,7 +1038,7 @@ func Test_SearchPullRequests(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := SearchPullRequests(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -1197,7 +1197,7 @@ func Test_GetPullRequestFiles(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -1357,7 +1357,7 @@ func Test_GetPullRequestStatus(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -1513,7 +1513,7 @@ func Test_GetPullRequestCheckRuns(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -1641,7 +1641,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := UpdatePullRequestBranch(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -1949,7 +1949,7 @@ func Test_GetPullRequestComments(t *testing.T) {
flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
- Client: github.NewClient(nil),
+ Client: mustNewGHClient(t, nil),
GQLClient: gqlClient,
RepoAccessCache: cache,
Flags: flags,
@@ -2133,7 +2133,7 @@ func Test_GetPullRequestReviews(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
var restClient *github.Client
if tc.lockdownEnabled {
restClient = mockRESTPermissionServer(t, "read", map[string]string{
@@ -2300,7 +2300,7 @@ func Test_CreatePullRequest(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := CreatePullRequest(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -2356,7 +2356,7 @@ func Test_CreatePullRequest_InsidersMode_UIGate(t *testing.T) {
serverTool := CreatePullRequest(translations.NullTranslationHelper)
- client := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR),
}))
@@ -3372,7 +3372,7 @@ index 5d6e7b2..8a4f5c3 100644
t.Parallel()
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := PullRequestRead(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -3609,7 +3609,7 @@ func TestAddReplyToPullRequestComment(t *testing.T) {
t.Parallel()
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
serverTool := AddReplyToPullRequestComment(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go
index 507677ee57..2ca1cf3a7a 100644
--- a/pkg/github/repositories.go
+++ b/pkg/github/repositories.go
@@ -16,7 +16,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@@ -654,34 +654,6 @@ func CreateRepository(t translations.TranslationHelperFunc) inventory.ServerTool
)
}
-// FetchRepoCollaborators returns the login names of all collaborators on a
-// repository. It is provided as a shared helper for IFC label computation so
-// tools can populate the reader set for private repositories. The full list
-// is fetched eagerly via pagination; callers are expected to invoke this only
-// when needed (e.g. private repos under InsidersMode).
-func FetchRepoCollaborators(ctx context.Context, client *github.Client, owner, repo string) ([]string, error) {
- opts := &github.ListCollaboratorsOptions{
- ListOptions: github.ListOptions{PerPage: 100},
- }
- var logins []string
- for {
- page, resp, err := client.Repositories.ListCollaborators(ctx, owner, repo, opts)
- if err != nil {
- return nil, err
- }
- for _, c := range page {
- if login := c.GetLogin(); login != "" {
- logins = append(logins, login)
- }
- }
- if resp == nil || resp.NextPage == 0 {
- break
- }
- opts.Page = resp.NextPage
- }
- return logins, nil
-}
-
// FetchRepoIsPrivate returns whether a repository is private. It is a thin
// wrapper around the GitHub Repositories.Get endpoint provided as a shared
// helper for IFC label computation across tools.
@@ -769,17 +741,15 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
}
// attachIFC adds the IFC label to a successful tool result when
- // InsidersMode is enabled. The visibility and (for private
- // repositories) collaborators lookups are performed lazily on
- // first use. If the visibility lookup fails we skip the label
- // rather than misclassify the result; the failure is not cached
- // so a later return path can retry. If only the collaborators
- // lookup fails for a private repo we fall back to the owner so
- // the reader set is never empty.
+ // InsidersMode is enabled. The visibility lookup is performed
+ // lazily on first use and cached because GetFileContents has
+ // many possible return paths and would otherwise re-fetch on
+ // each. If the visibility lookup fails we skip the label rather
+ // than misclassify the result; the failure is not cached so a
+ // later return path can retry.
var (
ifcLabelKnown bool
ifcIsPrivate bool
- ifcReaders []string
)
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
if r == nil || r.IsError || !deps.GetFlags(ctx).InsidersMode {
@@ -791,20 +761,12 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
return r
}
ifcIsPrivate = isPrivate
- if ifcIsPrivate {
- if collaborators, err := FetchRepoCollaborators(ctx, client, owner, repo); err == nil {
- ifcReaders = collaborators
- }
- if len(ifcReaders) == 0 {
- ifcReaders = []string{owner}
- }
- }
ifcLabelKnown = true
}
if r.Meta == nil {
r.Meta = mcp.Meta{}
}
- r.Meta["ifc"] = ifc.LabelGetFileContents(ifcIsPrivate, ifcReaders)
+ r.Meta["ifc"] = ifc.LabelGetFileContents(ifcIsPrivate)
return r
}
@@ -2240,3 +2202,111 @@ func UnstarRepository(t translations.TranslationHelperFunc) inventory.ServerTool
},
)
}
+
+// ListRepositoryCollaborators creates a tool to list collaborators of a GitHub repository.
+func ListRepositoryCollaborators(t translations.TranslationHelperFunc) inventory.ServerTool {
+ return NewTool(
+ ToolsetMetadataRepos,
+ mcp.Tool{
+ Name: "list_repository_collaborators",
+ Description: t("TOOL_LIST_REPOSITORY_COLLABORATORS_DESCRIPTION", "List collaborators of a GitHub repository. Results are paginated; the response includes `nextPage`, `prevPage`, `firstPage`, and `lastPage` fields. To get the next page, use the `nextPage` value as the `page` parameter."),
+ Annotations: &mcp.ToolAnnotations{
+ Title: t("TOOL_LIST_REPOSITORY_COLLABORATORS_USER_TITLE", "List repository collaborators"),
+ ReadOnlyHint: true,
+ },
+ InputSchema: func() *jsonschema.Schema {
+ schema := WithPagination(&jsonschema.Schema{
+ Type: "object",
+ Properties: map[string]*jsonschema.Schema{
+ "owner": {
+ Type: "string",
+ Description: "Repository owner",
+ },
+ "repo": {
+ Type: "string",
+ Description: "Repository name",
+ },
+ "affiliation": {
+ Type: "string",
+ Description: "Filter by affiliation. Can be one of: 'outside' (outside collaborators), 'direct' (all with permissions regardless of org membership), 'all' (all collaborators). Default: 'all'",
+ Enum: []any{"outside", "direct", "all"},
+ },
+ },
+ Required: []string{"owner", "repo"},
+ })
+ schema.Properties["page"].Description = "Page number for pagination (default 1, min 1)"
+ schema.Properties["perPage"].Description = "Results per page for pagination (default 30, min 1, max 100)"
+ return schema
+ }(),
+ },
+ []scopes.Scope{scopes.Repo},
+ func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, 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
+ }
+ affiliation, err := OptionalParam[string](args, "affiliation")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+ pagination, err := OptionalPaginationParams(args)
+ 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)
+ }
+
+ opts := &github.ListCollaboratorsOptions{
+ Affiliation: affiliation,
+ ListOptions: github.ListOptions{
+ Page: pagination.Page,
+ PerPage: pagination.PerPage,
+ },
+ }
+
+ collaborators, resp, err := client.Repositories.ListCollaborators(ctx, owner, repo, opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to list collaborators",
+ resp,
+ err,
+ ), nil, nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list collaborators", resp, body), nil, nil
+ }
+
+ result := make([]MinimalCollaborator, 0, len(collaborators))
+ for _, c := range collaborators {
+ result = append(result, MinimalCollaborator{
+ Login: c.GetLogin(),
+ ID: c.GetID(),
+ RoleName: c.GetRoleName(),
+ })
+ }
+
+ response := map[string]any{
+ "items": result,
+ "nextPage": resp.NextPage,
+ "prevPage": resp.PrevPage,
+ "firstPage": resp.FirstPage,
+ "lastPage": resp.LastPage,
+ }
+
+ return MarshalledTextResult(response), nil, nil
+ },
+ )
+}
diff --git a/pkg/github/repositories_helper.go b/pkg/github/repositories_helper.go
index a347ebdd6c..be377f773e 100644
--- a/pkg/github/repositories_helper.go
+++ b/pkg/github/repositories_helper.go
@@ -10,7 +10,7 @@ import (
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go
index ceaa959019..a44bad65b6 100644
--- a/pkg/github/repositories_test.go
+++ b/pkg/github/repositories_test.go
@@ -14,7 +14,7 @@ import (
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
@@ -412,8 +412,9 @@ func Test_GetFileContents(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
- mockRawClient := raw.NewClient(client, &url.URL{Scheme: "https", Host: "raw.example.com", Path: "/"})
+ client := mustNewGHClient(t, tc.mockedClient)
+ mockRawClient, err := raw.NewClient(client, &url.URL{Scheme: "https", Host: "raw.example.com", Path: "/"})
+ require.NoError(t, err)
deps := BaseDeps{
Client: client,
RawClient: mockRawClient,
@@ -492,10 +493,6 @@ func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {
"default_branch": "main",
"private": isPrivate,
}),
- GetReposCollaboratorsByOwnerByRepo: mockResponse(t, http.StatusOK, []*github.User{
- {Login: github.Ptr("octocat")},
- {Login: github.Ptr("alice")},
- }),
GetReposContentsByOwnerByRepoByPath: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
encodedContent := base64.StdEncoding.EncodeToString(mockRawContent)
@@ -523,7 +520,7 @@ func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {
t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) {
deps := BaseDeps{
- Client: github.NewClient(makeMockClient(false)),
+ Client: mustNewGHClient(t, makeMockClient(false)),
Flags: FeatureFlags{InsidersMode: false},
}
handler := serverTool.Handler(deps)
@@ -538,7 +535,7 @@ func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {
t.Run("insiders mode enabled on public repo emits public untrusted label", func(t *testing.T) {
deps := BaseDeps{
- Client: github.NewClient(makeMockClient(false)),
+ Client: mustNewGHClient(t, makeMockClient(false)),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)
@@ -558,15 +555,12 @@ func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {
require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
assert.Equal(t, "untrusted", ifcMap["integrity"])
- confList, ok := ifcMap["confidentiality"].([]any)
- require.True(t, ok, "confidentiality should be a list")
- require.Len(t, confList, 1)
- assert.Equal(t, "public", confList[0])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
})
t.Run("insiders mode enabled on private repo emits private trusted label", func(t *testing.T) {
deps := BaseDeps{
- Client: github.NewClient(makeMockClient(true)),
+ Client: mustNewGHClient(t, makeMockClient(true)),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)
@@ -586,9 +580,7 @@ func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {
require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
assert.Equal(t, "trusted", ifcMap["integrity"])
- confList, ok := ifcMap["confidentiality"].([]any)
- require.True(t, ok, "confidentiality should be a list")
- assert.Equal(t, []any{"octocat", "alice"}, confList)
+ assert.Equal(t, "private", ifcMap["confidentiality"])
})
t.Run("insiders mode skips ifc label when visibility lookup fails", func(t *testing.T) {
@@ -612,7 +604,7 @@ func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {
},
})
deps := BaseDeps{
- Client: github.NewClient(mockedClient),
+ Client: mustNewGHClient(t, mockedClient),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)
@@ -699,7 +691,7 @@ func Test_ForkRepository(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -871,7 +863,7 @@ func Test_CreateBranch(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -997,7 +989,7 @@ func Test_GetCommit(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -1288,7 +1280,7 @@ func Test_ListCommits(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -1645,7 +1637,7 @@ func Test_CreateOrUpdateFile(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -1834,7 +1826,7 @@ func Test_CreateRepository(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -2572,7 +2564,7 @@ func Test_PushFiles(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -2693,7 +2685,7 @@ func Test_ListBranches(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock client
- mockClient := github.NewClient(NewMockedHTTPClient(tt.mockResponses...))
+ mockClient := mustNewGHClient(t, NewMockedHTTPClient(tt.mockResponses...))
deps := BaseDeps{
Client: mockClient,
}
@@ -2881,7 +2873,7 @@ func Test_DeleteFile(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3008,7 +3000,7 @@ func Test_ListTags(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3199,7 +3191,7 @@ func Test_GetTag(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3325,7 +3317,7 @@ func Test_ListReleases(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3351,6 +3343,7 @@ func Test_ListReleases(t *testing.T) {
})
}
}
+
func Test_GetLatestRelease(t *testing.T) {
serverTool := GetLatestRelease(translations.NullTranslationHelper)
tool := serverTool.Tool
@@ -3416,7 +3409,7 @@ func Test_GetLatestRelease(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -3564,7 +3557,7 @@ func Test_GetReleaseByTag(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -4009,7 +4002,7 @@ func Test_resolveGitReference(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockSetup())
+ client := mustNewGHClient(t, tc.mockSetup())
opts, _, err := resolveGitReference(ctx, client, owner, repo, tc.ref, tc.sha)
if tc.expectError {
@@ -4155,7 +4148,7 @@ func Test_ListStarredRepositories(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -4256,7 +4249,7 @@ func Test_StarRepository(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -4347,7 +4340,7 @@ func Test_UnstarRepository(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -4376,3 +4369,149 @@ func Test_UnstarRepository(t *testing.T) {
})
}
}
+
+func Test_ListRepositoryCollaborators(t *testing.T) {
+ // Verify tool definition once
+ serverTool := ListRepositoryCollaborators(translations.NullTranslationHelper)
+ tool := serverTool.Tool
+ require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+ schema, ok := tool.InputSchema.(*jsonschema.Schema)
+ require.True(t, ok, "InputSchema should be *jsonschema.Schema")
+
+ assert.Equal(t, "list_repository_collaborators", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.True(t, tool.Annotations.ReadOnlyHint)
+ assert.Contains(t, schema.Properties, "owner")
+ assert.Contains(t, schema.Properties, "repo")
+ assert.Contains(t, schema.Properties, "affiliation")
+ assert.Contains(t, schema.Properties, "page")
+ assert.Contains(t, schema.Properties, "perPage")
+ assert.ElementsMatch(t, schema.Required, []string{"owner", "repo"})
+
+ mockCollaborators := []*github.User{
+ {
+ Login: github.Ptr("user1"),
+ ID: github.Ptr(int64(101)),
+ RoleName: github.Ptr("admin"),
+ },
+ {
+ Login: github.Ptr("user2"),
+ ID: github.Ptr(int64(102)),
+ RoleName: github.Ptr("write"),
+ },
+ }
+
+ tests := []struct {
+ name string
+ args map[string]any
+ mockResponses []MockBackendOption
+ wantErr bool
+ errContains string
+ }{
+ {
+ name: "success",
+ args: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ },
+ mockResponses: []MockBackendOption{
+ WithRequestMatch(
+ ListCollaborators,
+ mockCollaborators,
+ ),
+ },
+ },
+ {
+ name: "success with affiliation filter",
+ args: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ "affiliation": "direct",
+ },
+ mockResponses: []MockBackendOption{
+ WithRequestMatch(
+ ListCollaborators,
+ mockCollaborators,
+ ),
+ },
+ },
+ {
+ name: "missing owner",
+ args: map[string]any{
+ "repo": "repo",
+ },
+ mockResponses: []MockBackendOption{},
+ errContains: "missing required parameter: owner",
+ },
+ {
+ name: "missing repo",
+ args: map[string]any{
+ "owner": "owner",
+ },
+ mockResponses: []MockBackendOption{},
+ errContains: "missing required parameter: repo",
+ },
+ {
+ name: "empty collaborators returns empty array",
+ args: map[string]any{
+ "owner": "owner",
+ "repo": "repo",
+ },
+ mockResponses: []MockBackendOption{
+ WithRequestMatch(
+ ListCollaborators,
+ []*github.User{},
+ ),
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mockClient := mustNewGHClient(t, NewMockedHTTPClient(tt.mockResponses...))
+ deps := BaseDeps{
+ Client: mockClient,
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(tt.args)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.NotNil(t, result)
+
+ if tt.errContains != "" {
+ textContent := getTextResult(t, result)
+ assert.Contains(t, textContent.Text, tt.errContains)
+ return
+ }
+
+ textContent := getTextResult(t, result)
+ require.NotEmpty(t, textContent.Text)
+
+ var response struct {
+ Items []MinimalCollaborator `json:"items"`
+ NextPage int `json:"nextPage"`
+ PrevPage int `json:"prevPage"`
+ FirstPage int `json:"firstPage"`
+ LastPage int `json:"lastPage"`
+ }
+ err = json.Unmarshal([]byte(textContent.Text), &response)
+ require.NoError(t, err)
+
+ if tt.name == "empty collaborators returns empty array" {
+ assert.Empty(t, response.Items)
+ return
+ }
+
+ collaborators := response.Items
+ assert.Len(t, collaborators, 2)
+ assert.Equal(t, "user1", collaborators[0].Login)
+ assert.Equal(t, int64(101), collaborators[0].ID)
+ assert.Equal(t, "admin", collaborators[0].RoleName)
+ assert.Equal(t, "user2", collaborators[1].Login)
+ assert.Equal(t, int64(102), collaborators[1].ID)
+ assert.Equal(t, "write", collaborators[1].RoleName)
+ })
+ }
+}
diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go
index be86cc4519..3ab4cf3906 100644
--- a/pkg/github/repository_resource.go
+++ b/pkg/github/repository_resource.go
@@ -17,7 +17,7 @@ import (
"github.com/github/github-mcp-server/pkg/octicons"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/yosida95/uritemplate/v3"
)
diff --git a/pkg/github/repository_resource_completions.go b/pkg/github/repository_resource_completions.go
index ff9e23398a..18e7eb5f01 100644
--- a/pkg/github/repository_resource_completions.go
+++ b/pkg/github/repository_resource_completions.go
@@ -6,7 +6,7 @@ import (
"fmt"
"strings"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/repository_resource_completions_test.go b/pkg/github/repository_resource_completions_test.go
index e5f1a35f93..33df2761e6 100644
--- a/pkg/github/repository_resource_completions_test.go
+++ b/pkg/github/repository_resource_completions_test.go
@@ -6,7 +6,7 @@ import (
"fmt"
"testing"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go
index f0fba30dfb..cb57bae545 100644
--- a/pkg/github/repository_resource_test.go
+++ b/pkg/github/repository_resource_test.go
@@ -8,7 +8,6 @@ import (
"testing"
"github.com/github/github-mcp-server/pkg/raw"
- "github.com/google/go-github/v82/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
)
@@ -246,8 +245,9 @@ func Test_repositoryResourceContents(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
- mockRawClient := raw.NewClient(client, base)
+ client := mustNewGHClient(t, tc.mockedClient)
+ mockRawClient, err := raw.NewClient(client, base)
+ require.NoError(t, err)
deps := BaseDeps{
Client: client,
RawClient: mockRawClient,
@@ -290,8 +290,9 @@ func Test_repositoryResourceContentsHandler_NetworkError(t *testing.T) {
networkErr := errors.New("network error: connection refused")
httpClient := &http.Client{Transport: &errorTransport{err: networkErr}}
- client := github.NewClient(httpClient)
- mockRawClient := raw.NewClient(client, base)
+ client := mustNewGHClient(t, httpClient)
+ mockRawClient, err := raw.NewClient(client, base)
+ require.NoError(t, err)
deps := BaseDeps{
Client: client,
RawClient: mockRawClient,
diff --git a/pkg/github/search.go b/pkg/github/search.go
index d5ddb4a72a..a4acc44489 100644
--- a/pkg/github/search.go
+++ b/pkg/github/search.go
@@ -8,11 +8,12 @@ import (
"net/http"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
+ "github.com/github/github-mcp-server/pkg/ifc"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@@ -161,11 +162,37 @@ func SearchRepositories(t translations.TranslationHelperFunc) inventory.ServerTo
}
}
- return utils.NewToolResultText(string(r)), nil, nil
+ callResult := utils.NewToolResultText(string(r))
+ if deps.GetFlags(ctx).InsidersMode {
+ attachSearchRepositoriesIFCLabel(result.Repositories, callResult)
+ }
+ return callResult, nil, nil
},
)
}
+// attachSearchRepositoriesIFCLabel joins per-repository IFC labels across
+// every matched repository and attaches the result to callResult. Visibility
+// is read directly from the search response — no extra API call. The join
+// math is shared with search_issues via ifc.LabelSearchIssues: integrity is
+// always untrusted; confidentiality is private if any matched repository is
+// private, otherwise public.
+func attachSearchRepositoriesIFCLabel(repos []*github.Repository, callResult *mcp.CallToolResult) {
+ if callResult == nil || callResult.IsError {
+ return
+ }
+
+ visibilities := make([]bool, 0, len(repos))
+ for _, repo := range repos {
+ visibilities = append(visibilities, repo.GetPrivate())
+ }
+
+ if callResult.Meta == nil {
+ callResult.Meta = mcp.Meta{}
+ }
+ callResult.Meta["ifc"] = ifc.LabelSearchIssues(visibilities)
+}
+
// SearchCode creates a tool to search for code across GitHub repositories.
func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
@@ -220,8 +247,9 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
}
opts := &github.SearchOptions{
- Sort: sort,
- Order: order,
+ Sort: sort,
+ Order: order,
+ TextMatch: true,
ListOptions: github.ListOptions{
PerPage: pagination.PerPage,
Page: pagination.Page,
@@ -251,7 +279,27 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to search code", resp, body), nil, nil
}
- r, err := json.Marshal(result)
+ minimalItems := make([]MinimalCodeResult, 0, len(result.CodeResults))
+ for _, code := range result.CodeResults {
+ item := MinimalCodeResult{
+ Name: code.GetName(),
+ Path: code.GetPath(),
+ SHA: code.GetSHA(),
+ TextMatches: code.TextMatches,
+ }
+ if code.Repository != nil {
+ item.Repository = code.Repository.GetFullName()
+ }
+ minimalItems = append(minimalItems, item)
+ }
+
+ minimalResult := &MinimalCodeSearchResult{
+ TotalCount: result.GetTotal(),
+ IncompleteResults: result.GetIncompleteResults(),
+ Items: minimalItems,
+ }
+
+ r, err := json.Marshal(minimalResult)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}
diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go
index 85eb21bcb5..74a3ca52fc 100644
--- a/pkg/github/search_test.go
+++ b/pkg/github/search_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -123,7 +123,7 @@ func Test_SearchRepositories(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -163,9 +163,119 @@ func Test_SearchRepositories(t *testing.T) {
assert.Equal(t, *tc.expectedResult.Repositories[i].FullName, repo.FullName)
assert.Equal(t, *tc.expectedResult.Repositories[i].HTMLURL, repo.HTMLURL)
}
+ })
+ }
+}
+
+func Test_SearchRepositories_IFC_InsidersMode(t *testing.T) {
+ t.Parallel()
+
+ serverTool := SearchRepositories(translations.NullTranslationHelper)
+
+ type repoFixture struct {
+ owner string
+ name string
+ isPrivate bool
+ }
+ makeRepo := func(r repoFixture) *github.Repository {
+ return &github.Repository{
+ ID: github.Ptr(int64(1)),
+ Name: github.Ptr(r.name),
+ FullName: github.Ptr(r.owner + "/" + r.name),
+ Private: github.Ptr(r.isPrivate),
+ Owner: &github.User{Login: github.Ptr(r.owner)},
+ }
+ }
+
+ makeMockClient := func(repos []repoFixture) *http.Client {
+ searchResult := &github.RepositoriesSearchResult{
+ Total: github.Ptr(len(repos)),
+ IncompleteResults: github.Ptr(false),
+ }
+ for _, r := range repos {
+ searchResult.Repositories = append(searchResult.Repositories, makeRepo(r))
+ }
+ return MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
+ GetSearchRepositories: mockResponse(t, http.StatusOK, searchResult),
})
}
+
+ reqParams := map[string]any{"query": "octocat"}
+
+ t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient([]repoFixture{{owner: "octocat", name: "public-repo"}})),
+ Flags: FeatureFlags{InsidersMode: false},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+ assert.Nil(t, result.Meta)
+ })
+
+ t.Run("insiders mode all public emits public untrusted", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient([]repoFixture{
+ {owner: "octocat", name: "public-a"},
+ {owner: "octocat", name: "public-b"},
+ })),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
+ })
+
+ t.Run("insiders mode any private match emits private untrusted", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient([]repoFixture{
+ {owner: "octocat", name: "private-repo", isPrivate: true},
+ {owner: "octocat", name: "public-repo"},
+ })),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "private", ifcMap["confidentiality"])
+ })
+
+ t.Run("insiders mode empty results emits public untrusted", func(t *testing.T) {
+ deps := BaseDeps{
+ Client: mustNewGHClient(t, makeMockClient(nil)),
+ Flags: FeatureFlags{InsidersMode: true},
+ }
+ handler := serverTool.Handler(deps)
+
+ request := createMCPRequest(reqParams)
+ result, err := handler(ContextWithDeps(context.Background(), deps), &request)
+ require.NoError(t, err)
+ require.False(t, result.IsError)
+
+ require.NotNil(t, result.Meta)
+ ifcMap := unmarshalIFC(t, result.Meta["ifc"])
+ assert.Equal(t, "untrusted", ifcMap["integrity"])
+ assert.Equal(t, "public", ifcMap["confidentiality"])
+ })
}
func Test_SearchRepositories_FullOutput(t *testing.T) {
@@ -194,7 +304,7 @@ func Test_SearchRepositories_FullOutput(t *testing.T) {
),
})
- client := github.NewClient(mockedClient)
+ client := mustNewGHClient(t, mockedClient)
serverTool := SearchRepositories(translations.NullTranslationHelper)
deps := BaseDeps{
Client: client,
@@ -252,22 +362,35 @@ func Test_SearchCode(t *testing.T) {
IncompleteResults: github.Ptr(false),
CodeResults: []*github.CodeResult{
{
- Name: github.Ptr("file1.go"),
- Path: github.Ptr("path/to/file1.go"),
- SHA: github.Ptr("abc123def456"),
- HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/path/to/file1.go"),
- Repository: &github.Repository{Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo")},
+ Name: github.Ptr("file1.go"),
+ Path: github.Ptr("path/to/file1.go"),
+ SHA: github.Ptr("abc123def456"),
+ Repository: &github.Repository{
+ Name: github.Ptr("repo"),
+ FullName: github.Ptr("owner/repo"),
+ },
+ TextMatches: []*github.TextMatch{
+ {
+ Fragment: github.Ptr("func main() { fmt.Println(\"hello\") }"),
+ },
+ },
},
{
- Name: github.Ptr("file2.go"),
- Path: github.Ptr("path/to/file2.go"),
- SHA: github.Ptr("def456abc123"),
- HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/path/to/file2.go"),
- Repository: &github.Repository{Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo")},
+ Name: github.Ptr("file2.go"),
+ Path: github.Ptr("path/to/file2.go"),
+ SHA: github.Ptr("def456abc123"),
+ Repository: &github.Repository{
+ Name: github.Ptr("repo"),
+ FullName: github.Ptr("owner/repo"),
+ },
},
},
}
+ textMatchAcceptHeader := map[string]string{
+ "Accept": "text-match",
+ }
+
tests := []struct {
name string
mockedClient *http.Client
@@ -285,7 +408,7 @@ func Test_SearchCode(t *testing.T) {
"order": "desc",
"page": "1",
"per_page": "30",
- }).andThen(
+ }).withHeaders(textMatchAcceptHeader).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
}),
@@ -306,7 +429,7 @@ func Test_SearchCode(t *testing.T) {
"q": "fmt.Println language:go",
"page": "1",
"per_page": "30",
- }).andThen(
+ }).withHeaders(textMatchAcceptHeader).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
}),
@@ -335,7 +458,7 @@ func Test_SearchCode(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -359,22 +482,28 @@ func Test_SearchCode(t *testing.T) {
require.NoError(t, err)
require.False(t, result.IsError)
- // Parse the result and get the text content if no error
textContent := getTextResult(t, result)
- // Unmarshal and verify the result
- var returnedResult github.CodeSearchResult
+ var returnedResult MinimalCodeSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
- assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
- assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
- assert.Len(t, returnedResult.CodeResults, len(tc.expectedResult.CodeResults))
- for i, code := range returnedResult.CodeResults {
- assert.Equal(t, *tc.expectedResult.CodeResults[i].Name, *code.Name)
- assert.Equal(t, *tc.expectedResult.CodeResults[i].Path, *code.Path)
- assert.Equal(t, *tc.expectedResult.CodeResults[i].SHA, *code.SHA)
- assert.Equal(t, *tc.expectedResult.CodeResults[i].HTMLURL, *code.HTMLURL)
- assert.Equal(t, *tc.expectedResult.CodeResults[i].Repository.FullName, *code.Repository.FullName)
+ assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
+ assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
+ assert.Len(t, returnedResult.Items, len(tc.expectedResult.CodeResults))
+ for i, code := range returnedResult.Items {
+ assert.Equal(t, tc.expectedResult.CodeResults[i].GetName(), code.Name)
+ assert.Equal(t, tc.expectedResult.CodeResults[i].GetPath(), code.Path)
+ assert.Equal(t, tc.expectedResult.CodeResults[i].GetSHA(), code.SHA)
+ assert.Equal(t, tc.expectedResult.CodeResults[i].Repository.GetFullName(), code.Repository)
+ }
+
+ // Verify text matches are included when present
+ if len(tc.expectedResult.CodeResults[0].TextMatches) > 0 {
+ require.NotEmpty(t, returnedResult.Items[0].TextMatches)
+ assert.Equal(t,
+ tc.expectedResult.CodeResults[0].TextMatches[0].GetFragment(),
+ returnedResult.Items[0].TextMatches[0].GetFragment(),
+ )
}
})
}
@@ -520,7 +649,7 @@ func Test_SearchUsers(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -683,7 +812,7 @@ func Test_SearchOrgs(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go
index c5502f6308..287ea13e57 100644
--- a/pkg/github/search_utils.go
+++ b/pkg/github/search_utils.go
@@ -10,7 +10,7 @@ import (
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@@ -37,16 +37,24 @@ func hasTypeFilter(query string) bool {
return hasFilter(query, "type")
}
-func searchHandler(
- ctx context.Context,
- getClient GetClientFn,
- args map[string]any,
- searchType string,
- errorPrefix string,
-) (*mcp.CallToolResult, error) {
+// searchPostProcessFn is invoked after a successful search response, before
+// the call result is returned. It may attach additional metadata (such as IFC
+// labels) to the call result based on the search payload.
+type searchPostProcessFn func(ctx context.Context, result *github.IssuesSearchResult, callResult *mcp.CallToolResult)
+
+type searchConfig struct {
+ postProcess searchPostProcessFn
+}
+
+type searchOption func(*searchConfig)
+
+// prepareSearchArgs resolves the search query string and REST search options from the tool args,
+// applying the standard is: / repo:/ query transformations shared by
+// searchIssues and searchPullRequests.
+func prepareSearchArgs(args map[string]any, searchType string) (string, *github.SearchOptions, error) {
query, err := RequiredParam[string](args, "query")
if err != nil {
- return utils.NewToolResultError(err.Error()), nil
+ return "", nil, err
}
if !hasSpecificFilter(query, "is", searchType) {
@@ -55,12 +63,12 @@ func searchHandler(
owner, err := OptionalParam[string](args, "owner")
if err != nil {
- return utils.NewToolResultError(err.Error()), nil
+ return "", nil, err
}
repo, err := OptionalParam[string](args, "repo")
if err != nil {
- return utils.NewToolResultError(err.Error()), nil
+ return "", nil, err
}
if owner != "" && repo != "" && !hasRepoFilter(query) {
@@ -69,18 +77,18 @@ func searchHandler(
sort, err := OptionalParam[string](args, "sort")
if err != nil {
- return utils.NewToolResultError(err.Error()), nil
+ return "", nil, err
}
order, err := OptionalParam[string](args, "order")
if err != nil {
- return utils.NewToolResultError(err.Error()), nil
+ return "", nil, err
}
pagination, err := OptionalPaginationParams(args)
if err != nil {
- return utils.NewToolResultError(err.Error()), nil
+ return "", nil, err
}
- opts := &github.SearchOptions{
+ return query, &github.SearchOptions{
// Default to "created" if no sort is provided, as it's a common use case.
Sort: sort,
Order: order,
@@ -88,6 +96,25 @@ func searchHandler(
Page: pagination.Page,
PerPage: pagination.PerPage,
},
+ }, nil
+}
+
+func searchHandler(
+ ctx context.Context,
+ getClient GetClientFn,
+ args map[string]any,
+ searchType string,
+ errorPrefix string,
+ options ...searchOption,
+) (*mcp.CallToolResult, error) {
+ cfg := searchConfig{}
+ for _, opt := range options {
+ opt(&cfg)
+ }
+
+ query, opts, err := prepareSearchArgs(args, searchType)
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil
}
client, err := getClient(ctx)
@@ -113,5 +140,9 @@ func searchHandler(
return utils.NewToolResultErrorFromErr(errorPrefix+": failed to marshal response", err), nil
}
- return utils.NewToolResultText(string(r)), nil
+ callResult := utils.NewToolResultText(string(r))
+ if cfg.postProcess != nil {
+ cfg.postProcess(ctx, result, callResult)
+ }
+ return callResult, nil
}
diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go
index 676c2c1625..5cbe52c42a 100644
--- a/pkg/github/secret_scanning.go
+++ b/pkg/github/secret_scanning.go
@@ -12,7 +12,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go
index 7c53de35c5..1aa451e053 100644
--- a/pkg/github/secret_scanning_test.go
+++ b/pkg/github/secret_scanning_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -79,7 +79,7 @@ func Test_GetSecretScanningAlert(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
@@ -211,7 +211,7 @@ func Test_ListSecretScanningAlerts(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
diff --git a/pkg/github/security_advisories.go b/pkg/github/security_advisories.go
index e86e220eaf..ec84e27b15 100644
--- a/pkg/github/security_advisories.go
+++ b/pkg/github/security_advisories.go
@@ -12,7 +12,7 @@ import (
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
diff --git a/pkg/github/security_advisories_test.go b/pkg/github/security_advisories_test.go
index 3d4df43e63..f45c2e4210 100644
--- a/pkg/github/security_advisories_test.go
+++ b/pkg/github/security_advisories_test.go
@@ -8,7 +8,7 @@ import (
"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -92,7 +92,7 @@ func Test_ListGlobalSecurityAdvisories(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
handler := toolDef.Handler(deps)
@@ -204,7 +204,7 @@ func Test_GetGlobalSecurityAdvisory(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
handler := toolDef.Handler(deps)
@@ -337,7 +337,7 @@ func Test_ListRepositorySecurityAdvisories(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
handler := toolDef.Handler(deps)
@@ -467,7 +467,7 @@ func Test_ListOrgRepositorySecurityAdvisories(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- client := github.NewClient(tc.mockedClient)
+ client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
handler := toolDef.Handler(deps)
diff --git a/pkg/github/server.go b/pkg/github/server.go
index ee41e90e9e..a9a75642f8 100644
--- a/pkg/github/server.go
+++ b/pkg/github/server.go
@@ -204,6 +204,9 @@ func NewServer(version, name, title string, opts *mcp.ServerOptions) *mcp.Server
func CompletionsHandler(getClient GetClientFn) func(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
return func(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
+ if req == nil || req.Params == nil || req.Params.Ref == nil {
+ return nil, fmt.Errorf("missing required parameter: ref")
+ }
switch req.Params.Ref.Type {
case "ref/resource":
if strings.HasPrefix(req.Params.Ref.URI, "repo://") {
diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go
index 264ffa50fe..be078d3603 100644
--- a/pkg/github/server_test.go
+++ b/pkg/github/server_test.go
@@ -16,7 +16,7 @@ import (
"github.com/github/github-mcp-server/pkg/observability/metrics"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -80,9 +80,10 @@ func stubExporters() observability.Exporters {
return obs
}
-func stubClientFnFromHTTP(httpClient *http.Client) func(context.Context) (*gogithub.Client, error) {
+func stubClientFnFromHTTP(t *testing.T, httpClient *http.Client) func(context.Context) (*gogithub.Client, error) {
+ t.Helper()
return func(_ context.Context) (*gogithub.Client, error) {
- return gogithub.NewClient(httpClient), nil
+ return mustNewGHClient(t, httpClient), nil
}
}
@@ -110,7 +111,7 @@ func stubRepoAccessCache(restClient *gogithub.Client, ttl time.Duration) *lockdo
func mockRESTPermissionServer(t *testing.T, defaultPerm string, overrides map[string]string) *gogithub.Client {
t.Helper()
- return gogithub.NewClient(MockHTTPClientWithHandler(func(w http.ResponseWriter, r *http.Request) {
+ return mustNewGHClient(t, MockHTTPClientWithHandler(func(w http.ResponseWriter, r *http.Request) {
perm := defaultPerm
for user, p := range overrides {
if strings.Contains(r.URL.Path, "/collaborators/"+user+"/") {
@@ -348,3 +349,27 @@ func TestResolveEnabledToolsets(t *testing.T) {
})
}
}
+
+func TestCompletionsHandler_RejectsMissingRef(t *testing.T) {
+ getClient := func(_ context.Context) (*gogithub.Client, error) {
+ return &gogithub.Client{}, nil
+ }
+ handler := CompletionsHandler(getClient)
+
+ tests := []struct {
+ name string
+ req *mcp.CompleteRequest
+ }{
+ {name: "nil request", req: nil},
+ {name: "nil params", req: &mcp.CompleteRequest{}},
+ {name: "nil ref", req: &mcp.CompleteRequest{Params: &mcp.CompleteParams{}}},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ result, err := handler(context.Background(), tc.req)
+ require.Error(t, err)
+ assert.Nil(t, result)
+ assert.Contains(t, err.Error(), "missing required parameter: ref")
+ })
+ }
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 559088f6d6..f4c653bf8d 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -7,7 +7,7 @@ import (
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/shurcooL/githubv4"
)
@@ -199,6 +199,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
ListStarredRepositories(t),
StarRepository(t),
UnstarRepository(t),
+ ListRepositoryCollaborators(t),
// Git tools
GetRepositoryTree(t),
@@ -258,6 +259,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
ListDiscussions(t),
GetDiscussion(t),
GetDiscussionComments(t),
+ DiscussionCommentWrite(t),
ListDiscussionCategories(t),
// Actions tools
diff --git a/pkg/ifc/ifc.go b/pkg/ifc/ifc.go
index c0926d8a39..e6eeb407bc 100644
--- a/pkg/ifc/ifc.go
+++ b/pkg/ifc/ifc.go
@@ -13,19 +13,20 @@ const (
type Confidentiality string
const (
- ConfidentialityPublic Confidentiality = "public"
+ ConfidentialityPublic Confidentiality = "public"
+ ConfidentialityPrivate Confidentiality = "private"
)
type SecurityLabel struct {
- Integrity Integrity `json:"integrity"`
- Confidentiality []Confidentiality `json:"confidentiality"`
+ Integrity Integrity `json:"integrity"`
+ Confidentiality Confidentiality `json:"confidentiality"`
}
// PublicTrusted returns a label for trusted, publicly readable data.
func PublicTrusted() SecurityLabel {
return SecurityLabel{
Integrity: IntegrityTrusted,
- Confidentiality: []Confidentiality{ConfidentialityPublic},
+ Confidentiality: ConfidentialityPublic,
}
}
@@ -33,45 +34,42 @@ func PublicTrusted() SecurityLabel {
func PublicUntrusted() SecurityLabel {
return SecurityLabel{
Integrity: IntegrityUntrusted,
- Confidentiality: []Confidentiality{ConfidentialityPublic},
+ Confidentiality: ConfidentialityPublic,
}
}
-// PrivateTrusted returns a label for trusted data restricted to the given readers.
-func PrivateTrusted(readers []string) SecurityLabel {
+// PrivateTrusted returns a label for trusted data restricted to the readers
+// of the originating repository. The reader set is opaque on the wire (a
+// single "private" marker); the client engine resolves the concrete readers
+// from the GitHub API on demand at egress decision time.
+func PrivateTrusted() SecurityLabel {
return SecurityLabel{
Integrity: IntegrityTrusted,
- Confidentiality: toConfidentiality(readers),
+ Confidentiality: ConfidentialityPrivate,
}
}
-// PrivateUntrusted returns a label for untrusted data restricted to the given readers.
-func PrivateUntrusted(readers []string) SecurityLabel {
+// PrivateUntrusted returns a label for untrusted data restricted to the
+// readers of the originating repository. See PrivateTrusted for the reader
+// resolution model.
+func PrivateUntrusted() SecurityLabel {
return SecurityLabel{
Integrity: IntegrityUntrusted,
- Confidentiality: toConfidentiality(readers),
+ Confidentiality: ConfidentialityPrivate,
}
}
-func toConfidentiality(readers []string) []Confidentiality {
- out := make([]Confidentiality, len(readers))
- for i, r := range readers {
- out[i] = Confidentiality(r)
- }
- return out
-}
-
func LabelGetMe() SecurityLabel {
return PublicTrusted()
}
// LabelListIssues returns the IFC label for a list_issues result.
// Public repositories are universally readable; private repositories are
-// restricted to the provided reader set (typically repository collaborators).
+// restricted to their collaborators (resolved client-side from the marker).
// Issue contents are attacker-controllable, so integrity is always untrusted.
-func LabelListIssues(isPrivate bool, readers []string) SecurityLabel {
+func LabelListIssues(isPrivate bool) SecurityLabel {
if isPrivate {
- return PrivateUntrusted(readers)
+ return PrivateUntrusted()
}
return PublicUntrusted()
}
@@ -80,9 +78,31 @@ func LabelListIssues(isPrivate bool, readers []string) SecurityLabel {
// Public repository file contents may be authored by anyone via pull requests
// and are therefore untrusted. In private repositories only collaborators can
// land changes, so contents are treated as trusted.
-func LabelGetFileContents(isPrivate bool, readers []string) SecurityLabel {
+func LabelGetFileContents(isPrivate bool) SecurityLabel {
if isPrivate {
- return PrivateTrusted(readers)
+ return PrivateTrusted()
+ }
+ return PublicUntrusted()
+}
+
+// LabelSearchIssues returns the IFC label for a multi-repository search
+// result, joining per-repository labels across all matched repositories.
+// Used by both search_issues and search_repositories.
+//
+// Integrity is always untrusted because results expose user-authored content.
+//
+// Confidentiality follows the IFC meet (greatest lower bound): if any matched
+// repository is private the joined label is private; otherwise public. The
+// reader set is opaque (the "private" marker); the client engine resolves
+// concrete readers on demand at egress decision time.
+//
+// An empty result set is treated as public-untrusted (no repository data is
+// leaked).
+func LabelSearchIssues(repoVisibilities []bool) SecurityLabel {
+ for _, isPrivate := range repoVisibilities {
+ if isPrivate {
+ return PrivateUntrusted()
+ }
}
return PublicUntrusted()
}
diff --git a/pkg/ifc/ifc_test.go b/pkg/ifc/ifc_test.go
new file mode 100644
index 0000000000..669f5ff0cc
--- /dev/null
+++ b/pkg/ifc/ifc_test.go
@@ -0,0 +1,51 @@
+package ifc
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLabelSearchIssues(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ visibilities []bool
+ wantConfidential Confidentiality
+ }{
+ {
+ name: "empty result is treated as public",
+ wantConfidential: ConfidentialityPublic,
+ },
+ {
+ name: "single public repo",
+ visibilities: []bool{false},
+ wantConfidential: ConfidentialityPublic,
+ },
+ {
+ name: "all public repos stay public",
+ visibilities: []bool{false, false, false},
+ wantConfidential: ConfidentialityPublic,
+ },
+ {
+ name: "any private match flips to private",
+ visibilities: []bool{false, true, false},
+ wantConfidential: ConfidentialityPrivate,
+ },
+ {
+ name: "all private repos stay private",
+ visibilities: []bool{true, true},
+ wantConfidential: ConfidentialityPrivate,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ label := LabelSearchIssues(tc.visibilities)
+ assert.Equal(t, IntegrityUntrusted, label.Integrity)
+ assert.Equal(t, tc.wantConfidential, label.Confidentiality)
+ })
+ }
+}
diff --git a/pkg/lockdown/lockdown.go b/pkg/lockdown/lockdown.go
index 6edb4469d9..f787875b2e 100644
--- a/pkg/lockdown/lockdown.go
+++ b/pkg/lockdown/lockdown.go
@@ -8,7 +8,7 @@ import (
"sync"
"time"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/muesli/cache2go"
"github.com/shurcooL/githubv4"
)
diff --git a/pkg/lockdown/lockdown_test.go b/pkg/lockdown/lockdown_test.go
index 55e755a3ec..bb8887e709 100644
--- a/pkg/lockdown/lockdown_test.go
+++ b/pkg/lockdown/lockdown_test.go
@@ -4,13 +4,12 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
- "net/url"
"sync"
"testing"
"time"
"github.com/github/github-mcp-server/internal/githubv4mock"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/require"
)
@@ -81,8 +80,8 @@ func newMockRepoAccessCache(t *testing.T, ttl time.Duration) (*RepoAccessCache,
_ = json.NewEncoder(w).Encode(resp)
}))
t.Cleanup(restServer.Close)
- restClient := gogithub.NewClient(nil)
- restClient.BaseURL, _ = url.Parse(restServer.URL + "/")
+ restClient, err := gogithub.NewClient(gogithub.WithEnterpriseURLs(restServer.URL+"/", restServer.URL+"/"))
+ require.NoError(t, err)
return NewRepoAccessCache(gqlClient, restClient, WithTTL(ttl)), counting
}
diff --git a/pkg/raw/raw.go b/pkg/raw/raw.go
index df9cd0ad11..4f794ac1f6 100644
--- a/pkg/raw/raw.go
+++ b/pkg/raw/raw.go
@@ -6,7 +6,7 @@ import (
"net/http"
"net/url"
- gogithub "github.com/google/go-github/v82/github"
+ gogithub "github.com/google/go-github/v87/github"
)
// GetRawClientFn is a function type that returns a RawClient instance.
@@ -19,19 +19,19 @@ type Client struct {
}
// NewClient creates a new instance of the raw API Client with the provided GitHub client and provided URL.
-func NewClient(client *gogithub.Client, rawURL *url.URL) *Client {
- client = gogithub.NewClient(client.Client())
- client.BaseURL = rawURL
- return &Client{client: client, url: rawURL}
-}
-
-func (c *Client) newRequest(ctx context.Context, method string, urlStr string, body any, opts ...gogithub.RequestOption) (*http.Request, error) {
- req, err := c.client.NewRequest(method, urlStr, body, opts...)
+func NewClient(client *gogithub.Client, rawURL *url.URL) (*Client, error) {
+ newClient, err := gogithub.NewClient(
+ gogithub.WithHTTPClient(client.Client()),
+ gogithub.WithEnterpriseURLs(rawURL.String(), rawURL.String()),
+ )
if err != nil {
return nil, err
}
- req = req.WithContext(ctx)
- return req, nil
+ return &Client{client: newClient, url: rawURL}, nil
+}
+
+func (c *Client) newRequest(ctx context.Context, method string, urlStr string, body any, opts ...gogithub.RequestOption) (*http.Request, error) {
+ return c.client.NewRequest(ctx, method, urlStr, body, opts...)
}
func (c *Client) refURL(owner, repo, ref, path string) string {
diff --git a/pkg/raw/raw_test.go b/pkg/raw/raw_test.go
index 6897f492f6..60137684d7 100644
--- a/pkg/raw/raw_test.go
+++ b/pkg/raw/raw_test.go
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
- "github.com/google/go-github/v82/github"
+ "github.com/google/go-github/v87/github"
"github.com/stretchr/testify/require"
)
@@ -108,8 +108,10 @@ func TestGetRawContent(t *testing.T) {
body: tc.body,
},
}
- ghClient := github.NewClient(mockedClient)
- client := NewClient(ghClient, base)
+ ghClient, err := github.NewClient(github.WithHTTPClient(mockedClient))
+ require.NoError(t, err)
+ client, err := NewClient(ghClient, base)
+ require.NoError(t, err)
resp, err := client.GetRawContent(context.Background(), tc.owner, tc.repo, tc.path, tc.opts)
defer func() {
_ = resp.Body.Close()
@@ -133,8 +135,10 @@ func TestGetRawContent(t *testing.T) {
func TestUrlFromOpts(t *testing.T) {
base, _ := url.Parse("https://raw.example.com/")
- ghClient := github.NewClient(nil)
- client := NewClient(ghClient, base)
+ ghClient, err := github.NewClient(github.WithHTTPClient(&http.Client{}))
+ require.NoError(t, err)
+ client, err := NewClient(ghClient, base)
+ require.NoError(t, err)
tests := []struct {
name string
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 2e5ca59ec2..45b31069cb 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -17,15 +17,15 @@ The following packages are included for the amd64, arm64 architectures.
- [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.2.5/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/v82/github](https://pkg.go.dev/github.com/google/go-github/v82/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v82.0.0/LICENSE))
+ - [github.com/google/go-github/v87/github](https://pkg.go.dev/github.com/google/go-github/v87/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v87.0.0/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.2/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/27f29c1cef3b/LICENSE))
- - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/27f29c1cef3b/LICENSE))
+ - [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.6.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.6.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))
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index d818469896..d7029fb479 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -17,15 +17,15 @@ The following packages are included for the 386, amd64, arm64 architectures.
- [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.2.5/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/v82/github](https://pkg.go.dev/github.com/google/go-github/v82/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v82.0.0/LICENSE))
+ - [github.com/google/go-github/v87/github](https://pkg.go.dev/github.com/google/go-github/v87/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v87.0.0/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.2/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/27f29c1cef3b/LICENSE))
- - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/27f29c1cef3b/LICENSE))
+ - [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.6.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.6.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))
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index 6efed3338c..8d805400a0 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -17,16 +17,16 @@ The following packages are included for the 386, amd64, arm64 architectures.
- [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.2.5/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/v82/github](https://pkg.go.dev/github.com/google/go-github/v82/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v82.0.0/LICENSE))
+ - [github.com/google/go-github/v87/github](https://pkg.go.dev/github.com/google/go-github/v87/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v87.0.0/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.2/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/27f29c1cef3b/LICENSE))
- - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([MIT](https://github.com/modelcontextprotocol/go-sdk/blob/27f29c1cef3b/LICENSE))
+ - [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.6.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.6.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))
diff --git a/third-party/github.com/google/go-github/v82/github/LICENSE b/third-party/github.com/google/go-github/v87/github/LICENSE
similarity index 100%
rename from third-party/github.com/google/go-github/v82/github/LICENSE
rename to third-party/github.com/google/go-github/v87/github/LICENSE
diff --git a/ui/package-lock.json b/ui/package-lock.json
index f5314fb086..13d78a25a8 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@github/markdown-toolbar-element": "^2.2.3",
- "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.7.2",
"@primer/octicons-react": "^19.0.0",
"@primer/react": "^36.0.0",
"react": "^18.0.0",
@@ -21,11 +21,13 @@
"@types/node": "^25.2.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
- "@vitejs/plugin-react": "^4.3.0",
- "cross-env": "^7.0.3",
+ "@vitejs/plugin-react": "^6.0.2",
"typescript": "^5.7.0",
- "vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.0.0"
+ "vite": "^8.0.13",
+ "vite-plugin-singlefile": "^2.3.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@babel/code-frame": {
@@ -33,6 +35,7 @@
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
@@ -47,6 +50,7 @@
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -56,6 +60,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -86,6 +91,7 @@
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
@@ -115,6 +121,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
@@ -131,6 +138,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -140,6 +148,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
@@ -153,6 +162,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
@@ -170,6 +180,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -179,6 +190,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -188,6 +200,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -197,6 +210,7 @@
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -206,6 +220,7 @@
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.28.6"
@@ -219,6 +234,7 @@
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/types": "^7.29.0"
},
@@ -245,38 +261,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
- "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
- "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
@@ -291,6 +275,7 @@
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
@@ -305,6 +290,7 @@
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -323,6 +309,7 @@
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
@@ -331,6 +318,40 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@emotion/is-prop-valid": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
@@ -362,448 +383,6 @@
"license": "MIT",
"peer": true
},
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@github/combobox-nav": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.1.tgz",
@@ -835,9 +414,9 @@
"license": "MIT"
},
"node_modules/@hono/node-server": {
- "version": "1.19.9",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
- "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
+ "version": "1.19.14",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
+ "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
"license": "MIT",
"peer": true,
"engines": {
@@ -852,6 +431,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -862,6 +442,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -872,6 +453,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.0.0"
}
@@ -880,13 +462,15 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -905,35 +489,21 @@
"license": "BSD-3-Clause"
},
"node_modules/@modelcontextprotocol/ext-apps": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz",
- "integrity": "sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ==",
- "hasInstallScript": true,
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.2.tgz",
+ "integrity": "sha512-OOWKDxdAjYDcgHkmzVzccyyag3FK+jBWPaWu4WvTxFsU4R/cgOX4eep66zPRA5n4v6WfxUNibPyvX4iJ7egYTg==",
"license": "MIT",
"workspaces": [
"examples/*"
],
- "optionalDependencies": {
- "@oven/bun-darwin-aarch64": "^1.2.21",
- "@oven/bun-darwin-x64": "^1.2.21",
- "@oven/bun-darwin-x64-baseline": "^1.2.21",
- "@oven/bun-linux-aarch64": "^1.2.21",
- "@oven/bun-linux-aarch64-musl": "^1.2.21",
- "@oven/bun-linux-x64": "^1.2.21",
- "@oven/bun-linux-x64-baseline": "^1.2.21",
- "@oven/bun-linux-x64-musl": "^1.2.21",
- "@oven/bun-linux-x64-musl-baseline": "^1.2.21",
- "@oven/bun-windows-x64": "^1.2.21",
- "@oven/bun-windows-x64-baseline": "^1.2.21",
- "@rollup/rollup-darwin-arm64": "^4.53.3",
- "@rollup/rollup-darwin-x64": "^4.53.3",
- "@rollup/rollup-linux-arm64-gnu": "^4.53.3",
- "@rollup/rollup-linux-x64-gnu": "^4.53.3",
- "@rollup/rollup-win32-arm64-msvc": "^4.53.3",
- "@rollup/rollup-win32-x64-msvc": "^4.53.3"
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=20"
},
"peerDependencies": {
- "@modelcontextprotocol/sdk": "^1.24.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
"zod": "^3.25.0 || ^4.0.0"
@@ -948,194 +518,80 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.26.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
- "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+ "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.9",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
- "content-type": "^1.0.5",
- "cors": "^2.8.5",
- "cross-spawn": "^7.0.5",
- "eventsource": "^3.0.2",
- "eventsource-parser": "^3.0.0",
- "express": "^5.2.1",
- "express-rate-limit": "^8.2.1",
- "hono": "^4.11.4",
- "jose": "^6.1.3",
- "json-schema-typed": "^8.0.2",
- "pkce-challenge": "^5.0.0",
- "raw-body": "^3.0.0",
- "zod": "^3.25 || ^4.0",
- "zod-to-json-schema": "^3.25.1"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@cfworker/json-schema": "^4.1.1",
- "zod": "^3.25 || ^4.0"
- },
- "peerDependenciesMeta": {
- "@cfworker/json-schema": {
- "optional": true
- },
- "zod": {
- "optional": false
- }
- }
- },
- "node_modules/@oddbird/popover-polyfill": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@oddbird/popover-polyfill/-/popover-polyfill-0.3.8.tgz",
- "integrity": "sha512-+aK7EHL3VggfsWGVqUwvtli2+kP5OWyseAsrefhzR2XWoi2oALUCeoDn63i5WS3ZOmLiXHRNBwHPeta8w+aM1g==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@oven/bun-darwin-aarch64": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.8.tgz",
- "integrity": "sha512-hPERz4IgXCM6Y6GdEEsJAFceyJMt29f3HlFzsvE/k+TQjChRhar6S+JggL35b9VmFfsdxyCOOTPqgnSrdV0etA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@oven/bun-darwin-x64": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.8.tgz",
- "integrity": "sha512-SaWIxsRQYiT/eA60bqA4l8iNO7cJ6YD8ie82RerRp9voceBxPIZiwX4y20cTKy5qNaSGr9LxfYq7vDywTipiog==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@oven/bun-darwin-x64-baseline": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.8.tgz",
- "integrity": "sha512-ArHVWpCRZI3vGLoN2/8ud8Kzqlgn1Gv+fNw+pMB9x18IzgAEhKxFxsWffnoaH21amam4tAOhpeewRIgdNtB0Cw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@oven/bun-linux-aarch64": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.8.tgz",
- "integrity": "sha512-rq0nNckobtS+ONoB95/Frfqr8jCtmSjjjEZlN4oyUx0KEBV11Vj4v3cDVaWzuI34ryL8FCog3HaqjfKn8R82Tw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@oven/bun-linux-aarch64-musl": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.8.tgz",
- "integrity": "sha512-HvJmhrfipL7GtuqFz6xNpmf27NGcCOMwCalPjNR6fvkLpe8A7Z1+QbxKKjOglelmlmZc3Vi2TgDUtxSqfqOToQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@oven/bun-linux-x64": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.8.tgz",
- "integrity": "sha512-YDgqVx1MI8E0oDbCEUSkAMBKKGnUKfaRtMdLh9Bjhu7JQacQ/ZCpxwi4HPf5Q0O1TbWRrdxGw2tA2Ytxkn7s1Q==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@oven/bun-linux-x64-baseline": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.8.tgz",
- "integrity": "sha512-3IkS3TuVOzMqPW6Gg9/8FEoKF/rpKZ9DZUfNy9GQ54+k4PGcXpptU3+dy8D4iDFCt4qe6bvoiAOdM44OOsZ+Wg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@oven/bun-linux-x64-musl": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.8.tgz",
- "integrity": "sha512-o7Jm5zL4aw9UBs3BcZLVbgGm2V4F10MzAQAV+ziKzoEfYmYtvDqRVxgKEq7BzUOVy4LgfrfwzEXw5gAQGRrhQQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
},
- "node_modules/@oven/bun-linux-x64-musl-baseline": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.8.tgz",
- "integrity": "sha512-5g8XJwHhcTh8SGoKO7pR54ILYDbuFkGo+68DOMTiVB5eLxuLET+Or/camHgk4QWp3nUS5kNjip4G8BE8i0rHVQ==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "linux"
- ]
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
},
- "node_modules/@oven/bun-windows-x64": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.8.tgz",
- "integrity": "sha512-UDI3rowMm/tI6DIynpE4XqrOhr+1Ztk1NG707Wxv2nygup+anTswgCwjfjgmIe78LdoRNFrux2GpeolhQGW6vQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "node_modules/@oddbird/popover-polyfill": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@oddbird/popover-polyfill/-/popover-polyfill-0.3.8.tgz",
+ "integrity": "sha512-+aK7EHL3VggfsWGVqUwvtli2+kP5OWyseAsrefhzR2XWoi2oALUCeoDn63i5WS3ZOmLiXHRNBwHPeta8w+aM1g==",
+ "license": "BSD-3-Clause"
},
- "node_modules/@oven/bun-windows-x64-baseline": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.8.tgz",
- "integrity": "sha512-K6qBUKAZLXsjAwFxGTG87dsWlDjyDl2fqjJr7+x7lmv2m+aSEzmLOK+Z5pSvGkpjBp3LXV35UUgj8G0UTd0pPg==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@oxc-project/types": {
+ "version": "0.130.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
+ "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+ "dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
},
"node_modules/@primer/behaviors": {
"version": "1.10.1",
@@ -1889,102 +1345,359 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/@primer/react/node_modules/unist-util-position": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz",
- "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==",
+ "node_modules/@primer/react/node_modules/unist-util-position": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz",
+ "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@primer/react/node_modules/unist-util-stringify-position": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+ "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@primer/react/node_modules/unist-util-visit": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz",
+ "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0",
+ "unist-util-visit-parents": "^5.1.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@primer/react/node_modules/unist-util-visit-parents": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz",
+ "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@primer/react/node_modules/vfile": {
+ "version": "5.3.7",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz",
+ "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "is-buffer": "^2.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "vfile-message": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@primer/react/node_modules/vfile-message": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz",
+ "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "unist-util-stringify-position": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
+ "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
+ "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
+ "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
+ "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
+ "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
+ "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
+ "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
+ "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@primer/react/node_modules/unist-util-stringify-position": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
- "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
+ "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@primer/react/node_modules/unist-util-visit": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz",
- "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==",
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
+ "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^5.0.0",
- "unist-util-visit-parents": "^5.1.1"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@primer/react/node_modules/unist-util-visit-parents": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz",
- "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==",
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
+ "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^5.0.0"
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@primer/react/node_modules/vfile": {
- "version": "5.3.7",
- "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz",
- "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==",
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
+ "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "is-buffer": "^2.0.0",
- "unist-util-stringify-position": "^3.0.0",
- "vfile-message": "^3.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@primer/react/node_modules/vfile-message": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz",
- "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==",
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
+ "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "unist-util-stringify-position": "^3.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-beta.27",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
- "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
- "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
"cpu": [
"arm"
],
@@ -1993,12 +1706,13 @@
"optional": true,
"os": [
"android"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
- "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
"cpu": [
"arm64"
],
@@ -2007,38 +1721,43 @@
"optional": true,
"os": [
"android"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
- "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
- "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
- "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
"cpu": [
"arm64"
],
@@ -2047,12 +1766,13 @@
"optional": true,
"os": [
"freebsd"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
- "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
"cpu": [
"x64"
],
@@ -2061,12 +1781,13 @@
"optional": true,
"os": [
"freebsd"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
- "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
"cpu": [
"arm"
],
@@ -2075,12 +1796,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
- "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
"cpu": [
"arm"
],
@@ -2089,25 +1811,28 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
- "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
- "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
"cpu": [
"arm64"
],
@@ -2116,12 +1841,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
- "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
"cpu": [
"loong64"
],
@@ -2130,12 +1856,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
- "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
"cpu": [
"loong64"
],
@@ -2144,12 +1871,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
- "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
"cpu": [
"ppc64"
],
@@ -2158,12 +1886,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
- "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
"cpu": [
"ppc64"
],
@@ -2172,12 +1901,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
- "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
"cpu": [
"riscv64"
],
@@ -2186,12 +1916,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
- "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
"cpu": [
"riscv64"
],
@@ -2200,12 +1931,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
- "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
"cpu": [
"s390x"
],
@@ -2214,25 +1946,28 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
- "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
"cpu": [
"x64"
],
@@ -2241,12 +1976,13 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
- "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
"cpu": [
"x64"
],
@@ -2255,12 +1991,13 @@
"optional": true,
"os": [
"openbsd"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
- "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
"cpu": [
"arm64"
],
@@ -2269,25 +2006,28 @@
"optional": true,
"os": [
"openharmony"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
- "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
- "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
"cpu": [
"ia32"
],
@@ -2296,12 +2036,13 @@
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
"cpu": [
"x64"
],
@@ -2310,20 +2051,29 @@
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
- "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "peer": true
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
},
"node_modules/@styled-system/background": {
"version": "5.1.2",
@@ -2458,49 +2208,15 @@
"@styled-system/css": "^5.1.5"
}
},
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
- }
- },
- "node_modules/@types/babel__generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__traverse": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@babel/types": "^7.28.2"
+ "tslib": "^2.4.0"
}
},
"node_modules/@types/debug": {
@@ -2633,24 +2349,29 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
- "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.28.0",
- "@babel/plugin-transform-react-jsx-self": "^7.27.1",
- "@babel/plugin-transform-react-jsx-source": "^7.27.1",
- "@rolldown/pluginutils": "1.0.0-beta.27",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.17.0"
+ "@rolldown/pluginutils": "^1.0.0"
},
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
}
},
"node_modules/accepts": {
@@ -2668,9 +2389,9 @@
}
},
"node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -2734,6 +2455,7 @@
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
@@ -2795,6 +2517,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2878,7 +2601,8 @@
"url": "https://github.com/sponsors/ai"
}
],
- "license": "CC-BY-4.0"
+ "license": "CC-BY-4.0",
+ "peer": true
},
"node_modules/ccount": {
"version": "2.0.1",
@@ -2956,9 +2680,9 @@
}
},
"node_modules/content-disposition": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
- "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2983,7 +2707,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/cookie": {
"version": "0.7.2",
@@ -3023,30 +2748,12 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/cross-env": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
- "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.1"
- },
- "bin": {
- "cross-env": "src/bin/cross-env.js",
- "cross-env-shell": "src/bin/cross-env-shell.js"
- },
- "engines": {
- "node": ">=10.14",
- "npm": ">=6",
- "yarn": ">=1"
- }
- },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -3142,6 +2849,16 @@
"node": ">=6"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -3190,7 +2907,8 @@
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
- "license": "ISC"
+ "license": "ISC",
+ "peer": true
},
"node_modules/encodeurl": {
"version": "2.0.0",
@@ -3235,53 +2953,12 @@
"node": ">= 0.4"
}
},
- "node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
- }
- },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -3339,9 +3016,9 @@
}
},
"node_modules/eventsource-parser": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
- "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
+ "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
"license": "MIT",
"peer": true,
"engines": {
@@ -3393,13 +3070,13 @@
}
},
"node_modules/express-rate-limit": {
- "version": "8.2.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
- "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
+ "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
"license": "MIT",
"peer": true,
"dependencies": {
- "ip-address": "10.0.1"
+ "ip-address": "^10.2.0"
},
"engines": {
"node": ">= 16"
@@ -3425,9 +3102,9 @@
"peer": true
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",
@@ -3538,6 +3215,7 @@
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -3618,9 +3296,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -3697,9 +3375,9 @@
"peer": true
},
"node_modules/hono": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz",
- "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==",
+ "version": "4.12.19",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.19.tgz",
+ "integrity": "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==",
"license": "MIT",
"peer": true,
"engines": {
@@ -3768,9 +3446,9 @@
"license": "MIT"
},
"node_modules/ip-address": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
- "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
+ "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"peer": true,
"engines": {
@@ -3887,12 +3565,13 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
+ "license": "ISC",
+ "peer": true
},
"node_modules/jose": {
- "version": "6.1.3",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
- "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
+ "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"license": "MIT",
"peer": true,
"funding": {
@@ -3910,6 +3589,7 @@
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
+ "peer": true,
"bin": {
"jsesc": "bin/jsesc"
},
@@ -3936,6 +3616,7 @@
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
+ "peer": true,
"bin": {
"json5": "lib/cli.js"
},
@@ -3952,10 +3633,271 @@
"node": ">=6"
}
},
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT",
"peer": true
},
@@ -3998,6 +3940,7 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"license": "ISC",
+ "peer": true,
"dependencies": {
"yallist": "^3.0.2"
}
@@ -5039,7 +4982,8 @@
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/object-assign": {
"version": "4.1.1",
@@ -5126,14 +5070,15 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/path-to-regexp": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
- "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"peer": true,
"funding": {
@@ -5148,9 +5093,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -5170,9 +5115,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
@@ -5247,9 +5192,9 @@
}
},
"node_modules/qs": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
- "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
@@ -5362,16 +5307,6 @@
"react": ">=18"
}
},
- "node_modules/react-refresh": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
- "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -5448,12 +5383,48 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rolldown": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
+ "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.130.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.1",
+ "@rolldown/binding-darwin-arm64": "1.0.1",
+ "@rolldown/binding-darwin-x64": "1.0.1",
+ "@rolldown/binding-freebsd-x64": "1.0.1",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.1",
+ "@rolldown/binding-linux-arm64-musl": "1.0.1",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-musl": "1.0.1",
+ "@rolldown/binding-openharmony-arm64": "1.0.1",
+ "@rolldown/binding-wasm32-wasi": "1.0.1",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.1",
+ "@rolldown/binding-win32-x64-msvc": "1.0.1"
+ }
+ },
"node_modules/rollup": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
- "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
"dev": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -5465,31 +5436,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.57.1",
- "@rollup/rollup-android-arm64": "4.57.1",
- "@rollup/rollup-darwin-arm64": "4.57.1",
- "@rollup/rollup-darwin-x64": "4.57.1",
- "@rollup/rollup-freebsd-arm64": "4.57.1",
- "@rollup/rollup-freebsd-x64": "4.57.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
- "@rollup/rollup-linux-arm64-gnu": "4.57.1",
- "@rollup/rollup-linux-arm64-musl": "4.57.1",
- "@rollup/rollup-linux-loong64-gnu": "4.57.1",
- "@rollup/rollup-linux-loong64-musl": "4.57.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
- "@rollup/rollup-linux-ppc64-musl": "4.57.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
- "@rollup/rollup-linux-riscv64-musl": "4.57.1",
- "@rollup/rollup-linux-s390x-gnu": "4.57.1",
- "@rollup/rollup-linux-x64-gnu": "4.57.1",
- "@rollup/rollup-linux-x64-musl": "4.57.1",
- "@rollup/rollup-openbsd-x64": "4.57.1",
- "@rollup/rollup-openharmony-arm64": "4.57.1",
- "@rollup/rollup-win32-arm64-msvc": "4.57.1",
- "@rollup/rollup-win32-ia32-msvc": "4.57.1",
- "@rollup/rollup-win32-x64-gnu": "4.57.1",
- "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
"fsevents": "~2.3.2"
}
},
@@ -5543,6 +5514,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
+ "peer": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -5613,6 +5585,7 @@
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -5625,6 +5598,7 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -5650,14 +5624,14 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"peer": true,
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -5833,14 +5807,14 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -5868,9 +5842,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5923,19 +5897,45 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/type-is": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
- "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
"license": "MIT",
"peer": true,
"dependencies": {
- "content-type": "^1.0.5",
+ "content-type": "^2.0.0",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/typescript": {
@@ -6085,6 +6085,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@@ -6153,24 +6154,23 @@
}
},
"node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "8.0.13",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
+ "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "^0.25.0",
- "fdir": "^6.4.4",
- "picomatch": "^4.0.2",
- "postcss": "^8.5.3",
- "rollup": "^4.34.9",
- "tinyglobby": "^0.2.13"
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "rolldown": "1.0.1",
+ "tinyglobby": "^0.2.16"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -6179,14 +6179,15 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
@@ -6195,13 +6196,16 @@
"@types/node": {
"optional": true
},
- "jiti": {
+ "@vitejs/devtools": {
"optional": true
},
- "less": {
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
"optional": true
},
- "lightningcss": {
+ "less": {
"optional": true
},
"sass": {
@@ -6228,9 +6232,9 @@
}
},
"node_modules/vite-plugin-singlefile": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz",
- "integrity": "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz",
+ "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6240,32 +6244,19 @@
"node": ">18.0.0"
},
"peerDependencies": {
- "rollup": "^4.44.1",
- "vite": "^5.4.11 || ^6.0.0 || ^7.0.0"
- }
- },
- "node_modules/vite/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
+ "rollup": "^4.59.0",
+ "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
- "picomatch": {
+ "rollup": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6280,6 +6271,7 @@
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
+ "peer": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -6301,12 +6293,13 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "license": "ISC"
+ "license": "ISC",
+ "peer": true
},
"node_modules/zod": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
- "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"peer": true,
"funding": {
@@ -6314,13 +6307,13 @@
}
},
"node_modules/zod-to-json-schema": {
- "version": "3.25.1",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
- "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
"license": "ISC",
"peer": true,
"peerDependencies": {
- "zod": "^3.25 || ^4"
+ "zod": "^3.25.28 || ^4"
}
},
"node_modules/zwitch": {
diff --git a/ui/package.json b/ui/package.json
index 6b26ca3161..b5bf095851 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -4,18 +4,18 @@
"private": true,
"type": "module",
"description": "MCP App UIs for github-mcp-server using Primer React",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
"scripts": {
- "build": "npm run build:get-me && npm run build:issue-write && npm run build:pr-write",
- "build:get-me": "cross-env APP=get-me vite build",
- "build:issue-write": "cross-env APP=issue-write vite build",
- "build:pr-write": "cross-env APP=pr-write vite build",
+ "build": "node scripts/build.mjs",
"dev": "npm run build",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"@github/markdown-toolbar-element": "^2.2.3",
- "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.7.2",
"@primer/octicons-react": "^19.0.0",
"@primer/react": "^36.0.0",
"react": "^18.0.0",
@@ -27,10 +27,9 @@
"@types/node": "^25.2.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
- "@vitejs/plugin-react": "^4.3.0",
- "cross-env": "^7.0.3",
+ "@vitejs/plugin-react": "^6.0.2",
"typescript": "^5.7.0",
- "vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.0.0"
+ "vite": "^8.0.13",
+ "vite-plugin-singlefile": "^2.3.3"
}
}
diff --git a/ui/scripts/build.mjs b/ui/scripts/build.mjs
new file mode 100644
index 0000000000..c99d846039
--- /dev/null
+++ b/ui/scripts/build.mjs
@@ -0,0 +1,14 @@
+// Build all UI apps in a single Node process.
+//
+// Replaces three serial `cross-env APP= vite build` invocations: doing it
+// in one process avoids paying Vite/plugin startup cost three times and is
+// portable without `cross-env`.
+
+import { build } from "vite";
+
+const apps = ["get-me", "issue-write", "pr-write"];
+
+for (const app of apps) {
+ process.env.APP = app;
+ await build();
+}
diff --git a/ui/src/hooks/useMcpApp.ts b/ui/src/hooks/useMcpApp.ts
index 05798f5086..54bfa791a7 100644
--- a/ui/src/hooks/useMcpApp.ts
+++ b/ui/src/hooks/useMcpApp.ts
@@ -30,6 +30,8 @@ export function useMcpApp({
const { app, error } = useExtApp({
appInfo: { name: appName, version: appVersion },
capabilities: {},
+ autoResize: true,
+ strict: import.meta.env.DEV,
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
setToolResult(result);
diff --git a/ui/vite.config.ts b/ui/vite.config.ts
index 5b1777c706..963b39883d 100644
--- a/ui/vite.config.ts
+++ b/ui/vite.config.ts
@@ -1,39 +1,44 @@
import { defineConfig, Plugin } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
+import { existsSync, renameSync, rmSync } from "fs";
import { resolve } from "path";
-// Get the app to build from environment variable
const app = process.env.APP;
if (!app) {
throw new Error("APP environment variable must be set");
}
-// Plugin to rename the output file and remove the nested directory structure
-function renameOutput(): Plugin {
+const outDir = resolve(__dirname, "../pkg/github/ui_dist");
+
+// vite-plugin-singlefile inlines all JS/CSS into the HTML, but Vite preserves
+// the input file's relative path in the output (src/apps//index.html).
+// After the bundle is written, hoist that file to /.html and
+// remove the now-empty nested directories. Done in closeBundle (post-write)
+// because Rolldown disallows mutating the in-memory bundle in generateBundle.
+function flattenOutput(): Plugin {
return {
- name: "rename-output",
+ name: "flatten-output",
enforce: "post",
- generateBundle(_, bundle) {
- // Find the HTML file and rename it
- for (const fileName of Object.keys(bundle)) {
- if (fileName.endsWith("index.html")) {
- const chunk = bundle[fileName];
- chunk.fileName = `${app}.html`;
- delete bundle[fileName];
- bundle[`${app}.html`] = chunk;
- break;
- }
+ closeBundle() {
+ const nested = resolve(outDir, `src/apps/${app}/index.html`);
+ const flat = resolve(outDir, `${app}.html`);
+ if (!existsSync(nested)) {
+ throw new Error(
+ `flatten-output: expected built HTML at ${nested} for app "${app}" but it was not emitted`,
+ );
}
+ renameSync(nested, flat);
+ rmSync(resolve(outDir, "src"), { recursive: true, force: true });
},
};
}
export default defineConfig({
- plugins: [react(), viteSingleFile(), renameOutput()],
+ plugins: [react(), viteSingleFile(), flattenOutput()],
build: {
- outDir: resolve(__dirname, "../pkg/github/ui_dist"),
+ outDir,
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, `src/apps/${app}/index.html`),