Files
hanzo.ai/e2e/mcp.spec.ts
zeekay 06d7a66ecf MCP is native, and the shelf is under test
The listings are the platform's copy of registry.modelcontextprotocol.io
and installing is POST /v1/tools/mcp/servers with a listing id — the same
row typing a URL by hand creates, so an org has one place its servers live
however they got there. Both were true and neither was asserted.

The spec puts a reachable server and a stdio one on the shelf, because the
difference is the behaviour worth guarding: a remote endpoint is something
this app can connect, a stdio package is a process somebody still has to
run, and the card says so rather than offering a button that fails.
Measured: one Install control for the two rows.
2026-08-29 22:12:23 -07:00

69 lines
2.4 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { signIn } from './session'
/**
* MCP IS NATIVE, AND THE SHELF IS REAL.
*
* The listings are the platform's copy of registry.modelcontextprotocol.io, and
* installing is `POST /v1/tools/mcp/servers` — the same row typing a URL by hand
* creates, so an org has one place its servers live however they got there.
*
* A stdio-only listing cannot be installed from a browser: a remote endpoint is
* something this app can reach, a stdio package is a process somebody still has
* to run. The shelf must say that rather than offer a button that fails, so both
* kinds are on the shelf here and the difference is what is asserted.
*/
const REMOTE = {
id: 'gate-remote',
name: 'gate-remote',
title: 'Gate Remote',
vendor: 'gate',
description: 'A server this app can reach.',
transports: ['streamable-http'],
packages: [],
}
const STDIO = {
id: 'gate-stdio',
name: 'gate-stdio',
title: 'Gate Stdio',
vendor: 'gate',
description: 'A process somebody still has to run.',
transports: ['stdio'],
packages: [{ registry: 'npm', name: 'gate-stdio' }],
}
test('the shelf lists MCP servers and installs the ones it can reach', async ({ page }) => {
await signIn(page)
const json = (body: unknown) => ({
status: 200,
contentType: 'application/json',
body: JSON.stringify(body),
})
let installed: unknown = null
await page.route('**/v1/tools/catalog**', (r) => r.fulfill(json({ catalog: [REMOTE, STDIO], total: 2 })))
await page.route('**/v1/tools/mcp/servers', async (r) => {
if (r.request().method() === 'POST') {
installed = JSON.parse(r.request().postData() ?? '{}')
return r.fulfill(json({ id: 'srv_1' }))
}
return r.fulfill(json({ data: [] }))
})
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto('/work')
await page.waitForLoadState('networkidle')
await page.getByRole('button', { name: 'All agents', exact: true }).click()
// Both are on the shelf — the registry's rows are not filtered to what is
// convenient.
await expect(page.getByText(REMOTE.title)).toBeVisible({ timeout: 15_000 })
await expect(page.getByText(STDIO.title)).toBeVisible()
// And installing reaches the platform's one install.
await page.getByRole('button', { name: 'Install', exact: true }).click()
await expect
.poll(() => installed, { timeout: 10_000 })
.toMatchObject({ listing: REMOTE.id })
})