Skip to content

Commit 8e91bc2

Browse files
committed
only expose the log path
1 parent a13c958 commit 8e91bc2

File tree

2 files changed

+25
-277
lines changed

2 files changed

+25
-277
lines changed

packages/next/src/server/mcp/tools/get-logs.ts

Lines changed: 8 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
11
/**
2-
* MCP tool for retrieving development logs from the Next.js log file.
2+
* MCP tool for getting the path to the Next.js development log file.
33
*
4-
* This tool reads logs from the {nextConfig.distDir}/logs/next-development.log file
4+
* This tool returns the path to the {nextConfig.distDir}/logs/next-development.log file
55
* that contains browser console logs and other development information.
66
*/
77
import type { McpServer } from 'next/dist/compiled/@modelcontextprotocol/sdk/server/mcp'
8-
import { readFile, stat } from 'fs/promises'
8+
import { stat } from 'fs/promises'
99
import { join } from 'path'
10-
import { z } from 'next/dist/compiled/zod'
11-
12-
// Display 30 lines of logs by default
13-
const MAX_LINES = 30
1410

1511
export function registerGetLogsTool(server: McpServer, distDir: string) {
1612
server.registerTool(
1713
'get_logs',
1814
{
1915
description:
20-
'Get the development logs from the Next.js log file. Returns browser console logs and other development information.',
21-
inputSchema: {
22-
lines: z
23-
.number()
24-
.min(1)
25-
.max(100)
26-
.optional()
27-
.describe('Number of lines to return (default: 30)'),
28-
offset: z
29-
.number()
30-
.min(0)
31-
.optional()
32-
.describe(
33-
'Number of lines to skip from the end (default: 0, meaning start from the last lines)'
34-
),
35-
},
16+
'Get the path to the Next.js development log file. Returns the file path so the agent can read the logs directly.',
3617
},
37-
async (request) => {
18+
async () => {
3819
try {
3920
const logFilePath = join(distDir, 'logs', 'next-development.log')
4021

@@ -46,64 +27,17 @@ export function registerGetLogsTool(server: McpServer, distDir: string) {
4627
content: [
4728
{
4829
type: 'text',
49-
text: `Log file not found at ${logFilePath}. Make sure the MCP server is enabled and the development server is running.`,
50-
},
51-
],
52-
}
53-
}
54-
55-
// Read the log file
56-
const logContent = await readFile(logFilePath, 'utf-8')
57-
58-
// Check if file has any non-whitespace content
59-
const hasContent = logContent.split('\n').some((line) => line.trim())
60-
61-
if (!hasContent) {
62-
return {
63-
content: [
64-
{
65-
type: 'text',
66-
text: 'Log file is empty. No logs have been recorded yet.',
30+
text: `Log file not found at ${logFilePath}.`,
6731
},
6832
],
6933
}
7034
}
7135

72-
// Parse request parameters
73-
const lines = request.lines || MAX_LINES
74-
const offset = request.offset || 0
75-
76-
// Split into lines and filter out empty lines
77-
const allLines = logContent.split('\n').filter((line) => line.trim())
78-
const totalLines = allLines.length
79-
80-
// Calculate the slice range
81-
// offset=0, lines=10 means: get last 10 lines (slice(-10))
82-
// offset=10, lines=10 means: get lines 10-20 from the end (slice(-20, -10))
83-
const startIndex = Math.max(0, totalLines - offset - lines)
84-
const endIndex = totalLines - offset
85-
const selectedLines = allLines.slice(startIndex, endIndex)
86-
87-
const output = selectedLines.join('\n')
88-
const shownLines = selectedLines.length
89-
90-
// Generate descriptive text based on the parameters
91-
let description: string
92-
if (offset === 0) {
93-
description = `Showing last ${shownLines} of ${totalLines} log entries`
94-
} else if (endIndex <= 0 || shownLines === 0) {
95-
description = `No lines available with offset ${offset} (total: ${totalLines} log entries)`
96-
} else {
97-
const startLineNum = startIndex + 1
98-
const endLineNum = endIndex
99-
description = `Showing lines ${startLineNum}-${endLineNum} of ${totalLines} log entries (offset: ${offset}, count: ${lines})`
100-
}
101-
10236
return {
10337
content: [
10438
{
10539
type: 'text',
106-
text: `${description}:\n\n${output}`,
40+
text: `Next.js log file path: ${logFilePath}`,
10741
},
10842
],
10943
}
@@ -112,7 +46,7 @@ export function registerGetLogsTool(server: McpServer, distDir: string) {
11246
content: [
11347
{
11448
type: 'text',
115-
text: `Error reading logs: ${error instanceof Error ? error.message : String(error)}`,
49+
text: `Error getting log file path: ${error instanceof Error ? error.message : String(error)}`,
11650
},
11751
],
11852
}

test/development/mcp-server/mcp-server-get-logs.test.ts

Lines changed: 17 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'path'
22
import { nextTestSetup } from 'e2e-utils'
33
import { retry } from 'next-test-utils'
44

5-
describe('log-file MCP integration', () => {
5+
describe('get-logs MCP tool', () => {
66
const { next, skipped } = nextTestSetup({
77
files: path.join(__dirname, 'fixtures', 'log-file-app'),
88
skipDeployment: true,
@@ -12,48 +12,7 @@ describe('log-file MCP integration', () => {
1212
return
1313
}
1414

15-
function filterOutPaginationHeaders(content: string): string {
16-
return content
17-
.split('\n')
18-
.filter((line) => {
19-
if (/Showing last/.test(line)) {
20-
return false
21-
}
22-
return true
23-
})
24-
.join('\n')
25-
.trim()
26-
}
27-
28-
function normalizeLogContent(content: string): string {
29-
return (
30-
content
31-
// Strip lines containing "Download the React DevTools"
32-
.split('\n')
33-
.filter((line) => {
34-
// filter out the noise logs and pagination headers
35-
if (
36-
/Download the React DevTools|connected to ws at|received ws message|Next.js page already hydrated|Next.js hydrate callback fired|Compiling|Compiled|Ready in/.test(
37-
line
38-
)
39-
) {
40-
return false
41-
}
42-
return true
43-
})
44-
.join('\n')
45-
// Normalize timestamps to consistent format
46-
.replace(/\[\d{2}:\d{2}:\d{2}\.\d{3}\]/g, '[xx:xx:xx.xxx]')
47-
// Normalize dynamic content that might vary between test runs
48-
.replace(/localhost:\d+/g, 'localhost:PORT')
49-
.trim()
50-
)
51-
}
52-
53-
async function callGetLogs(
54-
id: string,
55-
args?: { lines?: number; offset?: number }
56-
): Promise<string> {
15+
async function callGetLogs(id: string): Promise<string> {
5716
const response = await fetch(`${next.url}/_next/mcp`, {
5817
method: 'POST',
5918
headers: {
@@ -64,7 +23,7 @@ describe('log-file MCP integration', () => {
6423
jsonrpc: '2.0',
6524
id,
6625
method: 'tools/call',
67-
params: { name: 'get_logs', arguments: args || {} },
26+
params: { name: 'get_logs', arguments: {} },
6827
}),
6928
})
7029

@@ -74,174 +33,29 @@ describe('log-file MCP integration', () => {
7433
return result.result?.content?.[0]?.text
7534
}
7635

77-
it('should retrieve logs via MCP get_logs tool', async () => {
78-
// Generate some logs by visiting pages that create log entries
79-
await next.browser('/server')
80-
await next.browser('/client')
81-
await next.browser('/pages-router-page')
82-
83-
let logs: string = ''
84-
85-
await retry(async () => {
86-
const sessionId = 'test-mcp-logs-' + Date.now()
87-
logs = await callGetLogs(sessionId)
88-
89-
// Should have some log content
90-
expect(logs).not.toBe(
91-
'Log file is empty. No logs have been recorded yet.'
92-
)
93-
expect(logs).not.toContain('Log file not found at')
94-
})
95-
96-
await retry(async () => {
97-
const normalizedLogs = filterOutPaginationHeaders(
98-
normalizeLogContent(logs)
99-
)
100-
101-
// Use inline snapshot to capture the actual log content
102-
expect(normalizedLogs).toMatchInlineSnapshot(`
103-
"[xx:xx:xx.xxx] Server LOG RSC: This is a log message from server component
104-
[xx:xx:xx.xxx] Server ERROR RSC: This is an error message from server component
105-
[xx:xx:xx.xxx] Server WARN RSC: This is a warning message from server component
106-
[xx:xx:xx.xxx] Server LOG Pages Router SSR: This is a log message from getServerSideProps
107-
[xx:xx:xx.xxx] Server ERROR Pages Router SSR: This is an error message from getServerSideProps
108-
[xx:xx:xx.xxx] Server WARN Pages Router SSR: This is a warning message from getServerSideProps
109-
[xx:xx:xx.xxx] Server LOG Pages Router isomorphic: This is a log message from render"
110-
`)
111-
}, 5 * 1000)
112-
})
113-
11436
it('should handle missing log file gracefully via MCP', async () => {
11537
// This test should run in a clean state, but since other tests may have run first,
11638
// we'll just verify the MCP tool responds correctly
117-
const logs = await callGetLogs('test-no-logs')
118-
119-
// Should have some response (either logs or error message)
120-
expect(logs).toBeDefined()
121-
expect(typeof logs).toBe('string')
122-
})
123-
124-
it('should return paginated results when many logs exist', async () => {
125-
// Generate logs by visiting multiple pages multiple times
126-
for (let i = 0; i < 3; i++) {
127-
await next.browser('/server')
128-
await next.browser('/client')
129-
await next.browser('/pages-router-page')
130-
// Small delay between visits
131-
await new Promise((resolve) => setTimeout(resolve, 100))
132-
}
133-
134-
let logs: string = ''
135-
await retry(async () => {
136-
const sessionId = 'test-pagination-' + Date.now()
137-
logs = await callGetLogs(sessionId)
138-
139-
// Should have log content
140-
expect(logs).not.toBe(
141-
'Log file is empty. No logs have been recorded yet.'
142-
)
143-
expect(logs).not.toContain('Log file not found at')
144-
})
145-
146-
await retry(async () => {
147-
const sessionId = 'test-pagination-' + Date.now()
148-
logs = await callGetLogs(sessionId)
149-
// Check that we have many log entries
150-
const lines = logs.split('\n').length
151-
expect(lines).toBeGreaterThan(30)
152-
153-
const normalizedLogs = filterOutPaginationHeaders(
154-
normalizeLogContent(logs)
155-
)
39+
const response = await callGetLogs('test-no-logs')
15640

157-
// Assert each unique log line individually (multiple instances expected)
158-
expect(normalizedLogs).toMatch(
159-
/\[xx:xx:xx\.xxx\]\s+Server\s+LOG\s+Pages Router SSR: This is a log message from getServerSideProps/
160-
)
161-
expect(normalizedLogs).toMatch(
162-
/\[xx:xx:xx\.xxx\]\s+Server\s+ERROR\s+Pages Router SSR: This is an error message from getServerSideProps/
163-
)
164-
expect(normalizedLogs).toMatch(
165-
/\[xx:xx:xx\.xxx\]\s+Server\s+WARN\s+Pages Router SSR: This is a warning message from getServerSideProps/
166-
)
167-
expect(normalizedLogs).toMatch(
168-
/\[xx:xx:xx\.xxx\]\s+Server\s+LOG\s+Pages Router isomorphic: This is a log message from render/
169-
)
170-
expect(normalizedLogs).toMatch(
171-
/\[xx:xx:xx\.xxx\]\s+Server\s+LOG\s+RSC: This is a log message from server component/
172-
)
173-
expect(normalizedLogs).toMatch(
174-
/\[xx:xx:xx\.xxx\]\s+Server\s+ERROR\s+RSC: This is an error message from server component/
175-
)
176-
expect(normalizedLogs).toMatch(
177-
/\[xx:xx:xx\.xxx\]\s+Server\s+WARN\s+RSC: This is a warning message from server component/
178-
)
179-
expect(normalizedLogs).toMatch(
180-
/\[xx:xx:xx\.xxx\]\s+Browser\s+LOG\s+Client: Complex circular object:/
181-
)
182-
expect(normalizedLogs).toMatch(
183-
/\[xx:xx:xx\.xxx\]\s+Browser\s+ERROR\s+Client: This is an error message from client component/
184-
)
185-
expect(normalizedLogs).toMatch(
186-
/\[xx:xx:xx\.xxx\]\s+Browser\s+WARN\s+Client: This is a warning message from client component/
187-
)
188-
})
41+
expect(response).toContain('Log file not found at')
42+
expect(response).not.toContain('Next.js log file path:')
18943
})
19044

191-
it('should show count of custom offset and lines parameters correctly', async () => {
192-
// Test with offset=10, lines=5 (should get lines 10-15 from the end)
193-
const logsWithOffset = await callGetLogs('test-offset', {
194-
offset: 10,
195-
lines: 5,
196-
})
197-
const normalizedLogsWithOffset = normalizeLogContent(logsWithOffset)
198-
199-
expect(normalizedLogsWithOffset).toMatch(
200-
/Showing lines \d+-\d+ of \d+ log entries/
201-
)
202-
203-
// Test with just lines=5 (should get last 5 lines)
204-
const logsWithLines = await callGetLogs('test-lines', { lines: 5 })
205-
const normalizedLogsWithLines = normalizeLogContent(logsWithLines)
206-
207-
expect(normalizedLogsWithLines).toMatch(
208-
/Showing last 5 of \d+ log entries:/
209-
)
210-
211-
// Test with offset=0, lines=3 (should get last 3 lines)
212-
const logsWithBoth = await callGetLogs('test-both', { offset: 0, lines: 3 })
213-
const normalizedLogsWithBoth = normalizeLogContent(logsWithBoth)
214-
215-
expect(normalizedLogsWithBoth).toMatch(/Showing last 3 of \d+ log entries:/)
216-
})
217-
218-
it('should show logs of custom offset and lines parameters', async () => {
219-
await retry(async () => {
220-
// Test with just lines=5 (should get last 5 lines)
221-
const logsWithLines = filterOutPaginationHeaders(
222-
await callGetLogs('test-lines', { lines: 5 })
223-
)
224-
const normalizedLogsWithLines = normalizeLogContent(logsWithLines)
225-
226-
// The logs are 4 because the unstable noisy logs are filtered out
227-
expect(normalizedLogsWithLines).toMatchInlineSnapshot(`
228-
"[xx:xx:xx.xxx] Server LOG RSC: This is a log message from server component
229-
[xx:xx:xx.xxx] Server ERROR RSC: This is an error message from server component
230-
[xx:xx:xx.xxx] Server WARN RSC: This is a warning message from server component"
231-
`)
232-
})
45+
it('should return log file path via MCP get_logs tool', async () => {
46+
// Generate some logs by visiting pages that create log entries
47+
await next.browser('/server')
48+
await next.browser('/client')
49+
await next.browser('/pages-router-page')
23350

23451
await retry(async () => {
235-
// Test with offset=0, lines=3 (should get last 3 lines)
236-
const logsWithBoth = filterOutPaginationHeaders(
237-
await callGetLogs('test-both', { offset: 0, lines: 3 })
238-
)
239-
const normalizedLogsWithBoth = normalizeLogContent(logsWithBoth)
52+
const sessionId = 'test-mcp-logs-' + Date.now()
53+
const response = await callGetLogs(sessionId)
24054

241-
// The logs are 2 because the unstable noisy logs are filtered out
242-
expect(normalizedLogsWithBoth).toMatchInlineSnapshot(
243-
`"[xx:xx:xx.xxx] Server WARN RSC: This is a warning message from server component"`
244-
)
55+
// Should return the log file path
56+
expect(response).toContain('Next.js log file path:')
57+
expect(response).toContain('logs/next-development.log')
58+
expect(response).not.toContain('Log file not found at')
24559
})
24660
})
24761
})

0 commit comments

Comments
 (0)