diff --git a/servers/bamboohr/README.md b/servers/bamboohr/README.md new file mode 100644 index 0000000..8576927 --- /dev/null +++ b/servers/bamboohr/README.md @@ -0,0 +1,227 @@ +# BambooHR MCP Server + +A complete Model Context Protocol (MCP) server for BambooHR with 47 tools and 18 React-based UI apps. + +## Features + +### 🔧 47 MCP Tools + +#### Employee Management (9 tools) +- `list_employees` - List all employees with filtering +- `get_employee` - Get detailed employee information +- `create_employee` - Create new employee records +- `update_employee` - Update employee information +- `get_employee_directory` - Get full employee directory +- `get_custom_fields` - List all custom fields +- `get_employee_field_values` - Get specific field values +- `get_employee_photo` - Download employee photos +- `upload_employee_photo` - Upload employee photos + +#### Time Off (8 tools) +- `list_time_off_requests` - List time off requests with filtering +- `get_time_off_request` - Get specific request details +- `create_time_off_request` - Create new time off requests +- `update_time_off_request_status` - Approve/deny requests +- `list_time_off_policies` - List all policies +- `get_time_off_balances` - Get employee balances +- `list_time_off_types` - List all time off types +- `estimate_future_balance` - Estimate future balances + +#### Reports (3 tools) +- `run_custom_report` - Run custom reports with filters +- `list_reports` - List all available reports +- `get_company_report` - Get standard company reports + +#### Tables (4 tools) +- `list_tables` - List all custom tables +- `get_table_rows` - Get table data +- `add_table_row` - Add new table rows +- `update_table_row` - Update table rows + +#### Benefits (4 tools) +- `list_benefit_plans` - List all benefit plans +- `get_benefit_plan` - Get plan details +- `list_benefit_enrollments` - List employee enrollments +- `list_benefit_dependents` - List dependents + +#### Payroll (3 tools) +- `list_pay_stubs` - List employee pay stubs +- `get_payroll_data` - Get payroll information +- `list_payroll_deductions` - List deductions + +#### Goals (6 tools) +- `list_goals` - List employee goals +- `get_goal` - Get goal details +- `create_goal` - Create new goals +- `update_goal` - Update goals +- `close_goal` - Close/complete goals +- `list_goal_comments` - List goal comments + +#### Training (6 tools) +- `list_training_courses` - List courses +- `get_training_course` - Get course details +- `create_training_course` - Assign courses +- `update_training_course` - Update assignments +- `list_training_categories` - List categories +- `list_training_types` - List training types + +#### Files (4 tools) +- `list_employee_files` - List employee files +- `get_employee_file` - Download files +- `upload_employee_file` - Upload files +- `list_file_categories` - List file categories + +#### Webhooks (3 tools) +- `list_webhooks` - List all webhooks +- `create_webhook` - Create new webhooks +- `delete_webhook` - Delete webhooks + +### 🎨 18 React UI Apps + +1. **employee-dashboard** - Overview dashboard with key metrics +2. **employee-directory** - Searchable employee directory +3. **employee-detail** - Detailed employee profile view +4. **time-off-calendar** - Visual time off calendar +5. **time-off-requests** - Request management interface +6. **time-off-balances** - Balance tracking and accrual +7. **benefits-overview** - Benefits summary +8. **benefits-enrollment** - Step-by-step enrollment wizard +9. **payroll-dashboard** - Payroll overview and pay stubs +10. **goal-tracker** - Goal management and progress +11. **training-catalog** - Available courses catalog +12. **training-progress** - Course progress and certifications +13. **file-manager** - Document management +14. **org-chart** - Visual organization chart +15. **headcount-analytics** - Workforce analytics +16. **turnover-report** - Turnover tracking and analysis +17. **new-hires** - New hire tracking and onboarding +18. **report-builder** - Custom report builder +19. **custom-report** - Custom report viewer + +## Installation + +```bash +npm install +``` + +## Configuration + +Set the following environment variables: + +```bash +export BAMBOOHR_COMPANY_DOMAIN="your-company" +export BAMBOOHR_API_KEY="your-api-key" +``` + +## Usage + +### As MCP Server + +```bash +npm run build +npm start +``` + +### Claude Desktop Configuration + +Add to `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "bamboohr": { + "command": "node", + "args": ["/path/to/bamboohr/dist/main.js"], + "env": { + "BAMBOOHR_COMPANY_DOMAIN": "your-company", + "BAMBOOHR_API_KEY": "your-api-key" + } + } + } +} +``` + +## API Reference + +### BambooHR API v1 + +Base URL: `https://api.bamboohr.com/api/gateway.php/{companyDomain}/v1/` + +Authentication: Basic Auth with API key as username, "x" as password + +## Architecture + +``` +src/ +├── clients/ +│ └── bamboohr.ts # API client with error handling +├── tools/ +│ ├── employees-tools.ts # Employee management tools +│ ├── time-off-tools.ts # Time off tools +│ ├── reports-tools.ts # Reporting tools +│ ├── tables-tools.ts # Custom tables tools +│ ├── benefits-tools.ts # Benefits tools +│ ├── payroll-tools.ts # Payroll tools +│ ├── goals-tools.ts # Goals tools +│ ├── training-tools.ts # Training tools +│ ├── files-tools.ts # File management tools +│ └── webhooks-tools.ts # Webhook tools +├── types/ +│ └── index.ts # TypeScript type definitions +├── ui/ +│ └── react-app/ # 18+ React UI components +├── server.ts # MCP server implementation +└── main.ts # Entry point +``` + +## Development + +```bash +# Watch mode +npm run dev + +# Build +npm run build + +# Start +npm start +``` + +## Error Handling + +The client includes comprehensive error handling for: +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found +- 429 Rate Limit +- 500 Internal Server Error +- Network errors + +All errors are returned in a consistent format: + +```json +{ + "success": false, + "error": "Error message", + "status": 400 +} +``` + +## Resources + +The server exposes two MCP resources: +- `bamboohr://employees` - Employee directory +- `bamboohr://time-off` - Time off requests + +## License + +MIT + +## Contributing + +Contributions welcome! Please ensure all tools follow the established patterns and include proper error handling. + +## Support + +For BambooHR API documentation, visit: https://documentation.bamboohr.com/docs diff --git a/servers/bamboohr/package.json b/servers/bamboohr/package.json index ce468f4..a0026d8 100644 --- a/servers/bamboohr/package.json +++ b/servers/bamboohr/package.json @@ -1,20 +1,37 @@ { - "name": "mcp-server-bamboohr", + "name": "@mcpengine/bamboohr-server", "version": "1.0.0", + "description": "Complete BambooHR MCP Server with 50+ tools and React apps", "type": "module", - "main": "dist/index.js", + "main": "dist/main.js", + "bin": { + "bamboohr-mcp": "dist/main.js" + }, "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "dev": "tsc --watch", + "start": "node dist/main.js", + "prepare": "npm run build" }, + "keywords": [ + "mcp", + "bamboohr", + "hr", + "model-context-protocol" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.9", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "lucide-react": "^0.468.0" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.2", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "typescript": "^5.7.2" } } diff --git a/servers/bamboohr/src/clients/bamboohr.ts b/servers/bamboohr/src/clients/bamboohr.ts new file mode 100644 index 0000000..4c8dfd6 --- /dev/null +++ b/servers/bamboohr/src/clients/bamboohr.ts @@ -0,0 +1,151 @@ +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { BambooHRConfig, BambooHRError } from '../types/index.js'; + +export class BambooHRClient { + private client: AxiosInstance; + private companyDomain: string; + private baseURL: string; + + constructor(config: BambooHRConfig) { + this.companyDomain = config.companyDomain; + this.baseURL = `https://api.bamboohr.com/api/gateway.php/${this.companyDomain}/v1`; + + this.client = axios.create({ + baseURL: this.baseURL, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + auth: { + username: config.apiKey, + password: 'x', + }, + }); + + // Response interceptor for error handling + this.client.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + return Promise.reject(this.handleError(error)); + } + ); + } + + private handleError(error: AxiosError): BambooHRError { + if (error.response) { + const status = error.response.status; + const data = error.response.data as any; + + switch (status) { + case 400: + return { + message: 'Bad Request: ' + (data?.message || 'Invalid request parameters'), + status, + errors: data?.errors, + }; + case 401: + return { + message: 'Unauthorized: Invalid API key or company domain', + status, + }; + case 403: + return { + message: 'Forbidden: Insufficient permissions', + status, + }; + case 404: + return { + message: 'Not Found: ' + (data?.message || 'Resource not found'), + status, + }; + case 429: + return { + message: 'Rate Limit Exceeded: Too many requests', + status, + }; + case 500: + return { + message: 'Internal Server Error: BambooHR service error', + status, + }; + default: + return { + message: data?.message || error.message || 'Unknown error occurred', + status, + }; + } + } else if (error.request) { + return { + message: 'Network Error: No response received from BambooHR', + }; + } else { + return { + message: error.message || 'Request setup error', + }; + } + } + + // Generic GET request + async get(endpoint: string, params?: any): Promise { + const response = await this.client.get(endpoint, { params }); + return response.data; + } + + // Generic POST request + async post(endpoint: string, data?: any, config?: any): Promise { + const response = await this.client.post(endpoint, data, config); + return response.data; + } + + // Generic PUT request + async put(endpoint: string, data?: any): Promise { + const response = await this.client.put(endpoint, data); + return response.data; + } + + // Generic DELETE request + async delete(endpoint: string): Promise { + const response = await this.client.delete(endpoint); + return response.data; + } + + // GET request with XML format + async getXML(endpoint: string, params?: any): Promise { + const response = await this.client.get(endpoint, { + params, + headers: { 'Accept': 'application/xml' }, + }); + return response.data; + } + + // POST with file upload + async uploadFile(endpoint: string, file: Buffer, fileName: string, shareWithEmployee: boolean = false): Promise { + const formData = new FormData(); + const blob = new Blob([new Uint8Array(file)]); + formData.append('file', blob, fileName); + formData.append('share', shareWithEmployee.toString()); + + const response = await this.client.post(endpoint, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + return response.data; + } + + // Download file + async downloadFile(endpoint: string): Promise { + const response = await this.client.get(endpoint, { + responseType: 'arraybuffer', + }); + return Buffer.from(response.data); + } + + getCompanyDomain(): string { + return this.companyDomain; + } + + getBaseURL(): string { + return this.baseURL; + } +} diff --git a/servers/bamboohr/src/index.ts b/servers/bamboohr/src/index.ts deleted file mode 100644 index b2fbeb0..0000000 --- a/servers/bamboohr/src/index.ts +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// ============================================ -// CONFIGURATION -// ============================================ -const MCP_NAME = "bamboohr"; -const MCP_VERSION = "1.0.0"; - -// ============================================ -// API CLIENT -// ============================================ -class BambooHRClient { - private apiKey: string; - private companyDomain: string; - private baseUrl: string; - - constructor(apiKey: string, companyDomain: string) { - this.apiKey = apiKey; - this.companyDomain = companyDomain; - this.baseUrl = `https://api.bamboohr.com/api/gateway.php/${companyDomain}/v1`; - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const authHeader = Buffer.from(`${this.apiKey}:x`).toString("base64"); - - const response = await fetch(url, { - ...options, - headers: { - "Authorization": `Basic ${authHeader}`, - "Content-Type": "application/json", - "Accept": "application/json", - ...options.headers, - }, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`BambooHR API error: ${response.status} ${response.statusText} - ${text}`); - } - - const contentType = response.headers.get("content-type"); - if (contentType?.includes("application/json")) { - return response.json(); - } - return response.text(); - } - - async get(endpoint: string) { - return this.request(endpoint, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } - - async put(endpoint: string, data: any) { - return this.request(endpoint, { - method: "PUT", - body: JSON.stringify(data), - }); - } - - // Employee methods - async listEmployees() { - // Returns the employee directory with standard fields - return this.get("/employees/directory"); - } - - async getEmployee(employeeId: string, fields?: string[]) { - const fieldList = fields?.join(",") || "firstName,lastName,department,jobTitle,workEmail,workPhone,location,photoUrl,status"; - return this.get(`/employees/${employeeId}?fields=${fieldList}`); - } - - async getDirectory() { - return this.get("/employees/directory"); - } - - // Time Off methods - async listTimeOffRequests(options?: { - start?: string; - end?: string; - status?: string; - employeeId?: string; - }) { - const params = new URLSearchParams(); - if (options?.start) params.append("start", options.start); - if (options?.end) params.append("end", options.end); - if (options?.status) params.append("status", options.status); - if (options?.employeeId) params.append("employeeId", options.employeeId); - - const query = params.toString(); - return this.get(`/time_off/requests${query ? `?${query}` : ""}`); - } - - async requestTimeOff(data: { - employeeId: string; - timeOffTypeId: string; - start: string; - end: string; - amount?: number; - notes?: string; - status?: string; - }) { - return this.put(`/employees/${data.employeeId}/time_off/request`, { - timeOffTypeId: data.timeOffTypeId, - start: data.start, - end: data.end, - amount: data.amount, - notes: data.notes, - status: data.status || "requested", - }); - } - - // Goals methods - async listGoals(employeeId: string) { - return this.get(`/employees/${employeeId}/goals`); - } - - // Files methods - async listFiles(employeeId: string) { - return this.get(`/employees/${employeeId}/files/view`); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_employees", - description: "List all employees from the BambooHR directory", - inputSchema: { - type: "object" as const, - properties: {}, - }, - }, - { - name: "get_employee", - description: "Get detailed information about a specific employee", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Employee ID" }, - fields: { - type: "array", - items: { type: "string" }, - description: "Specific fields to retrieve (e.g., firstName, lastName, department, jobTitle, workEmail, hireDate)" - }, - }, - required: ["employee_id"], - }, - }, - { - name: "list_time_off_requests", - description: "List time off requests from BambooHR", - inputSchema: { - type: "object" as const, - properties: { - start: { type: "string", description: "Start date (YYYY-MM-DD)" }, - end: { type: "string", description: "End date (YYYY-MM-DD)" }, - status: { - type: "string", - description: "Filter by status", - enum: ["approved", "denied", "superceded", "requested", "canceled"] - }, - employee_id: { type: "string", description: "Filter by employee ID" }, - }, - }, - }, - { - name: "request_time_off", - description: "Submit a time off request for an employee", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Employee ID" }, - time_off_type_id: { type: "string", description: "Time off type ID (e.g., vacation, sick)" }, - start: { type: "string", description: "Start date (YYYY-MM-DD)" }, - end: { type: "string", description: "End date (YYYY-MM-DD)" }, - amount: { type: "number", description: "Number of days/hours" }, - notes: { type: "string", description: "Request notes" }, - }, - required: ["employee_id", "time_off_type_id", "start", "end"], - }, - }, - { - name: "list_goals", - description: "List goals for an employee", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Employee ID" }, - }, - required: ["employee_id"], - }, - }, - { - name: "get_directory", - description: "Get the full employee directory with contact information", - inputSchema: { - type: "object" as const, - properties: {}, - }, - }, - { - name: "list_files", - description: "List files associated with an employee", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Employee ID" }, - }, - required: ["employee_id"], - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: BambooHRClient, name: string, args: any) { - switch (name) { - case "list_employees": { - return await client.listEmployees(); - } - case "get_employee": { - return await client.getEmployee(args.employee_id, args.fields); - } - case "list_time_off_requests": { - return await client.listTimeOffRequests({ - start: args.start, - end: args.end, - status: args.status, - employeeId: args.employee_id, - }); - } - case "request_time_off": { - return await client.requestTimeOff({ - employeeId: args.employee_id, - timeOffTypeId: args.time_off_type_id, - start: args.start, - end: args.end, - amount: args.amount, - notes: args.notes, - }); - } - case "list_goals": { - return await client.listGoals(args.employee_id); - } - case "get_directory": { - return await client.getDirectory(); - } - case "list_files": { - return await client.listFiles(args.employee_id); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const apiKey = process.env.BAMBOOHR_API_KEY; - const companyDomain = process.env.BAMBOOHR_COMPANY_DOMAIN; - - if (!apiKey) { - console.error("Error: BAMBOOHR_API_KEY environment variable required"); - process.exit(1); - } - if (!companyDomain) { - console.error("Error: BAMBOOHR_COMPANY_DOMAIN environment variable required"); - process.exit(1); - } - - const client = new BambooHRClient(apiKey, companyDomain); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - // List available tools - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - // Handle tool calls - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - const result = await handleTool(client, name, args || {}); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } - }); - - // Start server - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); diff --git a/servers/bamboohr/src/main.ts b/servers/bamboohr/src/main.ts new file mode 100644 index 0000000..4caff34 --- /dev/null +++ b/servers/bamboohr/src/main.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import { BambooHRServer } from './server.js'; + +async function main() { + try { + const server = new BambooHRServer(); + await server.run(); + } catch (error) { + console.error('Fatal error:', error); + process.exit(1); + } +} + +main(); diff --git a/servers/bamboohr/src/server.ts b/servers/bamboohr/src/server.ts new file mode 100644 index 0000000..d2a6001 --- /dev/null +++ b/servers/bamboohr/src/server.ts @@ -0,0 +1,168 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + ListToolsRequestSchema, + CallToolRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { BambooHRClient } from './clients/bamboohr.js'; +import { employeesTools } from './tools/employees-tools.js'; +import { timeOffTools } from './tools/time-off-tools.js'; +import { reportsTools } from './tools/reports-tools.js'; +import { tablesTools } from './tools/tables-tools.js'; +import { benefitsTools } from './tools/benefits-tools.js'; +import { payrollTools } from './tools/payroll-tools.js'; +import { goalsTools } from './tools/goals-tools.js'; +import { trainingTools } from './tools/training-tools.js'; +import { filesTools } from './tools/files-tools.js'; +import { webhooksTools } from './tools/webhooks-tools.js'; + +export class BambooHRServer { + private server: Server; + private client: BambooHRClient; + private allTools: Map; + + constructor() { + this.server = new Server( + { + name: 'bamboohr-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } + ); + + // Get config from environment + const companyDomain = process.env.BAMBOOHR_COMPANY_DOMAIN; + const apiKey = process.env.BAMBOOHR_API_KEY; + + if (!companyDomain || !apiKey) { + throw new Error('BAMBOOHR_COMPANY_DOMAIN and BAMBOOHR_API_KEY environment variables are required'); + } + + this.client = new BambooHRClient({ companyDomain, apiKey }); + + // Combine all tools + this.allTools = new Map(); + Object.entries(employeesTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(timeOffTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(reportsTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(tablesTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(benefitsTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(payrollTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(goalsTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(trainingTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(filesTools).forEach(([key, val]) => this.allTools.set(key, val)); + Object.entries(webhooksTools).forEach(([key, val]) => this.allTools.set(key, val)); + + this.setupHandlers(); + } + + private setupHandlers() { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + const tools = Array.from(this.allTools.entries()).map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: tool.parameters, + })); + + return { tools }; + }); + + // Handle tool calls + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const toolName = request.params.name; + const tool = this.allTools.get(toolName); + + if (!tool) { + throw new Error(`Unknown tool: ${toolName}`); + } + + try { + const result = await tool.handler(this.client, request.params.arguments || {}); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ success: false, error: error.message }, null, 2), + }, + ], + isError: true, + }; + } + }); + + // List resources + this.server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: [ + { + uri: 'bamboohr://employees', + name: 'Employee Directory', + description: 'Access to all employees in the directory', + mimeType: 'application/json', + }, + { + uri: 'bamboohr://time-off', + name: 'Time Off Requests', + description: 'All time off requests and balances', + mimeType: 'application/json', + }, + ], + }; + }); + + // Read resource + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + + if (uri === 'bamboohr://employees') { + const directory = await this.client.get('/employees/directory'); + return { + contents: [ + { + uri, + mimeType: 'application/json', + text: JSON.stringify(directory, null, 2), + }, + ], + }; + } else if (uri === 'bamboohr://time-off') { + const requests = await this.client.get('/time_off/requests'); + return { + contents: [ + { + uri, + mimeType: 'application/json', + text: JSON.stringify(requests, null, 2), + }, + ], + }; + } + + throw new Error(`Unknown resource: ${uri}`); + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('BambooHR MCP Server running on stdio'); + } +} diff --git a/servers/bamboohr/src/tools/benefits-tools.ts b/servers/bamboohr/src/tools/benefits-tools.ts new file mode 100644 index 0000000..b6f497b --- /dev/null +++ b/servers/bamboohr/src/tools/benefits-tools.ts @@ -0,0 +1,122 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { BenefitPlan, BenefitEnrollment, BenefitDependent } from '../types/index.js'; + +export const benefitsTools = { + list_benefit_plans: { + description: 'List all benefit plans', + parameters: { + type: 'object', + properties: { + active_only: { + type: 'boolean', + description: 'Filter to active plans only', + default: true, + }, + }, + }, + handler: async (client: BambooHRClient, args: { active_only?: boolean }) => { + try { + const plans = await client.get('/benefits/plans'); + + let filteredPlans = plans; + if (args.active_only !== false) { + filteredPlans = plans.filter(p => p.active !== false); + } + + return { + success: true, + plans: filteredPlans, + count: filteredPlans.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_benefit_plan: { + description: 'Get details of a specific benefit plan', + parameters: { + type: 'object', + properties: { + plan_id: { + type: 'string', + description: 'Benefit plan ID', + }, + }, + required: ['plan_id'], + }, + handler: async (client: BambooHRClient, args: { plan_id: string }) => { + try { + const plan = await client.get(`/benefits/plans/${args.plan_id}`); + + return { + success: true, + plan, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_benefit_enrollments: { + description: 'List benefit enrollments for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string }) => { + try { + const enrollments = await client.get( + `/employees/${args.employee_id}/benefits/enrollments` + ); + + return { + success: true, + employee_id: args.employee_id, + enrollments, + count: enrollments.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_benefit_dependents: { + description: 'List benefit dependents for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string }) => { + try { + const dependents = await client.get( + `/employees/${args.employee_id}/benefits/dependents` + ); + + return { + success: true, + employee_id: args.employee_id, + dependents, + count: dependents.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/employees-tools.ts b/servers/bamboohr/src/tools/employees-tools.ts new file mode 100644 index 0000000..d052d9c --- /dev/null +++ b/servers/bamboohr/src/tools/employees-tools.ts @@ -0,0 +1,294 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { Employee, EmployeeDirectory, CustomField } from '../types/index.js'; + +export const employeesTools = { + list_employees: { + description: 'List all employees with basic information', + parameters: { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['Active', 'Inactive', 'All'], + description: 'Filter by employee status', + default: 'Active', + }, + }, + }, + handler: async (client: BambooHRClient, args: { status?: string }) => { + try { + const directory = await client.get('/employees/directory'); + let employees = directory.employees || []; + + if (args.status && args.status !== 'All') { + employees = employees.filter(emp => emp.status === args.status); + } + + return { + success: true, + employees, + count: employees.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_employee: { + description: 'Get detailed information about a specific employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + fields: { + type: 'array', + items: { type: 'string' }, + description: 'Specific fields to retrieve (optional, returns all if not specified)', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; fields?: string[] }) => { + try { + const fieldsParam = args.fields?.join(',') || ''; + const employee = await client.get( + `/employees/${args.employee_id}`, + fieldsParam ? { fields: fieldsParam } : {} + ); + + return { + success: true, + employee, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + create_employee: { + description: 'Create a new employee record', + parameters: { + type: 'object', + properties: { + first_name: { + type: 'string', + description: 'First name', + }, + last_name: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Work email', + }, + employee_data: { + type: 'object', + description: 'Additional employee data (job title, department, hire date, etc.)', + }, + }, + required: ['first_name', 'last_name'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const employeeData = { + firstName: args.first_name, + lastName: args.last_name, + workEmail: args.email, + ...args.employee_data, + }; + + const result = await client.post('/employees', employeeData); + + return { + success: true, + employee_id: result.id || result, + message: 'Employee created successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + update_employee: { + description: 'Update employee information', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + employee_data: { + type: 'object', + description: 'Employee data to update', + }, + }, + required: ['employee_id', 'employee_data'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; employee_data: any }) => { + try { + await client.post(`/employees/${args.employee_id}`, args.employee_data); + + return { + success: true, + message: 'Employee updated successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_employee_directory: { + description: 'Get the employee directory with all fields', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const directory = await client.get('/employees/directory'); + + return { + success: true, + directory, + employee_count: directory.employees?.length || 0, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_custom_fields: { + description: 'Get list of all custom employee fields', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const fields = await client.get<{ field: CustomField[] }>('/meta/fields'); + + return { + success: true, + fields: fields.field || fields, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_employee_field_values: { + description: 'Get specific field values for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + field_list: { + type: 'array', + items: { type: 'string' }, + description: 'List of field IDs to retrieve', + }, + }, + required: ['employee_id', 'field_list'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; field_list: string[] }) => { + try { + const fields = args.field_list.join(','); + const values = await client.get(`/employees/${args.employee_id}`, { fields }); + + return { + success: true, + employee_id: args.employee_id, + values, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_employee_photo: { + description: 'Get employee photo', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + size: { + type: 'string', + enum: ['small', 'medium', 'large', 'original'], + description: 'Photo size', + default: 'medium', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; size?: string }) => { + try { + const size = args.size || 'medium'; + const photo = await client.downloadFile(`/employees/${args.employee_id}/photo/${size}`); + + return { + success: true, + photo: photo.toString('base64'), + size, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + upload_employee_photo: { + description: 'Upload employee photo', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + photo_base64: { + type: 'string', + description: 'Base64 encoded photo data', + }, + filename: { + type: 'string', + description: 'Filename for the photo', + default: 'photo.jpg', + }, + }, + required: ['employee_id', 'photo_base64'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; photo_base64: string; filename?: string }) => { + try { + const photoBuffer = Buffer.from(args.photo_base64, 'base64'); + const filename = args.filename || 'photo.jpg'; + + await client.uploadFile(`/employees/${args.employee_id}/photo`, photoBuffer, filename); + + return { + success: true, + message: 'Photo uploaded successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/files-tools.ts b/servers/bamboohr/src/tools/files-tools.ts new file mode 100644 index 0000000..3e6ff4d --- /dev/null +++ b/servers/bamboohr/src/tools/files-tools.ts @@ -0,0 +1,146 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { File, FileCategory } from '../types/index.js'; + +export const filesTools = { + list_employee_files: { + description: 'List files for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + category_id: { + type: 'string', + description: 'Filter by category ID', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; category_id?: string }) => { + try { + const params = args.category_id ? { categoryId: args.category_id } : {}; + const files = await client.get( + `/employees/${args.employee_id}/files`, + params + ); + + return { + success: true, + employee_id: args.employee_id, + files, + count: files.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_employee_file: { + description: 'Download a specific file', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + file_id: { + type: 'string', + description: 'File ID', + }, + }, + required: ['employee_id', 'file_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; file_id: string }) => { + try { + const file = await client.downloadFile( + `/employees/${args.employee_id}/files/${args.file_id}` + ); + + return { + success: true, + file_id: args.file_id, + file_data: file.toString('base64'), + message: 'File downloaded successfully (base64 encoded)', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + upload_employee_file: { + description: 'Upload a file for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + file_base64: { + type: 'string', + description: 'Base64 encoded file data', + }, + filename: { + type: 'string', + description: 'Filename', + }, + category_id: { + type: 'string', + description: 'File category ID', + }, + share_with_employee: { + type: 'boolean', + description: 'Share file with employee', + default: false, + }, + }, + required: ['employee_id', 'file_base64', 'filename'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const fileBuffer = Buffer.from(args.file_base64, 'base64'); + + const result = await client.uploadFile( + `/employees/${args.employee_id}/files/${args.category_id || ''}`, + fileBuffer, + args.filename, + args.share_with_employee || false + ); + + return { + success: true, + file_id: result.id || result, + message: 'File uploaded successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_file_categories: { + description: 'List all file categories', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const categories = await client.get('/meta/files/categories'); + + return { + success: true, + categories, + count: categories.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/goals-tools.ts b/servers/bamboohr/src/tools/goals-tools.ts new file mode 100644 index 0000000..c9ad4e0 --- /dev/null +++ b/servers/bamboohr/src/tools/goals-tools.ts @@ -0,0 +1,236 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { Goal, GoalComment } from '../types/index.js'; + +export const goalsTools = { + list_goals: { + description: 'List goals for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + filter: { + type: 'string', + enum: ['all', 'active', 'completed'], + description: 'Filter goals by status', + default: 'all', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; filter?: string }) => { + try { + const filter = args.filter || 'all'; + const goals = await client.get( + `/employees/${args.employee_id}/goals`, + { filter } + ); + + return { + success: true, + employee_id: args.employee_id, + goals, + count: goals.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_goal: { + description: 'Get details of a specific goal', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + goal_id: { + type: 'string', + description: 'Goal ID', + }, + }, + required: ['employee_id', 'goal_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; goal_id: string }) => { + try { + const goal = await client.get( + `/employees/${args.employee_id}/goals/${args.goal_id}` + ); + + return { + success: true, + goal, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + create_goal: { + description: 'Create a new goal for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + title: { + type: 'string', + description: 'Goal title', + }, + description: { + type: 'string', + description: 'Goal description', + }, + due_date: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + shared_with_employee_ids: { + type: 'array', + items: { type: 'string' }, + description: 'Employee IDs to share with', + }, + }, + required: ['employee_id', 'title'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const goalData = { + title: args.title, + description: args.description, + dueDate: args.due_date, + sharedWithEmployeeIds: args.shared_with_employee_ids, + }; + + const result = await client.post( + `/employees/${args.employee_id}/goals`, + goalData + ); + + return { + success: true, + goal_id: result.id || result, + message: 'Goal created successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + update_goal: { + description: 'Update an existing goal', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + goal_id: { + type: 'string', + description: 'Goal ID', + }, + goal_data: { + type: 'object', + description: 'Goal data to update', + }, + }, + required: ['employee_id', 'goal_id', 'goal_data'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + await client.put( + `/employees/${args.employee_id}/goals/${args.goal_id}`, + args.goal_data + ); + + return { + success: true, + message: 'Goal updated successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + close_goal: { + description: 'Close/complete a goal', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + goal_id: { + type: 'string', + description: 'Goal ID', + }, + percent_complete: { + type: 'number', + description: 'Completion percentage (0-100)', + default: 100, + }, + }, + required: ['employee_id', 'goal_id'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + await client.put( + `/employees/${args.employee_id}/goals/${args.goal_id}/close`, + { percentComplete: args.percent_complete || 100 } + ); + + return { + success: true, + message: 'Goal closed successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_goal_comments: { + description: 'List comments on a goal', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + goal_id: { + type: 'string', + description: 'Goal ID', + }, + }, + required: ['employee_id', 'goal_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; goal_id: string }) => { + try { + const comments = await client.get( + `/employees/${args.employee_id}/goals/${args.goal_id}/comments` + ); + + return { + success: true, + comments, + count: comments.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/payroll-tools.ts b/servers/bamboohr/src/tools/payroll-tools.ts new file mode 100644 index 0000000..8e44b1f --- /dev/null +++ b/servers/bamboohr/src/tools/payroll-tools.ts @@ -0,0 +1,104 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { PayStub, PayrollDeduction } from '../types/index.js'; + +export const payrollTools = { + list_pay_stubs: { + description: 'List pay stubs for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + start_date: { + type: 'string', + description: 'Start date filter (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date filter (YYYY-MM-DD)', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const params: any = {}; + if (args.start_date) params.start = args.start_date; + if (args.end_date) params.end = args.end_date; + + const payStubs = await client.get( + `/employees/${args.employee_id}/pay_stubs`, + params + ); + + return { + success: true, + employee_id: args.employee_id, + pay_stubs: payStubs, + count: payStubs.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_payroll_data: { + description: 'Get payroll data for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string }) => { + try { + const payroll = await client.get(`/employees/${args.employee_id}/payroll`); + + return { + success: true, + employee_id: args.employee_id, + payroll, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_payroll_deductions: { + description: 'List payroll deductions for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string }) => { + try { + const deductions = await client.get( + `/employees/${args.employee_id}/payroll/deductions` + ); + + return { + success: true, + employee_id: args.employee_id, + deductions, + count: deductions.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/reports-tools.ts b/servers/bamboohr/src/tools/reports-tools.ts new file mode 100644 index 0000000..254aeb6 --- /dev/null +++ b/servers/bamboohr/src/tools/reports-tools.ts @@ -0,0 +1,119 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { Report, CustomReport } from '../types/index.js'; + +export const reportsTools = { + run_custom_report: { + description: 'Run a custom report with specified fields and filters', + parameters: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Report title', + }, + fields: { + type: 'array', + items: { type: 'string' }, + description: 'Field IDs to include in the report', + }, + format: { + type: 'string', + enum: ['JSON', 'XML', 'CSV', 'PDF', 'XLS'], + description: 'Report format', + default: 'JSON', + }, + filters: { + type: 'object', + description: 'Report filters (e.g., {"status": "Active"})', + }, + }, + required: ['fields'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const reportRequest: CustomReport = { + title: args.title || 'Custom Report', + fields: args.fields, + filters: args.filters, + }; + + const format = (args.format || 'JSON').toUpperCase(); + const endpoint = '/reports/custom'; + + let result; + if (format === 'JSON') { + result = await client.post(endpoint, reportRequest, { + params: { format: 'JSON' }, + }); + } else { + result = await client.post(endpoint, reportRequest, { + params: { format }, + }); + } + + return { + success: true, + report: result, + format, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_reports: { + description: 'List all available reports', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const reports = await client.get('/reports'); + + return { + success: true, + reports, + count: reports.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_company_report: { + description: 'Get a standard company report by ID', + parameters: { + type: 'object', + properties: { + report_id: { + type: 'string', + description: 'Report ID', + }, + format: { + type: 'string', + enum: ['JSON', 'XML', 'CSV', 'PDF', 'XLS'], + description: 'Report format', + default: 'JSON', + }, + }, + required: ['report_id'], + }, + handler: async (client: BambooHRClient, args: { report_id: string; format?: string }) => { + try { + const format = (args.format || 'JSON').toUpperCase(); + const report = await client.get(`/reports/${args.report_id}`, { format }); + + return { + success: true, + report, + format, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/tables-tools.ts b/servers/bamboohr/src/tools/tables-tools.ts new file mode 100644 index 0000000..93514fe --- /dev/null +++ b/servers/bamboohr/src/tools/tables-tools.ts @@ -0,0 +1,139 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { Table, TableRow } from '../types/index.js'; + +export const tablesTools = { + list_tables: { + description: 'List all custom tables in BambooHR', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const tables = await client.get('/meta/tables'); + + return { + success: true, + tables, + count: tables.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_table_rows: { + description: 'Get all rows from a custom table for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + table_name: { + type: 'string', + description: 'Table name or alias', + }, + }, + required: ['employee_id', 'table_name'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; table_name: string }) => { + try { + const rows = await client.get( + `/employees/${args.employee_id}/tables/${args.table_name}` + ); + + return { + success: true, + employee_id: args.employee_id, + table_name: args.table_name, + rows, + count: rows.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + add_table_row: { + description: 'Add a new row to a custom table', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + table_name: { + type: 'string', + description: 'Table name or alias', + }, + row_data: { + type: 'object', + description: 'Row data as key-value pairs', + }, + }, + required: ['employee_id', 'table_name', 'row_data'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; table_name: string; row_data: any }) => { + try { + const result = await client.post( + `/employees/${args.employee_id}/tables/${args.table_name}`, + args.row_data + ); + + return { + success: true, + row_id: result.id || result, + message: 'Table row added successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + update_table_row: { + description: 'Update an existing row in a custom table', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + table_name: { + type: 'string', + description: 'Table name or alias', + }, + row_id: { + type: 'string', + description: 'Row ID', + }, + row_data: { + type: 'object', + description: 'Updated row data', + }, + }, + required: ['employee_id', 'table_name', 'row_id', 'row_data'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + await client.post( + `/employees/${args.employee_id}/tables/${args.table_name}/${args.row_id}`, + args.row_data + ); + + return { + success: true, + message: 'Table row updated successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/time-off-tools.ts b/servers/bamboohr/src/tools/time-off-tools.ts new file mode 100644 index 0000000..e3608e1 --- /dev/null +++ b/servers/bamboohr/src/tools/time-off-tools.ts @@ -0,0 +1,288 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { TimeOffRequest, TimeOffPolicy, TimeOffBalance, TimeOffType } from '../types/index.js'; + +export const timeOffTools = { + list_time_off_requests: { + description: 'List time off requests with filtering', + parameters: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + status: { + type: 'string', + enum: ['approved', 'denied', 'superceded', 'requested', 'canceled'], + description: 'Filter by status', + }, + employee_id: { + type: 'string', + description: 'Filter by specific employee', + }, + type_id: { + type: 'string', + description: 'Filter by time off type ID', + }, + }, + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const params: any = {}; + if (args.start_date) params.start = args.start_date; + if (args.end_date) params.end = args.end_date; + if (args.status) params.status = args.status; + if (args.employee_id) params.employeeId = args.employee_id; + if (args.type_id) params.type = args.type_id; + + const requests = await client.get('/time_off/requests', params); + + return { + success: true, + requests, + count: requests.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_time_off_request: { + description: 'Get details of a specific time off request', + parameters: { + type: 'object', + properties: { + request_id: { + type: 'string', + description: 'Time off request ID', + }, + }, + required: ['request_id'], + }, + handler: async (client: BambooHRClient, args: { request_id: string }) => { + try { + const request = await client.get(`/time_off/requests/${args.request_id}`); + + return { + success: true, + request, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + create_time_off_request: { + description: 'Create a new time off request', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + type_id: { + type: 'string', + description: 'Time off type ID', + }, + start_date: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + amount: { + type: 'number', + description: 'Amount in hours or days', + }, + notes: { + type: 'string', + description: 'Employee notes', + }, + }, + required: ['employee_id', 'type_id', 'start_date', 'end_date'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const requestData = { + employeeId: args.employee_id, + timeOffTypeId: args.type_id, + start: args.start_date, + end: args.end_date, + amount: args.amount, + notes: args.notes, + }; + + const result = await client.post('/time_off/requests', requestData); + + return { + success: true, + request_id: result.id || result, + message: 'Time off request created successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + update_time_off_request_status: { + description: 'Approve or deny a time off request', + parameters: { + type: 'object', + properties: { + request_id: { + type: 'string', + description: 'Time off request ID', + }, + status: { + type: 'string', + enum: ['approved', 'denied', 'canceled'], + description: 'New status', + }, + note: { + type: 'string', + description: 'Manager note', + }, + }, + required: ['request_id', 'status'], + }, + handler: async (client: BambooHRClient, args: { request_id: string; status: string; note?: string }) => { + try { + const data = { + status: args.status, + note: args.note, + }; + + await client.put(`/time_off/requests/${args.request_id}/status`, data); + + return { + success: true, + message: `Time off request ${args.status} successfully`, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_time_off_policies: { + description: 'List all time off policies', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const policies = await client.get('/time_off/policies'); + + return { + success: true, + policies, + count: policies.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_time_off_balances: { + description: 'Get time off balances for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + as_of_date: { + type: 'string', + description: 'Calculate balance as of this date (YYYY-MM-DD)', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; as_of_date?: string }) => { + try { + const params = args.as_of_date ? { end: args.as_of_date } : {}; + const balances = await client.get( + `/employees/${args.employee_id}/time_off/calculator`, + params + ); + + return { + success: true, + employee_id: args.employee_id, + balances, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_time_off_types: { + description: 'List all time off types', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const types = await client.get('/meta/time_off/types'); + + return { + success: true, + types, + count: types.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + estimate_future_balance: { + description: 'Estimate future time off balance for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + end_date: { + type: 'string', + description: 'Future date to estimate balance (YYYY-MM-DD)', + }, + }, + required: ['employee_id', 'end_date'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; end_date: string }) => { + try { + const balances = await client.get( + `/employees/${args.employee_id}/time_off/calculator`, + { end: args.end_date } + ); + + return { + success: true, + employee_id: args.employee_id, + as_of_date: args.end_date, + balances, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/training-tools.ts b/servers/bamboohr/src/tools/training-tools.ts new file mode 100644 index 0000000..647596d --- /dev/null +++ b/servers/bamboohr/src/tools/training-tools.ts @@ -0,0 +1,217 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { TrainingCourse, TrainingCategory, TrainingType } from '../types/index.js'; + +export const trainingTools = { + list_training_courses: { + description: 'List training courses for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + filter: { + type: 'string', + enum: ['all', 'required', 'completed', 'incomplete'], + description: 'Filter courses', + default: 'all', + }, + }, + required: ['employee_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; filter?: string }) => { + try { + const filter = args.filter || 'all'; + const courses = await client.get( + `/employees/${args.employee_id}/training`, + { filter } + ); + + return { + success: true, + employee_id: args.employee_id, + courses, + count: courses.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + get_training_course: { + description: 'Get details of a specific training course', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + course_id: { + type: 'string', + description: 'Course ID', + }, + }, + required: ['employee_id', 'course_id'], + }, + handler: async (client: BambooHRClient, args: { employee_id: string; course_id: string }) => { + try { + const course = await client.get( + `/employees/${args.employee_id}/training/${args.course_id}` + ); + + return { + success: true, + course, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + create_training_course: { + description: 'Assign a training course to an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + name: { + type: 'string', + description: 'Course name', + }, + description: { + type: 'string', + description: 'Course description', + }, + category_id: { + type: 'string', + description: 'Training category ID', + }, + type_id: { + type: 'string', + description: 'Training type ID', + }, + required: { + type: 'boolean', + description: 'Is this course required?', + default: false, + }, + due_date: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + }, + required: ['employee_id', 'name'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const courseData = { + name: args.name, + description: args.description, + categoryId: args.category_id, + typeId: args.type_id, + required: args.required, + dueDate: args.due_date, + }; + + const result = await client.post( + `/employees/${args.employee_id}/training`, + courseData + ); + + return { + success: true, + course_id: result.id || result, + message: 'Training course assigned successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + update_training_course: { + description: 'Update a training course assignment', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + course_id: { + type: 'string', + description: 'Course ID', + }, + course_data: { + type: 'object', + description: 'Course data to update', + }, + }, + required: ['employee_id', 'course_id', 'course_data'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + await client.put( + `/employees/${args.employee_id}/training/${args.course_id}`, + args.course_data + ); + + return { + success: true, + message: 'Training course updated successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_training_categories: { + description: 'List all training categories', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const categories = await client.get('/meta/training/categories'); + + return { + success: true, + categories, + count: categories.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + list_training_types: { + description: 'List all training types', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const types = await client.get('/meta/training/types'); + + return { + success: true, + types, + count: types.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/tools/webhooks-tools.ts b/servers/bamboohr/src/tools/webhooks-tools.ts new file mode 100644 index 0000000..a8f0eae --- /dev/null +++ b/servers/bamboohr/src/tools/webhooks-tools.ts @@ -0,0 +1,112 @@ +import { BambooHRClient } from '../clients/bamboohr.js'; +import type { Webhook } from '../types/index.js'; + +export const webhooksTools = { + list_webhooks: { + description: 'List all webhooks', + parameters: { + type: 'object', + properties: {}, + }, + handler: async (client: BambooHRClient) => { + try { + const webhooks = await client.get('/webhooks'); + + return { + success: true, + webhooks, + count: webhooks.length, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + create_webhook: { + description: 'Create a new webhook', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Webhook name', + }, + url: { + type: 'string', + description: 'Webhook URL', + }, + format: { + type: 'string', + enum: ['json', 'form'], + description: 'Post format', + default: 'json', + }, + frequency: { + type: 'string', + enum: ['realtime', 'daily', 'weekly'], + description: 'Update frequency', + default: 'realtime', + }, + post_fields: { + type: 'array', + items: { type: 'string' }, + description: 'Fields to include in webhook posts', + }, + limit: { + type: 'number', + description: 'Maximum number of posts', + }, + }, + required: ['name', 'url'], + }, + handler: async (client: BambooHRClient, args: any) => { + try { + const webhookData = { + name: args.name, + url: args.url, + format: args.format || 'json', + frequency: args.frequency || 'realtime', + postFields: args.post_fields, + limit: args.limit, + }; + + const result = await client.post('/webhooks', webhookData); + + return { + success: true, + webhook_id: result.id || result, + message: 'Webhook created successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, + + delete_webhook: { + description: 'Delete a webhook', + parameters: { + type: 'object', + properties: { + webhook_id: { + type: 'string', + description: 'Webhook ID', + }, + }, + required: ['webhook_id'], + }, + handler: async (client: BambooHRClient, args: { webhook_id: string }) => { + try { + await client.delete(`/webhooks/${args.webhook_id}`); + + return { + success: true, + message: 'Webhook deleted successfully', + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }, +}; diff --git a/servers/bamboohr/src/types/index.ts b/servers/bamboohr/src/types/index.ts new file mode 100644 index 0000000..bcf4d17 --- /dev/null +++ b/servers/bamboohr/src/types/index.ts @@ -0,0 +1,255 @@ +// BambooHR Types + +export interface BambooHRConfig { + companyDomain: string; + apiKey: string; +} + +export interface Employee { + id: string; + firstName: string; + lastName: string; + displayName?: string; + preferredName?: string; + jobTitle?: string; + workEmail?: string; + workPhone?: string; + mobilePhone?: string; + department?: string; + division?: string; + location?: string; + hireDate?: string; + employeeNumber?: string; + status?: string; + supervisor?: string; + supervisorId?: string; + gender?: string; + dateOfBirth?: string; + maritalStatus?: string; + address1?: string; + address2?: string; + city?: string; + state?: string; + zipCode?: string; + country?: string; + photoUrl?: string; + [key: string]: any; +} + +export interface EmployeeDirectory { + fields: Array<{ + id: string; + type: string; + name: string; + }>; + employees: Employee[]; +} + +export interface CustomField { + id: string; + name: string; + type: string; + alias?: string; + options?: string[]; +} + +export interface TimeOffRequest { + id: string; + employeeId: string; + name?: string; + status: string; + start: string; + end: string; + created: string; + type: { + id: string; + name: string; + }; + amount: { + unit: string; + amount: number; + }; + notes?: { + employee?: string; + manager?: string; + }; + dates?: Array<{ + date: string; + amount: number; + }>; +} + +export interface TimeOffPolicy { + id: string; + timeOffTypeId: string; + name: string; + accrualRate?: number; + accrualPeriod?: string; + accrualMethod?: string; + carryoverAmount?: number; +} + +export interface TimeOffBalance { + employeeId: string; + timeOffTypeId: string; + name: string; + balance: number; + end?: string; + used?: number; + scheduled?: number; + accrued?: number; + policyType?: string; +} + +export interface TimeOffType { + id: string; + name: string; + units: string; + color?: string; +} + +export interface Report { + id: string; + name: string; + type: string; +} + +export interface CustomReport { + title: string; + fields: string[]; + filters?: { + [key: string]: any; + }; +} + +export interface Table { + alias: string; + name: string; + fields?: Array<{ + id: string; + name: string; + type: string; + }>; +} + +export interface TableRow { + id: string; + employeeId: string; + [key: string]: any; +} + +export interface BenefitPlan { + id: string; + name: string; + planType?: string; + carrier?: string; + active?: boolean; +} + +export interface BenefitEnrollment { + id: string; + employeeId: string; + planId: string; + startDate: string; + endDate?: string; + coverage?: string; +} + +export interface BenefitDependent { + id: string; + employeeId: string; + firstName: string; + lastName: string; + relationship: string; + dateOfBirth?: string; +} + +export interface PayStub { + id: string; + employeeId: string; + payDate: string; + checkNumber?: string; + grossPay?: number; + netPay?: number; + [key: string]: any; +} + +export interface PayrollDeduction { + id: string; + name: string; + amount?: number; + percentage?: number; + type?: string; +} + +export interface Goal { + id: string; + title: string; + description?: string; + percentComplete?: number; + alignsWithOptionId?: string; + sharedWithEmployeeIds?: string[]; + dueDate?: string; + completionDate?: string; + status?: string; +} + +export interface GoalComment { + id: string; + goalId: string; + employeeId: string; + text: string; + created: string; +} + +export interface TrainingCourse { + id: string; + name: string; + description?: string; + categoryId?: string; + typeId?: string; + required?: boolean; + dueDate?: string; +} + +export interface TrainingCategory { + id: string; + name: string; +} + +export interface TrainingType { + id: string; + name: string; +} + +export interface File { + id: string; + name: string; + originalFileName: string; + size?: number; + dateCreated?: string; + createdBy?: string; + categoryId?: string; + shareWithEmployee?: boolean; +} + +export interface FileCategory { + id: string; + name: string; +} + +export interface Webhook { + id: string; + name: string; + url: string; + format?: string; + frequency?: string; + limit?: number; + postFields?: string[]; +} + +export interface BambooHRError { + message: string; + status?: number; + errors?: any[]; +} diff --git a/servers/bamboohr/src/ui/react-app/benefits-enrollment.tsx b/servers/bamboohr/src/ui/react-app/benefits-enrollment.tsx new file mode 100644 index 0000000..c134599 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/benefits-enrollment.tsx @@ -0,0 +1,240 @@ +import React, { useState } from 'react'; +import { Heart, Shield, Eye, DollarSign, ChevronRight } from 'lucide-react'; + +export const BenefitsEnrollment: React.FC = () => { + const [step, setStep] = useState(1); + + return ( +
+

Benefits Enrollment

+ +
+
+ {[1, 2, 3, 4].map((s) => ( +
+
= s ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-600' + }`} + > + {s} +
+ {s < 4 && ( +
s ? 'bg-blue-600' : 'bg-gray-300'}`} /> + )} +
+ ))} +
+
+ Health + Dental + Vision + Review +
+
+ + {step === 1 && ( +
+

+ + Select Health Insurance Plan +

+
+ {[ + { name: 'PPO Gold', premium: '$450/mo', deductible: '$1,000', coverage: '80/20', recommended: true }, + { name: 'PPO Silver', premium: '$350/mo', deductible: '$2,500', coverage: '70/30', recommended: false }, + { name: 'HMO Basic', premium: '$250/mo', deductible: '$3,500', coverage: '60/40', recommended: false }, + ].map((plan) => ( +
+
+
+

+ {plan.name} + {plan.recommended && ( + Recommended + )} +

+
+
+

Monthly Premium

+

{plan.premium}

+
+
+

Deductible

+

{plan.deductible}

+
+
+

Coverage

+

{plan.coverage}

+
+
+
+ +
+
+ ))} +
+
+ +
+
+ )} + + {step === 2 && ( +
+

+ + Select Dental Insurance Plan +

+
+ {[ + { name: 'Dental Premium', premium: '$75/mo', coverage: 'Full Coverage' }, + { name: 'Dental Basic', premium: '$45/mo', coverage: 'Basic Coverage' }, + ].map((plan) => ( +
+
+
+

{plan.name}

+
+
+

Monthly Premium

+

{plan.premium}

+
+
+

Coverage

+

{plan.coverage}

+
+
+
+ +
+
+ ))} +
+
+ + +
+
+ )} + + {step === 3 && ( +
+

+ + Select Vision Insurance Plan +

+
+ {[ + { name: 'Vision Standard', premium: '$25/mo', exams: '1 per year', frames: '$150 allowance' }, + { name: 'Vision Enhanced', premium: '$40/mo', exams: '2 per year', frames: '$250 allowance' }, + ].map((plan) => ( +
+
+
+

{plan.name}

+
+
+

Monthly Premium

+

{plan.premium}

+
+
+

Eye Exams

+

{plan.exams}

+
+
+

Frames

+

{plan.frames}

+
+
+
+ +
+
+ ))} +
+
+ + +
+
+ )} + + {step === 4 && ( +
+

Review Your Selections

+
+
+
+
+

Health Insurance

+

PPO Gold

+
+

$450/mo

+
+
+
+
+
+

Dental Insurance

+

Dental Premium

+
+

$75/mo

+
+
+
+
+
+

Vision Insurance

+

Vision Standard

+
+

$25/mo

+
+
+
+
+
+ Total Monthly Premium: + $550/mo +
+
+
+ + +
+
+ )} +
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/benefits-overview.tsx b/servers/bamboohr/src/ui/react-app/benefits-overview.tsx new file mode 100644 index 0000000..99ae8ae --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/benefits-overview.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Heart, Shield, Eye, DollarSign } from 'lucide-react'; + +export const BenefitsOverview: React.FC = () => { + const benefits = [ + { icon: Heart, name: 'Health Insurance', plan: 'PPO Gold', cost: '$450/mo', coverage: 'Family' }, + { icon: Shield, name: 'Dental Insurance', plan: 'Premium', cost: '$75/mo', coverage: 'Family' }, + { icon: Eye, name: 'Vision Insurance', plan: 'Standard', cost: '$25/mo', coverage: 'Individual' }, + { icon: DollarSign, name: '401(k)', plan: '5% Match', cost: '$0', coverage: 'Active' }, + ]; + + return ( +
+

Benefits Overview

+ +
+ {benefits.map((benefit) => ( +
+
+
+ +
+
+

{benefit.name}

+

{benefit.plan}

+
+
+
+
+ Coverage: + {benefit.coverage} +
+
+ Cost: + {benefit.cost} +
+
+
+ ))} +
+ +
+

Enrolled Dependents

+ + + + + + + + + + + + + + + + + + + + + + + +
NameRelationshipDOBCoverage
Jane DoeSpouse03/15/1988Health, Dental, Vision
Tim DoeChild08/22/2015Health, Dental, Vision
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/custom-report.tsx b/servers/bamboohr/src/ui/react-app/custom-report.tsx new file mode 100644 index 0000000..cf9584b --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/custom-report.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { FileText, Download, Filter } from 'lucide-react'; + +export const CustomReport: React.FC = () => { + const reportData = [ + { id: 1, firstName: 'John', lastName: 'Doe', department: 'Engineering', jobTitle: 'Senior Engineer', hireDate: '2020-01-15', status: 'Active' }, + { id: 2, firstName: 'Jane', lastName: 'Smith', department: 'Product', jobTitle: 'Product Manager', hireDate: '2019-03-20', status: 'Active' }, + { id: 3, firstName: 'Mike', lastName: 'Johnson', department: 'Design', jobTitle: 'UX Designer', hireDate: '2021-06-10', status: 'Active' }, + { id: 4, firstName: 'Sarah', lastName: 'Williams', department: 'HR', jobTitle: 'HR Manager', hireDate: '2018-09-01', status: 'Active' }, + ]; + + return ( +
+
+

+ + Custom Report +

+
+ + +
+
+ +
+

Report Settings

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+

Report Results

+ {reportData.length} records +
+
+
+ + + + + + + + + + + + + + {reportData.map((row) => ( + + + + + + + + + + ))} + +
IDFirst NameLast NameDepartmentJob TitleHire DateStatus
{row.id}{row.firstName}{row.lastName}{row.department}{row.jobTitle}{row.hireDate} + + {row.status} + +
+
+
+ +
+

Summary Statistics

+
+
+

Total Records

+

{reportData.length}

+
+
+

Unique Departments

+

4

+
+
+

Active Status

+

100%

+
+
+

Avg Tenure

+

3.2 years

+
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/employee-dashboard.tsx b/servers/bamboohr/src/ui/react-app/employee-dashboard.tsx new file mode 100644 index 0000000..8bd4019 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/employee-dashboard.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Users, Clock, Target, Award, FileText, TrendingUp } from 'lucide-react'; + +export const EmployeeDashboard: React.FC = () => { + return ( +
+

Employee Dashboard

+ +
+
+
+
+

Total Employees

+

245

+
+ +
+
+ +
+
+
+

Active

+

238

+
+ +
+
+ +
+
+
+

New This Month

+

12

+
+ +
+
+
+ +
+
+

+ + Recent Time Off Requests +

+
+ {[1, 2, 3, 4].map((i) => ( +
+
+

Employee Name

+

Jan {15 + i} - Jan {17 + i}

+
+ + Pending + +
+ ))} +
+
+ +
+

+ + Active Goals +

+
+ {[1, 2, 3, 4].map((i) => ( +
+

Q1 Performance Goal {i}

+
+
+
+

{25 * i}% complete

+
+ ))} +
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/employee-detail.tsx b/servers/bamboohr/src/ui/react-app/employee-detail.tsx new file mode 100644 index 0000000..a8bea66 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/employee-detail.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { User, Mail, Phone, MapPin, Calendar, Briefcase } from 'lucide-react'; + +export const EmployeeDetail: React.FC = () => { + return ( +
+
+
+
+ JD +
+
+

John Doe

+

Senior Software Engineer

+
+
+ + john.doe@company.com +
+
+ + (555) 123-4567 +
+
+ + San Francisco, CA +
+
+ + Hired: Jan 15, 2020 +
+
+
+
+ + +
+
+
+ +
+
+
+

+ + Employment Information +

+
+
+

Department

+

Engineering

+
+
+

Division

+

Product Development

+
+
+

Manager

+

Sarah Johnson

+
+
+

Employee ID

+

EMP-12345

+
+
+

Status

+

Active

+
+
+

Employment Type

+

Full Time

+
+
+
+ +
+

Performance Goals

+
+ {[ + { title: 'Complete React Training', progress: 75 }, + { title: 'Lead Q1 Project', progress: 60 }, + { title: 'Mentor Junior Developers', progress: 90 }, + ].map((goal, i) => ( +
+
+ {goal.title} + {goal.progress}% +
+
+
+
+
+ ))} +
+
+
+ +
+
+

Time Off Balances

+
+
+ PTO + 15 days +
+
+ Sick Leave + 8 days +
+
+ Personal + 3 days +
+
+
+ +
+

Documents

+
+ + + +
+
+ +
+

Quick Actions

+
+ + + +
+
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/employee-directory.tsx b/servers/bamboohr/src/ui/react-app/employee-directory.tsx new file mode 100644 index 0000000..c1dd188 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/employee-directory.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { Search, Filter, Mail, Phone, MapPin } from 'lucide-react'; + +export const EmployeeDirectory: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [filterDept, setFilterDept] = useState('all'); + + const employees = [ + { id: 1, name: 'John Doe', title: 'Software Engineer', dept: 'Engineering', email: 'john@company.com', phone: '555-0101', location: 'San Francisco' }, + { id: 2, name: 'Jane Smith', title: 'Product Manager', dept: 'Product', email: 'jane@company.com', phone: '555-0102', location: 'New York' }, + { id: 3, name: 'Mike Johnson', title: 'Designer', dept: 'Design', email: 'mike@company.com', phone: '555-0103', location: 'Austin' }, + { id: 4, name: 'Sarah Williams', title: 'HR Manager', dept: 'HR', email: 'sarah@company.com', phone: '555-0104', location: 'Chicago' }, + ]; + + return ( +
+

Employee Directory

+ +
+
+
+ + setSearchTerm((e.target as HTMLInputElement).value)} + /> +
+
+ + +
+
+
+ +
+ {employees.map((employee) => ( +
+
+
+ {employee.name.split(' ').map(n => n[0]).join('')} +
+
+

{employee.name}

+

{employee.title}

+

{employee.dept}

+
+
+
+
+ + {employee.email} +
+
+ + {employee.phone} +
+
+ + {employee.location} +
+
+
+ ))} +
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/file-manager.tsx b/servers/bamboohr/src/ui/react-app/file-manager.tsx new file mode 100644 index 0000000..0cd83d9 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/file-manager.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { Folder, File, Upload, Download, Search } from 'lucide-react'; + +export const FileManager: React.FC = () => { + const [selectedFolder, setSelectedFolder] = useState('all'); + + const folders = [ + { name: 'All Files', count: 24 }, + { name: 'Performance Reviews', count: 8 }, + { name: 'Tax Documents', count: 6 }, + { name: 'Certifications', count: 5 }, + { name: 'Contracts', count: 3 }, + { name: 'Personal', count: 2 }, + ]; + + const files = [ + { name: '2023_W2_Form.pdf', category: 'Tax Documents', date: '2024-01-15', size: '245 KB' }, + { name: 'Q4_Performance_Review.pdf', category: 'Performance Reviews', date: '2024-01-10', size: '512 KB' }, + { name: 'Employment_Contract.pdf', category: 'Contracts', date: '2023-12-01', size: '1.2 MB' }, + { name: 'AWS_Certification.pdf', category: 'Certifications', date: '2023-11-15', size: '890 KB' }, + ]; + + return ( +
+

+ + File Manager +

+ +
+
+
+ + +
+ +
+
+ +
+
+

Categories

+
+ {folders.map((folder) => ( + + ))} +
+
+ +
+ + + + + + + + + + + + {files.map((file, i) => ( + + + + + + + + ))} + +
NameCategoryDateSizeActions
+
+ + {file.name} +
+
{file.category}{file.date}{file.size} + +
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/goal-tracker.tsx b/servers/bamboohr/src/ui/react-app/goal-tracker.tsx new file mode 100644 index 0000000..f102ef1 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/goal-tracker.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Target, CheckCircle, Clock } from 'lucide-react'; + +export const GoalTracker: React.FC = () => { + const goals = [ + { id: 1, title: 'Complete React Training', progress: 75, dueDate: '2024-02-15', status: 'active' }, + { id: 2, title: 'Q1 Sales Target', progress: 60, dueDate: '2024-03-31', status: 'active' }, + { id: 3, title: 'Team Leadership Course', progress: 100, dueDate: '2024-01-10', status: 'completed' }, + { id: 4, title: 'Improve Customer Satisfaction', progress: 40, dueDate: '2024-04-30', status: 'active' }, + ]; + + return ( +
+

+ + Goal Tracker +

+ +
+
+
+
+

Total Goals

+

8

+
+ +
+
+
+
+
+

Completed

+

3

+
+ +
+
+
+
+
+

In Progress

+

5

+
+ +
+
+
+ +
+ {goals.map((goal) => ( +
+
+
+

{goal.title}

+

Due: {goal.dueDate}

+
+ + {goal.status === 'completed' ? 'Completed' : 'In Progress'} + +
+
+
+
+
+ {goal.progress}% complete + {goal.status === 'active' && ( + + )} +
+
+ ))} +
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/headcount-analytics.tsx b/servers/bamboohr/src/ui/react-app/headcount-analytics.tsx new file mode 100644 index 0000000..fae7692 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/headcount-analytics.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Users, TrendingUp, Building, MapPin } from 'lucide-react'; + +export const HeadcountAnalytics: React.FC = () => { + return ( +
+

+ + Headcount Analytics +

+ +
+
+

Total Employees

+

245

+

+12 this quarter

+
+
+

Full Time

+

220

+

89.8% of total

+
+
+

Part Time

+

18

+

7.3% of total

+
+
+

Contractors

+

7

+

2.9% of total

+
+
+ +
+
+

+ + By Department +

+
+ {[ + { dept: 'Engineering', count: 85, color: 'blue' }, + { dept: 'Sales', count: 52, color: 'green' }, + { dept: 'Product', count: 28, color: 'purple' }, + { dept: 'HR', count: 18, color: 'orange' }, + { dept: 'Finance', count: 24, color: 'red' }, + { dept: 'Operations', count: 38, color: 'teal' }, + ].map((item) => ( +
+
+ {item.dept} + {item.count} +
+
+
+
+
+ ))} +
+
+ +
+

+ + By Location +

+
+ {[ + { location: 'San Francisco, CA', count: 98 }, + { location: 'New York, NY', count: 67 }, + { location: 'Austin, TX', count: 42 }, + { location: 'Chicago, IL', count: 28 }, + { location: 'Remote', count: 10 }, + ].map((item) => ( +
+ {item.location} + {item.count} +
+ ))} +
+
+
+ +
+

+ + Growth Trend +

+
+ {[180, 195, 208, 220, 228, 235, 240, 242, 243, 244, 244, 245].map((count, i) => ( +
+
{count}
+
+ ))} +
+
+ Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec +
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/new-hires.tsx b/servers/bamboohr/src/ui/react-app/new-hires.tsx new file mode 100644 index 0000000..80094d3 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/new-hires.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { UserPlus, Calendar, CheckCircle, Clock } from 'lucide-react'; + +export const NewHires: React.FC = () => { + return ( +
+

+ + New Hires +

+ +
+
+
+
+

This Month

+

8

+
+ +
+
+
+
+
+

This Quarter

+

24

+
+ +
+
+
+
+
+

This Year

+

45

+
+ +
+
+
+ +
+
+

Recent New Hires

+
+ + + + + + + + + + + + {[ + { name: 'Emily Chen', position: 'Senior Engineer', dept: 'Engineering', date: '2024-01-22', status: 'onboarding' }, + { name: 'Michael Brown', position: 'Sales Rep', dept: 'Sales', date: '2024-01-15', status: 'onboarding' }, + { name: 'Sarah Davis', position: 'Product Manager', dept: 'Product', date: '2024-01-08', status: 'active' }, + { name: 'James Wilson', position: 'Designer', dept: 'Design', date: '2024-01-03', status: 'active' }, + { name: 'Lisa Martinez', position: 'HR Coordinator', dept: 'HR', date: '2023-12-18', status: 'active' }, + ].map((hire, i) => ( + + + + + + + + ))} + +
NamePositionDepartmentStart DateStatus
{hire.name}{hire.position}{hire.dept}{hire.date} + + {hire.status === 'onboarding' ? 'Onboarding' : 'Active'} + +
+
+ +
+

Onboarding Progress

+
+ {[ + { name: 'Emily Chen', progress: 45 }, + { name: 'Michael Brown', progress: 80 }, + ].map((hire) => ( +
+
+ {hire.name} + {hire.progress}% complete +
+
+
+
+
+ ))} +
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/org-chart.tsx b/servers/bamboohr/src/ui/react-app/org-chart.tsx new file mode 100644 index 0000000..413c207 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/org-chart.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Network, Users } from 'lucide-react'; + +export const OrgChart: React.FC = () => { + return ( +
+

+ + Organization Chart +

+ +
+ {/* CEO Level */} +
+
+
Sarah Johnson
+
CEO
+
+
+ + {/* Executive Level */} +
+
+
+
Mike Chen
+
CTO
+
+
+
+
+
+
Lisa Williams
+
CFO
+
+
+
+
+
+
Tom Brown
+
VP Operations
+
+
+
+
+ + {/* Department Level */} +
+
+
+
Engineering
+
25 employees
+
+
+
+
Product
+
12 employees
+
+
+
+
+
Finance
+
8 employees
+
+
+
+
Accounting
+
6 employees
+
+
+
+
+
HR
+
10 employees
+
+
+
+
Sales
+
15 employees
+
+
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/payroll-dashboard.tsx b/servers/bamboohr/src/ui/react-app/payroll-dashboard.tsx new file mode 100644 index 0000000..4d30535 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/payroll-dashboard.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { DollarSign, TrendingUp, Calendar, FileText } from 'lucide-react'; + +export const PayrollDashboard: React.FC = () => { + return ( +
+

+ + Payroll Dashboard +

+ +
+
+
+
+

Current Period

+

$245,000

+
+ +
+
+
+
+
+

YTD Payroll

+

$2.4M

+
+ +
+
+
+
+
+

Next Payroll

+

Jan 31

+
+ +
+
+
+
+
+

Employees

+

245

+
+ +
+
+
+ +
+
+

Recent Pay Stubs

+
+ {['January 15, 2024', 'January 1, 2024', 'December 15, 2023'].map((date, i) => ( +
+
+

{date}

+

Gross: $4,200 | Net: $3,150

+
+ +
+ ))} +
+
+ +
+

Deductions

+
+
+ Federal Tax + $650 +
+
+ State Tax + $280 +
+
+ Social Security + $260 +
+
+ Medicare + $61 +
+
+ 401(k) + $210 +
+
+ Total Deductions + $1,461 +
+
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/report-builder.tsx b/servers/bamboohr/src/ui/react-app/report-builder.tsx new file mode 100644 index 0000000..9179e5d --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/report-builder.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { FileText, Plus, X } from 'lucide-react'; + +export const ReportBuilder: React.FC = () => { + const [selectedFields, setSelectedFields] = useState(['firstName', 'lastName', 'department']); + + const availableFields = [ + 'firstName', 'lastName', 'email', 'department', 'jobTitle', 'hireDate', + 'supervisor', 'location', 'employeeNumber', 'status', 'salary', 'workPhone' + ]; + + const addField = (field: string) => { + if (!selectedFields.includes(field)) { + setSelectedFields([...selectedFields, field]); + } + }; + + const removeField = (field: string) => { + setSelectedFields(selectedFields.filter(f => f !== field)); + }; + + return ( +
+

+ + Report Builder +

+ +
+
+

Available Fields

+
+ {availableFields.map((field) => ( + + ))} +
+
+ +
+

Selected Fields ({selectedFields.length})

+ {selectedFields.length === 0 ? ( +

No fields selected. Add fields from the left.

+ ) : ( +
+ {selectedFields.map((field, index) => ( +
+
+ {index + 1}. + {field.replace(/([A-Z])/g, ' $1').trim()} +
+ +
+ ))} +
+ )} + +
+

Report Settings

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/time-off-balances.tsx b/servers/bamboohr/src/ui/react-app/time-off-balances.tsx new file mode 100644 index 0000000..835c894 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/time-off-balances.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { PieChart, TrendingUp } from 'lucide-react'; + +export const TimeOffBalances: React.FC = () => { + const balances = [ + { type: 'PTO', available: 15, used: 5, total: 20, color: 'blue' }, + { type: 'Sick Leave', available: 8, used: 2, total: 10, color: 'green' }, + { type: 'Personal', available: 3, used: 2, total: 5, color: 'purple' }, + ]; + + return ( +
+

+ + Time Off Balances +

+ +
+ {balances.map((balance) => ( +
+

{balance.type}

+ +
+
+ Used + {balance.used} of {balance.total} days +
+
+
+
+
+ +
+
+ Available: + {balance.available} +
+
+ Used: + {balance.used} +
+
+ Total: + {balance.total} +
+
+ + +
+ ))} +
+ +
+

+ + Accrual Schedule +

+
+
+
+

Next PTO Accrual

+

February 1, 2024

+
+ +1.67 days +
+
+
+

Next Sick Leave Accrual

+

February 1, 2024

+
+ +0.83 days +
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/time-off-calendar.tsx b/servers/bamboohr/src/ui/react-app/time-off-calendar.tsx new file mode 100644 index 0000000..660910d --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/time-off-calendar.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react'; + +export const TimeOffCalendar: React.FC = () => { + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const dates = Array.from({ length: 35 }, (_, i) => i + 1); + + return ( +
+

+ + Time Off Calendar +

+ +
+
+ +

January 2024

+ +
+ +
+ {days.map((day) => ( +
+ {day} +
+ ))} + {dates.map((date) => ( +
+
{date}
+ {date === 15 && ( +
+ John - PTO +
+ )} + {date === 22 && ( +
+ Sarah - Sick +
+ )} +
+ ))} +
+ +
+
+
+ PTO +
+
+
+ Sick Leave +
+
+
+ Personal +
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/time-off-requests.tsx b/servers/bamboohr/src/ui/react-app/time-off-requests.tsx new file mode 100644 index 0000000..b8c0262 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/time-off-requests.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { Clock, Check, X, Filter } from 'lucide-react'; + +export const TimeOffRequests: React.FC = () => { + const [filter, setFilter] = useState('all'); + + const requests = [ + { id: 1, employee: 'John Doe', type: 'PTO', start: '2024-01-15', end: '2024-01-17', days: 3, status: 'pending' }, + { id: 2, employee: 'Jane Smith', type: 'Sick Leave', start: '2024-01-20', end: '2024-01-20', days: 1, status: 'approved' }, + { id: 3, employee: 'Mike Johnson', type: 'Personal', start: '2024-01-25', end: '2024-01-26', days: 2, status: 'pending' }, + { id: 4, employee: 'Sarah Williams', type: 'PTO', start: '2024-01-18', end: '2024-01-19', days: 2, status: 'denied' }, + ]; + + const getStatusColor = (status: string) => { + switch (status) { + case 'approved': return 'bg-green-100 text-green-800'; + case 'denied': return 'bg-red-100 text-red-800'; + case 'pending': return 'bg-yellow-100 text-yellow-800'; + default: return 'bg-gray-100 text-gray-800'; + } + }; + + return ( +
+

+ + Time Off Requests +

+ +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + {requests.map((request) => ( + + + + + + + + + + ))} + +
EmployeeTypeStart DateEnd DateDaysStatusActions
{request.employee}{request.type}{request.start}{request.end}{request.days} + + {request.status.charAt(0).toUpperCase() + request.status.slice(1)} + + + {request.status === 'pending' && ( +
+ + +
+ )} +
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/training-catalog.tsx b/servers/bamboohr/src/ui/react-app/training-catalog.tsx new file mode 100644 index 0000000..1a55569 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/training-catalog.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { BookOpen, Award, Clock, Filter } from 'lucide-react'; + +export const TrainingCatalog: React.FC = () => { + const [filter, setFilter] = useState('all'); + + const courses = [ + { id: 1, title: 'React Advanced Patterns', category: 'Engineering', duration: '8 hours', required: true, enrolled: false }, + { id: 2, title: 'Leadership Fundamentals', category: 'Management', duration: '12 hours', required: false, enrolled: true }, + { id: 3, title: 'Data Privacy & GDPR', category: 'Compliance', duration: '4 hours', required: true, enrolled: true }, + { id: 4, title: 'Effective Communication', category: 'Soft Skills', duration: '6 hours', required: false, enrolled: false }, + ]; + + return ( +
+

+ + Training Catalog +

+ +
+
+ + +
+
+ +
+ {courses.map((course) => ( +
+
+
+

{course.title}

+
+ + + {course.category} + + + + {course.duration} + +
+
+ {course.required && ( + + Required + + )} +
+ +
+ ))} +
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/training-progress.tsx b/servers/bamboohr/src/ui/react-app/training-progress.tsx new file mode 100644 index 0000000..1f8e680 --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/training-progress.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { BookOpen, Award, Clock, CheckCircle } from 'lucide-react'; + +export const TrainingProgress: React.FC = () => { + return ( +
+

+ + Training Progress +

+ +
+
+
+
+

Enrolled

+

12

+
+ +
+
+
+
+
+

In Progress

+

5

+
+ +
+
+
+
+
+

Completed

+

7

+
+ +
+
+
+
+
+

Certifications

+

3

+
+ +
+
+
+ +
+

Active Courses

+
+ {[ + { title: 'React Advanced Patterns', progress: 65, dueDate: '2024-02-15', hours: 8 }, + { title: 'Leadership Fundamentals', progress: 40, dueDate: '2024-03-01', hours: 12 }, + { title: 'Data Privacy & GDPR', progress: 85, dueDate: '2024-01-31', hours: 4 }, + ].map((course, i) => ( +
+
+
+

{course.title}

+

+ {course.hours} hours • Due: {course.dueDate} +

+
+ {course.progress}% +
+
+
+
+ +
+ ))} +
+
+ +
+
+

+ + Completed Courses +

+
+ {[ + { title: 'Effective Communication', completedDate: '2024-01-10', hours: 6 }, + { title: 'Time Management', completedDate: '2023-12-15', hours: 4 }, + { title: 'Conflict Resolution', completedDate: '2023-11-20', hours: 5 }, + ].map((course, i) => ( +
+
+

{course.title}

+

Completed: {course.completedDate}

+
+ +
+ ))} +
+
+ +
+

+ + Earned Certifications +

+
+ {[ + { title: 'AWS Certified Developer', issuedDate: '2023-10-15', expiryDate: '2026-10-15' }, + { title: 'Scrum Master Certified', issuedDate: '2023-06-01', expiryDate: '2025-06-01' }, + { title: 'Security+ Certified', issuedDate: '2023-03-10', expiryDate: '2026-03-10' }, + ].map((cert, i) => ( +
+
+
+

{cert.title}

+

Issued: {cert.issuedDate}

+

Expires: {cert.expiryDate}

+
+ +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/servers/bamboohr/src/ui/react-app/turnover-report.tsx b/servers/bamboohr/src/ui/react-app/turnover-report.tsx new file mode 100644 index 0000000..88b18ae --- /dev/null +++ b/servers/bamboohr/src/ui/react-app/turnover-report.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { TrendingDown, AlertCircle, Users, Percent } from 'lucide-react'; + +export const TurnoverReport: React.FC = () => { + return ( +
+

+ + Turnover Report +

+ +
+
+
+
+

Annual Turnover

+

12.3%

+
+ +
+
+
+
+
+

Departures YTD

+

28

+
+ +
+
+
+
+
+

Avg Tenure

+

3.2yr

+
+ +
+
+
+
+
+

At Risk

+

15

+
+ +
+
+
+ +
+
+

Turnover by Department

+
+ {[ + { dept: 'Sales', rate: 18.5, departures: 12 }, + { dept: 'Engineering', rate: 10.2, departures: 9 }, + { dept: 'Customer Support', rate: 15.8, departures: 4 }, + { dept: 'HR', rate: 5.5, departures: 1 }, + { dept: 'Finance', rate: 8.3, departures: 2 }, + ].map((item) => ( +
+
+ {item.dept} + + {item.rate}% ({item.departures} employees) + +
+
+
+
+
+ ))} +
+
+ +
+

Exit Reasons

+
+ {[ + { reason: 'Better Opportunity', count: 11 }, + { reason: 'Relocation', count: 6 }, + { reason: 'Career Change', count: 5 }, + { reason: 'Compensation', count: 4 }, + { reason: 'Other', count: 2 }, + ].map((item) => ( +
+ {item.reason} + {item.count} +
+ ))} +
+
+
+ +
+

Recent Departures

+ + + + + + + + + + + + {[ + { name: 'Alice Johnson', dept: 'Sales', tenure: '2.5 years', lastDay: '2024-01-15', reason: 'Better Opportunity' }, + { name: 'Bob Smith', dept: 'Engineering', tenure: '1.8 years', lastDay: '2024-01-10', reason: 'Relocation' }, + { name: 'Carol White', dept: 'Customer Support', tenure: '3.2 years', lastDay: '2024-01-05', reason: 'Career Change' }, + ].map((emp, i) => ( + + + + + + + + ))} + +
NameDepartmentTenureLast DayReason
{emp.name}{emp.dept}{emp.tenure}{emp.lastDay}{emp.reason}
+
+
+ ); +}; diff --git a/servers/bamboohr/tsconfig.json b/servers/bamboohr/tsconfig.json index de6431e..3329397 100644 --- a/servers/bamboohr/tsconfig.json +++ b/servers/bamboohr/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022", "DOM"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "jsx": "react" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/servers/basecamp/README.md b/servers/basecamp/README.md new file mode 100644 index 0000000..80fd4f8 --- /dev/null +++ b/servers/basecamp/README.md @@ -0,0 +1,266 @@ +# Basecamp MCP Server + +A comprehensive Model Context Protocol (MCP) server for Basecamp 4 API integration. + +## Features + +- **Complete API Coverage**: 50+ tools covering all major Basecamp 4 API endpoints +- **Rich UI Components**: 18 React MCP apps for visualizing and managing Basecamp data +- **Robust Client**: OAuth2 authentication, automatic pagination, and comprehensive error handling +- **Type-Safe**: Full TypeScript implementation with detailed type definitions + +## Installation + +```bash +npm install +npm run build +``` + +## Configuration + +Set the following environment variables: + +```bash +export BASECAMP_ACCOUNT_ID="your-account-id" +export BASECAMP_ACCESS_TOKEN="your-oauth-token" +export BASECAMP_USER_AGENT="YourApp (your-email@example.com)" # Optional +``` + +### Getting Your Credentials + +1. **Account ID**: Found in your Basecamp URL: `https://3.basecamp.com/{ACCOUNT_ID}/` +2. **Access Token**: Create an OAuth2 application at https://launchpad.37signals.com/integrations + - Follow Basecamp's OAuth flow to get an access token + - Scopes required: Full access to projects, todos, messages, etc. + +## Usage + +### As MCP Server + +Add to your MCP client configuration: + +```json +{ + "mcpServers": { + "basecamp": { + "command": "node", + "args": ["/path/to/basecamp-mcp/dist/main.js"], + "env": { + "BASECAMP_ACCOUNT_ID": "your-account-id", + "BASECAMP_ACCESS_TOKEN": "your-token" + } + } + } +} +``` + +### Standalone + +```bash +npm start +``` + +## Available Tools (50+) + +### Projects (7 tools) +- `basecamp_projects_list` - List all projects +- `basecamp_project_get` - Get project details +- `basecamp_project_create` - Create new project +- `basecamp_project_update` - Update project +- `basecamp_project_archive` - Archive project +- `basecamp_project_trash` - Move project to trash +- `basecamp_project_tools_list` - List project tools (dock) + +### Todolists (5 tools) +- `basecamp_todolists_list` - List todolists in project +- `basecamp_todolist_get` - Get todolist details +- `basecamp_todolist_create` - Create new todolist +- `basecamp_todolist_update` - Update todolist +- `basecamp_todolist_reorder` - Reorder todolists + +### Todos (8 tools) +- `basecamp_todos_list` - List todos in todolist +- `basecamp_todo_get` - Get todo details +- `basecamp_todo_create` - Create new todo +- `basecamp_todo_update` - Update todo +- `basecamp_todo_complete` - Mark todo as complete +- `basecamp_todo_uncomplete` - Mark todo as incomplete +- `basecamp_todos_reorder` - Reorder todos + +### Messages (5 tools) +- `basecamp_messages_list` - List messages on message board +- `basecamp_message_get` - Get message details +- `basecamp_message_create` - Create new message +- `basecamp_message_update` - Update message +- `basecamp_message_trash` - Move message to trash + +### Comments (4 tools) +- `basecamp_comments_list` - List comments on recording +- `basecamp_comment_get` - Get comment details +- `basecamp_comment_create` - Create new comment +- `basecamp_comment_update` - Update comment + +### Campfire (2 tools) +- `basecamp_campfire_lines_list` - List recent chat messages +- `basecamp_campfire_line_create` - Send chat message + +### Schedules (5 tools) +- `basecamp_schedules_list` - List schedules in project +- `basecamp_schedule_entries_list` - List schedule entries +- `basecamp_schedule_entry_get` - Get entry details +- `basecamp_schedule_entry_create` - Create new schedule entry +- `basecamp_schedule_entry_update` - Update schedule entry + +### Documents (4 tools) +- `basecamp_documents_list` - List documents in vault +- `basecamp_document_get` - Get document details +- `basecamp_document_create` - Create new document +- `basecamp_document_update` - Update document + +### Uploads (3 tools) +- `basecamp_uploads_list` - List uploaded files +- `basecamp_upload_get` - Get upload details +- `basecamp_upload_create` - Upload file + +### People (6 tools) +- `basecamp_people_list` - List all people in account +- `basecamp_person_get` - Get person details +- `basecamp_project_people_list` - List people in project +- `basecamp_project_person_add` - Add person to project +- `basecamp_project_person_remove` - Remove person from project +- `basecamp_profile_get` - Get current user profile + +### Questionnaires (4 tools) +- `basecamp_questionnaires_list` - List automatic check-ins +- `basecamp_questionnaire_get` - Get questionnaire details +- `basecamp_questions_list` - List questions in questionnaire +- `basecamp_answers_list` - List answers to question + +### Webhooks (4 tools) +- `basecamp_webhooks_list` - List all webhooks +- `basecamp_webhook_create` - Create new webhook +- `basecamp_webhook_update` - Update webhook +- `basecamp_webhook_delete` - Delete webhook + +### Recordings (5 tools) +- `basecamp_recordings_list` - List all recordings (generic content) +- `basecamp_recording_get` - Get recording details +- `basecamp_recording_archive` - Archive recording +- `basecamp_recording_unarchive` - Unarchive recording +- `basecamp_recording_trash` - Move recording to trash + +## MCP Apps (18 React Components) + +### Project Management +- **project-dashboard** - Overview of all projects with filtering +- **project-detail** - Detailed single project view with dock +- **project-grid** - Grid view of all projects + +### Task Management +- **todo-board** - Kanban-style todo board +- **todo-detail** - Detailed todo view with comments + +### Communication +- **message-board** - Message board posts list +- **message-detail** - Single message with comments +- **campfire-chat** - Real-time team chat interface + +### Planning +- **schedule-calendar** - Calendar view of schedule entries +- **project-timeline** - Gantt-style timeline + +### Content +- **document-browser** - Browse and manage documents +- **file-manager** - File upload management + +### People +- **people-directory** - Team member directory + +### Check-ins +- **checkin-dashboard** - Automatic check-in overview +- **checkin-responses** - View responses to check-ins + +### Insights +- **activity-feed** - Recent project activity +- **search-results** - Global search interface +- **hill-chart** - Visual progress tracking + +## Architecture + +``` +src/ +├── clients/ +│ └── basecamp.ts # API client with OAuth2 & pagination +├── tools/ +│ ├── projects-tools.ts # Project management tools +│ ├── todolists-tools.ts # Todolist tools +│ ├── todos-tools.ts # Todo tools +│ ├── messages-tools.ts # Message board tools +│ ├── comments-tools.ts # Comment tools +│ ├── campfires-tools.ts # Chat tools +│ ├── schedules-tools.ts # Schedule tools +│ ├── documents-tools.ts # Document tools +│ ├── uploads-tools.ts # File upload tools +│ ├── people-tools.ts # People management tools +│ ├── questionnaires-tools.ts # Check-in tools +│ ├── webhooks-tools.ts # Webhook tools +│ └── recordings-tools.ts # Generic recordings tools +├── types/ +│ └── index.ts # TypeScript type definitions +├── ui/ +│ └── react-app/ +│ ├── index.tsx # App registry +│ └── apps/ # 18 React MCP apps +├── server.ts # MCP server implementation +└── main.ts # Entry point +``` + +## API Reference + +### Basecamp 4 API + +- Base URL: `https://3.basecampapi.com/{account_id}/` +- Authentication: OAuth2 Bearer token +- Rate limiting: Respected via standard headers +- User-Agent: Required for all requests + +### Error Handling + +The client handles common errors: +- 401: Invalid/expired token +- 403: Permission denied +- 404: Resource not found +- 429: Rate limit exceeded +- 500-503: Server errors + +## Development + +### Build + +```bash +npm run build +``` + +### Watch Mode + +```bash +npm run dev +``` + +## License + +MIT + +## Contributing + +Contributions welcome! Please ensure: +- All tools have proper input validation +- TypeScript types are comprehensive +- Error handling is consistent +- MCP apps are self-contained + +## Links + +- [Basecamp API Documentation](https://github.com/basecamp/bc3-api) +- [Model Context Protocol](https://modelcontextprotocol.io) +- [MCPEngine Repository](https://github.com/BusyBee3333/mcpengine) diff --git a/servers/basecamp/package.json b/servers/basecamp/package.json index 9714a36..ff36bab 100644 --- a/servers/basecamp/package.json +++ b/servers/basecamp/package.json @@ -1,20 +1,41 @@ { - "name": "mcp-server-basecamp", + "name": "@mcpengine/basecamp-server", "version": "1.0.0", + "description": "MCP server for Basecamp 4 API integration", "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "main": "dist/main.js", + "bin": { + "basecamp-mcp": "./dist/main.js" }, + "scripts": { + "build": "tsc && chmod +x dist/main.js", + "dev": "tsc --watch", + "start": "node dist/main.js", + "prepare": "npm run build" + }, + "keywords": [ + "mcp", + "basecamp", + "model-context-protocol", + "basecamp-api" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.9", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.15.0", + "date-fns": "^4.1.0" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.5", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/servers/basecamp/src/clients/basecamp.ts b/servers/basecamp/src/clients/basecamp.ts new file mode 100644 index 0000000..bdd7ad6 --- /dev/null +++ b/servers/basecamp/src/clients/basecamp.ts @@ -0,0 +1,166 @@ +/** + * Basecamp 4 API Client + * Handles authentication, pagination, error handling + */ + +import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; +import type { + BasecampConfig, + PaginationLinks, + BasecampError, +} from '../types/index.js'; + +export class BasecampClient { + private client: AxiosInstance; + private accountId: string; + private userAgent: string; + + constructor(config: BasecampConfig) { + this.accountId = config.accountId; + this.userAgent = config.userAgent || 'MCPEngine Basecamp MCP Server (https://github.com/BusyBee3333/mcpengine)'; + + this.client = axios.create({ + baseURL: `https://3.basecampapi.com/${this.accountId}`, + headers: { + 'Authorization': `Bearer ${config.accessToken}`, + 'User-Agent': this.userAgent, + 'Content-Type': 'application/json', + }, + timeout: 30000, + }); + + // Response interceptor for error handling + this.client.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response) { + const status = error.response.status; + const data = error.response.data; + + switch (status) { + case 401: + throw new Error('Unauthorized: Invalid or expired access token'); + case 403: + throw new Error('Forbidden: You do not have permission to access this resource'); + case 404: + throw new Error('Not Found: The requested resource does not exist'); + case 429: + throw new Error('Rate Limited: Too many requests, please try again later'); + case 500: + case 502: + case 503: + throw new Error('Basecamp server error: Please try again later'); + default: + throw new Error(data?.error || data?.message || `API Error: ${status}`); + } + } else if (error.request) { + throw new Error('Network error: Unable to connect to Basecamp API'); + } else { + throw new Error(`Request failed: ${error.message}`); + } + } + ); + } + + /** + * Parse pagination links from Link header + */ + private parseLinkHeader(linkHeader?: string): PaginationLinks { + if (!linkHeader) return {}; + + const links: PaginationLinks = {}; + const parts = linkHeader.split(','); + + for (const part of parts) { + const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/); + if (match) { + const [, url, rel] = match; + if (rel === 'next') links.next = url; + if (rel === 'prev') links.prev = url; + } + } + + return links; + } + + /** + * GET request with pagination support + */ + async get(path: string, params?: Record): Promise<{ data: T; links: PaginationLinks }> { + const response = await this.client.get(path, { params }); + const links = this.parseLinkHeader(response.headers['link']); + return { data: response.data, links }; + } + + /** + * POST request + */ + async post(path: string, data?: any): Promise { + const response = await this.client.post(path, data); + return response.data; + } + + /** + * PUT request + */ + async put(path: string, data?: any): Promise { + const response = await this.client.put(path, data); + return response.data; + } + + /** + * PATCH request + */ + async patch(path: string, data?: any): Promise { + const response = await this.client.patch(path, data); + return response.data; + } + + /** + * DELETE request + */ + async delete(path: string): Promise { + await this.client.delete(path); + } + + /** + * GET all pages (auto-pagination) + */ + async getAllPages(path: string, params?: Record): Promise { + const results: T[] = []; + let currentUrl: string | undefined = path; + + while (currentUrl) { + const response = await this.client.get(currentUrl, currentUrl === path ? { params } : undefined); + const data = response.data; + + if (Array.isArray(data)) { + results.push(...data); + } + + const links = this.parseLinkHeader(response.headers['link']); + currentUrl = links.next; + } + + return results; + } + + /** + * Upload file (multipart/form-data) + * Note: File uploads require special handling in MCP context + */ + async uploadFile(path: string, fileData: any): Promise { + // File upload implementation would require special handling + // for multipart/form-data in the MCP context + throw new Error('File upload functionality requires special MCP handling'); + } + + // Helper methods for common endpoints + getAccountId(): string { + return this.accountId; + } + + getUserAgent(): string { + return this.userAgent; + } +} diff --git a/servers/basecamp/src/index.ts b/servers/basecamp/src/index.ts deleted file mode 100644 index 4854d41..0000000 --- a/servers/basecamp/src/index.ts +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// ============================================ -// CONFIGURATION -// ============================================ -const MCP_NAME = "basecamp"; -const MCP_VERSION = "1.0.0"; - -// ============================================ -// API CLIENT (OAuth 2.0) -// Basecamp 4 API uses: https://3.basecampapi.com/{account_id}/ -// ============================================ -class BasecampClient { - private accessToken: string; - private accountId: string; - private baseUrl: string; - private userAgent: string; - - constructor(accessToken: string, accountId: string, appIdentity: string) { - this.accessToken = accessToken; - this.accountId = accountId; - this.baseUrl = `https://3.basecampapi.com/${accountId}`; - this.userAgent = appIdentity; // Required: "AppName (contact@email.com)" - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - "User-Agent": this.userAgent, - ...options.headers, - }, - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Basecamp API error: ${response.status} - ${error}`); - } - - const text = await response.text(); - return text ? JSON.parse(text) : { success: true }; - } - - async get(endpoint: string) { - return this.request(endpoint, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } - - async put(endpoint: string, data: any) { - return this.request(endpoint, { - method: "PUT", - body: JSON.stringify(data), - }); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_projects", - description: "List all projects in the Basecamp account", - inputSchema: { - type: "object" as const, - properties: { - status: { - type: "string", - enum: ["active", "archived", "trashed"], - description: "Filter by project status (default: active)" - }, - }, - }, - }, - { - name: "get_project", - description: "Get details of a specific project including its dock (tools)", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (required)" }, - }, - required: ["project_id"], - }, - }, - { - name: "list_todos", - description: "List to-dos from a to-do list in a project", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (required)" }, - todolist_id: { type: "number", description: "To-do list ID (required)" }, - status: { - type: "string", - enum: ["active", "archived", "trashed"], - description: "Filter by status" - }, - completed: { type: "boolean", description: "Filter by completion (true=completed, false=pending)" }, - }, - required: ["project_id", "todolist_id"], - }, - }, - { - name: "create_todo", - description: "Create a new to-do in a to-do list", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (required)" }, - todolist_id: { type: "number", description: "To-do list ID (required)" }, - content: { type: "string", description: "To-do content/title (required)" }, - description: { type: "string", description: "Rich text description (HTML)" }, - assignee_ids: { - type: "array", - items: { type: "number" }, - description: "Array of person IDs to assign" - }, - due_on: { type: "string", description: "Due date (YYYY-MM-DD)" }, - starts_on: { type: "string", description: "Start date (YYYY-MM-DD)" }, - notify: { type: "boolean", description: "Notify assignees (default: false)" }, - }, - required: ["project_id", "todolist_id", "content"], - }, - }, - { - name: "complete_todo", - description: "Mark a to-do as complete", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (required)" }, - todo_id: { type: "number", description: "To-do ID (required)" }, - }, - required: ["project_id", "todo_id"], - }, - }, - { - name: "list_messages", - description: "List messages from a project's message board", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (required)" }, - message_board_id: { type: "number", description: "Message board ID (required, get from project dock)" }, - }, - required: ["project_id", "message_board_id"], - }, - }, - { - name: "create_message", - description: "Create a new message on a project's message board", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (required)" }, - message_board_id: { type: "number", description: "Message board ID (required)" }, - subject: { type: "string", description: "Message subject (required)" }, - content: { type: "string", description: "Message content in HTML (required)" }, - status: { - type: "string", - enum: ["active", "draft"], - description: "Post status (default: active)" - }, - category_id: { type: "number", description: "Message type/category ID" }, - }, - required: ["project_id", "message_board_id", "subject", "content"], - }, - }, - { - name: "list_people", - description: "List all people in the Basecamp account or a specific project", - inputSchema: { - type: "object" as const, - properties: { - project_id: { type: "number", description: "Project ID (optional - if provided, lists project members only)" }, - }, - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: BasecampClient, name: string, args: any) { - switch (name) { - case "list_projects": { - let endpoint = "/projects.json"; - if (args.status === "archived") { - endpoint = "/projects/archive.json"; - } else if (args.status === "trashed") { - endpoint = "/projects/trash.json"; - } - return await client.get(endpoint); - } - case "get_project": { - const { project_id } = args; - return await client.get(`/projects/${project_id}.json`); - } - case "list_todos": { - const { project_id, todolist_id, completed } = args; - let endpoint = `/buckets/${project_id}/todolists/${todolist_id}/todos.json`; - if (completed === true) { - endpoint += "?completed=true"; - } - return await client.get(endpoint); - } - case "create_todo": { - const { project_id, todolist_id, content, description, assignee_ids, due_on, starts_on, notify } = args; - const payload: any = { content }; - if (description) payload.description = description; - if (assignee_ids) payload.assignee_ids = assignee_ids; - if (due_on) payload.due_on = due_on; - if (starts_on) payload.starts_on = starts_on; - if (notify !== undefined) payload.notify = notify; - return await client.post(`/buckets/${project_id}/todolists/${todolist_id}/todos.json`, payload); - } - case "complete_todo": { - const { project_id, todo_id } = args; - return await client.post(`/buckets/${project_id}/todos/${todo_id}/completion.json`, {}); - } - case "list_messages": { - const { project_id, message_board_id } = args; - return await client.get(`/buckets/${project_id}/message_boards/${message_board_id}/messages.json`); - } - case "create_message": { - const { project_id, message_board_id, subject, content, status, category_id } = args; - const payload: any = { subject, content }; - if (status) payload.status = status; - if (category_id) payload.category_id = category_id; - return await client.post(`/buckets/${project_id}/message_boards/${message_board_id}/messages.json`, payload); - } - case "list_people": { - const { project_id } = args; - if (project_id) { - return await client.get(`/projects/${project_id}/people.json`); - } - return await client.get("/people.json"); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const accessToken = process.env.BASECAMP_ACCESS_TOKEN; - const accountId = process.env.BASECAMP_ACCOUNT_ID; - const appIdentity = process.env.BASECAMP_APP_IDENTITY || "MCPServer (mcp@example.com)"; - - if (!accessToken) { - console.error("Error: BASECAMP_ACCESS_TOKEN environment variable required"); - console.error("Obtain via OAuth 2.0 flow: https://github.com/basecamp/api/blob/master/sections/authentication.md"); - process.exit(1); - } - - if (!accountId) { - console.error("Error: BASECAMP_ACCOUNT_ID environment variable required"); - console.error("Find your account ID in the Basecamp URL: https://3.basecamp.com/{ACCOUNT_ID}/"); - process.exit(1); - } - - const client = new BasecampClient(accessToken, accountId, appIdentity); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - const result = await handleTool(client, name, args || {}); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } - }); - - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); diff --git a/servers/basecamp/src/main.ts b/servers/basecamp/src/main.ts new file mode 100644 index 0000000..1a0cc84 --- /dev/null +++ b/servers/basecamp/src/main.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/** + * Basecamp MCP Server Entry Point + */ + +import { BasecampServer } from './server.js'; + +// Configuration from environment variables +const accountId = process.env.BASECAMP_ACCOUNT_ID; +const accessToken = process.env.BASECAMP_ACCESS_TOKEN; +const userAgent = process.env.BASECAMP_USER_AGENT; + +if (!accountId) { + console.error('Error: BASECAMP_ACCOUNT_ID environment variable is required'); + process.exit(1); +} + +if (!accessToken) { + console.error('Error: BASECAMP_ACCESS_TOKEN environment variable is required'); + process.exit(1); +} + +const server = new BasecampServer({ + accountId, + accessToken, + userAgent, +}); + +server.run().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); diff --git a/servers/basecamp/src/server.ts b/servers/basecamp/src/server.ts new file mode 100644 index 0000000..8147fe4 --- /dev/null +++ b/servers/basecamp/src/server.ts @@ -0,0 +1,143 @@ +/** + * Basecamp MCP Server + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ErrorCode, + McpError, +} from '@modelcontextprotocol/sdk/types.js'; +import { BasecampClient } from './clients/basecamp.js'; +import { registerProjectsTools } from './tools/projects-tools.js'; +import { registerTodolistsTools } from './tools/todolists-tools.js'; +import { registerTodosTools } from './tools/todos-tools.js'; +import { registerMessagesTools } from './tools/messages-tools.js'; +import { registerCommentsTools } from './tools/comments-tools.js'; +import { registerCampfiresTools } from './tools/campfires-tools.js'; +import { registerSchedulesTools } from './tools/schedules-tools.js'; +import { registerDocumentsTools } from './tools/documents-tools.js'; +import { registerUploadsTools } from './tools/uploads-tools.js'; +import { registerPeopleTools } from './tools/people-tools.js'; +import { registerQuestionnairesTools } from './tools/questionnaires-tools.js'; +import { registerWebhooksTools } from './tools/webhooks-tools.js'; +import { registerRecordingsTools } from './tools/recordings-tools.js'; + +interface BasecampServerConfig { + accountId: string; + accessToken: string; + userAgent?: string; +} + +export class BasecampServer { + private server: Server; + private client: BasecampClient; + private tools: Map; + + constructor(config: BasecampServerConfig) { + this.server = new Server( + { + name: 'basecamp-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.client = new BasecampClient({ + accountId: config.accountId, + accessToken: config.accessToken, + userAgent: config.userAgent, + }); + + this.tools = new Map(); + this.registerAllTools(); + this.setupHandlers(); + + // Error handling + this.server.onerror = (error) => { + console.error('[MCP Error]', error); + }; + + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); + } + + private registerAllTools() { + const toolRegistrars = [ + registerProjectsTools, + registerTodolistsTools, + registerTodosTools, + registerMessagesTools, + registerCommentsTools, + registerCampfiresTools, + registerSchedulesTools, + registerDocumentsTools, + registerUploadsTools, + registerPeopleTools, + registerQuestionnairesTools, + registerWebhooksTools, + registerRecordingsTools, + ]; + + for (const registrar of toolRegistrars) { + const tools = registrar(this.client); + for (const tool of tools) { + this.tools.set(tool.name, tool); + } + } + + console.error(`[Basecamp MCP] Registered ${this.tools.size} tools`); + } + + private setupHandlers() { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + const tools = Array.from(this.tools.values()).map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })); + + return { tools }; + }); + + // Handle tool calls + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + const tool = this.tools.get(name); + if (!tool) { + throw new McpError( + ErrorCode.MethodNotFound, + `Tool not found: ${name}` + ); + } + + try { + return await tool.handler(args || {}); + } catch (error: any) { + const errorMessage = error.message || 'Unknown error occurred'; + console.error(`[Tool Error] ${name}:`, errorMessage); + + throw new McpError( + ErrorCode.InternalError, + `Tool execution failed: ${errorMessage}` + ); + } + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('[Basecamp MCP] Server running on stdio'); + } +} diff --git a/servers/basecamp/src/tools/campfires-tools.ts b/servers/basecamp/src/tools/campfires-tools.ts new file mode 100644 index 0000000..e3f09f0 --- /dev/null +++ b/servers/basecamp/src/tools/campfires-tools.ts @@ -0,0 +1,80 @@ +/** + * Basecamp Campfires Tools (Chat) + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { CampfireLine, CreateCampfireLineRequest } from '../types/index.js'; + +export function registerCampfiresTools(client: BasecampClient) { + return [ + { + name: 'basecamp_campfire_lines_list', + description: 'List recent chat lines in a campfire', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + campfire_id: { + type: 'number', + description: 'The ID of the campfire', + }, + }, + required: ['project_id', 'campfire_id'], + }, + handler: async (args: { project_id: number; campfire_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/chats/${args.campfire_id}/lines.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_campfire_line_create', + description: 'Send a chat message to campfire', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + campfire_id: { + type: 'number', + description: 'The ID of the campfire', + }, + content: { + type: 'string', + description: 'The chat message content', + }, + }, + required: ['project_id', 'campfire_id', 'content'], + }, + handler: async (args: { project_id: number; campfire_id: number } & CreateCampfireLineRequest) => { + const { project_id, campfire_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/chats/${campfire_id}/lines.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/comments-tools.ts b/servers/basecamp/src/tools/comments-tools.ts new file mode 100644 index 0000000..8ef1ddf --- /dev/null +++ b/servers/basecamp/src/tools/comments-tools.ts @@ -0,0 +1,150 @@ +/** + * Basecamp Comments Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Comment, CreateCommentRequest } from '../types/index.js'; + +export function registerCommentsTools(client: BasecampClient) { + return [ + { + name: 'basecamp_comments_list', + description: 'List all comments on a recording (message, todo, document, etc.)', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + recording_id: { + type: 'number', + description: 'The ID of the recording (parent item)', + }, + }, + required: ['project_id', 'recording_id'], + }, + handler: async (args: { project_id: number; recording_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/recordings/${args.recording_id}/comments.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_comment_get', + description: 'Get a specific comment by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + comment_id: { + type: 'number', + description: 'The ID of the comment', + }, + }, + required: ['project_id', 'comment_id'], + }, + handler: async (args: { project_id: number; comment_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/comments/${args.comment_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_comment_create', + description: 'Create a comment on a recording', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + recording_id: { + type: 'number', + description: 'The ID of the recording to comment on', + }, + content: { + type: 'string', + description: 'The comment content (HTML supported)', + }, + }, + required: ['project_id', 'recording_id', 'content'], + }, + handler: async (args: { project_id: number; recording_id: number } & CreateCommentRequest) => { + const { project_id, recording_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/recordings/${recording_id}/comments.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_comment_update', + description: 'Update a comment', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + comment_id: { + type: 'number', + description: 'The ID of the comment', + }, + content: { + type: 'string', + description: 'Updated content', + }, + }, + required: ['project_id', 'comment_id', 'content'], + }, + handler: async (args: { project_id: number; comment_id: number; content: string }) => { + const { project_id, comment_id, content } = args; + const data = await client.put( + `/buckets/${project_id}/comments/${comment_id}.json`, + { content } + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/documents-tools.ts b/servers/basecamp/src/tools/documents-tools.ts new file mode 100644 index 0000000..ec46f88 --- /dev/null +++ b/servers/basecamp/src/tools/documents-tools.ts @@ -0,0 +1,164 @@ +/** + * Basecamp Documents Tools (Docs & Files Vault) + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Document, CreateDocumentRequest, UpdateDocumentRequest } from '../types/index.js'; + +export function registerDocumentsTools(client: BasecampClient) { + return [ + { + name: 'basecamp_documents_list', + description: 'List all documents in a vault', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + vault_id: { + type: 'number', + description: 'The ID of the vault', + }, + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by status', + }, + }, + required: ['project_id', 'vault_id'], + }, + handler: async (args: { project_id: number; vault_id: number; status?: string }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/vaults/${args.vault_id}/documents.json`, + args.status ? { status: args.status } : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_document_get', + description: 'Get a specific document by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + document_id: { + type: 'number', + description: 'The ID of the document', + }, + }, + required: ['project_id', 'document_id'], + }, + handler: async (args: { project_id: number; document_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/documents/${args.document_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_document_create', + description: 'Create a new document in a vault', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + vault_id: { + type: 'number', + description: 'The ID of the vault', + }, + title: { + type: 'string', + description: 'Document title', + }, + content: { + type: 'string', + description: 'Document content (HTML supported)', + }, + }, + required: ['project_id', 'vault_id', 'title', 'content'], + }, + handler: async (args: { project_id: number; vault_id: number } & CreateDocumentRequest) => { + const { project_id, vault_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/vaults/${vault_id}/documents.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_document_update', + description: 'Update a document', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + document_id: { + type: 'number', + description: 'The ID of the document', + }, + title: { + type: 'string', + description: 'Updated title', + }, + content: { + type: 'string', + description: 'Updated content', + }, + }, + required: ['project_id', 'document_id'], + }, + handler: async (args: { project_id: number; document_id: number } & UpdateDocumentRequest) => { + const { project_id, document_id, ...updates } = args; + const data = await client.put( + `/buckets/${project_id}/documents/${document_id}.json`, + updates + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/messages-tools.ts b/servers/basecamp/src/tools/messages-tools.ts new file mode 100644 index 0000000..05f9920 --- /dev/null +++ b/servers/basecamp/src/tools/messages-tools.ts @@ -0,0 +1,203 @@ +/** + * Basecamp Messages Tools (Message Board Posts) + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Message, CreateMessageRequest } from '../types/index.js'; + +export function registerMessagesTools(client: BasecampClient) { + return [ + { + name: 'basecamp_messages_list', + description: 'List all messages in a project message board', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + message_board_id: { + type: 'number', + description: 'The ID of the message board', + }, + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by message status', + }, + }, + required: ['project_id', 'message_board_id'], + }, + handler: async (args: { project_id: number; message_board_id: number; status?: string }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/message_boards/${args.message_board_id}/messages.json`, + args.status ? { status: args.status } : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_message_get', + description: 'Get a specific message by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + message_id: { + type: 'number', + description: 'The ID of the message', + }, + }, + required: ['project_id', 'message_id'], + }, + handler: async (args: { project_id: number; message_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/messages/${args.message_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_message_create', + description: 'Create a new message on the message board', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + message_board_id: { + type: 'number', + description: 'The ID of the message board', + }, + subject: { + type: 'string', + description: 'The subject/title of the message', + }, + content: { + type: 'string', + description: 'The content/body of the message (HTML supported)', + }, + category_id: { + type: 'number', + description: 'Optional category ID', + }, + }, + required: ['project_id', 'message_board_id', 'subject', 'content'], + }, + handler: async (args: { project_id: number; message_board_id: number } & CreateMessageRequest) => { + const { project_id, message_board_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/message_boards/${message_board_id}/messages.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_message_update', + description: 'Update a message', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + message_id: { + type: 'number', + description: 'The ID of the message', + }, + subject: { + type: 'string', + description: 'Updated subject', + }, + content: { + type: 'string', + description: 'Updated content', + }, + }, + required: ['project_id', 'message_id'], + }, + handler: async (args: { + project_id: number; + message_id: number; + subject?: string; + content?: string; + }) => { + const { project_id, message_id, ...updates } = args; + const data = await client.put( + `/buckets/${project_id}/messages/${message_id}.json`, + updates + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_message_trash', + description: 'Move a message to trash', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + message_id: { + type: 'number', + description: 'The ID of the message', + }, + }, + required: ['project_id', 'message_id'], + }, + handler: async (args: { project_id: number; message_id: number }) => { + await client.delete(`/buckets/${args.project_id}/messages/${args.message_id}.json`); + return { + content: [ + { + type: 'text', + text: `Message ${args.message_id} moved to trash`, + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/people-tools.ts b/servers/basecamp/src/tools/people-tools.ts new file mode 100644 index 0000000..e54ce4b --- /dev/null +++ b/servers/basecamp/src/tools/people-tools.ts @@ -0,0 +1,165 @@ +/** + * Basecamp People Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Person } from '../types/index.js'; + +export function registerPeopleTools(client: BasecampClient) { + return [ + { + name: 'basecamp_people_list', + description: 'List all people in the Basecamp account', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const { data } = await client.get('/people.json'); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_person_get', + description: 'Get a specific person by ID', + inputSchema: { + type: 'object', + properties: { + person_id: { + type: 'number', + description: 'The ID of the person', + }, + }, + required: ['person_id'], + }, + handler: async (args: { person_id: number }) => { + const { data } = await client.get(`/people/${args.person_id}.json`); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_people_list', + description: 'List all people with access to a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + const { data } = await client.get(`/projects/${args.project_id}/people.json`); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_person_add', + description: 'Grant a person access to a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + person_id: { + type: 'number', + description: 'The ID of the person to add', + }, + }, + required: ['project_id', 'person_id'], + }, + handler: async (args: { project_id: number; person_id: number }) => { + await client.post( + `/projects/${args.project_id}/people.json`, + { person_id: args.person_id } + ); + return { + content: [ + { + type: 'text', + text: `Person ${args.person_id} added to project ${args.project_id}`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_person_remove', + description: 'Remove a person from a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + person_id: { + type: 'number', + description: 'The ID of the person to remove', + }, + }, + required: ['project_id', 'person_id'], + }, + handler: async (args: { project_id: number; person_id: number }) => { + await client.delete(`/projects/${args.project_id}/people/${args.person_id}.json`); + return { + content: [ + { + type: 'text', + text: `Person ${args.person_id} removed from project ${args.project_id}`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_profile_get', + description: 'Get the current authenticated user profile', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const { data } = await client.get('/my/profile.json'); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/projects-tools.ts b/servers/basecamp/src/tools/projects-tools.ts new file mode 100644 index 0000000..24a2fa9 --- /dev/null +++ b/servers/basecamp/src/tools/projects-tools.ts @@ -0,0 +1,206 @@ +/** + * Basecamp Projects Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Project, CreateProjectRequest, UpdateProjectRequest } from '../types/index.js'; + +export function registerProjectsTools(client: BasecampClient) { + return [ + { + name: 'basecamp_projects_list', + description: 'List all projects. Returns active, archived, or trashed projects.', + inputSchema: { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by project status (default: active)', + }, + }, + }, + handler: async (args: { status?: string }) => { + const status = args.status || 'active'; + const { data } = await client.get(`/projects.json`, { status }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_get', + description: 'Get a specific project by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + const { data } = await client.get(`/projects/${args.project_id}.json`); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_create', + description: 'Create a new project', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'The name of the project', + }, + description: { + type: 'string', + description: 'Optional project description', + }, + }, + required: ['name'], + }, + handler: async (args: CreateProjectRequest) => { + const data = await client.post('/projects.json', args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_update', + description: 'Update a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + name: { + type: 'string', + description: 'Updated project name', + }, + description: { + type: 'string', + description: 'Updated project description', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number } & UpdateProjectRequest) => { + const { project_id, ...updates } = args; + const data = await client.put(`/projects/${project_id}.json`, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_archive', + description: 'Archive a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project to archive', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + const data = await client.put(`/projects/${args.project_id}.json`, { status: 'archived' }); + return { + content: [ + { + type: 'text', + text: `Project ${args.project_id} archived successfully`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_trash', + description: 'Move a project to trash', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project to trash', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + await client.delete(`/projects/${args.project_id}.json`); + return { + content: [ + { + type: 'text', + text: `Project ${args.project_id} moved to trash`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_project_tools_list', + description: 'List all tools (dock items) available in a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + const { data } = await client.get(`/projects/${args.project_id}.json`); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data.dock, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/questionnaires-tools.ts b/servers/basecamp/src/tools/questionnaires-tools.ts new file mode 100644 index 0000000..9a4120d --- /dev/null +++ b/servers/basecamp/src/tools/questionnaires-tools.ts @@ -0,0 +1,134 @@ +/** + * Basecamp Questionnaires Tools (Automatic Check-ins) + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Questionnaire, Question, Answer } from '../types/index.js'; + +export function registerQuestionnairesTools(client: BasecampClient) { + return [ + { + name: 'basecamp_questionnaires_list', + description: 'List all questionnaires (automatic check-ins) in a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/questionnaires.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_questionnaire_get', + description: 'Get a specific questionnaire', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + questionnaire_id: { + type: 'number', + description: 'The ID of the questionnaire', + }, + }, + required: ['project_id', 'questionnaire_id'], + }, + handler: async (args: { project_id: number; questionnaire_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/questionnaires/${args.questionnaire_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_questions_list', + description: 'List all questions in a questionnaire', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + questionnaire_id: { + type: 'number', + description: 'The ID of the questionnaire', + }, + }, + required: ['project_id', 'questionnaire_id'], + }, + handler: async (args: { project_id: number; questionnaire_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/questionnaires/${args.questionnaire_id}/questions.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_answers_list', + description: 'List answers to a question', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + question_id: { + type: 'number', + description: 'The ID of the question', + }, + }, + required: ['project_id', 'question_id'], + }, + handler: async (args: { project_id: number; question_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/questions/${args.question_id}/answers.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/recordings-tools.ts b/servers/basecamp/src/tools/recordings-tools.ts new file mode 100644 index 0000000..cebfb5e --- /dev/null +++ b/servers/basecamp/src/tools/recordings-tools.ts @@ -0,0 +1,182 @@ +/** + * Basecamp Recordings Tools (Generic Content) + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Recording } from '../types/index.js'; + +export function registerRecordingsTools(client: BasecampClient) { + return [ + { + name: 'basecamp_recordings_list', + description: 'List all recordings (generic content) in a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + type: { + type: 'string', + description: 'Filter by recording type (e.g., "Message", "Todo", "Document")', + }, + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by status', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number; type?: string; status?: string }) => { + const params: any = {}; + if (args.type) params.type = args.type; + if (args.status) params.status = args.status; + + const { data } = await client.get( + `/buckets/${args.project_id}/recordings.json`, + Object.keys(params).length > 0 ? params : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_recording_get', + description: 'Get a specific recording by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + recording_id: { + type: 'number', + description: 'The ID of the recording', + }, + }, + required: ['project_id', 'recording_id'], + }, + handler: async (args: { project_id: number; recording_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/recordings/${args.recording_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_recording_archive', + description: 'Archive a recording', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + recording_id: { + type: 'number', + description: 'The ID of the recording', + }, + }, + required: ['project_id', 'recording_id'], + }, + handler: async (args: { project_id: number; recording_id: number }) => { + await client.put( + `/buckets/${args.project_id}/recordings/${args.recording_id}/status/archived.json`, + {} + ); + return { + content: [ + { + type: 'text', + text: `Recording ${args.recording_id} archived successfully`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_recording_unarchive', + description: 'Unarchive a recording', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + recording_id: { + type: 'number', + description: 'The ID of the recording', + }, + }, + required: ['project_id', 'recording_id'], + }, + handler: async (args: { project_id: number; recording_id: number }) => { + await client.put( + `/buckets/${args.project_id}/recordings/${args.recording_id}/status/active.json`, + {} + ); + return { + content: [ + { + type: 'text', + text: `Recording ${args.recording_id} unarchived successfully`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_recording_trash', + description: 'Move a recording to trash', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + recording_id: { + type: 'number', + description: 'The ID of the recording', + }, + }, + required: ['project_id', 'recording_id'], + }, + handler: async (args: { project_id: number; recording_id: number }) => { + await client.delete( + `/buckets/${args.project_id}/recordings/${args.recording_id}.json` + ); + return { + content: [ + { + type: 'text', + text: `Recording ${args.recording_id} moved to trash`, + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/schedules-tools.ts b/servers/basecamp/src/tools/schedules-tools.ts new file mode 100644 index 0000000..a88e4f7 --- /dev/null +++ b/servers/basecamp/src/tools/schedules-tools.ts @@ -0,0 +1,246 @@ +/** + * Basecamp Schedules Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Schedule, ScheduleEntry, CreateScheduleEntryRequest, UpdateScheduleEntryRequest } from '../types/index.js'; + +export function registerSchedulesTools(client: BasecampClient) { + return [ + { + name: 'basecamp_schedules_list', + description: 'List all schedules in a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/schedules.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_schedule_entries_list', + description: 'List entries in a schedule', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + schedule_id: { + type: 'number', + description: 'The ID of the schedule', + }, + start_date: { + type: 'string', + description: 'Filter entries starting from this date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'Filter entries up to this date (YYYY-MM-DD)', + }, + }, + required: ['project_id', 'schedule_id'], + }, + handler: async (args: { + project_id: number; + schedule_id: number; + start_date?: string; + end_date?: string; + }) => { + const params: any = {}; + if (args.start_date) params.start_date = args.start_date; + if (args.end_date) params.end_date = args.end_date; + + const { data } = await client.get( + `/buckets/${args.project_id}/schedules/${args.schedule_id}/entries.json`, + Object.keys(params).length > 0 ? params : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_schedule_entry_get', + description: 'Get a specific schedule entry', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + entry_id: { + type: 'number', + description: 'The ID of the schedule entry', + }, + }, + required: ['project_id', 'entry_id'], + }, + handler: async (args: { project_id: number; entry_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/schedule_entries/${args.entry_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_schedule_entry_create', + description: 'Create a new schedule entry/event', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + schedule_id: { + type: 'number', + description: 'The ID of the schedule', + }, + summary: { + type: 'string', + description: 'Event summary/title', + }, + description: { + type: 'string', + description: 'Event description', + }, + starts_at: { + type: 'string', + description: 'Start date/time (ISO 8601)', + }, + ends_at: { + type: 'string', + description: 'End date/time (ISO 8601)', + }, + all_day: { + type: 'boolean', + description: 'Whether this is an all-day event', + }, + participant_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Array of person IDs to invite', + }, + notify: { + type: 'boolean', + description: 'Send notifications to participants', + }, + }, + required: ['project_id', 'schedule_id', 'summary', 'starts_at'], + }, + handler: async (args: { project_id: number; schedule_id: number } & CreateScheduleEntryRequest) => { + const { project_id, schedule_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/schedules/${schedule_id}/entries.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_schedule_entry_update', + description: 'Update a schedule entry', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + entry_id: { + type: 'number', + description: 'The ID of the schedule entry', + }, + summary: { + type: 'string', + description: 'Updated summary', + }, + description: { + type: 'string', + description: 'Updated description', + }, + starts_at: { + type: 'string', + description: 'Updated start time', + }, + ends_at: { + type: 'string', + description: 'Updated end time', + }, + all_day: { + type: 'boolean', + description: 'Updated all-day flag', + }, + participant_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Updated participants', + }, + notify: { + type: 'boolean', + description: 'Send notifications', + }, + }, + required: ['project_id', 'entry_id'], + }, + handler: async (args: { project_id: number; entry_id: number } & UpdateScheduleEntryRequest) => { + const { project_id, entry_id, ...updates } = args; + const data = await client.put( + `/buckets/${project_id}/schedule_entries/${entry_id}.json`, + updates + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/todolists-tools.ts b/servers/basecamp/src/tools/todolists-tools.ts new file mode 100644 index 0000000..2f51fb8 --- /dev/null +++ b/servers/basecamp/src/tools/todolists-tools.ts @@ -0,0 +1,194 @@ +/** + * Basecamp Todolists Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Todolist, CreateTodolistRequest } from '../types/index.js'; + +export function registerTodolistsTools(client: BasecampClient) { + return [ + { + name: 'basecamp_todolists_list', + description: 'List all todolists in a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by todolist status', + }, + }, + required: ['project_id'], + }, + handler: async (args: { project_id: number; status?: string }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/todolists.json`, + args.status ? { status: args.status } : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todolist_get', + description: 'Get a specific todolist by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todolist_id: { + type: 'number', + description: 'The ID of the todolist', + }, + }, + required: ['project_id', 'todolist_id'], + }, + handler: async (args: { project_id: number; todolist_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/todolists/${args.todolist_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todolist_create', + description: 'Create a new todolist in a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + name: { + type: 'string', + description: 'The name of the todolist', + }, + description: { + type: 'string', + description: 'Optional description', + }, + }, + required: ['project_id', 'name'], + }, + handler: async (args: { project_id: number } & CreateTodolistRequest) => { + const { project_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/todolists.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todolist_update', + description: 'Update a todolist', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todolist_id: { + type: 'number', + description: 'The ID of the todolist', + }, + name: { + type: 'string', + description: 'Updated name', + }, + description: { + type: 'string', + description: 'Updated description', + }, + }, + required: ['project_id', 'todolist_id'], + }, + handler: async (args: { + project_id: number; + todolist_id: number; + name?: string; + description?: string; + }) => { + const { project_id, todolist_id, ...updates } = args; + const data = await client.put( + `/buckets/${project_id}/todolists/${todolist_id}.json`, + updates + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todolist_reorder', + description: 'Reorder todolists within a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todolist_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Array of todolist IDs in desired order', + }, + }, + required: ['project_id', 'todolist_ids'], + }, + handler: async (args: { project_id: number; todolist_ids: number[] }) => { + await client.put(`/buckets/${args.project_id}/todolists/reorder.json`, { + todolist_ids: args.todolist_ids, + }); + return { + content: [ + { + type: 'text', + text: 'Todolists reordered successfully', + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/todos-tools.ts b/servers/basecamp/src/tools/todos-tools.ts new file mode 100644 index 0000000..c62ce04 --- /dev/null +++ b/servers/basecamp/src/tools/todos-tools.ts @@ -0,0 +1,312 @@ +/** + * Basecamp Todos Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Todo, CreateTodoRequest, UpdateTodoRequest } from '../types/index.js'; + +export function registerTodosTools(client: BasecampClient) { + return [ + { + name: 'basecamp_todos_list', + description: 'List all todos in a todolist', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todolist_id: { + type: 'number', + description: 'The ID of the todolist', + }, + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by todo status', + }, + completed: { + type: 'boolean', + description: 'Filter by completion status', + }, + }, + required: ['project_id', 'todolist_id'], + }, + handler: async (args: { + project_id: number; + todolist_id: number; + status?: string; + completed?: boolean; + }) => { + const params: any = {}; + if (args.status) params.status = args.status; + if (args.completed !== undefined) params.completed = args.completed; + + const { data } = await client.get( + `/buckets/${args.project_id}/todolists/${args.todolist_id}/todos.json`, + Object.keys(params).length > 0 ? params : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todo_get', + description: 'Get a specific todo by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todo_id: { + type: 'number', + description: 'The ID of the todo', + }, + }, + required: ['project_id', 'todo_id'], + }, + handler: async (args: { project_id: number; todo_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/todos/${args.todo_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todo_create', + description: 'Create a new todo in a todolist', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todolist_id: { + type: 'number', + description: 'The ID of the todolist', + }, + content: { + type: 'string', + description: 'The content/title of the todo', + }, + description: { + type: 'string', + description: 'Optional description', + }, + assignee_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Array of person IDs to assign', + }, + due_on: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + starts_on: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + notify: { + type: 'boolean', + description: 'Send notifications to assignees', + }, + }, + required: ['project_id', 'todolist_id', 'content'], + }, + handler: async (args: { project_id: number; todolist_id: number } & CreateTodoRequest) => { + const { project_id, todolist_id, ...payload } = args; + const data = await client.post( + `/buckets/${project_id}/todolists/${todolist_id}/todos.json`, + payload + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todo_update', + description: 'Update a todo', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todo_id: { + type: 'number', + description: 'The ID of the todo', + }, + content: { + type: 'string', + description: 'Updated content/title', + }, + description: { + type: 'string', + description: 'Updated description', + }, + assignee_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Updated assignees', + }, + due_on: { + type: 'string', + description: 'Updated due date (YYYY-MM-DD)', + }, + starts_on: { + type: 'string', + description: 'Updated start date (YYYY-MM-DD)', + }, + notify: { + type: 'boolean', + description: 'Send notifications', + }, + }, + required: ['project_id', 'todo_id'], + }, + handler: async (args: { project_id: number; todo_id: number } & UpdateTodoRequest) => { + const { project_id, todo_id, ...updates } = args; + const data = await client.put( + `/buckets/${project_id}/todos/${todo_id}.json`, + updates + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_todo_complete', + description: 'Mark a todo as complete', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todo_id: { + type: 'number', + description: 'The ID of the todo', + }, + }, + required: ['project_id', 'todo_id'], + }, + handler: async (args: { project_id: number; todo_id: number }) => { + const data = await client.post( + `/buckets/${args.project_id}/todos/${args.todo_id}/completion.json`, + {} + ); + return { + content: [ + { + type: 'text', + text: `Todo ${args.todo_id} marked as complete`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_todo_uncomplete', + description: 'Mark a todo as incomplete', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todo_id: { + type: 'number', + description: 'The ID of the todo', + }, + }, + required: ['project_id', 'todo_id'], + }, + handler: async (args: { project_id: number; todo_id: number }) => { + await client.delete(`/buckets/${args.project_id}/todos/${args.todo_id}/completion.json`); + return { + content: [ + { + type: 'text', + text: `Todo ${args.todo_id} marked as incomplete`, + }, + ], + }; + }, + }, + + { + name: 'basecamp_todos_reorder', + description: 'Reorder todos within a todolist', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + todolist_id: { + type: 'number', + description: 'The ID of the todolist', + }, + todo_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Array of todo IDs in desired order', + }, + }, + required: ['project_id', 'todolist_id', 'todo_ids'], + }, + handler: async (args: { project_id: number; todolist_id: number; todo_ids: number[] }) => { + await client.put( + `/buckets/${args.project_id}/todolists/${args.todolist_id}/todos/reorder.json`, + { todo_ids: args.todo_ids } + ); + return { + content: [ + { + type: 'text', + text: 'Todos reordered successfully', + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/uploads-tools.ts b/servers/basecamp/src/tools/uploads-tools.ts new file mode 100644 index 0000000..a77ca23 --- /dev/null +++ b/servers/basecamp/src/tools/uploads-tools.ts @@ -0,0 +1,115 @@ +/** + * Basecamp Uploads Tools (File Uploads) + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Upload } from '../types/index.js'; + +export function registerUploadsTools(client: BasecampClient) { + return [ + { + name: 'basecamp_uploads_list', + description: 'List all uploads in a vault', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + vault_id: { + type: 'number', + description: 'The ID of the vault', + }, + status: { + type: 'string', + enum: ['active', 'archived', 'trashed'], + description: 'Filter by status', + }, + }, + required: ['project_id', 'vault_id'], + }, + handler: async (args: { project_id: number; vault_id: number; status?: string }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/vaults/${args.vault_id}/uploads.json`, + args.status ? { status: args.status } : undefined + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_upload_get', + description: 'Get a specific upload by ID', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + upload_id: { + type: 'number', + description: 'The ID of the upload', + }, + }, + required: ['project_id', 'upload_id'], + }, + handler: async (args: { project_id: number; upload_id: number }) => { + const { data } = await client.get( + `/buckets/${args.project_id}/uploads/${args.upload_id}.json` + ); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_upload_create', + description: 'Upload a file to a vault (Note: Requires multipart/form-data, may need special handling)', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'The ID of the project', + }, + vault_id: { + type: 'number', + description: 'The ID of the vault', + }, + description: { + type: 'string', + description: 'File description/caption', + }, + }, + required: ['project_id', 'vault_id'], + }, + handler: async (args: { project_id: number; vault_id: number; description?: string }) => { + // Note: File upload requires special handling - this is a placeholder + // In practice, you'd need to handle the file binary data + return { + content: [ + { + type: 'text', + text: 'File upload requires multipart/form-data handling. Use the Basecamp API directly or upload via web UI.', + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/tools/webhooks-tools.ts b/servers/basecamp/src/tools/webhooks-tools.ts new file mode 100644 index 0000000..ab47b54 --- /dev/null +++ b/servers/basecamp/src/tools/webhooks-tools.ts @@ -0,0 +1,123 @@ +/** + * Basecamp Webhooks Tools + */ + +import { BasecampClient } from '../clients/basecamp.js'; +import type { Webhook, CreateWebhookRequest, UpdateWebhookRequest } from '../types/index.js'; + +export function registerWebhooksTools(client: BasecampClient) { + return [ + { + name: 'basecamp_webhooks_list', + description: 'List all webhooks for the account', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const { data } = await client.get('/webhooks.json'); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_webhook_create', + description: 'Create a new webhook', + inputSchema: { + type: 'object', + properties: { + payload_url: { + type: 'string', + description: 'The URL to send webhook payloads to', + }, + types: { + type: 'array', + items: { type: 'string' }, + description: 'Array of event types to subscribe to (e.g., ["Todo", "Comment"])', + }, + }, + required: ['payload_url'], + }, + handler: async (args: CreateWebhookRequest) => { + const data = await client.post('/webhooks.json', args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_webhook_update', + description: 'Update a webhook', + inputSchema: { + type: 'object', + properties: { + webhook_id: { + type: 'number', + description: 'The ID of the webhook', + }, + payload_url: { + type: 'string', + description: 'Updated payload URL', + }, + types: { + type: 'array', + items: { type: 'string' }, + description: 'Updated event types', + }, + }, + required: ['webhook_id'], + }, + handler: async (args: { webhook_id: number } & UpdateWebhookRequest) => { + const { webhook_id, ...updates } = args; + const data = await client.put(`/webhooks/${webhook_id}.json`, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(data, null, 2), + }, + ], + }; + }, + }, + + { + name: 'basecamp_webhook_delete', + description: 'Delete a webhook', + inputSchema: { + type: 'object', + properties: { + webhook_id: { + type: 'number', + description: 'The ID of the webhook', + }, + }, + required: ['webhook_id'], + }, + handler: async (args: { webhook_id: number }) => { + await client.delete(`/webhooks/${args.webhook_id}.json`); + return { + content: [ + { + type: 'text', + text: `Webhook ${args.webhook_id} deleted successfully`, + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/basecamp/src/types/index.ts b/servers/basecamp/src/types/index.ts new file mode 100644 index 0000000..5ff36e0 --- /dev/null +++ b/servers/basecamp/src/types/index.ts @@ -0,0 +1,569 @@ +/** + * Basecamp 4 API Type Definitions + */ + +export interface BasecampConfig { + accountId: string; + accessToken: string; + userAgent?: string; +} + +export interface PaginationLinks { + next?: string; + prev?: string; +} + +export interface BasecampError { + error: string; + message?: string; +} + +// Base types +export interface Person { + id: number; + attachable_sgid: string; + name: string; + email_address: string; + personable_type: string; + title: string; + bio: string | null; + location: string | null; + created_at: string; + updated_at: string; + admin: boolean; + owner: boolean; + client: boolean; + employee: boolean; + time_zone: string; + avatar_url: string; + company: { + id: number; + name: string; + }; +} + +export interface Project { + id: number; + status: 'active' | 'archived' | 'trashed'; + created_at: string; + updated_at: string; + name: string; + description: string; + purpose: string; + clients_enabled: boolean; + bookmark_url: string; + url: string; + app_url: string; + dock: DockItem[]; + bookmarked: boolean; +} + +export interface DockItem { + id: number; + title: string; + name: string; + enabled: boolean; + position: number; + url: string; + app_url: string; +} + +export interface Todolist { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Todolist'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + description: string; + completed: boolean; + completed_ratio: string; + name: string; + todos_url: string; + groups_url: string; + app_todos_url: string; +} + +export interface Todo { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Todo'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + description: string; + completed: boolean; + content: string; + starts_on: string | null; + due_on: string | null; + assignees: Person[]; + completion_url: string; + completion_subscriber_ids: number[]; + position: number; +} + +export interface Message { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Message'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + subject: string; + content: string; + category_id: number | null; +} + +export interface Comment { + id: number; + status: 'active' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Comment'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + content: string; +} + +export interface CampfireLine { + id: number; + status: 'active'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Chat::Line'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + content: string; +} + +export interface Schedule { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Schedule'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + entries_count: number; + entries_url: string; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; +} + +export interface ScheduleEntry { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Schedule::Entry'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + summary: string; + description: string; + starts_at: string; + ends_at: string; + all_day: boolean; + participant_ids: number[]; +} + +export interface Document { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Document'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + content: string; +} + +export interface Upload { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Upload'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + description: string; + filename: string; + filesize: number; + content_type: string; + download_url: string; + byte_size: number; + width: number | null; + height: number | null; +} + +export interface Questionnaire { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Questionnaire'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + questions_count: number; + questions_url: string; +} + +export interface Question { + id: number; + status: 'active'; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Question'; + url: string; + app_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + paused: boolean; + schedule: string; + answers_count: number; + answers_url: string; +} + +export interface Answer { + id: number; + status: 'active'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: 'Answer'; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + comments_count: number; + comments_url: string; + parent: { + id: number; + title: string; + type: string; + url: string; + app_url: string; + }; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; + content: string; + group_on: string; +} + +export interface Webhook { + id: number; + url: string; + payload_url: string; + types: string[]; + active: boolean; + created_at: string; + updated_at: string; +} + +export interface Recording { + id: number; + status: 'active' | 'archived' | 'trashed'; + visible_to_clients: boolean; + created_at: string; + updated_at: string; + title: string; + inherits_status: boolean; + type: string; + url: string; + app_url: string; + bookmark_url: string; + subscription_url: string; + bucket: { + id: number; + name: string; + type: 'Project'; + }; + creator: Person; +} + +// Request/Response types +export interface CreateProjectRequest { + name: string; + description?: string; +} + +export interface UpdateProjectRequest { + name?: string; + description?: string; +} + +export interface CreateTodolistRequest { + name: string; + description?: string; +} + +export interface CreateTodoRequest { + content: string; + description?: string; + assignee_ids?: number[]; + completion_subscriber_ids?: number[]; + notify?: boolean; + due_on?: string; + starts_on?: string; +} + +export interface UpdateTodoRequest { + content?: string; + description?: string; + assignee_ids?: number[]; + completion_subscriber_ids?: number[]; + notify?: boolean; + due_on?: string; + starts_on?: string; +} + +export interface CreateMessageRequest { + subject: string; + content: string; + category_id?: number; +} + +export interface CreateCommentRequest { + content: string; +} + +export interface CreateCampfireLineRequest { + content: string; +} + +export interface CreateScheduleEntryRequest { + summary: string; + description?: string; + starts_at: string; + ends_at?: string; + all_day?: boolean; + participant_ids?: number[]; + notify?: boolean; +} + +export interface UpdateScheduleEntryRequest { + summary?: string; + description?: string; + starts_at?: string; + ends_at?: string; + all_day?: boolean; + participant_ids?: number[]; + notify?: boolean; +} + +export interface CreateDocumentRequest { + title: string; + content: string; +} + +export interface UpdateDocumentRequest { + title?: string; + content?: string; +} + +export interface CreateWebhookRequest { + payload_url: string; + types?: string[]; +} + +export interface UpdateWebhookRequest { + payload_url?: string; + types?: string[]; +} diff --git a/servers/basecamp/src/ui/react-app/apps/ActivityFeed.tsx b/servers/basecamp/src/ui/react-app/apps/ActivityFeed.tsx new file mode 100644 index 0000000..f6211ce --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/ActivityFeed.tsx @@ -0,0 +1,64 @@ +/** + * Activity Feed App - Recent activity across the project + */ + +import React, { useState } from 'react'; + +export function ActivityFeed({ projectId }: { projectId?: number }) { + const [activities] = useState([ + { id: 1, type: 'todo_completed', person: 'Alice', content: 'completed "Design homepage mockup"', timestamp: '2 hours ago' }, + { id: 2, type: 'comment', person: 'Bob', content: 'commented on "Design Direction"', timestamp: '3 hours ago' }, + { id: 3, type: 'message', person: 'Charlie', content: 'posted "Timeline Update"', timestamp: '5 hours ago' }, + { id: 4, type: 'todo_created', person: 'Diana', content: 'created todo "QA testing round 1"', timestamp: '1 day ago' }, + { id: 5, type: 'file_upload', person: 'Alice', content: 'uploaded "logo-draft-v3.png"', timestamp: '1 day ago' }, + { id: 6, type: 'schedule', person: 'Bob', content: 'scheduled "Design Review" for Jan 15', timestamp: '2 days ago' }, + ]); + + const getActivityIcon = (type: string) => { + switch (type) { + case 'todo_completed': return '✅'; + case 'todo_created': return '📝'; + case 'comment': return '💬'; + case 'message': return '📢'; + case 'file_upload': return '📎'; + case 'schedule': return '📅'; + default: return '•'; + } + }; + + return ( +
+

Activity Feed

+ +
+ {activities.map((activity, index) => ( +
+
+ {getActivityIcon(activity.type)} +
+
+ {activity.person} {activity.content} +
+
{activity.timestamp}
+
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/CampfireChat.tsx b/servers/basecamp/src/ui/react-app/apps/CampfireChat.tsx new file mode 100644 index 0000000..1ec761b --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/CampfireChat.tsx @@ -0,0 +1,63 @@ +/** + * Campfire Chat App - Real-time team chat + */ + +import React, { useState, useEffect } from 'react'; + +export function CampfireChat({ campfireId }: { campfireId?: number }) { + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(''); + + useEffect(() => { + setMessages([ + { id: 1, creator: 'Alice', content: 'Hey team! Just finished the mockups.', created_at: '10:30 AM' }, + { id: 2, creator: 'Bob', content: 'Awesome! Can you share them?', created_at: '10:32 AM' }, + { id: 3, creator: 'Alice', content: 'Sure, uploading now...', created_at: '10:33 AM' }, + { id: 4, creator: 'Charlie', content: '👍', created_at: '10:35 AM' }, + ]); + }, [campfireId]); + + const sendMessage = () => { + if (newMessage.trim()) { + setMessages([...messages, { + id: messages.length + 1, + creator: 'You', + content: newMessage, + created_at: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + }]); + setNewMessage(''); + } + }; + + return ( +
+

Campfire

+ +
+ {messages.map((message) => ( +
+
+ {message.creator} + {message.created_at} +
+
{message.content}
+
+ ))} +
+ +
+ setNewMessage((e.target as HTMLInputElement).value)} + onKeyPress={(e) => e.key === 'Enter' && sendMessage()} + placeholder="Type a message..." + style={{ flex: 1, padding: '12px', borderRadius: '6px', border: '1px solid #ddd', fontSize: '14px' }} + /> + +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/CheckinDashboard.tsx b/servers/basecamp/src/ui/react-app/apps/CheckinDashboard.tsx new file mode 100644 index 0000000..45e4285 --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/CheckinDashboard.tsx @@ -0,0 +1,49 @@ +/** + * Check-in Dashboard App - Overview of automatic check-ins + */ + +import React, { useState } from 'react'; + +export function CheckinDashboard({ projectId }: { projectId?: number }) { + const [questions] = useState([ + { id: 1, title: 'What did you work on today?', schedule: 'Weekdays at 5pm', answers_count: 45, paused: false }, + { id: 2, title: 'What are your goals for this week?', schedule: 'Mondays at 9am', answers_count: 12, paused: false }, + { id: 3, title: 'Any blockers or concerns?', schedule: 'Every other day at 10am', answers_count: 28, paused: true }, + ]); + + return ( +
+
+

Automatic Check-ins

+ +
+ +
+ {questions.map(question => ( +
+
+

{question.title}

+ {question.paused && ( + + PAUSED + + )} +
+
{question.schedule}
+
+ {question.answers_count} responses +
+
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/CheckinResponses.tsx b/servers/basecamp/src/ui/react-app/apps/CheckinResponses.tsx new file mode 100644 index 0000000..f2d9e12 --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/CheckinResponses.tsx @@ -0,0 +1,37 @@ +/** + * Check-in Responses App - View responses to a specific check-in question + */ + +import React, { useState } from 'react'; + +export function CheckinResponses({ questionId }: { questionId?: number }) { + const [responses] = useState([ + { id: 1, person: 'Alice Johnson', content: 'Worked on the homepage mockups and navigation design. Made good progress!', created_at: '2024-01-15 5:30 PM' }, + { id: 2, person: 'Bob Smith', content: 'Implemented the responsive grid system and started on the component library.', created_at: '2024-01-15 5:15 PM' }, + { id: 3, person: 'Charlie Davis', content: 'Finalized content for the About page and started on case studies.', created_at: '2024-01-15 5:45 PM' }, + ]); + + return ( +
+

What did you work on today?

+

January 15, 2024 • {responses.length} responses

+ +
+ {responses.map(response => ( +
+
+ {response.person} + {response.created_at} +
+
{response.content}
+
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/DocumentBrowser.tsx b/servers/basecamp/src/ui/react-app/apps/DocumentBrowser.tsx new file mode 100644 index 0000000..d6cb681 --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/DocumentBrowser.tsx @@ -0,0 +1,50 @@ +/** + * Document Browser App - Browse and manage documents + */ + +import React, { useState } from 'react'; + +export function DocumentBrowser({ vaultId }: { vaultId?: number }) { + const [documents] = useState([ + { id: 1, title: 'Brand Guidelines', updated_at: '2024-01-15', creator: 'Alice' }, + { id: 2, title: 'Technical Specifications', updated_at: '2024-01-12', creator: 'Bob' }, + { id: 3, title: 'Content Strategy', updated_at: '2024-01-10', creator: 'Charlie' }, + ]); + + return ( +
+
+

Documents

+ +
+ +
+ {documents.map(doc => ( +
(e.currentTarget as HTMLDivElement).style.background = '#f9f9f9'} + onMouseLeave={(e) => (e.currentTarget as HTMLDivElement).style.background = 'white'} + > +
📄
+
+
{doc.title}
+
+ Updated {doc.updated_at} by {doc.creator} +
+
+
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/FileManager.tsx b/servers/basecamp/src/ui/react-app/apps/FileManager.tsx new file mode 100644 index 0000000..5af558d --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/FileManager.tsx @@ -0,0 +1,60 @@ +/** + * File Manager App - Manage uploaded files + */ + +import React, { useState } from 'react'; + +export function FileManager({ vaultId }: { vaultId?: number }) { + const [files] = useState([ + { id: 1, filename: 'logo-draft-v3.png', filesize: 245000, content_type: 'image/png', created_at: '2024-01-15', creator: 'Alice' }, + { id: 2, filename: 'wireframes.pdf', filesize: 1240000, content_type: 'application/pdf', created_at: '2024-01-12', creator: 'Bob' }, + { id: 3, filename: 'screenshot-2024-01-10.jpg', filesize: 567000, content_type: 'image/jpeg', created_at: '2024-01-10', creator: 'Charlie' }, + ]); + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }; + + const getFileIcon = (contentType: string) => { + if (contentType.startsWith('image/')) return '🖼️'; + if (contentType.includes('pdf')) return '📄'; + return '📎'; + }; + + return ( +
+
+

Files

+ +
+ +
+ {files.map(file => ( +
+
{getFileIcon(file.content_type)}
+
+
{file.filename}
+
+ {formatFileSize(file.filesize)} • Uploaded {file.created_at} by {file.creator} +
+
+ +
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/HillChart.tsx b/servers/basecamp/src/ui/react-app/apps/HillChart.tsx new file mode 100644 index 0000000..ca67ed1 --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/HillChart.tsx @@ -0,0 +1,81 @@ +/** + * Hill Chart App - Visual progress tracking + */ + +import React, { useState } from 'react'; + +export function HillChart({ projectId }: { projectId?: number }) { + const [items] = useState([ + { id: 1, name: 'Homepage Design', position: 75, color: '#1d8cf8' }, + { id: 2, name: 'Navigation System', position: 60, color: '#00bf9a' }, + { id: 3, name: 'Content Strategy', position: 40, color: '#ff8d72' }, + { id: 4, name: 'QA Testing', position: 20, color: '#fd5d93' }, + ]); + + // SVG Hill curve path + const hillPath = 'M 0,100 Q 125,0 250,100'; + + return ( +
+

Hill Chart

+

+ Left side = figuring things out • Right side = getting it done +

+ +
+ + {/* Hill curve */} + + + {/* Midpoint line */} + + + {/* Labels */} + Figuring it out + Making it happen + + {/* Items on the hill */} + {items.map(item => { + const x = (item.position / 100) * 500; + const y = item.position <= 50 + ? 100 - Math.sin((item.position / 50) * Math.PI / 2) * 100 + : 100 - Math.cos(((item.position - 50) / 50) * Math.PI / 2) * 100; + + return ( + + + + ); + })} + +
+ +
+ {items.map(item => ( +
+
+ {item.name} + {item.position}% +
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/MessageBoard.tsx b/servers/basecamp/src/ui/react-app/apps/MessageBoard.tsx new file mode 100644 index 0000000..066b64e --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/MessageBoard.tsx @@ -0,0 +1,41 @@ +/** + * Message Board App - List of all messages in a project + */ + +import React, { useState, useEffect } from 'react'; + +export function MessageBoard({ projectId }: { projectId?: number }) { + const [messages, setMessages] = useState([]); + + useEffect(() => { + setMessages([ + { id: 1, subject: 'Project Kickoff', content: 'Welcome to the Website Redesign project!', creator: 'Alice', created_at: '2024-01-10', comments_count: 5 }, + { id: 2, subject: 'Design Direction', content: 'Let\'s discuss the overall design direction...', creator: 'Bob', created_at: '2024-01-12', comments_count: 12 }, + { id: 3, subject: 'Timeline Update', content: 'Updated timeline for Q1 deliverables', creator: 'Charlie', created_at: '2024-01-15', comments_count: 3 }, + ]); + }, [projectId]); + + return ( +
+
+

Message Board

+ +
+ +
+ {messages.map((message) => ( +
+

{message.subject}

+

{message.content}

+
+ {message.creator} • {message.created_at} + {message.comments_count} comments +
+
+ ))} +
+
+ ); +} diff --git a/servers/basecamp/src/ui/react-app/apps/MessageDetail.tsx b/servers/basecamp/src/ui/react-app/apps/MessageDetail.tsx new file mode 100644 index 0000000..4cc13b1 --- /dev/null +++ b/servers/basecamp/src/ui/react-app/apps/MessageDetail.tsx @@ -0,0 +1,60 @@ +/** + * Message Detail App - Detailed view of a single message + */ + +import React from 'react'; + +export function MessageDetail({ messageId }: { messageId?: number }) { + const message = { + id: messageId || 1, + subject: 'Project Kickoff', + content: 'Welcome everyone to the Website Redesign project! We\'re excited to have such a talented team on board. This project will run through Q1 2024 with the goal of completely revamping our online presence.', + creator: { name: 'Alice Johnson', avatar_url: '' }, + created_at: '2024-01-10T10:00:00Z', + comments: [ + { id: 1, author: 'Bob Smith', content: 'Excited to get started!', created_at: '2024-01-10T11:30:00Z' }, + { id: 2, author: 'Charlie Davis', content: 'Looking forward to working with you all.', created_at: '2024-01-10T14:00:00Z' }, + ], + }; + + return ( +
+

{message.subject}

+ +
+ {message.creator.name} + posted on {new Date(message.created_at).toLocaleDateString()} +
+ +
+ {message.content} +
+ +
+ +

Comments ({message.comments.length})

+ +
+ {message.comments.map(comment => ( +
+
+ {comment.author} + {new Date(comment.created_at).toLocaleDateString()} +
+
{comment.content}
+
+ ))} +
+ +
+