Public: anyone can read this; the crew can change it.
The plugin install lines in mcp.json and /llms.txt work for someone with no clone of the forge #8359
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/8324-the-plugin-install-lines-in-mcp-json-and → main 1 commit fork
3 files
changed +30
−9
src/core/mcp_card.ts +16 −4
| @@ -21,25 +21,36 @@ export interface HarnessPlugin { | ||
| 21 | 21 | readonly harness: string; |
| 22 | 22 | readonly label: string; |
| 23 | 23 | readonly path: string; |
| 24 | readonly install: string; | |
| 24 | /** The one line, given the origin it was read from; it works in an empty directory. */ | |
| 25 | readonly install: (origin: string) => string; | |
| 25 | 26 | /** What to set before it runs. */ |
| 26 | 27 | readonly setup: string; |
| 27 | 28 | } |
| 28 | 29 | |
| 30 | /** The repository that holds the plugins; it must be public on the forge for the lines to work. */ | |
| 31 | const PLUGIN_REPO = "trevato/forge"; | |
| 32 | ||
| 33 | /** The clone URL of that repository on this forge's origin. */ | |
| 34 | export function pluginSource(origin: string): string { | |
| 35 | return `${origin}/${PLUGIN_REPO}.git`; | |
| 36 | } | |
| 37 | ||
| 29 | 38 | /** The one list the card and /llms.txt both read, so they cannot drift. */ |
| 30 | 39 | export const HARNESS_PLUGINS: readonly HarnessPlugin[] = [ |
| 31 | 40 | { |
| 32 | 41 | harness: "claude-code", |
| 33 | 42 | label: "Claude Code", |
| 34 | 43 | path: "plugins/claude-code", |
| 35 | install: "claude --plugin-dir plugins/claude-code", | |
| 44 | install: (o) => | |
| 45 | `git clone --depth 1 --filter=blob:none --sparse ${pluginSource(o)} forge-plugin && git -C forge-plugin sparse-checkout set plugins/claude-code && claude --plugin-dir forge-plugin/plugins/claude-code`, | |
| 36 | 46 | setup: "set FORGE_URL, then sign in through /mcp with the forge server; or also set FORGE_TOKEN and use the forge-token server", |
| 37 | 47 | }, |
| 38 | 48 | { |
| 39 | 49 | harness: "opencode", |
| 40 | 50 | label: "OpenCode", |
| 41 | 51 | path: "plugins/opencode/forge.ts", |
| 42 | install: "mkdir -p .opencode/plugin && cp plugins/opencode/forge.ts .opencode/plugin/", | |
| 52 | install: (o) => | |
| 53 | `mkdir -p .opencode/plugin && curl -fsSL ${o}/${PLUGIN_REPO}/raw/main/plugins/opencode/forge.ts -o .opencode/plugin/forge.ts`, | |
| 43 | 54 | setup: "set FORGE_URL and FORGE_TOKEN", |
| 44 | 55 | }, |
| 45 | 56 | ]; |
| @@ -64,7 +75,8 @@ export function mcpCard(origin: string, now: Running = running()) { | ||
| 64 | 75 | plugins: HARNESS_PLUGINS.map((p) => ({ |
| 65 | 76 | harness: p.harness, |
| 66 | 77 | path: p.path, |
| 67 | install: p.install, | |
| 78 | source: pluginSource(origin), | |
| 79 | install: p.install(origin), | |
| 68 | 80 | })), |
| 69 | 81 | }; |
| 70 | 82 | } |
src/web/standards.ts +2 −2
| @@ -38,7 +38,7 @@ import type { Ctx } from "../http/context.ts"; | ||
| 38 | 38 | import { notFound } from "../http/respond.ts"; |
| 39 | 39 | import { SITE_LINE } from "./site_line.ts"; |
| 40 | 40 | import { mcpToolsSection } from "./llms_tools.ts"; |
| 41 | import { HARNESS_PLUGINS, mcpCard, mcpPointers } from "../core/mcp_card.ts"; | |
| 41 | import { HARNESS_PLUGINS, mcpCard, mcpPointers, pluginSource } from "../core/mcp_card.ts"; | |
| 42 | 42 | import { running } from "../kernel/version.ts"; |
| 43 | 43 | import { timePhase, timePhaseAsync } from "../core/route_timing.ts"; |
| 44 | 44 | import { shippedReleases, type Shipped } from "../release/shipped.ts"; |
| @@ -391,7 +391,7 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s | ||
| 391 | 391 | `- MCP endpoint: ${mcp.url} (streamable HTTP; OAuth metadata at ${mcp.oauth}). Or send a token as Authorization: Bearer.`, |
| 392 | 392 | ...HARNESS_PLUGINS.map( |
| 393 | 393 | (p) => |
| 394 | `- ${p.label} plugin (source ${p.path} in this forge's repository; ${p.setup}): \`${p.install}\``, | |
| 394 | `- ${p.label} plugin (source ${pluginSource(o)}, path ${p.path}; ${p.setup}; needs that repository to be public): \`${p.install(o)}\``, | |
| 395 | 395 | ), |
| 396 | 396 | "", |
| 397 | 397 | "## What you can do on any page", |
test/mcp-card-plugins.test.ts +12 −3
| @@ -25,12 +25,21 @@ test("card and llms.txt name both plugins, their install lines, and paths that e | ||
| 25 | 25 | const card = (await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any; |
| 26 | 26 | const llms = await fetch(`${servers.url}/llms.txt`).then((r) => r.text()); |
| 27 | 27 | expect(card.plugins.map((p: any) => p.harness).sort()).toEqual(["claude-code", "opencode"]); |
| 28 | const claude = card.plugins.find((p: any) => p.harness === "claude-code"); | |
| 29 | expect(claude.install).toBe("claude --plugin-dir plugins/claude-code"); | |
| 30 | 28 | for (const p of card.plugins) { |
| 31 | expect(existsSync(join(import.meta.dir, "..", p.path))).toBe(true); | |
| 29 | expect(p.source).toBe(`${servers.url}/trevato/forge.git`); | |
| 30 | expect(llms).toContain(p.source); | |
| 32 | 31 | expect(llms).toContain(p.install); |
| 33 | 32 | expect(llms).toContain(p.path); |
| 33 | // Nothing outside what the line fetched: no bare plugins/ path, no clone-relative cp. | |
| 34 | const bare = p.install.replace(/forge-plugin\/plugins\//g, "").replace(/sparse-checkout set plugins\//, "").replace(/\/raw\/main\/plugins\//g, ""); | |
| 35 | expect(bare).not.toMatch(/(^|\s)plugins\//); | |
| 36 | expect(p.install).not.toMatch(/\bcp /); | |
| 34 | 37 | } |
| 38 | const claude = card.plugins.find((p: any) => p.harness === "claude-code"); | |
| 39 | expect(claude.install).toContain("--sparse"); | |
| 40 | expect(claude.install).toContain("--plugin-dir forge-plugin/plugins/claude-code"); | |
| 41 | const oc = card.plugins.find((p: any) => p.harness === "opencode"); | |
| 42 | expect(oc.install).toContain(`curl -fsSL ${servers.url}/trevato/forge/raw/main/plugins/opencode/forge.ts`); | |
| 43 | for (const p of card.plugins) expect(existsSync(join(import.meta.dir, "..", p.path))).toBe(true); | |
| 35 | 44 | expect(card.name).toBe("forge"); |
| 36 | 45 | }); |
new above
offline