import { randomUUID } from "node:crypto";
import {
  LINKED_IDP_SSO_MODE,
  MCP_APPS_EXTENSION_ID,
  MCP_CATALOG_INSTALL_PATH,
  MCP_CATALOG_REAUTH_QUERY_PARAM,
  MCP_CATALOG_SERVER_QUERY_PARAM,
  MCP_ENTERPRISE_AUTH_EXTENSION_ID,
  OAUTH_TOKEN_TYPE,
} from "@archestra/shared";
import { eq } from "drizzle-orm";
import { vi } from "vitest";
import config from "@/config";
import db, { schema } from "@/database";
import {
  AgentModel,
  AgentToolModel,
  AppToolModel,
  EnvironmentModel,
  InternalMcpCatalogModel,
  McpHttpSessionModel,
  McpServerModel,
  ToolModel,
} from "@/models";
import * as oauthRoutes from "@/routes/oauth";
import { secretManager } from "@/secrets-manager";
import { beforeEach, describe, expect, test } from "@/test";
import { agentOwner, appOwner } from "@/types";
import mcpClient, { type TokenAuthContext } from "./mcp-client";

// Mock the MCP SDK
const mockCallTool = vi.fn();
const mockConnect = vi.fn();
const mockClose = vi.fn();
const mockListTools = vi.fn();
const mockListResources = vi.fn();
const mockReadResource = vi.fn();
const mockPing = vi.fn();
const mockSetRequestHandler = vi.fn();
const mockSetNotificationHandler = vi.fn();

vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
  // biome-ignore lint/suspicious/noExplicitAny: test..
  Client: vi.fn(function (this: any) {
    this.connect = mockConnect;
    this.callTool = mockCallTool;
    this.close = mockClose;
    this.listTools = mockListTools;
    this.listResources = mockListResources;
    this.readResource = mockReadResource;
    this.ping = mockPing;
    this.setRequestHandler = mockSetRequestHandler;
    this.setNotificationHandler = mockSetNotificationHandler;
  }),
}));

vi.mock(
  "@modelcontextprotocol/sdk/client/streamableHttp.js",
  async (importOriginal) => {
    const actual =
      await importOriginal<
        typeof import("@modelcontextprotocol/sdk/client/streamableHttp.js")
      >();
    return {
      ...actual,
      StreamableHTTPClientTransport: vi.fn(),
    };
  },
);

// Mock McpServerRuntimeManager - use vi.hoisted to avoid initialization errors
const {
  mockUsesStreamableHttp,
  mockGetHttpEndpointUrl,
  mockGetRunningPodHttpEndpoint,
  mockGetOrLoadDeployment,
} = vi.hoisted(() => ({
  mockUsesStreamableHttp: vi.fn(),
  mockGetHttpEndpointUrl: vi.fn(),
  mockGetRunningPodHttpEndpoint: vi.fn(),
  mockGetOrLoadDeployment: vi.fn(),
}));

vi.mock("@/k8s/mcp-server-runtime", () => ({
  McpServerRuntimeManager: {
    usesStreamableHttp: mockUsesStreamableHttp,
    getHttpEndpointUrl: mockGetHttpEndpointUrl,
    getRunningPodHttpEndpoint: mockGetRunningPodHttpEndpoint,
    getOrLoadDeployment: mockGetOrLoadDeployment,
  },
}));

describe("McpClient", () => {
  let agentId: string;
  let mcpServerId: string;
  let catalogId: string;

  beforeEach(async () => {
    await mcpClient.disconnectAll();

    // Create test agent
    const agent = await AgentModel.create({
      name: "Test Agent",
      scope: "org",
      teams: [],
    });
    agentId = agent.id;

    // Create secret with access token
    const secret = await secretManager().createSecret(
      { access_token: "test-github-token-123" },
      "testmcptoken",
    );

    // Create catalog entry for the MCP server
    const catalogItem = await InternalMcpCatalogModel.create({
      name: "github-mcp-server",
      serverType: "remote",
      serverUrl: "https://api.githubcopilot.com/mcp/",
    });
    catalogId = catalogItem.id;

    // Create MCP server for testing with secret and catalog reference
    const mcpServer = await McpServerModel.create({
      name: "github-mcp-server",
      secretId: secret.id,
      catalogId: catalogItem.id,
      serverType: "remote",
    });
    mcpServerId = mcpServer.id;

    // Reset all mocks
    vi.clearAllMocks();
    mockCallTool.mockReset();
    mockConnect.mockReset();
    mockClose.mockReset();
    mockListTools.mockReset();
    mockListResources.mockReset();
    mockReadResource.mockReset();
    mockPing.mockReset();
    mockSetRequestHandler.mockReset();
    mockSetNotificationHandler.mockReset();
    mockUsesStreamableHttp.mockReset();
    mockGetHttpEndpointUrl.mockReset();
    mockGetRunningPodHttpEndpoint.mockReset();
    mockGetOrLoadDeployment.mockReset();

    // Spy on McpHttpSessionModel to prevent real DB writes during mcp-client tests
    // and to avoid errors from session persistence in the background
    vi.spyOn(
      McpHttpSessionModel,
      "findRecordByConnectionKey",
    ).mockResolvedValue(null);
    vi.spyOn(McpHttpSessionModel, "upsert").mockResolvedValue(undefined);
    vi.spyOn(McpHttpSessionModel, "deleteByConnectionKey").mockResolvedValue(
      undefined,
    );
    vi.spyOn(McpHttpSessionModel, "deleteStaleSession").mockResolvedValue(
      undefined,
    );
    vi.spyOn(McpHttpSessionModel, "deleteExpired").mockResolvedValue(0);

    // Default: listTools returns empty list (fallback to stripped name)
    mockListTools.mockResolvedValue({ tools: [] });
    mockListResources.mockResolvedValue({ resources: [] });
  });

  test("invalidateConnectionsForServer closes cached active connections for the server", async () => {
    const tool = await ToolModel.createToolIfNotExists({
      name: "github-mcp-server__list_repos",
      description: "List repos",
      parameters: {},
      catalogId,
    });

    await AgentToolModel.create(agentId, tool.id, {
      mcpServerId,
    });

    mockCallTool.mockResolvedValue({
      content: [{ type: "text", text: "ok" }],
      isError: false,
    });

    await mcpClient.executeToolCallForOwner(
      {
        id: "call_invalidate_connection",
        name: "github-mcp-server__list_repos",
        arguments: {},
      },
      agentOwner(agentId),
    );

    expect(mockConnect).toHaveBeenCalledTimes(1);

    await mcpClient.invalidateConnectionsForServer(mcpServerId);

    expect(mockClose).toHaveBeenCalled();
    expect(McpHttpSessionModel.deleteStaleSession).toHaveBeenCalled();

    mockConnect.mockClear();

    await mcpClient.executeToolCallForOwner(
      {
        id: "call_invalidate_connection_after",
        name: "github-mcp-server__list_repos",
        arguments: {},
      },
      agentOwner(agentId),
    );

    expect(mockConnect).toHaveBeenCalledTimes(1);
  });

  test("forwards the abort signal to client.callTool and listTools", async () => {
    const tool = await ToolModel.createToolIfNotExists({
      name: "github-mcp-server__list_repos",
      description: "List repos",
      parameters: {},
      catalogId,
    });
    await AgentToolModel.create(agentId, tool.id, { mcpServerId });

    mockCallTool.mockResolvedValue({
      content: [{ type: "text", text: "ok" }],
      isError: false,
    });

    const controller = new AbortController();
    await mcpClient.executeToolCallForOwner(
      {
        id: "call_abort_forward",
        name: "github-mcp-server__list_repos",
        arguments: { owner: "octocat" },
      },
      agentOwner(agentId),
      undefined,
      { abortSignal: controller.signal },
    );

    expect(mockCallTool).toHaveBeenCalledWith(
      { name: "list_repos", arguments: { owner: "octocat" } },
      undefined,
      { signal: controller.signal },
    );
    // Name resolution (listTools) is on the same cancellable path.
    expect(mockListTools).toHaveBeenCalledWith(undefined, {
      signal: controller.signal,
    });
  });

  test("rethrows an aborted call instead of retrying it", async () => {
    const tool = await ToolModel.createToolIfNotExists({
      name: "github-mcp-server__list_repos",
      description: "List repos",
      parameters: {},
      catalogId,
    });
    await AgentToolModel.create(agentId, tool.id, { mcpServerId });

    // A stale-session error normally triggers a fresh-session retry. With the
    // signal aborted it must short-circuit instead — no second callTool, no
    // session teardown, and the error propagates rather than becoming a result.
    mockCallTool.mockRejectedValue(new Error("Session not found"));

    const controller = new AbortController();
    controller.abort();

    await expect(
      mcpClient.executeToolCallForOwner(
        {
          id: "call_abort_no_retry",
          name: "github-mcp-server__list_repos",
          arguments: {},
        },
        agentOwner(agentId),
        undefined,
        { abortSignal: controller.signal },
      ),
    ).rejects.toThrow();

    expect(mockCallTool).toHaveBeenCalledTimes(1);
    expect(mockClose).not.toHaveBeenCalled();
  });

  test("returns an error result (does not throw) for a non-abort failure", async () => {
    const tool = await ToolModel.createToolIfNotExists({
      name: "github-mcp-server__list_repos",
      description: "List repos",
      parameters: {},
      catalogId,
    });
    await AgentToolModel.create(agentId, tool.id, { mcpServerId });

    mockCallTool.mockRejectedValue(new Error("upstream exploded"));

    const result = await mcpClient.executeToolCallForOwner(
      {
        id: "call_non_abort_failure",
        name: "github-mcp-server__list_repos",
        arguments: {},
      },
      agentOwner(agentId),
    );

    expect(result.isError).toBe(true);
    expect(mockCallTool).toHaveBeenCalledTimes(1);
  });

  test("connectAndGetTools synthesizes read-resource tools when upstream has no tools/list", async () => {
    mockListTools.mockRejectedValueOnce(new Error("Method not found"));
    mockListResources.mockResolvedValueOnce({
      resources: [
        {
          uri: "todo://todos",
          name: "Todos",
          description: "Read todos",
        },
      ],
    });

    const catalogItem = await InternalMcpCatalogModel.findById(catalogId);
    if (!catalogItem) throw new Error("expected catalog item");

    const tools = await mcpClient.connectAndGetTools({
      catalogItem,
      mcpServerId,
      secrets: { access_token: "resource-token" },
    });

    expect(tools).toEqual([
      {
        name: "read_resource_todos",
        description: "Read todos",
        inputSchema: {
          type: "object",
          properties: {},
          additionalProperties: false,
        },
        _meta: {
          archestraResourceUri: "todo://todos",
        },
        annotations: undefined,
      },
    ]);
    expect(mockListResources).toHaveBeenCalledTimes(1);
  });

  test("connectAndGetTools treats JSON-RPC method-not-found code as resource-only discovery", async () => {
    mockListTools.mockRejectedValueOnce({ code: -32601 });
    mockListResources.mockResolvedValueOnce({
      resources: [
        {
          uri: "todo://todos",
          name: "Todos",
        },
      ],
    });

    const catalogItem = await InternalMcpCatalogModel.findById(catalogId);
    if (!catalogItem) throw new Error("expected catalog item");

    const tools = await mcpClient.connectAndGetTools({
      catalogItem,
      mcpServerId,
      secrets: { access_token: "resource-token" },
    });

    expect(tools).toHaveLength(1);
    expect(tools[0].name).toBe("read_resource_todos");
    expect(mockListResources).toHaveBeenCalledTimes(1);
  });

  test("connectAndGetTools closes the client when tool discovery fails", async () => {
    // Non-method-not-found error: discovery rethrows instead of falling back
    mockListTools.mockRejectedValueOnce(new Error("tools/list exploded"));

    const catalogItem = await InternalMcpCatalogModel.findById(catalogId);
    if (!catalogItem) throw new Error("expected catalog item");

    await expect(
      mcpClient.connectAndGetTools({
        catalogItem,
        mcpServerId,
        secrets: { access_token: "resource-token" },
      }),
    ).rejects.toThrow("Failed to connect to MCP server");

    // The failed attempt must not leak its client/transport
    expect(mockClose).toHaveBeenCalledTimes(1);
  });

  describe("executeToolCallForOwner (app owner)", () => {
    test("executes an app-assigned tool and persists an app-owned audit row", async ({
      makeApp,
    }) => {
      const app = await makeApp();
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__app_list",
        description: "List",
        parameters: {},
        catalogId,
      });
      await AppToolModel.create(app.id, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
        isError: false,
      });

      const result = await mcpClient.executeToolCallForOwner(
        { id: "call_app_1", name: tool.name, arguments: {} },
        appOwner(app.id),
      );
      expect(result.isError).toBe(false);

      const [row] = await db
        .select()
        .from(schema.mcpToolCallsTable)
        .where(eq(schema.mcpToolCallsTable.appId, app.id));
      expect(row?.ownerType).toBe("app");
      expect(row?.appId).toBe(app.id);
      expect(row?.agentId).toBeNull();
    });

    test("resolves an app tool called by its unprefixed suffix", async ({
      makeApp,
    }) => {
      const app = await makeApp();
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__refresh_stats",
        description: "Refresh",
        parameters: {},
        catalogId,
      });
      await AppToolModel.create(app.id, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
        isError: false,
      });

      // Third-party hosts call oncalltool with the raw (unprefixed) tool name.
      const result = await mcpClient.executeToolCallForOwner(
        { id: "call_app_suffix", name: "refresh_stats", arguments: {} },
        appOwner(app.id),
      );
      expect(result.isError).toBe(false);
    });

    test("fails closed for a tool the app was never assigned", async ({
      makeApp,
    }) => {
      const app = await makeApp();
      const result = await mcpClient.executeToolCallForOwner(
        {
          id: "call_app_unknown",
          name: "github-mcp-server__nope",
          arguments: {},
        },
        appOwner(app.id),
      );
      expect(result.isError).toBe(true);
      expect(
        (result._meta as { archestraError?: { code?: string } } | undefined)
          ?.archestraError?.code,
      ).toBe("unknown_tool");

      const [row] = await db
        .select()
        .from(schema.mcpToolCallsTable)
        .where(eq(schema.mcpToolCallsTable.appId, app.id));
      expect(row?.ownerType).toBe("app");
      expect(row?.agentId).toBeNull();
    });
  });

  describe("executeToolCallForOwner (app backing launch tool)", () => {
    test("audits the open launch and still returns the ui:// pointer", async () => {
      const appCatalog = await InternalMcpCatalogModel.create({
        name: "Clock App",
        serverType: "app",
        scope: "org",
      });
      const appServer = await McpServerModel.create({
        name: "Clock App",
        catalogId: appCatalog.id,
        serverType: "app",
      });
      const uri = "ui://archestra-app/clock-app";
      const openTool = await ToolModel.create({
        name: "clock_app__open",
        description: "Open the Clock App.",
        parameters: { type: "object", properties: {} },
        catalogId: appCatalog.id,
        meta: { _meta: { ui: { resourceUri: uri } } },
      });
      await AgentToolModel.create(agentId, openTool.id, {
        mcpServerId: appServer.id,
      });

      const result = await mcpClient.executeToolCallForOwner(
        { id: "call_open", name: openTool.name, arguments: {} },
        agentOwner(agentId),
      );

      // The in-process short-circuit still hands the host the ui:// pointer...
      expect(result.isError).toBe(false);
      expect(
        (result._meta as { ui?: { resourceUri?: string } } | undefined)?.ui
          ?.resourceUri,
      ).toBe(uri);

      // ...and now records an audit row like any other gateway tool call.
      const [row] = await db
        .select()
        .from(schema.mcpToolCallsTable)
        .where(eq(schema.mcpToolCallsTable.agentId, agentId));
      expect(row).toBeDefined();
      expect(row?.ownerType).toBe("agent");
      expect(row?.mcpServerName).toBe("Clock App");
      expect((row?.toolCall as { name?: string } | null)?.name).toBe(
        openTool.name,
      );
    });
  });

  describe("resolveUiAppInstallIdForCaller", () => {
    const callerAuth = (
      userId: string,
      organizationId: string,
    ): TokenAuthContext => ({
      tokenId: "t",
      teamId: null,
      isOrganizationToken: false,
      isUserToken: true,
      organizationId,
      userId,
    });

    test("returns the caller's install for an external UI resource", async ({
      makeAgent,
      makeAgentTool,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeMember,
      makeOrganization,
      makeTool,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      await makeMember(user.id, org.id, { role: "admin" });
      const agent = await makeAgent({ organizationId: org.id });
      const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
      const install = await makeMcpServer({
        catalogId: catalog.id,
        scope: "org",
      });
      const uri = "ui://excalidraw/mcp-app.html";
      const tool = await makeTool({
        name: "excalidraw__create_view",
        catalogId: catalog.id,
        meta: { _meta: { ui: { resourceUri: uri } } },
      });
      await makeAgentTool(agent.id, tool.id);

      const resolved = await mcpClient.resolveUiAppInstallIdForCaller(
        uri,
        agent.id,
        callerAuth(user.id, org.id),
      );

      expect(resolved).toBe(install.id);
    });

    test("returns null for an owned-app backing (renders by app id via render_app)", async ({
      makeAgent,
      makeAgentTool,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeMember,
      makeOrganization,
      makeTool,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      await makeMember(user.id, org.id, { role: "admin" });
      const agent = await makeAgent({ organizationId: org.id });
      const catalog = await makeInternalMcpCatalog({
        organizationId: org.id,
        serverType: "app",
      });
      await makeMcpServer({ catalogId: catalog.id, scope: "org" });
      const uri = "ui://archestra-app/app-1";
      const tool = await makeTool({
        name: "my_app-abc123__open",
        catalogId: catalog.id,
        meta: { _meta: { ui: { resourceUri: uri } } },
      });
      await makeAgentTool(agent.id, tool.id);

      const resolved = await mcpClient.resolveUiAppInstallIdForCaller(
        uri,
        agent.id,
        callerAuth(user.id, org.id),
      );

      expect(resolved).toBeNull();
    });

    test("returns null when no tool declares the resource", async ({
      makeAgent,
      makeMember,
      makeOrganization,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      await makeMember(user.id, org.id, { role: "admin" });
      const agent = await makeAgent({ organizationId: org.id });

      const resolved = await mcpClient.resolveUiAppInstallIdForCaller(
        "ui://nonexistent/app.html",
        agent.id,
        callerAuth(user.id, org.id),
      );

      expect(resolved).toBeNull();
    });

    test("binds each caller to their own install when the catalog has a per-user install for several users", async ({
      makeAgent,
      makeAgentTool,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeMember,
      makeOrganization,
      makeTool,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const userA = await makeUser();
      const userB = await makeUser();
      await makeMember(userA.id, org.id, { role: "member" });
      await makeMember(userB.id, org.id, { role: "member" });
      const agent = await makeAgent({ organizationId: org.id });
      const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
      // Per-user credentials: each user connects their own install of the same
      // catalog, so the catalog has more than one install.
      const installA = await makeMcpServer({
        catalogId: catalog.id,
        ownerId: userA.id,
        scope: "personal",
      });
      const installB = await makeMcpServer({
        catalogId: catalog.id,
        ownerId: userB.id,
        scope: "personal",
      });
      const uri = "ui://excalidraw/mcp-app.html";
      const tool = await makeTool({
        name: "excalidraw__create_view",
        catalogId: catalog.id,
        meta: { _meta: { ui: { resourceUri: uri } } },
      });
      await makeAgentTool(agent.id, tool.id);

      // Each caller's render binds to their OWN install — interactive for both,
      // never the other user's install, never null.
      expect(
        await mcpClient.resolveUiAppInstallIdForCaller(
          uri,
          agent.id,
          callerAuth(userA.id, org.id),
        ),
      ).toBe(installA.id);
      expect(
        await mcpClient.resolveUiAppInstallIdForCaller(
          uri,
          agent.id,
          callerAuth(userB.id, org.id),
        ),
      ).toBe(installB.id);
    });

    test("binds every caller to the catalog's service-account pin over their own install", async ({
      makeAgent,
      makeAgentTool,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeMember,
      makeOrganization,
      makeTool,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      await makeMember(user.id, org.id, { role: "member" });
      const agent = await makeAgent({ organizationId: org.id });
      const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
      await makeMcpServer({
        catalogId: catalog.id,
        ownerId: user.id,
        scope: "personal",
      });
      const pinned = await makeMcpServer({
        catalogId: catalog.id,
        scope: "org",
      });
      // A service-account pin routes every caller's runtime through this one
      // install, regardless of their own→team→org resolution.
      await db
        .update(schema.internalMcpCatalogTable)
        .set({ dynamicConnectionMcpServerId: pinned.id })
        .where(eq(schema.internalMcpCatalogTable.id, catalog.id));
      const uri = "ui://excalidraw/mcp-app.html";
      const tool = await makeTool({
        name: "excalidraw__create_view",
        catalogId: catalog.id,
        meta: { _meta: { ui: { resourceUri: uri } } },
      });
      await makeAgentTool(agent.id, tool.id);

      // The callback binds to the pinned install run_tool executes against — not
      // the caller's own personal install.
      expect(
        await mcpClient.resolveUiAppInstallIdForCaller(
          uri,
          agent.id,
          callerAuth(user.id, org.id),
        ),
      ).toBe(pinned.id);
    });

    test("declines to bind an enterprise-managed catalog with more than one install", async ({
      makeAgent,
      makeAgentTool,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeMember,
      makeOrganization,
      makeTool,
      makeUser,
    }) => {
      const org = await makeOrganization();
      const user = await makeUser();
      await makeMember(user.id, org.id, { role: "member" });
      const agent = await makeAgent({ organizationId: org.id });
      const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
      await makeMcpServer({
        catalogId: catalog.id,
        ownerId: user.id,
        scope: "personal",
      });
      await makeMcpServer({ catalogId: catalog.id, scope: "org" });
      // Enterprise-managed credentials resolve the runtime install by their own
      // mechanism (which run_tool honors in all-tools mode), so the own→team→org
      // resolution here could pick a different install than execution uses.
      await db
        .update(schema.internalMcpCatalogTable)
        .set({ enterpriseManagedConfig: {} })
        .where(eq(schema.internalMcpCatalogTable.id, catalog.id));
      const uri = "ui://excalidraw/mcp-app.html";
      const tool = await makeTool({
        name: "excalidraw__create_view",
        catalogId: catalog.id,
        meta: { _meta: { ui: { resourceUri: uri } } },
      });
      await makeAgentTool(agent.id, tool.id);

      expect(
        await mcpClient.resolveUiAppInstallIdForCaller(
          uri,
          agent.id,
          callerAuth(user.id, org.id),
        ),
      ).toBeNull();
    });
  });

  describe("executeToolCallForOwner", () => {
    test("returns error when tool not found for agent", async () => {
      const toolCall = {
        id: "call_123",
        name: "non_mcp_tool",
        arguments: { param: "value" },
      };

      const result = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(result).toMatchObject({
        id: "call_123",
        isError: true,
        error: expect.stringContaining("No tool named"),
      });
      expect(result.error).toContain("Do not guess tool names");
      expect(
        (result._meta as { archestraError?: { code?: string } } | undefined)
          ?.archestraError?.code,
      ).toBe("unknown_tool");
    });

    test("a retained static assignment whose connection was uninstalled returns a typed reconnect error", async () => {
      // A catalog with no installed connection (the pinned install was removed).
      const orphanCatalog = await InternalMcpCatalogModel.create({
        name: "orphan-mcp-server",
        serverType: "remote",
        serverUrl: "https://example.invalid/mcp/",
      });
      const tool = await ToolModel.createToolIfNotExists({
        name: "orphan-mcp-server__do_thing",
        description: "needs a connection",
        parameters: {},
        catalogId: orphanCatalog.id,
      });
      // Retained assignment after uninstall: still static, but the server
      // binding is null and the catalog has no install.
      await AgentToolModel.create(agentId, tool.id, {
        mcpServerId: null,
        credentialResolutionMode: "static",
      });

      const result = await mcpClient.executeToolCallForOwner(
        { id: "call_orphan", name: tool.name, arguments: {} },
        agentOwner(agentId),
      );

      expect(result.isError).toBe(true);
      expect(
        (result._meta as { archestraError?: { type?: string } } | undefined)
          ?.archestraError?.type,
      ).toBe("auth_required");
      // Reconnect-framed wording (not the "no credentials" auth message).
      expect(result.error).toContain("reconnect");
      expect(result.error).toContain("is not connected");
      // The call is refused before any dispatch is attempted.
      expect(mockCallTool).not.toHaveBeenCalled();
    });

    test("declares MCP Apps and enterprise auth extensions during initialize", async () => {
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__declared_extensions",
        description: "Extension declaration test",
        parameters: {},
        catalogId,
      });

      await AgentToolModel.create(agentId, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
      });

      const result = await mcpClient.executeToolCallForOwner(
        {
          id: "call_extensions",
          name: tool.name,
          arguments: {},
        },
        agentOwner(agentId),
      );

      expect(result.isError).toBe(false);

      const clientConstructor = vi.mocked(
        (await import("@modelcontextprotocol/sdk/client/index.js")).Client,
      );
      expect(clientConstructor).toHaveBeenCalled();
      const options = clientConstructor.mock.calls.at(-1)?.[1] as
        | {
            capabilities?: {
              elicitation?: Record<string, unknown>;
              extensions?: Record<string, unknown>;
            };
          }
        | undefined;
      expect(options?.capabilities?.extensions).toEqual({
        [MCP_APPS_EXTENSION_ID]: {
          mimeTypes: ["text/html;profile=mcp-app"],
        },
        [MCP_ENTERPRISE_AUTH_EXTENSION_ID]: {},
      });
      expect(options?.capabilities?.elicitation).toBeUndefined();
      expect(mockSetRequestHandler).not.toHaveBeenCalled();
      expect(mockSetNotificationHandler).not.toHaveBeenCalled();
    });

    test("declares elicitation support when a gateway bridge handler is provided", async () => {
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__elicitation_bridge",
        description: "Elicitation bridge test",
        parameters: {},
        catalogId,
      });

      await AgentToolModel.create(agentId, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
      });

      const result = await mcpClient.executeToolCallForOwner(
        {
          id: "call_elicitation",
          name: tool.name,
          arguments: {},
        },
        agentOwner(agentId),
        undefined,
        {
          elicitationHandler: async () => ({
            action: "accept",
            content: {},
          }),
        },
      );

      expect(result.isError).toBe(false);

      const clientConstructor = vi.mocked(
        (await import("@modelcontextprotocol/sdk/client/index.js")).Client,
      );
      const options = clientConstructor.mock.calls.at(-1)?.[1] as
        | {
            capabilities?: {
              elicitation?: Record<string, unknown>;
            };
          }
        | undefined;
      expect(options?.capabilities?.elicitation).toEqual({
        form: { applyDefaults: true },
        url: {},
      });
      expect(mockSetRequestHandler).toHaveBeenCalledOnce();
      expect(mockSetNotificationHandler).toHaveBeenCalledOnce();
    });

    describe("Secrets caching (N+1 prevention)", () => {
      test("caches secret lookups across consecutive tool calls to same server", async () => {
        // Create two tools assigned to the same MCP server (same catalog)
        const tool1 = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__tool_a",
          description: "Tool A",
          parameters: {},
          catalogId,
        });
        const tool2 = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__tool_b",
          description: "Tool B",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool1.id, {
          mcpServerId: mcpServerId,
        });
        await AgentToolModel.create(agentId, tool2.id, {
          mcpServerId: mcpServerId,
        });

        mockCallTool
          .mockResolvedValueOnce({
            content: [{ type: "text", text: "Result A" }],
            isError: false,
          })
          .mockResolvedValueOnce({
            content: [{ type: "text", text: "Result B" }],
            isError: false,
          });

        // Spy on secretManager to count calls
        const getSecretSpy = vi.spyOn(secretManager(), "getSecret");

        const resultA = await mcpClient.executeToolCallForOwner(
          { id: "call_a", name: "github-mcp-server__tool_a", arguments: {} },
          agentOwner(agentId),
        );
        const resultB = await mcpClient.executeToolCallForOwner(
          { id: "call_b", name: "github-mcp-server__tool_b", arguments: {} },
          agentOwner(agentId),
        );

        expect(resultA.isError).toBe(false);
        expect(resultB.isError).toBe(false);

        // Secret should only be fetched once due to caching
        expect(getSecretSpy).toHaveBeenCalledTimes(1);

        getSecretSpy.mockRestore();
      });

      test("reloads cached secrets after server credentials are re-authenticated", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__rotated_secret",
          description: "Rotated secret tool",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        const getSecretSpy = vi.spyOn(secretManager(), "getSecret");
        const toolCall = {
          id: "call_rotated_secret",
          name: tool.name,
          arguments: {},
        };

        const firstResult = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );
        expect(firstResult.isError).toBe(false);
        expect(getSecretSpy).toHaveBeenCalledTimes(1);

        const rotatedSecret = await secretManager().createSecret(
          { access_token: "fresh-github-token-456" },
          "rotatedmcptoken",
        );
        await McpServerModel.update(mcpServerId, {
          secretId: rotatedSecret.id,
        });

        const secondResult = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );
        expect(secondResult.isError).toBe(false);
        expect(getSecretSpy).toHaveBeenCalledTimes(2);

        getSecretSpy.mockRestore();
      });

      test("resolves and executes a tool without the heavy server-detail lookup", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__lightweight_resolution",
          description: "Lightweight resolution tool",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        // findById() performs a 4-table join plus a per-server mcp_server_user
        // lookup; the tool-execution hot path must not pay that cost since it
        // only needs base columns (name/secretId). Resolving with the heavier
        // query is what produced the N+1 under repeated tool calls.
        const findByIdSpy = vi.spyOn(McpServerModel, "findById");

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_lightweight", name: tool.name, arguments: {} },
          agentOwner(agentId),
        );

        expect(result.isError).toBe(false);
        expect(findByIdSpy).not.toHaveBeenCalled();

        findByIdSpy.mockRestore();
      });
    });

    // The catalog item defines how agents connect when credentials resolve at
    // call time: NULL = the caller's own connection, falling back to a team or
    // org connection it can access; a pinned mcp_servers.id = a service account
    // every call uses regardless of the caller.
    describe("agent connections (catalog dynamic-connection policy)", () => {
      async function makeDynamicCatalogTool() {
        const catalogItem = await InternalMcpCatalogModel.create({
          name: `connected-server-${randomUUID().slice(0, 8)}`,
          serverType: "remote",
          serverUrl: "https://example.com/mcp",
        });
        const tool = await ToolModel.createToolIfNotExists({
          name: `${catalogItem.name}__do_thing`,
          description: "Connection-policy tool",
          parameters: {},
          catalogId: catalogItem.id,
        });
        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "dynamic",
        });
        return { catalogItem, tool };
      }

      function userToken(userId: string, organizationId: string) {
        return {
          tokenId: "tok-user",
          teamId: null,
          isOrganizationToken: false,
          isUserToken: true,
          userId,
          organizationId,
        };
      }

      test("resolve at call time falls back to a team connection the user can access", async ({
        makeMember,
        makeOrganization,
        makeTeam,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const user = await makeUser();
        await makeMember(user.id, org.id, { role: "member" });
        const team = await makeTeam(org.id, user.id);
        const { TeamModel } = await import("@/models");
        await TeamModel.addMember(team.id, user.id, "member");

        const { catalogItem, tool } = await makeDynamicCatalogTool();
        // The user has not connected their own account, but a connection for a
        // team they belong to exists — resolution falls back to it.
        await McpServerModel.create({
          name: `${catalogItem.name}-team`,
          catalogId: catalogItem.id,
          serverType: "remote",
          teamId: team.id,
        });
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "via team connection" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_conn", name: tool.name, arguments: {} },
          agentOwner(agentId),
          userToken(user.id, org.id),
        );

        expect(result.isError).toBe(false);
        expect(mockCallTool).toHaveBeenCalledTimes(1);
      });

      test("resolve at call time uses the caller's own connection", async ({
        makeMember,
        makeOrganization,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const user = await makeUser();
        await makeMember(user.id, org.id, { role: "member" });

        const { catalogItem, tool } = await makeDynamicCatalogTool();
        await McpServerModel.create({
          name: `${catalogItem.name}-personal`,
          catalogId: catalogItem.id,
          serverType: "remote",
          ownerId: user.id,
        });
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "via own connection" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_conn", name: tool.name, arguments: {} },
          agentOwner(agentId),
          userToken(user.id, org.id),
        );

        expect(result.isError).toBe(false);
        expect(mockCallTool).toHaveBeenCalledTimes(1);
      });

      test("a pinned service-account connection is used for any caller", async ({
        makeMember,
        makeOrganization,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const user = await makeUser();
        await makeMember(user.id, org.id, { role: "member" });

        const { catalogItem, tool } = await makeDynamicCatalogTool();
        const serviceAccount = await McpServerModel.create({
          name: `${catalogItem.name}-org`,
          catalogId: catalogItem.id,
          serverType: "remote",
          scope: "org",
        });
        await InternalMcpCatalogModel.update(catalogItem.id, {
          dynamicConnectionMcpServerId: serviceAccount.id,
        });
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "via service account" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_conn", name: tool.name, arguments: {} },
          agentOwner(agentId),
          userToken(user.id, org.id),
        );

        expect(result.isError).toBe(false);
        expect(mockCallTool).toHaveBeenCalledTimes(1);
      });

      test("a revoked pinned connection degrades to resolve at call time", async ({
        makeMember,
        makeOrganization,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const user = await makeUser();
        await makeMember(user.id, org.id, { role: "member" });

        const { catalogItem, tool } = await makeDynamicCatalogTool();
        // Pin points at a connection that no longer exists; the caller's own
        // connection takes over.
        await InternalMcpCatalogModel.update(catalogItem.id, {
          dynamicConnectionMcpServerId: randomUUID(),
        });
        await McpServerModel.create({
          name: `${catalogItem.name}-personal`,
          catalogId: catalogItem.id,
          serverType: "remote",
          ownerId: user.id,
        });
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "via own connection" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_conn", name: tool.name, arguments: {} },
          agentOwner(agentId),
          userToken(user.id, org.id),
        );

        expect(result.isError).toBe(false);
        expect(mockCallTool).toHaveBeenCalledTimes(1);
      });

      test("no self-service install link when the tool's catalog item is another user's personal server", async ({
        makeMember,
        makeOrganization,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const owner = await makeUser();
        await makeMember(owner.id, org.id, { role: "member" });
        const caller = await makeUser();
        await makeMember(caller.id, org.id, { role: "member" });

        // A personal-scope catalog item owned by `owner`, invisible to `caller`.
        const catalogItem = await InternalMcpCatalogModel.create(
          {
            name: `personal-${randomUUID().slice(0, 8)}`,
            serverType: "remote",
            serverUrl: "https://example.com/mcp",
            scope: "personal",
          },
          { organizationId: org.id, authorId: owner.id },
        );
        const tool = await ToolModel.createToolIfNotExists({
          name: `${catalogItem.name}__do_thing`,
          description: "Personal-server tool",
          parameters: {},
          catalogId: catalogItem.id,
        });
        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "dynamic",
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_deadend", name: tool.name, arguments: {} },
          agentOwner(agentId),
          userToken(caller.id, org.id),
        );

        expect(result.isError).toBe(true);
        const archestraError = result?._meta?.archestraError as
          | { type?: string; action?: string; actionUrl?: string }
          | undefined;
        expect(archestraError?.type).toBe("auth_required");
        // The caller cannot install another user's personal item, so no
        // self-service install link is offered.
        expect(archestraError?.actionUrl).toBeUndefined();
        expect(archestraError?.action).toBeUndefined();
        expect(result?.error).not.toContain("/mcp/registry?install=");
        expect(result?.error).not.toMatch(/visit[^.]*https?:\/\//i);
        // ...and it names a remediation the caller can actually pursue.
        expect(result?.error).toMatch(/owner|administrator|share/i);
      });

      test("still offers the install link when the caller can access the catalog (org-scoped, no install yet)", async ({
        makeMember,
        makeOrganization,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const caller = await makeUser();
        await makeMember(caller.id, org.id, { role: "member" });

        // An org-scoped catalog item the caller CAN see and install; with no
        // install yet, the self-service install link is legitimate and MUST be
        // preserved even though the caller-access check runs (orgId present).
        const catalogItem = await InternalMcpCatalogModel.create(
          {
            name: `org-${randomUUID().slice(0, 8)}`,
            serverType: "remote",
            serverUrl: "https://example.com/mcp",
            scope: "org",
          },
          { organizationId: org.id },
        );
        const tool = await ToolModel.createToolIfNotExists({
          name: `${catalogItem.name}__do_thing`,
          description: "Org-server tool",
          parameters: {},
          catalogId: catalogItem.id,
        });
        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "dynamic",
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_install", name: tool.name, arguments: {} },
          agentOwner(agentId),
          userToken(caller.id, org.id),
        );

        expect(result.isError).toBe(true);
        const archestraError = result?._meta?.archestraError as
          | { type?: string; action?: string; actionUrl?: string }
          | undefined;
        expect(archestraError?.type).toBe("auth_required");
        expect(archestraError?.action).toBe("install_mcp_credentials");
        expect(archestraError?.actionUrl).toContain(
          `/mcp/registry?install=${catalogItem.id}`,
        );
      });

      test("still offers the install link for a team-token caller (fail-open: no user identity to check accessibility against)", async ({
        makeMember,
        makeOrganization,
        makeTeam,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const owner = await makeUser();
        await makeMember(owner.id, org.id, { role: "member" });
        const team = await makeTeam(org.id, owner.id);

        // A personal-scope catalog item owned by `owner`. A team token has no
        // user identity, so accessibility cannot be evaluated for it; the
        // fail-open MUST keep offering the install link (the caller behind the
        // token may still be able to act on it). If accessibility were ever
        // computed for team tokens, this personal item would be inaccessible
        // and the link would be dropped — so this pin guards the fail-open.
        const catalogItem = await InternalMcpCatalogModel.create(
          {
            name: `personal-${randomUUID().slice(0, 8)}`,
            serverType: "remote",
            serverUrl: "https://example.com/mcp",
            scope: "personal",
          },
          { organizationId: org.id, authorId: owner.id },
        );
        const tool = await ToolModel.createToolIfNotExists({
          name: `${catalogItem.name}__do_thing`,
          description: "Personal-server tool",
          parameters: {},
          catalogId: catalogItem.id,
        });
        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "dynamic",
        });

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_teamtoken", name: tool.name, arguments: {} },
          agentOwner(agentId),
          {
            tokenId: "tok-team",
            teamId: team.id,
            isOrganizationToken: false,
            organizationId: org.id,
          },
        );

        expect(result.isError).toBe(true);
        const archestraError = result?._meta?.archestraError as
          | { type?: string; action?: string; actionUrl?: string }
          | undefined;
        expect(archestraError?.type).toBe("auth_required");
        expect(archestraError?.action).toBe("install_mcp_credentials");
        expect(archestraError?.actionUrl).toContain(
          `/mcp/registry?install=${catalogItem.id}`,
        );
      });

      test("All-tools mode ignores a static assignment pin and uses the server's connection policy", async ({
        makeAgent,
        makeMember,
        makeOrganization,
        makeUser,
      }) => {
        const org = await makeOrganization();
        const user = await makeUser();
        await makeMember(user.id, org.id, { role: "member" });
        // Agent in "All tools" mode (access_all_tools = true).
        const allAgent = await makeAgent({
          name: "All Tools Agent",
          organizationId: org.id,
          scope: "org",
          accessAllTools: true,
        });

        const catalogItem = await InternalMcpCatalogModel.create({
          name: `connected-server-${randomUUID().slice(0, 8)}`,
          serverType: "remote",
          serverUrl: "https://example.com/mcp",
        });
        const tool = await ToolModel.createToolIfNotExists({
          name: `${catalogItem.name}__do_thing`,
          description: "Connection-policy tool",
          parameters: {},
          catalogId: catalogItem.id,
        });
        const orgServer = await McpServerModel.create({
          name: `${catalogItem.name}-org`,
          catalogId: catalogItem.id,
          serverType: "remote",
          scope: "org",
        });
        const pinnedAwayServer = await McpServerModel.create({
          name: `${catalogItem.name}-personal`,
          catalogId: catalogItem.id,
          serverType: "remote",
          ownerId: user.id,
        });
        // Catalog pins the service account to the org connection...
        await InternalMcpCatalogModel.update(catalogItem.id, {
          dynamicConnectionMcpServerId: orgServer.id,
        });
        // ...but a leftover static assignment pins the tool elsewhere.
        await AgentToolModel.create(allAgent.id, tool.id, {
          mcpServerId: pinnedAwayServer.id,
          credentialResolutionMode: "static",
        });
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "via org service account" }],
          isError: false,
        });
        // The resolved target server is the one whose secrets get loaded, so
        // observe resolution through the lightweight server lookup the secrets
        // path performs for the chosen target.
        const findByIdsBasicSpy = vi.spyOn(McpServerModel, "findByIdsBasic");

        const result = await mcpClient.executeToolCallForOwner(
          { id: "call_all_mode", name: tool.name, arguments: {} },
          agentOwner(allAgent.id),
          userToken(user.id, org.id),
        );

        expect(result.isError).toBe(false);
        // Resolved via the catalog's org pin, not the static assignment's server.
        expect(findByIdsBasicSpy).toHaveBeenCalledWith([orgServer.id]);
        expect(findByIdsBasicSpy).not.toHaveBeenCalledWith([
          pinnedAwayServer.id,
        ]);
      });
    });

    test("expires idle active connections and recreates them on the next tool call", async () => {
      vi.useFakeTimers();

      try {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__ttl_reconnect",
          description: "TTL reconnect test",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        mockConnect.mockResolvedValue(undefined);
        mockPing.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        const toolCall = {
          id: "call_ttl_reconnect",
          name: tool.name,
          arguments: {},
        };

        const firstResult = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );
        expect(firstResult.isError).toBe(false);
        expect(mockConnect).toHaveBeenCalledTimes(1);

        await vi.advanceTimersByTimeAsync(15 * 60 * 1000 + 1);

        const secondResult = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );
        expect(secondResult.isError).toBe(false);

        expect(mockConnect).toHaveBeenCalledTimes(2);
        expect(mockClose).toHaveBeenCalledTimes(1);
      } finally {
        vi.useRealTimers();
      }
    });

    test("recreates cached client after server credentials change", async () => {
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__reauth_reconnect",
        description: "Reconnect after reauth",
        parameters: {},
        catalogId,
      });

      await AgentToolModel.create(agentId, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockPing.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
        isError: false,
      });

      const toolCall = {
        id: "call_reauth_reconnect",
        name: tool.name,
        arguments: {},
      };

      const firstResult = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(firstResult.isError).toBe(false);
      expect(mockConnect).toHaveBeenCalledTimes(1);

      const rotatedSecret = await secretManager().createSecret(
        { access_token: "fresh-github-token-789" },
        "reauthreconnecttoken",
      );
      await McpServerModel.update(mcpServerId, { secretId: rotatedSecret.id });

      const secondResult = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(secondResult.isError).toBe(false);
      expect(mockClose).toHaveBeenCalledTimes(1);
      expect(mockConnect).toHaveBeenCalledTimes(2);
    });

    test("reuses cached client when MCP server row changes without secret rotation", async () => {
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__metadata_update_reuse",
        description: "Reuse after metadata update",
        parameters: {},
        catalogId,
      });

      await AgentToolModel.create(agentId, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockPing.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
        isError: false,
      });

      const toolCall = {
        id: "call_metadata_update_reuse",
        name: tool.name,
        arguments: {},
      };

      const firstResult = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(firstResult.isError).toBe(false);
      expect(mockConnect).toHaveBeenCalledTimes(1);

      await McpServerModel.update(mcpServerId, {
        oauthRefreshError: "refresh_failed",
      });

      const secondResult = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(secondResult.isError).toBe(false);
      expect(mockClose).toHaveBeenCalledTimes(0);
      expect(mockConnect).toHaveBeenCalledTimes(1);
    });

    test("skips ping for recently validated active connections", async () => {
      const tool = await ToolModel.createToolIfNotExists({
        name: "github-mcp-server__recent_reuse",
        description: "Recent active connection reuse",
        parameters: {},
        catalogId,
      });

      await AgentToolModel.create(agentId, tool.id, {
        mcpServerId,
        credentialResolutionMode: "static",
      });

      mockConnect.mockResolvedValue(undefined);
      mockPing.mockResolvedValue(undefined);
      mockCallTool.mockResolvedValue({
        content: [{ type: "text", text: "ok" }],
        isError: false,
      });

      const toolCall = {
        id: "call_recent_reuse",
        name: tool.name,
        arguments: {},
      };

      const firstResult = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(firstResult.isError).toBe(false);
      expect(mockConnect).toHaveBeenCalledTimes(1);
      expect(mockPing).not.toHaveBeenCalled();

      mockPing.mockClear();

      const secondResult = await mcpClient.executeToolCallForOwner(
        toolCall,
        agentOwner(agentId),
      );
      expect(secondResult.isError).toBe(false);
      expect(mockConnect).toHaveBeenCalledTimes(1);
      expect(mockPing).not.toHaveBeenCalled();
    });

    describe("Concurrency limiter", () => {
      test("limits HTTP concurrency to 4", async () => {
        const clientWithInternals = mcpClient as unknown as {
          connectionLimiter: {
            runWithLimit: (
              connectionKey: string,
              limit: number,
              fn: () => Promise<unknown>,
            ) => Promise<unknown>;
          };
          getTransport: (
            catalogItem: unknown,
            targetLocalMcpServerId: string,
            secrets: Record<string, unknown>,
          ) => Promise<unknown>;
          getTransportWithKind: (
            catalogItem: unknown,
            targetLocalMcpServerId: string,
            secrets: Record<string, unknown>,
            transportKind: "stdio" | "http",
          ) => Promise<unknown>;
        };

        const runWithLimitSpy = vi.spyOn(
          clientWithInternals.connectionLimiter,
          "runWithLimit",
        );
        const getTransportSpy = vi.spyOn(clientWithInternals, "getTransport");
        const getTransportWithKindSpy = vi.spyOn(
          clientWithInternals,
          "getTransportWithKind",
        );

        try {
          const tool = await ToolModel.createToolIfNotExists({
            name: "github-mcp-server__limiter_http",
            description: "Limiter http tool",
            parameters: {},
            catalogId,
          });

          await AgentToolModel.create(agentId, tool.id, {
            mcpServerId: mcpServerId,
          });

          mockCallTool.mockResolvedValueOnce({
            content: [{ type: "text", text: "Limiter http" }],
            isError: false,
          });

          const toolCall = {
            id: "call_limiter_http",
            name: "github-mcp-server__limiter_http",
            arguments: {},
          };

          const result = await mcpClient.executeToolCallForOwner(
            toolCall,
            agentOwner(agentId),
          );

          expect(runWithLimitSpy).toHaveBeenCalled();
          expect(runWithLimitSpy.mock.calls[0]?.[1]).toBe(4);
          expect(getTransportSpy).not.toHaveBeenCalled();
          expect(getTransportWithKindSpy).toHaveBeenCalled();

          expect(result).toEqual({
            id: "call_limiter_http",
            content: [{ type: "text", text: "Limiter http" }],
            isError: false,
            name: "github-mcp-server__limiter_http",
          });
        } finally {
          runWithLimitSpy.mockRestore();
          getTransportSpy.mockRestore();
          getTransportWithKindSpy.mockRestore();
        }
      });
    });

    describe("Streamable HTTP Transport (Local Servers)", () => {
      let localMcpServerId: string;
      let localCatalogId: string;

      beforeEach(async ({ makeUser }) => {
        // Create test user for local MCP servers
        const testUser = await makeUser({
          email: "test-local-mcp@example.com",
        });

        // Create catalog entry for local streamable-http server
        const localCatalog = await InternalMcpCatalogModel.create({
          name: "local-streamable-http-server",
          serverType: "local",
          localConfig: {
            command: "npx",
            arguments: [
              "@modelcontextprotocol/server-everything",
              "streamableHttp",
            ],
            transportType: "streamable-http",
            httpPort: 3001,
            httpPath: "/mcp",
          },
        });
        localCatalogId = localCatalog.id;

        // Create MCP server for local streamable-http testing
        const localMcpServer = await McpServerModel.create({
          name: "local-streamable-http-server",
          catalogId: localCatalogId,
          serverType: "local",
          userId: testUser.id,
        });
        localMcpServerId = localMcpServer.id;

        // Reset mocks
        mockUsesStreamableHttp.mockReset();
        mockGetHttpEndpointUrl.mockReset();
        mockCallTool.mockReset();
        mockConnect.mockReset();
      });

      test("executes tools using HTTP transport for streamable-http servers", async () => {
        // Create tool assigned to agent
        const tool = await ToolModel.createToolIfNotExists({
          name: "local-streamable-http-server__test_tool",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        // Mock runtime manager responses
        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        // Mock successful tool call
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Success from HTTP transport" }],
          isError: false,
        });

        const toolCall = {
          id: "call_1",
          name: "local-streamable-http-server__test_tool",
          arguments: { input: "test" },
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Verify HTTP transport was detected
        expect(mockUsesStreamableHttp).toHaveBeenCalledWith(localMcpServerId);
        expect(mockGetHttpEndpointUrl).toHaveBeenCalledWith(localMcpServerId);

        // Verify tool was called via HTTP client
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "test_tool", // Server prefix stripped
            arguments: { input: "test" },
          },
          undefined,
          { signal: undefined },
        );

        // Verify result

        expect(result).toEqual({
          id: "call_1",
          content: [{ type: "text", text: "Success from HTTP transport" }],
          isError: false,
          name: "local-streamable-http-server__test_tool",
        });
      });

      test("returns error when HTTP endpoint URL is missing", async () => {
        // Create tool assigned to agent
        const tool = await ToolModel.createToolIfNotExists({
          name: "local-streamable-http-server__test_tool",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        // Mock runtime manager responses - no endpoint URL
        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue(undefined);

        const toolCall = {
          id: "call_1",
          name: "local-streamable-http-server__test_tool",
          arguments: { input: "test" },
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Verify error result

        expect(result).toEqual({
          id: "call_1",
          content: [
            {
              type: "text",
              text: expect.stringContaining("No HTTP endpoint URL found"),
            },
          ],
          isError: true,
          error: expect.stringContaining("No HTTP endpoint URL found"),
          name: "local-streamable-http-server__test_tool",
          _meta: {
            archestraError: {
              type: "generic",
              message: expect.stringContaining("No HTTP endpoint URL found"),
            },
          },
          structuredContent: {
            archestraError: {
              type: "generic",
              message: expect.stringContaining("No HTTP endpoint URL found"),
            },
          },
        });
      });

      test("uses K8s attach transport when streamable-http is false", async () => {
        // Create tool assigned to agent
        const tool = await ToolModel.createToolIfNotExists({
          name: "local-streamable-http-server__stdio_tool",
          description: "Tool using K8s attach",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        // Mock runtime manager to indicate stdio transport (not HTTP)
        mockUsesStreamableHttp.mockResolvedValue(false);

        // Mock K8sDeployment instance
        const mockK8sDeployment = {
          k8sAttachClient: {} as import("@kubernetes/client-node").Attach,
          k8sNamespace: "default",
          deploymentName: "mcp-test-deployment",
          getRunningPodName: vi.fn().mockResolvedValue("mcp-test-pod-actual"),
        };
        mockGetOrLoadDeployment.mockResolvedValue(mockK8sDeployment);

        // Mock the tool call response
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Success from K8s attach" }],
          isError: false,
        });

        const toolCall = {
          id: "call_1",
          name: "local-streamable-http-server__stdio_tool",
          arguments: { input: "test" },
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Verify K8s attach transport was used (not HTTP transport)
        expect(mockUsesStreamableHttp).toHaveBeenCalledWith(localMcpServerId);
        expect(mockGetHttpEndpointUrl).not.toHaveBeenCalled();
        expect(mockGetOrLoadDeployment).toHaveBeenCalledWith(localMcpServerId);
        expect(mockK8sDeployment.getRunningPodName).toHaveBeenCalled();

        // Verify MCP SDK client was used
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "stdio_tool",
            arguments: { input: "test" },
          },
          undefined,
          { signal: undefined },
        );

        // Verify result
        expect(result).toMatchObject({
          id: "call_1",
          content: [{ type: "text", text: "Success from K8s attach" }],
          isError: false,
        });
      });

      test("limits stdio concurrency to 1", async () => {
        const clientWithInternals = mcpClient as unknown as {
          connectionLimiter: {
            runWithLimit: (
              connectionKey: string,
              limit: number,
              fn: () => Promise<unknown>,
            ) => Promise<unknown>;
          };
        };

        const runWithLimitSpy = vi.spyOn(
          clientWithInternals.connectionLimiter,
          "runWithLimit",
        );

        try {
          const tool = await ToolModel.createToolIfNotExists({
            name: "local-streamable-http-server__limiter_stdio",
            description: "Limiter stdio tool",
            parameters: {},
            catalogId: localCatalogId,
          });

          await AgentToolModel.create(agentId, tool.id, {
            mcpServerId: localMcpServerId,
          });

          mockUsesStreamableHttp.mockResolvedValue(false);

          const mockK8sDeployment = {
            k8sAttachClient: {} as import("@kubernetes/client-node").Attach,
            k8sNamespace: "default",
            deploymentName: "mcp-test-deployment",
            getRunningPodName: vi.fn().mockResolvedValue("mcp-test-pod-actual"),
          };
          mockGetOrLoadDeployment.mockResolvedValue(mockK8sDeployment);

          mockCallTool.mockResolvedValue({
            content: [{ type: "text", text: "Limiter stdio" }],
            isError: false,
          });

          const toolCall = {
            id: "call_limiter_stdio",
            name: "local-streamable-http-server__limiter_stdio",
            arguments: {},
          };

          const result = await mcpClient.executeToolCallForOwner(
            toolCall,
            agentOwner(agentId),
          );

          expect(runWithLimitSpy).toHaveBeenCalled();
          expect(runWithLimitSpy.mock.calls[0]?.[1]).toBe(1);

          expect(result).toMatchObject({
            id: "call_limiter_stdio",
            content: [{ type: "text", text: "Limiter stdio" }],
            isError: false,
          });
        } finally {
          runWithLimitSpy.mockRestore();
        }
      });

      test("strips catalogName prefix when mcpServerName includes userId suffix (Issue #1179)", async () => {
        // Create tool with catalogName prefix (how local server tools are actually created)
        const tool = await ToolModel.createToolIfNotExists({
          name: "local-streamable-http-server__prefix_test_tool",
          description: "Tool for testing prefix stripping fallback",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        // Mock runtime manager responses
        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        // Mock successful tool call
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Prefix stripping works!" }],
          isError: false,
        });

        const toolCall = {
          id: "call_prefix_test",
          name: "local-streamable-http-server__prefix_test_tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Verify the tool was called with just the tool name (stripped using catalogName)
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "prefix_test_tool",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );

        expect(result).toMatchObject({
          id: "call_prefix_test",
          content: [{ type: "text", text: "Prefix stripping works!" }],
          isError: false,
        });
      });

      test("falls back to stripping mcpServerName when catalogName prefix is missing", async () => {
        // Create catalog with different name to ensure catalog prefix doesn't match
        const otherCatalog = await InternalMcpCatalogModel.create({
          name: "other-catalog",
          serverType: "local",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "custom-server-name__fallback_tool",
          description: "Tool using server name prefix",
          parameters: {},
          catalogId: otherCatalog.id,
        });

        // Ensure mcpServerName is 'custom-server-name' for this test
        await McpServerModel.update(localMcpServerId, {
          name: "custom-server-name",
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Fallback works!" }],
          isError: false,
        });

        const toolCall = {
          id: "call_fallback_test",
          name: "custom-server-name__fallback_tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Verify stripping worked using mcpServerName fallback
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "fallback_tool",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );

        expect(result).toMatchObject({
          id: "call_fallback_test",
          content: [{ type: "text", text: "Fallback works!" }],
          isError: false,
        });
      });

      test("does not modify tool name when no prefix matches (Identity Case)", async () => {
        // Create tool with a name that doesn't follow the prefix convention
        const tool = await ToolModel.createToolIfNotExists({
          name: "standalone_tool_name",
          description: "Tool without standard prefix",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Identity works!" }],
          isError: false,
        });

        const toolCall = {
          id: "call_identity_test",
          name: "standalone_tool_name",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Verify the tool name was not mangled since no prefix matched
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "standalone_tool_name",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );

        expect(result).toMatchObject({
          id: "call_identity_test",
          content: [{ type: "text", text: "Identity works!" }],
          isError: false,
        });
      });
    });

    describe("createErrorResult includes error in content", () => {
      test("error results include error message in content array", async () => {
        const toolCall = {
          id: "call_error_content",
          name: "non_existent_tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        expect(result).toMatchObject({
          id: "call_error_content",
          isError: true,
          error: expect.any(String),
        });
        // content should be an array with the error text, not null
        expect(result?.content).toEqual([
          { type: "text", text: expect.any(String) },
        ]);
      });
    });

    describe("Dynamic credential auth link", () => {
      test("returns install URL when no server found for user with dynamic credential", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({ email: "dynauth@example.com" });

        // Create a separate catalog + tool for dynamic credential testing
        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "jira-mcp-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.com/v1/mcp",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-mcp-server__search_issues",
          description: "Search Jira issues",
          parameters: {},
          catalogId: dynCatalog.id,
        });

        // Assign tool to agent with dynamic team credential enabled
        await AgentToolModel.createOrUpdateCredentials(
          agentId,
          tool.id,
          null,
          "dynamic",
        );

        const toolCall = {
          id: "call_dynauth",
          name: "jira-mcp-server__search_issues",
          arguments: { query: "test" },
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        // Should return an error with the install URL
        expect(result).toMatchObject({
          isError: true,
        });
        expect(result?.error).toContain(
          `Authentication required for "jira-mcp-server"`,
        );
        expect(result?.error).toContain(`user: ${testUser.id}`);
        expect(result?.error).toContain(
          `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?install=${dynCatalog.id}`,
        );
        expect(result?.error).toContain(
          "Once you have completed authentication, retry this tool call.",
        );

        // Content should also contain the error message
        expect(result?.content).toEqual([
          { type: "text", text: result?.error },
        ]);
        expect(result?._meta).toMatchObject({
          archestraError: {
            type: "auth_required",
            catalogId: dynCatalog.id,
            catalogName: "jira-mcp-server",
            action: "install_mcp_credentials",
            actionUrl: `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?install=${dynCatalog.id}`,
          },
        });
        expect(result?.structuredContent).toMatchObject({
          archestraError: {
            type: "auth_required",
          },
        });
      });

      test("shows the catalog name (not the catalog UUID) for an unassigned all-tools tool that needs auth", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({ email: "alltools-auth@example.com" });

        // A catalog the agent was never assigned a tool from. In "All tools"
        // mode the dispatcher pre-resolves the tool and passes it through as
        // `availableTool`, so the assignment carries no catalogName.
        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "Atlassian Cloud MCP",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.com/v1/mcp",
        });

        const availableTool = await ToolModel.createToolIfNotExists({
          name: "atlassian-cloud-mcp__search_issues",
          description: "Search Jira issues",
          parameters: {},
          catalogId: dynCatalog.id,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_alltools_auth",
            name: availableTool.name,
            arguments: { query: "test" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
          // Dynamic tool access: the tool is not assigned to the agent.
          { availableTool },
        );

        expect(result.isError).toBe(true);
        // The user-facing name must be the catalog's display name, never its UUID.
        expect(result?.error).toContain(
          'Authentication required for "Atlassian Cloud MCP"',
        );
        expect(result?.error).not.toContain(
          `Authentication required for "${dynCatalog.id}"`,
        );
        expect(result?._meta).toMatchObject({
          archestraError: {
            type: "auth_required",
            catalogId: dynCatalog.id,
            catalogName: "Atlassian Cloud MCP",
          },
        });
      });

      test("returns install URL with team context when team token has no server", async ({
        makeUser,
        makeTeam,
        makeOrganization,
      }) => {
        const org = await makeOrganization();
        const testUser = await makeUser({ email: "teamauth@example.com" });
        const team = await makeTeam(org.id, testUser.id, {
          name: "Test Team",
        });

        // Create catalog + tool
        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "jira-team-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.com/v1/mcp",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-team-server__get_issue",
          description: "Get Jira issue",
          parameters: {},
          catalogId: dynCatalog.id,
        });

        await AgentToolModel.createOrUpdateCredentials(
          agentId,
          tool.id,
          null,
          "dynamic",
        );

        const toolCall = {
          id: "call_team_dynauth",
          name: "jira-team-server__get_issue",
          arguments: { key: "PROJ-1" },
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "team-token",
            teamId: team.id,
            isOrganizationToken: false,
          },
        );

        expect(result).toMatchObject({
          isError: true,
        });
        expect(result?.error).toContain(`team: ${team.id}`);
        expect(result?.error).toContain(
          `${MCP_CATALOG_INSTALL_PATH}?install=${dynCatalog.id}`,
        );
      });

      test("returns auth-required error with team context when servers exist but no owner is in team", async ({
        makeUser,
        makeTeam,
        makeOrganization,
      }) => {
        const org = await makeOrganization();
        // Two users: one owns the server, the other is in the team
        const serverOwner = await makeUser({
          email: "server-owner@example.com",
        });
        const teamMember = await makeUser({
          email: "team-member@example.com",
        });
        const team = await makeTeam(org.id, teamMember.id, {
          name: "Marketing Team",
        });
        // serverOwner is NOT added to the team

        // Create catalog + server owned by serverOwner
        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "slack-mcp-server",
          serverType: "remote",
          serverUrl: "https://mcp.slack.com/v1/mcp",
        });

        const ownerSecret = await secretManager().createSecret(
          { access_token: "owner-slack-token" },
          "slack-owner-secret",
        );

        await McpServerModel.create({
          name: "slack-mcp-server",
          catalogId: dynCatalog.id,
          secretId: ownerSecret.id,
          serverType: "remote",
          ownerId: serverOwner.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "slack-mcp-server__send_message",
          description: "Send a Slack message",
          parameters: {},
          catalogId: dynCatalog.id,
        });

        await AgentToolModel.createOrUpdateCredentials(
          agentId,
          tool.id,
          null,
          "dynamic",
        );

        const toolCall = {
          id: "call_team_no_member_cred",
          name: "slack-mcp-server__send_message",
          arguments: { channel: "#general", text: "hello" },
        };

        // Call with teamMember's team token - serverOwner is NOT in this team
        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "team-token-no-cred",
            teamId: team.id,
            isOrganizationToken: false,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?.error).toContain(
          `Authentication required for "slack-mcp-server"`,
        );
        expect(result?.error).toContain(`team: ${team.id}`);
        expect(result?.error).toContain(
          `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?install=${dynCatalog.id}`,
        );
        expect(result?.error).toContain(
          "Once you have completed authentication, retry this tool call.",
        );
        expect(result?.content).toEqual([
          { type: "text", text: result?.error },
        ]);
      });

      test("returns a config error when a static personal connection belongs to another user", async ({
        makeUser,
      }) => {
        const connectionOwner = await makeUser({
          email: "static-owner@example.com",
        });
        const invokingUser = await makeUser({
          email: "static-invoker@example.com",
        });

        const staticCatalog = await InternalMcpCatalogModel.create({
          name: "githubcopilot__remote-mcp",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
        });

        const ownerSecret = await secretManager().createSecret(
          { access_token: "owner-token" },
          "static-owner-secret",
        );

        const personalServer = await McpServerModel.create({
          name: "githubcopilot__remote-mcp",
          catalogId: staticCatalog.id,
          secretId: ownerSecret.id,
          serverType: "remote",
          ownerId: connectionOwner.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "githubcopilot__remote-mcp__issue_write",
          description: "Create an issue",
          parameters: {},
          catalogId: staticCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: personalServer.id,
        });

        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool.mockRejectedValueOnce(new UnauthorizedError());
        mockConnect.mockResolvedValue(undefined);

        const toolCall = {
          id: "call_static_foreign_personal",
          name: "githubcopilot__remote-mcp__issue_write",
          arguments: { title: "Test issue" },
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "invoker-token",
            teamId: null,
            isOrganizationToken: false,
            userId: invokingUser.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?.error).toContain(
          'Expired / Invalid Authentication: credentials for "githubcopilot__remote-mcp" have expired or are invalid.',
        );
        expect(result?.error).toContain(
          "Re-authenticate to continue using this tool.",
        );
        expect(result?.error).toContain(
          "Ask the agent owner or an admin to re-authenticate.",
        );
        expect(result?._meta).toMatchObject({
          archestraError: {
            type: "assigned_credential_unavailable",
            catalogId: staticCatalog.id,
            catalogName: "githubcopilot__remote-mcp",
          },
        });
      });

      test("resolves the pinned org service account when the user has no connection", async ({
        makeUser,
        makeOrganization,
        makeMember,
      }) => {
        const org = await makeOrganization();
        const admin = await makeUser({ email: "org-admin@example.com" });
        const caller = await makeUser({ email: "org-member@example.com" });
        await makeMember(caller.id, org.id);

        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "linear-org",
          serverType: "remote",
          serverUrl: "https://mcp.linear.app/sse",
        });

        const orgSecret = await secretManager().createSecret(
          { access_token: "linear-org-token" },
          "linear-org-secret",
        );

        const orgServer = await McpServerModel.create({
          name: "linear-org",
          catalogId: dynCatalog.id,
          secretId: orgSecret.id,
          serverType: "remote",
          ownerId: admin.id,
          scope: "org",
        });
        // The org install only serves other callers when pinned as the
        // catalog's service-account connection.
        await InternalMcpCatalogModel.update(dynCatalog.id, {
          dynamicConnectionMcpServerId: orgServer.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "linear-org__list_projects",
          description: "List Linear projects",
          parameters: {},
          catalogId: dynCatalog.id,
        });

        await AgentToolModel.createOrUpdateCredentials(
          agentId,
          tool.id,
          null,
          "dynamic",
        );

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_org_scope",
            name: "linear-org__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "user-token",
            teamId: null,
            isOrganizationToken: false,
            isUserToken: true,
            userId: caller.id,
            organizationId: org.id,
          },
        );

        expect(result).toMatchObject({ isError: false });
        expect(result?._meta?.archestraError).toBeUndefined();

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe("Bearer linear-org-token");
      });

      test("prefers personal server over org-scoped server when both exist", async ({
        makeUser,
        makeOrganization,
        makeMember,
      }) => {
        const org = await makeOrganization();
        const caller = await makeUser({
          email: "prefers-personal@example.com",
        });
        const admin = await makeUser({ email: "org-admin-2@example.com" });
        await makeMember(caller.id, org.id);

        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "linear-priority",
          serverType: "remote",
          serverUrl: "https://mcp.linear.app/sse",
        });

        const personalSecret = await secretManager().createSecret(
          { access_token: "linear-personal-token" },
          "linear-personal-secret",
        );
        const orgSecret = await secretManager().createSecret(
          { access_token: "linear-org-token" },
          "linear-org-secret-2",
        );

        await McpServerModel.create({
          name: "linear-priority-personal",
          catalogId: dynCatalog.id,
          secretId: personalSecret.id,
          serverType: "remote",
          ownerId: caller.id,
          scope: "personal",
        });
        await McpServerModel.create({
          name: "linear-priority-org",
          catalogId: dynCatalog.id,
          secretId: orgSecret.id,
          serverType: "remote",
          ownerId: admin.id,
          scope: "org",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "linear-priority__list_projects",
          description: "List projects",
          parameters: {},
          catalogId: dynCatalog.id,
        });
        await AgentToolModel.createOrUpdateCredentials(
          agentId,
          tool.id,
          null,
          "dynamic",
        );

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_priority",
            name: "linear-priority__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "user-token",
            teamId: null,
            isOrganizationToken: false,
            isUserToken: true,
            userId: caller.id,
            organizationId: org.id,
          },
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer linear-personal-token",
        );
      });
    });

    describe("Enterprise-managed credentials", () => {
      test("uses an external IdP JWT as the exchange assertion when the caller authenticates via external IdP auth", async ({
        makeIdentityProvider,
        makeOrganization,
      }) => {
        const organization = await makeOrganization();
        const identityProvider = await makeIdentityProvider(organization.id, {
          providerId: "enterprise-external-jwt",
          issuer: "http://localhost:30081/realms/archestra",
          oidcConfig: {
            clientId: "archestra-oidc",
            tokenEndpoint:
              "http://localhost:30081/realms/archestra/protocol/openid-connect/token",
            enterpriseManagedCredentials: {
              exchangeStrategy: "rfc8693",
              clientId: "archestra-oidc",
              clientSecret: "archestra-oidc-secret",
              tokenEndpoint:
                "http://localhost:30081/realms/archestra/protocol/openid-connect/token",
              tokenEndpointAuthentication: "client_secret_post",
              subjectTokenType: OAUTH_TOKEN_TYPE.AccessToken,
            },
          },
        });

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: identityProvider.id,
        });

        await McpServerModel.update(mcpServerId, { secretId: null });
        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: {
            identityProviderId: identityProvider.id,
            requestedCredentialType: "bearer_token",
            resourceIdentifier: "archestra-oidc",
            tokenInjectionMode: "authorization_bearer",
          },
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "enterprise external jwt demo__debug-auth-token",
          description: "Managed credential tool",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "enterprise_managed",
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              access_token: "exchanged-downstream-token",
              expires_in: 300,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Managed result" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_enterprise_external_jwt",
            name: "enterprise external jwt demo__debug-auth-token",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "external-token",
            teamId: null,
            isOrganizationToken: false,
            userId: "external-user-id",
            isExternalIdp: true,
            rawToken: "external-idp-jwt",
          },
        );

        expect(result.isError).toBe(false);

        const [, requestInit] = fetchMock.mock.calls.at(0) ?? [];
        expect(String(requestInit?.body)).toContain(
          "subject_token=external-idp-jwt",
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const [, options] =
          vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1) ?? [];
        const headers =
          options?.requestInit?.headers instanceof Headers
            ? options.requestInit.headers
            : new Headers(options?.requestInit?.headers);
        expect(headers.get("Authorization")).toBe(
          "Bearer exchanged-downstream-token",
        );

        fetchMock.mockRestore();
      });

      test("uses a linked secondary IdP token when the MCP gateway IdP differs from the tool IdP", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({ email: "linked-entra@example.com" });
        const oktaIdentityProvider = await makeIdentityProvider(
          organization.id,
          {
            providerId: "Okta",
            issuer: "https://example.okta.com",
            oidcConfig: {
              clientId: "okta-gateway-client-id",
              tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
            },
          },
        );
        const entraIdentityProvider = await makeIdentityProvider(
          organization.id,
          {
            providerId: "EntraID",
            issuer: "https://login.microsoftonline.com/test-tenant/v2.0",
            ssoLoginEnabled: false,
            oidcConfig: {
              clientId: "archestra-entra-client-id",
              clientSecret: "archestra-entra-client-secret",
              tokenEndpoint:
                "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
              enterpriseManagedCredentials: {
                exchangeStrategy: "entra_obo",
                clientId: "archestra-entra-client-id",
                clientSecret: "archestra-entra-client-secret",
                tokenEndpoint:
                  "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
                tokenEndpointAuthentication: "client_secret_post",
                subjectTokenType: OAUTH_TOKEN_TYPE.AccessToken,
              },
            },
          },
        );

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: oktaIdentityProvider.id,
        });

        await db.insert(schema.accountsTable).values({
          id: randomUUID(),
          accountId: "acct-linked-entra",
          providerId: entraIdentityProvider.providerId,
          userId: user.id,
          accessToken: "linked-entra-access-token",
          accessTokenExpiresAt: new Date(Date.now() + 300_000),
          idToken: createJwt({ exp: futureExpSeconds() }),
          createdAt: new Date(),
          updatedAt: new Date(),
        });

        await McpServerModel.update(mcpServerId, { secretId: null });
        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: {
            identityProviderId: entraIdentityProvider.id,
            requestedCredentialType: "bearer_token",
            resourceIdentifier: "api://downstream-app-id",
            tokenInjectionMode: "authorization_bearer",
          },
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "entra protected api__query_codebase",
          description: "Query codebase",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "enterprise_managed",
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              access_token: "downstream-entra-token",
              expires_in: 300,
              token_type: "Bearer",
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Managed result" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_linked_secondary_idp",
            name: "entra protected api__query_codebase",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: `external_idp:${oktaIdentityProvider.id}:okta-sub`,
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
            isExternalIdp: true,
            rawToken: "okta-gateway-jwt",
          },
        );

        expect(result.isError).toBe(false);

        const [, requestInit] = fetchMock.mock.calls.at(0) ?? [];
        expect(String(requestInit?.body)).toContain(
          "requested_token_use=on_behalf_of",
        );
        expect(String(requestInit?.body)).toContain(
          "assertion=linked-entra-access-token",
        );
        expect(String(requestInit?.body)).not.toContain("okta-gateway-jwt");

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const [, options] =
          vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1) ?? [];
        const headers =
          options?.requestInit?.headers instanceof Headers
            ? options.requestInit.headers
            : new Headers(options?.requestInit?.headers);
        expect(headers.get("Authorization")).toBe(
          "Bearer downstream-entra-token",
        );

        fetchMock.mockRestore();
      });

      test("returns a direct SSO link when a downstream IdP token is missing", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({
          email: "missing-downstream@example.com",
        });
        const oktaIdentityProvider = await makeIdentityProvider(
          organization.id,
          {
            providerId: "Okta",
            issuer: "https://example.okta.com",
            oidcConfig: {
              clientId: "okta-gateway-client-id",
              tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
            },
          },
        );
        const entraIdentityProvider = await makeIdentityProvider(
          organization.id,
          {
            providerId: "EntraID",
            issuer: "https://login.microsoftonline.com/test-tenant/v2.0",
            ssoLoginEnabled: false,
            oidcConfig: {
              clientId: "archestra-entra-client-id",
              tokenEndpoint:
                "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
              enterpriseManagedCredentials: {
                exchangeStrategy: "entra_obo",
                tokenEndpoint:
                  "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token",
                tokenEndpointAuthentication: "client_secret_post",
                subjectTokenType: OAUTH_TOKEN_TYPE.AccessToken,
              },
            },
          },
        );

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: oktaIdentityProvider.id,
        });

        await McpServerModel.update(mcpServerId, { secretId: null });
        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: {
            identityProviderId: entraIdentityProvider.id,
            requestedCredentialType: "bearer_token",
            resourceIdentifier: "api://downstream-app-id",
            tokenInjectionMode: "authorization_bearer",
          },
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "entra protected api__query_codebase",
          description: "Query codebase",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "enterprise_managed",
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_missing_downstream_idp",
            name: "entra protected api__query_codebase",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: `external_idp:${oktaIdentityProvider.id}:okta-sub`,
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
            isExternalIdp: true,
            rawToken: "okta-gateway-jwt",
          },
          { conversationId: "00000000-0000-4000-8000-000000000123" },
        );

        const connectUrl = `${config.frontendBaseUrl}/auth/sso/EntraID?redirectTo=%2Fchat%2F00000000-0000-4000-8000-000000000123&mode=${LINKED_IDP_SSO_MODE}`;
        expect(result.isError).toBe(true);
        expect(result.error).toContain(
          'Authentication required for "github-mcp-server"',
        );
        expect(result.error).toContain(
          "This tool needs a current EntraID session",
        );
        expect(result.error).toContain(connectUrl);
        expect(result?._meta).toMatchObject({
          archestraError: {
            type: "auth_required",
            catalogId,
            catalogName: "github-mcp-server",
            action: "connect_identity_provider",
            actionUrl: connectUrl,
            providerId: "EntraID",
          },
        });
        expect(mockConnect).not.toHaveBeenCalled();
      });

      test("injects the brokered managed credential into the outgoing MCP request", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({ email: "managed-mcp@example.com" });
        const managedConfig = {
          requestedCredentialType: "secret" as const,
          resourceIdentifier: "orn:okta:pam:github-secret",
          tokenInjectionMode: "authorization_bearer" as const,
          responseFieldPath: "token",
        };
        const identityProvider = await makeIdentityProvider(organization.id, {
          providerId: "okta-managed-mcp",
          issuer: "https://example.okta.com",
          oidcConfig: {
            clientId: "web-client-id",
            tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
            enterpriseManagedCredentials: {
              exchangeStrategy: "okta_managed",
              clientId: "ai-agent-client-id",
              tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
              tokenEndpointAuthentication: "client_secret_post",
              clientSecret: "ai-agent-client-secret",
            },
          },
        });

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: identityProvider.id,
        });

        await McpServerModel.update(mcpServerId, { secretId: null });
        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: managedConfig,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__managed_tool",
          description: "Managed credential tool",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "enterprise_managed",
        });

        await db.insert(schema.accountsTable).values({
          id: randomUUID(),
          accountId: "acct-managed",
          providerId: identityProvider.providerId,
          userId: user.id,
          idToken: createJwt({ exp: futureExpSeconds() }),
          createdAt: new Date(),
          updatedAt: new Date(),
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              issued_token_type: "urn:okta:params:oauth:token-type:secret",
              secret: { token: "ghu_managed_token" },
              expires_in: 300,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Managed result" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_enterprise_managed",
            name: "github-mcp-server__managed_tool",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "session-token",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
          { conversationId: "enterprise-managed-conv" },
        );

        expect(result.isError).toBe(false);

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const [, options] =
          vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1) ?? [];
        const headers =
          options?.requestInit?.headers instanceof Headers
            ? options.requestInit.headers
            : new Headers(options?.requestInit?.headers);
        expect(headers.get("Authorization")).toBe("Bearer ghu_managed_token");

        fetchMock.mockRestore();
      });

      // Regression: assignments created before enterprise mode existed still
      // carry the default "static" mode. The catalog-level config must win,
      // otherwise runtime calls hit the protected server with no credential.
      test("brokers the managed credential even when the assignment row still says static", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({ email: "stale-static-mcp@example.com" });
        const managedConfig = {
          requestedCredentialType: "secret" as const,
          resourceIdentifier: "orn:okta:pam:github-secret",
          tokenInjectionMode: "authorization_bearer" as const,
          responseFieldPath: "token",
        };
        const identityProvider = await makeIdentityProvider(organization.id, {
          providerId: "okta-managed-stale-static",
          issuer: "https://example.okta.com",
          oidcConfig: {
            clientId: "web-client-id",
            tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
            enterpriseManagedCredentials: {
              exchangeStrategy: "okta_managed",
              clientId: "ai-agent-client-id",
              tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
              tokenEndpointAuthentication: "client_secret_post",
              clientSecret: "ai-agent-client-secret",
            },
          },
        });

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: identityProvider.id,
        });

        await McpServerModel.update(mcpServerId, { secretId: null });
        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: managedConfig,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__stale_static_tool",
          description: "Tool assigned before enterprise mode existed",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        await db.insert(schema.accountsTable).values({
          id: randomUUID(),
          accountId: "acct-stale-static",
          providerId: identityProvider.providerId,
          userId: user.id,
          idToken: createJwt({ exp: futureExpSeconds() }),
          createdAt: new Date(),
          updatedAt: new Date(),
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              issued_token_type: "urn:okta:params:oauth:token-type:secret",
              secret: { token: "ghu_managed_token" },
              expires_in: 300,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Managed result" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_stale_static",
            name: "github-mcp-server__stale_static_tool",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "session-token",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
          { conversationId: "stale-static-conv" },
        );

        expect(result.isError).toBe(false);

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const [, options] =
          vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1) ?? [];
        const headers =
          options?.requestInit?.headers instanceof Headers
            ? options.requestInit.headers
            : new Headers(options?.requestInit?.headers);
        expect(headers.get("Authorization")).toBe("Bearer ghu_managed_token");

        fetchMock.mockRestore();
      });

      test("caches the brokered enterprise-managed credential for repeated tool calls", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({
          email: "cached-managed-mcp@example.com",
        });
        const managedConfig = {
          requestedCredentialType: "secret" as const,
          resourceIdentifier: "orn:okta:pam:github-secret",
          tokenInjectionMode: "authorization_bearer" as const,
          responseFieldPath: "token",
        };
        const identityProvider = await makeIdentityProvider(organization.id, {
          providerId: "okta-managed-cache",
          issuer: "https://example.okta.com",
          oidcConfig: {
            clientId: "web-client-id",
            tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
            enterpriseManagedCredentials: {
              exchangeStrategy: "okta_managed",
              clientId: "ai-agent-client-id",
              tokenEndpoint: "https://example.okta.com/oauth2/v1/token",
              tokenEndpointAuthentication: "client_secret_post",
              clientSecret: "ai-agent-client-secret",
            },
          },
        });

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: identityProvider.id,
        });

        await McpServerModel.update(mcpServerId, { secretId: null });
        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: managedConfig,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__managed_cache_tool",
          description: "Managed credential cache tool",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "enterprise_managed",
        });

        await db.insert(schema.accountsTable).values({
          id: randomUUID(),
          accountId: "acct-managed-cache",
          providerId: identityProvider.providerId,
          userId: user.id,
          idToken: createJwt({ exp: futureExpSeconds() }),
          createdAt: new Date(),
          updatedAt: new Date(),
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              issued_token_type: "urn:okta:params:oauth:token-type:secret",
              secret: { token: "ghu_managed_token" },
              expires_in: 300,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Managed result" }],
          isError: false,
        });

        const firstResult = await mcpClient.executeToolCallForOwner(
          {
            id: "call_enterprise_managed_cache_1",
            name: "github-mcp-server__managed_cache_tool",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "session-token",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
          { conversationId: "enterprise-managed-cache-conv" },
        );
        const secondResult = await mcpClient.executeToolCallForOwner(
          {
            id: "call_enterprise_managed_cache_2",
            name: "github-mcp-server__managed_cache_tool",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "session-token",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
          { conversationId: "enterprise-managed-cache-conv" },
        );

        expect(firstResult.isError).toBe(false);
        expect(secondResult.isError).toBe(false);
        expect(fetchMock).toHaveBeenCalledTimes(1);

        fetchMock.mockRestore();
      });

      test("returns re-authentication error when no usable enterprise assertion is available", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({
          email: "missing-enterprise-assertion@example.com",
        });
        const identityProvider = await makeIdentityProvider(organization.id, {
          providerId: "keycloak-managed-mcp",
          issuer: "http://localhost:30081/realms/archestra",
          oidcConfig: {
            clientId: "archestra-oidc",
            tokenEndpoint:
              "http://localhost:30081/realms/archestra/protocol/openid-connect/token",
            enterpriseManagedCredentials: {
              exchangeStrategy: "rfc8693",
              clientId: "archestra-oidc",
              clientSecret: "archestra-oidc-secret",
              tokenEndpoint:
                "http://localhost:30081/realms/archestra/protocol/openid-connect/token",
              tokenEndpointAuthentication: "client_secret_post",
              subjectTokenType: OAUTH_TOKEN_TYPE.AccessToken,
            },
          },
        });

        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: identityProvider.id,
        });

        await InternalMcpCatalogModel.update(catalogId, {
          enterpriseManagedConfig: {
            identityProviderId: identityProvider.id,
            requestedCredentialType: "bearer_token",
            resourceIdentifier: "archestra-oidc",
            tokenInjectionMode: "authorization_bearer",
          },
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "keycloak protected demo__whoami",
          description: "Show the current authenticated user",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          credentialResolutionMode: "enterprise_managed",
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_missing_enterprise_assertion",
            name: "keycloak protected demo__whoami",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "session-token",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );

        expect(result.isError).toBe(true);
        expect(result.error).toContain(
          'Authentication required for "github-mcp-server"',
        );
        expect(result.error).toContain(
          "This tool needs a current keycloak-managed-mcp session",
        );
        expect(result.error).toContain(
          `${config.frontendBaseUrl}/auth/sso/keycloak-managed-mcp?redirectTo=%2Fchat&mode=${LINKED_IDP_SSO_MODE}`,
        );
        expect(result._meta).toMatchObject({
          archestraError: {
            type: "auth_required",
            catalogId,
            catalogName: "github-mcp-server",
            action: "connect_identity_provider",
            actionUrl: `${config.frontendBaseUrl}/auth/sso/keycloak-managed-mcp?redirectTo=%2Fchat&mode=${LINKED_IDP_SSO_MODE}`,
            providerId: "keycloak-managed-mcp",
          },
        });
      });
    });

    describe("Auth error actionable message", () => {
      test("refreshes and retries when an OAuth server returns an auth-related tool error result", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-tool-error-refresh@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-oauth-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "expired-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-oauth-refresh-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-oauth-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-oauth-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockImplementation(async () => {
            await secretManager().updateSecret(secret.id, {
              access_token: "refreshed-token",
              refresh_token: "refresh-token",
              expires_at: Date.now() + 3_600_000,
            });
            return { ok: true };
          });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool
          .mockResolvedValueOnce({
            content: [
              {
                type: "text",
                text: "Authentication failed: access token expired",
              },
            ],
            isError: true,
          })
          .mockResolvedValueOnce({
            content: [{ type: "text", text: "Issue fetched" }],
            isError: false,
          });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_oauth_tool_error_refresh",
            name: "jira-oauth-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(refreshSpy).toHaveBeenCalledWith(secret.id, oauthCatalog.id);
        expect(mockCallTool).toHaveBeenCalledTimes(2);
        expect(result).toMatchObject({
          isError: false,
          content: [{ type: "text", text: "Issue fetched" }],
        });

        refreshSpy.mockRestore();
      });

      test("does not refresh when a tool returns an application-level access denied error", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-tool-error-access-denied@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-oauth-access-denied-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "valid-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-oauth-access-denied-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-oauth-access-denied-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-oauth-access-denied-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi.spyOn(oauthRoutes, "refreshOAuthToken");

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [
            {
              type: "text",
              text: "Access denied: you do not have permission to view this project",
            },
          ],
          isError: true,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_oauth_tool_error_access_denied",
            name: "jira-oauth-access-denied-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(refreshSpy).not.toHaveBeenCalled();
        expect(result).toMatchObject({
          isError: true,
          content: [
            {
              type: "text",
              text: "Access denied: you do not have permission to view this project",
            },
          ],
        });

        refreshSpy.mockRestore();
      });

      test("a terminal refresh failure records the needs-reauthentication trio on the server row", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-terminal-persist@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-terminal-persist-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "expired-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-terminal-persist-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-terminal-persist-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-terminal-persist-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockResolvedValue({
            ok: false,
            kind: "terminal",
            category: "refresh_failed",
            message: "invalid_grant",
            description: "The refresh token is invalid or has expired",
          });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [
            {
              type: "text",
              text: "Authentication failed: access token expired",
            },
          ],
          isError: true,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_terminal_persist",
            name: "jira-terminal-persist-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        const row = await McpServerModel.findById(mcpServer.id);
        expect(row?.oauthRefreshError).toBe("refresh_failed");
        expect(row?.oauthRefreshErrorMessage).toBe("invalid_grant");
        expect(row?.oauthRefreshErrorDescription).toBe(
          "The refresh token is invalid or has expired",
        );
        expect(row?.oauthRefreshFailedAt).toBeInstanceOf(Date);

        refreshSpy.mockRestore();
      });

      test("a transient refresh failure persists nothing, leaving prior connection health untouched", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-transient-no-persist@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-transient-persist-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "expired-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-transient-persist-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-transient-persist-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        // Pre-seed a prior terminal failure: a transient outcome must neither
        // set nor clear health, so this state must survive unchanged.
        const priorFailedAt = new Date(Date.now() - 60_000);
        await McpServerModel.update(mcpServer.id, {
          oauthRefreshError: "refresh_failed",
          oauthRefreshErrorMessage: "invalid_grant",
          oauthRefreshFailedAt: priorFailedAt,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-transient-persist-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockResolvedValue({
            ok: false,
            kind: "transient",
            reason: "server_error",
          });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [
            {
              type: "text",
              text: "Authentication failed: access token expired",
            },
          ],
          isError: true,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_transient_persist",
            name: "jira-transient-persist-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        const row = await McpServerModel.findById(mcpServer.id);
        expect(row?.oauthRefreshError).toBe("refresh_failed");
        expect(row?.oauthRefreshErrorMessage).toBe("invalid_grant");
        expect(row?.oauthRefreshFailedAt?.getTime()).toBe(
          priorFailedAt.getTime(),
        );

        refreshSpy.mockRestore();
      });

      test("a successful refresh clears a prior needs-reauthentication state", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-success-clears@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-success-clears-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "expired-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-success-clears-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-success-clears-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        await McpServerModel.update(mcpServer.id, {
          oauthRefreshError: "refresh_failed",
          oauthRefreshErrorMessage: "invalid_grant",
          oauthRefreshErrorDescription: "The refresh token is invalid",
          oauthRefreshFailedAt: new Date(Date.now() - 60_000),
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-success-clears-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockImplementation(async () => {
            await secretManager().updateSecret(secret.id, {
              access_token: "refreshed-token",
              refresh_token: "refresh-token",
              expires_at: Date.now() + 3_600_000,
            });
            return { ok: true };
          });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool
          .mockResolvedValueOnce({
            content: [
              {
                type: "text",
                text: "Authentication failed: access token expired",
              },
            ],
            isError: true,
          })
          .mockResolvedValueOnce({
            content: [{ type: "text", text: "Issue fetched" }],
            isError: false,
          });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_success_clears",
            name: "jira-success-clears-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        const row = await McpServerModel.findById(mcpServer.id);
        expect(row?.oauthRefreshError).toBeNull();
        expect(row?.oauthRefreshErrorMessage).toBeNull();
        expect(row?.oauthRefreshErrorDescription).toBeNull();
        expect(row?.oauthRefreshFailedAt).toBeNull();

        refreshSpy.mockRestore();
      });

      test("does not refresh when a tool error only mentions bearer auth guidance", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-tool-error-bearer-guidance@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-oauth-bearer-guidance-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "valid-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-oauth-bearer-guidance-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-oauth-bearer-guidance-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-oauth-bearer-guidance-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi.spyOn(oauthRoutes, "refreshOAuthToken");

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [
            {
              type: "text",
              text: "This endpoint requires Bearer token authentication. See docs for setup steps.",
            },
          ],
          isError: true,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_oauth_tool_error_bearer_guidance",
            name: "jira-oauth-bearer-guidance-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(refreshSpy).not.toHaveBeenCalled();
        expect(result).toMatchObject({
          isError: true,
          content: [
            {
              type: "text",
              text: "This endpoint requires Bearer token authentication. See docs for setup steps.",
            },
          ],
        });

        refreshSpy.mockRestore();
      });

      test("proactively refreshes an OAuth token shortly before expiry", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-proactive-refresh@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-proactive-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "soon-expiring-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 30_000,
          },
          "jira-oauth-proactive-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-proactive-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-proactive-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockImplementation(async () => {
            await secretManager().updateSecret(secret.id, {
              access_token: "proactively-refreshed-token",
              refresh_token: "refresh-token",
              expires_at: Date.now() + 3_600_000,
            });
            return { ok: true };
          });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Issue fetched proactively" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_oauth_proactive_refresh",
            name: "jira-proactive-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(refreshSpy).toHaveBeenCalledWith(secret.id, oauthCatalog.id);
        expect(mockCallTool).toHaveBeenCalledTimes(1);
        expect(result).toMatchObject({
          isError: false,
          content: [{ type: "text", text: "Issue fetched proactively" }],
        });

        refreshSpy.mockRestore();
      });

      test("falls back to the existing token when proactive refresh fails transiently", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-proactive-refresh-fallback@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-proactive-fallback-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "still-valid-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 30_000,
          },
          "jira-oauth-proactive-fallback-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-proactive-fallback-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-proactive-fallback-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockResolvedValue({
            ok: false,
            kind: "terminal",
            category: "refresh_failed",
            message: "refresh_failed",
          });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [
            { type: "text", text: "Issue fetched with existing token" },
          ],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_oauth_proactive_refresh_fallback",
            name: "jira-proactive-fallback-server__get_issue",
            arguments: { issue_key: "CTAZ-1015" },
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(refreshSpy).toHaveBeenCalledWith(secret.id, oauthCatalog.id);
        expect(mockCallTool).toHaveBeenCalledTimes(1);
        expect(result).toMatchObject({
          isError: false,
          content: [
            { type: "text", text: "Issue fetched with existing token" },
          ],
        });

        refreshSpy.mockRestore();
      });

      test("deduplicates concurrent proactive refresh attempts for the same OAuth secret", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-concurrent-refresh@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "jira-concurrent-server",
          serverType: "remote",
          serverUrl: "https://mcp.atlassian.example.com/mcp/",
          oauthConfig: {
            name: "Jira",
            server_url: "https://mcp.atlassian.example.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["read:jira-work"],
            default_scopes: ["read:jira-work"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          {
            access_token: "initial-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 24 * 3_600_000,
          },
          "jira-oauth-concurrent-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "jira-concurrent-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jira-concurrent-server__get_issue",
          description: "Get issue",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Issue fetched concurrently" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_oauth_concurrent_seed",
            name: "jira-concurrent-server__get_issue",
            arguments: { issue_key: "CTAZ-1014" },
          },
          agentOwner(agentId),
          {
            tokenId: "seed-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        await secretManager().updateSecret(secret.id, {
          access_token: "soon-expiring-token",
          refresh_token: "refresh-token",
          expires_at: Date.now() + 30_000,
        });
        (
          mcpClient as unknown as {
            secretsCache: { set: (key: string, value: unknown) => void };
          }
        ).secretsCache.set(mcpServer.id, {
          secrets: {
            access_token: "soon-expiring-token",
            refresh_token: "refresh-token",
            expires_at: Date.now() + 30_000,
          },
          secretId: secret.id,
        });

        mockCallTool.mockClear();
        mockClose.mockClear();

        const refreshSpy = vi
          .spyOn(oauthRoutes, "refreshOAuthToken")
          .mockImplementation(async () => {
            await new Promise((resolve) => setTimeout(resolve, 25));
            await secretManager().updateSecret(secret.id, {
              access_token: "concurrently-refreshed-token",
              refresh_token: "rotated-refresh-token",
              expires_at: Date.now() + 3_600_000,
            });
            return { ok: true };
          });

        const toolCall = {
          id: "call_oauth_concurrent_refresh",
          name: "jira-concurrent-server__get_issue",
          arguments: { issue_key: "CTAZ-1015" },
        };

        const results = await Promise.all([
          mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
            tokenId: "test-token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          }),
          mcpClient.executeToolCallForOwner(
            { ...toolCall, id: "call_oauth_concurrent_refresh_2" },
            agentOwner(agentId),
            {
              tokenId: "test-token-2",
              teamId: null,
              isOrganizationToken: false,
              userId: testUser.id,
            },
          ),
          mcpClient.executeToolCallForOwner(
            { ...toolCall, id: "call_oauth_concurrent_refresh_3" },
            agentOwner(agentId),
            {
              tokenId: "test-token-3",
              teamId: null,
              isOrganizationToken: false,
              userId: testUser.id,
            },
          ),
        ]);

        expect(refreshSpy).toHaveBeenCalledTimes(1);
        expect(mockClose).toHaveBeenCalledTimes(1);
        for (const result of results) {
          expect(result).toMatchObject({
            isError: false,
            content: [{ type: "text", text: "Issue fetched concurrently" }],
          });
        }

        refreshSpy.mockRestore();
      });

      test("returns expired-auth message with manage URL when tool call throws UnauthorizedError on OAuth server with existing credentials", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-unauth@example.com",
        });

        // Create an OAuth-enabled catalog
        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "github-oauth-server",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
          oauthConfig: {
            name: "GitHub",
            server_url: "https://api.githubcopilot.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["repo"],
            default_scopes: ["repo"],
            supports_resource_metadata: false,
          },
        });

        // Create secret WITHOUT refresh_token (simulates expired token, no refresh)
        const secret = await secretManager().createSecret(
          { access_token: "expired-token" },
          "expired-oauth-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "github-oauth-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-oauth-server__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        // Mock callTool to throw UnauthorizedError
        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool.mockRejectedValueOnce(new UnauthorizedError());
        mockConnect.mockResolvedValue(undefined);

        const toolCall = {
          id: "call_oauth_unauth",
          name: "github-oauth-server__list_repos",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?.error).toContain(
          `Expired or invalid authentication for "github-oauth-server"`,
        );
        expect(result?.error).toContain(`user: ${testUser.id}`);
        expect(result?.error).toContain(
          `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?${MCP_CATALOG_REAUTH_QUERY_PARAM}=${oauthCatalog.id}&${MCP_CATALOG_SERVER_QUERY_PARAM}=${mcpServer.id}`,
        );
        expect(result?.error).toContain(
          "Once you have re-authenticated, retry this tool call.",
        );
        expect(result?._meta).toMatchObject({
          archestraError: {
            type: "auth_expired",
            catalogId: oauthCatalog.id,
            catalogName: "github-oauth-server",
            serverId: mcpServer.id,
            reauthUrl: `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?${MCP_CATALOG_REAUTH_QUERY_PARAM}=${oauthCatalog.id}&${MCP_CATALOG_SERVER_QUERY_PARAM}=${mcpServer.id}`,
            // Owner-invoked personal connection → personal credential, no team.
            credentialScope: "personal",
            credentialTeamName: null,
          },
        });
      });

      test("labels the expired-auth error with the owning team for a team-scoped credential", async ({
        makeUser,
        makeTeam,
        makeOrganization,
      }) => {
        const org = await makeOrganization();
        const teamMember = await makeUser({
          email: "team-expired-member@example.com",
        });
        const team = await makeTeam(org.id, teamMember.id, {
          name: "Platform Team",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "github-team-server",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
          oauthConfig: {
            name: "GitHub",
            server_url: "https://api.githubcopilot.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["repo"],
            default_scopes: ["repo"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          { access_token: "expired-token" },
          "team-expired-oauth-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "github-team-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          teamId: team.id,
          scope: "team",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-team-server__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool.mockRejectedValueOnce(new UnauthorizedError());
        mockConnect.mockResolvedValue(undefined);

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_team_expired",
            name: "github-team-server__list_repos",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "team-token",
            teamId: team.id,
            isOrganizationToken: false,
            userId: teamMember.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?._meta).toMatchObject({
          archestraError: {
            type: "auth_expired",
            serverId: mcpServer.id,
            credentialScope: "team",
            credentialTeamName: "Platform Team",
          },
        });
      });

      test("does not label a personal install as the caller's own when the caller is not the owner", async ({
        makeUser,
      }) => {
        const connectionOwner = await makeUser({
          email: "personal-owner@example.com",
        });
        const invokingUser = await makeUser({
          email: "personal-invoker@example.com",
        });

        // Non-OAuth (PAT) catalog so no token refresh intercepts the auth-error
        // tool result before the expired-auth message is built.
        const catalog = await InternalMcpCatalogModel.create({
          name: "github-shared-personal",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
        });

        const secret = await secretManager().createSecret(
          { access_token: "expired-pat" },
          "shared-personal-secret",
        );

        // Personal install owned by connectionOwner, statically assigned to the
        // agent so a different caller routes through it — the retained-assignment
        // shape the scope helper must not misattribute as the caller's own.
        const mcpServer = await McpServerModel.create({
          name: "github-shared-personal",
          catalogId: catalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: connectionOwner.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-shared-personal__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: catalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        // Tool RESULT (not a thrown error) carrying an auth failure — this path
        // builds the expired-auth message without the assigned-credential owner
        // guard that the thrown path has, so the scope helper is what protects
        // against misattribution here.
        mockCallTool.mockResolvedValueOnce({
          isError: true,
          content: [{ type: "text", text: "401 unauthorized: token expired" }],
        });
        mockConnect.mockResolvedValue(undefined);

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_shared_personal",
            name: "github-shared-personal__list_repos",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "invoker-token",
            teamId: null,
            isOrganizationToken: false,
            userId: invokingUser.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        const archestraError = (
          result?._meta as { archestraError?: Record<string, unknown> }
        )?.archestraError;
        expect(archestraError).toMatchObject({
          type: "auth_expired",
          serverId: mcpServer.id,
        });
        // Not the caller's own credential → no "Your personal credentials …".
        expect(archestraError?.credentialScope).toBeUndefined();
        expect(archestraError?.credentialTeamName).toBeUndefined();
      });

      test("records a no_refresh_token state when an OAuth tool call throws UnauthorizedError and no refresh token is stored", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-no-refresh-record@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "github-no-refresh-server",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
          oauthConfig: {
            name: "GitHub",
            server_url: "https://api.githubcopilot.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["repo"],
            default_scopes: ["repo"],
            supports_resource_metadata: false,
          },
        });

        // Secret WITHOUT a refresh token — refresh is impossible, so an auth
        // error must record the terminal no_refresh_token state on the row.
        const secret = await secretManager().createSecret(
          { access_token: "expired-token" },
          "no-refresh-record-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "github-no-refresh-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-no-refresh-server__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool.mockRejectedValueOnce(new UnauthorizedError());
        mockConnect.mockResolvedValue(undefined);

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_no_refresh_record",
            name: "github-no-refresh-server__list_repos",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        const row = await McpServerModel.findById(mcpServer.id);
        expect(row?.oauthRefreshError).toBe("no_refresh_token");
        expect(row?.oauthRefreshErrorMessage).toBe("no_refresh_token");
        expect(row?.oauthRefreshErrorDescription).toBeNull();
        expect(row?.oauthRefreshFailedAt).toBeInstanceOf(Date);
      });

      test("returns expired-auth message with manage URL when tool call throws StreamableHTTPError 401 on OAuth server", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "oauth-http401@example.com",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "github-http401-server",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
          oauthConfig: {
            name: "GitHub",
            server_url: "https://api.githubcopilot.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["repo"],
            default_scopes: ["repo"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          { access_token: "expired-token-2" },
          "expired-oauth-secret-2",
        );

        const mcpServer = await McpServerModel.create({
          name: "github-http401-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-http401-server__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        // Mock callTool to throw StreamableHTTPError with 401
        const { StreamableHTTPError } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        mockCallTool.mockRejectedValueOnce(
          new StreamableHTTPError(401, "Unauthorized"),
        );
        mockConnect.mockResolvedValue(undefined);

        const toolCall = {
          id: "call_oauth_http401",
          name: "github-http401-server__list_repos",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?.error).toContain(
          `Expired or invalid authentication for "github-http401-server"`,
        );
        expect(result?.error).toContain(
          `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?${MCP_CATALOG_REAUTH_QUERY_PARAM}=${oauthCatalog.id}&${MCP_CATALOG_SERVER_QUERY_PARAM}=${mcpServer.id}`,
        );
      });

      test("returns expired-auth message for auth error on non-OAuth server (PAT-based) with existing credentials", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "non-oauth-unauth@example.com",
        });

        // Create catalog WITHOUT oauthConfig (PAT-based auth like GitHub)
        const nonOauthCatalog = await InternalMcpCatalogModel.create({
          name: "private-api-server",
          serverType: "remote",
          serverUrl: "https://private-api.example.com/mcp/",
        });

        const secret = await secretManager().createSecret(
          { access_token: "bad-token" },
          "non-oauth-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "private-api-server",
          catalogId: nonOauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "private-api-server__get_data",
          description: "Get data",
          parameters: {},
          catalogId: nonOauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool.mockRejectedValueOnce(new UnauthorizedError());
        mockConnect.mockResolvedValue(undefined);

        const toolCall = {
          id: "call_non_oauth_unauth",
          name: "private-api-server__get_data",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        // Non-OAuth servers with existing credentials should get expired-auth message
        expect(result?.error).toContain(
          `Expired or invalid authentication for "private-api-server"`,
        );
        expect(result?.error).toContain(
          `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?${MCP_CATALOG_REAUTH_QUERY_PARAM}=${nonOauthCatalog.id}&${MCP_CATALOG_SERVER_QUERY_PARAM}=${mcpServer.id}`,
        );
      });

      test("returns expired-auth message when error message contains auth keywords", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "auth-keyword@example.com",
        });

        // Non-OAuth catalog (like GitHub with PAT)
        const catalog = await InternalMcpCatalogModel.create({
          name: "github-pat-server",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
        });

        const secret = await secretManager().createSecret(
          { access_token: "expired-pat" },
          "expired-pat-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "github-pat-server",
          catalogId: catalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-pat-server__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: catalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        // Mock callTool to throw StreamableHTTPError with non-401 code but auth message
        // (this is what GitHub actually does - returns error with "unauthorized" in body)
        const { StreamableHTTPError } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        mockCallTool.mockRejectedValueOnce(
          new StreamableHTTPError(
            500,
            "Error POSTing to endpoint: unauthorized: unauthorized: AuthenticateToken authentication failed",
          ),
        );
        mockConnect.mockResolvedValue(undefined);

        const toolCall = {
          id: "call_auth_keyword",
          name: "github-pat-server__list_repos",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "test-token",
            teamId: null,
            isOrganizationToken: false,
            userId: testUser.id,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?.error).toContain(
          `Expired or invalid authentication for "github-pat-server"`,
        );
        expect(result?.error).toContain(
          `${config.frontendBaseUrl}${MCP_CATALOG_INSTALL_PATH}?${MCP_CATALOG_REAUTH_QUERY_PARAM}=${catalog.id}&${MCP_CATALOG_SERVER_QUERY_PARAM}=${mcpServer.id}`,
        );
      });

      test("returns config error when a team token hits a personal static assignment", async ({
        makeUser,
        makeTeam,
        makeOrganization,
      }) => {
        const org = await makeOrganization();
        const testUser = await makeUser({
          email: "oauth-team-unauth@example.com",
        });
        const team = await makeTeam(org.id, testUser.id, {
          name: "Dev Team",
        });

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "github-team-oauth-server",
          serverType: "remote",
          serverUrl: "https://api.githubcopilot.com/mcp/",
          oauthConfig: {
            name: "GitHub",
            server_url: "https://api.githubcopilot.com/mcp/",
            client_id: "test-client-id",
            redirect_uris: ["http://localhost:3000/callback"],
            scopes: ["repo"],
            default_scopes: ["repo"],
            supports_resource_metadata: false,
          },
        });

        const secret = await secretManager().createSecret(
          { access_token: "expired-team-token" },
          "expired-team-oauth-secret",
        );

        const mcpServer = await McpServerModel.create({
          name: "github-team-oauth-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-team-oauth-server__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServer.id,
        });

        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool.mockRejectedValueOnce(new UnauthorizedError());
        mockConnect.mockResolvedValue(undefined);

        const toolCall = {
          id: "call_team_oauth_unauth",
          name: "github-team-oauth-server__list_repos",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
          {
            tokenId: "team-token",
            teamId: team.id,
            isOrganizationToken: false,
          },
        );

        expect(result).toMatchObject({ isError: true });
        expect(result?.error).toContain(
          'Expired / Invalid Authentication: credentials for "github-team-oauth-server" have expired or are invalid.',
        );
        expect(result?.error).toContain(
          "Re-authenticate to continue using this tool.",
        );
      });
    });

    describe("Stale session retry", () => {
      let localMcpServerId: string;
      let localCatalogId: string;

      beforeEach(async ({ makeUser }) => {
        const testUser = await makeUser({
          email: "test-stale-session@example.com",
        });

        const localCatalog = await InternalMcpCatalogModel.create({
          name: "stale-session-server",
          serverType: "local",
          localConfig: {
            dockerImage: "mcr.microsoft.com/playwright/mcp",
            transportType: "streamable-http",
            httpPort: 8080,
          },
        });
        localCatalogId = localCatalog.id;

        const localMcpServer = await McpServerModel.create({
          name: "stale-session-server",
          catalogId: localCatalogId,
          serverType: "local",
          userId: testUser.id,
        });
        localMcpServerId = localMcpServer.id;

        mockUsesStreamableHttp.mockReset();
        mockGetHttpEndpointUrl.mockReset();
        mockCallTool.mockReset();
        mockConnect.mockReset();
        mockPing.mockReset();

        // Make StreamableHTTPClientTransport mock store sessionId from options
        // so getOrCreateClient can detect stored sessions via `transport.sessionId`
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        vi.mocked(StreamableHTTPClientTransport).mockImplementation(function (
          this: { sessionId?: string },
          _url: URL,
          options?: { sessionId?: string },
        ) {
          this.sessionId = options?.sessionId;
        } as
          // biome-ignore lint/suspicious/noExplicitAny: cast required for mock constructor
          any);
      });

      test("uses stored endpoint URL when resuming HTTP session", async () => {
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );

        const tool = await ToolModel.createToolIfNotExists({
          name: "stale-session-server__stored_endpoint",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://service-url:8080/mcp");
        vi.spyOn(
          McpHttpSessionModel,
          "findRecordByConnectionKey",
        ).mockResolvedValueOnce({
          sessionId: "stored-session-id",
          sessionEndpointUrl: "http://10.42.1.88:8080/mcp",
          sessionEndpointPodName: "mcp-stale-session-server-abc123",
        });

        mockConnect.mockResolvedValue(undefined);
        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_stored_endpoint",
            name: "stale-session-server__stored_endpoint",
            arguments: {},
          },
          agentOwner(agentId),
          undefined,
          { conversationId: "conv-1" },
        );

        expect(result.isError).toBe(false);
        expect(vi.mocked(StreamableHTTPClientTransport)).toHaveBeenCalledWith(
          new URL("http://10.42.1.88:8080/mcp"),
          expect.objectContaining({ sessionId: "stored-session-id" }),
        );
      });

      test("retries with fresh session when stale session is detected", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "stale-session-server__test_tool",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        // First call: findRecordByConnectionKey returns a stored session
        // Second call (retry): findRecordByConnectionKey returns null (session was deleted)
        vi.spyOn(McpHttpSessionModel, "findRecordByConnectionKey")
          .mockResolvedValueOnce({
            sessionId: "stale-session-id",
            sessionEndpointUrl: null,
            sessionEndpointPodName: null,
          })
          .mockResolvedValueOnce(null);

        // First connect fails (stale session), second connect succeeds
        mockConnect
          .mockRejectedValueOnce(new Error("Session not found"))
          .mockResolvedValueOnce(undefined);

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "Success after retry" }],
          isError: false,
        });

        const toolCall = {
          id: "call_stale_retry",
          name: "stale-session-server__test_tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Should succeed after retry
        expect(result).toMatchObject({
          id: "call_stale_retry",
          content: [{ type: "text", text: "Success after retry" }],
          isError: false,
        });

        // deleteStaleSession should have been called
        expect(McpHttpSessionModel.deleteStaleSession).toHaveBeenCalled();

        // connect should have been called twice (first stale, then fresh)
        expect(mockConnect).toHaveBeenCalledTimes(2);
      });

      test("does not retry more than once for stale sessions", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "stale-session-server__no_double_retry",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        // Both calls return stored session IDs
        vi.spyOn(McpHttpSessionModel, "findRecordByConnectionKey")
          .mockResolvedValueOnce({
            sessionId: "stale-session-1",
            sessionEndpointUrl: null,
            sessionEndpointPodName: null,
          })
          .mockResolvedValueOnce({
            sessionId: "stale-session-2",
            sessionEndpointUrl: null,
            sessionEndpointPodName: null,
          });

        // Both connect attempts fail
        mockConnect
          .mockRejectedValueOnce(new Error("Session not found"))
          .mockRejectedValueOnce(new Error("Session not found again"));

        const toolCall = {
          id: "call_no_double_retry",
          name: "stale-session-server__no_double_retry",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Should return error (no infinite retry loop)
        expect(result).toMatchObject({
          id: "call_no_double_retry",
          isError: true,
        });
      });

      test("retries when callTool throws StreamableHTTPError with 'Session not found'", async () => {
        const { StreamableHTTPError } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );

        const tool = await ToolModel.createToolIfNotExists({
          name: "stale-session-server__http_error_retry",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");

        // First call: findRecordByConnectionKey returns a stored session
        // Second call (retry): findRecordByConnectionKey returns null (session was deleted)
        vi.spyOn(McpHttpSessionModel, "findRecordByConnectionKey")
          .mockResolvedValueOnce({
            sessionId: "stale-session-id",
            sessionEndpointUrl: null,
            sessionEndpointPodName: null,
          })
          .mockResolvedValueOnce(null);

        // connect() succeeds both times (SDK skips initialization for resumed sessions)
        mockConnect.mockResolvedValue(undefined);

        // First callTool throws StreamableHTTPError "Session not found",
        // second callTool succeeds (after retry with fresh session)
        mockCallTool
          .mockRejectedValueOnce(
            new StreamableHTTPError(
              404,
              "Error POSTing to endpoint: Session not found",
            ),
          )
          .mockResolvedValueOnce({
            content: [{ type: "text", text: "Success after retry" }],
            isError: false,
          });

        const toolCall = {
          id: "call_http_error_retry",
          name: "stale-session-server__http_error_retry",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        // Should succeed after retry
        expect(result).toMatchObject({
          id: "call_http_error_retry",
          content: [{ type: "text", text: "Success after retry" }],
          isError: false,
        });

        // deleteStaleSession should have been called
        expect(McpHttpSessionModel.deleteStaleSession).toHaveBeenCalled();

        // callTool should have been called twice (first stale, then fresh)
        expect(mockCallTool).toHaveBeenCalledTimes(2);
      });

      test("skips the stale-session retry when the call is aborted", async () => {
        const { StreamableHTTPError } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );

        const tool = await ToolModel.createToolIfNotExists({
          name: "stale-session-server__aborted_no_retry",
          description: "Test tool",
          parameters: {},
          catalogId: localCatalogId,
        });
        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localMcpServerId,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30123/mcp");
        vi.spyOn(
          McpHttpSessionModel,
          "findRecordByConnectionKey",
        ).mockResolvedValue({
          sessionId: "stale-session-id",
          sessionEndpointUrl: null,
          sessionEndpointPodName: null,
        });
        mockConnect.mockResolvedValue(undefined);

        // The sibling test above proves this exact error triggers a
        // fresh-session retry. With the run aborted, the abort guard must
        // short-circuit before that retry — no second callTool, no session
        // teardown — and propagate the abort.
        mockCallTool.mockRejectedValue(
          new StreamableHTTPError(
            404,
            "Error POSTing to endpoint: Session not found",
          ),
        );

        const controller = new AbortController();
        controller.abort();

        await expect(
          mcpClient.executeToolCallForOwner(
            {
              id: "call_aborted_no_retry",
              name: "stale-session-server__aborted_no_retry",
              arguments: {},
            },
            agentOwner(agentId),
            undefined,
            { abortSignal: controller.signal },
          ),
        ).rejects.toThrow();

        expect(mockCallTool).toHaveBeenCalledTimes(1);
        expect(McpHttpSessionModel.deleteStaleSession).not.toHaveBeenCalled();
      });
    });

    describe("Tool name casing resolution", () => {
      test("resolves camelCase tool name from remote server", async () => {
        // Create tool with lowercased name (as slugifyName produces)
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__getuserinfo",
          description: "Get user info",
          parameters: { type: "object", properties: {} },
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        // Remote server reports tool with camelCase name
        mockListTools.mockResolvedValueOnce({
          tools: [
            { name: "getUserInfo", inputSchema: { type: "object" } },
            { name: "searchIssues", inputSchema: { type: "object" } },
          ],
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "success" }],
          isError: false,
        });

        const toolCall = {
          id: "call_casing_1",
          name: "github-mcp-server__getuserinfo",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId));

        // Verify callTool was called with the original camelCase name
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "getUserInfo",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );
      });

      test("resolves PascalCase tool name from remote server", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__getrepository",
          description: "Get repository",
          parameters: { type: "object", properties: {} },
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        // Remote server reports tool with PascalCase name
        mockListTools.mockResolvedValueOnce({
          tools: [{ name: "GetRepository", inputSchema: { type: "object" } }],
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "success" }],
          isError: false,
        });

        const toolCall = {
          id: "call_casing_2",
          name: "github-mcp-server__getrepository",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId));

        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "GetRepository",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );
      });

      test("falls back to stripped name when listTools fails", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__sometool",
          description: "Some tool",
          parameters: { type: "object", properties: {} },
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        // listTools throws an error
        mockListTools.mockRejectedValueOnce(new Error("Connection timeout"));

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "success" }],
          isError: false,
        });

        const toolCall = {
          id: "call_casing_3",
          name: "github-mcp-server__sometool",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId));

        // Falls back to the lowercased stripped name
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "sometool",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );
      });

      test("falls back to stripped name when tool not in server list", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__missingtool",
          description: "Missing tool",
          parameters: { type: "object", properties: {} },
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        // Server returns tools, but not the one we're looking for
        mockListTools.mockResolvedValueOnce({
          tools: [{ name: "otherTool", inputSchema: { type: "object" } }],
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "success" }],
          isError: false,
        });

        const toolCall = {
          id: "call_casing_4",
          name: "github-mcp-server__missingtool",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId));

        // Falls back to stripped name since no match found
        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "missingtool",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );
      });

      test("preserves already-correct lowercase tool name", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__search_issues",
          description: "Search issues",
          parameters: { type: "object", properties: {} },
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        // Server also uses lowercase (snake_case)
        mockListTools.mockResolvedValueOnce({
          tools: [{ name: "search_issues", inputSchema: { type: "object" } }],
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "success" }],
          isError: false,
        });

        const toolCall = {
          id: "call_casing_5",
          name: "github-mcp-server__search_issues",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId));

        expect(mockCallTool).toHaveBeenCalledWith(
          {
            name: "search_issues",
            arguments: {},
          },
          undefined,
          { signal: undefined },
        );
      });
    });

    describe("Credential resolution priority (JWKS auth)", () => {
      test("JWKS auth with upstream credentials uses upstream token, not JWT (remote server)", async () => {
        // The existing setup creates a remote server with access_token: "test-github-token-123"
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__jwks_cred_test",
          description: "Test JWKS credential priority",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "GitHub response" }],
          isError: false,
        });

        const toolCall = {
          id: "call_jwks_cred",
          name: "github-mcp-server__jwks_cred_test",
          arguments: {},
        };

        // Call with JWKS tokenAuth — the gateway has both the JWT and upstream credentials
        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
          tokenId: "ext-token",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          rawToken: "keycloak-jwt-should-not-be-forwarded",
          userId: "ext-user-123",
        });

        // Verify the transport was created with the upstream GitHub token, NOT the Keycloak JWT
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer test-github-token-123",
        );
      });

      test("JWKS auth without upstream credentials falls back to JWT propagation (remote server)", async () => {
        // Create a remote server WITHOUT credentials
        const noCredCatalog = await InternalMcpCatalogModel.create({
          name: "jwks-echo-server",
          serverType: "remote",
          serverUrl: "https://jwks-echo.example.com/mcp",
        });

        const noCredServer = await McpServerModel.create({
          name: "jwks-echo-server",
          catalogId: noCredCatalog.id,
          serverType: "remote",
          // No secretId — this server has no upstream credentials
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "jwks-echo-server__get_info",
          description: "Get info with JWT passthrough",
          parameters: {},
          catalogId: noCredCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: noCredServer.id,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "JWT validated" }],
          isError: false,
        });

        const toolCall = {
          id: "call_jwks_passthrough",
          name: "jwks-echo-server__get_info",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
          tokenId: "ext-token",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          rawToken: "keycloak-jwt-for-passthrough",
          userId: "ext-user-456",
        });

        // Verify the transport was created with the Keycloak JWT (fallback)
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer keycloak-jwt-for-passthrough",
        );
      });

      test("JWKS auth with raw_access_token uses raw token (remote server)", async () => {
        // Create a server with raw_access_token instead of access_token
        const rawTokenCatalog = await InternalMcpCatalogModel.create({
          name: "raw-token-server",
          serverType: "remote",
          serverUrl: "https://raw-token.example.com/mcp",
        });

        const rawTokenSecret = await secretManager().createSecret(
          { raw_access_token: "Token github_pat_raw_abc123" },
          "raw-token-secret",
        );

        const rawTokenServer = await McpServerModel.create({
          name: "raw-token-server",
          secretId: rawTokenSecret.id,
          catalogId: rawTokenCatalog.id,
          serverType: "remote",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "raw-token-server__list_items",
          description: "List items with raw token",
          parameters: {},
          catalogId: rawTokenCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: rawTokenServer.id,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Raw token response" }],
          isError: false,
        });

        const toolCall = {
          id: "call_jwks_raw",
          name: "raw-token-server__list_items",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
          tokenId: "ext-token",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          rawToken: "keycloak-jwt-should-not-be-used",
          userId: "ext-user-789",
        });

        // Verify raw_access_token was used (not the JWT)
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Token github_pat_raw_abc123",
        );
      });

      test("uses a custom header name for static bearer credentials", async () => {
        const customHeaderCatalog = await InternalMcpCatalogModel.create({
          name: "custom-header-server",
          serverType: "remote",
          serverUrl: "https://custom-header.example.com/mcp",
          userConfig: {
            access_token: {
              type: "string",
              title: "Access Token",
              description: "Bearer token",
              required: true,
              sensitive: true,
              headerName: "x-api-key",
            },
          },
        });

        const customHeaderSecret = await secretManager().createSecret(
          { access_token: "header-secret-token" },
          "custom-header-secret",
        );

        const customHeaderServer = await McpServerModel.create({
          name: "custom-header-server",
          secretId: customHeaderSecret.id,
          catalogId: customHeaderCatalog.id,
          serverType: "remote",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "custom-header-server__list_items",
          description: "List items with custom header auth",
          parameters: {},
          catalogId: customHeaderCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: customHeaderServer.id,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Custom header response" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_custom_header_auth",
            name: "custom-header-server__list_items",
            arguments: {},
          },
          agentOwner(agentId),
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("x-api-key")).toBe("header-secret-token");
        expect(headers.get("authorization")).toBeNull();
      });

      test("treats lowercase authorization header names as satisfying the auth fallback", async () => {
        const lowercaseAuthorizationCatalog =
          await InternalMcpCatalogModel.create({
            name: "lowercase-authorization-server",
            serverType: "remote",
            serverUrl: "https://lowercase-authorization.example.com/mcp",
            userConfig: {
              access_token: {
                type: "string",
                title: "Access Token",
                description: "Bearer token",
                required: true,
                sensitive: true,
                headerName: "authorization",
              },
            },
          });

        const lowercaseAuthorizationSecret = await secretManager().createSecret(
          { access_token: "lowercase-auth-token" },
          "lowercase-authorization-secret",
        );

        const lowercaseAuthorizationServer = await McpServerModel.create({
          name: "lowercase-authorization-server",
          secretId: lowercaseAuthorizationSecret.id,
          catalogId: lowercaseAuthorizationCatalog.id,
          serverType: "remote",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "lowercase-authorization-server__list_items",
          description: "List items with lowercase authorization header",
          parameters: {},
          catalogId: lowercaseAuthorizationCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: lowercaseAuthorizationServer.id,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Lowercase authorization response" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_lowercase_authorization",
            name: "lowercase-authorization-server__list_items",
            arguments: {},
          },
          agentOwner(agentId),
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer lowercase-auth-token",
        );
        expect(Array.from(headers.entries())).toEqual([
          ["authorization", "Bearer lowercase-auth-token"],
        ]);
      });

      test("falls back to Authorization header for legacy bearer credentials without headerName", async () => {
        const legacyBearerCatalog = await InternalMcpCatalogModel.create({
          name: "legacy-bearer-server",
          serverType: "remote",
          serverUrl: "https://legacy-bearer.example.com/mcp",
          userConfig: {
            access_token: {
              type: "string",
              title: "Access Token",
              description: "Bearer token",
              required: true,
              sensitive: true,
            },
          },
        });

        const legacyBearerSecret = await secretManager().createSecret(
          { access_token: "legacy-bearer-token" },
          "legacy-bearer-secret",
        );

        const legacyBearerServer = await McpServerModel.create({
          name: "legacy-bearer-server",
          secretId: legacyBearerSecret.id,
          catalogId: legacyBearerCatalog.id,
          serverType: "remote",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "legacy-bearer-server__list_items",
          description: "List items with legacy bearer auth",
          parameters: {},
          catalogId: legacyBearerCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: legacyBearerServer.id,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Legacy bearer response" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_legacy_bearer_auth",
            name: "legacy-bearer-server__list_items",
            arguments: {},
          },
          agentOwner(agentId),
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe("Bearer legacy-bearer-token");
      });

      test("sends additional static headers alongside the auth header", async () => {
        const multiHeaderCatalog = await InternalMcpCatalogModel.create({
          name: "multi-header-server",
          serverType: "remote",
          serverUrl: "https://multi-header.example.com/mcp",
          userConfig: {
            access_token: {
              type: "string",
              title: "Access Token",
              description: "Bearer token",
              required: true,
              sensitive: true,
              headerName: "x-api-key",
            },
            header_x_tenant_id: {
              type: "string",
              title: "x-tenant-id",
              description: "Tenant ID",
              required: true,
              sensitive: true,
              headerName: "x-tenant-id",
            },
          },
        });

        const multiHeaderSecret = await secretManager().createSecret(
          {
            access_token: "header-secret-token",
            header_x_tenant_id: "tenant-42",
          },
          "multi-header-secret",
        );

        const multiHeaderServer = await McpServerModel.create({
          name: "multi-header-server",
          secretId: multiHeaderSecret.id,
          catalogId: multiHeaderCatalog.id,
          serverType: "remote",
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "multi-header-server__get_info",
          description: "Get info with multiple headers",
          parameters: {},
          catalogId: multiHeaderCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: multiHeaderServer.id,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Multi header response" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_multi_header_auth",
            name: "multi-header-server__get_info",
            arguments: {},
          },
          agentOwner(agentId),
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("x-api-key")).toBe("header-secret-token");
        expect(headers.get("x-tenant-id")).toBe("tenant-42");
      });

      test("non-JWKS auth (OAuth/Bearer) still uses upstream credentials", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__oauth_cred_test",
          description: "Test OAuth credential behavior",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "OAuth response" }],
          isError: false,
        });

        const toolCall = {
          id: "call_oauth_cred",
          name: "github-mcp-server__oauth_cred_test",
          arguments: {},
        };

        // Call with standard (non-JWKS) tokenAuth — isExternalIdp is false
        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
          tokenId: "user-token",
          teamId: null,
          isOrganizationToken: false,
          isUserToken: true,
          userId: "user-123",
        });

        // Verify upstream credentials are used (unchanged behavior)
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer test-github-token-123",
        );
      });

      test("JWKS auth with dynamic credentials resolves server and uses its credentials", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "jwks-dynamic@example.com",
        });

        // Create a catalog with dynamic credentials enabled
        const dynCatalog = await InternalMcpCatalogModel.create({
          name: "github-dynamic",
          serverType: "remote",
          serverUrl: "https://api.github.com/mcp",
        });

        // Create a server owned by the test user with credentials
        const dynSecret = await secretManager().createSecret(
          { access_token: "ghp_dynamic_user_token" },
          "github-dynamic-secret",
        );

        await McpServerModel.create({
          name: "github-dynamic",
          catalogId: dynCatalog.id,
          secretId: dynSecret.id,
          serverType: "remote",
          ownerId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "github-dynamic__list_repos",
          description: "List repos",
          parameters: {},
          catalogId: dynCatalog.id,
        });

        // Enable dynamic credential resolution
        await AgentToolModel.createOrUpdateCredentials(
          agentId,
          tool.id,
          null,
          "dynamic",
        );

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Dynamic response" }],
          isError: false,
        });

        const toolCall = {
          id: "call_jwks_dynamic",
          name: "github-dynamic__list_repos",
          arguments: {},
        };

        // Call with JWKS tokenAuth, userId matching the server owner
        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
          tokenId: "ext-dynamic-token",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          rawToken: "keycloak-jwt-not-for-github",
          userId: testUser.id,
        });

        // Verify the dynamically resolved server credentials were used
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer ghp_dynamic_user_token",
        );
      });

      test("JWKS auth with local streamable-http server uses upstream credentials over JWT", async ({
        makeUser,
      }) => {
        const testUser = await makeUser({
          email: "jwks-local@example.com",
        });

        // Create local server with credentials
        const localCatalog = await InternalMcpCatalogModel.create({
          name: "local-github-jwks",
          serverType: "local",
          localConfig: {
            command: "npx",
            arguments: ["github-mcp-server"],
            transportType: "streamable-http",
            httpPort: 3001,
            httpPath: "/mcp",
          },
        });

        const localSecret = await secretManager().createSecret(
          { access_token: "ghp_local_server_token" },
          "local-github-secret",
        );

        const localServer = await McpServerModel.create({
          name: "local-github-jwks",
          catalogId: localCatalog.id,
          secretId: localSecret.id,
          serverType: "local",
          userId: testUser.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "local-github-jwks__get_repos",
          description: "Get repos",
          parameters: {},
          catalogId: localCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: localServer.id,
        });

        mockUsesStreamableHttp.mockResolvedValue(true);
        mockGetHttpEndpointUrl.mockReturnValue("http://localhost:30456/mcp");

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "Local GitHub response" }],
          isError: false,
        });

        const toolCall = {
          id: "call_jwks_local",
          name: "local-github-jwks__get_repos",
          arguments: {},
        };

        await mcpClient.executeToolCallForOwner(toolCall, agentOwner(agentId), {
          tokenId: "ext-local-token",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          rawToken: "keycloak-jwt-not-for-local",
          userId: "ext-user-local",
        });

        // Verify local server used upstream credentials, not JWT
        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("authorization")).toBe(
          "Bearer ghp_local_server_token",
        );
      });
    });

    describe("Tool name suffix fallback", () => {
      test("resolves unprefixed tool name by suffix when no exact match", async () => {
        // Create a tool with the full prefixed name
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__refresh-stats",
          description: "Refresh stats",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "refreshed" }],
          isError: false,
        });

        // Call with unprefixed name (no "__") — triggers suffix fallback
        const toolCall = {
          id: "call_suffix_1",
          name: "refresh-stats",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        expect(result.isError).toBe(false);
        // The tool name should be rewritten to the full prefixed name
        expect(result.name).toBe("github-mcp-server__refresh-stats");
      });

      test("does not use suffix fallback when name contains separator", async () => {
        // Tool call with "__" in the name should NOT trigger suffix fallback
        const toolCall = {
          id: "call_suffix_2",
          name: "wrong-server__nonexistent-tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        expect(result.isError).toBe(true);
        expect(result.error).toContain("No tool named");
      });
    });

    describe("passthrough headers", () => {
      test("includes passthrough headers in transport for remote servers", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__passthrough_test",
          description: "Passthrough header test",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "ok" }],
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_passthrough_1",
            name: "github-mcp-server__passthrough_test",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "tok-1",
            teamId: null,
            isOrganizationToken: true,
            passthroughHeaders: {
              "x-correlation-id": "abc-123",
              "x-tenant-id": "tenant-1",
            },
          },
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        expect(headers.get("x-correlation-id")).toBe("abc-123");
        expect(headers.get("x-tenant-id")).toBe("tenant-1");
      });

      test("passthrough headers do not override existing auth headers", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__passthrough_no_override",
          description: "Passthrough should not override auth",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "ok" }],
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_passthrough_2",
            name: "github-mcp-server__passthrough_no_override",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "tok-1",
            teamId: null,
            isOrganizationToken: true,
            passthroughHeaders: {
              authorization: "Bearer malicious-override",
              "x-custom": "allowed",
            },
          },
        );

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        expect(transportCalls.length).toBeGreaterThan(0);
        const lastCall = transportCalls[transportCalls.length - 1];
        const headers = lastCall[1]?.requestInit?.headers as Headers;
        // Auth header should be the server's credential, not the passthrough
        expect(headers.get("authorization")).toBe(
          "Bearer test-github-token-123",
        );
        // Custom header should still be included
        expect(headers.get("x-custom")).toBe("allowed");
      });
    });

    describe("MCP aggregate methods with OAuth headers", () => {
      test("uses separate aggregate cached clients for external IdP users", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__external_idp_resources",
          description: "External IdP resources",
          parameters: {},
          catalogId,
        });
        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId,
          credentialResolutionMode: "static",
        });

        mockListResources
          .mockResolvedValueOnce({
            resources: [{ uri: "resource://external-a" }],
          })
          .mockResolvedValueOnce({
            resources: [{ uri: "resource://external-b" }],
          });

        const firstResult = await mcpClient.listResources(agentId, {
          tokenId: "external-token-a",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          userId: "external-user-a",
        });
        const secondResult = await mcpClient.listResources(agentId, {
          tokenId: "external-token-b",
          teamId: null,
          isOrganizationToken: false,
          isExternalIdp: true,
          userId: "external-user-b",
        });

        expect(firstResult.resources).toEqual([
          { uri: "resource://external-a" },
        ]);
        expect(secondResult.resources).toEqual([
          { uri: "resource://external-b" },
        ]);
        expect(mockConnect).toHaveBeenCalledTimes(2);
        expect(mockClose).not.toHaveBeenCalled();
      });

      const authCodeCases = [
        {
          label: "public authorization-code",
          catalogName: "public-auth-code-mcp",
          accessToken: "public-auth-code-access-token",
        },
        {
          label: "confidential authorization-code",
          catalogName: "confidential-auth-code-mcp",
          accessToken: "confidential-auth-code-access-token",
          clientSecret: "confidential-client-secret",
        },
      ];

      for (const authCodeCase of authCodeCases) {
        test(`passes Bearer token for ${authCodeCase.label} resources/list`, async ({
          makeUser,
        }) => {
          const user = await makeUser({
            email: `${authCodeCase.catalogName}@example.com`,
          });
          const secret = await secretManager().createSecret(
            {
              access_token: authCodeCase.accessToken,
              refresh_token: `${authCodeCase.catalogName}-refresh-token`,
              expires_at: Date.now() + 3_600_000,
            },
            `${authCodeCase.catalogName}-secret`,
          );
          const oauthCatalog = await InternalMcpCatalogModel.create({
            name: authCodeCase.catalogName,
            serverType: "remote",
            serverUrl: `https://${authCodeCase.catalogName}.example.com/mcp/`,
            oauthConfig: {
              name: authCodeCase.catalogName,
              server_url: `https://${authCodeCase.catalogName}.example.com/mcp/`,
              grant_type: "authorization_code",
              client_id: `${authCodeCase.catalogName}-client-id`,
              ...(authCodeCase.clientSecret
                ? { client_secret: authCodeCase.clientSecret }
                : {}),
              redirect_uris: ["http://localhost:3000/oauth-callback"],
              scopes: ["read"],
              default_scopes: ["read"],
              supports_resource_metadata: false,
              authorization_endpoint: `https://${authCodeCase.catalogName}.example.com/oauth/authorize`,
              token_endpoint: `https://${authCodeCase.catalogName}.example.com/oauth/token`,
            },
          });
          const oauthServer = await McpServerModel.create({
            name: authCodeCase.catalogName,
            catalogId: oauthCatalog.id,
            secretId: secret.id,
            serverType: "remote",
            ownerId: user.id,
          });
          const tool = await ToolModel.createToolIfNotExists({
            name: `${authCodeCase.catalogName}__list_resources`,
            description: "List resources",
            parameters: {},
            catalogId: oauthCatalog.id,
          });
          await AgentToolModel.create(agentId, tool.id, {
            mcpServerId: oauthServer.id,
            credentialResolutionMode: "static",
          });

          mockListResources.mockResolvedValueOnce({
            resources: [{ uri: "resource://example" }],
          });

          const result = await mcpClient.listResources(agentId, {
            tokenId: "profile-token",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          });

          expect(result.resources).toEqual([{ uri: "resource://example" }]);
          const { StreamableHTTPClientTransport } = await import(
            "@modelcontextprotocol/sdk/client/streamableHttp.js"
          );
          const [, options] =
            vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1) ?? [];
          const headers =
            options?.requestInit?.headers instanceof Headers
              ? options.requestInit.headers
              : new Headers(options?.requestInit?.headers);
          expect(headers.get("Authorization")).toBe(
            `Bearer ${authCodeCase.accessToken}`,
          );
        });
      }

      test("passes token-exchange Bearer token for resources/list and rebuilds after credential rotation", async ({
        makeIdentityProvider,
        makeOrganization,
        makeUser,
      }) => {
        const organization = await makeOrganization();
        const user = await makeUser({
          email: "aggregate-token-exchange@example.com",
        });
        const identityProvider = await makeIdentityProvider(organization.id, {
          providerId: "aggregate-token-exchange-idp",
          issuer: "https://idp.example.com",
          oidcConfig: {
            clientId: "aggregate-web-client",
            tokenEndpoint: "https://idp.example.com/oauth/token",
            enterpriseManagedCredentials: {
              exchangeStrategy: "rfc8693",
              clientId: "aggregate-agent-client",
              clientSecret: "aggregate-agent-secret",
              tokenEndpoint: "https://idp.example.com/oauth/token",
              tokenEndpointAuthentication: "client_secret_post",
              subjectTokenType: OAUTH_TOKEN_TYPE.AccessToken,
            },
          },
        });
        await AgentModel.update(agentId, {
          organizationId: organization.id,
          identityProviderId: identityProvider.id,
        });

        const exchangeCatalog = await InternalMcpCatalogModel.create({
          name: "aggregate-token-exchange-mcp",
          serverType: "remote",
          serverUrl: "https://aggregate-token-exchange.example.com/mcp/",
          enterpriseManagedConfig: {
            identityProviderId: identityProvider.id,
            requestedCredentialType: "bearer_token",
            resourceIdentifier: "api://aggregate-token-exchange",
            tokenInjectionMode: "authorization_bearer",
          },
        });
        const exchangeServer = await McpServerModel.create({
          name: "aggregate-token-exchange-mcp",
          catalogId: exchangeCatalog.id,
          secretId: null,
          serverType: "remote",
          ownerId: user.id,
        });
        const tool = await ToolModel.createToolIfNotExists({
          name: "aggregate-token-exchange-mcp__list_resources",
          description: "List resources",
          parameters: {},
          catalogId: exchangeCatalog.id,
        });
        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: exchangeServer.id,
          credentialResolutionMode: "enterprise_managed",
        });

        await db.insert(schema.accountsTable).values({
          id: randomUUID(),
          accountId: "acct-aggregate-token-exchange",
          providerId: identityProvider.providerId,
          userId: user.id,
          accessToken: "aggregate-login-access-token",
          accessTokenExpiresAt: new Date(Date.now() + 300_000),
          idToken: createJwt({ exp: futureExpSeconds() }),
          createdAt: new Date(),
          updatedAt: new Date(),
        });

        const downstreamTokens = [
          "aggregate-downstream-access-token",
          "aggregate-rotated-downstream-access-token",
        ];
        let tokenExchangeCount = 0;
        const fetchMock = vi
          .spyOn(globalThis, "fetch")
          .mockImplementation(async (input) => {
            const url = input instanceof Request ? input.url : input.toString();
            expect(url).toBe("https://idp.example.com/oauth/token");
            const accessToken = downstreamTokens[tokenExchangeCount];
            tokenExchangeCount += 1;

            return new Response(
              JSON.stringify({
                access_token: accessToken,
                expires_in: 300,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            );
          });
        try {
          mockListResources
            .mockResolvedValueOnce({
              resources: [{ uri: "resource://exchange" }],
            })
            .mockResolvedValueOnce({
              resources: [{ uri: "resource://exchange-rotated" }],
            });

          const result = await mcpClient.listResources(agentId, {
            tokenId: "session-token-a",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          });
          const rotatedResult = await mcpClient.listResources(agentId, {
            tokenId: "session-token-b",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          });

          expect(result.resources).toEqual([{ uri: "resource://exchange" }]);
          expect(rotatedResult.resources).toEqual([
            { uri: "resource://exchange-rotated" },
          ]);
          const { StreamableHTTPClientTransport } = await import(
            "@modelcontextprotocol/sdk/client/streamableHttp.js"
          );
          const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
            .calls;
          const [, firstOptions] = transportCalls.at(-2) ?? [];
          const firstHeaders =
            firstOptions?.requestInit?.headers instanceof Headers
              ? firstOptions.requestInit.headers
              : new Headers(firstOptions?.requestInit?.headers);
          expect(firstHeaders.get("Authorization")).toBe(
            "Bearer aggregate-downstream-access-token",
          );
          const [, secondOptions] = transportCalls.at(-1) ?? [];
          const secondHeaders =
            secondOptions?.requestInit?.headers instanceof Headers
              ? secondOptions.requestInit.headers
              : new Headers(secondOptions?.requestInit?.headers);
          expect(secondHeaders.get("Authorization")).toBe(
            "Bearer aggregate-rotated-downstream-access-token",
          );
          expect(fetchMock).toHaveBeenCalledTimes(2);
          expect(mockClose).toHaveBeenCalledTimes(1);
          expect(mockConnect).toHaveBeenCalledTimes(2);
        } finally {
          fetchMock.mockRestore();
        }
      });
    });

    describe("oauth client credentials", () => {
      test("exchanges stored client credentials for a bearer token on remote MCP calls", async ({
        makeUser,
      }) => {
        const user = await makeUser({
          email: "client-credentials@example.com",
        });

        const secret = await secretManager().createSecret(
          {
            client_id: "shared-client-id",
            client_secret: "shared-client-secret",
            audience: "https://service.example.com",
          },
          "client-credentials-secret",
        );

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "shared-client-credentials-server",
          serverType: "remote",
          serverUrl: "https://api.example.com/mcp/",
          oauthConfig: {
            name: "Shared Client Credentials",
            server_url: "https://api.example.com/mcp/",
            grant_type: "client_credentials",
            client_id: "",
            redirect_uris: [],
            scopes: [],
            default_scopes: [],
            supports_resource_metadata: false,
            token_endpoint: "https://auth.example.com/oauth/token",
          },
          userConfig: {
            client_id: {
              type: "string",
              title: "Client ID",
              description: "Client ID",
              required: true,
              sensitive: false,
            },
            client_secret: {
              type: "string",
              title: "Client Secret",
              description: "Client Secret",
              required: true,
              sensitive: true,
            },
            audience: {
              type: "string",
              title: "Audience",
              description: "Audience",
              required: false,
              sensitive: false,
            },
          },
        });

        const oauthServer = await McpServerModel.create({
          name: "shared-client-credentials-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: user.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "shared-client-credentials-server__list_projects",
          description: "List projects",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: oauthServer.id,
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              access_token: createJwt({ exp: futureExpSeconds() }),
              expires_in: 3600,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_client_credentials_1",
            name: "shared-client-credentials-server__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );

        expect(result.isError).toBe(false);
        expect(fetchMock).toHaveBeenCalledTimes(1);
        expect(fetchMock.mock.calls[0]?.[0]).toBe(
          "https://auth.example.com/oauth/token",
        );
        const requestOptions = fetchMock.mock.calls[0]?.[1];
        expect(requestOptions?.headers).toMatchObject({
          "Content-Type": "application/x-www-form-urlencoded",
          Accept: "application/json",
        });
        expect(requestOptions?.body).toBeInstanceOf(URLSearchParams);
        const requestBody = requestOptions?.body as URLSearchParams;
        expect(requestBody.get("grant_type")).toBe("client_credentials");
        expect(requestBody.get("client_id")).toBe("shared-client-id");
        expect(requestBody.get("client_secret")).toBe("shared-client-secret");
        expect(requestBody.get("audience")).toBe("https://service.example.com");

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const [, options] =
          vi.mocked(StreamableHTTPClientTransport).mock.calls.at(-1) ?? [];
        const headers =
          options?.requestInit?.headers instanceof Headers
            ? options.requestInit.headers
            : new Headers(options?.requestInit?.headers);
        expect(headers.get("Authorization")).toMatch(/^Bearer /);

        fetchMock.mockRestore();
      });

      test("reuses cached client credentials tokens until refresh time", async ({
        makeUser,
      }) => {
        const user = await makeUser({
          email: "client-credentials-cache@example.com",
        });

        const secret = await secretManager().createSecret(
          {
            client_id: "shared-client-id",
            client_secret: "shared-client-secret",
            audience: "https://service.example.com",
          },
          "client-credentials-cache-secret",
        );

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "shared-client-credentials-cache-server",
          serverType: "remote",
          serverUrl: "https://api.example.com/mcp/",
          oauthConfig: {
            name: "Shared Client Credentials Cache",
            server_url: "https://api.example.com/mcp/",
            grant_type: "client_credentials",
            client_id: "",
            redirect_uris: [],
            scopes: [],
            default_scopes: [],
            supports_resource_metadata: false,
            token_endpoint: "https://auth.example.com/oauth/token",
          },
          userConfig: {
            client_id: {
              type: "string",
              title: "Client ID",
              description: "Client ID",
              required: true,
              sensitive: false,
            },
            client_secret: {
              type: "string",
              title: "Client Secret",
              description: "Client Secret",
              required: true,
              sensitive: true,
            },
            audience: {
              type: "string",
              title: "Audience",
              description: "Audience",
              required: false,
              sensitive: false,
            },
          },
        });

        const oauthServer = await McpServerModel.create({
          name: "shared-client-credentials-cache-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: user.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "shared-client-credentials-cache-server__list_projects",
          description: "List projects",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: oauthServer.id,
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              access_token: createJwt({ exp: futureExpSeconds() }),
              expires_in: 3600,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        await mcpClient.executeToolCallForOwner(
          {
            id: "call_client_credentials_cache_1",
            name: "shared-client-credentials-cache-server__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );
        await mcpClient.executeToolCallForOwner(
          {
            id: "call_client_credentials_cache_2",
            name: "shared-client-credentials-cache-server__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );

        expect(fetchMock).toHaveBeenCalledTimes(1);
        fetchMock.mockRestore();
      });

      test("omits audience when the shared credential does not provide one", async ({
        makeUser,
      }) => {
        const user = await makeUser({
          email: "client-credentials-no-audience@example.com",
        });

        const secret = await secretManager().createSecret(
          {
            client_id: "shared-client-id",
            client_secret: "shared-client-secret",
          },
          "client-credentials-no-audience-secret",
        );

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "shared-client-credentials-no-audience-server",
          serverType: "remote",
          serverUrl: "https://api.example.com/mcp/",
          oauthConfig: {
            name: "Shared Client Credentials No Audience",
            server_url: "https://api.example.com/mcp/",
            grant_type: "client_credentials",
            client_id: "",
            redirect_uris: [],
            scopes: [],
            default_scopes: [],
            supports_resource_metadata: false,
            token_endpoint: "https://auth.example.com/oauth/token",
          },
          userConfig: {
            client_id: {
              type: "string",
              title: "Client ID",
              description: "Client ID",
              required: true,
              sensitive: false,
            },
            client_secret: {
              type: "string",
              title: "Client Secret",
              description: "Client Secret",
              required: true,
              sensitive: true,
            },
            audience: {
              type: "string",
              title: "Audience",
              description: "Audience",
              required: false,
              sensitive: false,
            },
          },
        });

        const oauthServer = await McpServerModel.create({
          name: "shared-client-credentials-no-audience-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: user.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "shared-client-credentials-no-audience-server__list_projects",
          description: "List projects",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: oauthServer.id,
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response(
            JSON.stringify({
              access_token: createJwt({ exp: futureExpSeconds() }),
              expires_in: 3600,
            }),
            {
              status: 200,
              headers: { "Content-Type": "application/json" },
            },
          ),
        );

        mockCallTool.mockResolvedValue({
          content: [{ type: "text", text: "ok" }],
          isError: false,
        });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_client_credentials_no_audience_1",
            name: "shared-client-credentials-no-audience-server__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );

        expect(result.isError).toBe(false);
        expect(fetchMock).toHaveBeenCalledTimes(1);
        const requestBody = fetchMock.mock.calls[0]?.[1]
          ?.body as URLSearchParams;
        expect(requestBody.get("grant_type")).toBe("client_credentials");
        expect(requestBody.get("audience")).toBeNull();

        fetchMock.mockRestore();
      });

      test("retries with a fresh token after the upstream MCP call returns UnauthorizedError", async ({
        makeUser,
      }) => {
        const user = await makeUser({
          email: "client-credentials-retry@example.com",
        });

        const secret = await secretManager().createSecret(
          {
            client_id: "shared-client-id",
            client_secret: "shared-client-secret",
            audience: "https://service.example.com",
          },
          "client-credentials-retry-secret",
        );

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "shared-client-credentials-retry-server",
          serverType: "remote",
          serverUrl: "https://api.example.com/mcp/",
          oauthConfig: {
            name: "Shared Client Credentials Retry",
            server_url: "https://api.example.com/mcp/",
            grant_type: "client_credentials",
            client_id: "",
            redirect_uris: [],
            scopes: [],
            default_scopes: [],
            supports_resource_metadata: false,
            token_endpoint: "https://auth.example.com/oauth/token",
          },
          userConfig: {
            client_id: {
              type: "string",
              title: "Client ID",
              description: "Client ID",
              required: true,
              sensitive: false,
            },
            client_secret: {
              type: "string",
              title: "Client Secret",
              description: "Client Secret",
              required: true,
              sensitive: true,
            },
            audience: {
              type: "string",
              title: "Audience",
              description: "Audience",
              required: false,
              sensitive: false,
            },
          },
        });

        const oauthServer = await McpServerModel.create({
          name: "shared-client-credentials-retry-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: user.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "shared-client-credentials-retry-server__list_projects",
          description: "List projects",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: oauthServer.id,
        });

        const firstAccessToken = createJwt({ exp: futureExpSeconds() });
        const secondAccessToken = createJwt({ exp: futureExpSeconds(7200) });
        const fetchMock = vi
          .spyOn(globalThis, "fetch")
          .mockResolvedValueOnce(
            new Response(
              JSON.stringify({
                access_token: firstAccessToken,
                expires_in: 3600,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          )
          .mockResolvedValueOnce(
            new Response(
              JSON.stringify({
                access_token: secondAccessToken,
                expires_in: 3600,
              }),
              {
                status: 200,
                headers: { "Content-Type": "application/json" },
              },
            ),
          );

        const { UnauthorizedError } = await import(
          "@modelcontextprotocol/sdk/client/auth.js"
        );
        mockCallTool
          .mockRejectedValueOnce(new UnauthorizedError())
          .mockResolvedValueOnce({
            content: [{ type: "text", text: "ok" }],
            isError: false,
          });

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_client_credentials_retry_1",
            name: "shared-client-credentials-retry-server__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );

        expect(result.isError).toBe(false);
        expect(fetchMock).toHaveBeenCalledTimes(2);
        expect(mockCallTool).toHaveBeenCalledTimes(2);

        const { StreamableHTTPClientTransport } = await import(
          "@modelcontextprotocol/sdk/client/streamableHttp.js"
        );
        const transportCalls = vi.mocked(StreamableHTTPClientTransport).mock
          .calls;
        const firstHeaders =
          transportCalls[0]?.[1]?.requestInit?.headers instanceof Headers
            ? transportCalls[0][1].requestInit.headers
            : new Headers(transportCalls[0]?.[1]?.requestInit?.headers);
        const secondHeaders =
          transportCalls[1]?.[1]?.requestInit?.headers instanceof Headers
            ? transportCalls[1][1].requestInit.headers
            : new Headers(transportCalls[1]?.[1]?.requestInit?.headers);
        expect(firstHeaders.get("Authorization")).toBe(
          `Bearer ${firstAccessToken}`,
        );
        expect(secondHeaders.get("Authorization")).toBe(
          `Bearer ${secondAccessToken}`,
        );

        const updatedSecret = await secretManager().getSecret(secret.id);
        expect(updatedSecret?.secret).toMatchObject({
          access_token: secondAccessToken,
        });

        fetchMock.mockRestore();
      });

      test("includes the token endpoint in client credential exchange failures", async ({
        makeUser,
      }) => {
        const user = await makeUser({
          email: "client-credentials-error@example.com",
        });

        const secret = await secretManager().createSecret(
          {
            client_id: "shared-client-id",
            client_secret: "shared-client-secret",
          },
          "client-credentials-error-secret",
        );

        const oauthCatalog = await InternalMcpCatalogModel.create({
          name: "shared-client-credentials-error-server",
          serverType: "remote",
          serverUrl: "https://api.example.com/mcp/",
          oauthConfig: {
            name: "Shared Client Credentials Error",
            server_url: "https://api.example.com/mcp/",
            grant_type: "client_credentials",
            client_id: "",
            redirect_uris: [],
            scopes: [],
            default_scopes: [],
            supports_resource_metadata: false,
            token_endpoint: "https://auth.example.com/oauth/token",
          },
          userConfig: {
            client_id: {
              type: "string",
              title: "Client ID",
              description: "Client ID",
              required: true,
              sensitive: false,
            },
            client_secret: {
              type: "string",
              title: "Client Secret",
              description: "Client Secret",
              required: true,
              sensitive: true,
            },
          },
        });

        const oauthServer = await McpServerModel.create({
          name: "shared-client-credentials-error-server",
          catalogId: oauthCatalog.id,
          secretId: secret.id,
          serverType: "remote",
          ownerId: user.id,
        });

        const tool = await ToolModel.createToolIfNotExists({
          name: "shared-client-credentials-error-server__list_projects",
          description: "List projects",
          parameters: {},
          catalogId: oauthCatalog.id,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: oauthServer.id,
        });

        const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
          new Response("invalid_client", {
            status: 401,
            headers: { "Content-Type": "text/plain" },
          }),
        );

        const result = await mcpClient.executeToolCallForOwner(
          {
            id: "call_client_credentials_error_1",
            name: "shared-client-credentials-error-server__list_projects",
            arguments: {},
          },
          agentOwner(agentId),
          {
            tokenId: "token-1",
            teamId: null,
            isOrganizationToken: false,
            userId: user.id,
          },
        );

        expect(result.isError).toBe(true);
        expect(result.error).toContain(
          "Client credentials token request to https://auth.example.com/oauth/token failed: 401 invalid_client",
        );

        fetchMock.mockRestore();
      });
    });

    describe("_meta and structuredContent passthrough", () => {
      test("passes _meta from callTool result into CommonToolResult", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__meta_tool",
          description: "Tool with meta",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        const toolMeta = { ui: { resourceUri: "mcp://widget/stats" } };
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "result" }],
          isError: false,
          _meta: toolMeta,
        });

        const toolCall = {
          id: "call_meta_1",
          name: "github-mcp-server__meta_tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        expect(result.isError).toBe(false);
        expect(result._meta).toEqual(toolMeta);
      });

      test("passes structuredContent from callTool result into CommonToolResult", async () => {
        const tool = await ToolModel.createToolIfNotExists({
          name: "github-mcp-server__structured_tool",
          description: "Tool with structured content",
          parameters: {},
          catalogId,
        });

        await AgentToolModel.create(agentId, tool.id, {
          mcpServerId: mcpServerId,
        });

        const structured = { dashboard: { widgets: ["chart", "table"] } };
        mockCallTool.mockResolvedValueOnce({
          content: [{ type: "text", text: "ok" }],
          isError: false,
          structuredContent: structured,
        });

        const toolCall = {
          id: "call_structured_1",
          name: "github-mcp-server__structured_tool",
          arguments: {},
        };

        const result = await mcpClient.executeToolCallForOwner(
          toolCall,
          agentOwner(agentId),
        );

        expect(result.isError).toBe(false);
        expect(result.structuredContent).toEqual(structured);
      });
    });
  });
});

function createJwt(payload: Record<string, unknown>): string {
  return [
    base64UrlEncode({ alg: "none", typ: "JWT" }),
    base64UrlEncode(payload),
    "",
  ].join(".");
}

function base64UrlEncode(value: unknown): string {
  return Buffer.from(JSON.stringify(value)).toString("base64url");
}

function futureExpSeconds(secondsFromNow: number = 3600): number {
  return Math.floor(Date.now() / 1000) + secondsFromNow;
}

describe("connectAndGetTools network egress enforcement", () => {
  test("blocks a remote server whose host is forbidden by its environment policy", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    const org = await makeOrganization();
    const env = await EnvironmentModel.create({
      organizationId: org.id,
      name: "locked",
      networkPolicy: {
        egressMode: "restricted",
        domainPreset: "none",
        allowedDomains: ["allowed.example.com"],
        allowedCidrs: [],
      },
    });
    // Seeded directly via the model (grandfathered): a remote server pointing at
    // a host the policy forbids, as if the env policy was tightened after it was
    // created. The create/edit-time check never re-ran for it.
    const catalogItem = await makeInternalMcpCatalog({
      organizationId: org.id,
      environmentId: env.id,
      serverType: "remote",
      serverUrl: "https://blocked.example.com/mcp",
    });

    mockConnect.mockClear();

    await expect(
      mcpClient.connectAndGetTools({
        catalogItem,
        mcpServerId: randomUUID(),
        secrets: {},
      }),
    ).rejects.toThrow(/not permitted by the "locked" environment/);

    // The guard fails the call before any connection is attempted.
    expect(mockConnect).not.toHaveBeenCalled();
  });

  test("blocks a tool call (the chat/gateway path) on a forbidden remote host", async ({
    makeOrganization,
    makeAgent,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    // Chat-UI tool calls run through the MCP Gateway, which executes via
    // executeToolCallForOwner — the same getTransport chokepoint as inspection.
    // This proves that path is guarded too (chat-mcp-client needs no change).
    const org = await makeOrganization();
    const env = await EnvironmentModel.create({
      organizationId: org.id,
      name: "locked",
      networkPolicy: {
        egressMode: "restricted",
        domainPreset: "none",
        allowedDomains: ["allowed.example.com"],
        allowedCidrs: [],
      },
    });
    const catalogItem = await makeInternalMcpCatalog({
      organizationId: org.id,
      environmentId: env.id,
      serverType: "remote",
      serverUrl: "https://blocked.example.com/mcp",
    });
    const mcpServer = await makeMcpServer({
      catalogId: catalogItem.id,
      scope: "org",
    });
    // The agent must share the catalog's environment, otherwise environment
    // isolation blocks the call before the network-policy guard under test.
    const agent = await makeAgent({
      organizationId: org.id,
      environmentId: env.id,
    });
    const tool = await ToolModel.createToolIfNotExists({
      name: "blocked-remote__do_thing",
      description: "do thing",
      parameters: {},
      catalogId: catalogItem.id,
    });
    await AgentToolModel.create(agent.id, tool.id, {
      mcpServerId: mcpServer.id,
    });

    mockConnect.mockClear();

    const result = await mcpClient.executeToolCallForOwner(
      { id: "call_blocked", name: tool.name, arguments: {} },
      agentOwner(agent.id),
    );

    expect(result.isError).toBe(true);
    expect(result.error).toContain('not permitted by the "locked" environment');
    expect(mockConnect).not.toHaveBeenCalled();
  });
});

describe("pickInstallForCaller (caller-scoped install selection)", () => {
  // Pins the credential boundary readResource relies on: when a shared catalog
  // has several installs, connect only to one the caller can actually reach
  // (own personal → team → org). Never an arbitrary servers[0], which could be
  // another user's personal install and would read with that user's secrets.
  const pickInstallForCaller = (
    servers: unknown[],
    tokenAuth: TokenAuthContext | undefined,
  ): Promise<{ id: string } | undefined> =>
    (
      mcpClient as unknown as {
        pickInstallForCaller: (
          allServers: unknown[],
          tokenAuth: TokenAuthContext | undefined,
        ) => Promise<{ id: string } | undefined>;
      }
    ).pickInstallForCaller(servers, tokenAuth);

  const userToken = (
    userId: string,
    organizationId: string,
  ): TokenAuthContext => ({
    tokenId: randomUUID(),
    teamId: null,
    isOrganizationToken: false,
    isUserToken: true,
    userId,
    organizationId,
  });

  test("prefers the caller's own personal install over another user's", async ({
    makeOrganization,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const caller = await makeUser();
    const other = await makeUser();
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    // Another user's install FIRST: the pre-fix servers[0] returned this one.
    const otherInstall = await makeMcpServer({
      catalogId: catalog.id,
      ownerId: other.id,
      scope: "personal",
    });
    const ownInstall = await makeMcpServer({
      catalogId: catalog.id,
      ownerId: caller.id,
      scope: "personal",
    });

    const picked = await pickInstallForCaller(
      [otherInstall, ownInstall],
      userToken(caller.id, org.id),
    );

    expect(picked?.id).toBe(ownInstall.id);
  });

  test("fails closed when only another user's personal install exists", async ({
    makeOrganization,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const caller = await makeUser();
    const other = await makeUser();
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    const otherInstall = await makeMcpServer({
      catalogId: catalog.id,
      ownerId: other.id,
      scope: "personal",
    });

    const picked = await pickInstallForCaller(
      [otherInstall],
      userToken(caller.id, org.id),
    );

    expect(picked).toBeUndefined();
  });

  test("selects a team install when the caller belongs to that team", async ({
    makeOrganization,
    makeUser,
    makeTeam,
    makeTeamMember,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const caller = await makeUser();
    const other = await makeUser();
    const team = await makeTeam(org.id, caller.id);
    await makeTeamMember(team.id, caller.id);
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    // Decoy first, so a naive servers[0] would miss the team install.
    const otherInstall = await makeMcpServer({
      catalogId: catalog.id,
      ownerId: other.id,
      scope: "personal",
    });
    const teamInstall = await makeMcpServer({
      catalogId: catalog.id,
      teamId: team.id,
      scope: "team",
    });

    const picked = await pickInstallForCaller(
      [otherInstall, teamInstall],
      userToken(caller.id, org.id),
    );

    expect(picked?.id).toBe(teamInstall.id);
  });

  test("falls back to an org-scoped install when the caller has no personal or team install", async ({
    makeOrganization,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const caller = await makeUser();
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    const orgInstall = await makeMcpServer({
      catalogId: catalog.id,
      ownerId: null,
      scope: "org",
    });

    const picked = await pickInstallForCaller(
      [orgInstall],
      userToken(caller.id, org.id),
    );

    expect(picked?.id).toBe(orgInstall.id);
  });

  // Team-scoped API tokens (routes/mcp-gateway.ts sets tokenAuth.teamId, no
  // userId) take the separate `if (tokenAuth?.teamId)` branch — untouched by
  // every test above, which all use a userId-bearing token.
  const teamToken = (
    teamId: string,
    organizationId: string,
  ): TokenAuthContext => ({
    tokenId: randomUUID(),
    teamId,
    isOrganizationToken: false,
    organizationId,
  });

  test("selects the caller's team install for a team-scoped token (no userId)", async ({
    makeOrganization,
    makeTeam,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const creator = await makeUser();
    const team = await makeTeam(org.id, creator.id);
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    const otherTeam = await makeTeam(org.id, creator.id);
    const otherTeamInstall = await makeMcpServer({
      catalogId: catalog.id,
      teamId: otherTeam.id,
      scope: "team",
    });
    const teamInstall = await makeMcpServer({
      catalogId: catalog.id,
      teamId: team.id,
      scope: "team",
    });

    const picked = await pickInstallForCaller(
      [otherTeamInstall, teamInstall],
      teamToken(team.id, org.id),
    );

    expect(picked?.id).toBe(teamInstall.id);
  });

  test("falls back to an org-scoped install for a team-scoped token when the team has no install", async ({
    makeOrganization,
    makeTeam,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const creator = await makeUser();
    const team = await makeTeam(org.id, creator.id);
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    const orgInstall = await makeMcpServer({
      catalogId: catalog.id,
      ownerId: null,
      scope: "org",
    });

    const picked = await pickInstallForCaller(
      [orgInstall],
      teamToken(team.id, org.id),
    );

    expect(picked?.id).toBe(orgInstall.id);
  });

  test("fails closed for a team-scoped token when no team or org install is reachable", async ({
    makeOrganization,
    makeTeam,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
  }) => {
    const org = await makeOrganization();
    const creator = await makeUser();
    const team = await makeTeam(org.id, creator.id);
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

    const otherTeam = await makeTeam(org.id, creator.id);
    const otherTeamInstall = await makeMcpServer({
      catalogId: catalog.id,
      teamId: otherTeam.id,
      scope: "team",
    });

    const picked = await pickInstallForCaller(
      [otherTeamInstall],
      teamToken(team.id, org.id),
    );

    expect(picked).toBeUndefined();
  });
});

describe("readResource (assignment + all-tools dynamic access, end to end)", () => {
  // Exercises findMcpServerForResource/readResource as assembled, not just its
  // two sub-components (resolveDynamicToolByUiResource, pickInstallForCaller)
  // in isolation — through the real assignment lookup, the dynamic-access
  // fallback, catalog/install resolution, and the actual client.readResource
  // call, the same path the frontend AppRenderer drives.
  const RESOURCE_URI = "ui://widget/dashboard.html";
  const RESOURCE_CONTENTS = {
    contents: [
      { uri: RESOURCE_URI, mimeType: "text/html", text: "<html>ok</html>" },
    ],
  };

  const dynamicToken = (
    userId: string,
    organizationId: string,
  ): TokenAuthContext => ({
    tokenId: randomUUID(),
    teamId: null,
    isOrganizationToken: false,
    isUserToken: true,
    userId,
    organizationId,
  });

  test("reads a UI resource for a tool assigned via agent_tools (pre-existing success path)", async ({
    makeOrganization,
    makeUser,
    makeMember,
    makeInternalMcpCatalog,
    makeMcpServer,
    makeAgent,
    makeTool,
    makeAgentTool,
  }) => {
    const org = await makeOrganization();
    const user = await makeUser();
    await makeMember(user.id, org.id, { role: "admin" });
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
    const server = await makeMcpServer({ catalogId: catalog.id, scope: "org" });
    const agent = await makeAgent({ organizationId: org.id });
    const tool = await makeTool({
      name: "widget__open",
      catalogId: catalog.id,
      meta: { _meta: { ui: { resourceUri: RESOURCE_URI } } },
    });
    await makeAgentTool(agent.id, tool.id, { mcpServerId: server.id });

    mockConnect.mockResolvedValue(undefined);
    mockReadResource.mockResolvedValueOnce(RESOURCE_CONTENTS);

    // A real caller always carries session tokenAuth; pickInstallForCaller
    // (the caller-scoped install selector) fails closed without one, even on
    // this assignment-scoped path — it doesn't only gate the dynamic fallback.
    const result = await mcpClient.readResource(
      RESOURCE_URI,
      agent.id,
      dynamicToken(user.id, org.id),
    );

    expect(result).toEqual(RESOURCE_CONTENTS);
    expect(mockReadResource).toHaveBeenCalledWith({ uri: RESOURCE_URI });
  });

  test("falls back to dynamic access to read a UI resource for an unassigned tool in all-tools mode", async ({
    makeOrganization,
    makeUser,
    makeMember,
    makeInternalMcpCatalog,
    makeMcpServer,
    makeAgent,
    makeTool,
  }) => {
    const org = await makeOrganization();
    const user = await makeUser();
    await makeMember(user.id, org.id, { role: "admin" });
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
    await makeMcpServer({ catalogId: catalog.id, scope: "org" });
    const agent = await makeAgent({
      organizationId: org.id,
      accessAllTools: true,
    });
    // Deliberately no makeAgentTool — this tool is unassigned and reachable
    // only through dynamic access.
    await makeTool({
      name: "widget__open",
      catalogId: catalog.id,
      meta: { _meta: { ui: { resourceUri: RESOURCE_URI } } },
    });

    mockConnect.mockResolvedValue(undefined);
    mockReadResource.mockResolvedValueOnce(RESOURCE_CONTENTS);

    const result = await mcpClient.readResource(
      RESOURCE_URI,
      agent.id,
      dynamicToken(user.id, org.id),
    );

    expect(result).toEqual(RESOURCE_CONTENTS);
    expect(mockReadResource).toHaveBeenCalledWith({ uri: RESOURCE_URI });
  });

  test("fails closed for an unassigned tool when the agent is not in all-tools mode", async ({
    makeOrganization,
    makeUser,
    makeMember,
    makeInternalMcpCatalog,
    makeMcpServer,
    makeAgent,
    makeTool,
  }) => {
    const org = await makeOrganization();
    const user = await makeUser();
    await makeMember(user.id, org.id, { role: "admin" });
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
    await makeMcpServer({ catalogId: catalog.id, scope: "org" });
    // accessAllTools defaults off — the dynamic-access fallback must not fire.
    const agent = await makeAgent({ organizationId: org.id });
    await makeTool({
      name: "widget__open",
      catalogId: catalog.id,
      meta: { _meta: { ui: { resourceUri: RESOURCE_URI } } },
    });

    await expect(
      mcpClient.readResource(
        RESOURCE_URI,
        agent.id,
        dynamicToken(user.id, org.id),
      ),
    ).rejects.toThrow(/Resource not found/);
    expect(mockReadResource).not.toHaveBeenCalled();
  });
});
