UNPKG

gologin-mcp

Version:

MCP server that connects to the GoLogin API

411 lines (410 loc) 15.6 kB
#!/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'; import yaml from 'js-yaml'; class GologinMcpServer { constructor(token) { this.apiSpec = null; this.baseUrl = ''; this.server = new Server({ name: 'gologin-mcp', version: '0.0.1', }, { capabilities: { tools: {}, }, }); this.token = token; this.setupHandlers(); } setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = []; if (this.apiSpec && this.apiSpec.paths) { for (const [path, pathItem] of Object.entries(this.apiSpec.paths)) { if (!pathItem) continue; for (const [method, operation] of Object.entries(pathItem)) { if (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'].includes(method) && operation) { const op = operation; const toolName = op.operationId || `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`; const inputSchema = this.buildInputSchema(op, path); tools.push({ name: toolName, description: op.summary || op.description || `${method.toUpperCase()} ${path}`, inputSchema, }); } } } } return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { throw new Error('No arguments provided'); } try { const parameters = { path: args.path, query: args.query, body: args.body || (args.parameters ? args.parameters : undefined), }; return await this.callDynamicTool(name, parameters, args.headers); } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); } async loadApiSpec() { const url = 'https://docs-download.gologin.com/openapi.json'; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentType = response.headers.get('content-type') || ''; let spec; if (contentType.includes('application/json')) { spec = await response.json(); } else { const text = await response.text(); try { spec = JSON.parse(text); } catch { spec = yaml.load(text); } } this.apiSpec = spec; this.baseUrl = this.getBaseUrl(spec); } buildInputSchema(operation, path) { const schema = { type: 'object', properties: {}, required: [], }; const pathParams = this.extractPathParameters(operation, path); const queryParams = this.extractQueryParameters(operation); const bodySchema = this.extractRequestBodySchema(operation); const requiredHeaders = this.extractRequiredHeaders(operation); if (pathParams.properties && Object.keys(pathParams.properties).length > 0) { schema.properties.path = { type: 'object', properties: pathParams.properties, description: 'Path parameters for URL substitution', }; if (pathParams.required.length > 0) { schema.properties.path.required = pathParams.required; schema.required.push('path'); } } if (queryParams.properties && Object.keys(queryParams.properties).length > 0) { schema.properties.query = { type: 'object', properties: queryParams.properties, description: 'Query parameters', }; if (queryParams.required.length > 0) { schema.properties.query.required = queryParams.required; if (queryParams.required.length === Object.keys(queryParams.properties).length) { schema.required.push('query'); } } } if (bodySchema) { schema.properties.body = { ...bodySchema, description: 'Request body parameters', }; schema.required.push('body'); } if (requiredHeaders.length > 0) { schema.properties.headers = { type: 'object', properties: {}, required: requiredHeaders, description: 'Additional headers for the request', }; schema.required.push('headers'); } else { schema.properties.headers = { type: 'object', description: 'Additional headers for the request', }; } return schema; } extractPathParameters(operation, path) { const properties = {}; const required = []; const pathParamNames = path.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; if (operation.parameters) { operation.parameters.forEach(param => { if ('$ref' in param) return; const parameter = param; if (parameter.in === 'path') { properties[parameter.name] = { type: parameter.schema ? this.getSchemaType(parameter.schema) : 'string', description: parameter.description || '', }; if (parameter.required) { required.push(parameter.name); } } }); } pathParamNames.forEach(paramName => { if (!properties[paramName]) { properties[paramName] = { type: 'string', description: `Path parameter: ${paramName}`, }; required.push(paramName); } }); return { properties, required }; } extractQueryParameters(operation) { const properties = {}; const required = []; if (operation.parameters) { operation.parameters.forEach(param => { if ('$ref' in param) return; const parameter = param; if (parameter.in === 'query') { properties[parameter.name] = { type: parameter.schema ? this.getSchemaType(parameter.schema) : 'string', description: parameter.description || '', }; if (parameter.required) { required.push(parameter.name); } } }); } return { properties, required }; } extractRequestBodySchema(operation) { if (!operation.requestBody || '$ref' in operation.requestBody) { return null; } const requestBody = operation.requestBody; if (!requestBody.content) { return null; } const jsonContent = requestBody.content['application/json']; if (!jsonContent || !jsonContent.schema) { return null; } return this.convertOpenAPISchemaToJsonSchema(jsonContent.schema); } extractRequiredHeaders(operation) { const required = []; if (operation.parameters) { operation.parameters.forEach(param => { if ('$ref' in param) return; const parameter = param; if (parameter.in === 'header' && parameter.required) { required.push(parameter.name); } }); } return required; } getSchemaType(schema) { if ('$ref' in schema) { const resolved = this.resolveReference(schema.$ref); return resolved.type || 'object'; } const schemaObj = schema; return schemaObj.type || 'string'; } convertOpenAPISchemaToJsonSchema(schema) { if ('$ref' in schema) { return this.resolveReference(schema.$ref); } const schemaObj = schema; const jsonSchema = { type: schemaObj.type || 'object', }; if (schemaObj.properties) { jsonSchema.properties = {}; Object.entries(schemaObj.properties).forEach(([key, prop]) => { jsonSchema.properties[key] = this.convertOpenAPISchemaToJsonSchema(prop); }); } if (schemaObj.required) { jsonSchema.required = schemaObj.required; } if (schemaObj.description) { jsonSchema.description = schemaObj.description; } if (schemaObj.type === 'array' && 'items' in schemaObj && schemaObj.items) { jsonSchema.items = this.convertOpenAPISchemaToJsonSchema(schemaObj.items); } if (schemaObj.enum) { jsonSchema.enum = schemaObj.enum; } if (schemaObj.format) { jsonSchema.format = schemaObj.format; } if (schemaObj.minimum !== undefined) { jsonSchema.minimum = schemaObj.minimum; } if (schemaObj.maximum !== undefined) { jsonSchema.maximum = schemaObj.maximum; } if (schemaObj.pattern) { jsonSchema.pattern = schemaObj.pattern; } return jsonSchema; } resolveReference(ref) { if (!this.apiSpec) { return { type: 'object' }; } const parts = ref.split('/'); if (parts[0] !== '#') { return { type: 'object' }; } let current = this.apiSpec; for (let i = 1; i < parts.length; i++) { if (!current || typeof current !== 'object') { return { type: 'object' }; } current = current[parts[i]]; } if (!current) { return { type: 'object' }; } return this.convertOpenAPISchemaToJsonSchema(current); } async callDynamicTool(toolName, parameters = {}, headers = {}) { if (!this.apiSpec || !this.apiSpec.paths) { throw new Error('API specification not loaded'); } let targetPath = ''; let targetMethod = ''; let operation; for (const [path, pathItem] of Object.entries(this.apiSpec.paths)) { if (!pathItem) continue; for (const [method, op] of Object.entries(pathItem)) { if (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'].includes(method) && op) { const opObj = op; const generatedToolName = opObj.operationId || `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`; if (generatedToolName === toolName) { targetPath = path; targetMethod = method.toUpperCase(); operation = opObj; break; } } } if (operation) break; } if (!operation) { throw new Error(`Tool "${toolName}" not found`); } let url = `${this.baseUrl}${targetPath}`; const requestHeaders = { ...headers }; let requestBody; requestHeaders['User-Agent'] = 'gologin-mcp'; if (this.token) { requestHeaders['Authorization'] = `Bearer ${this.token}`; } if (parameters.path) { for (const [key, value] of Object.entries(parameters.path)) { url = url.replace(`{${key}}`, encodeURIComponent(value)); } } const queryParams = new URLSearchParams(); if (parameters.query) { for (const [key, value] of Object.entries(parameters.query)) { queryParams.append(key, value); } } if (queryParams.toString()) { url += `?${queryParams.toString()}`; } if (parameters.body && ['POST', 'PUT', 'PATCH'].includes(targetMethod)) { requestHeaders['Content-Type'] = 'application/json'; requestBody = JSON.stringify(parameters.body); } try { const fetchOptions = { method: targetMethod, headers: requestHeaders, }; if (requestBody) { fetchOptions.body = requestBody; } const response = await fetch(url, fetchOptions); const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); let responseBody; const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { try { responseBody = await response.json(); } catch { responseBody = await response.text(); } } else { responseBody = await response.text(); } return { content: [ { type: 'text', text: `API Call Result:\n` + `URL: ${url}\n` + `Method: ${targetMethod}\n` + `Status: ${response.status} ${response.statusText}\n\n` + `Response Headers:\n${JSON.stringify(responseHeaders, null, 2)}\n\n` + `Response Body:\n${typeof responseBody === 'object' ? JSON.stringify(responseBody, null, 2) : responseBody}`, }, ], }; } catch (error) { throw new Error(`API call failed: ${error instanceof Error ? error.message : String(error)}`); } } getBaseUrl(spec) { if (spec.servers && spec.servers.length > 0) { return spec.servers[0].url; } throw new Error('No servers found in API spec'); } async run() { await this.loadApiSpec(); const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('GoLogin MCP server running on stdio'); } } const token = process.env.API_TOKEN || ''; const server = new GologinMcpServer(token); server.run().catch(console.error);