| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948 |
- /**
- * MCP Server Setup
- * Contains all the server configuration, tool schemas, and request handlers
- */
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
- import {
- CallToolRequestSchema,
- ListToolsRequestSchema,
- ListResourcesRequestSchema,
- ReadResourceRequestSchema,
- } from '@modelcontextprotocol/sdk/types.js';
- import { z } from 'zod';
- import { GogsClient } from './gogs-client.js';
- // 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'),
- });
- /**
- * Create and configure an MCP server with all tools and handlers
- */
- export function createMcpServer(gogsClient: GogsClient): Server {
- const server = new Server(
- {
- name: 'gogs-mcp-server',
- version: '1.0.0',
- },
- {
- capabilities: {
- tools: {},
- resources: {},
- },
- }
- );
- // 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,
- },
- ],
- };
- }
- 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');
- });
- return server;
- }
|