Holaspirit MCP Server

#!/usr/bin/env node import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { zodToJsonSchema } from 'zod-to-json-schema'; import * as schemas from './schemas.js'; import { createHolaspiritClient } from 'holaspirit-client-typescript-fetch'; const holaClient = createHolaspiritClient('https://app.holaspirit.com', { headers: { Authorization: `Bearer ${process.env.HOLASPIRIT_API_TOKEN}`, }, }); const server = new Server( { name: 'holaspirit-mcp-server', version: '0.0.1', }, { capabilities: { tools: {}, }, } ); const apiToken = process.env.HOLASPIRIT_API_TOKEN; if (!apiToken) { throw new Error('HOLASPIRIT_API_TOKEN environment variable is required'); } server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'holaspirit_list_tasks', description: 'List all tasks in the organization', inputSchema: zodToJsonSchema(schemas.ListTasksRequestSchema), }, { name: 'holaspirit_list_metrics', description: 'List all metrics in the organization', inputSchema: zodToJsonSchema(schemas.ListMetricsRequestSchema), }, { name: 'holaspirit_list_circles', description: 'List all circles in the organization', inputSchema: zodToJsonSchema(schemas.ListCirclesRequestSchema), }, { name: 'holaspirit_get_circle', description: 'Get details of a specific circle', inputSchema: zodToJsonSchema(schemas.GetCircleRequestSchema), }, { name: 'holaspirit_list_roles', description: 'List all roles in the organization', inputSchema: zodToJsonSchema(schemas.ListRolesRequestSchema), }, { name: 'holaspirit_get_role', description: 'Get details of a specific role', inputSchema: zodToJsonSchema(schemas.GetRoleRequestSchema), }, { name: 'holaspirit_list_domains', description: 'List all domains in the organization', inputSchema: zodToJsonSchema(schemas.ListDomainsRequestSchema), }, { name: 'holaspirit_list_policies', description: 'List all policies in the organization', inputSchema: zodToJsonSchema(schemas.ListPoliciesRequestSchema), }, { name: 'holaspirit_list_meetings', description: 'List all meetings in the organization', inputSchema: zodToJsonSchema(schemas.ListMeetingsRequestSchema), }, { name: 'holaspirit_get_meeting', description: 'Get details of a specific meeting', inputSchema: zodToJsonSchema(schemas.GetMeetingRequestSchema), }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (!request.params) { throw new Error('Params are required'); } switch (request.params.name) { case 'holaspirit_list_tasks': { const args = schemas.ListTasksRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/tasks', { params: { path: { organization_id: args.organizationId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Tasks not found or invalid response format'); } const parsed = schemas.ListTasksResponseSchema.parse(apiResponse.data); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_list_metrics': { const args = schemas.ListMetricsRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/metrics', { params: { path: { organization_id: args.organizationId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Metrics not found or invalid response format'); } const parsed = schemas.ListMetricsResponseSchema.parse( apiResponse.data ); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_list_circles': { const args = schemas.ListCirclesRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/circles', { params: { path: { organization_id: args.organizationId, }, query: { member: args.member, circle: args.circle, page: args.page, count: args.count, }, }, } ); if (apiResponse?.data == null) { throw new Error('Circle not found'); } const parsed = schemas.ListCirclesResponseSchema.parse( apiResponse.data ); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_get_circle': { const args = schemas.GetCircleRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/circles/{circle_id}', { params: { path: { organization_id: args.organizationId, circle_id: args.circleId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Circle not found'); } const parsed = schemas.GetCircleResponseSchema.parse({ ...apiResponse.data, linked: { roles: apiResponse?.linked?.roles, }, }); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_list_roles': { const args = schemas.ListRolesRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/roles', { params: { path: { organization_id: args.organizationId, }, query: { member: args.member, circle: args.circle, page: args.page, count: args.count, }, }, } ); if (apiResponse?.data == null) { throw new Error('Roles not found or invalid response format'); } const parsed = schemas.ListRolesResponseSchema.parse(apiResponse.data); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_get_role': { const args = schemas.GetRoleRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/roles/{role_id}', { params: { path: { organization_id: args.organizationId, role_id: args.roleId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Role not found or invalid response format'); } const parsed = schemas.GetRoleResponseSchema.parse(apiResponse.data); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_list_domains': { const args = schemas.ListDomainsRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/domains', { params: { path: { organization_id: args.organizationId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Domains not found or invalid response format'); } const parsed = schemas.ListDomainsResponseSchema.parse( apiResponse.data ); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_list_policies': { const args = schemas.ListPoliciesRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/policies', { params: { path: { organization_id: args.organizationId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Policies not found or invalid response format'); } const parsed = schemas.ListPoliciesResponseSchema.parse( apiResponse.data ); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_list_meetings': { const args = schemas.ListMeetingsRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/meetings', { params: { path: { organization_id: args.organizationId, }, query: { circle: args.circle, member: args.member, page: args.page, count: args.count, }, }, } ); if (apiResponse?.data == null) { throw new Error('Meetings not found or invalid response format'); } const parsed = schemas.ListMeetingsResponseSchema.parse( apiResponse.data ); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } case 'holaspirit_get_meeting': { const args = schemas.GetMeetingRequestSchema.parse( request.params.arguments ); const { data: apiResponse } = await holaClient.GET( '/api/organizations/{organization_id}/meetings/{meeting_id}', { params: { path: { organization_id: args.organizationId, meeting_id: args.meetingId, }, }, } ); if (apiResponse?.data == null) { throw new Error('Meeting not found or invalid response format'); } const parsed = schemas.GetMeetingResponseSchema.parse({ ...apiResponse.data, linked: { tensions: apiResponse?.linked?.tensions, }, }); return { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }], }; } default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { console.error('Error handling request:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; throw new Error(errorMessage); } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Holaspirit MCP Server running on stdio'); } runServer().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); });