Skip to content

Conversation

@duwenxin99
Copy link
Contributor

@duwenxin99 duwenxin99 commented Aug 4, 2025

Support end-user credential passthrough with the BigQuery source and the bigquery-sql tool.
Support for other BQ tools will be added in subsequent PRs.

Issue: #813

@duwenxin99 duwenxin99 requested a review from a team as a code owner August 4, 2025 20:18
@duwenxin99 duwenxin99 requested a review from averikitsch August 5, 2025 02:02
@duwenxin99 duwenxin99 force-pushed the end-cred branch 2 times, most recently from ff4f652 to 75b5fa6 Compare August 6, 2025 22:43
@kurtisvg kurtisvg changed the title feat(tools/bigquery): Support Tool invocation with end-user OAuth access token feat(tools/bigquery): add support for user-credential passthrough Aug 13, 2025
@drstrangelooker
Copy link
Contributor

This is great. If I understand correctly this is just capturing the token from the request header recieved by the toolbox, then putting it into the request made to bigquery.

Once this lands I will do the same for Looker. I already figured out how to get Gemini-CLI to authenticate with Looker like this in settings.json.

  "mcpServers": {
    "looker-opm": {
      "httpUrl": "http://localhost:5000/mcp",
      "oauth": {
        "enabled": true,
        "clientId": "gemini-cli",
        "authorizationUrl": "https://sandbox.looker-devrel.com/auth",
        "tokenUrl": "https://sandbox.looker-devrel.com/api/token",
        "scopes": ["cors_api"]
      }
    }
  }

That should make Gemini-CLI pass the authentication token to the looker tools.

@drstrangelooker
Copy link
Contributor

I'm looking at the code and I think we need to add something to capture the Authorization when called via internal/server/mcp/* paths.

@drstrangelooker
Copy link
Contributor

I figured out how to get the token when being used as an MCP server...

diff --git a/internal/server/api.go b/internal/server/api.go
index d7c1eeeda..d28c4edad 100644
--- a/internal/server/api.go
+++ b/internal/server/api.go
@@ -220,6 +220,7 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
        // Extract OAuth access token from the "Authorization" header (currently for
        // BigQuery end-user credentials usage only)
        accessToken := tools.OAuthAccessToken(r.Header.Get("Authorization"))
+       s.logger.DebugContext(ctx, fmt.Sprintf("headers: %v", r.Header))

        res, err := tool.Invoke(ctx, params, accessToken)
        if err != nil {
diff --git a/internal/server/mcp.go b/internal/server/mcp.go
index 9dba38d39..3e86bdebe 100644
--- a/internal/server/mcp.go
+++ b/internal/server/mcp.go
@@ -34,6 +34,7 @@ import (
        mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
        v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
        v20250326 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250326"
+       "github.com/googleapis/genai-toolbox/internal/tools"
        "github.com/googleapis/genai-toolbox/internal/util"
        "go.opentelemetry.io/otel/attribute"
        "go.opentelemetry.io/otel/codes"
@@ -141,7 +142,7 @@ func (s *stdioSession) readInputStream(ctx context.Context) error {
                        }
                        return err
                }
-               v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "")
+               v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", "")
                if err != nil {
                        // errors during the processing of message will generate a valid MCP Error response.
                        // server can continue to run.
@@ -401,7 +402,8 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
                return
        }

-       v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName)
+       accessToken := tools.OAuthAccessToken(r.Header.Get("Authorization"))
+       v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, accessToken)
        // notifications will return empty string
        if res == nil {
                // Notifications do not expect a response
@@ -437,7 +439,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
 }

 // processMcpMessage process the messages received from clients
-func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string) (string, any, error) {
+func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, accessToken tools.OAuthAccessToken) (string, any, error) {
        logger, err := util.LoggerFromContext(ctx)
        if err != nil {
                return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
@@ -492,7 +494,7 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
                        err = fmt.Errorf("toolset does not exist")
                        return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
                }
-               res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), body)
+               res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), body, accessToken)
                return "", res, err
        }
 }
