github-context-tools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ """
2
+ GitHub API tools for fetching code review context.
3
+
4
+ Call ``make_tools()`` once with an authenticated token to get a list of
5
+ plain callables. Pass them directly to any agent framework's tool registry.
6
+ The token is captured in a closure and never appears in any tool signature.
7
+
8
+ Usage::
9
+
10
+ from github_context_tools import make_tools
11
+
12
+ tools = make_tools(token="ghp_...") # or omit to read from GH_TOKEN / GITHUB_TOKEN env var
13
+
14
+ # Pass to any agent framework
15
+ agent.run(tools=tools)
16
+ """
17
+
18
+ from collections.abc import Callable
19
+
20
+ import httpx
21
+
22
+ from github_context_tools._request import make_requester
23
+ from github_context_tools.tools.code import make_code_tools
24
+ from github_context_tools.tools.conventions import make_conventions_tools
25
+ from github_context_tools.tools.history import make_history_tools
26
+ from github_context_tools.tools.issues import make_issues_tools
27
+ from github_context_tools.tools.pr import make_pr_tools
28
+
29
+
30
+ def make_tools(
31
+ token: str | None = None,
32
+ http_client: httpx.Client | None = None,
33
+ include: set[str] | None = None,
34
+ exclude: set[str] | None = None,
35
+ ) -> list[Callable]:
36
+ """
37
+ Return a list of GitHub API tool functions bound to the given token.
38
+
39
+ Each tool is a plain callable with a clean signature — no token, no
40
+ client object. Safe to call concurrently; each call opens and closes
41
+ its own HTTP connection.
42
+
43
+ :param token: GitHub personal access token. If omitted, reads from the
44
+ GH_TOKEN or GITHUB_TOKEN environment variable.
45
+ :param http_client: Optional httpx.Client instance. Use this to configure
46
+ SSL settings, proxies, timeouts, etc. If omitted, a new
47
+ client is created.
48
+ :param include: If provided, only tools whose names are in this set are
49
+ returned. Raises ValueError if any name is not recognised.
50
+ :param exclude: If provided, tools whose names are in this set are removed
51
+ from the result. Raises ValueError if any name is not
52
+ recognised. When both include and exclude are given,
53
+ exclusions are applied after inclusions.
54
+ """
55
+ if include is not None and exclude is not None and include.issubset(exclude):
56
+ raise ValueError(
57
+ "include and exclude are mutually exclusive: every tool in "
58
+ f"include {sorted(include)} is also in exclude {sorted(exclude)}, "
59
+ "which would return no tools."
60
+ )
61
+
62
+ get_json, get_text, post_json = make_requester(token, http_client)
63
+ all_tools = [
64
+ *make_pr_tools(get_json, get_text, post_json),
65
+ *make_code_tools(get_json, get_text, post_json),
66
+ *make_history_tools(get_json, get_text, post_json),
67
+ *make_issues_tools(get_json, get_text, post_json),
68
+ *make_conventions_tools(get_json, get_text, post_json),
69
+ ]
70
+
71
+ if include is None and exclude is None:
72
+ return all_tools
73
+
74
+ all_names = {t.__name__ for t in all_tools}
75
+
76
+ if include is not None:
77
+ unknown = include - all_names
78
+ if unknown:
79
+ raise ValueError(f"Unknown tool names in include: {sorted(unknown)}")
80
+
81
+ if exclude is not None:
82
+ unknown = exclude - all_names
83
+ if unknown:
84
+ raise ValueError(f"Unknown tool names in exclude: {sorted(unknown)}")
85
+
86
+ return [
87
+ t for t in all_tools
88
+ if (include is None or t.__name__ in include)
89
+ and (exclude is None or t.__name__ not in exclude)
90
+ ]
@@ -0,0 +1,192 @@
1
+ from collections.abc import Callable
2
+ from typing import Annotated
3
+
4
+ from git_unified_diff_parse import DiffParser
5
+
6
+ from github_context_tools._utils import validate_repo
7
+ from github_context_tools.models import BlameEntry, CommitDiff, CommitSummary
8
+
9
+ _BLAME_QUERY = """
10
+ query($owner: String!, $repo: String!, $expr: String!, $path: String!) {
11
+ repository(owner: $owner, name: $repo) {
12
+ object(expression: $expr) {
13
+ ... on Commit {
14
+ blame(path: $path) {
15
+ ranges {
16
+ startingLine
17
+ endingLine
18
+ age
19
+ commit {
20
+ oid
21
+ message
22
+ commitUrl
23
+ author { name date }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ """
32
+
33
+
34
+ def _blame_entries_from_response(data: dict) -> list[BlameEntry]:
35
+ if "errors" in data:
36
+ messages = ", ".join(e.get("message", "unknown") for e in data["errors"])
37
+ raise ValueError(f"GraphQL error from GitHub: {messages}")
38
+ try:
39
+ ranges = data["data"]["repository"]["object"]["blame"]["ranges"]
40
+ except (KeyError, TypeError) as e:
41
+ raise ValueError(
42
+ "Unexpected GraphQL response shape — the file or ref may not exist"
43
+ ) from e
44
+ return [
45
+ BlameEntry(
46
+ start_line=r["startingLine"],
47
+ end_line=r["endingLine"],
48
+ sha=r["commit"]["oid"],
49
+ message=r["commit"]["message"].splitlines()[0],
50
+ author=r["commit"]["author"]["name"],
51
+ date=r["commit"]["author"]["date"],
52
+ commit_url=r["commit"]["commitUrl"],
53
+ age=r["age"],
54
+ )
55
+ for r in ranges
56
+ ]
57
+
58
+
59
+ def make_history_tools(get_json, get_text, post_json) -> list[Callable]:
60
+ def get_file_commit_history(
61
+ repo: Annotated[str, "Repository in 'owner/name' format, e.g. 'acme/backend'"],
62
+ path: Annotated[
63
+ str, "File path relative to the repo root, e.g. 'src/auth/oauth.py'"
64
+ ],
65
+ max_commits: Annotated[int, "Maximum number of commits per page"] = 10,
66
+ page: Annotated[int, "Page number for pagination (1-indexed)"] = 1,
67
+ sha: Annotated[
68
+ str,
69
+ "Branch name, tag, or SHA to start listing from. Defaults to the repo's default branch",
70
+ ] = "",
71
+ author: Annotated[
72
+ str, "Filter by GitHub username or email address of the commit author"
73
+ ] = "",
74
+ since: Annotated[
75
+ str,
76
+ "Only include commits after this ISO 8601 timestamp, e.g. '2024-01-01T00:00:00Z'",
77
+ ] = "",
78
+ until: Annotated[
79
+ str,
80
+ "Only include commits before this ISO 8601 timestamp, e.g. '2024-12-31T23:59:59Z'",
81
+ ] = "",
82
+ ) -> Annotated[
83
+ list[CommitSummary],
84
+ (
85
+ "Recent commits that touched the file, newest-first. Each entry contains: "
86
+ "sha (full commit SHA), "
87
+ "message (first line of the commit message), "
88
+ "author (git author name), "
89
+ "date (ISO 8601 authored timestamp), "
90
+ "html_url (browser link to the commit on GitHub), "
91
+ "parents (list of parent SHAs — more than one parent indicates a merge commit)."
92
+ ),
93
+ ]:
94
+ """Fetch the commit history for a specific file."""
95
+ validate_repo(repo)
96
+ params = f"path={path}&per_page={max_commits}&page={page}"
97
+ if sha:
98
+ params += f"&sha={sha}"
99
+ if author:
100
+ params += f"&author={author}"
101
+ if since:
102
+ params += f"&since={since}"
103
+ if until:
104
+ params += f"&until={until}"
105
+ data = get_json(f"/repos/{repo}/commits?{params}")
106
+ results = []
107
+ for item in data:
108
+ commit = item["commit"]
109
+ git_author = commit.get("author") or {}
110
+ parents = tuple(p["sha"] for p in item.get("parents", []))
111
+ results.append(
112
+ CommitSummary(
113
+ sha=item["sha"],
114
+ message=commit["message"].splitlines()[0],
115
+ author=git_author.get("name", ""),
116
+ date=git_author.get("date", ""),
117
+ html_url=item.get("html_url", ""),
118
+ parents=parents,
119
+ )
120
+ )
121
+ return results
122
+
123
+ def get_commit_diff(
124
+ repo: Annotated[str, "Repository in 'owner/name' format, e.g. 'acme/backend'"],
125
+ commit_sha: Annotated[str, "Full or abbreviated commit SHA"],
126
+ ) -> Annotated[
127
+ CommitDiff,
128
+ (
129
+ "Parsed diff for the commit. Fields: "
130
+ "sha (the commit SHA), "
131
+ "diff (tuple of ChangedFile objects, one per file touched). "
132
+ "Each ChangedFile has: old_path (path before the change, None for added files), "
133
+ "new_path (path after the change, None for deleted files), "
134
+ "status (one of 'added', 'modified', 'removed', 'renamed', 'copied'), "
135
+ "is_binary (True for binary files — hunks will be empty), "
136
+ "hunks (list of DiffHunk objects). "
137
+ "Each DiffHunk has: header (raw @@ line), old_start, old_count, new_start, new_count, "
138
+ "lines (list of DiffLine). "
139
+ "Each DiffLine has: content (line text), is_addition, is_deletion, is_context (bool flags), "
140
+ "new_line_number, old_line_number (None on the side where the line doesn't exist)."
141
+ ),
142
+ ]:
143
+ """Fetch and parse the diff introduced by a single commit."""
144
+ validate_repo(repo)
145
+ diff_text = get_text(
146
+ f"/repos/{repo}/commits/{commit_sha}",
147
+ headers={"Accept": "application/vnd.github.diff"},
148
+ )
149
+ return CommitDiff(
150
+ sha=commit_sha,
151
+ diff=tuple(DiffParser().parse(diff_text)),
152
+ )
153
+
154
+ def get_blame(
155
+ repo: Annotated[str, "Repository in 'owner/name' format, e.g. 'acme/backend'"],
156
+ path: Annotated[
157
+ str, "File path relative to the repo root, e.g. 'src/auth/oauth.py'"
158
+ ],
159
+ ref: Annotated[str, "Branch name, tag, or commit SHA to blame at"],
160
+ ) -> Annotated[
161
+ list[BlameEntry],
162
+ (
163
+ "Blame ranges for the entire file, one entry per contiguous block of lines last touched "
164
+ "by the same commit. Each BlameEntry has: "
165
+ "start_line, end_line (1-indexed, inclusive line range covered by this entry), "
166
+ "sha (commit SHA that last modified these lines), "
167
+ "message (first line of that commit's message), "
168
+ "author (git author name), "
169
+ "date (ISO 8601 authored timestamp), "
170
+ "commit_url (browser link to the commit on GitHub), "
171
+ "age (recency score 1–10 relative to other changes in this file: 1 = most recent, 10 = oldest)."
172
+ ),
173
+ ]:
174
+ """Fetch git blame for an entire file via the GitHub GraphQL API."""
175
+ validate_repo(repo)
176
+ owner, repo_name = repo.split("/", 1)
177
+ return _blame_entries_from_response(
178
+ post_json(
179
+ "/graphql",
180
+ body={
181
+ "query": _BLAME_QUERY,
182
+ "variables": {
183
+ "owner": owner,
184
+ "repo": repo_name,
185
+ "expr": ref,
186
+ "path": path,
187
+ },
188
+ },
189
+ )
190
+ )
191
+
192
+ return [get_file_commit_history, get_commit_diff, get_blame]
@@ -0,0 +1,55 @@
1
+ from collections.abc import Callable
2
+ from typing import Annotated
3
+
4
+ from github_context_tools._utils import validate_repo
5
+ from github_context_tools.models import Issue, IssueComment
6
+
7
+
8
+ def make_issues_tools(get_json, _get_text, _post_json) -> list[Callable]:
9
+ def get_linked_issue(
10
+ repo: Annotated[str, "Repository in 'owner/name' format, e.g. 'acme/backend'"],
11
+ issue_number: Annotated[int, "GitHub issue number, e.g. 42"],
12
+ ) -> Annotated[
13
+ Issue,
14
+ (
15
+ "GitHub issue with fields: "
16
+ "number (issue number), "
17
+ "title, "
18
+ "body (issue description text), "
19
+ "author (GitHub login of the issue creator), "
20
+ "state ('open' or 'closed'), "
21
+ "state_reason (why it was closed: 'completed', 'not_planned', 'reopened', or null), "
22
+ "labels (tuple of label name strings), "
23
+ "created_at (ISO 8601 timestamp), "
24
+ "closed_at (ISO 8601 timestamp or null), "
25
+ "comments (tuple of IssueComment objects). "
26
+ "Each IssueComment has: author (GitHub login), body (comment text), "
27
+ "created_at (ISO 8601 timestamp), html_url (link to the comment)."
28
+ ),
29
+ ]:
30
+ """Fetch a GitHub issue and its comments by issue number."""
31
+ validate_repo(repo)
32
+ data = get_json(f"/repos/{repo}/issues/{issue_number}")
33
+ comments_data = get_json(f"/repos/{repo}/issues/{issue_number}/comments")
34
+ return Issue(
35
+ number=data["number"],
36
+ title=data["title"],
37
+ body=data["body"] or "",
38
+ author=data["user"]["login"],
39
+ state=data["state"],
40
+ state_reason=data.get("state_reason"),
41
+ labels=tuple(lb["name"] for lb in data.get("labels", [])),
42
+ created_at=data["created_at"],
43
+ closed_at=data.get("closed_at"),
44
+ comments=tuple(
45
+ IssueComment(
46
+ author=c["user"]["login"],
47
+ body=c["body"],
48
+ created_at=c["created_at"],
49
+ html_url=c["html_url"],
50
+ )
51
+ for c in comments_data
52
+ ),
53
+ )
54
+
55
+ return [get_linked_issue]
@@ -0,0 +1,159 @@
1
+ from collections.abc import Callable
2
+ from typing import Annotated
3
+
4
+ from git_unified_diff_parse import DiffParser
5
+
6
+ from github_context_tools._utils import parse_pr_url
7
+ from github_context_tools.models import PRComment, PRDescription, PRDiff, PRMetadata
8
+
9
+
10
+ def make_pr_tools(get_json, get_text, _post_json) -> list[Callable]:
11
+ def get_pr_metadata(
12
+ pr_url: Annotated[
13
+ str, "Full GitHub PR URL, e.g. 'https://github.com/owner/repo/pull/123'"
14
+ ],
15
+ ) -> Annotated[
16
+ PRMetadata,
17
+ (
18
+ "Pull request metadata with fields: repo (owner/name), pr_number, title, description,"
19
+ " author (GitHub login), base_branch, head_branch, base_sha, head_sha, html_url,"
20
+ " state ('open' or 'closed'), commits (count), changed_files (count),"
21
+ " additions (line count), deletions (line count)"
22
+ ),
23
+ ]:
24
+ """Fetch pull request metadata."""
25
+ owner, repo, pr_number = parse_pr_url(pr_url)
26
+ repo_full = f"{owner}/{repo}"
27
+ pr_data = get_json(f"/repos/{repo_full}/pulls/{pr_number}")
28
+ return PRMetadata(
29
+ repo=repo_full,
30
+ pr_number=pr_data["number"],
31
+ title=pr_data["title"],
32
+ description=pr_data["body"] or "",
33
+ author=pr_data["user"]["login"],
34
+ base_branch=pr_data["base"]["ref"],
35
+ head_branch=pr_data["head"]["ref"],
36
+ base_sha=pr_data["base"]["sha"],
37
+ head_sha=pr_data["head"]["sha"],
38
+ html_url=pr_data["html_url"],
39
+ state=pr_data["state"],
40
+ commits=pr_data["commits"],
41
+ changed_files=pr_data["changed_files"],
42
+ additions=pr_data["additions"],
43
+ deletions=pr_data["deletions"],
44
+ )
45
+
46
+ def get_parsed_pr_diff(
47
+ pr_url: Annotated[
48
+ str, "Full GitHub PR URL, e.g. 'https://github.com/owner/repo/pull/123'"
49
+ ],
50
+ ) -> Annotated[
51
+ PRDiff,
52
+ (
53
+ "Parsed PR diff with fields: repo (owner/name), diff (tuple of ChangedFile objects)."
54
+ " Each ChangedFile has: old_path, new_path, hunks (list of diff hunks)."
55
+ " Each hunk has: old_start, old_count, new_start, new_count, lines (list of DiffLine)."
56
+ " Each DiffLine has: kind ('added', 'removed', or 'context') and content (line text)."
57
+ ),
58
+ ]:
59
+ """Fetch and parse the pull request diff."""
60
+ owner, repo, pr_number = parse_pr_url(pr_url)
61
+ repo_full = f"{owner}/{repo}"
62
+ diff_text = get_text(
63
+ f"/repos/{repo_full}/pulls/{pr_number}",
64
+ headers={"Accept": "application/vnd.github.diff"},
65
+ )
66
+ return PRDiff(
67
+ repo=repo_full,
68
+ diff=tuple(DiffParser().parse(diff_text)),
69
+ )
70
+
71
+ def get_pr_description(
72
+ pr_url: Annotated[
73
+ str, "Full GitHub PR URL, e.g. 'https://github.com/owner/repo/pull/123'"
74
+ ],
75
+ ) -> Annotated[
76
+ PRDescription,
77
+ "PR description with fields: title (str), body (str, the full PR description text), labels (tuple of label name strings)",
78
+ ]:
79
+ """Fetch the pull request title, description body, and labels."""
80
+ owner, repo, pr_number = parse_pr_url(pr_url)
81
+ repo_full = f"{owner}/{repo}"
82
+ data = get_json(f"/repos/{repo_full}/pulls/{pr_number}")
83
+ return PRDescription(
84
+ title=data["title"],
85
+ body=data["body"] or "",
86
+ labels=tuple(label["name"] for label in data.get("labels", [])),
87
+ )
88
+
89
+ def get_pr_comments(
90
+ pr_url: Annotated[
91
+ str, "Full GitHub PR URL, e.g. 'https://github.com/owner/repo/pull/123'"
92
+ ],
93
+ ) -> Annotated[
94
+ list[PRComment],
95
+ (
96
+ "All comments on the pull request as a list of PRComment objects."
97
+ " Each PRComment has: author (GitHub login), body (comment text), created_at (ISO timestamp),"
98
+ " is_review_comment (True for inline file comments, False for PR-level conversation comments),"
99
+ " html_url (link to the comment)."
100
+ " For review comments (is_review_comment=True): path (file path), line (line number on the diff side),"
101
+ " side ('RIGHT' for additions, 'LEFT' for deletions), original_line (line number on the base commit),"
102
+ " original_side, start_line (first line for multi-line comments), start_side,"
103
+ " diff_hunk (surrounding diff context), commit_id (SHA of commit the comment was made on),"
104
+ " in_reply_to_id (ID of parent comment for replies, None for top-level)."
105
+ " For issue comments (is_review_comment=False): path, line, side, original_line, original_side,"
106
+ " start_line, start_side, diff_hunk, commit_id, in_reply_to_id are all None."
107
+ ),
108
+ ]:
109
+ """Fetch all existing comments on a pull request."""
110
+ owner, repo, pr_number = parse_pr_url(pr_url)
111
+ repo_full = f"{owner}/{repo}"
112
+ # Inline review comments attached to a specific file and line.
113
+ review_comments = get_json(f"/repos/{repo_full}/pulls/{pr_number}/comments")
114
+ # Every PR is also an issue in GitHub's data model and shares the same number.
115
+ # PR-level conversation comments (not tied to a file) are exposed via the Issues API.
116
+ issue_comments = get_json(f"/repos/{repo_full}/issues/{pr_number}/comments")
117
+ comments = [
118
+ PRComment(
119
+ author=c["user"]["login"],
120
+ body=c["body"],
121
+ created_at=c["created_at"],
122
+ path=c.get("path"),
123
+ line=c.get("line"),
124
+ side=c.get("side"),
125
+ original_line=c.get("original_line"),
126
+ original_side=c.get("original_side"),
127
+ start_line=c.get("start_line"),
128
+ start_side=c.get("start_side"),
129
+ diff_hunk=c.get("diff_hunk"),
130
+ commit_id=c.get("commit_id"),
131
+ in_reply_to_id=c.get("in_reply_to_id"),
132
+ html_url=c.get("html_url"),
133
+ is_review_comment=True,
134
+ )
135
+ for c in review_comments
136
+ ]
137
+ comments += [
138
+ PRComment(
139
+ author=c["user"]["login"],
140
+ body=c["body"],
141
+ created_at=c["created_at"],
142
+ path=None,
143
+ line=None,
144
+ side=None,
145
+ original_line=None,
146
+ original_side=None,
147
+ start_line=None,
148
+ start_side=None,
149
+ diff_hunk=None,
150
+ commit_id=None,
151
+ in_reply_to_id=None,
152
+ html_url=c.get("html_url"),
153
+ is_review_comment=False,
154
+ )
155
+ for c in issue_comments
156
+ ]
157
+ return comments
158
+
159
+ return [get_pr_metadata, get_parsed_pr_diff, get_pr_description, get_pr_comments]
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: github-context-tools
3
+ Version: 0.1.0
4
+ Summary: Typed GitHub API tools for fetching context in LLM-based workflows
5
+ Project-URL: Homepage, https://github.com/kruthis123/github-context-tools
6
+ Project-URL: Repository, https://github.com/kruthis123/github-context-tools
7
+ Project-URL: Bug Tracker, https://github.com/kruthis123/github-context-tools/issues
8
+ Project-URL: Changelog, https://github.com/kruthis123/github-context-tools/blob/main/CHANGELOG.md
9
+ Author-email: Kruthi Shiva Kumar <kruthis2601@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Kruthi Shiva Kumar
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ License-File: LICENSE
20
+ Keywords: code-review,context,diff,github,llm,pull-request
21
+ Classifier: Development Status :: 3 - Alpha
22
+ Classifier: Intended Audience :: Developers
23
+ Classifier: License :: OSI Approved :: MIT License
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Topic :: Software Development :: Version Control :: Git
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.12
30
+ Requires-Dist: git-unified-diff-parse>=0.1.1
31
+ Requires-Dist: httpx>=0.27
32
+ Description-Content-Type: text/markdown
33
+
34
+ # github-context-tools
35
+
36
+ Typed GitHub API tools that give LLMs structured access to pull requests, code, history, and issues.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install github-context-tools
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ from github_context_tools import make_tools
48
+
49
+ tools = make_tools(token="ghp_...") # or set GH_TOKEN / GITHUB_TOKEN env var
50
+
51
+ # Pass the list to your agent framework's tool registry
52
+ agent.run(tools=tools)
53
+ ```
54
+
55
+ ## How it works
56
+
57
+ `make_tools()` returns a list of plain Python functions. You pass that list directly to your agent framework's tool registry — no adapters or glue code needed.
58
+
59
+ Each tool is fully typed using `Annotated[type, "description"]` on every parameter and return value. Agent frameworks (Anthropic SDK, LangChain, OpenAI, etc.) read these annotations to automatically generate tool schemas, so the LLM always knows what each tool does, what to pass in, and what to expect back.
60
+
61
+ ## Available tools
62
+
63
+ ### Pull requests
64
+
65
+ | Tool | Description | Returns |
66
+ |---|---|---|
67
+ | [`get_pr_metadata`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/pr.py#L11) | Title, author, branch names, SHAs, state, and change stats for a PR | `PRMetadata` |
68
+ | [`get_parsed_pr_diff`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/pr.py#L46) | Structured diff of every file changed in a PR, broken into hunks and individual lines | `PRDiff` |
69
+ | [`get_pr_description`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/pr.py#L71) | Title, body, and labels of a PR | `PRDescription` |
70
+ | [`get_pr_comments`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/pr.py#L89) | All inline review comments and conversation comments on a PR | `list[PRComment]` |
71
+
72
+ ### Code
73
+
74
+ | Tool | Description | Returns |
75
+ |---|---|---|
76
+ | [`get_file_at_ref`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/code.py#L85) | Contents of a file at any branch, tag, or commit SHA | `FileContent` |
77
+ | [`get_directory_tree`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/code.py#L115) | Recursive file tree for a repo or subdirectory at a given ref | `dict` |
78
+ | [`search_codebase`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/code.py#L142) | Search a repo by content, file path, filename, or symbol name | `list[SearchResult]` |
79
+ | [`get_sibling_files`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/code.py#L189) | All other files in the same directory as a given file | `list[str]` |
80
+
81
+ ### History
82
+
83
+ | Tool | Description | Returns |
84
+ |---|---|---|
85
+ | [`get_file_commit_history`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/history.py#L60) | Recent commits that touched a file, newest-first. Supports filtering by author, date range, and branch | `list[CommitSummary]` |
86
+ | [`get_commit_diff`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/history.py#L123) | Structured diff introduced by a single commit | `CommitDiff` |
87
+ | [`get_blame`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/history.py#L154) | Blame ranges for an entire file, each annotated with the commit, author, and a recency score | `list[BlameEntry]` |
88
+
89
+ ### Issues
90
+
91
+ | Tool | Description | Returns |
92
+ |---|---|---|
93
+ | [`get_linked_issue`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/issues.py#L9) | Title, body, author, labels, state, and comments for a GitHub issue | `Issue` |
94
+
95
+ ### Conventions
96
+
97
+ | Tool | Description | Returns |
98
+ |---|---|---|
99
+ | [`get_repo_conventions`](https://github.com/kruthis123/github-context-tools/blob/master/src/github_context_tools/tools/conventions.py#L36) | Contents of well-known convention and context files from the default branch (`CONTRIBUTING.md`, `CLAUDE.md`, `AGENTS.md`, `.cursor/rules`, `.cursorrules`, `.github/copilot-instructions.md`, `docs/architecture.md`, `docs/ARCHITECTURE.md`, `docs/development.md`, `DEVELOPMENT.md`) | `RepoConventions` |
100
+
101
+ ## Selecting tools
102
+
103
+ By default `make_tools()` returns all 13 tools. Use `include` or `exclude` to control which tools are registered with your agent.
104
+
105
+ **Include only the tools you need:**
106
+
107
+ ```python
108
+ tools = make_tools(
109
+ token="ghp_...",
110
+ include={"get_pr_metadata", "get_parsed_pr_diff", "get_pr_comments"},
111
+ )
112
+ ```
113
+
114
+ **Exclude tools you don't want:**
115
+
116
+ ```python
117
+ tools = make_tools(
118
+ token="ghp_...",
119
+ exclude={"get_blame", "search_codebase"},
120
+ )
121
+ ```
122
+
123
+ Both parameters accept a `set[str]` of tool names (the function names listed in the [Available tools](#available-tools) table). Unrecognised names raise a `ValueError` immediately.
124
+
125
+ ## Authentication
126
+
127
+ Pass a token directly or set an environment variable:
128
+
129
+ ```bash
130
+ export GH_TOKEN=ghp_...
131
+ # or
132
+ export GITHUB_TOKEN=ghp_...
133
+ ```
134
+
135
+ ```python
136
+ tools = make_tools(token="ghp_...")
137
+ ```
138
+
139
+ ### Required token scopes
140
+
141
+ Use a classic personal access token (PAT) with the following scopes:
142
+
143
+ | Scope | Required for |
144
+ |---|---|
145
+ | `repo` | All tools on **private** repositories |
146
+ | `public_repo` | All tools on **public** repositories only |
147
+
148
+ Fine-grained PATs work too. Grant **read-only** access to the following permissions on the target repositories:
149
+
150
+ | Permission | Required for |
151
+ |---|---|
152
+ | Contents | `get_file_at_ref`, `get_directory_tree`, `get_sibling_files`, `get_repo_conventions` |
153
+ | Pull requests | `get_pr_metadata`, `get_parsed_pr_diff`, `get_pr_description`, `get_pr_comments` |
154
+ | Issues | `get_linked_issue` |
155
+ | Metadata | Required by GitHub for all repository access (granted automatically) |
156
+
157
+ `get_file_commit_history`, `get_commit_diff`, and `get_blame` use the Commits and GraphQL APIs, which are covered by the Contents and Metadata permissions above.
158
+
159
+ The token is captured in a closure and never appears in any tool signature, so it is never exposed to the LLM or included in generated schemas.
160
+
161
+ ## License
162
+
163
+ [MIT](https://github.com/kruthis123/github-context-tools/blob/master/LICENSE)