From 60014c1971d95ff22995b9f30b447911967cf5d0 Mon Sep 17 00:00:00 2001 From: Steve White Date: Mon, 6 Jan 2025 16:33:00 -0600 Subject: [PATCH] Initial commit --- .clinerules | 1 + .gitignore | 4 + README.md | 70 +++++++ package-lock.json | 288 ++++++++++++++++++++++++++ package.json | 27 +++ src/index.ts | 512 ++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 15 ++ 7 files changed, 917 insertions(+) create mode 100644 .clinerules create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..a9656ba --- /dev/null +++ b/.clinerules @@ -0,0 +1 @@ +- Use descriptive names for MCP functions. e.g. "add_file_to_repo" instead of "create_file". \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0feff8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +build/ +.env +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..d566dfd --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# gitea-server MCP Server + +"An MCP server providing access to gitea.r8z.us" + +This is a TypeScript-based MCP server that implements a simple notes system. It demonstrates core MCP concepts by providing: + +- Resources representing text notes with URIs and metadata +- Tools for creating new notes +- Prompts for generating summaries of notes + +## Features + +### Resources +- List and access notes via `note://` URIs +- Each note has a title, content and metadata +- Plain text mime type for simple content access + +### Tools +- `create_note` - Create new text notes + - Takes title and content as required parameters + - Stores note in server state + +### Prompts +- `summarize_notes` - Generate a summary of all stored notes + - Includes all note contents as embedded resources + - Returns structured prompt for LLM summarization + +## Development + +Install dependencies: +```bash +npm install +``` + +Build the server: +```bash +npm run build +``` + +For development with auto-rebuild: +```bash +npm run watch +``` + +## Installation + +To use with Claude Desktop, add the server config: + +On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +On Windows: `%APPDATA%/Claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "gitea-server": { + "command": "/path/to/gitea-server/build/index.js" + } + } +} +``` + +### Debugging + +Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: + +```bash +npm run inspector +``` + +The Inspector will provide a URL to access debugging tools in your browser. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ecad429 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,288 @@ +{ + "name": "gitea-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitea-server", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "axios": "^1.7.9" + }, + "bin": { + "gitea-server": "build/index.js" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", + "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@types/node": { + "version": "20.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", + "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..80a36ec --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "gitea-server", + "version": "0.1.0", + "description": ""An MCP server providing access to gitea.r8z.us"", + "private": true, + "type": "module", + "bin": { + "gitea-server": "./build/index.js" + }, + "files": [ + "build" + ], + "scripts": { + "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", + "prepare": "npm run build", + "watch": "tsc --watch", + "inspector": "npx @modelcontextprotocol/inspector build/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "axios": "^1.7.9" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9bf377a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,512 @@ +#!/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; +if (!GITEA_TOKEN) { + throw new Error('GITEA_TOKEN environment variable is required'); +} +const GITEA_API_URL = 'https://gitea.r8z.us/api/v1'; + +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); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a14bee0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}