diff --git a/internal/server/mcp/mcp.go b/internal/server/mcp/mcp.go
index 596b6af3c..4441760d4 100644
--- a/internal/server/mcp/mcp.go
+++ b/internal/server/mcp/mcp.go
@@ -93,14 +93,14 @@ func NotificationHandler(ctx context.Context, body []byte) error {

 // ProcessMethod returns a response for the request.
 // This is the Operation phase of the lifecycle for MCP client-server connections.
-func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte) (any, error) {
+func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        switch mcpVersion {
        case v20250618.PROTOCOL_VERSION:
-               return v20250618.ProcessMethod(ctx, id, method, toolset, tools, body)
+               return v20250618.ProcessMethod(ctx, id, method, toolset, tools, body, accessToken)
        case v20250326.PROTOCOL_VERSION:
-               return v20250326.ProcessMethod(ctx, id, method, toolset, tools, body)
+               return v20250326.ProcessMethod(ctx, id, method, toolset, tools, body, accessToken)
        default:
-               return v20241105.ProcessMethod(ctx, id, method, toolset, tools, body)
+               return v20241105.ProcessMethod(ctx, id, method, toolset, tools, body, accessToken)
        }
 }

diff --git a/internal/server/mcp/v20241105/method.go b/internal/server/mcp/v20241105/method.go
index 1e3ee2668..72e8f09c9 100644
--- a/internal/server/mcp/v20241105/method.go
+++ b/internal/server/mcp/v20241105/method.go
@@ -26,12 +26,12 @@ import (
 )

 // ProcessMethod returns a response for the request.
-func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte) (any, error) {
+func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        switch method {
        case TOOLS_LIST:
                return toolsListHandler(id, toolset, body)
        case TOOLS_CALL:
-               return toolsCallHandler(ctx, id, tools, body)
+               return toolsCallHandler(ctx, id, tools, body, accessToken)
        default:
                err := fmt.Errorf("invalid method %s", method)
                return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
@@ -56,7 +56,7 @@ func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte)
 }

 // toolsCallHandler generate a response for tools call.
