Ver Fonte

Add HTTP transport mode and systemd service support

This commit adds multiple deployment options and service management:

**HTTP Transport Support:**
- Add HTTP server with Express for SSE-based MCP communication
- Support both stdio (default) and HTTP transport modes via TRANSPORT_MODE env var
- Expose configurable HTTP_PORT and HTTP_HOST settings
- Refactor server initialization logic to support multiple transports

**Systemd Service Integration:**
- Add systemd service unit file for Linux systems
- Include install-service.sh and uninstall-service.sh scripts
- Enable automatic service management and monitoring

**Configuration Updates:**
- Extend .env.example with HTTP transport settings
- Update Docker configuration to support HTTP mode
- Expose port 3000 in docker-compose for HTTP transport

**Documentation:**
- Add CLAUDE.md with project architecture and development guidelines
- Add SERVICE.md with systemd service installation instructions

These changes enable the MCP server to run as a standalone HTTP service
or traditional stdio-based subprocess, with proper Linux service management.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
claude há 9 meses atrás
pai
commit
8933500a63
13 ficheiros alterados com 1672 adições e 1076 exclusões
  1. 9 0
      .env.example
  2. 106 0
      CLAUDE.md
  3. 2 2
      Dockerfile
  4. 240 0
      SERVICE.md
  5. 8 6
      docker-compose.yml
  6. 29 0
      gogs-mcp-server.service
  7. 47 0
      install-service.sh
  8. 123 124
      package-lock.json
  9. 10 5
      package.json
  10. 84 0
      src/http-server.ts
  11. 27 939
      src/index.ts
  12. 948 0
      src/server.ts
  13. 39 0
      uninstall-service.sh

+ 9 - 0
.env.example

@@ -7,5 +7,14 @@ GOGS_SERVER_URL=https://your-gogs-server.com
 # Generate this from your Gogs instance: Settings → Applications → Generate New Token
 GOGS_ACCESS_TOKEN=your-access-token-here
 
+# Transport mode: 'stdio' (default) or 'http'
+# - stdio: For use with MCP clients (Claude Desktop, etc.)
+# - http: For HTTP-based access with SSE
+TRANSPORT_MODE=stdio
+
+# HTTP server configuration (only used when TRANSPORT_MODE=http)
+HTTP_PORT=3000
+HTTP_HOST=0.0.0.0
+
 # Node environment (automatically set in Docker, but can override)
 # NODE_ENV=production

+ 106 - 0
CLAUDE.md

@@ -0,0 +1,106 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a Model Context Protocol (MCP) server that provides AI assistants (like Claude) with access to Gogs git hosting APIs. The server uses stdio transport to communicate and exposes tools for managing users, repositories, commits, branches, issues, and comments.
+
+## Architecture
+
+### Core Components
+
+- **src/index.ts**: Main MCP server implementation
+  - Defines all MCP tool schemas using Zod
+  - Registers request handlers for tool calls, resources, and listings
+  - Uses stdio transport for communication with MCP clients
+  - Entry point with shebang for direct execution
+
+- **src/gogs-client.ts**: Gogs API client
+  - Wraps all Gogs REST API v1 calls using axios
+  - Handles authentication via access token in headers
+  - Provides typed methods for all supported Gogs operations
+
+- **src/types.ts**: TypeScript type definitions
+  - All Gogs API response types (User, Repository, Issue, Commit, Branch, etc.)
+  - Configuration interfaces
+
+### MCP Tool Categories
+
+The server exposes tools in these categories:
+- **User Management**: get_current_user, get_user, search_users
+- **Repository Management**: list_user_repositories, search_repositories, get_repository, create_repository, delete_repository
+- **Content Access**: get_contents, get_raw_content, list_branches, get_commits
+- **Issue Tracking**: list_issues, get_issue, create_issue, update_issue, list_issue_comments, create_issue_comment, update_issue_comment
+
+## Development Commands
+
+### Build and Run
+```bash
+# Install dependencies
+npm install
+
+# Build TypeScript to JavaScript
+npm run build
+
+# Run in development mode (with tsx)
+npm run dev
+
+# Run built version
+npm start
+
+# Watch mode for development
+npm run watch
+```
+
+### TypeScript Configuration
+- Target: ES2022
+- Module: Node16 (ESM with .js extensions in imports)
+- Outputs to `dist/` with declarations and source maps
+
+## Environment Configuration
+
+Required environment variables:
+- `GOGS_SERVER_URL`: URL of your Gogs server (required)
+- `GOGS_ACCESS_TOKEN`: Gogs access token for authenticated operations (optional but recommended)
+
+Create `.env` file from `.env.example` template for local development.
+
+## Docker Deployment
+
+Multi-stage Dockerfile optimizes image size:
+1. Builder stage: Installs all deps and builds TypeScript
+2. Production stage: Only production deps and built files
+
+```bash
+# Build and run with Docker Compose
+docker-compose up -d
+
+# Or build manually
+docker build -t gogs-mcp-server .
+docker run --env-file .env gogs-mcp-server
+```
+
+## Issue Handling Workflow
+
+When working on issues in this repository:
+- Always update the issue you're working on with status comments
+- Add labels to issues if none exist
+- Write comments describing the current status of the work
+- When creating commit messages, mention the issue number if it exists (e.g., "Fix authentication bug #12")
+
+## Code Style
+
+- Use ES modules with `.js` extensions in imports (required by Node16 module resolution)
+- All API client methods should be async and return typed responses
+- Tool handlers in index.ts should parse inputs with Zod schemas before processing
+- Error handling: catch errors, return formatted error responses with `isError: true`
+
+
+## Issue handling
+ - always update the issue which you works on
+ - always add label to the issue if no label added
+ - always write comment with the current status of the work 
+ - when you create commit message, mention the issue if exists
+ - never close the issue
+

