Stop Letting Your LLM Do Calendar Math: A Field Test of OpenFate’s Open-Source Bazi MCP Server
Fifteen scenarios run through the official MCP TypeScript client, covering daylight saving, solar time, day boundaries, reverse lookup, bad input and a machine with no network. This is what an agent actually gets back, where it is slow, and the one parameter that decides whether the chart is right.
Independent review and field test by Akira, a Bazi enthusiast in Japan. Akira is unaffiliated with and unpaid by OpenFate, which maintains the MCP package. The client script is in the appendix so anyone can re-run the tests.
Test setup: @openfate/bazi-mcp v0.3.1 on Node 22.22 with MCP SDK 1.31.0, over stdio. Timings are from one machine and one session.
At a glance
- Six tools over stdio; MIT licensed; calculation runs locally and continued with networking disabled.
- Chart calls took 1–306 ms. Reverse lookup scaled with the year range at about 0.27 s per year; a 1900–2100 search took 54 seconds.
- Pass timezoneId, not a bare numeric timezone. An IANA zone applies historical daylight saving automatically.
- An unknown hour gives three pillars. Missing or nonexistent civil times mark the luck-cycle onset UNAVAILABLE with a reason code.
- Every checked pillar, solar-time value and onset date matched the web calculator and separate calculations.
Why an MCP server for this at all
Ask a general-purpose model for someone’s Four Pillars and it will answer. It will name a day pillar, maybe a Day Master, and explain it with confidence. It is also doing sexagenary-cycle arithmetic, solar-term lookups and time-zone history in its head, which is exactly the work language models are worst at. The answer reads well and is frequently wrong in the one place that matters, the hour or day pillar.
The fix is the usual one for agents: hand the deterministic part to a tool. @openfate/bazi-mcp wraps the same packages that OpenFate’s website uses ( @openfate/bazi-engine and @openfate/true-solar-time ) and exposes them as MCP tools, so the model receives structured JSON and only has to explain it.
Setup
Claude Desktop, Cursor and Cline all take the same configuration block:
{
“mcpServers”: {
“openfate-bazi”: { “command”: “npx”, “args”: [“-y”, “@openfate/bazi-mcp”] }
}
}
Node 20 or later is required. Our client connected and completed the handshake in 244 ms. The package also ships a portable Agent Skill ( skills/openfate-bazi/SKILL.md ) that tells an agent which inputs to ask for, including whether the recorded birth time was on daylight saving, and not to recompute anything the tool returns.
| Tool | What it returns |
| calculate_bazi_chart | Four pillars, Ten Gods, hidden stems, Na Yin, void branches, growth stages, true solar time, Da Yun timing receipt, normalized dates, applied policy. |
| calculate_true_solar_time | Clock-to-solar correction by longitude, equation of time, and DST. |
| detect_bazi_interactions | Earthly Branch clashes, combinations, trines, punishments, and harms by pillar. |
| reverse_bazi_to_solar_times | Candidate dates and clock hours for a four-pillar string. |
| get_openfate_bazi_policy | Defaults and guidance an agent should follow. |
| get_openfate_bazi_resources | Canonical OpenFate links. |
Times are wall-clock round trips through the SDK client, including JSON serialization. The first call in a session pays a warm-up cost.
Where the test cases come from. Scenarios 1 and 4 use the same births as two other reviews of OpenFate: David Hoo’s Taiwan daylight-saving test (Taipei 1979) and Chen Tian Ji’s five-system calculation audit (Chicago 1992), so the results can be compared across the three reviews. Scenarios 5 and 6 use the example OpenFate publishes on its Bazi calculator page and in its public validation file. The rest were chosen for this test.
The fifteen scenarios
| # | Scenario | Result and time |
| 1 | Taipei 1979-07-15 11:20, Asia/Taipei | Ji-Wei Xin-Wei Gui-Wei Ding-Si; true solar 10:20:21; DST inferred. 306 ms |
| 2 | Same, numeric timezone 8 and dstOffset 1 | Identical to #1. 21 ms |
| 3 | Same, numeric timezone 8 without dstOffset | Hour pillar Wu-Wu; standard time assumed. 22 ms |
| 4 | Chicago 1992-05-02 16:40, America/Chicago, female | Ren-Shen Jia-Chen Wu-Yin Geng-Shen; true solar 15:52:35; Da Yun onset 2001-10-17. 184 ms |
| 5 | 2024-06-15 23:45, 112°E, UTC+8, ZI_HOUR_23 | True solar 23:12:20; day pillar Xin-Hai. 24 ms |
| 6 | Same, MIDNIGHT_00 | Day pillar Geng-Xu. 3 ms |
| 7 | Reverse lookup Ji-Wei Xin-Wei Gui-Wei Ding-Si, 1900–2100 | 1919-07-30 and 1979-07-15, each at 09:00 and 10:00. 54.2 s |
| 8 | Same pillars, 1970–1990 and 1940–2020 | 1979-07-15 at 09:00 and 10:00. 5.7 s / 22.7 s |
| 9 | Impossible string Jia-Zi ×4 | Zero matches after full scan. 54.5 s |
| 10 | Interactions for branches Shen-Chen-Yin-Shen | Two Yin-Shen clashes; no Shen-Chen half-trine without Zi. 12 ms |
| 11 | February 30 | Invalid date error. 4 ms |
| 12 | Chicago 2021-03-14 02:30, skipped DST hour | Chart returned; onset UNAVAILABLE, NONEXISTENT_CIVIL_TIME. 193 ms |
| 13 | Chicago chart without timezone | Clock-time pillars, no solar correction; onset UNAVAILABLE, MISSING_TIMEZONE. 4 ms |
| 14 | Chicago chart without hour | Three pillars; onset UNAVAILABLE, UNKNOWN_BIRTH_TIME. 2 ms |
| 15 | Year 1700; then scenario 4 offline | Year below minimum 1800; offline chart matched #4. 1 ms / — |
Were the answers right?
We checked the core values three ways, avoiding the package’s own dependencies where we could.
Day pillars from a fixed anchor (1 January 2000 is a Wu-Wu day) with plain day counting: Gui-Wei for 1979-07-15, Wu Yin for 1992-05-02, Geng-Xu for 2024-06-15 and ⾟Hai for the next day. All four agree with the server. True solar time from longitude plus the NOAA equation-of-time approximation: about 10:20 for Taipei and 15:52.8 for Chicago, against the server’s 10:20:21 and 15:52:35. Luck-cycle onset. Swiss Ephemeris puts the 1992 Qingming solar term at 12:45 UT on 4 April. The server’s receipt says 1992-04-04T12:45:08Z , an interval of 2,451,292 seconds (28.37 days). At three days per year that is 9.46 years, which lands on 17 October 2001. That is the date the server returns. OpenFate’s website gave the same pillars and solar times for scenarios 1 and 4. Scenarios 5 and 6 reproduce the published example (23:12, ⾟Hai versus Geng-Xu).
The onset receipt deserves a closer look, because it is the part most Bazi tools hide:
The timing receipt includes status CALCULATED, policy THREE_DAYS_PER_YEAR, birth UTC 1992-05-02T21:40:00Z, Qingming at 1992-04-04T12:45:08Z, interval 2,451,292 seconds, timezone basis America/Chicago, and disambiguation REJECT. Every input to the onset is exposed for recomputation.
Every input to the onset is in the response, so an agent, or a skeptical user, can recompute it.
Gotchas
GOTCHA 1 · TIME ZONES
With timezoneId, the server looks up historical DST in the IANA database; metadata.dstOffset shows what it applied. With a numeric timezone, it doesn’t infer anything. Scenario 3 shows the cost: same birth, one missing parameter, different hour pillar. The policy tool’s guidance says to “pass dstOffset” when the birth time included DST, but it doesn’t mention that timezoneId handles this automatically. Prefer timezoneId.
GOTCHA 2 · REVERSE LOOKUP IS A LINEAR SCAN
Lookups scan every year in the range. At roughly 0.27 s per year, the default window (1900 to the current year) took 34 s in our run, and a 200-year window took 54 s, uncomfortably near the SDK’s 60,000 ms default. Some clients time out sooner. Narrow startYear and endYear, or raise the per-request timeout. An impossible pillar string also scans the full range before returning nothing (scenario 9); a quick consistency check could reject it instantly, and one hasn’t been added yet. Results are whole clock hours without solar correction, as documented, so treat them as candidates and recalculate.
GOTCHA 3 · NONEXISTENT TIMES STILL RETURN A CHART
For a clock time that never existed (scenario 12), you still get four pillars. The only signal is chart.daYun.timing.reason: “NONEXISTENT_CIVIL_TIME”; there is no top-level warning. Agents should check the timing receipt before presenting an exact onset, which is what the policy tool tells them to do.
GOTCHA 4 · SMALL THINGS
Keys are English but Ten God values come back in Simplified Chinese (e.g. Indirect Wealth), so a Traditional Chinese interface needs a conversion step. Supported years are 1800–2100. And the developer page on OpenFate still lists v0.2.6 and engine ^1.1.1, while npm has v0.3.1 on engine ^2.0.0. Trust the package README.
What it does well, what it isn’t
| Does well | Scope limit |
| No hour means no hour pillar; no timezone means no exact onset. | It returns chart facts, not a reading. |
| Timing receipts record the solar term, interval, and conversion rule. | It is not a geocoder; the caller supplies longitude and time zone. |
| Branch interactions retain every occurrence by pillar. | Calculation does not prove Bazi predicts events. |
| Runs locally; worked without networking. | This package does not implement Zi Wei, Western or Vedic charts. |
| Attribution arrives as data. |
Verdict
If you are building an agent that touches Chinese metaphysics, from a journaling app with a Bazi feature to a customer-service bot for an astrology product, this is a sensible default for the calculation layer. It is free, local, fast on the calls that matter, and unusually explicit about what it did and didn’t compute. Pass timezoneId and longitude , keep reverse-lookup windows narrow, and read the timing receipt before quoting an exact date. The model can then focus on the part it is actually good at, which is explaining the chart.
Before you build on it, run the appendix script against a few births you already trust.
Appendix: the client we used
// npm i @openfate/bazi-mcp @modelcontextprotocol/sdk (Node 20+)
import { Client } from “@modelcontextprotocol/sdk/client/index.js”;
import { StdioClientTransport } from “@modelcontextprotocol/sdk/client/stdio.js”;
const client = new Client({ name: “field-test”, version: “1.0.0” });
await client.connect(new StdioClientTransport({
command: “node”, args: [“node_modules/@openfate/bazi-mcp/dist/stdio.js”],
}));
const res = await client.callTool({
name: “calculate_bazi_chart”,
arguments: { year: 1992, month: 5, day: 2, hour: 16, minute: 40,
gender: “female”, longitude: -87.6298, timezoneId: “America/Chicago” },
});
const { chart } = JSON.parse(res.content[0].text).data;
console.log([“year”,”month”,”day”,”hour”].map(k => chart.pillars[k].ganZhi).join(” “));
console.log(chart.solarTimeInfo.trueSolarDateTime, chart.daYun.timing.status);
await client.callTool({ name: “reverse_bazi_to_solar_times”,
arguments: { bazi: “\u5df1\u672a \u8f9b\u672a \u7678\u672a \u4e01\u5df3”, startYear: 1970, endYear: 1990 } },
undefined, { timeout: 180000 });
Sources and further reading
References: npm package @openfate/bazi-mcp; Model Context Protocol TypeScript SDK; NOAA solar calculation notes; OpenFate developer documentation, free Bazi calculator and public validation cases. Official website: OpenFate.ai
Disclaimer: This article is for informational purposes only. Bazi calculations and interpretations may vary by methodology, software, and practitioner. Readers should independently verify results before relying on them for personal or important decisions.