@firefly-iii-mcp/core
Version:
core modules for Firefly III MCP server
230 lines • 9.35 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { generatedTools } from "./tools.js";
import { Validator } from "@cfworker/json-schema";
import { openapiSchemaToJsonSchema } from "@openapi-contrib/openapi-schema-to-json-schema";
import { DEFAULT_PRESET_TAGS } from "./presets.js";
export const executeApiTool = async (toolName, definition, toolArgs, serverConfig) => {
let validatedArgs;
try {
// Validate arguments against the input schema
const schema = openapiSchemaToJsonSchema(definition.inputSchema);
const validator = new Validator(schema, '4');
const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {};
const validatedResult = validator.validate(argsToParse);
if (validatedResult.valid) {
validatedArgs = argsToParse;
}
else {
const errors = validatedResult.errors;
return {
content: [{
type: 'text', text: JSON.stringify({
message: `Invalid arguments for tool '${toolName}'`,
errors: errors,
}, null, 2)
}]
};
}
}
catch (error) {
return {
content: [{
type: 'text', text: JSON.stringify({
message: `Error validating arguments for tool '${toolName}'`,
error: error,
}, null, 2)
}]
};
}
// Prepare URL, query parameters, headers, and request body
let urlPath = definition.pathTemplate;
const queryParams = {};
const headers = { 'Accept': 'application/json' };
let requestBodyData = undefined;
// Apply parameters to the URL path, query, or headers
definition.executionParameters.forEach((param) => {
const value = validatedArgs[param.name];
if (typeof value !== 'undefined' && value !== null) {
if (param.in === 'path') {
urlPath = urlPath.replace(`{${param.name}}`, encodeURIComponent(String(value)));
}
else if (param.in === 'query') {
queryParams[param.name] = value;
}
else if (param.in === 'header') {
headers[param.name.toLowerCase()] = String(value);
}
}
});
// Ensure all path parameters are resolved
if (urlPath.includes('{')) {
throw new Error(`Failed to resolve path parameters: ${urlPath}`);
}
// Handle request body if needed
if (definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined') {
requestBodyData = validatedArgs['requestBody'];
headers['content-type'] = definition.requestBodyContentType;
}
/**
* Used Preloaded Security Schemes, ignored for now
*/
const { pat, baseUrl } = serverConfig;
headers['Authorization'] = `Bearer ${pat}`;
// Construct the full URL
const requestEndpoint = `${baseUrl}/api${urlPath}`;
const requestUrl = queryParams ? `${requestEndpoint}?${new URLSearchParams(queryParams).toString()}` : requestEndpoint;
const requestMethod = definition.method.toUpperCase();
// Log request info to stderr (doesn't affect MCP output)
console.debug(`Executing tool "${toolName}": ${requestMethod} ${requestEndpoint}`);
const response = await fetch(requestUrl, {
method: definition.method.toUpperCase(),
headers: headers,
body: requestBodyData ? JSON.stringify(requestBodyData) : undefined,
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [{
type: 'text', text: JSON.stringify({
message: `Error executing tool '${toolName}': ${response.status} ${response.statusText}`,
error: errorText,
}, null, 2)
}]
};
}
const responseType = response.headers.get('content-type')?.split(';')[0].trim().toLowerCase();
if (responseType?.includes('json')) {
const responseData = await response.json();
return {
content: [{
type: 'text', text: JSON.stringify(responseData, null, 2)
}]
};
}
else if (responseType?.includes('text')) {
const responseText = await response.text();
return {
content: [{
type: 'text', text: responseText
}]
};
}
// Default to text response for unsupported types
return {
content: [{
type: 'text', text: JSON.stringify({
error: `Unsupported response type: ${responseType}`,
message: `Unsupported response type: ${responseType}`,
}, null, 2)
}]
};
};
/**
* Get the MCP server instance
* @param serverConfig - The server configuration
* @returns The MCP server instance
*/
export const getServer = (serverConfig) => {
const server = new Server({
name: 'Firefly III MCP Agent',
version: '1.3.0',
}, {
capabilities: { tools: {} }
});
if (!serverConfig.baseUrl) {
server.setRequestHandler(ListToolsRequestSchema, async () => {
const unavailableTool = {
name: 'unavailable',
description: 'This tool is not available because the base URL is not configured. Please check your configuration and restart the server.',
inputSchema: {
type: 'object'
},
outputSchema: {
type: 'object',
properties: {
error: {
type: 'string',
description: 'The error message'
},
message: {
type: 'string',
description: 'The error message'
}
}
}
};
return { tools: [unavailableTool] };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return {
content: [{
type: "text", text: JSON.stringify({
error: 'Unavailable',
message: 'Please check your configuration and restart the server.',
}, null, 2)
}]
};
});
return server;
}
if (!serverConfig.pat) {
server.setRequestHandler(ListToolsRequestSchema, async () => {
const unauthorizedTool = {
name: 'unauthorized',
description: 'This tool is not available because the user is not authenticated. Please check your configuration and restart the server.',
inputSchema: {
type: 'object'
},
outputSchema: {
type: 'object',
properties: {
error: {
type: 'string',
description: 'The error message'
},
message: {
type: 'string',
description: 'The error message'
}
}
}
};
return { tools: [unauthorizedTool] };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return {
content: [{
type: "text", text: JSON.stringify({
error: 'Unauthorized',
message: 'Please check your configuration and restart the server.',
}, null, 2)
}]
};
});
return server;
}
const enableToolTags = serverConfig.enableToolTags ?? DEFAULT_PRESET_TAGS;
server.setRequestHandler(ListToolsRequestSchema, async () => {
const toolsForClient = generatedTools.filter(def => enableToolTags.length === 0 || enableToolTags.some(tag => def.tags.includes(tag))).map(def => ({
name: def.name,
description: def.description,
inputSchema: {
...def.inputSchema,
type: 'object',
},
}));
return { tools: toolsForClient };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name: toolName, arguments: toolArgs } = request.params;
const toolDefinition = generatedTools.find(tool => tool.name === toolName);
if (!toolDefinition) {
console.error(`Error: Unknown tool requested: ${toolName}`);
return { content: [{ type: "text", text: `Error: Unknown tool requested: ${toolName}` }] };
}
return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, serverConfig);
});
return server;
};
//# sourceMappingURL=server.js.map