@boringstudio_org/gitea-mcp 1.4.1 → 1.6.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.
@@ -4,6 +4,7 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - 'v*'
7
+ workflow_dispatch:
7
8
 
8
9
  jobs:
9
10
  publish:
package/.versionrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "releaseCommitMessageFormat": "chore(release): {{currentTag}} [skip ci]"
3
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
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
4
 
5
+ ### [1.6.1](https://git.boringstudio.by/BoringStudio/mcp-gitea/compare/v1.6.0...v1.6.1) (2026-01-15)
6
+
7
+ ## [1.6.0](https://git.boringstudio.by/BoringStudio/mcp-gitea/compare/v1.4.1...v1.6.0) (2026-01-15)
8
+
9
+
10
+ ### Features
11
+
12
+ * enhance security and optimization ([#5](https://git.boringstudio.by/BoringStudio/mcp-gitea/issues/5)) ([f9f7055](https://git.boringstudio.by/BoringStudio/mcp-gitea/commit/f9f70556240482149ac245357f23f1160e873e5c))
13
+
5
14
  ### 1.4.1 (2026-01-14)
6
15
 
7
16
  ### [1.3.6](https://git.boringstudio.by/BoringStudio/mcp-gitea-proxy/compare/v1.3.5...v1.3.6) (2026-01-14)
package/index.js CHANGED
@@ -14,7 +14,7 @@ dotenv.config({ path: path.join(process.cwd(), ".env"), quiet: true });
14
14
  export async function runGiteaServer() {
15
15
  const server = new McpServer({
16
16
  name: "gitea-proxy-agent",
17
- version: "1.6.0",
17
+ version: "1.5.0",
18
18
  });
19
19
 
20
20
  const GITEA_TOKEN = process.env.GITEA_TOKEN;
@@ -57,6 +57,24 @@ export async function runGiteaServer() {
57
57
  }
58
58
  }
59
59
 
60
+ // Cache for labels: key="owner/repo", value={ timestamp, data }
61
+ const labelCache = new Map();
62
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
63
+
64
+ async function getRepoLabels(owner, repo) {
65
+ const key = `${owner}/${repo}`;
66
+ const now = Date.now();
67
+ const cached = labelCache.get(key);
68
+
69
+ if (cached && (now - cached.timestamp < CACHE_TTL)) {
70
+ return cached.data;
71
+ }
72
+
73
+ const labels = await giteaApi("GET", `/repos/${owner}/${repo}/labels`);
74
+ labelCache.set(key, { timestamp: now, data: labels });
75
+ return labels;
76
+ }
77
+
60
78
  // --- RESOURCES ---
61
79
  server.resource(
62
80
  "issues-list",
@@ -116,6 +134,23 @@ export async function runGiteaServer() {
116
134
  );
117
135
 
118
136
  // --- TOOLS ---
137
+
138
+ // Security: Allowed paths for run_safe_shell
139
+ const ALLOWED_PATHS = (process.env.MCP_ALLOWED_PATHS || process.cwd()).split(',').map(p => path.resolve(p.trim()));
140
+
141
+ function isPathAllowed(targetPath) {
142
+ const resolved = path.resolve(targetPath);
143
+ return ALLOWED_PATHS.some(allowed => resolved.startsWith(allowed));
144
+ }
145
+
146
+ // Security: Allowed git subcommands
147
+ const ALLOWED_GIT_COMMANDS = [
148
+ 'status', 'add', 'commit', 'push', 'pull', 'diff', 'log', 'show',
149
+ 'checkout', 'branch', 'merge', 'remote', 'fetch', 'clone', 'init',
150
+ 'ls-files', 'ls-tree', 'rev-parse', 'clean', 'restore', 'rm', 'mv',
151
+ 'reset', 'stash', 'grep'
152
+ ];
153
+
119
154
  server.tool(
120
155
  "run_safe_shell",
121
156
  "Execute a safe shell command (git only) in a specific directory",
@@ -129,6 +164,23 @@ export async function runGiteaServer() {
129
164
  return { content: [{ type: "text", text: "Error: Only 'git' commands are allowed." }], isError: true };
130
165
  }
131
166
 
167
+ if (!isPathAllowed(cwd)) {
168
+ return { content: [{ type: "text", text: `Error: Access denied. Path '${cwd}' is not within allowed directories.` }], isError: true };
169
+ }
170
+
171
+ if (!args || args.length === 0) {
172
+ return { content: [{ type: "text", text: "Error: No git subcommand provided." }], isError: true };
173
+ }
174
+
175
+ const subcommand = args[0];
176
+ if (subcommand.startsWith('-')) {
177
+ return { content: [{ type: "text", text: "Error: Global git flags are not allowed. Start with a subcommand." }], isError: true };
178
+ }
179
+
180
+ if (!ALLOWED_GIT_COMMANDS.includes(subcommand)) {
181
+ return { content: [{ type: "text", text: `Error: Git subcommand '${subcommand}' is not allowed.` }], isError: true };
182
+ }
183
+
132
184
  return new Promise((resolve) => {
133
185
  const child = spawn(command, args, {
134
186
  cwd: cwd,
@@ -267,7 +319,7 @@ export async function runGiteaServer() {
267
319
  repo: z.string()
268
320
  },
269
321
  async ({ owner, repo }) => {
270
- const labels = await giteaApi("GET", `/repos/${owner}/${repo}/labels`);
322
+ const labels = await getRepoLabels(owner, repo);
271
323
  const formatted = labels.map(l => `${l.name} (ID: ${l.id})`).join("\n");
272
324
  return { content: [{ type: "text", text: formatted || "No labels found." }] };
273
325
  }
@@ -323,7 +375,7 @@ body: z.string().optional()
323
375
  if (body) payload.body = body;
324
376
 
325
377
  if (labels) {
326
- const repoLabels = await giteaApi("GET", `/repos/${owner}/${repo}/labels`);
378
+ const repoLabels = await getRepoLabels(owner, repo);
327
379
  const labelIds = labels.map(name => {
328
380
  const found = repoLabels.find(l => l.name === name);
329
381
  if (!found) throw new Error(`Label '${name}' not found in repository.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boringstudio_org/gitea-mcp",
3
- "version": "1.4.1",
3
+ "version": "1.6.1",
4
4
  "description": "A Gitea MCP Server for interacting with repositories, issues, and more.",
5
5
  "type": "module",
6
6
  "main": "index.js",