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

```
├── .gitignore
├── .npmignore
├── .prettierrc
├── assets
│   ├── architecture.drawio
│   └── architecture.svg
├── examples
│   ├── public
│   │   ├── app.js
│   │   ├── index.html
│   │   └── main.css
│   └── public.zip
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README_zh-CN.md
├── README.md
├── src
│   └── utils.ts
├── tools
│   ├── deploy_folder_or_zip.ts
│   └── deploy_html.ts
└── tsconfig.json
```

# Files

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

```
{
  "singleQuote": true
}

```

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

```
# Dependencies
node_modules/

# Build output
dist/

# Environment variables
.env
.env.local
.env.*

# NPM
.npmrc
.npmrc.local
.npmrc.*
.npm

# IDE
.idea/
.vscode/
*.sublime-*

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# System files
.DS_Store
Thumbs.db 

test.ts
DEV*.md
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
# Source
*.ts
!*.d.ts
src/
tests/
examples/
test.ts

# Config files
tsconfig.json
.gitignore
.npmignore
.eslintrc
.prettierrc

# Dependencies
node_modules/

# Environment variables
.env
.env.local
.env.*

# IDE
.idea/
.vscode/
*.sublime-*

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# System files
.DS_Store
Thumbs.db

# Git files
.git/
.github/ 
```

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

```markdown
# EdgeOne Pages MCP

An MCP service for deploying HTML content, folders, or full-stack projects to EdgeOne Pages and obtaining publicly accessible URLs.

<a href="https://glama.ai/mcp/servers/@TencentEdgeOne/edgeone-pages-mcp">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@TencentEdgeOne/edgeone-pages-mcp/badge" alt="EdgeOne Pages MCP server" />
</a>

## Demo

### Deploy HTML

![](https://cdnstatic.tencentcs.com/edgeone/pages/assets/U_GpJ-1746519327306.gif)

### Deploy Folder

![](https://cdnstatic.tencentcs.com/edgeone/pages/assets/kR_Kk-1746519251292.gif)

## Requirements

- Node.js 18 or higher

## MCP Configuration

### stdio MCP Server

Full-featured MCP service that supports the `deploy_folder` tool for deploying full-stack projects.

```jsonc
// Tencent Cloud International (Default)
{
  "mcpServers": {
    "edgeone-pages-mcp-server": {
      "timeout": 600,
      "command": "npx",
      "args": ["edgeone-pages-mcp-fullstack@latest"]
    }
  }
}

// Tencent Cloud China
{
  "mcpServers": {
    "edgeone-pages-mcp-server": {
      "timeout": 600,
      "command": "npx",
      "args": ["edgeone-pages-mcp-fullstack@latest", "--region", "china"]
    }
  }
}
```

The following MCP Server will be deprecated soon:

Supports both `deploy_html` and `deploy_folder_or_zip` tools.

```jsonc
{
  "mcpServers": {
    "edgeone-pages-mcp-server": {
      "command": "npx",
      "args": ["edgeone-pages-mcp@latest"],
      "env": {
        // Optional. 
        // If you need to deploy folders or zip files to 
        // EdgeOne Pages projects, provide your EdgeOne Pages API token.
        // How to obtain your API token: 
        // https://edgeone.ai/document/177158578324279296
        "EDGEONE_PAGES_API_TOKEN": "",
        // Optional. Leave empty to create a new EdgeOne Pages project.
        // Provide a project name to update an existing project.
        "EDGEONE_PAGES_PROJECT_NAME": ""
      }
    }
  }
}
```

### Streaming HTTP MCP Server

For MCP clients that support HTTP streaming, only supports the `deploy_html` tool.

```json
{
  "mcpServers": {
    "edgeone-pages-mcp-server": {
      "url": "https://mcp-on-edge.edgeone.site/mcp-server"
    }
  }
}
```

## Tool Details

### deploy_html Tool

#### Architecture Design

![EdgeOne Pages MCP Architecture](./assets/architecture.svg)

The architecture diagram shows the complete workflow of the `deploy_html` tool:

1. Large Language Model generates HTML content
2. Content is sent to the EdgeOne Pages MCP Server
3. MCP Server deploys the content to EdgeOne Pages Edge Functions
4. Content is stored in EdgeOne KV Store for fast edge access
5. MCP Server returns a publicly accessible URL
6. Users can access the deployed content via browser with fast edge delivery

#### Implementation Details

This tool integrates with EdgeOne Pages Functions to deploy static HTML content:

1. **EdgeOne Pages Functions** - A serverless computing platform that supports executing JavaScript/TypeScript code at the edge

2. **Core Implementation Features**:

   - Uses EdgeOne Pages KV storage to save and serve HTML content
   - Automatically generates publicly accessible URLs for each deployment
   - Provides comprehensive API error handling and feedback

3. **How It Works**:
   - MCP server receives HTML content through the `deploy_html` tool
   - Connects to EdgeOne Pages API to obtain the base URL
   - Deploys HTML content using the EdgeOne Pages KV API
   - Returns an immediately accessible public URL

For more information, refer to the [EdgeOne Pages Functions documentation](https://edgeone.ai/document/162227908259442688) and [EdgeOne Pages KV Storage Guide](https://edgeone.ai/document/162227803822321664).

The source code is open source and can be self-deployed with custom domain binding: https://github.com/TencentEdgeOne/self-hosted-pages-mcp

### deploy_folder Tool

This tool supports deploying complete projects to EdgeOne Pages:

- Supports full deployment of static website projects
- Supports deployment of full-stack applications
- Option to update existing projects or create new ones

## License

MIT

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["index.ts", "src/**/*", "tools/**/*"],
  "exclude": ["node_modules", "dist"]
}

```

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

```json
{
  "name": "edgeone-pages-mcp",
  "version": "0.0.15",
  "repository": {
    "type": "git",
    "url": "https://github.com/TencentEdgeOne/edgeone-pages-mcp"
  },
  "homepage": "https://edgeone.ai/products/pages",
  "description": "An MCP service for deploying HTML content to EdgeOne Pages and obtaining a publicly accessible URL.",
  "main": "dist/index.js",
  "type": "module",
  "bin": {
    "edgeone-pages-mcp": "./dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc",
    "prepack": "npm run build",
    "prepublishOnly": "npm run build"
  },
  "keywords": [
    "edgeone",
    "pages",
    "deploy",
    "mcp"
  ],
  "author": "EdgeOne Pages",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "cos-nodejs-sdk-v5": "^2.14.7",
    "dotenv": "^16.4.7",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@types/node": "^20.11.28",
    "typescript": "^5.4.2"
  }
}

```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';


export const showPackageVersion = (): void => {
  try {
      // Print package.json version
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    const possiblePaths = [
      join(process.cwd(), 'package.json'),
      join(__dirname, 'package.json'),
      join(__dirname, '../package.json'),
      join(__dirname, '../../package.json'),
    ];

    let packageJson;
    let found = false;

    for (const packageJsonPath of possiblePaths) {
      try {
        packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
        found = true;
        break;
      } catch (error) {
        continue;
      }
    }

    if (found && packageJson?.version) {
      console.log(`Package version: ${packageJson.version}`);
    } else {
      console.log('Package version: unknown');
    }
  } catch (error) {
    console.error('Error reading package.json:', error);
  }
};

```

--------------------------------------------------------------------------------
/examples/public/app.js:
--------------------------------------------------------------------------------

```javascript
document.addEventListener('DOMContentLoaded', function () {
  // Smooth scroll for navigation links
  document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
    anchor.addEventListener('click', function (e) {
      e.preventDefault();
      const target = document.querySelector(this.getAttribute('href'));
      if (target) {
        target.scrollIntoView({
          behavior: 'smooth',
          block: 'start',
        });
      }
    });
  });

  // Header scroll effect
  let lastScroll = 0;
  const header = document.querySelector('.header');

  window.addEventListener('scroll', () => {
    const currentScroll = window.pageYOffset;

    if (currentScroll <= 0) {
      header.style.boxShadow = 'none';
    } else {
      header.style.boxShadow =
        '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
    }

    lastScroll = currentScroll;
  });

  // Button hover animation
  const buttons = document.querySelectorAll('.btn-primary');
  buttons.forEach((button) => {
    button.addEventListener('mouseover', () => {
      button.style.transform = 'scale(1.05)';
    });

    button.addEventListener('mouseout', () => {
      button.style.transform = 'scale(1)';
    });
  });
});

```

--------------------------------------------------------------------------------
/tools/deploy_html.ts:
--------------------------------------------------------------------------------

```typescript
import { randomBytes } from 'crypto';
import os from 'os';
import path from 'path';
import fs from 'fs';

let installationId: string;

function generateInstallationId(): string {
  try {
    const idFilePath = path.join(os.tmpdir(), 'edgeone-pages-id');

    if (fs.existsSync(idFilePath)) {
      const id = fs.readFileSync(idFilePath, 'utf8').trim();
      if (id) {
        return id;
      }
    }

    const newId = randomBytes(8).toString('hex');

    try {
      fs.writeFileSync(idFilePath, newId);
    } catch (writeError) {
      // do nothing
    }

    return newId;
  } catch (error) {
    return randomBytes(8).toString('hex');
  }
}

installationId = generateInstallationId();

/**
 * Get the base URL for EdgeOne Pages deployment
 */
async function getBaseUrl(): Promise<string> {
  const res = await fetch('https://mcp.edgeone.site/get_base_url');
  if (!res.ok) {
    throw new Error(`[getBaseUrl] HTTP error: ${res.status} ${res.statusText}`);
  }
  const data = await res.json();
  return data.baseUrl;
}

/**
 * Deploy HTML content to EdgeOne Pages
 */
async function deployHtml(value: string, baseUrl: string): Promise<string> {
  const res = await fetch(baseUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Installation-ID': installationId,
    },
    body: JSON.stringify({ value }),
  });

  if (!res.ok) {
    throw new Error(`[deployHtml] HTTP error: ${res.status} ${res.statusText}`);
  }

  const { url } = await res.json();
  return url;
}

/**
 * Deploy HTML content to EdgeOne Pages and return the deployment URL
 */
export const deployHtmlToEdgeOne = async (html: string): Promise<string> => {
  try {
    const baseUrl = await getBaseUrl();
    const url = await deployHtml(html, baseUrl);
    return url;
  } catch (e) {
    console.error('Error deploying HTML:', e);
    throw e;
  }
};

```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

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

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { deployFolderOrZipToEdgeOne } from './tools/deploy_folder_or_zip.js';
import { deployHtmlToEdgeOne } from './tools/deploy_html.js';
import { showPackageVersion } from './src/utils.js';

import dotenv from 'dotenv';
dotenv.config();

const server = new McpServer({
  name: 'edgeone-pages-deploy-mcp-server',
  version: '1.0.0',
  description: `Deploy HTML content to EdgeOne Pages with ease.
Provide your HTML and let the service handle deployment.
Also support to deploy a folder to EdgeOne Pages.
Receive a public URL to access your live page.`,
});

const handleUncaughtError = (error: any) => {
  const errorMessage = error.message || 'Unknown error occurred';
  return {
    content: [
      {
        type: 'text' as const,
        text: `Error: ${errorMessage}`,
      },
    ],
    isError: true,
  };
};

server.tool(
  'deploy_html',
  'Deploy HTML content to EdgeOne Pages, return the public URL',
  {
    value: z.string().describe(
      `Provide the full HTML markup you wish to publish.
After deployment, the system will generate and return a public URL where your content can be accessed.`
    ),
  },
  async ({ value }) => {
    try {
      const result = await deployHtmlToEdgeOne(value);

      return {
        content: [
          {
            type: 'text' as const,
            text: result,
          },
        ],
      };
    } catch (e) {
      return handleUncaughtError(e);
    }
  }
);

server.tool(
  'deploy_folder_or_zip',
  'Deploy a built frontend directory (or zip file) to EdgeOne Pages. Returns: the deployment URL and project metadata.',
  {
    builtFolderPath: z
      .string()
      .describe(
        'Provide the absolute path to the built frontend folder(or zip file) you wish to deploy.'
      ),
  },
  async ({ builtFolderPath }) => {
    try {
      const result = await deployFolderOrZipToEdgeOne(builtFolderPath);
      return {
        content: [
          {
            type: 'text' as const,
            text: result,
          },
        ],
      };
    } catch (e) {
      return handleUncaughtError(e);
    }
  }
);

async function main() {
  showPackageVersion();

  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error('Error starting server:', error);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/examples/public/main.css:
--------------------------------------------------------------------------------

```css
body {
  margin: 0;
  padding: 0;
  background-color: #121212;
  color: rgba(255, 255, 255, 0.87);
  font-family: 'Roboto', sans-serif;
  line-height: 1.6;
}

.header {
  position: fixed;
  width: 100%;
  top: 0;
  z-index: 50;
  background-color: rgba(18, 18, 18, 0.8);
  backdrop-filter: blur(8px);
}

.nav {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1rem 1.5rem;
}

.nav-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.logo {
  font-size: 1.5rem;
  font-weight: bold;
}

.nav-links {
  display: none;
}

.nav-links a {
  color: #9ca3af;
  text-decoration: none;
  margin-left: 2rem;
  transition: color 0.3s ease;
}

.nav-links a:hover {
  color: white;
}

.hero {
  padding: 8rem 1.5rem 5rem;
  text-align: center;
}

.hero-content {
  max-width: 1200px;
  margin: 0 auto;
}

.hero h1 {
  font-size: 3.5rem;
  font-weight: bold;
  margin-bottom: 1.5rem;
}

.hero p {
  font-size: 1.25rem;
  color: #9ca3af;
  max-width: 48rem;
  margin: 0 auto 3rem;
}

.hero-cta {
  display: flex;
  gap: 1.5rem;
  justify-content: center;
}

.features {
  padding: 5rem 1.5rem;
  background-color: #1e1e1e;
}

.features h2 {
  font-size: 2.5rem;
  text-align: center;
  margin-bottom: 4rem;
}

