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.github.pulls.GitHubCommitAuthor(**data)[source]

Bases: BaseModel

The author of a commit, as git records it.

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.github.pulls.GitHubPullRequest(**data)[source]

Bases: BaseModel

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

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.github.pulls.PullRequestFile(**data)[source]

Bases: BaseModel

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

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.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.

Parameters:
  • repository (ProjectRepository) – The repository.

  • token (str) – The caller’s classic personal access token, from validate_token().

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

Return the sha of a branch’s latest commit.

Return type:

str

default_branch()[source]

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

Return type:

str

existing_entry(root_tree, path)[source]

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

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

  • path (str) – The path.

Return type:

GitTreeEntry | None

Returns:

The entry, whose mode says whether it is a file, an executable, a link or a folder.

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.

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:

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

tree(sha)[source]

Return a tree, which is read once per pull request.

Return type:

GitTree

tree_entries(root_tree, files)[source]

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

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:

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

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

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

See create_pull_request().

Raises:

SmarterMCPServerToolError – If a step fails. If the branch was created, but the pull request was not, the error says so.

Return type:

GitHubPullRequest

smarter.apps.mcpserver.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.

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:

SmarterMCPServerToolError – 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.github.pulls.validate_branch(name)[source]

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

Parameters:

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

Return type:

str

Returns:

The name.

Raises:

SmarterMCPServerToolError – If git would refuse the name.

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

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

Parameters:

files (list[PullRequestFile]) – The files.

Return type:

list[PullRequestFile]

Returns:

The files.

Raises:

SmarterMCPServerToolError – 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.