expense-tracker-mcp

expense-tracker-mcp

MCP server for tracking personal expenses via natural language, with tools to add, list, and summarize expenses, backed by Postgres.

Category
访问服务器

README

expense-tracker-mcp

A remote MCP server for tracking personal expenses, backed by Postgres, designed to be driven by two different clients: Claude as a connector, and a custom LangGraph agent.

Log an expense by saying "spent 450 on groceries today", then ask "what did I spend on food this month?" — and get the same answer from either client, because the state lives in a database rather than in a chat session.

Claude (connector) ─┐
                    ├─► expense-tracker-mcp ─► Neon Postgres
LangGraph agent ────┘        (FastMCP)

Status

Phase
1 Server foundation — typed tools, Postgres, category validation works locally
2 LangGraph client — terminal, create_react_agent, checkpointed memory not started
3 Streamlit frontend on top of the working agent not started
4 OAuth 2.1, queries scoped to the authenticated user not started

Phase 1 is verified end to end against a real Neon database. Deployment is the next step.

Tools

Tool Purpose
list_categories The valid taxonomy, so the model can look it up instead of guessing.
add_expense Record one expense. Validates the category before writing.
list_expenses Individual rows, newest first. Optional date range and category filters.
summarize Totals over a date range, grouped by category — or by subcategory when you filter to one category.

The taxonomy is also published as a resource, expenses://categories. That duplication is deliberate, and testing against Claude is what put it there: resources are the correct MCP primitive for read-only reference data, but a client only reads one when a user attaches it — models are handed tools, not resources. Asked "what categories can I use?", Claude reported the taxonomy as unavailable and offered to write a junk row so it could read the valid values off the rejection error. The tool is what the model can actually reach; the resource remains for clients that browse resources directly.

Categories are a fixed two-level taxonomy defined in categories.json — 20 categories, each with subcategories. Anything outside it is rejected with the valid values included in the error, so the model can correct itself in one round trip.

Running it locally

Prerequisites: Python 3.10+, uv, and a Neon account (the free tier is enough).

git clone https://github.com/<your-username>/expense-tracker-mcp
cd expense-tracker-mcp
uv sync

Configure the database. Copy the example file and fill in your Neon connection string:

cp .env.example .env      # PowerShell: Copy-Item .env.example .env

Two things matter about that string:

  • Use the pooled connection — the host contains -pooler.
  • Strip the ?sslmode=require&channel_binding=require query string. asyncpg doesn't accept libpq's query parameters and will raise invalid dsn: invalid connection option "sslmode". TLS is requested explicitly in code instead. (The server strips these defensively too, so a raw pasted string still works.)

Create the table. Run schema.sql once, in the Neon SQL Editor or any Postgres client. Every statement is idempotent.

Start the server:

uv run python main.py            # http://127.0.0.1:8000/mcp

Or explore it interactively with the MCP Inspector (needs Node):

uv run fastmcp dev inspector main.py

A browser GET on /mcp returns 406 Not Acceptable. That's correct, not a failure — MCP requires POST with Accept: application/json, text/event-stream.

Deploying

Built for Prefect Horizon (formerly FastMCP Cloud). Point it at this repo with entrypoint main.py:mcp and set DATABASE_URL in the environment variables. Deployed servers get a *.fastmcp.app URL, which can be added directly to Claude as a connector.

Note there is deliberately no .python-version file. Horizon builds with UV_PROJECT_ENVIRONMENT=/usr/local, a system Python prefix rather than a virtualenv; a version pin makes uv reject it, download a managed CPython, and fail trying to recreate a non-venv directory. The requires-python = ">=3.10" floor in pyproject.toml is sufficient.

Design decisions

Money is NUMERIC(12,2), never a float. Binary floating point cannot represent 0.1 exactly, so summing float amounts accumulates error and totals drift by cents. Amounts are Decimal in Python, NUMERIC in Postgres, and cross the wire as strings — JSON numbers are IEEE-754 doubles, so serialising as a float would reintroduce the drift at the very last step. 450.55 + 120.45 returns exactly 571.00.

The connection pool is created lazily, never at import. Connecting at import time turns a transient database problem into a failed deploy; a lazy pool turns it into one failed tool call the caller can retry. Schema creation is likewise a separate one-time script, not something the server does on boot.

Every parameter is annotated. FastMCP builds the JSON schema the model sees from type hints, so date: date reaches the model as {"type": "string", "format": "date"} and amount carries exclusiveMinimum: 0. Untyped parameters measurably degrade tool-calling accuracy — and invalid input is rejected by schema validation before the tool body runs at all.

Every tool returns a dict, on success and on failure alike, with an ok key. A tool that returns a list on success and a dict on error forces every caller to type-check before using the result.

user_id exists from day one, defaulted and currently unused; phase 4 scopes every query by it. Adding a NOT NULL column to a populated table later is a migration — adding it now is free. It is deliberately not a tool parameter: if the model could choose the user_id, any client could read anyone's expenses just by asking.

Logging goes to stderr. Over the stdio transport, stdout is the JSON-RPC channel, and a stray print() corrupts the protocol stream.

Not implemented yet

Honest limitations rather than oversights:

  • No edit or delete tools. Correcting a mis-logged expense means going to the database directly. Deferred until it proves annoying in practice.
  • No currency column. Every amount is assumed to be in one currency.
  • No authentication. Every expense is written as user_id = 'default', so the deployed server is single-tenant until phase 4.

Layout

main.py           the server: three tools, one resource
schema.sql        one-time table + index creation
categories.json   the category taxonomy, single source of truth
.env.example      documents DATABASE_URL

Built with

FastMCP 3 · asyncpg · Neon Postgres

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选