golf-mcp 0.1.10__py3-none-any.whl → 0.1.12__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.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

Files changed (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +202 -82
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +119 -80
  12. golf/commands/run.py +14 -13
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +478 -353
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +169 -69
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +781 -276
  42. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.12.dist-info/RECORD +55 -0
  44. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
  45. golf_mcp-0.1.10.dist-info/RECORD +0 -55
  46. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/top_level.txt +0 -0
@@ -1,55 +1,55 @@
1
1
  """Create a new issue in a GitHub repository."""
2
2
 
3
- from typing import Annotated, List, Optional
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """Response from creating an issue."""
13
+
12
14
  success: bool
13
- issue_number: Optional[int] = None
14
- issue_url: Optional[str] = None
15
- error: Optional[str] = None
15
+ issue_number: int | None = None
16
+ issue_url: str | None = None
17
+ error: str | None = None
16
18
 
17
19
 
18
20
  async def create(
19
21
  repo: Annotated[str, Field(description="Repository name in format 'owner/repo'")],
20
22
  title: Annotated[str, Field(description="Issue title")],
21
- body: Annotated[str, Field(description="Issue description/body (supports Markdown)")] = "",
22
- labels: Annotated[Optional[List[str]], Field(description="List of label names to apply")] = None
23
+ body: Annotated[
24
+ str, Field(description="Issue description/body (supports Markdown)")
25
+ ] = "",
26
+ labels: Annotated[
27
+ list[str] | None, Field(description="List of label names to apply")
28
+ ] = None,
23
29
  ) -> Output:
24
30
  """Create a new issue.
25
-
31
+
26
32
  Requires authentication with appropriate permissions.
27
33
  """
28
34
  github_token = get_api_key()
29
-
35
+
30
36
  if not github_token:
31
37
  return Output(
32
38
  success=False,
33
- error="Authentication required. Please provide a GitHub token."
39
+ error="Authentication required. Please provide a GitHub token.",
34
40
  )
35
-
41
+
36
42
  # Validate repo format
37
- if '/' not in repo:
38
- return Output(
39
- success=False,
40
- error="Repository must be in format 'owner/repo'"
41
- )
42
-
43
+ if "/" not in repo:
44
+ return Output(success=False, error="Repository must be in format 'owner/repo'")
45
+
43
46
  url = f"https://api.github.com/repos/{repo}/issues"
44
-
47
+
45
48
  # Build request payload
46
- payload = {
47
- "title": title,
48
- "body": body
49
- }
49
+ payload = {"title": title, "body": body}
50
50
  if labels:
51
51
  payload["labels"] = labels
52
-
52
+
53
53
  try:
54
54
  async with httpx.AsyncClient() as client:
55
55
  response = await client.post(
@@ -57,38 +57,37 @@ async def create(
57
57
  headers={
58
58
  "Authorization": f"Bearer {github_token}",
59
59
  "Accept": "application/vnd.github.v3+json",
60
- "User-Agent": "Golf-GitHub-MCP-Server"
60
+ "User-Agent": "Golf-GitHub-MCP-Server",
61
61
  },
62
62
  json=payload,
63
- timeout=10.0
63
+ timeout=10.0,
64
64
  )
65
-
65
+
66
66
  response.raise_for_status()
67
67
  issue_data = response.json()
68
-
68
+
69
69
  return Output(
70
70
  success=True,
71
71
  issue_number=issue_data["number"],
72
- issue_url=issue_data["html_url"]
72
+ issue_url=issue_data["html_url"],
73
73
  )
74
-
74
+
75
75
  except httpx.HTTPStatusError as e:
76
76
  error_messages = {
77
77
  401: "Invalid or missing authentication token",
78
78
  403: "Insufficient permissions to create issues in this repository",
79
79
  404: "Repository not found",
80
- 422: "Invalid request data"
80
+ 422: "Invalid request data",
81
81
  }
82
82
  return Output(
83
83
  success=False,
84
- error=error_messages.get(e.response.status_code, f"GitHub API error: {e.response.status_code}")
84
+ error=error_messages.get(
85
+ e.response.status_code, f"GitHub API error: {e.response.status_code}"
86
+ ),
85
87
  )
86
88
  except Exception as e:
87
- return Output(
88
- success=False,
89
- error=f"Failed to create issue: {str(e)}"
90
- )
89
+ return Output(success=False, error=f"Failed to create issue: {str(e)}")
91
90
 
92
91
 
93
92
  # Export the function to be used as the tool
94
- export = create
93
+ export = create
@@ -1,87 +1,92 @@
1
1
  """List issues in a GitHub repository."""
2
2
 
3
- from typing import Annotated, List, Optional, Dict, Any
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated, Any
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """List of issues from the repository."""
12
- issues: List[Dict[str, Any]]
13
+
14
+ issues: list[dict[str, Any]]
13
15
  total_count: int
14
16
 
15
17
 
16
18
  async def list(
17
19
  repo: Annotated[str, Field(description="Repository name in format 'owner/repo'")],
18
- state: Annotated[str, Field(description="Filter by state - 'open', 'closed', or 'all'")] = "open",
19
- labels: Annotated[Optional[str], Field(description="Comma-separated list of label names to filter by")] = None,
20
- per_page: Annotated[int, Field(description="Number of results per page (max 100)")] = 20
20
+ state: Annotated[
21
+ str, Field(description="Filter by state - 'open', 'closed', or 'all'")
22
+ ] = "open",
23
+ labels: Annotated[
24
+ str | None,
25
+ Field(description="Comma-separated list of label names to filter by"),
26
+ ] = None,
27
+ per_page: Annotated[
28
+ int, Field(description="Number of results per page (max 100)")
29
+ ] = 20,
21
30
  ) -> Output:
22
31
  """List issues in a repository.
23
-
32
+
24
33
  Returns issues with their number, title, state, and other metadata.
25
34
  Pull requests are filtered out from the results.
26
35
  """
27
36
  github_token = get_api_key()
28
-
37
+
29
38
  # Validate repo format
30
- if '/' not in repo:
39
+ if "/" not in repo:
31
40
  return Output(issues=[], total_count=0)
32
-
41
+
33
42
  url = f"https://api.github.com/repos/{repo}/issues"
34
-
43
+
35
44
  # Prepare headers
36
45
  headers = {
37
46
  "Accept": "application/vnd.github.v3+json",
38
- "User-Agent": "Golf-GitHub-MCP-Server"
47
+ "User-Agent": "Golf-GitHub-MCP-Server",
39
48
  }
40
49
  if github_token:
41
50
  headers["Authorization"] = f"Bearer {github_token}"
42
-
51
+
43
52
  # Build query parameters
44
- params = {
45
- "state": state,
46
- "per_page": min(per_page, 100)
47
- }
53
+ params = {"state": state, "per_page": min(per_page, 100)}
48
54
  if labels:
49
55
  params["labels"] = labels
50
-
56
+
51
57
  try:
52
58
  async with httpx.AsyncClient() as client:
53
59
  response = await client.get(
54
- url,
55
- headers=headers,
56
- params=params,
57
- timeout=10.0
60
+ url, headers=headers, params=params, timeout=10.0
58
61
  )
59
-
62
+
60
63
  response.raise_for_status()
61
64
  issues_data = response.json()
62
-
65
+
63
66
  # Filter out pull requests and format issues
64
67
  issues = []
65
68
  for issue in issues_data:
66
69
  # Skip pull requests (they appear in issues endpoint too)
67
70
  if "pull_request" in issue:
68
71
  continue
69
-
70
- issues.append({
71
- "number": issue["number"],
72
- "title": issue["title"],
73
- "body": issue.get("body", ""),
74
- "state": issue["state"],
75
- "url": issue["html_url"],
76
- "user": issue["user"]["login"],
77
- "labels": [label["name"] for label in issue.get("labels", [])]
78
- })
79
-
72
+
73
+ issues.append(
74
+ {
75
+ "number": issue["number"],
76
+ "title": issue["title"],
77
+ "body": issue.get("body", ""),
78
+ "state": issue["state"],
79
+ "url": issue["html_url"],
80
+ "user": issue["user"]["login"],
81
+ "labels": [label["name"] for label in issue.get("labels", [])],
82
+ }
83
+ )
84
+
80
85
  return Output(issues=issues, total_count=len(issues))
81
-
86
+
82
87
  except Exception:
83
88
  return Output(issues=[], total_count=0)
84
89
 
85
90
 
86
91
  # Export the function to be used as the tool
87
- export = list
92
+ export = list
@@ -1,31 +1,50 @@
1
1
  """List GitHub repositories for a user or organization."""
2
2
 
3
- from typing import Annotated, List, Optional, Dict, Any
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated, Any
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """List of repositories."""
12
- repositories: List[Dict[str, Any]]
13
+
14
+ repositories: list[dict[str, Any]]
13
15
  total_count: int
14
16
 
15
17
 
16
18
  async def list(
17
- username: Annotated[Optional[str], Field(description="GitHub username (lists public repos, or all repos if authenticated as this user)")] = None,
18
- org: Annotated[Optional[str], Field(description="GitHub organization name (lists public repos, or all repos if authenticated member)")] = None,
19
- sort: Annotated[str, Field(description="How to sort results - 'created', 'updated', 'pushed', 'full_name'")] = "updated",
20
- per_page: Annotated[int, Field(description="Number of results per page (max 100)")] = 20
19
+ username: Annotated[
20
+ str | None,
21
+ Field(
22
+ description="GitHub username (lists public repos, or all repos if authenticated as this user)"
23
+ ),
24
+ ] = None,
25
+ org: Annotated[
26
+ str | None,
27
+ Field(
28
+ description="GitHub organization name (lists public repos, or all repos if authenticated member)"
29
+ ),
30
+ ] = None,
31
+ sort: Annotated[
32
+ str,
33
+ Field(
34
+ description="How to sort results - 'created', 'updated', 'pushed', 'full_name'"
35
+ ),
36
+ ] = "updated",
37
+ per_page: Annotated[
38
+ int, Field(description="Number of results per page (max 100)")
39
+ ] = 20,
21
40
  ) -> Output:
22
41
  """List GitHub repositories.
23
-
42
+
24
43
  If neither username nor org is provided, lists repositories for the authenticated user.
25
44
  """
26
45
  # Get the GitHub token from the request context
27
46
  github_token = get_api_key()
28
-
47
+
29
48
  # Determine the API endpoint
30
49
  if org:
31
50
  url = f"https://api.github.com/orgs/{org}/repos"
@@ -36,15 +55,15 @@ async def list(
36
55
  if not github_token:
37
56
  return Output(repositories=[], total_count=0)
38
57
  url = "https://api.github.com/user/repos"
39
-
58
+
40
59
  # Prepare headers
41
60
  headers = {
42
61
  "Accept": "application/vnd.github.v3+json",
43
- "User-Agent": "Golf-GitHub-MCP-Server"
62
+ "User-Agent": "Golf-GitHub-MCP-Server",
44
63
  }
45
64
  if github_token:
46
65
  headers["Authorization"] = f"Bearer {github_token}"
47
-
66
+
48
67
  try:
49
68
  async with httpx.AsyncClient() as client:
50
69
  response = await client.get(
@@ -53,30 +72,32 @@ async def list(
53
72
  params={
54
73
  "sort": sort,
55
74
  "per_page": min(per_page, 100),
56
- "type": "all" if not username and not org else None
75
+ "type": "all" if not username and not org else None,
57
76
  },
58
- timeout=10.0
77
+ timeout=10.0,
59
78
  )
60
-
79
+
61
80
  response.raise_for_status()
62
81
  repos_data = response.json()
63
-
82
+
64
83
  # Format repositories
65
84
  repositories = []
66
85
  for repo in repos_data:
67
- repositories.append({
68
- "name": repo["name"],
69
- "full_name": repo["full_name"],
70
- "description": repo.get("description", ""),
71
- "private": repo.get("private", False),
72
- "stars": repo.get("stargazers_count", 0),
73
- "forks": repo.get("forks_count", 0),
74
- "language": repo.get("language", ""),
75
- "url": repo["html_url"]
76
- })
77
-
86
+ repositories.append(
87
+ {
88
+ "name": repo["name"],
89
+ "full_name": repo["full_name"],
90
+ "description": repo.get("description", ""),
91
+ "private": repo.get("private", False),
92
+ "stars": repo.get("stargazers_count", 0),
93
+ "forks": repo.get("forks_count", 0),
94
+ "language": repo.get("language", ""),
95
+ "url": repo["html_url"],
96
+ }
97
+ )
98
+
78
99
  return Output(repositories=repositories, total_count=len(repositories))
79
-
100
+
80
101
  except httpx.HTTPStatusError as e:
81
102
  if e.response.status_code in [401, 404]:
82
103
  return Output(repositories=[], total_count=0)
@@ -87,4 +108,4 @@ async def list(
87
108
 
88
109
 
89
110
  # Export the function to be used as the tool
90
- export = list
111
+ export = list
@@ -1,32 +1,49 @@
1
1
  """Search GitHub code."""
2
2
 
3
- from typing import Annotated, List, Optional, Dict, Any
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated, Any
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """Code search results."""
12
- results: List[Dict[str, Any]]
13
+
14
+ results: list[dict[str, Any]]
13
15
  total_count: int
14
16
 
15
17
 
16
18
  async def search(
17
- query: Annotated[str, Field(description="Search query (e.g., 'addClass', 'TODO', etc.)")],
18
- language: Annotated[Optional[str], Field(description="Filter by programming language (e.g., 'python', 'javascript')")] = None,
19
- repo: Annotated[Optional[str], Field(description="Search within a specific repository (format: 'owner/repo')")] = None,
20
- org: Annotated[Optional[str], Field(description="Search within repositories of a specific organization")] = None,
21
- per_page: Annotated[int, Field(description="Number of results per page (max 100)")] = 10
19
+ query: Annotated[
20
+ str, Field(description="Search query (e.g., 'addClass', 'TODO', etc.)")
21
+ ],
22
+ language: Annotated[
23
+ str | None,
24
+ Field(
25
+ description="Filter by programming language (e.g., 'python', 'javascript')"
26
+ ),
27
+ ] = None,
28
+ repo: Annotated[
29
+ str | None,
30
+ Field(description="Search within a specific repository (format: 'owner/repo')"),
31
+ ] = None,
32
+ org: Annotated[
33
+ str | None,
34
+ Field(description="Search within repositories of a specific organization"),
35
+ ] = None,
36
+ per_page: Annotated[
37
+ int, Field(description="Number of results per page (max 100)")
38
+ ] = 10,
22
39
  ) -> Output:
23
40
  """Search for code on GitHub.
24
-
41
+
25
42
  Without authentication, you're limited to 10 requests per minute.
26
43
  With authentication, you can make up to 30 requests per minute.
27
44
  """
28
45
  github_token = get_api_key()
29
-
46
+
30
47
  # Build the search query
31
48
  search_parts = [query]
32
49
  if language:
@@ -35,54 +52,50 @@ async def search(
35
52
  search_parts.append(f"repo:{repo}")
36
53
  if org:
37
54
  search_parts.append(f"org:{org}")
38
-
55
+
39
56
  search_query = " ".join(search_parts)
40
-
57
+
41
58
  url = "https://api.github.com/search/code"
42
-
59
+
43
60
  # Prepare headers
44
61
  headers = {
45
62
  "Accept": "application/vnd.github.v3+json",
46
- "User-Agent": "Golf-GitHub-MCP-Server"
63
+ "User-Agent": "Golf-GitHub-MCP-Server",
47
64
  }
48
65
  if github_token:
49
66
  headers["Authorization"] = f"Bearer {github_token}"
50
-
67
+
51
68
  try:
52
69
  async with httpx.AsyncClient() as client:
53
70
  response = await client.get(
54
71
  url,
55
72
  headers=headers,
56
- params={
57
- "q": search_query,
58
- "per_page": min(per_page, 100)
59
- },
60
- timeout=10.0
73
+ params={"q": search_query, "per_page": min(per_page, 100)},
74
+ timeout=10.0,
61
75
  )
62
-
76
+
63
77
  if response.status_code == 403:
64
78
  # Rate limit exceeded
65
79
  return Output(results=[], total_count=0)
66
-
80
+
67
81
  response.raise_for_status()
68
82
  data = response.json()
69
-
83
+
70
84
  # Format results
71
85
  results = []
72
86
  for item in data.get("items", []):
73
- results.append({
74
- "name": item["name"],
75
- "path": item["path"],
76
- "repository": item["repository"]["full_name"],
77
- "url": item["html_url"],
78
- "score": item.get("score", 0.0)
79
- })
80
-
81
- return Output(
82
- results=results,
83
- total_count=data.get("total_count", 0)
84
- )
85
-
87
+ results.append(
88
+ {
89
+ "name": item["name"],
90
+ "path": item["path"],
91
+ "repository": item["repository"]["full_name"],
92
+ "url": item["html_url"],
93
+ "score": item.get("score", 0.0),
94
+ }
95
+ )
96
+
97
+ return Output(results=results, total_count=data.get("total_count", 0))
98
+
86
99
  except httpx.HTTPStatusError:
87
100
  return Output(results=[], total_count=0)
88
101
  except Exception:
@@ -90,4 +103,4 @@ async def search(
90
103
 
91
104
 
92
105
  # Export the function to be used as the tool
93
- export = search
106
+ export = search
@@ -1,28 +1,33 @@
1
1
  """Get GitHub user information."""
2
2
 
3
- from typing import Annotated, Optional, Dict, Any
4
- from pydantic import BaseModel, Field
3
+ from typing import Annotated, Any
4
+
5
5
  import httpx
6
+ from pydantic import BaseModel, Field
6
7
 
7
8
  from golf.auth import get_api_key
8
9
 
9
10
 
10
11
  class Output(BaseModel):
11
12
  """User information result."""
13
+
12
14
  found: bool
13
- user: Optional[Dict[str, Any]] = None
15
+ user: dict[str, Any] | None = None
14
16
 
15
17
 
16
18
  async def get(
17
- username: Annotated[Optional[str], Field(description="GitHub username (if not provided, gets authenticated user)")] = None
19
+ username: Annotated[
20
+ str | None,
21
+ Field(description="GitHub username (if not provided, gets authenticated user)"),
22
+ ] = None,
18
23
  ) -> Output:
19
24
  """Get information about a GitHub user.
20
-
25
+
21
26
  If no username is provided, returns information about the authenticated user.
22
27
  This is useful for testing if authentication is working correctly.
23
28
  """
24
29
  github_token = get_api_key()
25
-
30
+
26
31
  # Determine the API endpoint
27
32
  if username:
28
33
  url = f"https://api.github.com/users/{username}"
@@ -31,26 +36,22 @@ async def get(
31
36
  if not github_token:
32
37
  return Output(found=False)
33
38
  url = "https://api.github.com/user"
34
-
39
+
35
40
  # Prepare headers
36
41
  headers = {
37
42
  "Accept": "application/vnd.github.v3+json",
38
- "User-Agent": "Golf-GitHub-MCP-Server"
43
+ "User-Agent": "Golf-GitHub-MCP-Server",
39
44
  }
40
45
  if github_token:
41
46
  headers["Authorization"] = f"Bearer {github_token}"
42
-
47
+
43
48
  try:
44
49
  async with httpx.AsyncClient() as client:
45
- response = await client.get(
46
- url,
47
- headers=headers,
48
- timeout=10.0
49
- )
50
-
50
+ response = await client.get(url, headers=headers, timeout=10.0)
51
+
51
52
  response.raise_for_status()
52
53
  user_data = response.json()
53
-
54
+
54
55
  return Output(
55
56
  found=True,
56
57
  user={
@@ -64,10 +65,10 @@ async def get(
64
65
  "followers": user_data.get("followers", 0),
65
66
  "following": user_data.get("following", 0),
66
67
  "created_at": user_data["created_at"],
67
- "url": user_data["html_url"]
68
- }
68
+ "url": user_data["html_url"],
69
+ },
69
70
  )
70
-
71
+
71
72
  except httpx.HTTPStatusError as e:
72
73
  if e.response.status_code in [401, 404]:
73
74
  return Output(found=False)
@@ -78,4 +79,4 @@ async def get(
78
79
 
79
80
 
80
81
  # Export the function to be used as the tool
81
- export = get
82
+ export = get
@@ -16,13 +16,13 @@ github_provider = ProviderConfig(
16
16
  token_url="https://github.com/login/oauth/access_token",
17
17
  userinfo_url="https://api.github.com/user",
18
18
  scopes=["read:user", "user:email"],
19
- issuer_url="http://127.0.0.1:3000", # This should be your Golf server's accessible URL
20
- callback_path="/auth/callback", # Golf's callback path
21
- token_expiration=3600 # 1 hour
19
+ issuer_url="http://127.0.0.1:3000", # This should be your Golf server's accessible URL
20
+ callback_path="/auth/callback", # Golf's callback path
21
+ token_expiration=3600, # 1 hour
22
22
  )
23
23
 
24
24
  # Configure authentication with the provider
25
25
  configure_auth(
26
26
  provider=github_provider,
27
27
  required_scopes=["read:user"], # Require read:user scope for protected endpoints
28
- )
28
+ )