diff --git a/Legalidad b/Legalidad new file mode 100644 index 000000000..c810293f3 --- /dev/null +++ b/Legalidad @@ -0,0 +1,391 @@ +GitHub Copilot Chat Assistant — Preparé un paquete (parches) con la estructura completa y el scaffold Android + workflow CI. Copia y ejecuta el siguiente script en tu máquina para generar el ZIP listo para subir o aplicar en un repo. El script crea la estructura, los ficheros principales, el scaffold Android, el workflow de Actions (assembleDebug) y un Makefile básico. + +Instrucciones rápidas +1. Crea un fichero llamado prepare_tokyo_patch.sh y pega el contenido que sigue. +2. Hazlo ejecutable: chmod +x prepare_tokyo_patch.sh +3. Ejecútalo: ./prepare_tokyo_patch.sh +4. Obtendrás tokyo-predictor.zip en la carpeta actual. Descomprímelo o súbelo a GitHub y haz push desde tu máquina. + +prepare_tokyo_patch.sh +#!/usr/bin/env bash +set -euo pipefail + +OUTDIR="tokyo-predictor" +ZIPFILE="${OUTDIR}.zip" +MODULE="github.com/modelcontextprotocol/tokyo-predictor" +YEAR="2025" +AUTHOR="Melampe001" + +rm -rf "${OUTDIR}" "${ZIPFILE}" +mkdir -p "${OUTDIR}" + +cd "${OUTDIR}" + +# Directories +mkdir -p cmd internal lib admin config docs proto ruby testing .github .github/instructions +mkdir -p android/app/src/main/java/com/example/tokyopredictor +mkdir -p android/app/src/main/res +mkdir -p .github/workflows + +# README +cat > README.md <<'EOF' +tokyo-predictor + +Initial scaffold for the Tokyo Predictor repository. +EOF + +# .gitignore (Go) +cat > .gitignore <<'EOF' +# Binaries for programs and plugins +bin/ +# Test binary, build outputs +*.test +# Go workspace file +go.work +# IDE/editor files +.vscode/ +.idea/ +# Android builds +android/app/build/ +android/.gradle/ +EOF + +# LICENSE (MIT) +cat > LICENSE < .github/CONTRIBUTING.md <<'EOF' +This repository is primarily a Go service with a Ruby client for some API endpoints. It is responsible for ingesting metered usage and recording that usage. Please follow these guidelines when contributing. + +Required before each commit +- Run: make fmt — runs gofmt on all Go files. +- Ensure tests pass locally: make test. +- Add tests for new logic. + +Development flow +- Build: make build +- Test: make test +- Full CI: make ci (includes build, fmt, lint, test) + +If you modify proto/ or ruby/ +- proto/: run `make proto` after changes. +- ruby/: increment ruby/lib/billing-platform/version.rb using semantic versioning. + +Repository structure +- cmd/: Main service entry points and executables +- internal/: Logic related to interactions with other GitHub services +- lib/: Core Go packages for billing logic +- admin/: Admin interface components +- config/: Configuration files and templates +- docs/: Documentation +- proto/: Protocol buffer definitions (run `make proto` after updates) +- ruby/: Ruby implementation components (bump version file when updated) +- testing/: Test helpers and fixtures + +Key guidelines +1. Follow idiomatic Go practices. +2. Preserve existing structure. +3. Use dependency injection patterns where appropriate. +4. Write unit tests (prefer table-driven). +5. Document public APIs and complex logic. + +How to open a PR +- Create a descriptive branch (feature/..., fix/...). +- Ensure you ran: make fmt, make build, make test, make ci, make proto (if relevant), update ruby version file if modifying ruby/. +- PR description should include what it changes, how to test locally, API impacts and versioning needs. + +PR checklist +- [ ] make fmt passed +- [ ] make build passed +- [ ] make test passed +- [ ] make ci passed +- [ ] Docs updated if applicable +- [ ] proto/ or ruby/ changes handled +- [ ] Tests added for new functionality +EOF + +# Makefile (basic targets matching your Code Standards) +cat > Makefile <<'EOF' +.PHONY: fmt build test ci proto + +fmt: +\t@gofmt -w . + +build: +\t@go build ./... + +test: +\t@go test ./... + +ci: fmt test + +proto: +\t@echo "Run codegen for proto files (not implemented)." +EOF + +# Placeholder go.mod +cat > go.mod < android/settings.gradle <<'EOF' +rootProject.name = "TokyoPredictor" +include ':app' +EOF + +cat > android/build.gradle <<'EOF' +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:7.4.2" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} +EOF + +cat > android/gradle.properties <<'EOF' +org.gradle.jvmargs=-Xmx1536m +android.useAndroidX=true +android.enableJetifier=true +EOF + +cat > android/app/build.gradle <<'EOF' +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdk 33 + + defaultConfig { + applicationId "com.example.tokyopredictor" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "0.1" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0" + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} +EOF + +cat > android/app/src/main/AndroidManifest.xml <<'EOF' + + + + + + + + + + + + +EOF + +cat > android/app/src/main/java/com/example/tokyopredictor/MainActivity.kt <<'EOF' +package com.example.tokyopredictor + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.widget.TextView + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val tv = TextView(this) + tv.text = "Tokyo Predictor - App Scaffold" + tv.textSize = 20f + setContentView(tv) + } +} +EOF + +cat > android/app/proguard-rules.pro <<'EOF' +# Keep default rules (empty stub for now) +EOF + +# GitHub Actions workflow for Android build (assembleDebug) +cat > .github/workflows/android-build.yml <<'EOF' +name: Android Build (assembleDebug) + +on: + push: + branches: + - ci/android-build + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + env: + ANDROID_SDK_ROOT: ${{ runner.temp }}/android-sdk + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + + - name: Install Android SDK command-line tools & platforms + run: | + sudo apt-get update + sudo apt-get install -y wget unzip + mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" + wget -q -O /tmp/cmdline-tools.zip "https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip" + unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_SDK_ROOT/cmdline-tools" + mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest" || true + yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_SDK_ROOT" "platform-tools" "platforms;android-33" "build-tools;33.0.2" + + - name: Make gradlew executable + run: | + if [ -f ./android/gradlew ]; then chmod +x ./android/gradlew; fi + + - name: Build debug APK + working-directory: android + run: | + if [ -f ./gradlew ]; then ./gradlew assembleDebug --stacktrace; else gradle assembleDebug --no-daemon --stacktrace; fi + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: tokyopredictor-debug-apk + path: android/app/build/outputs/apk/debug/*.apk +EOF + +# optional: add a simple repo-level CI workflow (Go) - basic check +cat > .github/workflows/go-ci.yml <<'EOF' +name: Go CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + - name: Run fmt + run: make fmt || true + - name: Run tests + run: make test +EOF + +# Add .github/instructions placeholder and original Fiel if needed +cat > .github/instructions/Fiel <<'EOF' +# Original instructions placeholder (migrated to CONTRIBUTING.md) +EOF + +# finalize +git init -b main >/dev/null 2>&1 || true +git add -A >/dev/null 2>&1 || true +git commit -m "chore: prepare patch scaffold (initial files + Android scaffold + CI workflows)" >/dev/null 2>&1 || true +cd .. + +zip -r "${ZIPFILE}" "${OUTDIR}" >/dev/null + +echo "Created ${ZIPFILE} with the scaffold." + +Fin. + +Qué contiene el ZIP +- README.md, LICENSE, .gitignore +- .github/CONTRIBUTING.md (con tus Code Standards) +- Makefile con targets: fmt, build, test, ci, proto +- Estructura de carpetas: cmd/, internal/, lib/, admin/, config/, docs/, proto/, ruby/, testing/ +- Scaffold Android completo dentro de android/ (app module, manifest, MainActivity.kt) +- Workflow CI: .github/workflows/android-build.yml (genera APK debug y sube artifact) +- Workflow Go CI básico: .github/workflows/go-ci.yml + +Pasos sugeridos para aplicar el contenido como repo +1. Descomprime tokyo-predictor.zip +2. Revisa archivos y ajusta cualquier valor (module name, applicationId) si lo deseas. +3. Crea el repo y sube: + - cd tokyo-predictor + - git remote add origin git@github.com:MODEL/REPO.git (reemplaza MODEL/REPO) + - git push -u origin main + - o usa: gh repo create modelcontextprotocol/tokyo-predictor --public --source=. --remote=origin --push + +Cambios opcionales que recomiendo después de aplicar +- Añadir Gradle Wrapper al directorio android/ (en una máquina con gradle: cd android && gradle wrapper) y subir gradlew/gradlew.bat + gradle/wrapper/* +- Si vas a firmar releases en CI, añade secretos y ajusta workflow para assembleRelease + signing (te puedo generar el workflow si confirmas los nombres de los secrets). +- Ajustar go.mod module si tu organización tiene otro path. +- Habilitar branch protection y permisos en la org. + +¿Quieres que además: +- Genere un PR template / ISSUE template? (sí/no) +- Genere el workflow de release firmado para Android (requiere que indiques nombres de secrets)? (sí/no) + +Si quieres que suba el ZIP aquí para descargar, dime y lo preparo como contenido listo (te mostraré el comando curl/gh to upload); no puedo adjuntar archivos binarios directamente, pero te puedo devolver un patch (git diff) si prefieres aplicar con git apply. diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index e59182577..fc77caded 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -591,7 +591,9 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe mcp.WithArray("comments", mcp.Items( map[string]interface{}{ - "type": "object", + "type": "object", + "additionalProperties": false, + "required": []string{"path", "position", "body"}, "properties": map[string]interface{}{ "path": map[string]interface{}{ "type": "string", diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go deleted file mode 100644 index 5a4bef0f1..000000000 --- a/pkg/github/repositories.go +++ /dev/null @@ -1,610 +0,0 @@ -package github - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/aws/smithy-go/ptr" - "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v69/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" -) - -// listCommits creates a tool to get commits of a branch in a repository. -func listCommits(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("list_commits", - mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository")), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("sha", - mcp.Description("Branch name"), - ), - mcp.WithNumber("page", - mcp.Description("Page number"), - ), - mcp.WithNumber("per_page", - mcp.Description("Number of records per page"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := requiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := requiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - sha, err := optionalParam[string](request, "sha") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - page, err := optionalIntParamWithDefault(request, "page", 1) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - opts := &github.CommitsListOptions{ - SHA: sha, - ListOptions: github.ListOptions{ - Page: page, - PerPage: perPage, - }, - } - - commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) - if err != nil { - return nil, fmt.Errorf("failed to list commits: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list commits: %s", string(body))), nil - } - - r, err := json.Marshal(commits) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - -// createOrUpdateFile creates a tool to create or update a file in a GitHub repository. -func createOrUpdateFile(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("create_or_update_file", - mcp.WithDescription(t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository")), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("path", - mcp.Required(), - mcp.Description("Path where to create/update the file"), - ), - mcp.WithString("content", - mcp.Required(), - mcp.Description("Content of the file"), - ), - mcp.WithString("message", - mcp.Required(), - mcp.Description("Commit message"), - ), - mcp.WithString("branch", - mcp.Required(), - mcp.Description("Branch to create/update the file in"), - ), - mcp.WithString("sha", - mcp.Description("SHA of file being replaced (for updates)"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := requiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := requiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - path, err := requiredParam[string](request, "path") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - content, err := requiredParam[string](request, "content") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - message, err := requiredParam[string](request, "message") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - branch, err := requiredParam[string](request, "branch") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Convert content to base64 - contentBytes := []byte(content) - - // Create the file options - opts := &github.RepositoryContentFileOptions{ - Message: ptr.String(message), - Content: contentBytes, - Branch: ptr.String(branch), - } - - // If SHA is provided, set it (for updates) - sha, err := optionalParam[string](request, "sha") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - if sha != "" { - opts.SHA = ptr.String(sha) - } - - // Create or update the file - fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) - if err != nil { - return nil, fmt.Errorf("failed to create/update file: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != 200 && resp.StatusCode != 201 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to create/update file: %s", string(body))), nil - } - - r, err := json.Marshal(fileContent) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - -// createRepository creates a tool to create a new GitHub repository. -func createRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("create_repository", - mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account")), - mcp.WithString("name", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("description", - mcp.Description("Repository description"), - ), - mcp.WithBoolean("private", - mcp.Description("Whether repo should be private"), - ), - mcp.WithBoolean("auto_init", - mcp.Description("Initialize with README"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, err := requiredParam[string](request, "name") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - description, err := optionalParam[string](request, "description") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - private, err := optionalParam[bool](request, "private") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - autoInit, err := optionalParam[bool](request, "auto_init") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - repo := &github.Repository{ - Name: github.Ptr(name), - Description: github.Ptr(description), - Private: github.Ptr(private), - AutoInit: github.Ptr(autoInit), - } - - createdRepo, resp, err := client.Repositories.Create(ctx, "", repo) - if err != nil { - return nil, fmt.Errorf("failed to create repository: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to create repository: %s", string(body))), nil - } - - r, err := json.Marshal(createdRepo) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - -// getFileContents creates a tool to get the contents of a file or directory from a GitHub repository. -func getFileContents(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("get_file_contents", - mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("path", - mcp.Required(), - mcp.Description("Path to file/directory"), - ), - mcp.WithString("branch", - mcp.Description("Branch to get contents from"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := requiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := requiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - path, err := requiredParam[string](request, "path") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - branch, err := optionalParam[string](request, "branch") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - opts := &github.RepositoryContentGetOptions{Ref: branch} - fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) - if err != nil { - return nil, fmt.Errorf("failed to get file contents: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil - } - - var result interface{} - if fileContent != nil { - result = fileContent - } else { - result = dirContent - } - - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - -// forkRepository creates a tool to fork a repository. -func forkRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("fork_repository", - mcp.WithDescription(t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization")), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("organization", - mcp.Description("Organization to fork to"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := requiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := requiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - org, err := optionalParam[string](request, "organization") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - opts := &github.RepositoryCreateForkOptions{} - if org != "" { - opts.Organization = org - } - - forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts) - if err != nil { - // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, - // and it's not a real error. - if resp != nil && resp.StatusCode == http.StatusAccepted && isAcceptedError(err) { - return mcp.NewToolResultText("Fork is in progress"), nil - } - return nil, fmt.Errorf("failed to fork repository: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusAccepted { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil - } - - r, err := json.Marshal(forkedRepo) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - -// createBranch creates a tool to create a new branch. -func createBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("create_branch", - mcp.WithDescription(t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository")), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("branch", - mcp.Required(), - mcp.Description("Name for new branch"), - ), - mcp.WithString("from_branch", - mcp.Description("Source branch (defaults to repo default)"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := requiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := requiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - branch, err := requiredParam[string](request, "branch") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - fromBranch, err := optionalParam[string](request, "from_branch") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Get the source branch SHA - var ref *github.Reference - - if fromBranch == "" { - // Get default branch if from_branch not specified - repository, resp, err := client.Repositories.Get(ctx, owner, repo) - if err != nil { - return nil, fmt.Errorf("failed to get repository: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - fromBranch = *repository.DefaultBranch - } - - // Get SHA of source branch - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+fromBranch) - if err != nil { - return nil, fmt.Errorf("failed to get reference: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Create new branch - newRef := &github.Reference{ - Ref: github.Ptr("refs/heads/" + branch), - Object: &github.GitObject{SHA: ref.Object.SHA}, - } - - createdRef, resp, err := client.Git.CreateRef(ctx, owner, repo, newRef) - if err != nil { - return nil, fmt.Errorf("failed to create branch: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - r, err := json.Marshal(createdRef) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - -// pushFiles creates a tool to push multiple files in a single commit to a GitHub repository. -func pushFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("push_files", - mcp.WithDescription(t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit")), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("branch", - mcp.Required(), - mcp.Description("Branch to push to"), - ), - mcp.WithArray("files", - mcp.Required(), - mcp.Items( - map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ - "type": "string", - "description": "path to the file", - }, - "content": map[string]interface{}{ - "type": "string", - "description": "file content", - }, - }, - }), - mcp.Description("Array of file objects to push, each object with path (string) and content (string)"), - ), - mcp.WithString("message", - mcp.Required(), - mcp.Description("Commit message"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := requiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := requiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - branch, err := requiredParam[string](request, "branch") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - message, err := requiredParam[string](request, "message") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Parse files parameter - this should be an array of objects with path and content - filesObj, ok := request.Params.Arguments["files"].([]interface{}) - if !ok { - return mcp.NewToolResultError("files parameter must be an array of objects with path and content"), nil - } - - // Get the reference for the branch - ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch) - if err != nil { - return nil, fmt.Errorf("failed to get branch reference: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Get the commit object that the branch points to - baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA) - if err != nil { - return nil, fmt.Errorf("failed to get base commit: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Create tree entries for all files - var entries []*github.TreeEntry - - for _, file := range filesObj { - fileMap, ok := file.(map[string]interface{}) - if !ok { - return mcp.NewToolResultError("each file must be an object with path and content"), nil - } - - path, ok := fileMap["path"].(string) - if !ok || path == "" { - return mcp.NewToolResultError("each file must have a path"), nil - } - - content, ok := fileMap["content"].(string) - if !ok { - return mcp.NewToolResultError("each file must have content"), nil - } - - // Create a tree entry for the file - entries = append(entries, &github.TreeEntry{ - Path: github.Ptr(path), - Mode: github.Ptr("100644"), // Regular file mode - Type: github.Ptr("blob"), - Content: github.Ptr(content), - }) - } - - // Create a new tree with the file entries - newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries) - if err != nil { - return nil, fmt.Errorf("failed to create tree: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Create a new commit - commit := &github.Commit{ - Message: github.Ptr(message), - Tree: newTree, - Parents: []*github.Commit{{SHA: baseCommit.SHA}}, - } - newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil) - if err != nil { - return nil, fmt.Errorf("failed to create commit: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Update the reference to point to the new commit - ref.Object.SHA = newCommit.SHA - updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, ref, false) - if err != nil { - return nil, fmt.Errorf("failed to update reference: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - r, err := json.Marshal(updatedRef) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -}