github-mcp-agent 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.
File without changes
@@ -0,0 +1,103 @@
1
+ import os
2
+ from contextlib import contextmanager
3
+ from pathlib import Path
4
+
5
+ from dotenv import load_dotenv
6
+ from strands import Agent
7
+ from strands.tools.mcp.mcp_client import MCPClient
8
+ from mcp import StdioServerParameters
9
+ from mcp.client.stdio import stdio_client
10
+
11
+ from github_mcp_agent.tools import detect_current_repo, local_tools
12
+ from github_mcp_agent import providers
13
+
14
+
15
+ _config_dir = Path.home() / ".config" / "github-mcp-agent"
16
+ load_dotenv(_config_dir / ".env")
17
+ load_dotenv()
18
+
19
+ MODEL_ID = os.getenv("MODEL_ID", "us.anthropic.claude-haiku-4-5-20251001-v1:0")
20
+ PROVIDER = os.getenv("PROVIDER", "bedrock")
21
+ VERBOSE = os.getenv("VERBOSE", "").lower() in ("1", "true", "yes")
22
+
23
+
24
+ def _load_system_prompt() -> tuple[str, str | None]:
25
+ custom = _config_dir / "system_prompt.txt"
26
+ if custom.exists():
27
+ prompt = custom.read_text()
28
+ else:
29
+ from importlib.resources import files
30
+ prompt = files("github_mcp_agent").joinpath("system_prompt.txt").read_text()
31
+
32
+ repo = detect_current_repo()
33
+ if repo != "No GitHub remote detected":
34
+ prompt += f"\n\nThe user is currently working in the GitHub repository: {repo}. Default to this repository for all actions unless the user explicitly specifies another."
35
+ return prompt, repo
36
+ return prompt, None
37
+
38
+
39
+ def _make_verbose_callback(provider: str):
40
+ import json as _json
41
+ is_ollama = provider == "ollama"
42
+
43
+ def callback(**kwargs):
44
+ if "current_tool_use" in kwargs:
45
+ tool = kwargs["current_tool_use"]
46
+ if tool.get("name"):
47
+ print(f"\n \033[2;36m> {tool['name']}\033[0m", flush=True)
48
+ if "data" in kwargs:
49
+ text = kwargs["data"]
50
+ if is_ollama:
51
+ try:
52
+ parsed = _json.loads(text)
53
+ if isinstance(parsed, dict) and "text" in parsed:
54
+ text = parsed["text"]
55
+ except Exception:
56
+ pass
57
+ print(text, end="", flush=True)
58
+
59
+ return callback
60
+
61
+
62
+ @contextmanager
63
+ def create_agent(provider=None, model_id=None, verbose=False):
64
+ _provider = provider or PROVIDER
65
+ _model_id = model_id or MODEL_ID
66
+
67
+ token = os.environ.get("GITHUB_TOKEN")
68
+ if not token:
69
+ raise RuntimeError(
70
+ "GITHUB_TOKEN is not set. Run 'github-agent setup' to configure."
71
+ )
72
+
73
+ mcp_client = MCPClient(
74
+ lambda: stdio_client(
75
+ StdioServerParameters(
76
+ command="docker",
77
+ args=[
78
+ "run", "-i", "--rm",
79
+ "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
80
+ "ghcr.io/github/github-mcp-server",
81
+ "stdio",
82
+ "--toolsets", "all",
83
+ "--log-file", "/dev/null",
84
+ ],
85
+ env={"GITHUB_PERSONAL_ACCESS_TOKEN": token},
86
+ )
87
+ )
88
+ )
89
+
90
+ model = providers.build_model(_provider, _model_id)
91
+ system_prompt, current_repo = _load_system_prompt()
92
+
93
+ with mcp_client:
94
+ mcp_tools = mcp_client.list_tools_sync()
95
+ agent_kwargs = dict(model=model, tools=mcp_tools + local_tools, system_prompt=system_prompt)
96
+ if verbose or VERBOSE:
97
+ agent_kwargs["callback_handler"] = _make_verbose_callback(_provider)
98
+ else:
99
+ cb = providers.make_callback(_provider)
100
+ if cb:
101
+ agent_kwargs["callback_handler"] = cb
102
+ agent = Agent(**agent_kwargs)
103
+ yield agent, current_repo, len(mcp_tools) + len(local_tools)
@@ -0,0 +1,184 @@
1
+ import os
2
+ import readline
3
+ import select
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+ import questionary
10
+ from rich.console import Console
11
+ from rich.rule import Rule
12
+ from rich.text import Text
13
+
14
+ console = Console()
15
+
16
+ CONFIG_DIR = Path.home() / ".config" / "github-mcp-agent"
17
+
18
+ _PROVIDER_CHOICES = [
19
+ "AWS Bedrock", "Anthropic API", "OpenAI", "Google Gemini", "GitHub Copilot", "Local (Ollama)"
20
+ ]
21
+ _PROVIDER_KEY = {
22
+ "AWS Bedrock": "bedrock",
23
+ "Anthropic API": "anthropic",
24
+ "OpenAI": "openai",
25
+ "Google Gemini": "gemini",
26
+ "GitHub Copilot": "copilot",
27
+ "Local (Ollama)": "ollama",
28
+ }
29
+
30
+
31
+ def _pick_model_for_provider(provider: str, _ask) -> tuple[str, str]:
32
+ import github_mcp_agent.providers as pkg
33
+ mod = getattr(pkg, provider)
34
+ console.print(f" [dim]Provider: {provider} (to switch, run: github-agent provider)[/dim]")
35
+ if provider == "ollama":
36
+ model_id = mod.pick_model(_ask)
37
+ else:
38
+ model_choices = [f"{name} ({desc})" for _, name, desc in mod.MODELS]
39
+ model_display = _ask(questionary.select, "Model:", choices=model_choices)
40
+ model_id = mod.MODELS[model_choices.index(model_display)][0]
41
+ return provider, model_id
42
+
43
+
44
+ def _run_agent(verbose: bool = False):
45
+ from github_mcp_agent.agent import MODEL_ID, PROVIDER, create_agent
46
+ try:
47
+ with create_agent(verbose=verbose) as (agent, current_repo, total_tools):
48
+ console.print()
49
+ console.print(Rule("[bold green]GitHub MCP Agent[/bold green]"))
50
+ repo_label = f"[bold]{current_repo}[/bold]" if current_repo else "[dim]none detected[/dim]"
51
+ console.print(f" [dim]Loaded [bold]{total_tools}[/bold] tools | Repo: {repo_label} | Provider: [bold]{PROVIDER}[/bold] | Model: [bold]{MODEL_ID}[/bold] | Type 'exit' to quit[/dim]")
52
+ console.print(Rule())
53
+ console.print()
54
+
55
+ while True:
56
+ try:
57
+ user_input = input("\033[1;36m You > \033[0m")
58
+ while select.select([sys.stdin], [], [], 0.05)[0]:
59
+ user_input += "\n" + sys.stdin.readline().rstrip("\n")
60
+ except (KeyboardInterrupt, EOFError):
61
+ break
62
+
63
+ if not user_input.strip():
64
+ continue
65
+ if user_input.strip().lower() in ["exit", "quit"]:
66
+ break
67
+
68
+ console.print()
69
+ console.print(Text(" Agent", style="bold magenta"))
70
+ console.print()
71
+
72
+ try:
73
+ agent(user_input)
74
+ except Exception as e:
75
+ console.print(f"[bold red]Error:[/bold red] {e}")
76
+
77
+ console.print()
78
+
79
+ console.print()
80
+ console.print(Rule("[dim]Session ended[/dim]"))
81
+ console.print()
82
+
83
+ except RuntimeError as e:
84
+ console.print(f"\n[bold red]{e}[/bold red]\n")
85
+ sys.exit(1)
86
+ except Exception as e:
87
+ console.print(f"\n[bold red]Failed to start:[/bold red] {e}\n")
88
+ sys.exit(1)
89
+
90
+
91
+ def _open_config():
92
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
93
+ config_file = CONFIG_DIR / ".env"
94
+ if not config_file.exists():
95
+ config_file.write_text(
96
+ "GITHUB_TOKEN=\nAWS_PROFILE=default\nAWS_REGION=us-east-1\nMODEL_ID=us.anthropic.claude-haiku-4-5-20251001-v1:0\n"
97
+ )
98
+ editor = os.environ.get("EDITOR", "nano")
99
+ subprocess.run([editor, str(config_file)])
100
+
101
+
102
+ def _open_prompt():
103
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
104
+ prompt_file = CONFIG_DIR / "system_prompt.txt"
105
+ if not prompt_file.exists():
106
+ from importlib.resources import files
107
+ default = files("github_mcp_agent").joinpath("system_prompt.txt").read_text()
108
+ prompt_file.write_text(default)
109
+ console.print(f" [dim]Created {prompt_file} with default prompt - edit to customize.[/dim]\n")
110
+ editor = os.environ.get("EDITOR", "nano")
111
+ subprocess.run([editor, str(prompt_file)])
112
+
113
+
114
+ @click.group(invoke_without_command=True, context_settings={"help_option_names": ["-h", "--help"]})
115
+ @click.option("--verbose", "-v", is_flag=True, default=False, help="Print tool calls as they happen")
116
+ @click.pass_context
117
+ def cli(ctx, verbose):
118
+ """GitHub MCP Agent - talk to your repos in plain English."""
119
+ if ctx.invoked_subcommand is None:
120
+ _run_agent(verbose=verbose)
121
+
122
+
123
+ @cli.command()
124
+ def setup():
125
+ """Run the interactive setup wizard."""
126
+ from github_mcp_agent.setup_wizard import run
127
+ run()
128
+
129
+
130
+ @cli.command(name="provider")
131
+ def switch_provider():
132
+ """Switch AI provider - pick credentials + model and save to config."""
133
+ from github_mcp_agent.setup_wizard import _ask, _write_config
134
+ from github_mcp_agent import providers as _providers
135
+ provider_display = _ask(questionary.select, "Provider:", choices=_PROVIDER_CHOICES)
136
+ provider_key = _PROVIDER_KEY[provider_display]
137
+ provider_values = _providers.setup(provider_key, _ask)
138
+ _write_config({"PROVIDER": provider_key, **provider_values})
139
+ console.print(" [green]Saved.[/green]")
140
+
141
+
142
+ @cli.command(name="model")
143
+ def switch_model():
144
+ """Pick a model for the current provider and save to config."""
145
+ from github_mcp_agent.agent import PROVIDER
146
+ from github_mcp_agent.setup_wizard import _ask, _write_config
147
+ _, effective_model = _pick_model_for_provider(PROVIDER, _ask)
148
+ _write_config({"MODEL_ID": effective_model})
149
+ console.print(f" [green]Saved:[/green] model={effective_model}")
150
+
151
+
152
+ @cli.command()
153
+ def token():
154
+ """Update your GitHub Personal Access Token."""
155
+ from github_mcp_agent.setup_wizard import _ask, _validate_github_token, _write_config
156
+ new_token = _ask(questionary.password, "GitHub Personal Access Token:")
157
+ console.print(" Validating...", end=" ")
158
+ if _validate_github_token(new_token):
159
+ console.print("[green]valid[/green]")
160
+ _write_config({"GITHUB_TOKEN": new_token})
161
+ console.print(" [green]Saved.[/green]")
162
+ else:
163
+ console.print("[red]invalid - check the token and scopes[/red]")
164
+ sys.exit(1)
165
+
166
+
167
+ @cli.command()
168
+ def config():
169
+ """Open the config file in $EDITOR."""
170
+ _open_config()
171
+
172
+
173
+ @cli.command()
174
+ def prompt():
175
+ """Open the system prompt file in $EDITOR."""
176
+ _open_prompt()
177
+
178
+
179
+ def main():
180
+ cli()
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
@@ -0,0 +1,32 @@
1
+ from . import anthropic, bedrock, copilot, gemini, ollama, openai
2
+
3
+
4
+ _REGISTRY = {
5
+ "bedrock": bedrock,
6
+ "anthropic": anthropic,
7
+ "openai": openai,
8
+ "gemini": gemini,
9
+ "copilot": copilot,
10
+ "ollama": ollama,
11
+ }
12
+
13
+
14
+ def build_model(provider: str, model_id: str):
15
+ mod = _REGISTRY.get(provider)
16
+ if mod is None:
17
+ raise RuntimeError(f"Unknown provider: {provider}")
18
+ return mod.build_model(model_id)
19
+
20
+
21
+ def make_callback(provider: str):
22
+ if provider == "ollama":
23
+ return ollama.make_callback()
24
+ return None
25
+
26
+
27
+ def setup(provider: str, _ask) -> dict:
28
+ mod = _REGISTRY.get(provider)
29
+ if mod is None:
30
+ raise RuntimeError(f"Unknown provider: {provider}")
31
+ return mod.setup(_ask)
32
+ pypi-AgEIcHlwaS5vcmcCJGU0ZDI1YWQ5LTEyOGMtNDUyZi1iNmZkLTEzOWU0MmRkNDIzOAACKlszLCJjZTAzNzUyYy00ZjNhLTQ3MTYtYjM2ZS02YmY0ZGNhZjkyMDciXQAABiD04xa6bIcHRFlnmMHzKPunpg81Z6OhNO_P102kWxtY9g
@@ -0,0 +1,28 @@
1
+ import os
2
+
3
+ import questionary
4
+
5
+ MODELS = [
6
+ ("claude-haiku-4-5-20251001", "claude-haiku-4-5", "fastest"),
7
+ ("claude-sonnet-4-6", "claude-sonnet-4-6", "balanced"),
8
+ ("claude-opus-4-7", "claude-opus-4-7", "most capable"),
9
+ ]
10
+
11
+
12
+ def build_model(model_id: str):
13
+ from strands.models.anthropic import AnthropicModel
14
+ if not os.environ.get("ANTHROPIC_API_KEY"):
15
+ raise RuntimeError("ANTHROPIC_API_KEY is not set. Run 'github-agent setup' to configure.")
16
+ return AnthropicModel(model_id=model_id, max_tokens=8096)
17
+
18
+
19
+ def setup(_ask) -> dict:
20
+ from rich.console import Console
21
+ Console().print(" [dim]Get your key at: console.anthropic.com/settings/keys[/dim]")
22
+ key = _ask(questionary.password, "Anthropic API key (sk-ant-...):")
23
+ model_choices = [f"{name} ({desc})" for _, name, desc in MODELS]
24
+ model_display = _ask(questionary.select, "Model:", choices=model_choices)
25
+ return {
26
+ "ANTHROPIC_API_KEY": key,
27
+ "MODEL_ID": MODELS[model_choices.index(model_display)][0],
28
+ }
@@ -0,0 +1,112 @@
1
+ import os
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import questionary
6
+
7
+ REGIONS = [
8
+ ("us-east-1", "N. Virginia - recommended"),
9
+ ("us-west-2", "Oregon"),
10
+ ("eu-west-1", "Ireland"),
11
+ ("eu-central-1", "Frankfurt"),
12
+ ("eu-west-3", "Paris"),
13
+ ("ap-northeast-1", "Tokyo"),
14
+ ("ap-southeast-1", "Singapore"),
15
+ ("ap-southeast-2", "Sydney"),
16
+ ("ca-central-1", "Canada"),
17
+ ("sa-east-1", "Sao Paulo"),
18
+ ]
19
+
20
+ MODELS = [
21
+ ("us.anthropic.claude-haiku-4-5-20251001-v1:0", "claude-haiku-4-5", "fastest, cheapest"),
22
+ ("us.anthropic.claude-sonnet-4-6", "claude-sonnet-4-6", "balanced"),
23
+ ("us.anthropic.claude-opus-4-7", "claude-opus-4-7", "most capable"),
24
+ ]
25
+
26
+
27
+ def build_model(model_id: str):
28
+ from strands.models import BedrockModel
29
+ return BedrockModel(model_id=model_id, region_name=os.getenv("AWS_REGION", "us-east-1"))
30
+
31
+
32
+ def _list_aws_profiles() -> list[str]:
33
+ profiles = []
34
+ for f in [Path.home() / ".aws" / "credentials", Path.home() / ".aws" / "config"]:
35
+ if f.exists():
36
+ for line in f.read_text().splitlines():
37
+ if line.startswith("[") and line.endswith("]"):
38
+ name = line[1:-1].replace("profile ", "")
39
+ if name not in profiles:
40
+ profiles.append(name)
41
+ return profiles
42
+
43
+
44
+ def _validate_aws_profile(profile: str) -> bool:
45
+ env = os.environ.copy()
46
+ env["AWS_PROFILE"] = profile
47
+ return subprocess.run(["aws", "sts", "get-caller-identity"], capture_output=True, env=env).returncode == 0
48
+
49
+
50
+ def setup(_ask) -> dict:
51
+ values = {}
52
+
53
+ while True:
54
+ cred_method = _ask(
55
+ questionary.select,
56
+ "AWS credentials:",
57
+ choices=["Use existing profile", "Enter access keys directly", "AWS SSO / browser login"],
58
+ )
59
+
60
+ if cred_method == "Use existing profile":
61
+ profiles = _list_aws_profiles()
62
+ choices = profiles + ["other (type manually)"] if profiles else ["other (type manually)"]
63
+ choice = _ask(questionary.select, "AWS profile:", choices=choices)
64
+ if choice == "other (type manually)":
65
+ profile = _ask(questionary.text, "Profile name:", default="default")
66
+ else:
67
+ profile = choice
68
+ from rich.console import Console
69
+ Console().print(" Validating...", end=" ")
70
+ if _validate_aws_profile(profile):
71
+ Console().print("[green]valid[/green]")
72
+ values["AWS_PROFILE"] = profile
73
+ break
74
+ else:
75
+ Console().print("[red]invalid - check your AWS credentials[/red]")
76
+
77
+ elif cred_method == "Enter access keys directly":
78
+ values["AWS_ACCESS_KEY_ID"] = _ask(questionary.text, "AWS Access Key ID:")
79
+ values["AWS_SECRET_ACCESS_KEY"] = _ask(questionary.password, "AWS Secret Access Key:")
80
+ session_token = _ask(questionary.text, "AWS Session Token (leave blank if none):")
81
+ if session_token:
82
+ values["AWS_SESSION_TOKEN"] = session_token
83
+ break
84
+
85
+ elif cred_method == "AWS SSO / browser login":
86
+ profiles = _list_aws_profiles()
87
+ choices = profiles + ["other (type manually)"] if profiles else ["other (type manually)"]
88
+ choice = _ask(questionary.select, "SSO profile to use:", choices=choices)
89
+ if choice == "other (type manually)":
90
+ profile = _ask(questionary.text, "Profile name:")
91
+ else:
92
+ profile = choice
93
+ from rich.console import Console
94
+ console = Console()
95
+ console.print(f"\n Running [bold]aws sso login --profile {profile}[/bold]")
96
+ result = subprocess.run(["aws", "sso", "login", "--profile", profile])
97
+ if result.returncode == 0:
98
+ values["AWS_PROFILE"] = profile
99
+ break
100
+ console.print("[red]SSO login failed - profile may not be configured for SSO.[/red]")
101
+ console.print("[dim] Run: aws configure sso --profile <name>[/dim]")
102
+ console.print("[dim] Or pick a different credential method below.[/dim]\n")
103
+
104
+ region_choices = [f"{r} ({label})" for r, label in REGIONS]
105
+ region_display = _ask(questionary.select, "AWS Region:", choices=region_choices)
106
+ values["AWS_REGION"] = region_display.split()[0]
107
+
108
+ model_choices = [f"{name} ({desc}) -> {mid}" for mid, name, desc in MODELS]
109
+ model_display = _ask(questionary.select, "Model:", choices=model_choices)
110
+ values["MODEL_ID"] = MODELS[model_choices.index(model_display)][0]
111
+
112
+ return values
@@ -0,0 +1,32 @@
1
+ import os
2
+
3
+ import questionary
4
+
5
+ MODELS = [
6
+ ("gpt-4o", "gpt-4o", "flagship"),
7
+ ("gpt-4o-mini", "gpt-4o-mini", "fast, cheap"),
8
+ ("claude-sonnet-4-5", "claude-sonnet-4-5", "Anthropic via Copilot"),
9
+ ("o3-mini", "o3-mini", "reasoning"),
10
+ ("gemini-1.5-pro", "gemini-1.5-pro", "Google via Copilot"),
11
+ ]
12
+
13
+
14
+ def build_model(model_id: str):
15
+ from strands.models.litellm import LiteLLMModel
16
+ token = os.environ.get("GITHUB_TOKEN")
17
+ if not token:
18
+ raise RuntimeError("GITHUB_TOKEN is not set. Run 'github-agent setup' to configure.")
19
+ return LiteLLMModel(
20
+ model_id=f"openai/{model_id}",
21
+ params={"api_base": "https://api.githubcopilot.com", "api_key": token},
22
+ )
23
+
24
+
25
+ def setup(_ask) -> dict:
26
+ from rich.console import Console
27
+ console = Console()
28
+ console.print(" [dim]Uses your GitHub token - no extra API key needed.[/dim]")
29
+ console.print(" [dim]Requires an active Copilot subscription (student pack, individual, or business).[/dim]")
30
+ model_choices = [f"{name} ({desc})" for _, name, desc in MODELS]
31
+ model_display = _ask(questionary.select, "Model:", choices=model_choices)
32
+ return {"MODEL_ID": MODELS[model_choices.index(model_display)][0]}
@@ -0,0 +1,28 @@
1
+ import os
2
+
3
+ import questionary
4
+
5
+ MODELS = [
6
+ ("gemini-2.0-flash", "gemini-2.0-flash", "fast, recommended"),
7
+ ("gemini-1.5-pro", "gemini-1.5-pro", "most capable"),
8
+ ("gemini-1.5-flash", "gemini-1.5-flash", "fast"),
9
+ ]
10
+
11
+
12
+ def build_model(model_id: str):
13
+ from strands.models.litellm import LiteLLMModel
14
+ if not os.environ.get("GEMINI_API_KEY"):
15
+ raise RuntimeError("GEMINI_API_KEY is not set. Run 'github-agent setup' to configure.")
16
+ return LiteLLMModel(model_id=f"gemini/{model_id}")
17
+
18
+
19
+ def setup(_ask) -> dict:
20
+ from rich.console import Console
21
+ Console().print(" [dim]Get your key at: aistudio.google.com/apikey[/dim]")
22
+ key = _ask(questionary.password, "Google AI Studio API key:")
23
+ model_choices = [f"{name} ({desc})" for _, name, desc in MODELS]
24
+ model_display = _ask(questionary.select, "Model:", choices=model_choices)
25
+ return {
26
+ "GEMINI_API_KEY": key,
27
+ "MODEL_ID": MODELS[model_choices.index(model_display)][0],
28
+ }
@@ -0,0 +1,81 @@
1
+ import os
2
+ import subprocess
3
+
4
+ import questionary
5
+
6
+ POPULAR_MODELS = [
7
+ "llama3.2",
8
+ "llama3.1",
9
+ "mistral",
10
+ "gemma2",
11
+ "phi3",
12
+ "codellama",
13
+ "deepseek-r1",
14
+ ]
15
+
16
+
17
+ def build_model(model_id: str):
18
+ from strands.models.litellm import LiteLLMModel
19
+ base_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
20
+ return LiteLLMModel(model_id=f"ollama/{model_id}", params={"api_base": base_url})
21
+
22
+
23
+ def make_callback():
24
+ import json as _json
25
+
26
+ def callback(**kwargs):
27
+ if "data" in kwargs:
28
+ text = kwargs["data"]
29
+ try:
30
+ parsed = _json.loads(text)
31
+ if isinstance(parsed, dict) and "text" in parsed:
32
+ text = parsed["text"]
33
+ except Exception:
34
+ pass
35
+ print(text, end="", flush=True)
36
+
37
+ return callback
38
+
39
+
40
+ def pick_model(_ask) -> str:
41
+ result = subprocess.run(["ollama", "list"], capture_output=True, text=True)
42
+ installed = [line.split()[0] for line in result.stdout.splitlines()[1:] if line.split()]
43
+ choices = (installed if installed else POPULAR_MODELS) + ["other (type manually)"]
44
+ choice = _ask(questionary.select, "Model:", choices=choices)
45
+ if choice == "other (type manually)":
46
+ return _ask(questionary.text, "Model name:")
47
+ return choice
48
+
49
+
50
+ def setup(_ask) -> dict:
51
+ from rich.console import Console
52
+ console = Console()
53
+
54
+ result = subprocess.run(["ollama", "list"], capture_output=True, text=True)
55
+ installed = []
56
+ if result.returncode == 0:
57
+ for line in result.stdout.splitlines()[1:]:
58
+ parts = line.split()
59
+ if parts:
60
+ installed.append(parts[0])
61
+
62
+ if installed:
63
+ choices = installed + ["other (type manually)"]
64
+ choice = _ask(questionary.select, "Model:", choices=choices)
65
+ if choice == "other (type manually)":
66
+ model = _ask(questionary.text, "Model name (e.g. llama3.2):")
67
+ else:
68
+ model = choice
69
+ else:
70
+ console.print(" [dim]No models installed yet. Pick one to pull:[/dim]")
71
+ choices = POPULAR_MODELS + ["other (type manually)"]
72
+ choice = _ask(questionary.select, "Model:", choices=choices)
73
+ if choice == "other (type manually)":
74
+ model = _ask(questionary.text, "Model name:")
75
+ else:
76
+ model = choice
77
+ console.print(f"\n Running [bold]ollama pull {model}[/bold]")
78
+ subprocess.run(["ollama", "pull", model])
79
+
80
+ base_url = _ask(questionary.text, "Ollama base URL:", default="http://localhost:11434")
81
+ return {"MODEL_ID": model, "OLLAMA_BASE_URL": base_url}
@@ -0,0 +1,28 @@
1
+ import os
2
+
3
+ import questionary
4
+
5
+ MODELS = [
6
+ ("gpt-4o-mini", "gpt-4o-mini", "fast, cheap"),
7
+ ("gpt-4o", "gpt-4o", "flagship"),
8
+ ("gpt-4-turbo", "gpt-4-turbo", "legacy"),
9
+ ]
10
+
11
+
12
+ def build_model(model_id: str):
13
+ from strands.models.litellm import LiteLLMModel
14
+ if not os.environ.get("OPENAI_API_KEY"):
15
+ raise RuntimeError("OPENAI_API_KEY is not set. Run 'github-agent setup' to configure.")
16
+ return LiteLLMModel(model_id=f"openai/{model_id}")
17
+
18
+
19
+ def setup(_ask) -> dict:
20
+ from rich.console import Console
21
+ Console().print(" [dim]Get your key at: platform.openai.com/api-keys[/dim]")
22
+ key = _ask(questionary.password, "OpenAI API key (sk-...):")
23
+ model_choices = [f"{name} ({desc})" for _, name, desc in MODELS]
24
+ model_display = _ask(questionary.select, "Model:", choices=model_choices)
25
+ return {
26
+ "OPENAI_API_KEY": key,
27
+ "MODEL_ID": MODELS[model_choices.index(model_display)][0],
28
+ }
@@ -0,0 +1,134 @@
1
+ import subprocess
2
+ import sys
3
+ import urllib.request
4
+ import urllib.error
5
+ from pathlib import Path
6
+
7
+ import questionary
8
+ from rich.console import Console
9
+ from rich.rule import Rule
10
+
11
+ from github_mcp_agent import providers
12
+
13
+ console = Console()
14
+ CONFIG_DIR = Path.home() / ".config" / "github-mcp-agent"
15
+ CONFIG_FILE = CONFIG_DIR / ".env"
16
+
17
+
18
+ def _check(label: str, ok: bool, hint: str = ""):
19
+ if ok:
20
+ console.print(f" [green]OK[/green] {label}")
21
+ else:
22
+ console.print(f" [red]FAIL[/red] {label}" + (f" - {hint}" if hint else ""))
23
+ return ok
24
+
25
+
26
+ def _check_prerequisites(provider: str = "bedrock") -> bool:
27
+ console.print("\n[bold]Checking prerequisites...[/bold]")
28
+ ok = True
29
+ py = sys.version_info >= (3, 10)
30
+ ok &= _check("Python >= 3.10", py, f"found {sys.version.split()[0]}")
31
+ docker = subprocess.run(["docker", "info"], capture_output=True).returncode == 0
32
+ ok &= _check("Docker installed and running", docker, "install Docker from docker.com")
33
+ if provider == "bedrock":
34
+ aws = subprocess.run(["aws", "--version"], capture_output=True).returncode == 0
35
+ ok &= _check("AWS CLI installed", aws, "install from aws.amazon.com/cli")
36
+ if provider == "ollama":
37
+ ollama_ok = subprocess.run(["ollama", "list"], capture_output=True).returncode == 0
38
+ ok &= _check("Ollama installed and running", ollama_ok, "install from ollama.com then run: ollama serve")
39
+ return ok
40
+
41
+
42
+ def _ask(fn, *args, **kwargs):
43
+ try:
44
+ val = fn(*args, **kwargs).ask()
45
+ except (KeyboardInterrupt, EOFError):
46
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
47
+ sys.exit(0)
48
+ if val is None:
49
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
50
+ sys.exit(0)
51
+ return val
52
+
53
+
54
+ def _validate_github_token(token: str) -> bool:
55
+ req = urllib.request.Request("https://api.github.com/user")
56
+ req.add_header("Authorization", f"Bearer {token}")
57
+ req.add_header("User-Agent", "github-mcp-agent")
58
+ try:
59
+ with urllib.request.urlopen(req) as r:
60
+ return r.status == 200
61
+ except urllib.error.HTTPError:
62
+ return False
63
+
64
+
65
+ def _pull_docker_image():
66
+ console.print("\n[bold]Pulling GitHub MCP Docker image...[/bold]")
67
+ subprocess.run(["docker", "pull", "ghcr.io/github/github-mcp-server"], check=False)
68
+
69
+
70
+ def _write_config(values: dict):
71
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
72
+ existing = {}
73
+ if CONFIG_FILE.exists():
74
+ for line in CONFIG_FILE.read_text().splitlines():
75
+ if "=" in line and not line.startswith("#"):
76
+ k, _, v = line.partition("=")
77
+ existing[k.strip()] = v.strip()
78
+ existing.update({k: v for k, v in values.items() if v})
79
+ lines = [f"{k}={v}" for k, v in existing.items()]
80
+ CONFIG_FILE.write_text("\n".join(lines) + "\n")
81
+
82
+
83
+ def run():
84
+ console.print()
85
+ console.print(Rule("[bold green]GitHub MCP Agent - Setup[/bold green]"))
86
+
87
+ console.print()
88
+ console.print(" [dim]Create a token at: github.com/settings/tokens[/dim]")
89
+ console.print(" [dim]Required scopes: repo, read:org, project[/dim]")
90
+ token = _ask(questionary.password, "GitHub Personal Access Token:")
91
+ if token:
92
+ console.print(" Validating...", end=" ")
93
+ if _validate_github_token(token):
94
+ console.print("[green]valid[/green]")
95
+ else:
96
+ console.print("[red]invalid - check the token and scopes[/red]")
97
+ sys.exit(1)
98
+
99
+ console.print()
100
+
101
+ provider_display = _ask(
102
+ questionary.select,
103
+ "AI provider:",
104
+ choices=["AWS Bedrock", "Anthropic API", "OpenAI", "Google Gemini", "GitHub Copilot", "Local (Ollama)"],
105
+ )
106
+
107
+ provider_key = {
108
+ "AWS Bedrock": "bedrock",
109
+ "Anthropic API": "anthropic",
110
+ "OpenAI": "openai",
111
+ "Google Gemini": "gemini",
112
+ "GitHub Copilot": "copilot",
113
+ "Local (Ollama)": "ollama",
114
+ }[provider_display]
115
+
116
+ if not _check_prerequisites(provider=provider_key):
117
+ console.print("\n[red]Fix the issues above before continuing.[/red]")
118
+ sys.exit(1)
119
+
120
+ console.print()
121
+
122
+ provider_values = providers.setup(provider_key, _ask)
123
+
124
+ _write_config({"GITHUB_TOKEN": token, "PROVIDER": provider_key, **provider_values})
125
+ console.print(f"\n [dim]Config saved to {CONFIG_FILE}[/dim]")
126
+
127
+ _pull_docker_image()
128
+
129
+ console.print()
130
+ console.print(Rule("[bold green]Setup complete[/bold green]"))
131
+ console.print()
132
+ console.print(" Run [bold cyan]github-agent[/bold cyan] to start.")
133
+ console.print(" Run [bold cyan]github-agent prompt[/bold cyan] to customize the system prompt.")
134
+ console.print()
@@ -0,0 +1,57 @@
1
+ You are a GitHub project management assistant with full access to GitHub tools.
2
+
3
+ When a user asks about a repository, user, issue, PR, or anything GitHub-related, ALWAYS use your tools to look it up. Never say you don't have access or ask the user to provide information you can find yourself.
4
+
5
+ You also have two local file tools: read_local_file and list_local_files. Use these to read files directly from the user's local repo (e.g. TODO.md, README, config files) without going through the GitHub API.
6
+
7
+ Examples of proactive tool use:
8
+ - "what issues are open?" -> search for issues using the tools, do not ask which repo
9
+ - "summarize this repo" -> fetch the repo, read files, list issues and PRs
10
+ - "who contributed most?" -> look it up via the API
11
+ - If the user mentions a repo name or username, use it immediately without asking for clarification
12
+ - If a name search fails, try variations: different casing, hyphens vs underscores, with/without year suffixes, abbreviated names. Try at least 3-4 variations before giving up.
13
+ - If an org name is ambiguous, search for the org by keyword using search tools before asking the user.
14
+
15
+ Terminology - understand the difference:
16
+ - "GitHub Project" or "project board" = a kanban-style board for organizing issues and tasks (GitHub Projects v2). Use project-related tools to create and manage these.
17
+ - "repository" or "repo" = a codebase. Do NOT create a repository when the user asks for a "project board".
18
+ - When the user says "create a project", "create a github project", "project board", or "add issues to a project" they mean GitHub Projects, not a new repository.
19
+
20
+ GitHub Projects - field workflow:
21
+ - Fields like Status, Priority, Size, Estimate, and Iteration already exist on most projects. Never assume they don't exist.
22
+ - CRITICAL: Priority and Type are native GitHub ISSUE fields. Set them directly on the issue via issue_write. NEVER use projects_write for them. NEVER use labels for them.
23
+
24
+ Mandatory workflow when creating issues:
25
+ 1. Create the issue with issue_write — set "type" here (e.g. type: "Feature", type: "Bug")
26
+ 2. Set priority using the set_issue_priority tool — parameters: owner, repo, issue_number, priority ("urgent"/"high"/"medium"/"low")
27
+ 3. Add the issue to the project with projects_write method "add_project_item"
28
+ That's it. No project field updates needed for Priority or Type.
29
+
30
+ To update priority on an existing issue: use set_issue_priority directly.
31
+ GitHub's REST API does NOT support updating single_select project fields via projects_write (returns 400 "unsupported"). Do not attempt it.
32
+
33
+ When setting Status on a project item:
34
+ - Use projects_write method "update_project_item" ONLY for Status (not Priority, not Type)
35
+ - Get the Status field ID and option IDs first via projects_list method "list_project_fields"
36
+ - Status option IDs look like hex strings: "f75ad846", "47fc9ee4" — NOT integers
37
+ - Set Status to "Todo" by default unless told otherwise
38
+ - Do not set Size, Estimate, or Iteration unless explicitly asked.
39
+
40
+ Reading issues:
41
+ - NEVER call list_issues. It is forbidden. Always use list_issues_with_priorities instead — it returns number, title, priority, description, and state in one call.
42
+
43
+ Reading project board status (Todo / In Progress / Done):
44
+ - GitHub issue states (OPEN/CLOSED) are NOT the same as project board status.
45
+ - When the user asks what is "in progress", "todo", or "done", ALWAYS use the list_board_status tool. Never use projects_list for this — it causes context overflow.
46
+ - list_board_status requires owner (org name) and project_number (integer). To find the project number, use projects_list method "list_projects" with the owner first.
47
+ - Never tell the user that "in progress" doesn't exist just because issues are OPEN — always check the project board with list_board_status first.
48
+
49
+ Rules:
50
+ - Never delete repositories.
51
+ - Never change repository settings.
52
+ - Never create more than 5 issues at once unless clearly asked.
53
+ - Before creating issues, summarize what you are about to create.
54
+ - You are running in a terminal. Use plain text only. No markdown, no bold, no tables, no emojis, no bullet symbols.
55
+ - Always try tools first. Only ask the user for input if the tools truly cannot answer.
56
+ - Before saying you cannot do something, scan all available tools and try any that could be relevant. Never give up after one failed attempt.
57
+ - When you genuinely cannot do something, say so in one short sentence. Do not give the user a wall of manual instructions — they know how to use GitHub's website.
@@ -0,0 +1,214 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import subprocess
5
+ import urllib.error
6
+ import urllib.request
7
+
8
+ from strands import tool
9
+
10
+ LOCAL_REPO_PATH = os.getcwd()
11
+
12
+
13
+ def _gql(query, variables={}):
14
+ token = os.environ["GITHUB_TOKEN"]
15
+ data = json.dumps({"query": query, "variables": variables}).encode()
16
+ req = urllib.request.Request("https://api.github.com/graphql", data=data, method="POST")
17
+ req.add_header("Authorization", f"Bearer {token}")
18
+ req.add_header("Content-Type", "application/json")
19
+ with urllib.request.urlopen(req) as r:
20
+ return json.loads(r.read())
21
+
22
+
23
+ @tool
24
+ def set_issue_priority(owner: str, repo: str, issue_number: int, priority: str) -> str:
25
+ """Set the Priority field on a GitHub issue. priority: urgent, high, medium, or low."""
26
+ p = priority.lower()
27
+ if p not in ("urgent", "high", "medium", "low"):
28
+ return "Error: priority must be urgent, high, medium, or low"
29
+ try:
30
+ lookup = _gql("""
31
+ query($owner: String!, $repo: String!, $num: Int!) {
32
+ repository(owner: $owner, name: $repo) { issue(number: $num) { id } }
33
+ organization(login: $owner) {
34
+ issueFields(first: 10) {
35
+ nodes { ... on IssueFieldSingleSelect { id name options { id name } } }
36
+ }
37
+ }
38
+ }""", {"owner": owner, "repo": repo, "num": issue_number})
39
+ if "errors" in lookup:
40
+ return f"Error: {lookup['errors'][0]['message']}"
41
+ issue_id = lookup["data"]["repository"]["issue"]["id"]
42
+ pf = next((f for f in lookup["data"]["organization"]["issueFields"]["nodes"] if f.get("name") == "Priority"), None)
43
+ if not pf:
44
+ return "Priority field not found in org"
45
+ opt = next((o for o in pf["options"] if o["name"].lower() == p), None)
46
+ if not opt:
47
+ return f"Option '{p}' not found"
48
+ result = _gql("""
49
+ mutation($i: ID!, $f: ID!, $o: ID!) {
50
+ updateIssueFieldValue(input: {
51
+ issueId: $i
52
+ issueField: { fieldId: $f, singleSelectOptionId: $o }
53
+ }) { clientMutationId }
54
+ }""", {"i": issue_id, "f": pf["id"], "o": opt["id"]})
55
+ if "errors" in result:
56
+ return f"Error: {result['errors'][0]['message']}"
57
+ return f"Priority set to '{p}' on {owner}/{repo}#{issue_number}"
58
+ except urllib.error.HTTPError as e:
59
+ return f"HTTP {e.code}: {e.read().decode()}"
60
+ except Exception as e:
61
+ return f"Error: {e}"
62
+
63
+
64
+ @tool
65
+ def list_issues_with_priorities(owner: str, repo: str, state: str = "OPEN") -> str:
66
+ """List issues with their priority AND description in one call. Use this instead of list_issues whenever priorities matter. state: OPEN, CLOSED, or ALL."""
67
+ try:
68
+ result = _gql("""
69
+ query($owner: String!, $repo: String!, $state: [IssueState!]) {
70
+ repository(owner: $owner, name: $repo) {
71
+ issues(first: 100, states: $state, orderBy: {field: CREATED_AT, direction: DESC}) {
72
+ nodes {
73
+ number title body state
74
+ issueFieldValues(first: 5) {
75
+ nodes {
76
+ ... on IssueFieldSingleSelectValue {
77
+ name
78
+ field { ... on IssueFieldSingleSelect { name } }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }""", {"owner": owner, "repo": repo, "state": [state] if state != "ALL" else ["OPEN", "CLOSED"]})
86
+ if "errors" in result:
87
+ return f"Error: {result['errors'][0]['message']}"
88
+ lines = []
89
+ for issue in result["data"]["repository"]["issues"]["nodes"]:
90
+ priority = "not set"
91
+ for fv in issue.get("issueFieldValues", {}).get("nodes", []):
92
+ if fv.get("field", {}).get("name") == "Priority":
93
+ priority = fv.get("name", "not set").lower()
94
+ break
95
+ body = (issue.get("body") or "").strip().replace("\n", " ")[:150]
96
+ lines.append(f"#{issue['number']} [{issue['state']}] {issue['title']} | Priority: {priority} | {body}")
97
+ return "\n".join(lines) or "No issues found"
98
+ except urllib.error.HTTPError as e:
99
+ return f"HTTP {e.code}: {e.read().decode()}"
100
+ except Exception as e:
101
+ return f"Error: {e}"
102
+
103
+
104
+ @tool
105
+ def list_board_status(owner: str, project_number: int) -> str:
106
+ """List all project board items grouped by their Status (Todo / In Progress / Done). Use this when the user asks about board status, what is in progress, or what is done. Never use projects_list for this — it overflows."""
107
+ try:
108
+ result = _gql("""
109
+ query($owner: String!, $num: Int!) {
110
+ organization(login: $owner) {
111
+ projectV2(number: $num) {
112
+ items(first: 100) {
113
+ nodes {
114
+ content {
115
+ ... on Issue { number title state }
116
+ }
117
+ fieldValues(first: 10) {
118
+ nodes {
119
+ ... on ProjectV2ItemFieldSingleSelectValue {
120
+ name
121
+ field { ... on ProjectV2SingleSelectField { name } }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }""", {"owner": owner, "num": project_number})
130
+ if "errors" in result:
131
+ return f"Error: {result['errors'][0]['message']}"
132
+ buckets: dict = {}
133
+ for item in result["data"]["organization"]["projectV2"]["items"]["nodes"]:
134
+ content = item.get("content") or {}
135
+ if not content.get("number"):
136
+ continue
137
+ number = content["number"]
138
+ title = content.get("title", "")
139
+ status = "No Status"
140
+ for fv in item.get("fieldValues", {}).get("nodes", []):
141
+ if fv.get("field", {}).get("name") == "Status":
142
+ status = fv.get("name", "No Status")
143
+ break
144
+ buckets.setdefault(status, []).append(f" #{number} {title}")
145
+ lines = []
146
+ for status, items in buckets.items():
147
+ lines.append(f"{status} ({len(items)}):")
148
+ lines.extend(items)
149
+ lines.append("")
150
+ return "\n".join(lines) or "No items found"
151
+ except urllib.error.HTTPError as e:
152
+ return f"HTTP {e.code}: {e.read().decode()}"
153
+ except Exception as e:
154
+ return f"Error: {e}"
155
+
156
+
157
+ @tool
158
+ def read_local_file(path: str) -> str:
159
+ """Read a file from the local repository. Path is relative to the repo root."""
160
+ full_path = os.path.normpath(os.path.join(LOCAL_REPO_PATH, path))
161
+ if not full_path.startswith(LOCAL_REPO_PATH):
162
+ return "Error: path outside repo root"
163
+ try:
164
+ with open(full_path) as f:
165
+ return f.read()
166
+ except FileNotFoundError:
167
+ return f"File not found: {path}"
168
+ except Exception as e:
169
+ return f"Error reading file: {e}"
170
+
171
+
172
+ @tool
173
+ def list_local_files(path: str = ".") -> str:
174
+ """List files and directories in the local repository. Path is relative to the repo root."""
175
+ full_path = os.path.normpath(os.path.join(LOCAL_REPO_PATH, path))
176
+ if not full_path.startswith(LOCAL_REPO_PATH):
177
+ return "Error: path outside repo root"
178
+ try:
179
+ entries = []
180
+ for entry in sorted(os.scandir(full_path), key=lambda e: (not e.is_dir(), e.name)):
181
+ prefix = "/" if entry.is_dir() else ""
182
+ entries.append(f"{entry.name}{prefix}")
183
+ return "\n".join(entries)
184
+ except FileNotFoundError:
185
+ return f"Directory not found: {path}"
186
+ except Exception as e:
187
+ return f"Error listing files: {e}"
188
+
189
+
190
+ @tool
191
+ def detect_current_repo() -> str:
192
+ """Detect the GitHub repository the user is currently working in, based on the local git remote origin URL."""
193
+ try:
194
+ url = subprocess.check_output(
195
+ ["git", "remote", "get-url", "origin"],
196
+ stderr=subprocess.DEVNULL,
197
+ cwd=os.getcwd(),
198
+ ).decode().strip()
199
+ match = re.search(r"github\.com[:/](.+?)(?:\.git)?$", url)
200
+ if match:
201
+ return match.group(1)
202
+ except Exception:
203
+ pass
204
+ return "No GitHub remote detected"
205
+
206
+
207
+ local_tools = [
208
+ set_issue_priority,
209
+ list_issues_with_priorities,
210
+ list_board_status,
211
+ detect_current_repo,
212
+ read_local_file,
213
+ list_local_files,
214
+ ]
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: github-mcp-agent
3
+ Version: 0.1.0
4
+ Summary: A GitHub AI agent powered by AWS Bedrock and MCP
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: anthropic
7
+ Requires-Dist: boto3
8
+ Requires-Dist: click
9
+ Requires-Dist: litellm
10
+ Requires-Dist: mcp
11
+ Requires-Dist: python-dotenv
12
+ Requires-Dist: questionary
13
+ Requires-Dist: rich
14
+ Requires-Dist: strands-agents
@@ -0,0 +1,17 @@
1
+ github_mcp_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ github_mcp_agent/agent.py,sha256=vbMm-pGsYOD-z0TeUIfczdbXPrdi62jK-GK1ZwesFog,3520
3
+ github_mcp_agent/cli.py,sha256=0phwP9dRWzKzOzc6R-uZZQIWpwFtrJUammhe4Jyc3_k,6342
4
+ github_mcp_agent/setup_wizard.py,sha256=RD0az_GsXI8zzB2_GQeNsO9JNoUGYdMc9isguTf8fys,4640
5
+ github_mcp_agent/system_prompt.txt,sha256=K67KFba4IzsYd9priF-7ORFTRvWB0wqonlN4p3H01Do,4532
6
+ github_mcp_agent/tools.py,sha256=xg-EglraCAHamEkYeNcavdNc2UyGMFamNr1F6BTrGtY,8602
7
+ github_mcp_agent/providers/__init__.py,sha256=xdDXQVtKXE5A_cKjh5w9wD8tJBiZ7sJnNfYYng7MbTw,907
8
+ github_mcp_agent/providers/anthropic.py,sha256=DvJpbuMghY871zGbSuQ5NHiRQl0HZj_TedZU7XYMYVU,1022
9
+ github_mcp_agent/providers/bedrock.py,sha256=-HJFM0e-TgnPxEErkeaV7B6CBiutTKDklEIGNdMYRxE,4619
10
+ github_mcp_agent/providers/copilot.py,sha256=kmFsE7V6vxRn0vIQfRCw5j48SVi2kUnjblEdXWXqzUU,1208
11
+ github_mcp_agent/providers/gemini.py,sha256=6h2A1374mKBrjb9flJh0gmjhxjcmbEOv1_Iov1OCutM,980
12
+ github_mcp_agent/providers/ollama.py,sha256=2Qtx6crnH7FwnpN_nlej9kl5_tO4UC26vGexnpBHVU4,2633
13
+ github_mcp_agent/providers/openai.py,sha256=JpjlbccDuq7btdudA5PBbcjYVv5tyNCAGbVHQ0U2Kr0,937
14
+ github_mcp_agent-0.1.0.dist-info/METADATA,sha256=l6XFCYlQbs4YDtlGztiB-uMuUwOw2c_pNCVSNgEibUc,357
15
+ github_mcp_agent-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ github_mcp_agent-0.1.0.dist-info/entry_points.txt,sha256=ubnAt2X37Z0pEbZWMoS2rLMHIsI-1vtqS433IY6aZAk,59
17
+ github_mcp_agent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ github-agent = github_mcp_agent.cli:main