#
tokens: 42179/50000 49/49 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .c8rc.json
├── .github
│   ├── images
│   │   ├── deploy_from_apps.gif
│   │   ├── deploy_from_ide.gif
│   │   └── deploycli.gif
│   └── workflows
│       ├── lint-checker.yml
│       ├── local-tests.yml
│       └── npm-publish.yml
├── .gitignore
├── .kokoro
│   ├── kokoro_build.sh
│   ├── presubmit.cfg
│   └── run_tests.sh
├── .prettierignore
├── .prettierrc.json
├── CONTRIBUTING.md
├── Dockerfile
├── example-sources-to-deploy
│   ├── Dockerfile
│   ├── go.mod
│   └── main.go
├── gemini-extension
│   └── GEMINI.md
├── gemini-extension.json
├── lib
│   ├── cloud-api
│   │   ├── auth.js
│   │   ├── billing.js
│   │   ├── build.js
│   │   ├── helpers.js
│   │   ├── metadata.js
│   │   ├── projects.js
│   │   ├── registry.js
│   │   ├── run.js
│   │   └── storage.js
│   ├── deployment
│   │   └── deployer.js
│   └── util
│       ├── archive.js
│       └── helpers.js
├── LICENSE
├── mcp-server.js
├── package-lock.json
├── package.json
├── prompts.js
├── README.md
├── REVIEWING.md
├── test
│   ├── local
│   │   ├── cloud-api
│   │   │   ├── build.test.js
│   │   │   └── projects.test.js
│   │   ├── gemini-extension.test.js
│   │   ├── mcp-server-streamable-http.test.js
│   │   ├── mcp-server.test.js
│   │   ├── notifications.test.js
│   │   ├── npx.test.js
│   │   ├── prompts.test.js
│   │   └── tools.test.js
│   └── need-gcp
│       ├── cloud-run-services.test.js
│       ├── deployer.test.js
│       ├── gcp-auth-check.test.js
│       ├── gcp-projects.test.js
│       ├── test-helpers.js
│       └── workflows
│           └── deployment-workflows.test.js
└── tools
    ├── register-tools.js
    └── tools.js
```

# Files

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
node_modules
package-lock.json

```

--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------

```json
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false,
  "printWidth": 80,
  "trailingComma": "es5"
}

```

--------------------------------------------------------------------------------
/.c8rc.json:
--------------------------------------------------------------------------------

