@baruchiro/actual-mcp
Version:
Actual Budget MCP server exposing API functionality
161 lines (158 loc) • 6.83 kB
JavaScript
// ----------------------------
// RESOURCES
// ----------------------------
import { ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import api from "@actual-app/api";
import { formatAmount, formatDate, getDateRange } from "./utils.js";
import { initActualApi, shutdownActualApi } from "./actual-api.js";
export const setupResources = (server) => {
/**
* Handler for listing available resources (accounts)
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
try {
await initActualApi();
const accounts = await api.getAccounts();
return {
resources: accounts.map((account) => ({
uri: `actual://accounts/${account.id}`,
name: account.name,
description: `${account.name} (${account.type || "Account"})${account.closed ? " - CLOSED" : ""}`,
mimeType: "text/markdown",
})),
};
}
catch (error) {
console.error("Error listing resources:", error);
throw error;
}
finally {
await shutdownActualApi();
}
});
/**
* Handler for reading resources (account details and transactions)
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
try {
await initActualApi();
const uri = request.params.uri;
const url = new URL(uri);
// Parse the path to determine what to return
const pathParts = url.pathname.split("/").filter(Boolean);
// If the path is just "accounts", return list of all accounts
if (pathParts.length === 0 && url.hostname === "accounts") {
const accounts = await api.getAccounts();
const accountsText = accounts
.map((account) => {
const closed = account.closed ? " (CLOSED)" : "";
const offBudget = account.offbudget ? " (OFF BUDGET)" : "";
const balance = account.balance !== undefined
? ` - ${formatAmount(account.balance)}`
: "";
return `- ${account.name}${closed}${offBudget}${balance} [ID: ${account.id}]`;
})
.join("\n");
return {
contents: [
{
uri: uri,
text: `# Actual Budget Accounts\n\n${accountsText}\n\nTotal Accounts: ${accounts.length}`,
mimeType: "text/markdown",
},
],
};
}
// If the path is "accounts/{id}", return account details
if (pathParts.length === 1 && url.hostname === "accounts") {
const accountId = pathParts[0];
const accounts = await api.getAccounts();
const account = accounts.find((a) => a.id === accountId);
if (!account) {
return {
contents: [
{
uri: uri,
text: `Error: Account with ID ${accountId} not found`,
mimeType: "text/plain",
},
],
};
}
const balance = await api.getAccountBalance(accountId);
const formattedBalance = formatAmount(balance);
const details = `# Account: ${account.name}
ID: ${account.id}
Type: ${account.type || "Unknown"}
Balance: ${formattedBalance}
On Budget: ${!account.offbudget}
Status: ${account.closed ? "Closed" : "Open"}
To view transactions for this account, use the get-transactions tool.`;
return {
contents: [
{
uri: uri,
text: details,
mimeType: "text/markdown",
},
],
};
}
// If the path is "accounts/{id}/transactions", return transactions
if (pathParts.length === 2 &&
pathParts[1] === "transactions" &&
url.hostname === "accounts") {
const accountId = pathParts[0];
const { startDate, endDate } = getDateRange();
const transactions = await api.getTransactions(accountId, startDate, endDate);
if (!transactions || transactions.length === 0) {
return {
contents: [
{
uri: uri,
text: `No transactions found for account ID ${accountId} between ${startDate} and ${endDate}`,
mimeType: "text/plain",
},
],
};
}
// Create a markdown table of transactions
const header = "| Date | Payee | Category | Amount | Notes |\n| ---- | ----- | -------- | ------ | ----- |\n";
const rows = transactions
.map((t) => {
const amount = formatAmount(t.amount);
const date = formatDate(t.date);
const payee = t.payee_name || "(No payee)";
const category = t.category_name || "(Uncategorized)";
const notes = t.notes || "";
return `| ${date} | ${payee} | ${category} | ${amount} | ${notes} |`;
})
.join("\n");
const text = `# Transactions for Account\n\nTime period: ${startDate} to ${endDate}\nTotal Transactions: ${transactions.length}\n\n${header}${rows}`;
return {
contents: [
{
uri: uri,
text: text,
mimeType: "text/markdown",
},
],
};
}
// If we don't recognize the URI pattern, return an error
return {
contents: [
{
uri: uri,
text: `Error: Unrecognized resource URI: ${uri}`,
mimeType: "text/plain",
},
],
};
}
catch (error) {
console.error("Error reading resource:", error);
throw error;
}
});
};