@mokoconsulting/mcp-mokogitea-api 1.2.0
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/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogitea/ISSUE_TEMPLATE/enterprise_support.md +85 -0
- package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/firewall-request.md +190 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
- package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogitea/auto-assign.yml +76 -0
- package/.mokogitea/auto-dev-issue.yml +207 -0
- package/.mokogitea/auto-release.yml +337 -0
- package/.mokogitea/branch-protection.yml +251 -0
- package/.mokogitea/changelog-validation.yml +101 -0
- package/.mokogitea/codeql-analysis.yml +115 -0
- package/.mokogitea/copilot-agent.yml +44 -0
- package/.mokogitea/deploy-demo.yml +734 -0
- package/.mokogitea/deploy-dev.yml +700 -0
- package/.mokogitea/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/manifest.xml +25 -0
- package/.mokogitea/mcp-auto-release.yml +278 -0
- package/.mokogitea/mcp-build-test.yml +65 -0
- package/.mokogitea/mcp-sdk-check.yml +109 -0
- package/.mokogitea/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/pr-branch-check.yml +90 -0
- package/.mokogitea/repository-cleanup.yml +525 -0
- package/.mokogitea/standards-compliance.yml +2614 -0
- package/.mokogitea/sync-version-on-merge.yml +133 -0
- package/.mokogitea/workflows/auto-assign.yml +76 -0
- package/.mokogitea/workflows/auto-bump.yml +66 -0
- package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
- package/.mokogitea/workflows/auto-release.yml +341 -0
- package/.mokogitea/workflows/branch-cleanup.yml +48 -0
- package/.mokogitea/workflows/cascade-dev.yml +10 -0
- package/.mokogitea/workflows/changelog-validation.yml +101 -0
- package/.mokogitea/workflows/ci-generic.yml +204 -0
- package/.mokogitea/workflows/cleanup.yml +87 -0
- package/.mokogitea/workflows/codeql-analysis.yml +115 -0
- package/.mokogitea/workflows/copilot-agent.yml +44 -0
- package/.mokogitea/workflows/deploy-manual.yml +126 -0
- package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/workflows/gitleaks.yml +96 -0
- package/.mokogitea/workflows/issue-branch.yml +73 -0
- package/.mokogitea/workflows/mcp-auto-release.yml +280 -0
- package/.mokogitea/workflows/mcp-build-test.yml +65 -0
- package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
- package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/workflows/notify.yml +70 -0
- package/.mokogitea/workflows/npm-publish.yml +51 -0
- package/.mokogitea/workflows/pr-check.yml +508 -0
- package/.mokogitea/workflows/pre-release.yml +11 -0
- package/.mokogitea/workflows/repo-health.yml +711 -0
- package/.mokogitea/workflows/repository-cleanup.yml +525 -0
- package/.mokogitea/workflows/security-audit.yml +82 -0
- package/.mokogitea/workflows/standards-compliance.yml +2614 -0
- package/.mokogitea/workflows/sync-version-on-merge.yml +130 -0
- package/.mokogitea/workflows/update-server.yml +312 -0
- package/CHANGELOG.md +145 -0
- package/CLAUDE.md +43 -0
- package/CONTRIBUTING.md +161 -0
- package/README.md +286 -0
- package/SECURITY.md +91 -0
- package/automation/ci-issue-reporter.sh +237 -0
- package/config.example.json +13 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +104 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +48 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1119 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +16 -0
- package/package.json +34 -0
- package/scripts/setup.mjs +40 -0
- package/src/client.ts +120 -0
- package/src/config.ts +58 -0
- package/src/index.ts +1712 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
*
|
|
4
|
+
* This file is part of a Moko Consulting project.
|
|
5
|
+
*
|
|
6
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
7
|
+
*
|
|
8
|
+
* FILE INFORMATION
|
|
9
|
+
* DEFGROUP: gitea-api-mcp.Server
|
|
10
|
+
* INGROUP: gitea-api-mcp
|
|
11
|
+
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
|
|
12
|
+
* PATH: /src/index.ts
|
|
13
|
+
* VERSION: 01.00.00
|
|
14
|
+
* BRIEF: MCP server entry point — registers all Gitea API tools
|
|
15
|
+
*/
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { loadConfig, getConnection } from './config.js';
|
|
20
|
+
import { GiteaClient } from './client.js';
|
|
21
|
+
let config;
|
|
22
|
+
function clientFor(connection) {
|
|
23
|
+
return new GiteaClient(getConnection(config, connection));
|
|
24
|
+
}
|
|
25
|
+
function formatResponse(res) {
|
|
26
|
+
if (res.status >= 400) {
|
|
27
|
+
const err = res.data;
|
|
28
|
+
const msg = err?.message ?? `HTTP ${res.status}: ${JSON.stringify(res.data, null, 2)}`;
|
|
29
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }] };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const ConnectionParam = {
|
|
36
|
+
connection: z.string().optional().describe('Named connection from config (uses default if omitted)'),
|
|
37
|
+
};
|
|
38
|
+
const PaginationParams = {
|
|
39
|
+
page: z.number().optional().describe('Page number (1-based)'),
|
|
40
|
+
limit: z.number().optional().describe('Items per page (max 50)'),
|
|
41
|
+
};
|
|
42
|
+
function pageQuery(params) {
|
|
43
|
+
const q = {};
|
|
44
|
+
if (params.page !== undefined)
|
|
45
|
+
q['page'] = String(params.page);
|
|
46
|
+
if (params.limit !== undefined)
|
|
47
|
+
q['limit'] = String(params.limit);
|
|
48
|
+
return q;
|
|
49
|
+
}
|
|
50
|
+
const OwnerRepo = {
|
|
51
|
+
owner: z.string().describe('Repository owner (user or org)'),
|
|
52
|
+
repo: z.string().describe('Repository name'),
|
|
53
|
+
};
|
|
54
|
+
const server = new McpServer({
|
|
55
|
+
name: 'mokogitea-api-mcp',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
});
|
|
58
|
+
// ── User / Auth ─────────────────────────────────────────────────────────
|
|
59
|
+
server.tool('gitea_me', 'Get the authenticated user info', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get('/user')));
|
|
60
|
+
server.tool('gitea_user_orgs', 'List organizations the authenticated user belongs to', { ...PaginationParams, ...ConnectionParam }, async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/user/orgs', pageQuery({ page, limit }))));
|
|
61
|
+
server.tool('gitea_user_repos', 'List repositories owned by the authenticated user', { ...PaginationParams, ...ConnectionParam }, async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/user/repos', pageQuery({ page, limit }))));
|
|
62
|
+
// ── Repository CRUD ─────────────────────────────────────────────────────
|
|
63
|
+
server.tool('gitea_repo_get', 'Get repository details', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}`)));
|
|
64
|
+
server.tool('gitea_repo_create', 'Create a new repository', {
|
|
65
|
+
name: z.string().describe('Repository name'),
|
|
66
|
+
org: z.string().optional().describe('Organization (omit for personal)'),
|
|
67
|
+
description: z.string().optional().describe('Description'),
|
|
68
|
+
private: z.boolean().optional().describe('Private repository'),
|
|
69
|
+
auto_init: z.boolean().optional().describe('Initialize with README'),
|
|
70
|
+
default_branch: z.string().optional().describe('Default branch (default "main")'),
|
|
71
|
+
template: z.boolean().optional().describe('Mark as template repository'),
|
|
72
|
+
...ConnectionParam,
|
|
73
|
+
}, async ({ name, org, description, private: priv, auto_init, default_branch, template: tmpl, connection }) => {
|
|
74
|
+
const client = clientFor(connection);
|
|
75
|
+
const body = { name };
|
|
76
|
+
if (description)
|
|
77
|
+
body.description = description;
|
|
78
|
+
if (priv !== undefined)
|
|
79
|
+
body.private = priv;
|
|
80
|
+
if (auto_init !== undefined)
|
|
81
|
+
body.auto_init = auto_init;
|
|
82
|
+
if (default_branch)
|
|
83
|
+
body.default_branch = default_branch;
|
|
84
|
+
if (tmpl !== undefined)
|
|
85
|
+
body.template = tmpl;
|
|
86
|
+
const endpoint = org ? `/orgs/${org}/repos` : '/user/repos';
|
|
87
|
+
return formatResponse(await client.post(endpoint, body));
|
|
88
|
+
});
|
|
89
|
+
server.tool('gitea_repo_delete', 'Delete a repository', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}`)));
|
|
90
|
+
server.tool('gitea_repo_edit', 'Edit repository settings', {
|
|
91
|
+
...OwnerRepo,
|
|
92
|
+
description: z.string().optional().describe('New description'),
|
|
93
|
+
private: z.boolean().optional().describe('Set private/public'),
|
|
94
|
+
has_issues: z.boolean().optional().describe('Enable issues'),
|
|
95
|
+
has_wiki: z.boolean().optional().describe('Enable wiki'),
|
|
96
|
+
has_pull_requests: z.boolean().optional().describe('Enable PRs'),
|
|
97
|
+
default_branch: z.string().optional().describe('Default branch'),
|
|
98
|
+
archived: z.boolean().optional().describe('Archive/unarchive'),
|
|
99
|
+
...ConnectionParam,
|
|
100
|
+
}, async ({ owner, repo, description, private: priv, has_issues, has_wiki, has_pull_requests, default_branch, archived, connection }) => {
|
|
101
|
+
const body = {};
|
|
102
|
+
if (description !== undefined)
|
|
103
|
+
body.description = description;
|
|
104
|
+
if (priv !== undefined)
|
|
105
|
+
body.private = priv;
|
|
106
|
+
if (has_issues !== undefined)
|
|
107
|
+
body.has_issues = has_issues;
|
|
108
|
+
if (has_wiki !== undefined)
|
|
109
|
+
body.has_wiki = has_wiki;
|
|
110
|
+
if (has_pull_requests !== undefined)
|
|
111
|
+
body.has_pull_requests = has_pull_requests;
|
|
112
|
+
if (default_branch !== undefined)
|
|
113
|
+
body.default_branch = default_branch;
|
|
114
|
+
if (archived !== undefined)
|
|
115
|
+
body.archived = archived;
|
|
116
|
+
return formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}`, body));
|
|
117
|
+
});
|
|
118
|
+
server.tool('gitea_repo_fork', 'Fork a repository', {
|
|
119
|
+
...OwnerRepo,
|
|
120
|
+
organization: z.string().optional().describe('Fork to this org (omit for personal)'),
|
|
121
|
+
name: z.string().optional().describe('Custom name for fork'),
|
|
122
|
+
...ConnectionParam,
|
|
123
|
+
}, async ({ owner, repo, organization, name, connection }) => {
|
|
124
|
+
const body = {};
|
|
125
|
+
if (organization)
|
|
126
|
+
body.organization = organization;
|
|
127
|
+
if (name)
|
|
128
|
+
body.name = name;
|
|
129
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/forks`, body));
|
|
130
|
+
});
|
|
131
|
+
server.tool('gitea_repo_generate', 'Create a new repository from a template repository', {
|
|
132
|
+
template_owner: z.string().describe('Owner of the template repository'),
|
|
133
|
+
template_repo: z.string().describe('Name of the template repository'),
|
|
134
|
+
owner: z.string().describe('Target owner (user or org) for the new repo'),
|
|
135
|
+
name: z.string().describe('Name for the new repository'),
|
|
136
|
+
description: z.string().optional().describe('Description for the new repo'),
|
|
137
|
+
private: z.boolean().optional().describe('Make the new repo private'),
|
|
138
|
+
git_content: z.boolean().optional().describe('Copy git content (commits, branches) from template (default true)'),
|
|
139
|
+
topics: z.boolean().optional().describe('Copy topics from template'),
|
|
140
|
+
git_hooks: z.boolean().optional().describe('Copy git hooks from template'),
|
|
141
|
+
webhooks: z.boolean().optional().describe('Copy webhooks from template'),
|
|
142
|
+
labels: z.boolean().optional().describe('Copy labels from template'),
|
|
143
|
+
default_branch: z.string().optional().describe('Default branch for new repo'),
|
|
144
|
+
...ConnectionParam,
|
|
145
|
+
}, async ({ template_owner, template_repo, owner, name, description, private: priv, git_content, topics, git_hooks, webhooks, labels, default_branch, connection }) => {
|
|
146
|
+
const body = { owner, name };
|
|
147
|
+
if (description)
|
|
148
|
+
body.description = description;
|
|
149
|
+
if (priv !== undefined)
|
|
150
|
+
body.private = priv;
|
|
151
|
+
if (git_content !== undefined)
|
|
152
|
+
body.git_content = git_content;
|
|
153
|
+
if (topics !== undefined)
|
|
154
|
+
body.topics = topics;
|
|
155
|
+
if (git_hooks !== undefined)
|
|
156
|
+
body.git_hooks = git_hooks;
|
|
157
|
+
if (webhooks !== undefined)
|
|
158
|
+
body.webhooks = webhooks;
|
|
159
|
+
if (labels !== undefined)
|
|
160
|
+
body.labels = labels;
|
|
161
|
+
if (default_branch)
|
|
162
|
+
body.default_branch = default_branch;
|
|
163
|
+
return formatResponse(await clientFor(connection).post(`/repos/${template_owner}/${template_repo}/generate`, body));
|
|
164
|
+
});
|
|
165
|
+
server.tool('gitea_repo_search', 'Search repositories', {
|
|
166
|
+
q: z.string().describe('Search query'),
|
|
167
|
+
topic: z.boolean().optional().describe('Search in topics'),
|
|
168
|
+
sort: z.enum(['alpha', 'created', 'updated', 'size', 'stars', 'forks']).optional().describe('Sort field'),
|
|
169
|
+
order: z.enum(['asc', 'desc']).optional().describe('Sort order'),
|
|
170
|
+
...PaginationParams,
|
|
171
|
+
...ConnectionParam,
|
|
172
|
+
}, async ({ q, topic, sort, order, page, limit, connection }) => {
|
|
173
|
+
const params = { q, ...pageQuery({ page, limit }) };
|
|
174
|
+
if (topic !== undefined)
|
|
175
|
+
params['topic'] = String(topic);
|
|
176
|
+
if (sort)
|
|
177
|
+
params['sort'] = sort;
|
|
178
|
+
if (order)
|
|
179
|
+
params['order'] = order;
|
|
180
|
+
return formatResponse(await clientFor(connection).get('/repos/search', params));
|
|
181
|
+
});
|
|
182
|
+
server.tool('gitea_org_repos', 'List repositories in an organization', {
|
|
183
|
+
org: z.string().describe('Organization name'),
|
|
184
|
+
...PaginationParams,
|
|
185
|
+
...ConnectionParam,
|
|
186
|
+
}, async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/repos`, pageQuery({ page, limit }))));
|
|
187
|
+
// ── File Contents ───────────────────────────────────────────────────────
|
|
188
|
+
server.tool('gitea_file_get', 'Get file contents from a repository', {
|
|
189
|
+
...OwnerRepo,
|
|
190
|
+
filepath: z.string().describe('File path (e.g. "src/index.ts")'),
|
|
191
|
+
ref: z.string().optional().describe('Branch/tag/commit (default: default branch)'),
|
|
192
|
+
...ConnectionParam,
|
|
193
|
+
}, async ({ owner, repo, filepath, ref, connection }) => {
|
|
194
|
+
const params = {};
|
|
195
|
+
if (ref)
|
|
196
|
+
params['ref'] = ref;
|
|
197
|
+
return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/contents/${filepath}`, params));
|
|
198
|
+
});
|
|
199
|
+
server.tool('gitea_dir_get', 'Get directory contents (file listing) from a repository', {
|
|
200
|
+
...OwnerRepo,
|
|
201
|
+
dirpath: z.string().optional().describe('Directory path (default: root)'),
|
|
202
|
+
ref: z.string().optional().describe('Branch/tag/commit'),
|
|
203
|
+
...ConnectionParam,
|
|
204
|
+
}, async ({ owner, repo, dirpath, ref, connection }) => {
|
|
205
|
+
const path = dirpath ? `/repos/${owner}/${repo}/contents/${dirpath}` : `/repos/${owner}/${repo}/contents`;
|
|
206
|
+
const params = {};
|
|
207
|
+
if (ref)
|
|
208
|
+
params['ref'] = ref;
|
|
209
|
+
return formatResponse(await clientFor(connection).get(path, params));
|
|
210
|
+
});
|
|
211
|
+
server.tool('gitea_file_create_or_update', 'Create or update a file in a repository', {
|
|
212
|
+
...OwnerRepo,
|
|
213
|
+
filepath: z.string().describe('File path'),
|
|
214
|
+
content: z.string().describe('File content (will be base64-encoded automatically)'),
|
|
215
|
+
message: z.string().describe('Commit message'),
|
|
216
|
+
branch: z.string().optional().describe('Branch (default: default branch)'),
|
|
217
|
+
sha: z.string().optional().describe('SHA of existing file (required for updates)'),
|
|
218
|
+
...ConnectionParam,
|
|
219
|
+
}, async ({ owner, repo, filepath, content, message, branch, sha, connection }) => {
|
|
220
|
+
const body = {
|
|
221
|
+
content: Buffer.from(content).toString('base64'),
|
|
222
|
+
message,
|
|
223
|
+
};
|
|
224
|
+
if (branch)
|
|
225
|
+
body.branch = branch;
|
|
226
|
+
if (sha)
|
|
227
|
+
body.sha = sha;
|
|
228
|
+
return formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/contents/${filepath}`, body));
|
|
229
|
+
});
|
|
230
|
+
server.tool('gitea_file_delete', 'Delete a file from a repository', {
|
|
231
|
+
...OwnerRepo,
|
|
232
|
+
filepath: z.string().describe('File path to delete'),
|
|
233
|
+
sha: z.string().describe('SHA of file to delete'),
|
|
234
|
+
message: z.string().describe('Commit message'),
|
|
235
|
+
branch: z.string().optional().describe('Branch'),
|
|
236
|
+
...ConnectionParam,
|
|
237
|
+
}, async ({ owner, repo, filepath, sha, message, branch, connection }) => {
|
|
238
|
+
const client = clientFor(connection);
|
|
239
|
+
const body = { sha, message };
|
|
240
|
+
if (branch)
|
|
241
|
+
body.branch = branch;
|
|
242
|
+
return formatResponse(await client.delete(`/repos/${owner}/${repo}/contents/${filepath}`, body));
|
|
243
|
+
});
|
|
244
|
+
server.tool('gitea_tree_get', 'Get the git tree for a repository (recursive file listing)', {
|
|
245
|
+
...OwnerRepo,
|
|
246
|
+
sha: z.string().describe('Tree SHA or branch name'),
|
|
247
|
+
recursive: z.boolean().optional().describe('Recursive listing (default true)'),
|
|
248
|
+
...ConnectionParam,
|
|
249
|
+
}, async ({ owner, repo, sha, recursive, connection }) => {
|
|
250
|
+
const params = {};
|
|
251
|
+
if (recursive !== false)
|
|
252
|
+
params['recursive'] = 'true';
|
|
253
|
+
return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/git/trees/${sha}`, params));
|
|
254
|
+
});
|
|
255
|
+
// ── Branches ────────────────────────────────────────────────────────────
|
|
256
|
+
server.tool('gitea_branches_list', 'List branches in a repository', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/branches`, pageQuery({ page, limit }))));
|
|
257
|
+
server.tool('gitea_branch_get', 'Get a specific branch', { ...OwnerRepo, branch: z.string().describe('Branch name'), ...ConnectionParam }, async ({ owner, repo, branch, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/branches/${branch}`)));
|
|
258
|
+
server.tool('gitea_branch_create', 'Create a new branch', {
|
|
259
|
+
...OwnerRepo,
|
|
260
|
+
new_branch_name: z.string().describe('Name for new branch'),
|
|
261
|
+
old_branch_name: z.string().optional().describe('Source branch (default: default branch)'),
|
|
262
|
+
...ConnectionParam,
|
|
263
|
+
}, async ({ owner, repo, new_branch_name, old_branch_name, connection }) => {
|
|
264
|
+
const body = { new_branch_name };
|
|
265
|
+
if (old_branch_name)
|
|
266
|
+
body.old_branch_name = old_branch_name;
|
|
267
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/branches`, body));
|
|
268
|
+
});
|
|
269
|
+
server.tool('gitea_branch_delete', 'Delete a branch', { ...OwnerRepo, branch: z.string().describe('Branch name'), ...ConnectionParam }, async ({ owner, repo, branch, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/branches/${branch}`)));
|
|
270
|
+
// ── Commits ─────────────────────────────────────────────────────────────
|
|
271
|
+
server.tool('gitea_commits_list', 'List commits in a repository', {
|
|
272
|
+
...OwnerRepo,
|
|
273
|
+
sha: z.string().optional().describe('Branch or commit SHA'),
|
|
274
|
+
path: z.string().optional().describe('Filter by file path'),
|
|
275
|
+
...PaginationParams,
|
|
276
|
+
...ConnectionParam,
|
|
277
|
+
}, async ({ owner, repo, sha, path, page, limit, connection }) => {
|
|
278
|
+
const params = { ...pageQuery({ page, limit }) };
|
|
279
|
+
if (sha)
|
|
280
|
+
params['sha'] = sha;
|
|
281
|
+
if (path)
|
|
282
|
+
params['path'] = path;
|
|
283
|
+
return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/commits`, params));
|
|
284
|
+
});
|
|
285
|
+
server.tool('gitea_commit_get', 'Get a specific commit', { ...OwnerRepo, sha: z.string().describe('Commit SHA'), ...ConnectionParam }, async ({ owner, repo, sha, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/git/commits/${sha}`)));
|
|
286
|
+
// ── Issues ──────────────────────────────────────────────────────────────
|
|
287
|
+
server.tool('gitea_issues_list', 'List issues in a repository', {
|
|
288
|
+
...OwnerRepo,
|
|
289
|
+
state: z.enum(['open', 'closed', 'all']).optional().describe('Issue state filter'),
|
|
290
|
+
type: z.enum(['issues', 'pulls']).optional().describe('Filter by type'),
|
|
291
|
+
labels: z.string().optional().describe('Comma-separated label names'),
|
|
292
|
+
milestones: z.string().optional().describe('Comma-separated milestone names'),
|
|
293
|
+
q: z.string().optional().describe('Search query'),
|
|
294
|
+
...PaginationParams,
|
|
295
|
+
...ConnectionParam,
|
|
296
|
+
}, async ({ owner, repo, state, type, labels, milestones, q, page, limit, connection }) => {
|
|
297
|
+
const params = { ...pageQuery({ page, limit }) };
|
|
298
|
+
if (state)
|
|
299
|
+
params['state'] = state;
|
|
300
|
+
if (type)
|
|
301
|
+
params['type'] = type;
|
|
302
|
+
if (labels)
|
|
303
|
+
params['labels'] = labels;
|
|
304
|
+
if (milestones)
|
|
305
|
+
params['milestones'] = milestones;
|
|
306
|
+
if (q)
|
|
307
|
+
params['q'] = q;
|
|
308
|
+
return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues`, params));
|
|
309
|
+
});
|
|
310
|
+
server.tool('gitea_issue_get', 'Get a single issue by number', { ...OwnerRepo, number: z.number().describe('Issue number'), ...ConnectionParam }, async ({ owner, repo, number, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues/${number}`)));
|
|
311
|
+
server.tool('gitea_issue_create', 'Create a new issue', {
|
|
312
|
+
...OwnerRepo,
|
|
313
|
+
title: z.string().describe('Issue title'),
|
|
314
|
+
body: z.string().optional().describe('Issue body (markdown)'),
|
|
315
|
+
labels: z.array(z.union([z.number(), z.string()])).optional().describe('Label IDs or names (use gitea_labels_list to discover available labels)'),
|
|
316
|
+
milestone: z.number().optional().describe('Milestone ID'),
|
|
317
|
+
assignees: z.array(z.string()).optional().describe('Usernames to assign'),
|
|
318
|
+
status_id: z.number().optional().describe('Issue status definition ID (use gitea_org_issue_statuses_list to discover)'),
|
|
319
|
+
priority_id: z.number().optional().describe('Issue priority definition ID (use gitea_org_issue_priorities_list to discover)'),
|
|
320
|
+
type_id: z.number().optional().describe('Issue type definition ID (use gitea_org_issue_types_list to discover)'),
|
|
321
|
+
...ConnectionParam,
|
|
322
|
+
}, async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, type_id, connection }) => {
|
|
323
|
+
const body = { title };
|
|
324
|
+
if (issueBody)
|
|
325
|
+
body.body = issueBody;
|
|
326
|
+
if (labels)
|
|
327
|
+
body.labels = labels;
|
|
328
|
+
if (milestone)
|
|
329
|
+
body.milestone = milestone;
|
|
330
|
+
if (assignees)
|
|
331
|
+
body.assignees = assignees;
|
|
332
|
+
if (status_id !== undefined)
|
|
333
|
+
body.status_id = status_id;
|
|
334
|
+
if (priority_id !== undefined)
|
|
335
|
+
body.priority_id = priority_id;
|
|
336
|
+
if (type_id !== undefined)
|
|
337
|
+
body.type_id = type_id;
|
|
338
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/issues`, body));
|
|
339
|
+
});
|
|
340
|
+
server.tool('gitea_issue_update', 'Update an issue (supports title, body, state, assignees, milestone, and org-level metadata)', {
|
|
341
|
+
...OwnerRepo,
|
|
342
|
+
number: z.number().describe('Issue number'),
|
|
343
|
+
title: z.string().optional().describe('New title'),
|
|
344
|
+
body: z.string().optional().describe('New body'),
|
|
345
|
+
state: z.enum(['open', 'closed']).optional().describe('State'),
|
|
346
|
+
assignees: z.array(z.string()).optional().describe('Assignees'),
|
|
347
|
+
milestone: z.number().optional().describe('Milestone ID'),
|
|
348
|
+
status_id: z.number().optional().describe('Issue status definition ID (use gitea_org_issue_statuses_list to discover; 0 to clear)'),
|
|
349
|
+
priority_id: z.number().optional().describe('Issue priority definition ID (use gitea_org_issue_priorities_list to discover; 0 to clear)'),
|
|
350
|
+
type_id: z.number().optional().describe('Issue type definition ID (use gitea_org_issue_types_list to discover; 0 to clear)'),
|
|
351
|
+
...ConnectionParam,
|
|
352
|
+
}, async ({ owner, repo, number, title, body: issueBody, state, assignees, milestone, status_id, priority_id, type_id, connection }) => {
|
|
353
|
+
const body = {};
|
|
354
|
+
if (title !== undefined)
|
|
355
|
+
body.title = title;
|
|
356
|
+
if (issueBody !== undefined)
|
|
357
|
+
body.body = issueBody;
|
|
358
|
+
if (state)
|
|
359
|
+
body.state = state;
|
|
360
|
+
if (assignees)
|
|
361
|
+
body.assignees = assignees;
|
|
362
|
+
if (milestone !== undefined)
|
|
363
|
+
body.milestone = milestone;
|
|
364
|
+
if (status_id !== undefined)
|
|
365
|
+
body.status_id = status_id;
|
|
366
|
+
if (priority_id !== undefined)
|
|
367
|
+
body.priority_id = priority_id;
|
|
368
|
+
if (type_id !== undefined)
|
|
369
|
+
body.type_id = type_id;
|
|
370
|
+
return formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}/issues/${number}`, body));
|
|
371
|
+
});
|
|
372
|
+
server.tool('gitea_issue_set_status', 'Set or clear the status on an issue (convenience wrapper around issue update)', {
|
|
373
|
+
...OwnerRepo,
|
|
374
|
+
number: z.number().describe('Issue number'),
|
|
375
|
+
status_id: z.number().describe('Status definition ID (0 to clear)'),
|
|
376
|
+
...ConnectionParam,
|
|
377
|
+
}, async ({ owner, repo, number, status_id, connection }) => {
|
|
378
|
+
return formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}/issues/${number}`, { status_id }));
|
|
379
|
+
});
|
|
380
|
+
server.tool('gitea_issue_set_priority', 'Set or clear the priority on an issue (convenience wrapper around issue update)', {
|
|
381
|
+
...OwnerRepo,
|
|
382
|
+
number: z.number().describe('Issue number'),
|
|
383
|
+
priority_id: z.number().describe('Priority definition ID (0 to clear)'),
|
|
384
|
+
...ConnectionParam,
|
|
385
|
+
}, async ({ owner, repo, number, priority_id, connection }) => {
|
|
386
|
+
return formatResponse(await clientFor(connection).patch(`/repos/${owner}/${repo}/issues/${number}`, { priority_id }));
|
|
387
|
+
});
|
|
388
|
+
server.tool('gitea_issue_comments_list', 'List comments on an issue', { ...OwnerRepo, number: z.number().describe('Issue number'), ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, number, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/issues/${number}/comments`, pageQuery({ page, limit }))));
|
|
389
|
+
server.tool('gitea_issue_comment_create', 'Add a comment to an issue', {
|
|
390
|
+
...OwnerRepo,
|
|
391
|
+
number: z.number().describe('Issue number'),
|
|
392
|
+
body: z.string().describe('Comment body (markdown)'),
|
|
393
|
+
...ConnectionParam,
|
|
394
|
+
}, async ({ owner, repo, number, body: commentBody, connection }) => {
|
|
395
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/issues/${number}/comments`, { body: commentBody }));
|
|
396
|
+
});
|
|
397
|
+
server.tool('gitea_issue_search', 'Search issues across all repositories', {
|
|
398
|
+
q: z.string().describe('Search query'),
|
|
399
|
+
state: z.enum(['open', 'closed', 'all']).optional().describe('State filter'),
|
|
400
|
+
labels: z.string().optional().describe('Comma-separated label IDs or names'),
|
|
401
|
+
type: z.enum(['issues', 'pulls']).optional().describe('Filter type'),
|
|
402
|
+
...PaginationParams,
|
|
403
|
+
...ConnectionParam,
|
|
404
|
+
}, async ({ q, state, labels, type, page, limit, connection }) => {
|
|
405
|
+
const params = { q, ...pageQuery({ page, limit }) };
|
|
406
|
+
if (state)
|
|
407
|
+
params['state'] = state;
|
|
408
|
+
if (labels)
|
|
409
|
+
params['labels'] = labels;
|
|
410
|
+
if (type)
|
|
411
|
+
params['type'] = type;
|
|
412
|
+
return formatResponse(await clientFor(connection).get('/repos/search', params));
|
|
413
|
+
});
|
|
414
|
+
// ── Labels ──────────────────────────────────────────────────────────────
|
|
415
|
+
server.tool('gitea_labels_list', 'List all labels in a repository (includes id, name, color, description — use to discover available type/priority/status labels)', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/labels`, pageQuery({ page, limit }))));
|
|
416
|
+
server.tool('gitea_label_create', 'Create a label', {
|
|
417
|
+
...OwnerRepo,
|
|
418
|
+
name: z.string().describe('Label name'),
|
|
419
|
+
color: z.string().describe('Color hex (e.g. "#d73a4a")'),
|
|
420
|
+
description: z.string().optional().describe('Label description'),
|
|
421
|
+
...ConnectionParam,
|
|
422
|
+
}, async ({ owner, repo, name, color, description, connection }) => {
|
|
423
|
+
const body = { name, color };
|
|
424
|
+
if (description)
|
|
425
|
+
body.description = description;
|
|
426
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/labels`, body));
|
|
427
|
+
});
|
|
428
|
+
// ── Milestones ──────────────────────────────────────────────────────────
|
|
429
|
+
server.tool('gitea_milestones_list', 'List milestones in a repository', {
|
|
430
|
+
...OwnerRepo,
|
|
431
|
+
state: z.enum(['open', 'closed', 'all']).optional().describe('State filter'),
|
|
432
|
+
...PaginationParams,
|
|
433
|
+
...ConnectionParam,
|
|
434
|
+
}, async ({ owner, repo, state, page, limit, connection }) => {
|
|
435
|
+
const params = { ...pageQuery({ page, limit }) };
|
|
436
|
+
if (state)
|
|
437
|
+
params['state'] = state;
|
|
438
|
+
return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/milestones`, params));
|
|
439
|
+
});
|
|
440
|
+
server.tool('gitea_milestone_create', 'Create a milestone', {
|
|
441
|
+
...OwnerRepo,
|
|
442
|
+
title: z.string().describe('Milestone title'),
|
|
443
|
+
description: z.string().optional().describe('Description'),
|
|
444
|
+
due_on: z.string().optional().describe('Due date (ISO 8601)'),
|
|
445
|
+
...ConnectionParam,
|
|
446
|
+
}, async ({ owner, repo, title, description, due_on, connection }) => {
|
|
447
|
+
const body = { title };
|
|
448
|
+
if (description)
|
|
449
|
+
body.description = description;
|
|
450
|
+
if (due_on)
|
|
451
|
+
body.due_on = due_on;
|
|
452
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/milestones`, body));
|
|
453
|
+
});
|
|
454
|
+
// ── Pull Requests ───────────────────────────────────────────────────────
|
|
455
|
+
server.tool('gitea_pulls_list', 'List pull requests', {
|
|
456
|
+
...OwnerRepo,
|
|
457
|
+
state: z.enum(['open', 'closed', 'all']).optional().describe('State filter'),
|
|
458
|
+
sort: z.enum(['oldest', 'recentupdate', 'leastupdate', 'mostcomment', 'leastcomment', 'priority']).optional(),
|
|
459
|
+
labels: z.string().optional().describe('Comma-separated label IDs'),
|
|
460
|
+
milestone: z.number().optional().describe('Milestone ID'),
|
|
461
|
+
...PaginationParams,
|
|
462
|
+
...ConnectionParam,
|
|
463
|
+
}, async ({ owner, repo, state, sort, labels, milestone, page, limit, connection }) => {
|
|
464
|
+
const params = { ...pageQuery({ page, limit }) };
|
|
465
|
+
if (state)
|
|
466
|
+
params['state'] = state;
|
|
467
|
+
if (sort)
|
|
468
|
+
params['sort'] = sort;
|
|
469
|
+
if (labels)
|
|
470
|
+
params['labels'] = labels;
|
|
471
|
+
if (milestone !== undefined)
|
|
472
|
+
params['milestone'] = String(milestone);
|
|
473
|
+
return formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/pulls`, params));
|
|
474
|
+
});
|
|
475
|
+
server.tool('gitea_pull_get', 'Get a single pull request', { ...OwnerRepo, number: z.number().describe('PR number'), ...ConnectionParam }, async ({ owner, repo, number, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/pulls/${number}`)));
|
|
476
|
+
server.tool('gitea_pull_create', 'Create a pull request', {
|
|
477
|
+
...OwnerRepo,
|
|
478
|
+
title: z.string().describe('PR title'),
|
|
479
|
+
head: z.string().describe('Source branch'),
|
|
480
|
+
base: z.string().describe('Target branch'),
|
|
481
|
+
body: z.string().optional().describe('PR description (markdown)'),
|
|
482
|
+
labels: z.array(z.union([z.number(), z.string()])).optional().describe('Label IDs or names (use gitea_labels_list to discover available labels)'),
|
|
483
|
+
milestone: z.number().optional().describe('Milestone ID'),
|
|
484
|
+
assignees: z.array(z.string()).optional().describe('Assignees'),
|
|
485
|
+
...ConnectionParam,
|
|
486
|
+
}, async ({ owner, repo, title, head, base, body: prBody, labels, milestone, assignees, connection }) => {
|
|
487
|
+
const body = { title, head, base };
|
|
488
|
+
if (prBody)
|
|
489
|
+
body.body = prBody;
|
|
490
|
+
if (labels)
|
|
491
|
+
body.labels = labels;
|
|
492
|
+
if (milestone)
|
|
493
|
+
body.milestone = milestone;
|
|
494
|
+
if (assignees)
|
|
495
|
+
body.assignees = assignees;
|
|
496
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/pulls`, body));
|
|
497
|
+
});
|
|
498
|
+
server.tool('gitea_pull_merge', 'Merge a pull request', {
|
|
499
|
+
...OwnerRepo,
|
|
500
|
+
number: z.number().describe('PR number'),
|
|
501
|
+
merge_type: z.enum(['merge', 'rebase', 'squash', 'rebase-merge']).optional().describe('Merge method (default: merge)'),
|
|
502
|
+
title: z.string().optional().describe('Custom merge commit title'),
|
|
503
|
+
message: z.string().optional().describe('Custom merge commit message'),
|
|
504
|
+
delete_branch_after_merge: z.boolean().optional().describe('Delete head branch after merge'),
|
|
505
|
+
...ConnectionParam,
|
|
506
|
+
}, async ({ owner, repo, number, merge_type, title, message, delete_branch_after_merge, connection }) => {
|
|
507
|
+
const body = { Do: merge_type ?? 'merge' };
|
|
508
|
+
if (title)
|
|
509
|
+
body.MergeTitleField = title;
|
|
510
|
+
if (message)
|
|
511
|
+
body.MergeMessageField = message;
|
|
512
|
+
if (delete_branch_after_merge !== undefined)
|
|
513
|
+
body.delete_branch_after_merge = delete_branch_after_merge;
|
|
514
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/pulls/${number}/merge`, body));
|
|
515
|
+
});
|
|
516
|
+
server.tool('gitea_pull_files', 'List files changed in a pull request', { ...OwnerRepo, number: z.number().describe('PR number'), ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, number, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/pulls/${number}/files`, pageQuery({ page, limit }))));
|
|
517
|
+
server.tool('gitea_pull_review_create', 'Create a pull request review', {
|
|
518
|
+
...OwnerRepo,
|
|
519
|
+
number: z.number().describe('PR number'),
|
|
520
|
+
event: z.enum(['APPROVED', 'REQUEST_CHANGES', 'COMMENT']).describe('Review action'),
|
|
521
|
+
body: z.string().optional().describe('Review comment'),
|
|
522
|
+
...ConnectionParam,
|
|
523
|
+
}, async ({ owner, repo, number, event, body: reviewBody, connection }) => {
|
|
524
|
+
const body = { event };
|
|
525
|
+
if (reviewBody)
|
|
526
|
+
body.body = reviewBody;
|
|
527
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/pulls/${number}/reviews`, body));
|
|
528
|
+
});
|
|
529
|
+
// ── Releases ────────────────────────────────────────────────────────────
|
|
530
|
+
server.tool('gitea_releases_list', 'List releases', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/releases`, pageQuery({ page, limit }))));
|
|
531
|
+
server.tool('gitea_release_get', 'Get a single release by ID', { ...OwnerRepo, id: z.number().describe('Release ID'), ...ConnectionParam }, async ({ owner, repo, id, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/releases/${id}`)));
|
|
532
|
+
server.tool('gitea_release_latest', 'Get the latest release', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/releases/latest`)));
|
|
533
|
+
server.tool('gitea_release_create', 'Create a new release', {
|
|
534
|
+
...OwnerRepo,
|
|
535
|
+
tag_name: z.string().describe('Tag name (e.g. "v1.0.0")'),
|
|
536
|
+
name: z.string().optional().describe('Release title'),
|
|
537
|
+
body: z.string().optional().describe('Release notes (markdown)'),
|
|
538
|
+
target_commitish: z.string().optional().describe('Target branch/commit'),
|
|
539
|
+
draft: z.boolean().optional().describe('Create as draft'),
|
|
540
|
+
prerelease: z.boolean().optional().describe('Mark as prerelease'),
|
|
541
|
+
...ConnectionParam,
|
|
542
|
+
}, async ({ owner, repo, tag_name, name, body: notes, target_commitish, draft, prerelease, connection }) => {
|
|
543
|
+
const body = { tag_name };
|
|
544
|
+
if (name)
|
|
545
|
+
body.name = name;
|
|
546
|
+
if (notes)
|
|
547
|
+
body.body = notes;
|
|
548
|
+
if (target_commitish)
|
|
549
|
+
body.target_commitish = target_commitish;
|
|
550
|
+
if (draft !== undefined)
|
|
551
|
+
body.draft = draft;
|
|
552
|
+
if (prerelease !== undefined)
|
|
553
|
+
body.prerelease = prerelease;
|
|
554
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/releases`, body));
|
|
555
|
+
});
|
|
556
|
+
server.tool('gitea_release_delete', 'Delete a release', { ...OwnerRepo, id: z.number().describe('Release ID'), ...ConnectionParam }, async ({ owner, repo, id, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/releases/${id}`)));
|
|
557
|
+
// ── Tags ────────────────────────────────────────────────────────────────
|
|
558
|
+
server.tool('gitea_tags_list', 'List tags', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/tags`, pageQuery({ page, limit }))));
|
|
559
|
+
server.tool('gitea_tag_create', 'Create a tag', {
|
|
560
|
+
...OwnerRepo,
|
|
561
|
+
tag_name: z.string().describe('Tag name'),
|
|
562
|
+
target: z.string().optional().describe('Target branch/commit SHA'),
|
|
563
|
+
message: z.string().optional().describe('Tag message (annotated tag)'),
|
|
564
|
+
...ConnectionParam,
|
|
565
|
+
}, async ({ owner, repo, tag_name, target, message, connection }) => {
|
|
566
|
+
const body = { tag_name };
|
|
567
|
+
if (target)
|
|
568
|
+
body.target = target;
|
|
569
|
+
if (message)
|
|
570
|
+
body.message = message;
|
|
571
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/tags`, body));
|
|
572
|
+
});
|
|
573
|
+
server.tool('gitea_tag_delete', 'Delete a tag', { ...OwnerRepo, tag: z.string().describe('Tag name'), ...ConnectionParam }, async ({ owner, repo, tag, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/tags/${tag}`)));
|
|
574
|
+
// ── Actions (CI/CD) ─────────────────────────────────────────────────────
|
|
575
|
+
server.tool('gitea_actions_runs_list', 'List workflow runs for a repository', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs`, pageQuery({ page, limit }))));
|
|
576
|
+
server.tool('gitea_actions_run_get', 'Get a specific workflow run', { ...OwnerRepo, run_id: z.number().describe('Run ID'), ...ConnectionParam }, async ({ owner, repo, run_id, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs/${run_id}`)));
|
|
577
|
+
server.tool('gitea_actions_dispatch', 'Trigger a workflow dispatch (e.g. pre-release, deploy)', {
|
|
578
|
+
...OwnerRepo,
|
|
579
|
+
workflow: z.string().describe('Workflow filename (e.g. pre-release.yml)'),
|
|
580
|
+
ref: z.string().describe('Branch or tag to run on (e.g. dev, main)'),
|
|
581
|
+
inputs: z.record(z.string()).optional().describe('Workflow input key-value pairs'),
|
|
582
|
+
...ConnectionParam,
|
|
583
|
+
}, async ({ owner, repo, workflow, ref, inputs, connection }) => formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/actions/workflows/${workflow}/dispatches`, { ref, inputs: inputs ?? {} })));
|
|
584
|
+
server.tool('gitea_actions_jobs_list', 'List jobs for a workflow run', { ...OwnerRepo, run_id: z.number().describe('Run ID'), ...ConnectionParam }, async ({ owner, repo, run_id, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/runs/${run_id}/jobs`)));
|
|
585
|
+
server.tool('gitea_actions_job_logs', 'Get log output for a workflow job', { ...OwnerRepo, job_id: z.number().describe('Job ID'), ...ConnectionParam }, async ({ owner, repo, job_id, connection }) => {
|
|
586
|
+
const client = clientFor(connection);
|
|
587
|
+
const res = await client.get(`/repos/${owner}/${repo}/actions/jobs/${job_id}/logs`);
|
|
588
|
+
if (res.status >= 400)
|
|
589
|
+
return formatResponse(res);
|
|
590
|
+
// Logs come as plain text
|
|
591
|
+
const text = typeof res.data === 'string' ? res.data : JSON.stringify(res.data);
|
|
592
|
+
return { content: [{ type: 'text', text }] };
|
|
593
|
+
});
|
|
594
|
+
server.tool('gitea_release_asset_upload', 'Upload a file as a release asset (provide base64-encoded content)', {
|
|
595
|
+
...OwnerRepo,
|
|
596
|
+
release_id: z.number().describe('Release ID'),
|
|
597
|
+
name: z.string().describe('Asset filename'),
|
|
598
|
+
content_base64: z.string().describe('Base64-encoded file content'),
|
|
599
|
+
...ConnectionParam,
|
|
600
|
+
}, async ({ owner, repo, release_id, name, content_base64, connection }) => {
|
|
601
|
+
const client = clientFor(connection);
|
|
602
|
+
// Gitea expects multipart form data for asset upload
|
|
603
|
+
// For now, use the API with the binary content
|
|
604
|
+
const res = await client.post(`/repos/${owner}/${repo}/releases/${release_id}/assets?name=${encodeURIComponent(name)}`, Buffer.from(content_base64, 'base64'));
|
|
605
|
+
return formatResponse(res);
|
|
606
|
+
});
|
|
607
|
+
server.tool('gitea_release_asset_delete', 'Delete a release asset', {
|
|
608
|
+
...OwnerRepo,
|
|
609
|
+
release_id: z.number().describe('Release ID'),
|
|
610
|
+
asset_id: z.number().describe('Asset ID'),
|
|
611
|
+
...ConnectionParam,
|
|
612
|
+
}, async ({ owner, repo, release_id, asset_id, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/releases/${release_id}/assets/${asset_id}`)));
|
|
613
|
+
server.tool('gitea_bulk_file_push', 'Push the same file content to multiple repos (uses Contents API)', {
|
|
614
|
+
owner: z.string().describe('Organization name'),
|
|
615
|
+
repos: z.array(z.string()).describe('List of repository names'),
|
|
616
|
+
path: z.string().describe('File path in each repo (e.g. .mokogitea/workflows/pre-release.yml)'),
|
|
617
|
+
content_base64: z.string().describe('Base64-encoded file content'),
|
|
618
|
+
message: z.string().describe('Commit message'),
|
|
619
|
+
branch: z.string().optional().describe('Target branch (default: main)'),
|
|
620
|
+
...ConnectionParam,
|
|
621
|
+
}, async ({ owner, repos, path, content_base64, message, branch, connection }) => {
|
|
622
|
+
const client = clientFor(connection);
|
|
623
|
+
const targetBranch = branch ?? 'main';
|
|
624
|
+
const results = [];
|
|
625
|
+
for (const repo of repos) {
|
|
626
|
+
try {
|
|
627
|
+
// Get current file SHA
|
|
628
|
+
const existing = await client.get(`/repos/${owner}/${repo}/contents/${path}?ref=${targetBranch}`);
|
|
629
|
+
const sha = existing.data?.sha;
|
|
630
|
+
if (sha) {
|
|
631
|
+
// Update existing file
|
|
632
|
+
await client.put(`/repos/${owner}/${repo}/contents/${path}`, {
|
|
633
|
+
content: content_base64,
|
|
634
|
+
sha,
|
|
635
|
+
message,
|
|
636
|
+
branch: targetBranch,
|
|
637
|
+
});
|
|
638
|
+
results.push({ repo, status: 'updated' });
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// Create new file
|
|
642
|
+
await client.post(`/repos/${owner}/${repo}/contents/${path}`, {
|
|
643
|
+
content: content_base64,
|
|
644
|
+
message,
|
|
645
|
+
branch: targetBranch,
|
|
646
|
+
});
|
|
647
|
+
results.push({ repo, status: 'created' });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
results.push({ repo, status: `error: ${e}` });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const summary = results.map(r => `${r.repo}: ${r.status}`).join('\n');
|
|
655
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
656
|
+
});
|
|
657
|
+
// ── Organizations ───────────────────────────────────────────────────────
|
|
658
|
+
server.tool('gitea_org_get', 'Get organization details', { org: z.string().describe('Organization name'), ...ConnectionParam }, async ({ org, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}`)));
|
|
659
|
+
server.tool('gitea_org_teams_list', 'List teams in an organization', { org: z.string().describe('Organization name'), ...PaginationParams, ...ConnectionParam }, async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/teams`, pageQuery({ page, limit }))));
|
|
660
|
+
server.tool('gitea_org_members_list', 'List members of an organization', { org: z.string().describe('Organization name'), ...PaginationParams, ...ConnectionParam }, async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/members`, pageQuery({ page, limit }))));
|
|
661
|
+
// ── Organization Issue Metadata ──────────────────────────────────────────
|
|
662
|
+
server.tool('gitea_org_issue_statuses_list', 'List issue status definitions for an organization (id, name, color, closes_issue — use to discover valid status_id values)', { org: z.string().describe('Organization name'), ...ConnectionParam }, async ({ org, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/issue-statuses`)));
|
|
663
|
+
server.tool('gitea_org_issue_priorities_list', 'List issue priority definitions for an organization (id, name, color, is_default — use to discover valid priority_id values)', { org: z.string().describe('Organization name'), ...ConnectionParam }, async ({ org, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/issue-priorities`)));
|
|
664
|
+
server.tool('gitea_org_issue_types_list', 'List issue type definitions for an organization (id, name, color, is_default — use to discover valid type_id values)', { org: z.string().describe('Organization name'), ...ConnectionParam }, async ({ org, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/issue-types`)));
|
|
665
|
+
// ── Users ───────────────────────────────────────────────────────────────
|
|
666
|
+
server.tool('gitea_user_get', 'Get a user profile', { username: z.string().describe('Username'), ...ConnectionParam }, async ({ username, connection }) => formatResponse(await clientFor(connection).get(`/users/${username}`)));
|
|
667
|
+
server.tool('gitea_users_search', 'Search users', {
|
|
668
|
+
q: z.string().describe('Search query'),
|
|
669
|
+
...PaginationParams,
|
|
670
|
+
...ConnectionParam,
|
|
671
|
+
}, async ({ q, page, limit, connection }) => formatResponse(await clientFor(connection).get('/users/search', { q, ...pageQuery({ page, limit }) })));
|
|
672
|
+
// ── Webhooks ────────────────────────────────────────────────────────────
|
|
673
|
+
server.tool('gitea_webhooks_list', 'List webhooks for a repository', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/hooks`, pageQuery({ page, limit }))));
|
|
674
|
+
server.tool('gitea_webhook_create', 'Create a webhook', {
|
|
675
|
+
...OwnerRepo,
|
|
676
|
+
type: z.enum(['gitea', 'slack', 'discord', 'dingtalk', 'telegram', 'msteams', 'feishu', 'matrix', 'wechatwork', 'packagist']).describe('Hook type'),
|
|
677
|
+
url: z.string().describe('Webhook URL'),
|
|
678
|
+
events: z.array(z.string()).optional().describe('Events to listen for (e.g. ["push", "pull_request"])'),
|
|
679
|
+
active: z.boolean().optional().describe('Active status'),
|
|
680
|
+
...ConnectionParam,
|
|
681
|
+
}, async ({ owner, repo, type, url, events, active, connection }) => {
|
|
682
|
+
const body = {
|
|
683
|
+
type,
|
|
684
|
+
config: { url, content_type: 'json' },
|
|
685
|
+
events: events ?? ['push'],
|
|
686
|
+
active: active ?? true,
|
|
687
|
+
};
|
|
688
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/hooks`, body));
|
|
689
|
+
});
|
|
690
|
+
// ── Wiki ────────────────────────────────────────────────────────────────
|
|
691
|
+
server.tool('gitea_wiki_pages_list', 'List wiki pages', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/wiki/pages`, pageQuery({ page, limit }))));
|
|
692
|
+
server.tool('gitea_wiki_page_get', 'Get a wiki page', { ...OwnerRepo, page_name: z.string().describe('Page name/slug'), ...ConnectionParam }, async ({ owner, repo, page_name, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/wiki/page/${page_name}`)));
|
|
693
|
+
// ── Notifications ───────────────────────────────────────────────────────
|
|
694
|
+
server.tool('gitea_notifications_list', 'List notifications for the authenticated user', {
|
|
695
|
+
status_types: z.string().optional().describe('Comma-separated: read, unread, pinned'),
|
|
696
|
+
...PaginationParams,
|
|
697
|
+
...ConnectionParam,
|
|
698
|
+
}, async ({ status_types, page, limit, connection }) => {
|
|
699
|
+
const params = { ...pageQuery({ page, limit }) };
|
|
700
|
+
if (status_types)
|
|
701
|
+
params['status-types'] = status_types;
|
|
702
|
+
return formatResponse(await clientFor(connection).get('/notifications', params));
|
|
703
|
+
});
|
|
704
|
+
server.tool('gitea_notifications_read', 'Mark all notifications as read', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).put('/notifications', {})));
|
|
705
|
+
// ── Topics ──────────────────────────────────────────────────────────────
|
|
706
|
+
server.tool('gitea_repo_topics', 'Get topics (tags) for a repository', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/topics`)));
|
|
707
|
+
server.tool('gitea_repo_topics_set', 'Set topics for a repository (replaces all existing)', {
|
|
708
|
+
...OwnerRepo,
|
|
709
|
+
topics: z.array(z.string()).describe('Array of topic names'),
|
|
710
|
+
...ConnectionParam,
|
|
711
|
+
}, async ({ owner, repo, topics, connection }) => formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/topics`, { topics })));
|
|
712
|
+
server.tool('gitea_topic_search', 'Search topics across all repositories', {
|
|
713
|
+
q: z.string().describe('Search query'),
|
|
714
|
+
...PaginationParams,
|
|
715
|
+
...ConnectionParam,
|
|
716
|
+
}, async ({ q, page, limit, connection }) => formatResponse(await clientFor(connection).get('/topics/search', { q, ...pageQuery({ page, limit }) })));
|
|
717
|
+
// ── Collaborators ───────────────────────────────────────────────────────
|
|
718
|
+
server.tool('gitea_collaborators_list', 'List collaborators on a repository', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/collaborators`, pageQuery({ page, limit }))));
|
|
719
|
+
server.tool('gitea_collaborator_add', 'Add a collaborator to a repository', {
|
|
720
|
+
...OwnerRepo,
|
|
721
|
+
collaborator: z.string().describe('Username to add'),
|
|
722
|
+
permission: z.enum(['read', 'write', 'admin']).optional().describe('Permission level (default: write)'),
|
|
723
|
+
...ConnectionParam,
|
|
724
|
+
}, async ({ owner, repo, collaborator, permission, connection }) => {
|
|
725
|
+
const body = {};
|
|
726
|
+
if (permission)
|
|
727
|
+
body.permission = permission;
|
|
728
|
+
return formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/collaborators/${collaborator}`, body));
|
|
729
|
+
});
|
|
730
|
+
server.tool('gitea_collaborator_remove', 'Remove a collaborator from a repository', {
|
|
731
|
+
...OwnerRepo,
|
|
732
|
+
collaborator: z.string().describe('Username to remove'),
|
|
733
|
+
...ConnectionParam,
|
|
734
|
+
}, async ({ owner, repo, collaborator, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/collaborators/${collaborator}`)));
|
|
735
|
+
// ── Deploy Keys ─────────────────────────────────────────────────────────
|
|
736
|
+
server.tool('gitea_deploy_keys_list', 'List deploy keys for a repository', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/keys`, pageQuery({ page, limit }))));
|
|
737
|
+
server.tool('gitea_deploy_key_create', 'Add a deploy key to a repository', {
|
|
738
|
+
...OwnerRepo,
|
|
739
|
+
title: z.string().describe('Key title'),
|
|
740
|
+
key: z.string().describe('SSH public key content'),
|
|
741
|
+
read_only: z.boolean().optional().describe('Read-only access (default: true)'),
|
|
742
|
+
...ConnectionParam,
|
|
743
|
+
}, async ({ owner, repo, title, key, read_only, connection }) => {
|
|
744
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/keys`, {
|
|
745
|
+
title,
|
|
746
|
+
key,
|
|
747
|
+
read_only: read_only ?? true,
|
|
748
|
+
}));
|
|
749
|
+
});
|
|
750
|
+
server.tool('gitea_deploy_key_delete', 'Remove a deploy key from a repository', {
|
|
751
|
+
...OwnerRepo,
|
|
752
|
+
id: z.number().describe('Deploy key ID'),
|
|
753
|
+
...ConnectionParam,
|
|
754
|
+
}, async ({ owner, repo, id, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/keys/${id}`)));
|
|
755
|
+
// ── Branch Protection ───────────────────────────────────────────────────
|
|
756
|
+
server.tool('gitea_branch_protections_list', 'List branch protection rules for a repository', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/branch_protections`)));
|
|
757
|
+
server.tool('gitea_branch_protection_create', 'Create a branch protection rule', {
|
|
758
|
+
...OwnerRepo,
|
|
759
|
+
branch_name: z.string().describe('Branch name or pattern (e.g. "main", "release/*")'),
|
|
760
|
+
enable_push: z.boolean().optional().describe('Allow push (default: true)'),
|
|
761
|
+
enable_push_whitelist: z.boolean().optional().describe('Enable push whitelist'),
|
|
762
|
+
push_whitelist_usernames: z.array(z.string()).optional().describe('Users allowed to push'),
|
|
763
|
+
require_signed_commits: z.boolean().optional().describe('Require signed commits'),
|
|
764
|
+
required_approvals: z.number().optional().describe('Required PR approvals (0 = none)'),
|
|
765
|
+
enable_status_check: z.boolean().optional().describe('Require status checks'),
|
|
766
|
+
status_check_contexts: z.array(z.string()).optional().describe('Required status check names'),
|
|
767
|
+
...ConnectionParam,
|
|
768
|
+
}, async ({ owner, repo, branch_name, enable_push, enable_push_whitelist, push_whitelist_usernames, require_signed_commits, required_approvals, enable_status_check, status_check_contexts, connection }) => {
|
|
769
|
+
const body = { branch_name };
|
|
770
|
+
if (enable_push !== undefined)
|
|
771
|
+
body.enable_push = enable_push;
|
|
772
|
+
if (enable_push_whitelist !== undefined)
|
|
773
|
+
body.enable_push_whitelist = enable_push_whitelist;
|
|
774
|
+
if (push_whitelist_usernames)
|
|
775
|
+
body.push_whitelist_usernames = push_whitelist_usernames;
|
|
776
|
+
if (require_signed_commits !== undefined)
|
|
777
|
+
body.require_signed_commits = require_signed_commits;
|
|
778
|
+
if (required_approvals !== undefined)
|
|
779
|
+
body.required_approvals = required_approvals;
|
|
780
|
+
if (enable_status_check !== undefined)
|
|
781
|
+
body.enable_status_check = enable_status_check;
|
|
782
|
+
if (status_check_contexts)
|
|
783
|
+
body.status_check_contexts = status_check_contexts;
|
|
784
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/branch_protections`, body));
|
|
785
|
+
});
|
|
786
|
+
server.tool('gitea_branch_protection_delete', 'Delete a branch protection rule', {
|
|
787
|
+
...OwnerRepo,
|
|
788
|
+
name: z.string().describe('Branch protection rule name'),
|
|
789
|
+
...ConnectionParam,
|
|
790
|
+
}, async ({ owner, repo, name, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/branch_protections/${name}`)));
|
|
791
|
+
// ── Organization Labels ─────────────────────────────────────────────────
|
|
792
|
+
server.tool('gitea_org_labels_list', 'List labels for an organization (shared across repos — includes id, name, color, description for type/priority/status discovery)', {
|
|
793
|
+
org: z.string().describe('Organization name'),
|
|
794
|
+
...PaginationParams,
|
|
795
|
+
...ConnectionParam,
|
|
796
|
+
}, async ({ org, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/orgs/${org}/labels`, pageQuery({ page, limit }))));
|
|
797
|
+
server.tool('gitea_org_label_create', 'Create an organization-level label', {
|
|
798
|
+
org: z.string().describe('Organization name'),
|
|
799
|
+
name: z.string().describe('Label name'),
|
|
800
|
+
color: z.string().describe('Color hex (e.g. "#d73a4a")'),
|
|
801
|
+
description: z.string().optional().describe('Label description'),
|
|
802
|
+
...ConnectionParam,
|
|
803
|
+
}, async ({ org, name, color, description, connection }) => {
|
|
804
|
+
const body = { name, color };
|
|
805
|
+
if (description)
|
|
806
|
+
body.description = description;
|
|
807
|
+
return formatResponse(await clientFor(connection).post(`/orgs/${org}/labels`, body));
|
|
808
|
+
});
|
|
809
|
+
// ── Repository Secrets (Actions) ────────────────────────────────────────
|
|
810
|
+
server.tool('gitea_repo_actions_secrets_list', 'List Actions secrets for a repository (names only, values hidden)', { ...OwnerRepo, ...PaginationParams, ...ConnectionParam }, async ({ owner, repo, page, limit, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/actions/secrets`, pageQuery({ page, limit }))));
|
|
811
|
+
server.tool('gitea_repo_actions_secret_create', 'Create or update an Actions secret', {
|
|
812
|
+
...OwnerRepo,
|
|
813
|
+
name: z.string().describe('Secret name'),
|
|
814
|
+
data: z.string().describe('Secret value'),
|
|
815
|
+
...ConnectionParam,
|
|
816
|
+
}, async ({ owner, repo, name, data, connection }) => formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/actions/secrets/${name}`, { data })));
|
|
817
|
+
server.tool('gitea_repo_actions_secret_delete', 'Delete an Actions secret', {
|
|
818
|
+
...OwnerRepo,
|
|
819
|
+
name: z.string().describe('Secret name'),
|
|
820
|
+
...ConnectionParam,
|
|
821
|
+
}, async ({ owner, repo, name, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/actions/secrets/${name}`)));
|
|
822
|
+
// ── Repo Transfer / Mirror ──────────────────────────────────────────────
|
|
823
|
+
server.tool('gitea_repo_mirror_sync', 'Trigger a push mirror sync for a repository', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/mirror-sync`, {})));
|
|
824
|
+
server.tool('gitea_repo_mirrors_list', 'List push mirrors for a repository', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/push_mirrors`)));
|
|
825
|
+
server.tool('gitea_repo_mirror_create', 'Create a push mirror to GitHub (backup). Sets up automatic push mirroring from Gitea to a GitHub repo.', {
|
|
826
|
+
...OwnerRepo,
|
|
827
|
+
github_owner: z.string().optional().describe('GitHub org or user (defaults to config github.org)'),
|
|
828
|
+
github_repo: z.string().optional().describe('GitHub repo name (defaults to same as Gitea repo)'),
|
|
829
|
+
github_token: z.string().optional().describe('GitHub token (defaults to config github.token)'),
|
|
830
|
+
interval: z.string().optional().describe('Sync interval (e.g. "8h0m0s", default "8h0m0s")'),
|
|
831
|
+
...ConnectionParam,
|
|
832
|
+
}, async ({ owner, repo, github_owner, github_repo, github_token, interval, connection }) => {
|
|
833
|
+
const ghToken = github_token ?? config.github?.token;
|
|
834
|
+
const ghOwner = github_owner ?? config.github?.org;
|
|
835
|
+
if (!ghToken)
|
|
836
|
+
return { content: [{ type: 'text', text: 'Error: No GitHub token. Pass github_token or set github.token in config.' }] };
|
|
837
|
+
if (!ghOwner)
|
|
838
|
+
return { content: [{ type: 'text', text: 'Error: No GitHub owner. Pass github_owner or set github.org in config.' }] };
|
|
839
|
+
const ghRepo = github_repo ?? repo;
|
|
840
|
+
return formatResponse(await clientFor(connection).post(`/repos/${owner}/${repo}/push_mirrors`, {
|
|
841
|
+
remote_address: `https://github.com/${ghOwner}/${ghRepo}.git`,
|
|
842
|
+
remote_username: ghOwner,
|
|
843
|
+
remote_password: ghToken,
|
|
844
|
+
interval: interval ?? '8h0m0s',
|
|
845
|
+
sync_on_commit: true,
|
|
846
|
+
}));
|
|
847
|
+
});
|
|
848
|
+
server.tool('gitea_repo_mirror_delete', 'Delete a push mirror from a repository', {
|
|
849
|
+
...OwnerRepo,
|
|
850
|
+
mirror_name: z.string().describe('Push mirror remote name (from mirrors_list)'),
|
|
851
|
+
...ConnectionParam,
|
|
852
|
+
}, async ({ owner, repo, mirror_name, connection }) => formatResponse(await clientFor(connection).delete(`/repos/${owner}/${repo}/push_mirrors/${mirror_name}`)));
|
|
853
|
+
server.tool('gitea_repo_mirror_setup_github_backup', 'One-step GitHub backup mirror setup: creates the GitHub repo (if needed) and configures push mirror. Requires a GitHub token with repo+org scope.', {
|
|
854
|
+
...OwnerRepo,
|
|
855
|
+
github_org: z.string().optional().describe('GitHub org (defaults to config github.org)'),
|
|
856
|
+
github_token: z.string().optional().describe('GitHub token (defaults to config github.token)'),
|
|
857
|
+
private: z.boolean().optional().describe('Make GitHub repo private (default true)'),
|
|
858
|
+
description: z.string().optional().describe('GitHub repo description'),
|
|
859
|
+
interval: z.string().optional().describe('Mirror sync interval (default "8h0m0s")'),
|
|
860
|
+
...ConnectionParam,
|
|
861
|
+
}, async ({ owner, repo, github_org, github_token, private: isPrivate, description, interval, connection }) => {
|
|
862
|
+
const ghToken = github_token ?? config.github?.token;
|
|
863
|
+
const ghOrg = github_org ?? config.github?.org;
|
|
864
|
+
if (!ghToken)
|
|
865
|
+
return { content: [{ type: 'text', text: 'Error: No GitHub token. Pass github_token or set github.token in config.' }] };
|
|
866
|
+
if (!ghOrg)
|
|
867
|
+
return { content: [{ type: 'text', text: 'Error: No GitHub org. Pass github_org or set github.org in config.' }] };
|
|
868
|
+
const client = clientFor(connection);
|
|
869
|
+
const results = [];
|
|
870
|
+
// 1. Try to create the GitHub repo via GitHub API
|
|
871
|
+
const ghRes = await fetch(`https://api.github.com/orgs/${ghOrg}/repos`, {
|
|
872
|
+
method: 'POST',
|
|
873
|
+
headers: {
|
|
874
|
+
'Authorization': `Bearer ${ghToken}`,
|
|
875
|
+
'Accept': 'application/vnd.github+json',
|
|
876
|
+
'Content-Type': 'application/json',
|
|
877
|
+
},
|
|
878
|
+
body: JSON.stringify({
|
|
879
|
+
name: repo,
|
|
880
|
+
private: isPrivate ?? true,
|
|
881
|
+
description: description ?? `Backup mirror of ${owner}/${repo} from MokoGitea`,
|
|
882
|
+
auto_init: false,
|
|
883
|
+
has_issues: false,
|
|
884
|
+
has_wiki: false,
|
|
885
|
+
has_projects: false,
|
|
886
|
+
}),
|
|
887
|
+
});
|
|
888
|
+
const ghData = await ghRes.json();
|
|
889
|
+
if (ghRes.status === 201) {
|
|
890
|
+
results.push(`GitHub repo created: ${ghOrg}/${repo}`);
|
|
891
|
+
}
|
|
892
|
+
else if (ghRes.status === 422) {
|
|
893
|
+
results.push(`GitHub repo already exists: ${ghOrg}/${repo}`);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
return { content: [{ type: 'text', text: `GitHub repo creation failed (${ghRes.status}): ${JSON.stringify(ghData)}` }] };
|
|
897
|
+
}
|
|
898
|
+
// 2. Create push mirror on Gitea
|
|
899
|
+
const mirrorRes = await client.post(`/repos/${owner}/${repo}/push_mirrors`, {
|
|
900
|
+
remote_address: `https://github.com/${ghOrg}/${repo}.git`,
|
|
901
|
+
remote_username: ghOrg,
|
|
902
|
+
remote_password: ghToken,
|
|
903
|
+
interval: interval ?? '8h0m0s',
|
|
904
|
+
sync_on_commit: true,
|
|
905
|
+
});
|
|
906
|
+
if (mirrorRes.status >= 400) {
|
|
907
|
+
const err = mirrorRes.data;
|
|
908
|
+
results.push(`Push mirror failed: ${err?.message ?? mirrorRes.status}`);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
results.push(`Push mirror configured: sync every ${interval ?? '8h0m0s'}, sync on commit`);
|
|
912
|
+
}
|
|
913
|
+
// 3. Trigger initial sync
|
|
914
|
+
await client.post(`/repos/${owner}/${repo}/mirror-sync`, {});
|
|
915
|
+
results.push('Initial sync triggered');
|
|
916
|
+
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
917
|
+
});
|
|
918
|
+
server.tool('gitea_repo_mirror_setup_github_backup_full', 'Full GitHub backup: mirrors code repo + wiki repo to GitHub. Creates GitHub repo, sets up push mirrors for both code and wiki, triggers initial sync.', {
|
|
919
|
+
...OwnerRepo,
|
|
920
|
+
github_org: z.string().optional().describe('GitHub org (defaults to config github.org)'),
|
|
921
|
+
github_token: z.string().optional().describe('GitHub token (defaults to config github.token)'),
|
|
922
|
+
private: z.boolean().optional().describe('Make GitHub repo private (default true)'),
|
|
923
|
+
description: z.string().optional().describe('GitHub repo description'),
|
|
924
|
+
interval: z.string().optional().describe('Mirror sync interval (default "8h0m0s")'),
|
|
925
|
+
...ConnectionParam,
|
|
926
|
+
}, async ({ owner, repo, github_org, github_token, private: isPrivate, description, interval, connection }) => {
|
|
927
|
+
const ghToken = github_token ?? config.github?.token;
|
|
928
|
+
const ghOrg = github_org ?? config.github?.org;
|
|
929
|
+
if (!ghToken)
|
|
930
|
+
return { content: [{ type: 'text', text: 'Error: No GitHub token. Pass github_token or set github.token in config.' }] };
|
|
931
|
+
if (!ghOrg)
|
|
932
|
+
return { content: [{ type: 'text', text: 'Error: No GitHub org. Pass github_org or set github.org in config.' }] };
|
|
933
|
+
const client = clientFor(connection);
|
|
934
|
+
const syncInterval = interval ?? '8h0m0s';
|
|
935
|
+
const results = [];
|
|
936
|
+
// 1. Create GitHub repo (if needed)
|
|
937
|
+
const ghRes = await fetch(`https://api.github.com/orgs/${ghOrg}/repos`, {
|
|
938
|
+
method: 'POST',
|
|
939
|
+
headers: {
|
|
940
|
+
'Authorization': `Bearer ${ghToken}`,
|
|
941
|
+
'Accept': 'application/vnd.github+json',
|
|
942
|
+
'Content-Type': 'application/json',
|
|
943
|
+
},
|
|
944
|
+
body: JSON.stringify({
|
|
945
|
+
name: repo,
|
|
946
|
+
private: isPrivate ?? true,
|
|
947
|
+
description: description ?? `Backup mirror of ${owner}/${repo} from MokoGitea`,
|
|
948
|
+
auto_init: false,
|
|
949
|
+
has_issues: false,
|
|
950
|
+
has_wiki: true,
|
|
951
|
+
has_projects: false,
|
|
952
|
+
}),
|
|
953
|
+
});
|
|
954
|
+
const ghData = await ghRes.json();
|
|
955
|
+
if (ghRes.status === 201) {
|
|
956
|
+
results.push(`GitHub repo created: ${ghOrg}/${repo}`);
|
|
957
|
+
}
|
|
958
|
+
else if (ghRes.status === 422) {
|
|
959
|
+
results.push(`GitHub repo already exists: ${ghOrg}/${repo}`);
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
return { content: [{ type: 'text', text: `GitHub repo creation failed (${ghRes.status}): ${JSON.stringify(ghData)}` }] };
|
|
963
|
+
}
|
|
964
|
+
// 2. Enable wiki on GitHub repo (in case it was disabled)
|
|
965
|
+
await fetch(`https://api.github.com/repos/${ghOrg}/${repo}`, {
|
|
966
|
+
method: 'PATCH',
|
|
967
|
+
headers: {
|
|
968
|
+
'Authorization': `Bearer ${ghToken}`,
|
|
969
|
+
'Accept': 'application/vnd.github+json',
|
|
970
|
+
'Content-Type': 'application/json',
|
|
971
|
+
},
|
|
972
|
+
body: JSON.stringify({ has_wiki: true }),
|
|
973
|
+
});
|
|
974
|
+
// 3. Code push mirror
|
|
975
|
+
const codeRes = await client.post(`/repos/${owner}/${repo}/push_mirrors`, {
|
|
976
|
+
remote_address: `https://github.com/${ghOrg}/${repo}.git`,
|
|
977
|
+
remote_username: ghOrg,
|
|
978
|
+
remote_password: ghToken,
|
|
979
|
+
interval: syncInterval,
|
|
980
|
+
sync_on_commit: true,
|
|
981
|
+
});
|
|
982
|
+
if (codeRes.status >= 400) {
|
|
983
|
+
const err = codeRes.data;
|
|
984
|
+
results.push(`Code mirror: ${err?.message ?? `failed (${codeRes.status})`}`);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
results.push(`Code mirror configured: sync every ${syncInterval}, sync on commit`);
|
|
988
|
+
}
|
|
989
|
+
// 4. Wiki push mirror — Gitea wiki repos are at {repo}.wiki
|
|
990
|
+
// Check if wiki exists first
|
|
991
|
+
const wikiCheck = await client.get(`/repos/${owner}/${repo}/wiki/pages`);
|
|
992
|
+
if (wikiCheck.status < 400) {
|
|
993
|
+
const wikiRes = await client.post(`/repos/${owner}/${repo}/push_mirrors`, {
|
|
994
|
+
remote_address: `https://github.com/${ghOrg}/${repo}.wiki.git`,
|
|
995
|
+
remote_username: ghOrg,
|
|
996
|
+
remote_password: ghToken,
|
|
997
|
+
interval: syncInterval,
|
|
998
|
+
sync_on_commit: true,
|
|
999
|
+
});
|
|
1000
|
+
if (wikiRes.status >= 400) {
|
|
1001
|
+
const err = wikiRes.data;
|
|
1002
|
+
results.push(`Wiki mirror: ${err?.message ?? `failed (${wikiRes.status})`}`);
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
results.push(`Wiki mirror configured: ${ghOrg}/${repo}.wiki.git`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
results.push('Wiki mirror skipped: no wiki pages found');
|
|
1010
|
+
}
|
|
1011
|
+
// 5. Trigger initial sync
|
|
1012
|
+
await client.post(`/repos/${owner}/${repo}/mirror-sync`, {});
|
|
1013
|
+
results.push('Initial sync triggered');
|
|
1014
|
+
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
1015
|
+
});
|
|
1016
|
+
// ── Repo Statistics ─────────────────────────────────────────────────────
|
|
1017
|
+
server.tool('gitea_repo_languages', 'Get language breakdown for a repository', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/languages`)));
|
|
1018
|
+
server.tool('gitea_repo_contributors', 'List contributors with commit counts', { ...OwnerRepo, ...ConnectionParam }, async ({ owner, repo, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/contributors`)));
|
|
1019
|
+
// ── Issue Labels (Bulk) ─────────────────────────────────────────────────
|
|
1020
|
+
server.tool('gitea_issue_labels_set', 'Set labels on an issue (replaces existing)', {
|
|
1021
|
+
...OwnerRepo,
|
|
1022
|
+
number: z.number().describe('Issue/PR number'),
|
|
1023
|
+
labels: z.array(z.union([z.number(), z.string()])).describe('Label IDs or names to set (use gitea_labels_list to discover available labels)'),
|
|
1024
|
+
...ConnectionParam,
|
|
1025
|
+
}, async ({ owner, repo, number, labels, connection }) => formatResponse(await clientFor(connection).put(`/repos/${owner}/${repo}/issues/${number}/labels`, { labels })));
|
|
1026
|
+
// ── Diff / Compare ──────────────────────────────────────────────────────
|
|
1027
|
+
server.tool('gitea_compare', 'Compare two branches, tags, or commits (returns diff stats)', {
|
|
1028
|
+
...OwnerRepo,
|
|
1029
|
+
base: z.string().describe('Base branch/tag/SHA'),
|
|
1030
|
+
head: z.string().describe('Head branch/tag/SHA'),
|
|
1031
|
+
...ConnectionParam,
|
|
1032
|
+
}, async ({ owner, repo, base, head, connection }) => formatResponse(await clientFor(connection).get(`/repos/${owner}/${repo}/compare/${base}...${head}`)));
|
|
1033
|
+
// ── Gitea Admin (Instance-Level) ────────────────────────────────────────
|
|
1034
|
+
server.tool('gitea_admin_orgs_list', 'List all organizations (admin only)', { ...PaginationParams, ...ConnectionParam }, async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/admin/orgs', pageQuery({ page, limit }))));
|
|
1035
|
+
server.tool('gitea_admin_users_list', 'List all users (admin only)', { ...PaginationParams, ...ConnectionParam }, async ({ page, limit, connection }) => formatResponse(await clientFor(connection).get('/admin/users', pageQuery({ page, limit }))));
|
|
1036
|
+
server.tool('gitea_admin_cron_list', 'List cron tasks and their last run time (admin only)', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get('/admin/cron')));
|
|
1037
|
+
server.tool('gitea_admin_cron_run', 'Trigger a cron task (admin only)', {
|
|
1038
|
+
task: z.string().describe('Cron task name (e.g. "repo_health_check", "resync_all_hooks", "repo_archive_cleanup")'),
|
|
1039
|
+
...ConnectionParam,
|
|
1040
|
+
}, async ({ task, connection }) => formatResponse(await clientFor(connection).post(`/admin/cron/${task}`, {})));
|
|
1041
|
+
// ── Generic API Call ────────────────────────────────────────────────────
|
|
1042
|
+
server.tool('gitea_api_request', 'Make a raw API request to any Gitea v1 endpoint', {
|
|
1043
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),
|
|
1044
|
+
endpoint: z.string().describe('API endpoint path (e.g. "/repos/owner/repo")'),
|
|
1045
|
+
body: z.record(z.string(), z.unknown()).optional().describe('Request body'),
|
|
1046
|
+
params: z.record(z.string(), z.string()).optional().describe('Query parameters'),
|
|
1047
|
+
...ConnectionParam,
|
|
1048
|
+
}, async ({ method, endpoint, body, params, connection }) => {
|
|
1049
|
+
const client = clientFor(connection);
|
|
1050
|
+
switch (method) {
|
|
1051
|
+
case 'GET': return formatResponse(await client.get(endpoint, params));
|
|
1052
|
+
case 'POST': return formatResponse(await client.post(endpoint, body));
|
|
1053
|
+
case 'PUT': return formatResponse(await client.put(endpoint, body));
|
|
1054
|
+
case 'PATCH': return formatResponse(await client.patch(endpoint, body));
|
|
1055
|
+
case 'DELETE': return formatResponse(await client.delete(endpoint, body));
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
// ── Connections ──────────────────────────────────────────────────────────
|
|
1059
|
+
server.tool('gitea_list_connections', 'List configured Gitea connections', {}, async () => {
|
|
1060
|
+
const lines = Object.entries(config.connections).map(([name, conn]) => {
|
|
1061
|
+
const is_default = name === config.defaultConnection ? ' (default)' : '';
|
|
1062
|
+
return ` ${name}${is_default}: ${conn.baseUrl}`;
|
|
1063
|
+
});
|
|
1064
|
+
return { content: [{ type: 'text', text: `Configured connections:\n${lines.join('\n')}` }] };
|
|
1065
|
+
});
|
|
1066
|
+
// ── Manifest ────────────────────────────────────────────────────────────
|
|
1067
|
+
server.tool('gitea_manifest_get', 'Get manifest settings for a repository (identity, governance, distribution, build metadata)', {
|
|
1068
|
+
owner: z.string().describe('Repository owner'),
|
|
1069
|
+
repo: z.string().describe('Repository name'),
|
|
1070
|
+
...ConnectionParam,
|
|
1071
|
+
}, async ({ owner, repo, connection }) => {
|
|
1072
|
+
const c = clientFor(connection);
|
|
1073
|
+
return formatResponse(await c.get(`/repos/${owner}/${repo}/manifest`));
|
|
1074
|
+
});
|
|
1075
|
+
server.tool('gitea_manifest_update', 'Update manifest settings for a repository', {
|
|
1076
|
+
owner: z.string().describe('Repository owner'),
|
|
1077
|
+
repo: z.string().describe('Repository name'),
|
|
1078
|
+
name: z.string().optional().describe('Project name (Joomla: base element name, e.g. mokowaas)'),
|
|
1079
|
+
org: z.string().optional().describe('Organization'),
|
|
1080
|
+
description: z.string().optional().describe('Project description'),
|
|
1081
|
+
version: z.string().optional().describe('Version string (e.g. 06.00.00)'),
|
|
1082
|
+
version_prefix: z.string().optional().describe('Tag prefix for version display (e.g. v1.26.1-moko.)'),
|
|
1083
|
+
element_name: z.string().optional().describe('Full element name override (e.g. pkg_mokowaas). Auto-constructed from type + name if empty.'),
|
|
1084
|
+
license_spdx: z.string().optional().describe('SPDX license identifier'),
|
|
1085
|
+
license_name: z.string().optional().describe('Human-readable license name'),
|
|
1086
|
+
platform: z.string().optional().describe('Platform (joomla, wordpress, dolibarr, go, mcp, platform, generic)'),
|
|
1087
|
+
standards_version: z.string().optional().describe('MokoPlatform standards version'),
|
|
1088
|
+
standards_source: z.string().optional().describe('URL to standards repo'),
|
|
1089
|
+
display_name: z.string().optional().describe('Human-readable name for update feeds (e.g. Package - MokoWaaS)'),
|
|
1090
|
+
maintainer: z.string().optional().describe('Maintainer/author name'),
|
|
1091
|
+
maintainer_url: z.string().optional().describe('Maintainer website URL'),
|
|
1092
|
+
info_url: z.string().optional().describe('Extension info/product page URL'),
|
|
1093
|
+
target_version: z.string().optional().describe('Target platform version regex (e.g. (5|6)\\.*)'),
|
|
1094
|
+
php_minimum: z.string().optional().describe('Minimum PHP version (e.g. 8.1)'),
|
|
1095
|
+
language: z.string().optional().describe('Primary language (Go, PHP, TypeScript, etc.)'),
|
|
1096
|
+
package_type: z.string().optional().describe('Package type (application, library, component, module, plugin, package, template, file)'),
|
|
1097
|
+
entry_point: z.string().optional().describe('Build entry point path'),
|
|
1098
|
+
...ConnectionParam,
|
|
1099
|
+
}, async ({ owner, repo, connection, ...fields }) => {
|
|
1100
|
+
const c = clientFor(connection);
|
|
1101
|
+
const current = await c.get(`/repos/${owner}/${repo}/manifest`);
|
|
1102
|
+
const merged = { ...current.data };
|
|
1103
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1104
|
+
if (v !== undefined)
|
|
1105
|
+
merged[k] = v;
|
|
1106
|
+
}
|
|
1107
|
+
return formatResponse(await c.put(`/repos/${owner}/${repo}/manifest`, merged));
|
|
1108
|
+
});
|
|
1109
|
+
// ── Start Server ────────────────────────────────────────────────────────
|
|
1110
|
+
async function main() {
|
|
1111
|
+
config = await loadConfig();
|
|
1112
|
+
const transport = new StdioServerTransport();
|
|
1113
|
+
await server.connect(transport);
|
|
1114
|
+
}
|
|
1115
|
+
main().catch((err) => {
|
|
1116
|
+
process.stderr.write(`Fatal: ${err}\n`);
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
});
|
|
1119
|
+
//# sourceMappingURL=index.js.map
|