@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.
- package/.gitea/workflows/publish.yaml +1 -0
- package/.versionrc +3 -0
- package/CHANGELOG.md +9 -0
- package/index.js +55 -3
- package/package.json +1 -1
package/.versionrc
ADDED
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.
|
|
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
|
|
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
|
|
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.`);
|