```json
{
  "all": true,
  "reporter": ["lcov", "text"],
  "lines": 43,
  "functions": 35,
  "branches": 70,
  "statements": 43,
  "exclude": ["**/test/**", "**/coverage/**"]
}

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Node.js
node_modules/

# Test coverage
coverage/

# Settings
.gemini/

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Cloud Run MCP server and Gemini CLI extension

Enable MCP-compatible AI agents to deploy apps to Cloud Run.

```json
"mcpServers":{
  "cloud-run": {
    "command": "npx",
    "args": ["-y", "@google-cloud/cloud-run-mcp"]
  }
}
```

Deploy from Gemini CLI and other AI-powered CLI agents:

<img  src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploycli.gif" width="800">

Deploy from AI-powered IDEs:

<img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploy_from_ide.gif" width="800">

Deploy from AI assistant apps:

<img src="https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/refs/heads/main/.github/images/deploy_from_apps.gif" width="800">

Deploy from agent SDKs, like the [Google Gen AI SDK](https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#use_model_context_protocol_mcp) or [Agent Development Kit](https://google.github.io/adk-docs/tools/mcp-tools/).

> [!NOTE]  
> This is the repository of an MCP server to deploy code to Cloud Run, to learn how to **host** MCP servers on Cloud Run, [visit the Cloud Run documentation](https://cloud.google.com/run/docs/host-mcp-servers).

## Tools

- `deploy-file-contents`: Deploys files to Cloud Run by providing their contents directly.
- `list-services`: Lists Cloud Run services in a given project and region.
- `get-service`: Gets details for a specific Cloud Run service.
- `get-service-log`: Gets Logs and Error Messages for a specific Cloud Run service.

- `deploy-local-folder`\*: Deploys a local folder to a Google Cloud Run service.
- `list-projects`\*: Lists available GCP projects.
- `create-project`\*: Creates a new GCP project and attach it to the first available billing account. A project ID can be optionally specified.

_\* only available when running locally_

## Prompts

Prompts are natural language commands that can be used to perform common tasks. They are shortcuts for executing tool calls with pre-filled arguments.

- `deploy`: Deploys the current working directory to Cloud Run. If a service name is not provided, it will use the `DEFAULT_SERVICE_NAME` environment variable, or the name of the current working directory.
- `logs`: Gets the logs for a Cloud Run service. If a service name is not provided, it will use the `DEFAULT_SERVICE_NAME` environment variable, or the name of the current working directory.

## Environment Variables

The Cloud Run MCP server can be configured using the following environment variables:

| Variable                 | Description                                                                                                                                                                              |
| :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GOOGLE_CLOUD_PROJECT`   | The default project ID to use for Cloud Run services.                                                                                                                                    |
| `GOOGLE_CLOUD_REGION`    | The default region to use for Cloud Run services.                                                                                                                                        |
| `DEFAULT_SERVICE_NAME`   | The default service name to use for Cloud Run services.                                                                                                                                  |
| `SKIP_IAM_CHECK`         | Controls whether to check for IAM permissions for a Cloud Run service. Set to `false` to enable checks. This is `true` by default which is a recommended way to make the service public. |
| `ENABLE_HOST_VALIDATION` | Prevents [DNS Rebinding](https://en.wikipedia.org/wiki/DNS_rebinding) attacks by validating the Host header. This is disabled by default.                                                |
| `ALLOWED_HOSTS`          | Comma-separated list of allowed Host headers (if host validation is enabled). The default value is `localhost,127.0.0.1,::1`.                                                            |

## Use as a Gemini CLI extension

To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:

2. Install the extension:

   ```bash
   gemini extensions install https://github.com/GoogleCloudPlatform/cloud-run-mcp
   ```

3. Log in to your Google Cloud account using the command:

   ```bash
   gcloud auth login
   ```

4. Set up application credentials using the command:
   ```bash
   gcloud auth application-default login
   ```

## Use in MCP Clients

### Learn how to configure your MCP client

Most MCP clients require a configuration file to be created or modified to add the MCP server.

The configuration file syntax can be different across clients. Please refer to the following links for the latest expected syntax:

- [**Antigravity**](https://antigravity.google/docs/mcp)
- [**Windsurf**](https://docs.windsurf.com/windsurf/mcp)
- [**VSCode**](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
- [**Claude Desktop**](https://modelcontextprotocol.io/quickstart/user)
- [**Cursor**](https://docs.cursor.com/context/model-context-protocol)

Once you have identified how to configure your MCP client, select one of these two options to set up the MCP server.
We recommend setting up as a local MCP server using Node.js.

### Set up as local MCP server

Run the Cloud Run MCP server on your local machine using local Google Cloud credentials. This is best if you are using an AI-assisted IDE (e.g. Cursor) or a desktop AI application (e.g. Claude).

1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with your Google account.

2. Log in to your Google Cloud account using the command:

   ```bash
   gcloud auth login
   ```

3. Set up application credentials using the command:
   ```bash
   gcloud auth application-default login
   ```

Then configure the MCP server using either Node.js or Docker:

#### Using Node.js

0. Install [Node.js](https://nodejs.org/en/download/) (LTS version recommended).

1. Update the MCP configuration file of your MCP client with the following:

   ```json
      "cloud-run": {
        "command": "npx",
        "args": ["-y", "@google-cloud/cloud-run-mcp"]
      }
   ```

2. [Optional] Add default configurations

   ```json
      "cloud-run": {
         "command": "npx",
         "args": ["-y", "@google-cloud/cloud-run-mcp"],
         "env": {
               "GOOGLE_CLOUD_PROJECT": "PROJECT_NAME",
               "GOOGLE_CLOUD_REGION": "PROJECT_REGION",
               "DEFAULT_SERVICE_NAME": "SERVICE_NAME"
         }
      }
   ```

#### Using Docker

See Docker's [MCP catalog](https://hub.docker.com/mcp/server/cloud-run-mcp/overview), or use these manual instructions:

0. Install [Docker](https://www.docker.com/get-started/)

1. Update the MCP configuration file of your MCP client with the following:

   ```json
      "cloud-run": {
        "command": "docker",
        "args": [
          "run",
          "-i",
          "--rm",
          "-e",
          "GOOGLE_APPLICATION_CREDENTIALS",
          "-v",
          "/local-directory:/local-directory",
          "mcp/cloud-run-mcp:latest"
        ],
        "env": {
          "GOOGLE_APPLICATION_CREDENTIALS": "/Users/slim/.config/gcloud/application_default-credentials.json",
          "DEFAULT_SERVICE_NAME": "SERVICE_NAME"
        }
      }
   ```

### Set up as remote MCP server

> [!WARNING]  
> Do not use the remote MCP server without authentication. In the following instructions, we will use IAM authentication to secure the connection to the MCP server from your local machine. This is important to prevent unauthorized access to your Google Cloud resources.

Run the Cloud Run MCP server itself on Cloud Run with connection from your local machine authenticated via IAM.
With this option, you will only be able to deploy code to the same Google Cloud project as where the MCP server is running.

1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with your Google account.

2. Log in to your Google Cloud account using the command:

   ```bash
   gcloud auth login
   ```

3. Set your Google Cloud project ID using the command:
   ```bash
   gcloud config set project YOUR_PROJECT_ID
   ```
4. Deploy the Cloud Run MCP server to Cloud Run:

   ```bash
   gcloud run deploy cloud-run-mcp --image us-docker.pkg.dev/cloudrun/container/mcp --no-allow-unauthenticated
   ```

   When prompted, pick a region, for example `europe-west1`.

   Note that the MCP server is _not_ publicly accessible, it requires authentication via IAM.

5. [Optional] Add default configurations

   ```bash
   gcloud run services update cloud-run-mcp --region=REGION --update-env-vars GOOGLE_CLOUD_PROJECT=PROJECT_NAME,GOOGLE_CLOUD_REGION=PROJECT_REGION,DEFAULT_SERVICE_NAME=SERVICE_NAME,SKIP_IAM_CHECK=false
   ```

6. Run a Cloud Run proxy on your local machine to connect securely using your identity to the remote MCP server running on Cloud Run:

   ```bash
   gcloud run services proxy cloud-run-mcp --port=3000 --region=REGION --project=PROJECT_ID
   ```

   This will create a local proxy on port 3000 that forwards requests to the remote MCP server and injects your identity.

7. Update the MCP configuration file of your MCP client with the following:

   ```json
      "cloud-run": {
        "url": "http://localhost:3000/sse"
      }

   ```

   If your MCP client does not support the `url` attribute, you can use [mcp-remote](https://www.npmjs.com/package/mcp-remote):

   ```json
      "cloud-run": {
        "command": "npx",
        "args": ["-y", "mcp-remote", "http://localhost:3000/sse"]
      }
   ```

The Google Cloud Platform Terms of Service (available at https://cloud.google.com/terms/) and the Data Processing and Security Terms (available at https://cloud.google.com/terms/data-processing-terms) do not apply to any component of the Cloud Run MCP Server software.

```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# How to contribute

We'd love to accept your patches and contributions to this project.

## Before you begin

### Sign our Contributor License Agreement

Contributions to this project must be accompanied by a
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
You (or your employer) retain the copyright to your contribution; this simply
gives us permission to use and redistribute your contributions as part of the
project.

If you or your current employer have already signed the Google CLA (even if it
was for a different project), you probably don't need to do it again.

Visit <https://cla.developers.google.com/> to see your current agreements or to
sign a new one.

### Review our community guidelines

This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).

## Contribution process

### Start with an issue

Before sending a pull request, please open an issue describing the bug or feature
you would like to address. This will allow maintainers of this project to guide
you in your design and implementation.

### Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

## Development

```bash
npm install
```

### Using MCP inspector

Load MCP Inspector in your browser:

```bash
npm run test:mcp
```

Open http://localhost:6274/

### Using a real MCP client

To use local stdio MCP server. In your MCP client configuration, use the following:

```json
{
  "mcpServers": {
    "cloud-run": {
      "command": "node",
      "args": ["/path/to/this/repo/cloud-run-mcp/mcp-server.js"]
    }
  }
}
```

To use remote MCP Server in a MCP client:

Start the MCP server locally with:

```bash
GCP_STDIO=false node /path/to/this/repo/cloud-run-mcp/mcp-server.js
```

Then, in your MCP client configuration, use the following:

```json
{
  "mcpServers": {
    "cloud-run": {
      "command": "npx",
      "args": ["mcp-remote", "http://localhost:3000/mcp"]
    }
  }
}
```

or, if your client supports the `url` attribute, you can use:

```
{
  "mcpServers": {
    "cloud-run": {
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

## Testing

### To test creating a new project (not using MCP)

See the `test/test-create-project.js` script. Run it with:

```bash
npm run test:create-project
```

This script will guide you through creating a new Google Cloud project and attempting to link it to a billing account. You can optionally provide a desired project ID.

### To test a simple deployment (not using MCP)

See the `test/test-deploy.js` script. Run it with:

```bash
npm run test:deploy
```

This script requires an existing Google Cloud Project ID to be provided when prompted or as a command-line argument.

## Publishing to npm

To update the [npm package](https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)

Run the following:

- `git checkout -b new-release`
- `npm test`
- `npm version minor`
- `git push --set-upstream origin new-release --follow-tags`
- Create a [pull request for 'new-release' on GitHub](https://github.com/GoogleCloudPlatform/cloud-run-mcp/pull/new/new-release)
- Get approval
- Rebase and merge into main
- On GitHub, [create a new release](https://github.com/GoogleCloudPlatform/cloud-run-mcp/releases/new) for this tag.

Then a [GitHub Action](https://github.com/GoogleCloudPlatform/cloud-run-mcp/blob/main/.github/workflows/npm-publish.yml) automatically publishes to npm when new releases are pushes.

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:22-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
EXPOSE 3000
CMD [ "node", "mcp-server.js" ]

```

--------------------------------------------------------------------------------
/gemini-extension.json:
--------------------------------------------------------------------------------

```json
{
  "name": "cloud-run",
  "version": "1.0.0",
  "mcpServers": {
    "cloud-run": {
      "command": "npx",
      "args": ["-y", "@google-cloud/cloud-run-mcp"]
    }
  },
  "contextFileName": "gemini-extension/GEMINI.md"
}

```

--------------------------------------------------------------------------------
/.github/workflows/local-tests.yml:
--------------------------------------------------------------------------------

```yaml
name: Run Local Tests

on:
  push:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

```

--------------------------------------------------------------------------------
/.github/workflows/lint-checker.yml:
--------------------------------------------------------------------------------

```yaml
name: Run Lint Checker

on:
  push:
  pull_request:
    branches: [main]

jobs:
  lint-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint:check

```

--------------------------------------------------------------------------------
/test/local/cloud-api/projects.test.js:
--------------------------------------------------------------------------------

```javascript
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import esmock from 'esmock';

describe('projects', () => {
  describe('generateProjectId', () => {
    it('should generate a project id in the correct format', async () => {
      const { generateProjectId } = await esmock(
        '../../../lib/cloud-api/projects.js',
        {}
      );

      const projectId = generateProjectId();
      assert.ok(projectId.startsWith('mcp-'));
      assert.strictEqual(projectId.length, 11);
    });
  });
});

```

--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish to npm

on:
  release:
    types: [published]

jobs:
  publish-npm:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      # Now configure with the publish service for install.
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://wombat-dressing-room.appspot.com/
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}

```

--------------------------------------------------------------------------------
/test/need-gcp/gcp-projects.test.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { test } from 'node:test';
import { setupProject } from './test-helpers.js';

test('should create a new project and attach billing', async (t) => {
  console.log('Attempting to create a new project and attach billing...');
  await setupProject(t);
});

```

--------------------------------------------------------------------------------
/.kokoro/kokoro_build.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Code under repo is checked out to ${KOKORO_ARTIFACTS_DIR}/git.
# The final directory name in this path is determined by the scm name specified
# in the job configuration.

cd "${KOKORO_ARTIFACTS_DIR}/github/cloud-run-mcp"
.kokoro/run_tests.sh

```

--------------------------------------------------------------------------------
/.kokoro/run_tests.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash
#
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Fail on any error.
set -xe

# cd to project root
cd "$(dirname "$0")"/..

# Install dependencies
npm install

# Run tests
npm run test:workflows # Run tests related to all workflows
npm run test:projects # Run tests related to GCP projects
npm run test:services # Run tests related to services
npm run test:deploy # Run tests related to deployments
npm run test:gcp-auth # Run tests related to GCP authentication
```

--------------------------------------------------------------------------------
/REVIEWING.md:
--------------------------------------------------------------------------------

```markdown
# Reviewing Pull Requests

This document provides guidelines for reviewing Pull Requests.

## Guidelines

### Presubmit Tests

Running presubmit tests is a mandatory requirement for Pull Requests to be eligible for submission.

**Before triggering tests:**
Review the code changes, especially new or modified tests, to ensure they do not contain any malicious or harmful code that could unnecessarily consume or harm Google's resources during test execution. In particular, ensure the tests:

- **Do not modify environment variables:** The tests should not modify environment variables in an unexpected or harmful way.
- **Do not consume excessive resources:** Ensure tests do not create an excessive number of Google Cloud projects or other high-cost resources.
- **Do not contain hardcoded credentials:** Credentials should not be present in the code.
- **Do not rely on untrusted external dependencies:** Any new external dependencies should be reviewed for trustworthiness.

**Triggering tests:**
If the code is safe to run, trigger presubmit tests by adding a comment with `kokoro:run` to the Pull Request. Tests are run via Kokoro.

```

--------------------------------------------------------------------------------
/test/local/gemini-extension.test.js:
--------------------------------------------------------------------------------

```javascript
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..', '..');
const geminiExtensionJsonPath = path.resolve(rootDir, 'gemini-extension.json');

describe('Gemini CLI extension', () => {
  test('gemini-extension.json should be at the root', () => {
    assert.ok(
      fs.existsSync(geminiExtensionJsonPath),
      'gemini-extension.json not found at the project root'
    );
  });

  test('contextFileName from gemini-extension.json should exist', () => {
    const geminiExtensionJson = JSON.parse(
      fs.readFileSync(geminiExtensionJsonPath, 'utf-8')
    );
    const contextFileName = geminiExtensionJson.contextFileName;
    assert.ok(
      contextFileName,
      'contextFileName not found in gemini-extension.json'
    );
    const contextFilePath = path.resolve(rootDir, contextFileName);
    assert.ok(
      fs.existsSync(contextFilePath),
      `context file name '${contextFileName}' not found`
    );
  });
});

```

--------------------------------------------------------------------------------
/example-sources-to-deploy/main.go:
--------------------------------------------------------------------------------

```go
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/


// Sample run-helloworld is a minimal Cloud Run service.
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	log.Print("starting server...")
	http.HandleFunc("/", handler)

	// Determine port for HTTP service.
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("defaulting to port %s", port)
	}

	// Start HTTP server.
	log.Printf("listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	name := os.Getenv("NAME")
	if name == "" {
		name = "World"
	}
	fmt.Fprintf(w, "Hello %s!\n", name)
}
```

--------------------------------------------------------------------------------
/lib/util/helpers.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Helper function to log a message and call the progress callback.
 * @param {string} message - The message to log.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
 */
export async function logAndProgress(
  message,
  progressCallback,
  severity = 'info'
) {
  switch (severity) {
    case 'error':
      console.error(message);
      break;
    case 'warn':
    case 'info':
    case 'debug':
    default:
      console.log(message);
      break;
  }
  if (progressCallback) {
    progressCallback({ level: severity, data: message });
  }
}

```

--------------------------------------------------------------------------------
/test/local/notifications.test.js:
--------------------------------------------------------------------------------

```javascript
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
import esmock from 'esmock';

describe('Tool Notifications', () => {
  it('should send notifications during deployment', async () => {
    const server = {
      registerTool: mock.fn(),
    };

    const { registerTools } = await esmock(
      '../../tools/tools.js',
      {},
      {
        '../../lib/deployment/deployer.js': {
          deploy: () => Promise.resolve({ uri: 'my-uri' }),
        },
      }
    );

    registerTools(server, { gcpCredentialsAvailable: true });

    const handler = server.registerTool.mock.calls.find(
      (call) => call.arguments[0] === 'deploy_local_folder'
    ).arguments[2];

    const sendNotification = mock.fn();

    await handler(
      {
        project: 'my-project',
        region: 'my-region',
        service: 'my-service',
        folderPath: '/my/folder',
      },
      { sendNotification }
    );

    assert.strictEqual(sendNotification.mock.callCount(), 1);
    assert.deepStrictEqual(sendNotification.mock.calls[0].arguments[0], {
      method: 'notifications/message',
      params: {
        level: 'info',
        data: 'Starting deployment of local folder for service my-service in project my-project...',
      },
    });
  });
});

```

--------------------------------------------------------------------------------
/test/local/npx.test.js:
--------------------------------------------------------------------------------

```javascript
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
const mcpServerPath = path.resolve(__dirname, '..', '..', 'mcp-server.js');

describe('The repository is properly structured to be executed using npx', () => {
  test('package.json should be at the root', () => {
    assert.ok(
      fs.existsSync(packageJsonPath),
      'package.json not found at the project root'
    );
  });

  test('package.json should have a bin attribute for cloud-run-mcp', () => {
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
    assert.ok(packageJson.bin, 'bin attribute not found in package.json');
    assert.ok(
      packageJson.bin['cloud-run-mcp'],
      'cloud-run-mcp not found in bin attribute'
    );
  });

  test('mcp-server.js should start with #!/usr/bin/env node', () => {
    const mcpServerContent = fs.readFileSync(mcpServerPath, 'utf-8');
    assert.ok(
      mcpServerContent.startsWith('#!/usr/bin/env node'),
      'mcp-server.js does not start with #!/usr/bin/env node'
    );
  });
});

```

--------------------------------------------------------------------------------
/test/local/mcp-server.test.js:
--------------------------------------------------------------------------------

```javascript
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

describe('MCP Server in stdio mode', () => {
  let client;
  let transport;

  before(async () => {
    transport = new StdioClientTransport({
      command: 'node',
      args: ['mcp-server.js'],
    });
    client = new Client({
      name: 'test-client',
      version: '1.0.0',
    });
    await client.connect(transport);
  });

  after(() => {
    client.close();
  });

  test('should list tools', async () => {
    const response = await client.listTools();

    const tools = response.tools;
    assert(Array.isArray(tools));
    const toolNames = tools.map((t) => t.name);
    assert.deepStrictEqual(
      toolNames.sort(),
      [
        'create_project',
        'deploy_container_image',
        'deploy_file_contents',
        'deploy_local_folder',
        'get_service',
        'get_service_log',
        'list_projects',
        'list_services',
      ].sort()
    );
  });

  test('should list prompts', async () => {
    const response = await client.listPrompts();

    const prompts = response.prompts;
    assert(Array.isArray(prompts));
    const promptNames = prompts.map((p) => p.name);
    assert.deepStrictEqual(promptNames.sort(), ['deploy', 'logs'].sort());
  });
});

```

--------------------------------------------------------------------------------
/test/local/mcp-server-streamable-http.test.js:
--------------------------------------------------------------------------------

```javascript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'child_process';

class MCPClient {
  client = null;
  transport = null;

  constructor(serverName) {
    this.client = new Client({
      name: `mcp-client-for-${serverName}`,
      version: '1.0.0',
      url: `http://localhost:3000/mcp`,
    });
  }

  async connectToServer(serverUrl) {
    this.transport = new StreamableHTTPClientTransport(serverUrl);
    await this.client.connect(this.transport);
  }

  async cleanup() {
    await this.client.close();
  }
}

describe('MCP Server in Streamble HTTP mode', () => {
  let client;
  let serverProcess;

  before(async () => {
    // Start MCP server as a child process
    serverProcess = spawn('node', ['mcp-server.js'], {
      cwd: process.cwd(),
      env: { ...process.env, GCP_STDIO: 'false' },
      stdio: 'inherit',
    });

    // Wait for server to start (better: poll the port, here we just wait)
    await new Promise((resolve) => setTimeout(resolve, 2000));

    client = new MCPClient('http-server');
  });

  after(async () => {
    await client.cleanup();
    if (serverProcess) {
      serverProcess.kill();
    }
  });

  test('should start an HTTP server', async () => {
    await client.connectToServer('http://localhost:3000/mcp');
  });
});

```

--------------------------------------------------------------------------------
/gemini-extension/GEMINI.md:
--------------------------------------------------------------------------------

```markdown
# Cloud Run MCP Server

## Code that can be deployed

Only web servers can be deployed using this MCP server.
The code needs to listen for HTTP requests on the port defined by the $PORT environment variable or 8080.

### Supported languages

- If the code is in Node.js, Python, Go, Java, .NET, PHP, Ruby, a Dockerfile is not needed.
- If the code is in another language, or has any custom dependency needs, a Dockerfile is needed.

### Static-only apps

To deploy static-only applications, create a Dockerfile that serves these static files. For example using `nginx`:

`Dockerfile`

```
FROM nginx:stable

COPY ./static /var/www
COPY ./nginx.conf /etc/nginx/conf.d/default.conf

CMD ["nginx", "-g", "daemon off;"]
```

`nginx.conf`:

```
server {
    listen 8080;
    server_name _;

    root /var/www/;
    index index.html;

    # Force all paths to load either itself (js files) or go through index.html.
    location / {
        try_files $uri /index.html;
    }
}
```

## Google Cloud pre-requisities

The user must have an existing Google Cloud account with billing set up, and ideally an existing Google Cloud project.

If deployment fails because of an access or IAM error, it is likely that the user doesn't have Google Cloud credentials on the local machine.
The user must follow these steps:

1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with their Google account.

2. Set up application credentials using the command:
   ```bash
   gcloud auth application-default login
   ```

```

--------------------------------------------------------------------------------
/example-sources-to-deploy/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# [START cloudrun_helloworld_dockerfile_go]

# Use the official Go image to create a binary.
# This is based on Debian and sets the GOPATH to /go.
# https://hub.docker.com/_/golang
FROM golang:1.23-bookworm AS builder

# Create and change to the app directory.
WORKDIR /app

# Retrieve application dependencies.
# This allows the container build to reuse cached dependencies.
# Expecting to copy go.mod and if present go.sum.
COPY go.* ./
RUN go mod download

# Copy local code to the container image.
COPY . ./

# Build the binary.
RUN go build -v -o server

# Use the official Debian slim image for a lean production container.
# https://hub.docker.com/_/debian
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM debian:bookworm-slim
RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
    ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# Copy the binary to the production image from the builder stage.
COPY --from=builder /app/server /app/server

# Run the web service on container startup.
CMD ["/app/server"]
```

--------------------------------------------------------------------------------
/tools/tools.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
you may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { z } from 'zod';

import {
  registerListProjectsTool,
  registerCreateProjectTool,
  registerListServicesTool,
  registerGetServiceTool,
  registerGetServiceLogTool,
  registerDeployLocalFolderTool,
  registerDeployFileContentsTool,
  registerDeployContainerImageTool,
} from './register-tools.js';

export const registerTools = (server, options = {}) => {
  registerListProjectsTool(server, options);
  registerCreateProjectTool(server, options);
  registerListServicesTool(server, options);
  registerGetServiceTool(server, options);
  registerGetServiceLogTool(server, options);
  registerDeployLocalFolderTool(server, options);
  registerDeployFileContentsTool(server, options);
  registerDeployContainerImageTool(server, options);
};

export const registerToolsRemote = (server, options = {}) => {
  // For remote, use the same registration functions but with effective project/region passed in options
  registerListServicesTool(server, options);
  registerGetServiceTool(server, options);
  registerGetServiceLogTool(server, options);
  registerDeployFileContentsTool(server, options);
  registerDeployContainerImageTool(server, options);
};

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@google-cloud/cloud-run-mcp",
  "version": "1.6.0",
  "type": "module",
  "description": "Cloud Run MCP deployment tool",
  "main": "mcp-server.js",
  "bin": {
    "cloud-run-mcp": "mcp-server.js"
  },
  "scripts": {
    "deploy": "gcloud run deploy cloud-run-mcp --source . --no-invoker-iam-check",
    "test:workflows": "node --test test/need-gcp/workflows/*.test.js",
    "test:deploy": "node test/need-gcp/deployer.test.js",
    "test:projects": "node test/need-gcp/gcp-projects.test.js",
    "test:services": "node test/need-gcp/cloud-run-services.test.js",
    "test:gcp-auth": "node test/need-gcp/gcp-auth-check.test.js",
    "test:prompts": "node --test test/local/prompts.test.js",
    "test:mcp": "node --test test/local/mcp-server.test.js",
    "test:tools": "node --test test/local/tools.test.js",
    "test:local": "node --test test/local/*.test.js test/local/**/*.test.js",
    "test": "c8 --check-coverage npm run test:local",
    "mcp-inspector": "npx @modelcontextprotocol/inspector node mcp-server.js",
    "start": "node mcp-server.js",
    "lint:check": "prettier --check .",
    "lint": "prettier --write ."
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/GoogleCloudPlatform/cloud-run-mcp.git"
  },
  "keywords": [
    "mcp",
    "cloud-run",
    "gcp"
  ],
  "author": "[email protected]",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/GoogleCloudPlatform/cloud-run-mcp/issues"
  },
  "homepage": "https://github.com/GoogleCloudPlatform/cloud-run-mcp#readme",
  "dependencies": {
    "@google-cloud/artifact-registry": "^4.0.1",
    "@google-cloud/billing": "^5.0.1",
    "@google-cloud/cloudbuild": "^5.0.1",
    "@google-cloud/logging": "^11.2.0",
    "@google-cloud/resource-manager": "^6.0.1",
    "@google-cloud/run": "^2.0.1",
    "@google-cloud/service-usage": "^4.1.0",
    "@google-cloud/storage": "^7.16.0",
    "@modelcontextprotocol/sdk": "^1.24.3",
    "archiver": "^7.0.1",
    "dotenv": "^17.2.1",
    "express": "^5.1.0",
    "google-proto-files": "^5.0.0",
    "zod": "^3.25.76"
  },
  "devDependencies": {
    "c8": "^10.1.3",
    "esmock": "^2.7.1",
    "prettier": "3.6.2"
  }
}

```

--------------------------------------------------------------------------------
/lib/cloud-api/metadata.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Helper function to fetch data from GCP metadata server
/**
 * Fetches metadata from the Google Cloud metadata server.
 *
 * @param {string} path - The metadata path to fetch (e.g., `/computeMetadata/v1/...`).
 * @returns {Promise<string>} A promise that resolves to the metadata value as a string.
 * @throws {Error} If the metadata request fails with a non-OK status.
 */
async function fetchMetadata(path) {
  const response = await fetch(`http://metadata.google.internal${path}`, {
    headers: {
      'Metadata-Flavor': 'Google',
    },
  });
  if (!response.ok) {
    throw new Error(`Metadata request failed with status ${response.status}`);
  }
  return await response.text();
}

/**
 * Checks if the GCP metadata server is available and retrieves project ID and region.
 * @returns {Promise<Object|null>} A promise that resolves to an object { project: string, region: string } or null if not available or an error occurs.
 */
export async function checkGCP() {
  try {
    const projectId = await fetchMetadata(
      '/computeMetadata/v1/project/project-id'
    );
    // Expected format: projects/PROJECT_NUMBER/regions/REGION_NAME
    const regionPath = await fetchMetadata(
      '/computeMetadata/v1/instance/region'
    );

    if (projectId && regionPath) {
      const regionParts = regionPath.split('/');
      const region = regionParts[regionParts.length - 1];
      return { project: projectId, region: region };
    }
    return null;
  } catch (error) {
    // Intentionally suppress error logging for cleaner output if metadata server is not available.
    // console.warn('Failed to fetch GCP metadata:', error.message); // Uncomment for debugging
    return null;
  }
}

```

--------------------------------------------------------------------------------
/test/need-gcp/deployer.test.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { describe, test } from 'node:test';
import assert from 'node:assert';
import { deploy, deployImage } from '../../lib/deployment/deployer.js';

const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.argv[2];
if (!projectId) {
  console.error('Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT');
  process.exit(1);
}

describe('Cloud Run Deployments', () => {
  test('should fail to deploy without a service name', async () => {
    const config = {
      projectId: projectId,
      region: 'europe-west1',
      files: ['example-sources-to-deploy/main.go'],
    };
    await assert.rejects(deploy(config), {
      message: 'Error: serviceName is required in the configuration object.',
    });
  });

  test('should fail to deploy image without a service name', async () => {
    const config = {
      projectId: projectId,
      region: 'europe-west1',
      imageUrl: 'gcr.io/cloudrun/hello',
    };
    await assert.rejects(deployImage(config), {
      message: 'Error: serviceName is required in the configuration object.',
    });
  });

  test('should fail to deploy without a project id', async () => {
    const config = {
      serviceName: 'hello-from-image',
      region: 'europe-west1',
      files: ['example-sources-to-deploy/main.go'],
    };
    await assert.rejects(deploy(config), {
      message: 'Error: projectId is required in the configuration object.',
    });
  });

  test('should fail to deploy image without a project id', async () => {
    const config = {
      serviceName: 'hello-from-image',
      region: 'europe-west1',
      imageUrl: 'gcr.io/cloudrun/hello',
    };
    await assert.rejects(deployImage(config), {
      message: 'Error: projectId is required in the configuration object.',
    });
  });
});

```

--------------------------------------------------------------------------------
/test/need-gcp/cloud-run-services.test.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { test } from 'node:test';
import { getServiceLogs, listServices } from '../../lib/cloud-api/run.js';
import assert from 'node:assert';

/**
 * Get the GCP project ID from GOOGLE_CLOUD_PROJECT or command line argument.
 * @returns {string} The GCP project ID
 */
function getProjectId() {
  const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.argv[2];
  if (!projectId) {
    console.error(
      'Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT'
    );
    process.exit(1);
  }
  console.log(`Using Project ID: ${projectId}`);
  return projectId;
}

/**
 * Gets service details from GOOGLE_CLOUD_PROJECT or command line arguments.
 * @returns {{projectId: string, region: string, serviceId: string}} The service details
 */
async function getServiceDetails() {
  const projectId = getProjectId();
  let region, serviceId;

  try {
    const allServices = await listServices(projectId);

    if (!allServices || Object.keys(allServices).length === 0) {
      assert.fail('No services found for the given project.');
    }

    const regions = Object.keys(allServices);
    region = regions[0];

    if (!allServices[region] || allServices[region].length === 0) {
      assert.fail(`No services found in region: ${region}`);
    }

    const serviceName = allServices[region][0].name;
    serviceId = serviceName.split('/').pop();
    console.log(`Using region - ${region} and service ID - ${serviceId}`);

    return { projectId, region, serviceId };
  } catch (error) {
    // Better error handling for the API call itself
    console.error('Error fetching services:', error.message);
    throw error; // Re-throw the error to fail the test
  }
}

test('should list services', async () => {
  const projectId = getProjectId();
  const services = await listServices(projectId);
  console.log('Services found:', services ? Object.keys(services) : 'None');
  console.log('All Services', services);
});

test('should fetch service logs', async () => {
  const { projectId, region, serviceId } = await getServiceDetails();

  const result = await getServiceLogs(projectId, region, serviceId);

  if (result.logs) {
    console.log('\nLog entries:');
    console.log(result.logs);
  } else {
    console.log('No logs found for this service.');
  }
});

```

--------------------------------------------------------------------------------
/test/need-gcp/workflows/deployment-workflows.test.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import fs from 'fs/promises';
import assert from 'node:assert';
import { test, describe, before, after } from 'node:test';
import path from 'path';

import { deploy, deployImage } from '../../../lib/deployment/deployer.js';
import {
  cleanupProject,
  setSourceDeployProjectPermissions,
  setupProject,
} from '../test-helpers.js';

describe('Deployment workflows', () => {
  let projectId;

  before(async () => {
    try {
      projectId = await setupProject();
      await setSourceDeployProjectPermissions(projectId);
    } catch (err) {
      console.error('Error during project creation and setup:', err);
      throw err;
    }
  });

  test('Scenario-1: Starting deployment of hello image...', async () => {
    const configImageDeploy = {
      projectId: projectId,
      serviceName: 'hello-scenario',
      region: 'europe-west1',
      imageUrl: 'gcr.io/cloudrun/hello',
    };
    await deployImage(configImageDeploy);

    console.log('Scenario-1: Deployment completed.');
  });

  test('Scenario-2: Starting deployment with invalid files...', async () => {
    const configFailingBuild = {
      projectId: projectId,
      serviceName: 'example-failing-app',
      region: 'europe-west1',
      files: [
        {
          filename: 'main.txt',
          content:
            'This is not a valid application source file and should cause a build failure.',
        },
      ],
    };
    await assert.rejects(
      deploy(configFailingBuild),
      { message: /ERROR: failed to detect: no buildpacks participating/ },
      'Deployment should have failed with a buildpack detection error'
    );
  });

  test('Scenario-3: Starting deployment of Go app with file content...', async () => {
    const mainGoContent = await fs.readFile(
      path.resolve('example-sources-to-deploy/main.go'),
      'utf-8'
    );
    const goModContent = await fs.readFile(
      path.resolve('example-sources-to-deploy/go.mod'),
      'utf-8'
    );
    const configGoWithContent = {
      projectId: projectId,
      serviceName: 'example-go-app-content',
      region: 'europe-west1',
      files: [
        { filename: 'main.go', content: mainGoContent },
        { filename: 'go.mod', content: goModContent },
      ],
    };
    await deploy(configGoWithContent);
    console.log('Scenario-3: Deployment completed.');
  });

  after(async () => {
    // Clean up: delete the project created for tests
    cleanupProject(projectId);
  });
});

```

--------------------------------------------------------------------------------
/prompts.js:
--------------------------------------------------------------------------------

```javascript
/*
 * Copyright 2025 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { z } from 'zod';

export const registerPrompts = (server) => {
  // Prompts will be registered here.
  server.registerPrompt(
    'deploy',
    {
      description: 'Deploys the current working directory to Cloud Run.',
      argsSchema: {
        name: z
          .string()
          .describe(
            'Name of the Cloud Run service to deploy to.  Defaults to the name of the current directory'
          )
          .optional(),
        project: z.string().describe('Google Cloud project ID').optional(),
        region: z
          .string()
          .describe('Region where the services are located')
          .optional(),
      },
    },
    async ({ name, project, region }) => {
      const serviceNamePrompt =
        name ||
        'a name for the application based on the current working directory.';
      const projectPrompt = project ? ` in project ${project}` : '';
      const regionPrompt = region ? ` in region ${region}` : '';

      return {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use the deploy_local_folder tool to deploy the current folder${projectPrompt}${regionPrompt}. The service name should be ${serviceNamePrompt}`,
            },
          },
        ],
      };
    }
  );

  server.registerPrompt(
    'logs',
    {
      description: 'Gets the logs for a Cloud Run service.',
      argsSchema: {
        service: z
          .string()
          .describe(
            'Name of the Cloud Run service. Defaults to the name of the current directory.'
          )
          .optional(),
        project: z.string().describe('Google Cloud project ID').optional(),
        region: z
          .string()
          .describe('Region where the services are located')
          .optional(),
      },
    },
    async ({ service, project, region }) => {
      const serviceNamePrompt =
        service || 'named for the current working directory';
      const projectPrompt = project ? ` in project ${project}` : '';
      const regionPrompt = region ? ` in region ${region}` : '';

      return {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use get_service_log to get logs${projectPrompt}${regionPrompt} for the service ${serviceNamePrompt}`,
            },
          },
        ],
      };
    }
  );
};

```

--------------------------------------------------------------------------------
/lib/cloud-api/auth.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { GoogleAuth } from 'google-auth-library';

/**
 * Checks for the presence of properly setup Google Cloud Platform (GCP) authentication
 * using the official Google Auth Library for Node.js. If GCP auth is not found, it logs an
 * error message and returns false.
 * @async
 * @returns {Promise<boolean>} A promise that resolves to true if GCP auth are found, and false otherwise.
 */
export async function ensureGCPCredentials() {
  console.error('Checking for Google Cloud Application Default Credentials...');
  try {
    const auth = new GoogleAuth();
    // Attempt to get credentials. This will throw an error if ADC are not found.
    const client = await auth.getClient();
    // Attempt to get an access token to verify credentials are usable.
    // This is done because getClient() might succeed but credentials might be invalid/expired.
    await client.getAccessToken();

    console.log('Application Default Credentials found.');
    return true;
  } catch (error) {
    console.error(
      'ERROR: Google Cloud Application Default Credentials are not set up.'
    );

    if (error.response && error.response.status) {
      console.error(
        `An HTTP error occurred (Status: ${error.response.status}). This often means misconfigured, expired credentials, or a network issue.`
      );
    } else if (error instanceof TypeError) {
      // Catches TypeErrors specifically, which might indicate a malformed response or unexpected data type
      console.error(
        'An unexpected error occurred during credential verification (e.g., malformed response or invalid type).'
      );
    } else {
      // General fallback for any other unexpected errors
      console.error(
        'An unexpected error occurred during credential verification.'
      );
    }

    console.error('\nFor more details or alternative setup methods, consider:');
    console.error(
      '1. If running locally, run: gcloud auth application-default login.'
    );
    console.error(
      '2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.'
    );
    console.error(
      '3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.'
    );
    console.error(
      `\nOriginal error message from Google Auth Library: ${error.message}`
    );

    // Print the stack for debugging
    if (error.stack) {
      console.error('Error stack:', error.stack);
    }
    return false;
  }
}

```

--------------------------------------------------------------------------------
/lib/cloud-api/registry.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { callWithRetry } from './helpers.js';
import { logAndProgress } from '../util/helpers.js';

/**
 * Ensures that an Artifact Registry repository exists.
 * If the repository does not exist, it attempts to create it with the specified format.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string} location - The Google Cloud region for the repository.
 * @param {string} repositoryId - The ID for the Artifact Registry repository.
 * @param {string} [format='DOCKER'] - The format of the repository (e.g., 'DOCKER', 'NPM'). Defaults to 'DOCKER'.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<object>} A promise that resolves with the Artifact Registry repository object.
 * @throws {Error} If there's an error checking or creating the repository.
 */
export async function ensureArtifactRegistryRepoExists(
  context,
  projectId,
  location,
  repositoryId,
  format = 'DOCKER',
  progressCallback
) {
  const parent = `projects/${projectId}/locations/${location}`;
  const repoPath = context.artifactRegistryClient.repositoryPath(
    projectId,
    location,
    repositoryId
  );

  try {
    const [repository] = await callWithRetry(
      () => context.artifactRegistryClient.getRepository({ name: repoPath }),
      `artifactRegistry.getRepository ${repositoryId}`
    );
    await logAndProgress(
      `Repository ${repositoryId} already exists in ${location}.`,
      progressCallback
    );
    return repository;
  } catch (error) {
    if (error.code === 5) {
      await logAndProgress(
        `Repository ${repositoryId} does not exist in ${location}. Creating...`,
        progressCallback
      );
      const repositoryToCreate = {
        format: format,
      };

      const [operation] = await callWithRetry(
        () =>
          context.artifactRegistryClient.createRepository({
            parent: parent,
            repository: repositoryToCreate,
            repositoryId: repositoryId,
          }),
        `artifactRegistry.createRepository ${repositoryId}`
      );
      await logAndProgress(
        `Creating Artifact Registry repository ${repositoryId}...`,
        progressCallback
      );
      const [result] = await operation.promise();
      await logAndProgress(
        `Artifact Registry repository ${result.name} created successfully.`,
        progressCallback
      );
      return result;
    } else {
      const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
      console.error(
        `Error checking/creating repository ${repositoryId}:`,
        error
      );
      await logAndProgress(errorMessage, progressCallback, 'error');
      throw error;
    }
  }
}

```

--------------------------------------------------------------------------------
/lib/cloud-api/billing.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Lists all accessible Google Cloud Billing Accounts.
 * @async
 * @function listBillingAccounts
 * @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
 * each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
 */
export async function listBillingAccounts() {
  const { CloudBillingClient } = await import('@google-cloud/billing');
  const client = new CloudBillingClient();
  try {
    const [accounts] = await client.listBillingAccounts();
    if (!accounts || accounts.length === 0) {
      console.log('No billing accounts found.');
      return [];
    }
    return accounts.map((account) => ({
      name: account.name, // e.g., billingAccounts/0X0X0X-0X0X0X-0X0X0X
      displayName: account.displayName,
      open: account.open,
    }));
  } catch (error) {
    console.error('Error listing GCP billing accounts:', error);
    return [];
  }
}

/**
 * Attaches a Google Cloud Project to a specified Billing Account.
 * @async
 * @function attachProjectToBillingAccount
 * @param {string} projectId - The ID of the project to attach.
 * @param {string} billingAccountName - The resource name of the billing account (e.g., 'billingAccounts/0X0X0X-0X0X0X-0X0X0X').
 * @returns {Promise<object|null>} A promise that resolves to the updated project billing information object if successful, or null on error.
 */
export async function attachProjectToBillingAccount(
  projectId,
  billingAccountName
) {
  const { CloudBillingClient } = await import('@google-cloud/billing');
  const client = new CloudBillingClient();
  const projectName = `projects/${projectId}`;

  if (!projectId) {
    console.error('Error: projectId is required.');
    return null;
  }
  if (
    !billingAccountName ||
    !billingAccountName.startsWith('billingAccounts/')
  ) {
    console.error(
      'Error: billingAccountName is required and must be in the format "billingAccounts/XXXXXX-XXXXXX-XXXXXX".'
    );
    return null;
  }

  try {
    console.log(
      `Attempting to attach project ${projectId} to billing account ${billingAccountName}...`
    );
    const [updatedBillingInfo] = await client.updateProjectBillingInfo({
      name: projectName,
      projectBillingInfo: {
        billingAccountName: billingAccountName,
      },
    });
    console.log(
      `Successfully attached project ${projectId} to billing account ${billingAccountName}.`
    );
    console.log(`Billing enabled: ${updatedBillingInfo.billingEnabled}`);
    return updatedBillingInfo;
  } catch (error) {
    console.error(
      `Error attaching project ${projectId} to billing account ${billingAccountName}:`,
      error.message || error
    );
    // Log more details if available, e.g. error.details
    // if (error.details) console.error("Error details:", error.details);
    return null;
  }
}

```

--------------------------------------------------------------------------------
/lib/util/archive.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { logAndProgress } from './helpers.js';

/**
 * Creates a zip archive in memory from a list of file paths and/or file objects.
 * File objects should have `filename` (string) and `content` (Buffer or string) properties.
 *
 * @param {Array<string|{filename: string, content: Buffer|string}>} files - An array of items to zip.
 * Each item can be a string representing a file/directory path, or an object
 * with `filename` and `content` properties for in-memory files.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<Buffer>} A promise that resolves with a Buffer containing the zip data.
 * @throws {Error} If an input file path is not found, an input item has an invalid format, or an archiver error occurs.
 */
export async function zipFiles(files, progressCallback) {
  const path = await import('path');
  const fs = await import('fs');
  const archiver = (await import('archiver')).default;

  return new Promise((resolve, reject) => {
    logAndProgress('Creating in-memory zip archive...', progressCallback);
    const chunks = [];
    const archive = archiver('zip', {
      zlib: { level: 9 },
    });

    archive.on('data', (chunk) => chunks.push(chunk));
    archive.on('end', () => {
      logAndProgress(
        `Files zipped successfully. Total size: ${archive.pointer()} bytes`,
        progressCallback
      );
      resolve(Buffer.concat(chunks));
    });

    archive.on('warning', (err) => {
      const warningMessage = `Archiver warning: ${err}`;
      logAndProgress(warningMessage, progressCallback, 'warn');
      if (err.code !== 'ENOENT') {
        // ENOENT is often just a warning, others might be more critical for zip
        reject(err);
      }
    });

    archive.on('error', (err) => {
      const errorMessage = `Archiver error: ${err.message}`;
      console.error(errorMessage, err);
      logAndProgress(errorMessage, progressCallback, 'error');
      reject(err);
    });

    files.forEach((file) => {
      if (typeof file === 'object' && 'filename' in file && 'content' in file) {
        archive.append(file.content, { name: file.filename });
      } else if (typeof file === 'string') {
        let pathInput = file;

        // This is a "hack" to better support WSL on Windows. AI agents tend to send path that start with '/c' in that case. Re-write it to '/mnt/c'
        if (pathInput.startsWith('/c')) {
          pathInput = `/mnt${pathInput}`;
        }
        const filePath = path.resolve(pathInput);
        if (!fs.existsSync(filePath)) {
          throw new Error(`File or directory not found: ${filePath}`);
        }

        const stats = fs.statSync(filePath);
        if (stats.isDirectory()) {
          archive.directory(filePath, false);
        } else {
          archive.file(filePath, { name: path.basename(filePath) });
        }
      } else {
        throw new Error(`Invalid file format: ${JSON.stringify(file)}`);
      }
    });

    archive.finalize();
  });
}

```

--------------------------------------------------------------------------------
/lib/cloud-api/storage.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { callWithRetry } from './helpers.js';
import { logAndProgress } from '../util/helpers.js';

/**
 * Ensures that a Google Cloud Storage bucket exists.
 * If the bucket does not exist, it attempts to create it in the specified location.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {string} bucketName - The name of the storage bucket.
 * @param {string} [location='us'] - The location to create the bucket in if it doesn't exist. Defaults to 'us'.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<import('@google-cloud/storage').Bucket>} A promise that resolves with the GCS Bucket object.
 * @throws {Error} If there's an error checking or creating the bucket.
 */
export async function ensureStorageBucketExists(
  context,
  bucketName,
  location = 'us',
  progressCallback
) {
  const bucket = context.storage.bucket(bucketName);
  try {
    const [exists] = await callWithRetry(
      () => bucket.exists(),
      `storage.bucket.exists ${bucketName}`
    );
    if (exists) {
      await logAndProgress(
        `Bucket ${bucketName} already exists.`,
        progressCallback
      );
      return bucket;
    } else {
      await logAndProgress(
        `Bucket ${bucketName} does not exist. Creating in location ${location}...`,
        progressCallback
      );
      try {
        const [createdBucket] = await callWithRetry(
          () =>
            context.storage.createBucket(bucketName, { location: location }),
          `storage.createBucket ${bucketName}`
        );
        await logAndProgress(
          `Storage bucket ${createdBucket.name} created successfully in ${location}.`,
          progressCallback
        );
        return createdBucket;
      } catch (createError) {
        const errorMessage = `Failed to create storage bucket ${bucketName}. Error details: ${createError.message}`;
        console.error(
          `Failed to create storage bucket ${bucketName}. Error details:`,
          createError
        );
        await logAndProgress(errorMessage, progressCallback, 'error');
        throw createError;
      }
    }
  } catch (error) {
    const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
    console.error(`Error checking/creating bucket ${bucketName}:`, error);
    await logAndProgress(errorMessage, progressCallback, 'error');
    throw error;
  }
}

/**
 * Uploads a buffer to a specified Google Cloud Storage bucket and blob name.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {import('@google-cloud/storage').Bucket} bucket - The Google Cloud Storage bucket object.
 * @param {Buffer} buffer - The buffer containing the data to upload.
 * @param {string} destinationBlobName - The name for the blob in the bucket.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<import('@google-cloud/storage').File>} A promise that resolves with the GCS File object representing the uploaded blob.
 * @throws {Error} If the upload fails.
 */
export async function uploadToStorageBucket(
  context,
  bucket,
  buffer,
  destinationBlobName,
  progressCallback
) {
  try {
    await logAndProgress(
      `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
      progressCallback
    );
    await callWithRetry(
      () => bucket.file(destinationBlobName).save(buffer),
      `storage.bucket.file.save ${destinationBlobName}`
    );
    await logAndProgress(
      `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
      progressCallback
    );
    return bucket.file(destinationBlobName);
  } catch (error) {
    const errorMessage = `Error uploading buffer: ${error.message}`;
    console.error(`Error uploading buffer:`, error);
    await logAndProgress(errorMessage, progressCallback, 'error');
    throw error;
  }
}

```

--------------------------------------------------------------------------------
/test/local/prompts.test.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
import { registerPrompts } from '../../prompts.js';

describe('registerPrompts', () => {
  it('should register deploy and logs prompts', () => {
    const server = {
      registerPrompt: mock.fn(),
    };

    registerPrompts(server);

    assert.strictEqual(server.registerPrompt.mock.callCount(), 2);
    assert.strictEqual(
      server.registerPrompt.mock.calls[0].arguments[0],
      'deploy'
    );
    assert.strictEqual(
      server.registerPrompt.mock.calls[1].arguments[0],
      'logs'
    );
  });

  describe('deploy prompt', () => {
    it('should use the provided name', async () => {
      const server = {
        registerPrompt: mock.fn(),
      };
      registerPrompts(server);
      const handler = server.registerPrompt.mock.calls[0].arguments[2];
      const result = await handler({ name: 'my-service' });
      assert.deepStrictEqual(result, {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use the deploy_local_folder tool to deploy the current folder. The service name should be my-service`,
            },
          },
        ],
      });
    });

    it('should use the current directory name', async () => {
      const server = {
        registerPrompt: mock.fn(),
      };
      registerPrompts(server);
      const handler = server.registerPrompt.mock.calls[0].arguments[2];
      const result = await handler({});
      const serviceName =
        'a name for the application based on the current working directory.';
      assert.deepStrictEqual(result, {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use the deploy_local_folder tool to deploy the current folder. The service name should be ${serviceName}`,
            },
          },
        ],
      });
    });

    it('should use the provided project and region', async () => {
      const server = {
        registerPrompt: mock.fn(),
      };
      registerPrompts(server);
      const handler = server.registerPrompt.mock.calls[0].arguments[2];
      const result = await handler({
        name: 'my-service',
        project: 'my-project',
        region: 'my-region',
      });
      assert.deepStrictEqual(result, {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use the deploy_local_folder tool to deploy the current folder in project my-project in region my-region. The service name should be my-service`,
            },
          },
        ],
      });
    });
  });

  describe('logs prompt', () => {
    it('should use the provided service name', async () => {
      const server = {
        registerPrompt: mock.fn(),
      };
      registerPrompts(server);
      const handler = server.registerPrompt.mock.calls[1].arguments[2];
      const result = await handler({ service: 'my-service' });
      assert.deepStrictEqual(result, {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use get_service_log to get logs for the service my-service`,
            },
          },
        ],
      });
    });

    it('should use the current directory name for the service name', async () => {
      const server = {
        registerPrompt: mock.fn(),
      };
      registerPrompts(server);
      const handler = server.registerPrompt.mock.calls[1].arguments[2];
      const result = await handler({});
      const serviceName = 'named for the current working directory';
      assert.deepStrictEqual(result, {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use get_service_log to get logs for the service ${serviceName}`,
            },
          },
        ],
      });
    });

    it('should use the provided project and region', async () => {
      const server = {
        registerPrompt: mock.fn(),
      };
      registerPrompts(server);
      const handler = server.registerPrompt.mock.calls[1].arguments[2];
      const result = await handler({
        service: 'my-service',
        project: 'my-project',
        region: 'my-region',
      });
      assert.deepStrictEqual(result, {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Use get_service_log to get logs in project my-project in region my-region for the service my-service`,
            },
          },
        ],
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/lib/cloud-api/helpers.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Calls a function with retry logic for GCP API calls.
 * Retries on gRPC error code 7 (PERMISSION_DENIED).
 * @param {Function} fn The function to call.
 * @param {string} description A description of the function being called, for logging.
 * @returns {Promise<any>} The result of the function.
 */
export async function callWithRetry(fn, description) {
  const maxRetries = 7;
  const initialBackoff = 1000; // 1 second
  let retries = 0;

  while (true) {
    try {
      return await fn();
    } catch (error) {
      if (error.code === 7 && retries < maxRetries) {
        retries++;
        let backoff;
        if (retries === 1) {
          backoff = 15000; // 15 seconds for the first retry
        } else {
          backoff = initialBackoff * Math.pow(2, retries - 2);
        }
        console.warn(
          `API call "${description}" failed with PERMISSION_DENIED. Retrying in ${
            backoff / 1000
          }s... (attempt ${retries}/${maxRetries})`
        );
        await new Promise((resolve) => setTimeout(resolve, backoff));
      } else {
        throw error;
      }
    }
  }
}

/**
 * Checks if a single Google Cloud API is enabled and enables it if not.
 *
 * @param {object} serviceUsageClient - The Service Usage client.
 * @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
 * @param {string} api - The API identifier (e.g., 'run.googleapis.com').
 * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
 * @returns {Promise<void>} A promise that resolves when the API is enabled.
 * @throws {Error} If the API fails to enable or if there's an issue checking its status.
 */
async function checkAndEnableApi(
  serviceUsageClient,
  serviceName,
  api,
  progressCallback
) {
  const [service] = await callWithRetry(
    () => serviceUsageClient.getService({ name: serviceName }),
    `getService ${api}`
  );
  if (service.state !== 'ENABLED') {
    const message = `API [${api}] is not enabled. Enabling...`;
    console.log(message);
    if (progressCallback) progressCallback({ level: 'info', data: message });

    const [operation] = await callWithRetry(
      () => serviceUsageClient.enableService({ name: serviceName }),
      `enableService ${api}`
    );
    await operation.promise();
  }
}

/**
 * Ensures that the specified Google Cloud APIs are enabled for the given project.
 * If an API is not enabled, it attempts to enable it.  Retries any failure once after 1s.
 * Throws an error if an API cannot be enabled.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
 * @param {function(string, string=): void} progressCallback - A function to call with progress updates.
 * The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
 * @throws {Error} If an API fails to enable or if there's an issue checking its status.
 * @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
 */
export async function ensureApisEnabled(
  context,
  projectId,
  apis,
  progressCallback
) {
  const message = 'Checking and enabling required APIs...';
  console.log(message);
  if (progressCallback) progressCallback({ level: 'info', data: message });

  for (const api of apis) {
    const serviceName = `projects/${projectId}/services/${api}`;
    try {
      await checkAndEnableApi(
        context.serviceUsageClient,
        serviceName,
        api,
        progressCallback
      );
    } catch (error) {
      // First attempt failed, log a warning and retry once after a delay.
      const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
      console.warn(warnMsg);
      if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });

      await new Promise((resolve) => setTimeout(resolve, 1000));
      try {
        await checkAndEnableApi(
          context.serviceUsageClient,
          serviceName,
          api,
          progressCallback
        );
      } catch (retryError) {
        // If the retry also fails, throw an error.
        const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
        console.error(errorMessage, retryError);
        if (progressCallback)
          progressCallback({ level: 'error', data: errorMessage });
        throw new Error(errorMessage);
      }
    }
  }
  const successMsg = 'All required APIs are enabled.';
  console.log(successMsg);
  if (progressCallback) progressCallback({ level: 'info', data: successMsg });
}

```

--------------------------------------------------------------------------------
/test/need-gcp/test-helpers.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import assert from 'node:assert';
import {
  createProjectAndAttachBilling,
  deleteProject,
  generateProjectId,
} from '../../lib/cloud-api/projects.js';
import {
  callWithRetry,
  ensureApisEnabled,
} from '../../lib/cloud-api/helpers.js';

/**
 * Gets project number from project ID.
 * @param {string} projectId
 * @returns {Promise<string>} project number
 */
export async function getProjectNumber(projectId) {
  const { ProjectsClient } = await import('@google-cloud/resource-manager');
  const client = new ProjectsClient();
  try {
    const [project] = await client.getProject({
      name: `projects/${projectId}`,
    });
    // project.name is in format projects/123456
    return project.name.split('/')[1];
  } catch (error) {
    console.error(
      `Error getting project number for project ${projectId}:`,
      error.message
    );
    throw error;
  }
}

/**
 * Adds an IAM policy binding to a project.
 * @param {string} projectId The project ID.
 * @param {string} member The member to add, e.g., 'user:[email protected]'.
 * @param {string} role The role to grant, e.g., 'roles/viewer'.
 */
export async function addIamPolicyBinding(projectId, member, role) {
  const { ProjectsClient } = await import('@google-cloud/resource-manager');
  const client = new ProjectsClient();

  console.log(
    `Adding IAM binding for ${member} with role ${role} to project ${projectId}`
  );

  try {
    const [policy] = await client.getIamPolicy({
      resource: `projects/${projectId}`,
    });

    console.log('Current IAM Policy:', JSON.stringify(policy, null, 2));

    // Check if the binding already exists
    const binding = policy.bindings.find((b) => b.role === role);
    if (binding) {
      if (!binding.members.includes(member)) {
        binding.members.push(member);
      }
    } else {
      policy.bindings.push({
        role: role,
        members: [member],
      });
    }

    console.log('Updated IAM Policy:', JSON.stringify(policy, null, 2));

    // Set the updated policy
    await client.setIamPolicy({
      resource: `projects/${projectId}`,
      policy: policy,
    });

    console.log(
      `Successfully added IAM binding for ${member} with role ${
        role
      } to project ${projectId}`
    );
  } catch (error) {
    console.error(
      `Error adding IAM policy binding to project ${projectId}:`,
      error.message
    );
    throw error;
  }
}

/**
 * Create project, attach billing.
 * @returns {Promise<string>} projectId
 */
export async function setupProject() {
  const projectId = 'test-' + generateProjectId();
  console.log(`Generated project ID: ${projectId}`);
  const parent = process.env.GCP_PARENT || process.argv[2];
  const newProjectResult = await createProjectAndAttachBilling(
    projectId,
    parent
  );
  assert(newProjectResult, 'newProjectResult should not be null');
  assert(
    newProjectResult.projectId,
    'newProjectResult.projectId should not be null'
  );
  assert(
    newProjectResult.billingMessage,
    'newProjectResult.billingMessage should not be null'
  );
  assert(
    newProjectResult.billingMessage.startsWith(
      `Project ${newProjectResult.projectId} created successfully.`
    ),
    'newProjectResult.billingMessage should start with success message'
  );
  console.log(`Successfully created project: ${newProjectResult.projectId}`);
  console.log(newProjectResult.billingMessage);

  return projectId;
}

/**
 * Delete project
 * @param {string} projectId
 */
export async function cleanupProject(projectId) {
  try {
    await deleteProject(projectId);
    console.log(`Successfully deleted project: ${projectId}`);
  } catch (e) {
    console.error(`Failed to delete project ${projectId}:`, e.message);
  }
}

/**
 * Enable APIs and set IAM permissions for source deployments.
 * Note: This function is only needed for Cloud Build since it uses the compute
 * default service account. The compute service account needs the editor role to
 * deploy to Cloud Run, which is usually granted by default, but in this case we
 * ensure it due to restrictions in some organizations.
 * @param {string} projectId
 */
export async function setSourceDeployProjectPermissions(projectId) {
  const { ServiceUsageClient } = await import('@google-cloud/service-usage');
  const serviceUsageClient = new ServiceUsageClient({ projectId });
  const context = {
    serviceUsageClient: serviceUsageClient,
  };
  await ensureApisEnabled(context, projectId, ['run.googleapis.com']);
  console.log('Adding editor role to Compute SA...');
  const projectNumber = await getProjectNumber(projectId);
  const member = `serviceAccount:${projectNumber}[email protected]`;
  await callWithRetry(
    () => addIamPolicyBinding(projectId, member, 'roles/editor'),
    `addIamPolicyBinding roles/editor to ${member}`
  );
  console.log('Compute SA editor role added.');
}

```

--------------------------------------------------------------------------------
/lib/cloud-api/build.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { callWithRetry } from './helpers.js';
import { logAndProgress } from '../util/helpers.js';

const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet

/**
 * Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
 * It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
 * Waits for the build to complete and returns the build result.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string} location - The Google Cloud region for the build.
 * @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
 * @param {string} sourceBlobName - The GCS blob name (the zip file) for the source code.
 * @param {string} targetRepoName - The name of the target Artifact Registry repository (used for context, not directly in build steps).
 * @param {string} targetImageUrl - The full Artifact Registry URL for the image to be built (e.g., `location-docker.pkg.dev/project/repo/image:tag`).
 * @param {boolean} hasDockerfile - Indicates whether a Dockerfile is present in the source to guide the build process.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<object>} A promise that resolves with the completed Cloud Build object.
 * @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
 */
export async function triggerCloudBuild(
  context,
  projectId,
  location,
  sourceBucketName,
  sourceBlobName,
  targetRepoName,
  targetImageUrl,
  hasDockerfile,
  progressCallback
) {
  let buildSteps;

  if (hasDockerfile) {
    buildSteps = [
      {
        name: 'gcr.io/cloud-builders/docker',
        args: ['build', '-t', targetImageUrl, '.'],
        dir: '/workspace',
      },
    ];
  } else {
    buildSteps = [
      {
        name: 'gcr.io/k8s-skaffold/pack',
        entrypoint: 'pack',
        args: [
          'build',
          targetImageUrl,
          '--builder',
          'gcr.io/buildpacks/builder:latest',
        ],
        dir: '/workspace',
      },
    ];
  }

  const build = {
    source: {
      storageSource: {
        bucket: sourceBucketName,
        object: sourceBlobName,
      },
    },
    steps: buildSteps,
    images: [targetImageUrl],
  };

  await logAndProgress(
    `Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
    progressCallback
  );
  const [operation] = await callWithRetry(
    () =>
      context.cloudBuildClient.createBuild({
        projectId: projectId,
        build: build,
      }),
    'cloudBuild.createBuild'
  );

  await logAndProgress(`Cloud Build job started...`, progressCallback);
  const buildId = operation.metadata.build.id;
  let completedBuild;
  while (true) {
    const [getBuildOperation] = await callWithRetry(
      () =>
        context.cloudBuildClient.getBuild({
          projectId: projectId,
          id: buildId,
        }),
      `cloudBuild.getBuild ${buildId}`
    );
    if (
      ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
        getBuildOperation.status
      )
    ) {
      completedBuild = getBuildOperation;
      break;
    }
    await logAndProgress(
      `Build status: ${getBuildOperation.status}. Waiting...`,
      progressCallback,
      'debug'
    );
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }

  if (completedBuild.status === 'SUCCESS') {
    await logAndProgress(
      `Cloud Build job ${buildId} completed successfully.`,
      progressCallback
    );
    await logAndProgress(
      `Image built: ${completedBuild.results.images[0].name}`,
      progressCallback
    );
    return completedBuild;
  } else {
    const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
    await logAndProgress(failureMessage, progressCallback, 'error');
    const logsMessage = `Build logs: ${completedBuild.logUrl}`;
    await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error

    let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
    try {
      const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
      await logAndProgress(
        `Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
        progressCallback,
        'debug'
      );

      // Wait for a short period to allow logs to propagate
      await new Promise((resolve) =>
        setTimeout(resolve, DELAY_WAIT_FOR_BUILD_LOGS)
      );

      // Fetch the most recent N log entries
      const [entries] = await callWithRetry(
        () =>
          context.loggingClient.getEntries({
            filter: logFilter,
            orderBy: 'timestamp desc', // Get latest logs first
            pageSize: BUILD_LOGS_LINES_TO_FETCH,
          }),
        `logging.getEntries for build ${buildId}`
      );

      if (entries && entries.length > 0) {
        // Entries are newest first, reverse for chronological order of the snippet
        const logLines = entries.reverse().map((entry) => entry.data || '');
        if (logLines.length > 0) {
          buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
          await logAndProgress(
            `Successfully fetched snippet of build logs for ${buildId}.`,
            progressCallback,
            'info'
          );
        }
      } else {
        await logAndProgress(
          `No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
          progressCallback,
          'warn'
        );
      }
    } catch (logError) {
      console.error(`Error fetching build logs for ${buildId}:`, logError);
      await logAndProgress(
        `Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
        progressCallback,
        'warn'
      );
      // buildLogsSnippet already contains the Log URL as a fallback
    }
    throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
  }
}

```

--------------------------------------------------------------------------------
/lib/cloud-api/projects.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {
  listBillingAccounts,
  attachProjectToBillingAccount,
} from './billing.js';

/**
 * Lists all accessible Google Cloud Platform projects.
 * @async
 * @function listProjects
 * @returns {Promise<Array<{id: string}>>} A promise that resolves to an array of project objects, each with an 'id' property. Returns an empty array on error.
 */
export async function listProjects() {
  const { ProjectsClient } = await import('@google-cloud/resource-manager');
  const client = new ProjectsClient();
  try {
    const [projects] = await client.searchProjects();
    return projects.map((project) => ({
      id: project.projectId,
    }));
  } catch (error) {
    console.error('Error listing GCP projects:', error);
    return [];
  }
}

/**
 * Creates a compliant GCP project ID in the format 'mcp-cvc-cvc', where 'c' is a consonant and 'v' is a vowel.
 * @function generateProjectId
 * @returns {string} A randomly generated, compliant GCP project ID in the format 'mcp-cvc-cvc'.
 */
export function generateProjectId() {
  const consonants = 'bcdfghjklmnpqrstvwxyz';
  const vowels = 'aeiou';

  const getRandomChar = (source) =>
    source.charAt(Math.floor(Math.random() * source.length));

  const generateCVC = () => {
    const c1 = getRandomChar(consonants);
    const v = getRandomChar(vowels);
    const c2 = getRandomChar(consonants);
    return `${c1}${v}${c2}`;
  };

  const cvc1 = generateCVC();
  const cvc2 = generateCVC();
  return `mcp-${cvc1}-${cvc2}`;
}

/**
 * Creates a new Google Cloud Platform project.
 * @async
 * @function createProject
 * @param {string} [projectId] - Optional. The desired ID for the new project. If not provided, a compliant ID will be generated automatically (e.g., app-cvc-cvc).
 * @param {string} [parent] - Optional. The resource name of the parent under which the project is to be created. e.g., "organizations/123" or "folders/456".
 * @returns {Promise<{projectId: string}|null>} A promise that resolves to an object containing the new project's ID.
 */
export async function createProject(projectId, parent) {
  const { ProjectsClient } = await import('@google-cloud/resource-manager');
  const client = new ProjectsClient();
  let projectIdToUse = projectId;

  if (!projectIdToUse) {
    projectIdToUse = generateProjectId();
    console.log(`Project ID not provided, generated ID: ${projectIdToUse}`);
  }

  try {
    const projectPayload = { projectId: projectIdToUse, parent };

    console.log(`Attempting to create project with ID: ${projectIdToUse}`);

    const [operation] = await client.createProject({ project: projectPayload });

    const [createdProjectResponse] = await operation.promise();

    console.log(
      `Project ${createdProjectResponse.projectId} created successfully.`
    );
    return {
      projectId: createdProjectResponse.projectId,
    };
  } catch (error) {
    console.error(
      `Error creating GCP project ${projectIdToUse}:`,
      error.message
    );
    throw error; // Re-throw to be caught by the caller
  }
}

/**
 * Creates a new Google Cloud Platform project and attempts to attach it to the first available billing account.
 * @async
 * @function createProjectAndAttachBilling
 * @param {string} [projectIdParam] - Optional. The desired ID for the new project.
 * @param {string} [parent] - Optional. The resource name of the parent under which the project is to be created. e.g., "organizations/123" or "folders/456".
 * @returns {Promise<{projectId: string, billingMessage: string}>} A promise that resolves to an object containing the project ID and a billing status message.
 */
export async function createProjectAndAttachBilling(projectIdParam, parent) {
  let newProject;
  try {
    newProject = await createProject(projectIdParam, parent);
  } catch (error) {
    throw new Error(`Failed to create project: ${error.message}`);
  }

  if (!newProject || !newProject.projectId) {
    throw new Error('Project creation did not return a valid project ID.');
  }

  const { projectId } = newProject;
  let billingMessage = `Project ${projectId} created successfully.`;

  try {
    const billingAccounts = await listBillingAccounts();
    if (billingAccounts && billingAccounts.length > 0) {
      const firstBillingAccount = billingAccounts.find((acc) => acc.open); // Prefer an open account
      if (firstBillingAccount) {
        console.log(
          `Found billing account: ${firstBillingAccount.displayName} (${firstBillingAccount.name}). Attempting to attach project ${projectId}.`
        );
        const billingInfo = await attachProjectToBillingAccount(
          projectId,
          firstBillingAccount.name
        );
        if (billingInfo && billingInfo.billingEnabled) {
          billingMessage += ` It has been attached to billing account ${firstBillingAccount.displayName}.`;
        } else {
          billingMessage += ` However, it could not be attached to billing account ${firstBillingAccount.displayName} or billing not enabled. Please check manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
        }
      } else {
        const allBillingAccounts = billingAccounts
          .map((b) => `${b.displayName} (Open: ${b.open})`)
          .join(', ');
        billingMessage += ` However, no open billing accounts were found. Available (may not be usable): ${allBillingAccounts || 'None'}. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
      }
    } else {
      billingMessage += ` However, no billing accounts were found. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
    }
  } catch (billingError) {
    console.error(
      `Error during billing operations for project ${projectId}:`,
      billingError
    );
    billingMessage += ` However, an error occurred during billing operations: ${billingError.message}. Please check manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
  }

  return { projectId, billingMessage };
}

/**
 * Deletes a Google Cloud Platform project.
 * @async
 * @function deleteProject
 * @param {string} projectId - The ID of the project to delete.
 * @returns {Promise<void>} A promise that resolves when the delete operation is initiated.
 */
export async function deleteProject(projectId) {
  const { ProjectsClient } = await import('@google-cloud/resource-manager');
  const client = new ProjectsClient();
  try {
    console.log(`Attempting to delete project with ID: ${projectId}`);
    await client.deleteProject({ name: `projects/${projectId}` });
    console.log(`Project ${projectId} deletion initiated successfully.`);
  } catch (error) {
    console.error(`Error deleting GCP project ${projectId}:`, error.message);
    throw error; // Re-throw to be caught by the caller
  }
}

```

--------------------------------------------------------------------------------
/mcp-server.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
// Support SSE for backward compatibility
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
// Support stdio, as it is easier to use locally
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { registerTools, registerToolsRemote } from './tools/tools.js';
import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { registerPrompts } from './prompts.js';
import { checkGCP } from './lib/cloud-api/metadata.js';
import { ensureGCPCredentials } from './lib/cloud-api/auth.js';
import 'dotenv/config';

const gcpInfo = await checkGCP();
let gcpCredentialsAvailable = false;

/**
 * Ensure that console.log and console.error are compatible with stdio.
 * (Right now, it just disables them)
 */
function makeLoggingCompatibleWithStdio() {
  // redirect all console.log (which usually go to to stdout) to stderr.
  console.log = console.error;
}

function shouldStartStdio() {
  if (process.env.GCP_STDIO === 'false' || (gcpInfo && gcpInfo.project)) {
    return false;
  }
  return true;
}

if (shouldStartStdio()) {
  makeLoggingCompatibleWithStdio();
}

// Read default configurations from environment variables
const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || undefined;
const envRegion = process.env.GOOGLE_CLOUD_REGION;
const defaultServiceName = process.env.DEFAULT_SERVICE_NAME;
const skipIamCheck = process.env.SKIP_IAM_CHECK !== 'false';
const enableHostValidation = process.env.ENABLE_HOST_VALIDATION === 'true';
const allowedHosts = process.env.ALLOWED_HOSTS
  ? process.env.ALLOWED_HOSTS.split(',')
  : undefined;

async function getServer() {
  // Create an MCP server with implementation details
  const server = new McpServer(
    {
      name: 'cloud-run',
      version: '1.0.0',
    },
    { capabilities: { logging: {} } }
  );

  // this is no-op handler is required for mcp-inspector to function due to a mismatch between the SDK mcp-inspector
  server.server.setRequestHandler(SetLevelRequestSchema, (request) => {
    console.log(`Log Level: ${request.params.level}`);
    return {};
  });

  // Get GCP metadata info once
  const gcpInfo = await checkGCP();

  // Determine the effective project and region based on priority: Env Var > GCP Metadata > Hardcoded default
  const effectiveProjectId =
    envProjectId || (gcpInfo && gcpInfo.project) || undefined;
  const effectiveRegion =
    envRegion || (gcpInfo && gcpInfo.region) || 'europe-west1';

  if (shouldStartStdio() || !(gcpInfo && gcpInfo.project)) {
    console.log('Using tools optimized for local or stdio mode.');
    // Pass the determined defaults to the local tool registration
    await registerTools(server, {
      defaultProjectId: effectiveProjectId,
      defaultRegion: effectiveRegion,
      defaultServiceName,
      skipIamCheck,
      gcpCredentialsAvailable,
    });
  } else {
    console.log(
      `Running on GCP project: ${effectiveProjectId}, region: ${effectiveRegion}. Using tools optimized for remote use.`
    );
    // Pass the determined defaults to the remote tool registration
    await registerToolsRemote(server, {
      defaultProjectId: effectiveProjectId,
      defaultRegion: effectiveRegion,
      defaultServiceName,
      skipIamCheck,
      gcpCredentialsAvailable,
    });
  }

  // Register prompts with the server
  registerPrompts(server);

  return server;
}

// stdio
if (shouldStartStdio()) {
  gcpCredentialsAvailable = await ensureGCPCredentials();
  const stdioTransport = new StdioServerTransport();
  const server = await getServer();
  await server.connect(stdioTransport);
  console.log('Cloud Run MCP server stdio transport connected');
} else {
  // non-stdio mode
  console.log('Stdio transport mode is turned off.');
  gcpCredentialsAvailable = await ensureGCPCredentials();

  const app = enableHostValidation
    ? createMcpExpressApp({ allowedHosts })
    : createMcpExpressApp({ host: null });

  if (!enableHostValidation) {
    console.warn(
      `Warning: Server is running without DNS rebinding protection. ` +
        'Consider enabling host validation by passing env variable ENABLE_HOST_VALIDATION=true and adding the ALLOWED_HOSTS to restrict allowed hosts'
    );
  }

  app.post('/mcp', async (req, res) => {
    console.log('/mcp Received:', req.body);
    const server = await getServer();
    try {
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
      });
      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);
      res.on('close', () => {
        console.log('Request closed');
        transport.close();
        server.close();
      });
    } catch (error) {
      console.error('Error handling MCP request:', error);
      if (!res.headersSent) {
        res.status(500).json({
          jsonrpc: '2.0',
          error: {
            code: -32603,
            message: 'Internal server error',
          },
          id: null,
        });
      }
    }
  });

  app.get('/mcp', async (req, res) => {
    console.log('Received GET MCP request');
    res.writeHead(405).end(
      JSON.stringify({
        jsonrpc: '2.0',
        error: {
          code: -32000,
          message: 'Method not allowed.',
        },
        id: null,
      })
    );
  });

  app.delete('/mcp', async (req, res) => {
    console.log('Received DELETE MCP request');
    res.writeHead(405).end(
      JSON.stringify({
        jsonrpc: '2.0',
        error: {
          code: -32000,
          message: 'Method not allowed.',
        },
        id: null,
      })
    );
  });

  // Support SSE for baackward compatibility
  const sseTransports = {};

  // Legacy SSE endpoint for older clients
  app.get('/sse', async (req, res) => {
    console.log('/sse Received:', req.body);
    const server = await getServer();
    // Create SSE transport for legacy clients
    const transport = new SSEServerTransport('/messages', res);
    sseTransports[transport.sessionId] = transport;

    res.on('close', () => {
      delete sseTransports[transport.sessionId];
    });

    await server.connect(transport);
  });

  // Legacy message endpoint for older clients
  app.post('/messages', async (req, res) => {
    console.log('/messages Received:', req.body);
    const sessionId = req.query.sessionId;
    const transport = sseTransports[sessionId];
    if (transport) {
      await transport.handlePostMessage(req, res, req.body);
    } else {
      res.status(400).send('No transport found for sessionId');
    }
  });

  // Start the server
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Cloud Run MCP server listening on port ${PORT}`);
  });
}

// Handle server shutdown
process.on('SIGINT', async () => {
  console.log('Shutting down server...');
  process.exit(0);
});

```

--------------------------------------------------------------------------------
/test/need-gcp/gcp-auth-check.test.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES, OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import assert from 'node:assert/strict';
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
import { GoogleAuth } from 'google-auth-library';
import { ensureGCPCredentials } from '../../lib/cloud-api/auth.js';

describe('ensureGCPCredentials', () => {
  let originalConsoleLog;
  let originalConsoleError;

  let capturedConsoleOutput;

  let consoleLogMockFn;
  let consoleErrorMockFn;

  let getClientMock;
  let getAccessTokenMock;

  beforeEach(() => {
    // Store original methods
    originalConsoleLog = console.log;
    originalConsoleError = console.error;

    capturedConsoleOutput = []; // Reset for each test

    // Create mock.fn instances and assign them to global console/process
    consoleLogMockFn = mock.fn((...args) => {
      capturedConsoleOutput.push(args.join(' '));
    });
    consoleErrorMockFn = mock.fn((...args) => {
      capturedConsoleOutput.push(args.join(' '));
    });

    // Overwrite the global console and process methods
    console.log = consoleLogMockFn;
    console.error = consoleErrorMockFn;

    // Mock GoogleAuth.prototype.getClient and AuthClient.prototype.getAccessToken.
    const mockClient = {
      getAccessToken: mock.fn(async () => ({ token: 'mock-token' })),
    };
    getAccessTokenMock = mockClient.getAccessToken;
    getClientMock = mock.method(
      GoogleAuth.prototype,
      'getClient',
      async () => mockClient
    );
  });

  afterEach(() => {
    // Restore original methods
    console.log = originalConsoleLog;
    console.error = originalConsoleError;

    // Restore other mocks created with `mock.method`
    mock.restoreAll();
  });

  it('should return true when ADC are found', async () => {
    const result = await ensureGCPCredentials();
    assert.strictEqual(result, true, 'Should return true on success');

    assert.deepStrictEqual(
      capturedConsoleOutput,
      [
        'Checking for Google Cloud Application Default Credentials...',
        'Application Default Credentials found.',
      ],
      'Console output should indicate successful ADC discovery'
    );

    assert.strictEqual(
      consoleErrorMockFn.mock.callCount(),
      1,
      'console.error should be called once (checking message)'
    );
    assert.strictEqual(
      consoleLogMockFn.mock.callCount(),
      1,
      'console.log should be called once (for success message)'
    );

    assert.strictEqual(
      getClientMock.mock.callCount(),
      1,
      'GoogleAuth.getClient should be called once'
    );
    assert.strictEqual(
      getAccessTokenMock.mock.callCount(),
      1,
      'client.getAccessToken should be called once'
    );
  });

  it('should return false and log error when ADC are not found (generic error)', async () => {
    const errorMessage = 'Failed to find credentials.';
    const errorWithStack = new Error(errorMessage);
    getClientMock.mock.mockImplementation(async () => {
      throw errorWithStack;
    });

    const result = await ensureGCPCredentials();
    assert.strictEqual(result, false, 'Should return false on failure');

    const expectedOutput = [
      'Checking for Google Cloud Application Default Credentials...',
      'ERROR: Google Cloud Application Default Credentials are not set up.',
      'An unexpected error occurred during credential verification.',
      '\nFor more details or alternative setup methods, consider:',
      '1. If running locally, run: gcloud auth application-default login.',
      '2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
      '3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
      `\nOriginal error message from Google Auth Library: ${errorMessage}`,
      `Error stack: ${errorWithStack.stack}`,
    ];
    assert.deepStrictEqual(
      capturedConsoleOutput,
      expectedOutput,
      'Console output should match generic error messages'
    );
  });

  it('should return false and log HTTP error when google-auth-library throws an HTTP error', async () => {
    const httpErrorMessage = 'Request failed with status code 401';
    const httpError = new Error(httpErrorMessage);
    httpError.response = { status: 401 };
    const errorWithStack = httpError;

    getClientMock.mock.mockImplementation(async () => {
      throw httpError;
    });

    const result = await ensureGCPCredentials();
    assert.strictEqual(result, false, 'Should return false on failure');

    const expectedOutput = [
      'Checking for Google Cloud Application Default Credentials...',
      'ERROR: Google Cloud Application Default Credentials are not set up.',
      `An HTTP error occurred (Status: 401). This often means misconfigured, expired credentials, or a network issue.`,
      '\nFor more details or alternative setup methods, consider:',
      '1. If running locally, run: gcloud auth application-default login.',
      '2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
      '3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
      `\nOriginal error message from Google Auth Library: ${httpErrorMessage}`,
      `Error stack: ${errorWithStack.stack}`,
    ];
    assert.deepStrictEqual(
      capturedConsoleOutput,
      expectedOutput,
      'Console output should match HTTP error messages'
    );
  });

  it('should return false and log TypeError when google-auth-library throws a TypeError', async () => {
    const typeErrorMessage = 'Unexpected token in JSON at position 0';
    const typeError = new TypeError(typeErrorMessage);
    const errorWithStack = typeError;

    getClientMock.mock.mockImplementation(async () => {
      throw typeError;
    });

    const result = await ensureGCPCredentials();
    assert.strictEqual(result, false, 'Should return false on failure');

    const expectedOutput = [
      'Checking for Google Cloud Application Default Credentials...',
      'ERROR: Google Cloud Application Default Credentials are not set up.',
      'An unexpected error occurred during credential verification (e.g., malformed response or invalid type).',
      '\nFor more details or alternative setup methods, consider:',
      '1. If running locally, run: gcloud auth application-default login.',
      '2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
      '3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
      `\nOriginal error message from Google Auth Library: ${typeErrorMessage}`,
      `Error stack: ${errorWithStack.stack}`,
    ];
    assert.deepStrictEqual(
      capturedConsoleOutput,
      expectedOutput,
      'Console output should match TypeError messages'
    );
  });

  it('should return false and log general unexpected error for other errors', async () => {
    const unknownErrorMessage = 'Something unexpected happened.';
    const unknownError = new Error(unknownErrorMessage);
    const errorWithStack = unknownError;

    getClientMock.mock.mockImplementation(async () => {
      throw unknownError;
    });

    const result = await ensureGCPCredentials();
    assert.strictEqual(result, false, 'Should return false on failure');

    const expectedOutput = [
      'Checking for Google Cloud Application Default Credentials...',
      'ERROR: Google Cloud Application Default Credentials are not set up.',
      'An unexpected error occurred during credential verification.',
      '\nFor more details or alternative setup methods, consider:',
      '1. If running locally, run: gcloud auth application-default login.',
      '2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.',
      '3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.',
      `\nOriginal error message from Google Auth Library: ${unknownErrorMessage}`,
      `Error stack: ${errorWithStack.stack}`,
    ];
    assert.deepStrictEqual(
      capturedConsoleOutput,
      expectedOutput,
      'Console output should match general error messages'
    );
  });
});

```

--------------------------------------------------------------------------------
/lib/cloud-api/run.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import protofiles from 'google-proto-files';
import { callWithRetry, ensureApisEnabled } from './helpers.js';
import { logAndProgress } from '../util/helpers.js';

let runClient;
let serviceUsageClient;
let loggingClient;

async function listCloudRunLocations(projectId) {
  const listLocationsRequest = {
    name: `projects/${projectId}`,
  };

  const availableLocations = [];
  try {
    console.log('Listing Cloud Run supported locations:');
    const iterable = runClient.listLocationsAsync(listLocationsRequest);
    for await (const location of iterable) {
      if (location.labels.initialized) {
        console.log(`${location.locationId}: ${location.name}`);
        availableLocations.push(location.locationId);
      }
    }
  } catch (err) {
    console.error('Error listing locations:', err);
    throw err;
  }
  return availableLocations;
}

/**
 * Lists all Cloud Run services in a given project.
 * @param {string} projectId - The Google Cloud project ID.
 * @returns {Promise<object>} - A promise that resolves to an object mapping region to list of service objects in that region.
 */
export async function listServices(projectId) {
  if (!runClient) {
    const { v2 } = await import('@google-cloud/run');
    const { ServicesClient } = v2;
    const { ServiceUsageClient } = await import('@google-cloud/service-usage');
    runClient = new ServicesClient({ projectId });
    serviceUsageClient = new ServiceUsageClient({ projectId });
  }
  const context = {
    runClient: runClient,
    serviceUsageClient: serviceUsageClient,
  };
  await ensureApisEnabled(context, projectId, ['run.googleapis.com']);
  const locations = await listCloudRunLocations(projectId);

  const allServices = {};
  for (const location of locations) {
    const parent = runClient.locationPath(projectId, location);

    try {
      console.log(
        `Listing Cloud Run services in project ${projectId}, location ${location}...`
      );
      const [services] = await callWithRetry(
        () => runClient.listServices({ parent }),
        'listServices'
      );
      allServices[location] = services;
    } catch (error) {
      console.error(`Error listing Cloud Run services:`, error);
      throw error;
    }
  }
  return allServices;
}

/**
 * Gets details for a specific Cloud Run service.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string} location - The Google Cloud location (e.g., 'europe-west1').
 * @param {string} serviceId - The ID of the Cloud Run service.
 * @returns {Promise<object>} - A promise that resolves to the service object.
 */
export async function getService(projectId, location, serviceId) {
  if (!runClient) {
    const { v2 } = await import('@google-cloud/run');
    const { ServicesClient } = v2;
    runClient = new ServicesClient({ projectId });
  }

  const servicePath = runClient.servicePath(projectId, location, serviceId);

  try {
    console.log(
      `Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
    );
    const [service] = await callWithRetry(
      () => runClient.getService({ name: servicePath }),
      'getService'
    );
    return service;
  } catch (error) {
    console.error(
      `Error getting details for Cloud Run service ${serviceId}:`,
      error
    );
    // Check if the error is a "not found" error (gRPC code 5)
    if (error.code === 5) {
      console.log(`Cloud Run service ${serviceId} not found.`);
      return null; // Or throw a custom error, or handle as needed
    }
    throw error; // Re-throw other errors
  }
}

/**
 * Fetches a paginated list of logs for a specific Cloud Run service.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string} location - The Google Cloud location (e.g., 'europe-west1').
 * @param {string} serviceId - The ID of the Cloud Run service.
 * @param {string} [requestOptions] - The token for the next page of results.
 * @returns {Promise<{logs: string, requestOptions: object | undefined }>} - A promise that resolves to an object with log entries and a token for the next page.
 */
export async function getServiceLogs(
  projectId,
  location,
  serviceId,
  requestOptions
) {
  if (!loggingClient) {
    const { Logging } = await import('@google-cloud/logging');
    loggingClient = new Logging({ projectId });
  }

  try {
    const LOG_SEVERITY = 'DEFAULT'; // e.g., 'DEFAULT', 'INFO', 'WARNING', 'ERROR'
    const PAGE_SIZE = 100; // Number of log entries to retrieve per page

    const filter = `resource.type="cloud_run_revision"
                    resource.labels.service_name="${serviceId}"
                    resource.labels.location="${location}"
                    severity>=${LOG_SEVERITY}`;

    console.log(
      `Fetching logs for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
    );

    // Options for the getEntries API call
    const options = requestOptions || {
      filter: filter,
      orderBy: 'timestamp desc', // Get the latest logs first
      pageSize: PAGE_SIZE,
    };
    console.log(`Request options: ${JSON.stringify(options)}`);

    // getEntries returns the entries and the full API response
    const [entries, nextRequestOptions, apiResponse] = await callWithRetry(
      () => loggingClient.getEntries(options),
      'getEntries'
    );

    const formattedLogLines = entries
      .map((entry) => formatLogEntry(entry))
      .join('\n');

    // The nextPageToken is available in the apiResponse object
    const nextOptions = apiResponse?.nextPageToken
      ? nextRequestOptions
      : undefined;

    return {
      logs: formattedLogLines,
      requestOptions: nextOptions,
    };
  } catch (error) {
    console.error(
      `Error fetching logs for Cloud Run service ${serviceId}:`,
      error
    );
    throw error;
  }
}

/**
 * Formats a single log entry for display.
 * @param {object} entry - A log entry object from the Cloud Logging API.
 * @returns {string} - A formatted string representation of the log entry.
 */
function formatLogEntry(entry) {
  const timestampStr = entry.metadata.timestamp.toISOString() || 'N/A';
  const severity = entry.metadata.severity || 'N/A';
  let responseData = '';
  if (entry.metadata.httpRequest) {
    const responseMethod = entry.metadata.httpRequest.requestMethod;
    const responseCode = entry.metadata.httpRequest.status;
    const requestUrl = entry.metadata.httpRequest.requestUrl;
    const responseSize = entry.metadata.httpRequest.responseSize;
    responseData = `HTTP Request: ${responseMethod} StatusCode: ${responseCode} ResponseSize: ${responseSize} Byte - ${requestUrl}`;
  }

  let data = '';
  if (entry.data && entry.data.value) {
    const protopath = protofiles.getProtoPath(
      '../google/cloud/audit/audit_log.proto'
    );
    const root = protofiles.loadSync(protopath);
    const type = root.lookupType('google.cloud.audit.AuditLog');
    const value = type.decode(entry.data.value);
    data = `${value.methodName}: ${value.status?.message || ''}${value.authenticationInfo?.principalEmail || ''}`;
  } else if (entry.data) {
    data = entry.data;
  }
  return `[${timestampStr}] [${severity}] ${responseData} ${data}`;
}

/**
 * Checks if a Cloud Run service already exists.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string} location - The Google Cloud region where the service is located.
 * @param {string} serviceId - The ID of the Cloud Run service.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<boolean>} A promise that resolves to true if the service exists, false otherwise.
 * @throws {Error} If there's an error checking the service (other than not found).
 */
export async function checkCloudRunServiceExists(
  context,
  projectId,
  location,
  serviceId,
  progressCallback
) {
  const parent = context.runClient.locationPath(projectId, location);
  const servicePath = context.runClient.servicePath(
    projectId,
    location,
    serviceId
  );
  try {
    await callWithRetry(
      () => context.runClient.getService({ name: servicePath }),
      `getService ${serviceId}`
    );
    await logAndProgress(
      `Cloud Run service ${serviceId} already exists.`,
      progressCallback
    );
    return true;
  } catch (error) {
    if (error.code === 5) {
      await logAndProgress(
        `Cloud Run service ${serviceId} does not exist.`,
        progressCallback
      );
      return false;
    }
    const errorMessage = `Error checking Cloud Run service ${serviceId}: ${error.message}`;
    console.error(`Error checking Cloud Run service ${serviceId}:`, error);
    await logAndProgress(errorMessage, progressCallback, 'error');
    throw error;
  }
}

```

--------------------------------------------------------------------------------
/test/local/tools.test.js:
--------------------------------------------------------------------------------

```javascript
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
import esmock from 'esmock';

describe('registerTools', () => {
  it('should register all tools', async () => {
    const server = {
      registerTool: mock.fn(),
    };

    const { registerTools } = await esmock('../../tools/tools.js', {});

    registerTools(server);

    assert.strictEqual(server.registerTool.mock.callCount(), 8);
    const toolNames = server.registerTool.mock.calls.map(
      (call) => call.arguments[0]
    );
    assert.deepStrictEqual(
      toolNames.sort(),
      [
        'create_project',
        'deploy_container_image',
        'deploy_file_contents',
        'deploy_local_folder',
        'get_service',
        'get_service_log',
        'list_projects',
        'list_services',
      ].sort()
    );
  });

  describe('list_projects', () => {
    it('should list projects', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/cloud-api/projects.js': {
            listProjects: () =>
              Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'list_projects'
      ).arguments[2];
      const result = await handler({});

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Available GCP Projects:\n- project1\n- project2',
          },
        ],
      });
    });
  });

  describe('create_project', () => {
    it('should create a project with a provided id', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/cloud-api/projects.js': {
            createProjectAndAttachBilling: (projectId) =>
              Promise.resolve({
                projectId: projectId,
                billingMessage: `Project ${projectId} created successfully. Billing attached.`,
              }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'create_project'
      ).arguments[2];
      const result = await handler({ projectId: 'my-project' });

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Project my-project created successfully. Billing attached.',
          },
        ],
      });
    });

    it('should create a project with a generated id', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/cloud-api/projects.js': {
            createProjectAndAttachBilling: () =>
              Promise.resolve({
                projectId: 'generated-project',
                billingMessage:
                  'Project generated-project created successfully. Billing attached.',
              }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'create_project'
      ).arguments[2];
      const result = await handler({});

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Project generated-project created successfully. Billing attached.',
          },
        ],
      });
    });
  });

  describe('list_services', () => {
    it('should list services', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/cloud-api/run.js': {
            listServices: () =>
              Promise.resolve({
                'my-region1': [
                  { name: 'service1', uri: 'uri1' },
                  { name: 'service2', uri: 'uri2' },
                ],
                'my-region2': [
                  { name: 'service3', uri: 'uri3' },
                  { name: 'service4', uri: 'uri4' },
                ],
              }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'list_services'
      ).arguments[2];
      const result = await handler({
        project: 'my-project',
      });

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Services in project my-project (location my-region1):\n- service1 (URL: uri1)\n- service2 (URL: uri2)',
          },
          {
            type: 'text',
            text: 'Services in project my-project (location my-region2):\n- service3 (URL: uri3)\n- service4 (URL: uri4)',
          },
        ],
      });
    });
  });

  describe('get_service', () => {
    it('should get a service', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/cloud-api/run.js': {
            getService: () =>
              Promise.resolve({
                name: 'my-service',
                uri: 'my-uri',
                lastModifier: 'me',
              }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'get_service'
      ).arguments[2];
      const result = await handler({
        project: 'my-project',
        region: 'my-region',
        service: 'my-service',
      });

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Name: my-service\nRegion: my-region\nProject: my-project\nURL: my-uri\nLast deployed by: me',
          },
        ],
      });
    });
  });

  describe('get_service_log', () => {
    it('should get service logs', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      let callCount = 0;
      const getServiceLogs = () => {
        callCount++;
        if (callCount === 1) {
          return Promise.resolve({
            logs: 'log1\nlog2',
            requestOptions: { pageToken: 'nextPage' },
          });
        }
        return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/cloud-api/run.js': {
            getServiceLogs: getServiceLogs,
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'get_service_log'
      ).arguments[2];
      const result = await handler({
        project: 'my-project',
        region: 'my-region',
        service: 'my-service',
      });

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'log1\nlog2\nlog3\nlog4',
          },
        ],
      });
    });
  });

  describe('deploy_local_folder', () => {
    it('should deploy local folder', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/deployment/deployer.js': {
            deploy: () => Promise.resolve({ uri: 'my-uri' }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'deploy_local_folder'
      ).arguments[2];
      const result = await handler(
        {
          project: 'my-project',
          region: 'my-region',
          service: 'my-service',
          folderPath: '/my/folder',
        },
        { sendNotification: mock.fn() }
      );

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Cloud Run service my-service deployed from folder /my/folder in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
          },
        ],
      });
    });
  });

  describe('deploy_file_contents', () => {
    it('should deploy file contents', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/deployment/deployer.js': {
            deploy: () => Promise.resolve({ uri: 'my-uri' }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'deploy_file_contents'
      ).arguments[2];
      const result = await handler(
        {
          project: 'my-project',
          region: 'my-region',
          service: 'my-service',
          files: [{ filename: 'file1', content: 'content1' }],
        },
        { sendNotification: mock.fn() }
      );

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
          },
        ],
      });
    });
  });

  describe('deploy_container_image', () => {
    it('should deploy container image', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock(
        '../../tools/tools.js',
        {},
        {
          '../../lib/deployment/deployer.js': {
            deployImage: () => Promise.resolve({ uri: 'my-uri' }),
          },
        }
      );

      registerTools(server, { gcpCredentialsAvailable: true });

      const handler = server.registerTool.mock.calls.find(
        (call) => call.arguments[0] === 'deploy_container_image'
      ).arguments[2];
      const result = await handler(
        {
          project: 'my-project',
          region: 'my-region',
          service: 'my-service',
          imageUrl: 'gcr.io/my-project/my-image',
        },
        { sendNotification: mock.fn() }
      );

      assert.deepStrictEqual(result, {
        content: [
          {
            type: 'text',
            text: 'Cloud Run service my-service deployed in project my-project\nCloud Console: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nService URL: my-uri',
          },
        ],
      });
    });
  });

  describe('when gcp credentials are not available', () => {
    it('should return an error for all tools', async () => {
      const server = {
        registerTool: mock.fn(),
      };

      const { registerTools } = await esmock('../../tools/tools.js', {});

      registerTools(server, { gcpCredentialsAvailable: false });

      const toolNames = server.registerTool.mock.calls.map(
        (call) => call.arguments[0]
      );

      for (const toolName of toolNames) {
        const handler = server.registerTool.mock.calls.find(
          (call) => call.arguments[0] === toolName
        ).arguments[2];
        const result = await handler({});
        assert.deepStrictEqual(result, {
          content: [
            {
              type: 'text',
              text: 'GCP credentials are not available. Please configure your environment.',
            },
          ],
        });
      }
    });
  });
});