.features-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.feature-card {
  background: rgba(30, 30, 30, 0.7);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  padding: 2rem;
  border-radius: 0.75rem;
  transition: transform 0.3s ease;
}

.feature-card:hover {
  transform: translateY(-5px);
}

.feature-card i {
  font-size: 2.5rem;
  color: #7c4dff;
  margin-bottom: 1.5rem;
}

.feature-card h3 {
  font-size: 1.25rem;
  margin-bottom: 1rem;
}

.feature-card p {
  color: #9ca3af;
}

.cta {
  padding: 5rem 1.5rem;
}

.cta-content {
  max-width: 1200px;
  margin: 0 auto;
  text-align: center;
  padding: 3rem !important;
}

.cta h2 {
  font-size: 2.5rem;
  margin-bottom: 1.5rem;
}

.cta p {
  font-size: 1.25rem;
  color: #9ca3af;
  max-width: 32rem;
  margin: 0 auto 2rem;
}

.glow-text {
  text-shadow: 0 0 10px rgba(124, 77, 255, 0.5),
    0 0 20px rgba(124, 77, 255, 0.3);
}

.btn-primary {
  background: linear-gradient(45deg, #7c4dff 30%, #9d7fff 90%);
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 9999px;
  font-weight: 500;
  cursor: pointer;
  box-shadow: 0 0 20px rgba(124, 77, 255, 0.4);
  transition: all 0.3s ease;
}

.btn-primary:hover {
  transform: scale(1.05);
  box-shadow: 0 0 30px rgba(124, 77, 255, 0.6);
}

.btn-secondary {
  background: transparent;
  color: #7c4dff;
  border: 1px solid #7c4dff;
  padding: 0.75rem 1.5rem;
  border-radius: 9999px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn-secondary:hover {
  background: #7c4dff;
  color: white;
}

@media (min-width: 768px) {
  .nav-links {
    display: flex;
  }

  .hero h1 {
    font-size: 4.5rem;
  }

  .features-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

```

--------------------------------------------------------------------------------
/examples/public/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta
      name="description"
      content="Launch your web projects with modern solutions and innovative technology"
    />
    <meta
      name="keywords"
      content="web launch, web development, technology, innovation, digital solutions"
    />
    <meta property="og:title" content="Web Launch - Modern Web Solutions" />
    <meta
      property="og:description"
      content="Launch your web projects with modern solutions and innovative technology"
    />
    <meta property="og:type" content="website" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="Web Launch - Modern Web Solutions" />
    <meta
      name="twitter:description"
      content="Launch your web projects with modern solutions and innovative technology"
    />
    <title>Web Launch - Modern Web Solutions</title>
    <link
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
      rel="stylesheet"
    />
    <link href="main.css" rel="stylesheet" />
  </head>
  <body>
    <header class="header">
      <nav class="nav">
        <div class="nav-content">
          <div class="logo glow-text">WebLaunch</div>
          <div class="nav-links">
            <a href="#features">Features</a>
            <a href="#about">About</a>
            <a href="#contact">Contact</a>
          </div>
          <button class="btn-primary">Get Started</button>
        </div>
      </nav>
    </header>

    <main>
      <section class="hero">
        <div class="hero-content">
          <h1 class="glow-text">Launch Your Web Vision</h1>
          <p>
            Transform your ideas into reality with cutting-edge web solutions
            powered by modern technology
          </p>
          <div class="hero-cta">
            <button class="btn-primary">Start Building</button>
            <button class="btn-secondary">Learn More</button>
          </div>
        </div>
      </section>

      <section class="features">
        <h2 class="glow-text">Powerful Features</h2>
        <div class="features-grid">
          <div class="feature-card">
            <i class="fas fa-rocket"></i>
            <h3>Quick Launch</h3>
            <p>
              Deploy your projects faster than ever with our streamlined process
            </p>
          </div>
          <div class="feature-card">
            <i class="fas fa-shield-alt"></i>
            <h3>Secure Platform</h3>
            <p>Built-in security features to protect your applications</p>
          </div>
          <div class="feature-card">
            <i class="fas fa-tachometer-alt"></i>
            <h3>High Performance</h3>
            <p>Optimized infrastructure for maximum speed and efficiency</p>
          </div>
        </div>
      </section>

      <section class="cta">
        <div class="cta-content feature-card">
          <h2 class="glow-text">Ready to Launch Your Project?</h2>
          <p>
            Join thousands of developers who have already launched their web
            projects successfully
          </p>
          <button class="btn-primary">Get Started Now</button>
        </div>
      </section>
    </main>

    <script src="app.js"></script>
    <script type="text/babel" src="components/Header.js"></script>
    <script type="text/babel" src="components/Hero.js"></script>
    <script type="text/babel" src="components/Features.js"></script>
    <script type="text/babel" src="components/CallToAction.js"></script>
  </body>
</html>

```

--------------------------------------------------------------------------------
/assets/architecture.svg:
--------------------------------------------------------------------------------

```
<svg xmlns="http://www.w3.org/2000/svg" style="background: #FFFFFF; background-color: light-dark(#FFFFFF, #121212);" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="732px" height="341px" viewBox="-0.5 -0.5 732 341" content="&lt;mxfile&gt;&lt;diagram id=&quot;123456&quot; name=&quot;EdgeOne Pages Deploy MCP Architecture&quot;&gt;3Vrbbts4EP0aA9uHBKIuvjzGTtIC26JG0u42Twtaomyhsuil6MTZr1+OROpGypZj2dltDMTi8KpzDkfDkQfObL37yPBm9YUGJB7YVrAbOLcD20Yj5IgvsLzmluEE5YYliwLZqDQ8Rv8QabSkdRsFJK015JTGPNrUjT5NEuLzmg0zRl/qzUIa12fd4CXRDI8+jnXrn1HAV7l17Fml/ROJlis1M7JkzQL7P5eMbhM538B27rO/vHqN1ViyfbrCAX2pmJy7gTNjlPL8ar2bkRiwVbDl/e5baot1M5LwLh3svMMzjrfy1u+CJfmaEGGci7tMxfct2cT0VVx8mc3F/xvmryIuUN8yIm+CvyrgONmJeacrvo6FAYnLlDP6k8xoTJmwJFQM7UzDKI4bJhxHy0QUfbFyIuzTZ8J4JCi5kRXrKAhgmukLTP+4wT7M+SIEKGwZ5ARuyoLhacKlqNBYleUiDRBJE0xIdhWThOwjoWvCGSAgawudSnkrNl9KrbiOtK0qOlE2LOW5LEYuKRIXkiUzY47G2MAexgD6Qlws4SLnaRZHcIeyUgxb1GusleihFnwrhFa5E/IOEBmFoUa0qHHFhgndokZtJLtYwUkcOHUKkIEDNDRwUGzVU0hwD5Ogdk9OxiNh4tbMZMiOTFmCbL/9lUGe+71eCfPHZOhPTIQ5Lh57ztkIG3VgzDsXY56BsR5BDUPiLxwTqOFi4VveuUAdNjxRgdVBUN0eQB0e3gbwOBFNfv8DNgGn8Mg4s0MKw3Diu0dQgXqioumRRgYqkIEKrwcmRoeZ+J6CA7KmEB11dUXz7SKOfHjo+z5J03558uBj4mlC4HMpP+R03TLDHngaH3ZDJAluIISFWCjGaSrw3xNQCbhmI8ezLCNcZBfxH7IfXD9BgHTtydLtTsZLWeFVFRJxTz+qhUovKJbdspLqF+MFiec0jXhEjYHc50YDTjdd4r4F5Zyus9vDjCtoZNSY2e4jwDuP/Boaw8NxBk2rbkhQOwnoqqmowiQKZWMkxjx6rp8fTEqRM8xpBMGZEiWaNJxH0ymkdMt8IntVA/rGQNpTtjmQAGxJ+J6BVEMahinhmrgLaDrpfaLpvThNzERQTuQiTtL/nrDll9J/q4ZzbdQi04Oyfq01v4zIPbvlCXmsyLWopznQZUWuVlNR+QMRx+NE2L4/fD5d4XtiSKVwq6bw4X6JHy3VQoMH1ZqL+r/ubjUBTdDblOihSWOgbkp8i8qQprKz+s4Ap6sitIPCHHPBOZAsDlOWVxVf4VKfKs72kHstPepTzaF2c68sF0WrZIt0kZJsTEJuEGzR7hj3mrM6qIbeXT2u513S5bquV9Nnkao6VujNgZzmofzCLldPXJ6+GfacO04LJK7tqtLR/0LpXWX9PqGD00yldg4d3PpAdjMPcmEdt6dzg+i5eTCHPPYA3nooyQ7/3tK8gdiP8Fc11fKQxWk/H6M+rjAbZutpATIDdL9NfBDssWtpbGr9BUPvLw7qLwq0xIJh5+zJCdW9JnIMuQbLsG36eFOA2rPU6QYnNVwVaQDvFUBztaaBqLoRrRIqsdK5feRBREWTbwwn6YYyEMfXhwqV+UQHddXfehjBa7yIQXGfvn2b19e2d1nvKzz9hVj1JVaZ+jpJjqgZ9rp6MOK4w2uVLe87S4lMWfgDCpC+Bohny8VvYhViGkt9fciAsQCrqzQDCxoitNnlFZl+JFFQIx+NqqZdWZU3uVeNJWQhHMxevfhg1OM9TmFLSBeokqgtMryo4HpQk9fIWdlj3bmZ3sD14tv0xNK++E9uqX3B39TOHl+/fBaplkUt4+JqIjUPUIP2tCrxkFtJqzZixE4aeqe06sS99t4WOGqJVX2os4aOolj+LCRvXv72xrn7Fw==&lt;/diagram&gt;&lt;/mxfile&gt;"><defs/><rect fill="#FFFFFF" width="100%" height="100%" x="0" y="0" style="fill: light-dark(rgb(255, 255, 255), rgb(18, 18, 18));"/><g><g><rect x="181" y="0" width="430" height="30" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 428px; height: 1px; padding-top: 15px; margin-left: 182px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 18px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">EdgeOne Pages Deploy MCP Architecture</div></div></div></foreignObject><text x="396" y="20" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="18px" text-anchor="middle" font-weight="bold">EdgeOne Pages Deploy MCP Architecture</text></switch></g></g><g><rect x="1" y="100" width="160" height="100" rx="15" ry="15" fill="#d1e7ff" stroke="#4285f4" stroke-width="2" pointer-events="all" style="fill: light-dark(rgb(209, 231, 255), rgb(24, 43, 63)); stroke: light-dark(rgb(66, 133, 244), rgb(76, 133, 229));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 150px; margin-left: 2px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>MCP Client</b></div></div></div></foreignObject><text x="81" y="154" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">MCP Client</text></switch></g></g><g><rect x="341" y="100" width="150" height="100" rx="15" ry="15" fill="#c8e6c9" stroke="#34a853" stroke-width="2" pointer-events="all" style="fill: light-dark(rgb(200, 230, 201), rgb(28, 54, 29)); stroke: light-dark(rgb(52, 168, 83), rgb(47, 146, 73));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 150px; margin-left: 342px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>Pages MCP Server</b><br />deploy_html tool</div></div></div></foreignObject><text x="416" y="154" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Pages MCP Server...</text></switch></g></g><g><rect x="581" y="80" width="150" height="140" rx="21" ry="21" fill="#ffecb3" stroke="#fbbc05" stroke-width="2" pointer-events="all" style="fill: light-dark(rgb(255, 236, 179), rgb(50, 34, 0)); stroke: light-dark(rgb(251, 188, 5), rgb(129, 75, 0));"/></g><g><rect x="601" y="150" width="110" height="50" rx="7.5" ry="7.5" fill="#fff9c4" stroke="#fbbc05" pointer-events="all" style="fill: light-dark(rgb(255, 249, 196), rgb(33, 28, 0)); stroke: light-dark(rgb(251, 188, 5), rgb(129, 75, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 108px; height: 1px; padding-top: 175px; margin-left: 602px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>Edge KV Store</b></div></div></div></foreignObject><text x="656" y="179" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Edge KV Store</text></switch></g></g><g><rect x="341" y="280" width="150" height="60" rx="9" ry="9" fill="#f5f5f5" stroke="#9e9e9e" stroke-width="2" pointer-events="all" style="fill: light-dark(rgb(245, 245, 245), rgb(26, 26, 26)); stroke: light-dark(rgb(158, 158, 158), rgb(101, 101, 101));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 148px; height: 1px; padding-top: 310px; margin-left: 342px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>User Browser</b><br />Public Access</div></div></div></foreignObject><text x="416" y="314" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">User Browser...</text></switch></g></g><g><path d="M 161 130 L 332.76 130" fill="none" stroke="#c73500" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(199, 53, 0), rgb(255, 145, 100));"/><path d="M 338.76 130 L 330.76 134 L 332.76 130 L 330.76 126 Z" fill="#c73500" stroke="#c73500" stroke-width="2" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(199, 53, 0), rgb(255, 145, 100)); stroke: light-dark(rgb(199, 53, 0), rgb(255, 145, 100));"/></g><g><path d="M 491 150 L 572.76 150" fill="none" stroke="#34a853" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(52, 168, 83), rgb(47, 146, 73));"/><path d="M 578.76 150 L 570.76 154 L 572.76 150 L 570.76 146 Z" fill="#34a853" stroke="#34a853" stroke-width="2" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(52, 168, 83), rgb(47, 146, 73)); stroke: light-dark(rgb(52, 168, 83), rgb(47, 146, 73));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-end; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 143px; margin-left: 536px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">Deploy Content</div></div></div></foreignObject><text x="536" y="143" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="11px" text-anchor="middle">Deploy Con...</text></switch></g></g><g><path d="M 581 171 L 498.24 170.09" fill="none" stroke="#fbbc05" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(251, 188, 5), rgb(129, 75, 0));"/><path d="M 492.24 170.02 L 500.28 166.11 L 498.24 170.09 L 500.19 174.11 Z" fill="#fbbc05" stroke="#fbbc05" stroke-width="2" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(251, 188, 5), rgb(129, 75, 0)); stroke: light-dark(rgb(251, 188, 5), rgb(129, 75, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 177px; margin-left: 535px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">Return URL</div></div></div></foreignObject><text x="535" y="188" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="11px" text-anchor="middle">Return URL</text></switch></g></g><g><path d="M 416 200 L 416 271.76" fill="none" stroke="#34a853" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="10 10" pointer-events="stroke" style="stroke: light-dark(rgb(52, 168, 83), rgb(47, 146, 73));"/><path d="M 416 277.76 L 412 269.76 L 416 271.76 L 420 269.76 Z" fill="#34a853" stroke="#34a853" stroke-width="2" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(52, 168, 83), rgb(47, 146, 73)); stroke: light-dark(rgb(52, 168, 83), rgb(47, 146, 73));"/></g><g><path d="M 491 310 L 604.41 224.94" fill="none" stroke="#9e9e9e" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(158, 158, 158), rgb(101, 101, 101));"/><path d="M 609.21 221.34 L 605.21 229.34 L 604.41 224.94 L 600.41 222.94 Z" fill="#9e9e9e" stroke="#9e9e9e" stroke-width="2" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(158, 158, 158), rgb(101, 101, 101)); stroke: light-dark(rgb(158, 158, 158), rgb(101, 101, 101));"/></g><g><rect x="606" y="110" width="100" height="30" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 125px; margin-left: 607px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; "><div><font color="#000000" style="color: light-dark(rgb(0, 0, 0), rgb(237, 237, 237));">Pages</font></div><div><font color="#000000" style="color: light-dark(rgb(0, 0, 0), rgb(237, 237, 237));">Edge Functions</font></div></div></div></div></foreignObject><text x="656" y="129" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle" font-weight="bold">Pages...</text></switch></g></g><g><rect x="81" y="125" width="346.25" height="50" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 344px; height: 1px; padding-top: 150px; margin-left: 82px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><span style="text-wrap-mode: nowrap;">Stdio Transport OR</span><div><span style="text-wrap-mode: nowrap;">Streamable HTTP Transport</span></div></div></div></div></foreignObject><text x="254" y="154" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Stdio Transport OR...</text></switch></g></g><g><rect x="561" y="260" width="60" height="30" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 275px; margin-left: 562px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: &quot;Helvetica&quot;; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><span style="color: light-dark(rgb(0, 0, 0), rgb(237, 237, 237)); font-size: 11px; text-align: left; text-wrap-mode: nowrap; background-color: light-dark(rgb(255, 255, 255), rgb(18, 18, 18));">Fast Edge Access</span></div></div></div></foreignObject><text x="591" y="279" fill="light-dark(#000000, #ffffff)" font-family="&quot;Helvetica&quot;" font-size="12px" text-anchor="middle">Fast Edge...</text></switch></g></g><g><path d="M 169.24 174.5 L 341 174.5" fill="none" stroke="#b20000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(178, 0, 0), rgb(255, 172, 172));"/><path d="M 163.24 174.5 L 171.24 170.5 L 169.24 174.5 L 171.24 178.5 Z" fill="#b20000" stroke="#b20000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(178, 0, 0), rgb(255, 172, 172)); stroke: light-dark(rgb(178, 0, 0), rgb(255, 172, 172));"/></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg>
```

--------------------------------------------------------------------------------
/tools/deploy_folder_or_zip.ts:
--------------------------------------------------------------------------------

```typescript
import COS from 'cos-nodejs-sdk-v5';
import * as path from 'path';
import * as fs from 'fs/promises';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

const BASE_API_URL1 = 'https://pages-api.cloud.tencent.com/v1';
const BASE_API_URL2 = 'https://pages-api.edgeone.ai/v1';

let BASE_API_URL = '';

// Console override for logging
interface LogEntry {
  timestamp: string;
  level: string;
  message: string;
}

let deploymentLogs: LogEntry[] = [];
let originalConsole: Console;

const overrideConsole = () => {
  if (!originalConsole) {
    originalConsole = { ...console };
  }

  const createLogFunction = (level: string, originalFn: Function) => {
    return (...args: any[]) => {
      const timestamp = new Date().toISOString();
      const message = args
        .map((arg) =>
          typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
        )
        .join(' ');

      deploymentLogs.push({
        timestamp,
        level,
        message,
      });
    };
  };

  console.log = createLogFunction('LOG', originalConsole.log);
  console.error = createLogFunction('ERROR', originalConsole.error);
  console.warn = createLogFunction('WARN', originalConsole.warn);
  console.info = createLogFunction('INFO', originalConsole.info);
};

const restoreConsole = () => {
  if (originalConsole) {
    console.log = originalConsole.log;
    console.error = originalConsole.error;
    console.warn = originalConsole.warn;
    console.info = originalConsole.info;
  }
};

const resetLogs = () => {
  deploymentLogs = [];
};

const formatLogs = (): string => {
  if (deploymentLogs.length === 0) {
    return '';
  }

  // Remove duplicates by keeping track of seen messages
  const seenMessages = new Set<string>();
  const uniqueLogs = deploymentLogs.filter((log) => {
    const key = `${log.level}: ${log.message}`;
    if (seenMessages.has(key)) {
      return false;
    }
    seenMessages.add(key);
    return true;
  });

  const logLines = uniqueLogs.map((log) => {
    return `${log.level}: ${log.message}`;
  });

  return `Deployment Process Log:\n${'='.repeat(50)}\n${logLines.join(
    '\n'
  )}\n${'='.repeat(50)}\n\n`;
};

// Export BASE_API_URL for use in other files
export const getBaseApiUrl = () => BASE_API_URL;

// Get API key from environment variable or use argument
const getApiKey = () => {
  return process.env.EDGEONE_PAGES_API_TOKEN;
};

// Get authorization header with API key
export const getAuthorization = () => {
  const apiKey = getApiKey();
  if (!apiKey) {
    throw new Error(
      'Missing EDGEONE_PAGES_API_TOKEN. Please provide a token with --token or set it as an environment variable.'
    );
  }
  return `Bearer ${apiKey}`;
};

// Get projectName from environment variable
const getProjectName = () => process.env.EDGEONE_PAGES_PROJECT_NAME || '';

let tempProjectName: string | undefined;

const getTempProjectName = (): string => {
  if (!tempProjectName) {
    tempProjectName = `local-upload-${Date.now()}`;
  }
  return tempProjectName;
};

const resetTempProjectName = (): void => {
  tempProjectName = undefined;
};

// Types
interface FileInfo {
  isDir: boolean;
  path: string;
  size: number;
}

interface CosFile {
  Bucket: string;
  Region: string;
  Key: string;
  FilePath: string;
  [key: `x-cos-meta-${string}`]: string;
}

interface CosTempTokenResponse {
  Code: number;
  Data: {
    Response: {
      RequestId: string;
      Bucket: string;
      Region: string;
      TargetPath: string;
      ExpiredTime: number;
      Expiration: string;
      Credentials: {
        Token: string;
        TmpSecretId: string;
        TmpSecretKey: string;
      };
    };
  };
  Message: string;
  RequestId: string;
}

interface ApiResponse<T> {
  Code: number;
  Data: T;
  Message: string;
  RequestId: string;
}

interface Project {
  ProjectId: string;
  Name: string;
  Status: string;
  PresetDomain: string;
  CustomDomains?: Array<{
    Status: string;
    Domain: string;
  }>;
}

interface ProjectsResponse {
  Response: {
    Projects: Project[];
  };
}

interface CreatePagesProjectResponse {
  Response: {
    ProjectId: string;
  };
}

interface EncipherTokenResponse {
  Response: {
    Token: string;
    Timestamp: number;
  };
}

interface DeploymentResult {
  DeploymentId: string;
  ProjectId: string;
  Status: string;
  Code: number;
  BuildCost: string;
  ViaMeta: string;
  Env: string;
  RepoBranch: string | null;
  RepoCommitHash: string;
  RepoCommitMsg: string | null;
  PreviewUrl: string;
  CreatedOn: string;
  ModifiedOn: string;
  CoverUrl: string | null;
  UsedInProd: boolean;
  MetaData: string;
  ProjectUrl: string;
}

interface UploadResult {
  success: boolean;
  targetPath?: string;
  error?: any;
}

// Token cache mechanism
interface TokenCache {
  token: CosTempTokenResponse | null;
  cos: COS | null;
}

const tokenCache: TokenCache = {
  token: null,
  cos: null,
};

// Reset token cache
const resetTokenCache = () => {
  tokenCache.token = null;
  tokenCache.cos = null;
};

// Utility functions
const sleep = (ms: number): Promise<void> => {
  return new Promise((resolve) => setTimeout(resolve, ms));
};

const checkAndSetBaseUrl = async (): Promise<void> => {
  const res1 = await fetch(`${BASE_API_URL1}`, {
    method: 'POST',
    headers: {
      Authorization: getAuthorization(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      Action: 'DescribePagesProjects',
      PageNumber: 1,
      PageSize: 10,
    }),
  });

  const res2 = await fetch(`${BASE_API_URL2}`, {
    method: 'POST',
    headers: {
      Authorization: getAuthorization(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      Action: 'DescribePagesProjects',
      PageNumber: 1,
      PageSize: 10,
    }),
  });

  // Parse responses
  const json1 = (await res1
    .json()
    .catch(() => ({ Code: -1 }))) as ApiResponse<ProjectsResponse>;
  const json2 = (await res2
    .json()
    .catch(() => ({ Code: -1 }))) as ApiResponse<ProjectsResponse>;

  // Check if either endpoint worked
  if (json1.Code === 0) {
    BASE_API_URL = BASE_API_URL1;
    console.log('Using BASE_API_URL1 endpoint');
  } else if (json2.Code === 0) {
    BASE_API_URL = BASE_API_URL2;
    console.log('Using BASE_API_URL2 endpoint');
  } else {
    // Both endpoints failed
    throw new Error(
      'Invalid EDGEONE_PAGES_API_TOKEN. Please check your API token. For more information, please refer to https://edgeone.ai/document/177158578324279296'
    );
  }
};

// API functions
/**
 * Get temporary COS token for file uploads
 */
const getCosTempToken = async (): Promise<CosTempTokenResponse> => {
  // Return cached token if available
  if (tokenCache.token) {
    return tokenCache.token;
  }

  let body;
  if (getProjectName()) {
    const result = await describePagesProjects({
      projectName: getProjectName(),
    });
    if (result.Data.Response.Projects.length > 0) {
      body = { ProjectId: result.Data.Response.Projects[0].ProjectId };
    } else {
      throw new Error(`Project ${getProjectName()} not found`);
    }
  } else {
    body = { ProjectName: getTempProjectName() };
  }

  const res = await fetch(`${BASE_API_URL}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: getAuthorization(),
    },
    body: JSON.stringify(
      Object.assign(body, { Action: 'DescribePagesCosTempToken' })
    ),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`API request failed (${res.status}): ${errorText}`);
  }

  const tokenResponse = (await res.json()) as CosTempTokenResponse;
  // Cache the token
  tokenCache.token = tokenResponse;
  return tokenResponse;
};

