GitHub Pull Requests

Pull requests to The Smarter Project’s repositories, opened as the caller.

create_pull_request() writes a change as one commit on a new branch, and opens a pull request from it, with GitHub’s REST api alone, without a clone:

  1. GET /repos/{owner}/{repo}/git/ref/heads/{base}: the base branch’s latest commit.

  2. GET /repos/{owner}/{repo}/git/trees/{sha}: the trees of the folders of the changed files, so that an executable file stays executable, and a symbolic link or a folder is never overwritten by a file.

  3. POST /repos/{owner}/{repo}/git/trees: the base commit’s tree, with the changes.

  4. POST /repos/{owner}/{repo}/git/commits: the commit, optionally with its author.

  5. POST /repos/{owner}/{repo}/git/refs: the new branch. GitHub refuses a branch that exists, so the tool never changes an existing branch, e.g. main, nor anyone’s work.

  6. POST /repos/{owner}/{repo}/pulls: the pull request.

Like create_issue(), it writes as the caller, with the caller’s own classic personal access token, which needs the public_repo scope, and never writes a credential (see refuse_secrets()). It never changes a file under .github/: GitHub would refuse it without the workflow scope, and a workflow that a pull request changes may run with the repository’s secrets.

Note

Experimental. The MCPServer was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.

class smarter.apps.mcpserver.services.github.pulls.GitHubCommitAuthor(**data)[source]

Bases: BaseModel

The author of a commit, as git records it: a name and an email address.

It is an argument of the create_pull_request tool. When it is given, it is both the commit’s author and its committer, e.g. Claude <noreply@anthropic.com>. When it is omitted, GitHub records the token’s owner. extra="forbid" refuses any other field, so that a misspelt argument is reported to the LLM rather than ignored.

email: str
model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

name: str
class smarter.apps.mcpserver.services.github.pulls.GitHubPullRequest(**data)[source]

Bases: BaseModel

A pull request that GitHub created, with the fields that the create_pull_request tool reports.

PullRequestWriter.write() builds it from GitHub’s 201 Created response, plus the author, branch, base and commit_sha that it knows. GitHub’s response has many more fields, which are ignored.

