@folderr/folderr-mcp-server
Version:
A Model Context Protocol server
420 lines (419 loc) • 14.9 kB
JavaScript
/**
* Folderr MCP Server
*
* This server provides tools to interact with Folderr's API, specifically for managing
* and communicating with Folderr Assistants. It supports both token-based authentication
* and direct API key usage.
*
* @module folderr-server
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_FILE = path.join(__dirname, '../config.json');
class FolderrServer {
server;
config;
axiosInstance;
constructor() {
this.server = new Server({
name: 'folderr-server',
version: '0.1.0',
}, {
capabilities: {
tools: {},
},
});
// Load or initialize config
this.config = this.loadConfig();
this.axiosInstance = axios.create({
baseURL: this.config.baseUrl || 'https://api-staging.folderr.com',
headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {},
});
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
loadConfig() {
try {
if (fs.existsSync(CONFIG_FILE)) {
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
}
}
catch (error) {
console.error('Error loading config:', error);
}
return { baseUrl: 'https://api-staging.folderr.com' };
}
saveConfig() {
try {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2));
}
catch (error) {
console.error('Error saving config:', error);
}
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'set_api_token',
description: 'Set an API token for authentication (alternative to login)',
inputSchema: {
type: 'object',
properties: {
token: {
type: 'string',
description: 'API token generated from Folderr developers section',
},
},
required: ['token'],
},
},
{
name: 'login',
description: 'Login to Folderr with email and password',
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'User email',
},
password: {
type: 'string',
description: 'User password',
},
},
required: ['email', 'password'],
},
},
{
name: 'list_assistants',
description: 'List all available assistants',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'ask_assistant',
description: 'Ask a question to a specific assistant',
inputSchema: {
type: 'object',
properties: {
assistant_id: {
type: 'string',
description: 'ID of the assistant to ask',
},
question: {
type: 'string',
description: 'Question to ask the assistant',
},
},
required: ['assistant_id', 'question'],
},
},
{
name: 'list_workflows',
description: 'List all available workflows',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_workflow_inputs',
description: 'Get the required inputs for a workflow',
inputSchema: {
type: 'object',
properties: {
workflow_id: {
type: 'string',
description: 'ID of the workflow',
},
},
required: ['workflow_id'],
},
},
{
name: 'execute_workflow',
description: 'Execute a workflow with the required inputs',
inputSchema: {
type: 'object',
properties: {
workflow_id: {
type: 'string',
description: 'ID of the workflow',
},
inputs: {
type: 'object',
description: 'Input values required by the workflow',
additionalProperties: true,
},
},
required: ['workflow_id', 'inputs'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'login':
return await this.handleLogin(request.params.arguments);
case 'set_api_token':
return await this.handleSetApiToken(request.params.arguments);
case 'list_assistants':
return await this.handleListAssistants();
case 'ask_assistant':
return await this.handleAskAssistant(request.params.arguments);
case 'list_workflows':
return await this.handleListWorkflows();
case 'get_workflow_inputs':
return await this.handleGetWorkflowInputs(request.params.arguments);
case 'execute_workflow':
return await this.handleExecuteWorkflow(request.params.arguments);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
});
}
/**
* Handle login requests using email and password
* @param args Login arguments containing email and password
*/
async handleLogin(args) {
try {
const response = await this.axiosInstance.post('/api/auth/sign-in', {
email: args.email,
password: args.password,
});
// Update config and axios instance with new token
this.config.token = response.data.token;
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${this.config.token}`;
this.saveConfig();
return {
content: [
{
type: 'text',
text: 'Successfully logged in',
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Login failed: ${error.response?.data?.message || error.message}`,
},
],
isError: true,
};
}
}
/**
* Handle setting an API token for authentication
* @param args Arguments containing the API token
*/
async handleSetApiToken(args) {
try {
// Update config and axios instance with new token
this.config.token = args.token;
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${this.config.token}`;
this.saveConfig();
return {
content: [
{
type: 'text',
text: 'Successfully set API token',
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to set API token: ${error.message}`,
},
],
isError: true,
};
}
}
/**
* Handle requests to list all available assistants
*/
async handleListAssistants() {
if (!this.config.token) {
throw new McpError(ErrorCode.InvalidRequest, 'Not logged in');
}
try {
const response = await this.axiosInstance.get('/api/agent');
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to list assistants: ${error.response?.data?.message || error.message}`,
},
],
isError: true,
};
}
}
/**
* Handle requests to ask questions to specific assistants
* @param args Arguments containing assistant_id and question
*/
async handleAskAssistant(args) {
if (!this.config.token) {
throw new McpError(ErrorCode.InvalidRequest, 'Not logged in');
}
try {
const response = await this.axiosInstance.post(`/api/agent/${args.assistant_id}/message`, { message: args.question });
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to ask assistant: ${error.response?.data?.message || error.message}`,
},
],
isError: true,
};
}
}
/**
* Handle requests to list all available workflows
*/
async handleListWorkflows() {
if (!this.config.token) {
throw new McpError(ErrorCode.InvalidRequest, 'Not logged in');
}
try {
const response = await this.axiosInstance.get('/api/workflows');
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to list workflows: ${error.response?.data?.message || error.message}`,
},
],
isError: true,
};
}
}
/**
* Handle requests to get workflow inputs
* @param args Arguments containing workflow_id
*/
async handleGetWorkflowInputs(args) {
if (!this.config.token) {
throw new McpError(ErrorCode.InvalidRequest, 'Not logged in');
}
try {
const response = await this.axiosInstance.get(`/api/workflows/api-client/${args.workflow_id}/inputs`);
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to get workflow inputs: ${error.response?.data?.message || error.message}`,
},
],
isError: true,
};
}
}
/**
* Handle requests to execute a workflow
* @param args Arguments containing workflow_id and inputs
*/
async handleExecuteWorkflow(args) {
if (!this.config.token) {
throw new McpError(ErrorCode.InvalidRequest, 'Not logged in');
}
try {
const response = await this.axiosInstance.post(`/api/workflows/api-client/${args.workflow_id}`, args.inputs);
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to execute workflow: ${error.response?.data?.message || error.message}`,
},
],
isError: true,
};
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Folderr MCP server running on stdio');
}
}
const server = new FolderrServer();
server.run().catch(console.error);