/**
 * Get or create a project
 */
const getOrCreateProject = async (): Promise<ApiResponse<ProjectsResponse>> => {
  if (getProjectName()) {
    const result = await describePagesProjects({
      projectName: getProjectName(),
    });
    if (result.Data.Response.Projects.length > 0) {
      console.log(
        `[getOrCreateProject] Project ${getProjectName()} already exists. Using existing project.`
      );
      return result;
    }
  }
  console.log(
    `[getOrCreateProject] ProjectName is not provided. Creating new project.`
  );
  return await createPagesProject();
};

/**
 * Create a new pages project
 */
const createPagesProject = async (): Promise<ApiResponse<ProjectsResponse>> => {
  try {
    const res = await fetch(`${BASE_API_URL}`, {
      method: 'POST',
      headers: {
        Authorization: getAuthorization(),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        Action: 'CreatePagesProject',
        Name: getProjectName() || getTempProjectName(),
        Provider: 'Upload',
        Source: 'mcp',
        Channel: 'Custom',
        Area: 'global',
      }),
    });

    if (!res.ok) {
      const errorText = await res.text();
      throw new Error(`API request failed (${res.status}): ${errorText}`);
    }

    const data = (await res.json()) as ApiResponse<CreatePagesProjectResponse>;

    const projectInfo = await describePagesProjects({
      projectId: data?.Data?.Response?.ProjectId,
    });

    return projectInfo;
  } catch (error) {
    console.error('Error creating pages project: ' + error);
    throw error;
  }
};

