Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4bb325e
feat(mcp_registry): curated one-per-service catalog with namespace-aw…
YellowSnnowmann Jun 25, 2026
9d64f5d
feat(mcp/ui): curated catalog list with source/verified/transport badges
YellowSnnowmann Jun 25, 2026
6a0f90c
feat(mcp/ui): data-driven connect auth modal from declared spec
YellowSnnowmann Jun 25, 2026
b125143
refactor(mcp): drop one-per-service collapse; honest Official badge +…
YellowSnnowmann Jun 25, 2026
36ff13c
feat(mcp): make Smithery opt-in (API-key gated); official registry is…
YellowSnnowmann Jun 25, 2026
fe89708
Merge branch 'main' of https://github.com/tinyhumansai/openhuman into…
YellowSnnowmann Jun 25, 2026
b605fd1
fix(mcp/ui): make the servers table horizontally scrollable
YellowSnnowmann Jun 25, 2026
e42b7fa
feat(mcp): surface 'Get your key' deep links + tighten the auth resolver
YellowSnnowmann Jun 25, 2026
c1a7c98
Revert "feat(mcp): surface 'Get your key' deep links + tighten the au…
YellowSnnowmann Jun 25, 2026
6d672db
test(mcp): route install via smithery:: prefix now that Smithery is o…
YellowSnnowmann Jun 26, 2026
6a71a21
Merge upstream/main into feat/mcp-curated-registry
YellowSnnowmann Jun 26, 2026
2bab62f
Enhance ConnectAuthModal with improved link handling and UI updates
YellowSnnowmann Jun 26, 2026
0d2b17a
Refactor mcp_clients_config_assist to use shared env key collection
YellowSnnowmann Jun 26, 2026
e4178d8
fix(mcp): propagate env-load errors on idempotent re-install
YellowSnnowmann Jun 26, 2026
f8e551c
fix(mcp): atomic install-if-absent + propagate env-load error on refresh
YellowSnnowmann Jun 26, 2026
dd907f5
Merge remote-tracking branch 'origin/feat/mcp-curated-registry' into …
YellowSnnowmann Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions app/src/components/channels/mcp/ConnectAuthModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,172 @@ describe('ConnectAuthModal', () => {
// Non-fatal: the modal still renders and shows the declared key field.
expect(await screen.findByLabelText('Authorization')).toBeInTheDocument();
});

it('renders a declared field from config_schema with a linkified description', async () => {
mockRegistryGet.mockResolvedValue({
qualified_name: 'acme/test-server',
display_name: 'Test Server',
connections: [
{
type: 'http',
config_schema: {
properties: {
'X-API-Key': {
description: 'AnomalyArmor key. Generate at https://app.anomalyarmor.ai/api-key',
'x-secret': true,
},
},
required: ['X-API-Key'],
},
},
],
required_env_keys: [],
});
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
// The exact declared key is rendered as a labelled input…
expect(await screen.findByLabelText('X-API-Key')).toBeInTheDocument();
// …with its description, and the "get your key" URL becomes a clickable link.
expect(screen.getByText(/Generate at/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'https://app.anomalyarmor.ai/api-key' }));
expect(mockOpenUrl).toHaveBeenCalledWith('https://app.anomalyarmor.ai/api-key');
});

it('linkifies a bare-domain "get your key" hint (no scheme) and opens it as https', async () => {
// Registry copy often omits the scheme (e.g. the Apify-hosted GitHub server's
// "Free tier at console.apify.com"). The bare domain must still be a clickable
// link, or the user has no idea where the token comes from.
mockRegistryGet.mockResolvedValue({
qualified_name: 'acme/test-server',
display_name: 'Test Server',
connections: [
{
type: 'http',
config_schema: {
properties: {
'X-API-Key': {
description: 'Apify API token. Free tier at console.apify.com',
'x-secret': true,
},
},
required: ['X-API-Key'],
},
},
],
required_env_keys: [],
});
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
await screen.findByLabelText('X-API-Key');
// The ↗ affordance is aria-hidden, so the link's accessible name is the
// bare domain; clicking opens it with an https scheme prepended.
const link = screen.getByRole('button', { name: 'console.apify.com' });
fireEvent.click(link);
expect(mockOpenUrl).toHaveBeenCalledWith('https://console.apify.com');
});

it('surfaces the HTTP-remote provider host up front and links to the provider site', async () => {
// A server that declares no auth schema but has a hosted endpoint: name the
// provider before the user clicks Connect and eats a 401 just to learn it.
mockRegistryGet.mockResolvedValue({
qualified_name: 'acme/test-server',
display_name: 'Test Server',
connections: [{ type: 'http', deployment_url: 'https://mcp.lona.agency/mcp' }],
required_env_keys: [],
});
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
await screen.findByRole('dialog');
const host = await screen.findByRole('button', { name: 'mcp.lona.agency' });
fireEvent.click(host);
// Links to the provider's site (leading `mcp.` dropped), not the raw endpoint.
expect(mockOpenUrl).toHaveBeenCalledWith('https://lona.agency');
});

it('does not linkify file-like tokens in a description', async () => {
mockRegistryGet.mockResolvedValue({
qualified_name: 'acme/test-server',
display_name: 'Test Server',
connections: [
{
type: 'http',
config_schema: {
properties: {
'X-API-Key': {
description: 'Put the key in config.json, then visit example.com',
'x-secret': true,
},
},
required: ['X-API-Key'],
},
},
],
required_env_keys: [],
});
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
await screen.findByLabelText('X-API-Key');
// The real domain is a link; the filename is left as plain prose.
expect(screen.getByRole('button', { name: 'example.com' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'config.json' })).not.toBeInTheDocument();
});

it('does not strip a two-label provider host down to a public suffix', async () => {
mockRegistryGet.mockResolvedValue({
qualified_name: 'acme/test-server',
display_name: 'Test Server',
connections: [{ type: 'http', deployment_url: 'https://server.io/mcp' }],
required_env_keys: [],
});
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
await screen.findByRole('dialog');
const host = await screen.findByRole('button', { name: 'server.io' });
fireEvent.click(host);
// `server.io` must stay intact — never reduced to the bare TLD `https://io`.
expect(mockOpenUrl).toHaveBeenCalledWith('https://server.io');
});

it('offers a "Where do I get the token?" pointer that opens the config assistant', async () => {
mockDetectAuth.mockResolvedValue({ kind: 'none', grant_types: [] });
render(<ConnectAuthModal server={BASE_SERVER} onClose={() => {}} onConnected={() => {}} />);
await screen.findByRole('dialog');
fireEvent.click(screen.getByRole('button', { name: /Where do I get the token/ }));
await waitFor(() => {
// The connect modal plus the stacked config-help modal.
expect(screen.getAllByRole('dialog').length).toBeGreaterThan(1);
});
});

it('blocks Connect until a required declared field is filled', async () => {
mockRegistryGet.mockResolvedValue({
qualified_name: 'acme/test-server',
display_name: 'Test Server',
connections: [
{
type: 'http',
config_schema: {
properties: { 'X-API-Key': { 'x-secret': true } },
required: ['X-API-Key'],
},
},
],
required_env_keys: [],
});
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
await screen.findByLabelText('X-API-Key');
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
});
// A clear, per-field error — not a silent failed connect.
await waitFor(() => {
expect(screen.getByText('"X-API-Key" is required')).toBeInTheDocument();
});
expect(mockUpdateEnv).not.toHaveBeenCalled();
expect(mockConnect).not.toHaveBeenCalled();
});

it('does not seed a token box for OAuth servers', async () => {
mockDetectAuth.mockResolvedValue({ kind: 'oauth', grant_types: ['authorization_code'] });
render(<ConnectAuthModal server={NO_KEYS_SERVER} onClose={() => {}} onConnected={() => {}} />);
// OAuth servers get a sign-in button and no auto-seeded Authorization row —
// pasting a token there is exactly what fails (e.g. a GitHub PAT vs OAuth).
await screen.findByRole('button', { name: 'Sign in with browser' });
expect(screen.queryByDisplayValue('Authorization')).not.toBeInTheDocument();
});
});
Loading
Loading