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

```
├── .gitignore
├── bin.ts
├── common
│   ├── errors.ts
│   ├── server.ts
│   ├── types.ts
│   ├── utils.ts
│   └── version.ts
├── Dockerfile
├── index.ts
├── LICENSE
├── operations
│   ├── branches.ts
│   ├── files.ts
│   ├── issues.ts
│   ├── pulls.ts
│   ├── repos.ts
│   └── users.ts
├── package-lock.json
├── package.json
├── README.md
├── README.zh-cn.md
├── smithery.yaml
└── tsconfig.json
```

# Files

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

```
# node_modules
node_modules

# dist
dist

# logs
*.log

# dotenv
.env
```

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

```markdown
# Gitee MCP Server

Let AI operate Gitee repositories/Issues/Pull Requests for you through MCP

[![Node Version](https://img.shields.io/badge/node-%3E%3D22.12.0-brightgreen.svg)](./package.json)
![NPM Version](https://img.shields.io/npm/v/gitee-mcp-server)
![Docker Pulls](https://img.shields.io/docker/pulls/normalcoder/gitee-mcp-server)
![Docker Image Version](https://img.shields.io/docker/v/normalcoder/gitee-mcp-server)
[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![smithery badge](https://smithery.ai/badge/@normal-coder/gitee-mcp-server)](https://smithery.ai/server/@normal-coder/gitee-mcp-server)

[<img width="380" height="200" src="https://glama.ai/mcp/servers/cck9xigm1d/badge" />](https://glama.ai/mcp/servers/Cck9XigM1d)

---

## Supported AI Operations

| Category | MCP Tool | Description |
|:----:|:----|:----|
| Repository Operations | `create_repository` | Create a Gitee repository | 
| | `fork_repository` | Fork a Gitee repository | 
| Branch Operations | `create_branch` | Create a new branch in a Gitee repository | 
| | `list_branches` | List branches in a Gitee repository | 
| | `get_branch` | Get details of a specific branch in a Gitee repository | 
| File Operations | `get_file_contents` | Get contents of a file or directory in a Gitee repository | 
| | `create_or_update_file` | Create or update a file in a Gitee repository | 
| | `push_files` | Push multiple files to a Gitee repository | 
| Issue Operations | `create_issue` | Create an Issue in a Gitee repository | 
| | `list_issues` | List Issues in a Gitee repository | 
| | `get_issue` | Get details of a specific Issue in a Gitee repository | 
| | `update_issue` | Update an Issue in a Gitee repository | 
| | `add_issue_comment` | Add a comment to an Issue in a Gitee repository | 
| Pull Request Operations | `create_pull_request` | Create a Pull Request in a Gitee repository | 
| | `list_pull_requests` | List Pull Requests in a Gitee repository | 
| | `get_pull_request` | Get details of a specific Pull Request in a Gitee repository | 
| | `update_pull_request` | Update a Pull Request in a Gitee repository | 
| | `merge_pull_request` | Merge a Pull Request in a Gitee repository | 
| User Operations | `get_user` | Get Gitee user information | 
| | `get_current_user` | Get authenticated Gitee user information | 

## Usage

### Installing via Smithery

To install Gitee MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@normal-coder/gitee-mcp-server):

```bash
npx -y @smithery/cli install @normal-coder/gitee-mcp-server --client claude
```

### Configuration

- `GITEE_API_BASE_URL`: Optional, Gitee OpenAPI Endpoint, default is `https://gitee.com/api/v5`
- `GITEE_PERSONAL_ACCESS_TOKEN`: Required, Gitee account personal access token (PAT), can be obtained from Gitee account settings [Personal Access Tokens](https://gitee.com/profile/personal_access_tokens)
- `DEBUG`: Optional, set to `true` to enable debug logging, default is disabled

### Run MCP Server via NPX

```json
{
  "mcpServers": {
    "Gitee": {
      "command": "npx",
      "args": [
        "-y",
        "gitee-mcp-server"
      ],
      "env": {
        "GITEE_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
      }
    }
  }
}
```

### Run MCP Server via Docker Container

1. Get Docker Image

```bash
# Get from DockerHub
docker pull normalcoder/gitee-mcp-server

# Build locally
docker build -t normalcoder/gitee-mcp-server .
```

2. Configure MCP Server

```json
{
  "mcpServers": {
    "Gitee": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "GITEE_PERSONAL_ACCESS_TOKEN",
        "normalcoder/gitee-mcp-server"
      ],
      "env": {
        "GITEE_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
      }
    }
  }
}
```

## Development Guide

### Install Dependencies

```bash
npm install
```

### Build

```bash
npm run build
```

After successful build, `/dist` will contain the runnable MCP server.

### Run Server

```bash
npm start
```

The MCP server will run on stdio, allowing it to be used as a subprocess by MCP clients.

### Build Docker Image

You can also run the server using Docker:

```bash
docker build -t normalcoder/gitee-mcp-server .
```

Run MCP Server with Docker:

```bash
docker run -e GITEE_PERSONAL_ACCESS_TOKEN=<YOUR_TOKEN> normalcoder/gitee-mcp-server
```

### Debug MCP Server

You can use `@modelcontextprotocol/inspector` for debugging:

Create a `.env` file in the root directory for environment variables:

```.env
GITEE_API_BASE_URL=https://gitee.com/api/v5
GITEE_PERSONAL_ACCESS_TOKEN=<YOUR_TOKEN>
```

Run the debug tool to start the service and web debug interface:

```bash
npx @modelcontextprotocol/inspector npm run start --env-file=.env
```

The project includes a `debug()` function for printing debug information, usage:

```typescript
import { debug } from './common/utils.js';

debug('Message to log');
debug('Message with data:', { key: 'value' });
```

Debug logs are only printed when the `DEBUG` environment variable is set to `true`.

## Dependencies

- `@modelcontextprotocol/sdk`: MCP SDK for server implementation
- `universal-user-agent`: For generating user agent strings
- `zod`: For schema validation
- `zod-to-json-schema`: For converting Zod schemas to JSON schemas

## License

Licensed under MIT License. You are free to use, modify and distribute the software, subject to the terms and conditions of the MIT License. For more details, see the [LICENSE](./LICENSE) file in the project repository.

## Related Links

- [Model Context Protocol](https://modelcontextprotocol.io)
- [Gitee](https://gitee.com)

```

--------------------------------------------------------------------------------
/common/version.ts:
--------------------------------------------------------------------------------

```typescript
export const VERSION = "0.1.0";

```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "dist",
    "rootDir": "."
  },
  "include": [
    "./**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

--------------------------------------------------------------------------------
/bin.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { config } from "dotenv";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import createGiteeMCPServer from "./index.js";

// 加载 .env 文件
config();

async function runServer() {
  const server = createGiteeMCPServer();
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Gitee MCP Server running on stdio");
}

runServer().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

```

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

```json
{
  "name": "gitee-mcp-server",
  "version": "0.1.1",
  "description": "MCP Server for using the Gitee API",
  "license": "MIT",
  "author": {
    "name": "诺墨",
    "email": "[email protected]",
    "url": "https://gitee.com/normalcoder/gitee-mcp-server"
  },
  "homepage": "https://gitee.com/normalcoder/gitee-mcp-server",
  "type": "module",
  "bin": {
    "mcp-server-gitee": "dist/bin.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "watch": "tsc --watch",
    "start": "node dist/bin.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.1",
    "dotenv": "^16.4.7",
    "universal-user-agent": "^7.0.0",
    "zod": "^3.22.4",
    "zod-to-json-schema": "^3.22.3"
  },
  "devDependencies": {
    "@types/node": "^20.10.5",
    "shx": "^0.3.4",
    "typescript": "^5.8.2"
  }
}
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - giteePersonalAccessToken
    properties:
      giteePersonalAccessToken:
        type: string
        description: Gitee personal access token, required for authentication.
      giteeApiBaseUrl:
        type: string
        default: https://gitee.com/api/v5
        description: Optional Gitee API base URL
      debug:
        type: boolean
        description: Enable debug mode
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'node',
      args: ['dist/bin.js'],
      env: {
        GITEE_PERSONAL_ACCESS_TOKEN: config.giteePersonalAccessToken,
        GITEE_API_BASE_URL: config.giteeApiBaseUrl || 'https://gitee.com/api/v5',
        DEBUG: config.debug === true ? 'true' : undefined
      }
    })
  exampleConfig:
    giteePersonalAccessToken: <YOUR_GITEE_PERSONAL_ACCESS_TOKEN>
    giteeApiBaseUrl: https://gitee.com/api/v5
    debug: false

```

--------------------------------------------------------------------------------
/operations/users.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { giteeRequest, validateOwnerName, getGiteeApiBaseUrl } from "../common/utils.js";
import { GiteeUserSchema } from "../common/types.js";

// Schema definitions
export const GetUserSchema = z.object({
  // 用户名
  username: z.string().describe("Username"),
});

export const SearchUsersSchema = z.object({
  // 搜索关键词
  q: z.string().describe("Search keyword"),
  // 当前的页码
  page: z.number().int().min(1).default(1).optional().describe("Page number"),
  // 每页的数量,最大为 100
  per_page: z.number().int().min(1).max(100).default(30).optional().describe("Number of items per page, maximum 100"),
  // 排序字段
  sort: z.enum(["followers", "repositories", "joined"]).default("followers").optional().describe("Sort field"),
  // 排序方式
  order: z.enum(["desc", "asc"]).default("desc").optional().describe("Sort direction"),
});

// Type exports
export type GetUserOptions = z.infer<typeof GetUserSchema>;
export type SearchUsersOptions = z.infer<typeof SearchUsersSchema>;

// Function implementations
export async function getUser(username: string) {
  username = validateOwnerName(username);

  const url = `/users/${username}`;
  const response = await giteeRequest(url, "GET");

  return GiteeUserSchema.parse(response);
}

export async function getCurrentUser() {
  const url = "/user";
  const response = await giteeRequest(url, "GET");

  return GiteeUserSchema.parse(response);
}

export async function searchUsers(options: SearchUsersOptions) {
  const { q, page, per_page, sort, order } = options;

  const url = new URL(`${getGiteeApiBaseUrl()}/search/users`);
  url.searchParams.append("q", q);
  if (page !== undefined) {
    url.searchParams.append("page", page.toString());
  }
  if (per_page !== undefined) {
    url.searchParams.append("per_page", per_page.toString());
  }
  if (sort) {
    url.searchParams.append("sort", sort);
  }
  if (order) {
    url.searchParams.append("order", order);
  }

  const response = await giteeRequest(url.toString(), "GET");

  return {
    total_count: (response as any).total_count || 0,
    items: z.array(GiteeUserSchema).parse((response as any).items || []),
  };
}

```

--------------------------------------------------------------------------------
/common/server.ts:
--------------------------------------------------------------------------------

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { isGiteeError } from "./errors.js";

type MCPServerOptions = {
  name: string;
  version: string;
};

type ToolDefinition = {
  name: string;
  description: string;
  schema: z.ZodType<any, any, any>;
  handler: (params: any) => Promise<any>;
};

export class MCPServer {
  private server: Server;
  private tools: Map<string, ToolDefinition> = new Map();

  constructor(options: MCPServerOptions) {
    this.server = new Server(
      {
        name: options.name,
        version: options.version,
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupRequestHandlers();
  }

  private setupRequestHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      const toolsList = Array.from(this.tools.values()).map((tool) => ({
        name: tool.name,
        description: tool.description,
        inputSchema: zodToJsonSchema(tool.schema),
      }));

      return {
        tools: toolsList,
      };
    });

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      try {
        if (!request.params.arguments) {
          throw new Error("Parameters are required.");
        }

        const tool = this.tools.get(request.params.name);
        if (!tool) {
          throw new Error(`Unknown tool: ${request.params.name}`);
        }

        const args = tool.schema.parse(request.params.arguments);
        const result = await tool.handler(args);

        return {
          content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
        };
      } catch (error) {
        if (error instanceof z.ZodError) {
          throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
        }
        if (isGiteeError(error)) {
          throw error;
        }
        throw error;
      }
    });
  }

  public registerTool(tool: ToolDefinition) {
    this.tools.set(tool.name, tool);
  }

  public async connect(transport: StdioServerTransport) {
    await this.server.connect(transport);
  }
}

```

--------------------------------------------------------------------------------
/common/errors.ts:
--------------------------------------------------------------------------------

```typescript
export class GiteeError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "GiteeError";
  }
}

export class GiteeValidationError extends GiteeError {
  response?: unknown;

  constructor(message: string, response?: unknown) {
    super(message);
    this.name = "GiteeValidationError";
    this.response = response;
  }
}

export class GiteeResourceNotFoundError extends GiteeError {
  constructor(message: string) {
    super(message);
    this.name = "GiteeResourceNotFoundError";
  }
}

export class GiteeAuthenticationError extends GiteeError {
  constructor(message: string) {
    super(message);
    this.name = "GiteeAuthenticationError";
  }
}

export class GiteePermissionError extends GiteeError {
  constructor(message: string) {
    super(message);
    this.name = "GiteePermissionError";
  }
}

export class GiteeRateLimitError extends GiteeError {
  resetAt: Date;

  constructor(message: string, resetAt: Date) {
    super(message);
    this.name = "GiteeRateLimitError";
    this.resetAt = resetAt;
  }
}

export class GiteeConflictError extends GiteeError {
  constructor(message: string) {
    super(message);
    this.name = "GiteeConflictError";
  }
}

export function isGiteeError(error: unknown): error is GiteeError {
  return error instanceof GiteeError;
}

export function createGiteeError(status: number, responseBody: unknown): GiteeError {
  let message = "Gitee API request failed";
  let resetAt: Date | undefined;

  if (typeof responseBody === "object" && responseBody !== null) {
    const body = responseBody as Record<string, unknown>;

    if (body.message && typeof body.message === "string") {
      message = body.message;
    }

    if (body.documentation_url && typeof body.documentation_url === "string") {
      message += ` - Documentation: ${body.documentation_url}`;
    }
  }

  switch (status) {
    case 400:
      return new GiteeValidationError(message, responseBody);
    case 401:
      return new GiteeAuthenticationError(message);
    case 403:
      return new GiteePermissionError(message);
    case 404:
      return new GiteeResourceNotFoundError(message);
    case 409:
      return new GiteeConflictError(message);
    case 429:
      return new GiteeRateLimitError(
        message,
        resetAt || new Date(Date.now() + 60 * 1000) // Default: reset after 1 minute
      );
    default:
      return new GiteeError(message);
  }
}

```

--------------------------------------------------------------------------------
/operations/repos.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { giteeRequest, validateOwnerName, validateRepositoryName, getGiteeApiBaseUrl } from "../common/utils.js";
import { GiteeRepositorySchema } from "../common/types.js";

// Schema definitions
export const CreateRepositorySchema = z.object({
  // 仓库名称
  name: z.string().describe("Repository name"),
  // 仓库描述
  description: z.string().optional().describe("Repository description"),
  // 主页地址
  homepage: z.string().optional().describe("Homepage URL"),
  // 是否私有
  private: z.boolean().default(false).optional().describe("Whether the repository is private"),
  // 是否开启 Issue 功能
  has_issues: z.boolean().default(true).optional().describe("Whether to enable Issue functionality"),
  // 是否开启 Wiki 功能
  has_wiki: z.boolean().default(true).optional().describe("Whether to enable Wiki functionality"),
  // 是否自动初始化仓库
  auto_init: z.boolean().default(false).optional().describe("Whether to automatically initialize the repository"),
  // Git Ignore 模板
  gitignore_template: z.string().optional().describe("Git Ignore template"),
  // License 模板
  license_template: z.string().optional().describe("License template"),
  // 仓库路径
  path: z.string().optional().describe("Repository path"),
});

export const ForkRepositorySchema = z.object({
  // 仓库所属空间地址 (企业、组织或个人的地址 path)
  owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
  // 仓库路径 (path)
  repo: z.string().describe("Repository path"),
  // 组织空间地址,不传默认为个人
  organization: z.string().optional().describe("Organization path, defaults to personal account if not provided"),
});

// Type exports
export type CreateRepositoryOptions = z.infer<typeof CreateRepositorySchema>;
export type ForkRepositoryOptions = z.infer<typeof ForkRepositorySchema>;

// Function implementations
export async function createRepository(options: CreateRepositoryOptions) {
  try {
    console.log('Creating repository parameters:', JSON.stringify(options));
    const url = "/user/repos";
    const response = await giteeRequest(url, "POST", options);
    console.log('Create repository response:', JSON.stringify(response));

    // Try to parse the response
    try {
      return GiteeRepositorySchema.parse(response);
    } catch (parseError) {
      console.error('Failed to parse repository response:', parseError);
      // Return the original response to avoid parsing errors
      return response;
    }
  } catch (error) {
    console.error('Failed to create repository request:', error);
    throw error;
  }
}

export async function forkRepository(
  owner: string,
  repo: string,
  organization?: string
) {
  owner = validateOwnerName(owner);
  repo = validateRepositoryName(repo);

  const url = `/repos/${owner}/${repo}/forks`;
  const body: Record<string, string> = {};

  if (organization) {
    body.organization = validateOwnerName(organization);
  }

  const response = await giteeRequest(url, "POST", body);

  return GiteeRepositorySchema.parse(response);
}
```

--------------------------------------------------------------------------------
/operations/branches.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { giteeRequest, validateBranchName, validateOwnerName, validateRepositoryName, getGiteeApiBaseUrl } from "../common/utils.js";
import { GiteeCompleteBranchSchema, GiteeBranchSchema } from "../common/types.js";

// Schema definitions
export const CreateBranchSchema = z.object({
  // 仓库所属空间地址 (企业、组织或个人的地址 path)
  owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
  // 仓库路径 (path)
  repo: z.string().describe("Repository path"),
  // 新创建的分支名称
  branch_name: z.string().describe("Name for the new branch"),
  // 起点名称,默认:master
  refs: z.string().default("master").describe("Source reference for the branch, default: master"),
});

export const ListBranchesSchema = z.object({
  // 仓库所属空间地址 (企业、组织或个人的地址 path)
  owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
  // 仓库路径 (path)
  repo: z.string().describe("Repository path"),
  // 排序字段
  sort: z.enum(["name", "updated"]).default("name").optional().describe("Sort field"),
  // 排序方向
  direction: z.enum(["asc", "desc"]).default("asc").optional().describe("Sort direction"),
  // 当前的页码
  page: z.number().int().default(1).optional().describe("Page number"),
  // 每页的数量,最大为 100
  per_page: z.number().int().min(1).max(100).optional().describe("Number of items per page, maximum 100"),
});

export const GetBranchSchema = z.object({
  // 仓库所属空间地址 (企业、组织或个人的地址 path)
  owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
  // 仓库路径 (path)
  repo: z.string().describe("Repository path"),
  // 分支名称
  branch: z.string().describe("Branch name"),
});

// Type exports
export type CreateBranchOptions = z.infer<typeof CreateBranchSchema>;
export type ListBranchesOptions = z.infer<typeof ListBranchesSchema>;
export type GetBranchOptions = z.infer<typeof GetBranchSchema>;

// Function implementations
export async function createBranchFromRef(
  owner: string,
  repo: string,
  branchName: string,
  refs: string = "master"
) {
  owner = validateOwnerName(owner);
  repo = validateRepositoryName(repo);
  branchName = validateBranchName(branchName);

  const url = `/repos/${owner}/${repo}/branches`;
  const body = {
    branch_name: branchName,
    refs: refs,
  };

  const response = await giteeRequest(url, "POST", body);
  return GiteeBranchSchema.parse(response);
}

export async function listBranches(
  owner: string,
  repo: string,
  sort?: string,
  direction?: string,
  page?: number,
  per_page?: number
) {
  owner = validateOwnerName(owner);
  repo = validateRepositoryName(repo);

  const url = new URL(`${getGiteeApiBaseUrl()}/repos/${owner}/${repo}/branches`);

  if (sort) {
    url.searchParams.append("sort", sort);
  }
  if (direction) {
    url.searchParams.append("direction", direction);
  }
  if (page !== undefined) {
    url.searchParams.append("page", page.toString());
  }
  if (per_page !== undefined) {
    url.searchParams.append("per_page", per_page.toString());
  }

  const response = await giteeRequest(url.toString());
  return z.array(GiteeBranchSchema).parse(response);
}

export async function getBranch(owner: string, repo: string, branch: string) {
  owner = validateOwnerName(owner);
  repo = validateRepositoryName(repo);
  branch = validateBranchName(branch);

  const url = `/repos/${owner}/${repo}/branches/${branch}`;
  const response = await giteeRequest(url);

  return GiteeCompleteBranchSchema.parse(response);
}

```

--------------------------------------------------------------------------------
/common/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { getUserAgent } from "universal-user-agent";
import { createGiteeError } from "./errors.js";
import { VERSION } from "./version.js";

// Default Gitee API base URL
const DEFAULT_GITEE_API_BASE_URL = "https://gitee.com/api/v5";

/**
 * Get the Gitee API base URL from environment variables or use the default
 * @returns The Gitee API base URL
 */
export function getGiteeApiBaseUrl(): string {
  return process.env.GITEE_API_BASE_URL || DEFAULT_GITEE_API_BASE_URL;
}

type RequestOptions = {
  method?: string;
  body?: unknown;
  headers?: Record<string, string>;
}

async function parseResponseBody(response: Response): Promise<unknown> {
  const contentType = response.headers.get("content-type");
  if (contentType?.includes("application/json")) {
    return response.json();
  }
  return response.text();
}

export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): string {
  const url = new URL(baseUrl);
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined) {
      url.searchParams.append(key, value.toString());
    }
  });
  return url.toString();
}

const USER_AGENT = `modelcontextprotocol/servers/gitee/v${VERSION} ${getUserAgent()}`;

// Generate the equivalent curl command for debugging.
function generateCurlCommand(url: string, method: string, headers: Record<string, string>, body?: unknown): string {
  let curl = `curl -X ${method} "${url}"`;

  // Add request headers
  Object.entries(headers).forEach(([key, value]) => {
    curl += ` -H "${key}: ${value}"`;
  });

  // Add request body
  if (body) {
    curl += ` -d '${JSON.stringify(body)}'`;
  }

  return curl;
}

// debug utility function
export function debug(message: string, data?: unknown): void {
  // Only output debug logs if DEBUG environment variable is set
  if (process.env.DEBUG !== "true") {
    return;
  }
  
  if (data !== undefined) {
    console.error(`[DEBUG] ${message}`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data);
  } else {
    console.error(`[DEBUG] ${message}`);
  }
}

export async function giteeRequest(
  urlPath: string,
  method: string = "GET",
  body?: unknown,
  headers?: Record<string, string>
): Promise<unknown> {
  // Check if the URL is already a full URL or a path
  let url = urlPath.startsWith("http") ? urlPath : `${getGiteeApiBaseUrl()}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`;
  const requestHeaders: Record<string, string> = {
    "Accept": "application/json",
    "Content-Type": "application/json",
    "User-Agent": USER_AGENT,
    ...headers,
  };

  if (process.env.GITEE_PERSONAL_ACCESS_TOKEN) {
    // The Gitee API uses `access_token` as a query parameter or in the `Authorization` header.
    // Method 1: Add to URL Query Parameters
    let urlObj = new URL(url);
    urlObj.searchParams.append('access_token', process.env.GITEE_PERSONAL_ACCESS_TOKEN);
    url = urlObj.toString();

    // Method 2: Add to Request Headers (Two methods are tried to increase success rate)
    requestHeaders["Authorization"] = `token ${process.env.GITEE_PERSONAL_ACCESS_TOKEN}`;

    debug(`Using access token: ${process.env.GITEE_PERSONAL_ACCESS_TOKEN.substring(0, 4)}...`);
  } else {
    debug(`No access token found in environment variables`);
  }

  // Print the request
  debug(`Request: ${method} ${url}`);
  debug(`Headers:`, requestHeaders);
  if (body) {
    debug(`Body:`, body);
  }

  // Print the equivalent curl command
  const curlCommand = generateCurlCommand(url, method, requestHeaders, body);
  debug(`cURL: ${curlCommand}\n`);

  const response = await fetch(url, {
    method,
    headers: requestHeaders,
    body: body ? JSON.stringify(body) : undefined,
  });

  const responseBody = await parseResponseBody(response);

  // Print the response
  debug(`Response Status: ${response.status} ${response.statusText}`);
  debug(`Response Body:`, responseBody);

  if (!response.ok) {
    throw createGiteeError(response.status, responseBody);
  }

  return responseBody;
}

export function validateBranchName(branch: string): string {
  const sanitized = branch.trim();
  if (!sanitized) {
    throw new Error("分支名不能为空");
  }
  if (sanitized.includes("..")) {
    throw new Error("分支名不能包含 '..'");
  }
  if (/[\s~^:?*[\\\]]/.test(sanitized)) {
    throw new Error("分支名包含无效字符");
  }
  if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
    throw new Error("分支名不能以 '/' 开头或结尾");
  }
  if (sanitized.endsWith(".lock")) {
    throw new Error("分支名不能以 '.lock' 结尾");
  }
  return sanitized;
}

export function validateRepositoryName(name: string): string {
  const sanitized = name.trim();
  if (!sanitized) {
    throw new Error("仓库名不能为空");
  }
  if (!/^[a-zA-Z0-9_.-]+$/.test(sanitized)) {
    throw new Error(
      "仓库名只能包含字母、数字、连字符、句点和下划线"
    );
  }
  if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
    throw new Error("仓库名不能以句点开头或结尾");
  }
  return sanitized;
}

export function validateOwnerName(owner: string): string {
  const sanitized = owner.trim();
  if (!sanitized) {
    throw new Error("所有者名称不能为空");
  }
  if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(sanitized)) {
    throw new Error(
      "所有者名称只能包含字母、数字和连字符,且必须以字母或数字开头"
    );
  }
  return sanitized;
}

export async function checkBranchExists(
  owner: string,
  repo: string,
  branch: string
): Promise<boolean> {
  try {
    await giteeRequest(`/repos/${owner}/${repo}/branches/${branch}`, "GET");
    return true;
  } catch (error) {
    if (error && typeof error === "object" && "name" in error && error.name === "GiteeResourceNotFoundError") {
      return false;
    }
    throw error;
  }
}

export async function checkUserExists(username: string): Promise<boolean> {
  try {
    await giteeRequest(`/users/${username}`, "GET");
    return true;
  } catch (error) {
    if (error && typeof error === "object" && "name" in error && error.name === "GiteeResourceNotFoundError") {
      return false;
    }
    throw error;
  }
}

```