Skip to content

Commit 8ba9664

Browse files
fix(arcade-cli): swap minimal scaffold demo from Reddit to GitHub star (#850)
## Summary The minimal scaffold's authorized-tool demo (`get_posts_in_subreddit`, generated by `arcade new <name>`) currently dead-ends end users following the [build-mcp-server guide](https://docs.arcade.dev/en/guides/create-tools/tool-basics/build-mcp-server) and the [MCP server quickstart](https://docs.arcade.dev/en/get-started/quickstarts/mcp-server-quickstart). When the tutorial reader runs the tool and clicks the consent URL Arcade prints, Reddit's authorize endpoint returns: > bad request (reddit.com) — you sent an invalid request — invalid client id This isn't an isolated rotation/credential issue. Per Reddit's [Responsible Builder Policy](https://www.reddit.com/r/redditdev/comments/1oug31u/introducing_the_responsible_builder_policy_new/) in November 2025, Reddit **ended self-service API access**; creating a new Reddit OAuth app currently returns 500, and third-party integrations may be permanently blocked unless they migrate to Devvit. ArcadeAI/docs#976 captures this in detail. In other words, the Reddit demo can't be revived by rotating credentials on the Arcade Engine side — it requires Reddit's manual approval under the new policy or a full Devvit migration, neither of which is in this repo's scope. From the tutorial reader's perspective today, the scaffold's authorized-tool demo is broken on first contact and there's no near-term path to fixing it in place. This PR swaps the scaffold's authorized-tool demo to a GitHub `star_repo` tool so the tutorial works end-to-end again, and mirrors the same change into the deploy CLI test fixtures so the fixtures stay in sync with the scaffold they're derived from. A paired docs PR in ArcadeAI/docs updates the build-mcp-server guide and the MCP server quickstart to reflect the new demo. Resolves: ArcadeAI/docs#976 (scaffold side; the auth-provider reference page rewrite is separate) ## Design decisions - **Why GitHub, and not "fix the Reddit OAuth registration."** The Reddit OAuth credential is owned by the Arcade Engine, not this repo, so it can't be fixed here. More fundamentally, per ArcadeAI/docs#976 the Reddit Responsible Builder Policy ended self-service API access — even after rotation, restoring the Reddit demo requires Reddit's manual approval or a Devvit migration. GitHub is also a better default for a tutorial regardless: it's the auth provider the repo's own README already showcases (`@app.tool(requires_auth=GitHub(scopes=["repo"]))` + `list_my_repos`), so the scaffold becomes consistent with the headline pitch. - **Why "star a repo" specifically.** Three reasons: (1) tiny code surface, perfect for a tutorial; (2) visible side effect — the tutorial reader literally sees the star appear on the target repo, which is satisfying and good for demos/videos; (3) idempotent — re-running the tool is harmless. - **Why `public_repo` scope, not `repo`.** `public_repo` is the minimum scope GitHub requires for `PUT /user/starred/{owner}/{repo}` against public repos. The README's example uses `repo` because it lists private repos too; for a starring demo, asking for full read/write access to private repos is unnecessary consent inflation. Tutorial readers should see "minimum viable scopes" modeled. - **Why parameterized `star_repo(owner, repo)`, not hardcoded `star_arcade_mcp()`.** A parameterized tool is genuinely useful past the demo, and the natural demo call stays on-brand ("star `ArcadeAI/arcade-mcp`") without making the scaffold itself a vanity hook. - **Why mirror the change into `libs/tests/cli/deploy/test_servers/{valid,invalid}_server/server.py`.** Those fixtures are byte-for-byte copies of the scaffold's prior shape. `test_deploy.py` only asserts on `MY_SECRET_KEY` (verified — no tool-name assertions), so the mirror is a pure fixture refresh: no test-logic changes, but the fixtures stay aligned with the scaffold they were derived from, which avoids future drift surprises. `invalid_server`'s deliberate `def greet(name: dict) -> str:` validation-failure case is preserved. - **Kept the simpler tool shape in the deploy fixtures (no `ToolMetadata` block) and the richer shape in the scaffold template (with `ToolMetadata`).** That matches the pre-existing convention on each side; this PR doesn't try to harmonize them. - **`ServiceDomain.SOURCE_CODE` + `Operation.CREATE`** for the scaffold's classification block. `SOURCE_CODE` is the documented match for GitHub per the enum docstring ("Source code management, version control, and code review"). Starring brings a new "star" record into existence from nothing → `CREATE`, idempotent. ## Scope In scope: - `libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/src/{{ toolkit_name }}/server.py` — the scaffolded tool that ships to every new user. - `libs/tests/cli/deploy/test_servers/{valid,invalid}_server/server.py` — fixtures aligned with the scaffold. Not in scope (handled separately or out of band): - `libs/arcade-cli/arcade_cli/templates/full/…` — the `--full` scaffold has its own Reddit-flavored demo (`get_my_reddit_profile`) with tests + evals attached. Worth a sibling PR; left out to keep this one focused. - `examples/mcp_servers/simple/…` and `examples/mcp_servers/authorization/…` — standalone examples with their own READMEs. The `authorization/` one is specifically about teaching auth as a concept, so swapping that example is a separate editorial decision. - `libs/arcade-tdk/README.md` — separate package's marketing material; uses Reddit as a code snippet but isn't part of the new-user onboarding path. - Anything in `arcade-cli/arcade_cli/new.py` (the CLI's "Next steps" output and the broken `--transport http` hint I noticed during testing) — separate concern. - Fixing the underlying Reddit OAuth registration on the Arcade Engine side — out of repo. ## Test plan - [x] `uv sync --extra all --extra dev` (clean env on the branch) - [x] `uv run pytest -W ignore libs/tests/cli/test_new_cli.py libs/tests/cli/deploy/test_deploy.py` → **29 passed**, covering the scaffolder + the deploy CLI flow that consumes the fixtures - [x] `uv run ruff check` on all three changed files → clean - [x] `uv run ruff format` on the template file → applied (2 blank-line PEP 8 additions); deploy fixtures already formatted - [x] `uv run pre-commit run --files <all three modified files>` → all hooks pass (ruff skipped the template path via existing excludes, ran manually as above) - [x] Manually exercised the end-user path end-to-end before this PR: `arcade new <name>` → `uv sync` → drove the scaffolded server over stdio MCP → `tools/call` for `<Name>_StarRepo({owner:"ArcadeAI", repo:"arcade-mcp"})` surfaced a real GitHub App consent URL via MCP elicitation (`client_id=Iv23limhP7KYMJhtUYC3`, callback `cloud.arcade.dev/api/v1/oauth/.../callback`) → completed the OAuth round-trip in browser → re-invoked the tool → got `"Starred ArcadeAI/arcade-mcp."` with `isError: false` → independently verified via `gh api user/starred/ArcadeAI/arcade-mcp` (204) that the star landed on the target repo - [ ] `make check` (full pre-commit + mypy across all libs) — _not run; happy to run if reviewers want the full sweep, but the change is localized to template content and 2 deploy fixtures so the broader sweep is unlikely to be informative_ - [ ] `make test` (full pytest across all libs) — _not run for the same reason; ran the two relevant test files directly_ ## Risk note Blast radius: every new MCP server scaffolded with `arcade new` from the next release of `arcade-mcp` will get this tool instead of the Reddit one. There's no behavior change to existing scaffolded servers — those are user-owned files that don't get updated retroactively. The deploy CLI test fixtures change is internal-only and doesn't affect any shipped artifact. The new tool requires `GitHub(scopes=["public_repo"])` consent on first use, which is a smaller consent ask than the previous `Reddit(scopes=["read"])` from the user's perspective (no email/profile data, narrowly scoped to public repo write). ## Author checklist - [x] Linked to a Linear ticket or GitHub issue (above) - [x] I understand every change in the diff — not "an agent wrote it, I'm not sure why" - [x] Runs locally, exercised through the end-user path (not just unit tests) - [x] `make check` and `make test` are green locally; CI is expected to pass — _ran the targeted equivalents; see Test plan_ - [x] I've pulled the branch fresh and reviewed my own diff top-to-bottom - [x] I'd merge it myself if a teammate said LGTM right now <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are limited to CLI scaffolding templates and internal deploy test fixtures, so runtime impact is only on newly generated servers. Risk is mainly that the new GitHub OAuth/HTTP call and metadata settings could be misconfigured, affecting the tutorial flow rather than existing users. > > **Overview** > Updates the `arcade new` *minimal* scaffold to replace the previous authorized-tool demo with a GitHub-based `star_repo` tool that uses OAuth (`GitHub()`) and calls `PUT https://api.github.com/user/starred/{owner}/{repo}` via `httpx`. > > Refreshes the deploy CLI fixtures (`test_servers/valid_server` and `invalid_server`) to mirror the same new `star_repo` example (while keeping the `invalid_server`’s intentional `greet` type error), so scaffold-derived fixtures stay in sync. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7c41af3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Thierry Damiba <177760997+thierrypdamiba@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f469074 commit 8ba9664

4 files changed

Lines changed: 45 additions & 52 deletions

File tree

libs/arcade-cli/arcade_cli/templates/minimal/{{ toolkit_name }}/src/{{ toolkit_name }}/server.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import httpx
88
from arcade_mcp_server import Context, MCPApp
9-
from arcade_mcp_server.auth import Reddit
9+
from arcade_mcp_server.auth import GitHub
1010
from arcade_mcp_server.metadata import (
1111
Behavior,
1212
Classification,
@@ -38,48 +38,47 @@ def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of
3838

3939
return "The last 4 characters of the secret are: " + secret[-4:]
4040

41+
4142
# To use this tool locally, you need to install the Arcade CLI (uv tool install arcade-mcp)
4243
# and then run 'arcade login' to authenticate.
4344
@app.tool(
44-
requires_auth=Reddit(scopes=["read"]),
45+
requires_auth=GitHub(),
4546
metadata=ToolMetadata(
4647
classification=Classification(
47-
service_domains=[ServiceDomain.SOCIAL_MEDIA],
48+
service_domains=[ServiceDomain.SOURCE_CODE],
4849
),
4950
behavior=Behavior(
50-
operations=[Operation.READ],
51-
read_only=True,
51+
operations=[Operation.UPDATE],
52+
read_only=False,
5253
destructive=False,
5354
idempotent=True,
5455
open_world=True,
5556
),
5657
),
5758
)
58-
async def get_posts_in_subreddit(
59-
context: Context, subreddit: Annotated[str, "The name of the subreddit"]
60-
) -> dict:
61-
"""Get posts from a specific subreddit"""
62-
# Normalize the subreddit name
63-
subreddit = subreddit.lower().replace("r/", "").replace(" ", "")
64-
65-
# Prepare the httpx request
59+
async def star_repo(
60+
context: Context,
61+
owner: Annotated[str, "GitHub owner (user or org). E.g. 'ArcadeAI'"],
62+
repo: Annotated[str, "GitHub repository name. E.g. 'arcade-mcp'"],
63+
) -> Annotated[str, "Confirmation that the repository was starred"]:
64+
"""Star a public GitHub repository on behalf of the authenticated user."""
6665
# OAuth token is injected into the context at runtime.
6766
# LLMs and MCP clients cannot see or access your OAuth tokens.
6867
oauth_token = context.get_auth_token_or_empty()
6968
headers = {
7069
"Authorization": f"Bearer {oauth_token}",
70+
"Accept": "application/vnd.github+json",
71+
"X-GitHub-Api-Version": "2022-11-28",
7172
"User-Agent": "{{ toolkit_name }}-mcp-server",
7273
}
73-
params = {"limit": 5}
74-
url = f"https://oauth.reddit.com/r/{subreddit}/hot"
74+
url = f"https://api.github.com/user/starred/{owner}/{repo}"
7575

76-
# Make the request
7776
async with httpx.AsyncClient() as client:
78-
response = await client.get(url, headers=headers, params=params)
77+
response = await client.put(url, headers=headers)
7978
response.raise_for_status()
8079

81-
# Return the response
82-
return response.json()
80+
return f"Starred {owner}/{repo}."
81+
8382

8483
# Run with specific transport
8584
if __name__ == "__main__":

libs/tests/cli/deploy/test_servers/invalid_server/server.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import httpx
88
from arcade_mcp_server import Context, MCPApp
9-
from arcade_mcp_server.auth import Reddit
9+
from arcade_mcp_server.auth import GitHub
1010

1111
app = MCPApp(name="simpleserver", version="1.0.0", log_level="DEBUG")
1212

@@ -34,32 +34,29 @@ def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of
3434

3535
# To use this tool, you need to either set your ARCADE_API_KEY as an environment variable or
3636
# use the Arcade CLI (uv pip install arcade-mcp) and run 'arcade login' to authenticate.
37-
@app.tool(requires_auth=Reddit(scopes=["read"]))
38-
async def get_posts_in_subreddit(
39-
context: Context, subreddit: Annotated[str, "The name of the subreddit"]
40-
) -> dict:
41-
"""Get posts from a specific subreddit"""
42-
# Normalize the subreddit name
43-
subreddit = subreddit.lower().replace("r/", "").replace(" ", "")
44-
45-
# Prepare the httpx request
37+
@app.tool(requires_auth=GitHub())
38+
async def star_repo(
39+
context: Context,
40+
owner: Annotated[str, "GitHub owner (user or org). E.g. 'ArcadeAI'"],
41+
repo: Annotated[str, "GitHub repository name. E.g. 'arcade-mcp'"],
42+
) -> Annotated[str, "Confirmation that the repository was starred"]:
43+
"""Star a public GitHub repository on behalf of the authenticated user."""
4644
# OAuth token is injected into the context at runtime.
4745
# LLMs and MCP clients cannot see or access your OAuth tokens.
4846
oauth_token = context.get_auth_token_or_empty()
4947
headers = {
5048
"Authorization": f"Bearer {oauth_token}",
49+
"Accept": "application/vnd.github+json",
50+
"X-GitHub-Api-Version": "2022-11-28",
5151
"User-Agent": "simple_server-mcp-server",
5252
}
53-
params = {"limit": 5}
54-
url = f"https://oauth.reddit.com/r/{subreddit}/hot"
53+
url = f"https://api.github.com/user/starred/{owner}/{repo}"
5554

56-
# Make the request
5755
async with httpx.AsyncClient() as client:
58-
response = await client.get(url, headers=headers, params=params)
56+
response = await client.put(url, headers=headers)
5957
response.raise_for_status()
6058

61-
# Return the response
62-
return response.json()
59+
return f"Starred {owner}/{repo}."
6360

6461

6562
# Run with specific transport

libs/tests/cli/deploy/test_servers/valid_server/server.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import httpx
88
from arcade_mcp_server import Context, MCPApp
9-
from arcade_mcp_server.auth import Reddit
9+
from arcade_mcp_server.auth import GitHub
1010

1111
app = MCPApp(name="simpleserver", version="1.0.0", log_level="DEBUG")
1212

@@ -34,32 +34,29 @@ def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of
3434

3535
# To use this tool, you need to either set your ARCADE_API_KEY as an environment variable or
3636
# use the Arcade CLI (uv pip install arcade-mcp) and run 'arcade login' to authenticate.
37-
@app.tool(requires_auth=Reddit(scopes=["read"]))
38-
async def get_posts_in_subreddit(
39-
context: Context, subreddit: Annotated[str, "The name of the subreddit"]
40-
) -> dict:
41-
"""Get posts from a specific subreddit"""
42-
# Normalize the subreddit name
43-
subreddit = subreddit.lower().replace("r/", "").replace(" ", "")
44-
45-
# Prepare the httpx request
37+
@app.tool(requires_auth=GitHub())
38+
async def star_repo(
39+
context: Context,
40+
owner: Annotated[str, "GitHub owner (user or org). E.g. 'ArcadeAI'"],
41+
repo: Annotated[str, "GitHub repository name. E.g. 'arcade-mcp'"],
42+
) -> Annotated[str, "Confirmation that the repository was starred"]:
43+
"""Star a public GitHub repository on behalf of the authenticated user."""
4644
# OAuth token is injected into the context at runtime.
4745
# LLMs and MCP clients cannot see or access your OAuth tokens.
4846
oauth_token = context.get_auth_token_or_empty()
4947
headers = {
5048
"Authorization": f"Bearer {oauth_token}",
49+
"Accept": "application/vnd.github+json",
50+
"X-GitHub-Api-Version": "2022-11-28",
5151
"User-Agent": "simple_server-mcp-server",
5252
}
53-
params = {"limit": 5}
54-
url = f"https://oauth.reddit.com/r/{subreddit}/hot"
53+
url = f"https://api.github.com/user/starred/{owner}/{repo}"
5554

56-
# Make the request
5755
async with httpx.AsyncClient() as client:
58-
response = await client.get(url, headers=headers, params=params)
56+
response = await client.put(url, headers=headers)
5957
response.raise_for_status()
6058

61-
# Return the response
62-
return response.json()
59+
return f"Starred {owner}/{repo}."
6360

6461

6562
# Run with specific transport

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "arcade-mcp"
3-
version = "1.14.2"
3+
version = "1.14.3"
44
description = "Arcade.dev - Tool Calling platform for Agents"
55
readme = "README.md"
66
license = { file = "LICENSE" }

0 commit comments

Comments
 (0)