GitHub Issues

GitHub issues of The Smarter Project’s repositories: searched, opened and commented on.

  • search_issues() asks GitHub’s issue search, GET /search/issues, live, rather than the index: issues change by the minute, and an LLM that is about to open an issue must see one that was opened a minute ago. It searches one repository with repo:, or all of them with org:, and drops the results of the organization’s other repositories. Issues are public, so the result does not depend on who asks. Without the caller’s classic token the search is anonymous, and GitHub allows anonymous clients 10 searches a minute for each IP address, which the platform’s users share. A token raises that to 30 for its owner.

  • read_issue() reads an issue and its latest comments, with GET /repos/{owner}/{repo}/issues/{number} and its /comments, so that an LLM can see what an issue already says before it adds to it.

  • create_issue() opens an issue, optionally with labels, with POST /repos/{owner}/{repo}/issues.

  • close_issue() closes an issue that a merged pull request resolved, with a comment that says why, after GitHub confirms that the pull request was merged.

  • comment_on_issue() adds a comment to an existing issue, with POST /repos/{owner}/{repo}/issues/{number}/comments.

create_issue(), comment_on_issue() and close_issue() write as the caller, with the caller’s own classic personal access token, so the issue or comment is the caller’s, and GitHub decides what the caller may do. Each appends a footer that says where the text came from. Never cache them: every call writes. None of them writes a credential: see refuse_secrets(). See smarter.apps.mcpserver.services.github.client for how the token is checked and kept secret, and how GitHub’s errors are explained.

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.issues.GitHubComment(**data)[source]

Bases: BaseModel

A comment that GitHub created, with the fields of its response that the comment_on_issue tool reports.

comment_on_issue() builds it from GitHub’s 201 Created response, plus the author that created() adds. GitHub’s response has many more fields, which are ignored.

author: str | None
html_url: str
id: int
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.issues.GitHubIssue(**data)[source]

Bases: BaseModel

An issue that GitHub created, with the fields of its response that the create_issue tool reports.

create_issue() builds it from GitHub’s 201 Created response, plus the author that created() adds. GitHub’s response has many more fields, which are ignored.

author: str | None
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.issues.GitHubIssueClosed(**data)[source]

Bases: BaseModel

An issue that close_issue() closed, with the comment that says why.

The close_issue tool reports it. It is built from the comment that comment_on_issue() added and the pull request that GitHub confirmed was merged, rather than from GitHub’s response to the closing, which the tool does not need.

author: str | None
comment_url: str
html_url: str
model_config = {'frozen': True}

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

number: int
pull_request_url: str
class smarter.apps.mcpserver.services.github.issues.GitHubIssueComment(**data)[source]

Bases: BaseModel

A comment on an issue, with the fields that read_issue reports.

read_issue() builds it from an item of GitHub’s GET /repos/{owner}/{repo}/issues/{number}/comments response. GitHub’s response has many more fields, which are ignored.

body: str | None
created_at: str
html_url: str
model_config = {'extra': 'ignore', 'frozen': True}

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

user: GitHubUser | None
class smarter.apps.mcpserver.services.github.issues.GitHubIssueDetail(**data)[source]

Bases: BaseModel

One issue, as GET /repos/{owner}/{repo}/issues/{number} returns it, with the fields that read_issue reports.

GitHub’s response has many more fields, which are ignored. GitHub returns a pull request from this endpoint too, with a pull_request field, so read_issue can also read a pull request’s description and conversation, e.g. before close_issue.

body: str | None
closed_at: str | None
comments: int
created_at: str
html_url: str
labels: list[GitHubLabel]
model_config = {'extra': 'ignore', 'frozen': True}

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

number: int
pull_request: JsonValue | None
state: str
state_reason: str | None
title: str
updated_at: str
user: GitHubUser | None
class smarter.apps.mcpserver.services.github.issues.GitHubIssueHit(**data)[source]

Bases: BaseModel

One issue that GitHub’s search found, with the fields that the search_issues tool reports.

search_issues() builds it from an item of GitHub’s GET /search/issues response. GitHub’s response has many more fields, which are ignored. It is frozen, because it is a snapshot of GitHub’s answer.

body: str | None
comments: int
property excerpt: str

The start of the issue’s body, on one line, at most MAX_EXCERPT_LENGTH characters.

Runs of whitespace, including line breaks, become one space, so that the excerpt fits on one line of the tool’s result. A longer body is cut, and ends with .... An issue without a body has an empty excerpt.

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
property repository: str

The full name of the issue’s repository, e.g. smarter-sh/smarter.

GitHub’s search result names the repository only by its api url, e.g. https://api.github.com/repos/smarter-sh/smarter, so the name is that url without its prefix.

repository_url: str
state: str
title: str
updated_at: str
class smarter.apps.mcpserver.services.github.issues.GitHubIssueThread(**data)[source]

Bases: BaseModel

An issue and its latest comments, oldest first, as read_issue() returns them.

The read_issue tool formats it as Markdown.

comments: list[GitHubIssueComment]

The issue’s latest comments, at most COMMENTS_PER_PAGE, oldest first.

The issue’s comments count says how many it has in all.

issue: GitHubIssueDetail

The issue.

model_config = {'frozen': True}

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

class smarter.apps.mcpserver.services.github.issues.GitHubPullRequestState(**data)[source]

Bases: BaseModel

A pull request, as GET /repos/{owner}/{repo}/pulls/{number} returns it, with the fields that close_issue reads.

close_issue() closes an issue only if merged is true. GitHub’s response has many more fields, which are ignored.

html_url: str
merged: bool
merged_at: str | None
model_config = {'extra': 'ignore', 'frozen': True}

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

