diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 5447cf48307..f5d157ab5fb 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -1666,7 +1666,7 @@ function resolveOperationAuthLevel( ): OpenApiAuthLevel { const key = operationKey(method, path); if (key === "GET /api/mcp/requests/{id}") return "public"; - if (path.startsWith("/api/mcp/requests/") || path.startsWith("/api/mcp/connections")) return "board"; + if (path === "/api/mcp/setup" || path.startsWith("/api/mcp/requests/") || path.startsWith("/api/mcp/connections")) return "board"; if (PUBLIC_OPERATIONS.has(key)) return "public"; if (key === "POST /api/mcp/project-tools" || key === "POST /api/companies/{companyId}/slack/tasks/{issueId}/tools") return "agent_run"; if (RUNTIME_TOOLS_OPERATIONS.has(key)) return "runtime_tools"; @@ -11334,6 +11334,16 @@ registerCurrentRoute({ }, }); +registerCurrentRoute({ + method: "get", path: "/api/mcp/setup", tags: ["tool-gateway"], + summary: "Read assistant connection setup for a signed-in person", + description: "Returns the live experimental setting and canonical MCP server URL. Requires a human browser session; agent and board API keys are not accepted. Remains available while the experiment is disabled and grants no access.", + responses: { + 200: r.ok(z.object({ enabled: z.boolean(), serverUrl: z.string().url() })), + 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, + }, +}); + registerCurrentRoute({ method: "get", path: "/api/mcp/requests/{id}", tags: ["tool-gateway"], summary: "Describe an assistant connection request and available sign-in options", diff --git a/ui/src/pages/InstanceExperimentalSettings.test.tsx b/ui/src/pages/InstanceExperimentalSettings.test.tsx index 9e5c0b22c02..88dadbd6b9b 100644 --- a/ui/src/pages/InstanceExperimentalSettings.test.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.test.tsx @@ -1,6 +1,7 @@ // @vitest-environment jsdom import { flushSync } from "react-dom"; +import { MemoryRouter } from "react-router-dom"; import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { INSTANCE_FEATURE_KEYS } from "@paperclipai/shared"; @@ -21,6 +22,10 @@ vi.mock("@/api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi, })); +vi.mock("@/context/CompanyContext", () => ({ + useCompany: () => ({ selectedCompany: { id: "butter", issuePrefix: "BUT" } }), +})); + vi.mock("../context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }), })); @@ -165,7 +170,9 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233) flushSync(() => { root!.render( - + + + , ); }); @@ -242,6 +249,7 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233) await flushReact(); expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenLastCalledWith({ enablePublicMcp: enabled }); expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe(String(enabled)); + expect(Boolean(container.querySelector('a[href="/BUT/apps/assistant-connection"]'))).toBe(enabled); } }); @@ -784,7 +792,9 @@ describe("InstanceExperimentalSettings — cloud-managed keys", () => { flushSync(() => { root!.render( - + + + , ); }); @@ -937,7 +947,9 @@ describe("InstanceExperimentalSettings — card ordering and headings (PAP-393)" flushSync(() => { root!.render( - + + + , ); }); @@ -1052,7 +1064,9 @@ describe("InstanceExperimentalSettings — operator-hidden cards", () => { flushSync(() => { root!.render( - + + + , ); }); diff --git a/ui/src/pages/apps/AssistantConnection.tsx b/ui/src/pages/apps/AssistantConnection.tsx index 06aecaa70bf..49f3c3b394d 100644 --- a/ui/src/pages/apps/AssistantConnection.tsx +++ b/ui/src/pages/apps/AssistantConnection.tsx @@ -6,6 +6,7 @@ import { ApiError } from "@/api/client"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { Link } from "@/lib/router"; +import { copyTextToClipboard } from "@/lib/clipboard"; import { CompanyPatternIcon } from "@/components/CompanyPatternIcon"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -60,7 +61,7 @@ function CopyValue({ value, label }: { value: string; label: string }) {
{value}