mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
69 lines
2.4 KiB
TypeScript
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 })
|
|
})
|