@boringstudio_org/gitea-mcp 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Setup Node.js
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '24'
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - name: Install dependencies
22
+ run: npm ci
23
+
24
+ - name: Publish
25
+ run: npm publish
26
+ env:
27
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,43 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ if: "!contains(github.event.head_commit.message, 'chore(release)')"
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+ token: ${{ secrets.RELEASE_TOKEN }}
18
+
19
+ - name: Checkout branch
20
+ run: |
21
+ git checkout main
22
+ git pull origin main
23
+
24
+ - name: Setup Node.js
25
+ uses: actions/setup-node@v4
26
+ with:
27
+ node-version: '24'
28
+
29
+ - name: Configure Git
30
+ run: |
31
+ git config user.name "Gitea Actions"
32
+ git config user.email "sgmakgg@gmail.com"
33
+
34
+ - name: Install dependencies
35
+ run: npm ci
36
+
37
+ - name: Run standard-version
38
+ run: npm run release
39
+
40
+ - name: Push changes
41
+ run: git push --follow-tags origin main
42
+ env:
43
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
+
5
+ ### 1.4.1 (2026-01-14)
6
+
7
+ ### [1.3.6](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.5...v1.3.6) (2026-01-14)
8
+
9
+ ### [1.3.5](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.4...v1.3.5) (2026-01-14)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * Update start script to use global package and load env ([#20](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/issues/20)) ([c4598fc](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/c4598fce2bbfcd9acc48599fe0eddde013ecedb5))
15
+
16
+ ### [1.3.4](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.3...v1.3.4) (2026-01-14)
17
+
18
+
19
+ ### Features
20
+
21
+ * Native Node.js Google Search implementation ([#19](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/issues/19)) ([555f44a](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/555f44a9bd9017d2cd01351efe19b5b2c766b442))
22
+
23
+ ### [1.3.3](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.2...v1.3.3) (2026-01-14)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * Silence npx output, map Google ID, add start script and update docs ([#18](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/issues/18)) ([7c36bdc](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/7c36bdcedd5c29281a1562a7a801c3d0ac6d3256))
29
+
30
+ ### [1.3.2](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.1...v1.3.2) (2026-01-14)
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * Add ~/.local/bin to PATH for uvx discovery ([#16](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/issues/16)) ([b2a25d5](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/b2a25d5fa5ec6d5d9fface87d48eb431ff9fd175))
36
+
37
+ ### [1.3.1](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.0...v1.3.1) (2026-01-14)
38
+
39
+
40
+ ### Bug Fixes
41
+
42
+ * Remove hardcoded env from npm config and update docs ([9842c48](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/9842c48e07e91a0d5b79d547782aa0aa9b6d9390))
43
+
44
+ ## [1.3.0](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.2.0...v1.3.0) (2026-01-13)
45
+
46
+ ## 1.2.0 (2026-01-13)
47
+
48
+
49
+ ### Features
50
+
51
+ * add create_pull_request and list_branches tools (Closes [#3](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/issues/3)) ([465d4ed](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/465d4ed1738cf7e54d2b67b7a8c5f2df70194cfe))
52
+ * add list_labels and update_issue with labels support (Closes [#2](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/issues/2)) ([9006385](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/9006385ce82cd5adc5f7af9bdbf5e28c89789816))
53
+ * add memory schema docs and update project structure ([841b0d6](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/841b0d6075c9e9b501a7d76a7cfa10a1f228a2bb))
54
+ * add secure shell execution and full Gitea integration ([d22d48c](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/d22d48cb99de046b560313995b08d1c67955fb41))
55
+ * upgrade to v1.4.0 with Resources and Prompts ([1510e9e](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/1510e9e7e1b3d682c011a7fdc56ae2b26435c086))
56
+
57
+
58
+ ### Bug Fixes
59
+
60
+ * Add fallback for uvx path ([dba4e21](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/commit/dba4e213790c91a8d031c37cac9a0eaf8724664a))
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Gitea MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for interacting with Gitea.
4
+ This package is designed to be used as a standalone server or imported by a gateway.
5
+
6
+ ## 🛠 Capabilities
7
+
8
+ * **Issues:** List, search, create, update, comment.
9
+ * **Repositories:** List repos, branches, labels.
10
+ * **Files:** Read file content.
11
+ * **Pull Requests:** Create PRs.
12
+ * **Analysis:** Analyze issues with AI prompts.
13
+ * **Shell:** Execute safe git commands in WSL (`run_safe_shell`).
14
+
15
+ ## 📦 Installation
16
+
17
+ ```bash
18
+ npm install @boringstudio_org/gitea-mcp
19
+ ```
20
+
21
+ ## 🚀 Usage
22
+
23
+ ### Via NPX (Recommended)
24
+ ```bash
25
+ # Configure environment variables
26
+ export GITEA_TOKEN=your_token
27
+ # Optional: Defaults to https://git.boringstudio.by/api/v1
28
+ export GITEA_API_URL=https://git.your-instance.com/api/v1
29
+
30
+ # Run
31
+ npx @boringstudio_org/gitea-mcp
32
+ ```
33
+
34
+ ### Standalone (From Source)
35
+ ```bash
36
+ # Install dependencies
37
+ npm install
38
+
39
+ # Configure .env
40
+ echo "Gitea_TOKEN=..." > .env
41
+
42
+ # Run
43
+ node index.js
44
+ ```
45
+
46
+ ### As a Library
47
+ ```javascript
48
+ import { runGiteaServer } from "@boringstudio_org/gitea-mcp";
49
+ // Ensure environment variables are set
50
+ runGiteaServer().catch(console.error);
51
+ ```
52
+
53
+ ## 📝 License
54
+
55
+ MIT
package/index.js ADDED
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import axios from "axios";
5
+ import { z } from "zod";
6
+ import dotenv from "dotenv";
7
+ import path from "path";
8
+ import { fileURLToPath } from 'url';
9
+ import { spawn } from "child_process";
10
+
11
+ // Load .env from the current working directory
12
+ dotenv.config({ path: path.join(process.cwd(), ".env"), quiet: true });
13
+
14
+ export async function runGiteaServer() {
15
+ const server = new McpServer({
16
+ name: "gitea-proxy-agent",
17
+ version: "1.6.0",
18
+ });
19
+
20
+ const GITEA_TOKEN = process.env.GITEA_TOKEN;
21
+ const BASE_URL = process.env.GITEA_API_URL || "https://git.boringstudio.by/api/v1";
22
+
23
+ if (!GITEA_TOKEN) {
24
+ console.error("❌ Error: GITEA_TOKEN not found.");
25
+ console.error("Please provide a Gitea Token in a .env file (GITEA_TOKEN=your_token).");
26
+ process.exit(1);
27
+ }
28
+
29
+ const headers = {
30
+ "Authorization": `token ${GITEA_TOKEN}`,
31
+ "Content-Type": "application/json"
32
+ };
33
+
34
+ // For Cloudflare Access, set CF_ID and CF_SECRET in your .env file
35
+ if (process.env.CF_ID && process.env.CF_SECRET) {
36
+ headers["CF-Access-Client-Id"] = process.env.CF_ID;
37
+ headers["CF-Access-Client-Secret"] = process.env.CF_SECRET;
38
+ console.error("INFO: Running in Cloudflare Proxy mode");
39
+ }
40
+
41
+ const api = axios.create({
42
+ baseURL: BASE_URL,
43
+ headers: headers
44
+ });
45
+
46
+ async function giteaApi(method, endpoint, data = null, params = null) {
47
+ try {
48
+ const config = { method, url: endpoint };
49
+ if (data) config.data = data;
50
+ if (params) config.params = params;
51
+
52
+ const response = await api(config);
53
+ return response.data;
54
+ } catch (error) {
55
+ const msg = error.response?.data?.message || error.message;
56
+ throw new Error(`Gitea API Error: ${msg}`);
57
+ }
58
+ }
59
+
60
+ // --- RESOURCES ---
61
+ server.resource(
62
+ "issues-list",
63
+ new ResourceTemplate("gitea://{owner}/{repo}/issues", { list: undefined }),
64
+ async (uri, { owner, repo }) => {
65
+ const issues = await giteaApi("GET", `/repos/${owner}/${repo}/issues`, null, { limit: 50 });
66
+ return {
67
+ contents: [{
68
+ uri: uri.href,
69
+ text: JSON.stringify(issues, null, 2),
70
+ mimeType: "application/json"
71
+ }]
72
+ };
73
+ }
74
+ );
75
+
76
+ server.resource(
77
+ "file-content",
78
+ new ResourceTemplate("gitea://{owner}/{repo}/files/{branch}/{path}", { list: undefined }),
79
+ async (uri, { owner, repo, branch, path: filePath }) => {
80
+ const content = await giteaApi("GET", `/repos/${owner}/${repo}/raw/${branch}/${filePath}`);
81
+ return {
82
+ contents: [{
83
+ uri: uri.href,
84
+ text: typeof content === 'string' ? content : JSON.stringify(content),
85
+ mimeType: "text/plain"
86
+ }]
87
+ };
88
+ }
89
+ );
90
+
91
+ // --- PROMPTS ---
92
+ server.prompt(
93
+ "analyze-issue",
94
+ {
95
+ owner: z.string(),
96
+ repo: z.string(),
97
+ issue_number: z.string().describe("Issue number (as string)")
98
+ },
99
+ async ({ owner, repo, issue_number }) => {
100
+ const issue = await giteaApi("GET", `/repos/${owner}/${repo}/issues/${issue_number}`);
101
+ const comments = await giteaApi("GET", `/repos/${owner}/${repo}/issues/${issue_number}/comments`);
102
+ return {
103
+ messages: [{
104
+ role: "user",
105
+ content: {
106
+ type: "text",
107
+ text: `Please analyze the following Gitea issue and suggest a solution or next steps.\n\n` +
108
+ `Title: ${issue.title}\n` +
109
+ `State: ${issue.state}\n` +
110
+ `Description:\n${issue.body}\n\n` +
111
+ `Comments:\n${comments.map(c => `- ${c.user.username}: ${c.body}`).join("\n")}`
112
+ }
113
+ }]
114
+ };
115
+ }
116
+ );
117
+
118
+ // --- TOOLS ---
119
+ server.tool(
120
+ "run_safe_shell",
121
+ "Execute a safe shell command (git only) in a specific directory",
122
+ {
123
+ cwd: z.string().describe("The working directory for the command"),
124
+ command: z.string().describe("The command to execute (must start with 'git')"),
125
+ args: z.array(z.string()).describe("The arguments for the command")
126
+ },
127
+ async ({ cwd, command, args }) => {
128
+ if (command !== 'git') {
129
+ return { content: [{ type: "text", text: "Error: Only 'git' commands are allowed." }], isError: true };
130
+ }
131
+
132
+ return new Promise((resolve) => {
133
+ const child = spawn(command, args, {
134
+ cwd: cwd,
135
+ env: process.env,
136
+ shell: false
137
+ });
138
+
139
+ let stdout = "";
140
+ let stderr = "";
141
+
142
+ child.stdout.on("data", (data) => {
143
+ stdout += data.toString();
144
+ });
145
+
146
+ child.stderr.on("data", (data) => {
147
+ stderr += data.toString();
148
+ });
149
+
150
+ child.on("close", (code) => {
151
+ if (code === 0) {
152
+ resolve({
153
+ content: [{ type: "text", text: stdout || "Success (no output)" }]
154
+ });
155
+ } else {
156
+ resolve({
157
+ content: [{ type: "text", text: `Exit code: ${code}\nStdout: ${stdout}\nStderr: ${stderr}` }],
158
+ isError: true
159
+ });
160
+ }
161
+ });
162
+
163
+ child.on("error", (err) => {
164
+ resolve({
165
+ content: [{ type: "text", text: `Failed to start command: ${err.message}` }],
166
+ isError: true
167
+ });
168
+ });
169
+ });
170
+ }
171
+ );
172
+
173
+ server.tool(
174
+ "list_repos",
175
+ "Get a list of all user/organization repositories",
176
+ { org: z.string().optional().describe("Organization name") },
177
+ async ({ org }) => {
178
+ const endpoint = org ? `/orgs/${org}/repos` : "/user/repos";
179
+ const repos = await giteaApi("GET", endpoint, null, { limit: 50 });
180
+ const formatted = repos.map(r => `${r.full_name} (ID: ${r.id}) - ${r.private ? 'Private' : 'Public'}`).join("\n");
181
+ return { content: [{ type: "text", text: formatted || "No repositories found." }] };
182
+ }
183
+ );
184
+
185
+ server.tool(
186
+ "list_issues",
187
+ "Get a list of all open issues in the repository",
188
+ {
189
+ owner: z.string(),
190
+ repo: z.string(),
191
+ page: z.number().optional(),
192
+ limit: z.number().optional()
193
+ },
194
+ async ({ owner, repo, page = 1, limit = 20 }) => {
195
+ const issues = await giteaApi("GET", `/repos/${owner}/${repo}/issues`, null, { page, limit });
196
+ const formatted = issues.map(i => `#${i.number}: ${i.title}`).join("\n");
197
+ return { content: [{ type: "text", text: formatted || "No issues yet." }] };
198
+ }
199
+ );
200
+
201
+ server.tool(
202
+ "search_issues",
203
+ "Search for issues in a repository using keywords",
204
+ {
205
+ owner: z.string(),
206
+ repo: z.string(),
207
+ query: z.string(),
208
+ state: z.enum(["open", "closed", "all"]).optional()
209
+ },
210
+ async ({ owner, repo, query, state = "open" }) => {
211
+ const issues = await giteaApi("GET", `/repos/${owner}/${repo}/issues`, null, { q: query, state, limit: 20 });
212
+ const formatted = issues.map(i => `#${i.number} (${i.state}): ${i.title}`).join("\n");
213
+ return { content: [{ type: "text", text: formatted || "No matching issues found." }] };
214
+ }
215
+ );
216
+
217
+ server.tool(
218
+ "get_issue_details",
219
+ "Read the full issue description and its status",
220
+ {
221
+ owner: z.string(),
222
+ repo: z.string(),
223
+ issue_number: z.number()
224
+ },
225
+ async ({ owner, repo, issue_number }) => {
226
+ const i = await giteaApi("GET", `/repos/${owner}/${repo}/issues/${issue_number}`);
227
+ const result = `ISSUE #${i.number}: ${i.title}\nStatus: ${i.state}\nDescription:\n${i.body || "No description"}`;
228
+ return { content: [{ type: "text", text: result }] };
229
+ }
230
+ );
231
+
232
+ server.tool(
233
+ "add_comment",
234
+ "Leave a comment on a Gitea issue",
235
+ {
236
+ owner: z.string(),
237
+ repo: z.string(),
238
+ issue_number: z.number(),
239
+ body: z.string()
240
+ },
241
+ async ({ owner, repo, issue_number, body }) => {
242
+ await giteaApi("POST", `/repos/${owner}/${repo}/issues/${issue_number}/comments`, { body });
243
+ return { content: [{ type: "text", text: "Success: Comment added." }] };
244
+ }
245
+ );
246
+
247
+ server.tool(
248
+ "create_issue",
249
+ "Create a new issue in the repository",
250
+ {
251
+ owner: z.string(),
252
+ repo: z.string(),
253
+ title: z.string(),
254
+ body: z.string().optional()
255
+ },
256
+ async ({ owner, repo, title, body }) => {
257
+ const response = await giteaApi("POST", `/repos/${owner}/${repo}/issues`, { title, body });
258
+ return { content: [{ type: "text", text: `Success: Issue #${response.number} created.` }] };
259
+ }
260
+ );
261
+
262
+ server.tool(
263
+ "list_labels",
264
+ "Get a list of all labels in the repository",
265
+ {
266
+ owner: z.string(),
267
+ repo: z.string()
268
+ },
269
+ async ({ owner, repo }) => {
270
+ const labels = await giteaApi("GET", `/repos/${owner}/${repo}/labels`);
271
+ const formatted = labels.map(l => `${l.name} (ID: ${l.id})`).join("\n");
272
+ return { content: [{ type: "text", text: formatted || "No labels found." }] };
273
+ }
274
+ );
275
+
276
+ server.tool(
277
+ "list_branches",
278
+ "List branches in the repository",
279
+ {
280
+ owner: z.string(),
281
+ repo: z.string()
282
+ },
283
+ async ({ owner, repo }) => {
284
+ const branches = await giteaApi("GET", `/repos/${owner}/${repo}/branches`);
285
+ const formatted = branches.map(b => b.name).join("\n");
286
+ return { content: [{ type: "text", text: formatted || "No branches found." }] };
287
+ }
288
+ );
289
+
290
+ server.tool(
291
+ "create_pull_request",
292
+ "Create a Pull Request",
293
+ {
294
+ owner: z.string(),
295
+ repo: z.string(),
296
+ head: z.string(),
297
+ base: z.string().optional(),
298
+ title: z.string(),
299
+ body: z.string().optional()
300
+ },
301
+ async ({ owner, repo, head, base = "main", title, body }) => {
302
+ const response = await giteaApi("POST", `/repos/${owner}/${repo}/pulls`, { head, base, title, body });
303
+ return { content: [{ type: "text", text: `Success: PR #${response.number} created: ${response.html_url}` }] };
304
+ }
305
+ );
306
+
307
+ server.tool(
308
+ "update_issue",
309
+ "Update an issue (change state, title, body, or labels)",
310
+ {
311
+ owner: z.string(),
312
+ repo: z.string(),
313
+ issue_number: z.number(),
314
+ state: z.enum(["open", "closed"]).optional(),
315
+ title: z.string().optional(),
316
+ body: z.string().optional(),
317
+ labels: z.array(z.string()).optional()
318
+ },
319
+ async ({ owner, repo, issue_number, state, title, body, labels }) => {
320
+ const payload = {};
321
+ if (state) payload.state = state;
322
+ if (title) payload.title = title;
323
+ if (body) payload.body = body;
324
+
325
+ if (labels) {
326
+ const repoLabels = await giteaApi("GET", `/repos/${owner}/${repo}/labels`);
327
+ const labelIds = labels.map(name => {
328
+ const found = repoLabels.find(l => l.name === name);
329
+ if (!found) throw new Error(`Label '${name}' not found in repository.`);
330
+ return found.id;
331
+ });
332
+ payload.labels = labelIds;
333
+ }
334
+
335
+ if (Object.keys(payload).length === 0) {
336
+ return { content: [{ type: "text", text: "No changes requested." }] };
337
+ }
338
+
339
+ await giteaApi("PATCH", `/repos/${owner}/${repo}/issues/${issue_number}`, payload);
340
+ return { content: [{ type: "text", text: `Success: Issue #${issue_number} updated.` }] };
341
+ }
342
+ );
343
+
344
+ const transport = new StdioServerTransport();
345
+ await server.connect(transport);
346
+ }
347
+
348
+ // Only run if executed directly
349
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
350
+ runGiteaServer().catch(error => {
351
+ console.error("FATAL ERROR:", error.message);
352
+ process.exit(1);
353
+ });
354
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@boringstudio_org/gitea-mcp",
3
+ "version": "1.4.1",
4
+ "description": "A Gitea MCP Server for interacting with repositories, issues, and more.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "gitea-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "release": "standard-version"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.25.2",
16
+ "axios": "^1.7.9",
17
+ "dotenv": "^17.2.3",
18
+ "zod": "^3.24.1"
19
+ },
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://git.boringstudio.by/BoringStudio/mcp-gitea.git"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "gitea",
28
+ "ai",
29
+ "llm",
30
+ "server"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "devDependencies": {
36
+ "standard-version": "^9.5.0"
37
+ }
38
+ }