feat: initial Gitea MCP server with 30 tools

Covers repos, commits, branches, tags, releases, PRs, issues, comments,
labels, milestones, notifications, orgs, and CI status. Stdio transport,
token auth, same architecture as OpenProject MCP server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 15:56:28 +08:00
commit 06480d5503
10 changed files with 2463 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
GITEA_URL=https://gitea.kollect.biz
GITEA_TOKEN=your-api-token-here
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.env
.mcp.json
+12
View File
@@ -0,0 +1,12 @@
{
"mcpServers": {
"gitea": {
"command": "node",
"args": ["mcp/gitea/dist/index.js"],
"env": {
"GITEA_URL": "https://gitea.kollect.biz",
"GITEA_TOKEN": "your-api-token-here"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@kollect/gitea-mcp",
"version": "0.1.0",
"description": "MCP server for Gitea integration",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}
+1106
View File
File diff suppressed because it is too large Load Diff
+408
View File
@@ -0,0 +1,408 @@
/**
* Gitea API v1 HTTP client with token authentication.
*/
export interface GiteaConfig {
baseUrl: string;
token: string;
}
export class GiteaClient {
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: GiteaConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "") + "/api/v1";
this.headers = {
Authorization: `token ${config.token}`,
"Content-Type": "application/json",
};
}
async request<T>(
method: string,
path: string,
body?: unknown,
params?: Record<string, string>,
): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
}
const response = await fetch(url.toString(), {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`Gitea API ${method} ${path} failed (${response.status}): ${errorBody}`,
);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
// --- User ---
async getMe(): Promise<any> {
return this.request("GET", "/user");
}
async listMyRepos(page = 1, limit = 20): Promise<any[]> {
return this.request("GET", "/user/repos", undefined, {
page: String(page),
limit: String(limit),
});
}
// --- Repository ---
async getRepo(owner: string, repo: string): Promise<any> {
return this.request("GET", `/repos/${owner}/${repo}`);
}
async searchRepos(
query?: string,
page = 1,
limit = 20,
): Promise<{ data: any[]; ok: boolean }> {
const params: Record<string, string> = {
page: String(page),
limit: String(limit),
};
if (query) params.q = query;
return this.request("GET", "/repos/search", undefined, params);
}
// --- Commits ---
async listCommits(
owner: string,
repo: string,
sha?: string,
page = 1,
limit = 20,
): Promise<any[]> {
const params: Record<string, string> = {
page: String(page),
limit: String(limit),
stat: "true",
};
if (sha) params.sha = sha;
return this.request(
"GET",
`/repos/${owner}/${repo}/commits`,
undefined,
params,
);
}
async getCommit(owner: string, repo: string, sha: string): Promise<any> {
return this.request("GET", `/repos/${owner}/${repo}/git/commits/${sha}`, undefined, {
stat: "true",
files: "true",
});
}
// --- Branches ---
async listBranches(
owner: string,
repo: string,
page = 1,
limit = 20,
): Promise<any[]> {
return this.request(
"GET",
`/repos/${owner}/${repo}/branches`,
undefined,
{ page: String(page), limit: String(limit) },
);
}
async getBranch(owner: string, repo: string, branch: string): Promise<any> {
return this.request(
"GET",
`/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`,
);
}
// --- Tags ---
async listTags(
owner: string,
repo: string,
page = 1,
limit = 20,
): Promise<any[]> {
return this.request("GET", `/repos/${owner}/${repo}/tags`, undefined, {
page: String(page),
limit: String(limit),
});
}
// --- Releases ---
async listReleases(
owner: string,
repo: string,
page = 1,
limit = 20,
): Promise<any[]> {
return this.request(
"GET",
`/repos/${owner}/${repo}/releases`,
undefined,
{ page: String(page), limit: String(limit) },
);
}
async getLatestRelease(owner: string, repo: string): Promise<any> {
return this.request("GET", `/repos/${owner}/${repo}/releases/latest`);
}
async createRelease(
owner: string,
repo: string,
data: any,
): Promise<any> {
return this.request("POST", `/repos/${owner}/${repo}/releases`, data);
}
// --- Pull Requests ---
async listPullRequests(
owner: string,
repo: string,
state = "open",
page = 1,
limit = 20,
): Promise<any[]> {
return this.request("GET", `/repos/${owner}/${repo}/pulls`, undefined, {
state,
page: String(page),
limit: String(limit),
});
}
async getPullRequest(
owner: string,
repo: string,
index: number,
): Promise<any> {
return this.request("GET", `/repos/${owner}/${repo}/pulls/${index}`);
}
async createPullRequest(
owner: string,
repo: string,
data: any,
): Promise<any> {
return this.request("POST", `/repos/${owner}/${repo}/pulls`, data);
}
async getPullRequestFiles(
owner: string,
repo: string,
index: number,
page = 1,
limit = 50,
): Promise<any[]> {
return this.request(
"GET",
`/repos/${owner}/${repo}/pulls/${index}/files`,
undefined,
{ page: String(page), limit: String(limit) },
);
}
// --- Issues ---
async listIssues(
owner: string,
repo: string,
state = "open",
type = "issues",
page = 1,
limit = 20,
): Promise<any[]> {
return this.request(
"GET",
`/repos/${owner}/${repo}/issues`,
undefined,
{ state, type, page: String(page), limit: String(limit) },
);
}
async getIssue(owner: string, repo: string, index: number): Promise<any> {
return this.request("GET", `/repos/${owner}/${repo}/issues/${index}`);
}
async createIssue(
owner: string,
repo: string,
data: any,
): Promise<any> {
return this.request("POST", `/repos/${owner}/${repo}/issues`, data);
}
async updateIssue(
owner: string,
repo: string,
index: number,
data: any,
): Promise<any> {
return this.request(
"PATCH",
`/repos/${owner}/${repo}/issues/${index}`,
data,
);
}
async listIssueComments(
owner: string,
repo: string,
index: number,
): Promise<any[]> {
return this.request(
"GET",
`/repos/${owner}/${repo}/issues/${index}/comments`,
);
}
async addIssueComment(
owner: string,
repo: string,
index: number,
body: string,
): Promise<any> {
return this.request(
"POST",
`/repos/${owner}/${repo}/issues/${index}/comments`,
{ body },
);
}
async searchIssues(
query?: string,
state = "open",
type = "issues",
owner?: string,
page = 1,
limit = 20,
): Promise<any[]> {
const params: Record<string, string> = {
state,
type,
page: String(page),
limit: String(limit),
};
if (query) params.q = query;
if (owner) params.owner = owner;
return this.request("GET", "/repos/issues/search", undefined, params);
}
// --- Labels ---
async listLabels(owner: string, repo: string): Promise<any[]> {
return this.request("GET", `/repos/${owner}/${repo}/labels`);
}
// --- Milestones ---
async listMilestones(
owner: string,
repo: string,
state = "open",
): Promise<any[]> {
return this.request(
"GET",
`/repos/${owner}/${repo}/milestones`,
undefined,
{ state },
);
}
// --- Notifications ---
async listNotifications(
all = false,
page = 1,
limit = 20,
): Promise<any[]> {
return this.request("GET", "/notifications", undefined, {
all: String(all),
page: String(page),
limit: String(limit),
});
}
async markNotificationsRead(): Promise<any[]> {
return this.request("PUT", "/notifications");
}
// --- Organizations ---
async listMyOrgs(page = 1, limit = 20): Promise<any[]> {
return this.request("GET", "/user/orgs", undefined, {
page: String(page),
limit: String(limit),
});
}
async getOrg(name: string): Promise<any> {
return this.request("GET", `/orgs/${name}`);
}
async listOrgRepos(
org: string,
page = 1,
limit = 20,
): Promise<any[]> {
return this.request("GET", `/orgs/${org}/repos`, undefined, {
page: String(page),
limit: String(limit),
});
}
// --- Commit Status ---
async getCommitStatus(
owner: string,
repo: string,
ref: string,
): Promise<any> {
return this.request(
"GET",
`/repos/${owner}/${repo}/commits/${ref}/status`,
);
}
// --- File Contents ---
async getFileContents(
owner: string,
repo: string,
filepath: string,
ref?: string,
): Promise<any> {
const params: Record<string, string> = {};
if (ref) params.ref = ref;
return this.request(
"GET",
`/repos/${owner}/${repo}/contents/${filepath}`,
undefined,
params,
);
}
}
+490
View File
@@ -0,0 +1,490 @@
/**
* Tool handler implementations — maps MCP tool calls to Gitea API client methods.
*/
import { GiteaClient } from "./client.js";
function formatRepo(r: any): string {
return [
`${r.full_name}${r.private ? " (private)" : ""}${r.fork ? " (fork)" : ""}`,
` ID: ${r.id} | Default branch: ${r.default_branch}`,
r.description ? ` ${r.description}` : null,
` Stars: ${r.stars_count} | Forks: ${r.forks_count} | Issues: ${r.open_issues_count}`,
` URL: ${r.html_url}`,
]
.filter(Boolean)
.join("\n");
}
function formatCommit(c: any): string {
const author = c.commit?.author?.name ?? c.author?.login ?? "Unknown";
const date = c.commit?.author?.date?.split("T")[0] ?? "";
const msg = c.commit?.message?.split("\n")[0] ?? "";
const stats = c.stats
? ` (+${c.stats.additions}/-${c.stats.deletions})`
: "";
return `${c.sha?.substring(0, 7)} ${date} ${author}: ${msg}${stats}`;
}
function formatBranch(b: any): string {
const sha = b.commit?.id?.substring(0, 7) ?? "?";
const prot = b.protected ? " [protected]" : "";
return `${b.name}${prot} (${sha})`;
}
function formatIssue(i: any): string {
const labels = i.labels?.map((l: any) => l.name).join(", ") ?? "";
const assignee = i.assignee?.login ?? "unassigned";
return [
`#${i.number}: ${i.title} [${i.state}]`,
` Assignee: ${assignee}${labels ? ` | Labels: ${labels}` : ""}`,
` Created: ${i.created_at?.split("T")[0]} | Comments: ${i.comments ?? 0}`,
` URL: ${i.html_url}`,
].join("\n");
}
function formatPR(pr: any): string {
const mergeable = pr.mergeable != null ? ` | Mergeable: ${pr.mergeable}` : "";
return [
`#${pr.number}: ${pr.title} [${pr.state}]`,
` ${pr.head?.label ?? "?"}${pr.base?.label ?? "?"}${mergeable}`,
` Author: ${pr.user?.login ?? "?"} | Created: ${pr.created_at?.split("T")[0]}`,
` URL: ${pr.html_url}`,
].join("\n");
}
function formatList(items: any[], formatter: (item: any) => string, label: string): string {
if (!items || items.length === 0) return `No ${label} found.`;
return [`${label} (${items.length} results)`, "", ...items.map(formatter)].join("\n");
}
export function createHandlers(client: GiteaClient) {
return {
// --- User ---
async get_me() {
const user = await client.getMe();
return [
`Authenticated as: ${user.full_name || user.login} (@${user.login})`,
` ID: ${user.id} | Email: ${user.email ?? "N/A"}`,
` Admin: ${user.is_admin ?? false}`,
` Followers: ${user.followers_count} | Following: ${user.following_count}`,
].join("\n");
},
async list_my_repos(args: { page?: number; limit?: number }) {
const repos = await client.listMyRepos(args.page, args.limit);
return formatList(repos, formatRepo, "My Repositories");
},
// --- Repository ---
async get_repo(args: { owner: string; repo: string }) {
const r = await client.getRepo(args.owner, args.repo);
return formatRepo(r);
},
async search_repos(args: { query?: string; page?: number; limit?: number }) {
const result = await client.searchRepos(args.query, args.page, args.limit);
return formatList(result.data ?? result, formatRepo, "Repositories");
},
// --- Commits ---
async list_commits(args: {
owner: string;
repo: string;
sha?: string;
page?: number;
limit?: number;
}) {
const commits = await client.listCommits(
args.owner,
args.repo,
args.sha,
args.page,
args.limit,
);
return formatList(commits, formatCommit, "Commits");
},
async get_commit(args: { owner: string; repo: string; sha: string }) {
const c = await client.getCommit(args.owner, args.repo, args.sha);
const files = c.files
?.map(
(f: any) =>
` ${f.status ?? "modified"} ${f.filename} (+${f.additions}/-${f.deletions})`,
)
.join("\n") ?? "";
return [
formatCommit(c),
c.commit?.message ? `\n${c.commit.message}` : "",
files ? `\nChanged files:\n${files}` : "",
]
.filter(Boolean)
.join("\n");
},
// --- Branches ---
async list_branches(args: {
owner: string;
repo: string;
page?: number;
limit?: number;
}) {
const branches = await client.listBranches(
args.owner,
args.repo,
args.page,
args.limit,
);
return formatList(branches, formatBranch, "Branches");
},
async get_branch(args: { owner: string; repo: string; branch: string }) {
const b = await client.getBranch(args.owner, args.repo, args.branch);
return [
formatBranch(b),
` Can push: ${b.user_can_push} | Can merge: ${b.user_can_merge}`,
b.required_approvals ? ` Required approvals: ${b.required_approvals}` : null,
]
.filter(Boolean)
.join("\n");
},
// --- Tags ---
async list_tags(args: {
owner: string;
repo: string;
page?: number;
limit?: number;
}) {
const tags = await client.listTags(args.owner, args.repo, args.page, args.limit);
return formatList(
tags,
(t: any) => `${t.name} (${t.id?.substring(0, 7) ?? "?"})${t.message ? `: ${t.message}` : ""}`,
"Tags",
);
},
// --- Releases ---
async list_releases(args: {
owner: string;
repo: string;
page?: number;
limit?: number;
}) {
const releases = await client.listReleases(
args.owner,
args.repo,
args.page,
args.limit,
);
return formatList(
releases,
(r: any) =>
`${r.tag_name}: ${r.name ?? "(untitled)"}${r.draft ? " [draft]" : ""}${r.prerelease ? " [pre-release]" : ""}${r.published_at?.split("T")[0] ?? "unpublished"}`,
"Releases",
);
},
async get_latest_release(args: { owner: string; repo: string }) {
const r = await client.getLatestRelease(args.owner, args.repo);
return [
`${r.tag_name}: ${r.name ?? "(untitled)"}`,
` Published: ${r.published_at?.split("T")[0] ?? "N/A"} by ${r.author?.login ?? "?"}`,
r.body ? `\n${r.body.slice(0, 500)}` : "",
` URL: ${r.html_url}`,
]
.filter(Boolean)
.join("\n");
},
async create_release(args: {
owner: string;
repo: string;
tag_name: string;
name?: string;
body?: string;
draft?: boolean;
prerelease?: boolean;
target_commitish?: string;
}) {
const { owner, repo, ...data } = args;
const r = await client.createRelease(owner, repo, data);
return `Created release ${r.tag_name}: ${r.name ?? "(untitled)"}\n URL: ${r.html_url}`;
},
// --- Pull Requests ---
async list_pull_requests(args: {
owner: string;
repo: string;
state?: string;
page?: number;
limit?: number;
}) {
const prs = await client.listPullRequests(
args.owner,
args.repo,
args.state,
args.page,
args.limit,
);
return formatList(prs, formatPR, "Pull Requests");
},
async get_pull_request(args: {
owner: string;
repo: string;
index: number;
}) {
const pr = await client.getPullRequest(args.owner, args.repo, args.index);
return formatPR(pr) + (pr.body ? `\n\n${pr.body.slice(0, 500)}` : "");
},
async create_pull_request(args: {
owner: string;
repo: string;
title: string;
body?: string;
head: string;
base: string;
assignees?: string[];
labels?: number[];
milestone?: number;
}) {
const { owner, repo, ...data } = args;
const pr = await client.createPullRequest(owner, repo, data);
return `Created PR #${pr.number}: ${pr.title}\n URL: ${pr.html_url}`;
},
async get_pull_request_files(args: {
owner: string;
repo: string;
index: number;
}) {
const files = await client.getPullRequestFiles(
args.owner,
args.repo,
args.index,
);
if (!files || files.length === 0) return "No files changed.";
return files
.map(
(f: any) =>
`${f.status ?? "modified"} ${f.filename} (+${f.additions ?? 0}/-${f.deletions ?? 0})`,
)
.join("\n");
},
// --- Issues ---
async list_issues(args: {
owner: string;
repo: string;
state?: string;
page?: number;
limit?: number;
}) {
const issues = await client.listIssues(
args.owner,
args.repo,
args.state,
"issues",
args.page,
args.limit,
);
return formatList(issues, formatIssue, "Issues");
},
async get_issue(args: { owner: string; repo: string; index: number }) {
const i = await client.getIssue(args.owner, args.repo, args.index);
return formatIssue(i) + (i.body ? `\n\n${i.body.slice(0, 500)}` : "");
},
async create_issue(args: {
owner: string;
repo: string;
title: string;
body?: string;
assignees?: string[];
labels?: number[];
milestone?: number;
}) {
const { owner, repo, ...data } = args;
const i = await client.createIssue(owner, repo, data);
return `Created issue #${i.number}: ${i.title}\n URL: ${i.html_url}`;
},
async update_issue(args: {
owner: string;
repo: string;
index: number;
title?: string;
body?: string;
state?: string;
assignees?: string[];
}) {
const { owner, repo, index, ...data } = args;
const i = await client.updateIssue(owner, repo, index, data);
return `Updated issue #${i.number}: ${i.title} [${i.state}]`;
},
async list_issue_comments(args: {
owner: string;
repo: string;
index: number;
}) {
const comments = await client.listIssueComments(
args.owner,
args.repo,
args.index,
);
if (!comments || comments.length === 0) return "No comments.";
return comments
.map(
(c: any) =>
`${c.id}${c.user?.login ?? "?"} (${c.created_at?.split("T")[0] ?? "?"}):\n ${c.body?.slice(0, 200) ?? ""}`,
)
.join("\n\n");
},
async add_issue_comment(args: {
owner: string;
repo: string;
index: number;
body: string;
}) {
const c = await client.addIssueComment(
args.owner,
args.repo,
args.index,
args.body,
);
return `Comment #${c.id} added to #${args.index}.`;
},
async search_issues(args: {
query?: string;
state?: string;
type?: string;
owner?: string;
page?: number;
limit?: number;
}) {
const issues = await client.searchIssues(
args.query,
args.state,
args.type,
args.owner,
args.page,
args.limit,
);
return formatList(issues, formatIssue, "Issues");
},
// --- Labels & Milestones ---
async list_labels(args: { owner: string; repo: string }) {
const labels = await client.listLabels(args.owner, args.repo);
if (!labels || labels.length === 0) return "No labels found.";
return labels
.map((l: any) => `${l.id}: ${l.name} (#${l.color})${l.description ? `${l.description}` : ""}`)
.join("\n");
},
async list_milestones(args: {
owner: string;
repo: string;
state?: string;
}) {
const milestones = await client.listMilestones(
args.owner,
args.repo,
args.state,
);
if (!milestones || milestones.length === 0) return "No milestones found.";
return milestones
.map(
(m: any) =>
`${m.id}: ${m.title} [${m.state}] — ${m.open_issues}/${m.open_issues + m.closed_issues} open${m.due_on ? `, due ${m.due_on.split("T")[0]}` : ""}`,
)
.join("\n");
},
// --- Notifications ---
async list_notifications(args: {
all?: boolean;
page?: number;
limit?: number;
}) {
const notifs = await client.listNotifications(
args.all,
args.page,
args.limit,
);
if (!notifs || notifs.length === 0) return "No notifications.";
return notifs
.map(
(n: any) =>
`${n.id}: ${n.unread ? "[unread]" : "[read]"} ${n.subject?.type ?? "?"}: ${n.subject?.title ?? "?"} (${n.repository?.full_name ?? "?"})`,
)
.join("\n");
},
async mark_notifications_read() {
await client.markNotificationsRead();
return "All notifications marked as read.";
},
// --- Organizations ---
async list_my_orgs(args: { page?: number; limit?: number }) {
const orgs = await client.listMyOrgs(args.page, args.limit);
if (!orgs || orgs.length === 0) return "No organizations found.";
return orgs
.map(
(o: any) =>
`${o.name}${o.full_name ? ` (${o.full_name})` : ""}${o.description ? `${o.description}` : ""}`,
)
.join("\n");
},
async get_org(args: { name: string }) {
const o = await client.getOrg(args.name);
return [
`${o.name}${o.full_name ? ` (${o.full_name})` : ""}`,
o.description ? ` ${o.description}` : null,
o.website ? ` Website: ${o.website}` : null,
o.location ? ` Location: ${o.location}` : null,
` Visibility: ${o.visibility ?? "public"}`,
]
.filter(Boolean)
.join("\n");
},
async list_org_repos(args: {
org: string;
page?: number;
limit?: number;
}) {
const repos = await client.listOrgRepos(args.org, args.page, args.limit);
return formatList(repos, formatRepo, `${args.org} Repositories`);
},
// --- CI Status ---
async get_commit_status(args: {
owner: string;
repo: string;
ref: string;
}) {
const status = await client.getCommitStatus(
args.owner,
args.repo,
args.ref,
);
const state = status.state ?? "unknown";
const statuses = status.statuses ?? [];
if (statuses.length === 0) return `${args.ref}: ${state} (no CI statuses)`;
const lines = statuses.map(
(s: any) =>
` ${s.status ?? s.state}: ${s.context ?? "?"}${s.description ?? ""}${s.target_url ? ` (${s.target_url})` : ""}`,
);
return [`${args.ref}: ${state}`, ...lines].join("\n");
},
};
}
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env node
/**
* Gitea MCP Server
*
* Connects Claude to Gitea via the v1 REST API.
*
* Environment variables:
* GITEA_URL — Base URL of your Gitea instance (e.g. https://gitea.example.com)
* GITEA_TOKEN — API token (generated under Settings > Applications > Access Tokens)
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { GiteaClient } from "./client.js";
import { toolDefs } from "./tools.js";
import { createHandlers } from "./handlers.js";
function getEnvOrThrow(name: string): string {
const value = process.env[name];
if (!value) {
console.error(`Missing required environment variable: ${name}`);
process.exit(1);
}
return value;
}
const client = new GiteaClient({
baseUrl: getEnvOrThrow("GITEA_URL"),
token: getEnvOrThrow("GITEA_TOKEN"),
});
const handlers = createHandlers(client);
const server = new McpServer({
name: "gitea",
version: "0.1.0",
});
// Register each tool from toolDefs with its corresponding handler.
for (const [name, def] of Object.entries(toolDefs)) {
const handler = handlers[name as keyof typeof handlers];
if (!handler) {
console.error(`No handler for tool: ${name}`);
continue;
}
server.tool(
name,
def.description,
def.inputSchema,
async (args: any) => {
try {
const result = await (handler as Function)(args);
return { content: [{ type: "text" as const, text: String(result) }] };
} catch (err: any) {
return {
content: [{ type: "text" as const, text: `Error: ${err.message}` }],
isError: true,
};
}
},
);
}
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Gitea MCP server running on stdio");
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
+332
View File
@@ -0,0 +1,332 @@
/**
* MCP tool definitions for Gitea operations.
*/
import { z } from "zod";
const ownerRepo = {
owner: z.string().describe("Repository owner (user or org)"),
repo: z.string().describe("Repository name"),
};
const pagination = {
page: z.number().optional().default(1).describe("Page number (1-based)"),
limit: z.number().optional().default(20).describe("Items per page (max 50)"),
};
export const toolDefs = {
// --- User ---
get_me: {
description:
"Get the currently authenticated Gitea user. Useful for verifying the connection.",
inputSchema: {},
},
list_my_repos: {
description: "List repositories owned by the authenticated user.",
inputSchema: { ...pagination },
},
// --- Repository ---
get_repo: {
description: "Get detailed information about a specific repository.",
inputSchema: { ...ownerRepo },
},
search_repos: {
description: "Search for repositories across the Gitea instance.",
inputSchema: {
query: z.string().optional().describe("Search query"),
...pagination,
},
},
// --- Commits ---
list_commits: {
description:
"List commits in a repository, optionally filtered by branch/SHA.",
inputSchema: {
...ownerRepo,
sha: z
.string()
.optional()
.describe("Branch name or commit SHA to start from"),
...pagination,
},
},
get_commit: {
description:
"Get a specific commit with diff stats and changed files.",
inputSchema: {
...ownerRepo,
sha: z.string().describe("Commit SHA"),
},
},
// --- Branches ---
list_branches: {
description: "List branches in a repository.",
inputSchema: {
...ownerRepo,
...pagination,
},
},
get_branch: {
description:
"Get a specific branch including protection status and latest commit.",
inputSchema: {
...ownerRepo,
branch: z.string().describe("Branch name"),
},
},
// --- Tags ---
list_tags: {
description: "List tags in a repository.",
inputSchema: {
...ownerRepo,
...pagination,
},
},
// --- Releases ---
list_releases: {
description: "List releases for a repository.",
inputSchema: {
...ownerRepo,
...pagination,
},
},
get_latest_release: {
description:
"Get the latest published release (non-draft, non-prerelease).",
inputSchema: { ...ownerRepo },
},
create_release: {
description: "Create a new release for a repository.",
inputSchema: {
...ownerRepo,
tag_name: z.string().describe("Tag name for the release"),
name: z.string().optional().describe("Release title"),
body: z.string().optional().describe("Release notes (markdown)"),
draft: z.boolean().optional().default(false).describe("Create as draft"),
prerelease: z
.boolean()
.optional()
.default(false)
.describe("Mark as pre-release"),
target_commitish: z
.string()
.optional()
.describe("Branch or commit SHA to tag (defaults to default branch)"),
},
},
// --- Pull Requests ---
list_pull_requests: {
description: "List pull requests in a repository.",
inputSchema: {
...ownerRepo,
state: z
.enum(["open", "closed", "all"])
.optional()
.default("open")
.describe("Filter by state"),
...pagination,
},
},
get_pull_request: {
description:
"Get a specific pull request with merge status and review info.",
inputSchema: {
...ownerRepo,
index: z.number().describe("Pull request number"),
},
},
create_pull_request: {
description: "Create a new pull request.",
inputSchema: {
...ownerRepo,
title: z.string().describe("PR title"),
body: z.string().optional().describe("PR description (markdown)"),
head: z.string().describe("Source branch"),
base: z.string().describe("Target branch"),
assignees: z
.array(z.string())
.optional()
.describe("Assignee usernames"),
labels: z.array(z.number()).optional().describe("Label IDs"),
milestone: z.number().optional().describe("Milestone ID"),
},
},
get_pull_request_files: {
description: "List files changed in a pull request.",
inputSchema: {
...ownerRepo,
index: z.number().describe("Pull request number"),
},
},
// --- Issues ---
list_issues: {
description: "List issues in a repository (excludes pull requests by default).",
inputSchema: {
...ownerRepo,
state: z
.enum(["open", "closed", "all"])
.optional()
.default("open")
.describe("Filter by state"),
...pagination,
},
},
get_issue: {
description: "Get a specific issue by number.",
inputSchema: {
...ownerRepo,
index: z.number().describe("Issue number"),
},
},
create_issue: {
description: "Create a new issue in a repository.",
inputSchema: {
...ownerRepo,
title: z.string().describe("Issue title"),
body: z.string().optional().describe("Issue body (markdown)"),
assignees: z
.array(z.string())
.optional()
.describe("Assignee usernames"),
labels: z.array(z.number()).optional().describe("Label IDs"),
milestone: z.number().optional().describe("Milestone ID"),
},
},
update_issue: {
description:
"Update an existing issue. Only provided fields are changed.",
inputSchema: {
...ownerRepo,
index: z.number().describe("Issue number"),
title: z.string().optional().describe("New title"),
body: z.string().optional().describe("New body"),
state: z.enum(["open", "closed"]).optional().describe("New state"),
assignees: z
.array(z.string())
.optional()
.describe("New assignee usernames"),
},
},
list_issue_comments: {
description: "List all comments on an issue or pull request.",
inputSchema: {
...ownerRepo,
index: z.number().describe("Issue or PR number"),
},
},
add_issue_comment: {
description: "Add a comment to an issue or pull request.",
inputSchema: {
...ownerRepo,
index: z.number().describe("Issue or PR number"),
body: z.string().describe("Comment body (markdown)"),
},
},
search_issues: {
description:
"Search issues across all accessible repositories.",
inputSchema: {
query: z.string().optional().describe("Search query"),
state: z
.enum(["open", "closed", "all"])
.optional()
.default("open"),
type: z
.enum(["issues", "pulls"])
.optional()
.default("issues")
.describe("Filter by type"),
owner: z.string().optional().describe("Filter by repo owner"),
...pagination,
},
},
// --- Labels & Milestones ---
list_labels: {
description: "List all labels in a repository.",
inputSchema: { ...ownerRepo },
},
list_milestones: {
description: "List milestones in a repository.",
inputSchema: {
...ownerRepo,
state: z
.enum(["open", "closed", "all"])
.optional()
.default("open"),
},
},
// --- Notifications ---
list_notifications: {
description:
"List notification threads for the authenticated user.",
inputSchema: {
all: z
.boolean()
.optional()
.default(false)
.describe("Include read notifications"),
...pagination,
},
},
mark_notifications_read: {
description: "Mark all notifications as read.",
inputSchema: {},
},
// --- Organizations ---
list_my_orgs: {
description: "List organizations the authenticated user belongs to.",
inputSchema: { ...pagination },
},
get_org: {
description: "Get detailed information about an organization.",
inputSchema: {
name: z.string().describe("Organization name"),
},
},
list_org_repos: {
description: "List repositories belonging to an organization.",
inputSchema: {
org: z.string().describe("Organization name"),
...pagination,
},
},
// --- CI Status ---
get_commit_status: {
description:
"Get the combined CI/CD status for a commit ref (branch, tag, or SHA).",
inputSchema: {
...ownerRepo,
ref: z.string().describe("Branch name, tag, or commit SHA"),
},
},
} as const;
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}