From a9da4d20e5e2f53fc9a811d6cb3bb42218b821e9 Mon Sep 17 00:00:00 2001 From: Chris Griffing Date: Tue, 14 Jul 2026 10:48:11 -0700 Subject: [PATCH 1/2] feat: add script and tools.json file in prep for docker mcp catalog --- README.md | 6 + scripts/generate-tools-json.mjs | 74 ++++ tools.json | 621 ++++++++++++++++++++++++++++++++ 3 files changed, 701 insertions(+) create mode 100644 scripts/generate-tools-json.mjs create mode 100644 tools.json diff --git a/README.md b/README.md index 8436c2c..4606485 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ If you want to read more about the MCP server, you can check out the [introducto Tools are the primary purpose of the MCP server. They are a set of finely curated commands that AI can use to interact with GitKraken without exploding your context. Some of those tools include: `issues_assigned_to_me`, `gitlens_commit_composer`, and `pull_request_create_review`. A full list of tools can be found in the GitKraken Help Center's [Tools Reference](https://help.gitkraken.com/mcp/mcp-tools-reference/). +The repository also includes a Docker MCP Catalog-compatible [`tools.json`](tools.json). To refresh it from the local GitKraken CLI, run: + +```bash +node scripts/generate-tools-json.mjs +``` + ## Prompts Prompts are the secondary purpose of the MCP server. They are a set of carefully crafted instructions that AI can use to understand how to use the tools, when to use them, and what information to provide when using them. A full list of prompts can be found in the GitKraken Help Center's [Prompts Reference](https://help.gitkraken.com/mcp/mcp-prompts-reference/). diff --git a/scripts/generate-tools-json.mjs b/scripts/generate-tools-json.mjs new file mode 100644 index 0000000..300b69b --- /dev/null +++ b/scripts/generate-tools-json.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const outputPath = resolve(process.argv[2] ?? "tools.json"); + +const output = execFileSync("gk", ["mcp", "--list-tools"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], +}); + +const tools = []; +let currentTool; +let currentArgument; + +function finishArgument() { + currentArgument = undefined; +} + +function finishTool() { + finishArgument(); + currentTool = undefined; +} + +for (const line of output.split(/\r?\n/)) { + const toolMatch = line.match(/^Tool: (.+)$/); + if (toolMatch) { + finishTool(); + currentTool = { + name: toolMatch[1], + description: "", + arguments: [], + }; + tools.push(currentTool); + continue; + } + + if (!currentTool) { + continue; + } + + const descriptionMatch = line.match(/^ Description:\s*(.+)$/); + if (descriptionMatch) { + currentTool.description = descriptionMatch[1].trim(); + continue; + } + + const argumentMatch = line.match(/^ - ([^\s]+) \[([^\]]+)\](?: \(required\))?$/); + if (argumentMatch) { + currentArgument = { + name: argumentMatch[1], + type: argumentMatch[2], + desc: "", + }; + currentTool.arguments.push(currentArgument); + continue; + } + + const argumentDescriptionMatch = line.match(/^ (.+)$/); + if (argumentDescriptionMatch && currentArgument) { + currentArgument.desc = currentArgument.desc + ? `${currentArgument.desc} ${argumentDescriptionMatch[1].trim()}` + : argumentDescriptionMatch[1].trim(); + } +} + +if (tools.length === 0) { + throw new Error("No MCP tools were found in `gk mcp --list-tools` output."); +} + +writeFileSync(outputPath, `${JSON.stringify(tools, null, 2)}\n`); +console.log(`Wrote ${tools.length} tools to ${outputPath}`); diff --git a/tools.json b/tools.json new file mode 100644 index 0000000..931536f --- /dev/null +++ b/tools.json @@ -0,0 +1,621 @@ +[ + { + "name": "git_add_or_commit", + "description": "Add file contents to the index (git add ) OR record changes to the repository (git commit -m [files...]). Use the 'action' parameter to specify which action to perform.", + "arguments": [ + { + "name": "action", + "type": "string", + "desc": "The action to perform: 'add' or 'commit'" + }, + { + "name": "directory", + "type": "string", + "desc": "The directory to run git add or commit in" + }, + { + "name": "files", + "type": "array", + "desc": "Optional array of files to add or commit. If omitted, all files are added or all staged changes are committed." + }, + { + "name": "message", + "type": "string", + "desc": "The commit message (required if action is 'commit')" + } + ] + }, + { + "name": "git_blame", + "description": "Show what revision and author last modified each line of a file (git blame ).", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "The directory to run git blame in" + }, + { + "name": "file", + "type": "string", + "desc": "The file to blame" + } + ] + }, + { + "name": "git_branch", + "description": "List or create branches (git branch).", + "arguments": [ + { + "name": "action", + "type": "string", + "desc": "Git branch action to be executed" + }, + { + "name": "branch_name", + "type": "string", + "desc": "(Optional) Name of the branch to create or delete" + }, + { + "name": "directory", + "type": "string", + "desc": "The directory to run git branch in" + } + ] + }, + { + "name": "git_checkout", + "description": "Switch branches or restore working tree files (git checkout ).", + "arguments": [ + { + "name": "branch", + "type": "string", + "desc": "The branch to checkout. This must be a valid branch name without spaces" + }, + { + "name": "directory", + "type": "string", + "desc": "The directory to run git checkout in" + } + ] + }, + { + "name": "git_fetch", + "description": "Download objects and refs from another repository (git fetch).", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "The directory to run git fetch in" + } + ] + }, + { + "name": "git_log_or_diff", + "description": "Show commit logs or changes between commits (git log --oneline or git diff).", + "arguments": [ + { + "name": "action", + "type": "string", + "desc": "The action to perform: 'log' for commit logs or 'diff' for changes" + }, + { + "name": "authors", + "type": "array", + "desc": "Optional array of author names or emails to filter commits by (only applies to 'log' action)" + }, + { + "name": "directory", + "type": "string", + "desc": "The directory to run the command in" + }, + { + "name": "revision_range", + "type": "string", + "desc": "Optional revision range (e.g., 'HEAD', 'main..feature', 'abc123', 'HEAD~5..HEAD'). For 'diff' action, shows differences for the specified range. For 'log' action, shows commits in the range. Defaults to HEAD." + }, + { + "name": "since", + "type": "string", + "desc": "Optional date/timestamp/relative time to show commits after (e.g., '2024-01-01', '2 weeks ago', 'yesterday', '1 hour ago', '3 days ago'). Only applies to 'log' action." + }, + { + "name": "until", + "type": "string", + "desc": "Optional date/timestamp/relative time to show commits before (e.g., '2024-12-31', '1 week ago', 'today', 'yesterday', '2 hours ago'). Only applies to 'log' action." + } + ] + }, + { + "name": "git_pull", + "description": "Fetch from and integrate with another repository or a local branch (git pull).", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "The directory to run git pull in" + } + ] + }, + { + "name": "git_push", + "description": "Update remote refs along with associated objects (git push).", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "The directory to run git push in" + } + ] + }, + { + "name": "git_stash", + "description": "Stash the changes in a dirty working directory (git stash).", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "The directory to run git stash in" + }, + { + "name": "include_untracked", + "type": "boolean", + "desc": "When true, include untracked files in the stash." + }, + { + "name": "name", + "type": "string", + "desc": "Optional name for the stash (used as the stash message)" + }, + { + "name": "staged_only", + "type": "boolean", + "desc": "When true, stash only the currently staged changes and leave unstaged work untouched." + } + ] + }, + { + "name": "git_status", + "description": "Show the working tree status (git status), will open a UI if client supports it.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + } + ] + }, + { + "name": "git_worktree", + "description": "List or add git worktrees (git worktree ).", + "arguments": [ + { + "name": "action", + "type": "string", + "desc": "Git worktree action to be executed" + }, + { + "name": "branch", + "type": "string", + "desc": "(Optional) Existing branch for the new worktree (used for add)" + }, + { + "name": "directory", + "type": "string", + "desc": "The directory to run git worktree in" + }, + { + "name": "path", + "type": "string", + "desc": "(Optional) Path for the worktree (required for add)" + } + ] + }, + { + "name": "gitkraken_workspace_list", + "description": "Lists all Gitkraken workspaces", + "arguments": [] + }, + { + "name": "gitlens_commit_composer", + "description": "Gitlens Commit Composer. Organize your changes into well-formed commits with clear messages and descriptions. Useful for breaking large changes into smaller commits.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + }, + { + "name": "instructions", + "type": "string", + "desc": "OPTIONAL. Use this ONLY if the user explicitly provided specific requirements about how commits should be organized, what commit messages should say, or particular commit structure preferences. Do NOT use this parameter unless you are 100% certain about the user's intentions. Examples: 'use conventional commits', 'prefix each commit with JIRA-123', 'keep commit messages under 50 characters'." + } + ] + }, + { + "name": "gitlens_launchpad", + "description": "Gitlens Launchpad. Gets your open pull requests prioritized by what needs attention: ready to merge, has conflicts, awaiting review, etc. Helpful for checking todos, outstanding tasks, or deciding what to work on next.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + } + ] + }, + { + "name": "gitlens_start_review", + "description": "Gitlens Start Review. Creates a dedicated worktree and reviews your PR with an AI agent.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + }, + { + "name": "instructions", + "type": "string", + "desc": "OPTIONAL. Use this ONLY if the user explicitly provided specific requirements about the review focus, review criteria, or what aspects to check. Do NOT use this parameter unless you are 100% certain about the user's intentions. Examples: 'focus on security issues', 'check for performance regressions', 'review only the authentication logic'." + }, + { + "name": "pr_url", + "type": "string", + "desc": "URL of the PR to start review." + } + ] + }, + { + "name": "gitlens_start_work", + "description": "Gitlens Start Work. Creates a work based on an issue. This tool will create a branch and link it with the issue, keeping context visible throughout your work.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + }, + { + "name": "instructions", + "type": "string", + "desc": "OPTIONAL. Use this ONLY if the user explicitly provided specific requirements about the implementation approach, or additional context to supplement the issue. Do NOT use this parameter unless you are 100% certain about the user's intentions." + }, + { + "name": "issue_url", + "type": "string", + "desc": "URL of the issue to start work on." + } + ] + }, + { + "name": "issues_add_comment", + "description": "Add a comment to an issue", + "arguments": [ + { + "name": "azure_organization", + "type": "string", + "desc": "Optionally set the Azure DevOps organization name. Required for Azure DevOps" + }, + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name. Required for Azure DevOps" + }, + { + "name": "comment", + "type": "string", + "desc": "The text content of the comment" + }, + { + "name": "issue_id", + "type": "string", + "desc": "The ID of the issue to comment on" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the issue provider" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Repository name. This is required for GitHub and GitLab" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Organization name. This is required for GitHub and GitLab" + } + ] + }, + { + "name": "issues_assigned_to_me", + "description": "Fetch issues assigned to the user", + "arguments": [ + { + "name": "azure_organization", + "type": "string", + "desc": "Optionally set the Azure DevOps organization name. Required for Azure DevOps" + }, + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name. Required for Azure DevOps" + }, + { + "name": "page", + "type": "number", + "desc": "Optional parameter to specify the page number, defaults to 1" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the issue provider" + } + ] + }, + { + "name": "issues_get_detail", + "description": "Retrieve detailed information about a specific issue by its unique ID. For Jira Epics, the response includes a childIssues array containing all issues linked to the epic.", + "arguments": [ + { + "name": "azure_organization", + "type": "string", + "desc": "Optionally set the Azure DevOps organization name. Required for Azure DevOps" + }, + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name. Required for Azure DevOps" + }, + { + "name": "issue_id", + "type": "string", + "desc": "The Number or ID of the issue to retrieve. Supported formats include GitHub/GitLab numeric IDs (e.g., 123 or #123), Jira keys (e.g., PROJ-123), Linear issue IDs (UUID format), etc." + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the issue provider" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Repository name. This is required for GitHub and GitLab" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Organization name. This is required for GitHub and GitLab" + } + ] + }, + { + "name": "pull_request_assigned_to_me", + "description": "Search pull requests where you are the assignee, author, or reviewer", + "arguments": [ + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" + }, + { + "name": "is_closed", + "type": "boolean", + "desc": "Set to true if you want to search for closed pull requests" + }, + { + "name": "page", + "type": "number", + "desc": "Optional parameter to specify the page number, defaults to 1" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the git provider" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Set the repository name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Set the organization name of the pull request. Required for Azure DevOps and Bitbucket" + } + ] + }, + { + "name": "pull_request_create", + "description": "Create a new pull request", + "arguments": [ + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" + }, + { + "name": "body", + "type": "string", + "desc": "The body/description of the pull request" + }, + { + "name": "is_draft", + "type": "boolean", + "desc": "Create as draft pull request" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the git provider" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Set the repository name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Set the organization name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "source_branch", + "type": "string", + "desc": "Source branch from which the pull request will be created" + }, + { + "name": "target_branch", + "type": "string", + "desc": "Target branch where the pull request will be merged" + }, + { + "name": "title", + "type": "string", + "desc": "The title of the pull request" + } + ] + }, + { + "name": "pull_request_create_review", + "description": "Create a review for a pull request", + "arguments": [ + { + "name": "approve", + "type": "boolean", + "desc": "Set to true if you want to approve the pull request" + }, + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the git provider" + }, + { + "name": "pull_request_id", + "type": "string", + "desc": "ID of the pull request to create the review for" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Set the repository name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Set the organization name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "review", + "type": "string", + "desc": "Comment to add to the pull request review" + } + ] + }, + { + "name": "pull_request_get_comments", + "description": "Get all the comments in a pull requests", + "arguments": [ + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the git provider" + }, + { + "name": "pull_request_id", + "type": "string", + "desc": "ID of the pull request to add the comment to" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Set the repository name of the pull request" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Set the organization name of the pull request" + } + ] + }, + { + "name": "pull_request_get_detail", + "description": "Get an specific pull request", + "arguments": [ + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the git provider" + }, + { + "name": "pull_request_files", + "type": "boolean", + "desc": "Set to true if you want to retrieve the files changed in the pull request. Not supported by Azure DevOps." + }, + { + "name": "pull_request_id", + "type": "string", + "desc": "ID of the pull request to retrieve" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Set the repository name of the pull request" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Set the organization name of the pull request" + } + ] + }, + { + "name": "repository_get_file_content", + "description": "Get file content from a repository", + "arguments": [ + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" + }, + { + "name": "file_path", + "type": "string", + "desc": "File path to retrieve from the repository" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the git provider" + }, + { + "name": "ref", + "type": "string", + "desc": "Set the branch, tag, or commit SHA to retrieve the file from" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Set the repository name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Set the organization name of the pull request. Required for Azure DevOps and Bitbucket" + } + ] + } +] From 071fc2325d2d335116beecf155ac8dca4f1c670e Mon Sep 17 00:00:00 2001 From: Chris Griffing Date: Fri, 17 Jul 2026 13:00:56 -0700 Subject: [PATCH 2/2] feat: add docker support for submission to the docker MCP registry --- .dockerignore | 3 + .github/workflows/validate.yml | 61 ++++ .gitignore | 1 + Dockerfile | 28 ++ README.md | 44 ++- docker/catalog/README.md | 21 ++ docker/catalog/server.yaml.template | 33 ++ package-lock.json | 539 ++++++++++++++++++++++++++++ package.json | 18 + scripts/check-tools-json.mjs | 63 ++++ scripts/generate-tools-json.mjs | 23 +- scripts/smoke-mcp.mjs | 328 +++++++++++++++++ tools.json | 179 ++++++++- 13 files changed, 1321 insertions(+), 20 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker/catalog/README.md create mode 100644 docker/catalog/server.yaml.template create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/check-tools-json.mjs create mode 100644 scripts/smoke-mcp.mjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be2f2c2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +** +!package.json +!package-lock.json diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..e76ffb9 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,61 @@ +name: Validate + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + container: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci --ignore-scripts + + - name: Check scripts + run: | + node --check scripts/generate-tools-json.mjs + node --check scripts/check-tools-json.mjs + node --check scripts/smoke-mcp.mjs + + - name: Build image + run: docker build -t gitkraken-mcp:test . + + - name: Check catalog tool-list drift + env: + TOOLS_COMMAND: '["docker","run","--rm","gitkraken-mcp:test","--list-tools"]' + run: npm run check:tools + + - name: Smoke test MCP and mounted repository access + run: | + repository="$(mktemp -d)" + git init "$repository" + export repository + MCP_COMMAND="$(node -e 'console.log(JSON.stringify(["docker", "run", "--rm", "-i", "--mount", `type=bind,source=${process.env.repository},target=/workspace/repo`, "gitkraken-mcp:test"]))')" + MCP_COMMAND="$MCP_COMMAND" MCP_SMOKE_REPOSITORY=/workspace/repo npm run smoke:mcp + + multiarch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Build Linux architectures + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9dd1858 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-bookworm-slim + +RUN apt-get update \ + && apt-get install --no-install-recommends -y ca-certificates git \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /app /workspace \ + && chown node:node /app /workspace + +WORKDIR /app + +COPY --chown=node:node package.json package-lock.json ./ + +USER node + +RUN --mount=type=cache,target=/home/node/.npm,uid=1000,gid=1000 \ + npm ci --omit=dev + +RUN /app/node_modules/.bin/gk mcp --list-tools --no-telemetry > /dev/null + +RUN git config --global --add safe.directory '*' + +WORKDIR /workspace + +LABEL io.modelcontextprotocol.server.name="com.gitkraken/gk-cli" + +ENTRYPOINT ["/app/node_modules/.bin/gk", "mcp"] diff --git a/README.md b/README.md index 4606485..489f7d1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ If you want to read more about the MCP server, you can check out the [introducto - [Tools](#tools) - [Prompts](#prompts) - [Installation](#installation) +- [Docker](#docker) - [Troubleshooting](#troubleshooting) - [Support](#support) @@ -18,12 +19,16 @@ If you want to read more about the MCP server, you can check out the [introducto Tools are the primary purpose of the MCP server. They are a set of finely curated commands that AI can use to interact with GitKraken without exploding your context. Some of those tools include: `issues_assigned_to_me`, `gitlens_commit_composer`, and `pull_request_create_review`. A full list of tools can be found in the GitKraken Help Center's [Tools Reference](https://help.gitkraken.com/mcp/mcp-tools-reference/). -The repository also includes a Docker MCP Catalog-compatible [`tools.json`](tools.json). To refresh it from the local GitKraken CLI, run: +The repository also includes a Docker MCP Catalog-compatible [`tools.json`](tools.json). To refresh it from an installed GitKraken CLI, run: ```bash node scripts/generate-tools-json.mjs ``` +Set `GK_BIN` when `gk` is not on `PATH`, or set `TOOLS_COMMAND` to a JSON +command array when generating from a container. The generator intentionally +excludes app-only tools that MCP agents must not call. + ## Prompts Prompts are the secondary purpose of the MCP server. They are a set of carefully crafted instructions that AI can use to understand how to use the tools, when to use them, and what information to provide when using them. A full list of prompts can be found in the GitKraken Help Center's [Prompts Reference](https://help.gitkraken.com/mcp/mcp-prompts-reference/). @@ -54,6 +59,43 @@ The installation process details may vary by AI tool, but the general gist is th } ``` +## Docker + +This repository includes auth-independent Docker packaging for the local MCP +server. The image can list tools and run local Git tools against explicitly +mounted repositories. Cloud workspaces, issues, pull requests, and provider +operations still require a supported non-interactive GitKraken authentication +contract. + +Build the image: + +```bash +docker build -t gitkraken-mcp:local . +``` + +List the tools without authenticating: + +```bash +docker run --rm gitkraken-mcp:local --list-tools +``` + +Check `tools.json` against the clean image: + +```bash +npm install +TOOLS_COMMAND='["docker","run","--rm","gitkraken-mcp:local","--list-tools"]' npm run check:tools +``` + +The image runs as a non-root user and starts `gk mcp` over stdio. When using it +through Docker MCP Toolkit, only mount repository paths that the server should +be allowed to read and modify. Linked Git worktrees also require access to the +external Git directory referenced by their `.git` file. A draft registry entry +is available in +[`docker/catalog/server.yaml.template`](docker/catalog/server.yaml.template). +Copy that template and `tools.json` into `servers/gitkraken/` in a checkout of +[`docker/mcp-registry`](https://github.com/docker/mcp-registry) when preparing +the catalog pull request. + ### CLI #### macOS diff --git a/docker/catalog/README.md b/docker/catalog/README.md new file mode 100644 index 0000000..18387a1 --- /dev/null +++ b/docker/catalog/README.md @@ -0,0 +1,21 @@ +# Docker MCP Catalog submission + +This directory contains the source template for the GitKraken entry in +[`docker/mcp-registry`](https://github.com/docker/mcp-registry). + +To prepare a registry pull request: + +1. Copy `server.yaml.template` to `servers/gitkraken/server.yaml` in a checkout + of `docker/mcp-registry`. +2. Replace `` with the full commit SHA containing the + Dockerfile being submitted. +3. Copy the repository root `tools.json` next to `server.yaml`. +4. Add the approved GitKraken authentication configuration once its + non-interactive container contract is finalized. +5. Confirm that the license declared by the distributed `@gitkraken/gk` + package is acceptable for Docker Catalog redistribution. +6. Run the registry's `task build -- --tools gitkraken` and + `task catalog -- gitkraken` checks before opening the pull request. + +The `paths` parameter is intentionally required because the local Git tools +must only receive access to repositories explicitly selected by the user. diff --git a/docker/catalog/server.yaml.template b/docker/catalog/server.yaml.template new file mode 100644 index 0000000..71bafc2 --- /dev/null +++ b/docker/catalog/server.yaml.template @@ -0,0 +1,33 @@ +name: gitkraken +image: mcp/gitkraken +type: server +meta: + category: devops + tags: + - git + - devops + - issues + - pull-requests +about: + title: GitKraken + description: Work with Git repositories, issues, and pull requests through GitKraken. + icon: https://avatars.githubusercontent.com/u/92606490?v=4 +source: + project: https://github.com/gitkraken/mcp + commit: +run: + volumes: + - "{{gitkraken.paths|volume|into}}" +config: + description: Configure the local repository paths GitKraken is allowed to access. + parameters: + type: object + properties: + paths: + type: array + items: + type: string + default: + - /Users/local-test + required: + - paths diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c9f0d25 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,539 @@ +{ + "name": "gitkraken-mcp-catalog", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitkraken-mcp-catalog", + "version": "0.0.0", + "dependencies": { + "@gitkraken/gk": "3.1.70" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gitkraken/gk": { + "version": "3.1.70", + "resolved": "https://registry.npmjs.org/@gitkraken/gk/-/gk-3.1.70.tgz", + "integrity": "sha512-ZWef75qI7Y2zzghDygGrECuZItSEXOWje8HX0sk9qlq6scgAlgswhlS6ve/y1PpyH8lM5R2T2L2ZpxLoBp6xnA==", + "hasInstallScript": true, + "license": "CC-BY-3.0-US", + "dependencies": { + "jszip": "3.10.1", + "proxy-agent": "8.0.2", + "tar": "7.5.19" + }, + "bin": { + "gk": "run-gk.js" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", + "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", + "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-uri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.1.tgz", + "integrity": "sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.3.1", + "data-uri-to-buffer": "8.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/pac-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz", + "integrity": "sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "get-uri": "8.0.1", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "pac-resolver": "9.0.1", + "quickjs-wasi": "^2.2.0", + "socks-proxy-agent": "10.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-resolver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", + "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", + "license": "MIT", + "dependencies": { + "degenerator": "7.0.1", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.2.tgz", + "integrity": "sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "9.1.0", + "proxy-from-env": "^2.0.0", + "socks-proxy-agent": "10.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/quickjs-wasi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7c88d5 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "gitkraken-mcp-catalog", + "version": "0.0.0", + "private": true, + "description": "Docker MCP Catalog packaging and validation for the GitKraken MCP server", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "generate:tools": "node scripts/generate-tools-json.mjs", + "check:tools": "node scripts/check-tools-json.mjs", + "smoke:mcp": "node scripts/smoke-mcp.mjs" + }, + "dependencies": { + "@gitkraken/gk": "3.1.70" + } +} diff --git a/scripts/check-tools-json.mjs b/scripts/check-tools-json.mjs new file mode 100644 index 0000000..85feb2b --- /dev/null +++ b/scripts/check-tools-json.mjs @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = resolve(scriptDirectory, ".."); +const generatorPath = join(scriptDirectory, "generate-tools-json.mjs"); +const committedPath = join(repositoryRoot, "tools.json"); +const temporaryDirectory = mkdtempSync(join(tmpdir(), "gk-mcp-tools-")); +const generatedPath = join(temporaryDirectory, "tools.json"); + +try { + const result = spawnSync(process.execPath, [generatorPath, generatedPath], { + cwd: repositoryRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 60_000, + }); + + if (result.error) { + throw new Error(`Unable to regenerate tools.json: ${result.error.message}`); + } + + if (result.status !== 0) { + const details = [result.stdout, result.stderr] + .filter(Boolean) + .join("\n") + .trim(); + throw new Error( + `The tools.json generator exited with status ${result.status}.${ + details ? `\n${details}` : "" + }`, + ); + } + + const committed = readFileSync(committedPath, "utf8"); + const generated = readFileSync(generatedPath, "utf8"); + + if (committed !== generated) { + const committedLines = committed.split("\n"); + const generatedLines = generated.split("\n"); + const differingLine = generatedLines.findIndex( + (line, index) => line !== committedLines[index], + ); + const location = differingLine === -1 ? "at end of file" : `at line ${differingLine + 1}`; + + throw new Error( + `tools.json is out of date (${location}). Run ` + + "`node scripts/generate-tools-json.mjs` and commit the result.", + ); + } + + console.log("tools.json matches the current GitKraken MCP tool list."); +} catch (error) { + console.error(`check-tools-json: ${error.message}`); + process.exitCode = 1; +} finally { + rmSync(temporaryDirectory, { recursive: true, force: true }); +} diff --git a/scripts/generate-tools-json.mjs b/scripts/generate-tools-json.mjs index 300b69b..bf52b5c 100644 --- a/scripts/generate-tools-json.mjs +++ b/scripts/generate-tools-json.mjs @@ -5,8 +5,10 @@ import { writeFileSync } from "node:fs"; import { resolve } from "node:path"; const outputPath = resolve(process.argv[2] ?? "tools.json"); +const command = parseCommand(); +const deniedToolNames = new Set(["app_tool_box", "app_update_user_preferences"]); -const output = execFileSync("gk", ["mcp", "--list-tools"], { +const output = execFileSync(command[0], command.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], }); @@ -28,6 +30,9 @@ for (const line of output.split(/\r?\n/)) { const toolMatch = line.match(/^Tool: (.+)$/); if (toolMatch) { finishTool(); + if (deniedToolNames.has(toolMatch[1])) { + continue; + } currentTool = { name: toolMatch[1], description: "", @@ -72,3 +77,19 @@ if (tools.length === 0) { writeFileSync(outputPath, `${JSON.stringify(tools, null, 2)}\n`); console.log(`Wrote ${tools.length} tools to ${outputPath}`); + +function parseCommand() { + if (process.env.TOOLS_COMMAND) { + const parsed = JSON.parse(process.env.TOOLS_COMMAND); + if ( + !Array.isArray(parsed) || + parsed.length === 0 || + parsed.some((argument) => typeof argument !== "string" || argument.length === 0) + ) { + throw new Error("TOOLS_COMMAND must be a JSON array of nonempty strings."); + } + return parsed; + } + + return [process.env.GK_BIN ?? "gk", "mcp", "--list-tools"]; +} diff --git a/scripts/smoke-mcp.mjs b/scripts/smoke-mcp.mjs new file mode 100644 index 0000000..fe064d4 --- /dev/null +++ b/scripts/smoke-mcp.mjs @@ -0,0 +1,328 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; + +let child; +let exit; + +try { + const requestTimeoutMs = parseTimeout("MCP_TIMEOUT_MS", 15_000); + const shutdownTimeoutMs = parseTimeout("MCP_SHUTDOWN_TIMEOUT_MS", 5_000); + const command = parseCommand(process.env.MCP_COMMAND); + + child = spawn(command.file, command.shell ? [] : command.args.slice(1), { + env: process.env, + shell: command.shell, + stdio: ["pipe", "pipe", "pipe"], + }); + + let stderr = ""; + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (chunk) => { + stderr = `${stderr}${chunk}`.slice(-16_384); + }); + + exit = waitForExit(child); + const rpc = createJsonRpcClient(child, requestTimeoutMs, () => stderr); + + const initialize = await rpc.request("initialize", { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { + name: "gk-mcp-smoke-test", + version: "1.0.0", + }, + }); + + assertObject(initialize, "initialize result"); + if (typeof initialize.protocolVersion !== "string") { + throw new Error("initialize result is missing a string protocolVersion"); + } + + rpc.notify("notifications/initialized"); + + const listed = await rpc.request("tools/list", {}); + const tools = validateToolsResult(listed); + + if (process.env.MCP_SMOKE_REPOSITORY !== undefined) { + if (!tools.some((tool) => tool.name === "git_status")) { + throw new Error("tools/list did not advertise the required git_status tool"); + } + + const callResult = await rpc.request("tools/call", { + name: "git_status", + arguments: { directory: process.env.MCP_SMOKE_REPOSITORY }, + }); + assertObject(callResult, "git_status result"); + if (callResult.isError === true) { + throw new Error(`git_status returned an MCP error: ${formatToolContent(callResult.content)}`); + } + } + + child.stdin.end(); + const outcome = await withTimeout( + exit, + shutdownTimeoutMs, + `MCP server did not exit within ${shutdownTimeoutMs}ms after stdin closed`, + ); + assertSuccessfulExit(outcome, stderr); + + console.log(`MCP smoke test passed: ${tools.length} tools.`); +} catch (error) { + await stopChild(child, exit); + console.error(`smoke-mcp: ${error.message}`); + process.exitCode = 1; +} + +function parseCommand(value) { + if (!value?.trim()) { + return { file: "gk", args: ["gk", "mcp"], shell: false }; + } + + const trimmed = value.trim(); + if (trimmed.startsWith("[")) { + let args; + try { + args = JSON.parse(trimmed); + } catch (error) { + throw new Error(`MCP_COMMAND is not valid JSON: ${error.message}`); + } + if ( + !Array.isArray(args) || + args.length === 0 || + args.some((argument) => typeof argument !== "string" || argument.length === 0) + ) { + throw new Error("MCP_COMMAND JSON must be a nonempty array of nonempty strings"); + } + return { file: args[0], args, shell: false }; + } + + return { file: trimmed, args: [], shell: true }; +} + +function parseTimeout(name, fallback) { + const value = process.env[name]; + if (value === undefined) return fallback; + + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`${name} must be a positive number of milliseconds`); + } + return parsed; +} + +function createJsonRpcClient(process, timeoutMs, getStderr) { + let nextId = 1; + let stdoutBuffer = ""; + const pending = new Map(); + + process.stdout.setEncoding("utf8"); + process.stdout.on("data", (chunk) => { + stdoutBuffer += chunk; + let newline; + while ((newline = stdoutBuffer.indexOf("\n")) !== -1) { + const line = stdoutBuffer.slice(0, newline).replace(/\r$/, ""); + stdoutBuffer = stdoutBuffer.slice(newline + 1); + if (line.trim()) handleLine(line); + } + }); + + process.stdout.on("end", () => { + if (stdoutBuffer.trim()) handleLine(stdoutBuffer.replace(/\r$/, "")); + }); + + function handleLine(line) { + let message; + try { + message = JSON.parse(line); + } catch (error) { + rejectAll(new Error(`MCP server wrote invalid JSON to stdout: ${error.message}\n${line}`)); + return; + } + + if (message?.jsonrpc !== "2.0" || !("id" in message)) return; + const request = pending.get(message.id); + if (!request) return; + + clearTimeout(request.timer); + pending.delete(message.id); + if (message.error) { + request.reject( + new Error( + `JSON-RPC request ${request.method} failed: ${JSON.stringify(message.error)}`, + ), + ); + } else if (!("result" in message)) { + request.reject(new Error(`JSON-RPC response to ${request.method} has no result`)); + } else { + request.resolve(message.result); + } + } + + function write(message) { + if (!process.stdin.writable) { + throw new Error("MCP server stdin is not writable"); + } + process.stdin.write(`${JSON.stringify(message)}\n`); + } + + function rejectAll(error) { + for (const request of pending.values()) { + clearTimeout(request.timer); + request.reject(error); + } + pending.clear(); + } + + process.once("error", (error) => { + rejectAll(new Error(`Unable to launch MCP server: ${error.message}`)); + }); + process.stdin.on("error", (error) => { + rejectAll(new Error(`Unable to write to MCP server stdin: ${error.message}`)); + }); + process.once("exit", (code, signal) => { + const details = formatStderr(getStderr()); + rejectAll( + new Error( + `MCP server exited before completing requests (${formatExit(code, signal)})${details}`, + ), + ); + }); + + return { + request(method, params) { + const id = nextId++; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + pending.delete(id); + reject( + new Error( + `JSON-RPC request ${method} timed out after ${timeoutMs}ms${formatStderr( + getStderr(), + )}`, + ), + ); + }, timeoutMs); + pending.set(id, { method, resolve, reject, timer }); + try { + write({ jsonrpc: "2.0", id, method, params }); + } catch (error) { + clearTimeout(timer); + pending.delete(id); + reject(error); + } + }); + }, + notify(method, params) { + write({ jsonrpc: "2.0", method, ...(params === undefined ? {} : { params }) }); + }, + }; +} + +function validateToolsResult(result) { + assertObject(result, "tools/list result"); + if (!Array.isArray(result.tools) || result.tools.length === 0) { + throw new Error("tools/list result must contain a nonempty tools array"); + } + + const names = new Set(); + for (const [index, tool] of result.tools.entries()) { + assertObject(tool, `tool at index ${index}`); + if (typeof tool.name !== "string" || tool.name.trim() === "") { + throw new Error(`tool at index ${index} has an invalid name`); + } + if (names.has(tool.name)) { + throw new Error(`tools/list returned duplicate tool name ${JSON.stringify(tool.name)}`); + } + names.add(tool.name); + + if (tool.description !== undefined && typeof tool.description !== "string") { + throw new Error(`tool ${JSON.stringify(tool.name)} has an invalid description`); + } + assertObject(tool.inputSchema, `inputSchema for tool ${JSON.stringify(tool.name)}`); + if (tool.inputSchema.type !== "object") { + throw new Error(`inputSchema for tool ${JSON.stringify(tool.name)} must have type object`); + } + } + + return result.tools; +} + +function assertObject(value, label) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } +} + +function waitForExit(process) { + return new Promise((resolve) => { + process.once("error", (error) => { + resolve({ error }); + }); + process.once("exit", (code, signal) => { + resolve({ code, signal }); + }); + }); +} + +function assertSuccessfulExit(outcome, stderr) { + if (outcome.error) { + throw new Error(`Unable to launch MCP server: ${outcome.error.message}`); + } + if (outcome.code !== 0) { + throw new Error( + `MCP server ${formatExit(outcome.code, outcome.signal)}${formatStderr(stderr)}`, + ); + } +} + +async function stopChild(process, exit) { + if (!process || process.exitCode !== null || process.signalCode !== null) return; + + process.kill("SIGTERM"); + if (exit) { + const stopped = await Promise.race([ + exit.then(() => true), + new Promise((resolve) => setTimeout(() => resolve(false), 1_000)), + ]); + if (stopped) return; + } + + process.kill("SIGKILL"); +} + +function withTimeout(promise, milliseconds, message) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(message)), milliseconds); + promise.then( + (value) => { + clearTimeout(timer); + resolve(value); + }, + (error) => { + clearTimeout(timer); + reject(error); + }, + ); + }); +} + +function formatExit(code, signal) { + return signal ? `was terminated by ${signal}` : `exited with status ${code}`; +} + +function formatStderr(stderr) { + const trimmed = stderr.trim(); + return trimmed ? `\nServer stderr:\n${trimmed}` : ""; +} + +function formatToolContent(content) { + if (!Array.isArray(content)) return JSON.stringify(content); + + const text = content + .filter((item) => item && typeof item.text === "string") + .map((item) => item.text) + .join("\n") + .trim(); + return text || JSON.stringify(content); +} diff --git a/tools.json b/tools.json index 931536f..4f79ea1 100644 --- a/tools.json +++ b/tools.json @@ -8,6 +8,11 @@ "type": "string", "desc": "The action to perform: 'add' or 'commit'" }, + { + "name": "description", + "type": "string", + "desc": "Optional commit description/body (only applies when action is 'commit')" + }, { "name": "directory", "type": "string", @@ -78,6 +83,27 @@ } ] }, + { + "name": "git_commit_composer", + "description": "Show the commit composer for a repository, will open a UI if client supports it.", + "arguments": [ + { + "name": "custom_instructions", + "type": "string", + "desc": "Free-form guidance for the AI commit composer. Only meaningful when 'direction' is 'custom'." + }, + { + "name": "direction", + "type": "string", + "desc": "Initial grouping preset for the composer. 'auto' lets the AI choose; 'area' groups by codebase area / subsystem; 'type' groups by conventional commit type; 'custom' uses the supplied 'custom_instructions' text. Defaults to 'auto'." + }, + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + } + ] + }, { "name": "git_fetch", "description": "Download objects and refs from another repository (git fetch).", @@ -89,6 +115,17 @@ } ] }, + { + "name": "git_graph", + "description": "Show the commit graph for a repository (git log --graph), will open a UI if client supports it.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + } + ] + }, { "name": "git_log_or_diff", "description": "Show commit logs or changes between commits (git log --oneline or git diff).", @@ -147,6 +184,22 @@ } ] }, + { + "name": "git_resolve", + "description": "Show the conflict resolution UI for a repository, will open a UI if client supports it.", + "arguments": [ + { + "name": "directory", + "type": "string", + "desc": "Path of the working directory." + }, + { + "name": "instructions", + "type": "string", + "desc": "Optional guidance for the AI conflict resolution flow." + } + ] + }, { "name": "git_stash", "description": "Stash the changes in a dirty working directory (git stash).", @@ -215,22 +268,6 @@ "description": "Lists all Gitkraken workspaces", "arguments": [] }, - { - "name": "gitlens_commit_composer", - "description": "Gitlens Commit Composer. Organize your changes into well-formed commits with clear messages and descriptions. Useful for breaking large changes into smaller commits.", - "arguments": [ - { - "name": "directory", - "type": "string", - "desc": "Path of the working directory." - }, - { - "name": "instructions", - "type": "string", - "desc": "OPTIONAL. Use this ONLY if the user explicitly provided specific requirements about how commits should be organized, what commit messages should say, or particular commit structure preferences. Do NOT use this parameter unless you are 100% certain about the user's intentions. Examples: 'use conventional commits', 'prefix each commit with JIRA-123', 'keep commit messages under 50 characters'." - } - ] - }, { "name": "gitlens_launchpad", "description": "Gitlens Launchpad. Gets your open pull requests prioritized by what needs attention: ready to merge, has conflicts, awaiting review, etc. Helpful for checking todos, outstanding tasks, or deciding what to work on next.", @@ -327,8 +364,13 @@ }, { "name": "issues_assigned_to_me", - "description": "Fetch issues assigned to the user", + "description": "Fetch issues assigned to the user. Set assigned_to_me=false to broaden the result to all visible issues (so the caller can then filter client-side). Narrow results server-side with repository_name/repository_organization (GitHub), project/issue_type (Jira), or is_closed.", "arguments": [ + { + "name": "assigned_to_me", + "type": "boolean", + "desc": "Whether to limit results to issues assigned to you. Defaults to true." + }, { "name": "azure_organization", "type": "string", @@ -339,15 +381,96 @@ "type": "string", "desc": "Optionally set the Azure DevOps project name. Required for Azure DevOps" }, + { + "name": "fields", + "type": "string", + "desc": "Optional comma-separated list of top-level fields to keep on each result. Recommended for list/overview calls: omitting it returns every field, which on large lists can overflow the context window. Omit only when you need the full objects. Available fields: id, url, number, provider, repoName, repoOwner, repoHost, project, title, description, issueType, status, assignees, comments, childIssues." + }, + { + "name": "is_closed", + "type": "boolean", + "desc": "Set to true to also include closed/done issues. Defaults to false." + }, + { + "name": "issue_type", + "type": "string", + "desc": "Issue type. Jira only (e.g. Bug, Story, Task)." + }, { "name": "page", "type": "number", "desc": "Optional parameter to specify the page number, defaults to 1" }, + { + "name": "project", + "type": "string", + "desc": "Project key. Jira only (e.g. GKDEV)." + }, { "name": "provider", "type": "string", "desc": "Specify the issue provider" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Repository name. GitHub only; must be combined with repository_organization." + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Organization name. GitHub only." + } + ] + }, + { + "name": "issues_create", + "description": "Create a new issue. For Jira, repository_name is the project key. For Linear, repository_organization is the team UUID, key, or name.", + "arguments": [ + { + "name": "assignees", + "type": "array", + "desc": "Optional list of assignees. GitHub: usernames; GitLab: user IDs; Jira/Linear/Azure: first entry is used" + }, + { + "name": "azure_organization", + "type": "string", + "desc": "Optionally set the Azure DevOps organization name. Required for Azure DevOps" + }, + { + "name": "azure_project", + "type": "string", + "desc": "Optionally set the Azure DevOps project name. Required for Azure DevOps" + }, + { + "name": "body", + "type": "string", + "desc": "The body/description of the issue (markdown supported where the provider allows)" + }, + { + "name": "labels", + "type": "array", + "desc": "Optional list of label names to apply" + }, + { + "name": "provider", + "type": "string", + "desc": "Specify the issue provider" + }, + { + "name": "repository_name", + "type": "string", + "desc": "Repository name. Required for GitHub/GitLab; for Jira this is the project key" + }, + { + "name": "repository_organization", + "type": "string", + "desc": "Organization name. Required for GitHub/GitLab; for Linear use as team identifier" + }, + { + "name": "title", + "type": "string", + "desc": "The title of the issue" } ] }, @@ -389,13 +512,18 @@ }, { "name": "pull_request_assigned_to_me", - "description": "Search pull requests where you are the assignee, author, or reviewer", + "description": "Search pull requests where you are the author or assignee. Set reviewer to true to also include pull requests where you are a requested reviewer (github and gitlab only).", "arguments": [ { "name": "azure_project", "type": "string", "desc": "Optionally set the Azure DevOps project name of the pull request. Required for Azure DevOps" }, + { + "name": "fields", + "type": "string", + "desc": "Optional comma-separated list of top-level fields to keep on each result. Recommended for list/overview calls: omitting it returns every field, which on large lists can overflow the context window. Omit only when you need the full objects. Available fields: timestampCreatedAt, title, description, url, state, owner, repoName, repoHost, provider, id, files, clone, number, merged, draft, approved, authoredByMe, headBranch, baseBranch, headRepo." + }, { "name": "is_closed", "type": "boolean", @@ -420,6 +548,16 @@ "name": "repository_organization", "type": "string", "desc": "Set the organization name of the pull request. Required for Azure DevOps and Bitbucket" + }, + { + "name": "reviewer", + "type": "boolean", + "desc": "Set to true to also include pull requests where you are a requested reviewer. Supported by github, github_enterprise, gitlab, gitlab_self_hosted; ignored for Azure DevOps and Bitbucket." + }, + { + "name": "with_branches", + "type": "boolean", + "desc": "Include head/base branch names on each result. For GitHub this triggers an extra API call per PR, so leave off unless branches are needed." } ] }, @@ -427,6 +565,11 @@ "name": "pull_request_create", "description": "Create a new pull request", "arguments": [ + { + "name": "assign_to_me", + "type": "boolean", + "desc": "Assign the newly created pull request to the current authenticated user when supported by the provider" + }, { "name": "azure_project", "type": "string",