517 lines
15 KiB
JavaScript
517 lines
15 KiB
JavaScript
#!/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);
|