kakeibo-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kakeibo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # Kakeibo MCP Server
2
+
3
+ Read-only MCP server for connecting AI clients to the Kakeibo Public Developer API.
4
+
5
+ Phase 1 exposes personal `me` scope tools only. It can inspect profile, usage, currencies,
6
+ accounts, categories, tags, transactions, and reports. It does not create, update, void, delete,
7
+ upload, or otherwise mutate Kakeibo data.
8
+
9
+ This package is MIT licensed. The Kakeibo application, backend, and hosted services remain
10
+ proprietary.
11
+
12
+ ## Requirements
13
+
14
+ - Node.js 20+
15
+ - A Kakeibo Solo or Family account
16
+ - A Kakeibo Public API key created in the dashboard under Settings -> Developer
17
+ - An MCP-compatible client that can launch local stdio servers
18
+
19
+ Do not commit API keys. Pass them through environment variables or your local MCP client
20
+ configuration.
21
+
22
+ ## Configuration
23
+
24
+ | Variable | Description |
25
+ |---|---|
26
+ | `KAKEIBO_API_BASE_URL` | Kakeibo API origin, for example `https://api.kakei.io` |
27
+ | `KAKEIBO_API_KEY` | Public API key from Settings -> Developer |
28
+
29
+ For production use:
30
+
31
+ ```bash
32
+ export KAKEIBO_API_BASE_URL="https://api.kakei.io"
33
+ export KAKEIBO_API_KEY="kak_xxxxxxxxxxxxxxxxxxxxxxxx"
34
+ ```
35
+
36
+ For local backend development, use your Laravel API origin, for example `http://localhost:8000`.
37
+ Do not include `/api/public/v1`; the server adds that path internally.
38
+
39
+ ## Install with npx
40
+
41
+ Most users should run the published npm package directly from their MCP client configuration:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "kakeibo": {
47
+ "command": "npx",
48
+ "args": ["-y", "kakeibo-mcp-server"],
49
+ "env": {
50
+ "KAKEIBO_API_BASE_URL": "https://api.kakei.io",
51
+ "KAKEIBO_API_KEY": "kak_xxxxxxxxxxxxxxxxxxxxxxxx"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ Restart your MCP client after updating its configuration.
59
+
60
+ ## Local Development
61
+
62
+ Install dependencies from the repository root:
63
+
64
+ ```bash
65
+ pnpm install
66
+ ```
67
+
68
+ Run typecheck, build, and tests:
69
+
70
+ ```bash
71
+ pnpm --filter kakeibo-mcp-server typecheck
72
+ pnpm --filter kakeibo-mcp-server build
73
+ pnpm --filter kakeibo-mcp-server test
74
+ ```
75
+
76
+ Start the server in development mode:
77
+
78
+ ```bash
79
+ KAKEIBO_API_BASE_URL="http://localhost:8000" \
80
+ KAKEIBO_API_KEY="your-public-api-key" \
81
+ pnpm --filter kakeibo-mcp-server dev
82
+ ```
83
+
84
+ Build and run the compiled server:
85
+
86
+ ```bash
87
+ pnpm --filter kakeibo-mcp-server build
88
+
89
+ KAKEIBO_API_BASE_URL="http://localhost:8000" \
90
+ KAKEIBO_API_KEY="your-public-api-key" \
91
+ pnpm --filter kakeibo-mcp-server start
92
+ ```
93
+
94
+ ## Local MCP Client Configuration
95
+
96
+ When developing from a local checkout, point your MCP client at the built server entrypoint:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "kakeibo": {
102
+ "command": "node",
103
+ "args": ["/absolute/path/to/kakeibo/apps/mcp-server/dist/index.js"],
104
+ "env": {
105
+ "KAKEIBO_API_BASE_URL": "http://localhost:8000",
106
+ "KAKEIBO_API_KEY": "your-public-api-key"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ Use an absolute path for `dist/index.js`. Rebuild after code changes.
114
+
115
+ ## Publishing
116
+
117
+ Run these checks before publishing:
118
+
119
+ ```bash
120
+ pnpm --filter kakeibo-mcp-server typecheck
121
+ pnpm --filter kakeibo-mcp-server build
122
+ pnpm --filter kakeibo-mcp-server test
123
+ pnpm --filter kakeibo-mcp-server exec npm pack --dry-run
124
+ ```
125
+
126
+ Publish from the package directory after confirming the packed files:
127
+
128
+ ```bash
129
+ cd apps/mcp-server
130
+ npm publish --access public
131
+ ```
132
+
133
+ ## Tools
134
+
135
+ | Tool | Purpose |
136
+ |---|---|
137
+ | `get_current_user` | Get user profile and plan context |
138
+ | `get_usage` | Inspect plan usage and quotas |
139
+ | `list_currencies` | List supported currencies |
140
+ | `list_accounts` | List accounts and balances |
141
+ | `list_categories` | List transaction categories |
142
+ | `list_tags` | List transaction tags |
143
+ | `search_transactions` | Search and filter transactions |
144
+ | `get_transaction` | Fetch a transaction by id |
145
+ | `get_total_spending_report` | Get income, expense, and net totals |
146
+ | `get_categories_spending_report` | Get spending grouped by category |
147
+ | `get_spending_trends` | Get spending trend time series |
148
+ | `get_dashboard_report` | Get combined report data |
149
+
150
+ Report tools return Kakeibo Public API output as-is. Multi-currency totals may be limited while
151
+ `converted_amount = amount` in the backend.
152
+
153
+ ## Example Prompts
154
+
155
+ - Show my spending by category this month.
156
+ - Find my largest expenses this year.
157
+ - List my recent transactions for the default account.
158
+ - How close am I to my plan limits?
159
+ - Summarize my income and expenses for last month.
160
+
161
+ ## Smoke Test
162
+
163
+ 1. Start the Laravel API locally and confirm the Public Developer API is reachable.
164
+ 2. Create a Public API key in Settings -> Developer.
165
+ 3. Build this package with `pnpm --filter kakeibo-mcp-server build`.
166
+ 4. Configure an MCP client with the stdio command shown above.
167
+ 5. Ask for profile, reference data, transaction search, and a report.
168
+
169
+ You can also run the built-in smoke script:
170
+
171
+ ```bash
172
+ KAKEIBO_API_BASE_URL="http://localhost:8000" \
173
+ KAKEIBO_API_KEY="your-public-api-key" \
174
+ pnpm --filter kakeibo-mcp-server smoke
175
+ ```
176
+
177
+ Minimum tools to verify:
178
+
179
+ | Area | Tool |
180
+ |---|---|
181
+ | Profile | `get_current_user` |
182
+ | Reference data | `list_accounts` |
183
+ | Transactions | `search_transactions` |
184
+ | Reports | `get_dashboard_report` |
185
+
186
+ ## Phase 2
187
+
188
+ Mutation tools are planned separately. They will require stricter confirmation behavior and should
189
+ not be added to Phase 1. Candidate future tools include transaction creation, transaction voiding,
190
+ account lifecycle changes, category/tag management, and file attachment operations.
package/dist/config.js ADDED
@@ -0,0 +1,22 @@
1
+ const normalizeBaseUrl = (value) => value.replace(/\/+$/, '');
2
+ export const loadConfig = (env = process.env) => {
3
+ const apiBaseUrl = env.KAKEIBO_API_BASE_URL?.trim();
4
+ const apiKey = env.KAKEIBO_API_KEY?.trim();
5
+ const missing = [];
6
+ if (!apiBaseUrl) {
7
+ missing.push('KAKEIBO_API_BASE_URL');
8
+ }
9
+ if (!apiKey) {
10
+ missing.push('KAKEIBO_API_KEY');
11
+ }
12
+ if (missing.length > 0) {
13
+ throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
14
+ }
15
+ if (!apiBaseUrl || !apiKey) {
16
+ throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
17
+ }
18
+ return {
19
+ apiBaseUrl: normalizeBaseUrl(apiBaseUrl),
20
+ apiKey,
21
+ };
22
+ };
package/dist/errors.js ADDED
@@ -0,0 +1,37 @@
1
+ export class KakeiboApiError extends Error {
2
+ status;
3
+ validationErrors;
4
+ constructor(status, message, validationErrors) {
5
+ super(message);
6
+ this.name = 'KakeiboApiError';
7
+ this.status = status;
8
+ this.validationErrors = validationErrors;
9
+ }
10
+ }
11
+ export const statusToMessage = (status) => {
12
+ if (status === 401) {
13
+ return 'Invalid or missing Kakeibo API key.';
14
+ }
15
+ if (status === 402) {
16
+ return 'The current Kakeibo plan does not allow Public API access.';
17
+ }
18
+ if (status === 403) {
19
+ return 'The Kakeibo API key is not allowed to access this resource.';
20
+ }
21
+ if (status === 404) {
22
+ return 'The requested Kakeibo resource was not found.';
23
+ }
24
+ if (status === 409) {
25
+ return 'The request conflicts with Kakeibo business rules.';
26
+ }
27
+ if (status === 422) {
28
+ return 'The request contains invalid Kakeibo API parameters.';
29
+ }
30
+ if (status === 429) {
31
+ return 'Kakeibo Public API rate limit exceeded.';
32
+ }
33
+ if (status >= 500) {
34
+ return 'Kakeibo Public API returned an upstream server error.';
35
+ }
36
+ return `Kakeibo Public API request failed with status ${status}.`;
37
+ };
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { loadConfig } from './config.js';
5
+ import { KakeiboClient } from './kakeibo-client.js';
6
+ import { buildReadOnlyTools, registerTools } from './tools/index.js';
7
+ const main = async () => {
8
+ const config = loadConfig();
9
+ const server = new McpServer({
10
+ name: 'kakeibo',
11
+ version: '0.1.0',
12
+ });
13
+ const client = new KakeiboClient(config);
14
+ registerTools(server, buildReadOnlyTools(client));
15
+ await server.connect(new StdioServerTransport());
16
+ };
17
+ main().catch((error) => {
18
+ const message = error instanceof Error ? error.message : 'Unknown MCP server startup error';
19
+ console.error(`Kakeibo MCP server failed to start: ${message}`);
20
+ process.exitCode = 1;
21
+ });
@@ -0,0 +1,111 @@
1
+ import { KakeiboApiError, statusToMessage } from './errors.js';
2
+ export class KakeiboClient {
3
+ apiBaseUrl;
4
+ apiKey;
5
+ fetchFn;
6
+ constructor(config, fetchFn = fetch) {
7
+ this.apiBaseUrl = config.apiBaseUrl;
8
+ this.apiKey = config.apiKey;
9
+ this.fetchFn = fetchFn;
10
+ }
11
+ getCurrentUser() {
12
+ return this.get('/api/public/v1/me');
13
+ }
14
+ getUsage() {
15
+ return this.get('/api/public/v1/me/usage');
16
+ }
17
+ listCurrencies() {
18
+ return this.get('/api/public/v1/currencies');
19
+ }
20
+ listAccounts() {
21
+ return this.get('/api/public/v1/me/accounts');
22
+ }
23
+ listCategories() {
24
+ return this.get('/api/public/v1/me/categories');
25
+ }
26
+ listTags() {
27
+ return this.get('/api/public/v1/me/tags');
28
+ }
29
+ searchTransactions(params = {}) {
30
+ return this.get('/api/public/v1/me/transactions', params);
31
+ }
32
+ getTransaction(transactionId) {
33
+ return this.get(`/api/public/v1/me/transactions/${encodeURIComponent(transactionId)}`);
34
+ }
35
+ getTotalSpendingReport(params) {
36
+ return this.get('/api/public/v1/me/reports/total-spending', params);
37
+ }
38
+ getCategoriesSpendingReport(params) {
39
+ return this.get('/api/public/v1/me/reports/categories-spending', params);
40
+ }
41
+ getSpendingTrends(params) {
42
+ return this.get('/api/public/v1/me/reports/spending-trends', params);
43
+ }
44
+ getDashboardReport(params) {
45
+ return this.get('/api/public/v1/me/reports/dashboard', params);
46
+ }
47
+ async get(path, query = {}) {
48
+ const response = await this.fetchFn(this.buildUrl(path, query), {
49
+ method: 'GET',
50
+ headers: {
51
+ Accept: 'application/json',
52
+ Authorization: `Bearer ${this.apiKey}`,
53
+ },
54
+ });
55
+ const body = await this.parseJson(response);
56
+ if (!response.ok) {
57
+ throw this.toApiError(response.status, body);
58
+ }
59
+ return body;
60
+ }
61
+ buildUrl(path, query) {
62
+ const url = new URL(path, `${this.apiBaseUrl}/`);
63
+ Object.entries(query).forEach(([key, value]) => {
64
+ if (value === undefined || value === null || value === '') {
65
+ return;
66
+ }
67
+ url.searchParams.set(key, String(value));
68
+ });
69
+ return url.toString();
70
+ }
71
+ async parseJson(response) {
72
+ const text = await response.text();
73
+ if (!text) {
74
+ return null;
75
+ }
76
+ try {
77
+ return JSON.parse(text);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ toApiError(status, body) {
84
+ const errorBody = this.asErrorBody(body);
85
+ const upstreamMessage = this.extractMessage(errorBody);
86
+ const fallbackMessage = statusToMessage(status);
87
+ const message = upstreamMessage ? `${fallbackMessage} ${upstreamMessage}` : fallbackMessage;
88
+ const validationErrors = this.extractValidationErrors(errorBody);
89
+ return new KakeiboApiError(status, message, validationErrors);
90
+ }
91
+ asErrorBody(body) {
92
+ if (!body || typeof body !== 'object') {
93
+ return null;
94
+ }
95
+ return body;
96
+ }
97
+ extractMessage(body) {
98
+ if (!body) {
99
+ return null;
100
+ }
101
+ const candidates = [body.meta?.message, body.error?.message, body.message];
102
+ const message = candidates.find((candidate) => typeof candidate === 'string' && candidate);
103
+ return typeof message === 'string' ? message : null;
104
+ }
105
+ extractValidationErrors(body) {
106
+ if (!body || !body.errors || typeof body.errors !== 'object') {
107
+ return undefined;
108
+ }
109
+ return body.errors;
110
+ }
111
+ }
package/dist/smoke.js ADDED
@@ -0,0 +1,23 @@
1
+ import { loadConfig } from './config.js';
2
+ import { KakeiboClient } from './kakeibo-client.js';
3
+ import { buildReadOnlyTools } from './tools/index.js';
4
+ const requiredTools = ['get_current_user', 'list_accounts', 'search_transactions', 'get_dashboard_report'];
5
+ const main = async () => {
6
+ const client = new KakeiboClient(loadConfig());
7
+ const tools = buildReadOnlyTools(client);
8
+ for (const toolName of requiredTools) {
9
+ const tool = tools.find((candidate) => candidate.name === toolName);
10
+ if (!tool) {
11
+ throw new Error(`Missing smoke-test tool: ${toolName}`);
12
+ }
13
+ const args = toolName === 'search_transactions' ? { limit: 1 } : toolName === 'get_dashboard_report' ? { time_range: 'this_month' } : {};
14
+ const result = await tool.handler(args);
15
+ const text = result.content[0]?.text ?? '';
16
+ console.error(`Smoke test passed: ${toolName} returned ${text.length} characters.`);
17
+ }
18
+ };
19
+ main().catch((error) => {
20
+ const message = error instanceof Error ? error.message : 'Unknown smoke test error';
21
+ console.error(`Kakeibo MCP smoke test failed: ${message}`);
22
+ process.exitCode = 1;
23
+ });
@@ -0,0 +1,13 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { emptySchema } from './schemas.js';
3
+ export const accountTools = (client) => [
4
+ {
5
+ name: 'list_accounts',
6
+ description: 'List Kakeibo financial accounts and balances for the authenticated user.',
7
+ schema: emptySchema,
8
+ handler: async (args) => {
9
+ emptySchema.parse(args);
10
+ return publicApiContent(await client.listAccounts());
11
+ },
12
+ },
13
+ ];
@@ -0,0 +1,13 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { emptySchema } from './schemas.js';
3
+ export const categoryTools = (client) => [
4
+ {
5
+ name: 'list_categories',
6
+ description: 'List Kakeibo transaction categories for the authenticated user.',
7
+ schema: emptySchema,
8
+ handler: async (args) => {
9
+ emptySchema.parse(args);
10
+ return publicApiContent(await client.listCategories());
11
+ },
12
+ },
13
+ ];
@@ -0,0 +1,13 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { emptySchema } from './schemas.js';
3
+ export const currencyTools = (client) => [
4
+ {
5
+ name: 'list_currencies',
6
+ description: 'List global currencies supported by Kakeibo.',
7
+ schema: emptySchema,
8
+ handler: async (args) => {
9
+ emptySchema.parse(args);
10
+ return publicApiContent(await client.listCurrencies());
11
+ },
12
+ },
13
+ ];
@@ -0,0 +1,17 @@
1
+ import { accountTools } from './accounts.js';
2
+ import { categoryTools } from './categories.js';
3
+ import { currencyTools } from './currencies.js';
4
+ import { meTools } from './me.js';
5
+ import { reportTools } from './reports.js';
6
+ import { tagTools } from './tags.js';
7
+ import { transactionTools } from './transactions.js';
8
+ export { registerTools } from './tool-definition.js';
9
+ export const buildReadOnlyTools = (client) => [
10
+ ...meTools(client),
11
+ ...currencyTools(client),
12
+ ...accountTools(client),
13
+ ...categoryTools(client),
14
+ ...tagTools(client),
15
+ ...transactionTools(client),
16
+ ...reportTools(client),
17
+ ];
@@ -0,0 +1,22 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { emptySchema } from './schemas.js';
3
+ export const meTools = (client) => [
4
+ {
5
+ name: 'get_current_user',
6
+ description: 'Get the authenticated Kakeibo user profile and plan context.',
7
+ schema: emptySchema,
8
+ handler: async (args) => {
9
+ emptySchema.parse(args);
10
+ return publicApiContent(await client.getCurrentUser());
11
+ },
12
+ },
13
+ {
14
+ name: 'get_usage',
15
+ description: 'Inspect Kakeibo plan usage and quota information for the authenticated user.',
16
+ schema: emptySchema,
17
+ handler: async (args) => {
18
+ emptySchema.parse(args);
19
+ return publicApiContent(await client.getUsage());
20
+ },
21
+ },
22
+ ];
@@ -0,0 +1,41 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { reportSchema, spendingTrendsSchema } from './schemas.js';
3
+ const multiCurrencyNote = 'Report totals use Kakeibo Public API output as-is. Multi-currency totals may be limited while converted_amount equals amount.';
4
+ export const reportTools = (client) => [
5
+ {
6
+ name: 'get_total_spending_report',
7
+ description: `Get Kakeibo income, expense, and net totals for a period. ${multiCurrencyNote}`,
8
+ schema: reportSchema,
9
+ handler: async (args) => {
10
+ const params = reportSchema.parse(args);
11
+ return publicApiContent(await client.getTotalSpendingReport(params), [multiCurrencyNote]);
12
+ },
13
+ },
14
+ {
15
+ name: 'get_categories_spending_report',
16
+ description: `Get Kakeibo spending grouped by category for a period. ${multiCurrencyNote}`,
17
+ schema: reportSchema,
18
+ handler: async (args) => {
19
+ const params = reportSchema.parse(args);
20
+ return publicApiContent(await client.getCategoriesSpendingReport(params), [multiCurrencyNote]);
21
+ },
22
+ },
23
+ {
24
+ name: 'get_spending_trends',
25
+ description: `Get Kakeibo spending trend time series for a period. ${multiCurrencyNote}`,
26
+ schema: spendingTrendsSchema,
27
+ handler: async (args) => {
28
+ const params = spendingTrendsSchema.parse(args);
29
+ return publicApiContent(await client.getSpendingTrends(params), [multiCurrencyNote]);
30
+ },
31
+ },
32
+ {
33
+ name: 'get_dashboard_report',
34
+ description: `Get combined Kakeibo dashboard report data for an overview answer. ${multiCurrencyNote}`,
35
+ schema: reportSchema,
36
+ handler: async (args) => {
37
+ const params = reportSchema.parse(args);
38
+ return publicApiContent(await client.getDashboardReport(params), [multiCurrencyNote]);
39
+ },
40
+ },
41
+ ];
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export const emptySchema = z.object({});
3
+ export const idSchema = z.object({
4
+ id: z.string().min(1),
5
+ });
6
+ export const transactionSearchSchema = z.object({
7
+ account_id: z.number().int().positive().optional(),
8
+ category_id: z.number().int().positive().optional(),
9
+ type: z.enum(['income', 'expense']).optional(),
10
+ search: z.string().min(1).optional(),
11
+ sort_by: z.enum(['amount', 'transaction_at']).optional(),
12
+ sort_order: z.enum(['asc', 'desc']).optional(),
13
+ limit: z.number().int().positive().max(100).default(20),
14
+ page: z.number().int().positive().default(1),
15
+ include_voided: z.boolean().default(false),
16
+ });
17
+ export const reportSchema = z
18
+ .object({
19
+ time_range: z.enum(['this_week', 'this_month', 'last_month', 'this_quarter', 'this_year', 'custom']),
20
+ start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
21
+ end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
22
+ })
23
+ .refine((value) => value.time_range !== 'custom' || (value.start_date && value.end_date), {
24
+ message: 'start_date and end_date are required when time_range is custom.',
25
+ path: ['start_date'],
26
+ });
27
+ export const spendingTrendsSchema = reportSchema.extend({
28
+ group_by: z.enum(['day', 'week', 'month']).default('day'),
29
+ });
@@ -0,0 +1,13 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { emptySchema } from './schemas.js';
3
+ export const tagTools = (client) => [
4
+ {
5
+ name: 'list_tags',
6
+ description: 'List Kakeibo transaction tags for the authenticated user.',
7
+ schema: emptySchema,
8
+ handler: async (args) => {
9
+ emptySchema.parse(args);
10
+ return publicApiContent(await client.listTags());
11
+ },
12
+ },
13
+ ];
@@ -0,0 +1,39 @@
1
+ export const jsonContent = (payload) => ({
2
+ content: [
3
+ {
4
+ type: 'text',
5
+ text: JSON.stringify(payload, null, 2),
6
+ },
7
+ ],
8
+ });
9
+ export const publicApiContent = (payload, notes = []) => {
10
+ const shapedPayload = shapePublicApiPayload(payload, notes);
11
+ return jsonContent(shapedPayload);
12
+ };
13
+ const shapePublicApiPayload = (payload, notes) => {
14
+ if (!payload || typeof payload !== 'object') {
15
+ return notes.length > 0 ? { data: payload, notes } : payload;
16
+ }
17
+ const record = payload;
18
+ if (!('data' in record)) {
19
+ return notes.length > 0 ? { ...record, notes } : payload;
20
+ }
21
+ const shaped = {
22
+ data: record.data,
23
+ };
24
+ if ('meta' in record) {
25
+ shaped.meta = record.meta;
26
+ }
27
+ if (notes.length > 0) {
28
+ shaped.notes = notes;
29
+ }
30
+ return shaped;
31
+ };
32
+ export const registerTools = (server, tools) => {
33
+ tools.forEach((tool) => {
34
+ server.registerTool(tool.name, {
35
+ description: tool.description,
36
+ inputSchema: tool.schema.shape,
37
+ }, async (args) => tool.handler(args));
38
+ });
39
+ };
@@ -0,0 +1,22 @@
1
+ import { publicApiContent } from './tool-definition.js';
2
+ import { idSchema, transactionSearchSchema } from './schemas.js';
3
+ export const transactionTools = (client) => [
4
+ {
5
+ name: 'search_transactions',
6
+ description: 'Search Kakeibo transactions with optional filters, pagination, and sorting. Defaults hide voided transactions.',
7
+ schema: transactionSearchSchema,
8
+ handler: async (args) => {
9
+ const params = transactionSearchSchema.parse(args);
10
+ return publicApiContent(await client.searchTransactions(params));
11
+ },
12
+ },
13
+ {
14
+ name: 'get_transaction',
15
+ description: 'Fetch a single Kakeibo transaction by id.',
16
+ schema: idSchema,
17
+ handler: async (args) => {
18
+ const { id } = idSchema.parse(args);
19
+ return publicApiContent(await client.getTransaction(id));
20
+ },
21
+ },
22
+ ];
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "kakeibo-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Read-only MCP server for connecting AI clients to the Kakeibo Public Developer API.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kakeibo-mcp-server": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "LICENSE",
12
+ "README.md",
13
+ "package.json"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "keywords": [
19
+ "kakeibo",
20
+ "kakei",
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "personal-finance"
24
+ ],
25
+ "homepage": "https://kakei.io/developers#mcp-server",
26
+ "bugs": {
27
+ "url": "https://github.com/mrsuner/kakeibo/issues"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+ssh://git@github.com/mrsuner/kakeibo.git",
32
+ "directory": "apps/mcp-server"
33
+ },
34
+ "license": "MIT",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "dev": "tsx src/index.ts",
40
+ "build": "tsc",
41
+ "start": "node dist/index.js",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run --passWithNoTests --exclude \"dist/**\"",
44
+ "smoke": "node dist/smoke.js",
45
+ "prepublishOnly": "npm run typecheck && npm run build && npm test"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.29.0",
49
+ "zod": "^4.1.5"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20",
53
+ "tsx": "^4.20.6",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.0.15"
56
+ }
57
+ }