author: str | None
base: str
branch: str
commit_sha: str
draft: bool
html_url: str
model_config = {'extra': 'ignore', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

number: int
title: str
class smarter.apps.mcpserver.services.github.pulls.PullRequestFile(**data)[source]

Bases: BaseModel

One file that a pull request changes: its whole new content, or its deletion.

It is an argument of the create_pull_request tool. Exactly one of content and delete must be given, which validate_files() checks. A file is given whole, not as a diff, because GitHub’s api writes whole files.

content: str | None
delete: bool
model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

path: str
class smarter.apps.mcpserver.services.github.pulls.PullRequestWriter(repository, token)[source]

Bases: object

Write a change as one commit on a new branch, and open a pull request from it.

One writer opens one pull request: create_pull_request() makes it, after checking the token, the branch names, the files and the text, and calls write(), which takes the steps that the module’s docstring lists. Each step is one request to GitHub’s api, with the caller’s token, through request(), which turns a refusal into an explanation for the LLM. The folders that the change touches are read once each, and kept in trees.

The token is kept only for the writer’s lifetime, and is never logged.

Parameters:
__init__(repository, token)[source]
branch_commit(branch)[source]

Return the sha of a branch’s latest commit, which the new branch starts from.

Side effects: one GET /repos/{owner}/{repo}/git/ref/heads/{branch} request to GitHub.

Parameters:

branch (str) – The branch’s name, e.g. main.

Return type:

str

Returns:

The commit’s sha.

Raises:

SmarterMCPServerServiceError – If the branch was not found, GitHub could not be reached or refused the request, or its response could not be read.

default_branch()[source]

Return the repository’s default branch, e.g. main.

Side effects: one GET /repos/{owner}/{repo} request to GitHub.

Return type:

str

Returns:

The default branch’s name.

Raises:

SmarterMCPServerServiceError – If GitHub could not be reached, refused the request, or its response could not be read.

existing_entry(root_tree, path)[source]

Return the tree entry of a path at the base commit, or None if it does not exist.

It walks from the root folder down the path’s folders, one tree at a time, rather than reading the whole repository’s tree recursively, which may be large.

Side effects: one GET request to GitHub for each folder on the path that has not been read yet (see tree()).

Parameters:
  • root_tree (str) – The sha of the base commit’s tree.

  • path (str) – The path, e.g. smarter/smarter/apps/plugin/models.py.

Return type:

GitTreeEntry | None

Returns:

The entry, whose mode says whether it is a file, an executable, a link or a folder, or None if the path, or one of its folders, does not exist.

Raises:

SmarterMCPServerServiceError – If a folder could not be read.

repository

The repository that the pull request is opened in.

request(method, path, action, expected=200, json=None, not_found=None)[source]

Send one request to the repository’s api, and return GitHub’s response, if it succeeded.

Side effects: one request to GitHub (see github_request()), which writes when method is POST. A refusal is logged with its status.

Parameters:
  • method (str) – The HTTP method.

  • path (str) – The path after /repos/{owner}/{repo}, e.g. /git/refs.

  • action (str) – What the request does, for the error message, e.g. create the branch.

  • expected (int) – The status of success.

  • json (dict[str, Any] | None) – The request’s JSON body, if any.

  • not_found (str | None) – What a 404 means, if not that the repository was not found.

Return type:

Response

Returns:

GitHub’s response.

Raises:

SmarterMCPServerServiceError – If GitHub could not be reached, or refused the request.

token

The caller’s classic personal access token, which every request is sent with.

tree(sha)[source]

Return a tree, i.e. one folder, which is read once per pull request.

Side effects: the first time that a sha is asked for, one GET /repos/{owner}/{repo}/git/trees/{sha} request to GitHub. The tree is then kept in trees, because several changed files may share a folder.

Parameters:

sha (str) – The tree’s sha.

Return type:

GitTree

Returns:

The tree.

Raises:

SmarterMCPServerServiceError – If GitHub could not be reached or refused the request, or its response could not be read.

tree_entries(root_tree, files)[source]

Return the entries of the new tree: each file’s content and mode, or its deletion.

Each file is compared with the base commit, so that a changed file keeps its mode, e.g. an executable stays executable, a new file is an ordinary file, and a folder or a symbolic link is never overwritten. A deletion is an entry whose sha is None.

Side effects: GET requests to GitHub for the folders on the files’ paths (see existing_entry()).

Parameters:
  • root_tree (str) – The sha of the base commit’s tree.

  • files (list[PullRequestFile]) – The changed files.

Return type:

list[dict[str, Any]]

Returns:

The entries, for POST /git/trees.

Raises:

SmarterMCPServerServiceError – If a deleted file does not exist, or a path is a folder or a symbolic link.

trees

The folders read so far, by their sha, so that each is read once.

write(branch, base, title, body, commit_message, files, author, draft)[source]

Write the commit on a new branch, and open the pull request.

It takes the steps that the module’s docstring lists, in order. The arguments were already checked by create_pull_request(), which documents them.

Side effects: GET requests to GitHub for the base branch, its commit and the changed files’ folders, then POST requests that write a tree, a commit, a new branch and the pull request, which notifies the repository’s watchers. The new branch and the pull request’s url are logged. Nothing is cached.

Parameters:
  • branch (str) – The new branch.

  • base (str | None) – The branch to merge into, or None for the repository’s default branch.

  • title (str) – The pull request’s title.

  • body (str) – The pull request’s description. PULL_REQUEST_FOOTER is appended.

  • commit_message (str) – The commit’s message.

  • files (list[PullRequestFile]) – The changed files.

  • author (GitHubCommitAuthor | None) – The commit’s author and committer, or None for the token’s owner.

  • draft (bool) – Whether to open the pull request as a draft.

Return type:

GitHubPullRequest

Returns:

The new pull request.

Raises:

SmarterMCPServerServiceError – If a step fails. If the branch was created, but the pull request was not, the error says so, so that the LLM does not create it again.

smarter.apps.mcpserver.services.github.pulls.create_pull_request(repository, branch, title, body, commit_message, files, token, base=None, author=None, draft=False)[source]

Open a pull request in a repository of The Smarter Project, as the owner of a GitHub token.

The change is one commit, on a new branch from the latest commit of base. Each file is given whole: its new content, or its deletion. A changed file keeps its mode, e.g. an executable stays executable.

Everything that can be checked without GitHub is checked first: the token (validate_token()), the branch names, the files, and the text and file contents for credentials (refuse_secrets()), because the pull request is public. Then PullRequestWriter.write() takes the steps that the module’s docstring lists.

Side effects: the requests to GitHub of PullRequestWriter.write(), which create a new branch and a public pull request, as the caller. Nothing is cached: every call writes.

Parameters:
  • repository (ProjectRepository) – The repository.

  • branch (str) – The new branch, e.g. claude/fix-plugin-timeout. It must not exist.

  • title (str) – The pull request’s title.

  • body (str) – The pull request’s description, in Markdown. PULL_REQUEST_FOOTER is appended.

  • commit_message (str) – The commit’s message.

  • files (list[PullRequestFile]) – The changed files.

  • token (SecretStr | None) – The caller’s classic personal access token.

  • base (str | None) – The branch to merge into, and to branch from. Defaults to the repository’s default branch.

  • author (GitHubCommitAuthor | None) – The commit’s author and committer. Defaults to the token’s owner.

  • draft (bool) – Whether to open it as a draft.

Return type:

GitHubPullRequest

Returns:

The new pull request.

Raises:

SmarterMCPServerServiceError – If the token is not a classic personal access token, the branch name or a file is not valid, the change holds a credential, or GitHub could not be reached, or refused a step, e.g. because the branch exists.

smarter.apps.mcpserver.services.github.pulls.validate_branch(name)[source]

Return a branch name, if git and GitHub accept it.

It is checked before anything is sent to GitHub, so that the LLM gets a clear explanation, rather than GitHub’s, and so that a name cannot change the path of an api request.

Parameters:

name (str) – The branch name, e.g. claude/fix-plugin-timeout.

Return type:

str

Returns:

The name.

Raises:

SmarterMCPServerServiceError – If git would refuse the name.

smarter.apps.mcpserver.services.github.pulls.validate_files(files)[source]

Return a pull request’s files, if they are a change that the tool may write.

They are checked before anything is sent to GitHub. Whether a file exists, and whether it is a folder or a symbolic link, is checked later, against the base commit, by PullRequestWriter.tree_entries().

Parameters:

files (list[PullRequestFile]) – The files.

Return type:

list[PullRequestFile]

Returns:

The files.

Raises:

SmarterMCPServerServiceError – If there are none or too many, a path is not a plain relative path, is under .github/, or appears twice, or a file has both or neither of content and delete.