```

--------------------------------------------------------------------------------
/test/local/cloud-api/build.test.js:
--------------------------------------------------------------------------------

```javascript
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
import esmock from 'esmock';

describe('triggerCloudBuild', () => {
  it('should return a successful build and log correct messages', async () => {
    const mockBuildId = 'mock-build-id';
    const mockSuccessResult = {
      id: mockBuildId,
      status: 'SUCCESS',
      results: { images: [{ name: 'gcr.io/mock-project/mock-image' }] },
    };

    const getBuildMock = mock.fn(() => Promise.resolve([mockSuccessResult]));
    const logAndProgressMock = mock.fn();

    const { triggerCloudBuild } = await esmock(
      '../../../lib/cloud-api/build.js',
      {
        '../../../lib/cloud-api/helpers.js': {
          callWithRetry: (fn) => fn(), // Directly execute the function
        },
        '../../../lib/util/helpers.js': {
          logAndProgress: logAndProgressMock,
        },
      }
    );

    const context = {
      cloudBuildClient: {
        createBuild: mock.fn(() =>
          Promise.resolve([
            {
              metadata: {
                build: {
                  id: mockBuildId,
                },
              },
            },
          ])
        ),
        getBuild: getBuildMock,
      },
    };

    const result = await triggerCloudBuild(
      context,
      'mock-project',
      'mock-location',
      'mock-bucket',
      'mock-blob',
      'mock-repo',
      'gcr.io/mock-project/mock-image',
      true,
      () => {}
    );

    assert.deepStrictEqual(result, mockSuccessResult);
    assert.strictEqual(
      context.cloudBuildClient.createBuild.mock.callCount(),
      1
    );
    assert.strictEqual(context.cloudBuildClient.getBuild.mock.callCount(), 1);

    const { calls: logCalls } = logAndProgressMock.mock;
    assert.match(logCalls[0].arguments[0], /Initiating Cloud Build/);
    assert.match(logCalls[1].arguments[0], /Cloud Build job started/);
    assert.match(logCalls[2].arguments[0], /completed successfully/);
    assert.match(logCalls[3].arguments[0], /Image built/);
  });

  it('should throw an error for a failed build and log correct messages', async () => {
    const mockBuildId = 'mock-build-id-failure';
    const mockFailureResult = {
      id: mockBuildId,
      status: 'FAILURE',
      logUrl: 'http://mock-log-url.com',
    };

    const getBuildMock = mock.fn(() => Promise.resolve([mockFailureResult]));
    const logAndProgressMock = mock.fn();
    const setTimeoutMock = mock.fn((resolve) => resolve());
    mock.method(global, 'setTimeout', setTimeoutMock);

    const { triggerCloudBuild } = await esmock(
      '../../../lib/cloud-api/build.js',
      {
        '../../../lib/cloud-api/helpers.js': {
          callWithRetry: (fn) => fn(),
        },
        '../../../lib/util/helpers.js': {
          logAndProgress: logAndProgressMock,
        },
      }
    );

    const context = {
      cloudBuildClient: {
        createBuild: mock.fn(() =>
          Promise.resolve([
            {
              metadata: {
                build: {
                  id: mockBuildId,
                },
              },
            },
          ])
        ),
        getBuild: getBuildMock,
      },
      loggingClient: {
        getEntries: mock.fn(() =>
          Promise.resolve([[{ data: 'log line 1' }, { data: 'log line 2' }]])
        ),
      },
    };

    await assert.rejects(
      () =>
        triggerCloudBuild(
          context,
          'mock-project',
          'mock-location',
          'mock-bucket',
          'mock-blob',
          'mock-repo',
          'gcr.io/mock-project/mock-image',
          true,
          () => {}
        ),
      (err) => {
        assert.match(err.message, /Build mock-build-id-failure failed/);
        assert.match(err.message, /log line 1/);
        assert.match(err.message, /log line 2/);
        return true;
      }
    );

    assert.strictEqual(
      context.cloudBuildClient.createBuild.mock.callCount(),
      1
    );
    assert.strictEqual(context.cloudBuildClient.getBuild.mock.callCount(), 1);
    assert.strictEqual(context.loggingClient.getEntries.mock.callCount(), 1);
    assert.strictEqual(setTimeoutMock.mock.callCount(), 1);
    assert.strictEqual(setTimeoutMock.mock.calls[0].arguments[1], 10000);

    const { calls: logCalls } = logAndProgressMock.mock;
    assert.match(logCalls[0].arguments[0], /Initiating Cloud Build/);
    assert.match(logCalls[1].arguments[0], /Cloud Build job started/);
    assert.match(logCalls[2].arguments[0], /failed with status: FAILURE/);
    assert.match(logCalls[3].arguments[0], /Build logs:/);
    assert.match(logCalls[4].arguments[0], /Attempting to fetch last/);
    assert.match(logCalls[5].arguments[0], /Successfully fetched snippet/);
  });

  it('should use buildpacks when no Dockerfile is present', async () => {
    const mockBuildId = 'mock-build-id-buildpacks';
    const mockSuccessResult = {
      id: mockBuildId,
      status: 'SUCCESS',
      results: { images: [{ name: 'gcr.io/mock-project/mock-image' }] },
    };

    const getBuildMock = mock.fn(() => Promise.resolve([mockSuccessResult]));
    const createBuildMock = mock.fn(() =>
      Promise.resolve([
        {
          metadata: {
            build: {
              id: mockBuildId,
            },
          },
        },
      ])
    );

    const { triggerCloudBuild } = await esmock(
      '../../../lib/cloud-api/build.js',
      {
        '../../../lib/cloud-api/helpers.js': {
          callWithRetry: (fn) => fn(),
        },
        '../../../lib/util/helpers.js': {
          logAndProgress: () => {},
        },
      }
    );

    const context = {
      cloudBuildClient: {
        createBuild: createBuildMock,
        getBuild: getBuildMock,
      },
    };

    await triggerCloudBuild(
      context,
      'mock-project',
      'mock-location',
      'mock-bucket',
      'mock-blob',
      'mock-repo',
      'gcr.io/mock-project/mock-image',
      false, // hasDockerfile = false
      () => {}
    );

    assert.strictEqual(createBuildMock.mock.callCount(), 1);
    const buildArg = createBuildMock.mock.calls[0].arguments[0].build;
    const buildStep = buildArg.steps[0];
    assert.strictEqual(buildStep.name, 'gcr.io/k8s-skaffold/pack');
  });

  it('should poll for build status until completion', async () => {
    const mockBuildId = 'mock-build-id-polling';
    const mockWorkingResult = { id: mockBuildId, status: 'WORKING' };
    const mockSuccessResult = {
      id: mockBuildId,
      status: 'SUCCESS',
      results: { images: [{ name: 'gcr.io/mock-project/mock-image' }] },
    };

    let getBuildCallCount = 0;
    const getBuildMock = mock.fn(() => {
      getBuildCallCount++;
      if (getBuildCallCount === 1) {
        return Promise.resolve([mockWorkingResult]);
      }
      return Promise.resolve([mockSuccessResult]);
    });

    const logAndProgressMock = mock.fn();
    const setTimeoutMock = mock.fn((resolve) => resolve());
    mock.method(global, 'setTimeout', setTimeoutMock);

    const { triggerCloudBuild } = await esmock(
      '../../../lib/cloud-api/build.js',
      {
        '../../../lib/cloud-api/helpers.js': {
          callWithRetry: (fn) => fn(),
        },
        '../../../lib/util/helpers.js': {
          logAndProgress: logAndProgressMock,
        },
      }
    );

    const context = {
      cloudBuildClient: {
        createBuild: mock.fn(() =>
          Promise.resolve([
            {
              metadata: {
                build: {
                  id: mockBuildId,
                },
              },
            },
          ])
        ),
        getBuild: getBuildMock,
      },
    };

    await triggerCloudBuild(
      context,
      'mock-project',
      'mock-location',
      'mock-bucket',
      'mock-blob',
      'mock-repo',
      'gcr.io/mock-project/mock-image',
      true,
      () => {}
    );

    assert.strictEqual(getBuildMock.mock.callCount(), 2);
    assert.strictEqual(setTimeoutMock.mock.callCount(), 1);
    assert.strictEqual(setTimeoutMock.mock.calls[0].arguments[1], 5000);
    const { calls: logCalls } = logAndProgressMock.mock;
    assert.match(logCalls[0].arguments[0], /Initiating Cloud Build/);
    assert.match(logCalls[1].arguments[0], /Cloud Build job started/);
    assert.match(logCalls[2].arguments[0], /Build status: WORKING/);
    assert.match(logCalls[3].arguments[0], /completed successfully/);
    assert.match(logCalls[4].arguments[0], /Image built/);
  });

  it('should handle failed build when no logs are found', async () => {
    const mockBuildId = 'mock-build-id-no-logs';
    const mockFailureResult = {
      id: mockBuildId,
      status: 'FAILURE',
      logUrl: 'http://mock-log-url.com',
    };

    const getBuildMock = mock.fn(() => Promise.resolve([mockFailureResult]));
    const logAndProgressMock = mock.fn();

    const { triggerCloudBuild } = await esmock(
      '../../../lib/cloud-api/build.js',
      {
        '../../../lib/cloud-api/helpers.js': {
          callWithRetry: (fn) => fn(),
        },
        '../../../lib/util/helpers.js': {
          logAndProgress: logAndProgressMock,
        },
      }
    );

    const context = {
      cloudBuildClient: {
        createBuild: mock.fn(() =>
          Promise.resolve([
            {
              metadata: {
                build: {
                  id: mockBuildId,
                },
              },
            },
          ])
        ),
        getBuild: getBuildMock,
      },
      loggingClient: {
        getEntries: mock.fn(() => Promise.resolve([[]])), // No log entries
      },
    };

    await assert.rejects(
      () =>
        triggerCloudBuild(
          context,
          'mock-project',
          'mock-location',
          'mock-bucket',
          'mock-blob',
          'mock-repo',
          'gcr.io/mock-project/mock-image',
          true,
          () => {}
        ),
      (err) => {
        assert.match(err.message, /Build mock-build-id-no-logs failed/);
        assert.doesNotMatch(err.message, /Last log lines/);
        return true;
      }
    );

    const { calls: logCalls } = logAndProgressMock.mock;
    assert.match(logCalls[0].arguments[0], /Initiating Cloud Build/);
    assert.match(logCalls[1].arguments[0], /Cloud Build job started/);
    assert.match(logCalls[2].arguments[0], /failed with status: FAILURE/);
    assert.match(logCalls[3].arguments[0], /Build logs:/);
    assert.match(logCalls[4].arguments[0], /Attempting to fetch last/);
    assert.match(logCalls[5].arguments[0], /No specific log entries retrieved/);
  });

  it('should handle error when fetching logs for a failed build', async () => {
    const mockBuildId = 'mock-build-id-log-error';
    const mockFailureResult = {
      id: mockBuildId,
      status: 'FAILURE',
      logUrl: 'http://mock-log-url.com',
    };

    const getBuildMock = mock.fn(() => Promise.resolve([mockFailureResult]));
    const logAndProgressMock = mock.fn();

    const { triggerCloudBuild } = await esmock(
      '../../../lib/cloud-api/build.js',
      {
        '../../../lib/cloud-api/helpers.js': {
          callWithRetry: (fn) => fn(),
        },
        '../../../lib/util/helpers.js': {
          logAndProgress: logAndProgressMock,
        },
      }
    );

    const context = {
      cloudBuildClient: {
        createBuild: mock.fn(() =>
          Promise.resolve([
            {
              metadata: {
                build: {
                  id: mockBuildId,
                },
              },
            },
          ])
        ),
        getBuild: getBuildMock,
      },
      loggingClient: {
        getEntries: mock.fn(() => Promise.reject(new Error('Log fetch error'))),
      },
    };

    await assert.rejects(
      () =>
        triggerCloudBuild(
          context,
          'mock-project',
          'mock-location',
          'mock-bucket',
          'mock-blob',
          'mock-repo',
          'gcr.io/mock-project/mock-image',
          true,
          () => {}
        ),
      (err) => {
        assert.match(err.message, /Build mock-build-id-log-error failed/);
        assert.doesNotMatch(err.message, /Last log lines/);
        return true;
      }
    );

    const { calls: logCalls } = logAndProgressMock.mock;
    assert.match(logCalls[0].arguments[0], /Initiating Cloud Build/);
    assert.match(logCalls[1].arguments[0], /Cloud Build job started/);
    assert.match(logCalls[2].arguments[0], /failed with status: FAILURE/);
    assert.match(logCalls[3].arguments[0], /Build logs:/);
    assert.match(logCalls[4].arguments[0], /Attempting to fetch last/);
    assert.match(
      logCalls[5].arguments[0],
      /Failed to fetch build logs snippet/
    );
  });
});

