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:
190
server.ts
Normal file
190
server.ts
Normal 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);
|
||||
Reference in New Issue
Block a user