next.js/test/development/mcp-server/mcp-server-get-page-metadata.test.ts
mcp-server-get-page-metadata.test.ts235 lines7.9 KB
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { retry } from 'next-test-utils'
import { launchStandaloneSession } from './test-utils'

describe('mcp-server get_page_metadata tool', () => {
  async function callGetPageMetadata(url: string, id: string) {
    const response = await fetch(`${url}/_next/mcp`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json, text/event-stream',
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id,
        method: 'tools/call',
        params: { name: 'get_page_metadata', arguments: {} },
      }),
    })

    const text = await response.text()
    const match = text.match(/data: ({.*})/s)
    const result = JSON.parse(match![1])
    return result.result?.content?.[0]?.text
  }

  describe('app router', () => {
    const { next } = nextTestSetup({
      files: new FileRef(
        path.join(__dirname, 'fixtures', 'parallel-routes-template')
      ),
    })

    it('should return metadata for basic page', async () => {
      await next.browser('/')
      const metadataText = await callGetPageMetadata(next.url, 'test-basic')
      const metadata = JSON.parse(metadataText)

      expect(metadata.sessions).toHaveLength(1)
      expect(metadata.sessions[0]).toMatchObject({
        url: '/',
        routerType: 'app',
      })
      expect(metadata.sessions[0].segments).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ path: 'app/layout.tsx' }),
          expect.objectContaining({
            path: 'global-error.js',
            isBoundary: true,
            isBuiltin: true,
          }),
          expect.objectContaining({ path: 'app/error.tsx', isBoundary: true }),
          expect.objectContaining({
            path: 'app/loading.tsx',
            isBoundary: true,
          }),
          expect.objectContaining({
            path: 'app/not-found.tsx',
            isBoundary: true,
          }),
          expect.objectContaining({ path: 'app/page.tsx' }),
        ])
      )
    })

    it('should return metadata for parallel routes', async () => {
      await next.browser('/parallel')

      let metadata: any = null
      await retry(async () => {
        const sessionId = 'test-parallel-' + Date.now()
        const metadataText = await callGetPageMetadata(next.url, sessionId)
        metadata = JSON.parse(metadataText)
        expect(metadata.sessions).toHaveLength(1)
        // Ensure we have the parallel route files
        const paths = metadata.sessions[0].segments.map((s: any) => s.path)
        expect(paths).toContain('app/parallel/@sidebar/page.tsx')
        expect(paths).toContain('app/parallel/@content/page.tsx')
        expect(paths).toContain('app/parallel/page.tsx')
      })

      expect(metadata.sessions[0]).toMatchObject({
        url: '/parallel',
        routerType: 'app',
      })
      expect(metadata.sessions[0].segments).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ path: 'app/layout.tsx' }),
          expect.objectContaining({ path: 'app/parallel/layout.tsx' }),
          expect.objectContaining({
            path: 'global-error.js',
            isBoundary: true,
            isBuiltin: true,
          }),
          expect.objectContaining({ path: 'app/parallel/@content/page.tsx' }),
          expect.objectContaining({ path: 'app/parallel/@sidebar/page.tsx' }),
          expect.objectContaining({ path: 'app/parallel/page.tsx' }),
        ])
      )
    })

    it('should handle multiple browser sessions', async () => {
      // Open two browser tabs using standalone sessions for true concurrent tabs
      const session1 = await launchStandaloneSession(next.url, '/')
      const session2 = await launchStandaloneSession(next.url, '/parallel')

      try {
        await new Promise((resolve) => setTimeout(resolve, 1000))

        let metadata: any = null
        await retry(async () => {
          const sessionId = 'test-multi-' + Date.now()
          const metadataText = await callGetPageMetadata(next.url, sessionId)
          metadata = JSON.parse(metadataText)
          expect(metadata.sessions.length).toBeGreaterThanOrEqual(2)
          // Ensure both our sessions are present
          const urls = metadata.sessions.map((s: any) => s.url)
          expect(urls).toContain('/')
          expect(urls).toContain('/parallel')
        })

        // Find each session's metadata
        const rootSession = metadata.sessions.find((s: any) => s.url === '/')
        const parallelSession = metadata.sessions.find(
          (s: any) => s.url === '/parallel'
        )

        expect(rootSession).toMatchObject({
          url: '/',
          routerType: 'app',
        })
        expect(rootSession.segments).toEqual(
          expect.arrayContaining([
            expect.objectContaining({ path: 'app/layout.tsx' }),
            expect.objectContaining({ path: 'app/page.tsx' }),
          ])
        )

        expect(parallelSession).toMatchObject({
          url: '/parallel',
          routerType: 'app',
        })
        expect(parallelSession.segments).toEqual(
          expect.arrayContaining([
            expect.objectContaining({ path: 'app/layout.tsx' }),
            expect.objectContaining({ path: 'app/parallel/layout.tsx' }),
            expect.objectContaining({ path: 'app/parallel/@content/page.tsx' }),
            expect.objectContaining({ path: 'app/parallel/@sidebar/page.tsx' }),
            expect.objectContaining({ path: 'app/parallel/page.tsx' }),
          ])
        )
      } finally {
        // Clean up sessions
        await session1.close()
        await session2.close()
      }
    })

    it('should count multiple browser tabs with the same URL separately', async () => {
      await new Promise((resolve) => setTimeout(resolve, 500))

      const session1 = await launchStandaloneSession(next.url, '/')
      const session2 = await launchStandaloneSession(next.url, '/')

      try {
        await new Promise((resolve) => setTimeout(resolve, 1000))

        let metadata: any = null
        await retry(async () => {
          const sessionId = 'test-same-url-' + Date.now()
          const metadataText = await callGetPageMetadata(next.url, sessionId)
          metadata = JSON.parse(metadataText)
          const rootSessions = metadata.sessions.filter(
            (s: any) => s.url === '/'
          ).length
          expect(rootSessions).toBeGreaterThanOrEqual(2)
        })

        const rootSessions = metadata.sessions.filter(
          (s: any) => s.url === '/'
        ).length
        expect(rootSessions).toBeGreaterThanOrEqual(2)
      } finally {
        await session1.close()
        await session2.close()
      }
    })
  })

  describe('pages router', () => {
    const { next } = nextTestSetup({
      files: new FileRef(
        path.join(__dirname, 'fixtures', 'pages-router-template')
      ),
    })

    it('should return metadata showing pages router type', async () => {
      await next.browser('/')

      let metadata: any = null
      await retry(async () => {
        const sessionId = 'test-pages-' + Date.now()
        const metadataText = await callGetPageMetadata(next.url, sessionId)
        metadata = JSON.parse(metadataText)
        expect(metadata.sessions).toHaveLength(1)
      })

      expect(metadata.sessions[0]).toMatchObject({
        url: '/',
        routerType: 'pages',
        segments: [],
      })
    })

    it('should show pages router type for about page', async () => {
      await next.browser('/about')

      let metadata: any = null
      await retry(async () => {
        const sessionId = 'test-pages-about-' + Date.now()
        const metadataText = await callGetPageMetadata(next.url, sessionId)
        metadata = JSON.parse(metadataText)
        expect(metadata.sessions).toHaveLength(1)
      })

      expect(metadata.sessions[0]).toMatchObject({
        url: '/about',
        routerType: 'pages',
        segments: [],
      })
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN