Public: anyone can read this; the crew can change it.
/.well-known/mcp.json and /llms.txt name the Claude Code and OpenCode plugins, each with its one install line #8296
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/8273-well-known-mcp-json-and-llms-txt-name-th → main 1 commit fork
3 files
changed +70
−3
src/core/mcp_card.ts +29
| @@ -16,6 +16,30 @@ export function mcpPointers(origin: string): { url: string; oauth: string } { | ||
| 16 | 16 | }; |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | /** A thin plugin the forge ships for a harness: where its source is and its one install line. */ | |
| 20 | export interface HarnessPlugin { | |
| 21 | readonly harness: string; | |
| 22 | readonly label: string; | |
| 23 | readonly path: string; | |
| 24 | readonly install: string; | |
| 25 | } | |
| 26 | ||
| 27 | /** The one list the card and /llms.txt both read, so they cannot drift. */ | |
| 28 | export const HARNESS_PLUGINS: readonly HarnessPlugin[] = [ | |
| 29 | { | |
| 30 | harness: "claude-code", | |
| 31 | label: "Claude Code", | |
| 32 | path: "plugins/claude-code", | |
| 33 | install: "claude --plugin-dir plugins/claude-code", | |
| 34 | }, | |
| 35 | { | |
| 36 | harness: "opencode", | |
| 37 | label: "OpenCode", | |
| 38 | path: "plugins/opencode/forge.ts", | |
| 39 | install: "mkdir -p .opencode/plugin && cp plugins/opencode/forge.ts .opencode/plugin/", | |
| 40 | }, | |
| 41 | ]; | |
| 42 | ||
| 19 | 43 | /** |
| 20 | 44 | * The server card. The format is still a draft, so only what we can keep |
| 21 | 45 | * true is named: who answers, where, over what transport, how to sign in, |
| @@ -32,5 +56,10 @@ export function mcpCard(origin: string, now: Running = running()) { | ||
| 32 | 56 | protectedResourceMetadata: oauth, |
| 33 | 57 | documentation: `${origin}/llms.txt`, |
| 34 | 58 | tokenFreeTools: ["report"], |
| 59 | plugins: HARNESS_PLUGINS.map((p) => ({ | |
| 60 | harness: p.harness, | |
| 61 | path: p.path, | |
| 62 | install: p.install, | |
| 63 | })), | |
| 35 | 64 | }; |
| 36 | 65 | } |
src/web/standards.ts +5 −3
| @@ -36,7 +36,7 @@ import type { Ctx } from "../http/context.ts"; | ||
| 36 | 36 | import { notFound } from "../http/respond.ts"; |
| 37 | 37 | import { SITE_LINE } from "./site_line.ts"; |
| 38 | 38 | import { mcpToolsSection } from "./llms_tools.ts"; |
| 39 | import { mcpCard, mcpPointers } from "../core/mcp_card.ts"; | |
| 39 | import { HARNESS_PLUGINS, mcpCard, mcpPointers } from "../core/mcp_card.ts"; | |
| 40 | 40 | import { running } from "../kernel/version.ts"; |
| 41 | 41 | import { timePhase } from "../core/route_timing.ts"; |
| 42 | 42 | import { shippedReleases, type Shipped } from "../release/shipped.ts"; |
| @@ -359,8 +359,10 @@ export function llmsTxt(ctx: Ctx): Response { | ||
| 359 | 359 | "## Use it from your harness", |
| 360 | 360 | "", |
| 361 | 361 | `- MCP endpoint: ${mcp.url} (streamable HTTP; OAuth metadata at ${mcp.oauth}). Or send a token as Authorization: Bearer.`, |
| 362 | "- Claude Code plugin: `claude --plugin-dir plugins/claude-code` in a clone of this forge's repository (set FORGE_URL and FORGE_TOKEN).", | |
| 363 | "- OpenCode plugin: copy plugins/opencode/forge.ts into .opencode/plugin/ (set FORGE_URL and FORGE_TOKEN).", | |
| 362 | ...HARNESS_PLUGINS.map( | |
| 363 | (p) => | |
| 364 | `- ${p.label} plugin (source ${p.path} in this forge's repository; set FORGE_URL and FORGE_TOKEN): \`${p.install}\``, | |
| 365 | ), | |
| 364 | 366 | "", |
| 365 | 367 | "## What you can do on any page", |
| 366 | 368 | "", |
test/mcp-card-plugins.test.ts added +36
| @@ -0,0 +1,36 @@ | ||
| 1 | /** The card and /llms.txt name each shipped plugin with its install line, from one constant. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { existsSync } from "node:fs"; | |
| 4 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 5 | import { tmpdir } from "node:os"; | |
| 6 | import { join } from "node:path"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 9 | ||
| 10 | let dataDir: string; | |
| 11 | let app: App; | |
| 12 | let servers: Servers; | |
| 13 | ||
| 14 | beforeAll(async () => { | |
| 15 | dataDir = await mkdtemp(join(tmpdir(), "forge-card-plugins-")); | |
| 16 | app = createApp({ dataDir, port: 0 }); | |
| 17 | servers = await serve(app); | |
| 18 | }); | |
| 19 | afterAll(async () => { | |
| 20 | await servers.stop(); | |
| 21 | await rm(dataDir, { recursive: true, force: true }); | |
| 22 | }); | |
| 23 | ||
| 24 | test("card and llms.txt name both plugins, their install lines, and paths that exist", async () => { | |
| 25 | const card = (await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any; | |
| 26 | const llms = await fetch(`${servers.url}/llms.txt`).then((r) => r.text()); | |
| 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 | for (const p of card.plugins) { | |
| 31 | expect(existsSync(join(import.meta.dir, "..", p.path))).toBe(true); | |
| 32 | expect(llms).toContain(p.install); | |
| 33 | expect(llms).toContain(p.path); | |
| 34 | } | |
| 35 | expect(card.name).toBe("forge"); | |
| 36 | }); |
new above
offline