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:
GET /repos/{owner}/{repo}/git/ref/heads/{base}: the base branch’s latest commit.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.POST /repos/{owner}/{repo}/git/trees: the base commit’s tree, with the changes.POST /repos/{owner}/{repo}/git/commits: the commit, optionally with its author.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.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:
BaseModelThe author of a commit, as git records it: a name and an email address.
It is an argument of the
create_pull_requesttool. 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.- model_config = {'extra': 'forbid', 'frozen': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class smarter.apps.mcpserver.services.github.pulls.GitHubPullRequest(**data)[source]
Bases:
BaseModelA pull request that GitHub created, with the fields that the
create_pull_requesttool reports.PullRequestWriter.write()builds it from GitHub’s201 Createdresponse, plus theauthor,branch,baseandcommit_shathat it knows. GitHub’s response has many more fields, which are ignored.- model_config = {'extra': 'ignore', 'frozen': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class smarter.apps.mcpserver.services.github.pulls.PullRequestFile(**data)[source]
Bases:
BaseModelOne file that a pull request changes: its whole new content, or its deletion.
It is an argument of the
create_pull_requesttool. Exactly one ofcontentanddeletemust be given, whichvalidate_files()checks. A file is given whole, not as a diff, because GitHub’s api writes whole files.- model_config = {'extra': 'forbid', 'frozen': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class smarter.apps.mcpserver.services.github.pulls.PullRequestWriter(repository, token)[source]
Bases:
objectWrite 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 callswrite(), which takes the steps that the module’s docstring lists. Each step is one request to GitHub’s api, with the caller’s token, throughrequest(), which turns a refusal into an explanation for the LLM. The folders that the change touches are read once each, and kept intrees.The token is kept only for the writer’s lifetime, and is never logged.
- Parameters:
repository (
ProjectRepository) – The repository.token (
str) – The caller’s classic personal access token, fromvalidate_token().
- 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:
- 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:
- 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
GETrequest to GitHub for each folder on the path that has not been read yet (seetree()).- Parameters:
- 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 whenmethodisPOST. 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 intrees, 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
shais None.Side effects:
GETrequests to GitHub for the folders on the files’ paths (seeexisting_entry()).- Parameters:
root_tree (
str) – The sha of the base commit’s tree.files (
list[PullRequestFile]) – The changed files.
- Return type:
- 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:
GETrequests to GitHub for the base branch, its commit and the changed files’ folders, thenPOSTrequests 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_FOOTERis 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:
- 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. ThenPullRequestWriter.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_FOOTERis 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:
- 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:
- 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:
- 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 anddelete.