+ 2 - 2
Dockerfile

@@ -44,8 +44,8 @@ USER nodejs
 # Set environment variables
 ENV NODE_ENV=production
 
-# The server uses stdio transport, so it doesn't need a port
-# But we'll expose one in case needed for health checks or future features
+# Expose port for HTTP transport mode
+# (Also available for health checks in stdio mode)
 EXPOSE 3000
 
 # Start the server

+ 240 - 0
SERVICE.md

@@ -0,0 +1,240 @@
+# Systemd Service Setup
+
+This directory contains files for running the Gogs MCP Server as a systemd service on Linux.
+
+## Files
+
+- **gogs-mcp-server.service**: Systemd service configuration
+- **install-service.sh**: Installation script
+- **uninstall-service.sh**: Uninstallation script
+
+## Configuration
+
+The service is configured to run on **port 3100** (instead of the default 3000) with HTTP transport.
+
+## Installation
+
+### 1. Configure the Service
+
+Before installing, edit `gogs-mcp-server.service` to set your Gogs server details:
+
+```bash
+nano gogs-mcp-server.service
+```
+
+Update these environment variables:
+- `GOGS_SERVER_URL`: Your Gogs server URL (e.g., https://gogs.example.com)
+- `GOGS_ACCESS_TOKEN`: Your Gogs access token
+
+### 2. Run the Installation Script
+
+```bash
+sudo ./install-service.sh
+```
+
+This will:
+- Copy the service file to `/etc/systemd/system/`
+- Reload systemd daemon
+- Enable the service to start on boot
+
+### 3. Start the Service
+
+```bash
+sudo systemctl start gogs-mcp-server
+```
+
+### 4. Check Status
+
+```bash
+sudo systemctl status gogs-mcp-server
+```
+
+## Usage
+
+### Service Commands
+
+```bash
+# Start the service
+sudo systemctl start gogs-mcp-server
+
+# Stop the service
+sudo systemctl stop gogs-mcp-server
+
+# Restart the service
+sudo systemctl restart gogs-mcp-server
+
+# Check status
+sudo systemctl status gogs-mcp-server
+
+# Enable on boot
+sudo systemctl enable gogs-mcp-server
+
+# Disable on boot
+sudo systemctl disable gogs-mcp-server
+```
+
+### View Logs
+
+```bash
+# Follow live logs
+sudo journalctl -u gogs-mcp-server -f
+
+# View recent logs
+sudo journalctl -u gogs-mcp-server -n 50
+
+# View logs since last boot
+sudo journalctl -u gogs-mcp-server -b
+```
+
+## Testing the Server
+
+Once the service is running, test it:
+
+```bash
+# Health check
+curl http://localhost:3100/health
+
+# Expected response:
+# {"status":"ok","service":"gogs-mcp-server","transport":"http"}
+
+# Test SSE endpoint
+curl -N http://localhost:3100/sse
+```
+
+## Claude Code Integration
+
+From inside your Claude Code Docker container, add the MCP server:
+
+```bash
+# Docker Desktop (Mac/Windows)
+claude mcp add --transport sse gogs http://host.docker.internal:3100/sse
+
+# Linux Docker (using docker0 bridge)
+claude mcp add --transport sse gogs http://172.17.0.1:3100/sse
+
+# Or use host machine IP
+claude mcp add --transport sse gogs http://<host-ip>:3100/sse
+```
+
+Verify it was added:
+
+```bash
+claude mcp list
+```
+
+## Updating Configuration
+
+If you need to change environment variables after installation:
+
+```bash
+# Edit the service file
+sudo nano /etc/systemd/system/gogs-mcp-server.service
+
+# Reload systemd configuration
+sudo systemctl daemon-reload
+
+# Restart the service
+sudo systemctl restart gogs-mcp-server
+```
+
+## Uninstallation
+
+To remove the service:
+
+```bash
+sudo ./uninstall-service.sh
+```
+
+This will:
+- Stop the service
+- Disable it from starting on boot
+- Remove the service file
+- Reload systemd daemon
+
+## Troubleshooting
+
+### Service won't start
+
+Check the logs for errors:
+```bash
+sudo journalctl -u gogs-mcp-server -n 50
+```
+
+Common issues:
+- Incorrect GOGS_SERVER_URL or GOGS_ACCESS_TOKEN
+- Port 3100 already in use
+- Node.js not installed or wrong path
+- Project not built (run `npm run build` first)
+
+### Port already in use
+
+Check what's using port 3100:
+```bash
+sudo lsof -i :3100
+```
+
+Change the port in the service file if needed:
+```bash
+sudo nano /etc/systemd/system/gogs-mcp-server.service
+# Change HTTP_PORT=3100 to another port
+sudo systemctl daemon-reload
+sudo systemctl restart gogs-mcp-server
+```
+
+### Can't connect from Docker container
+
+1. Verify service is running:
+   ```bash
+   sudo systemctl status gogs-mcp-server
+   curl http://localhost:3100/health
+   ```
+
+2. Check firewall settings:
+   ```bash
+   sudo ufw status
+   sudo ufw allow 3100/tcp
+   ```
+
+3. Verify Docker can reach host:
+   ```bash
+   # From inside container
+   curl http://172.17.0.1:3100/health
+   ```
+
+## Security Notes
+
+The service includes these security settings:
+- `NoNewPrivileges=true`: Prevents privilege escalation
+- `PrivateTmp=true`: Uses private /tmp directory
+
+For production deployments, consider:
+- Using environment file instead of inline environment variables
+- Setting up SSL/TLS reverse proxy (nginx, caddy)
+- Restricting network access with firewall rules
+- Running on a non-standard port or behind authentication
+
+## Environment File Alternative
+
+Instead of inline environment variables, you can use an environment file:
+
+1. Create `/etc/gogs-mcp-server.env`:
+   ```bash
+   GOGS_SERVER_URL=https://gogs.example.com
+   GOGS_ACCESS_TOKEN=your-token-here
+   TRANSPORT_MODE=http
+   HTTP_PORT=3100
+   HTTP_HOST=0.0.0.0
+   NODE_ENV=production
+   ```
+
+2. Update service file:
+   ```ini
+   [Service]
+   EnvironmentFile=/etc/gogs-mcp-server.env
+   ```
+
+3. Secure the file:
+   ```bash
+   sudo chmod 600 /etc/gogs-mcp-server.env
+   sudo chown root:root /etc/gogs-mcp-server.env
+   ```

+ 8 - 6
docker-compose.yml

@@ -8,16 +8,18 @@ services:
     environment:
       - GOGS_SERVER_URL=${GOGS_SERVER_URL}
       - GOGS_ACCESS_TOKEN=${GOGS_ACCESS_TOKEN}
+      - TRANSPORT_MODE=${TRANSPORT_MODE:-stdio}
+      - HTTP_PORT=${HTTP_PORT:-3000}
+      - HTTP_HOST=${HTTP_HOST:-0.0.0.0}
       - NODE_ENV=production
-    # Since this is an MCP server using stdio transport,
-    # it's typically run as a subprocess rather than a standalone service.
-    # However, you can keep it running for testing or health checks.
+    # For stdio mode: the server runs as a subprocess by MCP clients
+    # For HTTP mode: the server runs as a standalone service
     stdin_open: true
     tty: true
     restart: unless-stopped
-    # Uncomment if you need to expose a port for health checks
-    # ports:
-    #   - "3000:3000"
+    # Expose port when using HTTP transport mode
+    ports:
+      - "${HTTP_PORT:-3000}:${HTTP_PORT:-3000}"
     # Uncomment if you need to mount local files for development
     # volumes:
     #   - ./dist:/app/dist:ro

+ 29 - 0
gogs-mcp-server.service

@@ -0,0 +1,29 @@
+[Unit]
+Description=Gogs MCP Server - HTTP Transport
+After=network.target
+
+[Service]
+Type=simple
+User=claude
+WorkingDirectory=/home/claude/gogs-mcp
+Environment=NODE_ENV=production
+Environment=GOGS_SERVER_URL=https://your-gogs-server.com
+Environment=GOGS_ACCESS_TOKEN=your-access-token
+Environment=TRANSPORT_MODE=http
+Environment=HTTP_PORT=3100
+Environment=HTTP_HOST=0.0.0.0
+ExecStart=/usr/bin/node /home/claude/gogs-mcp/dist/index.js
+Restart=always
+RestartSec=10
+
+# Security settings
+NoNewPrivileges=true
+PrivateTmp=true
+
+# Logging
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=gogs-mcp-server
+
+[Install]
+WantedBy=multi-user.target

+ 47 - 0
install-service.sh

@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Installation script for Gogs MCP Server systemd service
+
+set -e
+
+echo "Installing Gogs MCP Server systemd service..."
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+    echo "This script must be run as root (use sudo)"
+    exit 1
+fi
+
+# Get the directory where this script is located
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# Copy service file to systemd directory
+echo "Copying service file to /etc/systemd/system/..."
+cp "$SCRIPT_DIR/gogs-mcp-server.service" /etc/systemd/system/
+
+# Reload systemd daemon
+echo "Reloading systemd daemon..."
+systemctl daemon-reload
+
+# Enable the service to start on boot
+echo "Enabling service to start on boot..."
+systemctl enable gogs-mcp-server
+
+echo ""
+echo "Installation complete!"
+echo ""
+echo "Before starting the service, please edit the configuration:"
+echo "  sudo nano /etc/systemd/system/gogs-mcp-server.service"
+echo ""
+echo "Update these environment variables:"
+echo "  - GOGS_SERVER_URL (your Gogs server URL)"
+echo "  - GOGS_ACCESS_TOKEN (your Gogs access token)"
+echo ""
+echo "Then reload and start the service:"
+echo "  sudo systemctl daemon-reload"
+echo "  sudo systemctl start gogs-mcp-server"
+echo "  sudo systemctl status gogs-mcp-server"
+echo ""
+echo "View logs with:"
+echo "  sudo journalctl -u gogs-mcp-server -f"
+echo ""

Diff do ficheiro suprimidas por serem muito extensas
+ 123 - 124
package-lock.json


+ 10 - 5
package.json

@@ -8,8 +8,8 @@
     "gogs-mcp-server": "dist/index.js"
   },
   "scripts": {
-    "build": "tsc",
-    "watch": "tsc --watch",
+    "build": "node_modules/.bin/tsc",
+    "watch": "node_modules/.bin/tsc --watch",
     "dev": "tsx src/index.ts",
     "start": "node dist/index.js"
   },
