Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,6 @@ QSTASH_TOKEN=
# QStash webhook signature verification keys (used to verify webhook requests)
QSTASH_CURRENT_SIGNING_KEY=
QSTASH_NEXT_SIGNING_KEY=

# MCP API Key for Claude Code
SUPERSET_MCP_API_KEY=
309 changes: 309 additions & 0 deletions apps/api/MCP_TOOLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
# MCP Server Tools

Superset MCP server exposing tools for task management and device orchestration.

## Authentication

API key passed via `X-API-Key` header. Key encodes:
- `userId` - who is making the request
- `organizationId` - which org context
- `defaultDeviceId` - default target for device commands (usually caller's own device)

## Device Targeting

Device commands can target **any device in the organization**:
- If `deviceId` not specified, defaults to `defaultDeviceId` from API key
- Any org member can run commands on any org device (permissions can be added later)
- Device must be online (heartbeat within last 60s) to receive commands

## Tool Categories

### Task Tools (Cloud - Immediate Execution)

#### `create_task`
Create a new task in the organization.

```typescript
const createTaskInput = z.object({
title: z.string().min(1).describe("Task title"),
description: z.string().optional().describe("Task description (markdown)"),
priority: z.enum(["urgent", "high", "medium", "low", "none"]).default("none").describe("Task priority"),
assigneeId: z.string().uuid().optional().describe("User ID to assign the task to"),
statusId: z.string().uuid().optional().describe("Status ID (defaults to first backlog status)"),
labels: z.array(z.string()).optional().describe("Array of label strings"),
dueDate: z.string().datetime().optional().describe("Due date in ISO format"),
estimate: z.number().int().positive().optional().describe("Estimate in points/hours"),
});

const createTaskOutput = z.object({
id: z.string().uuid(),
slug: z.string(),
title: z.string(),
// ... full task object
});
```

#### `update_task`
Update an existing task.

```typescript
const updateTaskInput = z.object({
taskId: z.string().describe("Task ID or slug"),
title: z.string().min(1).optional().describe("New title"),
description: z.string().optional().describe("New description"),
priority: z.enum(["urgent", "high", "medium", "low", "none"]).optional(),
assigneeId: z.string().uuid().nullable().optional().describe("New assignee (null to unassign)"),
statusId: z.string().uuid().optional().describe("New status ID"),
labels: z.array(z.string()).optional().describe("Replace labels"),
dueDate: z.string().datetime().nullable().optional().describe("New due date (null to clear)"),
estimate: z.number().int().positive().nullable().optional(),
});

const updateTaskOutput = z.object({
id: z.string().uuid(),
slug: z.string(),
// ... updated task object
});
```

#### `list_tasks`
List tasks with optional filters.

```typescript
const listTasksInput = z.object({
statusId: z.string().uuid().optional().describe("Filter by status ID"),
statusType: z.enum(["backlog", "unstarted", "started", "completed", "canceled"]).optional().describe("Filter by status type"),
assigneeId: z.string().uuid().optional().describe("Filter by assignee"),
assignedToMe: z.boolean().optional().describe("Filter to tasks assigned to current user"),
priority: z.enum(["urgent", "high", "medium", "low", "none"]).optional(),
search: z.string().optional().describe("Search in title/description"),
limit: z.number().int().min(1).max(100).default(50),
offset: z.number().int().min(0).default(0),
});

const listTasksOutput = z.object({
tasks: z.array(taskSchema),
total: z.number(),
hasMore: z.boolean(),
});
```

#### `get_task`
Get a single task by ID or slug.

```typescript
const getTaskInput = z.object({
taskId: z.string().describe("Task ID (uuid) or slug"),
});

const getTaskOutput = taskSchema; // Full task with relations
```

#### `delete_task`
Soft delete a task.

```typescript
const deleteTaskInput = z.object({
taskId: z.string().describe("Task ID or slug"),
});

const deleteTaskOutput = z.object({
success: z.boolean(),
deletedAt: z.string().datetime(),
});
```

---

### Organization Tools (Cloud - Immediate Execution)

#### `list_members`
List members in the organization.

```typescript
const listMembersInput = z.object({
search: z.string().optional().describe("Search by name or email"),
limit: z.number().int().min(1).max(100).default(50),
});

const listMembersOutput = z.object({
members: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
image: z.string().url().nullable(),
role: z.enum(["owner", "admin", "member"]),
})),
});
```

#### `list_task_statuses`
List available task statuses for the organization.

```typescript
const listTaskStatusesInput = z.object({});

const listTaskStatusesOutput = z.object({
statuses: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
color: z.string(),
type: z.enum(["backlog", "unstarted", "started", "completed", "canceled"]),
position: z.number(),
})),
});
```

---

### Device Tools (Routed to Desktop Executor)

These tools write to `agent_commands` table and poll for results.

#### `list_devices`
List online devices in the organization.

```typescript
const listDevicesInput = z.object({
includeOffline: z.boolean().default(false).describe("Include recently offline devices"),
});

const listDevicesOutput = z.object({
devices: z.array(z.object({
deviceId: z.string(),
deviceName: z.string(),
deviceType: z.enum(["desktop", "mobile", "web"]),
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(),
})),
});
```

#### `list_workspaces`
List all workspaces/worktrees on a device.

```typescript
const listWorkspacesInput = z.object({
deviceId: z.string().optional().describe("Target device (defaults to caller's device)"),
});

const listWorkspacesOutput = z.object({
workspaces: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
path: z.string(),
branch: z.string(),
isActive: z.boolean(),
repositoryId: z.string().uuid().nullable(),
})),
});
```

#### `get_current_workspace`
Get the currently active workspace on a device.

```typescript
const getCurrentWorkspaceInput = z.object({
deviceId: z.string().optional(),
});

const getCurrentWorkspaceOutput = z.object({
workspace: z.object({
id: z.string().uuid(),
name: z.string(),
path: z.string(),
branch: z.string(),
repositoryId: z.string().uuid().nullable(),
// Additional context
uncommittedChanges: z.number().int(),
currentTask: taskSchema.nullable(),
}).nullable(),
});
```
Comment on lines +204 to +224

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation doesn't match implementation.

The docs reference get_current_workspace but the implementation uses get_app_context. Similarly, create_worktree in docs vs create_workspace in code. The list_tasks output schema shows total but the implementation returns count.

🤖 Prompt for AI Agents
In `@apps/api/MCP_TOOLS.md` around lines 204 - 224, The documentation is out of
sync with the code: update the docs to match the actual function and schema
names used in code (or vice versa). Specifically, replace references to
get_current_workspace with get_app_context, replace create_worktree with
create_workspace, and change the list_tasks output schema to use count instead
of total (or update the implementation to return total if you prefer). Ensure
the zod schemas in the docs (e.g., getCurrentWorkspaceInput/output and the
list_tasks output) reflect the exact field names and nullable types used by the
functions get_app_context, create_workspace, and list_tasks so names and fields
match between docs and implementation.


#### `create_worktree`
Create a new git worktree workspace.

```typescript
const createWorktreeInput = z.object({
deviceId: z.string().optional(),
name: z.string().optional().describe("Workspace name (auto-generated if not provided)"),
branchName: z.string().optional().describe("Branch name (auto-generated if not provided)"),
baseBranch: z.string().optional().describe("Branch to create from (defaults to main)"),
taskId: z.string().optional().describe("Task ID to associate with workspace"),
});

const createWorktreeOutput = z.object({
workspace: z.object({
id: z.string().uuid(),
name: z.string(),
path: z.string(),
branch: z.string(),
}),
});
```

#### `switch_workspace`
Switch to a different workspace.

```typescript
const switchWorkspaceInput = z.object({
deviceId: z.string().optional(),
workspaceId: z.string().uuid().optional().describe("Workspace ID to switch to"),
workspaceName: z.string().optional().describe("Workspace name to switch to"),
});

const switchWorkspaceOutput = z.object({
success: z.boolean(),
workspace: z.object({
id: z.string().uuid(),
name: z.string(),
path: z.string(),
branch: z.string(),
}),
});
```

---

## Shared Schemas

```typescript
const taskSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
title: z.string(),
description: z.string().nullable(),
priority: z.enum(["urgent", "high", "medium", "low", "none"]),
status: z.object({
id: z.string().uuid(),
name: z.string(),
color: z.string(),
type: z.enum(["backlog", "unstarted", "started", "completed", "canceled"]),
}),
assignee: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string(),
image: z.string().nullable(),
}).nullable(),
labels: z.array(z.string()),
estimate: z.number().nullable(),
dueDate: z.string().datetime().nullable(),
branch: z.string().nullable(),
prUrl: z.string().url().nullable(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
```

---

## Implementation Notes

1. **Validation**: All inputs validated with Zod, converted to JSON Schema for MCP
2. **Device routing**: Device tools check `canRunTool(deviceType, toolName)` before routing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Implementation note references non-existent function.

"Device tools check canRunTool(deviceType, toolName) before routing" — this function doesn't exist in the implementation. Either add it or remove this note.

🤖 Prompt for AI Agents
In `@apps/api/MCP_TOOLS.md` at line 307, The docs reference a non-existent helper
canRunTool(deviceType, toolName); either remove that line or implement the
helper and wire it into routing: add a function named canRunTool(deviceType,
toolName) that consults the existing tool registry (e.g., DeviceToolRegistry or
the module that maps device types to supported tools) and returns a boolean,
then call it from the device routing logic (e.g., routeDeviceTool or
DeviceRouter.handleRequest) to gate routing decisions; update the docs if you
remove the note or ensure the new function name and signature match the
documentation.

3. **Timeouts**: Device commands have 30s default timeout, configurable per-call
4. **Auth**: API key required, encodes user/org/device context
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"@electric-sql/client": "https://pkg.pr.new/@electric-sql/client@3724",
"@linear/sdk": "^68.1.0",
"@modelcontextprotocol/sdk": "^1.25.3",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify the MCP SDK version is current and stable.The latest published version is 1.25.2, but you're specifying ^1.25.3. This version does not exist on npm yet. Update to the latest available version.

-		"@modelcontextprotocol/sdk": "^1.25.3",
+		"@modelcontextprotocol/sdk": "^1.25.2",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@modelcontextprotocol/sdk": "^1.25.3",
"@modelcontextprotocol/sdk": "^1.25.2",
🤖 Prompt for AI Agents
In `@apps/api/package.json` at line 16, The package.json dependency for
`@modelcontextprotocol/sdk` points to a non-existent version (^1.25.3); update the
dependency entry for "@modelcontextprotocol/sdk" to the latest published version
(e.g., "1.25.2" or "^1.25.2") in package.json and then run your package manager
(npm install or yarn install) to refresh lockfiles and ensure the project uses
the valid SDK release.

"@octokit/app": "^16.1.2",
"@octokit/rest": "^22.0.1",
"@octokit/webhooks": "^14.2.0",
Expand Down
16 changes: 15 additions & 1 deletion apps/api/src/app/api/electric/[...path]/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { db } from "@superset/db/client";
import {
agentCommands,
devicePresence,
invitations,
members,
organizations,
Expand All @@ -18,7 +20,9 @@ export type AllowedTable =
| "auth.members"
| "auth.organizations"
| "auth.users"
| "auth.invitations";
| "auth.invitations"
| "device_presence"
| "agent_commands";

interface WhereClause {
fragment: string;
Expand Down Expand Up @@ -89,6 +93,16 @@ export async function buildWhereClause(
return { fragment, params: [organizationId] };
}

case "device_presence":
return build(
devicePresence,
devicePresence.organizationId,
organizationId,
);

case "agent_commands":
return build(agentCommands, agentCommands.organizationId, organizationId);

default:
return null;
}
Expand Down
Loading