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
JavaScript
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