Skip to content

Commit 17a8d81

Browse files
committed
Add nextjs_docs back as a version-aware docs gateway
Instead of fetching docs over the network, nextjs_docs now detects the project's installed Next.js version and points the agent at the docs bundled in node_modules/next/dist/docs/ (Next.js 16+), or recommends the upgrade codemod for older projects. It enables agents to read version-accurate docs themselves rather than doing the search.
1 parent e4fc16d commit 17a8d81

7 files changed

Lines changed: 281 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ The main server entry point is `src/index.ts` which uses the standard MCP SDK wi
5757
**MCP Tools** (`src/tools/`):
5858
- Each tool exports: `inputSchema` (Zod schemas), `metadata` (name, description), `handler` (async function)
5959
- Tools are manually imported and registered in `src/index.ts`
60+
- `nextjs_docs`: Version-aware docs gateway — points agents at the bundled docs in `node_modules/next/dist/docs/` (Next.js 16+) or recommends the upgrade codemod. Does NOT fetch docs.
6061
- `nextjs_index`: Discover all running Next.js dev servers and list their available MCP tools
6162
- `nextjs_call`: Execute specific MCP tools on a running Next.js dev server
6263
- `browser_eval`: Playwright browser automation (via `playwright-mcp` server)

README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
`next-devtools-mcp` is a Model Context Protocol (MCP) server that connects coding agents like Claude and Cursor to your running Next.js dev server. It discovers running servers and proxies their built-in MCP endpoint (`/_next/mcp`), giving agents live access to runtime errors, routes, and logs — plus Playwright-based browser testing.
66

77
> [!NOTE]
8-
> Documentation and migration guidance no longer ship in this server. Next.js bundles its docs in `node_modules/next/dist/docs/` (surfaced via `AGENTS.md`), and upgrade/Cache Components workflows are now distributed as agent skills. See [Migrating from 0.3.x](#migrating-from-03x).
8+
> This server no longer bundles documentation or migration prompts. Next.js ships its own docs in `node_modules/next/dist/docs/` (surfaced via `AGENTS.md`); the `nextjs_docs` tool now points agents there instead of fetching. Upgrade/Cache Components workflows are moving to agent skills. See [Migrating from 0.3.x](#migrating-from-03x).
99
1010

1111
## Getting Started
@@ -216,6 +216,25 @@ Your agent uses the `nextjs_index` and `nextjs_call` tools to query your running
216216
217217
## MCP Tools
218218

219+
<details>
220+
<summary><code>nextjs_docs</code></summary>
221+
222+
Points your agent at the version-accurate Next.js documentation for the current project. **It does not fetch docs** — Next.js 16+ ships its full documentation inside the installed package at `node_modules/next/dist/docs/` (markdown, matching your exact version), and this tool tells the agent where to read it.
223+
224+
**What it does:**
225+
- Detects the project's installed Next.js version
226+
- On Next.js 16+: returns the local docs path and how to read/grep it, so the agent uses version-accurate docs instead of training-data guesses
227+
- On older versions: recommends upgrading with `npx @next/codemod@latest upgrade latest`, which brings the bundled docs and an `AGENTS.md` that points agents to them
228+
229+
**Input:**
230+
- `topic` (optional) - What you're looking for (e.g. `use cache`, `generateMetadata`); used only to suggest where to look
231+
- `project_path` (optional) - Path to the project (defaults to current directory)
232+
233+
**Output:**
234+
- JSON describing where to read the docs, or how to upgrade to get them
235+
236+
</details>
237+
219238
<details>
220239
<summary><code>browser_eval</code></summary>
221240

@@ -334,14 +353,17 @@ Calls a specific runtime diagnostic tool on a Next.js 16+ dev server's built-in
334353

335354
## Migrating from 0.3.x
336355

337-
Starting in 0.4.0, `next-devtools-mcp` is a thin connector to the Next.js dev server. These were removed:
356+
Starting in 0.4.0, `next-devtools-mcp` is a thin connector to the Next.js dev server.
357+
358+
**Changed:**
359+
- **`nextjs_docs`** no longer fetches documentation over the network. It is now a gateway that points your agent at the version-accurate docs Next.js bundles at `node_modules/next/dist/docs/` (or recommends upgrading if the project is too old). The `nextjs-docs://llms-index` resource is removed.
338360

339-
- **`nextjs_docs` tool and `nextjs-docs://llms-index` resource** — Next.js bundles its full documentation in `node_modules/next/dist/docs/` (as markdown), surfaced to agents through the project's `AGENTS.md`. Point your agent at those files instead of fetching docs over MCP.
340-
- **`init` tool** — it existed only to enforce the docs-fetch workflow above, which is no longer needed.
361+
**Removed:**
362+
- **`init` tool** — it existed only to enforce the old docs-fetch workflow, which is no longer needed.
341363
- **`upgrade_nextjs_16` and `enable_cache_components` tools, and their prompts** — these workflows are moving to distributable agent skills.
342364
- **All `cache-components://`, `nextjs16://`, and `nextjs-fundamentals://` resources** — superseded by the bundled docs.
343365

344-
What remains is server discovery (`nextjs_index`), runtime proxying (`nextjs_call`), and browser automation (`browser_eval`).
366+
What remains: the docs gateway (`nextjs_docs`), server discovery (`nextjs_index`), runtime proxying (`nextjs_call`), and browser automation (`browser_eval`).
345367

346368
## Privacy & Telemetry
347369

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ const __filename = fileURLToPath(import.meta.url)
1818
const __dirname = dirname(__filename)
1919

2020
import * as browserEval from "./tools/browser-eval.js"
21+
import * as nextjsDocs from "./tools/nextjs-docs.js"
2122
import * as nextjsIndex from "./tools/nextjs_index.js"
2223
import * as nextjsCall from "./tools/nextjs_call.js"
2324

24-
const tools = [browserEval, nextjsIndex, nextjsCall]
25+
const tools = [browserEval, nextjsDocs, nextjsIndex, nextjsCall]
2526

2627
const toolNameToTelemetryName: Record<string, McpToolName> = {
2728
browser_eval: "mcp/browser_eval",
29+
nextjs_docs: "mcp/nextjs_docs",
2830
nextjs_index: "mcp/nextjs_index",
2931
nextjs_call: "mcp/nextjs_call",
3032
}

src/telemetry/mcp-telemetry-tracker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export type McpToolName =
22
| "mcp/browser_eval"
3+
| "mcp/nextjs_docs"
34
| "mcp/nextjs_index"
45
| "mcp/nextjs_call"
56

src/tools/nextjs-docs.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { z } from "zod"
2+
import fs from "node:fs"
3+
import path from "node:path"
4+
5+
// Next.js started bundling its full documentation inside the npm package
6+
// (node_modules/next/dist/docs/**/*.md) and generating an AGENTS.md that points
7+
// there in v16.0.0. At or above this version, agents should read those local,
8+
// version-accurate docs directly instead of fetching anything over MCP.
9+
const BUNDLED_DOCS_MIN_MAJOR = 16
10+
11+
export const inputSchema = {
12+
topic: z
13+
.string()
14+
.optional()
15+
.describe(
16+
"Optional: what you're looking for (e.g. 'use cache', 'generateMetadata', 'middleware'). Used only to suggest where to look in the bundled docs."
17+
),
18+
project_path: z
19+
.string()
20+
.optional()
21+
.describe("Path to the Next.js project (defaults to current directory)"),
22+
}
23+
24+
type NextjsDocsArgs = {
25+
topic?: string
26+
project_path?: string
27+
}
28+
29+
export const metadata = {
30+
name: "nextjs_docs",
31+
description: `Find the version-accurate Next.js documentation for THIS project.
32+
33+
This tool does NOT fetch documentation. Next.js 16+ ships its full docs inside the installed package at \`node_modules/next/dist/docs/\` (markdown), kept in sync with the exact version you have installed. This tool tells you where those docs are and how to read them — so you read the docs that match this project, not a generic or outdated copy.
34+
35+
Call this before answering Next.js questions or writing Next.js code. Then read the relevant guide from the path it returns. If the project is on an older Next.js, it will tell you how to upgrade.`,
36+
}
37+
38+
// Extract the major version from an installed version ("16.3.0-canary.49") or a
39+
// declared range ("^16.0.0", "~15.2"). Returns null when it can't be determined
40+
// (e.g. "latest", "canary", a git/file specifier).
41+
function parseMajor(versionish: string | null | undefined): number | null {
42+
if (!versionish) return null
43+
const match = versionish.match(/(\d+)\./)
44+
if (!match) {
45+
// Bare integer like "16"
46+
const bare = versionish.match(/^\D*(\d+)\D*$/)
47+
return bare ? parseInt(bare[1], 10) : null
48+
}
49+
return parseInt(match[1], 10)
50+
}
51+
52+
// Resolve the Next.js version for a project, preferring the actually-installed
53+
// version (most accurate) over the declared dependency range.
54+
function resolveNextVersion(projectPath: string): {
55+
version: string | null
56+
source: "installed" | "declared" | null
57+
} {
58+
try {
59+
const installedPkg = path.join(
60+
projectPath,
61+
"node_modules",
62+
"next",
63+
"package.json"
64+
)
65+
if (fs.existsSync(installedPkg)) {
66+
const { version } = JSON.parse(fs.readFileSync(installedPkg, "utf8"))
67+
if (typeof version === "string") return { version, source: "installed" }
68+
}
69+
} catch {
70+
// fall through to declared
71+
}
72+
73+
try {
74+
const pkgPath = path.join(projectPath, "package.json")
75+
if (fs.existsSync(pkgPath)) {
76+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
77+
const declared = pkg.dependencies?.next ?? pkg.devDependencies?.next
78+
if (typeof declared === "string") return { version: declared, source: "declared" }
79+
}
80+
} catch {
81+
// fall through to unknown
82+
}
83+
84+
return { version: null, source: null }
85+
}
86+
87+
export async function handler({ topic, project_path }: NextjsDocsArgs): Promise<string> {
88+
const projectPath = project_path || process.cwd()
89+
const { version, source } = resolveNextVersion(projectPath)
90+
const major = parseMajor(version)
91+
92+
// Treat unknown declared versions like "latest"/"canary" as modern.
93+
const isModern =
94+
major !== null
95+
? major >= BUNDLED_DOCS_MIN_MAJOR
96+
: /latest|canary|rc|beta/i.test(version ?? "")
97+
98+
if (isModern) {
99+
const docsDir = path.join(projectPath, "node_modules", "next", "dist", "docs")
100+
const docsExist = fs.existsSync(docsDir)
101+
return JSON.stringify({
102+
status: "use_bundled_docs",
103+
nextVersion: version,
104+
versionSource: source,
105+
docsPath: "node_modules/next/dist/docs/",
106+
docsAvailable: docsExist,
107+
instructions: [
108+
"Next.js ships its full documentation with the installed package, matching your exact version.",
109+
`Read the relevant guide directly from \`${docsDir}\` (markdown files mirroring the nextjs.org/docs structure).`,
110+
topic
111+
? `For "${topic}", search those files, e.g.: grep -ril "${topic.replace(/"/g, "")}" node_modules/next/dist/docs`
112+
: "Browse the directory or grep it for the API/topic you need.",
113+
"Do not rely on training-data knowledge of Next.js APIs — this version may differ. Prefer the bundled docs.",
114+
...(docsExist
115+
? []
116+
: [
117+
"Note: the docs directory was not found. Make sure dependencies are installed (the docs ship inside the `next` package).",
118+
]),
119+
],
120+
})
121+
}
122+
123+
// Older Next.js (or no Next.js found): point to the upgrade path.
124+
return JSON.stringify({
125+
status: "upgrade_required",
126+
nextVersion: version,
127+
versionSource: source,
128+
message:
129+
version
130+
? `This project is on Next.js ${version}. Version-accurate documentation is bundled with Next.js ${BUNDLED_DOCS_MIN_MAJOR}+ (at node_modules/next/dist/docs/) and surfaced to agents via AGENTS.md.`
131+
: `No installed Next.js was detected in ${projectPath}. Next.js ${BUNDLED_DOCS_MIN_MAJOR}+ bundles version-accurate documentation at node_modules/next/dist/docs/.`,
132+
instructions: [
133+
`Upgrade to the latest Next.js by running: npx @next/codemod@latest upgrade latest`,
134+
"After upgrading, this project will ship version-accurate docs locally and `next dev` will generate/update an AGENTS.md pointing agents to them.",
135+
"Until then, refer to https://nextjs.org/docs and avoid guessing version-specific APIs.",
136+
],
137+
})
138+
}

test/e2e/mcp-registration.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,16 @@ describe("MCP Server Registration", () => {
9797
const tools = (toolsResponse.result as any).tools
9898
const toolNames = tools.map((t: any) => t.name).sort()
9999

100-
// Thin wrapper: server discovery/proxy + browser automation only.
101-
// Docs search, prompts, and the upgrade/cache-components knowledge tools
102-
// were removed (docs ship in node_modules/next/dist/docs; workflows are skills).
103-
expect(toolNames).toEqual(["browser_eval", "nextjs_call", "nextjs_index"])
100+
// Thin wrapper: server discovery/proxy, browser automation, and a
101+
// version-aware docs gateway. The upgrade/cache-components knowledge tools,
102+
// prompts, and resources were removed (workflows are skills); nextjs_docs
103+
// no longer fetches — it points at the bundled docs in node_modules.
104+
expect(toolNames).toEqual([
105+
"browser_eval",
106+
"nextjs_call",
107+
"nextjs_docs",
108+
"nextjs_index",
109+
])
104110
} finally {
105111
serverProcess.kill()
106112
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest"
2+
import fs from "node:fs"
3+
import os from "node:os"
4+
import path from "node:path"
5+
import { handler } from "../../src/tools/nextjs-docs.js"
6+
7+
let tmpDir: string
8+
9+
function makeProject(opts: {
10+
declared?: string
11+
installed?: string
12+
withDocs?: boolean
13+
}): string {
14+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nextjs-docs-gateway-"))
15+
if (opts.declared) {
16+
fs.writeFileSync(
17+
path.join(dir, "package.json"),
18+
JSON.stringify({ dependencies: { next: opts.declared } })
19+
)
20+
}
21+
if (opts.installed) {
22+
const nextPkgDir = path.join(dir, "node_modules", "next")
23+
fs.mkdirSync(nextPkgDir, { recursive: true })
24+
fs.writeFileSync(
25+
path.join(nextPkgDir, "package.json"),
26+
JSON.stringify({ name: "next", version: opts.installed })
27+
)
28+
if (opts.withDocs) {
29+
const docsDir = path.join(nextPkgDir, "dist", "docs")
30+
fs.mkdirSync(docsDir, { recursive: true })
31+
fs.writeFileSync(path.join(docsDir, "index.md"), "# docs")
32+
}
33+
}
34+
return dir
35+
}
36+
37+
describe("nextjs_docs gateway", () => {
38+
beforeEach(() => {
39+
tmpDir = ""
40+
})
41+
42+
afterEach(() => {
43+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true })
44+
})
45+
46+
it("points at bundled docs for installed Next.js 16+", async () => {
47+
tmpDir = makeProject({ installed: "16.3.0", withDocs: true })
48+
const result = JSON.parse(await handler({ project_path: tmpDir }))
49+
50+
expect(result.status).toBe("use_bundled_docs")
51+
expect(result.nextVersion).toBe("16.3.0")
52+
expect(result.versionSource).toBe("installed")
53+
expect(result.docsAvailable).toBe(true)
54+
expect(result.docsPath).toBe("node_modules/next/dist/docs/")
55+
})
56+
57+
it("treats a canary install as modern", async () => {
58+
tmpDir = makeProject({ installed: "16.0.0-canary.49", withDocs: true })
59+
const result = JSON.parse(await handler({ project_path: tmpDir }))
60+
expect(result.status).toBe("use_bundled_docs")
61+
})
62+
63+
it("prefers the installed version over the declared range", async () => {
64+
// Declares ^15 but actually has 16 installed -> should use installed (modern).
65+
tmpDir = makeProject({ declared: "^15.0.0", installed: "16.1.0", withDocs: true })
66+
const result = JSON.parse(await handler({ project_path: tmpDir }))
67+
expect(result.status).toBe("use_bundled_docs")
68+
expect(result.versionSource).toBe("installed")
69+
})
70+
71+
it("recommends the codemod for Next.js below 16", async () => {
72+
tmpDir = makeProject({ declared: "15.2.0", installed: "15.2.0" })
73+
const result = JSON.parse(await handler({ project_path: tmpDir }))
74+
75+
expect(result.status).toBe("upgrade_required")
76+
expect(result.nextVersion).toBe("15.2.0")
77+
expect(JSON.stringify(result.instructions)).toContain(
78+
"npx @next/codemod@latest upgrade latest"
79+
)
80+
})
81+
82+
it("recommends upgrade when no Next.js is detected", async () => {
83+
tmpDir = makeProject({})
84+
const result = JSON.parse(await handler({ project_path: tmpDir }))
85+
expect(result.status).toBe("upgrade_required")
86+
expect(result.nextVersion).toBeNull()
87+
})
88+
89+
it("flags missing docs dir even on a modern version", async () => {
90+
tmpDir = makeProject({ installed: "16.0.0", withDocs: false })
91+
const result = JSON.parse(await handler({ project_path: tmpDir }))
92+
expect(result.status).toBe("use_bundled_docs")
93+
expect(result.docsAvailable).toBe(false)
94+
})
95+
96+
it("includes a grep hint when a topic is provided", async () => {
97+
tmpDir = makeProject({ installed: "16.2.0", withDocs: true })
98+
const result = JSON.parse(await handler({ project_path: tmpDir, topic: "use cache" }))
99+
expect(JSON.stringify(result.instructions)).toContain("use cache")
100+
})
101+
})

0 commit comments

Comments
 (0)