|
@@ -3,22 +3,24 @@
|
|
|
/**
|
|
/**
|
|
|
* Gogs MCP Server
|
|
* Gogs MCP Server
|
|
|
* Provides Model Context Protocol server for Gogs git hosting
|
|
* Provides Model Context Protocol server for Gogs git hosting
|
|
|
|
|
+ * Supports both stdio and HTTP transports
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
-import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
|
|
|
|
+import { config } from 'dotenv';
|
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
|
-import {
|
|
|
|
|
- CallToolRequestSchema,
|
|
|
|
|
- ListToolsRequestSchema,
|
|
|
|
|
- ListResourcesRequestSchema,
|
|
|
|
|
- ReadResourceRequestSchema,
|
|
|
|
|
-} from '@modelcontextprotocol/sdk/types.js';
|
|
|
|
|
-import { z } from 'zod';
|
|
|
|
|
import { GogsClient } from './gogs-client.js';
|
|
import { GogsClient } from './gogs-client.js';
|
|
|
|
|
+import { createMcpServer } from './server.js';
|
|
|
|
|
+import { HttpMcpServer } from './http-server.js';
|
|
|
|
|
+
|
|
|
|
|
+// Load environment variables from .env file
|
|
|
|
|
+config();
|
|
|
|
|
|
|
|
// Get configuration from environment variables
|
|
// Get configuration from environment variables
|
|
|
const GOGS_SERVER_URL = process.env.GOGS_SERVER_URL;
|
|
const GOGS_SERVER_URL = process.env.GOGS_SERVER_URL;
|
|
|
const GOGS_ACCESS_TOKEN = process.env.GOGS_ACCESS_TOKEN;
|
|
const GOGS_ACCESS_TOKEN = process.env.GOGS_ACCESS_TOKEN;
|
|
|
|
|
+const TRANSPORT_MODE = process.env.TRANSPORT_MODE || 'stdio';
|
|
|
|
|
+const HTTP_PORT = parseInt(process.env.HTTP_PORT || '3000', 10);
|
|
|
|
|
+const HTTP_HOST = process.env.HTTP_HOST || '0.0.0.0';
|
|
|
|
|
|
|
|
if (!GOGS_SERVER_URL) {
|
|
if (!GOGS_SERVER_URL) {
|
|
|
console.error('Error: GOGS_SERVER_URL environment variable is required');
|
|
console.error('Error: GOGS_SERVER_URL environment variable is required');
|
|
@@ -31,939 +33,25 @@ const gogsClient = new GogsClient({
|
|
|
accessToken: GOGS_ACCESS_TOKEN,
|
|
accessToken: GOGS_ACCESS_TOKEN,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// Create MCP server
|
|
|
|
|
-const server = new Server(
|
|
|
|
|
- {
|
|
|
|
|
- name: 'gogs-mcp-server',
|
|
|
|
|
- version: '1.0.0',
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- capabilities: {
|
|
|
|
|
- tools: {},
|
|
|
|
|
- resources: {},
|
|
|
|
|
- },
|
|
|
|
|
- }
|
|
|
|
|
-);
|
|
|
|
|
-
|
|
|
|
|
-// Define tool schemas
|
|
|
|
|
-const GetUserSchema = z.object({
|
|
|
|
|
- username: z.string().describe('Username to get information for'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const SearchUsersSchema = z.object({
|
|
|
|
|
- query: z.string().describe('Search query for users'),
|
|
|
|
|
- limit: z.number().optional().describe('Maximum number of results (default: 10)'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const ListUserReposSchema = z.object({
|
|
|
|
|
- username: z.string().optional().describe('Username (omit for authenticated user)'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const SearchReposSchema = z.object({
|
|
|
|
|
- query: z.string().describe('Search query for repositories'),
|
|
|
|
|
- uid: z.number().optional().describe('User ID to filter repositories'),
|
|
|
|
|
- limit: z.number().optional().describe('Maximum number of results (default: 10)'),
|
|
|
|
|
- page: z.number().optional().describe('Page number (default: 1)'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const GetRepoSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const CreateRepoSchema = z.object({
|
|
|
|
|
- name: z.string().describe('Repository name'),
|
|
|
|
|
- description: z.string().optional().describe('Repository description'),
|
|
|
|
|
- private: z.boolean().optional().describe('Create as private repository (default: false)'),
|
|
|
|
|
- auto_init: z.boolean().optional().describe('Initialize with README (default: false)'),
|
|
|
|
|
- gitignores: z.string().optional().describe('Gitignore templates (e.g., "Go,VisualStudioCode")'),
|
|
|
|
|
- license: z.string().optional().describe('License template (e.g., "MIT License")'),
|
|
|
|
|
- readme: z.string().optional().describe('README template (default: "Default")'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const DeleteRepoSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const GetContentsSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- path: z.string().describe('File or directory path'),
|
|
|
|
|
- ref: z.string().optional().describe('Branch, tag, or commit SHA (default: default branch)'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const GetRawContentSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- ref: z.string().describe('Branch, tag, or commit SHA'),
|
|
|
|
|
- path: z.string().describe('File path'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const ListBranchesSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const GetCommitsSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- sha: z.string().optional().describe('Branch name or commit SHA'),
|
|
|
|
|
- page: z.number().optional().describe('Page number (default: 1)'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const ListIssuesSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- state: z.enum(['open', 'closed', 'all']).optional().describe('Filter by state (default: open)'),
|
|
|
|
|
- labels: z.string().optional().describe('Comma-separated list of label names'),
|
|
|
|
|
- page: z.number().optional().describe('Page number (default: 1)'),
|
|
|
|
|
- per_page: z.number().optional().describe('Results per page (default: 30)'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const GetIssueSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- number: z.number().describe('Issue number'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const CreateIssueSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- title: z.string().describe('Issue title'),
|
|
|
|
|
- body: z.string().optional().describe('Issue description'),
|
|
|
|
|
- assignee: z.string().optional().describe('Username to assign the issue to'),
|
|
|
|
|
- milestone: z.number().optional().describe('Milestone ID'),
|
|
|
|
|
- labels: z.array(z.number()).optional().describe('Array of label IDs'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const UpdateIssueSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- number: z.number().describe('Issue number'),
|
|
|
|
|
- title: z.string().optional().describe('Issue title'),
|
|
|
|
|
- body: z.string().optional().describe('Issue description'),
|
|
|
|
|
- assignee: z.string().optional().describe('Username to assign the issue to'),
|
|
|
|
|
- milestone: z.number().optional().describe('Milestone ID'),
|
|
|
|
|
- state: z.enum(['open', 'closed']).optional().describe('Issue state'),
|
|
|
|
|
- labels: z.array(z.number()).optional().describe('Array of label IDs'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const ListIssueCommentsSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- number: z.number().describe('Issue number'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const CreateIssueCommentSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- number: z.number().describe('Issue number'),
|
|
|
|
|
- body: z.string().describe('Comment text'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const UpdateIssueCommentSchema = z.object({
|
|
|
|
|
- owner: z.string().describe('Repository owner username'),
|
|
|
|
|
- repo: z.string().describe('Repository name'),
|
|
|
|
|
- commentId: z.number().describe('Comment ID'),
|
|
|
|
|
- body: z.string().describe('Updated comment text'),
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// Register tools
|
|
|
|
|
-server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
|
|
- return {
|
|
|
|
|
- tools: [
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_current_user',
|
|
|
|
|
- description: 'Get information about the authenticated user',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {},
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_user',
|
|
|
|
|
- description: 'Get information about a specific user',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- username: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Username to get information for',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['username'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'search_users',
|
|
|
|
|
- description: 'Search for users by username',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- query: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Search query for users',
|
|
|
|
|
- },
|
|
|
|
|
- limit: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Maximum number of results (default: 10)',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['query'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'list_user_repositories',
|
|
|
|
|
- description: 'List repositories for a user (or authenticated user if no username provided)',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- username: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Username (omit for authenticated user)',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'search_repositories',
|
|
|
|
|
- description: 'Search for repositories',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- query: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Search query for repositories',
|
|
|
|
|
- },
|
|
|
|
|
- uid: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'User ID to filter repositories',
|
|
|
|
|
- },
|
|
|
|
|
- limit: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Maximum number of results (default: 10)',
|
|
|
|
|
- },
|
|
|
|
|
- page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Page number (default: 1)',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['query'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_repository',
|
|
|
|
|
- description: 'Get information about a specific repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'create_repository',
|
|
|
|
|
- description: 'Create a new repository for the authenticated user',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- name: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- description: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository description',
|
|
|
|
|
- },
|
|
|
|
|
- private: {
|
|
|
|
|
- type: 'boolean',
|
|
|
|
|
- description: 'Create as private repository (default: false)',
|
|
|
|
|
- },
|
|
|
|
|
- auto_init: {
|
|
|
|
|
- type: 'boolean',
|
|
|
|
|
- description: 'Initialize with README (default: false)',
|
|
|
|
|
- },
|
|
|
|
|
- gitignores: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Gitignore templates (e.g., "Go,VisualStudioCode")',
|
|
|
|
|
- },
|
|
|
|
|
- license: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'License template (e.g., "MIT License")',
|
|
|
|
|
- },
|
|
|
|
|
- readme: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'README template (default: "Default")',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['name'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'delete_repository',
|
|
|
|
|
- description: 'Delete a repository (requires admin access)',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_contents',
|
|
|
|
|
- description: 'Get file or directory contents from a repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- path: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'File or directory path',
|
|
|
|
|
- },
|
|
|
|
|
- ref: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Branch, tag, or commit SHA (default: default branch)',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'path'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_raw_content',
|
|
|
|
|
- description: 'Get raw file content from a repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- ref: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Branch, tag, or commit SHA',
|
|
|
|
|
- },
|
|
|
|
|
- path: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'File path',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'ref', 'path'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'list_branches',
|
|
|
|
|
- description: 'List all branches in a repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_commits',
|
|
|
|
|
- description: 'Get commit history from a repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- sha: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Branch name or commit SHA',
|
|
|
|
|
- },
|
|
|
|
|
- page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Page number (default: 1)',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'list_issues',
|
|
|
|
|
- description: 'List issues in a repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- state: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- enum: ['open', 'closed', 'all'],
|
|
|
|
|
- description: 'Filter by state (default: open)',
|
|
|
|
|
- },
|
|
|
|
|
- labels: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Comma-separated list of label names',
|
|
|
|
|
- },
|
|
|
|
|
- page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Page number (default: 1)',
|
|
|
|
|
- },
|
|
|
|
|
- per_page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Results per page (default: 30)',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'get_issue',
|
|
|
|
|
- description: 'Get a specific issue by number',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- number: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Issue number',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'number'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'create_issue',
|
|
|
|
|
- description: 'Create a new issue in a repository',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- title: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Issue title',
|
|
|
|
|
- },
|
|
|
|
|
- body: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Issue description',
|
|
|
|
|
- },
|
|
|
|
|
- assignee: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Username to assign the issue to',
|
|
|
|
|
- },
|
|
|
|
|
- milestone: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Milestone ID',
|
|
|
|
|
- },
|
|
|
|
|
- labels: {
|
|
|
|
|
- type: 'array',
|
|
|
|
|
- items: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- },
|
|
|
|
|
- description: 'Array of label IDs',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'title'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'update_issue',
|
|
|
|
|
- description: 'Update an existing issue (including closing or reopening)',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- number: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Issue number',
|
|
|
|
|
- },
|
|
|
|
|
- title: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Issue title',
|
|
|
|
|
- },
|
|
|
|
|
- body: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Issue description',
|
|
|
|
|
- },
|
|
|
|
|
- assignee: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Username to assign the issue to',
|
|
|
|
|
- },
|
|
|
|
|
- milestone: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Milestone ID',
|
|
|
|
|
- },
|
|
|
|
|
- state: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- enum: ['open', 'closed'],
|
|
|
|
|
- description: 'Issue state',
|
|
|
|
|
- },
|
|
|
|
|
- labels: {
|
|
|
|
|
- type: 'array',
|
|
|
|
|
- items: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- },
|
|
|
|
|
- description: 'Array of label IDs',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'number'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'list_issue_comments',
|
|
|
|
|
- description: 'List all comments on an issue',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- number: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Issue number',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'number'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'create_issue_comment',
|
|
|
|
|
- description: 'Add a comment to an issue',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- number: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Issue number',
|
|
|
|
|
- },
|
|
|
|
|
- body: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Comment text',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'number', 'body'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'update_issue_comment',
|
|
|
|
|
- description: 'Edit an existing comment on an issue',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- owner: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository owner username',
|
|
|
|
|
- },
|
|
|
|
|
- repo: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Repository name',
|
|
|
|
|
- },
|
|
|
|
|
- commentId: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Comment ID',
|
|
|
|
|
- },
|
|
|
|
|
- body: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Updated comment text',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['owner', 'repo', 'commentId', 'body'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// Handle tool calls
|
|
|
|
|
-server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
|
|
- try {
|
|
|
|
|
- const { name, arguments: args } = request.params;
|
|
|
|
|
-
|
|
|
|
|
- switch (name) {
|
|
|
|
|
- case 'get_current_user': {
|
|
|
|
|
- const user = await gogsClient.getCurrentUser();
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(user, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'get_user': {
|
|
|
|
|
- const { username } = GetUserSchema.parse(args);
|
|
|
|
|
- const user = await gogsClient.getUser(username);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(user, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'search_users': {
|
|
|
|
|
- const { query, limit } = SearchUsersSchema.parse(args);
|
|
|
|
|
- const users = await gogsClient.searchUsers(query, limit);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(users, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'list_user_repositories': {
|
|
|
|
|
- const { username } = ListUserReposSchema.parse(args);
|
|
|
|
|
- const repos = username
|
|
|
|
|
- ? await gogsClient.listUserRepositories(username)
|
|
|
|
|
- : await gogsClient.listMyRepositories();
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(repos, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'search_repositories': {
|
|
|
|
|
- const { query, uid, limit, page } = SearchReposSchema.parse(args);
|
|
|
|
|
- const repos = await gogsClient.searchRepositories(query, { uid, limit, page });
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(repos, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'get_repository': {
|
|
|
|
|
- const { owner, repo } = GetRepoSchema.parse(args);
|
|
|
|
|
- const repository = await gogsClient.getRepository(owner, repo);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(repository, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'create_repository': {
|
|
|
|
|
- const data = CreateRepoSchema.parse(args);
|
|
|
|
|
- const repo = await gogsClient.createRepository(data);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(repo, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'delete_repository': {
|
|
|
|
|
- const { owner, repo } = DeleteRepoSchema.parse(args);
|
|
|
|
|
- await gogsClient.deleteRepository(owner, repo);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: `Repository ${owner}/${repo} deleted successfully`,
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'get_contents': {
|
|
|
|
|
- const { owner, repo, path, ref } = GetContentsSchema.parse(args);
|
|
|
|
|
- const contents = await gogsClient.getContents(owner, repo, path, ref);
|
|
|
|
|
-
|
|
|
|
|
- // If it's a file with base64 content, decode it
|
|
|
|
|
- if (!Array.isArray(contents) && contents.type === 'file' && contents.content) {
|
|
|
|
|
- const decodedContent = Buffer.from(contents.content, 'base64').toString('utf-8');
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: `File: ${contents.path}\nSize: ${contents.size} bytes\nSHA: ${contents.sha}\n\nContent:\n${decodedContent}`,
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(contents, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'get_raw_content': {
|
|
|
|
|
- const { owner, repo, ref, path } = GetRawContentSchema.parse(args);
|
|
|
|
|
- const content = await gogsClient.getRawContent(owner, repo, ref, path);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: content,
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// Create MCP server with all tools and handlers
|
|
|
|
|
+const server = createMcpServer(gogsClient);
|
|
|
|
|
|
|
|
- case 'list_branches': {
|
|
|
|
|
- const { owner, repo } = ListBranchesSchema.parse(args);
|
|
|
|
|
- const branches = await gogsClient.listBranches(owner, repo);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(branches, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'get_commits': {
|
|
|
|
|
- const { owner, repo, sha, page } = GetCommitsSchema.parse(args);
|
|
|
|
|
- const commits = await gogsClient.getCommits(owner, repo, { sha, page });
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(commits, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'list_issues': {
|
|
|
|
|
- const { owner, repo, state, labels, page, per_page } = ListIssuesSchema.parse(args);
|
|
|
|
|
- const issues = await gogsClient.listIssues(owner, repo, { state, labels, page, per_page });
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(issues, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'get_issue': {
|
|
|
|
|
- const { owner, repo, number } = GetIssueSchema.parse(args);
|
|
|
|
|
- const issue = await gogsClient.getIssue(owner, repo, number);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(issue, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'create_issue': {
|
|
|
|
|
- const { owner, repo, title, body, assignee, milestone, labels } = CreateIssueSchema.parse(args);
|
|
|
|
|
- const issue = await gogsClient.createIssue(owner, repo, {
|
|
|
|
|
- title,
|
|
|
|
|
- body,
|
|
|
|
|
- assignee,
|
|
|
|
|
- milestone,
|
|
|
|
|
- labels,
|
|
|
|
|
- });
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(issue, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'update_issue': {
|
|
|
|
|
- const { owner, repo, number, title, body, assignee, milestone, state, labels } = UpdateIssueSchema.parse(args);
|
|
|
|
|
- const issue = await gogsClient.updateIssue(owner, repo, number, {
|
|
|
|
|
- title,
|
|
|
|
|
- body,
|
|
|
|
|
- assignee,
|
|
|
|
|
- milestone,
|
|
|
|
|
- state,
|
|
|
|
|
- labels,
|
|
|
|
|
- });
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(issue, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'list_issue_comments': {
|
|
|
|
|
- const { owner, repo, number } = ListIssueCommentsSchema.parse(args);
|
|
|
|
|
- const comments = await gogsClient.listIssueComments(owner, repo, number);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(comments, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'create_issue_comment': {
|
|
|
|
|
- const { owner, repo, number, body } = CreateIssueCommentSchema.parse(args);
|
|
|
|
|
- const comment = await gogsClient.createIssueComment(owner, repo, number, body);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(comment, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- case 'update_issue_comment': {
|
|
|
|
|
- const { owner, repo, commentId, body } = UpdateIssueCommentSchema.parse(args);
|
|
|
|
|
- const comment = await gogsClient.updateIssueComment(owner, repo, commentId, body);
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: JSON.stringify(comment, null, 2),
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- default:
|
|
|
|
|
- throw new Error(`Unknown tool: ${name}`);
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- if (error instanceof Error) {
|
|
|
|
|
- return {
|
|
|
|
|
- content: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'text',
|
|
|
|
|
- text: `Error: ${error.message}`,
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- isError: true,
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
- throw error;
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// Handle resources (optional - for browsing repository structure)
|
|
|
|
|
-server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
|
|
- return {
|
|
|
|
|
- resources: [],
|
|
|
|
|
- };
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
|
|
- throw new Error('Resource reading not yet implemented');
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// Start server
|
|
|
|
|
|
|
+// Start server with appropriate transport
|
|
|
async function main() {
|
|
async function main() {
|
|
|
- const transport = new StdioServerTransport();
|
|
|
|
|
- await server.connect(transport);
|
|
|
|
|
- console.error('Gogs MCP Server running on stdio');
|
|
|
|
|
|
|
+ if (TRANSPORT_MODE === 'http') {
|
|
|
|
|
+ // Start HTTP server
|
|
|
|
|
+ const httpServer = new HttpMcpServer({
|
|
|
|
|
+ server,
|
|
|
|
|
+ port: HTTP_PORT,
|
|
|
|
|
+ host: HTTP_HOST,
|
|
|
|
|
+ });
|
|
|
|
|
+ await httpServer.start();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Start stdio server (default)
|
|
|
|
|
+ const transport = new StdioServerTransport();
|
|
|
|
|
+ await server.connect(transport);
|
|
|
|
|
+ console.error('Gogs MCP Server running on stdio');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
main().catch((error) => {
|
|
main().catch((error) => {
|