diff --git a/apps/api/MCP_TOOLS.md b/apps/api/MCP_TOOLS.md index f6074babcf0..142469c8ec9 100644 --- a/apps/api/MCP_TOOLS.md +++ b/apps/api/MCP_TOOLS.md @@ -161,12 +161,10 @@ const listTaskStatusesOutput = z.object({ These tools write to `agent_commands` table and poll for results. #### `list_devices` -List online devices in the organization. +List registered devices in the organization. ```typescript -const listDevicesInput = z.object({ - includeOffline: z.boolean().default(false).describe("Include recently offline devices"), -}); +const listDevicesInput = z.object({}); const listDevicesOutput = z.object({ devices: z.array(z.object({ @@ -176,7 +174,6 @@ const listDevicesOutput = z.object({ ownerId: z.string().uuid().describe("User who owns this device"), ownerName: z.string().describe("Name of device owner"), lastSeenAt: z.string().datetime(), - isOnline: z.boolean(), })), }); ``` diff --git a/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts b/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts index 33ee354fb61..86467136eef 100644 --- a/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts +++ b/apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts @@ -370,7 +370,7 @@ async function fetchAgentContext({ mcpClient.callTool({ name: "list_task_statuses", arguments: {} }), mcpClient.callTool({ name: "list_devices", - arguments: { includeOffline: true }, + arguments: {}, }), ]); @@ -409,13 +409,12 @@ async function fetchAgentContext({ deviceName: string | null; ownerName: string | null; ownerEmail: string; - isOnline: boolean; }[]; } | null; if (devicesData?.devices?.length) { const lines = devicesData.devices.map( (d) => - `- ${d.deviceName ?? "Unknown"} (id: ${d.deviceId}, owner: ${d.ownerName ?? d.ownerEmail}, status: ${d.isOnline ? "online" : "offline"})`, + `- ${d.deviceName ?? "Unknown"} (id: ${d.deviceId}, owner: ${d.ownerName ?? d.ownerEmail})`, ); sections.push(`Devices:\n${lines.join("\n")}`); } diff --git a/apps/docs/content/docs/mcp.mdx b/apps/docs/content/docs/mcp.mdx index afa52a67af8..93f0b1ec346 100644 --- a/apps/docs/content/docs/mcp.mdx +++ b/apps/docs/content/docs/mcp.mdx @@ -248,7 +248,7 @@ API keys grant full access to your organization. Keep them secret and never comm | Tool | Description | |------|-------------| -| `list_devices` | List online devices in your organization | +| `list_devices` | List registered devices in your organization | | `list_projects` | List all projects on a device | | `get_app_context` | Get current app state (active workspace, pathname) | | `list_members` | List organization members | diff --git a/packages/cli/src/commands/devices/list/command.ts b/packages/cli/src/commands/devices/list/command.ts index 440ad1e2e4c..2e42440c9e4 100644 --- a/packages/cli/src/commands/devices/list/command.ts +++ b/packages/cli/src/commands/devices/list/command.ts @@ -1,15 +1,12 @@ -import { boolean, CLIError, command, table } from "@superset/cli-framework"; +import { CLIError, command, table } from "@superset/cli-framework"; export default command({ description: "List all devices in the org", - options: { - includeOffline: boolean().desc("Include offline devices"), - }, + options: {}, display: (data) => table(data as Record[], [ "deviceName", "deviceType", - "status", "lastSeen", ]), run: async () => { diff --git a/packages/mcp/src/tools/devices/list-devices/list-devices.test.ts b/packages/mcp/src/tools/devices/list-devices/list-devices.test.ts index 562b323d9d1..6ca08ce5d27 100644 --- a/packages/mcp/src/tools/devices/list-devices/list-devices.test.ts +++ b/packages/mcp/src/tools/devices/list-devices/list-devices.test.ts @@ -5,7 +5,7 @@ const getMcpContextMock = mock(() => ({ organizationId: "org-1" })); let fetchedDevices = [ { - deviceId: "device-online", + deviceId: "device-a", deviceName: "Ada's MacBook", deviceType: "desktop", lastSeenAt: new Date(Date.now() - 30_000), @@ -14,7 +14,7 @@ let fetchedDevices = [ ownerEmail: "ada@example.com", }, { - deviceId: "device-offline", + deviceId: "device-b", deviceName: "Grace's iPhone", deviceType: "mobile", lastSeenAt: new Date(Date.now() - 120_000), @@ -40,7 +40,12 @@ mock.module("@superset/db/client", () => ({ }, })); +// mock.module is process-global in bun test. Preserve every real export +// (notably executeOnDevice, which start-agent-session.test.ts imports after +// this file mounts its mocks) so sibling test files don't break. +const realUtils = await import("../../utils"); mock.module("../../utils", () => ({ + ...realUtils, getMcpContext: getMcpContextMock, })); @@ -61,7 +66,6 @@ type RegisteredToolHandler = ( ownerId: string; ownerName: string | null; ownerEmail: string; - isOnline: boolean; }>; }; }>; @@ -102,7 +106,7 @@ describe("list_devices MCP tool", () => { beforeEach(() => { fetchedDevices = [ { - deviceId: "device-online", + deviceId: "device-a", deviceName: "Ada's MacBook", deviceType: "desktop", lastSeenAt: new Date(Date.now() - 30_000), @@ -111,7 +115,7 @@ describe("list_devices MCP tool", () => { ownerEmail: "ada@example.com", }, { - deviceId: "device-offline", + deviceId: "device-b", deviceName: "Grace's iPhone", deviceType: "mobile", lastSeenAt: new Date(Date.now() - 120_000), @@ -124,22 +128,16 @@ describe("list_devices MCP tool", () => { selectMock.mockClear(); }); - it("registers input and output schemas that validate includeOffline and isOnline", async () => { + it("registers an output schema that validates the device list", async () => { const { config, handler } = createTool(); - const inputSchema = z.object(config.inputSchema); const outputSchema = z.object(config.outputSchema); - expect(inputSchema.parse({})).toEqual({ includeOffline: false }); - expect(inputSchema.parse({ includeOffline: true })).toEqual({ - includeOffline: true, - }); - - const result = await handler({ includeOffline: true }, {}); + const result = await handler({}, {}); expect(() => outputSchema.parse(result.structuredContent)).not.toThrow(); }); - it("returns only online devices by default", async () => { + it("returns every registered device regardless of lastSeenAt", async () => { const { handler } = createTool(); const result = await handler({}, {}); @@ -148,35 +146,23 @@ describe("list_devices MCP tool", () => { expect(selectMock).toHaveBeenCalledTimes(1); expect(result.structuredContent?.devices).toEqual([ { - deviceId: "device-online", + deviceId: "device-a", deviceName: "Ada's MacBook", deviceType: "desktop", lastSeenAt: expect.any(String), ownerId: "user-1", ownerName: "Ada", ownerEmail: "ada@example.com", - isOnline: true, + }, + { + deviceId: "device-b", + deviceName: "Grace's iPhone", + deviceType: "mobile", + lastSeenAt: expect.any(String), + ownerId: "user-2", + ownerName: "Grace", + ownerEmail: "grace@example.com", }, ]); }); - - it("includes offline devices when requested and marks them offline", async () => { - const { handler } = createTool(); - - const result = await handler({ includeOffline: true }, {}); - - expect(result.structuredContent?.devices).toHaveLength(2); - expect(result.structuredContent?.devices).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - deviceId: "device-online", - isOnline: true, - }), - expect.objectContaining({ - deviceId: "device-offline", - isOnline: false, - }), - ]), - ); - }); }); diff --git a/packages/mcp/src/tools/devices/list-devices/list-devices.ts b/packages/mcp/src/tools/devices/list-devices/list-devices.ts index dc06856f4c9..14feb8ad76f 100644 --- a/packages/mcp/src/tools/devices/list-devices/list-devices.ts +++ b/packages/mcp/src/tools/devices/list-devices/list-devices.ts @@ -5,20 +5,12 @@ import { desc, eq } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; -const DEVICE_ONLINE_WINDOW_MS = 60_000; - export function register(server: McpServer) { server.registerTool( "list_devices", { - description: - "List devices in the organization. By default, only devices seen within the last 60 seconds are returned.", - inputSchema: { - includeOffline: z - .boolean() - .default(false) - .describe("Include devices that have not checked in recently"), - }, + description: "List registered devices in the organization.", + inputSchema: {}, outputSchema: { devices: z.array( z.object({ @@ -29,15 +21,12 @@ export function register(server: McpServer) { ownerId: z.string(), ownerName: z.string().nullable(), ownerEmail: z.string(), - isOnline: z.boolean(), }), ), }, }, - async (args, extra) => { + async (_args, extra) => { const ctx = getMcpContext(extra); - const includeOffline = args.includeOffline === true; - const onlineCutoff = Date.now() - DEVICE_ONLINE_WINDOW_MS; const devices = await db .select({ @@ -54,17 +43,10 @@ export function register(server: McpServer) { .where(eq(devicePresence.organizationId, ctx.organizationId)) .orderBy(desc(devicePresence.lastSeenAt)); - const result = devices - .map((d) => { - const isOnline = d.lastSeenAt.getTime() >= onlineCutoff; - - return { - ...d, - lastSeenAt: d.lastSeenAt.toISOString(), - isOnline, - }; - }) - .filter((device) => includeOffline || device.isOnline); + const result = devices.map((d) => ({ + ...d, + lastSeenAt: d.lastSeenAt.toISOString(), + })); return { structuredContent: { devices: result },