# 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:
--------------------------------------------------------------------------------
```
1 | # node_modules
2 | node_modules
3 |
4 | # dist
5 | dist
6 |
7 | # logs
8 | *.log
9 |
10 | # dotenv
11 | .env
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Gitee MCP Server
2 |
3 | Let AI operate Gitee repositories/Issues/Pull Requests for you through MCP
4 |
5 | [](./package.json)
6 | 
7 | 
8 | 
9 | [](./LICENSE)
10 | [](https://smithery.ai/server/@normal-coder/gitee-mcp-server)
11 |
12 | [<img width="380" height="200" src="https://glama.ai/mcp/servers/cck9xigm1d/badge" />](https://glama.ai/mcp/servers/Cck9XigM1d)
13 |
14 | ---
15 |
16 | ## Supported AI Operations
17 |
18 | | Category | MCP Tool | Description |
19 | |:----:|:----|:----|
20 | | Repository Operations | `create_repository` | Create a Gitee repository |
21 | | | `fork_repository` | Fork a Gitee repository |
22 | | Branch Operations | `create_branch` | Create a new branch in a Gitee repository |
23 | | | `list_branches` | List branches in a Gitee repository |
24 | | | `get_branch` | Get details of a specific branch in a Gitee repository |
25 | | File Operations | `get_file_contents` | Get contents of a file or directory in a Gitee repository |
26 | | | `create_or_update_file` | Create or update a file in a Gitee repository |
27 | | | `push_files` | Push multiple files to a Gitee repository |
28 | | Issue Operations | `create_issue` | Create an Issue in a Gitee repository |
29 | | | `list_issues` | List Issues in a Gitee repository |
30 | | | `get_issue` | Get details of a specific Issue in a Gitee repository |
31 | | | `update_issue` | Update an Issue in a Gitee repository |
32 | | | `add_issue_comment` | Add a comment to an Issue in a Gitee repository |
33 | | Pull Request Operations | `create_pull_request` | Create a Pull Request in a Gitee repository |
34 | | | `list_pull_requests` | List Pull Requests in a Gitee repository |
35 | | | `get_pull_request` | Get details of a specific Pull Request in a Gitee repository |
36 | | | `update_pull_request` | Update a Pull Request in a Gitee repository |
37 | | | `merge_pull_request` | Merge a Pull Request in a Gitee repository |
38 | | User Operations | `get_user` | Get Gitee user information |
39 | | | `get_current_user` | Get authenticated Gitee user information |
40 |
41 | ## Usage
42 |
43 | ### Installing via Smithery
44 |
45 | To install Gitee MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@normal-coder/gitee-mcp-server):
46 |
47 | ```bash
48 | npx -y @smithery/cli install @normal-coder/gitee-mcp-server --client claude
49 | ```
50 |
51 | ### Configuration
52 |
53 | - `GITEE_API_BASE_URL`: Optional, Gitee OpenAPI Endpoint, default is `https://gitee.com/api/v5`
54 | - `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)
55 | - `DEBUG`: Optional, set to `true` to enable debug logging, default is disabled
56 |
57 | ### Run MCP Server via NPX
58 |
59 | ```json
60 | {
61 | "mcpServers": {
62 | "Gitee": {
63 | "command": "npx",
64 | "args": [
65 | "-y",
66 | "gitee-mcp-server"
67 | ],
68 | "env": {
69 | "GITEE_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
70 | }
71 | }
72 | }
73 | }
74 | ```
75 |
76 | ### Run MCP Server via Docker Container
77 |
78 | 1. Get Docker Image
79 |
80 | ```bash
81 | # Get from DockerHub
82 | docker pull normalcoder/gitee-mcp-server
83 |
84 | # Build locally
85 | docker build -t normalcoder/gitee-mcp-server .
86 | ```
87 |
88 | 2. Configure MCP Server
89 |
90 | ```json
91 | {
92 | "mcpServers": {
93 | "Gitee": {
94 | "command": "docker",
95 | "args": [
96 | "run",
97 | "-i",
98 | "--rm",
99 | "-e",
100 | "GITEE_PERSONAL_ACCESS_TOKEN",
101 | "normalcoder/gitee-mcp-server"
102 | ],
103 | "env": {
104 | "GITEE_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
105 | }
106 | }
107 | }
108 | }
109 | ```
110 |
111 | ## Development Guide
112 |
113 | ### Install Dependencies
114 |
115 | ```bash
116 | npm install
117 | ```
118 |
119 | ### Build
120 |
121 | ```bash
122 | npm run build
123 | ```
124 |
125 | After successful build, `/dist` will contain the runnable MCP server.
126 |
127 | ### Run Server
128 |
129 | ```bash
130 | npm start
131 | ```
132 |
133 | The MCP server will run on stdio, allowing it to be used as a subprocess by MCP clients.
134 |
135 | ### Build Docker Image
136 |
137 | You can also run the server using Docker:
138 |
139 | ```bash
140 | docker build -t normalcoder/gitee-mcp-server .
141 | ```
142 |
143 | Run MCP Server with Docker:
144 |
145 | ```bash
146 | docker run -e GITEE_PERSONAL_ACCESS_TOKEN=<YOUR_TOKEN> normalcoder/gitee-mcp-server
147 | ```
148 |
149 | ### Debug MCP Server
150 |
151 | You can use `@modelcontextprotocol/inspector` for debugging:
152 |
153 | Create a `.env` file in the root directory for environment variables:
154 |
155 | ```.env
156 | GITEE_API_BASE_URL=https://gitee.com/api/v5
157 | GITEE_PERSONAL_ACCESS_TOKEN=<YOUR_TOKEN>
158 | ```
159 |
160 | Run the debug tool to start the service and web debug interface:
161 |
162 | ```bash
163 | npx @modelcontextprotocol/inspector npm run start --env-file=.env
164 | ```
165 |
166 | The project includes a `debug()` function for printing debug information, usage:
167 |
168 | ```typescript
169 | import { debug } from './common/utils.js';
170 |
171 | debug('Message to log');
172 | debug('Message with data:', { key: 'value' });
173 | ```
174 |
175 | Debug logs are only printed when the `DEBUG` environment variable is set to `true`.
176 |
177 | ## Dependencies
178 |
179 | - `@modelcontextprotocol/sdk`: MCP SDK for server implementation
180 | - `universal-user-agent`: For generating user agent strings
181 | - `zod`: For schema validation
182 | - `zod-to-json-schema`: For converting Zod schemas to JSON schemas
183 |
184 | ## License
185 |
186 | 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.
187 |
188 | ## Related Links
189 |
190 | - [Model Context Protocol](https://modelcontextprotocol.io)
191 | - [Gitee](https://gitee.com)
192 |
```
--------------------------------------------------------------------------------
/common/version.ts:
--------------------------------------------------------------------------------
```typescript
1 | export const VERSION = "0.1.0";
2 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "outDir": "dist",
12 | "rootDir": "."
13 | },
14 | "include": [
15 | "./**/*.ts"
16 | ],
17 | "exclude": [
18 | "node_modules"
19 | ]
20 | }
```
--------------------------------------------------------------------------------
/bin.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { config } from "dotenv";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import createGiteeMCPServer from "./index.js";
5 |
6 | // 加载 .env 文件
7 | config();
8 |
9 | async function runServer() {
10 | const server = createGiteeMCPServer();
11 | const transport = new StdioServerTransport();
12 | await server.connect(transport);
13 | console.error("Gitee MCP Server running on stdio");
14 | }
15 |
16 | runServer().catch((error) => {
17 | console.error("Fatal error in main():", error);
18 | process.exit(1);
19 | });
20 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "gitee-mcp-server",
3 | "version": "0.1.1",
4 | "description": "MCP Server for using the Gitee API",
5 | "license": "MIT",
6 | "author": {
7 | "name": "诺墨",
8 | "email": "[email protected]",
9 | "url": "https://gitee.com/normalcoder/gitee-mcp-server"
10 | },
11 | "homepage": "https://gitee.com/normalcoder/gitee-mcp-server",
12 | "type": "module",
13 | "bin": {
14 | "mcp-server-gitee": "dist/bin.js"
15 | },
16 | "files": [
17 | "dist"
18 | ],
19 | "scripts": {
20 | "build": "tsc && shx chmod +x dist/*.js",
21 | "watch": "tsc --watch",
22 | "start": "node dist/bin.js"
23 | },
24 | "dependencies": {
25 | "@modelcontextprotocol/sdk": "^1.0.1",
26 | "dotenv": "^16.4.7",
27 | "universal-user-agent": "^7.0.0",
28 | "zod": "^3.22.4",
29 | "zod-to-json-schema": "^3.22.3"
30 | },
31 | "devDependencies": {
32 | "@types/node": "^20.10.5",
33 | "shx": "^0.3.4",
34 | "typescript": "^5.8.2"
35 | }
36 | }
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | required:
9 | - giteePersonalAccessToken
10 | properties:
11 | giteePersonalAccessToken:
12 | type: string
13 | description: Gitee personal access token, required for authentication.
14 | giteeApiBaseUrl:
15 | type: string
16 | default: https://gitee.com/api/v5
17 | description: Optional Gitee API base URL
18 | debug:
19 | type: boolean
20 | description: Enable debug mode
21 | commandFunction:
22 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
23 | |-
24 | (config) => ({
25 | command: 'node',
26 | args: ['dist/bin.js'],
27 | env: {
28 | GITEE_PERSONAL_ACCESS_TOKEN: config.giteePersonalAccessToken,
29 | GITEE_API_BASE_URL: config.giteeApiBaseUrl || 'https://gitee.com/api/v5',
30 | DEBUG: config.debug === true ? 'true' : undefined
31 | }
32 | })
33 | exampleConfig:
34 | giteePersonalAccessToken: <YOUR_GITEE_PERSONAL_ACCESS_TOKEN>
35 | giteeApiBaseUrl: https://gitee.com/api/v5
36 | debug: false
37 |
```
--------------------------------------------------------------------------------
/operations/users.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { giteeRequest, validateOwnerName, getGiteeApiBaseUrl } from "../common/utils.js";
3 | import { GiteeUserSchema } from "../common/types.js";
4 |
5 | // Schema definitions
6 | export const GetUserSchema = z.object({
7 | // 用户名
8 | username: z.string().describe("Username"),
9 | });
10 |
11 | export const SearchUsersSchema = z.object({
12 | // 搜索关键词
13 | q: z.string().describe("Search keyword"),
14 | // 当前的页码
15 | page: z.number().int().min(1).default(1).optional().describe("Page number"),
16 | // 每页的数量,最大为 100
17 | per_page: z.number().int().min(1).max(100).default(30).optional().describe("Number of items per page, maximum 100"),
18 | // 排序字段
19 | sort: z.enum(["followers", "repositories", "joined"]).default("followers").optional().describe("Sort field"),
20 | // 排序方式
21 | order: z.enum(["desc", "asc"]).default("desc").optional().describe("Sort direction"),
22 | });
23 |
24 | // Type exports
25 | export type GetUserOptions = z.infer<typeof GetUserSchema>;
26 | export type SearchUsersOptions = z.infer<typeof SearchUsersSchema>;
27 |
28 | // Function implementations
29 | export async function getUser(username: string) {
30 | username = validateOwnerName(username);
31 |
32 | const url = `/users/${username}`;
33 | const response = await giteeRequest(url, "GET");
34 |
35 | return GiteeUserSchema.parse(response);
36 | }
37 |
38 | export async function getCurrentUser() {
39 | const url = "/user";
40 | const response = await giteeRequest(url, "GET");
41 |
42 | return GiteeUserSchema.parse(response);
43 | }
44 |
45 | export async function searchUsers(options: SearchUsersOptions) {
46 | const { q, page, per_page, sort, order } = options;
47 |
48 | const url = new URL(`${getGiteeApiBaseUrl()}/search/users`);
49 | url.searchParams.append("q", q);
50 | if (page !== undefined) {
51 | url.searchParams.append("page", page.toString());
52 | }
53 | if (per_page !== undefined) {
54 | url.searchParams.append("per_page", per_page.toString());
55 | }
56 | if (sort) {
57 | url.searchParams.append("sort", sort);
58 | }
59 | if (order) {
60 | url.searchParams.append("order", order);
61 | }
62 |
63 | const response = await giteeRequest(url.toString(), "GET");
64 |
65 | return {
66 | total_count: (response as any).total_count || 0,
67 | items: z.array(GiteeUserSchema).parse((response as any).items || []),
68 | };
69 | }
70 |
```
--------------------------------------------------------------------------------
/common/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4 | import { z } from 'zod';
5 | import { zodToJsonSchema } from 'zod-to-json-schema';
6 | import { isGiteeError } from "./errors.js";
7 |
8 | type MCPServerOptions = {
9 | name: string;
10 | version: string;
11 | };
12 |
13 | type ToolDefinition = {
14 | name: string;
15 | description: string;
16 | schema: z.ZodType<any, any, any>;
17 | handler: (params: any) => Promise<any>;
18 | };
19 |
20 | export class MCPServer {
21 | private server: Server;
22 | private tools: Map<string, ToolDefinition> = new Map();
23 |
24 | constructor(options: MCPServerOptions) {
25 | this.server = new Server(
26 | {
27 | name: options.name,
28 | version: options.version,
29 | },
30 | {
31 | capabilities: {
32 | tools: {},
33 | },
34 | }
35 | );
36 |
37 | this.setupRequestHandlers();
38 | }
39 |
40 | private setupRequestHandlers() {
41 | this.server.setRequestHandler(ListToolsRequestSchema, async () => {
42 | const toolsList = Array.from(this.tools.values()).map((tool) => ({
43 | name: tool.name,
44 | description: tool.description,
45 | inputSchema: zodToJsonSchema(tool.schema),
46 | }));
47 |
48 | return {
49 | tools: toolsList,
50 | };
51 | });
52 |
53 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
54 | try {
55 | if (!request.params.arguments) {
56 | throw new Error("Parameters are required.");
57 | }
58 |
59 | const tool = this.tools.get(request.params.name);
60 | if (!tool) {
61 | throw new Error(`Unknown tool: ${request.params.name}`);
62 | }
63 |
64 | const args = tool.schema.parse(request.params.arguments);
65 | const result = await tool.handler(args);
66 |
67 | return {
68 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
69 | };
70 | } catch (error) {
71 | if (error instanceof z.ZodError) {
72 | throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
73 | }
74 | if (isGiteeError(error)) {
75 | throw error;
76 | }
77 | throw error;
78 | }
79 | });
80 | }
81 |
82 | public registerTool(tool: ToolDefinition) {
83 | this.tools.set(tool.name, tool);
84 | }
85 |
86 | public async connect(transport: StdioServerTransport) {
87 | await this.server.connect(transport);
88 | }
89 | }
90 |
```
--------------------------------------------------------------------------------
/common/errors.ts:
--------------------------------------------------------------------------------
```typescript
1 | export class GiteeError extends Error {
2 | constructor(message: string) {
3 | super(message);
4 | this.name = "GiteeError";
5 | }
6 | }
7 |
8 | export class GiteeValidationError extends GiteeError {
9 | response?: unknown;
10 |
11 | constructor(message: string, response?: unknown) {
12 | super(message);
13 | this.name = "GiteeValidationError";
14 | this.response = response;
15 | }
16 | }
17 |
18 | export class GiteeResourceNotFoundError extends GiteeError {
19 | constructor(message: string) {
20 | super(message);
21 | this.name = "GiteeResourceNotFoundError";
22 | }
23 | }
24 |
25 | export class GiteeAuthenticationError extends GiteeError {
26 | constructor(message: string) {
27 | super(message);
28 | this.name = "GiteeAuthenticationError";
29 | }
30 | }
31 |
32 | export class GiteePermissionError extends GiteeError {
33 | constructor(message: string) {
34 | super(message);
35 | this.name = "GiteePermissionError";
36 | }
37 | }
38 |
39 | export class GiteeRateLimitError extends GiteeError {
40 | resetAt: Date;
41 |
42 | constructor(message: string, resetAt: Date) {
43 | super(message);
44 | this.name = "GiteeRateLimitError";
45 | this.resetAt = resetAt;
46 | }
47 | }
48 |
49 | export class GiteeConflictError extends GiteeError {
50 | constructor(message: string) {
51 | super(message);
52 | this.name = "GiteeConflictError";
53 | }
54 | }
55 |
56 | export function isGiteeError(error: unknown): error is GiteeError {
57 | return error instanceof GiteeError;
58 | }
59 |
60 | export function createGiteeError(status: number, responseBody: unknown): GiteeError {
61 | let message = "Gitee API request failed";
62 | let resetAt: Date | undefined;
63 |
64 | if (typeof responseBody === "object" && responseBody !== null) {
65 | const body = responseBody as Record<string, unknown>;
66 |
67 | if (body.message && typeof body.message === "string") {
68 | message = body.message;
69 | }
70 |
71 | if (body.documentation_url && typeof body.documentation_url === "string") {
72 | message += ` - Documentation: ${body.documentation_url}`;
73 | }
74 | }
75 |
76 | switch (status) {
77 | case 400:
78 | return new GiteeValidationError(message, responseBody);
79 | case 401:
80 | return new GiteeAuthenticationError(message);
81 | case 403:
82 | return new GiteePermissionError(message);
83 | case 404:
84 | return new GiteeResourceNotFoundError(message);
85 | case 409:
86 | return new GiteeConflictError(message);
87 | case 429:
88 | return new GiteeRateLimitError(
89 | message,
90 | resetAt || new Date(Date.now() + 60 * 1000) // Default: reset after 1 minute
91 | );
92 | default:
93 | return new GiteeError(message);
94 | }
95 | }
96 |
```
--------------------------------------------------------------------------------
/operations/repos.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { giteeRequest, validateOwnerName, validateRepositoryName, getGiteeApiBaseUrl } from "../common/utils.js";
3 | import { GiteeRepositorySchema } from "../common/types.js";
4 |
5 | // Schema definitions
6 | export const CreateRepositorySchema = z.object({
7 | // 仓库名称
8 | name: z.string().describe("Repository name"),
9 | // 仓库描述
10 | description: z.string().optional().describe("Repository description"),
11 | // 主页地址
12 | homepage: z.string().optional().describe("Homepage URL"),
13 | // 是否私有
14 | private: z.boolean().default(false).optional().describe("Whether the repository is private"),
15 | // 是否开启 Issue 功能
16 | has_issues: z.boolean().default(true).optional().describe("Whether to enable Issue functionality"),
17 | // 是否开启 Wiki 功能
18 | has_wiki: z.boolean().default(true).optional().describe("Whether to enable Wiki functionality"),
19 | // 是否自动初始化仓库
20 | auto_init: z.boolean().default(false).optional().describe("Whether to automatically initialize the repository"),
21 | // Git Ignore 模板
22 | gitignore_template: z.string().optional().describe("Git Ignore template"),
23 | // License 模板
24 | license_template: z.string().optional().describe("License template"),
25 | // 仓库路径
26 | path: z.string().optional().describe("Repository path"),
27 | });
28 |
29 | export const ForkRepositorySchema = z.object({
30 | // 仓库所属空间地址 (企业、组织或个人的地址 path)
31 | owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
32 | // 仓库路径 (path)
33 | repo: z.string().describe("Repository path"),
34 | // 组织空间地址,不传默认为个人
35 | organization: z.string().optional().describe("Organization path, defaults to personal account if not provided"),
36 | });
37 |
38 | // Type exports
39 | export type CreateRepositoryOptions = z.infer<typeof CreateRepositorySchema>;
40 | export type ForkRepositoryOptions = z.infer<typeof ForkRepositorySchema>;
41 |
42 | // Function implementations
43 | export async function createRepository(options: CreateRepositoryOptions) {
44 | try {
45 | console.log('Creating repository parameters:', JSON.stringify(options));
46 | const url = "/user/repos";
47 | const response = await giteeRequest(url, "POST", options);
48 | console.log('Create repository response:', JSON.stringify(response));
49 |
50 | // Try to parse the response
51 | try {
52 | return GiteeRepositorySchema.parse(response);
53 | } catch (parseError) {
54 | console.error('Failed to parse repository response:', parseError);
55 | // Return the original response to avoid parsing errors
56 | return response;
57 | }
58 | } catch (error) {
59 | console.error('Failed to create repository request:', error);
60 | throw error;
61 | }
62 | }
63 |
64 | export async function forkRepository(
65 | owner: string,
66 | repo: string,
67 | organization?: string
68 | ) {
69 | owner = validateOwnerName(owner);
70 | repo = validateRepositoryName(repo);
71 |
72 | const url = `/repos/${owner}/${repo}/forks`;
73 | const body: Record<string, string> = {};
74 |
75 | if (organization) {
76 | body.organization = validateOwnerName(organization);
77 | }
78 |
79 | const response = await giteeRequest(url, "POST", body);
80 |
81 | return GiteeRepositorySchema.parse(response);
82 | }
```
--------------------------------------------------------------------------------
/operations/branches.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { giteeRequest, validateBranchName, validateOwnerName, validateRepositoryName, getGiteeApiBaseUrl } from "../common/utils.js";
3 | import { GiteeCompleteBranchSchema, GiteeBranchSchema } from "../common/types.js";
4 |
5 | // Schema definitions
6 | export const CreateBranchSchema = z.object({
7 | // 仓库所属空间地址 (企业、组织或个人的地址 path)
8 | owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
9 | // 仓库路径 (path)
10 | repo: z.string().describe("Repository path"),
11 | // 新创建的分支名称
12 | branch_name: z.string().describe("Name for the new branch"),
13 | // 起点名称,默认:master
14 | refs: z.string().default("master").describe("Source reference for the branch, default: master"),
15 | });
16 |
17 | export const ListBranchesSchema = z.object({
18 | // 仓库所属空间地址 (企业、组织或个人的地址 path)
19 | owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
20 | // 仓库路径 (path)
21 | repo: z.string().describe("Repository path"),
22 | // 排序字段
23 | sort: z.enum(["name", "updated"]).default("name").optional().describe("Sort field"),
24 | // 排序方向
25 | direction: z.enum(["asc", "desc"]).default("asc").optional().describe("Sort direction"),
26 | // 当前的页码
27 | page: z.number().int().default(1).optional().describe("Page number"),
28 | // 每页的数量,最大为 100
29 | per_page: z.number().int().min(1).max(100).optional().describe("Number of items per page, maximum 100"),
30 | });
31 |
32 | export const GetBranchSchema = z.object({
33 | // 仓库所属空间地址 (企业、组织或个人的地址 path)
34 | owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
35 | // 仓库路径 (path)
36 | repo: z.string().describe("Repository path"),
37 | // 分支名称
38 | branch: z.string().describe("Branch name"),
39 | });
40 |
41 | // Type exports
42 | export type CreateBranchOptions = z.infer<typeof CreateBranchSchema>;
43 | export type ListBranchesOptions = z.infer<typeof ListBranchesSchema>;
44 | export type GetBranchOptions = z.infer<typeof GetBranchSchema>;
45 |
46 | // Function implementations
47 | export async function createBranchFromRef(
48 | owner: string,
49 | repo: string,
50 | branchName: string,
51 | refs: string = "master"
52 | ) {
53 | owner = validateOwnerName(owner);
54 | repo = validateRepositoryName(repo);
55 | branchName = validateBranchName(branchName);
56 |
57 | const url = `/repos/${owner}/${repo}/branches`;
58 | const body = {
59 | branch_name: branchName,
60 | refs: refs,
61 | };
62 |
63 | const response = await giteeRequest(url, "POST", body);
64 | return GiteeBranchSchema.parse(response);
65 | }
66 |
67 | export async function listBranches(
68 | owner: string,
69 | repo: string,
70 | sort?: string,
71 | direction?: string,
72 | page?: number,
73 | per_page?: number
74 | ) {
75 | owner = validateOwnerName(owner);
76 | repo = validateRepositoryName(repo);
77 |
78 | const url = new URL(`${getGiteeApiBaseUrl()}/repos/${owner}/${repo}/branches`);
79 |
80 | if (sort) {
81 | url.searchParams.append("sort", sort);
82 | }
83 | if (direction) {
84 | url.searchParams.append("direction", direction);
85 | }
86 | if (page !== undefined) {
87 | url.searchParams.append("page", page.toString());
88 | }
89 | if (per_page !== undefined) {
90 | url.searchParams.append("per_page", per_page.toString());
91 | }
92 |
93 | const response = await giteeRequest(url.toString());
94 | return z.array(GiteeBranchSchema).parse(response);
95 | }
96 |
97 | export async function getBranch(owner: string, repo: string, branch: string) {
98 | owner = validateOwnerName(owner);
99 | repo = validateRepositoryName(repo);
100 | branch = validateBranchName(branch);
101 |
102 | const url = `/repos/${owner}/${repo}/branches/${branch}`;
103 | const response = await giteeRequest(url);
104 |
105 | return GiteeCompleteBranchSchema.parse(response);
106 | }
107 |
```
--------------------------------------------------------------------------------
/common/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { getUserAgent } from "universal-user-agent";
2 | import { createGiteeError } from "./errors.js";
3 | import { VERSION } from "./version.js";
4 |
5 | // Default Gitee API base URL
6 | const DEFAULT_GITEE_API_BASE_URL = "https://gitee.com/api/v5";
7 |
8 | /**
9 | * Get the Gitee API base URL from environment variables or use the default
10 | * @returns The Gitee API base URL
11 | */
12 | export function getGiteeApiBaseUrl(): string {
13 | return process.env.GITEE_API_BASE_URL || DEFAULT_GITEE_API_BASE_URL;
14 | }
15 |
16 | type RequestOptions = {
17 | method?: string;
18 | body?: unknown;
19 | headers?: Record<string, string>;
20 | }
21 |
22 | async function parseResponseBody(response: Response): Promise<unknown> {
23 | const contentType = response.headers.get("content-type");
24 | if (contentType?.includes("application/json")) {
25 | return response.json();
26 | }
27 | return response.text();
28 | }
29 |
30 | export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): string {
31 | const url = new URL(baseUrl);
32 | Object.entries(params).forEach(([key, value]) => {
33 | if (value !== undefined) {
34 | url.searchParams.append(key, value.toString());
35 | }
36 | });
37 | return url.toString();
38 | }
39 |
40 | const USER_AGENT = `modelcontextprotocol/servers/gitee/v${VERSION} ${getUserAgent()}`;
41 |
42 | // Generate the equivalent curl command for debugging.
43 | function generateCurlCommand(url: string, method: string, headers: Record<string, string>, body?: unknown): string {
44 | let curl = `curl -X ${method} "${url}"`;
45 |
46 | // Add request headers
47 | Object.entries(headers).forEach(([key, value]) => {
48 | curl += ` -H "${key}: ${value}"`;
49 | });
50 |
51 | // Add request body
52 | if (body) {
53 | curl += ` -d '${JSON.stringify(body)}'`;
54 | }
55 |
56 | return curl;
57 | }
58 |
59 | // debug utility function
60 | export function debug(message: string, data?: unknown): void {
61 | // Only output debug logs if DEBUG environment variable is set
62 | if (process.env.DEBUG !== "true") {
63 | return;
64 | }
65 |
66 | if (data !== undefined) {
67 | console.error(`[DEBUG] ${message}`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data);
68 | } else {
69 | console.error(`[DEBUG] ${message}`);
70 | }
71 | }
72 |
73 | export async function giteeRequest(
74 | urlPath: string,
75 | method: string = "GET",
76 | body?: unknown,
77 | headers?: Record<string, string>
78 | ): Promise<unknown> {
79 | // Check if the URL is already a full URL or a path
80 | let url = urlPath.startsWith("http") ? urlPath : `${getGiteeApiBaseUrl()}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`;
81 | const requestHeaders: Record<string, string> = {
82 | "Accept": "application/json",
83 | "Content-Type": "application/json",
84 | "User-Agent": USER_AGENT,
85 | ...headers,
86 | };
87 |
88 | if (process.env.GITEE_PERSONAL_ACCESS_TOKEN) {
89 | // The Gitee API uses `access_token` as a query parameter or in the `Authorization` header.
90 | // Method 1: Add to URL Query Parameters
91 | let urlObj = new URL(url);
92 | urlObj.searchParams.append('access_token', process.env.GITEE_PERSONAL_ACCESS_TOKEN);
93 | url = urlObj.toString();
94 |
95 | // Method 2: Add to Request Headers (Two methods are tried to increase success rate)
96 | requestHeaders["Authorization"] = `token ${process.env.GITEE_PERSONAL_ACCESS_TOKEN}`;
97 |
98 | debug(`Using access token: ${process.env.GITEE_PERSONAL_ACCESS_TOKEN.substring(0, 4)}...`);
99 | } else {
100 | debug(`No access token found in environment variables`);
101 | }
102 |
103 | // Print the request
104 | debug(`Request: ${method} ${url}`);
105 | debug(`Headers:`, requestHeaders);
106 | if (body) {
107 | debug(`Body:`, body);
108 | }
109 |
110 | // Print the equivalent curl command
111 | const curlCommand = generateCurlCommand(url, method, requestHeaders, body);
112 | debug(`cURL: ${curlCommand}\n`);
113 |
114 | const response = await fetch(url, {
115 | method,
116 | headers: requestHeaders,
117 | body: body ? JSON.stringify(body) : undefined,
118 | });
119 |
120 | const responseBody = await parseResponseBody(response);
121 |
122 | // Print the response
123 | debug(`Response Status: ${response.status} ${response.statusText}`);
124 | debug(`Response Body:`, responseBody);
125 |
126 | if (!response.ok) {
127 | throw createGiteeError(response.status, responseBody);
128 | }
129 |
130 | return responseBody;
131 | }
132 |
133 | export function validateBranchName(branch: string): string {
134 | const sanitized = branch.trim();
135 | if (!sanitized) {
136 | throw new Error("分支名不能为空");
137 | }
138 | if (sanitized.includes("..")) {
139 | throw new Error("分支名不能包含 '..'");
140 | }
141 | if (/[\s~^:?*[\\\]]/.test(sanitized)) {
142 | throw new Error("分支名包含无效字符");
143 | }
144 | if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
145 | throw new Error("分支名不能以 '/' 开头或结尾");
146 | }
147 | if (sanitized.endsWith(".lock")) {
148 | throw new Error("分支名不能以 '.lock' 结尾");
149 | }
150 | return sanitized;
151 | }
152 |
153 | export function validateRepositoryName(name: string): string {
154 | const sanitized = name.trim();
155 | if (!sanitized) {
156 | throw new Error("仓库名不能为空");
157 | }
158 | if (!/^[a-zA-Z0-9_.-]+$/.test(sanitized)) {
159 | throw new Error(
160 | "仓库名只能包含字母、数字、连字符、句点和下划线"
161 | );
162 | }
163 | if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
164 | throw new Error("仓库名不能以句点开头或结尾");
165 | }
166 | return sanitized;
167 | }
168 |
169 | export function validateOwnerName(owner: string): string {
170 | const sanitized = owner.trim();
171 | if (!sanitized) {
172 | throw new Error("所有者名称不能为空");
173 | }
174 | if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(sanitized)) {
175 | throw new Error(
176 | "所有者名称只能包含字母、数字和连字符,且必须以字母或数字开头"
177 | );
178 | }
179 | return sanitized;
180 | }
181 |
182 | export async function checkBranchExists(
183 | owner: string,
184 | repo: string,
185 | branch: string
186 | ): Promise<boolean> {
187 | try {
188 | await giteeRequest(`/repos/${owner}/${repo}/branches/${branch}`, "GET");
189 | return true;
190 | } catch (error) {
191 | if (error && typeof error === "object" && "name" in error && error.name === "GiteeResourceNotFoundError") {
192 | return false;
193 | }
194 | throw error;
195 | }
196 | }
197 |
198 | export async function checkUserExists(username: string): Promise<boolean> {
199 | try {
200 | await giteeRequest(`/users/${username}`, "GET");
201 | return true;
202 | } catch (error) {
203 | if (error && typeof error === "object" && "name" in error && error.name === "GiteeResourceNotFoundError") {
204 | return false;
205 | }
206 | throw error;
207 | }
208 | }
209 |
```