#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios, { AxiosInstance } from 'axios'; interface RepoArgs { owner: string; repo: string; } interface CreateRepoArgs { name: string; description?: string; private?: boolean; autoInit?: boolean; } interface CreateFileArgs extends RepoArgs { path: string; content?: string; file_text?: string; // Alternative parameter name for content message: string; branch?: string; } interface GetContentsArgs extends RepoArgs { path: string; ref?: string; } interface CreateBranchArgs extends RepoArgs { branch: string; from?: string; } interface IssueArgs extends RepoArgs { state?: 'open' | 'closed'; } interface CreateIssueArgs extends RepoArgs { title: string; body: string; } interface PullRequestArgs extends RepoArgs { state?: 'open' | 'closed' | 'all'; } const GITEA_TOKEN = process.env.GITEA_TOKEN; const GITEA_API_URL = process.env.GITEA_API_URL; if (!GITEA_TOKEN) { throw new Error('GITEA_TOKEN environment variable is required'); } if (!GITEA_API_URL) { throw new Error('GITEA_API_URL environment variable is required'); } class GiteaServer { private server: Server; private api: AxiosInstance; constructor() { this.server = new Server( { name: 'gitea-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.api = axios.create({ baseURL: GITEA_API_URL, headers: { 'Authorization': `token ${GITEA_TOKEN}`, 'Accept': 'application/json', }, }); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_repositories', description: 'List repositories for the authenticated user', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'get_repository', description: 'Get details about a specific repository', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, }, required: ['owner', 'repo'], }, }, { name: 'list_issues', description: 'List issues in a repository', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, state: { type: 'string', description: 'Issue state (open/closed)', enum: ['open', 'closed'], }, }, required: ['owner', 'repo'], }, }, { name: 'create_issue', description: 'Create a new issue in a repository', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, title: { type: 'string', description: 'Issue title', }, body: { type: 'string', description: 'Issue body/description', }, }, required: ['owner', 'repo', 'title', 'body'], }, }, { name: 'list_pull_requests', description: 'List pull requests in a repository', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, state: { type: 'string', description: 'PR state (open/closed/all)', enum: ['open', 'closed', 'all'], }, }, required: ['owner', 'repo'], }, }, { name: 'create_repository', description: 'Create a new repository', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Repository name', }, description: { type: 'string', description: 'Repository description', }, private: { type: 'boolean', description: 'Whether the repository is private', }, autoInit: { type: 'boolean', description: 'Whether to initialize with README', }, }, required: ['name'], }, }, { name: 'get_contents', description: 'Get contents of a file or directory in a repository', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, path: { type: 'string', description: 'Path to file or directory', }, ref: { type: 'string', description: 'Git reference (branch/tag/commit)', }, }, required: ['owner', 'repo', 'path'], }, }, { name: 'add_file_to_repo', description: 'Add a new file to a repository (supports both "content" and "file_text" parameters)', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, path: { type: 'string', description: 'Path to file', }, content: { type: 'string', description: 'File content (will be base64 encoded). Either content or file_text must be provided.', }, file_text: { type: 'string', description: 'Alternative parameter for file content. Either content or file_text must be provided.', }, message: { type: 'string', description: 'Commit message', }, branch: { type: 'string', description: 'Branch name', }, }, required: ['owner', 'repo', 'path', 'message'], }, }, { name: 'create_branch', description: 'Create a new branch in a repository', inputSchema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner', }, repo: { type: 'string', description: 'Repository name', }, branch: { type: 'string', description: 'New branch name', }, from: { type: 'string', description: 'Base branch/commit to create from (defaults to default branch)', }, }, required: ['owner', 'repo', 'branch'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case 'list_repositories': { const response = await this.api.get('/user/repos'); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'get_repository': { const { owner, repo } = request.params.arguments as unknown as RepoArgs; const response = await this.api.get(`/repos/${owner}/${repo}`); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'list_issues': { const { owner, repo, state } = request.params.arguments as unknown as IssueArgs; const response = await this.api.get(`/repos/${owner}/${repo}/issues`, { params: { state }, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'create_issue': { const { owner, repo, title, body } = request.params.arguments as unknown as CreateIssueArgs; const response = await this.api.post(`/repos/${owner}/${repo}/issues`, { title, body, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'list_pull_requests': { const { owner, repo, state } = request.params.arguments as unknown as PullRequestArgs; const response = await this.api.get(`/repos/${owner}/${repo}/pulls`, { params: { state }, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'create_repository': { const { name, description, private: isPrivate, autoInit } = request.params.arguments as unknown as CreateRepoArgs; const response = await this.api.post('/user/repos', { name, description, private: isPrivate, auto_init: autoInit, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'get_contents': { const { owner, repo, path, ref } = request.params.arguments as unknown as GetContentsArgs; const response = await this.api.get(`/repos/${owner}/${repo}/contents/${path}`, { params: { ref }, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } case 'add_file_to_repo': { const args = request.params.arguments as unknown as CreateFileArgs; // Support both 'content' and 'file_text' for backward compatibility const fileContent = args.content || (args as any).file_text; if (!fileContent) { throw new McpError( ErrorCode.InvalidParams, 'Either "content" or "file_text" parameter is required' ); } const response = await this.api.post(`/repos/${args.owner}/${args.repo}/contents/${args.path}`, { content: Buffer.from(fileContent).toString('base64'), message: args.message, branch: args.branch, }); // Verify the file exists after creation const verifyResponse = await this.api.get(`/repos/${args.owner}/${args.repo}/contents/${args.path}`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'File created successfully', file: { path: args.path, url: response.data.content.html_url, commit: response.data.commit, }, content: verifyResponse.data, }, null, 2), }, ], }; } case 'create_branch': { const { owner, repo, branch, from } = request.params.arguments as unknown as CreateBranchArgs; // Get the default branch if 'from' is not specified const sourceBranch = from || (await this.api.get(`/repos/${owner}/${repo}`)).data.default_branch; // Get the commit SHA from the source branch const branchResponse = await this.api.get(`/repos/${owner}/${repo}/branches/${sourceBranch}`); const sha = branchResponse.data.commit.id; // Create the new branch const response = await this.api.post(`/repos/${owner}/${repo}/branches`, { new_branch_name: branch, old_branch_name: sourceBranch }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Gitea API error: ${error.response?.data?.message ?? error.message}` ); } throw error; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Gitea MCP server running on stdio'); } } const server = new GiteaServer(); server.run().catch(console.error);