Merge BAT-017 / REQ-021 implementation after delivery-runner implementation, independent QC, and passing GitHub Actions CI.
421 lines
24 KiB
Python
421 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from dotenv import load_dotenv
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
from . import tool_contracts as contracts
|
|
|
|
load_dotenv(Path(__file__).resolve().parents[3] / ".env")
|
|
|
|
McpTransport = Literal["stdio", "sse", "streamable-http"]
|
|
_ALLOWED_TRANSPORTS: set[str] = {"stdio", "sse", "streamable-http"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class McpRuntimeConfig:
|
|
transport: McpTransport = "stdio"
|
|
host: str = "127.0.0.1"
|
|
port: int = 8000
|
|
streamable_http_path: str = "/mcp"
|
|
sse_path: str = "/sse"
|
|
message_path: str = "/messages/"
|
|
stateless_http: bool = False
|
|
|
|
|
|
def _env_bool(name: str, default: bool = False) -> bool:
|
|
raw = os.getenv(name)
|
|
if raw is None or raw == "":
|
|
return default
|
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def _env_int(name: str, default: int) -> int:
|
|
raw = os.getenv(name)
|
|
if raw is None or raw == "":
|
|
return default
|
|
try:
|
|
return int(raw)
|
|
except ValueError as exc:
|
|
raise ValueError(f"{name} must be an integer, got {raw!r}") from exc
|
|
|
|
|
|
def _env_path(name: str, default: str) -> str:
|
|
raw = os.getenv(name, default).strip() or default
|
|
return raw if raw.startswith("/") else f"/{raw}"
|
|
|
|
|
|
def get_mcp_runtime_config() -> McpRuntimeConfig:
|
|
transport = os.getenv("JARCA_MCP_TRANSPORT", "stdio").strip() or "stdio"
|
|
if transport not in _ALLOWED_TRANSPORTS:
|
|
allowed = ", ".join(sorted(_ALLOWED_TRANSPORTS))
|
|
raise ValueError(f"JARCA_MCP_TRANSPORT must be one of: {allowed}; got {transport!r}")
|
|
return McpRuntimeConfig(
|
|
transport=transport, # type: ignore[arg-type]
|
|
host=os.getenv("JARCA_MCP_HOST", "127.0.0.1").strip() or "127.0.0.1",
|
|
port=_env_int("JARCA_MCP_PORT", 8000),
|
|
streamable_http_path=_env_path("JARCA_MCP_STREAMABLE_HTTP_PATH", "/mcp"),
|
|
sse_path=_env_path("JARCA_MCP_SSE_PATH", "/sse"),
|
|
message_path=_env_path("JARCA_MCP_MESSAGE_PATH", "/messages/"),
|
|
stateless_http=_env_bool("JARCA_MCP_STATELESS_HTTP", False),
|
|
)
|
|
|
|
|
|
_runtime_config = get_mcp_runtime_config()
|
|
mcp = FastMCP(
|
|
"jarca-ledger-mcp",
|
|
host=_runtime_config.host,
|
|
port=_runtime_config.port,
|
|
streamable_http_path=_runtime_config.streamable_http_path,
|
|
sse_path=_runtime_config.sse_path,
|
|
message_path=_runtime_config.message_path,
|
|
stateless_http=_runtime_config.stateless_http,
|
|
)
|
|
|
|
@mcp.tool()
|
|
def ledger_health_check() -> dict:
|
|
"""Verify the Jarca Ledger MCP server and SQLite database are reachable."""
|
|
return contracts.health_check()
|
|
|
|
@mcp.tool()
|
|
def get_ledger_db_status(include_hash: bool = True, include_integrity: bool = True) -> dict:
|
|
"""Return non-sensitive DB status evidence: path, size, mtime, checksum, table row counts, and aggregate totals."""
|
|
return contracts.get_ledger_db_status(include_hash=include_hash, include_integrity=include_integrity)
|
|
|
|
@mcp.tool()
|
|
def create_ledger_backup(backup_dir: str = "", retention_days: int = 30, retention_count: int = 20) -> dict:
|
|
"""Create a timestamped ledger DB backup. Restore is intentionally not exposed as an MCP tool."""
|
|
return contracts.create_ledger_backup(backup_dir=backup_dir or None, retention_days=retention_days, retention_count=retention_count)
|
|
|
|
@mcp.tool()
|
|
def list_ledger_backups(backup_dir: str = "", include_hash: bool = False, limit: int = 50) -> dict:
|
|
"""List ledger DB backups with metadata-only evidence."""
|
|
return contracts.list_ledger_backups(backup_dir=backup_dir or None, include_hash=include_hash, limit=limit)
|
|
|
|
@mcp.tool()
|
|
def get_ledger_backup_status(file_path: str, include_hash: bool = True, include_integrity: bool = True) -> dict:
|
|
"""Return metadata-only status and integrity evidence for one backup file."""
|
|
return contracts.get_ledger_backup_status(file_path, include_hash=include_hash, include_integrity=include_integrity)
|
|
|
|
@mcp.tool()
|
|
def create_ledger_account(name: str, type: str = "bank", institution: str = "", currency: str = "KRW", active: bool = True, memo: str = "", account_number_masked: str = "", holder_name: str = "", last_balance: int | None = None, last_synced_at: str = "", statement_status: str = "unknown", liquidity_bucket: str = "other") -> dict:
|
|
"""Create an account/card/cash/investment/loan ledger account."""
|
|
return contracts.create_ledger_account(name, type, institution, currency, active, memo, account_number_masked, holder_name, last_balance, last_synced_at, statement_status, liquidity_bucket)
|
|
|
|
@mcp.tool()
|
|
def list_ledger_accounts(active_only: bool = True) -> dict:
|
|
"""List ledger accounts and cards."""
|
|
return contracts.list_ledger_accounts(active_only)
|
|
|
|
@mcp.tool()
|
|
def update_ledger_account(account_id: str, name: str | None = None, type: str | None = None, institution: str | None = None, currency: str | None = None, active: bool | None = None, memo: str | None = None, account_number_masked: str | None = None, holder_name: str | None = None, last_balance: int | None = None, last_synced_at: str | None = None, statement_status: str | None = None, liquidity_bucket: str | None = None) -> dict:
|
|
"""Update selected account fields, including statement_status/liquidity_bucket for manual reconciliation."""
|
|
return contracts.update_ledger_account(account_id, name=name, type=type, institution=institution, currency=currency, active=active, memo=memo, account_number_masked=account_number_masked, holder_name=holder_name, last_balance=last_balance, last_synced_at=last_synced_at, statement_status=statement_status, liquidity_bucket=liquidity_bucket)
|
|
|
|
@mcp.tool()
|
|
def get_asset_liquidity_report() -> dict:
|
|
"""Return asset liquidity, debt, and net worth from shared account/manual valuation sources."""
|
|
return contracts.get_asset_liquidity_report()
|
|
|
|
@mcp.tool()
|
|
def list_manual_assets(active_only: bool = True) -> dict:
|
|
"""List manual non-transaction assets/liabilities used for net-worth valuation."""
|
|
return contracts.list_manual_assets(active_only=active_only)
|
|
|
|
@mcp.tool()
|
|
def create_manual_asset(name: str, valuation_amount: int, valuation_date: str, liquidity_bucket: str, source_note: str, memo: str = "", currency: str = "KRW", active: bool = True) -> dict:
|
|
"""Create a manual non-transaction asset/liability valuation row."""
|
|
return contracts.create_manual_asset(name, valuation_amount, valuation_date, liquidity_bucket, source_note=source_note, memo=memo, currency=currency, active=active)
|
|
|
|
@mcp.tool()
|
|
def update_manual_asset(asset_id: str, name: str | None = None, valuation_amount: int | None = None, valuation_date: str | None = None, liquidity_bucket: str | None = None, source_note: str | None = None, memo: str | None = None, currency: str | None = None, active: bool | None = None) -> dict:
|
|
"""Update selected fields on a manual asset/liability valuation row."""
|
|
return contracts.update_manual_asset(asset_id, name=name, valuation_amount=valuation_amount, valuation_date=valuation_date, liquidity_bucket=liquidity_bucket, source_note=source_note, memo=memo, currency=currency, active=active)
|
|
|
|
@mcp.tool()
|
|
def deactivate_manual_asset(asset_id: str, hard_delete: bool = False) -> dict:
|
|
"""Deactivate a manual asset/liability by default; hard delete only for exceptional cleanup."""
|
|
return contracts.deactivate_manual_asset(asset_id, hard_delete=hard_delete)
|
|
|
|
@mcp.tool()
|
|
def preview_asset_snapshot_close(basis_date: str = "") -> dict:
|
|
"""Preview a month-end asset snapshot close without mutating the database."""
|
|
return contracts.preview_asset_snapshot_close(basis_date=basis_date)
|
|
|
|
@mcp.tool()
|
|
def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str = "", correction_reason: str = "") -> dict:
|
|
"""Close an asset snapshot for a basis date; same-date re-close appends correction rows."""
|
|
return contracts.close_asset_snapshot(basis_date=basis_date, source_note=source_note, memo=memo, correction_reason=correction_reason)
|
|
|
|
@mcp.tool()
|
|
def get_asset_snapshot_report(basis_date: str = "") -> dict:
|
|
"""Return basis-date asset snapshot/net-worth report with live-authority fallback."""
|
|
return contracts.get_asset_snapshot_report(basis_date=basis_date)
|
|
|
|
@mcp.tool()
|
|
def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: int = 12) -> dict:
|
|
"""Return net-worth timeseries from stored asset snapshot rows."""
|
|
return contracts.get_net_worth_timeseries(start_date=start_date, end_date=end_date, months=months)
|
|
|
|
@mcp.tool()
|
|
def get_balance_reconciliation_report(account_id: str = "", baseline_balance: int | None = None, baseline_at: str = "", source_note: str = "", memo: str = "", tolerance: int = 1000) -> dict:
|
|
"""Preview balance reconciliation for manual_only/partial_import accounts without mutating the DB."""
|
|
return contracts.get_balance_reconciliation_report(account_id, baseline_balance, baseline_at, source_note, memo, tolerance)
|
|
|
|
@mcp.tool()
|
|
def list_review_decisions(target_type: str = "", target_id: str = "", status: str = "", limit: int = 100) -> dict:
|
|
"""List persisted review decisions for workbench targets."""
|
|
return contracts.list_review_decisions(target_type=target_type, target_id=target_id, status=status, limit=limit)
|
|
|
|
@mcp.tool()
|
|
def upsert_review_decision(target_type: str, target_id: str, status: str = "decided", decision_type: str = "unknown", reason_code: str = "", memo: str = "", operator: str = "operator", review_after: str = "", target_payload: dict | None = None, decision_id: str = "") -> dict:
|
|
"""Create or update a non-destructive persisted review decision."""
|
|
return contracts.upsert_review_decision(target_type, target_id, status, decision_type, reason_code, memo, operator, review_after, target_payload, decision_id)
|
|
|
|
@mcp.tool()
|
|
def hold_review_decision(target_type: str, target_id: str, reason_code: str = "held", memo: str = "", operator: str = "operator", review_after: str = "") -> dict:
|
|
"""Mark a review target as held without deleting evidence."""
|
|
return contracts.hold_review_decision(target_type, target_id, reason_code, memo, operator, review_after)
|
|
|
|
@mcp.tool()
|
|
def mark_review_recheck(target_type: str, target_id: str, reason_code: str = "needs_recheck", memo: str = "", operator: str = "operator", review_after: str = "") -> dict:
|
|
"""Mark a review target as needing recheck."""
|
|
return contracts.mark_review_recheck(target_type, target_id, reason_code, memo, operator, review_after)
|
|
|
|
@mcp.tool()
|
|
def release_review_decision(target_type: str, target_id: str, memo: str = "", operator: str = "operator") -> dict:
|
|
"""Release a held/recheck review target back to pending."""
|
|
return contracts.release_review_decision(target_type, target_id, memo, operator)
|
|
|
|
@mcp.tool()
|
|
def list_reconciliation_sessions(account_id: str = "", status: str = "", basis_date: str = "", limit: int = 100) -> dict:
|
|
"""List persisted reconciliation sessions."""
|
|
return contracts.list_reconciliation_sessions(account_id=account_id, status=status, basis_date=basis_date, limit=limit)
|
|
|
|
@mcp.tool()
|
|
def upsert_reconciliation_session(account_id: str, basis_date: str, expected_balance: int | None = None, actual_balance: int | None = None, difference: int | None = None, status: str = "open", source_note: str = "", memo: str = "", operator: str = "operator", session_id: str = "") -> dict:
|
|
"""Create or update an account/basis-date reconciliation session."""
|
|
return contracts.upsert_reconciliation_session(account_id, basis_date, expected_balance, actual_balance, difference, status, source_note, memo, operator, session_id)
|
|
|
|
@mcp.tool()
|
|
def close_reconciliation_session(session_id: str = "", account_id: str = "", basis_date: str = "", memo: str = "", operator: str = "operator") -> dict:
|
|
"""Close an existing reconciliation session without destructive reset/delete."""
|
|
return contracts.close_reconciliation_session(session_id=session_id, account_id=account_id, basis_date=basis_date, memo=memo, operator=operator)
|
|
|
|
@mcp.tool()
|
|
def get_review_operating_indicators() -> dict:
|
|
"""Return workbench indicators: pending, held/recheck, unreconciled, last close, uncategorized backlog."""
|
|
return contracts.get_review_operating_indicators()
|
|
|
|
@mcp.tool()
|
|
def create_ledger_transaction(transaction_date: str = "", type: str = "expense", amount: int = 0, category: str = "", subcategory: str = "", merchant: str = "", description: str = "", account_id: str = "", payment_method: str = "", person: str = "", project: str = "", tags: list[str] | str | None = None, is_fixed: bool = False, currency: str = "KRW", posted_date: str = "", source: str = "manual", external_id: str = "", memo: str = "", dedupe: bool = True) -> dict:
|
|
"""Create an income, expense, transfer, refund, or adjustment transaction. Amount is positive KRW minor unit; type determines direction."""
|
|
return contracts.create_ledger_transaction(transaction_date, type, amount, category, subcategory, merchant, description, account_id, payment_method, person, project, tags, is_fixed, currency, posted_date, source, external_id, memo, dedupe)
|
|
|
|
@mcp.tool()
|
|
def list_ledger_transactions(start_date: str = "", end_date: str = "", type: str = "", category: str = "", person: str = "", account_id: str = "", include_deleted: bool = False, limit: int = 100) -> dict:
|
|
"""List ledger transactions with date/category/person/account filters."""
|
|
return contracts.list_ledger_transactions(start_date, end_date, type, category, person, account_id, include_deleted, limit)
|
|
|
|
@mcp.tool()
|
|
def update_ledger_transaction(transaction_id: str, transaction_date: str | None = None, type: str | None = None, amount: int | None = None, category: str | None = None, subcategory: str | None = None, merchant: str | None = None, description: str | None = None, account_id: str | None = None, payment_method: str | None = None, person: str | None = None, project: str | None = None, tags: list[str] | str | None = None, is_fixed: bool | None = None, memo: str | None = None, status: str | None = None) -> dict:
|
|
"""Update selected fields on a ledger transaction."""
|
|
return contracts.update_ledger_transaction(transaction_id, transaction_date=transaction_date, type=type, amount=amount, category=category, subcategory=subcategory, merchant=merchant, description=description, account_id=account_id, payment_method=payment_method, person=person, project=project, tags=tags, is_fixed=is_fixed, memo=memo, status=status)
|
|
|
|
@mcp.tool()
|
|
def delete_ledger_transaction(transaction_id: str, hard_delete: bool = False) -> dict:
|
|
"""Soft-delete a ledger transaction by default, or permanently delete with hard_delete=true."""
|
|
return contracts.delete_ledger_transaction(transaction_id, hard_delete)
|
|
|
|
@mcp.tool()
|
|
def create_ledger_budget(month: str, category: str, amount: int, subcategory: str = "", person: str = "", currency: str = "KRW", memo: str = "") -> dict:
|
|
"""Create or update a monthly category/person budget."""
|
|
return contracts.create_ledger_budget(month, category, amount, subcategory, person, currency, memo)
|
|
|
|
@mcp.tool()
|
|
def list_ledger_budgets(month: str = "") -> dict:
|
|
"""List ledger budgets, optionally for a month YYYY-MM."""
|
|
return contracts.list_ledger_budgets(month)
|
|
|
|
@mcp.tool()
|
|
def get_monthly_cashflow_report(month: str = "") -> dict:
|
|
"""Return income, expenses, net cashflow, and savings rate for a month."""
|
|
return contracts.get_monthly_cashflow_report(month)
|
|
|
|
@mcp.tool()
|
|
def get_category_spending_report(month: str = "", start_date: str = "", end_date: str = "", person: str = "") -> dict:
|
|
"""Return category/subcategory spending for a month or date range."""
|
|
return contracts.get_category_spending_report(month, start_date, end_date, person)
|
|
|
|
@mcp.tool()
|
|
def get_fixed_cost_report(month: str = "") -> dict:
|
|
"""Return fixed expense transactions and total fixed cost for a month."""
|
|
return contracts.get_fixed_cost_report(month)
|
|
|
|
@mcp.tool()
|
|
def get_budget_vs_actual_report(month: str = "") -> dict:
|
|
"""Compare monthly budgets against actual spending."""
|
|
return contracts.get_budget_vs_actual_report(month)
|
|
|
|
@mcp.tool()
|
|
def get_investable_cashflow_report(month: str = "", minimum_cash_buffer: int = 0, planned_extra_expense: int = 0) -> dict:
|
|
"""Estimate cashflow available for investing after buffer and planned extra expenses."""
|
|
return contracts.get_investable_cashflow_report(month, minimum_cash_buffer, planned_extra_expense)
|
|
|
|
@mcp.tool()
|
|
def preview_ledger_csv_import(file_path: str, source: str = "generic") -> dict:
|
|
"""Preview a UTF-8/UTF-8-BOM CSV import before saving transactions."""
|
|
return contracts.preview_ledger_csv_import(file_path, source)
|
|
|
|
@mcp.tool()
|
|
def preview_kb_bank_pdf_import(file_path: str, password: str, preview_limit: int = 20) -> dict:
|
|
"""Preview transactions from a password-protected KB Kookmin Bank account-history PDF without saving them."""
|
|
return contracts.preview_kb_bank_pdf_import(file_path, password, preview_limit)
|
|
|
|
@mcp.tool()
|
|
def import_kb_bank_pdf(file_path: str, password: str, account_id: str = "", skip_duplicates: bool = True, skip_validation: bool = False) -> dict:
|
|
"""Import transactions from a KB Kookmin Bank account-history PDF into the ledger database."""
|
|
return contracts.import_kb_bank_pdf(file_path, password, account_id, skip_duplicates, skip_validation=skip_validation)
|
|
|
|
@mcp.tool()
|
|
def preview_seoulpay_xlsx_import(file_path: str, password: str = "", preview_limit: int = 20) -> dict:
|
|
"""Preview transactions from a SeoulPay usage-history XLSX before saving them."""
|
|
return contracts.preview_seoulpay_xlsx_import(file_path, password, preview_limit)
|
|
|
|
@mcp.tool()
|
|
def import_seoulpay_xlsx(file_path: str, password: str = "", account_id: str = "", skip_duplicates: bool = True) -> dict:
|
|
"""Import transactions from a SeoulPay usage-history XLSX into the ledger database."""
|
|
return contracts.import_seoulpay_xlsx(file_path, password, account_id, skip_duplicates)
|
|
|
|
@mcp.tool()
|
|
def preview_toss_securities_pdf_import(file_path: str, preview_limit: int = 20) -> dict:
|
|
"""Preview transactions from a Toss Securities transaction-history PDF without saving them."""
|
|
return contracts.preview_toss_securities_pdf_import(file_path, preview_limit)
|
|
|
|
@mcp.tool()
|
|
def import_toss_securities_pdf(file_path: str, account_id: str = "", skip_duplicates: bool = True) -> dict:
|
|
"""Import transactions from a Toss Securities transaction-history PDF into the ledger database."""
|
|
return contracts.import_toss_securities_pdf(file_path, account_id, skip_duplicates)
|
|
|
|
@mcp.tool()
|
|
def preview_toss_bank_pdf_import(file_path: str, preview_limit: int = 20) -> dict:
|
|
"""Preview transactions from a Toss Bank transaction-history PDF without saving them."""
|
|
return contracts.preview_toss_bank_pdf_import(file_path, preview_limit)
|
|
|
|
@mcp.tool()
|
|
def import_toss_bank_pdf(file_path: str, account_id: str = "", skip_duplicates: bool = True) -> dict:
|
|
"""Import transactions from a Toss Bank transaction-history PDF into the ledger database."""
|
|
return contracts.import_toss_bank_pdf(file_path, account_id, skip_duplicates)
|
|
|
|
@mcp.tool()
|
|
def list_ledger_import_batches(
|
|
source: str = "",
|
|
include_rolled_back: bool = True,
|
|
limit: int = 50,
|
|
provenance_exposure: str = "minimal",
|
|
debug_provenance_gate: bool = False,
|
|
) -> dict:
|
|
"""List CSV/PDF/API import batches."""
|
|
return contracts.list_ledger_import_batches(
|
|
source,
|
|
include_rolled_back,
|
|
limit,
|
|
provenance_exposure=provenance_exposure,
|
|
debug_provenance_gate=debug_provenance_gate,
|
|
)
|
|
|
|
@mcp.tool()
|
|
def rollback_ledger_import_batch(import_batch_id: str, hard_delete: bool = False) -> dict:
|
|
"""Roll back all transactions created by one import batch. Soft-deletes by default."""
|
|
return contracts.rollback_ledger_import_batch(import_batch_id, hard_delete)
|
|
|
|
@mcp.tool()
|
|
def create_category_rule(match_field: str, pattern: str, category: str, subcategory: str = "", transaction_type: str = "", match_type: str = "contains", priority: int = 100, person: str = "", is_fixed: bool | None = None, confidence: float = 1.0, memo: str = "", amount_min: int | None = None, amount_max: int | None = None, day_of_month: int | None = None) -> dict:
|
|
"""Create or update a ledger transaction categorization rule."""
|
|
return contracts.create_category_rule(match_field, pattern, category, subcategory, transaction_type, match_type, priority, person, is_fixed, confidence, memo, amount_min, amount_max, day_of_month)
|
|
|
|
@mcp.tool()
|
|
def seed_default_category_rules(overwrite: bool = False) -> dict:
|
|
"""Seed default Jarca category rules for common Korean household ledger patterns."""
|
|
return contracts.seed_default_category_rules(overwrite)
|
|
|
|
@mcp.tool()
|
|
def list_category_rules(active_only: bool = True) -> dict:
|
|
"""List transaction categorization rules."""
|
|
return contracts.list_category_rules(active_only)
|
|
|
|
@mcp.tool()
|
|
def update_category_rule(rule_id: str, match_field: str, pattern: str, category: str, subcategory: str = "", transaction_type: str = "", match_type: str = "contains", priority: int = 100, person: str = "", is_fixed: bool | None = None, confidence: float = 1.0, memo: str = "", amount_min: int | None = None, amount_max: int | None = None, day_of_month: int | None = None, active: bool = True) -> dict:
|
|
"""Update an existing ledger transaction categorization rule."""
|
|
return contracts.update_category_rule(rule_id, match_field, pattern, category, subcategory, transaction_type, match_type, priority, person, is_fixed, confidence, memo, amount_min, amount_max, day_of_month, active)
|
|
|
|
@mcp.tool()
|
|
def delete_category_rule(rule_id: str) -> dict:
|
|
"""Delete a transaction categorization rule."""
|
|
return contracts.delete_category_rule(rule_id)
|
|
|
|
@mcp.tool()
|
|
def preview_categorization(limit: int = 1000, only_uncategorized: bool = True) -> dict:
|
|
"""Preview category-rule matches before writing categories to transactions."""
|
|
return contracts.preview_categorization(limit, only_uncategorized)
|
|
|
|
@mcp.tool()
|
|
def apply_categorization_rules(limit: int = 1000, only_uncategorized: bool = True) -> dict:
|
|
"""Apply active category rules to matching ledger transactions."""
|
|
return contracts.apply_categorization_rules(limit, only_uncategorized)
|
|
|
|
@mcp.tool()
|
|
def list_uncategorized_transactions(limit: int = 100, min_amount: int = 0) -> dict:
|
|
"""List active transactions that still have no category."""
|
|
return contracts.list_uncategorized_transactions(limit, min_amount)
|
|
|
|
@mcp.tool()
|
|
def get_uncategorized_merchant_summary(limit: int = 50) -> dict:
|
|
"""Summarize uncategorized transactions by merchant/raw_summary/type for user review."""
|
|
return contracts.get_uncategorized_merchant_summary(limit)
|
|
|
|
@mcp.tool()
|
|
def create_ledger_category(category: str, subcategory: str = "", sort_order: int = 100, memo: str = "", cashflow_kind: str = "") -> dict:
|
|
"""Create or reactivate a ledger category/subcategory used by the dashboard and transactions."""
|
|
return contracts.create_ledger_category(category, subcategory, sort_order, memo, cashflow_kind)
|
|
|
|
@mcp.tool()
|
|
def seed_default_ledger_categories(overwrite: bool = False) -> dict:
|
|
"""Seed default ledger categories and subcategories."""
|
|
return contracts.seed_default_ledger_categories(overwrite)
|
|
|
|
@mcp.tool()
|
|
def list_ledger_categories(active_only: bool = True, include_usage: bool = True) -> dict:
|
|
"""List ledger categories and subcategories, optionally with usage counts."""
|
|
return contracts.list_ledger_categories(active_only, include_usage)
|
|
|
|
@mcp.tool()
|
|
def list_cross_transfer_review_queue(limit: int = 300) -> dict:
|
|
"""List KB↔토스 계좌 이체 확인필요 큐 항목."""
|
|
return contracts.list_cross_transfer_review_queue(limit)
|
|
|
|
@mcp.tool()
|
|
def preview_cross_account_transfers(limit: int = 500) -> dict:
|
|
"""Preview KB↔토스은행 이체 매칭 후보를 반환합니다."""
|
|
return contracts.preview_cross_account_transfers(limit)
|
|
|
|
@mcp.tool()
|
|
def run_cross_account_transfer_match(limit: int = 500) -> dict:
|
|
"""Run KB↔토스은행 자동 이체 매칭 및 태그/분류 반영을 수행합니다."""
|
|
return contracts.run_cross_account_transfer_match(limit)
|
|
|
|
@mcp.tool()
|
|
def delete_ledger_category(category: str, subcategory: str = "", hard_delete: bool = False, force: bool = False) -> dict:
|
|
"""Deactivate or delete a ledger category/subcategory."""
|
|
return contracts.delete_ledger_category(category, subcategory, hard_delete, force)
|
|
|
|
def main() -> None:
|
|
mcp.run(transport=_runtime_config.transport)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|