feat: add SolidTime MCP server for time tracking integration
Implements a Model Context Protocol server that exposes SolidTime's time tracking API as 22+ tools for use with Claude, Cursor, and other MCP-compatible clients. Supports stdio and HTTP transport modes, Docker deployment, and self-hosted SolidTime instances. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
147
src/tools/project-members.ts
Normal file
147
src/tools/project-members.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { ApiClient } from "../api-client.js";
|
||||
import { API_PATHS } from "../constants.js";
|
||||
import { formatCurrency } from "../formatting.js";
|
||||
import type { ProjectMember, PaginatedResponse } from "../types.js";
|
||||
|
||||
export function registerProjectMemberTools(server: McpServer, api: ApiClient, orgId: string) {
|
||||
server.registerTool(
|
||||
"solidtime_list_project_members",
|
||||
{
|
||||
title: "List Project Members",
|
||||
description: "List all members assigned to a specific project.",
|
||||
inputSchema: {
|
||||
project_id: z.string().uuid().describe("Project UUID"),
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params) => {
|
||||
const result = await api.get<PaginatedResponse<ProjectMember>>(
|
||||
API_PATHS.projectMembers(orgId, params.project_id)
|
||||
);
|
||||
const members = result.data ?? [];
|
||||
if (members.length === 0) {
|
||||
return { content: [{ type: "text", text: "No project members found." }] };
|
||||
}
|
||||
|
||||
const lines = members.map((m) => formatProjectMember(m));
|
||||
lines.unshift(`Found ${members.length} project members:\n`);
|
||||
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"solidtime_add_project_member",
|
||||
{
|
||||
title: "Add Project Member",
|
||||
description: "Add a member to a project with an optional custom billable rate.",
|
||||
inputSchema: {
|
||||
project_id: z.string().uuid().describe("Project UUID"),
|
||||
member_id: z.string().uuid().describe("Member UUID to add"),
|
||||
billable_rate: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe("Custom billable rate in cents for this member on this project"),
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
member_id: params.member_id,
|
||||
};
|
||||
if (params.billable_rate !== undefined) body.billable_rate = params.billable_rate;
|
||||
|
||||
const result = await api.post<{ data: ProjectMember }>(
|
||||
API_PATHS.projectMembers(orgId, params.project_id),
|
||||
body
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Project member added.\n\n${formatProjectMember(result.data)}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"solidtime_update_project_member",
|
||||
{
|
||||
title: "Update Project Member",
|
||||
description: "Update a project member's billable rate.",
|
||||
inputSchema: {
|
||||
id: z.string().uuid().describe("Project member UUID to update"),
|
||||
billable_rate: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe("New billable rate in cents for this project member"),
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params) => {
|
||||
const result = await api.put<{ data: ProjectMember }>(
|
||||
API_PATHS.projectMember(orgId, params.id),
|
||||
{ billable_rate: params.billable_rate }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Project member updated.\n\n${formatProjectMember(result.data)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"solidtime_remove_project_member",
|
||||
{
|
||||
title: "Remove Project Member",
|
||||
description: "Remove a member from a project.",
|
||||
inputSchema: {
|
||||
id: z.string().uuid().describe("Project member UUID to remove"),
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: true,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params) => {
|
||||
await api.delete(API_PATHS.projectMember(orgId, params.id));
|
||||
return { content: [{ type: "text", text: `Project member ${params.id} removed.` }] };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function formatProjectMember(pm: ProjectMember): string {
|
||||
const parts = [
|
||||
`ID: ${pm.id}`,
|
||||
`Member ID: ${pm.member_id}`,
|
||||
`Project ID: ${pm.project_id}`,
|
||||
];
|
||||
if (pm.billable_rate !== null) {
|
||||
parts.push(`Billable Rate: ${formatCurrency(pm.billable_rate)}/h`);
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user