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(fn: (session: Session) => Promise): Promise { 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; 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);