Skip to content

feat: add GOOGLE_WORKSPACE_CLI_API_BASE_URL for custom/mock endpoint support#100

Open
xdotli wants to merge 8 commits intogoogleworkspace:mainfrom
benchflow-ai:feat/custom-endpoint-override
Open

feat: add GOOGLE_WORKSPACE_CLI_API_BASE_URL for custom/mock endpoint support#100
xdotli wants to merge 8 commits intogoogleworkspace:mainfrom
benchflow-ai:feat/custom-endpoint-override

Conversation

@xdotli
Copy link

@xdotli xdotli commented Mar 5, 2026

Reproduce steps:

# Install from this branch
cargo install --git https://github.com/benchflow-ai/cli --branch feat/custom-endpoint-override

Without env var — request goes to Google APIs:

$ gws gmail users getProfile --params '{"userId":"me"}' --dry-run
{
  "method": "GET",
  "url": "https://gmail.googleapis.com/gmail/v1/users/me/profile"
}

With GWS_API_BASE_URL — request redirected to custom endpoint:

$ GWS_API_BASE_URL=http://localhost:8001 gws gmail users getProfile --params '{"userId":"me"}' --dry-run
[gws] Custom API endpoint active: http://localhost:8001
[gws] Authentication is disabled. Requests will NOT go to Google APIs.
{
  "method": "GET",
  "url": "http://localhost:8001/gmail/v1/users/me/profile"
}

--dry-run builds the request without sending it. Discovery Documents are still fetched from Google so the full command tree works — only the API request URLs are rewritten.

Summary