```

--------------------------------------------------------------------------------
/lib/deployment/deployer.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { callWithRetry, ensureApisEnabled } from '../cloud-api/helpers.js';
import { zipFiles } from '../util/archive.js';
import {
  ensureStorageBucketExists,
  uploadToStorageBucket,
} from '../cloud-api/storage.js';
import { ensureArtifactRegistryRepoExists } from '../cloud-api/registry.js';
import { triggerCloudBuild } from '../cloud-api/build.js';
import { logAndProgress } from '../util/helpers.js';
import { checkCloudRunServiceExists } from '../cloud-api/run.js';

// Configuration
const REPO_NAME = 'mcp-cloud-run-deployments';
const ZIP_FILE_NAME = 'source.zip';
const IMAGE_TAG = 'latest';

// APIs required for deploying from source code.
const REQUIRED_APIS_FOR_SOURCE_DEPLOY = [
  'iam.googleapis.com',
  'storage.googleapis.com',
  'cloudbuild.googleapis.com',
  'artifactregistry.googleapis.com',
  'run.googleapis.com',
  'serviceusage.googleapis.com',
];

// APIs required for deploying a container image.
const REQUIRED_APIS_FOR_IMAGE_DEPLOY = [
  'run.googleapis.com',
  'serviceusage.googleapis.com',
];

/**
 * Deploys or updates a Cloud Run service with the specified container image.
 * If the service exists, it's updated; otherwise, a new service is created.
 * The service is configured to be publicly accessible.
 *
 * @async
 * @param {object} context - The context object containing clients and other parameters.
 * @param {string} projectId - The Google Cloud project ID.
 * @param {string} location - The Google Cloud region for the deployment.
 * @param {string} serviceId - The ID for the Cloud Run service.
 * @param {string} imgUrl - The URL of the container image to deploy.
 * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
 * @returns {Promise<object>} A promise that resolves with the Cloud Run service object upon successful deployment or update.
 * @throws {Error} If the deployment or update process fails.
 */
async function deployToCloudRun(
  context,
  projectId,
  location,
  serviceId,
  imgUrl,
  progressCallback,
  skipIamCheck
) {
  const parent = context.runClient.locationPath(projectId, location);
  const servicePath = context.runClient.servicePath(
    projectId,
    location,
    serviceId
  );
  const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name

  const service = {
    template: {
      revision: revisionName,
      containers: [{ image: imgUrl }],
    },
    labels: {
      'created-by': 'cloud-run-mcp',
    },
  };

  // Conditionally set invokerIamDisabled based on the skipIamCheck flag
  if (skipIamCheck) {
    service.invokerIamDisabled = true;
  }

  try {
    const exists = await checkCloudRunServiceExists(
      context,
      projectId,
      location,
      serviceId,
      progressCallback
    );

    // Always perform a dry run first for general validation
    try {
      await logAndProgress(
        `Performing dry run for service ${serviceId}...`,
        progressCallback,
        'debug'
      );
      const dryRunServiceConfig = JSON.parse(JSON.stringify(service)); // Deep copy for dry run

      if (exists) {
        dryRunServiceConfig.name = servicePath;
        await callWithRetry(
          () =>
            context.runClient.updateService({
              service: dryRunServiceConfig,
              validateOnly: true,
            }),
          `updateService (dry run) ${serviceId}`
        );
      } else {
        await callWithRetry(
          () =>
            context.runClient.createService({
              parent: parent,
              service: dryRunServiceConfig,
              serviceId: serviceId,
              validateOnly: true,
            }),
          `createService (dry run) ${serviceId}`
        );
      }
      await logAndProgress(
        `Dry run successful for ${serviceId} with current configuration.`,
        progressCallback,
        'debug'
      );
    } catch (dryRunError) {
      await logAndProgress(
        `Dry run for ${serviceId} failed: ${dryRunError.message}`,
        progressCallback,
        'warn'
      );

      // Check if the error is related to invokerIamDisabled (this is a heuristic)
      if (
        skipIamCheck &&
        dryRunError.message &&
        (dryRunError.message.toLowerCase().includes('invokeriamdisabled') ||
          dryRunError.message.toLowerCase().includes('iam policy violation') ||
          dryRunError.code === 3) /* INVALID_ARGUMENT */
      ) {
        await logAndProgress(
          `Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
          progressCallback,
          'warn'
        );
        delete service.invokerIamDisabled; // Modify the main service object for actual deployment
      } else {
        // For any other validation errors, rethrow to stop the deployment
        const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
        await logAndProgress(errorMessage, progressCallback, 'error');
        throw new Error(errorMessage);
      }
    }

    let operation;
    if (exists) {
      await logAndProgress(
        `Updating existing service ${serviceId}...`,
        progressCallback
      );
      service.name = servicePath;
      [operation] = await callWithRetry(
        () => context.runClient.updateService({ service }),
        `updateService ${serviceId}`
      );
    } else {
      await logAndProgress(
        `Creating new service ${serviceId}...`,
        progressCallback
      );
      [operation] = await callWithRetry(
        () =>
          context.runClient.createService({
            parent: parent,
            service: service, // 'service' object might have invokerIamDisabled removed
            serviceId: serviceId,
          }),
        `createService ${serviceId}`
      );
    }

    await logAndProgress(
      `Deploying ${serviceId} to Cloud Run...`,
      progressCallback
    );
    const [response] = await operation.promise();

    await logAndProgress(
      `Service deployed/updated successfully: ${response.uri}`,
      progressCallback
    );
    return response;
  } catch (error) {
    const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
    console.error(`Error deploying/updating service ${serviceId}:`, error);
    await logAndProgress(errorMessage, progressCallback, 'error');
    throw error;
  }
}

/**
 * Deploys a service to Google Cloud Run.
 * @param {object} config - The deployment configuration.
 * @param {string} config.projectId - The Google Cloud project ID.
 * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
 * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
 * @param {Array<string|{filename: string, content: Buffer|string}>} config.files - An array of file paths or file objects (with `filename` and `content`) to deploy.
 * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
 * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
 * @throws {Error} If deployment fails or required configuration is missing.
 */
export async function deploy({
  projectId,
  serviceName,
  region,
  files,
  progressCallback,
  skipIamCheck,
}) {
  if (!projectId) {
    const errorMsg =
      'Error: projectId is required in the configuration object.';
    await logAndProgress(errorMsg, progressCallback, 'error');
    throw new Error(errorMsg);
  }

  if (!serviceName) {
    const errorMsg =
      'Error: serviceName is required in the configuration object.';
    await logAndProgress(errorMsg, progressCallback, 'error');
    throw new Error(errorMsg);
  }

  if (!files || !Array.isArray(files) || files.length === 0) {
    const errorMsg =
      'Error: files array is required in the configuration object.';
    await logAndProgress(errorMsg, progressCallback, 'error');
    if (typeof process !== 'undefined' && process.exit) {
      process.exit(1);
    } else {
      throw new Error(errorMsg);
    }
  }

  const path = await import('path');
  const fs = await import('fs');
  const { Storage } = await import('@google-cloud/storage');
  const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
  const { ArtifactRegistryClient } = await import(
    '@google-cloud/artifact-registry'
  );
  const { v2: CloudRunV2Module } = await import('@google-cloud/run');
  const { ServicesClient } = CloudRunV2Module;
  const { ServiceUsageClient } = await import('@google-cloud/service-usage');
  const { Logging } = await import('@google-cloud/logging');

  try {
    const context = {
      storage: new Storage({ projectId }),
      cloudBuildClient: new CloudBuildClient({ projectId }),
      artifactRegistryClient: new ArtifactRegistryClient({ projectId }),
      runClient: new ServicesClient({ projectId }),
      serviceUsageClient: new ServiceUsageClient({ projectId }),
      loggingClient: new Logging({ projectId }),
    };

    await ensureApisEnabled(
      context,
      projectId,
      REQUIRED_APIS_FOR_SOURCE_DEPLOY,
      progressCallback
    );

    const bucketName = `${projectId}-source-bucket`;
    const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;

    await logAndProgress(`Project: ${projectId}`, progressCallback);
    await logAndProgress(`Region: ${region}`, progressCallback);
    await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
    await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);

    let hasDockerfile = false;
    if (
      files.length === 1 &&
      typeof files[0] === 'string' &&
      fs.statSync(files[0]).isDirectory()
    ) {
      // Handle folder deployment: check for Dockerfile inside the folder
      const dockerfilePath = path.join(files[0], 'Dockerfile');
      const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
      if (
        fs.existsSync(dockerfilePath) ||
        fs.existsSync(dockerfilePathLowerCase)
      ) {
        hasDockerfile = true;
      }
    } else {
      // Handle file list deployment or file content deployment
      for (const file of files) {
        if (typeof file === 'string') {
          if (path.basename(file).toLowerCase() === 'dockerfile') {
            hasDockerfile = true;
            break;
          }
        } else if (typeof file === 'object' && file.filename) {
          if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
            hasDockerfile = true;
            break;
          }
        }
      }
    }
    await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);

    const bucket = await ensureStorageBucketExists(
      context,
      bucketName,
      region,
      progressCallback
    );

    const zipBuffer = await zipFiles(files, progressCallback);
    await uploadToStorageBucket(
      context,
      bucket,
      zipBuffer,
      ZIP_FILE_NAME,
      progressCallback
    );
    await logAndProgress('Source code uploaded successfully', progressCallback);

    await ensureArtifactRegistryRepoExists(
      context,
      projectId,
      region,
      REPO_NAME,
      'DOCKER',
      progressCallback
    );

    const buildResult = await triggerCloudBuild(
      context,
      projectId,
      region,
      bucketName,
      ZIP_FILE_NAME,
      REPO_NAME,
      imageUrl,
      hasDockerfile,
      progressCallback
    );

    const builtImageUrl = buildResult.results.images[0].name;

    const service = await deployToCloudRun(
      context,
      projectId,
      region,
      serviceName,
      builtImageUrl,
      progressCallback,
      skipIamCheck
    );

    await logAndProgress(`Deployment Completed Successfully`, progressCallback);
    return service;
  } catch (error) {
    const deployFailedMessage = `Deployment Failed: ${error.message}`;
    console.error(`Deployment Failed`, error);
    await logAndProgress(deployFailedMessage, progressCallback, 'error');
    throw error;
  }
}

/**
 * Deploys a container image to Google Cloud Run.
 * @param {object} config - The deployment configuration.
 * @param {string} config.projectId - The Google Cloud project ID.
 * @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
 * @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
 * @param {string} config.imageUrl - The URL of the container image to deploy.
 * @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
 * @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check.
 * @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
 * @throws {Error} If deployment fails or required configuration is missing.
 */
export async function deployImage({
  projectId,
  serviceName,
  region,
  imageUrl,
  progressCallback,
  skipIamCheck,
}) {
  if (!projectId) {
    const errorMsg =
      'Error: projectId is required in the configuration object.';
    await logAndProgress(errorMsg, progressCallback, 'error');
    throw new Error(errorMsg);
  }

  if (!serviceName) {
    const errorMsg =
      'Error: serviceName is required in the configuration object.';
    await logAndProgress(errorMsg, progressCallback, 'error');
    throw new Error(errorMsg);
  }

  if (!imageUrl) {
    const errorMsg = 'Error: imageUrl is required in the configuration object.';
    await logAndProgress(errorMsg, progressCallback, 'error');
    if (typeof process !== 'undefined' && process.exit) {
      process.exit(1);
    } else {
      throw new Error(errorMsg);
    }
  }

  const { v2: CloudRunV2Module } = await import('@google-cloud/run');
  const { ServicesClient } = CloudRunV2Module;
  const { ServiceUsageClient } = await import('@google-cloud/service-usage');

  try {
    const context = {
      runClient: new ServicesClient({ projectId }),
      serviceUsageClient: new ServiceUsageClient({ projectId }),
    };

    await ensureApisEnabled(
      context,
      projectId,
      REQUIRED_APIS_FOR_IMAGE_DEPLOY,
      progressCallback
    );

    await logAndProgress(`Project: ${projectId}`, progressCallback);
    await logAndProgress(`Region: ${region}`, progressCallback);
    await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
    await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);

    const service = await deployToCloudRun(
      context,
      projectId,
      region,
      serviceName,
      imageUrl,
      progressCallback,
      skipIamCheck
    );

    await logAndProgress(`Deployment Completed Successfully`, progressCallback);
    return service;
  } catch (error) {
    const deployFailedMessage = `Deployment Failed: ${error.message}`;
    console.error(`Deployment Failed`, error);
    await logAndProgress(deployFailedMessage, progressCallback, 'error');
    throw error;
  }
}

```

--------------------------------------------------------------------------------
/tools/register-tools.js:
--------------------------------------------------------------------------------

```javascript
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { z } from 'zod';
import {
  listProjects,
  createProjectAndAttachBilling,
} from '../lib/cloud-api/projects.js';
import {
  listServices,
  getService,
  getServiceLogs,
} from '../lib/cloud-api/run.js';
import { deploy, deployImage } from '../lib/deployment/deployer.js';

function createProgressCallback(sendNotification) {
  return (progress) => {
    sendNotification({
      method: 'notifications/message',
      params: { level: progress.level || 'info', data: progress.data },
    });
  };
}

function gcpTool(gcpCredentialsAvailable, fn) {
  if (!gcpCredentialsAvailable) {
    return () => ({
      content: [
        {
          type: 'text',
          text: 'GCP credentials are not available. Please configure your environment.',
        },
      ],
    });
  }
  return fn;
}

// Tool to list GCP projects
function registerListProjectsTool(server, options) {
  server.registerTool(
    'list_projects',
    {
      description: 'Lists available GCP projects',
      inputSchema: {},
    },
    gcpTool(options.gcpCredentialsAvailable, async () => {
      try {
        const projects = await listProjects();
        return {
          content: [
            {
              type: 'text',
              text: `Available GCP Projects:\n${projects.map((p) => `- ${p.id}`).join('\n')}`,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: `Error listing GCP projects: ${error.message}`,
            },
          ],
        };
      }
    })
  );
}