/**
 * Describe pages projects
 */
const describePagesProjects = async (opts: {
  projectId?: string;
  projectName?: string;
}): Promise<ApiResponse<ProjectsResponse>> => {
  const { projectId, projectName } = opts;

  const filters = [];
  if (projectId) {
    filters.push({ Name: 'ProjectId', Values: [projectId] });
  }
  if (projectName) {
    filters.push({ Name: 'Name', Values: [projectName] });
  }

  const res = await fetch(`${BASE_API_URL}`, {
    method: 'POST',
    headers: {
      Authorization: getAuthorization(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      Action: 'DescribePagesProjects',
      Filters: filters,
      Offset: 0,
      Limit: 10,
      OrderBy: 'CreatedOn',
    }),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`API request failed (${res.status}): ${errorText}`);
  }

  return (await res.json()) as ApiResponse<ProjectsResponse>;
};

/**
 * Describe pages deployments
 */
const describePagesDeployments = async (
  projectId: string
): Promise<ApiResponse<any>> => {
  const res = await fetch(`${BASE_API_URL}`, {
    method: 'POST',
    headers: {
      Authorization: getAuthorization(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      Action: 'DescribePagesDeployments',
      ProjectId: projectId,
      Offset: 0,
      Limit: 50,
      OrderBy: 'CreatedOn',
      Order: 'Desc',
    }),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`API request failed (${res.status}): ${errorText}`);
  }

  return (await res.json()) as ApiResponse<any>;
};

