Public: anyone can read this; the crew can change it.
The OpenCode plugin connects with OAuth when FORGE_TOKEN is not set #8300
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8272-the-opencode-plugin-connects-with-oauth → main 2 commits fork
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, | ||
| 26 | 26 | parked and why, failed), flagged ones first, through MCP `asked` with |
| 27 | 27 | `show: "all"`; with an app, only that app's asks. |
| 28 | 28 | |
| 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 | ||
| 29 | 35 | Code Mode can also read `GET /contract/openapi.json`. |
| 30 | 36 | |
| 31 | 37 | `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 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * 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. | |
| 4 | 5 | * No logic lives here: the tools are the forge's own (`docs/MCP.md`). |
| 5 | 6 | */ |
| 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> }>; | |
| 7 | 9 | |
| 8 | 10 | const COMMAND = `Use the forge MCP tools for: $ARGUMENTS |
| 9 | 11 | - 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 () => ({ | ||
| 22 | 24 | config: async (config: Record<string, any>) => { |
| 23 | 25 | const url = process.env["FORGE_URL"]; |
| 24 | 26 | const token = process.env["FORGE_TOKEN"]; |
| 25 | if (!url || !token) return; | |
| 27 | if (!url) return; | |
| 26 | 28 | config["mcp"] = { |
| 27 | 29 | ...config["mcp"], |
| 28 | 30 | forge: { |
| 29 | 31 | type: "remote", |
| 30 | 32 | 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: {} }), | |
| 32 | 35 | }, |
| 33 | 36 | }; |
| 34 | 37 | 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 | }); |
new above
offline