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>
191 lines
6.1 KiB
TypeScript
191 lines
6.1 KiB
TypeScript
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);
|