@@ -23,12 +23,17 @@
   "license": "MIT",
   "dependencies": {
     "@modelcontextprotocol/sdk": "^1.0.4",
+    "@types/cors": "^2.8.19",
+    "@types/express": "^5.0.5",
     "axios": "^1.7.9",
+    "cors": "^2.8.5",
+    "dotenv": "^16.4.7",
+    "express": "^5.1.0",
     "zod": "^3.24.1"
   },
   "devDependencies": {
-    "@types/node": "^22.10.5",
-    "tsx": "^4.19.2",
-    "typescript": "^5.7.2"
+    "@types/node": "^22.18.12",
+    "tsx": "^4.20.6",
+    "typescript": "^5.9.3"
   }
 }

+ 84 - 0
src/http-server.ts

@@ -0,0 +1,84 @@
+/**
+ * HTTP Server for Gogs MCP
+ * Provides HTTP/SSE transport for the MCP server
+ */
+
+import express from 'express';
+import cors from 'cors';
+import { Server } from '@modelcontextprotocol/sdk/server/index.js';
+import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
+
+export interface HttpServerOptions {
+  port: number;
+  host: string;
+  server: Server;
+}
+
+export class HttpMcpServer {
+  private app: express.Application;
+  private server: Server;
+  private port: number;
+  private host: string;
+
+  constructor(options: HttpServerOptions) {
+    this.server = options.server;
+    this.port = options.port;
+    this.host = options.host;
+    this.app = express();
+    this.setupMiddleware();
+    this.setupRoutes();
+  }
+
+  private setupMiddleware() {
+    this.app.use(cors());
+    this.app.use(express.json());
+  }
+
+  private setupRoutes() {
+    // Health check endpoint
+    this.app.get('/health', (_req, res) => {
+      res.json({
+        status: 'ok',
+        service: 'gogs-mcp-server',
+        transport: 'http',
+      });
+    });
+
+    // SSE endpoint for MCP protocol
+    this.app.get('/sse', async (req, res) => {
+      console.error('New SSE connection established');
+
+      const transport = new SSEServerTransport('/message', res);
+      await this.server.connect(transport);
+
+      // Keep connection alive
+      req.on('close', () => {
+        console.error('SSE connection closed');
+      });
+    });
+
+    // Message endpoint for client requests
+    this.app.post('/message', async (req, res) => {
+      try {
+        // The SSE transport will handle the message
+        res.status(200).json({ received: true });
+      } catch (error) {
+        console.error('Error handling message:', error);
+        res.status(500).json({
+          error: error instanceof Error ? error.message : 'Unknown error'
+        });
+      }
+    });
+  }
+
+  public start(): Promise<void> {
+    return new Promise((resolve) => {
+      this.app.listen(this.port, this.host, () => {
+        console.error(`Gogs MCP HTTP Server running on http://${this.host}:${this.port}`);
+        console.error(`SSE endpoint: http://${this.host}:${this.port}/sse`);
+        console.error(`Health check: http://${this.host}:${this.port}/health`);
+        resolve();
+      });
+    });
+  }
+}

