Public: anyone can read this; the crew can change it.

The OpenCode plugin connects with OAuth when FORGE_TOKEN is not set #8300

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/8272-the-opencode-plugin-connects-with-oauth → main 2 commits fork

  • Merged. scout/8272-the-opencode-plugin-connects-with-oauth was merged into main as 297b3f7.

Review

Sign in to review this change.
3 files changed +49 −4
plugins/opencode/README.md +6
@@ -26,6 +26,12 @@ the pull that closes it, and the outcome sentence: landed, who is building,
2626 parked and why, failed), flagged ones first, through MCP `asked` with
2727 `show: "all"`; with an app, only that app's asks.
2828
29 Two ways to connect. With only `FORGE_URL` set, the plugin registers the MCP
30 server with no Authorization header and `oauth: {}`, so OpenCode signs in
31 through the forge's OAuth (run `opencode mcp auth forge`). With `FORGE_TOKEN`
32 set too, it sends the token as a bearer header and no sign-in is needed. With
33 neither set, it registers nothing.
34
2935 Code Mode can also read `GET /contract/openapi.json`.
3036
3137 `test/plugins.test.ts` fails when this plugin names an MCP tool the forge does not list, so a rename in `src/http/mcp.ts` cannot leave it behind.
plugins/opencode/forge.ts +7 −4
@@ -1,9 +1,11 @@
11 /**
22 * forge for OpenCode: registers the forge's MCP server and a `forge`
3 * command (new, tell, status, decide, plan, preview, pulls, runs, log, asked). Set FORGE_URL and FORGE_TOKEN.
3 * command (new, tell, status, decide, plan, preview, pulls, runs, log, asked). Set FORGE_URL, and FORGE_TOKEN too
4 * to skip the OAuth sign-in.
45 * No logic lives here: the tools are the forge's own (`docs/MCP.md`).
56 */
6 import type { Plugin } from "@opencode-ai/plugin";
7 // The shape OpenCode loads; typed here so the forge need not install its SDK.
8 type Plugin = () => Promise<{ config: (config: Record<string, any>) => Promise<void> }>;
79
810 const COMMAND = `Use the forge MCP tools for: $ARGUMENTS
911 - new <name> <words>: say the name and description back and ask the person to confirm. Only after the person says yes, call \`new_app\` with name and description (the words); relay in two lines the app's page (its url) and the next step (the first issue and who picked it up). If \`new_app\` refuses (name_taken, unverified, app_limit, repository_limit, storage_limit, no_such_agent), say its own message in one line; for name_taken, offer another name. Never retry without asking.
@@ -22,13 +24,14 @@ export const Forge: Plugin = async () => ({
2224 config: async (config: Record<string, any>) => {
2325 const url = process.env["FORGE_URL"];
2426 const token = process.env["FORGE_TOKEN"];
25 if (!url || !token) return;
27 if (!url) return;
2628 config["mcp"] = {
2729 ...config["mcp"],
2830 forge: {
2931 type: "remote",
3032 url: `${url.replace(/\/$/, "")}/mcp`,
31 headers: { Authorization: `Bearer ${token}` },
33 // No token: no header, and `oauth: {}` lets OpenCode sign in through the forge's OAuth.
34 ...(token ? { headers: { Authorization: `Bearer ${token}` } } : { oauth: {} }),
3235 },
3336 };
3437 config["command"] = {
test/plugin-opencode-oauth.test.ts added +36
@@ -0,0 +1,36 @@
1 import { afterEach, expect, test } from "bun:test";
2 import { Forge } from "../plugins/opencode/forge.ts";
3
4 const saved = { url: process.env["FORGE_URL"], token: process.env["FORGE_TOKEN"] };
5 afterEach(() => {
6 for (const [k, v] of [["FORGE_URL", saved.url], ["FORGE_TOKEN", saved.token]] as const) {
7 if (v === undefined) delete process.env[k];
8 else process.env[k] = v;
9 }
10 });
11
12 async function configure(url?: string, token?: string) {
13 if (url) process.env["FORGE_URL"] = url; else delete process.env["FORGE_URL"];
14 if (token) process.env["FORGE_TOKEN"] = token; else delete process.env["FORGE_TOKEN"];
15 const config: Record<string, any> = {};
16 const hooks = await Forge();
17 await hooks.config(config);
18 return config;
19 }
20
21 test("with a token it sends the bearer header", async () => {
22 const c = await configure("https://f.test/", "tok");
23 expect(c.mcp.forge).toEqual({ type: "remote", url: "https://f.test/mcp", headers: { Authorization: "Bearer tok" } });
24 expect(c.command.forge).toBeDefined();
25 });
26
27 test("with only the url it leaves out Authorization and asks for OAuth", async () => {
28 const c = await configure("https://f.test");
29 expect(c.mcp.forge).toEqual({ type: "remote", url: "https://f.test/mcp", oauth: {} });
30 expect(c.mcp.forge.headers).toBeUndefined();
31 expect(c.command.forge).toBeDefined();
32 });
33
34 test("with neither it registers nothing", async () => {
35 expect(await configure()).toEqual({});
36 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs