@kansei-link/bantou 0.2.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 +110 -0
- package/data/exclusion-rules/README.md +104 -0
- package/data/exclusion-rules/jp-tax-baseline-v1.json +185 -0
- package/data/exclusion-rules-schema.json +109 -0
- package/data/keyword-dict/README.md +91 -0
- package/data/keyword-dict/jp-tax-baseline-v1.json +398 -0
- package/data/keyword-dict-schema.json +117 -0
- package/data/tax-rules/jp-tax-rules-v1.json +170 -0
- package/dist/adapters/csv-parser.d.ts +11 -0
- package/dist/adapters/csv-parser.js +133 -0
- package/dist/adapters/freee-csv-adapter.d.ts +14 -0
- package/dist/adapters/freee-csv-adapter.js +67 -0
- package/dist/adapters/generic-adapter.d.ts +20 -0
- package/dist/adapters/generic-adapter.js +73 -0
- package/dist/adapters/index.d.ts +23 -0
- package/dist/adapters/index.js +386 -0
- package/dist/adapters/types.d.ts +111 -0
- package/dist/adapters/types.js +9 -0
- package/dist/adapters/yayoi-adapter.d.ts +46 -0
- package/dist/adapters/yayoi-adapter.js +181 -0
- package/dist/bin/freee-doctor.d.ts +3 -0
- package/dist/bin/freee-doctor.js +15 -0
- package/dist/classifier/claude-classifier.d.ts +24 -0
- package/dist/classifier/claude-classifier.js +154 -0
- package/dist/classifier/keyword-classifier.d.ts +22 -0
- package/dist/classifier/keyword-classifier.js +124 -0
- package/dist/classifier/keyword-match.d.ts +21 -0
- package/dist/classifier/keyword-match.js +57 -0
- package/dist/classifier/normalize.d.ts +3 -0
- package/dist/classifier/normalize.js +27 -0
- package/dist/classifier/two-stage-classifier.d.ts +21 -0
- package/dist/classifier/two-stage-classifier.js +51 -0
- package/dist/classifier/types.d.ts +31 -0
- package/dist/classifier/types.js +3 -0
- package/dist/connectors/freee.d.ts +115 -0
- package/dist/connectors/freee.js +177 -0
- package/dist/exclusion/exclusion-checker.d.ts +10 -0
- package/dist/exclusion/exclusion-checker.js +162 -0
- package/dist/freee-doctor.d.ts +26 -0
- package/dist/freee-doctor.js +82 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +656 -0
- package/dist/memory/cockpit-memory.d.ts +73 -0
- package/dist/memory/cockpit-memory.js +473 -0
- package/dist/memory/types.d.ts +114 -0
- package/dist/memory/types.js +11 -0
- package/dist/pipeline/confidence-router.d.ts +38 -0
- package/dist/pipeline/confidence-router.js +129 -0
- package/dist/pipeline/nightly-pipeline.d.ts +44 -0
- package/dist/pipeline/nightly-pipeline.js +497 -0
- package/dist/pipeline/types.d.ts +84 -0
- package/dist/pipeline/types.js +12 -0
- package/dist/reports/monthly-report.d.ts +64 -0
- package/dist/reports/monthly-report.js +230 -0
- package/dist/secrets.d.ts +14 -0
- package/dist/secrets.js +86 -0
- package/dist/tax-rules/tax-rule-engine.d.ts +103 -0
- package/dist/tax-rules/tax-rule-engine.js +449 -0
- package/dist/tax-rules/types.d.ts +103 -0
- package/dist/tax-rules/types.js +7 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @kansei-link/cockpit — Pre-built accounting automation MCP server for Japanese tax firms.
|
|
3
|
+
//
|
|
4
|
+
// Tools (v0.0.5-pre):
|
|
5
|
+
// - classify_transaction: Stage 1 keyword + Stage 2 Claude API fallback
|
|
6
|
+
// - check_exclusion: Stage 0 = 7-rule exclusion filter
|
|
7
|
+
// - import_csv: Multi-platform CSV import (弥生/freee/MF/汎用) + classification pipeline
|
|
8
|
+
// - generate_monthly_report: Monthly review report generation
|
|
9
|
+
// - correct_classification: 税理士修正フィードバック (Memory caveat layer — 永続記憶)
|
|
10
|
+
// - recall_memory: 過去パターン・修正履歴検索
|
|
11
|
+
// - list_freee_deals: List transactions from freee API
|
|
12
|
+
// - list_freee_companies: List all companies accessible by token (multi-company batch)
|
|
13
|
+
// - freee_doctor: One-shot freee connection diagnostic (token source / expiry / which company_id)
|
|
14
|
+
// - reconcile_cross_saas: freee ↔ MF double-entry detection (= MF connector pending)
|
|
15
|
+
// - check_duplicate: Existing transaction lookup in target SaaS
|
|
16
|
+
// - upsert_partner: 取引先 master auto-creation
|
|
17
|
+
// - nightly_run: Nightly batch pipeline orchestrator (Memory-enabled)
|
|
18
|
+
//
|
|
19
|
+
// Architecture: see kansei-link-cockpit/docs/architecture.md
|
|
20
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
21
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
22
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
23
|
+
import { loadFreeeSecrets } from './secrets.js';
|
|
24
|
+
import { FreeeConnector } from './connectors/freee.js';
|
|
25
|
+
import { runFreeeDoctor } from './freee-doctor.js';
|
|
26
|
+
import { KeywordClassifier } from './classifier/keyword-classifier.js';
|
|
27
|
+
import { ClaudeClassifier } from './classifier/claude-classifier.js';
|
|
28
|
+
import { TwoStageClassifier } from './classifier/two-stage-classifier.js';
|
|
29
|
+
import { ExclusionChecker } from './exclusion/exclusion-checker.js';
|
|
30
|
+
import { NightlyPipeline } from './pipeline/nightly-pipeline.js';
|
|
31
|
+
import { ConfidenceRouter } from './pipeline/confidence-router.js';
|
|
32
|
+
import { importCsv } from './adapters/index.js';
|
|
33
|
+
import { generateMonthlyReport } from './reports/monthly-report.js';
|
|
34
|
+
import { CockpitMemory } from './memory/cockpit-memory.js';
|
|
35
|
+
import { TaxRuleEngine } from './tax-rules/tax-rule-engine.js';
|
|
36
|
+
const SERVER_VERSION = '0.1.0';
|
|
37
|
+
// Lazy-init: load Stage 1 + Stage 2 at startup. Stage 2 optional (= requires ANTHROPIC_API_KEY).
|
|
38
|
+
const keywordClassifier = new KeywordClassifier();
|
|
39
|
+
const claudeApiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
40
|
+
const claudeClassifier = claudeApiKey
|
|
41
|
+
? new ClaudeClassifier(claudeApiKey, keywordClassifier.getCategoriesMeta())
|
|
42
|
+
: null;
|
|
43
|
+
const classifier = new TwoStageClassifier(keywordClassifier, claudeClassifier);
|
|
44
|
+
const exclusion = new ExclusionChecker();
|
|
45
|
+
const confidenceRouter = new ConfidenceRouter();
|
|
46
|
+
const memory = new CockpitMemory();
|
|
47
|
+
const taxRuleEngine = new TaxRuleEngine();
|
|
48
|
+
let freeeConnector = null;
|
|
49
|
+
function getFreeeConnector() {
|
|
50
|
+
if (!freeeConnector) {
|
|
51
|
+
const secrets = loadFreeeSecrets();
|
|
52
|
+
freeeConnector = new FreeeConnector(secrets);
|
|
53
|
+
}
|
|
54
|
+
return freeeConnector;
|
|
55
|
+
}
|
|
56
|
+
const server = new Server({ name: '@kansei-link/cockpit', version: SERVER_VERSION }, {
|
|
57
|
+
capabilities: {
|
|
58
|
+
tools: {},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const TOOLS = [
|
|
62
|
+
{
|
|
63
|
+
name: 'classify_transaction',
|
|
64
|
+
description: 'Two-stage classifier for Japanese tax accounting. Stage 1: keyword dictionary match (14 categories × ~50 keywords). Stage 2 (= deferred to Phase 1.B): Claude API fallback. Returns 勘定科目 + 税区分 + confidence.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
amount: { type: 'number', description: 'Transaction amount (JPY)' },
|
|
69
|
+
memo: { type: 'string', description: '取引摘要' },
|
|
70
|
+
date: { type: 'string', description: 'ISO 8601 date YYYY-MM-DD' },
|
|
71
|
+
partner_name: { type: 'string', description: '取引先名 (optional)' },
|
|
72
|
+
},
|
|
73
|
+
required: ['amount', 'memo', 'date'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'check_exclusion',
|
|
78
|
+
description: '7-rule exclusion check for Japanese accounting. Returns excluded:true if transaction should NOT be auto-journalized. Rules: 内容不明デビット / 借入金返済 / 社保税金 / 給与支払い / 投資 / ATM出金 / 公共料金.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
amount: { type: 'number' },
|
|
83
|
+
memo: { type: 'string' },
|
|
84
|
+
partner_name: { type: 'string' },
|
|
85
|
+
employees: {
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: { type: 'string' },
|
|
88
|
+
description: 'Optional employee name list (= for salary_payment detection)',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ['amount', 'memo'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'import_csv',
|
|
96
|
+
description: 'Import CSV from 弥生会計/freee/MoneyForward or generic format. Auto-detects source format from headers. Runs each transaction through the full classification pipeline (Stage 0 exclusion → Stage 1+2 classification → confidence routing). Returns categorized results + review queue + Markdown report. 弥生 users: export 仕訳日記帳 as UTF-8 CSV.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
csv_content: { type: 'string', description: 'Raw CSV text (UTF-8). Paste the full CSV content.' },
|
|
101
|
+
source: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
enum: ['yayoi', 'freee_export', 'mf_export', 'generic'],
|
|
104
|
+
description: 'Force source format (default: auto-detect from headers)',
|
|
105
|
+
},
|
|
106
|
+
date_column: { type: 'string', description: 'Column name for date (generic CSV only)' },
|
|
107
|
+
amount_column: { type: 'string', description: 'Column name for amount (generic CSV only)' },
|
|
108
|
+
memo_column: { type: 'string', description: 'Column name for memo/description (generic CSV only)' },
|
|
109
|
+
partner_column: { type: 'string', description: 'Column name for partner name (generic CSV only, optional)' },
|
|
110
|
+
},
|
|
111
|
+
required: ['csv_content'],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'generate_monthly_report',
|
|
116
|
+
description: 'Generate a monthly review report. Takes transaction data (from freee API or CSV import), classifies all transactions, detects anomalies, and produces a structured Markdown report with category breakdown, anomaly alerts, and review items. Designed for tax accountants to present to clients.',
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
company_name: { type: 'string', description: '会社名 / 顧問先名' },
|
|
121
|
+
month: { type: 'string', description: 'Target month YYYY-MM (e.g. "2026-05")' },
|
|
122
|
+
transactions: {
|
|
123
|
+
type: 'array',
|
|
124
|
+
items: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
amount: { type: 'number' },
|
|
128
|
+
memo: { type: 'string' },
|
|
129
|
+
date: { type: 'string' },
|
|
130
|
+
partner_name: { type: 'string' },
|
|
131
|
+
},
|
|
132
|
+
required: ['amount', 'memo', 'date'],
|
|
133
|
+
},
|
|
134
|
+
description: 'Array of transactions to analyze. If omitted, fetches from freee API for the given month.',
|
|
135
|
+
},
|
|
136
|
+
compare_transactions: {
|
|
137
|
+
type: 'array',
|
|
138
|
+
items: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
amount: { type: 'number' },
|
|
142
|
+
memo: { type: 'string' },
|
|
143
|
+
date: { type: 'string' },
|
|
144
|
+
partner_name: { type: 'string' },
|
|
145
|
+
},
|
|
146
|
+
required: ['amount', 'memo', 'date'],
|
|
147
|
+
},
|
|
148
|
+
description: 'Previous period transactions for comparison (optional)',
|
|
149
|
+
},
|
|
150
|
+
compare_label: { type: 'string', description: 'Comparison label (e.g. "前月", "前年同月")', default: '前月' },
|
|
151
|
+
format: { type: 'string', enum: ['markdown', 'json'], default: 'markdown' },
|
|
152
|
+
use_freee: { type: 'boolean', description: 'Fetch transactions from freee API instead of providing them', default: false },
|
|
153
|
+
},
|
|
154
|
+
required: ['month'],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'list_freee_deals',
|
|
159
|
+
description: 'List transactions (取引) from freee API for the configured company. Useful for sync + classification dogfood.',
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
type: { type: 'string', enum: ['income', 'expense'], description: 'Filter by income or expense' },
|
|
164
|
+
start_issue_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
165
|
+
end_issue_date: { type: 'string', description: 'YYYY-MM-DD' },
|
|
166
|
+
limit: { type: 'number', default: 20 },
|
|
167
|
+
offset: { type: 'number', default: 0 },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'list_freee_companies',
|
|
173
|
+
description: 'List all companies (事業所) accessible by the configured freee OAuth token. Returns company IDs + names. Used for multi-company batch processing.',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'freee_doctor',
|
|
181
|
+
description: 'Diagnose the freee connection in ONE shot. Checks: (1) where the access token is sourced (FREEE_ACCESS_TOKEN env vs secrets file), (2) whether the token is expired (24h TTL, no auto-refresh), (3) lists ALL 事業所 the token can access and flags whether your configured company_id is live — fixes the "which of my test companies is the real one?" problem. Run this FIRST whenever a freee call fails.',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'reconcile_cross_saas',
|
|
189
|
+
description: 'Cross-SaaS reconciliation (= freee ↔ MF). Currently freee-only mode (= MF connector pending Phase 1.B). Detects duplicate fingerprints within freee.',
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
period_start: { type: 'string' },
|
|
194
|
+
period_end: { type: 'string' },
|
|
195
|
+
},
|
|
196
|
+
required: ['period_start', 'period_end'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'check_duplicate',
|
|
201
|
+
description: 'Check if a transaction already exists in freee (= by date + amount + memo prefix). Use BEFORE register to prevent double-posting.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
date: { type: 'string' },
|
|
206
|
+
amount: { type: 'number' },
|
|
207
|
+
memo: { type: 'string' },
|
|
208
|
+
},
|
|
209
|
+
required: ['date', 'amount', 'memo'],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'upsert_partner',
|
|
214
|
+
description: '取引先マスタ auto-creation in freee. Fuzzy match against existing partners; if new, create. Returns partner_id.',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
partner_name: { type: 'string' },
|
|
219
|
+
},
|
|
220
|
+
required: ['partner_name'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'correct_classification',
|
|
225
|
+
description: '税理士修正フィードバック。誤分類を記録し、同パターンの取引が今後来たら修正後の勘定科目を自動適用。Linksee Memory caveat layer と同等(= 永続記憶、二度と同じ誤りをしない)。修正は全社共通 or 特定会社のみに適用可能。',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
memo_pattern: { type: 'string', description: 'この修正が適用される摘要パターン(例: "スターバックス 渋谷")' },
|
|
230
|
+
partner_name: { type: 'string', description: '取引先名(optional — memo_pattern だけでもOK)' },
|
|
231
|
+
from_category_id: { type: 'string', description: '誤分類だった勘定科目ID(optional)' },
|
|
232
|
+
from_category_name_ja: { type: 'string', description: '誤分類だった勘定科目名(optional)' },
|
|
233
|
+
to_category_id: { type: 'string', description: '正しい勘定科目ID' },
|
|
234
|
+
to_category_name_ja: { type: 'string', description: '正しい勘定科目名' },
|
|
235
|
+
reason: { type: 'string', description: '修正理由(例: "1人利用・5,000円以下は会議費")' },
|
|
236
|
+
company_id: { type: 'number', description: '特定会社のみに適用(省略 = 全社共通)' },
|
|
237
|
+
},
|
|
238
|
+
required: ['memo_pattern', 'to_category_id', 'to_category_name_ja', 'reason'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'recall_memory',
|
|
243
|
+
description: '過去の分類パターン・修正履歴を検索。取引の摘要/取引先から過去パターンを参照し、分類結果の根拠を確認。Memory stats(pattern hit率等)の確認にも使用。',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
memo: { type: 'string', description: '取引摘要(パターン検索用)' },
|
|
248
|
+
partner_name: { type: 'string', description: '取引先名(optional)' },
|
|
249
|
+
amount: { type: 'number', description: '金額(±5% 範囲でマッチ)' },
|
|
250
|
+
show_stats: { type: 'boolean', description: 'Memory 全体統計を表示', default: false },
|
|
251
|
+
show_corrections: { type: 'boolean', description: '全修正履歴を表示', default: false },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'nightly_run',
|
|
257
|
+
description: 'Nightly batch pipeline. Processes ALL companies accessible by the token (= multi-company batch). Pipeline per company: fetch unprocessed (status=unsettled) → Stage 0 exclusion → Stage 1+2 classify → confidence routing (high=auto, medium=auto+log, low=human_review) → aggregate summary. Currently dry-run only (write-back pending Phase 1.B).',
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: {
|
|
261
|
+
dry_run: { type: 'boolean', default: true, description: 'Currently always dry_run (= write-back pending Phase 1.B)' },
|
|
262
|
+
company_ids: { type: 'array', items: { type: 'number' }, description: 'Override: process only these company IDs (default: all accessible)' },
|
|
263
|
+
concurrency: { type: 'number', default: 3, description: 'Max parallel companies (default: 3, freee rate limit safe)' },
|
|
264
|
+
deals_per_company: { type: 'number', default: 100, description: 'Max deals fetched per company' },
|
|
265
|
+
period_start: { type: 'string', description: 'YYYY-MM-DD start date filter' },
|
|
266
|
+
period_end: { type: 'string', description: 'YYYY-MM-DD end date filter' },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
// ============================================================
|
|
272
|
+
// Handlers
|
|
273
|
+
// ============================================================
|
|
274
|
+
async function handleClassifyTransaction(args) {
|
|
275
|
+
const tx = {
|
|
276
|
+
amount: args.amount,
|
|
277
|
+
memo: args.memo,
|
|
278
|
+
date: args.date,
|
|
279
|
+
partner_name: args.partner_name,
|
|
280
|
+
};
|
|
281
|
+
const result = await classifier.classify(tx);
|
|
282
|
+
return JSON.stringify({ ok: true, ...result }, null, 2);
|
|
283
|
+
}
|
|
284
|
+
function handleCheckExclusion(args) {
|
|
285
|
+
const tx = {
|
|
286
|
+
amount: args.amount,
|
|
287
|
+
memo: args.memo,
|
|
288
|
+
date: args.date || '',
|
|
289
|
+
partner_name: args.partner_name,
|
|
290
|
+
};
|
|
291
|
+
const result = exclusion.check(tx, args.employees);
|
|
292
|
+
return JSON.stringify({ ok: true, ...result }, null, 2);
|
|
293
|
+
}
|
|
294
|
+
async function handleImportCsv(args) {
|
|
295
|
+
const csvContent = String(args.csv_content || '');
|
|
296
|
+
if (!csvContent.trim()) {
|
|
297
|
+
return JSON.stringify({ ok: false, error: 'csv_content is empty' }, null, 2);
|
|
298
|
+
}
|
|
299
|
+
const mapping = args.date_column && args.amount_column && args.memo_column
|
|
300
|
+
? {
|
|
301
|
+
date: args.date_column,
|
|
302
|
+
amount: args.amount_column,
|
|
303
|
+
memo: args.memo_column,
|
|
304
|
+
partner_name: args.partner_column,
|
|
305
|
+
}
|
|
306
|
+
: undefined;
|
|
307
|
+
const result = await importCsv(csvContent, classifier, exclusion, confidenceRouter, {
|
|
308
|
+
source: args.source,
|
|
309
|
+
mapping,
|
|
310
|
+
memory,
|
|
311
|
+
taxRuleEngine,
|
|
312
|
+
});
|
|
313
|
+
// Trim per-transaction details for response size management.
|
|
314
|
+
// Keep summary + review items + markdown report.
|
|
315
|
+
return JSON.stringify({
|
|
316
|
+
ok: result.ok,
|
|
317
|
+
source: result.source,
|
|
318
|
+
source_label: result.source_label,
|
|
319
|
+
total_rows: result.total_rows,
|
|
320
|
+
parsed_count: result.parsed_count,
|
|
321
|
+
skipped_count: result.skipped_count,
|
|
322
|
+
warnings: result.warnings.slice(0, 20),
|
|
323
|
+
summary: result.summary,
|
|
324
|
+
review_queue: result.human_review.slice(0, 30).map(ct => ({
|
|
325
|
+
row: ct.row_number,
|
|
326
|
+
date: ct.transaction.date,
|
|
327
|
+
amount: ct.transaction.amount,
|
|
328
|
+
memo: ct.transaction.memo,
|
|
329
|
+
flags: ct.routing_flags,
|
|
330
|
+
})),
|
|
331
|
+
excluded_sample: result.excluded.slice(0, 10).map(ct => ({
|
|
332
|
+
row: ct.row_number,
|
|
333
|
+
memo: ct.transaction.memo,
|
|
334
|
+
rule: ct.exclusion_rule,
|
|
335
|
+
})),
|
|
336
|
+
auto_sample: result.auto_register.slice(0, 10).map(ct => ({
|
|
337
|
+
row: ct.row_number,
|
|
338
|
+
memo: ct.transaction.memo,
|
|
339
|
+
category: ct.category_name_ja,
|
|
340
|
+
confidence: ct.confidence,
|
|
341
|
+
})),
|
|
342
|
+
markdown_report: result.markdown_report,
|
|
343
|
+
csv_output_preview: result.csv_output?.split('\n').slice(0, 5).join('\n'),
|
|
344
|
+
}, null, 2);
|
|
345
|
+
}
|
|
346
|
+
async function handleGenerateMonthlyReport(args) {
|
|
347
|
+
const month = String(args.month || '');
|
|
348
|
+
if (!month.match(/^\d{4}-\d{2}$/)) {
|
|
349
|
+
return JSON.stringify({ ok: false, error: 'month must be YYYY-MM format' }, null, 2);
|
|
350
|
+
}
|
|
351
|
+
let transactions = args.transactions || [];
|
|
352
|
+
// If use_freee = true and no transactions provided, fetch from freee
|
|
353
|
+
if (args.use_freee && transactions.length === 0) {
|
|
354
|
+
try {
|
|
355
|
+
const conn = getFreeeConnector();
|
|
356
|
+
const [year, mon] = month.split('-');
|
|
357
|
+
const startDate = `${year}-${mon}-01`;
|
|
358
|
+
// Calculate last day of month
|
|
359
|
+
const lastDay = new Date(parseInt(year), parseInt(mon), 0).getDate();
|
|
360
|
+
const endDate = `${year}-${mon}-${String(lastDay).padStart(2, '0')}`;
|
|
361
|
+
const deals = await conn.listDeals({
|
|
362
|
+
start_issue_date: startDate,
|
|
363
|
+
end_issue_date: endDate,
|
|
364
|
+
limit: 500,
|
|
365
|
+
});
|
|
366
|
+
transactions = deals.map(d => ({
|
|
367
|
+
amount: d.amount,
|
|
368
|
+
memo: d.memo || d.description || d.details?.[0]?.description || d.ref_number || '',
|
|
369
|
+
date: d.issue_date,
|
|
370
|
+
partner_name: d.partner_name,
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
return JSON.stringify({
|
|
375
|
+
ok: false,
|
|
376
|
+
error: `Failed to fetch from freee: ${err.message}. Provide transactions directly or fix freee connection.`,
|
|
377
|
+
}, null, 2);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (transactions.length === 0) {
|
|
381
|
+
return JSON.stringify({
|
|
382
|
+
ok: false,
|
|
383
|
+
error: 'No transactions provided. Either pass transactions array or set use_freee=true.',
|
|
384
|
+
}, null, 2);
|
|
385
|
+
}
|
|
386
|
+
const compareTransactions = args.compare_transactions || [];
|
|
387
|
+
const result = await generateMonthlyReport(transactions, classifier, exclusion, confidenceRouter, {
|
|
388
|
+
company_name: args.company_name,
|
|
389
|
+
month,
|
|
390
|
+
compare_transactions: compareTransactions.length > 0 ? compareTransactions : undefined,
|
|
391
|
+
compare_label: args.compare_label || '前月',
|
|
392
|
+
format: args.format || 'markdown',
|
|
393
|
+
});
|
|
394
|
+
// Return structured result with markdown
|
|
395
|
+
return JSON.stringify({
|
|
396
|
+
ok: result.ok,
|
|
397
|
+
company_name: result.company_name,
|
|
398
|
+
month: result.month,
|
|
399
|
+
total_transactions: result.total_transactions,
|
|
400
|
+
total_expense: result.total_expense,
|
|
401
|
+
classification_rate: result.classification_rate,
|
|
402
|
+
auto_register_rate: result.auto_register_rate,
|
|
403
|
+
anomaly_count: result.anomalies.length,
|
|
404
|
+
anomalies: result.anomalies,
|
|
405
|
+
review_count: result.review_items.length,
|
|
406
|
+
category_summary: result.categories.map(c => ({
|
|
407
|
+
category: c.category_name_ja,
|
|
408
|
+
count: c.count,
|
|
409
|
+
total: c.total_amount,
|
|
410
|
+
})),
|
|
411
|
+
markdown: result.markdown,
|
|
412
|
+
}, null, 2);
|
|
413
|
+
}
|
|
414
|
+
function handleCorrectClassification(args) {
|
|
415
|
+
const record = memory.rememberCorrection({
|
|
416
|
+
memo_pattern: args.memo_pattern,
|
|
417
|
+
partner_name: args.partner_name,
|
|
418
|
+
from_category_id: args.from_category_id,
|
|
419
|
+
from_category_name_ja: args.from_category_name_ja,
|
|
420
|
+
to_category_id: args.to_category_id,
|
|
421
|
+
to_category_name_ja: args.to_category_name_ja,
|
|
422
|
+
reason: args.reason,
|
|
423
|
+
company_id: args.company_id,
|
|
424
|
+
});
|
|
425
|
+
memory.save();
|
|
426
|
+
return JSON.stringify({
|
|
427
|
+
ok: true,
|
|
428
|
+
correction_id: record.id,
|
|
429
|
+
partner_key: record.partner_key,
|
|
430
|
+
memo_pattern: record.memo_pattern,
|
|
431
|
+
from: record.from_category_name_ja || '(不明)',
|
|
432
|
+
to: record.to_category_name_ja,
|
|
433
|
+
reason: record.reason,
|
|
434
|
+
scope: record.company_id ? `Company ${record.company_id} のみ` : '全社共通',
|
|
435
|
+
note: 'この修正は今後の分類で自動適用されます(caveat layer = 永続記憶)',
|
|
436
|
+
stats: memory.getStats(),
|
|
437
|
+
}, null, 2);
|
|
438
|
+
}
|
|
439
|
+
function handleRecallMemory(args) {
|
|
440
|
+
const result = { ok: true };
|
|
441
|
+
// Pattern recall
|
|
442
|
+
if (args.memo || args.partner_name) {
|
|
443
|
+
const tx = {
|
|
444
|
+
amount: args.amount || 0,
|
|
445
|
+
memo: args.memo || '',
|
|
446
|
+
date: new Date().toISOString().slice(0, 10),
|
|
447
|
+
partner_name: args.partner_name,
|
|
448
|
+
};
|
|
449
|
+
const recall = memory.recallPattern(tx);
|
|
450
|
+
result.recall = recall;
|
|
451
|
+
}
|
|
452
|
+
// Stats
|
|
453
|
+
if (args.show_stats) {
|
|
454
|
+
result.stats = memory.getStats();
|
|
455
|
+
result.total_patterns = memory.getPatternCount();
|
|
456
|
+
result.total_corrections = memory.getCorrectionCount();
|
|
457
|
+
}
|
|
458
|
+
// Corrections list
|
|
459
|
+
if (args.show_corrections) {
|
|
460
|
+
result.corrections = memory.getCorrections();
|
|
461
|
+
}
|
|
462
|
+
return JSON.stringify(result, null, 2);
|
|
463
|
+
}
|
|
464
|
+
async function handleListFreeeDeals(args) {
|
|
465
|
+
const conn = getFreeeConnector();
|
|
466
|
+
const deals = await conn.listDeals({
|
|
467
|
+
type: args.type,
|
|
468
|
+
start_issue_date: args.start_issue_date,
|
|
469
|
+
end_issue_date: args.end_issue_date,
|
|
470
|
+
limit: args.limit,
|
|
471
|
+
offset: args.offset,
|
|
472
|
+
});
|
|
473
|
+
return JSON.stringify({
|
|
474
|
+
ok: true,
|
|
475
|
+
company_id: conn.companyId,
|
|
476
|
+
company_name: conn.companyName,
|
|
477
|
+
count: deals.length,
|
|
478
|
+
deals: deals.map(d => ({
|
|
479
|
+
id: d.id,
|
|
480
|
+
date: d.issue_date,
|
|
481
|
+
type: d.type,
|
|
482
|
+
amount: d.amount,
|
|
483
|
+
partner_id: d.partner_id,
|
|
484
|
+
partner_name: d.partner_name,
|
|
485
|
+
ref_number: d.ref_number,
|
|
486
|
+
description: d.description,
|
|
487
|
+
memo: d.memo,
|
|
488
|
+
status: d.status,
|
|
489
|
+
})),
|
|
490
|
+
}, null, 2);
|
|
491
|
+
}
|
|
492
|
+
async function handleReconcileCrossSaas(args) {
|
|
493
|
+
// MF connector is not yet implemented (= Phase 1.B). For now, return placeholder.
|
|
494
|
+
return JSON.stringify({
|
|
495
|
+
ok: false,
|
|
496
|
+
error: 'NOT_IMPLEMENTED',
|
|
497
|
+
todo: 'MF connector pending Phase 1.B. Current implementation will detect freee-internal duplicates only.',
|
|
498
|
+
}, null, 2);
|
|
499
|
+
}
|
|
500
|
+
async function handleCheckDuplicate(args) {
|
|
501
|
+
const conn = getFreeeConnector();
|
|
502
|
+
// Fetch recent deals around the given date, then match by fingerprint
|
|
503
|
+
const target = {
|
|
504
|
+
date: args.date,
|
|
505
|
+
amount: args.amount,
|
|
506
|
+
memo: String(args.memo || '').slice(0, 40),
|
|
507
|
+
};
|
|
508
|
+
// Look ±7 days
|
|
509
|
+
const dateObj = new Date(args.date);
|
|
510
|
+
const startDate = new Date(dateObj.getTime() - 7 * 86400 * 1000).toISOString().slice(0, 10);
|
|
511
|
+
const endDate = new Date(dateObj.getTime() + 7 * 86400 * 1000).toISOString().slice(0, 10);
|
|
512
|
+
const deals = await conn.listDeals({ start_issue_date: startDate, end_issue_date: endDate, limit: 100 });
|
|
513
|
+
const matches = deals.filter(d => d.issue_date === target.date &&
|
|
514
|
+
Math.abs(d.amount - target.amount) < 1 &&
|
|
515
|
+
(d.memo || '').slice(0, 40) === target.memo);
|
|
516
|
+
return JSON.stringify({
|
|
517
|
+
ok: true,
|
|
518
|
+
duplicate_found: matches.length > 0,
|
|
519
|
+
match_count: matches.length,
|
|
520
|
+
matches: matches.map(m => ({ id: m.id, date: m.issue_date, amount: m.amount })),
|
|
521
|
+
}, null, 2);
|
|
522
|
+
}
|
|
523
|
+
async function handleUpsertPartner(args) {
|
|
524
|
+
const conn = getFreeeConnector();
|
|
525
|
+
const partners = await conn.listPartners({ limit: 100 });
|
|
526
|
+
const target = String(args.partner_name || '').trim();
|
|
527
|
+
const existing = partners.find(p => p.name === target);
|
|
528
|
+
if (existing) {
|
|
529
|
+
return JSON.stringify({
|
|
530
|
+
ok: true,
|
|
531
|
+
action: 'found_existing',
|
|
532
|
+
partner_id: existing.id,
|
|
533
|
+
partner_name: existing.name,
|
|
534
|
+
}, null, 2);
|
|
535
|
+
}
|
|
536
|
+
// POST /partners not implemented yet (= write deferred to Phase 1.B)
|
|
537
|
+
return JSON.stringify({
|
|
538
|
+
ok: false,
|
|
539
|
+
error: 'WRITE_NOT_IMPLEMENTED',
|
|
540
|
+
todo: 'POST /partners write deferred to Phase 1.B. Currently read-only fuzzy match.',
|
|
541
|
+
suggested_name: target,
|
|
542
|
+
}, null, 2);
|
|
543
|
+
}
|
|
544
|
+
async function handleListFreeeCompanies() {
|
|
545
|
+
const conn = getFreeeConnector();
|
|
546
|
+
const companies = await conn.listCompanies();
|
|
547
|
+
return JSON.stringify({
|
|
548
|
+
ok: true,
|
|
549
|
+
count: companies.length,
|
|
550
|
+
companies: companies.map(c => ({
|
|
551
|
+
id: c.id,
|
|
552
|
+
display_name: c.display_name,
|
|
553
|
+
contact_name: c.contact_name,
|
|
554
|
+
fiscal_yearmonth: c.fiscal_yearmonth,
|
|
555
|
+
})),
|
|
556
|
+
}, null, 2);
|
|
557
|
+
}
|
|
558
|
+
async function handleFreeeDoctor() {
|
|
559
|
+
const report = await runFreeeDoctor();
|
|
560
|
+
return JSON.stringify(report, null, 2);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Nightly batch pipeline for multi-company processing.
|
|
564
|
+
*
|
|
565
|
+
* Replaces the old single-company loop with the full NightlyPipeline:
|
|
566
|
+
* - Multi-company support (list_companies → parallel processing)
|
|
567
|
+
* - Unprocessed filter (status = 'unsettled')
|
|
568
|
+
* - Confidence routing (high/medium/low → auto/auto+log/human)
|
|
569
|
+
* - Business rule guards (100万超/新規取引先/月次決算期間)
|
|
570
|
+
* - Batch summary (Slack-ready format)
|
|
571
|
+
*/
|
|
572
|
+
async function handleNightlyRun(args) {
|
|
573
|
+
const conn = getFreeeConnector();
|
|
574
|
+
const pipeline = new NightlyPipeline(conn, classifier, exclusion, {
|
|
575
|
+
dry_run: args.dry_run !== false, // default true
|
|
576
|
+
company_ids: args.company_ids,
|
|
577
|
+
concurrency: args.concurrency ?? 3,
|
|
578
|
+
deals_per_company: args.deals_per_company ?? 100,
|
|
579
|
+
period_start: args.period_start,
|
|
580
|
+
period_end: args.period_end,
|
|
581
|
+
}, memory);
|
|
582
|
+
const result = await pipeline.run();
|
|
583
|
+
// Return the full result but trim per-transaction details to keep response manageable.
|
|
584
|
+
// Full review_queue and sample are included for each company.
|
|
585
|
+
return JSON.stringify(result, null, 2);
|
|
586
|
+
}
|
|
587
|
+
// ============================================================
|
|
588
|
+
// MCP wiring
|
|
589
|
+
// ============================================================
|
|
590
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
591
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
592
|
+
const { name, arguments: args } = req.params;
|
|
593
|
+
try {
|
|
594
|
+
let text;
|
|
595
|
+
switch (name) {
|
|
596
|
+
case 'classify_transaction':
|
|
597
|
+
text = await handleClassifyTransaction(args);
|
|
598
|
+
break;
|
|
599
|
+
case 'check_exclusion':
|
|
600
|
+
text = handleCheckExclusion(args);
|
|
601
|
+
break;
|
|
602
|
+
case 'import_csv':
|
|
603
|
+
text = await handleImportCsv(args);
|
|
604
|
+
break;
|
|
605
|
+
case 'generate_monthly_report':
|
|
606
|
+
text = await handleGenerateMonthlyReport(args);
|
|
607
|
+
break;
|
|
608
|
+
case 'list_freee_deals':
|
|
609
|
+
text = await handleListFreeeDeals(args);
|
|
610
|
+
break;
|
|
611
|
+
case 'list_freee_companies':
|
|
612
|
+
text = await handleListFreeeCompanies();
|
|
613
|
+
break;
|
|
614
|
+
case 'freee_doctor':
|
|
615
|
+
text = await handleFreeeDoctor();
|
|
616
|
+
break;
|
|
617
|
+
case 'reconcile_cross_saas':
|
|
618
|
+
text = await handleReconcileCrossSaas(args);
|
|
619
|
+
break;
|
|
620
|
+
case 'check_duplicate':
|
|
621
|
+
text = await handleCheckDuplicate(args);
|
|
622
|
+
break;
|
|
623
|
+
case 'upsert_partner':
|
|
624
|
+
text = await handleUpsertPartner(args);
|
|
625
|
+
break;
|
|
626
|
+
case 'correct_classification':
|
|
627
|
+
text = handleCorrectClassification(args);
|
|
628
|
+
break;
|
|
629
|
+
case 'recall_memory':
|
|
630
|
+
text = handleRecallMemory(args);
|
|
631
|
+
break;
|
|
632
|
+
case 'nightly_run':
|
|
633
|
+
text = await handleNightlyRun(args);
|
|
634
|
+
break;
|
|
635
|
+
default: throw new Error(`Unknown tool: ${name}`);
|
|
636
|
+
}
|
|
637
|
+
return { content: [{ type: 'text', text }] };
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
return {
|
|
641
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err?.message ?? String(err) }) }],
|
|
642
|
+
isError: true,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
const transport = new StdioServerTransport();
|
|
647
|
+
await server.connect(transport);
|
|
648
|
+
console.error(`[@kansei-link/cockpit] MCP server ready on stdio (v${SERVER_VERSION})`);
|
|
649
|
+
console.error(` Stage 1: ${keywordClassifier.getCategoriesCount()} categories, ${keywordClassifier.getKeywordsCount()} keywords (v${keywordClassifier.getVersion()})`);
|
|
650
|
+
console.error(` Stage 2: ${claudeClassifier ? `enabled (${claudeClassifier.getModel()})` : 'disabled (set ANTHROPIC_API_KEY to enable)'}`);
|
|
651
|
+
console.error(` Exclusion: ${exclusion.getRulesCount()} rules (v${exclusion.getVersion()})`);
|
|
652
|
+
console.error(` Pipeline: Multi-company batch + confidence routing enabled`);
|
|
653
|
+
console.error(` CSV Import: 弥生/freee/MF/generic auto-detect enabled`);
|
|
654
|
+
console.error(` Reports: monthly review report generation enabled`);
|
|
655
|
+
console.error(` Memory: ${memory.getPatternCount()} patterns, ${memory.getCorrectionCount()} corrections loaded`);
|
|
656
|
+
//# sourceMappingURL=index.js.map
|