// Tool to create a new GCP project
function registerCreateProjectTool(server, options) {
  server.registerTool(
    'create_project',
    {
      description:
        'Creates a new GCP project and attempts to attach it to the first available billing account. A project ID can be optionally specified; otherwise it will be automatically generated.',
      inputSchema: {
        projectId: z
          .string()
          .optional()
          .describe(
            'Optional. The desired ID for the new GCP project. If not provided, an ID will be auto-generated.'
          ),
      },
    },
    gcpTool(options.gcpCredentialsAvailable, async ({ projectId }) => {
      if (
        projectId !== undefined &&
        (typeof projectId !== 'string' || projectId.trim() === '')
      ) {
        return {
          content: [
            {
              type: 'text',
              text: 'Error: If provided, Project ID must be a non-empty string.',
            },
          ],
        };
      }
      try {
        const result = await createProjectAndAttachBilling(projectId);
        return {
          content: [
            {
              type: 'text',
              text: result.billingMessage,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: `Error creating GCP project or attaching billing: ${error.message}`,
            },
          ],
        };
      }
    })
  );
}

// Listing Cloud Run services
function registerListServicesTool(server, options) {
  server.registerTool(
    'list_services',
    {
      description: 'Lists all Cloud Run services in a given project.',
      inputSchema: {
        project: z
          .string()
          .describe('Google Cloud project ID')
          .default(options.defaultProjectId),
      },
    },
    gcpTool(options.gcpCredentialsAvailable, async ({ project }) => {
      if (typeof project !== 'string') {
        return {
          content: [
            {
              type: 'text',
              text: 'Error: Project ID must be provided and be a non-empty string.',
            },
          ],
        };
      }
      try {
        const allServices = await listServices(project);
        const content = [];
        for (const region of Object.keys(allServices)) {
          const serviceList = allServices[region];
          const servicesText = serviceList
            .map((s) => {
              const serviceName = s.name.split('/').pop();
              return `- ${serviceName} (URL: ${s.uri})`;
            })
            .join('\n');
          content.push({
            type: 'text',
            text: `Services in project ${project} (location ${region}):\n${servicesText}`,
          });
        }
        return { content };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: `Error listing services for project ${project}: ${error.message}`,
            },
          ],
        };
      }
    })
  );
}

// Dynamic resource for getting a specific service
function registerGetServiceTool(server, options) {
  server.registerTool(
    'get_service',
    {
      description: 'Gets details for a specific Cloud Run service.',
      inputSchema: {
        project: z
          .string()
          .describe('Google Cloud project ID containing the service')
          .default(options.defaultProjectId),
        region: z
          .string()
          .describe('Region where the service is located')
          .default(options.defaultRegion),
        service: z
          .string()
          .describe('Name of the Cloud Run service')
          .default(options.defaultServiceName),
      },
    },
    gcpTool(
      options.gcpCredentialsAvailable,
      async ({ project, region, service }) => {
        if (typeof project !== 'string') {
          return {
            content: [
              { type: 'text', text: 'Error: Project ID must be provided.' },
            ],
          };
        }
        if (typeof service !== 'string') {
          return {
            content: [
              { type: 'text', text: 'Error: Service name must be provided.' },
            ],
          };
        }
        try {
          const serviceDetails = await getService(project, region, service);
          if (serviceDetails) {
            return {
              content: [
                {
                  type: 'text',
                  text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
                },
              ],
            };
          } else {
            return {
              content: [
                {
                  type: 'text',
                  text: `Service ${service} not found in project ${project} (region ${region}).`,
                },
              ],
            };
          }
        } catch (error) {
          return {
            content: [
              {
                type: 'text',
                text: `Error getting service ${service} in project ${project} (region ${region}): ${error.message}`,
              },
            ],
          };
        }
      }
    )
  );
}

// Get logs for a service
function registerGetServiceLogTool(server, options) {
  server.registerTool(
    'get_service_log',
    {
      description:
        'Gets Logs and Error Messages for a specific Cloud Run service.',
      inputSchema: {
        project: z
          .string()
          .describe('Google Cloud project ID containing the service')
          .default(options.defaultProjectId),
        region: z
          .string()
          .describe('Region where the service is located')
          .default(options.defaultRegion),
        service: z
          .string()
          .describe('Name of the Cloud Run service')
          .default(options.defaultServiceName),
      },
    },
    gcpTool(
      options.gcpCredentialsAvailable,
      async ({ project, region, service }) => {
        let allLogs = [];
        let requestOptions;
        try {
          do {
            // Fetch a page of logs
            const response = await getServiceLogs(
              project,
              region,
              service,
              requestOptions
            );

            if (response.logs) {
              allLogs.push(response.logs);
            }

            // Set the requestOptions incl pagintion token for the next iteration

            requestOptions = response.requestOptions;
          } while (requestOptions); // Continue as long as there is a next page token
          return {
            content: [
              {
                type: 'text',
                text: allLogs.join('\n'),
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: 'text',
                text: `Error getting Logs for service ${service} in project ${project} (region ${region}): ${error.message}`,
              },
            ],
          };
        }
      }
    )
  );
}

// Tool to deploy to Cloud Run from local folder
function registerDeployLocalFolderTool(server, options) {
  server.registerTool(
    'deploy_local_folder',
    {
      description:
        'Deploy a local folder to Cloud Run. Takes an absolute folder path from the local filesystem that will be deployed. Use this tool if the entire folder content needs to be deployed.',
      inputSchema: {
        project: z
          .string()
          .describe(
            'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
          )
          .default(options.defaultProjectId),
        region: z
          .string()
          .optional()
          .default(options.defaultRegion)
          .describe('Region to deploy the service to'),
        service: z
          .string()
          .optional()
          .default(options.defaultServiceName)
          .describe('Name of the Cloud Run service to deploy to'),
        folderPath: z
          .string()
          .describe(
            'Absolute path to the folder to deploy (e.g. "/home/user/project/src")'
          ),
      },
    },
    gcpTool(
      options.gcpCredentialsAvailable,
      async (
        { project, region, service, folderPath },
        { sendNotification }
      ) => {
        if (typeof project !== 'string') {
          throw new Error(
            'Project must be specified, please prompt the user for a valid existing Google Cloud project ID.'
          );
        }
        if (typeof folderPath !== 'string' || folderPath.trim() === '') {
          throw new Error(
            'Folder path must be specified and be a non-empty string.'
          );
        }

        const progressCallback = createProgressCallback(sendNotification);

        // Deploy to Cloud Run
        try {
          await progressCallback({
            data: `Starting deployment of local folder for service ${service} in project ${project}...`,
          });
          const response = await deploy({
            projectId: project,
            serviceName: service,
            region: region,
            files: [folderPath],
            skipIamCheck: options.skipIamCheck, // Pass the new flag
            progressCallback,
          });
          return {
            content: [
              {
                type: 'text',
                text: `Cloud Run service ${service} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: 'text',
                text: `Error deploying folder to Cloud Run: ${error.message || error}`,
              },
            ],
          };
        }
      }
    )
  );
}

// Tool to deploy to Cloud Run from file contents
function registerDeployFileContentsTool(server, options) {
  server.registerTool(
    'deploy_file_contents',
    {
      description:
        'Deploy files to Cloud Run by providing their contents directly. Takes an array of file objects containing filename and content. Use this tool if the files only exist in the current chat context.',
      inputSchema: {
        project: z
          .string()
          .describe(
            'Google Cloud project ID. Leave unset for the app to be deployed in a new project. If provided, make sure the user confirms the project ID they want to deploy to.'
          )
          .default(options.defaultProjectId),
        region: z
          .string()
          .optional()
          .default(options.defaultRegion)
          .describe('Region to deploy the service to'),
        service: z
          .string()
          .optional()
          .default(options.defaultServiceName)
          .describe('Name of the Cloud Run service to deploy to'),
        files: z
          .array(
            z.object({
              filename: z
                .string()
                .describe(
                  'Name and path of the file (e.g. "src/index.js" or "data/config.json")'
                ),
              content: z
                .string()
                .optional()
                .describe('Text content of the file'),
            })
          )
          .describe('Array of file objects containing filename and content'),
      },
    },
    gcpTool(
      options.gcpCredentialsAvailable,
      async ({ project, region, service, files }, { sendNotification }) => {
        if (typeof project !== 'string') {
          throw new Error(
            'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
          );
        }
        if (typeof files !== 'object' || !Array.isArray(files)) {
          throw new Error('Files must be specified');
        }
        if (files.length === 0) {
          throw new Error('No files specified for deployment');
        }

        // Validate that each file has either content
        for (const file of files) {
          if (!file.content) {
            throw new Error(`File ${file.filename} must have content`);
          }
        }

        const progressCallback = createProgressCallback(sendNotification);

        // Deploy to Cloud Run
        try {
          await progressCallback({
            data: `Starting deployment of file contents for service ${service} in project ${project}...`,
          });
          const response = await deploy({
            projectId: project,
            serviceName: service,
            region: region,
            files: files,
            skipIamCheck: options.skipIamCheck, // Pass the new flag
            progressCallback,
          });
          return {
            content: [
              {
                type: 'text',
                text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: 'text',
                text: `Error deploying to Cloud Run: ${error.message || error}`,
              },
            ],
          };
        }
      }
    )
  );
}

// Tool to deploy to Cloud Run from container image
function registerDeployContainerImageTool(server, options) {
  server.registerTool(
    'deploy_container_image',
    {
      description:
        'Deploys a container image to Cloud Run. Use this tool if the user provides a container image URL.',
      inputSchema: {
        project: z
          .string()
          .describe(
            'Google Cloud project ID. Do not select it yourself, make sure the user provides or confirms the project ID.'
          )
          .default(options.defaultProjectId),
        region: z
          .string()
          .optional()
          .default(options.defaultRegion)
          .describe('Region to deploy the service to'),
        service: z
          .string()
          .optional()
          .default(options.defaultServiceName)
          .describe('Name of the Cloud Run service to deploy to'),
        imageUrl: z
          .string()
          .describe(
            'The URL of the container image to deploy (e.g. "gcr.io/cloudrun/hello")'
          ),
      },
    },
    gcpTool(
      options.gcpCredentialsAvailable,
      async ({ project, region, service, imageUrl }, { sendNotification }) => {
        if (typeof project !== 'string') {
          throw new Error(
            'Project must specified, please prompt the user for a valid existing Google Cloud project ID.'
          );
        }
        if (typeof imageUrl !== 'string' || imageUrl.trim() === '') {
          throw new Error(
            'Container image URL must be specified and be a non-empty string.'
          );
        }

        const progressCallback = createProgressCallback(sendNotification);

        // Deploy to Cloud Run
        try {
          const response = await deployImage({
            projectId: project,
            serviceName: service,
            region: region,
            imageUrl: imageUrl,
            skipIamCheck: options.skipIamCheck,
            progressCallback,
          });
          return {
            content: [
              {
                type: 'text',
                text: `Cloud Run service ${service} deployed in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}\nService URL: ${response.uri}`,
              },
            ],
          };
        } catch (error) {
          return {
            content: [
              {
                type: 'text',
                text: `Error deploying to Cloud Run: ${error.message || error}`,
              },
            ],
          };
        }
      }
    )
  );
}

export {
  registerListProjectsTool,
  registerCreateProjectTool,
  registerListServicesTool,
  registerGetServiceTool,
  registerGetServiceLogTool,
  registerDeployLocalFolderTool,
  registerDeployFileContentsTool,
  registerDeployContainerImageTool,
};

```