diff --git a/packages/frontend/src/app/connectors/store/page.tsx b/packages/frontend/src/app/connectors/store/page.tsx
index 8bae4e7d..c641df8a 100644
--- a/packages/frontend/src/app/connectors/store/page.tsx
+++ b/packages/frontend/src/app/connectors/store/page.tsx
@@ -11,7 +11,7 @@ import { AppShell } from '@/components/app-shell';
import { Card } from '@/components/ui/card';
import { Button, buttonVariants } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
-import { authTypeLabel, cn } from '@/lib/utils';
+import { adapterAuthLabel, adapterNeedsCredentials, cn } from '@/lib/utils';
import { McpAssignModal } from '@/components/mcp-assign-modal';
import { matchesSearch } from '@/lib/marketplace-search';
import { isTrialLimitMessage, TrialLimitNotice } from '@/lib/trial-limit';
@@ -606,7 +606,7 @@ function AdapterStoreContent() {
) : (
{filtered.map((adapter) => {
- const isPublic = adapter.authType === 'NONE';
+ const isPublic = !adapterNeedsCredentials(adapter);
const isImporting = importing === adapter.slug;
/* log-ish 1..10 segment scale, same as the marketing-site card */
const fillCount = Math.max(
@@ -666,7 +666,7 @@ function AdapterStoreContent() {
className="max-w-full min-w-0 gap-1 truncate font-mono uppercase tracking-wider"
>
{isPublic ?
:
}
- {authTypeLabel(adapter.authType)}
+ {adapterAuthLabel(adapter)}
)}
{adapter.docsUrl && (
@@ -734,7 +734,7 @@ function AdapterStoreContent() {
- Auth type: {authTypeLabel(configAdapter.connector?.authType)}
+ Auth type: {adapterAuthLabel({ authType: configAdapter.connector?.authType, requiredEnvVars: configAdapter.requiredEnvVars })}
{/* Setup instructions — collapsible details block, default open
@@ -784,6 +784,16 @@ function AdapterStoreContent() {
diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts
index 9c2bb526..c6aee376 100644
--- a/packages/frontend/src/lib/utils.ts
+++ b/packages/frontend/src/lib/utils.ts
@@ -31,3 +31,28 @@ export function authTypeLabel(authType: string | null | undefined): string {
if (!authType) return 'None';
return AUTH_TYPE_LABELS[authType] ?? authType;
}
+
+/** A variable that holds a credential, judged by its name. */
+const SECRET_VARIABLE = /KEY|TOKEN|SECRET|PASSWORD/i;
+
+/**
+ * Whether an adapter needs a credential from the user. Some carry the key in
+ * the request body (Odoo's JSON-RPC) or a URL, so their authType is NONE
+ * although an API key is required: the variables decide too.
+ */
+export function adapterNeedsCredentials(adapter: {
+ authType?: string | null;
+ requiredEnvVars?: string[] | null;
+}): boolean {
+ if (adapter.authType && adapter.authType !== 'NONE') return true;
+ return (adapter.requiredEnvVars ?? []).some((v) => SECRET_VARIABLE.test(v));
+}
+
+/** The auth chip of a catalog adapter: "API Key" rather than "Public API" when a key is needed. */
+export function adapterAuthLabel(adapter: {
+ authType?: string | null;
+ requiredEnvVars?: string[] | null;
+}): string {
+ if (adapter.authType === 'NONE' && adapterNeedsCredentials(adapter)) return 'API Key';
+ return authTypeLabel(adapter.authType);
+}
diff --git a/packages/frontend/tests/e2e/install-form-no-autofill.spec.ts b/packages/frontend/tests/e2e/install-form-no-autofill.spec.ts
new file mode 100644
index 00000000..9f85649b
--- /dev/null
+++ b/packages/frontend/tests/e2e/install-form-no-autofill.spec.ts
@@ -0,0 +1,57 @@
+import { expect, test } from '@playwright/test';
+
+/**
+ * The install form asks for API credentials. A browser that sees a text field
+ * followed by a password field takes it for a login and fills in the saved
+ * AnythingMCP e-mail and password: seen on live installs, where a user's own
+ * login ended up as a connector's Basic Auth or Odoo user id. The fields must
+ * opt out of autofill and password managers.
+ *
+ * And an adapter whose key travels in the body (authType NONE, Odoo's
+ * JSON-RPC) is not a "Public API".
+ */
+
+const USER = { id: 'u1', email: 'test@example.com', name: 'Test User', role: 'ADMIN', organizationId: 'o1', emailVerified: true };
+const ODOO = {
+ slug: 'odoo-jsonrpc', name: 'Odoo 14-18 (JSON-RPC)', description: 'Odoo over JSON-RPC.', region: 'intl', category: 'erp',
+ icon: 'odoo', docsUrl: null, requiredEnvVars: ['ODOO_URL', 'ODOO_DB', 'ODOO_UID', 'ODOO_API_KEY'], toolCount: 11, authType: 'NONE',
+};
+const HN = { slug: 'hackernews', name: 'Hacker News', description: 'Public stories.', region: 'intl', category: 'news', icon: 'hackernews', docsUrl: null, requiredEnvVars: [], toolCount: 5, authType: 'NONE' };
+
+test('credential fields opt out of autofill, and a body-key adapter is not labelled public', async ({ page, baseURL }) => {
+ await page.context().addCookies([{ name: 'amcp_token', value: 't', url: baseURL! }]);
+ await page.addInitScript((u) => {
+ localStorage.setItem('amcp_token', 't');
+ localStorage.setItem('amcp_user', JSON.stringify(u));
+ }, USER);
+ await page.route(/\/api\//, async (route) => {
+ const url = route.request().url();
+ const json = (b: unknown) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(b) });
+ if (/\/api\/adapters\/odoo-jsonrpc$/.test(url)) {
+ return json({ ...ODOO, instructions: 'Four values.', connector: { authType: 'NONE', baseUrl: '{{ODOO_URL}}' }, tools: [] });
+ }
+ if (/\/api\/adapters(\?|$)/.test(url)) return json([ODOO, HN]);
+ if (url.includes('/api/users/me/onboarding-state')) return json({ onboardingCompletedAt: '2026-01-01T00:00:00Z' });
+ if (url.includes('/api/users/me')) return json(USER);
+ if (url.includes('/api/license/status')) return json({ plan: 'community', status: 'active' });
+ if (url.includes('/api/connectors')) return json([]);
+ return json({});
+ });
+
+ await page.goto('/connectors/store');
+ await expect(page.getByText('Odoo 14-18 (JSON-RPC)', { exact: true })).toBeVisible({ timeout: 15_000 });
+ await expect(page.getByText('API Key', { exact: true }).first()).toBeVisible();
+ await expect(page.getByText('Public API', { exact: true })).toHaveCount(1); // Hacker News only
+
+ await page.getByRole('button', { name: 'Install' }).first().click();
+ const uid = page.locator('#cred-ODOO_UID');
+ const key = page.locator('#cred-ODOO_API_KEY');
+ await expect(uid).toBeVisible({ timeout: 10_000 });
+ await expect(uid).toHaveAttribute('autocomplete', 'off');
+ await expect(key).toHaveAttribute('autocomplete', 'new-password');
+ for (const field of [uid, key]) {
+ await expect(field).toHaveAttribute('data-1p-ignore');
+ await expect(field).toHaveAttribute('data-lpignore', 'true');
+ await expect(field).toHaveAttribute('name', /^amcp-connector-var-/);
+ }
+});