UNPKG

breathe-api

Version:

Model Context Protocol server for Breathe HR APIs with Swagger/OpenAPI support - also works with custom APIs

636 lines (577 loc) 22.3 kB
import { parseSwagger } from './swagger-parser.js'; export async function explainApiFeature(swaggerUrl, headers, feature, platform) { const parsed = await parseSwagger({ url: swaggerUrl, headers, generateTypes: false }); const keywords = extractKeywords(feature); const relatedEndpoints = findRelatedEndpoints(parsed, keywords); const workflow = analyzeWorkflow(feature, relatedEndpoints, parsed.spec); const dataModels = extractDataModels(relatedEndpoints, parsed.spec); const codeExamples = generateCodeExamples(feature, relatedEndpoints, platform, parsed); const considerations = generateConsiderations(feature, relatedEndpoints); return { feature, description: generateFeatureDescription(feature, relatedEndpoints), relatedEndpoints: relatedEndpoints.map(ep => ({ method: ep.method, path: ep.path, description: ep.operation.summary || ep.operation.description || 'No description', authentication: extractAuth(ep.operation), parameters: extractParameters(ep.operation), requestBody: ep.operation.requestBody, responses: ep.operation.responses })), workflow, codeExamples, dataModels, considerations }; } function extractKeywords(feature) { const normalized = feature.toLowerCase(); const keywords = []; const featureMappings = { 'leave request': ['leave', 'absence', 'holiday', 'vacation', 'time off', 'pto'], 'shift scheduling': ['shift', 'roster', 'schedule', 'rota'], 'employee onboarding': ['employee', 'onboard', 'create', 'new hire'], 'time tracking': ['time', 'clock', 'punch', 'attendance', 'hours'], 'expense': ['expense', 'claim', 'reimbursement', 'mileage'], 'payroll': ['payroll', 'salary', 'wage', 'payment'], 'performance': ['performance', 'review', 'appraisal', 'feedback'], 'documents': ['document', 'file', 'attachment', 'upload'] }; for (const [key, values] of Object.entries(featureMappings)) { if (normalized.includes(key) || values.some(v => normalized.includes(v))) { keywords.push(...values); } } keywords.push(...normalized.split(' ').filter(word => word.length > 2)); return [...new Set(keywords)]; } function findRelatedEndpoints(parsed, keywords) { const related = []; const spec = parsed.spec; if (!spec.paths) return []; for (const [path, pathItem] of Object.entries(spec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (!['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase())) { continue; } let score = 0; const searchText = `${path} ${operation.summary || ''} ${operation.description || ''} ${operation.operationId || ''}`.toLowerCase(); for (const keyword of keywords) { if (searchText.includes(keyword)) { score += searchText.split(keyword).length - 1; } } if (score > 0) { related.push({ method: method.toUpperCase(), path, operation, score }); } } } return related .sort((a, b) => b.score - a.score) .slice(0, 10) .map(({ method, path, operation }) => ({ method, path, operation })); } function analyzeWorkflow(feature, endpoints, _spec) { const workflow = []; if (feature.toLowerCase().includes('leave') || feature.toLowerCase().includes('absence')) { workflow.push('1. Check available leave balance (GET endpoints)'); workflow.push('2. View leave policies and types'); workflow.push('3. Submit leave request (POST endpoint)'); workflow.push('4. Track request status'); workflow.push('5. Handle approval/rejection notifications'); workflow.push('6. Update leave balance after approval'); } else if (feature.toLowerCase().includes('shift') || feature.toLowerCase().includes('schedule')) { workflow.push('1. View current schedule/roster'); workflow.push('2. Check shift availability'); workflow.push('3. Create or request shift changes'); workflow.push('4. Handle shift swaps between employees'); workflow.push('5. Track attendance against scheduled shifts'); } else { const hasGet = endpoints.some(ep => ep.method === 'GET'); const hasPost = endpoints.some(ep => ep.method === 'POST'); const hasPut = endpoints.some(ep => ep.method === 'PUT' || ep.method === 'PATCH'); const hasDelete = endpoints.some(ep => ep.method === 'DELETE'); if (hasGet) workflow.push('1. Retrieve and display current data'); if (hasPost) workflow.push('2. Create new records'); if (hasPut) workflow.push('3. Update existing records'); if (hasDelete) workflow.push('4. Delete records when needed'); } return workflow; } function extractDataModels(endpoints, _spec) { const models = new Set(); for (const endpoint of endpoints) { if (endpoint.operation.requestBody?.content) { const content = endpoint.operation.requestBody.content; const firstContent = Object.values(content)[0]; const schema = firstContent?.schema; if (schema?.$ref) { const modelName = schema.$ref.split('/').pop(); models.add(modelName); } } if (endpoint.operation.responses) { for (const response of Object.values(endpoint.operation.responses)) { if (response.content) { const content = response.content; const firstContent = Object.values(content)[0]; const schema = firstContent?.schema; if (schema?.$ref) { const modelName = schema.$ref.split('/').pop(); models.add(modelName); } } } } } return Array.from(models); } function generateCodeExamples(feature, endpoints, platform, parsed) { const examples = {}; const getEndpoint = endpoints.find(ep => ep.method === 'GET'); const postEndpoint = endpoints.find(ep => ep.method === 'POST'); if (platform === 'react-native' || platform === 'both') { examples.reactNative = generateReactNativeExample(feature, getEndpoint, postEndpoint, parsed); } if (platform === 'nextjs' || platform === 'both') { examples.nextjs = generateNextJsExample(feature, getEndpoint, postEndpoint, parsed); } if (platform === 'ruby' || platform === 'both') { examples.ruby = generateRubyExample(feature, getEndpoint, postEndpoint, parsed); } return examples; } function generateReactNativeExample(feature, _getEndpoint, _postEndpoint, _parsed) { const isLeaveRequest = feature.toLowerCase().includes('leave'); const isShift = feature.toLowerCase().includes('shift'); let example = `import { useState } from 'react'; import { View, Text, Button, FlatList } from 'react-native'; import { useApi, apiClient } from './generated/api'; `; if (isLeaveRequest) { example += `function LeaveRequestFeature() { // Fetch current leave balance and requests const { data: leaveData, loading, refetch } = useApi('getLeaveRequests'); const [submitting, setSubmitting] = useState(false); const submitLeaveRequest = async (leaveDetails) => { setSubmitting(true); try { // Submit the leave request const response = await apiClient.createLeaveRequest({ body: { startDate: leaveDetails.startDate, endDate: leaveDetails.endDate, leaveType: leaveDetails.type, reason: leaveDetails.reason, } }); // Refresh the list await refetch(); // Show success message Alert.alert('Success', 'Leave request submitted successfully'); } catch (error) { Alert.alert('Error', 'Failed to submit leave request'); } finally { setSubmitting(false); } }; return ( <View> <LeaveBalance balance={leaveData?.balance} /> <LeaveRequestForm onSubmit={submitLeaveRequest} loading={submitting} /> <FlatList data={leaveData?.requests} renderItem={({ item }) => <LeaveRequestCard request={item} />} /> </View> ); }`; } else if (isShift) { example += `function ShiftSchedulingFeature() { const [selectedDate, setSelectedDate] = useState(new Date()); // Fetch shifts for the selected period const { data: shifts, loading, refetch } = useApi('getRosteredShifts', { startDate: selectedDate.toISOString(), endDate: new Date(selectedDate.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString() }); const createShift = async (shiftData) => { try { await apiClient.createRosteredShift({ body: { employeeId: shiftData.employeeId, startTime: shiftData.startTime, endTime: shiftData.endTime, location: shiftData.location, } }); await refetch(); Alert.alert('Success', 'Shift created successfully'); } catch (error) { Alert.alert('Error', 'Failed to create shift'); } }; return ( <View> <WeekCalendar selectedDate={selectedDate} onDateChange={setSelectedDate} /> <ShiftGrid shifts={shifts} onAddShift={createShift} /> </View> ); }`; } else { example += `function ${feature.replace(/\s+/g, '')}Feature() { const { data, loading, error, refetch } = useApi('get${feature.replace(/\s+/g, '')}'); if (loading) return <ActivityIndicator />; if (error) return <Text>Error: {error.message}</Text>; return ( <View> <FlatList data={data} renderItem={({ item }) => <ItemCard item={item} />} refreshing={loading} onRefresh={refetch} /> </View> ); }`; } return example; } function generateNextJsExample(feature, _getEndpoint, _postEndpoint, _parsed) { const isLeaveRequest = feature.toLowerCase().includes('leave'); let example = `// Server Component (app/${feature.toLowerCase().replace(/\s+/g, '-')}/page.tsx) import { apiClient } from '@/lib/api/client'; export default async function ${feature.replace(/\s+/g, '')}Page() { const data = await apiClient.get${feature.replace(/\s+/g, '')}(); return ( <div> <h1>${feature}</h1> <${feature.replace(/\s+/g, '')}List initialData={data} /> </div> ); } // Client Component with Server Action 'use client'; import { useTransition } from 'react'; import { useRouter } from 'next/navigation'; import { create${feature.replace(/\s+/g, '')}Action } from './actions'; `; if (isLeaveRequest) { example += `export function LeaveRequestForm() { const [isPending, startTransition] = useTransition(); const router = useRouter(); async function handleSubmit(formData: FormData) { startTransition(async () => { const result = await createLeaveRequestAction(formData); if (result.success) { router.push('/leave-requests'); router.refresh(); } else { // Handle error console.error(result.error); } }); } return ( <form action={handleSubmit}> <input type="date" name="startDate" required /> <input type="date" name="endDate" required /> <select name="leaveType" required> <option value="annual">Annual Leave</option> <option value="sick">Sick Leave</option> <option value="personal">Personal Leave</option> </select> <textarea name="reason" placeholder="Reason for leave" /> <button type="submit" disabled={isPending}> {isPending ? 'Submitting...' : 'Submit Request'} </button> </form> ); } // Server Action (app/${feature.toLowerCase().replace(/\s+/g, '-')}/actions.ts) 'use server'; import { apiClient } from '@/lib/api/client'; import { revalidatePath } from 'next/cache'; export async function createLeaveRequestAction(formData: FormData) { try { const result = await apiClient.createLeaveRequest({ body: { startDate: formData.get('startDate') as string, endDate: formData.get('endDate') as string, leaveType: formData.get('leaveType') as string, reason: formData.get('reason') as string, } }); revalidatePath('/leave-requests'); return { success: true, data: result }; } catch (error) { return { success: false, error: 'Failed to create leave request' }; } }`; } else { example += `export function ${feature.replace(/\s+/g, '')}Form() { const [isPending, startTransition] = useTransition(); return ( <form action={handleSubmit}> {/* Add form fields based on your API */} <button type="submit" disabled={isPending}> Submit </button> </form> ); }`; } return example; } function extractAuth(operation) { if (operation.security && operation.security.length > 0) { return Object.keys(operation.security[0])[0] || 'Required'; } return 'None'; } function extractParameters(operation) { const params = []; if (operation.parameters) { for (const param of operation.parameters) { params.push(`${param.name} (${param.in}${param.required ? ', required' : ', optional'})`); } } return params; } function generateFeatureDescription(feature, endpoints) { const hasGet = endpoints.some(ep => ep.method === 'GET'); const hasPost = endpoints.some(ep => ep.method === 'POST'); const hasPut = endpoints.some(ep => ep.method === 'PUT' || ep.method === 'PATCH'); const hasDelete = endpoints.some(ep => ep.method === 'DELETE'); let description = `The ${feature} feature provides functionality to `; const capabilities = []; if (hasGet) capabilities.push('view and retrieve data'); if (hasPost) capabilities.push('create new records'); if (hasPut) capabilities.push('update existing records'); if (hasDelete) capabilities.push('delete records'); description += capabilities.join(', '); description += `. This feature involves ${endpoints.length} API endpoints that work together to provide a complete workflow.`; return description; } function generateConsiderations(feature, endpoints) { const considerations = []; const requiresAuth = endpoints.some(ep => ep.operation.security && ep.operation.security.length > 0); if (requiresAuth) { considerations.push('Ensure proper authentication tokens are included in requests'); } const hasListEndpoints = endpoints.some(ep => ep.method === 'GET' && ep.path.endsWith('s')); if (hasListEndpoints) { considerations.push('Implement pagination for list endpoints to handle large datasets'); } const hasFileUpload = endpoints.some(ep => ep.operation.requestBody?.content?.['multipart/form-data']); if (hasFileUpload) { considerations.push('Handle file uploads with proper validation and size limits'); } if (feature.toLowerCase().includes('leave')) { considerations.push('Validate leave dates against company calendar and existing requests'); considerations.push('Check leave balance before allowing new requests'); considerations.push('Implement approval workflow notifications'); } else if (feature.toLowerCase().includes('shift')) { considerations.push('Prevent scheduling conflicts and overlapping shifts'); considerations.push('Validate shift times against business hours'); considerations.push('Consider timezone handling for multi-location businesses'); } considerations.push('Implement proper error handling and user feedback'); considerations.push('Add loading states for better user experience'); considerations.push('Cache frequently accessed data to reduce API calls'); return considerations; } function generateRubyExample(feature, _getEndpoint, _postEndpoint, parsed) { const isLeaveRequest = feature.toLowerCase().includes('leave'); const isShift = feature.toLowerCase().includes('shift') || feature.toLowerCase().includes('roster'); const isTimesheet = feature.toLowerCase().includes('timesheet'); const isKudos = feature.toLowerCase().includes('kudos'); const baseUrl = parsed.spec.servers?.[0]?.url || 'https://api.example.com'; let example = `require 'net/http' require 'json' require 'uri' class ${feature.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}Service def initialize(api_token) @api_token = api_token @base_url = "${baseUrl}" end `; if (isLeaveRequest) { example += ` # Fetch all leave requests def get_leave_requests(employee_id = nil) uri = URI("#{@base_url}/leaves") uri.query = URI.encode_www_form(employee_id: employee_id) if employee_id request = Net::HTTP::Get.new(uri) request['Authorization'] = "Bearer #{@api_token}" request['Content-Type'] = 'application/json' response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end # Submit a new leave request def create_leave_request(start_date:, end_date:, leave_type:, reason:) uri = URI("#{@base_url}/leaves") request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{@api_token}" request['Content-Type'] = 'application/json' request.body = { start_date: start_date, end_date: end_date, leave_type: leave_type, reason: reason }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end end # Usage example service = LeaveRequestService.new(ENV['API_TOKEN']) leaves = service.get_leave_requests`; } else if (isShift || isTimesheet) { example += ` # Fetch roster/timesheet data def get_roster(start_date:, end_date:, employee_id: nil) uri = URI("#{@base_url}/rosters") params = { start_date: start_date, end_date: end_date } params[:employee_id] = employee_id if employee_id uri.query = URI.encode_www_form(params) request = Net::HTTP::Get.new(uri) request['Authorization'] = "Bearer #{@api_token}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end # Clock in/out def clock_action(action:, timestamp: Time.now.iso8601, location: nil) uri = URI("#{@base_url}/clock") request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{@api_token}" request['Content-Type'] = 'application/json' body = { action: action, timestamp: timestamp } body[:location] = location if location request.body = body.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end end # Usage example service = RosterService.new(ENV['API_TOKEN']) roster = service.get_roster( start_date: Date.today.beginning_of_week.iso8601, end_date: Date.today.end_of_week.iso8601 )`; } else if (isKudos) { example += ` # Get kudos for an employee or team def get_kudos(employee_id: nil, team_id: nil, limit: 20) uri = URI("#{@base_url}/kudos") params = { limit: limit } params[:employee_id] = employee_id if employee_id params[:team_id] = team_id if team_id uri.query = URI.encode_www_form(params) request = Net::HTTP::Get.new(uri) request['Authorization'] = "Bearer #{@api_token}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end # Send kudos to a colleague def send_kudos(recipient_id:, message:, category: nil) uri = URI("#{@base_url}/kudos") request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{@api_token}" request['Content-Type'] = 'application/json' body = { recipient_id: recipient_id, message: message } body[:category] = category if category request.body = body.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end end # Rails controller example class KudosController < ApplicationController def create service = KudosService.new(current_user.api_token) result = service.send_kudos( recipient_id: params[:recipient_id], message: params[:message] ) redirect_to kudos_path end end`; } else { example += ` # Generic API method def api_request(method:, path:, params: nil, body: nil) uri = URI("#{@base_url}#{path}") uri.query = URI.encode_www_form(params) if params && method == 'GET' request = case method.upcase when 'GET' then Net::HTTP::Get.new(uri) when 'POST' then Net::HTTP::Post.new(uri) when 'PUT' then Net::HTTP::Put.new(uri) when 'DELETE' then Net::HTTP::Delete.new(uri) end request['Authorization'] = "Bearer #{@api_token}" request['Content-Type'] = 'application/json' if body request.body = body.to_json if body response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(request) end JSON.parse(response.body) end end # Example usage with HTTParty gem (more convenient) require 'httparty' class ApiClient include HTTParty base_uri '${baseUrl}' def initialize(token) @options = { headers: { 'Authorization' => "Bearer #{token}" } } end def get_resource(id) self.class.get("/resource/#{id}", @options) end end`; } return example; } //# sourceMappingURL=api-explainer.js.map