From dc36aee6e19b9e3849646e5d0c81d0ac3b2c5151 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 9 Nov 2026 02:02:13 -0600 Subject: [PATCH] fix(api-catalog): send an own MCP server's API key to the secure setup page An agent asked to connect a person's own MCP server with an API key saw the connections_search instruction but kept searching the API for a way to create the connection itself. The two connection-creation entries it finds now say the person enters the key on Connect your own MCP server, and that the agent links that page and replies. Co-Authored-By: Claude Opus 5.5 --- .../src/__tests__/person-only-page-guidance.test.ts | 13 +++++++++++-- server/src/routes/openapi.ts | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/person-only-page-guidance.test.ts b/server/src/__tests__/person-only-page-guidance.test.ts index 1a9be567acc..c02e6b586e6 100644 --- a/server/src/__tests__/person-only-page-guidance.test.ts +++ b/server/src/__tests__/person-only-page-guidance.test.ts @@ -54,10 +54,10 @@ it("names only pages that the board UI mounts", () => { const appRoutes = readFileSync(path.join(repoRoot, "ui/src/App.tsx"), "utf8"); const pages = new Set( runnerApiCatalog() - .filter(row => /^\/api\/(mcp\/(setup|requests\/\{id\}\/consent)$|connection-intents\/|tool-connections\/\{connectionId\}\/(catalog|review-counts)$|companies\/\{companyId\}\/(tools\/apps\/attention|chat-endpoints)$)/.test(row.path)) + .filter(row => /^\/api\/(mcp\/(setup|requests\/\{id\}\/consent)$|connection-intents\/|tool-connections\/\{connectionId\}\/(catalog|review-counts)$|companies\/\{companyId\}\/(tools\/apps\/attention|tools\/apps\/connect|tools\/connections|chat-endpoints)$)/.test(row.path)) .flatMap(row => [...(row.description ?? "").matchAll(/\/\/([a-z0-9/<>-]+)/gi)].map(match => match[1]!)), ); - expect([...pages].sort()).toEqual(["apps//permissions", "apps/assistant-connection", "company/settings/instance/experimental", "issues/"]); + expect([...pages].sort()).toEqual(["apps//permissions", "apps/assistant-connection", "apps/byo", "company/settings/instance/experimental", "issues/"]); const mounted = (page: string) => page .replace("apps//permissions", "apps/:connectionId/:tab") .replace("issues/", "issues/:issueId"); @@ -65,6 +65,15 @@ it("names only pages that the board UI mounts", () => { expect(readFileSync(path.join(repoRoot, "ui/src/pages/apps/app-tabs.ts"), "utf8")).toContain("\"permissions\""); }); +it.each([ + "connect custom MCP server with server URL and API key BYO connection", + "byo custom mcp server create tool app connection", +])("search %j sends an own-server API key to the secure page", (query) => { + const found = searchRunnerApi({ query, limit: 10 }).results.filter(row => ["POST /api/companies/{companyId}/tools/apps/connect", "POST /api/companies/{companyId}/tools/connections"].includes(row.operationId)); + expect(found.length, query).toBeGreaterThan(0); + for (const operation of found) expect(operation.description).toContain("[Connect your own MCP server](//apps/byo)"); +}); + it("links only mounted pages from connections_search", () => { const appRoutes = readFileSync(path.join(repoRoot, "ui/src/App.tsx"), "utf8"); const search = readFileSync(path.join(repoRoot, "server/src/services/connection-intents.ts"), "utf8"); diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 5bedd837dae..1be9582aea0 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -11667,6 +11667,9 @@ registerCurrentRoute({ "Inspect a curated app's public MCP and OAuth metadata without credentials or registration", }); +const OWN_MCP_SERVER_KEY = "When a person's own MCP server needs an API key, the person adds it themselves on [Connect your own MCP server](//apps/byo): they enter its URL and type the key into that page's secure field. " + + "An agent cannot enter the key and must not ask for it in chat. Link that page and reply instead of creating the connection here."; + registerCurrentRoute({ "x-paperclip-transport": "rest", "x-paperclip-connection-management": "connections:configure", @@ -11674,6 +11677,7 @@ registerCurrentRoute({ path: "/api/companies/{companyId}/tools/apps/connect", tags: ["tool-access"], summary: "Create a draft app connection from gallery input", + description: OWN_MCP_SERVER_KEY, body: connectToolAppSchema, responses: { 200: r.ok(), @@ -11845,7 +11849,7 @@ registerCurrentRoute({ path: "/api/companies/{companyId}/tools/connections", tags: ["tool-access"], summary: "Create a tool connection", - description: "Saves a connection as given without checking that its server answers. To connect an MCP server from its URL, use POST /api/companies/{companyId}/tools/apps/connect with link instead: it checks the server first, and a fresh setup saves nothing when the server cannot be reached.", + description: "Saves a connection as given without checking that its server answers. To connect an MCP server from its URL, use POST /api/companies/{companyId}/tools/apps/connect with link instead: it checks the server first, and a fresh setup saves nothing when the server cannot be reached." + " " + OWN_MCP_SERVER_KEY, body: createToolConnectionSchema, responses: { 201: r.ok(),