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 +21 -0
- package/README.md +190 -0
- package/dist/config.js +22 -0
- package/dist/errors.js +37 -0
- package/dist/index.js +21 -0
- package/dist/kakeibo-client.js +111 -0
- package/dist/smoke.js +23 -0
- package/dist/tools/accounts.js +13 -0
- package/dist/tools/categories.js +13 -0
- package/dist/tools/currencies.js +13 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/me.js +22 -0
- package/dist/tools/reports.js +41 -0
- package/dist/tools/schemas.js +29 -0
- package/dist/tools/tags.js +13 -0
- package/dist/tools/tool-definition.js +39 -0
- package/dist/tools/transactions.js +22 -0
- package/package.json +57 -0
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
|
+
];
|
package/dist/tools/me.js
ADDED
|
@@ -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
|
+
}
|