/**
 * Describe pages encipher token
 */
const describePagesEncipherToken = async (
  url: string
): Promise<ApiResponse<EncipherTokenResponse>> => {
  const res = await fetch(`${BASE_API_URL}`, {
    method: 'POST',
    headers: {
      Authorization: getAuthorization(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      Action: 'DescribePagesEncipherToken',
      Text: url,
    }),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`API request failed (${res.status}): ${errorText}`);
  }

  return (await res.json()) as ApiResponse<EncipherTokenResponse>;
};

/**
 * Check if a path is a zip file
 */
const isZipFile = (filePath: string): boolean => {
  return path.extname(filePath).toLowerCase() === '.zip';
};

/**
 * Create a new deployment
 */
const createPagesDeployment = async (opts: {
  projectId: string;
  targetPath: string;
  isZip: boolean;
  env: 'Production' | 'Preview';
}): Promise<ApiResponse<any>> => {
  const { projectId, targetPath, isZip, env } = opts;

  const res = await fetch(`${BASE_API_URL}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: getAuthorization(),
    },
    body: JSON.stringify({
      Action: 'CreatePagesDeployment',
      ProjectId: projectId,
      ViaMeta: 'Upload',
      Provider: 'Upload',
      Env: env,
      DistType: isZip ? 'Zip' : 'Folder',
      TempBucketPath: targetPath,
    }),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(
      `[createPagesDeployment] API request failed (${res.status}): ${errorText}`
    );
  }

  const data = (await res.json()) as ApiResponse<any>;

  if (data?.Data?.Response?.Error) {
    throw new Error(
      `[createPagesDeployment] Deployment creation failed: ${data.Data.Response.Error.Message}`
    );
  }

  return data;
};

// COS (Cloud Object Storage) functions
// Initialize COS with dynamic authentication
const getCosInstance = async (): Promise<COS> => {
  if (tokenCache.cos) {
    return tokenCache.cos;
  }

  const result = await getCosTempToken();
  if (
    result.Code !== 0 ||
    !result.Data ||
    !result.Data.Response ||
    !result.Data.Response.Credentials
  ) {
    throw new Error('Failed to get COS temp token');
  }

  const response = result.Data.Response;
  const credentials = response.Credentials;

  const cos = new COS({
    SecretId: credentials.TmpSecretId,
    SecretKey: credentials.TmpSecretKey,
    SecurityToken: credentials.Token,
  });

  tokenCache.cos = cos;
  return cos;
};

/**
 * Recursively list all files in a directory
 */
const fastListFolder = async (rootPath: string): Promise<FileInfo[]> => {
  const list: FileInfo[] = [];

  const deep = async (dirPath: string): Promise<void> => {
    const files = await fs.readdir(dirPath, { withFileTypes: true });

    for (const file of files) {
      const filePath = path.join(dirPath, file.name);
      const isDir = file.isDirectory();
      const stats = await fs.stat(filePath);

      list.push({
        isDir,
        path: isDir ? `${filePath}/` : filePath,
        size: isDir ? 0 : stats.size,
      });

      if (isDir) {
        await deep(filePath);
      }
    }
  };

  try {
    await deep(rootPath);
    if (list.length > 1000000) {
      throw new Error('too_much_files');
    }
    return list;
  } catch (error) {
    console.error('Error in fastListFolder: ' + error);
    throw error;
  }
};

/**
 * Convert file list to COS format
 */
const getFiles = (
  list: FileInfo[],
  localFolder: string,
  bucket: string,
  region: string,
  targetPath: string
): CosFile[] => {
  return list
    .map((file) => {
      const filename = path
        .relative(localFolder, file.path)
        .replace(/\\/g, '/');
      const Key = `${targetPath}/${filename || ''}`;
      return {
        Bucket: bucket,
        Region: region,
        Key,
        FilePath: file.path,
      };
    })
    .filter((file) => {
      if (file.FilePath.endsWith('/')) {
        return false;
      }
      if (file.Key === '/' || file.Key === '') {
        return false;
      }
      return true;
    });
};

/**
 * Upload files to COS
 */
const uploadFiles = async (files: CosFile[]): Promise<any> => {
  const cos = await getCosInstance();
  return new Promise((resolve, reject) => {
    cos.uploadFiles(
      {
        files: files,
        SliceSize: 1024 * 1024,
      },
      function (err, data) {
        if (err) {
          reject(err);
        } else {
          resolve(data);
        }
      }
    );
  });
};

/**
 * Upload a directory or zip file to EdgeOne COS
 */
const uploadToEdgeOneCOS = async (localPath: string): Promise<UploadResult> => {
  try {
    const tokenResult = await getCosTempToken();
    if (tokenResult.Code !== 0 || !tokenResult?.Data?.Response) {
      throw new Error(
        `Failed to get COS token: ${
          tokenResult.Message || 'Invalid token response'
        }`
      );
    }

    const response = tokenResult.Data.Response;
    const bucket = response.Bucket;
    const region = response.Region;
    const targetPath = response.TargetPath;

    if (!bucket || !region || !targetPath) {
      throw new Error(
        'Missing required COS parameters (Bucket, Region, TargetPath) in token response.'
      );
    }

    const isZip = isZipFile(localPath);

    if (isZip) {
      // Upload single zip file to COS
      console.log(
        `[uploadToEdgeOneCOS] Uploading zip file to COS with targetPath: ${targetPath}...`
      );

      const fileName = path.basename(localPath);
      const key = `${targetPath}/${fileName}`;

      // Read the file data
      const fileBuffer = await fs.readFile(localPath);
      const fileStats = await fs.stat(localPath);

      const cos = await getCosInstance();
      return new Promise((resolve, reject) => {
        cos.putObject(
          {
            Bucket: bucket,
            Region: region,
            Key: key,
            Body: fileBuffer,
            ContentLength: fileStats.size,
          },
          function (err) {
            if (err) {
              console.error('Error uploading zip file to COS: ' + err);
              reject(err);
            } else {
              console.log(`[uploadToEdgeOneCOS] Upload successful.`);
              resolve({
                success: true,
                targetPath: key,
              });
            }
          }
        );
      });
    } else {
      // List all files in the directory
      const folderList = await fastListFolder(localPath);

      // Convert to COS format
      const files = getFiles(folderList, localPath, bucket, region, targetPath);

      // Upload files to COS
      console.log(
        `[uploadToEdgeOneCOS] Uploading ${files.length} files to COS with targetPath: ${targetPath}...`
      );
      await uploadFiles(files);
      console.log(`[uploadToEdgeOneCOS] Upload successful.`);

      return {
        success: true,
        targetPath,
      };
    }
  } catch (error) {
    console.error('Error uploading to COS: ' + error);
    throw error;
  }
};

/**
 * Poll for deployment status until it's no longer processing
 */
const pollProjectStatus = async (
  projectId: string,
  deploymentId: string
): Promise<DeploymentResult> => {
  let isProcessing = true;
  let deployment = null;

  while (isProcessing) {
    // Get list of deployments
    const deploymentsResult = await describePagesDeployments(projectId);

    // Find the specific deployment by deploymentId
    deployment = deploymentsResult.Data.Response.Deployments.find(
      (deploy: any) => deploy.DeploymentId === deploymentId
    );

    if (!deployment) {
      throw new Error(`Deployment with ID ${deploymentId} not found`);
    }

    console.log(`[pollProjectStatus] Deployment status: ${deployment.Status}`);

    // Check if deployment is still processing
    if (deployment.Status !== 'Process') {
      isProcessing = false;
    } else {
      // Wait before next poll
      await sleep(5000);
    }
  }

  return deployment as DeploymentResult;
};

/**
 * Validate that the path exists and is a directory or zip file
 */
const validateFolder = async (localPath: string): Promise<boolean> => {
  try {
    await fs.access(localPath);
  } catch (error) {
    throw new Error('localPath does not exist');
  }

  const stats = await fs.stat(localPath);
  const isZip = isZipFile(localPath);

  if (!stats.isDirectory() && !isZip) {
    throw new Error('localPath must be a folder or zip file');
  }

  return isZip;
};

/**
 * Get project console URL based on the current API endpoint
 */
const getProjectConsoleUrl = (projectId: string): string => {
  const url1 = `https://console.cloud.tencent.com/edgeone/pages/project/${projectId}/index`;
  const url2 = `https://console.tencentcloud.com/edgeone/pages/project/${projectId}/index`;

  if (BASE_API_URL === BASE_API_URL1) {
    return url1;
  } else if (BASE_API_URL === BASE_API_URL2) {
    return url2;
  } else {
    return url1;
  }
};

/**
 * Get structured deployment result
 * @param deploymentResult The result from polling deployment status
 * @param projectId The project ID
 * @param env Environment to deploy to, either 'Production' or 'Preview'
 * @returns Structured deployment result with type, url, projectId, and consoleUrl
 */
const getDeploymentStructuredResult = async (
  deploymentResult: DeploymentResult,
  projectId: string,
  env: 'Production' | 'Preview' = 'Production'
): Promise<{
  type: 'custom' | 'temporary';
  url: string;
  projectId: string;
  consoleUrl: string;
  projectName: string;
}> => {
  // Get project details to get domain information
  const projectStatusResult = await describePagesProjects({
    projectId: projectId,
  });

  if (!projectStatusResult?.Data?.Response?.Projects?.[0]) {
    throw new Error('Failed to retrieve project status information.');
  }

  const project = projectStatusResult.Data.Response.Projects[0];

  // Check deployment status
  if (deploymentResult.Status === 'Success') {
    // For Production environment, check for custom domains
    if (
      env === 'Production' &&
      project.CustomDomains &&
      project.CustomDomains.length > 0
    ) {
      const customDomain = project.CustomDomains[0];
      if (customDomain.Status === 'Pass') {
        return {
          type: 'custom',
          url: `https://${customDomain.Domain}`,
          projectId,
          projectName: project.Name,
          consoleUrl: getProjectConsoleUrl(projectId),
        };
      }
    }

    // Process domain information
    const domain = deploymentResult.PreviewUrl
      ? deploymentResult.PreviewUrl.replace('https://', '')
      : project.PresetDomain;

    const encipherTokenResult = await describePagesEncipherToken(domain);

    if (
      encipherTokenResult.Code !== 0 ||
      !encipherTokenResult?.Data?.Response?.Token ||
      !encipherTokenResult?.Data?.Response?.Timestamp
    ) {
      throw new Error(
        `Deployment completed, but failed to get access token: ${
          encipherTokenResult.Message || 'Invalid token data'
        }`
      );
    }
    const { Token, Timestamp } = encipherTokenResult.Data.Response;
    const url = `https://${domain}?eo_token=${Token}&eo_time=${Timestamp}`;
    return {
      type: 'temporary',
      url: url,
      projectId,
      projectName: project.Name,
      consoleUrl: getProjectConsoleUrl(projectId),
    };
  } else {
    console.log(
      `[getDeploymentStructuredResult] Deployment failed with status: ${deploymentResult.Status}`
    );
    throw new Error(
      `Deployment failed with status: ${deploymentResult.Status}`
    );
  }
};

/**
 * Deploy a local folder or zip file to EdgeOne Pages
 * @param localPath Path to the local folder or zip file to deploy
 * @param env Environment to deploy to, either 'Production' or 'Preview'
 * @returns URL to the deployed site
 */
export const deployFolderOrZipToEdgeOne = async (
  localPath: string,
  env: 'Production' | 'Preview' = 'Production'
): Promise<string> => {
  // Reset logs and override console at the start
  resetLogs();
  overrideConsole();

  try {
    // Reset token cache at the start of deployment
    resetTokenCache();
    resetTempProjectName();

    // Validate folder or zip file
    const isZip = await validateFolder(localPath);

    await checkAndSetBaseUrl();

    // 1. Upload folder to COS
    const uploadResult = await uploadToEdgeOneCOS(localPath);
    if (!uploadResult.targetPath) {
      throw new Error('COS upload succeeded but targetPath is missing.');
    }
    const targetPath = uploadResult.targetPath;

    // 2. Get or create project
    console.log(`[getOrCreateProject] Getting or creating project...`);
    const projectResult = await getOrCreateProject();
    if (!projectResult?.Data?.Response?.Projects?.[0]?.ProjectId) {
      console.error('Invalid project data received: ' + projectResult);
      throw new Error('Failed to retrieve Project ID after get/create.');
    }
    const projectId = projectResult.Data.Response.Projects[0].ProjectId;
    console.log(`[getOrCreateProject] Using Project ID: ${projectId}`);

    // 3. Create deployment
    console.log(
      `[createPagesDeployment] Creating deployment in ${env} environment...`
    );
    const res = await createPagesDeployment({
      projectId,
      targetPath: targetPath,
      isZip,
      env,
    });
    const deploymentId = res.Data.Response.DeploymentId;

    // 4. Wait for deployment to complete
    console.log(
      `[pollProjectStatus] Waiting for deployment to complete (polling status)...`
    );
    await sleep(5000);
    const deploymentResult = await pollProjectStatus(projectId, deploymentId);

    // 5. Get structured deployment result and format message
    const structuredResult = await getDeploymentStructuredResult(
      deploymentResult,
      projectId,
      env
    );

    /**
     * Format deployment result into user-friendly message
     * @param deploymentResult The structured deployment result
     * @returns Text message describing the deployment status
     */

    // Append deployment logs to the result
    const logs = formatLogs();
    const finalText = `${logs}

results:
${JSON.stringify(structuredResult, null, 2)}`;

    return finalText;
  } catch (error) {
    // Ensure logs are captured even on error
    const logs = formatLogs();
    const errorMessage = error instanceof Error ? error.message : String(error);
    const finalText = `${logs}Deployment failed: ${errorMessage}`;
    throw new Error(finalText);
  } finally {
    // Always restore console
    restoreConsole();
  }
};

```