number: int
smarter.apps.mcpserver.services.github.issues.IssueState

open, closed, or all, which leaves the state out of the query.

Type:

Which issues a search asks for

alias of Literal[‘open’, ‘closed’, ‘all’]

smarter.apps.mcpserver.services.github.issues.close_issue(repository, number, pull_request_number, comment, token)[source]

Close an issue that a merged pull request resolved, with a comment that says why, as the owner of a GitHub token.

The issue is closed only if GitHub says that the pull request was merged, so that an LLM cannot close an issue on its own judgment alone, nor because a log or an issue told it to. The comment is added first, and the issue is then closed as completed, so that an issue is never closed without saying why.

Side effects, in order: a GET request for the pull request; a POST that adds the comment (see comment_on_issue()); and a PATCH that closes the issue, which notifies its participants. If the closing fails after the comment was added, the error says so, so that the LLM does not add the comment again. Nothing is cached.

Parameters:
  • repository (ProjectRepository) – The repository of the issue and the pull request.

  • number (int) – The issue’s number.

  • pull_request_number (int) – The number of the merged pull request that resolved the issue.

  • comment (str) – Why the issue is resolved, in Markdown. COMMENT_FOOTER is appended.

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

Return type:

GitHubIssueClosed

Returns:

The closed issue.

Raises:

SmarterMCPServerServiceError – If the token is not a classic personal access token, the comment holds a credential, the pull request was not found or not merged, or GitHub could not be reached, or refused the comment or the closing.

smarter.apps.mcpserver.services.github.issues.comment_on_issue(repository, number, body, token)[source]

Add a comment to an issue of a repository of The Smarter Project, as the owner of a GitHub token.

Like create_issue(), it writes with the caller’s own token, after checking the token and the body for credentials. It also comments on a pull request, whose number GitHub shares with issues. close_issue() calls it to say why an issue is closed.

Side effects: one POST /repos/{owner}/{repo}/issues/{number}/comments request, which creates a public comment and notifies the issue’s participants. The result is never cached: every call writes. The new comment’s url is logged.

Parameters:
  • repository (ProjectRepository) – The repository.

  • number (int) – The issue’s number.

  • body (str) – The comment, in Markdown. COMMENT_FOOTER is appended.

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

Return type:

GitHubComment

Returns:

The new comment.

Raises:

SmarterMCPServerServiceError – If the token is not a classic personal access token, the body holds a credential, or GitHub could not be reached, or did not create the comment, e.g. the issue is locked.

smarter.apps.mcpserver.services.github.issues.create_issue(repository, title, body, token, labels=())[source]

Open an issue in a repository of The Smarter Project, as the owner of a GitHub token.

The issue is written with the caller’s own token, so it is theirs on GitHub, and GitHub decides what they may do, e.g. whether their labels are kept. Before anything is sent, the token is checked (validate_token()), and the title, body and labels are checked for credentials (refuse_secrets()), because the issue is public.

Side effects: one POST /repos/{owner}/{repo}/issues request, which creates a public issue and notifies the repository’s watchers. The result is never cached: every call writes. The new issue’s url is logged.

Parameters:
  • repository (ProjectRepository) – The repository.

  • title (str) – The issue’s title.

  • body (str) – The issue’s body, in Markdown. ISSUE_FOOTER is appended.

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

  • labels (tuple[str, ...]) – The issue’s labels, e.g. bug. GitHub creates a label that the repository does not have, and silently drops the labels of a caller who cannot push to the repository.

Return type:

GitHubIssue

Returns:

The new issue.

Raises:

SmarterMCPServerServiceError – If the token is not a classic personal access token, the title or body holds a credential, or GitHub could not be reached, or did not create the issue.

smarter.apps.mcpserver.services.github.issues.read_issue(repository, number, token)[source]

Read an issue of a repository, and its latest COMMENTS_PER_PAGE comments.

Issues are public, so the result does not depend on who asks. Without the caller’s classic token the request is anonymous, at GitHub’s anonymous rate limit.

Side effects: one GET request to GitHub for the issue, and, if it has comments, a second one for the last page of its comments, which holds the latest. A failure is logged.

Parameters:
  • repository (ProjectRepository) – The repository.

  • number (int) – The issue’s number.

  • token (SecretStr | None) – The caller’s GitHub token, or None. Only a classic personal access token is sent.

Return type:

GitHubIssueThread

Returns:

The issue and its latest comments, oldest first.

Raises:

SmarterMCPServerServiceError – If GitHub could not be reached, the issue was not found, or GitHub’s response could not be read.

smarter.apps.mcpserver.services.github.issues.search_issues(repositories, query, one, state, limit, token)[source]

Search the issues of the repositories, best match first.

GitHub’s issue search is asked live, rather than an index, because issues change by the minute (see the module’s docstring). Side effects: one GET /search/issues request to GitHub, which counts against the caller’s, or the platform’s anonymous, search rate limit. A failure is logged.

Parameters:
  • repositories (tuple[ProjectRepository, ...]) – The repositories to search. They have the same owner.

  • query (str) – The keywords.

  • one (bool) – Whether to search only the first repository, rather than all of them.

  • state (Literal['open', 'closed', 'all']) – open, closed or all.

  • limit (int) – The maximum number of issues.

  • token (SecretStr | None) – The caller’s GitHub token, or None. Only a classic personal access token is sent: otherwise the search is anonymous.

Return type:

list[GitHubIssueHit]

Returns:

The issues of the repositories.

Raises:

SmarterMCPServerServiceError – If GitHub could not be reached, or refused the search, e.g. its rate limit was reached.