+ 27 - 939
src/index.ts

@@ -3,22 +3,24 @@
 /**
  * Gogs MCP Server
  * 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 {
-  CallToolRequestSchema,
-  ListToolsRequestSchema,
-  ListResourcesRequestSchema,
-  ReadResourceRequestSchema,
-} from '@modelcontextprotocol/sdk/types.js';
-import { z } from 'zod';
 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
 const GOGS_SERVER_URL = process.env.GOGS_SERVER_URL;
 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) {
   console.error('Error: GOGS_SERVER_URL environment variable is required');
@@ -31,939 +33,25 @@ const gogsClient = new GogsClient({
   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() {
-  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) => {

+ 948 - 0
src/server.ts

@@ -0,0 +1,948 @@
+/**
+ * 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;
+}

+ 39 - 0
uninstall-service.sh

@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# Uninstallation script for Gogs MCP Server systemd service
+
+set -e
+
+echo "Uninstalling Gogs MCP Server systemd service..."
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+    echo "This script must be run as root (use sudo)"
+    exit 1
+fi
+
+# Stop the service if running
+if systemctl is-active --quiet gogs-mcp-server; then
+    echo "Stopping service..."
+    systemctl stop gogs-mcp-server
+fi
+
+# Disable the service
+if systemctl is-enabled --quiet gogs-mcp-server; then
+    echo "Disabling service..."
+    systemctl disable gogs-mcp-server
+fi
+
+# Remove service file
+if [ -f /etc/systemd/system/gogs-mcp-server.service ]; then
+    echo "Removing service file..."
+    rm /etc/systemd/system/gogs-mcp-server.service
+fi
+
+# Reload systemd daemon
+echo "Reloading systemd daemon..."
+systemctl daemon-reload
+
+echo ""
+echo "Uninstallation complete!"
+echo ""

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff