diff --git a/AGENTS.md b/AGENTS.md index 9ad77f611e..3f3f90718b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -400,7 +400,7 @@ return new ConversationClient(getAgentServerClientOptions()).someMethod(...); - Custom secrets are NOT auto-attached by the agent-server. `POST /api/conversations` only persists what the client sends in `request.secrets`; the persisted secrets store (`/api/settings/secrets`) is never read at conversation-start. `buildStartConversationRequestWithEncryptedSettings` enumerates `SecretsService.getSecrets()` and turns each entry into a `LookupSecret` whose `url` points back at `/api/settings/secrets/{name}` and whose `headers` carry `X-Session-API-Key` for auth. Pre-1.21.x agent-server SDKs would silently drop that header during validation when `secrets_encrypted=true` (the cipher in the validation context tried to `cipher.decrypt(plaintext_session_key)`, failed, and the validator removed the header — the conversation runtime then got 401s for every saved secret). The SDK fix preserves plaintext header values when decryption fails; if you still see saved secrets unavailable inside a conversation, verify the running agent-server bundles a `LookupSecret._validate_secrets` that falls back to plaintext on decrypt failure. -- MCP page layout: MCP is a **top-level** nav entry at `/mcp` (rendered by `src/routes/mcp.tsx`), shown right below "Skills" in `src/components/features/sidebar/sidebar.tsx`. The legacy `/settings/mcp` route still works as a redirect via `src/routes/mcp-settings-redirect.tsx`, and `src/routes/mcp-settings.tsx` re-exports the new page so the published `MCPSettings` library symbol (in `src/components/settings/index.ts`) keeps the same shape. Marketplace catalog data and MCP logo mappings live in `@openhands/extensions/mcps`; the Slack catalog entry there should point at `https://github.com/zencoderai/slack-mcp-server` and use `@zencoderai/slack-mcp-server`. `src/utils/mcp-marketplace-utils.ts` matches installed servers back to catalog entries. Tavily is a regular stdio MCP entry (`tavily-mcp` + `TAVILY_API_KEY`), not a special built-in sentinel anymore. Components are colocated under `src/components/features/mcp-page/` and reuse the existing `MCPServerForm` for the "Add custom server" / edit flow. +- MCP page layout: MCP is a **top-level** nav entry at `/mcp` (rendered by `src/routes/mcp.tsx`), shown right below "Skills" in `src/components/features/sidebar/sidebar.tsx`. The legacy `/settings/mcp` route still works as a redirect via `src/routes/mcp-settings-redirect.tsx`, and `src/routes/mcp-settings.tsx` re-exports the new page so the published `MCPSettings` library symbol (in `src/components/settings/index.ts`) keeps the same shape. Marketplace catalog data and MCP logo mappings live in `@openhands/extensions/mcps`; the Slack catalog entry there should point at `https://github.com/zencoderai/slack-mcp-server` and use `@zencoderai/slack-mcp-server`. Deprecated marketplace entries removed upstream (for example GitLab / Google Maps / Postgres / Puppeteer / SQLite) should disappear from the marketplace grid, but existing installed servers with those names must still render in the Installed section and stay searchable/editable/deletable via the raw server `name` / `command` fallback in `src/utils/mcp-marketplace-utils.ts` + `InstalledServerCard`. Tavily is a regular stdio MCP entry (`tavily-mcp` + `TAVILY_API_KEY`), not a special built-in sentinel anymore. Components are colocated under `src/components/features/mcp-page/` and reuse the existing `MCPServerForm` for the "Add custom server" / edit flow. - Library packaging notes: - Public npm entrypoints now come from `src/index.ts` → `src/lib/index.ts`, with domain barrels under `src/components/{conversation,terminal,browser,files,settings,sidebar}/index.ts`. diff --git a/__tests__/constants/extensions-catalogs.test.ts b/__tests__/constants/extensions-catalogs.test.ts index 51ae5a6b64..af11d280f7 100644 --- a/__tests__/constants/extensions-catalogs.test.ts +++ b/__tests__/constants/extensions-catalogs.test.ts @@ -31,6 +31,16 @@ describe("OpenHands extensions catalogs", () => { ); }); + it("drops deprecated MCP entries that no longer have maintained replacements", () => { + const catalogIds = new Set(MCP_CATALOG.map((entry) => entry.id)); + + expect(catalogIds.has("gitlab")).toBe(false); + expect(catalogIds.has("google-maps")).toBe(false); + expect(catalogIds.has("postgres")).toBe(false); + expect(catalogIds.has("puppeteer")).toBe(false); + expect(catalogIds.has("sqlite")).toBe(false); + }); + it("loads recommended automations from @openhands/extensions", () => { expect(AUTOMATION_CATALOG.length).toBeGreaterThan(0); diff --git a/__tests__/routes/mcp-page.test.tsx b/__tests__/routes/mcp-page.test.tsx index 0c0a8ad0e6..3587ea2ca9 100644 --- a/__tests__/routes/mcp-page.test.tsx +++ b/__tests__/routes/mcp-page.test.tsx @@ -107,10 +107,44 @@ describe("MCPPage", () => { screen.queryByTestId("mcp-marketplace-card-github"), ).not.toBeInTheDocument(); expect( - screen.queryByTestId("mcp-marketplace-card-postgres"), + screen.queryByTestId("mcp-marketplace-card-gitlab"), ).not.toBeInTheDocument(); }); + it("keeps installed legacy servers visible even after their marketplace entries are removed", async () => { + vi.spyOn(SettingsService, "getSettings").mockResolvedValue( + buildSettings({ + agent_settings: { + ...MOCK_DEFAULT_USER_SETTINGS.agent_settings, + mcp_config: { + mcpServers: { + gitlab: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-gitlab"], + }, + }, + }, + }, + }), + ); + + renderPage(); + + await screen.findByTestId("mcp-installed-list"); + expect(screen.getByText("gitlab")).toBeInTheDocument(); + expect( + screen.queryByTestId("mcp-marketplace-card-gitlab"), + ).not.toBeInTheDocument(); + + const search = screen.getByTestId("mcp-search-input"); + fireEvent.change(search, { target: { value: "gitlab" } }); + + await waitFor(() => { + expect(screen.getByText("gitlab")).toBeInTheDocument(); + }); + expect(screen.getByTestId("mcp-marketplace-empty")).toBeInTheDocument(); + }); + it("shows a search-empty state when the query matches nothing", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue(buildSettings()); diff --git a/package-lock.json b/package-lock.json index 1945bdd5ad..336a80283a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@heroui/react": "2.8.10", "@microlink/react-json-view": "1.31.20", "@monaco-editor/react": "4.7.0", - "@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#c3f896acc55d8ee8717ab40f236960e71c2d477d", + "@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#7b33f64ffccf95f7ccd2ec640aeae84ab1cc2c75", "@openhands/typescript-client": "git+https://github.com/OpenHands/typescript-client.git#v0.6.0", "@react-router/node": "7.14.2", "@react-router/serve": "7.14.2", @@ -3441,8 +3441,8 @@ }, "node_modules/@openhands/extensions": { "version": "0.0.0", - "resolved": "git+https://github.com/OpenHands/extensions.git#c3f896acc55d8ee8717ab40f236960e71c2d477d", - "integrity": "sha512-PzLYaZA9ys8is9dNqNDDAExkz40DJ7t1sXUUrUUdha6vrO1/J4xqspK4YLOp0V5i4DewP3U9Rzqi2nFC/bVIMQ==", + "resolved": "git+https://github.com/OpenHands/extensions.git#7b33f64ffccf95f7ccd2ec640aeae84ab1cc2c75", + "integrity": "sha512-vozWq4MJiKoColb7zvXYgtxmWJsk7LdjdRH9SQebheHDOKvDAn7xy+sM7d0mEFRCk3QDEaVyR5HMH9w/1HH25Q==", "license": "MIT", "engines": { "node": ">=18.20.0" diff --git a/package.json b/package.json index 862cafdd9d..43dba5b4dd 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@heroui/react": "2.8.10", "@microlink/react-json-view": "1.31.20", "@monaco-editor/react": "4.7.0", - "@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#c3f896acc55d8ee8717ab40f236960e71c2d477d", + "@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#7b33f64ffccf95f7ccd2ec640aeae84ab1cc2c75", "@openhands/typescript-client": "git+https://github.com/OpenHands/typescript-client.git#v0.6.0", "@react-router/node": "7.14.2", "@react-router/serve": "7.14.2",