-func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte) (any, error) {
+func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        // retrieve logger from context
        logger, err := util.LoggerFromContext(ctx)
        if err != nil {
@@ -108,7 +108,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[strin
        }

        // run tool invocation and generate response.
-       results, err := tool.Invoke(ctx, params, "")
+       results, err := tool.Invoke(ctx, params, accessToken)
        if err != nil {
                text := TextContent{
                        Type: "text",
diff --git a/internal/server/mcp/v20250326/method.go b/internal/server/mcp/v20250326/method.go
index c8447969a..566166146 100644
--- a/internal/server/mcp/v20250326/method.go
+++ b/internal/server/mcp/v20250326/method.go
@@ -26,12 +26,12 @@ import (
 )

 // ProcessMethod returns a response for the request.
-func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte) (any, error) {
+func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        switch method {
        case TOOLS_LIST:
                return toolsListHandler(id, toolset, body)
        case TOOLS_CALL:
-               return toolsCallHandler(ctx, id, tools, body)
+               return toolsCallHandler(ctx, id, tools, body, accessToken)
        default:
                err := fmt.Errorf("invalid method %s", method)
                return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
@@ -56,7 +56,7 @@ func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte)
 }

 // toolsCallHandler generate a response for tools call.
-func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte) (any, error) {
+func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        // retrieve logger from context
        logger, err := util.LoggerFromContext(ctx)
        if err != nil {
@@ -108,7 +108,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[strin
        }

        // run tool invocation and generate response.
-       results, err := tool.Invoke(ctx, params, "")
+       results, err := tool.Invoke(ctx, params, accessToken)
        if err != nil {
                text := TextContent{
                        Type: "text",
diff --git a/internal/server/mcp/v20250618/method.go b/internal/server/mcp/v20250618/method.go
index 4ba2764cd..c871cd8c3 100644
--- a/internal/server/mcp/v20250618/method.go
+++ b/internal/server/mcp/v20250618/method.go
@@ -26,12 +26,12 @@ import (
 )

 // ProcessMethod returns a response for the request.
-func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte) (any, error) {
+func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        switch method {
        case TOOLS_LIST:
                return toolsListHandler(id, toolset, body)
        case TOOLS_CALL:
-               return toolsCallHandler(ctx, id, tools, body)
+               return toolsCallHandler(ctx, id, tools, body, accessToken)
        default:
                err := fmt.Errorf("invalid method %s", method)
                return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
@@ -56,7 +56,7 @@ func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte)
 }

 // toolsCallHandler generate a response for tools call.
-func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte) (any, error) {
+func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, body []byte, accessToken tools.OAuthAccessToken) (any, error) {
        // retrieve logger from context
        logger, err := util.LoggerFromContext(ctx)
        if err != nil {
@@ -108,7 +108,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[strin
        }

        // run tool invocation and generate response.
-       results, err := tool.Invoke(ctx, params, "")
+       results, err := tool.Invoke(ctx, params, accessToken)
        if err != nil {
                text := TextContent{
                        Type: "text",

@drstrangelooker
Copy link
Contributor

I confirmed that I successfully get passed the proper authorization token when called from Gemini-CLI. The trick for the Looker implementation now is passing the token to the Looker SDK. There is no provision in the Looker SDK to accept a token so I might need to enhance the SDK a bit.

@duwenxin99 duwenxin99 changed the base branch from main to refactor-invoke August 20, 2025 20:15
duwenxin99 added a commit that referenced this pull request Aug 21, 2025
…1200)

Pass in authorization token to the Tool invocation functions.
Support: #1067
Base automatically changed from refactor-invoke to main August 21, 2025 22:20
@duwenxin99 duwenxin99 force-pushed the end-cred branch 2 times, most recently from 0cef7b4 to 82fc59c Compare August 22, 2025 20:39
duwenxin99 added a commit that referenced this pull request Aug 22, 2025
Add `RequiresClientAuthorization()` method to the `Tool` interface.
Currently returning false for all tools.
Supports: #1067
@duwenxin99 duwenxin99 force-pushed the end-cred branch 2 times, most recently from 7c99781 to a8a544f Compare August 22, 2025 21:09
github-actions bot pushed a commit to Jaleel-zhu/genai-toolbox that referenced this pull request Aug 22, 2025
…leapis#1217)

Add `RequiresClientAuthorization()` method to the `Tool` interface.
Currently returning false for all tools.
Supports: googleapis#1067 b1abbeb
@duwenxin99 duwenxin99 requested a review from Genesis929 August 23, 2025 19:30
@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

@duwenxin99 duwenxin99 merged commit 650e2e2 into main Aug 26, 2025
9 checks passed
@duwenxin99 duwenxin99 deleted the end-cred branch August 26, 2025 21:52
@github-actions
Copy link
Contributor

🧨 Preview deployments removed.

github-actions bot pushed a commit that referenced this pull request Aug 26, 2025
…hrough (#1067)

Support end-user credential passthrough with the BigQuery source and the
`bigquery-sql` tool.
Support for other BQ tools will be added in subsequent PRs.

Issue: #813 650e2e2
github-actions bot pushed a commit to renovate-bot/googleapis-_-genai-toolbox that referenced this pull request Aug 26, 2025
…hrough (googleapis#1067)

Support end-user credential passthrough with the BigQuery source and the
`bigquery-sql` tool.
Support for other BQ tools will be added in subsequent PRs.

Issue: googleapis#813 650e2e2
Yuan325 added a commit that referenced this pull request Aug 27, 2025
🤖 I have created a release *beep* *boop*
---


##
[0.13.0](v0.12.0...v0.13.0)
(2025-08-27)


### ⚠ BREAKING CHANGES

* **prebuilt/alloydb:** Add bearer token support for
alloydb-wait-for-operation
([#1183](#1183))


### Features

* Add capability to set default for environment variable in config
([#1248](#1248))
([5bcd52e](5bcd52e))
* **firebird:** Add Firebird SQL 2.5+ source and tool
([#1011](#1011))
([4f6b806](4f6b806))
* **oceanbase:** Add Oceanbase source and tool
([#895](#895))
([6fc4982](6fc4982))
* **server/mcp:** Support `ping` mechanism
([#1178](#1178))
([5dcc66c](5dcc66c))
* **server:** Fail-fast on environment variable substitution
([#1177](#1177))
([212aaba](212aaba))
* **server:** Implement Tool call auth error propagation
([#1235](#1235))
([b94a021](b94a021))
* **sources/bigquery:** Add support for user-credential passthrough
([#1067](#1067))
([650e2e2](650e2e2))
* **tool/looker:** Add support for `description` field in looker tool
([#1199](#1199))
([97f0dd2](97f0dd2))
* **tools/bigquery-ask-data-insights:** Add bigquery `ask-data-insights`
tool ([#932](#932))
([7651357](7651357))
* **tools/bigquery-forecast:** Add bigqueryforecast tool
([#1148](#1148))
([2ad0ccf](2ad0ccf))
* **tools/firestore-add-documents:** Add firestore-add-documents tool
([#1107](#1107))
([ee4a70a](ee4a70a))
* **tools/firestore-update-document:** Add firestore-update-document
tool ([#1191](#1191))
([0010123](0010123))
* **tools/looker:** Control over whether hidden objects are surfaced
([#1222](#1222))
([bc91559](bc91559))
* **trino:** Add Trino source and tools
([#948](#948))
([7dd123b](7dd123b))


### Bug Fixes

* **tools/looker:** Lookergetdashboards uses proper Authorized helper
func ([#1255](#1255))
([00866bc](00866bc))
* **tools/mongodb-find-one:** ProjectPayload unmarshaling
([#1167](#1167))
([8ea6a98](8ea6a98))
* **tools/mysql:** Fix encoded text for mysql
([#1161](#1161))
([a37cfa8](a37cfa8)),
closes [#840](#840)


---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
github-actions bot pushed a commit that referenced this pull request Aug 27, 2025
🤖 I have created a release *beep* *boop*
---

##
[0.13.0](v0.12.0...v0.13.0)
(2025-08-27)

### ⚠ BREAKING CHANGES

* **prebuilt/alloydb:** Add bearer token support for
alloydb-wait-for-operation
([#1183](#1183))

### Features

* Add capability to set default for environment variable in config
([#1248](#1248))
([5bcd52e](5bcd52e))
* **firebird:** Add Firebird SQL 2.5+ source and tool
([#1011](#1011))
([4f6b806](4f6b806))
* **oceanbase:** Add Oceanbase source and tool
([#895](#895))
([6fc4982](6fc4982))
* **server/mcp:** Support `ping` mechanism
([#1178](#1178))
([5dcc66c](5dcc66c))
* **server:** Fail-fast on environment variable substitution
([#1177](#1177))
([212aaba](212aaba))
* **server:** Implement Tool call auth error propagation
([#1235](#1235))
([b94a021](b94a021))
* **sources/bigquery:** Add support for user-credential passthrough
([#1067](#1067))
([650e2e2](650e2e2))
* **tool/looker:** Add support for `description` field in looker tool
([#1199](#1199))
([97f0dd2](97f0dd2))
* **tools/bigquery-ask-data-insights:** Add bigquery `ask-data-insights`
tool ([#932](#932))
([7651357](7651357))
* **tools/bigquery-forecast:** Add bigqueryforecast tool
([#1148](#1148))
([2ad0ccf](2ad0ccf))
* **tools/firestore-add-documents:** Add firestore-add-documents tool
([#1107](#1107))
([ee4a70a](ee4a70a))
* **tools/firestore-update-document:** Add firestore-update-document
tool ([#1191](#1191))
([0010123](0010123))
* **tools/looker:** Control over whether hidden objects are surfaced
([#1222](#1222))
([bc91559](bc91559))
* **trino:** Add Trino source and tools
([#948](#948))
([7dd123b](7dd123b))

### Bug Fixes

* **tools/looker:** Lookergetdashboards uses proper Authorized helper
func ([#1255](#1255))
([00866bc](00866bc))
* **tools/mongodb-find-one:** ProjectPayload unmarshaling
([#1167](#1167))
([8ea6a98](8ea6a98))
* **tools/mysql:** Fix encoded text for mysql
([#1161](#1161))
([a37cfa8](a37cfa8)),
closes [#840](#840)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> 1a6dfe8
github-actions bot pushed a commit to renovate-bot/googleapis-_-genai-toolbox that referenced this pull request Aug 27, 2025
🤖 I have created a release *beep* *boop*
---

##
[0.13.0](googleapis/genai-toolbox@v0.12.0...v0.13.0)
(2025-08-27)

### ⚠ BREAKING CHANGES

* **prebuilt/alloydb:** Add bearer token support for
alloydb-wait-for-operation
([googleapis#1183](googleapis#1183))

### Features

* Add capability to set default for environment variable in config
([googleapis#1248](googleapis#1248))
([5bcd52e](googleapis@5bcd52e))
* **firebird:** Add Firebird SQL 2.5+ source and tool
([googleapis#1011](googleapis#1011))
([4f6b806](googleapis@4f6b806))
* **oceanbase:** Add Oceanbase source and tool
([googleapis#895](googleapis#895))
([6fc4982](googleapis@6fc4982))
* **server/mcp:** Support `ping` mechanism
([googleapis#1178](googleapis#1178))
([5dcc66c](googleapis@5dcc66c))
* **server:** Fail-fast on environment variable substitution
([googleapis#1177](googleapis#1177))
([212aaba](googleapis@212aaba))
* **server:** Implement Tool call auth error propagation
([googleapis#1235](googleapis#1235))
([b94a021](googleapis@b94a021))
* **sources/bigquery:** Add support for user-credential passthrough
([googleapis#1067](googleapis#1067))
([650e2e2](googleapis@650e2e2))
* **tool/looker:** Add support for `description` field in looker tool
([googleapis#1199](googleapis#1199))
([97f0dd2](googleapis@97f0dd2))
* **tools/bigquery-ask-data-insights:** Add bigquery `ask-data-insights`
tool ([googleapis#932](googleapis#932))
([7651357](googleapis@7651357))
* **tools/bigquery-forecast:** Add bigqueryforecast tool
([googleapis#1148](googleapis#1148))
([2ad0ccf](googleapis@2ad0ccf))
* **tools/firestore-add-documents:** Add firestore-add-documents tool
([googleapis#1107](googleapis#1107))
([ee4a70a](googleapis@ee4a70a))
* **tools/firestore-update-document:** Add firestore-update-document
tool ([googleapis#1191](googleapis#1191))
([0010123](googleapis@0010123))
* **tools/looker:** Control over whether hidden objects are surfaced
([googleapis#1222](googleapis#1222))
([bc91559](googleapis@bc91559))
* **trino:** Add Trino source and tools
([googleapis#948](googleapis#948))
([7dd123b](googleapis@7dd123b))

### Bug Fixes

* **tools/looker:** Lookergetdashboards uses proper Authorized helper
func ([googleapis#1255](googleapis#1255))
([00866bc](googleapis@00866bc))
* **tools/mongodb-find-one:** ProjectPayload unmarshaling
([googleapis#1167](googleapis#1167))
([8ea6a98](googleapis@8ea6a98))
* **tools/mysql:** Fix encoded text for mysql
([googleapis#1161](googleapis#1161))
([a37cfa8](googleapis@a37cfa8)),
closes [googleapis#840](googleapis#840)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> 1a6dfe8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants