Initial commit: Gitea browser automation MCP server

TypeScript/Playwright MCP server exposing 9 Gitea tools (list_repos,
view_repo, view_file, list/create/view/comment/merge/close PR).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
agent
2026-03-24 09:14:57 -04:00
commit 9132394fd5
10 changed files with 2432 additions and 0 deletions

190
server.ts Normal file
View File

@@ -0,0 +1,190 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { createSession, type Session } from './lib/auth';
import * as gitea from './lib/gitea';
const server = new Server(
{ name: 'gitea-browser', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
async function withSession<T>(fn: (session: Session) => Promise<T>): Promise<T> {
const session = await createSession();
try {
return await fn(session);
} finally {
await session.browser.close();
}
}
const TOOLS = [
{
name: 'list_repos',
description: 'List repositories accessible to the Gitea agent account.',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'view_repo',
description: 'View a Gitea repository: file list, default branch, and README excerpt.',
inputSchema: {
type: 'object',
required: ['owner', 'repo'],
properties: {
owner: { type: 'string', description: 'Repository owner (username or org)' },
repo: { type: 'string', description: 'Repository name' },
},
},
},
{
name: 'view_file',
description: 'Get the raw content of a file in a Gitea repository.',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'branch', 'filepath'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
branch: { type: 'string', description: 'Branch name' },
filepath: { type: 'string', description: 'Path to the file (e.g. src/main.js)' },
},
},
},
{
name: 'list_prs',
description: 'List pull requests for a Gitea repository.',
inputSchema: {
type: 'object',
required: ['owner', 'repo'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
state: { type: 'string', enum: ['open', 'closed', 'all'], default: 'open' },
},
},
},
{
name: 'create_pr',
description: 'Create a new pull request in a Gitea repository.',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'head', 'base', 'title'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
head: { type: 'string', description: 'Source branch (the branch with changes)' },
base: { type: 'string', description: 'Target branch to merge into' },
title: { type: 'string' },
body: { type: 'string', description: 'PR description (optional)' },
},
},
},
{
name: 'view_pr',
description: 'View details of a pull request including title, status, branches, and comments.',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'number'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
number: { type: 'number', description: 'PR number' },
},
},
},
{
name: 'comment_pr',
description: 'Add a comment to a pull request.',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'number', 'body'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
number: { type: 'number' },
body: { type: 'string', description: 'Comment text' },
},
},
},
{
name: 'merge_pr',
description: 'Merge an open pull request.',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'number'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
number: { type: 'number' },
},
},
},
{
name: 'close_pr',
description: 'Close a pull request without merging.',
inputSchema: {
type: 'object',
required: ['owner', 'repo', 'number'],
properties: {
owner: { type: 'string' },
repo: { type: 'string' },
number: { type: 'number' },
},
},
},
] as const;
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
const a = args as Record<string, unknown>;
try {
let result: unknown;
switch (name) {
case 'list_repos':
result = await withSession(s => gitea.listRepos(s));
break;
case 'view_repo':
result = await withSession(s => gitea.viewRepo(s, a.owner as string, a.repo as string));
break;
case 'view_file':
result = await withSession(s => gitea.viewFile(s, a.owner as string, a.repo as string, a.branch as string, a.filepath as string));
break;
case 'list_prs':
result = await withSession(s => gitea.listPRs(s, a.owner as string, a.repo as string, a.state as 'open' | 'closed' | 'all'));
break;
case 'create_pr':
result = await withSession(s => gitea.createPR(s, a.owner as string, a.repo as string, a.head as string, a.base as string, a.title as string, a.body as string | undefined));
break;
case 'view_pr':
result = await withSession(s => gitea.viewPR(s, a.owner as string, a.repo as string, a.number as number));
break;
case 'comment_pr':
result = await withSession(s => gitea.commentPR(s, a.owner as string, a.repo as string, a.number as number, a.body as string));
break;
case 'merge_pr':
result = await withSession(s => gitea.mergePR(s, a.owner as string, a.repo as string, a.number as number));
break;
case 'close_pr':
result = await withSession(s => gitea.closePR(s, a.owner as string, a.repo as string, a.number as number));
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
} catch (err) {
return {
content: [{ type: 'text' as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);