Adds a GWS_API_BASE_URL environment variable that redirects all API requests to a custom endpoint (e.g., a mock server). When set:

  • The real Discovery Document is still fetched so the CLI command tree works normally
  • root_url and base_url in the Discovery Document are rewritten to the custom endpoint
  • OAuth authentication is automatically skipped (mock servers don't need it)
  • Works in both CLI mode and MCP server mode

Motivation

Testing agents and skills against mock API servers is a common need for development and CI. The gog CLI already supports this via GOG_ENDPOINT. This PR brings the same capability to gws.

Example usage

# Point gws at a local mock Gmail server
export GWS_API_BASE_URL=http://localhost:8099

# All commands now hit the mock server, no auth needed
gws gmail users messages list --params '{"userId": "me", "q": "is:unread"}'

Changes

  • src/discovery.rs: Add custom_api_base_url() with LazyLock caching; rewrite Discovery Document URLs when env var is set
  • src/executor.rs: Add resolve_auth() that skips OAuth for custom endpoints
  • src/main.rs, src/mcp_server.rs: Use resolve_auth() for consistent behavior

@xdotli xdotli requested a review from jpoehnelt as a code owner March 5, 2026 04:23
@changeset-bot
Copy link

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: e2f436a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the application's testability and development workflow by allowing users to specify a custom API base URL via an environment variable. This enables seamless interaction with mock API servers, facilitating local development and continuous integration testing without requiring real Google API calls or OAuth authentication. The change ensures that the application's command structure remains intact while redirecting actual API requests to the specified endpoint.

Highlights

  • Custom API Endpoint Support: Introduced a GWS_API_BASE_URL environment variable to redirect all API requests to a custom endpoint, such as a mock server, for development and testing purposes.
  • Discovery Document URL Rewriting: Implemented logic to rewrite the root_url and base_url within the fetched Google Discovery Document to point to the custom API endpoint, ensuring the CLI command tree remains functional while hitting the mock server.
  • Automatic OAuth Skipping: Configured the system to automatically skip OAuth authentication when GWS_API_BASE_URL is set, as mock servers typically do not require or support Google OAuth.
  • Unified Authentication Logic: Centralized the authentication resolution logic into a new resolve_auth function, ensuring consistent behavior across both CLI and MCP server modes.
Changelog
  • src/discovery.rs
    • Introduced CUSTOM_API_BASE_URL using LazyLock to cache the GWS_API_BASE_URL environment variable.
    • Added custom_api_base_url() function to provide access to the cached custom URL.
    • Modified fetch_discovery_document to dynamically rewrite the root_url and base_url of the Discovery Document if a custom API base URL is present.
  • src/executor.rs
    • Added a new asynchronous function resolve_auth to encapsulate authentication logic.
    • Implemented conditional logic within resolve_auth to skip OAuth if a custom API base URL is detected.
  • src/main.rs
    • Updated the main application's authentication flow to utilize the new executor::resolve_auth function.
  • src/mcp_server.rs
    • Modified the MCP server's authentication process to call the new crate::executor::resolve_auth function.
Activity
  • The cargo check command has passed, indicating no compilation errors.
  • Manual testing against a mock Gmail API server is planned to verify functionality.
  • Verification of MCP server mode respecting the environment variable is planned.
  • Verification of normal operation (without the environment variable) being unaffected is planned.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link

codecov bot commented Mar 5, 2026

Codecov Report

❌ Patch coverage is 48.38710% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.89%. Comparing base (6ed836c) to head (30e0f85).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
src/discovery.rs 60.00% 20 Missing ⚠️
src/executor.rs 0.00% 8 Missing ⚠️
src/main.rs 0.00% 3 Missing ⚠️
src/mcp_server.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #100      +/-   ##
==========================================
+ Coverage   54.88%   54.89%   +0.01%     
==========================================
  Files          38       38              
  Lines       13085    13137      +52     
==========================================
+ Hits         7182     7212      +30     
- Misses       5903     5925      +22     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for testing against mock API servers by using the GWS_API_BASE_URL environment variable, with a well-designed implementation using LazyLock and a refactored resolve_auth function. However, a critical security vulnerability has been identified: the current implementation allows for silent redirection of sensitive API traffic and automatically disables authentication when GWS_API_BASE_URL is set. This, combined with the automatic loading of .env files, creates a risk where an attacker could hijack a user's API requests by placing a malicious .env file in their working directory. To address this, I recommend adding a mandatory warning message when a custom endpoint is active to ensure users are aware of the redirection and the disabled security state. Additionally, consider improving error handling for authentication failures to provide better feedback to the user.

src/executor.rs Outdated
}
match crate::auth::get_token(scopes).await {
Ok(t) => (Some(t), AuthMethod::OAuth),
Err(_) => (None, AuthMethod::None),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation swallows errors from auth::get_token, which can hide useful debugging information from users if their authentication setup is misconfigured (e.g., a corrupted credentials file). While the subsequent API call will likely fail, the original, more specific error is lost.

Consider logging the error to stderr to improve debuggability, while maintaining the current behavior of attempting an unauthenticated request.

        Err(e) => {
            // Not a fatal error; the request will be tried without auth.
            // But logging it helps debug credential issues.
            eprintln!("[gws] Warning: could not acquire authentication token: {e}. Proceeding unauthenticated.");
            (None, AuthMethod::None)
        }

@xdotli xdotli force-pushed the feat/custom-endpoint-override branch from ed54a90 to c22dc58 Compare March 5, 2026 04:32
@jpoehnelt
Copy link
Member

Review Feedback

Thanks for this PR — the design is clean and smart. A few items to address before merging:

Required

  1. Env var naming: GWS_API_BASE_URL should follow the existing convention → GOOGLE_WORKSPACE_CLI_API_BASE_URL (consistent with GOOGLE_WORKSPACE_CLI_TOKEN, GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE, etc.)

  2. Missing changeset: Please add a .changeset/ file (e.g. pnpx changeset).

  3. Needs rebase: The branch is behind main — get_token() signature has changed. Please rebase onto main.

Recommended

  1. Add a test for apply_base_url_override(): A simple unit test that constructs a RestDescription, calls the function, and asserts root_url/base_url were rewritten would be valuable.

  2. Security note: Consider adding a note in the README or AGENTS.md that this env var should never be set in production, since it silently disables authentication.

Otherwise the approach looks solid — rewriting the Discovery Document URLs while still fetching the real schema from Google is the right call.

Copy link
Member

@jpoehnelt jpoehnelt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

…support

Add an environment variable that redirects all API requests to a custom
endpoint (e.g., a mock server). When set, OAuth authentication is
automatically skipped while the real Discovery Document is still fetched
so the CLI command tree remains fully functional.

Changes:
- src/discovery.rs: add custom_api_base_url() with LazyLock caching;
  rewrite Discovery Document URLs when env var is set
- src/executor.rs: add resolve_auth() that skips OAuth for custom endpoints
- src/main.rs, src/mcp_server.rs: use resolve_auth() for consistent behavior
- AGENTS.md: document env var with security note
- Add changeset and unit tests for URL rewriting
@xdotli xdotli force-pushed the feat/custom-endpoint-override branch from 8f9a36e to 30e0f85 Compare March 5, 2026 07:22
@xdotli
Copy link
Author

xdotli commented Mar 5, 2026

.

Review Feedback

Thanks for this PR — the design is clean and smart. A few items to address before merging:

Required

  1. Env var naming: GWS_API_BASE_URL should follow the existing convention → GOOGLE_WORKSPACE_CLI_API_BASE_URL (consistent with GOOGLE_WORKSPACE_CLI_TOKEN, GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE, etc.)
  2. Missing changeset: Please add a .changeset/ file (e.g. pnpx changeset).
  3. Needs rebase: The branch is behind main — get_token() signature has changed. Please rebase onto main.

Recommended

  1. Add a test for apply_base_url_override(): A simple unit test that constructs a RestDescription, calls the function, and asserts root_url/base_url were rewritten would be valuable.
  2. Security note: Consider adding a note in the README or AGENTS.md that this env var should never be set in production, since it silently disables authentication.

Otherwise the approach looks solid — rewriting the Discovery Document URLs while still fetching the real schema from Google is the right call.

hi @jpoehnelt thanks for the speedy and thorough review! I have addressed all the changes and let cursor go thru it + reran my reproduce steps and it work as expected. Would appreciate another review. Thanks!

@xdotli xdotli requested a review from jpoehnelt March 5, 2026 07:29
Copy link
Member

@jpoehnelt jpoehnelt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. base_url rewrite loses the service path: rewrite_base_url sets
    base_url to just the custom host, dropping the /gmail/v1/ prefix. This
    means final request URLs won't include the service path. Please preserve
    the path portion from the original base_url or verify that build_url()
    reconstructs it from root_url + service_path.
  2. Auth error logging regression: resolve_auth() silently discards auth
    errors. The old mcp_server.rs code logged them. Please add an
    eprintln! warning in the Err branch.

@jpoehnelt
Copy link
Member

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for testing against mock API servers by adding the GOOGLE_WORKSPACE_CLI_API_BASE_URL environment variable. However, a critical vulnerability exists due to an inconsistency between the environment variable name implemented in the code (GOOGLE_WORKSPACE_CLI_API_BASE_URL) and the one documented in the PR description and examples (GWS_API_BASE_URL). This mismatch could lead to users accidentally performing actions on production Google Workspace accounts, posing a significant risk to data integrity. Additionally, the implementation of URL rewriting in rewrite_base_url has been identified as potentially leading to incorrect request URLs and may inadvertently strip the service path (e.g., /gmail/v1/) from API requests. Furthermore, a refactoring silently removed a useful warning on authentication failure in the MCP server, which should be restored.

Comment on lines +299 to +303
fn rewrite_base_url(doc: &mut RestDescription, base: &str) {
let base_trimmed = base.trim_end_matches('/');
doc.root_url = format!("{base_trimmed}/");
doc.base_url = Some(format!("{base_trimmed}/"));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current implementation of rewrite_base_url incorrectly rewrites base_url. It replaces the entire base_url with the custom endpoint, which causes the service path (e.g., /gmail/v1/) to be lost. This will result in incorrect API request URLs.

For example, a request to gmail.users.getProfile would be sent to http://localhost:8001/users/me/profile instead of the correct http://localhost:8001/gmail/v1/users/me/profile.

The fix is to replace only the host part of the base_url, preserving the path. Here is a suggested implementation:

fn rewrite_base_url(doc: &mut RestDescription, base: &str) {
    let base_trimmed = base.trim_end_matches('/');
    let new_root_url = format!("{base_trimmed}/");
    let original_root_url = std::mem::replace(&mut doc.root_url, new_root_url);

    if let Some(base_url) = &mut doc.base_url {
        *base_url = base_url.replace(&original_root_url, &doc.root_url);
    }
}

@xdotli xdotli changed the title feat: add GWS_API_BASE_URL for custom/mock endpoint support feat: add GOOGLE_WORKSPACE_CLI_API_BASE_URL for custom/mock endpoint support Mar 5, 2026
@xdotli
Copy link
Author

xdotli commented Mar 5, 2026

@jpoehnelt thanks for the thorough review! Here's what was done to address everything:

For the base_url rewrite losing the service path — this was a real bug. The original implementation set base_url to just the custom host, which broke any API that uses a non-empty servicePath. Gmail worked fine because its servicePath is empty and the method paths include the full path (e.g. gmail/v1/users/{userId}/profile), but Drive has servicePath "drive/v3/" with short method paths like "files", so the final URL became http://localhost:8001/files instead of http://localhost:8001/drive/v3/files. Fixed by using str::replace on the original root_url so only the host gets swapped and the path portion is preserved. Added three unit tests covering both styles plus the None base_url edge case.

For the auth error logging regression — resolve_auth now returns Result so each call site can handle errors with its own context. main.rs silently falls back to unauthenticated (same as before), and mcp_server.rs restores the original "[gws mcp] Warning: Authentication failed, proceeding without credentials" message that was there before the refactor.

Also fixed cargo fmt issues that were in the original commit.

Tested all of this against a mock Gmail API server running on localhost:8001 (a FastAPI/uvicorn server implementing the full Gmail REST surface — messages, threads, labels, drafts, settings, history, profile, attachments, watch/stop, plus admin endpoints for seeding/reset/snapshots). After seeding the server, verified the following endpoints all return correct data through gws: users.getProfile, users.messages.list, users.messages.get (with path param substitution for messageId), users.labels.list, users.threads.list, users.drafts.list. Also verified Drive files.list dry-run now produces the correct URL with the service path preserved. Without the env var set, everything still hits the real Google APIs as expected.

Also changed PR title, added logging as mentioned.

Lmk if I should start mock servers on other services and test them.

@xdotli xdotli requested a review from jpoehnelt March 5, 2026 09:55
@jpoehnelt jpoehnelt added area: http cla: yes This human has signed the Contributor License Agreement. complexity: medium Moderate change, some review needed labels Mar 5, 2026
Resolve merge conflicts in AGENTS.md, src/main.rs, and src/mcp_server.rs:
- AGENTS.md: adopt upstream's table format for env vars, add API_BASE_URL entry
- src/main.rs: keep resolve_auth() for custom endpoint support, adopt upstream's
  stricter error handling (propagate auth errors, only fall back for missing creds)
- src/mcp_server.rs: keep resolve_auth(), adopt upstream's select_scope() and
  account env var support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@googleworkspace-bot googleworkspace-bot added area: mcp area: docs area: discovery area: core Core CLI parsing, commands, error handling, utilities labels Mar 6, 2026
@google-cla google-cla bot added the cla: no This human has *not* signed the Contributor License Agreement. label Mar 6, 2026
@google-cla google-cla bot removed the cla: yes This human has signed the Contributor License Agreement. label Mar 6, 2026
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for testing against mock servers by adding the GOOGLE_WORKSPACE_CLI_API_BASE_URL environment variable. The implementation correctly redirects API calls, disables authentication, and updates discovery documents. The changes are consistently applied across the CLI and MCP server modes, and the documentation is updated accordingly. I found one area for improvement in the URL rewriting logic to make it more robust against edge cases.

@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a GOOGLE_WORKSPACE_CLI_API_BASE_URL environment variable to support custom API endpoints, which is a great feature for testing against mock servers. The implementation correctly uses a LazyLock for caching, prints warnings when the override is active, and skips authentication as expected. The changes are consistently applied across the CLI and MCP server modes.

I've found one high-severity issue in src/discovery.rs where the base_url might not be rewritten in some cases, causing requests to silently go to the production API instead of the custom endpoint. I've added a review comment with a suggested fix.

Also, please note that the pull request description mentions GWS_API_BASE_URL, but the implementation uses GOOGLE_WORKSPACE_CLI_API_BASE_URL. It would be good to update the description for consistency.

@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature to support custom API endpoints via the GOOGLE_WORKSPACE_CLI_API_BASE_URL environment variable, which is particularly useful for testing against mock servers. The implementation is well-executed: it correctly disables authentication when a custom endpoint is active, provides clear user feedback, and applies the logic consistently across both the standard CLI and the MCP server mode. The URL rewriting logic in the discovery document is robust and is accompanied by a comprehensive set of unit tests covering various scenarios. The documentation has also been updated to reflect this new feature, including an important security warning. Overall, this is a high-quality contribution that enhances the tool's testability.

@xdotli
Copy link
Author

xdotli commented Mar 6, 2026

hi @jpoehnelt, I applied all gemini's suggestions and resolved merge conflicts. I reviewed every file again myself + dry run locally + used our gmail mock server to test. everything works as expected. would be great to get a rereview when you got the time. much appreciated!

Upstream removed multi-account support (googleworkspace#253), which changed
auth::get_token to no longer accept an account parameter.
Updated resolve_auth() and its call sites accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for testing by allowing API requests to be redirected to a custom endpoint via the GOOGLE_WORKSPACE_CLI_API_BASE_URL environment variable. The implementation is well-structured, using a LazyLock to cache the environment variable and cleanly abstracting the authentication logic in resolve_auth. The changes are consistently applied across both the main CLI and the MCP server.

One minor point of feedback: the pull request description mentions GWS_API_BASE_URL, but the code and documentation consistently use GOOGLE_WORKSPACE_CLI_API_BASE_URL. It would be helpful to update the PR description to match the implementation for clarity.

I've added one comment regarding the use of eprintln! which could lead to a panic in certain scenarios.

Comment on lines +190 to +193
if let Some(ref u) = url {
eprintln!("[gws] Custom API endpoint active: {u}");
eprintln!("[gws] Authentication is disabled. Requests will NOT go to Google APIs.");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The eprintln! macro can panic if it fails to write to stderr, for instance, due to a broken pipe when the output is redirected. This can cause the application to crash unexpectedly. To improve robustness, it's safer to use std::io::stderr() with writeln! and handle the Result, even if it's just by ignoring it.

    if let Some(ref u) = url {
        use std::io::Write;
        let mut stderr = std::io::stderr();
        let _ = writeln!(stderr, "[gws] Custom API endpoint active: {u}");
        let _ = writeln!(stderr, "[gws] Authentication is disabled. Requests will NOT go to Google APIs.");
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core CLI parsing, commands, error handling, utilities area: discovery area: docs area: http area: mcp cla: no This human has *not* signed the Contributor License Agreement. complexity: medium Moderate change, some review needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants